diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts
index 8a141a5..0d75c6b 100644
--- a/web/src/lib/format.ts
+++ b/web/src/lib/format.ts
@@ -7,6 +7,16 @@ export function formatBidAmount(amount: number): string {
}
export function formatDate(iso: string): string {
+ const d = new Date(iso);
+ const isCurrentYear = d.getFullYear() === new Date().getFullYear();
+ return d.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ ...(isCurrentYear ? {} : { year: 'numeric' }),
+ });
+}
+
+export function formatDateFull(iso: string): string {
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
diff --git a/web/src/pages/admin/dashboard/AdminDashboard.tsx b/web/src/pages/admin/dashboard/AdminDashboard.tsx
index d17f4ee..c1157b1 100644
--- a/web/src/pages/admin/dashboard/AdminDashboard.tsx
+++ b/web/src/pages/admin/dashboard/AdminDashboard.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useMemo, useState, Fragment } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import {
@@ -18,32 +18,50 @@ import {
TextField,
InputAdornment,
MenuItem,
+ Button,
Skeleton,
+ Tabs,
+ Tab,
+ Collapse,
+ IconButton,
+ Tooltip,
} 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 KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
+import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import { usePaginatedList } from '../../../hooks/usePaginatedList';
import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals';
import { adminApi, type DashboardStats } from '../../../lib/api/admin';
import { QUERY_KEYS } from '../../../constants/queryKeys';
-import { SERVICE_CATEGORIES, PRIORITIES, PROPOSAL_STATUSES, STATUS_CHIP_STYLES, PRIORITY_CHIP_STYLES } from '../../../constants';
-import { formatCurrency, formatDate } from '../../../lib/format';
+import { 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';
-function StatCard({ icon, label, value, color }: { icon: React.ReactNode; label: string; value: string; color: string }) {
+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}
+
+ )}
);
@@ -52,6 +70,7 @@ function StatCard({ icon, label, value, color }: { icon: React.ReactNode; label:
export default function AdminDashboard({ defaultStatus }: { defaultStatus?: string }) {
useDocumentTitle(defaultStatus ? 'Admin Queue' : 'Admin Dashboard');
const navigate = useNavigate();
+ const showTabs = !defaultStatus;
const { data: stats, isLoading: statsLoading } = useQuery({
queryKey: [QUERY_KEYS.dashboard],
@@ -61,6 +80,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
const [statusFilter, setStatusFilter] = useState(defaultStatus ?? '');
const [categoryFilter, setCategoryFilter] = useState('');
const [priorityFilter, setPriorityFilter] = useState('');
+ const [expanded, setExpanded] = useState>(new Set());
const {
rows,
@@ -79,9 +99,88 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
...(priorityFilter && { priority: priorityFilter }),
});
+ 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('');
+ };
+
+ 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 (
- Admin Dashboard
+ {defaultStatus ? 'Admin Queue' : 'All Proposals'}
@@ -93,6 +192,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
label="Pending Review"
value={String(stats?.pendingCount ?? 0)}
color="#1565c0"
+ subtitle="Awaiting action"
/>
)}
@@ -105,6 +205,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
label="Approved This Week"
value={String(stats?.approvedThisWeek ?? 0)}
color="#2e7d32"
+ subtitle={`${stats?.approvedThisWeek ?? 0} approved`}
/>
)}
@@ -117,6 +218,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
label="Avg Turnaround"
value={stats ? `${Math.round(stats.avgTurnaroundHours)}h` : '-'}
color="#f57c00"
+ subtitle="Target: <24h"
/>
)}
@@ -129,6 +231,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
label="Total Proposals"
value={String(stats?.totalProposals ?? 0)}
color="#5e35b1"
+ subtitle="All time"
/>
)}
@@ -136,6 +239,19 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
+ {showTabs && (
+ = 0 ? tabIndex : 0}
+ onChange={handleTabChange}
+ sx={{ mb: 2, borderBottom: 1, borderColor: 'divider' }}
+ >
+
+ {TAB_STATUSES.slice(1).map((s) => (
+
+ ))}
+
+ )}
+
setPriorityFilter(e.target.value)}
>
{PRIORITIES.map((p) => (
-
- ))}
-
- setStatusFilter(e.target.value)}
- >
-
- {PROPOSAL_STATUSES.map((s) => (
-
+
))}
+ {!showTabs && (
+ setStatusFilter(e.target.value)}
+ >
+
+ {PROPOSAL_STATUSES.map((s) => (
+
+ ))}
+
+ )}
+ {hasActiveFilters && (
+
+ )}
{err && (
@@ -202,6 +325,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
+
Proposal #
Customer
WO #
@@ -211,43 +335,54 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
Bid Amount
Submitted
Submitted By
+ Age
{loading
? Array.from({ length: 5 }).map((_, i) => (
- {Array.from({ length: 9 }).map((_, j) => (
+ {Array.from({ length: COL_COUNT }).map((_, j) => (
))}
))
- : rows.map((row) => (
- navigate(`/admin/proposals/${row.id}`)}
- >
- {row.proposalNumber}
- {row.customerName}
- {row.workOrderNumber}
- {row.serviceCategory}
-
-
-
-
-
-
- {formatCurrency(row.totalBidAmount)}
- {formatDate(row.submittedAt)}
- {row.submittedByName || '-'}
-
- ))}
+ : 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 && (
-
- No proposals in queue
+
+
+ {hasActiveFilters
+ ? 'No proposals match your current filters.'
+ : 'No proposals in queue.'}
+
+ {hasActiveFilters && (
+
+ )}
)}
diff --git a/web/src/pages/dashboard/Dashboard.tsx b/web/src/pages/dashboard/Dashboard.tsx
index 29a67ef..52311ae 100644
--- a/web/src/pages/dashboard/Dashboard.tsx
+++ b/web/src/pages/dashboard/Dashboard.tsx
@@ -21,23 +21,29 @@ import DescriptionIcon from '@mui/icons-material/Description';
import HourglassTopIcon from '@mui/icons-material/HourglassTop';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import SendIcon from '@mui/icons-material/Send';
+import Tooltip from '@mui/material/Tooltip';
import { proposalsApi, type ProposalListItem, type ProposalStats } from '../../lib/api/proposals';
import { QUERY_KEYS } from '../../constants/queryKeys';
import { STATUS_CHIP_STYLES } from '../../constants';
-import { formatCurrency, formatDate } from '../../lib/format';
+import { formatCurrency, formatDate, formatDateFull } from '../../lib/format';
import { useDocumentTitle } from '../../hooks/useDocumentTitle';
-function KpiCard({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
+function KpiCard({ icon, label, value, subtitle }: { icon: React.ReactNode; label: string; value: string; subtitle?: string }) {
return (
- {icon}
-
+ {icon}
+
{label}
-
+
{value}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
);
@@ -84,28 +90,28 @@ export default function Dashboard() {
{isLoading ? (
) : (
- } label="Total Submitted" value={String(total)} />
+ } label="Total Submitted" value={String(total)} subtitle="All time" />
)}
{isLoading ? (
) : (
- } label="In Review" value={String(counts.inReview)} />
+ } label="In Review" value={String(counts.inReview)} subtitle="Awaiting pricing" />
)}
{isLoading ? (
) : (
- } label="Approved" value={String(counts.approved)} />
+ } label="Approved" value={String(counts.approved)} subtitle="Ready to send" />
)}
{isLoading ? (
) : (
- } label="Sent" value={String(counts.sent)} />
+ } label="Sent" value={String(counts.sent)} subtitle="Delivered to customer" />
)}
@@ -154,7 +160,11 @@ export default function Dashboard() {
{formatCurrency(row.totalBidAmount)}
- {formatDate(row.submittedAt)}
+
+
+ {formatDate(row.submittedAt)}
+
+
))}
{!isLoading && recentError && (