"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"; 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 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 (!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, }); break; case "BIDDING": result = await createBidding({ rfqId: data.rfqInfo.id, vendorId: selectedVendor.vendorId, vendorName: selectedVendor.vendorName, totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, }); break; default: throw new Error("올바른 계약 유형이 아닙니다."); } if (result.success) { toast.success(result.message || "계약 프로세스가 시작되었습니다."); setShowContractDialog(false); setSelectedContractType(""); window.location.reload(); } 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(""); window.location.reload(); // 페이지 새로고침으로 선정 상태 반영 } 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(""); window.location.reload(); } 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 && ( 제시 조건과 차이가 있습니다. 선정 사유를 명확히 기재해주세요. )}