diff options
Diffstat (limited to 'lib/vendor-document-list/plant/upload/components/single-upload-dialog.tsx')
| -rw-r--r-- | lib/vendor-document-list/plant/upload/components/single-upload-dialog.tsx | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/lib/vendor-document-list/plant/upload/components/single-upload-dialog.tsx b/lib/vendor-document-list/plant/upload/components/single-upload-dialog.tsx new file mode 100644 index 00000000..a33a7160 --- /dev/null +++ b/lib/vendor-document-list/plant/upload/components/single-upload-dialog.tsx @@ -0,0 +1,265 @@ +// lib/vendor-document-list/plant/upload/components/single-upload-dialog.tsx +"use client" + +import * as React from "react" +import { useState } from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { + FileList, + FileListAction, + FileListIcon, + FileListInfo, + FileListItem, + FileListName, + FileListSize, +} from "@/components/ui/file-list" +import { + Upload, + X, + FileIcon, + Loader2, + AlertCircle +} from "lucide-react" +import { toast } from "sonner" +import { StageSubmissionView } from "@/db/schema" + +interface SingleUploadDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + submission: StageSubmissionView + onUploadComplete?: () => void +} + +export function SingleUploadDialog({ + open, + onOpenChange, + submission, + onUploadComplete +}: SingleUploadDialogProps) { + const [files, setFiles] = useState<File[]>([]) + const [description, setDescription] = useState("") + const [isUploading, setIsUploading] = useState(false) + const fileInputRef = React.useRef<HTMLInputElement>(null) + + // 파일 선택 + const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const fileList = e.target.files + if (fileList) { + setFiles(Array.from(fileList)) + } + } + + // 파일 제거 + const removeFile = (index: number) => { + setFiles(prev => prev.filter((_, i) => i !== index)) + } + + // 업로드 처리 + const handleUpload = async () => { + if (files.length === 0) { + toast.error("Please select files to upload") + return + } + + setIsUploading(true) + + try { + const formData = new FormData() + + files.forEach((file) => { + formData.append("files", file) + }) + + formData.append("documentId", submission.documentId.toString()) + formData.append("stageId", submission.stageId!.toString()) + formData.append("description", description) + + // 현재 리비전 + 1 + const nextRevision = (submission.latestRevisionNumber || 0) + 1 + formData.append("revision", nextRevision.toString()) + + const response = await fetch("/api/stage-submissions/upload", { + method: "POST", + body: formData, + }) + + if (!response.ok) { + throw new Error("Upload failed") + } + + const result = await response.json() + toast.success(`Successfully uploaded ${files.length} file(s)`) + + // 초기화 및 닫기 + setFiles([]) + setDescription("") + onOpenChange(false) + onUploadComplete?.() + + } catch (error) { + console.error("Upload error:", error) + toast.error("Failed to upload files") + } finally { + setIsUploading(false) + } + } + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + const totalSize = files.reduce((acc, file) => acc + file.size, 0) + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-2xl"> + <DialogHeader> + <DialogTitle>Upload Documents</DialogTitle> + <DialogDescription> + Upload documents for this stage submission + </DialogDescription> + </DialogHeader> + + {/* Document Info */} + <div className="grid gap-2 p-4 bg-muted rounded-lg"> + <div className="flex items-center gap-2"> + <span className="text-sm font-medium">Document:</span> + <span className="text-sm">{submission.docNumber}</span> + {submission.vendorDocNumber && ( + <span className="text-sm text-muted-foreground"> + ({submission.vendorDocNumber}) + </span> + )} + </div> + <div className="flex items-center gap-2"> + <span className="text-sm font-medium">Stage:</span> + <Badge variant="secondary">{submission.stageName}</Badge> + </div> + <div className="flex items-center gap-2"> + <span className="text-sm font-medium">Current Revision:</span> + <span className="text-sm">Rev. {submission.latestRevisionNumber || 0}</span> + <Badge variant="outline" className="ml-2"> + Next: Rev. {(submission.latestRevisionNumber || 0) + 1} + </Badge> + </div> + </div> + + {/* File Upload Area */} + <div + className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors cursor-pointer" + onClick={() => fileInputRef.current?.click()} + > + <input + ref={fileInputRef} + type="file" + multiple + className="hidden" + onChange={handleFileChange} + accept="*/*" + /> + <Upload className="mx-auto h-10 w-10 text-gray-400 mb-3" /> + <p className="text-sm font-medium">Click to browse files</p> + <p className="text-xs text-gray-500 mt-1"> + You can select multiple files + </p> + </div> + + {/* File List */} + {files.length > 0 && ( + <> + <FileList> + {files.map((file, index) => ( + <FileListItem key={index}> + <FileListIcon> + <FileIcon className="h-4 w-4 text-muted-foreground" /> + </FileListIcon> + <FileListInfo> + <FileListName>{file.name}</FileListName> + </FileListInfo> + <FileListSize> + {file.size} + </FileListSize> + <FileListAction> + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={(e) => { + e.stopPropagation() + removeFile(index) + }} + disabled={isUploading} + > + <X className="h-4 w-4" /> + </Button> + </FileListAction> + </FileListItem> + ))} + </FileList> + + <div className="flex justify-between text-sm text-muted-foreground"> + <span>{files.length} file(s) selected</span> + <span>Total: {formatFileSize(totalSize)}</span> + </div> + </> + )} + + {/* Description */} + <div className="space-y-2"> + <Label htmlFor="description">Description (Optional)</Label> + <Textarea + id="description" + placeholder="Add a description for this submission..." + value={description} + onChange={(e) => setDescription(e.target.value)} + rows={3} + /> + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isUploading} + > + Cancel + </Button> + <Button + onClick={handleUpload} + disabled={files.length === 0 || isUploading} + className="gap-2" + > + {isUploading ? ( + <> + <Loader2 className="h-4 w-4 animate-spin" /> + Uploading... + </> + ) : ( + <> + <Upload className="h-4 w-4" /> + Upload + </> + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
