diff --git a/web/src/constants/index.ts b/web/src/constants/index.ts index 4cdf5c2..c3e3746 100644 --- a/web/src/constants/index.ts +++ b/web/src/constants/index.ts @@ -17,21 +17,47 @@ export const STORAGE_KEY_TOKEN = 'proposal_system_token'; export const STORAGE_KEY_SIDEBAR = 'sidebarOpen'; export const PROPOSAL_STATUSES = ['Draft', 'InReview', 'Approved', 'Sent', 'Revised'] as const; -export const SERVICE_CATEGORIES = ['HVAC', 'Plumbing', 'Electrical', 'General', 'Renovation'] as const; +export const SERVICE_CATEGORIES = ['HVAC', 'Plumbing', 'Electrical', 'General', 'Renovation', 'Other'] as const; export const PRIORITIES = ['Standard', 'Urgent', 'Emergency'] as const; -export const STATUS_COLORS: Record = { +export const STATUS_LABELS: Record = { + Draft: 'Draft', + InReview: 'In Review', + Approved: 'Approved', + Sent: 'Sent', + Revised: 'Revised', +}; + +export const STATUS_COLORS: Record = { Draft: 'default', InReview: 'info', Approved: 'success', - Sent: 'success', + Sent: 'primary', Revised: 'warning', }; export const DRAWER_WIDTH = 220; +export const PRIORITY_LABELS: Record = { + Standard: 'Standard', + Urgent: 'Urgent', + Emergency: 'Emergency Dispatch', +}; + export const PRIORITY_COLORS: Record = { Standard: 'default', Urgent: 'warning', Emergency: 'error', }; + +export const PRIORITY_ROW_SX: Record = { + Urgent: { + borderLeft: '4px solid', + borderLeftColor: 'warning.main', + }, + Emergency: { + borderLeft: '4px solid', + borderLeftColor: 'error.main', + backgroundColor: 'error.50', + }, +}; diff --git a/web/src/pages/admin/dashboard/AdminDashboard.tsx b/web/src/pages/admin/dashboard/AdminDashboard.tsx index f821aae..648e3ad 100644 --- a/web/src/pages/admin/dashboard/AdminDashboard.tsx +++ b/web/src/pages/admin/dashboard/AdminDashboard.tsx @@ -25,11 +25,13 @@ 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 { 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_COLORS, PRIORITY_COLORS } from '../../../constants'; +import { SERVICE_CATEGORIES, PRIORITIES, PROPOSAL_STATUSES, STATUS_COLORS, STATUS_LABELS, PRIORITY_LABELS, PRIORITY_COLORS, PRIORITY_ROW_SX } from '../../../constants'; import { formatCurrency, formatDate } from '../../../lib/format'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; @@ -171,12 +173,12 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri select label="Priority" value={priorityFilter} - sx={{ width: 140 }} + sx={{ width: 200 }} onChange={(e) => setPriorityFilter(e.target.value)} > All {PRIORITIES.map((p) => ( - {p} + {PRIORITY_LABELS[p] || p} ))} All {PROPOSAL_STATUSES.map((s) => ( - {s} + {STATUS_LABELS[s] || s} ))} @@ -226,7 +228,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri navigate(`/admin/proposals/${row.id}`)} > {row.proposalNumber} @@ -234,10 +236,15 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri {row.workOrderNumber} {row.serviceCategory} - + : row.priority === 'Urgent' ? : undefined} + /> - + {formatCurrency(row.totalBidAmount)} {formatDate(row.submittedAt)} diff --git a/web/src/pages/dashboard/Dashboard.tsx b/web/src/pages/dashboard/Dashboard.tsx index 235665c..00e1d95 100644 --- a/web/src/pages/dashboard/Dashboard.tsx +++ b/web/src/pages/dashboard/Dashboard.tsx @@ -23,7 +23,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import SendIcon from '@mui/icons-material/Send'; import { proposalsApi, type ProposalListItem, type ProposalStats } from '../../lib/api/proposals'; import { QUERY_KEYS } from '../../constants/queryKeys'; -import { STATUS_COLORS } from '../../constants'; +import { STATUS_COLORS, STATUS_LABELS, PRIORITY_ROW_SX } from '../../constants'; import { formatCurrency, formatDate } from '../../lib/format'; import { useDocumentTitle } from '../../hooks/useDocumentTitle'; @@ -144,14 +144,14 @@ export default function Dashboard() { navigate(`/proposals/${row.id}`)} > {row.proposalNumber} {row.customerName} {row.serviceCategory} - + {formatCurrency(row.totalBidAmount)} {formatDate(row.submittedAt)} diff --git a/web/src/pages/proposals/list/ProposalListPage.tsx b/web/src/pages/proposals/list/ProposalListPage.tsx index d11f5b4..2929bcc 100644 --- a/web/src/pages/proposals/list/ProposalListPage.tsx +++ b/web/src/pages/proposals/list/ProposalListPage.tsx @@ -1,3 +1,4 @@ +import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, @@ -11,24 +12,52 @@ import { TableHead, TableRow, TablePagination, + TableSortLabel, Chip, TextField, + MenuItem, InputAdornment, Button, Skeleton, } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; import AddCircleIcon from '@mui/icons-material/AddCircle'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import ReportIcon from '@mui/icons-material/Report'; import { usePaginatedList } from '../../../hooks/usePaginatedList'; import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals'; -import { STATUS_COLORS } from '../../../constants'; +import { STATUS_COLORS, STATUS_LABELS, PROPOSAL_STATUSES, SERVICE_CATEGORIES, PRIORITIES, PRIORITY_LABELS, PRIORITY_COLORS, PRIORITY_ROW_SX } from '../../../constants'; import { formatCurrency, formatDate } from '../../../lib/format'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; +type SortKey = keyof ProposalListItem; +type SortDirection = 'asc' | 'desc'; + +const columns: { key: SortKey; label: string; align?: 'right' }[] = [ + { key: 'proposalNumber', label: 'Proposal #' }, + { key: 'customerName', label: 'Customer' }, + { key: 'workOrderNumber', label: 'WO #' }, + { key: 'serviceCategory', label: 'Category' }, + { key: 'priority', label: 'Priority' }, + { key: 'status', label: 'Status' }, + { key: 'totalBidAmount', label: 'Bid Amount', align: 'right' }, + { key: 'submittedAt', label: 'Submitted' }, +]; + export default function ProposalListPage() { useDocumentTitle('My Proposals'); const navigate = useNavigate(); - const mineParams = { mine: true }; + const [sortKey, setSortKey] = useState('submittedAt'); + const [sortDir, setSortDir] = useState('desc'); + const [filterStatus, setFilterStatus] = useState(''); + const [filterCategory, setFilterCategory] = useState(''); + const [filterPriority, setFilterPriority] = useState(''); + const extraParams = useMemo(() => ({ + mine: true, + ...(filterStatus && { status: filterStatus }), + ...(filterCategory && { serviceCategory: filterCategory }), + ...(filterPriority && { priority: filterPriority }), + }), [filterStatus, filterCategory, filterPriority]); const { rows, search, @@ -40,7 +69,26 @@ export default function ProposalListPage() { totalCount, loading, err, - } = usePaginatedList(proposalsApi.getAll, mineParams); + } = usePaginatedList(proposalsApi.getAll, extraParams); + + const sortedRows = useMemo(() => { + if (!rows.length) return rows; + return [...rows].sort((a, b) => { + const aVal = a[sortKey] ?? ''; + const bVal = b[sortKey] ?? ''; + const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; + return sortDir === 'asc' ? cmp : -cmp; + }); + }, [rows, sortKey, sortDir]); + + const handleSort = (key: SortKey) => { + if (key === sortKey) { + setSortDir((prev) => (prev === 'asc' ? 'desc' : 'asc')); + } else { + setSortKey(key); + setSortDir('asc'); + } + }; return ( @@ -57,22 +105,63 @@ export default function ProposalListPage() { - setSearch(e.target.value)} - sx={{ mb: 2, width: 320 }} - slotProps={{ - input: { - startAdornment: ( - - - - ), - }, - }} - /> + + setSearch(e.target.value)} + sx={{ width: 260 }} + slotProps={{ + input: { + startAdornment: ( + + + + ), + }, + }} + /> + setFilterStatus(e.target.value)} + sx={{ width: 140 }} + > + All + {PROPOSAL_STATUSES.map((s) => ( + {STATUS_LABELS[s] || s} + ))} + + setFilterCategory(e.target.value)} + sx={{ width: 150 }} + > + All + {SERVICE_CATEGORIES.map((c) => ( + {c} + ))} + + setFilterPriority(e.target.value)} + sx={{ width: 200 }} + > + All + {PRIORITIES.map((p) => ( + {PRIORITY_LABELS[p] || p} + ))} + + {err && ( @@ -84,14 +173,17 @@ export default function ProposalListPage() { - Proposal # - Customer - WO # - Category - Priority - Status - Bid Amount - Submitted + {columns.map((col) => ( + + handleSort(col.key)} + > + {col.label} + + + ))} @@ -105,21 +197,28 @@ export default function ProposalListPage() { ))} )) - : rows.map((row) => ( + : sortedRows.map((row) => ( navigate(`/proposals/${row.id}`)} > {row.proposalNumber} {row.customerName} {row.workOrderNumber} {row.serviceCategory} - {row.priority} : row.priority === 'Urgent' ? : undefined} + /> + + +