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 SERVICE_CATEGORIES = ['HVAC', 'Plumbing', 'Electrical', 'General', 'Renovation'] 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 { 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<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' });
}
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<ProposalListItem>(proposalsApi.getAll);
} = usePaginatedList<ProposalListItem>(proposalsApi.getAll, {
...(categoryFilter && { serviceCategory: categoryFilter }),
...(priorityFilter && { priority: priorityFilter }),
});
return (
<Box>
@ -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)}
>
<MenuItem value="">All</MenuItem>
{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)}
>
<MenuItem value="">All</MenuItem>
{PRIORITIES.map((p) => (

View file

@ -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<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',
});
}
const STATUS_ORDER = ['Draft', 'InReview', 'Approved', 'Sent', 'Revised'];
function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
return (

View file

@ -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<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 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();