"use client" import * as React from "react" import { useEffect, useState } from "react" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { toast } from "sonner" // Lucide 아이콘 import { Plus, Minus } from "lucide-react" import { ProcurementRfqsView } from "@/db/schema" import { fetchVendorQuotations, fetchQuotationItems } from "@/lib/procurement-rfqs/services" import { formatCurrency, formatDate } from "@/lib/utils" // 견적 정보 타입 interface VendorQuotation { id: number rfqId: number vendorId: number vendorName?: string | null quotationCode: string quotationVersion: number totalItemsCount: number subTotal: string taxTotal: string discountTotal: string totalPrice: string currency: string validUntil: string | Date // 수정: string | Date 허용 estimatedDeliveryDate: string | Date // 수정: string | Date 허용 paymentTermsCode: string paymentTermsDescription?: string | null incotermsCode: string incotermsDescription?: string | null incotermsDetail: string status: string remark: string rejectionReason: string submittedAt: string | Date // 수정: string | Date 허용 acceptedAt: string | Date // 수정: string | Date 허용 createdAt: string | Date // 수정: string | Date 허용 updatedAt: string | Date // 수정: string | Date 허용 } // 견적 아이템 타입 interface QuotationItem { id: number quotationId: number prItemId: number materialCode: string | null // Changed from string to string | null materialDescription: string | null // Changed from string to string | null quantity: string uom: string | null // Changed assuming this might be null unitPrice: string totalPrice: string currency: string | null // Changed from string to string | null vendorMaterialCode: string | null // Changed from string to string | null vendorMaterialDescription: string | null // Changed from string to string | null deliveryDate: Date | null // Changed from string to string | null leadTimeInDays: number | null // Changed from number to number | null taxRate: string | null // Changed from string to string | null taxAmount: string | null // Changed from string to string | null discountRate: string | null // Changed from string to string | null discountAmount: string | null // Changed from string to string | null remark: string | null // Changed from string to string | null isAlternative: boolean | null // Changed from boolean to boolean | null isRecommended: boolean | null // Changed from boolean to boolean | null } interface VendorQuotationComparisonDialogProps { open: boolean onOpenChange: (open: boolean) => void selectedRfq: ProcurementRfqsView | null } export function VendorQuotationComparisonDialog({ open, onOpenChange, selectedRfq, }: VendorQuotationComparisonDialogProps) { const [isLoading, setIsLoading] = useState(false) const [quotations, setQuotations] = useState([]) const [quotationItems, setQuotationItems] = useState>({}) const [activeTab, setActiveTab] = useState("summary") // 벤더별 접힘 상태 (true=접힘, false=펼침), 기본값: 접힘 const [collapsedVendors, setCollapsedVendors] = useState>({}) useEffect(() => { async function loadQuotationData() { if (!open || !selectedRfq?.id) return try { setIsLoading(true) // 1) 견적 목록 const quotationsResult = await fetchVendorQuotations(selectedRfq.id) const rawQuotationsData = quotationsResult.data || [] const quotationsData = rawQuotationsData.map((rawData): VendorQuotation => ({ id: rawData.id, rfqId: rawData.rfqId, vendorId: rawData.vendorId, vendorName: rawData.vendorName || null, quotationCode: rawData.quotationCode || '', quotationVersion: rawData.quotationVersion || 0, totalItemsCount: rawData.totalItemsCount || 0, subTotal: rawData.subTotal || '0', taxTotal: rawData.taxTotal || '0', discountTotal: rawData.discountTotal || '0', totalPrice: rawData.totalPrice || '0', currency: rawData.currency || 'KRW', validUntil: rawData.validUntil || '', estimatedDeliveryDate: rawData.estimatedDeliveryDate || '', paymentTermsCode: rawData.paymentTermsCode || '', paymentTermsDescription: rawData.paymentTermsDescription || null, incotermsCode: rawData.incotermsCode || '', incotermsDescription: rawData.incotermsDescription || null, incotermsDetail: rawData.incotermsDetail || '', status: rawData.status || '', remark: rawData.remark || '', rejectionReason: rawData.rejectionReason || '', submittedAt: rawData.submittedAt || '', acceptedAt: rawData.acceptedAt || '', createdAt: rawData.createdAt || '', updatedAt: rawData.updatedAt || '', })); setQuotations(quotationsData); // 벤더별로 접힘 상태 기본값(true) 설정 const collapsedInit: Record = {} quotationsData.forEach((q) => { collapsedInit[q.id] = true }) setCollapsedVendors(collapsedInit) // 2) 견적 아이템 const qIds = quotationsData.map((q) => q.id) if (qIds.length > 0) { const itemsResult = await fetchQuotationItems(qIds) const itemsData = itemsResult.data || [] const itemsByQuotation: Record = {} itemsData.forEach((item) => { if (!itemsByQuotation[item.quotationId]) { itemsByQuotation[item.quotationId] = [] } itemsByQuotation[item.quotationId].push(item) }) setQuotationItems(itemsByQuotation) } } catch (error) { console.error("견적 데이터 로드 오류:", error) toast.error("견적 데이터를 불러오는 데 실패했습니다") } finally { setIsLoading(false) } } loadQuotationData() }, [open, selectedRfq]) // 견적 상태 -> 뱃지 색 const getStatusBadgeVariant = (status: string) => { switch (status) { case "Submitted": return "default" case "Accepted": return "default" case "Rejected": return "destructive" case "Revised": return "destructive" default: return "secondary" } } // 모든 prItemId 모음 const allItemIds = React.useMemo(() => { const itemSet = new Set() Object.values(quotationItems).forEach((items) => { items.forEach((it) => itemSet.add(it.prItemId)) }) return Array.from(itemSet) }, [quotationItems]) // 아이템 찾는 함수 const findItemByQuotationId = (prItemId: number, qid: number) => { const items = quotationItems[qid] || [] return items.find((i) => i.prItemId === prItemId) } // 접힘 상태 토글 const toggleVendor = (qid: number) => { setCollapsedVendors((prev) => ({ ...prev, [qid]: !prev[qid], })) } return ( {/* 다이얼로그 자체는 max-h, max-w 설정, 내부에 스크롤 컨테이너를 둠 */} 벤더 견적 비교 {selectedRfq ? `RFQ ${selectedRfq.rfqCode} - ${selectedRfq.itemName || ""}` : ""} {isLoading ? (
) : quotations.length === 0 ? (
제출된(Submitted) 견적이 없습니다
) : ( 견적 요약 비교 아이템별 비교 {/* ======================== 요약 비교 탭 ======================== */} {/* table-fixed + 가로 너비를 크게 잡아줌 (예: w-[1200px]) -> 컨테이너보다 넓으면 수평 스크롤 발생. */}
항목 {quotations.map((q) => ( {q.vendorName || `벤더 ID: ${q.vendorId}`} ))} {/* 견적 상태 */} 견적 상태 {quotations.map((q) => ( {q.status} ))} {/* 견적 버전 */} 견적 버전 {quotations.map((q) => ( v{q.quotationVersion} ))} {/* 총 금액 */} 총 금액 {quotations.map((q) => ( {formatCurrency(Number(q.totalPrice), q.currency)} ))} {/* 소계 */} 소계 {quotations.map((q) => ( {formatCurrency(Number(q.subTotal), q.currency)} ))} {/* 세금 */} 세금 {quotations.map((q) => ( {formatCurrency(Number(q.taxTotal), q.currency)} ))} {/* 할인 */} 할인 {quotations.map((q) => ( {formatCurrency(Number(q.discountTotal), q.currency)} ))} {/* 통화 */} 통화 {quotations.map((q) => ( {q.currency} ))} {/* 유효기간 */} 유효 기간 {quotations.map((q) => ( {formatDate(q.validUntil, "KR")} ))} {/* 예상 배송일 */} 예상 배송일 {quotations.map((q) => ( {formatDate(q.estimatedDeliveryDate, "KR")} ))} {/* 지불 조건 */} 지불 조건 {quotations.map((q) => ( {q.paymentTermsDescription || q.paymentTermsCode} ))} {/* 인코텀즈 */} 인코텀즈 {quotations.map((q) => ( {q.incotermsDescription || q.incotermsCode} {q.incotermsDetail && (
{q.incotermsDetail}
)}
))}
{/* 제출일 */} 제출일 {quotations.map((q) => ( {formatDate(q.submittedAt, "KR")} ))} {/* 비고 */} 비고 {quotations.map((q) => ( {q.remark || "-"} ))}
{/* ====================== 아이템별 비교 탭 ====================== */} {/* 컨테이너에 테이블 관련 클래스 직접 적용 */}
{/* 첫 번째 헤더 행 */} {/* 첫 행: 자재(코드) 컬럼 */} {/* 벤더 헤더 (접힘/펼침) */} {quotations.map((q, index) => { const collapsed = collapsedVendors[q.id] // 접힌 상태면 1칸, 펼친 상태면 6칸 return ( ) })} {/* 두 번째 헤더 행 - 하위 컬럼들 */} {/* 펼쳐진 벤더의 하위 컬럼들만 표시 */} {quotations.flatMap((q, qIndex) => { // 접힌 상태면 추가 헤더 없음 if (collapsedVendors[q.id]) { return [ ]; } // 펼친 상태면 6개 컬럼 표시 const columns = [ { key: 'unitprice', label: '단가' }, { key: 'totalprice', label: '총액' }, { key: 'tax', label: '세금' }, { key: 'discount', label: '할인' }, { key: 'leadtime', label: '리드타임' }, { key: 'alternative', label: '대체품' }, ]; return columns.map((col, colIndex) => { const isFirstInGroup = colIndex === 0; const isLastInGroup = colIndex === columns.length - 1; return ( ); }); })} {/* 테이블 바디 */} {allItemIds.map((itemId) => { // 자재 기본 정보는 첫 번째 벤더 아이템 기준 const firstQid = quotations[0]?.id const sampleItem = firstQid ? findItemByQuotationId(itemId, firstQid) : undefined return ( {/* 자재 (코드) 셀 */} {/* 벤더별 아이템 데이터 */} {quotations.flatMap((q, qIndex) => { const collapsed = collapsedVendors[q.id] const itemData = findItemByQuotationId(itemId, q.id) // 접힌 상태면 총액만 표시 if (collapsed) { return [ ]; } // 펼친 상태 - 아이템 없음 if (!itemData) { return [ ]; } // 펼친 상태 - 모든 컬럼 표시 const columns = [ { key: 'unitprice', render: () => formatCurrency(Number(itemData.unitPrice), itemData.currency), align: 'right' }, { key: 'totalprice', render: () => formatCurrency(Number(itemData.totalPrice), itemData.currency), align: 'right', bold: true }, { key: 'tax', render: () => itemData.taxRate ? `${itemData.taxRate}% (${formatCurrency(Number(itemData.taxAmount), itemData.currency)})` : "-", align: 'right' }, { key: 'discount', render: () => itemData.discountRate ? `${itemData.discountRate}% (${formatCurrency(Number(itemData.discountAmount), itemData.currency)})` : "-", align: 'right' }, { key: 'leadtime', render: () => itemData.leadTimeInDays ? `${itemData.leadTimeInDays}일` : "-", align: 'center' }, { key: 'alternative', render: () => itemData.isAlternative ? "대체품" : "표준품", align: 'center' }, ]; return columns.map((col, colIndex) => { const isFirstInGroup = colIndex === 0; const isLastInGroup = colIndex === columns.length - 1; return ( ); }); })} ); })} {/* 아이템이 전혀 없는 경우 */} {allItemIds.length === 0 && ( )}
자재 (코드) {/* + / - 버튼 */}
{q.vendorName || `벤더 ID: ${q.vendorId}`}
총액 {col.label}
{sampleItem?.materialDescription || sampleItem?.materialCode || ""} {sampleItem && (
코드: {sampleItem.materialCode} | 수량:{" "} {sampleItem.quantity} {sampleItem.uom}
)}
{itemData ? formatCurrency(Number(itemData.totalPrice), itemData.currency) : "N/A"} 없음 {col.render()}
아이템 정보가 없습니다
)}
) }