summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/table/detail-table/vendor-quotation-comparison-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/table/detail-table/vendor-quotation-comparison-dialog.tsx')
-rw-r--r--lib/techsales-rfq/table/detail-table/vendor-quotation-comparison-dialog.tsx340
1 files changed, 340 insertions, 0 deletions
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<TechSalesVendorQuotation[]>([])
+ const [selectedVendorId, setSelectedVendorId] = useState<number | null>(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 (
+ <>
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-[90vw] lg:max-w-5xl max-h-[90vh]">
+ <DialogHeader>
+ <DialogTitle>벤더 견적 비교 및 선택</DialogTitle>
+ <DialogDescription>
+ {selectedRfq
+ ? `RFQ ${selectedRfq.rfqCode} - 제출된 견적을 비교하고 벤더를 선택하세요`
+ : ""}
+ </DialogDescription>
+ </DialogHeader>
+
+ {isLoading ? (
+ <div className="space-y-4">
+ <Skeleton className="h-8 w-1/2" />
+ <Skeleton className="h-48 w-full" />
+ </div>
+ ) : quotations.length === 0 ? (
+ <div className="py-8 text-center text-muted-foreground">
+ 제출된(Submitted) 견적이 없습니다
+ </div>
+ ) : (
+ <div className="border rounded-md max-h-[60vh] overflow-auto">
+ <table className="table-fixed w-full border-collapse">
+ <thead className="sticky top-0 bg-background z-10">
+ <TableRow>
+ <TableHead className="sticky left-0 top-0 z-20 bg-background p-2 w-32">
+ 항목
+ </TableHead>
+ {quotations.map((q) => (
+ <TableHead key={q.id} className="p-2 text-center whitespace-nowrap w-48">
+ <div className="flex flex-col items-center gap-2">
+ <span>{q.vendorName || `벤더 ID: ${q.vendorId}`}</span>
+ <Button
+ size="sm"
+ variant={q.status === "Accepted" ? "default" : "outline"}
+ onClick={() => handleSelectVendor(q.vendorId)}
+ disabled={q.status === "Accepted"}
+ className="gap-1"
+ >
+ {q.status === "Accepted" ? (
+ <>
+ <CheckCircle className="h-4 w-4" />
+ 선택됨
+ </>
+ ) : (
+ "선택"
+ )}
+ </Button>
+ </div>
+ </TableHead>
+ ))}
+ </TableRow>
+ </thead>
+ <tbody>
+ {/* 견적 상태 */}
+ <TableRow>
+ <TableCell className="sticky left-0 z-10 bg-muted/30 p-2 font-medium">
+ 견적 상태
+ </TableCell>
+ {quotations.map((q) => (
+ <TableCell key={`status-${q.id}`} className="p-2 text-center">
+ <Badge variant={getStatusBadgeVariant(q.status)}>
+ {q.status}
+ </Badge>
+ </TableCell>
+ ))}
+ </TableRow>
+
+ {/* 총 금액 */}
+ <TableRow>
+ <TableCell className="sticky left-0 z-10 bg-muted/30 p-2 font-medium">
+ 총 금액
+ </TableCell>
+ {quotations.map((q) => (
+ <TableCell key={`total-${q.id}`} className="p-2 font-semibold text-center">
+ {q.totalPrice ? formatCurrency(Number(q.totalPrice), q.currency || 'USD') : '-'}
+ </TableCell>
+ ))}
+ </TableRow>
+
+ {/* 통화 */}
+ <TableRow>
+ <TableCell className="sticky left-0 z-10 bg-muted/30 p-2 font-medium">
+ 통화
+ </TableCell>
+ {quotations.map((q) => (
+ <TableCell key={`currency-${q.id}`} className="p-2 text-center">
+ {q.currency || '-'}
+ </TableCell>
+ ))}
+ </TableRow>
+
+ {/* 유효기간 */}
+ <TableRow>
+ <TableCell className="sticky left-0 z-10 bg-muted/30 p-2 font-medium">
+ 유효 기간
+ </TableCell>
+ {quotations.map((q) => (
+ <TableCell key={`valid-${q.id}`} className="p-2 text-center">
+ {q.validUntil ? formatDate(q.validUntil, "KR") : '-'}
+ </TableCell>
+ ))}
+ </TableRow>
+
+ {/* 제출일 */}
+ <TableRow>
+ <TableCell className="sticky left-0 z-10 bg-muted/30 p-2 font-medium">
+ 제출일
+ </TableCell>
+ {quotations.map((q) => (
+ <TableCell key={`submitted-${q.id}`} className="p-2 text-center">
+ {q.submittedAt ? formatDate(q.submittedAt, "KR") : '-'}
+ </TableCell>
+ ))}
+ </TableRow>
+
+ {/* 비고 */}
+ <TableRow>
+ <TableCell className="sticky left-0 z-10 bg-muted/30 p-2 font-medium">
+ 비고
+ </TableCell>
+ {quotations.map((q) => (
+ <TableCell
+ key={`remark-${q.id}`}
+ className="p-2 whitespace-pre-wrap text-center"
+ >
+ {q.remark || "-"}
+ </TableCell>
+ ))}
+ </TableRow>
+ </tbody>
+ </table>
+ </div>
+ )}
+
+ <DialogFooter>
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ 닫기
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ {/* 벤더 선택 확인 다이얼로그 */}
+ <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>벤더 선택 확인</AlertDialogTitle>
+ <AlertDialogDescription>
+ <strong>{selectedVendor?.vendorName || `벤더 ID: ${selectedVendorId}`}</strong>를 선택하시겠습니까?
+ <br />
+ <br />
+ 선택된 벤더의 견적이 승인되며, 다른 벤더들의 견적은 자동으로 거절됩니다.
+ 이 작업은 되돌릴 수 없습니다.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel disabled={isAccepting}>취소</AlertDialogCancel>
+ <AlertDialogAction
+ onClick={handleConfirmSelection}
+ disabled={isAccepting}
+ className="gap-2"
+ >
+ {isAccepting && <Loader2 className="h-4 w-4 animate-spin" />}
+ 확인
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ </>
+ )
+}