// lib/excelUtils.ts (continued) import ExcelJS from "exceljs"; import { saveAs } from "file-saver"; import { toast } from "sonner"; import { DataTableColumnJSON } from "./form-data-table-columns"; import { updateFormDataInDB } from "@/lib/forms/services"; import { decryptWithServerAction } from "../drm/drmUtils"; // Assuming the previous types are defined above export interface ImportExcelOptions { file: File; tableData: GenericData[]; columnsJSON: DataTableColumnJSON[]; formCode?: string; // Optional - provide to enable direct DB save contractItemId?: number; // Optional - provide to enable direct DB save onPendingChange?: (isPending: boolean) => void; onDataUpdate?: (updater: ((prev: GenericData[]) => GenericData[]) | GenericData[]) => void; } export interface ImportExcelResult { success: boolean; importedCount?: number; error?: any; message?: string; } export interface ExportExcelOptions { tableData: GenericData[]; columnsJSON: DataTableColumnJSON[]; formCode: string; onPendingChange?: (isPending: boolean) => void; } // For typing consistency interface GenericData { [key: string]: any; } export async function importExcelData({ file, tableData, columnsJSON, formCode, contractItemId, onPendingChange, onDataUpdate }: ImportExcelOptions): Promise { if (!file) return { success: false, error: "No file provided" }; try { if (onPendingChange) onPendingChange(true); // Get existing tag numbers const existingTagNumbers = new Set(tableData.map((d) => d.TAG_NO)); const workbook = new ExcelJS.Workbook(); // const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await decryptWithServerAction(file); await workbook.xlsx.load(arrayBuffer); const worksheet = workbook.worksheets[0]; // Parse headers const headerRow = worksheet.getRow(1); const headerRowValues = headerRow.values as ExcelJS.CellValue[]; console.log("Original headers:", headerRowValues); // Create mappings between Excel headers and column definitions const headerToIndexMap = new Map(); for (let i = 1; i < headerRowValues.length; i++) { const headerValue = String(headerRowValues[i] || "").trim(); if (headerValue) { headerToIndexMap.set(headerValue, i); } } // Validate headers let headerErrorMessage = ""; // Check for missing required columns columnsJSON.forEach((col) => { const label = col.label; if (!headerToIndexMap.has(label)) { headerErrorMessage += `Column "${label}" is missing. `; } }); // Check for unexpected columns headerToIndexMap.forEach((index, headerLabel) => { const found = columnsJSON.some((col) => col.label === headerLabel); if (!found) { headerErrorMessage += `Unexpected column "${headerLabel}" found in Excel. `; } }); // Add error column const lastColIndex = worksheet.columnCount + 1; worksheet.getRow(1).getCell(lastColIndex).value = "Error"; // If header validation fails, download error report and exit if (headerErrorMessage) { headerRow.getCell(lastColIndex).value = headerErrorMessage.trim(); const outBuffer = await workbook.xlsx.writeBuffer(); saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`); toast.error(`Header mismatch found. Please check downloaded file.`); return { success: false, error: "Header mismatch" }; } // Create column key to Excel index mapping const keyToIndexMap = new Map(); columnsJSON.forEach((col) => { const index = headerToIndexMap.get(col.label); if (index !== undefined) { keyToIndexMap.set(col.key, index); } }); // Parse and validate data rows const importedData: GenericData[] = []; const lastRowNumber = worksheet.lastRow?.number || 1; let errorCount = 0; // Process each data row for (let rowNum = 2; rowNum <= lastRowNumber; rowNum++) { const row = worksheet.getRow(rowNum); const rowValues = row.values as ExcelJS.CellValue[]; if (!rowValues || rowValues.length <= 1) continue; // Skip empty rows let errorMessage = ""; const rowObj: Record = {}; // Process each column columnsJSON.forEach((col) => { const colIndex = keyToIndexMap.get(col.key); if (colIndex === undefined) return; const cellValue = rowValues[colIndex] ?? ""; let stringVal = String(cellValue).trim(); // Type-specific validation switch (col.type) { case "STRING": if (!stringVal && col.key === "TAG_NO") { errorMessage += `[${col.label}] is empty. `; } rowObj[col.key] = stringVal; break; case "NUMBER": if (stringVal) { const num = parseFloat(stringVal); if (isNaN(num)) { errorMessage += `[${col.label}] '${stringVal}' is not a valid number. `; } else { rowObj[col.key] = num; } } else { rowObj[col.key] = null; } break; case "LIST": if ( stringVal && col.options && !col.options.includes(stringVal) ) { errorMessage += `[${ col.label }] '${stringVal}' not in ${col.options.join(", ")}. `; } rowObj[col.key] = stringVal; break; default: rowObj[col.key] = stringVal; break; } }); // Validate TAG_NO const tagNum = rowObj["TAG_NO"]; if (!tagNum) { errorMessage += `No TAG_NO found. `; } else if (!existingTagNumbers.has(tagNum)) { errorMessage += `TagNumber '${tagNum}' is not in current data. `; } // Record errors or add to valid data if (errorMessage) { row.getCell(lastColIndex).value = errorMessage.trim(); errorCount++; } else { importedData.push(rowObj); } } // If there are validation errors, download error report and exit if (errorCount > 0) { const outBuffer = await workbook.xlsx.writeBuffer(); saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`); toast.error( `There are ${errorCount} error row(s). Please check downloaded file.` ); return { success: false, error: "Data validation errors" }; } // If we reached here, all data is valid // Create locally merged data for UI update const mergedData = [...tableData]; const dataMap = new Map(); // Map existing data by TAG_NO mergedData.forEach(item => { if (item.TAG_NO) { dataMap.set(item.TAG_NO, item); } }); // Update with imported data importedData.forEach(item => { if (item.TAG_NO) { const existingItem = dataMap.get(item.TAG_NO); if (existingItem) { // Update existing item with imported values Object.assign(existingItem, item); } } }); // If formCode and contractItemId are provided, save directly to DB if (formCode && contractItemId) { try { // Process each imported row individually let successCount = 0; let errorCount = 0; const errors = []; // Since updateFormDataInDB expects a single row at a time, // we need to process each imported row individually for (const importedRow of importedData) { try { const result = await updateFormDataInDB( formCode, contractItemId, importedRow ); if (result.success) { successCount++; } else { errorCount++; errors.push(`Error updating tag ${importedRow.TAG_NO}: ${result.message}`); } } catch (rowError) { errorCount++; errors.push(`Exception updating tag ${importedRow.TAG_NO}: ${rowError instanceof Error ? rowError.message : 'Unknown error'}`); } } // If any errors occurred if (errorCount > 0) { console.error("Errors during import:", errors); if (successCount > 0) { toast.warning(`Partially successful: ${successCount} rows updated, ${errorCount} errors`); } else { toast.error(`Failed to update all ${errorCount} rows`); } // If some rows were updated successfully, update the local state if (successCount > 0) { if (onDataUpdate) { onDataUpdate(() => mergedData); } return { success: true, importedCount: successCount, message: `Partially successful: ${successCount} rows updated, ${errorCount} errors` }; } else { return { success: false, error: "All updates failed", message: errors.join("\n") }; } } // All rows were updated successfully if (onDataUpdate) { onDataUpdate(() => mergedData); } toast.success(`Successfully updated ${successCount} rows`); return { success: true, importedCount: successCount, message: "All data imported and saved to database" }; } catch (saveError) { console.error("Failed to save imported data:", saveError); toast.error("Failed to save imported data to database"); return { success: false, error: saveError }; } } else { // Fall back to just updating local state if DB parameters aren't provided if (onDataUpdate) { onDataUpdate(() => mergedData); } toast.success(`Imported ${importedData.length} rows successfully (local only)`); return { success: true, importedCount: importedData.length }; } } catch (err) { console.error("Excel import error:", err); toast.error("Excel import failed."); return { success: false, error: err }; } finally { if (onPendingChange) onPendingChange(false); } }