diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-19 07:51:27 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-19 07:51:27 +0000 |
| commit | 9ecdfb23fe3df6a5df86782385002c562dfc1198 (patch) | |
| tree | 4188cb7e6bf2c862d9c86a59d79946bd41217227 /lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx | |
| parent | b67861fbb424c7ad47ad1538f75e2945bd8890c5 (diff) | |
(대표님) rfq 히스토리, swp 등
Diffstat (limited to 'lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx')
| -rw-r--r-- | lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx | 520 |
1 files changed, 520 insertions, 0 deletions
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 | WebViewerInstance>(null) + const [viewerLoading, setViewerLoading] = useState<boolean>(true) + const [fileSetLoading, setFileSetLoading] = useState<boolean>(true) + const viewer = useRef<HTMLDivElement>(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 ( + <Dialog open={open} onOpenChange={(val) => !val && handleClose()}> + <DialogContent className="w-[90vw] h-[90vh]" style={{ maxWidth: "none" }}> + <DialogHeader className="h-[38px]"> + <DialogTitle>Preview</DialogTitle> + {/* <DialogDescription>첨부파일 미리보기</DialogDescription> */} + </DialogHeader> + <div + ref={viewer} + style={{ height: "calc(90vh - 20px - 38px - 1rem - 48px)" }} + > + {viewerLoading && ( + <div className="flex flex-col items-center justify-center py-12"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> + <p className="text-sm text-muted-foreground"> + 문서 뷰어 로딩 중... + </p> + </div> + )} + </div> + </DialogContent> + </Dialog> + ) +} + +export function ViewSubmissionDialog({ + open, + onOpenChange, + submission +}: ViewSubmissionDialogProps) { + const [loading, setLoading] = useState(false) + const [submissionDetail, setSubmissionDetail] = useState<SubmissionDetail | null>(null) + const [downloadingFiles, setDownloadingFiles] = useState<Set<number>>(new Set()) + const [viewerOpen, setViewerOpen] = useState(false) + const [selectedFiles, setSelectedFiles] = useState<Array<{ + id: number + originalFileName: string + storageUrl: string + }>>([]) + + 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 <Badge variant={variant}>{status}</Badge> + } + + return ( + <> + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl max-h-[80vh]"> + <DialogHeader> + <DialogTitle>View Submission</DialogTitle> + <DialogDescription> + Submission details and attached files + </DialogDescription> + </DialogHeader> + + {loading ? ( + <div className="flex items-center justify-center py-8"> + <RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" /> + </div> + ) : submissionDetail ? ( + <Tabs defaultValue="details" className="w-full"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="details">Details</TabsTrigger> + <TabsTrigger value="files"> + Files ({submissionDetail.files.length}) + </TabsTrigger> + </TabsList> + + <TabsContent value="details" className="space-y-4"> + <div className="grid gap-4"> + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-1"> + <p className="text-sm font-medium text-muted-foreground"> + Revision + </p> + <p className="text-lg font-medium"> + Rev. {submissionDetail.revisionNumber} + </p> + </div> + <div className="space-y-1"> + <p className="text-sm font-medium text-muted-foreground"> + Status + </p> + <div className="flex items-center gap-2"> + {getStatusBadge(submissionDetail.submissionStatus)} + {submissionDetail.reviewStatus && + getStatusBadge(submissionDetail.reviewStatus)} + </div> + </div> + </div> + + <div className="grid grid-cols-2 gap-4"> + <div className="space-y-1"> + <p className="text-sm font-medium text-muted-foreground"> + Submitted By + </p> + <div className="flex items-center gap-2"> + <User className="h-4 w-4 text-muted-foreground" /> + <span>{submissionDetail.submittedBy}</span> + </div> + </div> + <div className="space-y-1"> + <p className="text-sm font-medium text-muted-foreground"> + Submitted At + </p> + <div className="flex items-center gap-2"> + <Calendar className="h-4 w-4 text-muted-foreground" /> + <span>{formatDateTime(submissionDetail.submittedAt)}</span> + </div> + </div> + </div> + + {submissionDetail.reviewComments && ( + <div className="space-y-1"> + <p className="text-sm font-medium text-muted-foreground"> + Review Comments + </p> + <div className="p-3 bg-muted rounded-lg"> + <p className="text-sm">{submissionDetail.reviewComments}</p> + </div> + </div> + )} + </div> + </TabsContent> + + <TabsContent value="files"> + <div className="flex justify-end mb-4"> + <Button + variant="outline" + size="sm" + onClick={handlePreviewAll} + disabled={submissionDetail.files.length === 0} + > + <Eye className="h-4 w-4 mr-2" /> + 모든 파일 미리보기 + </Button> + </div> + <ScrollArea className="h-[400px]"> + <Table> + <TableHeader> + <TableRow> + <TableHead>File Name</TableHead> + <TableHead>Size</TableHead> + <TableHead>Upload Date</TableHead> + <TableHead>Sync Status</TableHead> + <TableHead className="text-right">Actions</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {submissionDetail.files.map((file) => { + const isDownloading = downloadingFiles.has(file.id) + + return ( + <TableRow key={file.id}> + <TableCell className="font-medium"> + <div className="flex items-center gap-2"> + <FileText className="h-4 w-4 text-muted-foreground" /> + {file.originalFileName} + </div> + </TableCell> + <TableCell>{formatFileSize(file.fileSize)}</TableCell> + <TableCell>{formatDate(file.uploadedAt)}</TableCell> + <TableCell> + <Badge + variant={ + file.syncStatus === "synced" ? "success" : + file.syncStatus === "failed" ? "destructive" : + "secondary" + } + className="text-xs" + > + {file.syncStatus} + </Badge> + </TableCell> + <TableCell className="text-right"> + <div className="flex justify-end gap-2"> + <Button + variant="ghost" + size="icon" + onClick={() => handleDownload(file)} + disabled={isDownloading} + title="Download" + > + {isDownloading ? ( + <RefreshCw className="h-4 w-4 animate-spin" /> + ) : ( + <Download className="h-4 w-4" /> + )} + </Button> + <Button + variant="ghost" + size="icon" + onClick={() => handlePreview(file)} + disabled={isDownloading} + title="Preview" + > + {isDownloading ? ( + <RefreshCw className="h-4 w-4 animate-spin" /> + ) : ( + <Eye className="h-4 w-4" /> + )} + </Button> + </div> + </TableCell> + </TableRow> + ) + })} + </TableBody> + </Table> + </ScrollArea> + </TabsContent> + </Tabs> + ) : ( + <div className="text-center py-8 text-muted-foreground"> + No submission found + </div> + )} + </DialogContent> + </Dialog> + + {/* PDFTron 문서 뷰어 다이얼로그 */} + {viewerOpen && ( + <DocumentViewer + open={viewerOpen} + onClose={() => { + setViewerOpen(false) + setSelectedFiles([]) + }} + files={selectedFiles} + /> + )} + </> + ) +}
\ No newline at end of file |
