mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-09-30 08:53:15 +00:00
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.
155 lines
5 KiB
TypeScript
155 lines
5 KiB
TypeScript
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Card,
|
|
CardContent,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
TablePagination,
|
|
Chip,
|
|
TextField,
|
|
InputAdornment,
|
|
Button,
|
|
Skeleton,
|
|
} from '@mui/material';
|
|
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';
|
|
import { STATUS_COLORS } from '../../../constants';
|
|
import { formatCurrency, formatDate } from '../../../lib/format';
|
|
|
|
export default function ProposalListPage() {
|
|
const navigate = useNavigate();
|
|
const mineParams = { mine: true };
|
|
const {
|
|
rows,
|
|
search,
|
|
setSearch,
|
|
page,
|
|
setPage,
|
|
pageSize,
|
|
setPageSize,
|
|
totalCount,
|
|
loading,
|
|
err,
|
|
} = usePaginatedList<ProposalListItem>(proposalsApi.getAll, mineParams);
|
|
|
|
return (
|
|
<Box>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
<Typography variant="h5">My Proposals</Typography>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<AddCircleIcon />}
|
|
onClick={() => navigate('/proposals/new')}
|
|
>
|
|
New Proposal
|
|
</Button>
|
|
</Box>
|
|
|
|
<Card>
|
|
<CardContent sx={{ pb: 0 }}>
|
|
<TextField
|
|
size="small"
|
|
placeholder="Search proposals..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
sx={{ mb: 2, width: 320 }}
|
|
slotProps={{
|
|
input: {
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon fontSize="small" />
|
|
</InputAdornment>
|
|
),
|
|
},
|
|
}}
|
|
/>
|
|
|
|
{err && (
|
|
<Typography color="error" sx={{ mb: 2 }}>
|
|
{err}
|
|
</Typography>
|
|
)}
|
|
|
|
<TableContainer>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Proposal #</TableCell>
|
|
<TableCell>Customer</TableCell>
|
|
<TableCell>WO #</TableCell>
|
|
<TableCell>Category</TableCell>
|
|
<TableCell>Priority</TableCell>
|
|
<TableCell>Status</TableCell>
|
|
<TableCell align="right">Bid Amount</TableCell>
|
|
<TableCell>Submitted</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{loading
|
|
? Array.from({ length: 5 }).map((_, i) => (
|
|
<TableRow key={i}>
|
|
{Array.from({ length: 8 }).map((_, j) => (
|
|
<TableCell key={j}>
|
|
<Skeleton variant="text" />
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
: rows.map((row) => (
|
|
<TableRow
|
|
key={row.id}
|
|
hover
|
|
sx={{ cursor: 'pointer' }}
|
|
onClick={() => navigate(`/proposals/${row.id}`)}
|
|
>
|
|
<TableCell sx={{ fontWeight: 600 }}>{row.proposalNumber}</TableCell>
|
|
<TableCell>{row.customerName}</TableCell>
|
|
<TableCell>{row.workOrderNumber}</TableCell>
|
|
<TableCell>{row.serviceCategory}</TableCell>
|
|
<TableCell>{row.priority}</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
label={row.status}
|
|
size="small"
|
|
color={STATUS_COLORS[row.status] || 'default'}
|
|
/>
|
|
</TableCell>
|
|
<TableCell align="right">{formatCurrency(row.totalBidAmount)}</TableCell>
|
|
<TableCell>{formatDate(row.submittedAt)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{!loading && rows.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={8} align="center" sx={{ py: 4 }}>
|
|
<Typography color="text.secondary">
|
|
No proposals found. Create your first one!
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
|
|
<TablePagination
|
|
component="div"
|
|
count={totalCount}
|
|
page={page - 1}
|
|
onPageChange={(_, newPage) => setPage(newPage + 1)}
|
|
rowsPerPage={pageSize}
|
|
onRowsPerPageChange={(e) => setPageSize(parseInt(e.target.value, 10))}
|
|
rowsPerPageOptions={[12, 24, 48]}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
);
|
|
}
|