ソースを参照

WIP
feat(cintas): add complete Excel import workflow for Cintas install calendar processing

- Implement processCintasImportData action for handling Excel file imports
- Add CintasImportProcessor class for processing Cintas install calendar data
- Create process-cintas-import server actions for import validation and processing
- Enhance Excel reader with detailed logging and error handling
- Add bulk inserter support for Prisma model compatibility
- Implement WebSocket progress tracking for long-running imports
- Update calendar summary page with process import functionality

BREAKING CHANGE: New stored procedure cintas_calculate_summary required for summary calculations

vtugulan 6 ヶ月 前
コミット
f8542d83a0

+ 38 - 0
app/actions/cintas-workflow.ts

@@ -53,6 +53,44 @@ export async function createCintasImportRecord(fileId: string, fileName: string)
   }
 }
 
+/**
+ * Processes the Cintas import by reading the Excel file and importing data into PostgreSQL
+ * @param importId - The import record ID to process
+ * @returns The processing result
+ */
+export async function processCintasImportData(importId: number) {
+  try {
+    console.log(`Starting Cintas import data processing for import ID: ${importId}`);
+    
+    // Import the processCintasImport function
+    const { processCintasImport } = await import('./process-cintas-import');
+    
+    // Process the import
+    const result = await processCintasImport(importId);
+    
+    if (result.success) {
+      console.log('Cintas import data processing completed successfully:', result);
+      return {
+        success: true,
+        data: result,
+        message: `Successfully imported ${'totalInserted' in result ? result.totalInserted : 0} records`
+      };
+    } else {
+      console.error('Cintas import data processing failed:', result);
+      return {
+        success: false,
+        error: 'error' in result ? result.error : 'Failed to process import data'
+      };
+    }
+  } catch (error) {
+    console.error('Error processing Cintas import data:', error);
+    return {
+      success: false,
+      error: error instanceof Error ? error.message : 'Failed to process Cintas import data'
+    };
+  }
+}
+
 /**
  * Gets the Cintas Install Calendar layout configuration
  * @returns The layout configuration or null if not found

+ 41 - 0
app/actions/process-cintas-import.ts

@@ -0,0 +1,41 @@
+'use server';
+
+import { CintasImportProcessor } from '@/app/lib/excel-import/cintas-import-processor';
+
+export async function processCintasImport(importId: number) {
+  try {
+    console.log(`Starting Cintas import processing for import ID: ${importId}`);
+    
+    const processor = new CintasImportProcessor();
+    
+    // Process the import
+    const result = await processor.processCintasImport(importId);
+    
+    console.log('Cintas import processing completed:', result);
+    return result;
+    
+  } catch (error) {
+    console.error('Error processing Cintas import:', error);
+    return {
+      success: false,
+      error: error instanceof Error ? error.message : 'Failed to process Cintas import'
+    };
+  }
+}
+
+export async function validateCintasImport(importId: number) {
+  try {
+    const processor = new CintasImportProcessor();
+    
+    const validation = await processor.validateImport(importId);
+    
+    return validation;
+    
+  } catch (error) {
+    console.error('Error validating Cintas import:', error);
+    return {
+      valid: false,
+      errors: [error instanceof Error ? error.message : 'Failed to validate Cintas import']
+    };
+  }
+}

+ 54 - 7
app/cintas-calendar-summary/page.tsx

@@ -5,7 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
 import { Button } from '@/components/ui/button';
 import { Upload, FileText, Database, BarChart3, CheckCircle, Loader2 } from 'lucide-react';
 import { UploadForm } from '@/app/components/uploadForm';
-import { createCintasImportRecord } from '@/app/actions/cintas-workflow';
+import { createCintasImportRecord, processCintasImportData } from '@/app/actions/cintas-workflow';
 
 interface FileData {
   id: string;
@@ -62,6 +62,53 @@ 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
+        await handleGenerateSummary();
+      } else {
+        setError(result.error || 'Failed to process import data');
+      }
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Unknown error occurred');
+    } finally {
+      setIsProcessing(false);
+    }
+  };
+
+  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);
+      } else {
+        throw new Error('Failed to generate summary');
+      }
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to generate summary');
+    } finally {
+      setIsProcessing(false);
+    }
+  };
+
   const steps = [
     {
       id: 1,
@@ -82,14 +129,14 @@ export default function CintasCalendarSummaryPage() {
       title: 'Import Data',
       description: 'Read the Excel file and import data into PostgreSQL database',
       icon: Database,
-      status: currentStep >= 3 ? 'pending' : 'disabled',
+      status: currentStep >= 3 ? (summaryData.length > 0 ? 'completed' : 'pending') : 'disabled',
     },
     {
       id: 4,
       title: 'Generate Summary',
       description: 'Run summary calculations and display results',
       icon: BarChart3,
-      status: currentStep >= 4 ? 'pending' : 'disabled',
+      status: currentStep >= 4 ? (summaryData.length > 0 ? 'completed' : 'pending') : 'disabled',
     },
   ];
 
@@ -229,8 +276,8 @@ export default function CintasCalendarSummaryPage() {
                 <p className="text-sm text-muted-foreground">
                   Import ID: {importRecord?.id}
                 </p>
-                <Button 
-                  onClick={() => setCurrentStep(4)}
+                <Button
+                  onClick={handleProcessImportData}
                   disabled={isProcessing}
                   className="w-full"
                 >
@@ -258,8 +305,8 @@ export default function CintasCalendarSummaryPage() {
             </CardHeader>
             <CardContent>
               <div className="space-y-4">
-                <Button 
-                  onClick={() => setCurrentStep(4)}
+                <Button
+                  onClick={handleGenerateSummary}
                   disabled={isProcessing}
                   className="w-full"
                 >

+ 33 - 12
app/lib/excel-import/bulk-inserter.ts

@@ -19,7 +19,7 @@ export class BulkInserter {
     let insertedRows = 0;
 
     try {
-      // Create table name safely
+      // Handle specific table names with Prisma models
       const tableName = sectionData.tableName;
       
       for (let i = 0; i < totalRows; i += batchSize) {
@@ -27,18 +27,39 @@ export class BulkInserter {
         
         if (batch.length === 0) continue;
 
-        // Prepare data for insertion
-        const values = batch.map(row => ({
-          import_id: importId,
-          ...row
-        }));
+        // Prepare data for insertion with proper field mapping
+        const values = batch.map(row => {
+          const mappedRow: any = {
+            importId: importId
+          };
+          
+          // Map the row data to match Prisma model field names
+          Object.keys(row).forEach(key => {
+            // Convert snake_case to camelCase for Prisma model compatibility
+            const camelKey = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
+            mappedRow[camelKey] = row[key];
+          });
+          
+          return mappedRow;
+        });
 
-        // Use Prisma's createMany for batch insertion
-        // Note: This assumes the table has a corresponding Prisma model
-        // For dynamic table names, we would need to use raw SQL
-        await this.prisma.$executeRawUnsafe(
-          this.buildInsertQuery(tableName, values)
-        );
+        // Use appropriate Prisma model based on table name
+        if (tableName === 'cintas_install_calendar') {
+          await this.prisma.cintasInstallCalendar.createMany({
+            data: values,
+            skipDuplicates: false
+          });
+        } else if (tableName === 'cintas_install_calendar_summary') {
+          await this.prisma.cintasSummary.createMany({
+            data: values,
+            skipDuplicates: false
+          });
+        } else {
+          // Fallback to raw SQL for other tables
+          await this.prisma.$executeRawUnsafe(
+            this.buildInsertQuery(tableName, values)
+          );
+        }
 
         insertedRows += batch.length;
         onProgress(insertedRows);

+ 216 - 0
app/lib/excel-import/cintas-import-processor.ts

@@ -0,0 +1,216 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { PrismaClient } from '@prisma/client';
+import { ExcelReaderService } from './excel-reader';
+import { BulkInserter } from './bulk-inserter';
+import { ImportProgressServer } from './websocket-server';
+import { ImportProgress, ImportResult } from './types';
+
+export class CintasImportProcessor {
+  private prisma: PrismaClient;
+  private reader: ExcelReaderService;
+  private inserter: BulkInserter;
+  private progressServer: ImportProgressServer;
+
+  constructor() {
+    this.prisma = new PrismaClient();
+    this.reader = new ExcelReaderService();
+    this.inserter = new BulkInserter();
+    this.progressServer = ImportProgressServer.getInstance();
+  }
+
+  async processCintasImport(importId: number): Promise<ImportResult> {
+    try {
+      console.log(`[${new Date().toISOString()}] [CintasImport] Starting import processing for ID: ${importId}`);
+      
+      // Initialize the progress server if not already done
+      if (!this.progressServer.isServerInitialized()) {
+        this.progressServer.initialize();
+      }
+
+      // Get import record with layout configuration
+      const importRecord = await this.prisma.import.findUnique({
+        where: { id: importId },
+        include: {
+          layout: {
+            include: {
+              sections: {
+                include: { fields: true }
+              }
+            }
+          }
+        }
+      });
+
+      // Get the file separately
+      const file = importRecord?.fileId ? await this.prisma.file.findUnique({
+        where: { id: importRecord.fileId }
+      }) : null;
+
+      if (!importRecord || !file) {
+        console.error(`[${new Date().toISOString()}] [CintasImport] ERROR: Import not found or no file attached`);
+        throw new Error('Import not found or no file attached');
+      }
+
+      console.log(`[${new Date().toISOString()}] [CintasImport] Loaded import record: ${importRecord.id}`);
+
+      // Initialize progress tracking
+      const progress: ImportProgress = {
+        importId,
+        status: 'processing',
+        currentSection: '',
+        currentRow: 0,
+        totalRows: 0,
+        errors: [],
+        processedSections: 0,
+        totalSections: importRecord.layout?.sections?.length || 0
+      };
+
+      // Read Excel file
+      console.log(`[${new Date().toISOString()}] [CintasImport] Starting Excel file reading...`);
+      const sections = await this.reader.readExcelFile(
+        Buffer.from(file.data),
+        importRecord.layout,
+        (sectionProgress) => {
+          this.progressServer.broadcastProgress(importId, sectionProgress);
+        }
+      );
+
+      console.log(`[${new Date().toISOString()}] [CintasImport] Excel file read successfully. Found ${sections.length} sections`);
+
+      // Process each section
+      const processedSections = [];
+      let totalInserted = 0;
+
+      for (let i = 0; i < sections.length; i++) {
+        const section = sections[i];
+        
+        console.log(`[${new Date().toISOString()}] [CintasImport] Processing section ${i+1}/${sections.length}: ${section.name}`);
+        progress.currentSection = section.name;
+        progress.processedSections = i + 1;
+        this.progressServer.broadcastProgress(importId, progress);
+
+        try {
+          // Ensure table exists for this section
+          console.log(`[${new Date().toISOString()}] [CintasImport] Creating table ${section.tableName} for section ${section.name}`);
+          await this.inserter.createImportTable(section.tableName, section.fields);
+          
+          const insertedRows = await this.inserter.insertSectionData(
+            section,
+            importId,
+            (rows) => {
+              progress.currentRow = rows;
+              this.progressServer.broadcastProgress(importId, progress);
+            }
+          );
+
+          processedSections.push({
+            sectionData: section,
+            insertedRows
+          });
+
+          totalInserted += insertedRows;
+          console.log(`[${new Date().toISOString()}] [CintasImport] Completed section ${section.name}: ${insertedRows} rows inserted`);
+
+        } catch (error) {
+          const errorMessage = `Error processing section ${section.name}: ${error instanceof Error ? error.message : 'Unknown error'}`;
+          progress.errors.push(errorMessage);
+          console.error(`[${new Date().toISOString()}] [CintasImport] ERROR: ${errorMessage}`);
+          this.progressServer.broadcastProgress(importId, progress);
+        }
+      }
+
+      // Run the stored procedure to calculate summary
+      console.log(`[${new Date().toISOString()}] [CintasImport] Running summary calculation procedure...`);
+      try {
+        await this.prisma.$executeRawUnsafe(
+          `CALL cintas_calculate_summary(${importId})`
+        );
+        console.log(`[${new Date().toISOString()}] [CintasImport] Summary calculation completed successfully`);
+      } catch (error) {
+        console.error(`[${new Date().toISOString()}] [CintasImport] ERROR: Summary calculation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
+        progress.errors.push(`Stored procedure error: ${error instanceof Error ? error.message : 'Unknown error'}`);
+      }
+
+      progress.status = 'completed';
+      this.progressServer.broadcastProgress(importId, progress);
+      console.log(`[${new Date().toISOString()}] [CintasImport] Import processing completed successfully. Total inserted: ${totalInserted}`);
+
+      return {
+        success: true,
+        totalInserted,
+        sections: processedSections
+      };
+
+    } catch (error) {
+      const progress: ImportProgress = {
+        importId,
+        status: 'failed',
+        currentSection: '',
+        currentRow: 0,
+        totalRows: 0,
+        errors: [error instanceof Error ? error.message : 'Unknown error'],
+        processedSections: 0,
+        totalSections: 0
+      };
+
+      this.progressServer.broadcastProgress(importId, progress);
+      console.error(`[${new Date().toISOString()}] [CintasImport] ERROR: Import processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
+
+      return {
+        success: false,
+        totalInserted: 0,
+        sections: [],
+        errors: [error instanceof Error ? error.message : 'Unknown error']
+      };
+    }
+  }
+
+  async validateImport(importId: number): Promise<{ valid: boolean; errors: string[] }> {
+    const errors: string[] = [];
+    console.log(`[${new Date().toISOString()}] [CintasImport] Starting validation for import ID: ${importId}`);
+
+    try {
+      const importRecord = await this.prisma.import.findUnique({
+        where: { id: importId }
+      });
+
+      if (!importRecord) {
+        errors.push('Import record not found');
+        console.error(`[${new Date().toISOString()}] [CintasImport] Validation failed: Import record not found`);
+        return { valid: false, errors };
+      }
+
+      const file = importRecord.fileId ? await this.prisma.file.findUnique({
+        where: { id: importRecord.fileId }
+      }) : null;
+
+      if (!file) {
+        errors.push('No file attached to import');
+        console.error(`[${new Date().toISOString()}] [CintasImport] Validation failed: No file attached`);
+      }
+
+      const layout = importRecord.layoutId ? await this.prisma.layoutConfiguration.findUnique({
+        where: { id: importRecord.layoutId }
+      }) : null;
+
+      if (!layout) {
+        errors.push('No layout configuration found');
+        console.error(`[${new Date().toISOString()}] [CintasImport] Validation failed: No layout configuration`);
+      }
+
+      // Check if this is a Cintas layout
+      if (layout?.name !== 'Cintas Install Calendar') {
+        errors.push('This import is not configured for Cintas Install Calendar');
+        console.error(`[${new Date().toISOString()}] [CintasImport] Validation failed: Not a Cintas layout`);
+      }
+
+      console.log(`[${new Date().toISOString()}] [CintasImport] Validation completed: ${errors.length === 0 ? 'Valid' : 'Invalid'}`);
+      return { valid: errors.length === 0, errors };
+
+    } catch (error) {
+      errors.push(`Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
+      console.error(`[${new Date().toISOString()}] [CintasImport] Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
+      return { valid: false, errors };
+    }
+  }
+}

+ 329 - 114
app/lib/excel-import/excel-reader.ts

@@ -2,53 +2,112 @@
 import * as ExcelJS from 'exceljs';
 import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types';
 
+// Simple logger utility for debugging
+const logger = {
+  debug: (message: string, ...args: any[]) => {
+    console.debug(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+  },
+  info: (message: string, ...args: any[]) => {
+    console.info(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+  },
+  warn: (message: string, ...args: any[]) => {
+    console.warn(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+  },
+  error: (message: string, ...args: any[]) => {
+    console.error(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+  }
+};
+
 export class ExcelReaderService {
   async readExcelFile(
     fileBuffer: Buffer,
     layoutConfig: any,
     onProgress: (progress: ImportProgress) => void
   ): Promise<ReadSectionData[]> {
-    const workbook = new ExcelJS.Workbook();
-    await workbook.xlsx.load(fileBuffer as any);
-    
-    const results: ReadSectionData[] = [];
-    const totalSections = layoutConfig.sections?.length || 0;
-    
-    // Initialize progress
-    onProgress({
-      importId: 0, // Will be set by caller
-      status: 'processing',
-      currentSection: '',
-      currentRow: 0,
-      totalRows: 0,
-      errors: [],
-      processedSections: 0,
-      totalSections
+    logger.info('Starting Excel file import', {
+      fileSize: fileBuffer.length,
+      layoutConfigSections: layoutConfig.sections?.length || 0
     });
 
-    for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) {
-      const section = layoutConfig.sections[sectionIndex];
-      const worksheet = workbook.getWorksheet(section.sheetName);
+    const startTime = Date.now();
+    
+    try {
+      const workbook = new ExcelJS.Workbook();
+      logger.debug('Loading Excel workbook from buffer...');
+      await workbook.xlsx.load(fileBuffer as any);
+      logger.info('Excel workbook loaded successfully', {
+        worksheets: workbook.worksheets.map(ws => ({ name: ws.name, rowCount: ws.rowCount }))
+      });
       
-      if (!worksheet) {
-        onProgress({
-          importId: 0,
-          status: 'processing',
-          currentSection: section.name,
-          currentRow: 0,
-          totalRows: 0,
-          errors: [`Worksheet '${section.sheetName}' not found`],
-          processedSections: sectionIndex + 1,
-          totalSections
+      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
+        status: 'processing',
+        currentSection: '',
+        currentRow: 0,
+        totalRows: 0,
+        errors: [],
+        processedSections: 0,
+        totalSections
+      });
+
+      for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) {
+        const section = layoutConfig.sections[sectionIndex];
+        logger.debug(`Processing section ${sectionIndex + 1}/${totalSections}`, {
+          sectionName: section.name,
+          sheetName: section.sheetName,
+          startingRow: section.startingRow,
+          endingRow: section.endingRow
+        });
+        
+        const worksheet = workbook.getWorksheet(section.sheetName);
+        
+        if (!worksheet) {
+          const error = `Worksheet '${section.sheetName}' not found`;
+          logger.warn(error, { availableWorksheets: workbook.worksheets.map(ws => ws.name) });
+          
+          onProgress({
+            importId: 0,
+            status: 'processing',
+            currentSection: section.name,
+            currentRow: 0,
+            totalRows: 0,
+            errors: [error],
+            processedSections: sectionIndex + 1,
+            totalSections
+          });
+          continue;
+        }
+
+        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
         });
-        continue;
       }
 
-      const sectionData = await this.processSection(worksheet, section, sectionIndex, totalSections, onProgress);
-      results.push(sectionData);
-    }
+      const totalTime = Date.now() - startTime;
+      logger.info('Excel file import completed', {
+        totalSections: results.length,
+        totalRows: results.reduce((sum, section) => sum + section.data.length, 0),
+        totalTimeMs: totalTime
+      });
 
-    return results;
+      return results;
+    } catch (error) {
+      logger.error('Error reading Excel file', {
+        error: error instanceof Error ? error.message : String(error),
+        stack: error instanceof Error ? error.stack : undefined
+      });
+      throw error;
+    }
   }
 
   private async processSection(
@@ -58,40 +117,101 @@ export class ExcelReaderService {
     totalSections: number,
     onProgress: (progress: ImportProgress) => void
   ): Promise<ReadSectionData> {
-    const startingRow = section.startingRow || 1;
+    const sectionStartTime = Date.now();
+    logger.info(`Starting section processing`, {
+      sectionName: section.name,
+      sheetName: section.sheetName,
+      sectionIndex: sectionIndex + 1,
+      totalSections
+    });
+
+    const startingRow = section.startingRow || 2; // Default to 2 to skip header
     const endingRow = section.endingRow || worksheet.rowCount;
     
-    // Get headers from the first row (assuming row 1 has headers)
-    const headers: string[] = [];
-    const headerRow = worksheet.getRow(1);
-    headerRow.eachCell((cell) => {
-      headers.push(cell.text || '');
+    logger.debug('Section configuration', {
+      sectionName: section.name,
+      startingRow,
+      endingRow,
+      totalRowsInSheet: worksheet.rowCount,
+      fieldsCount: section.fields?.length || 0
     });
-
+    
     // Process data rows
     const data: Record<string, any>[] = [];
     const totalRows = endingRow - startingRow + 1;
     
+    let processedRows = 0;
+    let skippedRows = 0;
+    
     for (let rowNum = startingRow; rowNum <= endingRow; rowNum++) {
       const row = worksheet.getRow(rowNum);
-      if (!row.hasValues) continue;
+      
+      if (!row.hasValues) {
+        skippedRows++;
+        logger.debug(`Skipping empty row ${rowNum}`);
+        continue;
+      }
 
       const rowData: Record<string, any> = {};
+      let fieldsProcessed = 0;
       
       // Map cell values based on field configuration
       for (const field of section.fields || []) {
-        const cellAddress = this.parseCellAddress(field.cellPosition);
-        const cell = row.getCell(cellAddress.col);
-        
-        if (cell && cell.value !== null && cell.value !== undefined) {
-          rowData[field.importTableColumnName] = this.convertCellValue(
-            cell.value,
-            field.parsedType || FieldTypeEnum.String
-          );
+        try {
+          const cellAddress = this.parseCellAddress(field.cellPosition);
+          const cell = row.getCell(cellAddress.col);
+          
+          logger.debug(`Processing field`, {
+            fieldName: field.name,
+            cellPosition: field.cellPosition,
+            cellAddress,
+            rawValue: cell?.value,
+            rowNum
+          });
+          
+          if (cell && cell.value !== null && cell.value !== undefined) {
+            const value = this.convertCellValue(
+              cell.value,
+              field.parsedType || FieldTypeEnum.String
+            );
+            
+            logger.debug(`Value converted`, {
+              fieldName: field.name,
+              originalValue: cell.value,
+              convertedValue: value,
+              fieldType: field.parsedType || FieldTypeEnum.String
+            });
+            
+            // Map to the correct column name for Prisma model
+            const columnName = field.importTableColumnName;
+            rowData[columnName] = value;
+            fieldsProcessed++;
+          }
+        } catch (error) {
+          logger.error(`Error processing field ${field.name} at row ${rowNum}`, {
+            error: error instanceof Error ? error.message : String(error),
+            field,
+            rowNum
+          });
         }
       }
       
-      data.push(rowData);
+      // 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,
+            fieldsProcessed,
+            rowDataKeys: Object.keys(rowData),
+            dataLength: data.length
+          });
+        }
+      } else {
+        logger.debug(`Skipping row with no valid data`, { rowNum });
+      }
       
       // Update progress every 100 rows
       if (rowNum % 100 === 0 || rowNum === endingRow) {
@@ -108,7 +228,17 @@ export class ExcelReaderService {
       }
     }
 
-    return {
+    const sectionTime = Date.now() - sectionStartTime;
+    logger.info(`Section processing completed`, {
+      sectionName: section.name,
+      processedRows,
+      skippedRows,
+      totalRows,
+      dataRows: data.length,
+      processingTimeMs: sectionTime
+    });
+
+    const result = {
       id: section.id || 0,
       name: section.name || '',
       tableName: section.tableName || '',
@@ -120,91 +250,176 @@ export class ExcelReaderService {
       fields: this.mapFields(section.fields || []),
       data
     };
+
+    logger.debug('Section result', {
+      sectionName: section.name,
+      resultSummary: {
+        id: result.id,
+        name: result.name,
+        tableName: result.tableName,
+        dataRows: result.data.length,
+        fields: result.fields.length
+      }
+    });
+
+    return result;
   }
 
   private parseCellAddress(cellPosition: string): { row: number; col: number } {
+    logger.debug(`Parsing cell address: ${cellPosition}`);
+    
     const match = cellPosition.match(/([A-Z]+)(\d+)/);
-    if (!match) return { row: 1, col: 1 };
+    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 {
-    switch (type?.toLowerCase()) {
-      case 'grid':
-        return SectionTypeEnum.Grid;
-      case 'properties':
-        return SectionTypeEnum.Properties;
-      default:
-        return SectionTypeEnum.Unknown;
-    }
+    logger.debug(`Mapping section type: ${type}`);
+    
+    const mappedType = (() => {
+      switch (type?.toLowerCase()) {
+        case 'grid':
+          return SectionTypeEnum.Grid;
+        case 'properties':
+          return SectionTypeEnum.Properties;
+        default:
+          return SectionTypeEnum.Unknown;
+      }
+    })();
+    
+    logger.debug(`Section type mapped`, {
+      originalType: type,
+      mappedType: SectionTypeEnum[mappedType]
+    });
+    
+    return mappedType;
   }
 
   private mapFields(fields: any[]): LayoutSectionField[] {
-    return fields.map((field, index) => ({
-      id: field.id || index,
-      cellPosition: field.cellPosition || '',
-      name: field.name || '',
-      dataType: field.dataType || 'string',
-      dataTypeFormat: field.dataTypeFormat,
-      importTableColumnName: field.importTableColumnName || field.name || `column_${index}`,
-      importColumnOrderNumber: field.importColumnOrderNumber || index,
-      parsedType: this.mapFieldType(field.dataType)
-    }));
+    logger.debug(`Mapping ${fields.length} fields`);
+    
+    const mappedFields = fields.map((field, index) => {
+      const mappedField = {
+        id: field.id || index,
+        cellPosition: field.cellPosition || '',
+        name: field.name || '',
+        dataType: field.dataType || 'string',
+        dataTypeFormat: field.dataTypeFormat,
+        importTableColumnName: field.importTableColumnName || field.name || `column_${index}`,
+        importColumnOrderNumber: field.importColumnOrderNumber || index,
+        parsedType: this.mapFieldType(field.dataType)
+      };
+      
+      logger.debug(`Field mapped`, {
+        index,
+        originalName: field.name,
+        mappedName: mappedField.name,
+        cellPosition: mappedField.cellPosition,
+        parsedType: FieldTypeEnum[mappedField.parsedType]
+      });
+      
+      return mappedField;
+    });
+    
+    return mappedFields;
   }
 
   private mapFieldType(dataType: string): FieldTypeEnum {
     const type = dataType?.toLowerCase();
     
-    switch (type) {
-      case 'time':
-        return FieldTypeEnum.Time;
-      case 'decimal':
-      case 'number':
-      case 'float':
-        return FieldTypeEnum.Decimal;
-      case 'date':
-        return FieldTypeEnum.Date;
-      case 'int':
-      case 'integer':
-      case 'numeric':
-        return FieldTypeEnum.Numeric;
-      default:
-        return FieldTypeEnum.String;
-    }
+    const mappedType = (() => {
+      switch (type) {
+        case 'time':
+          return FieldTypeEnum.Time;
+        case 'decimal':
+        case 'number':
+        case 'float':
+          return FieldTypeEnum.Decimal;
+        case 'date':
+          return FieldTypeEnum.Date;
+        case 'int':
+        case 'integer':
+        case 'numeric':
+          return FieldTypeEnum.Numeric;
+        default:
+          return FieldTypeEnum.String;
+      }
+    })();
+    
+    logger.debug(`Field type mapped`, {
+      originalDataType: dataType,
+      mappedType: FieldTypeEnum[mappedType]
+    });
+    
+    return mappedType;
   }
 
   private convertCellValue(value: any, fieldType: FieldTypeEnum): any {
-    if (value === null || value === undefined) return null;
-    
-    switch (fieldType) {
-      case FieldTypeEnum.Time:
-        if (typeof value === 'number') {
-          // Excel time is fraction of a day
-          return value * 24 * 60 * 60 * 1000; // Convert to milliseconds
-        }
-        return value;
-      
-      case FieldTypeEnum.Decimal:
-        return parseFloat(value.toString()) || 0;
-      
-      case FieldTypeEnum.Date:
-        if (typeof value === 'number') {
-          // Excel date is days since 1900-01-01
-          const excelEpoch = new Date(1900, 0, 1);
-          return new Date(excelEpoch.getTime() + (value - 1) * 24 * 60 * 60 * 1000);
-        }
-        return new Date(value);
-      
-      case FieldTypeEnum.Numeric:
-        return parseInt(value.toString()) || 0;
-      
-      case FieldTypeEnum.String:
-      default:
-        return value.toString();
+    if (value === null || value === undefined) {
+      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:
+          if (typeof value === 'number') {
+            // Excel time is fraction of a day
+            const result = value * 24 * 60 * 60 * 1000; // Convert to milliseconds
+            logger.debug(`Time conversion`, { original: value, converted: result });
+            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
+            const excelEpoch = new Date(1900, 0, 1);
+            const dateResult = new Date(excelEpoch.getTime() + (value - 1) * 24 * 60 * 60 * 1000);
+            logger.debug(`Date conversion`, { original: value, converted: dateResult });
+            return dateResult;
+          }
+          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();
+          logger.debug(`String conversion`, { original: value, converted: stringResult });
+          return stringResult;
+      }
+    })();
+    
+    return convertedValue;
   }
 }

+ 52 - 17
app/lib/excel-import/websocket-server.ts

@@ -6,17 +6,50 @@ import { ImportProgress } from './types';
 export class ImportProgressServer {
   private wss: WebSocketServer | null = null;
   private clients: Set<WebSocket> = new Set();
+  private static instance: ImportProgressServer | null = null;
+  private port: number = 8081;
+  private isInitialized: boolean = false;
 
-  constructor() {
-    // Initialize WebSocket server
-    this.setupWebSocketServer();
+  private constructor() {
+    // Private constructor for singleton pattern
   }
 
-  private setupWebSocketServer() {
-    // This will be initialized when the HTTP server is available
-    // For now, we'll create a standalone WebSocket server
-    this.wss = new WebSocketServer({ port: 8080 });
-    
+  public static getInstance(): ImportProgressServer {
+    if (!ImportProgressServer.instance) {
+      ImportProgressServer.instance = new ImportProgressServer();
+    }
+    return ImportProgressServer.instance;
+  }
+
+  public initialize(): void {
+    if (this.isInitialized) {
+      console.log('WebSocket server already initialized');
+      return;
+    }
+
+    try {
+      // Try to create WebSocket server on the specified port
+      this.wss = new WebSocketServer({ port: this.port });
+      
+      this.setupWebSocketHandlers();
+      this.isInitialized = true;
+      console.log(`WebSocket server started on port ${this.port}`);
+      
+    } catch (error: any) {
+      if (error.code === 'EADDRINUSE') {
+        console.log(`Port ${this.port} is already in use, using existing server`);
+        // Server is already running, we'll use the existing one
+        this.isInitialized = true;
+      } else {
+        console.error('Failed to start WebSocket server:', error);
+        throw error;
+      }
+    }
+  }
+
+  private setupWebSocketHandlers(): void {
+    if (!this.wss) return;
+
     this.wss.on('connection', (ws: WebSocket) => {
       console.log('New WebSocket connection established');
       this.clients.add(ws);
@@ -31,12 +64,10 @@ export class ImportProgressServer {
         this.clients.delete(ws);
       });
     });
-
-    console.log('WebSocket server started on port 8080');
   }
 
   // Alternative method to attach to existing HTTP server
-  attachToServer(server: Server) {
+  attachToServer(server: Server): void {
     if (this.wss) {
       this.wss.close();
     }
@@ -50,7 +81,6 @@ export class ImportProgressServer {
       console.log('New WebSocket connection on import progress endpoint');
       this.clients.add(ws);
 
-      // Handle specific import ID subscriptions
       const url = new URL(request.url || '', `http://${request.headers.host}`);
       const importId = url.searchParams.get('importId');
       
@@ -74,7 +104,7 @@ export class ImportProgressServer {
     });
   }
 
-  broadcastProgress(importId: number, progress: ImportProgress) {
+  broadcastProgress(importId: number, progress: ImportProgress): void {
     const message = JSON.stringify({
       type: 'progress',
       importId,
@@ -88,7 +118,7 @@ export class ImportProgressServer {
     });
   }
 
-  broadcastError(importId: number, error: string) {
+  broadcastError(importId: number, error: string): void {
     const message = JSON.stringify({
       type: 'error',
       importId,
@@ -102,7 +132,7 @@ export class ImportProgressServer {
     });
   }
 
-  broadcastComplete(importId: number, result: any) {
+  broadcastComplete(importId: number, result: any): void {
     const message = JSON.stringify({
       type: 'complete',
       importId,
@@ -116,13 +146,18 @@ export class ImportProgressServer {
     });
   }
 
-  close() {
+  close(): void {
     if (this.wss) {
       this.wss.close();
       this.clients.clear();
+      this.isInitialized = false;
     }
   }
+
+  isServerInitialized(): boolean {
+    return this.isInitialized;
+  }
 }
 
 // Export a singleton instance
-export const progressServer = new ImportProgressServer();
+export const progressServer = ImportProgressServer.getInstance();