diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-17 08:08:33 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-17 08:08:33 +0000 |
| commit | 1540eac291761ffd8fc1947ed626e4e4a4407922 (patch) | |
| tree | a7b6ae8060e164f249651cf6ef8b0c2e868019e9 /lib/rfq-last/quotation-compare-view.tsx | |
| parent | 55b6153dfce83a1cf2be72cbc3413d78084e8da1 (diff) | |
(최겸) 견적입찰 비교관련 수정
Diffstat (limited to 'lib/rfq-last/quotation-compare-view.tsx')
| -rw-r--r-- | lib/rfq-last/quotation-compare-view.tsx | 466 |
1 files changed, 306 insertions, 160 deletions
diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx index 723d1044..527fc4d8 100644 --- a/lib/rfq-last/quotation-compare-view.tsx +++ b/lib/rfq-last/quotation-compare-view.tsx @@ -28,6 +28,8 @@ import { X, RefreshCw, Clock, + Download, + Paperclip, } from "lucide-react"; import { cn } from "@/lib/utils"; import { format } from "date-fns"; @@ -193,13 +195,14 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { switch (selectedContractType) { case "PO": + const poSelectionReason: string | undefined = selectedVendor.selectionReason || undefined; result = await createPO({ rfqId: data.rfqInfo.id, vendorId: selectedVendor.vendorId, vendorName: selectedVendor.vendorName, totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, - selectionReason: selectedVendor.selectionReason, + selectionReason: poSelectionReason, }); break; @@ -712,18 +715,29 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <table className="w-full"> <thead> <tr className="border-b"> - <th className="text-left p-3 font-semibold">항목</th> - <th className="text-left p-3 font-semibold">구매자 제시</th> + <th className="text-left p-3 font-semibold" rowSpan={2}>항목</th> + </tr> + <tr className="border-b bg-gray-50"> {data.vendors.map((vendor) => ( - <th key={vendor.vendorId} className={cn( - "text-left p-3 font-semibold", - vendor.isSelected && "bg-blue-50" - )}> - {vendor.vendorName} + <React.Fragment key={vendor.vendorId}> + <th className={cn( + "text-center p-2 text-xs font-medium border-r", + vendor.isSelected && "bg-blue-50" + )}> + SHI 제시 + </th> + <th className={cn( + "text-center p-2 text-xs font-medium", + vendor.isSelected && "bg-blue-50" + )}> + <div className="font-bold"> + {vendor.vendorName} + </div> {vendor.isSelected && ( <Badge className="ml-2 bg-blue-600 text-xs">선정</Badge> )} - </th> + </th> + </React.Fragment> ))} </tr> </thead> @@ -731,21 +745,28 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 통화 */} <tr> <td className="p-3 font-medium">통화</td> - <td className="p-3">{data.vendors[0]?.buyerConditions.currency}</td> {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; // 최신 응답 (이미 정렬되어 있음) return ( - <td key={vendor.vendorId} className={cn( - "p-3", - vendor.isSelected && "bg-blue-50" - )}> - <div className="flex items-center gap-2"> - {latestResponse?.vendorConditions?.currency || vendor.buyerConditions.currency} - {latestResponse?.vendorConditions?.currency && latestResponse.vendorConditions.currency !== vendor.buyerConditions.currency && ( - <Badge variant="destructive" className="text-xs">변경</Badge> - )} - </div> - </td> + <React.Fragment key={vendor.vendorId}> + <td className={cn( + "p-3 text-center border-r", + vendor.isSelected && "bg-blue-50" + )}> + {vendor.buyerConditions.currency} + </td> + <td className={cn( + "p-3 text-center", + vendor.isSelected && "bg-blue-50" + )}> + <div className="flex items-center justify-center gap-2"> + {latestResponse?.vendorConditions?.currency || vendor.buyerConditions.currency} + {latestResponse?.vendorConditions?.currency && latestResponse.vendorConditions.currency !== vendor.buyerConditions.currency && ( + <Badge variant="destructive" className="text-xs">변경</Badge> + )} + </div> + </td> + </React.Fragment> ); })} </tr> @@ -753,42 +774,47 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 지급조건 */} <tr> <td className="p-3 font-medium">지급조건</td> - <td className="p-3"> - <TooltipProvider> - <Tooltip> - <TooltipTrigger className="cursor-help border-b border-dashed"> - {data.vendors[0]?.buyerConditions.paymentTermsCode} - </TooltipTrigger> - <TooltipContent> - {data.vendors[0]?.buyerConditions.paymentTermsDesc} - </TooltipContent> - </Tooltip> - </TooltipProvider> - </td> {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( - <td key={vendor.vendorId} className={cn( - "p-3", - vendor.isSelected && "bg-blue-50" - )}> - <div className="flex items-center gap-2"> + <React.Fragment key={vendor.vendorId}> + <td className={cn( + "p-3 text-center border-r", + vendor.isSelected && "bg-blue-50" + )}> <TooltipProvider> <Tooltip> <TooltipTrigger className="cursor-help border-b border-dashed"> - {latestResponse?.vendorConditions?.paymentTermsCode || vendor.buyerConditions.paymentTermsCode} + {vendor.buyerConditions.paymentTermsCode} </TooltipTrigger> <TooltipContent> - {latestResponse?.vendorConditions?.paymentTermsDesc || vendor.buyerConditions.paymentTermsDesc} + {vendor.buyerConditions.paymentTermsDesc} </TooltipContent> </Tooltip> </TooltipProvider> - {latestResponse?.vendorConditions?.paymentTermsCode && - latestResponse.vendorConditions.paymentTermsCode !== vendor.buyerConditions.paymentTermsCode && ( - <Badge variant="outline" className="text-xs">변경</Badge> - )} - </div> - </td> + </td> + <td className={cn( + "p-3 text-center", + vendor.isSelected && "bg-blue-50" + )}> + <div className="flex items-center justify-center gap-2"> + <TooltipProvider> + <Tooltip> + <TooltipTrigger className="cursor-help border-b border-dashed"> + {latestResponse?.vendorConditions?.paymentTermsCode || vendor.buyerConditions.paymentTermsCode} + </TooltipTrigger> + <TooltipContent> + {latestResponse?.vendorConditions?.paymentTermsDesc || vendor.buyerConditions.paymentTermsDesc} + </TooltipContent> + </Tooltip> + </TooltipProvider> + {latestResponse?.vendorConditions?.paymentTermsCode && + latestResponse.vendorConditions.paymentTermsCode !== vendor.buyerConditions.paymentTermsCode && ( + <Badge variant="outline" className="text-xs">변경</Badge> + )} + </div> + </td> + </React.Fragment> ); })} </tr> @@ -796,42 +822,47 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 인코텀즈 */} <tr> <td className="p-3 font-medium">인코텀즈</td> - <td className="p-3"> - <TooltipProvider> - <Tooltip> - <TooltipTrigger className="cursor-help border-b border-dashed"> - {data.vendors[0]?.buyerConditions.incotermsCode} - </TooltipTrigger> - <TooltipContent> - {data.vendors[0]?.buyerConditions.incotermsDesc} - </TooltipContent> - </Tooltip> - </TooltipProvider> - </td> {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( - <td key={vendor.vendorId} className={cn( - "p-3", - vendor.isSelected && "bg-blue-50" - )}> - <div className="flex items-center gap-2"> + <React.Fragment key={vendor.vendorId}> + <td className={cn( + "p-3 text-center border-r", + vendor.isSelected && "bg-blue-50" + )}> <TooltipProvider> <Tooltip> <TooltipTrigger className="cursor-help border-b border-dashed"> - {latestResponse?.vendorConditions?.incotermsCode || vendor.buyerConditions.incotermsCode} + {vendor.buyerConditions.incotermsCode} </TooltipTrigger> <TooltipContent> - {latestResponse?.vendorConditions?.incotermsDesc || vendor.buyerConditions.incotermsDesc} + {vendor.buyerConditions.incotermsDesc} </TooltipContent> </Tooltip> </TooltipProvider> - {latestResponse?.vendorConditions?.incotermsCode && - latestResponse.vendorConditions.incotermsCode !== vendor.buyerConditions.incotermsCode && ( - <Badge variant="outline" className="text-xs">변경</Badge> - )} - </div> - </td> + </td> + <td className={cn( + "p-3 text-center", + vendor.isSelected && "bg-blue-50" + )}> + <div className="flex items-center justify-center gap-2"> + <TooltipProvider> + <Tooltip> + <TooltipTrigger className="cursor-help border-b border-dashed"> + {latestResponse?.vendorConditions?.incotermsCode || vendor.buyerConditions.incotermsCode} + </TooltipTrigger> + <TooltipContent> + {latestResponse?.vendorConditions?.incotermsDesc || vendor.buyerConditions.incotermsDesc} + </TooltipContent> + </Tooltip> + </TooltipProvider> + {latestResponse?.vendorConditions?.incotermsCode && + latestResponse.vendorConditions.incotermsCode !== vendor.buyerConditions.incotermsCode && ( + <Badge variant="outline" className="text-xs">변경</Badge> + )} + </div> + </td> + </React.Fragment> ); })} </tr> @@ -839,22 +870,29 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 선적지 */} <tr> <td className="p-3 font-medium">선적지</td> - <td className="p-3">{data.vendors[0]?.buyerConditions.placeOfShipping || "-"}</td> {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( - <td key={vendor.vendorId} className={cn( - "p-3", - vendor.isSelected && "bg-blue-50" - )}> - <div className="flex items-center gap-2"> - {latestResponse?.vendorConditions?.placeOfShipping || vendor.buyerConditions.placeOfShipping || "-"} - {latestResponse?.vendorConditions?.placeOfShipping && - latestResponse.vendorConditions.placeOfShipping !== vendor.buyerConditions.placeOfShipping && ( - <Badge variant="outline" className="text-xs">변경</Badge> - )} - </div> - </td> + <React.Fragment key={vendor.vendorId}> + <td className={cn( + "p-3 text-center border-r", + vendor.isSelected && "bg-blue-50" + )}> + {vendor.buyerConditions.placeOfShipping || "-"} + </td> + <td className={cn( + "p-3 text-center", + vendor.isSelected && "bg-blue-50" + )}> + <div className="flex items-center justify-center gap-2"> + {latestResponse?.vendorConditions?.placeOfShipping || vendor.buyerConditions.placeOfShipping || "-"} + {latestResponse?.vendorConditions?.placeOfShipping && + latestResponse.vendorConditions.placeOfShipping !== vendor.buyerConditions.placeOfShipping && ( + <Badge variant="outline" className="text-xs">변경</Badge> + )} + </div> + </td> + </React.Fragment> ); })} </tr> @@ -862,22 +900,29 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 하역지 */} <tr> <td className="p-3 font-medium">하역지</td> - <td className="p-3">{data.vendors[0]?.buyerConditions.placeOfDestination || "-"}</td> {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( - <td key={vendor.vendorId} className={cn( - "p-3", - vendor.isSelected && "bg-blue-50" - )}> - <div className="flex items-center gap-2"> - {latestResponse?.vendorConditions?.placeOfDestination || vendor.buyerConditions.placeOfDestination || "-"} - {latestResponse?.vendorConditions?.placeOfDestination && - latestResponse.vendorConditions.placeOfDestination !== vendor.buyerConditions.placeOfDestination && ( - <Badge variant="outline" className="text-xs">변경</Badge> - )} - </div> - </td> + <React.Fragment key={vendor.vendorId}> + <td className={cn( + "p-3 text-center border-r", + vendor.isSelected && "bg-blue-50" + )}> + {vendor.buyerConditions.placeOfDestination || "-"} + </td> + <td className={cn( + "p-3 text-center", + vendor.isSelected && "bg-blue-50" + )}> + <div className="flex items-center justify-center gap-2"> + {latestResponse?.vendorConditions?.placeOfDestination || vendor.buyerConditions.placeOfDestination || "-"} + {latestResponse?.vendorConditions?.placeOfDestination && + latestResponse.vendorConditions.placeOfDestination !== vendor.buyerConditions.placeOfDestination && ( + <Badge variant="outline" className="text-xs">변경</Badge> + )} + </div> + </td> + </React.Fragment> ); })} </tr> @@ -885,30 +930,35 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 납기일 */} <tr> <td className="p-3 font-medium">납기일</td> - <td className="p-3"> - {data.vendors[0]?.buyerConditions.deliveryDate - ? format(new Date(data.vendors[0].buyerConditions.deliveryDate), "yyyy-MM-dd") - : "-"} - </td> {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( - <td key={vendor.vendorId} className={cn( - "p-3", - vendor.isSelected && "bg-blue-50" - )}> - <div className="flex items-center gap-2"> - {latestResponse?.vendorConditions?.deliveryDate - ? format(new Date(latestResponse.vendorConditions.deliveryDate), "yyyy-MM-dd") - : vendor.buyerConditions.deliveryDate - ? format(new Date(vendor.buyerConditions.deliveryDate), "yyyy-MM-dd") - : "-"} - {latestResponse?.vendorConditions?.deliveryDate && vendor.buyerConditions.deliveryDate && - new Date(latestResponse.vendorConditions.deliveryDate).getTime() !== new Date(vendor.buyerConditions.deliveryDate).getTime() && ( - <Badge variant="outline" className="text-xs">변경</Badge> - )} - </div> - </td> + <React.Fragment key={vendor.vendorId}> + <td className={cn( + "p-3 text-center border-r", + vendor.isSelected && "bg-blue-50" + )}> + {vendor.buyerConditions.deliveryDate + ? format(new Date(vendor.buyerConditions.deliveryDate), "yyyy-MM-dd") + : "-"} + </td> + <td className={cn( + "p-3 text-center", + vendor.isSelected && "bg-blue-50" + )}> + <div className="flex items-center justify-center gap-2"> + {latestResponse?.vendorConditions?.deliveryDate + ? format(new Date(latestResponse.vendorConditions.deliveryDate), "yyyy-MM-dd") + : vendor.buyerConditions.deliveryDate + ? format(new Date(vendor.buyerConditions.deliveryDate), "yyyy-MM-dd") + : "-"} + {/* {latestResponse?.vendorConditions?.deliveryDate && vendor.buyerConditions.deliveryDate && + new Date(latestResponse.vendorConditions.deliveryDate).getTime() !== new Date(vendor.buyerConditions.deliveryDate).getTime() && ( + <Badge variant="outline" className="text-xs">변경</Badge> + )} */} + </div> + </td> + </React.Fragment> ); })} </tr> @@ -916,22 +966,29 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 세금조건 */} <tr> <td className="p-3 font-medium">세금조건</td> - <td className="p-3">{data.vendors[0]?.buyerConditions.taxCode || "-"}</td> {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( - <td key={vendor.vendorId} className={cn( - "p-3", - vendor.isSelected && "bg-blue-50" - )}> - <div className="flex items-center gap-2"> - {latestResponse?.vendorConditions?.taxCode || vendor.buyerConditions.taxCode || "-"} - {latestResponse?.vendorConditions?.taxCode && - latestResponse.vendorConditions.taxCode !== vendor.buyerConditions.taxCode && ( - <Badge variant="outline" className="text-xs">변경</Badge> - )} - </div> - </td> + <React.Fragment key={vendor.vendorId}> + <td className={cn( + "p-3 text-center border-r", + vendor.isSelected && "bg-blue-50" + )}> + {vendor.buyerConditions.taxCode || "-"} + </td> + <td className={cn( + "p-3 text-center", + vendor.isSelected && "bg-blue-50" + )}> + <div className="flex items-center justify-center gap-2"> + {latestResponse?.vendorConditions?.taxCode || vendor.buyerConditions.taxCode || "-"} + {latestResponse?.vendorConditions?.taxCode && + latestResponse.vendorConditions.taxCode !== vendor.buyerConditions.taxCode && ( + <Badge variant="outline" className="text-xs">변경</Badge> + )} + </div> + </td> + </React.Fragment> ); })} </tr> @@ -939,22 +996,29 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 계약기간 */} <tr> <td className="p-3 font-medium">계약기간</td> - <td className="p-3">{data.vendors[0]?.buyerConditions.contractDuration || "-"}</td> {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( - <td key={vendor.vendorId} className={cn( - "p-3", - vendor.isSelected && "bg-blue-50" - )}> - <div className="flex items-center gap-2"> - {latestResponse?.vendorConditions?.contractDuration || vendor.buyerConditions.contractDuration || "-"} - {latestResponse?.vendorConditions?.contractDuration && - latestResponse.vendorConditions.contractDuration !== vendor.buyerConditions.contractDuration && ( - <Badge variant="outline" className="text-xs">변경</Badge> - )} - </div> - </td> + <React.Fragment key={vendor.vendorId}> + <td className={cn( + "p-3 text-center border-r", + vendor.isSelected && "bg-blue-50" + )}> + {vendor.buyerConditions.contractDuration || "-"} + </td> + <td className={cn( + "p-3 text-center", + vendor.isSelected && "bg-blue-50" + )}> + <div className="flex items-center justify-center gap-2"> + {latestResponse?.vendorConditions?.contractDuration || vendor.buyerConditions.contractDuration || "-"} + {latestResponse?.vendorConditions?.contractDuration && + latestResponse.vendorConditions.contractDuration !== vendor.buyerConditions.contractDuration && ( + <Badge variant="outline" className="text-xs">변경</Badge> + )} + </div> + </td> + </React.Fragment> ); })} </tr> @@ -980,11 +1044,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { <tr className="border-b bg-gray-50"> <th className="text-left p-3 font-semibold">품목코드</th> <th className="text-left p-3 font-semibold">품목명</th> + <th className="text-left p-3 font-semibold">자재분류</th> <th className="text-right p-3 font-semibold">수량</th> + <th className="text-left p-3 font-semibold">규격</th> + <th className="text-right p-3 font-semibold">중량</th> <th className="text-right p-3 font-semibold">단가</th> <th className="text-right p-3 font-semibold">총액</th> - <th className="text-left p-3 font-semibold">납기</th> - <th className="text-left p-3 font-semibold">제조사</th> + <th className="text-left p-3 font-semibold">납기일자</th> </tr> </thead> <tbody className="divide-y"> @@ -994,15 +1060,30 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { return ( <tr key={quoteItem.prItemId} className="hover:bg-gray-50"> - <td className="p-3 text-sm">{prItem.materialCode}</td> - <td className="p-3"> - <p className="font-medium">{prItem.materialDescription}</p> + <td className="p-3 text-sm"> + <p className="font-medium">{prItem.materialCode}</p> <p className="text-xs text-muted-foreground"> {prItem.prNo} • {prItem.prItem} </p> </td> + <td className="p-3"> + <p className="font-medium">{prItem.materialDescription}</p> + {prItem.remark && ( + <p className="text-xs text-muted-foreground mt-1">{prItem.remark}</p> + )} + </td> + <td className="p-3 text-sm"> + {prItem.materialCategory || "-"} + </td> <td className="p-3 text-right"> - {quoteItem.quantity} {prItem.uom} + <p>{quoteItem.quantity} {prItem.uom}</p> + <p className="text-xs text-muted-foreground">요청: {prItem.requestedQuantity}</p> + </td> + <td className="p-3 text-sm"> + {prItem.size || "-"} + </td> + <td className="p-3 text-right text-sm"> + {prItem.grossWeight ? `${prItem.grossWeight} ${prItem.gwUom || ""}` : "-"} </td> <td className="p-3 text-right font-medium"> {formatAmount(quoteItem.unitPrice, quoteItem.currency)} @@ -1016,16 +1097,11 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { : quoteItem.leadTime ? `${quoteItem.leadTime}일` : "-"} - </td> - <td className="p-3 text-sm"> - {quoteItem.manufacturer ? ( - <div> - <p>{quoteItem.manufacturer}</p> - {quoteItem.modelNo && ( - <p className="text-xs text-muted-foreground">{quoteItem.modelNo}</p> - )} - </div> - ) : "-"} + {prItem.requestedDeliveryDate && ( + <p className="text-xs text-muted-foreground"> + 요청: {format(new Date(prItem.requestedDeliveryDate), "yyyy-MM-dd")} + </p> + )} </td> </tr> ); @@ -1054,6 +1130,76 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { </div> </div> )} + + {/* 첨부파일 */} + {selectedResponse?.attachments && selectedResponse.attachments.length > 0 && ( + <div className="mt-6 pt-6 border-t"> + <h4 className="font-semibold mb-3 flex items-center gap-2"> + <Paperclip className="h-4 w-4" /> + 제출 문서 ({selectedResponse.attachments.length}건) + </h4> + <div className="space-y-2"> + {selectedResponse.attachments.map((attachment) => ( + <div + key={attachment.id} + className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50" + > + <div className="flex items-center gap-3 flex-1"> + <FileText className="h-5 w-5 text-blue-500" /> + <div className="flex-1 min-w-0"> + <p className="font-medium text-sm truncate"> + {attachment.originalFileName} + </p> + <div className="flex items-center gap-3 text-xs text-muted-foreground mt-1"> + <span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded"> + {attachment.attachmentType} + </span> + {attachment.documentNo && ( + <span>문서번호: {attachment.documentNo}</span> + )} + {attachment.fileSize && ( + <span>{(attachment.fileSize / 1024).toFixed(1)} KB</span> + )} + {attachment.uploadedAt && ( + <span> + 업로드: {format(new Date(attachment.uploadedAt), "yyyy-MM-dd HH:mm")} + </span> + )} + </div> + {attachment.description && ( + <p className="text-xs text-muted-foreground mt-1"> + {attachment.description} + </p> + )} + {(attachment.validFrom || attachment.validTo) && ( + <p className="text-xs text-orange-600 mt-1"> + 유효기간: {attachment.validFrom ? format(new Date(attachment.validFrom), "yyyy-MM-dd") : "-"} ~ {attachment.validTo ? format(new Date(attachment.validTo), "yyyy-MM-dd") : "-"} + </p> + )} + </div> + </div> + <Button + size="sm" + variant="outline" + onClick={() => { + // 다운로드 처리 + const link = document.createElement('a'); + link.href = attachment.filePath; + link.download = attachment.originalFileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }} + className="gap-1" + > + <Download className="h-3 w-3" /> + 다운로드 + </Button> + </div> + ))} + </div> + </div> + )} </DialogContent> </Dialog> |
