From 5036cf2908792cef45f06256e71f10920f647f49 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 28 May 2025 19:03:21 +0000 Subject: (김준회) 기술영업 조선 RFQ (SHI/벤더) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor-quotation-comparison-dialog.tsx | 340 +++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 lib/techsales-rfq/table/detail-table/vendor-quotation-comparison-dialog.tsx (limited to 'lib/techsales-rfq/table/detail-table/vendor-quotation-comparison-dialog.tsx') diff --git a/lib/techsales-rfq/table/detail-table/vendor-quotation-comparison-dialog.tsx b/lib/techsales-rfq/table/detail-table/vendor-quotation-comparison-dialog.tsx new file mode 100644 index 00000000..d58dbd00 --- /dev/null +++ b/lib/techsales-rfq/table/detail-table/vendor-quotation-comparison-dialog.tsx @@ -0,0 +1,340 @@ +"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" +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog" + +// Lucide 아이콘 +import { Plus, Minus, CheckCircle, Loader2 } from "lucide-react" + +import { getTechSalesVendorQuotationsWithJoin } from "@/lib/techsales-rfq/service" +import { acceptTechSalesVendorQuotationAction } from "@/lib/techsales-rfq/actions" +import { formatCurrency, formatDate } from "@/lib/utils" + +// 기술영업 견적 정보 타입 +interface TechSalesVendorQuotation { + id: number + rfqId: number + vendorId: number + vendorName?: string | null + totalPrice: string | null + currency: string | null + validUntil: Date | null + status: string + remark: string | null + submittedAt: Date | null + acceptedAt: Date | null + createdAt: Date + updatedAt: Date +} + +interface VendorQuotationComparisonDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + selectedRfq: { + id: number; + rfqCode: string | null; + status: string; + [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any + } | null +} + +export function VendorQuotationComparisonDialog({ + open, + onOpenChange, + selectedRfq, +}: VendorQuotationComparisonDialogProps) { + const [isLoading, setIsLoading] = useState(false) + const [quotations, setQuotations] = useState([]) + const [selectedVendorId, setSelectedVendorId] = useState(null) + const [isAccepting, setIsAccepting] = useState(false) + const [showConfirmDialog, setShowConfirmDialog] = useState(false) + + useEffect(() => { + async function loadQuotationData() { + if (!open || !selectedRfq?.id) return + + try { + setIsLoading(true) + // 기술영업 견적 목록 조회 (제출된 견적만) + const result = await getTechSalesVendorQuotationsWithJoin({ + rfqId: selectedRfq.id, + page: 1, + perPage: 100, + filters: [ + { + id: "status" as keyof typeof techSalesVendorQuotations, + value: "Submitted", + type: "select" as const, + operator: "eq" as const, + rowId: "status" + } + ] + }) + + setQuotations(result.data || []) + } 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" + } + } + + // 벤더 선택 핸들러 + const handleSelectVendor = (vendorId: number) => { + setSelectedVendorId(vendorId) + setShowConfirmDialog(true) + } + + // 벤더 선택 확정 + const handleConfirmSelection = async () => { + if (!selectedVendorId) return + + try { + setIsAccepting(true) + + // 선택된 견적의 ID 찾기 + const selectedQuotation = quotations.find(q => q.vendorId === selectedVendorId) + if (!selectedQuotation) { + toast.error("선택된 견적을 찾을 수 없습니다") + return + } + + // 벤더 선택 API 호출 + const result = await acceptTechSalesVendorQuotationAction(selectedQuotation.id) + + if (result.success) { + toast.success(result.message || "벤더가 선택되었습니다") + setShowConfirmDialog(false) + onOpenChange(false) + + // 페이지 새로고침 또는 데이터 재로드 + window.location.reload() + } else { + toast.error(result.error || "벤더 선택에 실패했습니다") + } + } catch (error) { + console.error("벤더 선택 오류:", error) + toast.error("벤더 선택에 실패했습니다") + } finally { + setIsAccepting(false) + } + } + + const selectedVendor = quotations.find(q => q.vendorId === selectedVendorId) + + return ( + <> + + + + 벤더 견적 비교 및 선택 + + {selectedRfq + ? `RFQ ${selectedRfq.rfqCode} - 제출된 견적을 비교하고 벤더를 선택하세요` + : ""} + + + + {isLoading ? ( +
+ + +
+ ) : quotations.length === 0 ? ( +
+ 제출된(Submitted) 견적이 없습니다 +
+ ) : ( +
+ + + + + 항목 + + {quotations.map((q) => ( + +
+ {q.vendorName || `벤더 ID: ${q.vendorId}`} + +
+
+ ))} +
+ + + {/* 견적 상태 */} + + + 견적 상태 + + {quotations.map((q) => ( + + + {q.status} + + + ))} + + + {/* 총 금액 */} + + + 총 금액 + + {quotations.map((q) => ( + + {q.totalPrice ? formatCurrency(Number(q.totalPrice), q.currency || 'USD') : '-'} + + ))} + + + {/* 통화 */} + + + 통화 + + {quotations.map((q) => ( + + {q.currency || '-'} + + ))} + + + {/* 유효기간 */} + + + 유효 기간 + + {quotations.map((q) => ( + + {q.validUntil ? formatDate(q.validUntil, "KR") : '-'} + + ))} + + + {/* 제출일 */} + + + 제출일 + + {quotations.map((q) => ( + + {q.submittedAt ? formatDate(q.submittedAt, "KR") : '-'} + + ))} + + + {/* 비고 */} + + + 비고 + + {quotations.map((q) => ( + + {q.remark || "-"} + + ))} + + +
+
+ )} + + + + +
+
+ + {/* 벤더 선택 확인 다이얼로그 */} + + + + 벤더 선택 확인 + + {selectedVendor?.vendorName || `벤더 ID: ${selectedVendorId}`}를 선택하시겠습니까? +
+
+ 선택된 벤더의 견적이 승인되며, 다른 벤더들의 견적은 자동으로 거절됩니다. + 이 작업은 되돌릴 수 없습니다. +
+
+ + 취소 + + {isAccepting && } + 확인 + + +
+
+ + ) +} -- cgit v1.2.3