mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-04 23:02:03 +00:00
- 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
128 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|