proposal-system/web/src/components/admin/VendorDataPanel.tsx
Adam Moussa 9d856a9619
Some checks failed
Deploy / Deploy to AWS (push) Has been cancelled
Phase 3 audit fixes: FIX-01–47, accessibility NITs, code quality NITs [skip deploy]
## Summary
Implements Phase 3 of the AUDIT-2026-05-20 findings:
- 29 FIX-severity items across API, web, infra, and lambdas
- 7 accessibility NITs (aria-labels, document titles)
- 4 code quality NITs (deduplication, constants extraction)

Key changes:
- API: N+1 fix, pagination clamping, idempotent transitions, upload confirm endpoint, revision TotalBidAmount carry-forward
- Web: confirmation dialogs, currency formatting, error states, date range filters, document titles
- Infra: S3 CORS lockdown, API Gateway throttling, AOSS network policy fix, CI concurrency
- Lambdas: skip empty suggestions, remove status side-effect
- Scripts: post-deploy health check

## Test plan
- [x] tsc --noEmit (web + infra)
- [x] dotnet build (api)
- [x] ruff check + format (lambdas)
- [x] Cross-review via orchestrator (no blockers)

[skip deploy]
2026-05-20 19:38:36 -04:00

91 lines
2.9 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { Box, Typography, Skeleton, Chip } from '@mui/material';
import apiClient from '../../lib/api/client';
import { formatCurrency } from '../../lib/format';
interface VendorProposal {
id: string;
vendorName: string;
fileName: string;
totalVendorCost: number;
processingStatus: string;
extractedData: {
lineItems?: Array<{
description: string;
quantity?: number;
unitPrice?: number;
total?: number;
}>;
} | null;
}
interface VendorDataPanelProps {
proposalId: string;
}
export default function VendorDataPanel({ proposalId }: VendorDataPanelProps) {
const { data: vendors, isLoading } = useQuery<VendorProposal[]>({
queryKey: ['vendorProposals', proposalId],
queryFn: async () => {
const res = await apiClient.get(`/proposals/${proposalId}/vendors`);
return res.data;
},
enabled: !!proposalId,
});
if (isLoading) {
return <Skeleton variant="rectangular" height={60} />;
}
if (!vendors || vendors.length === 0) {
return (
<Box>
<Typography variant="subtitle2" sx={{ mb: 0.5 }}>Vendor Data</Typography>
<Typography variant="body2" color="text.secondary">No vendor proposals attached</Typography>
</Box>
);
}
return (
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Vendor Data</Typography>
{vendors.map((vp) => (
<Box key={vp.id} sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{vp.vendorName}</Typography>
<Chip
label={vp.processingStatus}
size="small"
color={vp.processingStatus === 'Complete' ? 'success' : 'default'}
variant="outlined"
/>
</Box>
<Typography variant="caption" color="text.secondary">{vp.fileName}</Typography>
{vp.totalVendorCost > 0 && (
<Typography variant="body2" sx={{ mt: 0.5, fontWeight: 600 }}>
Vendor Total: {formatCurrency(vp.totalVendorCost)}
</Typography>
)}
{vp.extractedData?.lineItems && vp.extractedData.lineItems.length > 0 && (
<Box sx={{ mt: 1, pl: 1, borderLeft: 2, borderColor: 'divider' }}>
{vp.extractedData.lineItems.map((li, i) => (
<Box key={i} sx={{ mb: 0.5 }}>
<Typography variant="caption" sx={{ display: 'block' }}>
{li.description}
</Typography>
{li.total != null && (
<Typography variant="caption" color="text.secondary">
{formatCurrency(li.total)}
</Typography>
)}
</Box>
))}
</Box>
)}
</Box>
))}
</Box>
);
}