瀏覽代碼

WIP
feat(vscode): add debug configurations and settings for Next.js development

- Add comprehensive VS Code launch configurations for Next.js debugging
- Include server-side, client-side, and full-stack debug profiles
- Add TypeScript and ESLint settings for consistent development experience
- Configure auto-formatting and import organization

fix(imports): resolve async params handling in API routes
- Fix Promise-based params destructuring in trigger route handler
- Update import validation to use fileId instead of file relationship

refactor(excel-import): enhance database Excel reader with better error handling
- Improve cell address parsing with row number support
- Add detailed logging for debugging import processes
- Optimize worksheet processing with progress tracking
- Fix date and time value conversion edge cases

style: apply consistent code formatting and lint fixes
- Remove unused variables and imports
- Fix ESLint warnings across multiple files
- Standardize TypeScript configuration

vtugulan 6 月之前
父節點
當前提交
dad53e4369

+ 106 - 0
.vscode/launch.json

@@ -0,0 +1,106 @@
+{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "Next.js: debug server-side",
+      "type": "node",
+      "request": "launch",
+      "program": "${workspaceFolder}/node_modules/next/dist/bin/next",
+      "args": [
+        "dev"
+      ],
+      "env": {
+        "NODE_OPTIONS": "--inspect"
+      },
+      "console": "integratedTerminal",
+      "cwd": "${workspaceFolder}",
+      "skipFiles": [
+        "<node_internals>/**"
+      ]
+    },
+    {
+      "name": "Next.js: Debug Server-Side (Turbopack)",
+      "type": "node",
+      "request": "launch",
+      "program": "${workspaceFolder}/node_modules/next/dist/bin/next",
+      "args": [
+        "dev",
+        "--turbopack"
+      ],
+      "env": {
+        "NODE_OPTIONS": "--inspect",
+        "TURBOPACK": "1"
+      },
+      "sourceMaps": true,
+      "resolveSourceMapLocations": [
+        "${workspaceFolder}/**"
+      ]
+    },
+    {
+      "name": "Next.js: debug client-side",
+      "type": "chrome",
+      "request": "launch",
+      "url": "http://localhost:3000",
+      "webRoot": "${workspaceFolder}"
+    },
+    {
+      "name": "Next.js: debug full stack",
+      "type": "node",
+      "request": "launch",
+      "program": "${workspaceFolder}/node_modules/next/dist/bin/next",
+      "args": [
+        "dev"
+      ],
+      "env": {
+        "NODE_OPTIONS": "--inspect"
+      },
+      "console": "integratedTerminal",
+      "cwd": "${workspaceFolder}",
+      "serverReadyAction": {
+        "action": "debugWithEdge",
+        "killOnServerStop": true,
+        "pattern": "- Local:.+(https?://.+)",
+        "uriFormat": "%s",
+        "webRoot": "${workspaceFolder}"
+      },
+      "skipFiles": [
+        "<node_internals>/**"
+      ]
+    },
+    {
+      "name": "Next.js: debug server-side (npm)",
+      "type": "node",
+      "request": "launch",
+      "runtimeExecutable": "npm",
+      "runtimeArgs": [
+        "run",
+        "dev"
+      ],
+      "env": {
+        "NODE_OPTIONS": "--inspect"
+      },
+      "console": "integratedTerminal",
+      "cwd": "${workspaceFolder}",
+      "skipFiles": [
+        "<node_internals>/**"
+      ]
+    },
+    {
+      "name": "Next.js: debug server-side (pnpm)",
+      "type": "node",
+      "request": "launch",
+      "runtimeExecutable": "pnpm",
+      "runtimeArgs": [
+        "dev"
+      ],
+      "env": {
+        "NODE_OPTIONS": "--inspect"
+      },
+      "console": "integratedTerminal",
+      "cwd": "${workspaceFolder}",
+      "skipFiles": [
+        "<node_internals>/**"
+      ]
+    }
+  ]
+}

+ 17 - 0
.vscode/settings.json

@@ -0,0 +1,17 @@
+{
+  "typescript.preferences.importModuleSpecifier": "relative",
+  "typescript.suggest.autoImports": true,
+  "typescript.updateImportsOnFileMove.enabled": "always",
+  "editor.formatOnSave": true,
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": "explicit"
+  },
+  "debug.javascript.autoAttachFilter": "onlyWithFlag",
+  "debug.javascript.breakOnConditionalError": true,
+  "debug.javascript.defaultRuntimeExecutable": {
+    "pwa-node": "node"
+  },
+  "debug.javascript.terminalOptions": {
+    "shell": "C:\\Windows\\System32\\cmd.exe"
+  }
+}

+ 8 - 8
app/actions/imports.ts

@@ -25,7 +25,7 @@ export async function createImport(data: {
 }) {
   try {
     const validatedData = createImportSchema.parse(data);
-    
+
     const importRecord = await prisma.import.create({
       data: {
         name: validatedData.name,
@@ -57,7 +57,7 @@ export async function getImports() {
         importDate: 'desc',
       },
     });
-    
+
     return { success: true, data: imports };
   } catch (error) {
     console.error('Error fetching imports:', error);
@@ -107,7 +107,7 @@ export async function updateImport(data: {
 }) {
   try {
     const validatedData = updateImportSchema.parse(data);
-    
+
     const importRecord = await prisma.import.update({
       where: { id: validatedData.id },
       data: {
@@ -147,10 +147,10 @@ export async function calculateCintasSummaries(importId: number) {
   try {
     // This would typically call a stored procedure or perform calculations
     // For now, we'll simulate the calculation
-    
+
     // In a real implementation, you might call:
     // await prisma.$executeRaw`CALL cintas_calculate_summary(${importId})`;
-    
+
     // For demo purposes, we'll create some sample data
     const summaries = [
       {
@@ -209,7 +209,7 @@ export async function getLayoutConfigurations() {
         name: 'asc',
       },
     });
-    
+
     return { success: true, data: layouts };
   } catch (error) {
     console.error('Error fetching layout configurations:', error);
@@ -223,14 +223,14 @@ export async function triggerImport(importId: number) {
     // Validate import exists
     const importRecord = await prisma.import.findUnique({
       where: { id: importId },
-      include: { file: true, layout: true }
+      include: { layout: true }
     });
 
     if (!importRecord) {
       return { success: false, error: 'Import not found' };
     }
 
-    if (!importRecord.file) {
+    if (!importRecord.fileId) {
       return { success: false, error: 'No file attached to import' };
     }
 

+ 6 - 4
app/api/imports/[id]/trigger/route.ts

@@ -3,11 +3,13 @@ import { ImportProcessor } from '@/app/lib/excel-import/import-processor';
 
 export async function POST(
   request: NextRequest,
-  { params }: { params: { id: string } }
+  { params }: { params: Promise<{ id: string }> }
 ) {
   try {
-    const importId = parseInt(params.id);
-    
+    // Await the params promise to get the actual params
+    const resolvedParams = await params;
+    const importId = parseInt(resolvedParams.id);
+
     if (isNaN(importId)) {
       return NextResponse.json(
         { success: false, error: 'Invalid import ID' },
@@ -17,7 +19,7 @@ export async function POST(
 
     // Initialize import processor
     const processor = new ImportProcessor();
-    
+
     // Validate import before processing
     const validation = await processor.validateImport(importId);
     if (!validation.valid) {

+ 12 - 11
app/cintas-calendar-summary/page.tsx

@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
 "use client";
 
 import { useState } from 'react';
@@ -42,13 +43,13 @@ export default function CintasCalendarSummaryPage() {
 
   const handleCreateImportRecord = async () => {
     if (!uploadedFile) return;
-    
+
     setIsProcessing(true);
     setError(null);
-    
+
     try {
       const result = await createCintasImportRecord(uploadedFile.id, uploadedFile.filename);
-      
+
       if (result.success) {
         setImportRecord(result.data);
         setCurrentStep(3);
@@ -64,13 +65,13 @@ export default function CintasCalendarSummaryPage() {
 
   const handleProcessImportData = async () => {
     if (!importRecord) return;
-    
+
     setIsProcessing(true);
     setError(null);
-    
+
     try {
       const result = await processCintasImportData(importRecord.id);
-      
+
       if (result.success) {
         setCurrentStep(4);
         // After processing, fetch the summary data
@@ -87,15 +88,15 @@ export default function CintasCalendarSummaryPage() {
 
   const handleGenerateSummary = async () => {
     if (!importRecord) return;
-    
+
     setIsProcessing(true);
     setError(null);
-    
+
     try {
       // This would typically call an API endpoint to run the stored procedure
       // For now, we'll simulate the summary generation
       const response = await fetch(`/api/imports/${importRecord.id}/summary`);
-      
+
       if (response.ok) {
         const data = await response.json();
         setSummaryData(data);
@@ -212,7 +213,7 @@ export default function CintasCalendarSummaryPage() {
             <CardContent>
               <div className="space-y-4">
                 <UploadForm onFileUploaded={handleFileUploaded} />
-                
+
                 {uploadedFile && (
                   <div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
                     <div className="flex items-center space-x-2">
@@ -244,7 +245,7 @@ export default function CintasCalendarSummaryPage() {
                 <p className="text-sm text-muted-foreground">
                   File: {uploadedFile?.filename}
                 </p>
-                <Button 
+                <Button
                   onClick={handleCreateImportRecord}
                   disabled={isProcessing}
                   className="w-full"

+ 5 - 5
app/components/imports/ImportDetailDialog.tsx

@@ -138,7 +138,7 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
 
     setProcessing(true);
     setImportStatus('processing');
-    
+
     try {
       const response = await fetch(`/api/imports/${importId}/trigger`, {
         method: 'POST',
@@ -151,13 +151,13 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
           title: 'Success',
           description: 'Import process started successfully',
         });
-        
+
         // Set up WebSocket connection for progress updates
         const ws = new WebSocket(`ws://localhost:3001/import-progress/${importId}`);
-        
+
         ws.onmessage = (event) => {
           const progress = JSON.parse(event.data);
-          
+
           if (progress.status === 'completed') {
             setImportStatus('completed');
             setProcessing(false);
@@ -197,7 +197,7 @@ export function ImportDetailDialog({ open, onOpenChange, importId }: ImportDetai
         setProcessing(false);
         setImportStatus('failed');
       }
-    } catch (error) {
+    } catch {
       toast({
         title: 'Error',
         description: 'Failed to trigger import process',

+ 85 - 76
app/lib/excel-import/database-excel-reader.ts

@@ -1,4 +1,5 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/no-unused-vars */
 import * as XLSX from 'xlsx';
 import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types';
 import { prisma } from '@/lib/prisma';
@@ -6,16 +7,16 @@ import { prisma } from '@/lib/prisma';
 // Simple logger utility for debugging
 const logger = {
   debug: (message: string, ...args: any[]) => {
-    console.debug(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+    //console.debug(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
   },
   info: (message: string, ...args: any[]) => {
-    console.info(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+    //console.info(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
   },
   warn: (message: string, ...args: any[]) => {
-    console.warn(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+    //console.warn(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
   },
   error: (message: string, ...args: any[]) => {
-    console.error(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+    //console.error(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
   }
 };
 
@@ -31,7 +32,7 @@ export class DatabaseExcelReaderService {
     });
 
     const startTime = Date.now();
-    
+
     try {
       // Fetch file from database
       const fileRecord = await prisma.file.findUnique({
@@ -56,19 +57,19 @@ export class DatabaseExcelReaderService {
 
       logger.debug('Loading Excel workbook from buffer...');
       const workbook = XLSX.read(arrayBuffer, { type: 'array' });
-      
+
       logger.info('Excel workbook loaded successfully from database', {
-        worksheets: workbook.SheetNames.map(name => ({ 
-          name, 
-          rowCount: XLSX.utils.sheet_to_json(workbook.Sheets[name]).length 
+        worksheets: workbook.SheetNames.map(name => ({
+          name,
+          rowCount: XLSX.utils.sheet_to_json(workbook.Sheets[name]).length
         }))
       });
-        
+
       const results: ReadSectionData[] = [];
       const totalSections = layoutConfig.sections?.length || 0;
-       
+
       logger.info('Processing Excel import from database', { totalSections });
-       
+
       // Initialize progress
       onProgress({
         importId: 0,
@@ -89,13 +90,13 @@ export class DatabaseExcelReaderService {
           startingRow: section.startingRow,
           endingRow: section.endingRow
         });
-         
+
         const worksheet = workbook.Sheets[section.sheetName];
-         
+
         if (!worksheet) {
           const error = `Worksheet '${section.sheetName}' not found`;
           logger.warn(error, { availableWorksheets: workbook.SheetNames });
-           
+
           onProgress({
             importId: 0,
             status: 'processing',
@@ -110,14 +111,14 @@ export class DatabaseExcelReaderService {
         }
 
         const sectionData = await this.processSectionFromWorksheet(
-          worksheet, 
-          section, 
-          sectionIndex, 
-          totalSections, 
+          worksheet,
+          section,
+          sectionIndex,
+          totalSections,
           onProgress
         );
         results.push(sectionData);
-         
+
         logger.info(`Section ${section.name} processed successfully`, {
           rowsProcessed: sectionData.data.length,
           fields: sectionData.fields.length
@@ -152,7 +153,7 @@ export class DatabaseExcelReaderService {
     });
 
     const startTime = Date.now();
-    
+
     try {
       // Convert Buffer to ArrayBuffer for xlsx library
       const arrayBuffer = buffer.buffer.slice(
@@ -162,19 +163,19 @@ export class DatabaseExcelReaderService {
 
       logger.debug('Loading Excel workbook from buffer...');
       const workbook = XLSX.read(arrayBuffer, { type: 'array' });
-      
+
       logger.info('Excel workbook loaded successfully from buffer', {
-        worksheets: workbook.SheetNames.map(name => ({ 
-          name, 
-          rowCount: XLSX.utils.sheet_to_json(workbook.Sheets[name]).length 
+        worksheets: workbook.SheetNames.map(name => ({
+          name,
+          rowCount: XLSX.utils.sheet_to_json(workbook.Sheets[name]).length
         }))
       });
-        
+
       const results: ReadSectionData[] = [];
       const totalSections = layoutConfig.sections?.length || 0;
-       
+
       logger.info('Processing Excel import from buffer', { totalSections });
-       
+
       // Initialize progress
       onProgress({
         importId: 0,
@@ -195,13 +196,13 @@ export class DatabaseExcelReaderService {
           startingRow: section.startingRow,
           endingRow: section.endingRow
         });
-         
+
         const worksheet = workbook.Sheets[section.sheetName];
-         
+
         if (!worksheet) {
           const error = `Worksheet '${section.sheetName}' not found`;
           logger.warn(error, { availableWorksheets: workbook.SheetNames });
-           
+
           onProgress({
             importId: 0,
             status: 'processing',
@@ -216,14 +217,14 @@ export class DatabaseExcelReaderService {
         }
 
         const sectionData = await this.processSectionFromWorksheet(
-          worksheet, 
-          section, 
-          sectionIndex, 
-          totalSections, 
+          worksheet,
+          section,
+          sectionIndex,
+          totalSections,
           onProgress
         );
         results.push(sectionData);
-         
+
         logger.info(`Section ${section.name} processed successfully`, {
           rowsProcessed: sectionData.data.length,
           fields: sectionData.fields.length
@@ -264,27 +265,27 @@ export class DatabaseExcelReaderService {
 
     const startingRow = section.startingRow || 2; // Default to 2 to skip header
     const endingRow = section.endingRow || Infinity;
-    
+
     logger.debug('Section configuration', {
       sectionName: section.name,
       startingRow,
       endingRow,
       fieldsCount: section.fields?.length || 0
     });
-    
+
     // Convert worksheet to JSON array
     const worksheetData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
-    
+
     // Process data rows
     const data: Record<string, any>[] = [];
     const totalRows = Math.min(endingRow, worksheetData.length) - startingRow + 1;
-    
+
     let processedRows = 0;
     let skippedRows = 0;
-    
+
     for (let rowNum = startingRow; rowNum <= Math.min(endingRow, worksheetData.length); rowNum++) {
       const row = worksheetData[rowNum - 1]; // Convert to 0-based index
-      
+
       if (!row || row.every(cell => cell === null || cell === undefined || cell === '')) {
         skippedRows++;
         logger.debug(`Skipping empty row ${rowNum}`);
@@ -293,13 +294,13 @@ export class DatabaseExcelReaderService {
 
       const rowData: Record<string, any> = {};
       let fieldsProcessed = 0;
-      
+
       // Map cell values based on field configuration
       for (const field of section.fields || []) {
         try {
-          const cellAddress = this.parseCellAddress(field.cellPosition);
+          const cellAddress = this.parseCellAddress(field.cellPosition, rowNum);
           const cellValue = row[cellAddress.col - 1]; // Convert to 0-based index
-          
+
           logger.debug(`Processing field`, {
             fieldName: field.name,
             cellPosition: field.cellPosition,
@@ -307,20 +308,21 @@ export class DatabaseExcelReaderService {
             rawValue: cellValue,
             rowNum
           });
-          
+
           if (cellValue !== null && cellValue !== undefined && cellValue !== '') {
+            console.log(field.name, field.dataType, field.dataTypeFormat, cellValue, field.parsedType);
             const value = this.convertCellValue(
               cellValue,
               field.parsedType || FieldTypeEnum.String
             );
-            
+
             logger.debug(`Value converted`, {
               fieldName: field.name,
               originalValue: cellValue,
               convertedValue: value,
               fieldType: field.parsedType || FieldTypeEnum.String
             });
-            
+
             // Map to the correct column name for Prisma model
             const columnName = field.importTableColumnName;
             rowData[columnName] = value;
@@ -334,12 +336,12 @@ export class DatabaseExcelReaderService {
           });
         }
       }
-      
+
       // Only add non-empty rows
       if (Object.keys(rowData).length > 0) {
         data.push(rowData);
         processedRows++;
-        
+
         if (processedRows <= 5 || processedRows % 100 === 0) {
           logger.debug(`Row processed`, {
             rowNum,
@@ -351,7 +353,7 @@ export class DatabaseExcelReaderService {
       } else {
         logger.debug(`Skipping row with no valid data`, { rowNum });
       }
-      
+
       // Update progress every 100 rows
       if (rowNum % 100 === 0 || rowNum === Math.min(endingRow, worksheetData.length)) {
         onProgress({
@@ -404,30 +406,35 @@ export class DatabaseExcelReaderService {
     return result;
   }
 
-  private parseCellAddress(cellPosition: string): { row: number; col: number } {
+  private parseCellAddress(cellPosition: string, rowNumber: number): { row: number; col: number } {
     logger.debug(`Parsing cell address: ${cellPosition}`);
-    
-    const match = cellPosition.match(/([A-Z]+)(\d+)/);
+
+    let match = cellPosition.match(/([A-Z]+)(\d+)/);
     if (!match) {
-      logger.warn(`Invalid cell position format: ${cellPosition}, using default 1,1`);
-      return { row: 1, col: 1 };
+      logger.warn(`Invalid cell position format: ${cellPosition}, trying to add row number ${rowNumber}`);
+      const appendedCellPosition = `${cellPosition}${rowNumber}`;
+      match = appendedCellPosition.match(/([A-Z]+)(\d+)/);
+      if (!match) {
+        logger.warn(`Invalid cell position format: ${appendedCellPosition}, using default 1,1`);
+        return { row: 1, col: 1 };
+      }
     }
-    
+
     const col = match[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1;
     const row = parseInt(match[2]);
-    
+
     logger.debug(`Parsed cell address`, {
       original: cellPosition,
       row,
       col
     });
-    
+
     return { row, col };
   }
 
   private mapSectionType(type: string): SectionTypeEnum {
     logger.debug(`Mapping section type: ${type}`);
-    
+
     const mappedType = (() => {
       switch (type?.toLowerCase()) {
         case 'grid':
@@ -438,18 +445,18 @@ export class DatabaseExcelReaderService {
           return SectionTypeEnum.Unknown;
       }
     })();
-    
+
     logger.debug(`Section type mapped`, {
       originalType: type,
       mappedType: SectionTypeEnum[mappedType]
     });
-    
+
     return mappedType;
   }
 
   private mapFields(fields: any[]): LayoutSectionField[] {
     logger.debug(`Mapping ${fields.length} fields`);
-    
+
     const mappedFields = fields.map((field, index) => {
       const mappedField = {
         id: field.id || index,
@@ -461,7 +468,7 @@ export class DatabaseExcelReaderService {
         importColumnOrderNumber: field.importColumnOrderNumber || index,
         parsedType: this.mapFieldType(field.dataType)
       };
-      
+
       logger.debug(`Field mapped`, {
         index,
         originalName: field.name,
@@ -469,16 +476,16 @@ export class DatabaseExcelReaderService {
         cellPosition: mappedField.cellPosition,
         parsedType: FieldTypeEnum[mappedField.parsedType]
       });
-      
+
       return mappedField;
     });
-    
+
     return mappedFields;
   }
 
   private mapFieldType(dataType: string): FieldTypeEnum {
     const type = dataType?.toLowerCase();
-    
+
     const mappedType = (() => {
       switch (type) {
         case 'time':
@@ -497,12 +504,12 @@ export class DatabaseExcelReaderService {
           return FieldTypeEnum.String;
       }
     })();
-    
+
     logger.debug(`Field type mapped`, {
       originalDataType: dataType,
       mappedType: FieldTypeEnum[mappedType]
     });
-    
+
     return mappedType;
   }
 
@@ -511,14 +518,16 @@ export class DatabaseExcelReaderService {
       logger.debug(`Converting null/undefined value to null`, { fieldType: FieldTypeEnum[fieldType] });
       return null;
     }
-    
+
     logger.debug(`Converting cell value`, {
       originalValue: value,
       originalType: typeof value,
       targetFieldType: FieldTypeEnum[fieldType]
     });
-    
+
     const convertedValue = (() => {
+      if (fieldType === FieldTypeEnum.Date)
+        console.log(value, fieldType);
       switch (fieldType) {
         case FieldTypeEnum.Time:
           if (typeof value === 'number') {
@@ -528,12 +537,12 @@ export class DatabaseExcelReaderService {
             return result;
           }
           return value;
-        
+
         case FieldTypeEnum.Decimal:
           const decimalResult = parseFloat(value.toString()) || 0;
           logger.debug(`Decimal conversion`, { original: value, converted: decimalResult });
           return decimalResult;
-        
+
         case FieldTypeEnum.Date:
           if (typeof value === 'number') {
             // Excel date is days since 1900-01-01
@@ -545,12 +554,12 @@ export class DatabaseExcelReaderService {
           const dateResult = new Date(value);
           logger.debug(`Date conversion from string`, { original: value, converted: dateResult });
           return dateResult;
-        
+
         case FieldTypeEnum.Numeric:
           const numericResult = parseInt(value.toString()) || 0;
           logger.debug(`Numeric conversion`, { original: value, converted: numericResult });
           return numericResult;
-        
+
         case FieldTypeEnum.String:
         default:
           const stringResult = value.toString();
@@ -558,7 +567,7 @@ export class DatabaseExcelReaderService {
           return stringResult;
       }
     })();
-    
+
     return convertedValue;
   }
 }

+ 44 - 43
app/lib/excel-import/excel-reader.ts

@@ -1,4 +1,5 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/no-require-imports */
 import * as XLSX from 'xlsx';
 import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types';
 
@@ -31,19 +32,19 @@ export class ExcelReaderService {
     });
 
     const startTime = Date.now();
-    
+
     try {
       logger.debug('Loading Excel workbook from file path...');
       const workbook = XLSX.readFile(filePath);
       logger.info('Excel workbook loaded successfully', {
         worksheets: workbook.SheetNames.map(name => ({ name, rowCount: XLSX.utils.sheet_to_json(workbook.Sheets[name]).length }))
       });
-        
+
       const results: ReadSectionData[] = [];
       const totalSections = layoutConfig.sections?.length || 0;
-      
+
       logger.info('Processing Excel import', { totalSections });
-      
+
       // Initialize progress
       onProgress({
         importId: 0, // Will be set by caller
@@ -64,13 +65,13 @@ export class ExcelReaderService {
           startingRow: section.startingRow,
           endingRow: section.endingRow
         });
-        
+
         const worksheet = workbook.Sheets[section.sheetName];
-        
+
         if (!worksheet) {
           const error = `Worksheet '${section.sheetName}' not found`;
           logger.warn(error, { availableWorksheets: workbook.SheetNames });
-          
+
           onProgress({
             importId: 0,
             status: 'processing',
@@ -86,7 +87,7 @@ export class ExcelReaderService {
 
         const sectionData = await this.processSection(worksheet, section, sectionIndex, totalSections, onProgress);
         results.push(sectionData);
-        
+
         logger.info(`Section ${section.name} processed successfully`, {
           rowsProcessed: sectionData.data.length,
           fields: sectionData.fields.length
@@ -127,27 +128,27 @@ export class ExcelReaderService {
 
     const startingRow = section.startingRow || 2; // Default to 2 to skip header
     const endingRow = section.endingRow || Infinity;
-    
+
     logger.debug('Section configuration', {
       sectionName: section.name,
       startingRow,
       endingRow,
       fieldsCount: section.fields?.length || 0
     });
-    
+
     // Convert worksheet to JSON array
     const worksheetData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
-    
+
     // Process data rows
     const data: Record<string, any>[] = [];
     const totalRows = Math.min(endingRow, worksheetData.length) - startingRow + 1;
-    
+
     let processedRows = 0;
     let skippedRows = 0;
-    
+
     for (let rowNum = startingRow; rowNum <= Math.min(endingRow, worksheetData.length); rowNum++) {
       const row = worksheetData[rowNum - 1]; // Convert to 0-based index
-      
+
       if (!row || row.every(cell => cell === null || cell === undefined || cell === '')) {
         skippedRows++;
         logger.debug(`Skipping empty row ${rowNum}`);
@@ -156,13 +157,13 @@ export class ExcelReaderService {
 
       const rowData: Record<string, any> = {};
       let fieldsProcessed = 0;
-      
+
       // Map cell values based on field configuration
       for (const field of section.fields || []) {
         try {
           const cellAddress = this.parseCellAddress(field.cellPosition);
           const cellValue = row[cellAddress.col - 1]; // Convert to 0-based index
-          
+
           logger.debug(`Processing field`, {
             fieldName: field.name,
             cellPosition: field.cellPosition,
@@ -170,20 +171,20 @@ export class ExcelReaderService {
             rawValue: cellValue,
             rowNum
           });
-          
+
           if (cellValue !== null && cellValue !== undefined && cellValue !== '') {
             const value = this.convertCellValue(
               cellValue,
               field.parsedType || FieldTypeEnum.String
             );
-            
+
             logger.debug(`Value converted`, {
               fieldName: field.name,
               originalValue: cellValue,
               convertedValue: value,
               fieldType: field.parsedType || FieldTypeEnum.String
             });
-            
+
             // Map to the correct column name for Prisma model
             const columnName = field.importTableColumnName;
             rowData[columnName] = value;
@@ -197,12 +198,12 @@ export class ExcelReaderService {
           });
         }
       }
-      
+
       // Only add non-empty rows
       if (Object.keys(rowData).length > 0) {
         data.push(rowData);
         processedRows++;
-        
+
         if (processedRows <= 5 || processedRows % 100 === 0) {
           logger.debug(`Row processed`, {
             rowNum,
@@ -214,7 +215,7 @@ export class ExcelReaderService {
       } else {
         logger.debug(`Skipping row with no valid data`, { rowNum });
       }
-      
+
       // Update progress every 100 rows
       if (rowNum % 100 === 0 || rowNum === Math.min(endingRow, worksheetData.length)) {
         onProgress({
@@ -269,28 +270,28 @@ export class ExcelReaderService {
 
   private parseCellAddress(cellPosition: string): { row: number; col: number } {
     logger.debug(`Parsing cell address: ${cellPosition}`);
-    
+
     const match = cellPosition.match(/([A-Z]+)(\d+)/);
     if (!match) {
       logger.warn(`Invalid cell position format: ${cellPosition}, using default 1,1`);
       return { row: 1, col: 1 };
     }
-    
+
     const col = match[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1;
     const row = parseInt(match[2]);
-    
+
     logger.debug(`Parsed cell address`, {
       original: cellPosition,
       row,
       col
     });
-    
+
     return { row, col };
   }
 
   private mapSectionType(type: string): SectionTypeEnum {
     logger.debug(`Mapping section type: ${type}`);
-    
+
     const mappedType = (() => {
       switch (type?.toLowerCase()) {
         case 'grid':
@@ -301,18 +302,18 @@ export class ExcelReaderService {
           return SectionTypeEnum.Unknown;
       }
     })();
-    
+
     logger.debug(`Section type mapped`, {
       originalType: type,
       mappedType: SectionTypeEnum[mappedType]
     });
-    
+
     return mappedType;
   }
 
   private mapFields(fields: any[]): LayoutSectionField[] {
     logger.debug(`Mapping ${fields.length} fields`);
-    
+
     const mappedFields = fields.map((field, index) => {
       const mappedField = {
         id: field.id || index,
@@ -324,7 +325,7 @@ export class ExcelReaderService {
         importColumnOrderNumber: field.importColumnOrderNumber || index,
         parsedType: this.mapFieldType(field.dataType)
       };
-      
+
       logger.debug(`Field mapped`, {
         index,
         originalName: field.name,
@@ -332,16 +333,16 @@ export class ExcelReaderService {
         cellPosition: mappedField.cellPosition,
         parsedType: FieldTypeEnum[mappedField.parsedType]
       });
-      
+
       return mappedField;
     });
-    
+
     return mappedFields;
   }
 
   private mapFieldType(dataType: string): FieldTypeEnum {
     const type = dataType?.toLowerCase();
-    
+
     const mappedType = (() => {
       switch (type) {
         case 'time':
@@ -360,12 +361,12 @@ export class ExcelReaderService {
           return FieldTypeEnum.String;
       }
     })();
-    
+
     logger.debug(`Field type mapped`, {
       originalDataType: dataType,
       mappedType: FieldTypeEnum[mappedType]
     });
-    
+
     return mappedType;
   }
 
@@ -374,13 +375,13 @@ export class ExcelReaderService {
       logger.debug(`Converting null/undefined value to null`, { fieldType: FieldTypeEnum[fieldType] });
       return null;
     }
-    
+
     logger.debug(`Converting cell value`, {
       originalValue: value,
       originalType: typeof value,
       targetFieldType: FieldTypeEnum[fieldType]
     });
-    
+
     const convertedValue = (() => {
       switch (fieldType) {
         case FieldTypeEnum.Time:
@@ -391,12 +392,12 @@ export class ExcelReaderService {
             return result;
           }
           return value;
-        
+
         case FieldTypeEnum.Decimal:
           const decimalResult = parseFloat(value.toString()) || 0;
           logger.debug(`Decimal conversion`, { original: value, converted: decimalResult });
           return decimalResult;
-        
+
         case FieldTypeEnum.Date:
           if (typeof value === 'number') {
             // Excel date is days since 1900-01-01
@@ -408,12 +409,12 @@ export class ExcelReaderService {
           const dateResult = new Date(value);
           logger.debug(`Date conversion from string`, { original: value, converted: dateResult });
           return dateResult;
-        
+
         case FieldTypeEnum.Numeric:
           const numericResult = parseInt(value.toString()) || 0;
           logger.debug(`Numeric conversion`, { original: value, converted: numericResult });
           return numericResult;
-        
+
         case FieldTypeEnum.String:
         default:
           const stringResult = value.toString();
@@ -421,7 +422,7 @@ export class ExcelReaderService {
           return stringResult;
       }
     })();
-    
+
     return convertedValue;
   }
 }

+ 9 - 9
app/lib/excel-import/import-processor.ts

@@ -14,7 +14,7 @@ export class ImportProcessor {
     this.prisma = new PrismaClient();
     this.reader = new ExcelReaderService();
     this.inserter = new BulkInserter();
-    this.progressServer = new ImportProgressServer();
+    this.progressServer = ImportProgressServer.getInstance();
   }
 
   async processImport(importId: number): Promise<ImportResult> {
@@ -22,13 +22,13 @@ export class ImportProcessor {
       // Get import record with layout configuration
       const importRecord = await this.prisma.import.findUnique({
         where: { id: importId },
-        include: { 
-          layout: { 
-            include: { 
-              sections: { 
-                include: { fields: true } 
-              } 
-            } 
+        include: {
+          layout: {
+            include: {
+              sections: {
+                include: { fields: true }
+              }
+            }
           },
           file: true
         }
@@ -65,7 +65,7 @@ export class ImportProcessor {
 
       for (let i = 0; i < sections.length; i++) {
         const section = sections[i];
-        
+
         progress.currentSection = section.name;
         progress.processedSections = i + 1;
         this.progressServer.broadcastProgress(importId, progress);