diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-02 02:27:28 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-02 02:27:28 +0000 |
| commit | 37611339fea096e47aaa42311a13a6313b4200db (patch) | |
| tree | dd9c7dba27a3db2aebd18bf2087c6a30987aa957 /components/form-data | |
| parent | bac0228d21b7195065e9cddcc327ae33659c7bcc (diff) | |
(대표님) 20250602 오전 작업사항 (코드프리징)
Diffstat (limited to 'components/form-data')
| -rw-r--r-- | components/form-data/export-excel-form.tsx | 321 | ||||
| -rw-r--r-- | components/form-data/form-data-table-columns.tsx | 47 | ||||
| -rw-r--r-- | components/form-data/import-excel-form.tsx | 284 | ||||
| -rw-r--r-- | components/form-data/sedp-compare-dialog.tsx | 2 |
4 files changed, 583 insertions, 71 deletions
diff --git a/components/form-data/export-excel-form.tsx b/components/form-data/export-excel-form.tsx index d0ccf980..07d3c447 100644 --- a/components/form-data/export-excel-form.tsx +++ b/components/form-data/export-excel-form.tsx @@ -13,6 +13,7 @@ export interface DataTableColumnJSON { type: ColumnType; options?: string[]; shi?: boolean; // SHI-only field indicator + required?: boolean; // Required field indicator // Add any other properties that might be in columnsJSON } @@ -22,18 +23,249 @@ export interface GenericData { TAG_NO?: string; // Since TAG_NO seems important in the code } +// Define error structure +export interface DataError { + tagNo: string; + rowIndex: number; + columnKey: string; + columnLabel: string; + errorType: string; + errorMessage: string; + currentValue?: any; + expectedFormat?: string; +} + // Define the options interface for the export function export interface ExportExcelOptions { tableData: GenericData[]; columnsJSON: DataTableColumnJSON[]; formCode: string; onPendingChange?: (isPending: boolean) => void; + validateData?: boolean; // Option to enable/disable data validation } // Define the return type export interface ExportExcelResult { success: boolean; error?: any; + errorCount?: number; + hasErrors?: boolean; +} + +/** + * Validate data and collect errors + */ +function validateTableData( + tableData: GenericData[], + columnsJSON: DataTableColumnJSON[] +): DataError[] { + const errors: DataError[] = []; + const tagNoSet = new Set<string>(); + + tableData.forEach((rowData, index) => { + const rowIndex = index + 2; // Excel row number (header is row 1) + const tagNo = rowData.TAG_NO || `Row-${rowIndex}`; + + // Check for duplicate TAG_NO + if (rowData.TAG_NO) { + if (tagNoSet.has(rowData.TAG_NO)) { + errors.push({ + tagNo, + rowIndex, + columnKey: "TAG_NO", + columnLabel: "TAG NO", + errorType: "DUPLICATE", + errorMessage: "Duplicate TAG_NO found", + currentValue: rowData.TAG_NO, + }); + } else { + tagNoSet.add(rowData.TAG_NO); + } + } + + // Validate each column + columnsJSON.forEach((column) => { + const value = rowData[column.key]; + const isEmpty = value === undefined || value === null || value === ""; + + // Required field validation + if (column.required && isEmpty) { + errors.push({ + tagNo, + rowIndex, + columnKey: column.key, + columnLabel: column.label, + errorType: "REQUIRED", + errorMessage: "Required field is empty", + currentValue: value, + }); + } + + if (!isEmpty) { + // Type validation + switch (column.type) { + case "NUMBER": + if (isNaN(Number(value))) { + errors.push({ + tagNo, + rowIndex, + columnKey: column.key, + columnLabel: column.label, + errorType: "TYPE_MISMATCH", + errorMessage: "Value is not a valid number", + currentValue: value, + expectedFormat: "Number", + }); + } + break; + + case "LIST": + if (column.options && !column.options.includes(String(value))) { + errors.push({ + tagNo, + rowIndex, + columnKey: column.key, + columnLabel: column.label, + errorType: "INVALID_OPTION", + errorMessage: "Value is not in the allowed options list", + currentValue: value, + expectedFormat: column.options.join(", "), + }); + } + break; + + case "STRING": + // Additional string validations can be added here + if (typeof value !== "string" && typeof value !== "number") { + errors.push({ + tagNo, + rowIndex, + columnKey: column.key, + columnLabel: column.label, + errorType: "TYPE_MISMATCH", + errorMessage: "Value is not a valid string", + currentValue: value, + expectedFormat: "String", + }); + } + break; + } + } + }); + }); + + return errors; +} + +/** + * Create error sheet with validation results + */ +function createErrorSheet(workbook: ExcelJS.Workbook, errors: DataError[]) { + const errorSheet = workbook.addWorksheet("Errors"); + + // Error sheet headers + const errorHeaders = [ + "TAG NO", + "Row Number", + "Column", + "Error Type", + "Error Message", + "Current Value", + "Expected Format", + ]; + + errorSheet.addRow(errorHeaders); + + // Style error sheet header + const errorHeaderRow = errorSheet.getRow(1); + errorHeaderRow.font = { bold: true, color: { argb: "FFFFFFFF" } }; + errorHeaderRow.alignment = { horizontal: "center" }; + + errorHeaderRow.eachCell((cell) => { + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFDC143C" }, // Crimson background + }; + }); + + // Add error data + errors.forEach((error) => { + const errorRow = errorSheet.addRow([ + error.tagNo, + error.rowIndex, + error.columnLabel, + error.errorType, + error.errorMessage, + error.currentValue || "", + error.expectedFormat || "", + ]); + + // Color code by error type + errorRow.eachCell((cell, colNumber) => { + let bgColor = "FFFFFFFF"; // Default white + + switch (error.errorType) { + case "REQUIRED": + bgColor = "FFFFCCCC"; // Light red + break; + case "TYPE_MISMATCH": + bgColor = "FFFFEECC"; // Light orange + break; + case "INVALID_OPTION": + bgColor = "FFFFFFE0"; // Light yellow + break; + case "DUPLICATE": + bgColor = "FFFFE0E0"; // Very light red + break; + } + + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: bgColor }, + }; + }); + }); + + // Auto-fit columns + errorSheet.columns.forEach((column) => { + let maxLength = 0; + column.eachCell({ includeEmpty: false }, (cell) => { + const columnLength = String(cell.value).length; + if (columnLength > maxLength) { + maxLength = columnLength; + } + }); + column.width = Math.min(Math.max(maxLength + 2, 10), 50); + }); + + // Add summary at the top + errorSheet.insertRow(1, [`Total Errors Found: ${errors.length}`]); + const summaryRow = errorSheet.getRow(1); + summaryRow.font = { bold: true, size: 14 }; + if (errors.length > 0) { + summaryRow.getCell(1).fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFFFC0C0" }, // Light red background + }; + } + + // Adjust header row number + const newHeaderRow = errorSheet.getRow(2); + newHeaderRow.font = { bold: true, color: { argb: "FFFFFFFF" } }; + newHeaderRow.alignment = { horizontal: "center" }; + + newHeaderRow.eachCell((cell) => { + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFDC143C" }, + }; + }); + + return errorSheet; } /** @@ -45,11 +277,15 @@ export async function exportExcelData({ tableData, columnsJSON, formCode, - onPendingChange + onPendingChange, + validateData = true }: ExportExcelOptions): Promise<ExportExcelResult> { try { if (onPendingChange) onPendingChange(true); + // Validate data first if validation is enabled + const errors = validateData ? validateTableData(tableData, columnsJSON) : []; + // Create a new workbook const workbook = new ExcelJS.Workbook(); @@ -92,7 +328,13 @@ export async function exportExcelData({ }); // 2. 데이터 시트에 헤더 추가 - const headers = columnsJSON.map((col) => col.label); + const headers = columnsJSON.map((col) => { + let headerLabel = col.label; + if (col.required) { + headerLabel += " *"; // Required fields marked with asterisk + } + return headerLabel; + }); worksheet.addRow(headers); // 헤더 스타일 적용 @@ -106,13 +348,21 @@ export async function exportExcelData({ const column = columnsJSON[columnIndex]; if (column?.shi === true) { - // SHI-only 필드는 더 진한 음영으로 표시 (헤더 라벨은 원본 유지) + // SHI-only 필드는 더 진한 음영으로 표시 cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFFF9999" }, // 연한 빨간색 배경 }; cell.font = { bold: true, color: { argb: "FF800000" } }; // 진한 빨간색 글자 + } else if (column?.required) { + // Required 필드는 파란색 배경 + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFCCE5FF" }, // 연한 파란색 배경 + }; + cell.font = { bold: true, color: { argb: "FF000080" } }; // 진한 파란색 글자 } else { // 일반 필드는 기존 스타일 cell.fill = { @@ -131,20 +381,34 @@ export async function exportExcelData({ }); const dataRow = worksheet.addRow(rowValues); + // Get errors for this row + const rowErrors = errors.filter(err => err.rowIndex === rowIndex + 2); + const hasErrors = rowErrors.length > 0; + // SHI-only 컬럼의 데이터 셀에도 음영 적용 dataRow.eachCell((cell, colNumber) => { const columnIndex = colNumber - 1; const column = columnsJSON[columnIndex]; + // Check if this cell has errors + const cellHasError = rowErrors.some(err => err.columnKey === column.key); + if (column?.shi === true) { // SHI-only 필드의 데이터 셀에 연한 음영 적용 cell.fill = { type: "pattern", pattern: "solid", - fgColor: { argb: "FFFFCCCC" }, // 매우 연한 빨간색 배경 + fgColor: { argb: cellHasError ? "FFFF6666" : "FFFFCCCC" }, // 에러가 있으면 더 진한 빨간색 }; - // 읽기 전용임을 나타내기 위해 이탤릭 적용 cell.font = { italic: true, color: { argb: "FF666666" } }; + } else if (cellHasError) { + // 에러가 있는 셀은 연한 빨간색 배경 + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFFFDDDD" }, + }; + cell.font = { color: { argb: "FFCC0000" } }; } }); }); @@ -162,7 +426,7 @@ export async function exportExcelData({ // 유효성 검사 정의 const validation = { type: "list" as const, - allowBlank: true, + allowBlank: !col.required, formulae: [validationRange], showErrorMessage: true, errorStyle: "warning" as const, @@ -227,15 +491,28 @@ export async function exportExcelData({ column.width = Math.min(Math.max(maxLength + 2, 10), 50); }); - // 6. 범례 추가 (별도 시트) + // 6. 에러 시트 생성 (에러가 있을 경우에만) + if (errors.length > 0) { + createErrorSheet(workbook, errors); + } + + // 7. 범례 추가 (별도 시트) const legendSheet = workbook.addWorksheet("Legend"); legendSheet.addRow(["Excel Template Legend"]); legendSheet.addRow([]); legendSheet.addRow(["Symbol", "Description"]); legendSheet.addRow(["Red background header", "SHI-only fields that cannot be edited"]); - legendSheet.addRow(["Gray background header", "Regular editable fields"]); - legendSheet.addRow(["Light red background cells", "Data in SHI-only fields (read-only)"]); - legendSheet.addRow(["Red text color", "SHI-only field headers"]); + legendSheet.addRow(["Blue background header", "Required fields (marked with *)"]); + legendSheet.addRow(["Gray background header", "Regular optional fields"]); + legendSheet.addRow(["Light red background cells", "Cells with validation errors"]); + legendSheet.addRow(["Light red data cells", "Data in SHI-only fields (read-only)"]); + + if (errors.length > 0) { + legendSheet.addRow([]); + legendSheet.addRow([`Note: ${errors.length} validation errors found in the 'Errors' sheet`]); + const errorNoteRow = legendSheet.getRow(legendSheet.rowCount); + errorNoteRow.font = { bold: true, color: { argb: "FFCC0000" } }; + } // 범례 스타일 적용 const legendHeaderRow = legendSheet.getRow(1); @@ -251,15 +528,25 @@ export async function exportExcelData({ }; }); - // 7. 파일 다운로드 + // 8. 파일 다운로드 const buffer = await workbook.xlsx.writeBuffer(); - saveAs( - new Blob([buffer]), - `${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx` - ); + const fileName = errors.length > 0 + ? `${formCode}_data_with_errors_${new Date().toISOString().slice(0, 10)}.xlsx` + : `${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx`; + + saveAs(new Blob([buffer]), fileName); - toast.success("Excel 내보내기 완료!"); - return { success: true }; + const message = errors.length > 0 + ? `Excel 내보내기 완료! (${errors.length}개의 검증 오류 발견)` + : "Excel 내보내기 완료!"; + + toast.success(message); + + return { + success: true, + errorCount: errors.length, + hasErrors: errors.length > 0 + }; } catch (err) { console.error("Excel export error:", err); toast.error("Excel 내보내기 실패."); diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx index bba2a208..3749fe02 100644 --- a/components/form-data/form-data-table-columns.tsx +++ b/components/form-data/form-data-table-columns.tsx @@ -2,6 +2,7 @@ import type { ColumnDef, Row } from "@tanstack/react-table"; import { ClientDataTableColumnHeaderSimple } from "../client-data-table/data-table-column-simple-header"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; // Badge import 추가 import { Ellipsis } from "lucide-react"; import { formatDate } from "@/lib/utils"; import { @@ -61,6 +62,31 @@ interface GetColumnsProps<TData> { } /** + * status 값에 따라 Badge variant를 결정하는 헬퍼 함수 + */ +function getStatusBadgeVariant(status: string): "default" | "secondary" | "destructive" | "outline" { + const statusStr = String(status).toLowerCase(); + + switch (statusStr) { + case 'NEW': + case 'New': + // case 'approved': + return 'default'; // 초록색 계열 + case 'Updated or Modified': + // case 'in progress': + // case 'processing': + return 'secondary'; // 노란색 계열 + case 'inactive': + case 'rejected': + case 'failed': + case 'cancelled': + return 'destructive'; // 빨간색 계열 + default: + return 'outline'; // 기본 회색 계열 + } +} + +/** * getColumns 함수 * 1) columnsJSON 배열을 순회하면서 accessorKey / header / cell 등을 설정 * 2) 체크박스 컬럼 추가 (showBatchSelection이 true일 때) @@ -122,8 +148,7 @@ export function getColumns<TData extends object>({ ), enableSorting: false, enableHiding: false, - enablePinning: true, // ← 이 줄 추가 - + enablePinning: true, size: 40, }; columns.push(selectColumn); @@ -160,6 +185,24 @@ export function getColumns<TData extends object>({ // 툴팁 메시지 설정 (SHI 필드만) const tooltipMessage = isReadOnly ? "SHI 전용 필드입니다" : ""; + // status 컬럼인 경우 Badge 적용 + if (col.key === "status") { + const statusValue = String(cellValue ?? ""); + const badgeVariant = getStatusBadgeVariant(statusValue); + + return ( + <div + className={readOnlyClass} + style={cellStyle} + title={tooltipMessage} + > + <Badge variant={badgeVariant}> + {statusValue} + </Badge> + </div> + ); + } + // 데이터 타입별 처리 switch (col.type) { case "NUMBER": diff --git a/components/form-data/import-excel-form.tsx b/components/form-data/import-excel-form.tsx index 6f0828b0..82c7afc8 100644 --- a/components/form-data/import-excel-form.tsx +++ b/components/form-data/import-excel-form.tsx @@ -5,6 +5,18 @@ import { DataTableColumnJSON } from "./form-data-table-columns"; import { updateFormDataInDB } from "@/lib/forms/services"; import { decryptWithServerAction } from "../drm/drmUtils"; +// Define error structure for import +export interface ImportError { + tagNo: string; + rowIndex: number; + columnKey: string; + columnLabel: string; + errorType: string; + errorMessage: string; + currentValue?: any; + expectedFormat?: string; +} + // Simplified options interface without editableFieldsMap export interface ImportExcelOptions { file: File; @@ -22,6 +34,8 @@ export interface ImportExcelResult { error?: any; message?: string; skippedFields?: { tagNo: string, fields: string[] }[]; // 건너뛴 필드 정보 + errorCount?: number; + hasErrors?: boolean; } export interface ExportExcelOptions { @@ -35,6 +49,133 @@ interface GenericData { [key: string]: any; } +/** + * Create error sheet with import validation results + */ +function createImportErrorSheet(workbook: ExcelJS.Workbook, errors: ImportError[], headerErrors?: string[]) { + const errorSheet = workbook.addWorksheet("Import_Errors"); + + // Add header error section if exists + if (headerErrors && headerErrors.length > 0) { + errorSheet.addRow(["HEADER VALIDATION ERRORS"]); + const headerErrorTitleRow = errorSheet.getRow(1); + headerErrorTitleRow.font = { bold: true, size: 14, color: { argb: "FFFFFFFF" } }; + headerErrorTitleRow.getCell(1).fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFDC143C" }, + }; + + headerErrors.forEach((error, index) => { + const errorRow = errorSheet.addRow([`${index + 1}. ${error}`]); + errorRow.getCell(1).fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFFFCCCC" }, + }; + }); + + errorSheet.addRow([]); // Empty row for separation + } + + // Data validation errors section + const startRow = errorSheet.rowCount + 1; + + // Summary row + errorSheet.addRow([`DATA VALIDATION ERRORS: ${errors.length} errors found`]); + const summaryRow = errorSheet.getRow(startRow); + summaryRow.font = { bold: true, size: 12 }; + if (errors.length > 0) { + summaryRow.getCell(1).fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFFFC0C0" }, + }; + } + + if (errors.length > 0) { + // Error data headers + const errorHeaders = [ + "TAG NO", + "Row Number", + "Column", + "Error Type", + "Error Message", + "Current Value", + "Expected Format", + ]; + + errorSheet.addRow(errorHeaders); + const headerRow = errorSheet.getRow(errorSheet.rowCount); + headerRow.font = { bold: true, color: { argb: "FFFFFFFF" } }; + headerRow.alignment = { horizontal: "center" }; + + headerRow.eachCell((cell) => { + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFDC143C" }, + }; + }); + + // Add error data + errors.forEach((error) => { + const errorRow = errorSheet.addRow([ + error.tagNo, + error.rowIndex, + error.columnLabel, + error.errorType, + error.errorMessage, + error.currentValue || "", + error.expectedFormat || "", + ]); + + // Color code by error type + errorRow.eachCell((cell) => { + let bgColor = "FFFFFFFF"; // Default white + + switch (error.errorType) { + case "MISSING_TAG_NO": + bgColor = "FFFFCCCC"; // Light red + break; + case "TAG_NOT_FOUND": + bgColor = "FFFFDDDD"; // Very light red + break; + case "TYPE_MISMATCH": + bgColor = "FFFFEECC"; // Light orange + break; + case "INVALID_OPTION": + bgColor = "FFFFFFE0"; // Light yellow + break; + case "HEADER_MISMATCH": + bgColor = "FFFFE0E0"; // Very light red + break; + } + + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: bgColor }, + }; + }); + }); + } + + // Auto-fit columns + errorSheet.columns.forEach((column) => { + let maxLength = 0; + column.eachCell({ includeEmpty: false }, (cell) => { + const columnLength = String(cell.value).length; + if (columnLength > maxLength) { + maxLength = columnLength; + } + }); + column.width = Math.min(Math.max(maxLength + 2, 10), 50); + }); + + return errorSheet; +} + export async function importExcelData({ file, tableData, @@ -80,13 +221,13 @@ export async function importExcelData({ } // Validate headers - let headerErrorMessage = ""; + const headerErrors: string[] = []; // Check for missing required columns columnsJSON.forEach((col) => { const label = col.label; if (!headerToIndexMap.has(label)) { - headerErrorMessage += `Column "${label}" is missing. `; + headerErrors.push(`Column "${label}" is missing from Excel file`); } }); @@ -94,23 +235,24 @@ export async function importExcelData({ headerToIndexMap.forEach((index, headerLabel) => { const found = columnsJSON.some((col) => col.label === headerLabel); if (!found) { - headerErrorMessage += `Unexpected column "${headerLabel}" found in Excel. `; + headerErrors.push(`Unexpected column "${headerLabel}" found in Excel file`); } }); - // 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(); + // If header validation fails, create error report and exit + if (headerErrors.length > 0) { + createImportErrorSheet(workbook, [], headerErrors); const outBuffer = await workbook.xlsx.writeBuffer(); - saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`); + saveAs(new Blob([outBuffer]), `import-error-report_${Date.now()}.xlsx`); - toast.error(`Header mismatch found. Please check downloaded file.`); - return { success: false, error: "Header mismatch" }; + toast.error(`Header validation failed. ${headerErrors.length} errors found. Check downloaded error report.`); + return { + success: false, + error: "Header validation errors", + errorCount: headerErrors.length, + hasErrors: true + }; } // Create column key to Excel index mapping @@ -124,8 +266,8 @@ export async function importExcelData({ // Parse and validate data rows const importedData: GenericData[] = []; + const validationErrors: ImportError[] = []; const lastRowNumber = worksheet.lastRow?.number || 1; - let errorCount = 0; const skippedFieldsLog: { tagNo: string, fields: string[] }[] = []; // 건너뛴 필드 로그 // Process each data row @@ -134,16 +276,40 @@ export async function importExcelData({ const rowValues = row.values as ExcelJS.CellValue[]; if (!rowValues || rowValues.length <= 1) continue; // Skip empty rows - let errorMessage = ""; - let warningMessage = ""; const rowObj: Record<string, any> = {}; const skippedFields: string[] = []; // 현재 행에서 건너뛴 필드들 + let hasErrors = false; // Get the TAG_NO first to identify existing data const tagNoColIndex = keyToIndexMap.get("TAG_NO"); const tagNo = tagNoColIndex ? String(rowValues[tagNoColIndex] ?? "").trim() : ""; const existingRowData = existingDataMap.get(tagNo); + // Validate TAG_NO first + if (!tagNo) { + validationErrors.push({ + tagNo: `Row-${rowNum}`, + rowIndex: rowNum, + columnKey: "TAG_NO", + columnLabel: "TAG NO", + errorType: "MISSING_TAG_NO", + errorMessage: "TAG_NO is empty or missing", + currentValue: tagNo, + }); + hasErrors = true; + } else if (!existingTagNumbers.has(tagNo)) { + validationErrors.push({ + tagNo: tagNo, + rowIndex: rowNum, + columnKey: "TAG_NO", + columnLabel: "TAG NO", + errorType: "TAG_NOT_FOUND", + errorMessage: "TAG_NO not found in current data", + currentValue: tagNo, + }); + hasErrors = true; + } + // Process each column columnsJSON.forEach((col) => { const colIndex = keyToIndexMap.get(col.key); @@ -179,9 +345,6 @@ export async function importExcelData({ // Type-specific validation switch (col.type) { case "STRING": - if (!stringVal && col.key === "TAG_NO") { - errorMessage += `[${col.label}] is empty. `; - } rowObj[col.key] = stringVal; break; @@ -189,7 +352,17 @@ export async function importExcelData({ if (stringVal) { const num = parseFloat(stringVal); if (isNaN(num)) { - errorMessage += `[${col.label}] '${stringVal}' is not a valid number. `; + validationErrors.push({ + tagNo: tagNo || `Row-${rowNum}`, + rowIndex: rowNum, + columnKey: col.key, + columnLabel: col.label, + errorType: "TYPE_MISMATCH", + errorMessage: "Value is not a valid number", + currentValue: stringVal, + expectedFormat: "Number", + }); + hasErrors = true; } else { rowObj[col.key] = num; } @@ -199,14 +372,18 @@ export async function importExcelData({ break; case "LIST": - if ( - stringVal && - col.options && - !col.options.includes(stringVal) - ) { - errorMessage += `[${ - col.label - }] '${stringVal}' not in ${col.options.join(", ")}. `; + if (stringVal && col.options && !col.options.includes(stringVal)) { + validationErrors.push({ + tagNo: tagNo || `Row-${rowNum}`, + rowIndex: rowNum, + columnKey: col.key, + columnLabel: col.label, + errorType: "INVALID_OPTION", + errorMessage: "Value is not in the allowed options list", + currentValue: stringVal, + expectedFormat: col.options.join(", "), + }); + hasErrors = true; } rowObj[col.key] = stringVal; break; @@ -223,26 +400,10 @@ export async function importExcelData({ tagNo: tagNo, fields: skippedFields }); - warningMessage += `Skipped ${skippedFields.length} SHI-only fields. `; } - // 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 { - // Add warning message to Excel if there are skipped fields - if (warningMessage) { - row.getCell(lastColIndex).value = `WARNING: ${warningMessage.trim()}`; - } + // Add to valid data only if no errors + if (!hasErrors) { importedData.push(rowObj); } } @@ -256,16 +417,22 @@ export async function importExcelData({ ); } - // If there are validation errors, download error report and exit - if (errorCount > 0) { + // If there are validation errors, create error report and exit + if (validationErrors.length > 0) { + createImportErrorSheet(workbook, validationErrors); + const outBuffer = await workbook.xlsx.writeBuffer(); - saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`); + saveAs(new Blob([outBuffer]), `import-error-report_${Date.now()}.xlsx`); + toast.error( - `There are ${errorCount} error row(s). Please check downloaded file.` + `Data validation failed. ${validationErrors.length} errors found across ${new Set(validationErrors.map(e => e.tagNo)).size} TAG(s). Check downloaded error report.` ); + return { success: false, error: "Data validation errors", + errorCount: validationErrors.length, + hasErrors: true, skippedFields: skippedFieldsLog }; } @@ -342,6 +509,8 @@ export async function importExcelData({ success: true, importedCount: successCount, message: `Partially successful: ${successCount} rows updated, ${errorCount} errors`, + errorCount: errorCount, + hasErrors: errorCount > 0, skippedFields: skippedFieldsLog }; } else { @@ -349,6 +518,8 @@ export async function importExcelData({ success: false, error: "All updates failed", message: errors.join("\n"), + errorCount: errorCount, + hasErrors: true, skippedFields: skippedFieldsLog }; } @@ -368,6 +539,8 @@ export async function importExcelData({ success: true, importedCount: successCount, message: "All data imported and saved to database", + errorCount: 0, + hasErrors: false, skippedFields: skippedFieldsLog }; } catch (saveError) { @@ -376,6 +549,8 @@ export async function importExcelData({ return { success: false, error: saveError, + errorCount: 1, + hasErrors: true, skippedFields: skippedFieldsLog }; } @@ -393,6 +568,8 @@ export async function importExcelData({ return { success: true, importedCount: importedData.length, + errorCount: 0, + hasErrors: false, skippedFields: skippedFieldsLog }; } @@ -400,7 +577,12 @@ export async function importExcelData({ } catch (err) { console.error("Excel import error:", err); toast.error("Excel import failed."); - return { success: false, error: err }; + return { + success: false, + error: err, + errorCount: 1, + hasErrors: true + }; } finally { if (onPendingChange) onPendingChange(false); } diff --git a/components/form-data/sedp-compare-dialog.tsx b/components/form-data/sedp-compare-dialog.tsx index 3107193a..647f2810 100644 --- a/components/form-data/sedp-compare-dialog.tsx +++ b/components/form-data/sedp-compare-dialog.tsx @@ -291,7 +291,7 @@ export function SEDPCompareDialog({ // Compare attributes const attributeComparisons = columnsJSON - .filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC") + .filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC"&& col.key !== "status") .map(col => { const localValue = localItem[col.key]; const sedpValue = sedpItem.attributes.get(col.key); |
