"use client"; import * as React from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Trophy, TrendingUp, TrendingDown, AlertCircle, CheckCircle, XCircle, ChevronDown, ChevronUp, Info, DollarSign, Calendar, Package, Globe, FileText, Truck, AlertTriangle, Award, UserCheck, X, RefreshCw, Clock, } from "lucide-react"; import { cn } from "@/lib/utils"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ComparisonData, selectVendor, cancelVendorSelection, VendorResponseVersion } from "./compare-action"; import { createPO, createGeneralContract, createBidding } from "./contract-actions"; import { toast } from "sonner"; import { useRouter } from "next/navigation" interface QuotationCompareViewProps { data: ComparisonData; } export function QuotationCompareView({ data }: QuotationCompareViewProps) { const [expandedItems, setExpandedItems] = React.useState>(new Set()); const [selectedVendorId, setSelectedVendorId] = React.useState(""); const [showSelectionDialog, setShowSelectionDialog] = React.useState(false); const [showCancelDialog, setShowCancelDialog] = React.useState(false); const [showContractDialog, setShowContractDialog] = React.useState(false); const [showItemDetailsDialog, setShowItemDetailsDialog] = React.useState(false); const [selectedResponse, setSelectedResponse] = React.useState(null); const [selectedVendorName, setSelectedVendorName] = React.useState(""); const [selectedContractType, setSelectedContractType] = React.useState<"PO" | "CONTRACT" | "BIDDING" | "">(""); const [selectionReason, setSelectionReason] = React.useState(""); const [cancelReason, setCancelReason] = React.useState(""); const [isSubmitting, setIsSubmitting] = React.useState(false); const router = useRouter() const [selectedGeneralContractType, setSelectedGeneralContractType] = React.useState(""); const [contractStartDate, setContractStartDate] = React.useState(""); const [contractEndDate, setContractEndDate] = React.useState(""); // 계약종류 옵션 const contractTypes = [ { value: 'UP', label: 'UP - 자재단가계약' }, { value: 'LE', label: 'LE - 임대차계약' }, { value: 'IL', label: 'IL - 개별운송계약' }, { value: 'AL', label: 'AL - 연간운송계약' }, { value: 'OS', label: 'OS - 외주용역계약' }, { value: 'OW', label: 'OW - 도급계약' }, { value: 'IS', label: 'IS - 검사계약' }, { value: 'LO', label: 'LO - LOI (의향서)' }, { value: 'FA', label: 'FA - Frame Agreement' }, { value: 'SC', label: 'SC - 납품합의계약' }, { value: 'OF', label: 'OF - 클레임상계계약' }, { value: 'AW', label: 'AW - 사전작업합의' }, { value: 'AD', label: 'AD - 사전납품합의' }, { value: 'AM', label: 'AM - 설계계약' }, { value: 'SC_SELL', label: 'SC - 폐기물매각계약' }, ]; // 입찰 관련 state 추가 const [biddingContractType, setBiddingContractType] = React.useState<"unit_price" | "general" | "sale">("unit_price"); const [biddingType, setBiddingType] = React.useState(""); const [awardCount, setAwardCount] = React.useState<"single" | "multiple">("single"); const [biddingStartDate, setBiddingStartDate] = React.useState(""); const [biddingEndDate, setBiddingEndDate] = React.useState(""); // 입찰 옵션들 const biddingContractTypes = [ { value: 'unit_price', label: '단가계약' }, { value: 'general', label: '일반계약' }, { value: 'sale', label: '매각계약' } ]; const biddingTypes = [ { value: 'equipment', label: '기자재' }, { value: 'construction', label: '공사' }, { value: 'service', label: '용역' }, { value: 'lease', label: '임차' }, { value: 'steel_stock', label: '형강스톡' }, { value: 'piping', label: '배관' }, { value: 'transport', label: '운송' }, { value: 'waste', label: '폐기물' }, { value: 'sale', label: '매각' } ]; const awardCounts = [ { value: 'single', label: '단수' }, { value: 'multiple', label: '복수' } ]; // 선정된 업체 정보 확인 const selectedVendor = data.vendors.find(v => v.isSelected); const hasSelection = !!selectedVendor; const isSelectionApproved = selectedVendor?.selectionApprovalStatus === "승인"; const isPendingApproval = selectedVendor?.selectionApprovalStatus === "대기"; const hasContract = selectedVendor?.contractStatus ? true : false; // 계약 진행 처리 const handleContractCreation = async () => { if (!selectedContractType) { toast.error("계약 유형을 선택해주세요."); return; } // 일반계약인 경우 계약종류와 날짜 확인 if (selectedContractType === "CONTRACT") { if (!selectedGeneralContractType) { toast.error("계약종류를 선택해주세요."); return; } if (!contractStartDate) { toast.error("계약 시작일을 선택해주세요."); return; } if (!contractEndDate) { toast.error("계약 종료일을 선택해주세요."); return; } if (new Date(contractStartDate) >= new Date(contractEndDate)) { toast.error("계약 종료일은 시작일보다 이후여야 합니다."); return; } } // 입찰 검증 if (selectedContractType === "BIDDING") { if (!biddingContractType) { toast.error("계약구분을 선택해주세요."); return; } if (!biddingType) { toast.error("입찰유형을 선택해주세요."); return; } if (!biddingStartDate || !biddingEndDate) { toast.error("입찰 기간을 입력해주세요."); return; } if (new Date(biddingStartDate) >= new Date(biddingEndDate)) { toast.error("입찰 마감일은 시작일보다 이후여야 합니다."); return; } } if (!selectedVendor) { toast.error("선정된 업체가 없습니다."); return; } setIsSubmitting(true); try { let result; switch (selectedContractType) { case "PO": result = await createPO({ rfqId: data.rfqInfo.id, vendorId: selectedVendor.vendorId, vendorName: selectedVendor.vendorName, totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, selectionReason: selectedVendor.selectionReason, }); break; case "CONTRACT": result = await createGeneralContract({ rfqId: data.rfqInfo.id, vendorId: selectedVendor.vendorId, vendorName: selectedVendor.vendorName, totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, contractStartDate: new Date(contractStartDate), contractEndDate: new Date(contractEndDate), contractType: selectedGeneralContractType, // 계약종류 추가 }); break; case "BIDDING": result = await createBidding({ rfqId: data.rfqInfo.id, vendorId: selectedVendor.vendorId, vendorName: selectedVendor.vendorName, totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, contractType: biddingContractType, biddingType: biddingType, awardCount: awardCount, biddingStartDate: new Date(biddingStartDate), biddingEndDate: new Date(biddingEndDate), }); break; default: throw new Error("올바른 계약 유형이 아닙니다."); } if (result.success) { toast.success(result.message || "계약 프로세스가 시작되었습니다."); setShowContractDialog(false); // 모든 state 초기화 setSelectedContractType(""); setSelectedGeneralContractType(""); setContractStartDate(""); setContractEndDate(""); setBiddingContractType("unit_price"); setBiddingType(""); setAwardCount("single"); setBiddingStartDate(""); setBiddingEndDate(""); router.refresh(); } else { throw new Error(result.error || "계약 진행 중 오류가 발생했습니다."); } } catch (error) { console.error("계약 생성 오류:", error); toast.error(error instanceof Error ? error.message : "계약 진행 중 오류가 발생했습니다."); } finally { setIsSubmitting(false); } }; // 컴포넌트 마운트 시 선정된 업체가 있으면 자동 선택 React.useEffect(() => { if (selectedVendor) { setSelectedVendorId(selectedVendor.vendorId.toString()); } }, [selectedVendor]); // 아이템 확장/축소 토글 const toggleItemExpansion = (itemId: number) => { setExpandedItems((prev) => { const newSet = new Set(prev); if (newSet.has(itemId)) { newSet.delete(itemId); } else { newSet.add(itemId); } return newSet; }); }; // 순위에 따른 색상 const getRankColor = (rank: number) => { switch (rank) { case 1: return "text-green-600 bg-green-50"; case 2: return "text-blue-600 bg-blue-50"; case 3: return "text-orange-600 bg-orange-50"; default: return "text-gray-600 bg-gray-50"; } }; // 가격 차이 색상 const getVarianceColor = (variance: number) => { if (variance < -5) return "text-green-600"; if (variance > 5) return "text-red-600"; return "text-gray-600"; }; // 금액 포맷 const formatAmount = (amount: number, currency: string = "KRW") => { return new Intl.NumberFormat("ko-KR", { style: "currency", currency: currency, minimumFractionDigits: 0, maximumFractionDigits: 2, }).format(amount); }; // 업체 선정 처리 const handleVendorSelection = async () => { if (!selectedVendorId) { toast.error("선정할 업체를 선택해주세요."); return; } if (!selectionReason.trim()) { toast.error("선정 사유를 입력해주세요."); return; } setIsSubmitting(true); try { const vendor = data.vendors.find(v => v.vendorId === parseInt(selectedVendorId)); if (!vendor) { throw new Error("선택한 업체를 찾을 수 없습니다."); } // 최신 응답 정보 사용 const latestResponse = vendor.responses[0]; if (!latestResponse) { throw new Error("업체의 견적 정보를 찾을 수 없습니다."); } const result = await selectVendor({ rfqId: data.rfqInfo.id, vendorId: vendor.vendorId, vendorName: vendor.vendorName, vendorCode: vendor.vendorCode, totalAmount: latestResponse.totalAmount, currency: latestResponse.currency, selectionReason: selectionReason, priceRank: latestResponse.rank || 0, hasConditionDifferences: latestResponse.conditionDifferences.hasDifferences, criticalDifferences: latestResponse.conditionDifferences.criticalDifferences, }); if (result.success) { toast.success("업체가 성공적으로 선정되었습니다."); setShowSelectionDialog(false); setSelectionReason(""); router.refresh() } else { throw new Error(result.error || "업체 선정 중 오류가 발생했습니다."); } } catch (error) { console.error("업체 선정 오류:", error); toast.error(error instanceof Error ? error.message : "업체 선정 중 오류가 발생했습니다."); } finally { setIsSubmitting(false); } }; // 업체 선정 취소 처리 const handleCancelSelection = async () => { if (!cancelReason.trim()) { toast.error("취소 사유를 입력해주세요."); return; } setIsSubmitting(true); try { // 파라미터를 올바르게 전달 const result = await cancelVendorSelection(Number(data.rfqInfo.id), cancelReason); if (result.success) { toast.success("업체 선정이 취소되었습니다."); setShowCancelDialog(false); setCancelReason(""); router.refresh() } else { throw new Error(result.error || "선정 취소 중 오류가 발생했습니다."); } } catch (error) { console.error("선정 취소 오류:", error); toast.error(error instanceof Error ? error.message : "선정 취소 중 오류가 발생했습니다."); } finally { setIsSubmitting(false); } }; return (
{/* 상단 액션 바 */}

견적 비교 분석

{hasSelection ? ( <> {!isSelectionApproved && ( )} ) : ( )}
{/* 선정 상태 알림 */} {hasSelection && (
{hasContract ? ( ) : isSelectionApproved ? ( ) : isPendingApproval ? ( ) : ( )}
{hasContract ? "계약 진행중" : isSelectionApproved ? "업체 선정 승인 완료" : isPendingApproval ? "업체 선정 승인 대기중" : "업체 선정 완료"}

선정 업체: {selectedVendor.vendorName} ({selectedVendor.vendorCode})

선정 금액: {formatAmount(selectedVendor.totalAmount, selectedVendor.currency)}

선정일: {selectedVendor.selectionDate ? format(new Date(selectedVendor.selectionDate), "yyyy년 MM월 dd일", { locale: ko }) : "-"}

선정 사유: {selectedVendor.selectionReason || "-"}

{selectedVendor.contractNo && ( <>

계약 정보

계약 번호: {selectedVendor.contractNo}

계약 상태: {selectedVendor.contractStatus}

{selectedVendor.contractCreatedAt && (

계약 생성일: {format(new Date(selectedVendor.contractCreatedAt), "yyyy년 MM월 dd일", { locale: ko })}

)}
)} {selectedVendor.selectedByName && (

선정자: {selectedVendor.selectedByName}

)}
{/* 계약 진행 버튼들을 알림 카드 안에도 추가 (선택사항) */} {!hasContract && !isPendingApproval && (
{/* */}
)}
)} {/* 요약 카드 */}
{/* 최저가 벤더 */} 최저가 벤더

{data.summary.lowestBidder}

{formatAmount(data.summary.priceRange.min, data.summary.currency)}

{/* 선정 업체 또는 평균 가격 */} {hasSelection ? ( 선정 업체

{selectedVendor.vendorName}

{formatAmount(selectedVendor.totalAmount, selectedVendor.currency)}

) : ( 평균 가격

{formatAmount(data.summary.priceRange.average, data.summary.currency)}

{data.vendors.length}개 업체 평균

)}
{/* 탭 뷰 */} 종합 비교 조건 비교 {/* 종합 비교 */} 협력업체별 차수별 견적
{!hasSelection && } {(() => { // 모든 차수 추출 (중복 제거 및 내림차순 정렬) const allVersions = Array.from( new Set( data.vendors.flatMap(v => v.responses.map(r => r.responseVersion)) ) ).sort((a, b) => b - a); return allVersions.map(version => ( )); })()} {data.vendors.map((vendor) => ( {!hasSelection && ( )} {(() => { const allVersions = Array.from( new Set( data.vendors.flatMap(v => v.responses.map(r => r.responseVersion)) ) ).sort((a, b) => b - a); return allVersions.map(version => { const response = vendor.responses.find(r => r.responseVersion === version); return ( ); }); })()} ))}
선택협력사 코드 협력사 명 {version}차
setSelectedVendorId(e.target.value)} className="h-4 w-4 text-blue-600" /> {vendor.vendorCode}
{vendor.vendorName} {vendor.isSelected && ( 선정 )}
{response ? ( ) : ( - )}
{/* 조건 비교 */} 거래 조건 비교 {data.vendors.map((vendor) => ( ))} {/* 통화 */} {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; // 최신 응답 (이미 정렬되어 있음) return ( ); })} {/* 지급조건 */} {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( ); })} {/* 인코텀즈 */} {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( ); })} {/* 선적지 */} {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( ); })} {/* 하역지 */} {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( ); })} {/* 납기일 */} {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( ); })} {/* 세금조건 */} {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( ); })} {/* 계약기간 */} {data.vendors.map((vendor) => { const latestResponse = vendor.responses[0]; return ( ); })}
항목 구매자 제시 {vendor.vendorName} {vendor.isSelected && ( 선정 )}
통화 {data.vendors[0]?.buyerConditions.currency}
{latestResponse?.vendorConditions?.currency || vendor.buyerConditions.currency} {latestResponse?.vendorConditions?.currency && latestResponse.vendorConditions.currency !== vendor.buyerConditions.currency && ( 변경 )}
지급조건 {data.vendors[0]?.buyerConditions.paymentTermsCode} {data.vendors[0]?.buyerConditions.paymentTermsDesc}
{latestResponse?.vendorConditions?.paymentTermsCode || vendor.buyerConditions.paymentTermsCode} {latestResponse?.vendorConditions?.paymentTermsDesc || vendor.buyerConditions.paymentTermsDesc} {latestResponse?.vendorConditions?.paymentTermsCode && latestResponse.vendorConditions.paymentTermsCode !== vendor.buyerConditions.paymentTermsCode && ( 변경 )}
인코텀즈 {data.vendors[0]?.buyerConditions.incotermsCode} {data.vendors[0]?.buyerConditions.incotermsDesc}
{latestResponse?.vendorConditions?.incotermsCode || vendor.buyerConditions.incotermsCode} {latestResponse?.vendorConditions?.incotermsDesc || vendor.buyerConditions.incotermsDesc} {latestResponse?.vendorConditions?.incotermsCode && latestResponse.vendorConditions.incotermsCode !== vendor.buyerConditions.incotermsCode && ( 변경 )}
선적지 {data.vendors[0]?.buyerConditions.placeOfShipping || "-"}
{latestResponse?.vendorConditions?.placeOfShipping || vendor.buyerConditions.placeOfShipping || "-"} {latestResponse?.vendorConditions?.placeOfShipping && latestResponse.vendorConditions.placeOfShipping !== vendor.buyerConditions.placeOfShipping && ( 변경 )}
하역지 {data.vendors[0]?.buyerConditions.placeOfDestination || "-"}
{latestResponse?.vendorConditions?.placeOfDestination || vendor.buyerConditions.placeOfDestination || "-"} {latestResponse?.vendorConditions?.placeOfDestination && latestResponse.vendorConditions.placeOfDestination !== vendor.buyerConditions.placeOfDestination && ( 변경 )}
납기일 {data.vendors[0]?.buyerConditions.deliveryDate ? format(new Date(data.vendors[0].buyerConditions.deliveryDate), "yyyy-MM-dd") : "-"}
{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() && ( 변경 )}
세금조건 {data.vendors[0]?.buyerConditions.taxCode || "-"}
{latestResponse?.vendorConditions?.taxCode || vendor.buyerConditions.taxCode || "-"} {latestResponse?.vendorConditions?.taxCode && latestResponse.vendorConditions.taxCode !== vendor.buyerConditions.taxCode && ( 변경 )}
계약기간 {data.vendors[0]?.buyerConditions.contractDuration || "-"}
{latestResponse?.vendorConditions?.contractDuration || vendor.buyerConditions.contractDuration || "-"} {latestResponse?.vendorConditions?.contractDuration && latestResponse.vendorConditions.contractDuration !== vendor.buyerConditions.contractDuration && ( 변경 )}
{/* 품목별 상세 정보 다이얼로그 */} {selectedVendorName} - {selectedResponse?.responseVersion}차 품목별 견적 상세
{selectedResponse?.quotationItems?.map((quoteItem) => { const prItem = data.prItems.find(item => item.prItemId === quoteItem.prItemId); if (!prItem) return null; return ( ); })}
품목코드 품목명 수량 단가 총액 납기 제조사
{prItem.materialCode}

{prItem.materialDescription}

{prItem.prNo} • {prItem.prItem}

{quoteItem.quantity} {prItem.uom} {formatAmount(quoteItem.unitPrice, quoteItem.currency)} {formatAmount(quoteItem.totalPrice, quoteItem.currency)} {quoteItem.deliveryDate ? format(new Date(quoteItem.deliveryDate), "yyyy-MM-dd") : quoteItem.leadTime ? `${quoteItem.leadTime}일` : "-"} {quoteItem.manufacturer ? (

{quoteItem.manufacturer}

{quoteItem.modelNo && (

{quoteItem.modelNo}

)}
) : "-"}
{/* 비고사항 */} {(selectedResponse?.generalRemark || selectedResponse?.technicalProposal) && (

비고사항

{selectedResponse.generalRemark && (

일반 비고:

{selectedResponse.generalRemark}

)} {selectedResponse.technicalProposal && (

기술 제안:

{selectedResponse.technicalProposal}

)}
)}
{/* 업체 선정 모달 */} {showSelectionDialog && (

{hasSelection ? "업체 재선정 확인" : "업체 선정 확인"}

{selectedVendorId && (() => { const vendor = data.vendors.find(v => v.vendorId === parseInt(selectedVendorId)); const latestResponse = vendor?.responses[0]; return (
선정 업체 {vendor?.vendorName}
견적 금액 {formatAmount( latestResponse?.totalAmount || 0, latestResponse?.currency || "USD" )}
가격 순위 #{latestResponse?.rank || 0}
{latestResponse?.conditionDifferences.hasDifferences && ( 제시 조건과 차이가 있습니다. 선정 사유를 명확히 기재해주세요. )}