summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-19 07:51:27 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-19 07:51:27 +0000
commit9ecdfb23fe3df6a5df86782385002c562dfc1198 (patch)
tree4188cb7e6bf2c862d9c86a59d79946bd41217227 /lib/vendor-document-list/plant/upload/components/view-submission-dialog.tsx
parentb67861fbb424c7ad47ad1538f75e2945bd8890c5 (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.tsx520
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