mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-03 17:43:18 +00:00
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:
parent
4d72b63547
commit
5e89e42e6a
5 changed files with 51 additions and 67 deletions
|
|
@ -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
18
web/src/lib/format.ts
Normal 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue