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]}
/>
)}
>
)}
>
)}
);
};