From 9ecdfb23fe3df6a5df86782385002c562dfc1198 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 19 Sep 2025 07:51:27 +0000 Subject: (대표님) rfq 히스토리, swp 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../upload/components/view-submission-dialog.tsx | 520 +++++++++++++++++++++ 1 file changed, 520 insertions(+) create mode 100644 lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx (limited to 'lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx') diff --git a/lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx b/lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx new file mode 100644 index 00000000..9a55a7fa --- /dev/null +++ b/lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx @@ -0,0 +1,520 @@ +// lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx +"use client" + +import * as React from "react" +import { useState, useEffect, useRef } from "react" +import { WebViewerInstance } from "@pdftron/webviewer" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Download, + Eye, + FileText, + Calendar, + User, + CheckCircle2, + XCircle, + Clock, + RefreshCw, + Loader2 +} from "lucide-react" +import { StageSubmissionView } from "@/db/schema" +import { formatDateTime, formatDate } from "@/lib/utils" +import { toast } from "sonner" +import { downloadFile, formatFileSize } from "@/lib/file-download" + +interface ViewSubmissionDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + submission: StageSubmissionView +} + +interface SubmissionDetail { + id: number + revisionNumber: number + submissionStatus: string + reviewStatus?: string + reviewComments?: string + submittedBy: string + submittedAt: Date + files: Array<{ + id: number + originalFileName: string + fileSize: number + uploadedAt: Date + syncStatus: string + storageUrl: string + }> +} + +// PDFTron 문서 뷰어 컴포넌트 +const DocumentViewer: React.FC<{ + open: boolean + onClose: () => void + files: Array<{ + id: number + originalFileName: string + storageUrl: string + }> +}> = ({ open, onClose, files }) => { + const [instance, setInstance] = useState(null) + const [viewerLoading, setViewerLoading] = useState(true) + const [fileSetLoading, setFileSetLoading] = useState(true) + const viewer = useRef(null) + const initialized = useRef(false) + const isCancelled = useRef(false) + + const cleanupHtmlStyle = () => { + const htmlElement = 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") + } + } + + useEffect(() => { + if (open && !initialized.current) { + initialized.current = true + isCancelled.current = false + + requestAnimationFrame(() => { + if (viewer.current) { + import("@pdftron/webviewer").then(({ default: WebViewer }) => { + if (isCancelled.current) { + console.log("WebViewer 초기화 취소됨") + return + } + + WebViewer( + { + path: "/pdftronWeb", + licenseKey: + "demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd", + fullAPI: true, + css: "/globals.css", + }, + viewer.current as HTMLDivElement + ).then(async (instance: WebViewerInstance) => { + 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) + } + }, [open]) + + useEffect(() => { + const loadDocuments = async () => { + if (instance && files.length > 0) { + const { UI } = instance + const tabIds = [] + + for (const file of files) { + const fileExtension = file.originalFileName.split('.').pop()?.toLowerCase() + + const options = { + filename: file.originalFileName, + ...(fileExtension === 'xlsx' || fileExtension === 'xls' ? { + officeOptions: { + formatOptions: { + applyPageBreaksToSheet: true, + }, + }, + } : {}), + } + + try { + const response = await fetch(file.storageUrl) + const blob = await response.blob() + const tab = await UI.TabManager.addTab(blob, options) + tabIds.push(tab) + } catch (error) { + console.error(`Failed to load ${file.originalFileName}:`, error) + toast.error(`Failed to load ${file.originalFileName}`) + } + } + + if (tabIds.length > 0) { + await UI.TabManager.setActiveTab(tabIds[0]) + } + + setFileSetLoading(false) + } + } + + loadDocuments() + }, [instance, files]) + + const handleClose = async () => { + if (!fileSetLoading) { + if (instance) { + try { + await instance.UI.dispose() + setInstance(null) + } catch (e) { + console.warn("dispose error", e) + } + } + + setTimeout(() => cleanupHtmlStyle(), 1000) + onClose() + } + } + + return ( + !val && handleClose()}> + + + Preview + {/* 첨부파일 미리보기 */} + +
+ {viewerLoading && ( +
+ +

+ 문서 뷰어 로딩 중... +

+
+ )} +
+
+
+ ) +} + +export function ViewSubmissionDialog({ + open, + onOpenChange, + submission +}: ViewSubmissionDialogProps) { + const [loading, setLoading] = useState(false) + const [submissionDetail, setSubmissionDetail] = useState(null) + const [downloadingFiles, setDownloadingFiles] = useState>(new Set()) + const [viewerOpen, setViewerOpen] = useState(false) + const [selectedFiles, setSelectedFiles] = useState>([]) + + useEffect(() => { + if (open && submission.latestSubmissionId) { + fetchSubmissionDetail() + } + }, [open, submission.latestSubmissionId]) + + const fetchSubmissionDetail = async () => { + if (!submission.latestSubmissionId) return + + setLoading(true) + try { + const response = await fetch(`/api/stage-submissions/${submission.latestSubmissionId}`) + if (response.ok) { + const data = await response.json() + setSubmissionDetail(data) + } + } catch (error) { + console.error("Failed to fetch submission details:", error) + toast.error("Failed to load submission details") + } finally { + setLoading(false) + } + } + + const handleDownload = async (file: any) => { + setDownloadingFiles(prev => new Set(prev).add(file.id)) + + try { + const result = await downloadFile( + file.storageUrl, + file.originalFileName, + { + action: 'download', + showToast: true, + showSuccessToast: true, + onError: (error) => { + console.error("Download failed:", error) + toast.error(`Failed to download ${file.originalFileName}`) + }, + onSuccess: (fileName, fileSize) => { + console.log(`Successfully downloaded ${fileName}`) + } + } + ) + + if (!result.success) { + console.error("Download failed:", result.error) + } + } finally { + setDownloadingFiles(prev => { + const newSet = new Set(prev) + newSet.delete(file.id) + return newSet + }) + } + } + + // PDFTron으로 미리보기 처리 + const handlePreview = (file: any) => { + setSelectedFiles([{ + id: file.id, + originalFileName: file.originalFileName, + storageUrl: file.storageUrl + }]) + setViewerOpen(true) + } + + // 모든 파일 미리보기 + const handlePreviewAll = () => { + if (submissionDetail) { + const files = submissionDetail.files.map(file => ({ + id: file.id, + originalFileName: file.originalFileName, + storageUrl: file.storageUrl + })) + setSelectedFiles(files) + setViewerOpen(true) + } + } + + const getStatusBadge = (status?: string) => { + if (!status) return null + + const variant = status === "APPROVED" ? "success" : + status === "REJECTED" ? "destructive" : + status === "SUBMITTED" ? "default" : "secondary" + + return {status} + } + + return ( + <> + + + + View Submission + + Submission details and attached files + + + + {loading ? ( +
+ +
+ ) : submissionDetail ? ( + + + Details + + Files ({submissionDetail.files.length}) + + + + +
+
+
+

+ Revision +

+

+ Rev. {submissionDetail.revisionNumber} +

+
+
+

+ Status +

+
+ {getStatusBadge(submissionDetail.submissionStatus)} + {submissionDetail.reviewStatus && + getStatusBadge(submissionDetail.reviewStatus)} +
+
+
+ +
+
+

+ Submitted By +

+
+ + {submissionDetail.submittedBy} +
+
+
+

+ Submitted At +

+
+ + {formatDateTime(submissionDetail.submittedAt)} +
+
+
+ + {submissionDetail.reviewComments && ( +
+

+ Review Comments +

+
+

{submissionDetail.reviewComments}

+
+
+ )} +
+
+ + +
+ +
+ + + + + File Name + Size + Upload Date + Sync Status + Actions + + + + {submissionDetail.files.map((file) => { + const isDownloading = downloadingFiles.has(file.id) + + return ( + + +
+ + {file.originalFileName} +
+
+ {formatFileSize(file.fileSize)} + {formatDate(file.uploadedAt)} + + + {file.syncStatus} + + + +
+ + +
+
+
+ ) + })} +
+
+
+
+
+ ) : ( +
+ No submission found +
+ )} +
+
+ + {/* PDFTron 문서 뷰어 다이얼로그 */} + {viewerOpen && ( + { + setViewerOpen(false) + setSelectedFiles([]) + }} + files={selectedFiles} + /> + )} + + ) +} \ No newline at end of file -- cgit v1.2.3