diff options
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.tsx | 340 |
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> + </> + ) +} |
