diff options
Diffstat (limited to 'lib/tbe-last/vendor/vendor-document-upload-dialog.tsx')
| -rw-r--r-- | lib/tbe-last/vendor/vendor-document-upload-dialog.tsx | 326 |
1 files changed, 326 insertions, 0 deletions
diff --git a/lib/tbe-last/vendor/vendor-document-upload-dialog.tsx b/lib/tbe-last/vendor/vendor-document-upload-dialog.tsx new file mode 100644 index 00000000..c6f6c3d5 --- /dev/null +++ b/lib/tbe-last/vendor/vendor-document-upload-dialog.tsx @@ -0,0 +1,326 @@ +// lib/vendor-rfq-response/vendor-tbe-table/vendor-document-upload-dialog.tsx + +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { toast } from "sonner" +import { Upload, FileText, X, Loader2 } from "lucide-react" +import { uploadVendorDocument } from "@/lib/tbe-last/service" + +interface VendorDocumentUploadDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + sessionId: number | null + sessionDetail: any + onUploadSuccess: () => void +} + +interface FileUpload { + id: string + file: File + documentType: string + description: string + status: "pending" | "uploading" | "success" | "error" + errorMessage?: string +} + +export function VendorDocumentUploadDialog({ + open, + onOpenChange, + sessionId, + sessionDetail, + onUploadSuccess +}: VendorDocumentUploadDialogProps) { + + const [files, setFiles] = React.useState<FileUpload[]>([]) + const [isUploading, setIsUploading] = React.useState(false) + const fileInputRef = React.useRef<HTMLInputElement>(null) + + // Document types for vendor + const documentTypes = [ + { value: "technical_spec", label: "Technical Specification" }, + { value: "compliance_cert", label: "Compliance Certificate" }, + { value: "test_report", label: "Test Report" }, + { value: "drawing", label: "Drawing" }, + { value: "datasheet", label: "Datasheet" }, + { value: "quality_doc", label: "Quality Document" }, + { value: "warranty", label: "Warranty Document" }, + { value: "other", label: "Other" }, + ] + + // Handle file selection + const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { + const selectedFiles = Array.from(e.target.files || []) + + const newFiles: FileUpload[] = selectedFiles.map(file => ({ + id: Math.random().toString(36).substr(2, 9), + file, + documentType: "technical_spec", + description: "", + status: "pending" as const + })) + + setFiles(prev => [...prev, ...newFiles]) + + // Reset input + if (fileInputRef.current) { + fileInputRef.current.value = "" + } + } + + // Remove file + const handleRemoveFile = (id: string) => { + setFiles(prev => prev.filter(f => f.id !== id)) + } + + // Update file details + const handleUpdateFile = (id: string, field: keyof FileUpload, value: string) => { + setFiles(prev => prev.map(f => + f.id === id ? { ...f, [field]: value } : f + )) + } + + // Upload all files + const handleUploadAll = async () => { + if (!sessionId || files.length === 0) return + + setIsUploading(true) + + try { + for (const fileUpload of files) { + if (fileUpload.status === "success") continue + + // Update status to uploading + setFiles(prev => prev.map(f => + f.id === fileUpload.id ? { ...f, status: "uploading" } : f + )) + + try { + // Create FormData for upload + const formData = new FormData() + formData.append("file", fileUpload.file) + formData.append("sessionId", sessionId.toString()) + formData.append("documentType", fileUpload.documentType) + formData.append("description", fileUpload.description) + + // Upload file (API call) + const response = await fetch("/api/tbe/vendor-documents/upload", { + method: "POST", + body: formData + }) + + if (!response.ok) throw new Error("Upload failed") + + // Update status to success + setFiles(prev => prev.map(f => + f.id === fileUpload.id ? { ...f, status: "success" } : f + )) + + } catch (error) { + // Update status to error + setFiles(prev => prev.map(f => + f.id === fileUpload.id ? { + ...f, + status: "error", + errorMessage: error instanceof Error ? error.message : "Upload failed" + } : f + )) + } + } + + // Check if all files uploaded successfully + const allSuccess = files.every(f => f.status === "success") + + if (allSuccess) { + toast.success("모든 문서가 업로드되었습니다") + onUploadSuccess() + onOpenChange(false) + setFiles([]) + } else { + const failedCount = files.filter(f => f.status === "error").length + toast.error(`${failedCount}개 문서 업로드 실패`) + } + + } catch (error) { + console.error("Upload error:", error) + toast.error("문서 업로드 중 오류가 발생했습니다") + } finally { + setIsUploading(false) + } + } + + // Get file size in readable format + const formatFileSize = (bytes: number) => { + if (bytes < 1024) return bytes + " B" + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB" + return (bytes / (1024 * 1024)).toFixed(1) + " MB" + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl max-h-[80vh]"> + <DialogHeader> + <DialogTitle>Upload Documents for TBE</DialogTitle> + <DialogDescription> + {sessionDetail?.session?.sessionCode} - Technical Bid Evaluation 문서 업로드 + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* File Upload Area */} + <div className="border-2 border-dashed rounded-lg p-6 text-center"> + <Upload className="h-12 w-12 mx-auto text-muted-foreground mb-2" /> + <p className="text-sm text-muted-foreground mb-2"> + 파일을 드래그하거나 클릭하여 선택하세요 + </p> + <Input + ref={fileInputRef} + type="file" + multiple + onChange={handleFileSelect} + className="hidden" + accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.zip" + /> + <Button + variant="outline" + onClick={() => fileInputRef.current?.click()} + disabled={isUploading} + > + 파일 선택 + </Button> + </div> + + {/* Selected Files List */} + {files.length > 0 && ( + <ScrollArea className="h-[300px] border rounded-lg p-4"> + <div className="space-y-4"> + {files.map(fileUpload => ( + <div key={fileUpload.id} className="border rounded-lg p-4 space-y-3"> + <div className="flex items-start justify-between"> + <div className="flex items-center gap-2"> + <FileText className="h-5 w-5 text-muted-foreground" /> + <div> + <p className="font-medium text-sm">{fileUpload.file.name}</p> + <p className="text-xs text-muted-foreground"> + {formatFileSize(fileUpload.file.size)} + </p> + </div> + </div> + <div className="flex items-center gap-2"> + {fileUpload.status === "uploading" && ( + <Loader2 className="h-4 w-4 animate-spin" /> + )} + {fileUpload.status === "success" && ( + <Badge variant="default">Uploaded</Badge> + )} + {fileUpload.status === "error" && ( + <Badge variant="destructive">Failed</Badge> + )} + <Button + variant="ghost" + size="sm" + onClick={() => handleRemoveFile(fileUpload.id)} + disabled={isUploading} + > + <X className="h-4 w-4" /> + </Button> + </div> + </div> + + {fileUpload.status !== "success" && ( + <> + <div className="grid grid-cols-2 gap-3"> + <div> + <Label className="text-xs">Document Type</Label> + <Select + value={fileUpload.documentType} + onValueChange={(value) => handleUpdateFile(fileUpload.id, "documentType", value)} + disabled={isUploading} + > + <SelectTrigger className="h-8 text-xs"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + {documentTypes.map(type => ( + <SelectItem key={type.value} value={type.value}> + {type.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + </div> + + <div> + <Label className="text-xs">Description (Optional)</Label> + <Textarea + value={fileUpload.description} + onChange={(e) => handleUpdateFile(fileUpload.id, "description", e.target.value)} + placeholder="문서에 대한 설명을 입력하세요..." + className="min-h-[60px] text-xs" + disabled={isUploading} + /> + </div> + </> + )} + + {fileUpload.errorMessage && ( + <p className="text-xs text-red-600">{fileUpload.errorMessage}</p> + )} + </div> + ))} + </div> + </ScrollArea> + )} + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isUploading} + > + 취소 + </Button> + <Button + onClick={handleUploadAll} + disabled={files.length === 0 || isUploading} + > + {isUploading ? ( + <> + <Loader2 className="h-4 w-4 mr-2 animate-spin" /> + 업로드 중... + </> + ) : ( + <> + <Upload className="h-4 w-4 mr-2" /> + 업로드 ({files.filter(f => f.status !== "success").length}개) + </> + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
