"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 { FolderOpen, Loader2, ChevronRight, ChevronLeft, CheckCircle2 } from "lucide-react"; import { toast } from "sonner"; import { Progress } from "@/components/ui/progress"; import { useTranslation } from "@/i18n/client"; import { validateB4FileName, B4UploadValidationDialog, type FileValidationResult, } from "./b4-upload-validation-dialog"; import { checkB4MappingStatus, saveB4MappingBatch, type MappingCheckResult, type B4BulkUploadResult, type B4MappingSaveItem, } from "../actions"; import { uploadFilesWithProgress } from "../utils/upload-with-progress"; import { FileUploadProgressList } from "../components/file-upload-progress-list"; import type { FileUploadProgress } from "../hooks/use-file-upload-with-progress"; import { v4 as uuidv4 } from "uuid"; interface B4BulkUploadDialogV3Props { open: boolean; onOpenChange: (open: boolean) => void; projectNo: string; userId: string; userName: string; userEmail: string; vendorCode: string; onUploadComplete?: () => void; lng: string; } type UploadStep = "files" | "validation" | "uploading" | "complete"; export function B4BulkUploadDialogV3({ open, onOpenChange, projectNo, userId, userName, userEmail, vendorCode, onUploadComplete, lng, }: B4BulkUploadDialogV3Props) { const { t } = useTranslation(lng, "dolce"); const [currentStep, setCurrentStep] = useState("files"); const [selectedFiles, setSelectedFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [validationResults, setValidationResults] = useState([]); const [mappingResultsMap, setMappingResultsMap] = useState>(new Map()); const [showValidationDialog, setShowValidationDialog] = useState(false); const [isDragging, setIsDragging] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadResult, setUploadResult] = useState(null); const [fileProgresses, setFileProgresses] = useState([]); // Reset on close React.useEffect(() => { if (!open) { setCurrentStep("files"); setSelectedFiles([]); setValidationResults([]); setMappingResultsMap(new Map()); setShowValidationDialog(false); setIsDragging(false); setUploadProgress(0); setUploadResult(null); setFileProgresses([]); } }, [open]); // File Selection Handler const handleFilesChange = (files: File[]) => { if (files.length === 0) return; const existingNames = new Set(selectedFiles.map((f) => f.name)); const newFiles = files.filter((f) => !existingNames.has(f.name)); if (newFiles.length === 0) { toast.error(t("bulkUpload.duplicateFileError")); return; } setSelectedFiles((prev) => [...prev, ...newFiles]); toast.success(t("bulkUpload.filesSelectedSuccess", { count: newFiles.length })); }; // Drag & Drop Handlers const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target) { setIsDragging(false); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = "copy"; }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const droppedFiles = Array.from(e.dataTransfer.files); if (droppedFiles.length > 0) { handleFilesChange(droppedFiles); } }; const handleRemoveFile = (index: number) => { setSelectedFiles((prev) => prev.filter((_, i) => i !== index)); }; // Step 1 Next: Validation const handleFilesNext = () => { if (selectedFiles.length === 0) { toast.error(t("bulkUpload.selectFilesError")); return; } setCurrentStep("validation"); handleValidate(); }; // Validation Process (V3) const handleValidate = async () => { try { console.log("[V3 Dialog] Validation started"); // 1. Parse Filenames const parseResults: FileValidationResult[] = selectedFiles.map((file) => { const validation = validateB4FileName(file.name); return { file, valid: validation.valid, parsed: validation.parsed, error: validation.error, }; }); const parsedFiles = parseResults.filter((r) => r.valid && r.parsed); // If no files parsed correctly, show dialog immediately with errors if (parsedFiles.length === 0) { setValidationResults(parseResults); setShowValidationDialog(true); return; } // 2. Call MatchBatchFileDwg to check mapping status const mappingCheckItems = parsedFiles.map((r) => ({ DrawingNo: r.parsed!.drawingNo, RevNo: r.parsed!.revNo, FileNm: r.file.name, })); console.log(`[V3 Dialog] Checking mapping for ${mappingCheckItems.length} files`); const mappingResults = await checkB4MappingStatus( projectNo, mappingCheckItems ); // Store mapping results for later use (upload/save) const newMappingResultsMap = new Map(); mappingResults.forEach((result) => { newMappingResultsMap.set(result.FileNm, result); }); setMappingResultsMap(newMappingResultsMap); // 3. Merge results const finalResults: FileValidationResult[] = parseResults.map((parseResult) => { if (!parseResult.valid || !parseResult.parsed) { return parseResult; } const mappingResult = newMappingResultsMap.get(parseResult.file.name); if (!mappingResult) { return { ...parseResult, mappingStatus: "not_found" as const, error: t("validation.notFound"), }; } // According to prompt: "API 응답에서 매핑되지 않은 경우는, 파일명으로부터 파싱된 도면이 없는 경우임." // Also "MappingYN 의 값이 현재 매핑이 되어있는지를 나타냄. Y인 건들은 저장 가능" if (mappingResult.MappingYN !== "Y") { return { ...parseResult, mappingStatus: "not_found" as const, error: t("validation.notRegistered"), // Or specific message for MappingYN=N }; } // Check DrawingMoveGbn = "도면입수" (implied by requirements to use MatchBatchFileDwg with 도면입수) if (mappingResult.DrawingMoveGbn !== "도면입수") { return { ...parseResult, mappingStatus: "not_found" as const, error: t("validation.notGttDeliverables"), }; } return { ...parseResult, mappingStatus: "available" as const, drawingName: mappingResult.DrawingName || undefined, registerGroupId: mappingResult.RegisterGroupId, }; }); console.log("[V3 Dialog] Validation complete"); setValidationResults(finalResults); setShowValidationDialog(true); } catch (error) { console.error("[V3 Dialog] Validation failed:", error); toast.error( error instanceof Error ? error.message : t("bulkUpload.validationError") ); // Go back to files step if validation crashes completely setCurrentStep("files"); } }; // Confirm Upload & Save (V3) const handleConfirmUpload = async (validFiles: FileValidationResult[]) => { setIsUploading(true); setCurrentStep("uploading"); setShowValidationDialog(false); try { console.log(`[V3 Dialog] Upload started: ${validFiles.length} files`); // 0. Initialize progress const initialProgresses: FileUploadProgress[] = validFiles.map((fileResult) => ({ file: fileResult.file, progress: 0, status: "pending" as const, })); setFileProgresses(initialProgresses); // 1. Group by DrawingNo + RevNo (to share UploadId if needed) const uploadGroups = new Map< string, Array<{ file: File; fileIndex: number; // Index in validFiles mappingData: MappingCheckResult; }> >(); // Pre-process groups validFiles.forEach((fileResult, index) => { const mappingData = mappingResultsMap.get(fileResult.file.name); if (!mappingData) return; // Should not happen for valid files const groupKey = `${mappingData.DrawingNo}_${mappingData.RevNo}`; if (!uploadGroups.has(groupKey)) { uploadGroups.set(groupKey, []); } uploadGroups.get(groupKey)!.push({ file: fileResult.file, fileIndex: index, mappingData }); }); let successCount = 0; let failCount = 0; let completedGroups = 0; const results: B4BulkUploadResult["results"] = []; // 2. Process each group for (const [groupKey, groupItems] of uploadGroups.entries()) { // Reuse UploadId from the first item's mapping data if available, else generate new const firstItemMapping = groupItems[0].mappingData; // Reuse existing UploadId if present in API response, otherwise generate new one // The prompt says: "UploadId는 있으면 재활용하고, 없으면 UUID로 만들어줌" const uploadId = firstItemMapping.UploadId || uuidv4(); console.log(`[V3 Dialog] Processing group ${groupKey}, UploadId: ${uploadId}`); try { // Update status to uploading setFileProgresses((prev) => prev.map((fp, idx) => groupItems.some(item => item.fileIndex === idx) ? { ...fp, status: "uploading" as const } : fp ) ); // A. Upload Files (Physical Upload) const uploadResult = await uploadFilesWithProgress({ uploadId: uploadId, userId: userId, files: groupItems.map(item => item.file), callbacks: { onProgress: (fileIndexInGroup, progress) => { const globalFileIndex = groupItems[fileIndexInGroup].fileIndex; setFileProgresses((prev) => prev.map((fp, idx) => idx === globalFileIndex ? { ...fp, progress, status: "uploading" as const } : fp ) ); // Overall progress approximation const groupProgress = (completedGroups / uploadGroups.size) * 100; const currentGroupProgress = (progress / 100) * (100 / uploadGroups.size); setUploadProgress(Math.round(groupProgress + currentGroupProgress)); }, onFileComplete: (fileIndexInGroup) => { const globalFileIndex = groupItems[fileIndexInGroup].fileIndex; setFileProgresses((prev) => prev.map((fp, idx) => idx === globalFileIndex ? { ...fp, progress: 100, status: "completed" as const } : fp ) ); }, onFileError: (fileIndexInGroup, error) => { const globalFileIndex = groupItems[fileIndexInGroup].fileIndex; console.error(`[V3 Dialog] File upload error:`, error); setFileProgresses((prev) => prev.map((fp, idx) => idx === globalFileIndex ? { ...fp, status: "error" as const, error } : fp ) ); } } }); if (!uploadResult.success) { throw new Error(uploadResult.error || "File upload failed"); } // B. Save Metadata (MatchBatchFileDwgEdit) // Construct payload from mappingData + generated UploadId + hardcoded values as per prompt const mappingSaveLists: B4MappingSaveItem[] = groupItems.map(item => { const m = item.mappingData; return { CGbn: m.CGbn, Category: "TS", // Hardcoded as per prompt CheckBox: "0", DGbn: m.DGbn, DegreeGbn: m.DegreeGbn, DeptGbn: m.DeptGbn, Discipline: m.Discipline, DrawingKind: "B4", DrawingMoveGbn: "도면입수", DrawingName: m.DrawingName, DrawingNo: m.DrawingNo, DrawingUsage: "입수용", FileNm: item.file.name, JGbn: m.JGbn, Manager: m.Manager || "970043", // Fallback/Default MappingYN: "Y", NewOrNot: "N", ProjectNo: projectNo, RegisterGroup: 0, RegisterGroupId: m.RegisterGroupId, RegisterKindCode: m.RegisterKindCode, RegisterSerialNo: m.RegisterSerialNo, RevNo: m.RevNo, SGbn: m.SGbn, UploadId: uploadId // Used for all files in this group }; }); await saveB4MappingBatch(mappingSaveLists, { userId, userName, vendorCode, email: userEmail, }); console.log(`[V3 Dialog] Group ${groupKey} complete`); successCount += groupItems.length; groupItems.forEach(item => { results.push({ drawingNo: item.mappingData.DrawingNo, revNo: item.mappingData.RevNo || "", fileName: item.file.name, success: true }); }); } catch (error) { console.error(`[V3 Dialog] Group ${groupKey} failed:`, error); failCount += groupItems.length; const errorMessage = error instanceof Error ? error.message : "Unknown error"; groupItems.forEach(item => { results.push({ drawingNo: item.mappingData.DrawingNo, revNo: item.mappingData.RevNo || "", fileName: item.file.name, success: false, error: errorMessage }); }); } completedGroups++; setUploadProgress(Math.round((completedGroups / uploadGroups.size) * 100)); } // Finalize const result: B4BulkUploadResult = { success: successCount > 0, successCount, failCount, results, }; setUploadResult(result); setCurrentStep("complete"); if (result.success) { toast.success( t("bulkUpload.uploadSuccessToast", { successCount: result.successCount, total: validFiles.length, }) ); } else { toast.error(result.error || t("bulkUpload.uploadError")); } } catch (error) { console.error("[V3 Dialog] Upload process failed:", error); toast.error( error instanceof Error ? error.message : t("bulkUpload.uploadError") ); setCurrentStep("files"); } finally { setIsUploading(false); } }; return ( <> {t("bulkUpload.title")} (V3) {currentStep === "files" && t("bulkUpload.stepFiles")} {currentStep === "validation" && t("bulkUpload.stepValidation")}
{/* Step 1: Files */} {currentStep === "files" && ( <>
handleFilesChange(Array.from(e.target.files || []))} className="hidden" id="b4-file-upload-v3" />
{selectedFiles.length > 0 && (

{t("bulkUpload.selectedFiles", { count: selectedFiles.length })}

{selectedFiles.map((file, index) => (

{file.name}

{(file.size / 1024 / 1024).toFixed(2)} MB

))}
)} )} {/* Loading Indicator */} {currentStep === "validation" && (

{t("bulkUpload.validating")}

)} {/* Uploading Progress */} {currentStep === "uploading" && (

{t("bulkUpload.uploading")}

{t("bulkUpload.uploadingWait")}

{t("bulkUpload.uploadProgress")} {uploadProgress}%
{fileProgresses.length > 0 && (
)}
)} {/* Completion Screen */} {currentStep === "complete" && uploadResult && (

{t("bulkUpload.uploadComplete")}

{t("bulkUpload.uploadSuccessMessage", { count: uploadResult.successCount })}

{uploadResult.failCount && uploadResult.failCount > 0 && (

{t("bulkUpload.uploadFailMessage", { count: uploadResult.failCount })}

)}
)}
{/* Footer */} {currentStep !== "uploading" && currentStep !== "complete" && currentStep !== "validation" && ( {currentStep === "files" && ( <> )} )}
{/* Validation Dialog */} { setShowValidationDialog(open); if (!open && currentStep !== "uploading" && currentStep !== "complete") { // If canceled during validation view (and not proceeding to upload), go back to file selection or close? // Usually just close the validation dialog allows user to fix files in the main dialog, // but here the main dialog is in "validation" state which is just a loader. // So we should reset main dialog to "files" step. setCurrentStep("files"); } }} validationResults={validationResults} onConfirmUpload={handleConfirmUpload} isUploading={isUploading} /> ); }