import { useEffect, useMemo, useState, Fragment } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Typography, Card, CardContent, Grid, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, Chip, TextField, InputAdornment, MenuItem, Button, Skeleton, Tabs, Tab, Collapse, IconButton, Tooltip, Alert, } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; import PendingActionsIcon from '@mui/icons-material/PendingActions'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import TimerIcon from '@mui/icons-material/Timer'; import DescriptionIcon from '@mui/icons-material/Description'; import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import ReportIcon from '@mui/icons-material/Report'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlined'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; import { useProposals } from '../../../domain/proposals/use-cases'; import type { ProposalListItem } from '../../../domain/proposals/types'; import { useAdminDashboard } from '../../../domain/admin/use-cases'; import { DEBOUNCE_SEARCH, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, SERVICE_CATEGORIES, PRIORITIES, PROPOSAL_STATUSES, STATUS_CHIP_STYLES, STATUS_LABELS, PRIORITY_LABELS, PRIORITY_CHIP_STYLES, PRIORITY_ROW_SX } from '../../../constants'; import { formatBidAmount, formatDate, formatDateFull, formatAge, getAgeColor, groupRevisions } from '../../../lib/format'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; const COL_COUNT = 11; const TAB_STATUSES = ['', 'InReview', 'Approved', 'Sent', 'Revised'] as const; function StatCard({ icon, label, value, color, subtitle }: { icon: React.ReactNode; label: string; value: string; color: string; subtitle?: string }) { return ( {icon} {label} {value} {subtitle && ( {subtitle} )} ); } export default function AdminDashboard({ defaultStatus }: { defaultStatus?: string }) { useDocumentTitle(defaultStatus ? 'Admin Queue' : 'Admin Dashboard'); const navigate = useNavigate(); const showTabs = !defaultStatus; // Fix: WEB-M8 — capture error state for admin stats to show error instead of misleading zeros const { data: stats, isLoading: statsLoading, isError: statsError, refetch: refetchStats } = useAdminDashboard(); const [statusFilter, setStatusFilter] = useState(defaultStatus ?? ''); const [categoryFilter, setCategoryFilter] = useState(''); const [priorityFilter, setPriorityFilter] = useState(''); const [expanded, setExpanded] = useState>(new Set()); const [search, setSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [page, setPage] = useState(DEFAULT_PAGE); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); // Debounced search resets to page 1 (mirrors the old usePaginatedList behavior). useEffect(() => { const handle = setTimeout(() => { setDebouncedSearch(search); setPage(1); }, DEBOUNCE_SEARCH); return () => clearTimeout(handle); }, [search]); const { data: listData, isPending: listPending, isPlaceholderData: listIsPlaceholder, error: listError, } = useProposals({ search: debouncedSearch, page, pageSize, ...(statusFilter && { status: statusFilter }), ...(categoryFilter && { serviceCategory: categoryFilter }), ...(priorityFilter && { priority: priorityFilter }), }); const rows = useMemo(() => listData?.items ?? [], [listData]); const totalCount = listData?.totalCount ?? 0; // Show skeletons on initial load and while previous-page placeholder data is // displayed for a new filter/page/search key (matches the old loading flag). const loading = listPending || listIsPlaceholder; const err = listError ? listError.message || 'Failed to load' : ''; const grouped = useMemo(() => groupRevisions(rows), [rows]); const toggleExpand = (baseNumber: string) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(baseNumber)) next.delete(baseNumber); else next.add(baseNumber); return next; }); }; const tabIndex = TAB_STATUSES.indexOf(statusFilter as typeof TAB_STATUSES[number]); const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { setStatusFilter(TAB_STATUSES[newValue] ?? ''); setPage(1); }; const hasActiveFilters = statusFilter || categoryFilter || priorityFilter || search; const clearFilters = () => { setStatusFilter(defaultStatus ?? ''); setCategoryFilter(''); setPriorityFilter(''); setSearch(''); setPage(1); }; const renderRow = (row: ProposalListItem, indent: boolean, showExpandIcon: boolean, isExpanded: boolean, baseNumber: string) => ( navigate(`/admin/proposals/${row.id}`)} > {showExpandIcon && ( { e.stopPropagation(); toggleExpand(baseNumber); }} > {isExpanded ? : } )} {row.proposalNumber} {row.customerName} {row.workOrderNumber} {row.serviceCategory} : row.priority === 'Urgent' ? : undefined} /> {formatBidAmount(row.totalBidAmount)} {formatDate(row.submittedAt)} {row.submittedByName || '-'} {formatAge(row.submittedAt)} ); return ( {defaultStatus ? 'Admin Queue' : 'All Proposals'} {/* Fix: WEB-M8 — show error state instead of misleading zeros when stats fetch fails */} {statsError ? ( } action={ } > Failed to load dashboard statistics. Please try again. ) : ( <> {statsLoading ? ( ) : ( } label="Pending Review" value={String(stats?.pendingCount ?? 0)} color="#1565c0" subtitle="Awaiting action" /> )} {statsLoading ? ( ) : ( } label="Approved This Week" value={String(stats?.approvedThisWeek ?? 0)} color="#2e7d32" subtitle={`${stats?.approvedThisWeek ?? 0} approved`} /> )} {statsLoading ? ( ) : ( } label="Avg Turnaround" value={stats ? `${Math.round(stats.avgTurnaroundHours)}h` : '-'} color="#f57c00" subtitle="Target: <24h" /> )} {statsLoading ? ( ) : ( } label="Total Proposals" value={String(stats?.totalProposals ?? 0)} color="#5e35b1" subtitle="All time" /> )} )} {showTabs && ( = 0 ? tabIndex : 0} onChange={handleTabChange} sx={{ mb: 2, borderBottom: 1, borderColor: 'divider' }} > {TAB_STATUSES.slice(1).map((s) => ( ))} )} setSearch(e.target.value)} sx={{ width: 240 }} slotProps={{ input: { startAdornment: ( ), }, }} /> { setCategoryFilter(e.target.value); setPage(1); }} > All {SERVICE_CATEGORIES.map((c) => ( {c} ))} { setPriorityFilter(e.target.value); setPage(1); }} > All {PRIORITIES.map((p) => ( {PRIORITY_LABELS[p] || p} ))} {!showTabs && ( { setStatusFilter(e.target.value); setPage(1); }} > All {PROPOSAL_STATUSES.map((s) => ( {STATUS_LABELS[s] || s} ))} )} {hasActiveFilters && ( )} {err && ( {err} )} Proposal # Customer WO # Category Priority Status Bid Amount Submitted Submitted By Age {loading ? Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: COL_COUNT }).map((_, j) => ( ))} )) : grouped.map((group) => { const isExpanded = expanded.has(group.baseNumber); return ( {renderRow(group.latest, false, group.hasRevisions, isExpanded, group.baseNumber)} {group.hasRevisions && (
{group.revisions.map((rev) => renderRow(rev, true, false, false, group.baseNumber) )}
)} ); })} {!loading && rows.length === 0 && ( {hasActiveFilters ? 'No proposals match your current filters.' : 'No proposals in queue.'} {hasActiveFilters && ( )} )}
setPage(newPage + 1)} rowsPerPage={pageSize} onRowsPerPageChange={(e) => setPageSize(parseInt(e.target.value, 10))} rowsPerPageOptions={[12, 24, 48]} />
); }