mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-05 02:32:02 +00:00
136 lines
4.5 KiB
TypeScript
136 lines
4.5 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 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: async () => {
|
||
|
|
const result = await adminApi.getSimilar(proposalId);
|
||
|
|
return result as 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 || similar.length === 0) && (
|
||
|
|
<Typography variant="body2" color="text.secondary">
|
||
|
|
No similar proposals found. AI suggestions will populate this panel once the RAG engine is active.
|
||
|
|
</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} @ ${li.totalPrice.toFixed(2)}
|
||
|
|
</Typography>
|
||
|
|
</Box>
|
||
|
|
{!disabled && (
|
||
|
|
<Button
|
||
|
|
size="small"
|
||
|
|
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>
|
||
|
|
);
|
||
|
|
}
|