From 5e89e42e6a964d3f5f15359326b1da8094b13852 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Wed, 20 May 2026 18:09:11 -0400 Subject: [PATCH] Extract shared constants and format utils, wire admin dashboard filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move STATUS_COLORS and PRIORITY_COLORS to constants/index.ts and formatCurrency/formatDate/formatDateTime to lib/format.ts — previously duplicated across 5 and 4 files respectively. AdminDashboard: Wire Category and Priority filter dropdowns to usePaginatedList extraParams (were no-op onChange handlers). ProposalDetailPage: Add 'Revised' to STATUS_ORDER so the stepper renders correctly for revised proposals. --- web/src/constants/index.ts | 14 +++++++ web/src/lib/format.ts | 18 ++++++++ .../pages/admin/dashboard/AdminDashboard.tsx | 42 +++++++------------ .../proposals/detail/ProposalDetailPage.tsx | 26 ++---------- .../pages/proposals/list/ProposalListPage.tsx | 18 +------- 5 files changed, 51 insertions(+), 67 deletions(-) create mode 100644 web/src/lib/format.ts diff --git a/web/src/constants/index.ts b/web/src/constants/index.ts index ac30c2d..ed93bf0 100644 --- a/web/src/constants/index.ts +++ b/web/src/constants/index.ts @@ -19,3 +19,17 @@ 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 PRIORITIES = ['Standard', 'Urgent', 'Emergency'] as const; + +export const STATUS_COLORS: Record = { + Draft: 'default', + InReview: 'info', + Approved: 'success', + Sent: 'success', + Revised: 'warning', +}; + +export const PRIORITY_COLORS: Record = { + Standard: 'default', + Urgent: 'warning', + Emergency: 'error', +}; diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts new file mode 100644 index 0000000..3a72acc --- /dev/null +++ b/web/src/lib/format.ts @@ -0,0 +1,18 @@ +export function formatCurrency(amount: number): string { + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); +} + +export function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + +export function formatDateTime(iso: string | null): string { + if (!iso) return '-'; + return new Date(iso).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + }); +} diff --git a/web/src/pages/admin/dashboard/AdminDashboard.tsx b/web/src/pages/admin/dashboard/AdminDashboard.tsx index bdc54a8..e6c22c3 100644 --- a/web/src/pages/admin/dashboard/AdminDashboard.tsx +++ b/web/src/pages/admin/dashboard/AdminDashboard.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { @@ -28,29 +29,8 @@ 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 } from '../../../constants'; - -const STATUS_COLORS: Record = { - Draft: 'default', - InReview: 'info', - Approved: 'success', - Sent: 'success', - Revised: 'warning', -}; - -const PRIORITY_COLORS: Record = { - Standard: 'default', - Urgent: 'warning', - Emergency: 'error', -}; - -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); -} - -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); -} +import { SERVICE_CATEGORIES, PRIORITIES, STATUS_COLORS, PRIORITY_COLORS } from '../../../constants'; +import { formatCurrency, formatDate } from '../../../lib/format'; function StatCard({ icon, label, value, color }: { icon: React.ReactNode; label: string; value: string; color: string }) { return ( @@ -76,6 +56,9 @@ export default function AdminDashboard() { queryFn: adminApi.getDashboard, }); + const [categoryFilter, setCategoryFilter] = useState(''); + const [priorityFilter, setPriorityFilter] = useState(''); + const { rows, search, @@ -87,7 +70,10 @@ export default function AdminDashboard() { totalCount, loading, err, - } = usePaginatedList(proposalsApi.getAll); + } = usePaginatedList(proposalsApi.getAll, { + ...(categoryFilter && { serviceCategory: categoryFilter }), + ...(priorityFilter && { priority: priorityFilter }), + }); return ( @@ -167,9 +153,9 @@ export default function AdminDashboard() { size="small" select label="Category" - value="" + value={categoryFilter} sx={{ width: 160 }} - onChange={() => {}} + onChange={(e) => setCategoryFilter(e.target.value)} > All {SERVICE_CATEGORIES.map((c) => ( @@ -180,9 +166,9 @@ export default function AdminDashboard() { size="small" select label="Priority" - value="" + value={priorityFilter} sx={{ width: 140 }} - onChange={() => {}} + onChange={(e) => setPriorityFilter(e.target.value)} > All {PRIORITIES.map((p) => ( diff --git a/web/src/pages/proposals/detail/ProposalDetailPage.tsx b/web/src/pages/proposals/detail/ProposalDetailPage.tsx index cacbecc..4c87695 100644 --- a/web/src/pages/proposals/detail/ProposalDetailPage.tsx +++ b/web/src/pages/proposals/detail/ProposalDetailPage.tsx @@ -16,30 +16,10 @@ import { } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { proposalsApi, type ProposalDetail } from '../../../lib/api/proposals'; +import { STATUS_COLORS } from '../../../constants'; +import { formatCurrency, formatDateTime } from '../../../lib/format'; -const STATUS_ORDER = ['Draft', 'InReview', 'Approved', 'Sent']; -const STATUS_COLORS: Record = { - Draft: 'default', - InReview: 'info', - Approved: 'success', - Sent: 'success', - Revised: 'warning', -}; - -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); -} - -function formatDateTime(iso: string | null): string { - if (!iso) return '-'; - return new Date(iso).toLocaleString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - }); -} +const STATUS_ORDER = ['Draft', 'InReview', 'Approved', 'Sent', 'Revised']; function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { return ( diff --git a/web/src/pages/proposals/list/ProposalListPage.tsx b/web/src/pages/proposals/list/ProposalListPage.tsx index 829a66a..63c2c68 100644 --- a/web/src/pages/proposals/list/ProposalListPage.tsx +++ b/web/src/pages/proposals/list/ProposalListPage.tsx @@ -21,22 +21,8 @@ import SearchIcon from '@mui/icons-material/Search'; import AddCircleIcon from '@mui/icons-material/AddCircle'; import { usePaginatedList } from '../../../hooks/usePaginatedList'; import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals'; - -const STATUS_COLORS: Record = { - Draft: 'default', - InReview: 'info', - Approved: 'success', - Sent: 'success', - Revised: 'warning', -}; - -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); -} - -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); -} +import { STATUS_COLORS } from '../../../constants'; +import { formatCurrency, formatDate } from '../../../lib/format'; export default function ProposalListPage() { const navigate = useNavigate();