"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, 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 dragCounter = React.useRef(0); 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([]); dragCounter.current = 0; } }, [open]); // File Selection Handler const handleFilesChange = (files: File[]) => { if (files.length === 0) return; const existingNames = new Set(selectedFiles.map((f) => f.name)); const uniqueFiles: File[] = []; const duplicates: string[] = []; const newNames = new Set(); // To check duplicates within the new batch files.forEach((file) => { if (existingNames.has(file.name) || newNames.has(file.name)) { duplicates.push(file.name); } else { newNames.add(file.name); uniqueFiles.push(file); } }); if (duplicates.length > 0) { toast.error(t("bulkUpload.duplicateFileError")); } if (uniqueFiles.length > 0) { setSelectedFiles((prev) => [...prev, ...uniqueFiles]); toast.success(t("bulkUpload.filesSelectedSuccess", { count: uniqueFiles.length })); } }; // Drag & Drop Handlers const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (currentStep !== "files") return; dragCounter.current++; if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { setIsDragging(true); } }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (currentStep !== "files") return; dragCounter.current--; if (dragCounter.current === 0) { setIsDragging(false); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (currentStep !== "files") return; e.dataTransfer.dropEffect = "copy"; }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (currentStep !== "files") return; setIsDragging(false); dragCounter.current = 0; 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 (Format check only) const parseResults: FileValidationResult[] = selectedFiles.map((file) => { const validation = validateB4FileName(file.name); return { file, valid: validation.valid, parsed: validation.parsed, error: validation.error, }; }); // Check for duplicates ignoring extension const nameMap = new Map(); // nameWithoutExt -> count parseResults.forEach(r => { const nameWithoutExt = r.file.name.substring(0, r.file.name.lastIndexOf('.')); nameMap.set(nameWithoutExt, (nameMap.get(nameWithoutExt) || 0) + 1); }); const duplicateNames = new Set(); nameMap.forEach((count, name) => { if (count > 1) duplicateNames.add(name); }); // 2. Call MatchBatchFileDwg to check mapping status for ALL files // Even if local parsing failed, we send the filename to the server const mappingCheckItems = parseResults.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) // Use FileNm (without extension) to match response results const responseMap = new Map(); if (mappingResults && Array.isArray(mappingResults)) { mappingResults.forEach((res) => { if (res && res.FileNm) { responseMap.set(res.FileNm, res); } }); } const newMappingResultsMap = new Map(); parseResults.forEach((parseResult) => { // Look up by file name without extension const nameWithoutExt = parseResult.file.name.substring(0, parseResult.file.name.lastIndexOf('.')); const result = responseMap.get(nameWithoutExt); if (result) { newMappingResultsMap.set(parseResult.file.name, result); } }); setMappingResultsMap(newMappingResultsMap); // 3. Merge results const finalResults: FileValidationResult[] = parseResults.map((parseResult) => { const nameWithoutExt = parseResult.file.name.substring(0, parseResult.file.name.lastIndexOf('.')); // Priority 1: Check for duplicates first (must be invalid regardless of server response) if (duplicateNames.has(nameWithoutExt)) { return { ...parseResult, valid: false, mappingStatus: "not_found" as const, error: "Duplicate file name", }; } // Priority 2: If local parsing failed, keep it as invalid (server response doesn't matter) if (!parseResult.valid || !parseResult.parsed) { // Return the original parse error return { ...parseResult, valid: false, mappingStatus: "not_found" as const, }; } // Priority 3: Check server mapping result for valid local parses only const mappingResult = newMappingResultsMap.get(parseResult.file.name); // If mapping exists and is valid on server if (mappingResult && mappingResult.MappingYN === "Y" && mappingResult.DrawingMoveGbn === "도면입수") { return { file: parseResult.file, valid: true, parsed: { drawingNo: mappingResult.DrawingNo, revNo: mappingResult.RevNo || "", fileName: parseResult.file.name }, mappingStatus: "available" as const, drawingName: mappingResult.DrawingName || undefined, registerGroupId: mappingResult.RegisterGroupId, }; } // Server validation failed - provide specific error messages if (!mappingResult) { return { ...parseResult, valid: false, mappingStatus: "not_found" as const, error: "No matching drawing number found", }; } if (mappingResult.MappingYN !== "Y") { return { ...parseResult, valid: false, mappingStatus: "not_found" as const, error: "Drawing not registered in the project", }; } if (mappingResult.DrawingMoveGbn !== "도면입수") { return { ...parseResult, valid: false, mappingStatus: "not_found" as const, error: "Not GTT Deliverables", }; } // Fallback (should not reach here) return { ...parseResult, valid: false, 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[]) => { // Prevent duplicate calls if (isUploading) { console.warn("[V3 Dialog] Upload already in progress, ignoring duplicate call"); return; } 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. Prepare Metadata for Batch Save (Call ONCE) const allMappingSaveItems: B4MappingSaveItem[] = []; const groupUploadIdMap = new Map(); for (const [groupKey, groupItems] of uploadGroups.entries()) { const firstItemMapping = groupItems[0].mappingData; // Reuse existing UploadId if present in API response, otherwise generate new one const uploadId = firstItemMapping.UploadId || uuidv4(); groupUploadIdMap.set(groupKey, uploadId); const groupMappingItems = groupItems.map((item) => { const m = item.mappingData; return { CGbn: m.CGbn, Category: "TS", // Hardcoded fixed value is required! CheckBox: m.CheckBox, DGbn: m.DGbn, DegreeGbn: m.DegreeGbn, DeptGbn: m.DeptGbn, Discipline: m.Discipline, DrawingKind: m.DrawingKind, DrawingMoveGbn: m.DrawingMoveGbn, DrawingName: m.DrawingName, DrawingNo: m.DrawingNo, DrawingUsage: m.DrawingUsage, FileNm: item.file.name, JGbn: m.JGbn, Manager: m.Manager, MappingYN: m.MappingYN, NewOrNot: m.NewOrNot, ProjectNo: projectNo, RegisterGroup: m.RegisterGroup, RegisterGroupId: m.RegisterGroupId, RegisterKindCode: m.RegisterKindCode, RegisterSerialNo: m.RegisterSerialNo, RevNo: m.RevNo, SGbn: m.SGbn, UploadId: uploadId, status: "Standby", // Hardcoded fixed value is required! }; }); allMappingSaveItems.push(...groupMappingItems); } // Call API Once try { console.log(`[V3 Dialog] Saving metadata batch for ${allMappingSaveItems.length} items`); await saveB4MappingBatch(allMappingSaveItems, { userId, userName, vendorCode, email: userEmail, }); } catch (error) { console.error("[V3 Dialog] Metadata save failed:", error); throw new Error( error instanceof Error ? error.message : t("bulkUpload.uploadError") ); } // 3. Process each group for Physical Upload for (const [groupKey, groupItems] of uploadGroups.entries()) { const uploadId = groupUploadIdMap.get(groupKey)!; 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 ) ); // B. 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"); } 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} /> ); }