From 4ee8b24cfadf47452807fa2af801385ed60ab47c Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 15 Sep 2025 14:41:01 +0000 Subject: (대표님) 작업사항 - rfqLast, tbeLast, pdfTron, userAuth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rfq-last/quotation-compare-view.tsx | 755 ++++++++++++++++++++++++++++++++ 1 file changed, 755 insertions(+) create mode 100644 lib/rfq-last/quotation-compare-view.tsx (limited to 'lib/rfq-last/quotation-compare-view.tsx') diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx new file mode 100644 index 00000000..0e15a7bf --- /dev/null +++ b/lib/rfq-last/quotation-compare-view.tsx @@ -0,0 +1,755 @@ +"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 { + Trophy, + TrendingUp, + TrendingDown, + AlertCircle, + CheckCircle, + XCircle, + ChevronDown, + ChevronUp, + Info, + DollarSign, + Calendar, + Package, + Globe, + FileText, + Truck, + AlertTriangle, +} 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 type { ComparisonData, VendorComparison, PrItemComparison } from "../actions"; + +interface QuotationCompareViewProps { + data: ComparisonData; +} + +export function QuotationCompareView({ data }: QuotationCompareViewProps) { + const [expandedItems, setExpandedItems] = React.useState>(new Set()); + const [selectedMetric, setSelectedMetric] = React.useState<"price" | "delivery" | "compliance">("price"); + + // 아이템 확장/축소 토글 + 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 getComplianceIcon = (matches: boolean) => { + return matches ? ( + + ) : ( + + ); + }; + + // 금액 포맷 + const formatAmount = (amount: number, currency: string = "USD") => { + return new Intl.NumberFormat("ko-KR", { + style: "currency", + currency: currency, + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }).format(amount); + }; + + return ( +
+ {/* 요약 카드 */} +
+ {/* 최저가 벤더 */} + + + + + 최저가 벤더 + + + +

{data.summary.lowestBidder}

+

+ {formatAmount(data.summary.priceRange.min, data.summary.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) => ( +
+
+
+ {vendor.rank} +
+
+

{vendor.vendorName}

+

+ {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) => ( + + ))} + + + {/* 인코텀즈 */} + + + + {data.vendors.map((vendor) => ( + + ))} + + + {/* 납기 */} + + + + {data.vendors.map((vendor) => { + const vendorDate = vendor.vendorConditions.deliveryDate || vendor.buyerConditions.deliveryDate; + const isDelayed = vendorDate && vendor.buyerConditions.deliveryDate && + new Date(vendorDate) > new Date(vendor.buyerConditions.deliveryDate); + + return ( + + ); + })} + + + {/* 초도품 */} + + + + {data.vendors.map((vendor) => ( + + ))} + + + {/* 스페어파트 */} + + + + {data.vendors.map((vendor) => ( + + ))} + + + {/* 연동제 */} + + + + {data.vendors.map((vendor) => ( + + ))} + + +
항목구매자 제시 + {vendor.vendorName} +
통화{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 && ( + 변경 + )} +
+
인코텀즈{data.vendors[0]?.buyerConditions.incotermsCode} +
+ {vendor.vendorConditions.incotermsCode || vendor.buyerConditions.incotermsCode} + {vendor.vendorConditions.incotermsCode !== vendor.buyerConditions.incotermsCode && ( + 변경 + )} +
+
납기 + {data.vendors[0]?.buyerConditions.deliveryDate + ? format(new Date(data.vendors[0].buyerConditions.deliveryDate), "yyyy-MM-dd") + : "-"} + +
+ {vendorDate ? format(new Date(vendorDate), "yyyy-MM-dd") : "-"} + {isDelayed && ( + 지연 + )} +
+
초도품 + {data.vendors[0]?.buyerConditions.firstYn ? "요구" : "해당없음"} + + {vendor.buyerConditions.firstYn && ( + + {vendor.vendorConditions.firstAcceptance || "미응답"} + + )} + {!vendor.buyerConditions.firstYn && "-"} +
스페어파트 + {data.vendors[0]?.buyerConditions.sparepartYn ? "요구" : "해당없음"} + + {vendor.buyerConditions.sparepartYn && ( + + {vendor.vendorConditions.sparepartAcceptance || "미응답"} + + )} + {!vendor.buyerConditions.sparepartYn && "-"} +
연동제 + {data.vendors[0]?.buyerConditions.materialPriceRelatedYn ? "적용" : "미적용"} + +
+ {vendor.vendorConditions.materialPriceRelatedYn !== undefined + ? vendor.vendorConditions.materialPriceRelatedYn ? "적용" : "미적용" + : vendor.buyerConditions.materialPriceRelatedYn ? "적용" : "미적용"} + {vendor.vendorConditions.materialPriceRelatedReason && ( + + + + + + +

+ {vendor.vendorConditions.materialPriceRelatedReason} +

+
+
+
+ )} +
+
+
+
+
+ + {/* 아이템별 비교 */} + + + + 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}

+
+ )} +
+ ); + })} +
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file -- cgit v1.2.3