Files
WebUITest-RealProjecT/FrontEnd/pages/HistoryPage.tsx
arDTDev 3bd35ad852 feat: Add real-time IO/interlock updates, HW status display, and history page
- Implement real-time IO value updates via IOValueChanged event
- Add interlock toggle and real-time interlock change events
- Fix ToggleLight to check return value of DIO.SetRoomLight
- Add HW status display in Footer matching WinForms HWState
- Implement GetHWStatus API and 250ms broadcast interval
- Create HistoryPage React component for work history viewing
- Add GetHistoryData API for database queries
- Add date range selection, search, filter, and CSV export
- Add History button in Header navigation
- Add PickerMoveDialog component for manage operations
- Fix DataSet column names (idx, PRNATTACH, PRNVALID, qtymax)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 00:18:27 +09:00

321 lines
14 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Search, Download, ChevronLeft, ChevronRight, Filter, X } from 'lucide-react';
import { comms } from '../communication';
interface HistoryRow {
idx: number;
jtype: string;
rid: string;
sid: string;
qty: number;
vname: string;
vlot: string;
loc: string;
qr: string;
stime: string;
etime: string;
prnattach: boolean;
prnvalid: boolean;
rid0: string;
sid0: string;
qtymax: number;
}
export const HistoryPage: React.FC = () => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<HistoryRow[]>([]);
const [filteredData, setFilteredData] = useState<HistoryRow[]>([]);
const [mcName, setMcName] = useState('');
// Date range state
const today = new Date();
const [startDate, setStartDate] = useState(today.toISOString().split('T')[0]);
const [endDate, setEndDate] = useState(today.toISOString().split('T')[0]);
// Search state
const [dbSearch, setDbSearch] = useState('');
const [localFilter, setLocalFilter] = useState('');
// Load data on mount
useEffect(() => {
fetchData();
}, []);
// Apply local filter when it changes
useEffect(() => {
if (!localFilter.trim()) {
setFilteredData(data);
} else {
const keyword = localFilter.toLowerCase();
setFilteredData(data.filter(row =>
row.rid0?.toLowerCase().includes(keyword) ||
row.sid0?.toLowerCase().includes(keyword) ||
row.qr?.toLowerCase().includes(keyword) ||
row.vlot?.toLowerCase().includes(keyword) ||
row.vname?.toLowerCase().includes(keyword) ||
row.rid?.toLowerCase().includes(keyword) ||
row.sid?.toLowerCase().includes(keyword)
));
}
}, [localFilter, data]);
const fetchData = async () => {
setIsLoading(true);
try {
const result = await comms.getHistoryData(startDate, endDate, dbSearch);
if (result.success) {
setData(result.data || []);
setFilteredData(result.data || []);
if (result.mcName) {
setMcName(result.mcName);
}
} else {
console.error('Failed to fetch history:', result.message);
setData([]);
setFilteredData([]);
}
} catch (error) {
console.error('Error fetching history:', error);
setData([]);
setFilteredData([]);
}
setIsLoading(false);
};
const handleSearch = () => {
// Validate dates
if (new Date(endDate) < new Date(startDate)) {
alert('End date is earlier than start date');
return;
}
fetchData();
};
const handlePrevDay = () => {
const sd = new Date(startDate);
sd.setDate(sd.getDate() - 1);
const newDate = sd.toISOString().split('T')[0];
setStartDate(newDate);
setEndDate(newDate);
};
const handleNextDay = () => {
const ed = new Date(endDate);
ed.setDate(ed.getDate() + 1);
const newDate = ed.toISOString().split('T')[0];
setStartDate(newDate);
setEndDate(newDate);
};
const handleExport = () => {
if (filteredData.length === 0) {
alert('No data to export');
return;
}
// Create CSV content
const headers = ['IDX', 'JTYPE', 'RID', 'SID', 'QTY', 'QTY_MAX', 'VENDOR', 'V.LOT', 'LOC', 'QR', 'START', 'END', 'ATTACH', 'VALID', 'RID0', 'SID0'];
const csvContent = [
headers.join(','),
...filteredData.map(row => [
row.idx,
`"${row.jtype || ''}"`,
`"${row.rid || ''}"`,
`"${row.sid || ''}"`,
row.qty || 0,
row.qtymax || 0,
`"${row.vname || ''}"`,
`"${row.vlot || ''}"`,
`"${row.loc || ''}"`,
`"${row.qr || ''}"`,
`"${row.stime || ''}"`,
`"${row.etime || ''}"`,
row.prnattach ? 'Y' : 'N',
row.prnvalid ? 'Y' : 'N',
`"${row.rid0 || ''}"`,
`"${row.sid0 || ''}"`,
].join(','))
].join('\n');
// Download file
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `export_${startDate.replace(/-/g, '')}~${endDate.replace(/-/g, '')}.csv`;
link.click();
};
return (
<div className="min-h-screen bg-gray-900 text-white p-4">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/')}
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-2xl font-tech font-bold text-neon-blue">
WORK HISTORY {mcName && <span className="text-gray-400 text-lg">({mcName})</span>}
</h1>
</div>
<div className="text-sm text-gray-400">
Total: <span className="text-white font-bold">{filteredData.length}</span> records
{localFilter && <span className="text-yellow-400 ml-2">(filtered from {data.length})</span>}
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-4 mb-4 p-4 bg-gray-800/50 rounded-xl border border-gray-700">
{/* Date Range */}
<div className="flex items-center gap-2">
<button
onClick={handlePrevDay}
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 transition-colors"
title="Previous Day"
>
<ChevronLeft className="w-4 h-4" />
</button>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="px-3 py-2 rounded-lg bg-gray-700 border border-gray-600 text-white text-sm focus:outline-none focus:border-neon-blue"
/>
<span className="text-gray-500">~</span>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="px-3 py-2 rounded-lg bg-gray-700 border border-gray-600 text-white text-sm focus:outline-none focus:border-neon-blue"
/>
<button
onClick={handleNextDay}
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 transition-colors"
title="Next Day"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
{/* DB Search */}
<div className="flex items-center gap-2">
<input
type="text"
value={dbSearch}
onChange={(e) => setDbSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="Search in DB..."
className="px-3 py-2 rounded-lg bg-gray-700 border border-gray-600 text-white text-sm focus:outline-none focus:border-neon-blue w-48"
/>
<button
onClick={handleSearch}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-neon-blue/20 text-neon-blue border border-neon-blue hover:bg-neon-blue/30 transition-colors disabled:opacity-50"
>
<Search className="w-4 h-4" />
<span>Search</span>
</button>
</div>
{/* Local Filter */}
<div className="flex items-center gap-2 ml-auto">
<Filter className="w-4 h-4 text-gray-500" />
<input
type="text"
value={localFilter}
onChange={(e) => setLocalFilter(e.target.value)}
placeholder="Filter results..."
className={`px-3 py-2 rounded-lg border text-white text-sm focus:outline-none focus:border-neon-blue w-48 ${localFilter ? 'bg-lime-900/30 border-lime-500' : 'bg-gray-700 border-gray-600'
}`}
/>
{localFilter && (
<button
onClick={() => setLocalFilter('')}
className="p-1 rounded hover:bg-gray-700"
>
<X className="w-4 h-4 text-gray-400" />
</button>
)}
</div>
{/* Export */}
<button
onClick={handleExport}
disabled={filteredData.length === 0}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-green-600/20 text-green-400 border border-green-600 hover:bg-green-600/30 transition-colors disabled:opacity-50"
>
<Download className="w-4 h-4" />
<span>Export CSV</span>
</button>
</div>
{/* Data Table */}
<div className="bg-gray-800/50 rounded-xl border border-gray-700 overflow-hidden">
<div className="overflow-x-auto max-h-[calc(100vh-280px)]">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-gray-800 border-b border-gray-700">
<tr>
<th className="px-3 py-2 text-left text-gray-400 font-medium">IDX</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">TYPE</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">RID</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">SID</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">QTY</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">VENDOR</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">V.LOT</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">LOC</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">START</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">END</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">ATTACH</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">VALID</th>
</tr>
</thead>
<tbody>
{isLoading ? (
<tr>
<td colSpan={12} className="px-3 py-8 text-center text-gray-500">
Loading...
</td>
</tr>
) : filteredData.length === 0 ? (
<tr>
<td colSpan={12} className="px-3 py-8 text-center text-gray-500">
No data found
</td>
</tr>
) : (
filteredData.map((row, index) => (
<tr
key={row.idx || index}
className="border-b border-gray-700/50 hover:bg-gray-700/30 transition-colors"
>
<td className="px-3 py-2 text-gray-500">{row.idx}</td>
<td className="px-3 py-2 text-xs">{row.jtype}</td>
<td className="px-3 py-2 font-mono text-xs">{row.rid}</td>
<td className="px-3 py-2 font-mono text-xs">{row.sid}</td>
<td className="px-3 py-2 text-right">{row.qty}/{row.qtymax}</td>
<td className="px-3 py-2">{row.vname}</td>
<td className="px-3 py-2 font-mono text-xs">{row.vlot}</td>
<td className="px-3 py-2 text-center">{row.loc}</td>
<td className="px-3 py-2 text-xs text-gray-400">{row.stime}</td>
<td className="px-3 py-2 text-xs text-gray-400">{row.etime}</td>
<td className={`px-3 py-2 text-center font-bold ${row.prnattach ? 'text-green-400' : 'text-red-400'}`}>
{row.prnattach ? 'Y' : 'N'}
</td>
<td className={`px-3 py-2 text-center font-bold ${row.prnvalid ? 'text-green-400' : 'text-red-400'}`}>
{row.prnvalid ? 'Y' : 'N'}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
};