"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 { ComparisonData, selectVendor, cancelVendorSelection } 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 [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" | "">(""); 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(""); 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 result = await selectVendor({ rfqId: data.rfqInfo.id, vendorId: vendor.vendorId, vendorName: vendor.vendorName, vendorCode: vendor.vendorCode, totalAmount: vendor.totalAmount, currency: vendor.currency, selectionReason: selectionReason, priceRank: vendor.rank || 0, hasConditionDifferences: vendor.conditionDifferences.hasDifferences, criticalDifferences: vendor.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}개 업체 평균

)} {/* 가격 범위 */} 가격 범위

{((data.summary.priceRange.max - data.summary.priceRange.min) / data.summary.priceRange.min * 100).toFixed(1)}%

최저가 대비 최고가 차이

{/* 조건 불일치 */} 조건 불일치

{data.vendors.filter(v => v.conditionDifferences.hasDifferences).length}개

제시 조건과 차이 있음

{/* 탭 뷰 */} 종합 비교 조건 비교 아이템별 비교 상세 분석 {/* 종합 비교 */} 가격 순위 및 업체 선정
{data.vendors.map((vendor) => (
{ if (!hasSelection) { setSelectedVendorId(vendor.vendorId.toString()); } }} >
{!hasSelection && ( setSelectedVendorId(e.target.value)} className="h-4 w-4 text-blue-600" /> )}
{vendor.rank}

{vendor.vendorName} {vendor.isSelected && ( 선정 )}

{vendor.vendorCode} • {vendor.vendorCountry}

{/* 조건 차이 표시 */} {vendor.conditionDifferences.criticalDifferences.length > 0 && ( 중요 차이 {vendor.conditionDifferences.criticalDifferences.length}
{vendor.conditionDifferences.criticalDifferences.map((diff, idx) => (

{diff}

))}
)} {/* 가격 정보 */}

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

{vendor.priceVariance && vendor.priceVariance > 0 ? "+" : ""} {vendor.priceVariance?.toFixed(1)}% vs 평균

))}
{/* 조건 비교 */} 거래 조건 비교 {data.vendors.map((vendor) => ( ))} {/* 통화 */} {data.vendors.map((vendor) => ( ))} {/* 지급조건 */} {data.vendors.map((vendor) => ( ))} {/* 나머지 조건들도 동일한 패턴으로 처리 */}
항목 구매자 제시 {vendor.vendorName} {vendor.isSelected && ( 선정 )}
통화 {data.vendors[0]?.buyerConditions.currency}
{vendor.vendorConditions.currency || vendor.buyerConditions.currency} {vendor.vendorConditions.currency !== vendor.buyerConditions.currency && ( 변경 )}
지급조건 {data.vendors[0]?.buyerConditions.paymentTermsCode} {data.vendors[0]?.buyerConditions.paymentTermsDesc}
{vendor.vendorConditions.paymentTermsCode || vendor.buyerConditions.paymentTermsCode} {vendor.vendorConditions.paymentTermsDesc || vendor.buyerConditions.paymentTermsDesc} {vendor.vendorConditions.paymentTermsCode && vendor.vendorConditions.paymentTermsCode !== vendor.buyerConditions.paymentTermsCode && ( 변경 )}
{/* 아이템별 비교 */} PR 아이템별 가격 비교
{data.prItems.map((item) => ( toggleItemExpansion(item.prItemId)} >
{expandedItems.has(item.prItemId) ? ( ) : ( )}

{item.materialDescription}

{item.materialCode} • {item.prNo} • {item.requestedQuantity} {item.uom}

단가 범위

{formatAmount(item.priceAnalysis.lowestPrice)} ~ {formatAmount(item.priceAnalysis.highestPrice)}

{item.vendorQuotes.map((quote) => ( ))}
벤더 단가 총액 수량 납기 제조사 순위
{quote.vendorName} {formatAmount(quote.unitPrice, quote.currency)} {formatAmount(quote.totalPrice, quote.currency)} {quote.quotedQuantity} {quote.deliveryDate ? format(new Date(quote.deliveryDate), "yyyy-MM-dd") : quote.leadTime ? `${quote.leadTime}일` : "-"} {quote.manufacturer && (

{quote.manufacturer}

{quote.modelNo && (

{quote.modelNo}

)}
)}
#{quote.priceRank}
{/* 가격 분석 요약 */}

평균 단가

{formatAmount(item.priceAnalysis.averagePrice)}

가격 편차

±{formatAmount(item.priceAnalysis.priceVariance)}

최저가 업체

{item.vendorQuotes.find(q => q.priceRank === 1)?.vendorName}

가격 차이

{((item.priceAnalysis.highestPrice - item.priceAnalysis.lowestPrice) / item.priceAnalysis.lowestPrice * 100).toFixed(1)}%

))}
{/* 상세 분석 */}
{/* 위험 요소 분석 */} 위험 요소 분석
{data.vendors.map((vendor) => { if (!vendor.conditionDifferences.hasDifferences) return null; return (

{vendor.vendorName}

{vendor.conditionDifferences.criticalDifferences.length > 0 && (

중요 차이점:

{vendor.conditionDifferences.criticalDifferences.map((diff, idx) => (

• {diff}

))}
)} {vendor.conditionDifferences.differences.length > 0 && (

일반 차이점:

{vendor.conditionDifferences.differences.map((diff, idx) => (

• {diff}

))}
)}
); })}
{/* 추천 사항 */} 선정 추천
{/* 가격 기준 추천 */}

가격 우선 선정

{data.vendors[0]?.vendorName} - {formatAmount(data.vendors[0]?.totalAmount || 0)}

{data.vendors[0]?.conditionDifferences.hasDifferences && (

⚠️ 조건 차이 검토 필요

)}
{/* 조건 준수 기준 추천 */}

조건 준수 우선 선정

{(() => { const compliantVendor = data.vendors.find(v => !v.conditionDifferences.hasDifferences); if (compliantVendor) { return (

{compliantVendor.vendorName} - {formatAmount(compliantVendor.totalAmount)}

모든 조건 충족 (가격 순위: #{compliantVendor.rank})

); } return (

모든 조건을 충족하는 벤더 없음

); })()}
{/* 균형 추천 */}

균형 선정 (추천)

{(() => { // 가격 순위와 조건 차이를 고려한 점수 계산 const scoredVendors = data.vendors.map(v => ({ ...v, score: (v.rank || 10) + v.conditionDifferences.criticalDifferences.length * 3 + v.conditionDifferences.differences.length })); scoredVendors.sort((a, b) => a.score - b.score); const recommended = scoredVendors[0]; return (

{recommended.vendorName} - {formatAmount(recommended.totalAmount)}

가격 순위 #{recommended.rank}, 조건 차이 최소화

); })()}
{/* 벤더별 비고사항 */} {data.vendors.some(v => v.generalRemark || v.technicalProposal) && ( 벤더 제안사항 및 비고
{data.vendors.map((vendor) => { if (!vendor.generalRemark && !vendor.technicalProposal) return null; return (

{vendor.vendorName}

{vendor.generalRemark && (

일반 비고:

{vendor.generalRemark}

)} {vendor.technicalProposal && (

기술 제안:

{vendor.technicalProposal}

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

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

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