proposal-system/web/src/components/admin/SimilarProposalsPanel.tsx
Adam Moussa f9081fabf4 docs: Phase 6 cleanup — update AUDIT-REPORT, README, remove stale docs
- AUDIT-REPORT.md: mark all Phase 6 findings fixed (API-M2/M5/M7/M9/M10/M12/M13,
  WEB-M3/M4/M8/M9/M11, LAM-M2/M3/M6/M9, INF-M1/M2/M9), update test count to 149
- README.md: Function URL NONE→AWS_IAM, add Testing and Security sections,
  expand CI table with test jobs, note SQS encryption/OpenSearch VPC/access logging
- Remove stale session docs (AUDIT-2026-05-20, HANDOFF, RETROSPECTIVE, CHATGPT prompt)
- Add .claude/agents/ to .gitignore
- Remove empty-state placeholder from SimilarProposalsPanel
2026-05-27 18:18:44 -04:00

128 lines
4.4 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import {
Box,
Typography,
Skeleton,
Accordion,
AccordionSummary,
AccordionDetails,
Button,
Chip,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { adminApi } from '../../lib/api/admin';
import { formatCurrency } from '../../lib/format';
import type { EditableLineItem } from './LineItemEditor';
interface SimilarProposal {
proposalNumber: string;
customerName: string;
serviceCategory: string;
similarityScore: number;
totalBidAmount: number;
lineItems: Array<{
description: string;
quantity: number;
unit: string;
unitPrice: number | null;
totalPrice: number;
}>;
}
interface SimilarProposalsPanelProps {
proposalId: string;
onPullLineItem: (item: Omit<EditableLineItem, 'id' | 'sortOrder' | 'source'>) => void;
disabled?: boolean;
}
export default function SimilarProposalsPanel({ proposalId, onPullLineItem, disabled }: SimilarProposalsPanelProps) {
const { data: similar, isLoading } = useQuery<SimilarProposal[]>({
queryKey: ['similarProposals', proposalId],
queryFn: () => adminApi.getSimilar(proposalId) as Promise<SimilarProposal[]>,
enabled: !!proposalId,
});
if (isLoading) {
return (
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Similar Proposals</Typography>
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} variant="rectangular" height={40} sx={{ mb: 1, borderRadius: 1 }} />
))}
</Box>
);
}
return (
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>
Similar Proposals {similar && similar.length > 0 && `(${similar.length})`}
</Typography>
{similar?.map((sp, idx) => (
<Accordion key={idx} disableGutters sx={{ '&:before': { display: 'none' }, mb: 1 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ minHeight: 'auto', px: 1 }}>
<Box sx={{ width: '100%' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" sx={{ fontWeight: 600 }}>
{sp.proposalNumber}
</Typography>
<Chip
label={`${Math.round(sp.similarityScore * 100)}%`}
size="small"
variant="outlined"
color="primary"
/>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
{sp.customerName} - {sp.serviceCategory}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ px: 1, pt: 0 }}>
{sp.lineItems.map((li, liIdx) => (
<Box
key={liIdx}
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 0.5,
borderBottom: 1,
borderColor: 'divider',
}}
>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="caption" noWrap>{li.description}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
{li.quantity} {li.unit} @ {formatCurrency(li.totalPrice)}
</Typography>
</Box>
{!disabled && (
<Button
size="small"
aria-label={`Copy line item: ${li.description}`}
sx={{ minWidth: 'auto', p: 0.5 }}
onClick={() =>
onPullLineItem({
description: li.description,
quantity: li.quantity,
unit: li.unit,
unitPrice: li.unitPrice,
totalPrice: li.totalPrice,
pricingMode: li.unitPrice ? 'UnitPrice' : 'TotalPrice',
})
}
>
<ContentCopyIcon sx={{ fontSize: 14 }} />
</Button>
)}
</Box>
))}
</AccordionDetails>
</Accordion>
))}
</Box>
);
}