import React, { useEffect, useState, useCallback } from 'react'; import { Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Checkbox, FormControl, InputLabel, Select, MenuItem, ToggleButtonGroup, ToggleButton, Button, CircularProgress, TablePagination, Typography, Chip, Tooltip, } from '@mui/material'; import { adminService } from '../../api/adminService'; import type { MissingMediaRecord, MissingMediaSummary, MissingMediaFilter } from '../../api/adminService'; import { accountService } from '../../api/accountService'; import { useToastStore } from '../../store/toastStore'; interface Organization { org_id: string; org_name: string; db_name: string; } const PAGE_SIZE = 25; const missingColor = (missing: string): 'default' | 'warning' | 'error' => { if (missing === 'not checked yet') return 'default'; if (missing === 'video') return 'warning'; return 'error'; // image or image + video - definitely unauditable }; export const MissingMedia: React.FC = () => { const [organizations, setOrganizations] = useState([]); const [dbName, setDbName] = useState(''); const [type, setType] = useState<'audits' | 'rectification'>('audits'); const [missing, setMissing] = useState<'all' | MissingMediaFilter>('all'); const [records, setRecords] = useState([]); const [summary, setSummary] = useState(null); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [selectedIds, setSelectedIds] = useState>(new Set()); const [loading, setLoading] = useState(false); const [rechecking, setRechecking] = useState(false); useEffect(() => { (async () => { try { const orgs = ((await accountService.getOrganizations()) as any) || []; setOrganizations(orgs); if (orgs.length > 0) setDbName(orgs[0].db_name); } catch { useToastStore.getState().showToast('Failed to load organizations.', 'error'); } })(); }, []); const load = useCallback(async () => { if (!dbName) return; setLoading(true); try { const result = await adminService.getMissingMedia({ dbName, type, limit: PAGE_SIZE, offset: page * PAGE_SIZE, ...(missing !== 'all' ? { missing } : {}), }); setRecords(result.records); setTotal(result.total); setSummary(result.summary); // A recheck under an active filter can shrink the filtered total below // the current page - snap back instead of stranding on an empty page. if (page > 0 && result.total <= page * PAGE_SIZE) setPage(0); } catch { useToastStore.getState().showToast('Failed to load missing-media records.', 'error'); } finally { setLoading(false); } }, [dbName, type, page, missing]); useEffect(() => { load(); }, [load]); useEffect(() => { setPage(0); setSelectedIds(new Set()); }, [dbName, type, missing]); const toggleRow = (id: number) => { setSelectedIds((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const toggleAll = () => { setSelectedIds(selectedIds.size === records.length ? new Set() : new Set(records.map((r) => r.id))); }; const handleRecheck = async (ids: number[]) => { setRechecking(true); try { const result = await adminService.recheckMedia({ dbName, type, ids }); useToastStore.getState().showToast( `Rechecked ${result.checked}: ${result.ready} now ready, ${result.missing} still missing${result.skipped ? `, ${result.skipped} unreachable` : ''}.`, result.ready > 0 ? 'success' : 'info' ); setSelectedIds(new Set()); load(); } catch (err: any) { useToastStore.getState().showToast(err?.response?.data?.message || 'Recheck failed.', 'error'); } finally { setRechecking(false); } }; const summaryChip = (label: string, value: number | undefined, color: 'default' | 'warning' | 'error' | 'info') => ( ); return ( Organization v && setType(v)} size="small"> Audits Rectification v && setMissing(v)} size="small"> All Image missing Video missing Both missing max 50 at a time — the 15-min cron keeps checking the rest automatically {summaryChip('Hidden from auditing', summary?.total, 'error')} {summaryChip('Not checked yet', summary?.neverChecked, 'default')} {summaryChip('Image missing', summary?.missingImage, 'error')} {summaryChip('Video missing', summary?.missingVideo, 'warning')} {summaryChip('Both missing', summary?.missingBoth, 'error')} {summaryChip('Gave up (24h of retries)', summary?.gaveUp, 'info')} {loading ? ( ) : ( 0 && selectedIds.size === records.length} indeterminate={selectedIds.size > 0 && selectedIds.size < records.length} onChange={toggleAll} /> Record ID Asset Plaza Site Video Date Missing Checks Last Checked {records.length === 0 && ( {missing !== 'all' ? 'No records match this filter.' : 'No pending records with missing media — everything is auditable. 🎉'} )} {records.map((r) => ( toggleRow(r.id)} /> {r.id} {r.Asset || '—'} {r.Plaza || '—'} {r.site_id} {r.video_date ? new Date(r.video_date).toLocaleDateString() : '—'} {!r.has_video_declared && ( )} {r.media_check_count >= 96 ? ( ) : r.media_check_count} {r.media_checked_at ? new Date(r.media_checked_at).toLocaleString() : 'never'} ))}
setPage(newPage)} rowsPerPage={PAGE_SIZE} rowsPerPageOptions={[PAGE_SIZE]} />
)}
); };