Extract shared constants and format utils, wire admin dashboard filters

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.
This commit is contained in:
Adam Moussa 2026-05-20 18:09:11 -04:00
parent 4d72b63547
commit 5e89e42e6a
5 changed files with 51 additions and 67 deletions

View file

@ -19,3 +19,17 @@ export const STORAGE_KEY_SIDEBAR = 'sidebarOpen';
export const PROPOSAL_STATUSES = ['Draft', 'InReview', 'Approved', 'Sent', 'Revised'] as const; 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'] as const;
export const PRIORITIES = ['Standard', 'Urgent', 'Emergency'] as const; export const PRIORITIES = ['Standard', 'Urgent', 'Emergency'] as const;
export const STATUS_COLORS: Record<string, 'default' | 'info' | 'warning' | 'success' | 'error'> = {
Draft: 'default',
InReview: 'info',
Approved: 'success',
Sent: 'success',
Revised: 'warning',
};
export const PRIORITY_COLORS: Record<string, 'default' | 'warning' | 'error'> = {
Standard: 'default',
Urgent: 'warning',
Emergency: 'error',
};

18
web/src/lib/format.ts Normal file
View file

@ -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',
});
}

View file

@ -1,3 +1,4 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { import {
@ -28,29 +29,8 @@ import { usePaginatedList } from '../../../hooks/usePaginatedList';
import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals'; import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals';
import { adminApi, type DashboardStats } from '../../../lib/api/admin'; import { adminApi, type DashboardStats } from '../../../lib/api/admin';
import { QUERY_KEYS } from '../../../constants/queryKeys'; import { QUERY_KEYS } from '../../../constants/queryKeys';
import { SERVICE_CATEGORIES, PRIORITIES } from '../../../constants'; import { SERVICE_CATEGORIES, PRIORITIES, STATUS_COLORS, PRIORITY_COLORS } from '../../../constants';
import { formatCurrency, formatDate } from '../../../lib/format';
const STATUS_COLORS: Record<string, 'default' | 'info' | 'warning' | 'success'> = {
Draft: 'default',
InReview: 'info',
Approved: 'success',
Sent: 'success',
Revised: 'warning',
};
const PRIORITY_COLORS: Record<string, 'default' | 'warning' | 'error'> = {
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' });
}
function StatCard({ icon, label, value, color }: { icon: React.ReactNode; label: string; value: string; color: string }) { function StatCard({ icon, label, value, color }: { icon: React.ReactNode; label: string; value: string; color: string }) {
return ( return (
@ -76,6 +56,9 @@ export default function AdminDashboard() {
queryFn: adminApi.getDashboard, queryFn: adminApi.getDashboard,
}); });
const [categoryFilter, setCategoryFilter] = useState('');
const [priorityFilter, setPriorityFilter] = useState('');
const { const {
rows, rows,
search, search,
@ -87,7 +70,10 @@ export default function AdminDashboard() {
totalCount, totalCount,
loading, loading,
err, err,
} = usePaginatedList<ProposalListItem>(proposalsApi.getAll); } = usePaginatedList<ProposalListItem>(proposalsApi.getAll, {
...(categoryFilter && { serviceCategory: categoryFilter }),
...(priorityFilter && { priority: priorityFilter }),
});
return ( return (
<Box> <Box>
@ -167,9 +153,9 @@ export default function AdminDashboard() {
size="small" size="small"
select select
label="Category" label="Category"
value="" value={categoryFilter}
sx={{ width: 160 }} sx={{ width: 160 }}
onChange={() => {}} onChange={(e) => setCategoryFilter(e.target.value)}
> >
<MenuItem value="">All</MenuItem> <MenuItem value="">All</MenuItem>
{SERVICE_CATEGORIES.map((c) => ( {SERVICE_CATEGORIES.map((c) => (
@ -180,9 +166,9 @@ export default function AdminDashboard() {
size="small" size="small"
select select
label="Priority" label="Priority"
value="" value={priorityFilter}
sx={{ width: 140 }} sx={{ width: 140 }}
onChange={() => {}} onChange={(e) => setPriorityFilter(e.target.value)}
> >
<MenuItem value="">All</MenuItem> <MenuItem value="">All</MenuItem>
{PRIORITIES.map((p) => ( {PRIORITIES.map((p) => (

View file

@ -16,30 +16,10 @@ import {
} from '@mui/material'; } from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { proposalsApi, type ProposalDetail } from '../../../lib/api/proposals'; 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_ORDER = ['Draft', 'InReview', 'Approved', 'Sent', 'Revised'];
const STATUS_COLORS: Record<string, 'default' | 'info' | 'warning' | 'success' | 'error'> = {
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',
});
}
function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
return ( return (

View file

@ -21,22 +21,8 @@ import SearchIcon from '@mui/icons-material/Search';
import AddCircleIcon from '@mui/icons-material/AddCircle'; import AddCircleIcon from '@mui/icons-material/AddCircle';
import { usePaginatedList } from '../../../hooks/usePaginatedList'; import { usePaginatedList } from '../../../hooks/usePaginatedList';
import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals'; import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals';
import { STATUS_COLORS } from '../../../constants';
const STATUS_COLORS: Record<string, 'default' | 'info' | 'warning' | 'success' | 'error'> = { import { formatCurrency, formatDate } from '../../../lib/format';
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' });
}
export default function ProposalListPage() { export default function ProposalListPage() {
const navigate = useNavigate(); const navigate = useNavigate();