From a3525f8bdfcf849cc1716fab81cb8facadbe9a8e Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 27 Oct 2025 10:03:06 +0000
Subject: (최겸) 구매 협력업체 관리(PQ/실사관리, 정기평가 협력업체 제출 상세
 dialog 개발,
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/evaluation/table/vendor-submission-dialog.tsx | 623 ++++++++++++++++++++++
 1 file changed, 623 insertions(+)
 create mode 100644 lib/evaluation/table/vendor-submission-dialog.tsx

(limited to 'lib/evaluation/table/vendor-submission-dialog.tsx')

diff --git a/lib/evaluation/table/vendor-submission-dialog.tsx b/lib/evaluation/table/vendor-submission-dialog.tsx
new file mode 100644
index 00000000..aff8dc56
--- /dev/null
+++ b/lib/evaluation/table/vendor-submission-dialog.tsx
@@ -0,0 +1,623 @@
+"use client"
+
+import * as React from "react"
+import { 
+  Eye, 
+  Building2, 
+  User, 
+  Calendar, 
+  CheckCircle2, 
+  Clock, 
+  MessageSquare,
+  Award,
+  FileText,
+  Paperclip,
+  Download,
+  File,
+  BarChart3,
+  ChevronDown,
+  ChevronRight
+} from "lucide-react"
+
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog"
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from "@/components/ui/tooltip"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
+import { Separator } from "@/components/ui/separator"
+import { Skeleton } from "@/components/ui/skeleton"
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { PeriodicEvaluationView } from "@/db/schema"
+import { getVendorSubmissionDetails, type VendorSubmissionDetail } from "../vendor-submission-service"
+import { formatFileSize, getFileInfo } from "@/lib/file-download"
+
+interface VendorSubmissionDialogProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  evaluation: PeriodicEvaluationView | null
+}
+
+// 상태별 배지 색상
+const getSubmissionStatusBadge = (status: string) => {
+  switch (status) {
+    case "submitted":
+      return <Badge variant="default" className="bg-green-600">제출완료</Badge>
+    case "draft":
+      return <Badge variant="secondary">임시저장</Badge>
+    case "reviewed":
+      return <Badge variant="outline">검토완료</Badge>
+    default:
+      return <Badge variant="outline">{status}</Badge>
+  }
+}
+
+// 진행률 계산
+const getProgressPercentage = (completed: number, total: number) => {
+  if (total === 0) return 0
+  return Math.round((completed / total) * 100)
+}
+
+export function VendorSubmissionDialog({
+  open,
+  onOpenChange,
+  evaluation,
+}: VendorSubmissionDialogProps) {
+  const [isLoading, setIsLoading] = React.useState(false)
+  const [submissionDetails, setSubmissionDetails] = React.useState<VendorSubmissionDetail | null>(null)
+  const [expandedGeneralItems, setExpandedGeneralItems] = React.useState<Set<number>>(new Set())
+  const [expandedEsgItems, setExpandedEsgItems] = React.useState<Set<number>>(new Set())
+
+  // 첨부파일 다운로드 핸들러
+  const handleDownloadAttachment = async (filePath: string, fileName: string) => {
+    try {
+        const { downloadFile } = await import('@/lib/file-download')
+      await downloadFile(filePath, fileName, {
+        action: 'download',
+        showToast: true,
+        showSuccessToast: true,
+        onError: (error) => {
+          console.error("파일 다운로드 실패:", error)
+        },
+      })
+    } catch (error) {
+      console.error("파일 다운로드 실패:", error)
+    }
+  }
+
+  // 일반평가 항목 토글
+  const toggleGeneralItem = (itemId: number) => {
+    const newExpanded = new Set(expandedGeneralItems)
+    if (newExpanded.has(itemId)) {
+      newExpanded.delete(itemId)
+    } else {
+      newExpanded.add(itemId)
+    }
+    setExpandedGeneralItems(newExpanded)
+  }
+
+  // ESG 평가 항목 토글
+  const toggleEsgItem = (itemId: number) => {
+    const newExpanded = new Set(expandedEsgItems)
+    if (newExpanded.has(itemId)) {
+      newExpanded.delete(itemId)
+    } else {
+      newExpanded.add(itemId)
+    }
+    setExpandedEsgItems(newExpanded)
+  }
+
+  // 제출 상세 정보 로드
+  React.useEffect(() => {
+    if (open && evaluation?.id) {
+      const loadSubmissionDetails = async () => {
+        try {
+          setIsLoading(true)
+          const details = await getVendorSubmissionDetails(evaluation.id)
+          setSubmissionDetails(details)
+        } catch (error) {
+          console.error("Failed to load vendor submission details:", error)
+        } finally {
+          setIsLoading(false)
+        }
+      }
+
+      loadSubmissionDetails()
+    }
+  }, [open, evaluation?.id])
+
+  // 다이얼로그 닫을 때 데이터 리셋
+  React.useEffect(() => {
+    if (!open) {
+      setSubmissionDetails(null)
+      setExpandedGeneralItems(new Set())
+      setExpandedEsgItems(new Set())
+    }
+  }, [open])
+
+  if (!evaluation) return null
+
+  return (
+    <TooltipProvider>
+      <Dialog open={open} onOpenChange={onOpenChange}>
+        <DialogContent className="max-w-7xl h-[90vh] flex flex-col">
+          {/* 고정 헤더 */}
+          <DialogHeader className="space-y-4 flex-shrink-0">
+            <DialogTitle className="flex items-center gap-2">
+              <Eye className="h-5 w-5 text-blue-600" />
+              협력업체 제출 상세
+            </DialogTitle>
+            
+            {/* 평가 기본 정보 */}
+            <Card>
+              <CardHeader>
+                <CardTitle className="flex items-center gap-2 text-lg">
+                  <Building2 className="h-5 w-5" />
+                  평가 정보
+                </CardTitle>
+              </CardHeader>
+              <CardContent>
+                <div className="flex flex-wrap items-center gap-6 text-sm mb-4">
+                  {/* 협력업체 */}
+                  <div className="flex items-center gap-2">
+                    <span className="text-muted-foreground">협력업체:</span>
+                    <span className="font-medium">{evaluation.vendorName}</span>
+                    <span className="text-muted-foreground">({evaluation.vendorCode})</span>
+                  </div>
+
+                  {/* 평가년도 */}
+                  <div className="flex items-center gap-2">
+                    <span className="text-muted-foreground">년도:</span>
+                    <span className="font-medium">{evaluation.evaluationYear}년</span>
+                  </div>
+
+                  {/* 구분 */}
+                  <div className="flex items-center gap-2">
+                    <span className="text-muted-foreground">구분:</span>
+                    <Badge variant="outline" className="text-xs">
+                      {evaluation.division === "PLANT" ? "해양" : "조선"}
+                    </Badge>
+                  </div>
+
+                  {/* 진행상태 */}
+                  <div className="flex items-center gap-2">
+                    <span className="text-muted-foreground">상태:</span>
+                    <Badge variant="secondary" className="text-xs">{evaluation.status}</Badge>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          </DialogHeader>
+
+          {/* 스크롤 가능한 컨텐츠 영역 */}
+          <div className="flex-1 overflow-y-auto min-h-0">
+            {isLoading ? (
+              <div className="space-y-4 p-1">
+                <Card>
+                  <CardHeader>
+                    <Skeleton className="h-6 w-48" />
+                  </CardHeader>
+                  <CardContent>
+                    <Skeleton className="h-64 w-full" />
+                  </CardContent>
+                </Card>
+              </div>
+            ) : submissionDetails ? (
+              <div className="space-y-6 p-1">
+                {/* 제출 정보 요약 */}
+                <Card>
+                  <CardHeader>
+                    <CardTitle className="flex items-center gap-2">
+                      <FileText className="h-5 w-5" />
+                      제출 정보
+                    </CardTitle>
+                  </CardHeader>
+                  <CardContent>
+                    <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
+                      <div className="space-y-1">
+                        <div className="text-muted-foreground">제출 상태</div>
+                        <div>{getSubmissionStatusBadge(submissionDetails.submissionStatus)}</div>
+                      </div>
+                      <div className="space-y-1">
+                        <div className="text-muted-foreground">제출일</div>
+                        <div className="font-medium">
+                          {submissionDetails.submittedAt 
+                            ? new Date(submissionDetails.submittedAt).toLocaleDateString('ko-KR')
+                            : "-"
+                          }
+                        </div>
+                      </div>
+                      {/* <div className="space-y-1">
+                        <div className="text-muted-foreground">ESG 평균 점수</div>
+                        <div className="font-medium">
+                          {submissionDetails.averageEsgScore 
+                            ? `${submissionDetails.averageEsgScore.toFixed(1)}점`
+                            : "-"
+                          }
+                        </div>
+                      </div> */}
+                      {/* <div className="space-y-1">
+                        <div className="text-muted-foreground">검토자</div>
+                        <div className="font-medium">
+                          {submissionDetails.reviewedBy || "-"}
+                        </div>
+                      </div> */}
+                    </div>
+
+                    {/* 진행률 표시 */}
+                    {/* <div className="mt-6 space-y-4">
+                      <div className="space-y-2">
+                        <div className="flex items-center justify-between text-sm">
+                          <span className="text-muted-foreground">일반평가 진행률</span>
+                          <span className="font-medium">
+                            {submissionDetails.completedGeneralItems}/{submissionDetails.totalGeneralItems} 
+                            ({getProgressPercentage(submissionDetails.completedGeneralItems, submissionDetails.totalGeneralItems)}%)
+                          </span>
+                        </div>
+                        <div className="w-full bg-gray-200 rounded-full h-2">
+                          <div 
+                            className="bg-blue-600 h-2 rounded-full transition-all duration-300"
+                            style={{ 
+                              width: `${getProgressPercentage(submissionDetails.completedGeneralItems, submissionDetails.totalGeneralItems)}%` 
+                            }}
+                          />
+                        </div>
+                      </div>
+
+                      <div className="space-y-2">
+                        <div className="flex items-center justify-between text-sm">
+                          <span className="text-muted-foreground">ESG 평가 진행률</span>
+                          <span className="font-medium">
+                            {submissionDetails.completedEsgItems}/{submissionDetails.totalEsgItems} 
+                            ({getProgressPercentage(submissionDetails.completedEsgItems, submissionDetails.totalEsgItems)}%)
+                          </span>
+                        </div>
+                        <div className="w-full bg-gray-200 rounded-full h-2">
+                          <div 
+                            className="bg-green-600 h-2 rounded-full transition-all duration-300"
+                            style={{ 
+                              width: `${getProgressPercentage(submissionDetails.completedEsgItems, submissionDetails.totalEsgItems)}%` 
+                            }}
+                          />
+                        </div>
+                      </div>
+                    </div> */}
+                  </CardContent>
+                </Card>
+
+                {/* 탭으로 일반평가와 ESG 평가 구분 */}
+                <Tabs defaultValue="general" className="w-full">
+                  <TabsList className={`grid w-full ${submissionDetails.vendor.country === "KR" ? "grid-cols-2" : "grid-cols-1"}`}>
+                    <TabsTrigger value="general" className="flex items-center gap-2">
+                      <FileText className="h-4 w-4" />
+                      일반평가 ({submissionDetails.generalEvaluations.length}개)
+                    </TabsTrigger>
+                    {submissionDetails.vendor.country === "KR" && (
+                      <TabsTrigger value="esg" className="flex items-center gap-2">
+                        <Award className="h-4 w-4" />
+                        ESG 평가 ({submissionDetails.esgEvaluations.length}개)
+                      </TabsTrigger>
+                    )}
+                  </TabsList>
+
+                  {/* 일반평가 탭 */}
+                  <TabsContent value="general" className="space-y-4">
+                    {submissionDetails.generalEvaluations.length > 0 ? (
+                      <div className="space-y-4">
+                        {submissionDetails.generalEvaluations.map((item) => (
+                          <Card key={item.id}>
+                            <Collapsible>
+                              <CollapsibleTrigger asChild>
+                                <CardHeader 
+                                  className="cursor-pointer hover:bg-muted/50 transition-colors"
+                                  onClick={() => toggleGeneralItem(item.id)}
+                                >
+                                  <div className="flex items-center justify-between">
+                                    <div className="flex items-center gap-2">
+                                      {expandedGeneralItems.has(item.id) ? (
+                                        <ChevronDown className="h-4 w-4" />
+                                      ) : (
+                                        <ChevronRight className="h-4 w-4" />
+                                      )}
+                                      <CardTitle className="text-base">
+                                        {item.serialNumber}. {item.category}
+                                      </CardTitle>
+                                    </div>
+                                    <div className="flex items-center gap-2">
+                                      {item.response ? (
+                                        <Badge variant="default" className="bg-green-600">응답완료</Badge>
+                                      ) : (
+                                        <Badge variant="outline">미응답</Badge>
+                                      )}
+                                      {item.response?.hasAttachments && (
+                                        <Badge variant="secondary" className="text-xs">
+                                          <Paperclip className="h-3 w-3 mr-1" />
+                                          첨부파일
+                                        </Badge>
+                                      )}
+                                    </div>
+                                  </div>
+                                </CardHeader>
+                              </CollapsibleTrigger>
+                              <CollapsibleContent>
+                                <CardContent className="space-y-4">
+                                  <div>
+                                    <div className="text-sm font-medium mb-2">평가 항목</div>
+                                    <div className="text-sm text-muted-foreground">{item.inspectionItem}</div>
+                                    {item.remarks && (
+                                      <div className="mt-2">
+                                        <div className="text-sm font-medium mb-1">비고</div>
+                                        <div className="text-sm text-muted-foreground">{item.remarks}</div>
+                                      </div>
+                                    )}
+                                  </div>
+
+                                  {item.response ? (
+                                    <div className="space-y-4">
+                                      <Separator />
+                                      <div>
+                                        <div className="text-sm font-medium mb-2">협력업체 응답</div>
+                                        <div className="bg-muted p-3 rounded-md text-sm">
+                                          {item.response.responseText || "응답 내용이 없습니다."}
+                                        </div>
+                                      </div>
+
+                                      {/* 첨부파일 */}
+                                      {item.response.attachments.length > 0 && (
+                                        <div>
+                                          <div className="text-sm font-medium mb-2">첨부파일</div>
+                                          <div className="space-y-2">
+                                            {item.response.attachments.map((attachment) => {
+                                              const fileInfo = getFileInfo(attachment.originalFileName)
+                                              return (
+                                                <div key={attachment.id} className="flex items-center gap-2 p-2 bg-muted rounded">
+                                                  <span className="text-sm">{fileInfo.icon}</span>
+                                                  <Tooltip>
+                                                    <TooltipTrigger asChild>
+                                                      <span className="text-sm truncate flex-1 cursor-help">
+                                                        {attachment.originalFileName}
+                                                      </span>
+                                                    </TooltipTrigger>
+                                                    <TooltipContent>
+                                                      <div className="text-xs space-y-1">
+                                                        <div className="font-medium">{attachment.originalFileName}</div>
+                                                        <div>크기: {formatFileSize(attachment.fileSize)}</div>
+                                                        <div>타입: {fileInfo.type}</div>
+                                                        <div>업로드: {attachment.uploadedBy}</div>
+                                                      </div>
+                                                    </TooltipContent>
+                                                  </Tooltip>
+                                                  <Button
+                                                    variant="ghost"
+                                                    size="sm"
+                                                    className="h-6 w-6 p-0"
+                                                    onClick={() => handleDownloadAttachment(attachment.filePath, attachment.originalFileName)}
+                                                  >
+                                                    <Download className="h-3 w-3" />
+                                                  </Button>
+                                                </div>
+                                              )
+                                            })}
+                                          </div>
+                                        </div>
+                                      )}
+
+                                      {/* 검토자 의견 */}
+                                      {item.response.reviewComments && (
+                                        <div>
+                                          <div className="text-sm font-medium mb-2">검토자 의견</div>
+                                          <div className="bg-blue-50 p-3 rounded-md text-sm">
+                                            {item.response.reviewComments}
+                                          </div>
+                                        </div>
+                                      )}
+                                    </div>
+                                  ) : (
+                                    <div className="text-center text-muted-foreground py-4">
+                                      <Clock className="h-8 w-8 mx-auto mb-2" />
+                                      <div>아직 응답하지 않았습니다</div>
+                                    </div>
+                                  )}
+                                </CardContent>
+                              </CollapsibleContent>
+                            </Collapsible>
+                          </Card>
+                        ))}
+                      </div>
+                    ) : (
+                      <Card>
+                        <CardContent className="py-8">
+                          <div className="text-center text-muted-foreground">
+                            <FileText className="h-8 w-8 mx-auto mb-2" />
+                            <div>일반평가 항목이 없습니다</div>
+                          </div>
+                        </CardContent>
+                      </Card>
+                    )}
+                  </TabsContent>
+
+                  {/* ESG 평가 탭 */}
+                  <TabsContent value="esg" className="space-y-4">
+                    {submissionDetails.esgEvaluations.length > 0 ? (
+                      <div className="space-y-4">
+                        {submissionDetails.esgEvaluations.map((esgEvaluation) => (
+                          <Card key={esgEvaluation.id}>
+                            <Collapsible>
+                              <CollapsibleTrigger asChild>
+                                <CardHeader 
+                                  className="cursor-pointer hover:bg-muted/50 transition-colors"
+                                  onClick={() => toggleEsgItem(esgEvaluation.id)}
+                                >
+                                  <div className="flex items-center justify-between">
+                                    <div className="flex items-center gap-2">
+                                      {expandedEsgItems.has(esgEvaluation.id) ? (
+                                        <ChevronDown className="h-4 w-4" />
+                                      ) : (
+                                        <ChevronRight className="h-4 w-4" />
+                                      )}
+                                      <CardTitle className="text-base">
+                                        {esgEvaluation.serialNumber}. {esgEvaluation.category}
+                                      </CardTitle>
+                                    </div>
+                                    <div className="flex items-center gap-2">
+                                      <Badge variant="outline" className="text-xs">
+                                        {esgEvaluation.evaluationItems.length}개 항목
+                                      </Badge>
+                                    </div>
+                                  </div>
+                                </CardHeader>
+                              </CollapsibleTrigger>
+                              <CollapsibleContent>
+                                <CardContent className="space-y-4">
+                                  <div>
+                                    <div className="text-sm font-medium mb-2">평가 항목</div>
+                                    <div className="text-sm text-muted-foreground">{esgEvaluation.inspectionItem}</div>
+                                  </div>
+
+                                  <Separator />
+
+                                  {/* ESG 평가 세부 항목들 */}
+                                  <div className="space-y-3">
+                                    {esgEvaluation.evaluationItems.map((item) => (
+                                      <div key={item.id} className="border rounded-lg p-4">
+                                        <div className="space-y-2">
+                                          <div className="text-sm font-medium">{item.evaluationItem}</div>
+                                          {item.evaluationItemDescription && (
+                                            <div className="text-xs text-muted-foreground">
+                                              {item.evaluationItemDescription}
+                                            </div>
+                                          )}
+
+                                          {item.response ? (
+                                            <div className="space-y-2">
+                                              <div className="flex items-center gap-2">
+                                                <Badge variant="default" className="bg-green-600 text-xs">
+                                                  선택된 답변
+                                                </Badge>
+                                                <span className="text-sm font-medium">
+                                                  {item.response.answerOption.answerText}
+                                                </span>
+                                                <Badge variant="outline" className="text-xs">
+                                                  {item.response.selectedScore}점
+                                                </Badge>
+                                              </div>
+                                              {item.response.additionalComments && (
+                                                <div className="bg-muted p-2 rounded text-xs">
+                                                  <div className="font-medium mb-1">추가 의견:</div>
+                                                  <div>{item.response.additionalComments}</div>
+                                                </div>
+                                              )}
+                                            </div>
+                                          ) : (
+                                            <div className="text-center text-muted-foreground py-2">
+                                              <Clock className="h-4 w-4 mx-auto mb-1" />
+                                              <div className="text-xs">아직 응답하지 않았습니다</div>
+                                            </div>
+                                          )}
+                                        </div>
+                                      </div>
+                                    ))}
+                                  </div>
+                                </CardContent>
+                              </CollapsibleContent>
+                            </Collapsible>
+                          </Card>
+                        ))}
+                      </div>
+                    ) : (
+                      <Card>
+                        <CardContent className="py-8">
+                          <div className="text-center text-muted-foreground">
+                            <Award className="h-8 w-8 mx-auto mb-2" />
+                            <div>ESG 평가 항목이 없습니다</div>
+                          </div>
+                        </CardContent>
+                      </Card>
+                    )}
+                  </TabsContent>
+                </Tabs>
+
+                {/* 첨부파일 요약 */}
+                {submissionDetails.attachmentStats.totalFiles > 0 && (
+                  <Card>
+                    <CardHeader>
+                      <CardTitle className="flex items-center gap-2">
+                        <BarChart3 className="h-5 w-5" />
+                        첨부파일 요약
+                      </CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
+                        <div className="space-y-1">
+                          <div className="text-muted-foreground">전체 파일 수</div>
+                          <div className="font-bold text-lg">{submissionDetails.attachmentStats.totalFiles}개</div>
+                        </div>
+                        <div className="space-y-1">
+                          <div className="text-muted-foreground">전체 파일 크기</div>
+                          <div className="font-bold text-lg">{formatFileSize(submissionDetails.attachmentStats.totalSize)}</div>
+                        </div>
+                        <div className="space-y-1">
+                          <div className="text-muted-foreground">일반평가 첨부파일</div>
+                          <div className="font-bold text-lg">{submissionDetails.attachmentStats.generalEvaluationFiles}개</div>
+                        </div>
+                        <div className="space-y-1">
+                          <div className="text-muted-foreground">ESG 평가 첨부파일</div>
+                          <div className="font-bold text-lg">{submissionDetails.attachmentStats.esgEvaluationFiles}개</div>
+                        </div>
+                      </div>
+                    </CardContent>
+                  </Card>
+                )}
+
+                {/* 검토자 의견 */}
+                {submissionDetails.reviewComments && (
+                  <Card>
+                    <CardHeader>
+                      <CardTitle className="flex items-center gap-2">
+                        <MessageSquare className="h-5 w-5" />
+                        검토자 의견
+                      </CardTitle>
+                    </CardHeader>
+                    <CardContent>
+                      <div className="bg-muted p-3 rounded-md text-sm">
+                        {submissionDetails.reviewComments}
+                      </div>
+                    </CardContent>
+                  </Card>
+                )}
+              </div>
+            ) : (
+              <Card className="m-1">
+                <CardContent className="py-8">
+                  <div className="text-center text-muted-foreground">
+                    <User className="h-8 w-8 mx-auto mb-2" />
+                    <div>제출 내용이 없습니다</div>
+                    <div className="text-xs mt-1">협력업체가 아직 평가를 제출하지 않았습니다</div>
+                  </div>
+                </CardContent>
+              </Card>
+            )}
+          </div>
+
+          {/* 고정 푸터 */}
+          <div className="flex justify-end pt-4 border-t flex-shrink-0">
+            <Button variant="outline" onClick={() => onOpenChange(false)}>
+              닫기
+            </Button>
+          </div>
+        </DialogContent>
+      </Dialog>
+    </TooltipProvider>
+  )
+}
-- 
cgit v1.2.3

