import React, { useEffect, useState, useCallback } from 'react'; import { Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, FormControl, InputLabel, Select, MenuItem, ToggleButtonGroup, ToggleButton, CircularProgress, Typography, TextField, Chip, TablePagination, Tooltip, } from '@mui/material'; import { adminService } from '../../api/adminService'; import type { AuditHistoryUserCount, AuditHistoryRecord } from '../../api/adminService'; import { accountService } from '../../api/accountService'; import { useToastStore } from '../../store/toastStore'; interface Organization { org_id: string; org_name: string; db_name: string; } type Preset = 'today' | 'overall' | 'custom_day' | 'custom_range'; const startOfDay = (dateStr: string) => `${dateStr}T00:00:00`; const endOfDay = (dateStr: string) => `${dateStr}T23:59:59`; const todayStr = () => new Date().toISOString().split('T')[0]; const RECORDS_PAGE_SIZE = 25; const efficiencyColor = (value: number): 'success' | 'warning' | 'error' => { if (value >= 75) return 'success'; if (value >= 40) return 'warning'; return 'error'; }; // "Assigned" is video_date-based (work whose video arrived in this window) // and "Completed" is Audited_on-based (work actually finished in this // window) - two independent measures, not two views of the same row set. A // user can complete backlog items today with nothing newly assigned today. const SummaryCard: React.FC<{ label: string; value: string; color?: string }> = ({ label, value, color }) => ( {label} {value} ); export const AuditHistory: React.FC = () => { const [organizations, setOrganizations] = useState([]); const [dbName, setDbName] = useState(''); const [type, setType] = useState<'audits' | 'rectification'>('audits'); const [preset, setPreset] = useState('today'); const [customDay, setCustomDay] = useState(todayStr()); const [rangeFrom, setRangeFrom] = useState(todayStr()); const [rangeTo, setRangeTo] = useState(todayStr()); const [total, setTotal] = useState(0); const [audited, setAudited] = useState(0); const [notAudited, setNotAudited] = useState(0); const [byUser, setByUser] = useState([]); const [loading, setLoading] = useState(false); // Drill-down: which user's individual records are currently shown below. const [drillUser, setDrillUser] = useState(null); const [drillRecords, setDrillRecords] = useState([]); const [drillTotal, setDrillTotal] = useState(0); const [drillPage, setDrillPage] = useState(0); const [drillLoading, setDrillLoading] = useState(false); useEffect(() => { (async () => { try { const orgsRes = await accountService.getOrganizations(); const orgs = (orgsRes as any) || []; setOrganizations(orgs); if (orgs.length > 0) setDbName(orgs[0].db_name); } catch (err) { useToastStore.getState().showToast('Failed to load organizations.', 'error'); } })(); }, []); const { from, to } = (() => { switch (preset) { case 'today': return { from: startOfDay(todayStr()), to: endOfDay(todayStr()) }; case 'custom_day': return { from: startOfDay(customDay), to: endOfDay(customDay) }; case 'custom_range': // Both fields default to today, so it's easy to end up with an // inverted range while adjusting them one at a time (e.g. picking a // past "To" before touching "From") - that would silently match zero // rows instead of erroring, so normalize the order here. return rangeFrom <= rangeTo ? { from: startOfDay(rangeFrom), to: endOfDay(rangeTo) } : { from: startOfDay(rangeTo), to: endOfDay(rangeFrom) }; case 'overall': default: return { from: undefined, to: undefined }; } })(); const load = useCallback(async () => { if (!dbName) return; setLoading(true); try { const result = await adminService.getAuditHistorySummary({ dbName, type, from, to }); setTotal(result.total); setAudited(result.audited); setNotAudited(result.notAudited); setByUser(result.byUser); } catch (err) { useToastStore.getState().showToast('Failed to load audit history.', 'error'); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [dbName, type, from, to]); useEffect(() => { load(); // Changing org/type/date invalidates whatever drill-down was open. setDrillUser(null); }, [load]); const loadDrillRecords = useCallback(async (user: AuditHistoryUserCount, pageIndex: number) => { setDrillLoading(true); try { const result = await adminService.getAuditHistoryUserRecords({ dbName, type, userId: user.user_id, from, to, limit: RECORDS_PAGE_SIZE, offset: pageIndex * RECORDS_PAGE_SIZE, }); setDrillRecords(result.records); setDrillTotal(result.total); } catch (err) { useToastStore.getState().showToast('Failed to load user records.', 'error'); } finally { setDrillLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [dbName, type, from, to]); const handleRowClick = (user: AuditHistoryUserCount) => { if (drillUser?.user_id === user.user_id) { setDrillUser(null); return; } setDrillUser(user); setDrillPage(0); loadDrillRecords(user, 0); }; const handleDrillPageChange = (newPage: number) => { setDrillPage(newPage); if (drillUser) loadDrillRecords(drillUser, newPage); }; const completionRate = total > 0 ? Math.round((audited / total) * 1000) / 10 : 0; return ( Organization value && setType(value)} size="small" > Audits Rectification value && setPreset(value)} size="small" > Today Overall Custom day Custom range {preset === 'custom_day' && ( setCustomDay(e.target.value)} slotProps={{ inputLabel: { shrink: true } }} /> )} {preset === 'custom_range' && ( <> setRangeFrom(e.target.value)} slotProps={{ inputLabel: { shrink: true } }} /> setRangeTo(e.target.value)} slotProps={{ inputLabel: { shrink: true } }} /> )} {loading ? ( ) : ( <> Per-user activity — click a row for their individual records Assigned = video arrived in this window · Completed = audited in this window (may include older backlog) User Assigned Completed Queue Done % {byUser.length === 0 && ( No activity in this range. )} {byUser.map((u) => ( handleRowClick(u)} sx={{ cursor: 'pointer' }} > {u.username} {u.assigned.toLocaleString()} {u.completed.toLocaleString()} {u.efficiency === null ? ( ) : ( )} ))}
{drillUser && ( <> Records assigned to {drillUser.username} {drillLoading ? ( ) : ( Anomaly ID Asset Plaza Chainage Video Date Status Audited On Audited By {drillRecords.length === 0 && ( No records found. )} {drillRecords.map((r) => ( {r.id} {r.Asset || '—'} {r.Plaza || '—'} {r.Chainage || '—'} {r.video_date ? new Date(r.video_date).toLocaleDateString() : '—'} {r.IsAudited ? ( ) : ( )} {r.Audited_on ? new Date(r.Audited_on).toLocaleString() : '—'} {r.audited_username ? r.audited_username === drillUser.username ? r.audited_username : `${r.audited_username} (reassigned)` : '—'} ))}
handleDrillPageChange(newPage)} rowsPerPage={RECORDS_PAGE_SIZE} rowsPerPageOptions={[RECORDS_PAGE_SIZE]} />
)} )} )}
); };