diff options
Diffstat (limited to 'lib/rfq-last/vendor/vendor-detail-dialog.tsx')
| -rw-r--r-- | lib/rfq-last/vendor/vendor-detail-dialog.tsx | 695 |
1 files changed, 695 insertions, 0 deletions
diff --git a/lib/rfq-last/vendor/vendor-detail-dialog.tsx b/lib/rfq-last/vendor/vendor-detail-dialog.tsx index e69de29b..e4c78656 100644 --- a/lib/rfq-last/vendor/vendor-detail-dialog.tsx +++ b/lib/rfq-last/vendor/vendor-detail-dialog.tsx @@ -0,0 +1,695 @@ +"use client"; + +import * as React from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { + Building2, + Calendar, + DollarSign, + FileText, + Package, + Globe, + MapPin, + Clock, + CheckCircle, + XCircle, + AlertCircle, + Download, + Eye, + User, + Mail, + Phone, + CreditCard, + Truck, + Shield, + Paperclip, + Info, + Edit, +} from "lucide-react"; +import { format } from "date-fns"; +import { ko } from "date-fns/locale"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; + +// Props 타입 정의 +interface VendorResponseDetailDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + data: any; // mergedData의 row + rfqId: number; +} + +// 상태별 설정 +const getStatusConfig = (status: string) => { + switch (status) { + case "초대됨": + return { + icon: <Mail className="h-4 w-4" />, + color: "text-blue-600", + bgColor: "bg-blue-50", + variant: "secondary" as const, + }; + case "작성중": + return { + icon: <Clock className="h-4 w-4" />, + color: "text-yellow-600", + bgColor: "bg-yellow-50", + variant: "outline" as const, + }; + case "제출완료": + return { + icon: <CheckCircle className="h-4 w-4" />, + color: "text-green-600", + bgColor: "bg-green-50", + variant: "default" as const, + }; + case "수정요청": + return { + icon: <AlertCircle className="h-4 w-4" />, + color: "text-orange-600", + bgColor: "bg-orange-50", + variant: "warning" as const, + }; + case "최종확정": + return { + icon: <Shield className="h-4 w-4" />, + color: "text-indigo-600", + bgColor: "bg-indigo-50", + variant: "success" as const, + }; + case "취소": + return { + icon: <XCircle className="h-4 w-4" />, + color: "text-red-600", + bgColor: "bg-red-50", + variant: "destructive" as const, + }; + default: + return { + icon: <Info className="h-4 w-4" />, + color: "text-gray-600", + bgColor: "bg-gray-50", + variant: "outline" as const, + }; + } +}; + +export function VendorResponseDetailDialog({ + open, + onOpenChange, + data, + rfqId, +}: VendorResponseDetailDialogProps) { + if (!data) return null; + + const response = data.response; + const statusConfig = getStatusConfig(response?.status || "초대됨"); + const hasSubmitted = !!response?.submission?.submittedAt; + + // 이메일 발송 정보 파싱 + let emailRecipients = { to: [], cc: [], sentBy: "" }; + try { + if (data.emailSentTo) { + emailRecipients = JSON.parse(data.emailSentTo); + } + } catch (e) { + console.error("Failed to parse emailSentTo"); + } + + // 견적 아이템 (실제로는 response.quotationItems에서 가져옴) + const quotationItems = response?.quotationItems || []; + + // 첨부파일 (실제로는 response.attachments에서 가져옴) + const attachments = response?.attachments || []; + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <div className="flex items-center justify-between"> + <div> + <DialogTitle className="text-xl font-bold"> + 벤더 응답 상세 + </DialogTitle> + <DialogDescription className="mt-1"> + {data.vendorName} ({data.vendorCode || "코드없음"}) - {data.rfqCode} + </DialogDescription> + </div> + <div className="flex items-center gap-2"> + {/* {onEdit && ( + <Button variant="outline" size="sm" onClick={onEdit}> + <Edit className="h-4 w-4 mr-2" /> + 수정 + </Button> + )} */} + </div> + </div> + </DialogHeader> + + <Tabs defaultValue="overview" className="mt-4"> + <TabsList className="grid w-full grid-cols-4"> + <TabsTrigger value="overview">개요</TabsTrigger> + <TabsTrigger value="quotation">견적정보</TabsTrigger> + <TabsTrigger value="items">품목상세</TabsTrigger> + <TabsTrigger value="attachments">첨부파일</TabsTrigger> + </TabsList> + + {/* 개요 탭 */} + <TabsContent value="overview" className="space-y-4"> + {/* 상태 정보 */} + <Card> + <CardHeader> + <CardTitle className="text-base">응답 상태</CardTitle> + </CardHeader> + <CardContent> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">현재 상태</span> + <Badge variant={statusConfig.variant}> + {statusConfig.icon} + <span className="ml-1">{response?.status || "초대됨"}</span> + </Badge> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">응답 버전</span> + <span className="font-medium">v{response?.responseVersion || 1}</span> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">Short List</span> + <Badge variant={data.shortList ? "default" : "outline"}> + {data.shortList ? "선정" : "대기"} + </Badge> + </div> + </div> + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">제출일시</span> + <span className="text-sm"> + {response?.submission?.submittedAt + ? format(new Date(response.submission.submittedAt), "yyyy-MM-dd HH:mm", { locale: ko }) + : "-"} + </span> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">제출자</span> + <span className="text-sm">{response?.submission?.submittedByName || "-"}</span> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">최종 수정일</span> + <span className="text-sm"> + {data.updatedAt + ? format(new Date(data.updatedAt), "yyyy-MM-dd HH:mm", { locale: ko }) + : "-"} + </span> + </div> + </div> + </div> + </CardContent> + </Card> + + {/* 벤더 정보 */} + <Card> + <CardHeader> + <CardTitle className="text-base">벤더 정보</CardTitle> + </CardHeader> + <CardContent> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-3"> + <div className="flex items-center gap-2"> + <Building2 className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm text-muted-foreground">업체명</span> + <span className="font-medium ml-auto">{data.vendorName}</span> + </div> + <div className="flex items-center gap-2"> + <Package className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm text-muted-foreground">업체코드</span> + <span className="font-medium ml-auto">{data.vendorCode || "-"}</span> + </div> + <div className="flex items-center gap-2"> + <Globe className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm text-muted-foreground">국가</span> + <Badge variant={data.vendorCountry === "KR" ? "default" : "secondary"} className="ml-auto"> + {data.vendorCountry} + </Badge> + </div> + </div> + <div className="space-y-3"> + <div className="flex items-center gap-2"> + <Mail className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm text-muted-foreground">이메일</span> + <span className="text-sm ml-auto">{response?.vendor?.email || "-"}</span> + </div> + <div className="flex items-center gap-2"> + <Shield className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm text-muted-foreground">업체분류</span> + <span className="font-medium ml-auto">{data.vendorCategory || "-"}</span> + </div> + <div className="flex items-center gap-2"> + <Info className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm text-muted-foreground">AVL 등급</span> + <span className="font-medium ml-auto">{data.vendorGrade || "-"}</span> + </div> + </div> + </div> + </CardContent> + </Card> + + {/* 이메일 발송 정보 */} + {data.emailSentAt && ( + <Card> + <CardHeader> + <CardTitle className="text-base">이메일 발송 정보</CardTitle> + </CardHeader> + <CardContent> + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">최초 발송일시</span> + <span className="text-sm"> + {format(new Date(data.emailSentAt), "yyyy-MM-dd HH:mm", { locale: ko })} + </span> + </div> + {data.lastEmailSentAt && data.emailResentCount > 1 && ( + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">최근 재발송일시</span> + <span className="text-sm"> + {format(new Date(data.lastEmailSentAt), "yyyy-MM-dd HH:mm", { locale: ko })} + <Badge variant="secondary" className="ml-2"> + 재발송 {data.emailResentCount - 1}회 + </Badge> + </span> + </div> + )} + {emailRecipients.to.length > 0 && ( + <div className="flex items-start justify-between"> + <span className="text-sm text-muted-foreground">수신자</span> + <span className="text-sm text-right">{emailRecipients.to.join(", ")}</span> + </div> + )} + {emailRecipients.cc.length > 0 && ( + <div className="flex items-start justify-between"> + <span className="text-sm text-muted-foreground">참조</span> + <span className="text-sm text-right">{emailRecipients.cc.join(", ")}</span> + </div> + )} + {emailRecipients.sentBy && ( + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">발신자</span> + <span className="text-sm">{emailRecipients.sentBy}</span> + </div> + )} + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">발송 상태</span> + <Badge variant={data.emailStatus === "failed" ? "destructive" : "default"}> + {data.emailStatus === "failed" ? "발송 실패" : "발송 완료"} + </Badge> + </div> + </div> + </CardContent> + </Card> + )} + </TabsContent> + + {/* 견적정보 탭 */} + <TabsContent value="quotation" className="space-y-4"> + {/* 요청 조건 */} + <Card> + <CardHeader> + <CardTitle className="text-base">요청 조건</CardTitle> + </CardHeader> + <CardContent> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">통화</span> + <Badge variant="outline">{data.currency}</Badge> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">지급조건</span> + <TooltipProvider> + <Tooltip> + <TooltipTrigger> + <span className="font-medium">{data.paymentTermsCode}</span> + </TooltipTrigger> + {data.paymentTermsDescription && ( + <TooltipContent> + <p>{data.paymentTermsDescription}</p> + </TooltipContent> + )} + </Tooltip> + </TooltipProvider> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">인코텀즈</span> + <TooltipProvider> + <Tooltip> + <TooltipTrigger> + <span className="font-medium">{data.incotermsCode}</span> + </TooltipTrigger> + {data.incotermsDescription && ( + <TooltipContent> + <p>{data.incotermsDescription}</p> + {data.incotermsDetail && <p className="text-xs">{data.incotermsDetail}</p>} + </TooltipContent> + )} + </Tooltip> + </TooltipProvider> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">Tax</span> + <span className="font-medium">{data.taxCode || "-"}</span> + </div> + </div> + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">납기일</span> + <span className="text-sm"> + {data.deliveryDate + ? format(new Date(data.deliveryDate), "yyyy-MM-dd") + : "-"} + </span> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">계약기간</span> + <span className="text-sm">{data.contractDuration || "-"}</span> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">선적지</span> + <span className="text-sm">{data.placeOfShipping || "-"}</span> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">도착지</span> + <span className="text-sm">{data.placeOfDestination || "-"}</span> + </div> + </div> + </div> + + {/* 추가 조건 */} + <Separator className="my-4" /> + <div className="space-y-3"> + {data.firstYn && ( + <div className="flex items-start justify-between"> + <div className="flex items-center gap-2"> + <Badge variant="outline">초도품</Badge> + <span className="text-sm text-muted-foreground">요구사항</span> + </div> + <span className="text-sm text-right max-w-xs">{data.firstDescription || "초도품 제출 필요"}</span> + </div> + )} + {data.sparepartYn && ( + <div className="flex items-start justify-between"> + <div className="flex items-center gap-2"> + <Badge variant="outline">스페어파트</Badge> + <span className="text-sm text-muted-foreground">요구사항</span> + </div> + <span className="text-sm text-right max-w-xs">{data.sparepartDescription || "스페어파트 제공 필요"}</span> + </div> + )} + {data.materialPriceRelatedYn && ( + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <Badge variant="outline">연동제</Badge> + <span className="text-sm text-muted-foreground">적용</span> + </div> + <span className="text-sm">적용</span> + </div> + )} + </div> + </CardContent> + </Card> + + {/* 벤더 제안 조건 (제출된 경우) */} + {hasSubmitted && ( + <Card> + <CardHeader> + <CardTitle className="text-base">벤더 제안 조건</CardTitle> + </CardHeader> + <CardContent> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">제안 통화</span> + <Badge variant={response?.pricing?.vendorCurrency === data.currency ? "outline" : "default"}> + {response?.pricing?.vendorCurrency || data.currency} + </Badge> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">제안 지급조건</span> + <span className="font-medium"> + {response?.vendorTerms?.paymentTermsCode || data.paymentTermsCode} + </span> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">제안 인코텀즈</span> + <span className="font-medium"> + {response?.vendorTerms?.incotermsCode || data.incotermsCode} + </span> + </div> + </div> + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">제안 납기일</span> + <span className="text-sm"> + {response?.vendorTerms?.deliveryDate + ? format(new Date(response.vendorTerms.deliveryDate), "yyyy-MM-dd") + : data.deliveryDate + ? format(new Date(data.deliveryDate), "yyyy-MM-dd") + : "-"} + </span> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">총 견적금액</span> + <span className="font-bold text-lg"> + {response?.pricing?.totalAmount + ? new Intl.NumberFormat("ko-KR", { + style: "currency", + currency: response.pricing.vendorCurrency || data.currency, + }).format(response.pricing.totalAmount) + : "-"} + </span> + </div> + </div> + </div> + + {/* 벤더 추가 응답 */} + {(response?.additionalRequirements?.firstArticle?.acceptance || + response?.additionalRequirements?.sparePart?.acceptance) && ( + <> + <Separator className="my-4" /> + <div className="space-y-3"> + {response?.additionalRequirements?.firstArticle?.acceptance && ( + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">초도품 수용여부</span> + <Badge + variant={ + response.additionalRequirements.firstArticle.acceptance === "수용" + ? "default" + : response.additionalRequirements.firstArticle.acceptance === "부분수용" + ? "secondary" + : "destructive" + } + > + {response.additionalRequirements.firstArticle.acceptance} + </Badge> + </div> + )} + {response?.additionalRequirements?.sparePart?.acceptance && ( + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">스페어파트 수용여부</span> + <Badge + variant={ + response.additionalRequirements.sparePart.acceptance === "수용" + ? "default" + : response.additionalRequirements.sparePart.acceptance === "부분수용" + ? "secondary" + : "destructive" + } + > + {response.additionalRequirements.sparePart.acceptance} + </Badge> + </div> + )} + </div> + </> + )} + + {/* 벤더 비고 */} + {(response?.remarks?.general || response?.remarks?.technical) && ( + <> + <Separator className="my-4" /> + <div className="space-y-3"> + {response?.remarks?.general && ( + <div> + <span className="text-sm text-muted-foreground">일반 비고</span> + <p className="mt-1 text-sm">{response.remarks.general}</p> + </div> + )} + {response?.remarks?.technical && ( + <div> + <span className="text-sm text-muted-foreground">기술 제안</span> + <p className="mt-1 text-sm">{response.remarks.technical}</p> + </div> + )} + </div> + </> + )} + </CardContent> + </Card> + )} + </TabsContent> + + {/* 품목상세 탭 */} + <TabsContent value="items" className="space-y-4"> + {quotationItems.length > 0 ? ( + <Card> + <CardHeader> + <CardTitle className="text-base">견적 품목 상세</CardTitle> + <CardDescription> + 총 {quotationItems.length}개 품목 + </CardDescription> + </CardHeader> + <CardContent> + <Table> + <TableHeader> + <TableRow> + <TableHead>PR No.</TableHead> + <TableHead>자재코드</TableHead> + <TableHead>자재명</TableHead> + <TableHead className="text-right">수량</TableHead> + <TableHead>단위</TableHead> + <TableHead className="text-right">단가</TableHead> + <TableHead className="text-right">금액</TableHead> + <TableHead>납기일</TableHead> + <TableHead>제조사</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {quotationItems.map((item: any) => ( + <TableRow key={item.id}> + <TableCell className="font-mono text-xs">{item.prNo}</TableCell> + <TableCell className="font-mono text-xs">{item.materialCode}</TableCell> + <TableCell className="text-xs">{item.materialDescription}</TableCell> + <TableCell className="text-right">{item.quantity}</TableCell> + <TableCell>{item.uom}</TableCell> + <TableCell className="text-right"> + {new Intl.NumberFormat("ko-KR").format(item.unitPrice)} + </TableCell> + <TableCell className="text-right font-medium"> + {new Intl.NumberFormat("ko-KR").format(item.totalPrice)} + </TableCell> + <TableCell> + {item.vendorDeliveryDate + ? format(new Date(item.vendorDeliveryDate), "MM-dd") + : "-"} + </TableCell> + <TableCell>{item.manufacturer || "-"}</TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </CardContent> + </Card> + ) : ( + <Card> + <CardContent className="pt-6"> + <div className="text-center text-muted-foreground"> + 아직 제출된 견적 품목이 없습니다. + </div> + </CardContent> + </Card> + )} + </TabsContent> + + {/* 첨부파일 탭 */} + <TabsContent value="attachments" className="space-y-4"> + {attachments.length > 0 ? ( + <Card> + <CardHeader> + <CardTitle className="text-base">첨부파일</CardTitle> + <CardDescription> + 총 {attachments.length}개 파일 + </CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-2"> + {attachments.map((file: any) => ( + <div + key={file.id} + className="flex items-center justify-between p-3 border rounded-lg hover:bg-accent" + > + <div className="flex items-center gap-3"> + <Paperclip className="h-4 w-4 text-muted-foreground" /> + <div> + <p className="text-sm font-medium">{file.originalFileName}</p> + <p className="text-xs text-muted-foreground"> + {file.attachmentType} • {file.fileSize ? `${(file.fileSize / 1024).toFixed(2)} KB` : "크기 미상"} + {file.description && ` • ${file.description}`} + </p> + </div> + </div> + <div className="flex items-center gap-2"> + <Button + variant="ghost" + size="sm" + onClick={() => { + // 파일 미리보기 로직 + console.log("Preview file:", file.filePath); + }} + > + <Eye className="h-4 w-4" /> + </Button> + <Button + variant="ghost" + size="sm" + onClick={() => { + // 파일 다운로드 로직 + window.open(file.filePath, "_blank"); + }} + > + <Download className="h-4 w-4" /> + </Button> + </div> + </div> + ))} + </div> + </CardContent> + </Card> + ) : ( + <Card> + <CardContent className="pt-6"> + <div className="text-center text-muted-foreground"> + 아직 제출된 첨부파일이 없습니다. + </div> + </CardContent> + </Card> + )} + </TabsContent> + </Tabs> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
