From 02b1cf005cf3e1df64183d20ba42930eb2767a9f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 21 Aug 2025 06:57:36 +0000 Subject: (대표님, 최겸) 설계메뉴추가, 작업사항 업데이트 설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user-vendor-document-table-container.tsx | 898 +++++++++++++++++++++ 1 file changed, 898 insertions(+) create mode 100644 components/ship-vendor-document-all/user-vendor-document-table-container.tsx (limited to 'components/ship-vendor-document-all/user-vendor-document-table-container.tsx') diff --git a/components/ship-vendor-document-all/user-vendor-document-table-container.tsx b/components/ship-vendor-document-all/user-vendor-document-table-container.tsx new file mode 100644 index 00000000..157bdb03 --- /dev/null +++ b/components/ship-vendor-document-all/user-vendor-document-table-container.tsx @@ -0,0 +1,898 @@ +// user-vendor-document-display.tsx - 수정된 버전 +"use client" + +import React from "react" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Building, FileText, AlertCircle, Eye, Download, Loader2, Plus, Trash2, Edit } from "lucide-react" +import { SimplifiedDocumentsTable } from "@/lib/vendor-document-list/ship-all/enhanced-documents-table" +import { + getUserVendorDocumentsAll, + getUserVendorDocumentStatsAll, +} from "@/lib/vendor-document-list/enhanced-document-service" +import { SimplifiedDocumentsView } from "@/db/schema" +import { WebViewerInstance } from "@pdftron/webviewer" +import { useRouter } from 'next/navigation' + +/* ------------------------------------------------------------------------------------------------- + * Types & Constants + * -----------------------------------------------------------------------------------------------*/ +interface UserVendorDocumentDisplayProps { + allPromises: Promise<[ + Awaited>, // 문서 목록 + Awaited>, // 통계 데이터 + ]> +} + +interface StageInfo { + id: number + stageName: string + stageStatus: string + stageOrder: number + planDate: string | null + actualDate: string | null + assigneeName: string | null + priority: string + revisions: RevisionInfo[] +} + +interface RevisionInfo { + id: number + serialNo: string | null + issueStageId: number + revision: string + uploaderType: string + uploaderId: number | null + uploaderName: string | null + comment: string | null + usage: string | null + usageType: string | null + revisionStatus: string + submittedDate: string | null + approvedDate: string | null + uploadedAt: string | null + reviewStartDate: string | null + rejectedDate: string | null + reviewerId: number | null + reviewerName: string | null + reviewComments: string | null + createdAt: Date + updatedAt: Date + stageName?: string + attachments: AttachmentInfo[] +} + +interface AttachmentInfo { + id: number + revisionId: number + fileName: string + filePath: string + dolceFilePath: string | null + fileSize: number | null + fileType: string | null + createdAt: Date + updatedAt: Date +} + +interface DocumentSelectionContextType { + selectedDocumentId: number | null + selectedStageId: number | null + selectedRevisionId: number | null + setSelectedDocumentId: (id: number | null) => void + setSelectedStageId: (id: number | null) => void + setSelectedRevisionId: (id: number | null) => void + allData: SimplifiedDocumentsView[] | null + setAllData: (data: SimplifiedDocumentsView[]) => void +} + +export const DocumentSelectionContextAll = React.createContext( + { + selectedDocumentId: null, + selectedStageId: null, + selectedRevisionId: null, + setSelectedDocumentId: (_id: number | null) => { }, + setSelectedStageId: (_id: number | null) => { }, + setSelectedRevisionId: (_id: number | null) => { }, + allData: null, + setAllData: (_data: SimplifiedDocumentsView[]) => { }, + }, +) + +/* ------------------------------------------------------------------------------------------------- + * Revision & Attachment Tables - 너비 최적화 + * -----------------------------------------------------------------------------------------------*/ + +function getUsageTypeDisplay(usageType: string | null): string { + if (!usageType) return '-' + + // B3 용도 타입 축약 표시 + const abbreviations: Record = { + 'Approval Submission Full': 'AS-F', + 'Approval Submission Partial': 'AS-P', + 'Approval Completion Full': 'AC-F', + 'Approval Completion Partial': 'AC-P', + 'Working Full': 'W-F', + 'Working Partial': 'W-P', + 'Reference Full': 'R-F', + 'Reference Partial': 'R-P', + 'Reference Series Full': 'RS-F', + 'Reference Series Partial': 'RS-P', + } + + return abbreviations[usageType] || usageType +} + +function RevisionTable({ + revisions, + onViewRevision, +}: { + revisions: RevisionInfo[] + onViewRevision: (revision: RevisionInfo) => void +}) { + const { selectedRevisionId, setSelectedRevisionId } = + React.useContext(DocumentSelectionContextAll) + + const toggleSelect = (revisionId: number) => { + setSelectedRevisionId(revisionId === selectedRevisionId ? null : revisionId) + } + + const canEditRevision = React.useCallback((revision: RevisionInfo) => { + if ((!revision.attachments || revision.attachments.length === 0) && revision.uploaderType === "vendor") { + return true + } + + return revision.attachments.every(attachment => + !attachment.dolceFilePath || attachment.dolceFilePath.trim() === '' + ) + }, []) + + const getRevisionProcessStatus = React.useCallback((revision: RevisionInfo) => { + if (!revision.attachments || revision.attachments.length === 0) { + return 'no-files' + } + + const processedCount = revision.attachments.filter(attachment => + attachment.dolceFilePath && attachment.dolceFilePath.trim() !== '' + ).length + + if (processedCount === 0) { + return 'not-processed' + } else if (processedCount === revision.attachments.length) { + return 'fully-processed' + } else { + return 'partially-processed' + } + }, []) + + return ( + + + Revisions + + +
+ + + + Sel + Serial No + Rev + Category + Usage + Type + Status + Uploader + Comment + Date + Files + Actions + + + + {revisions.map((revision) => { + const canEdit = canEditRevision(revision) + const processStatus = getRevisionProcessStatus(revision) + + return ( + + + toggleSelect(revision.id)} + className="h-3 w-3 cursor-pointer" + /> + + + {revision.serialNo || ''} + + +
+ {revision.revision} + {processStatus === 'fully-processed' && ( +
+ )} + {processStatus === 'partially-processed' && ( +
+ )} +
+ + + {revision.uploaderType === "vendor" ? "To" : "From"} + + + + {revision.usage || '-'} + + + + + {revision.usageType ? ( + getUsageTypeDisplay(revision.usageType) + ) : ( + - + )} + + + + + {revision.revisionStatus.slice(0, 8)} + + + + {revision.uploaderName || '-'} + + + {revision.comment ? ( +
+

+ {revision.comment} +

+
+ ) : ( + - + )} +
+ + + {revision.uploadedAt + ? new Date(revision.uploadedAt).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric' + }) + : '-'} + + + +
+ {revision.attachments.length} +
+
+ +
+ {revision.attachments.length > 0 && ( + + )} +
+
+ + ) + })} + +
+
+
+
+ ) +} + +function AttachmentTable({ + attachments, + onDownloadFile, +}: { + attachments: AttachmentInfo[] + onDownloadFile: (attachment: AttachmentInfo) => void +}) { + const { selectedRevisionId, allData, setAllData } = React.useContext(DocumentSelectionContextAll) + const [addAttachmentDialogOpen, setAddAttachmentDialogOpen] = React.useState(false) + const router = useRouter() + + const selectedRevisionInfo = React.useMemo(() => { + if (!selectedRevisionId || !allData) return null + + for (const doc of allData) { + if (doc.allStages) { + for (const stage of doc.allStages as StageInfo[]) { + const revision = stage.revisions.find(r => r.id === selectedRevisionId) + if (revision) return revision + } + } + } + return null + }, [selectedRevisionId, allData]) + + const handleAddAttachment = React.useCallback(() => { + if (selectedRevisionInfo) { + setAddAttachmentDialogOpen(true) + } + }, [selectedRevisionInfo]) + + const canDeleteFile = React.useCallback((attachment: AttachmentInfo) => { + return !attachment.dolceFilePath || attachment.dolceFilePath.trim() === '' + }, []) + + const handleAttachmentUploadSuccess = React.useCallback((uploadResult?: any) => { + if (!selectedRevisionId || !allData || !uploadResult?.data) { + console.log('🔄 Full refresh') + router.refresh() + return + } + + try { + const newAttachments: AttachmentInfo[] = uploadResult.data.uploadedFiles?.map((file: any) => ({ + id: file.id, + revisionId: selectedRevisionId, + fileName: file.fileName, + filePath: file.filePath, + dolceFilePath: null, + fileSize: file.fileSize, + fileType: file.fileType || null, + createdAt: new Date(), + updatedAt: new Date(), + })) || [] + + const updatedData = allData.map(doc => { + const updatedDoc = { ...doc } + + if (updatedDoc.allStages) { + const stages = [...updatedDoc.allStages as StageInfo[]] + + for (const stage of stages) { + const revisionIndex = stage.revisions.findIndex(r => r.id === selectedRevisionId) + if (revisionIndex !== -1) { + stage.revisions[revisionIndex] = { + ...stage.revisions[revisionIndex], + attachments: [...stage.revisions[revisionIndex].attachments, ...newAttachments] + } + updatedDoc.allStages = stages + break + } + } + } + + return updatedDoc + }) + + setAllData(updatedData) + console.log('✅ AttachmentTable update complete') + + setTimeout(() => { + router.refresh() + }, 1500) + + } catch (error) { + console.error('❌ AttachmentTable update failed:', error) + router.refresh() + } + }, [selectedRevisionId, allData, setAllData, router]) + + return ( + + +
+ Attachments + {selectedRevisionId && selectedRevisionInfo && ( + + )} +
+
+ +
+ + + + File Name + Actions + + + + {!selectedRevisionId || attachments.length === 0 ? ( + + +
+ + + {!selectedRevisionId + ? 'Please select a revision' + : 'No attached files'} + + {selectedRevisionId && selectedRevisionInfo && ( + + )} +
+
+
+ ) : ( + attachments.map((file) => ( + + +
+
+ {file.fileName} +
+
+ + {file.fileSize + ? file.fileSize >= 1024 * 1024 + ? `${(file.fileSize / 1024 / 1024).toFixed(1)}MB` + : `${(file.fileSize / 1024).toFixed(1)}KB` + : '-'} + + {file.dolceFilePath && file.dolceFilePath.trim() !== '' && ( + Processed + )} +
+
+
+ +
+ +
+
+
+ )) + )} +
+
+
+
+
+ ) +} + +// SubTables 컴포넌트 - 컨테이너 너비 제한 강화 +function SubTables() { + const router = useRouter() + const { selectedDocumentId, selectedRevisionId, setSelectedRevisionId, allData, setAllData } = + React.useContext(DocumentSelectionContextAll) + + // PDF 뷰어 상태 관리 + const [viewerOpen, setViewerOpen] = React.useState(false) + const [selectedRevision, setSelectedRevision] = React.useState(null) + const [instance, setInstance] = React.useState(null) + const [viewerLoading, setViewerLoading] = React.useState(true) + const [fileSetLoading, setFileSetLoading] = React.useState(true) + const viewer = React.useRef(null) + const initialized = React.useRef(false) + const isCancelled = React.useRef(false) + + const selectedDocument = React.useMemo(() => { + if (!selectedDocumentId || !allData) return null + return allData.find((d) => d.documentId === selectedDocumentId) || null + }, [selectedDocumentId, allData]) + + const allRevisions = React.useMemo(() => { + if (!selectedDocument?.allStages) return [] + + const revisions: RevisionInfo[] = [] + for (const stage of selectedDocument.allStages as StageInfo[]) { + const stageRevisions = stage.revisions.map(revision => ({ + ...revision, + stageName: stage.stageName + })) + revisions.push(...stageRevisions) + } + + return revisions.sort((a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ) + }, [selectedDocument]) + + const selectedRevisionData = React.useMemo(() => { + if (!selectedRevisionId) return null + return allRevisions.find(r => r.id === selectedRevisionId) || null + }, [selectedRevisionId, allRevisions]) + + const cleanupHtmlStyle = React.useCallback(() => { + const htmlElement = window.document.documentElement + const originalStyle = htmlElement.getAttribute("style") || "" + const colorSchemeStyle = originalStyle + .split(";") + .map((s) => s.trim()) + .find((s) => s.startsWith("color-scheme:")) + + if (colorSchemeStyle) { + htmlElement.setAttribute("style", colorSchemeStyle + ";") + } else { + htmlElement.removeAttribute("style") + } + }, []) + + const handleViewRevision = React.useCallback((revision: RevisionInfo) => { + setSelectedRevision(revision) + setViewerOpen(true) + setViewerLoading(true) + setFileSetLoading(true) + initialized.current = false + }, []) + + const handleDownloadFile = React.useCallback(async (attachment: AttachmentInfo) => { + try { + const queryParam = attachment.id + ? `id=${encodeURIComponent(attachment.id)}` + : `path=${encodeURIComponent(attachment.filePath)}` + + const response = await fetch(`/api/document-download?${queryParam}`) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to download file.') + } + + const blob = await response.blob() + const url = window.URL.createObjectURL(blob) + const link = window.document.createElement('a') + link.href = url + link.download = attachment.fileName + window.document.body.appendChild(link) + link.click() + window.document.body.removeChild(link) + window.URL.revokeObjectURL(url) + } catch (error) { + console.error('File download error:', error) + alert(`File download failed: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + }, []) + + // WebViewer 초기화 + React.useEffect(() => { + if (viewerOpen && !initialized.current) { + initialized.current = true + isCancelled.current = false + + requestAnimationFrame(() => { + if (viewer.current && !isCancelled.current) { + import("@pdftron/webviewer").then(({ default: WebViewer }) => { + if (isCancelled.current) { + console.log("WebViewer initialization cancelled (Dialog closed)") + return + } + + WebViewer( + { + path: "/pdftronWeb", + licenseKey: "demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd", + fullAPI: true, + css: "/globals.css", + }, + viewer.current as HTMLDivElement + ).then(async (instance: WebViewerInstance) => { + if (!isCancelled.current) { + setInstance(instance) + instance.UI.enableFeatures([instance.UI.Feature.MultiTab]) + instance.UI.disableElements(["addTabButton", "multiTabsEmptyPage"]) + setViewerLoading(false) + } + }) + }) + } + }) + } + + return () => { + if (instance) { + instance.UI.dispose() + } + setTimeout(() => cleanupHtmlStyle(), 500) + } + }, [viewerOpen, cleanupHtmlStyle, instance]) + + // 문서 로드 + React.useEffect(() => { + const loadDocument = async () => { + if (instance && selectedRevision?.attachments?.length) { + const { UI } = instance + + const tabIds = [] + for (const attachment of selectedRevision.attachments) { + try { + const response = await fetch(attachment.filePath) + const blob = await response.blob() + const options = { + filename: attachment.fileName, + ...(attachment.fileType?.includes("xlsx") && { + officeOptions: { + formatOptions: { + applyPageBreaksToSheet: true, + }, + }, + }), + } + const tab = await UI.TabManager.addTab(blob, options) + tabIds.push(tab) + } catch (error) { + console.error("File load failed:", attachment.filePath, error) + } + } + + if (tabIds.length > 0) { + await UI.TabManager.setActiveTab(tabIds[0]) + } + + setFileSetLoading(false) + } + } + loadDocument() + }, [instance, selectedRevision]) + + const handleCloseViewer = React.useCallback(async () => { + if (!fileSetLoading) { + isCancelled.current = true + + if (instance) { + try { + await instance.UI.dispose() + setInstance(null) + } catch (e) { + console.warn("dispose error", e) + } + } + + setViewerLoading(false) + setViewerOpen(false) + setTimeout(() => cleanupHtmlStyle(), 1000) + } + }, [fileSetLoading, instance, cleanupHtmlStyle]) + + if (!selectedDocument) return null + + return ( + <> + {/* 컨테이너 너비 제한 강화 */} +
+
+ + +
+
+ + {/* 문서 뷰어 다이얼로그 */} + + + + Document Preview + + Revision {selectedRevision?.revision} attachments + + +
+ {viewerLoading && ( +
+ +

+ Loading document viewer... +

+
+ )} +
+
+
+ + ) +} + +/* ------------------------------------------------------------------------------------------------- + * High‑level Selected Document Summary + * -----------------------------------------------------------------------------------------------*/ +function SelectedDocumentInfo() { + const { selectedDocumentId, selectedRevisionId, allData } = + React.useContext(DocumentSelectionContextAll) + + if (!selectedDocumentId || !allData) return null + + const doc = allData.find((d) => d.documentId === selectedDocumentId) + if (!doc) return null + + const totalRevisions = doc.allStages + ? (doc.allStages as StageInfo[]).reduce( + (acc, s) => acc + s.revisions.length, + 0, + ) + : 0 + + let selectedRevision: RevisionInfo | null = null + if (selectedRevisionId && doc.allStages) { + for (const stage of doc.allStages as StageInfo[]) { + const rev = stage.revisions.find((r) => r.id === selectedRevisionId) + if (rev) { + selectedRevision = rev + break + } + } + } + + return ( +
+
+
+ + Document: {doc.docNumber} + + + {doc.title} + +
+
+ + Total {totalRevisions} revisions + {selectedRevision && ( + <> + + + Selected revision: {selectedRevision.revision} + + ({selectedRevision.attachments.length} files) + + )} +
+
+
+ ) +} + +/* ------------------------------------------------------------------------------------------------- + * Main Exported Component + * -----------------------------------------------------------------------------------------------*/ +export function UserVendorALLDocumentDisplay({ + allPromises, +}: UserVendorDocumentDisplayProps) { + const [selectedDocumentId, setSelectedDocumentId] = + React.useState(null) + const [selectedStageId, setSelectedStageId] = React.useState( + null, + ) + const [selectedRevisionId, setSelectedRevisionId] = + React.useState(null) + const [allData, setAllData] = + React.useState(null) + + const handleDocumentSelect = React.useCallback((id: number | null) => { + setSelectedDocumentId(id) + setSelectedStageId(null) + setSelectedRevisionId(null) + }, []) + + const ctx = React.useMemo( + () => ({ + selectedDocumentId, + selectedStageId, + selectedRevisionId, + setSelectedDocumentId: handleDocumentSelect, + setSelectedStageId, + setSelectedRevisionId, + allData, + setAllData, + }), + [ + selectedDocumentId, + selectedStageId, + selectedRevisionId, + handleDocumentSelect, + allData, + setAllData, + ], + ) + + if (!allPromises) { + return ( + + +
+ +

Unable to load data.

+
+
+
+ ) + } + + return ( + +
+ + +
+ +
+
+
+ + +
+
+ ) +} \ No newline at end of file -- cgit v1.2.3