import * as ExcelJS from 'exceljs'; import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types'; export class ExcelReaderService { async readExcelFile( fileBuffer: Buffer, layoutConfig: any, onProgress: (progress: ImportProgress) => void ): Promise { const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(fileBuffer); 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 }); for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) { const section = layoutConfig.sections[sectionIndex]; const worksheet = workbook.getWorksheet(section.sheetName); if (!worksheet) { onProgress({ importId: 0, status: 'processing', currentSection: section.name, currentRow: 0, totalRows: 0, errors: [`Worksheet '${section.sheetName}' not found`], processedSections: sectionIndex + 1, totalSections }); continue; } const sectionData = await this.processSection(worksheet, section, sectionIndex, totalSections, onProgress); results.push(sectionData); } return results; } private async processSection( worksheet: ExcelJS.Worksheet, section: any, sectionIndex: number, totalSections: number, onProgress: (progress: ImportProgress) => void ): Promise { const startingRow = section.startingRow || 1; 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 || ''); }); // Process data rows const data: Record[] = []; const totalRows = endingRow - startingRow + 1; for (let rowNum = startingRow; rowNum <= endingRow; rowNum++) { const row = worksheet.getRow(rowNum); if (!row.hasValues) continue; const rowData: Record = {}; // 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 ); } } data.push(rowData); // Update progress every 100 rows if (rowNum % 100 === 0 || rowNum === endingRow) { onProgress({ importId: 0, status: 'processing', currentSection: section.name, currentRow: rowNum - startingRow + 1, totalRows, errors: [], processedSections: sectionIndex, totalSections }); } } return { id: section.id || 0, name: section.name || '', tableName: section.tableName || '', sheet: section.sheetName || '', type: section.type || '', startingRow, endingRow, parsedType: this.mapSectionType(section.type), fields: this.mapFields(section.fields || []), data }; } private parseCellAddress(cellPosition: string): { row: number; col: number } { const match = cellPosition.match(/([A-Z]+)(\d+)/); if (!match) return { row: 1, col: 1 }; const col = match[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1; const row = parseInt(match[2]); 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; } } 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) })); } 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; } } 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(); } } }