excel-reader.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. import * as ExcelJS from 'exceljs';
  3. import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types';
  4. export class ExcelReaderService {
  5. async readExcelFile(
  6. fileBuffer: Buffer,
  7. layoutConfig: any,
  8. onProgress: (progress: ImportProgress) => void
  9. ): Promise<ReadSectionData[]> {
  10. const workbook = new ExcelJS.Workbook();
  11. await workbook.xlsx.load(fileBuffer as any);
  12. const results: ReadSectionData[] = [];
  13. const totalSections = layoutConfig.sections?.length || 0;
  14. // Initialize progress
  15. onProgress({
  16. importId: 0, // Will be set by caller
  17. status: 'processing',
  18. currentSection: '',
  19. currentRow: 0,
  20. totalRows: 0,
  21. errors: [],
  22. processedSections: 0,
  23. totalSections
  24. });
  25. for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) {
  26. const section = layoutConfig.sections[sectionIndex];
  27. const worksheet = workbook.getWorksheet(section.sheetName);
  28. if (!worksheet) {
  29. onProgress({
  30. importId: 0,
  31. status: 'processing',
  32. currentSection: section.name,
  33. currentRow: 0,
  34. totalRows: 0,
  35. errors: [`Worksheet '${section.sheetName}' not found`],
  36. processedSections: sectionIndex + 1,
  37. totalSections
  38. });
  39. continue;
  40. }
  41. const sectionData = await this.processSection(worksheet, section, sectionIndex, totalSections, onProgress);
  42. results.push(sectionData);
  43. }
  44. return results;
  45. }
  46. private async processSection(
  47. worksheet: ExcelJS.Worksheet,
  48. section: any,
  49. sectionIndex: number,
  50. totalSections: number,
  51. onProgress: (progress: ImportProgress) => void
  52. ): Promise<ReadSectionData> {
  53. const startingRow = section.startingRow || 1;
  54. const endingRow = section.endingRow || worksheet.rowCount;
  55. // Get headers from the first row (assuming row 1 has headers)
  56. const headers: string[] = [];
  57. const headerRow = worksheet.getRow(1);
  58. headerRow.eachCell((cell) => {
  59. headers.push(cell.text || '');
  60. });
  61. // Process data rows
  62. const data: Record<string, any>[] = [];
  63. const totalRows = endingRow - startingRow + 1;
  64. for (let rowNum = startingRow; rowNum <= endingRow; rowNum++) {
  65. const row = worksheet.getRow(rowNum);
  66. if (!row.hasValues) continue;
  67. const rowData: Record<string, any> = {};
  68. // Map cell values based on field configuration
  69. for (const field of section.fields || []) {
  70. const cellAddress = this.parseCellAddress(field.cellPosition);
  71. const cell = row.getCell(cellAddress.col);
  72. if (cell && cell.value !== null && cell.value !== undefined) {
  73. rowData[field.importTableColumnName] = this.convertCellValue(
  74. cell.value,
  75. field.parsedType || FieldTypeEnum.String
  76. );
  77. }
  78. }
  79. data.push(rowData);
  80. // Update progress every 100 rows
  81. if (rowNum % 100 === 0 || rowNum === endingRow) {
  82. onProgress({
  83. importId: 0,
  84. status: 'processing',
  85. currentSection: section.name,
  86. currentRow: rowNum - startingRow + 1,
  87. totalRows,
  88. errors: [],
  89. processedSections: sectionIndex,
  90. totalSections
  91. });
  92. }
  93. }
  94. return {
  95. id: section.id || 0,
  96. name: section.name || '',
  97. tableName: section.tableName || '',
  98. sheet: section.sheetName || '',
  99. type: section.type || '',
  100. startingRow,
  101. endingRow,
  102. parsedType: this.mapSectionType(section.type),
  103. fields: this.mapFields(section.fields || []),
  104. data
  105. };
  106. }
  107. private parseCellAddress(cellPosition: string): { row: number; col: number } {
  108. const match = cellPosition.match(/([A-Z]+)(\d+)/);
  109. if (!match) return { row: 1, col: 1 };
  110. const col = match[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1;
  111. const row = parseInt(match[2]);
  112. return { row, col };
  113. }
  114. private mapSectionType(type: string): SectionTypeEnum {
  115. switch (type?.toLowerCase()) {
  116. case 'grid':
  117. return SectionTypeEnum.Grid;
  118. case 'properties':
  119. return SectionTypeEnum.Properties;
  120. default:
  121. return SectionTypeEnum.Unknown;
  122. }
  123. }
  124. private mapFields(fields: any[]): LayoutSectionField[] {
  125. return fields.map((field, index) => ({
  126. id: field.id || index,
  127. cellPosition: field.cellPosition || '',
  128. name: field.name || '',
  129. dataType: field.dataType || 'string',
  130. dataTypeFormat: field.dataTypeFormat,
  131. importTableColumnName: field.importTableColumnName || field.name || `column_${index}`,
  132. importColumnOrderNumber: field.importColumnOrderNumber || index,
  133. parsedType: this.mapFieldType(field.dataType)
  134. }));
  135. }
  136. private mapFieldType(dataType: string): FieldTypeEnum {
  137. const type = dataType?.toLowerCase();
  138. switch (type) {
  139. case 'time':
  140. return FieldTypeEnum.Time;
  141. case 'decimal':
  142. case 'number':
  143. case 'float':
  144. return FieldTypeEnum.Decimal;
  145. case 'date':
  146. return FieldTypeEnum.Date;
  147. case 'int':
  148. case 'integer':
  149. case 'numeric':
  150. return FieldTypeEnum.Numeric;
  151. default:
  152. return FieldTypeEnum.String;
  153. }
  154. }
  155. private convertCellValue(value: any, fieldType: FieldTypeEnum): any {
  156. if (value === null || value === undefined) return null;
  157. switch (fieldType) {
  158. case FieldTypeEnum.Time:
  159. if (typeof value === 'number') {
  160. // Excel time is fraction of a day
  161. return value * 24 * 60 * 60 * 1000; // Convert to milliseconds
  162. }
  163. return value;
  164. case FieldTypeEnum.Decimal:
  165. return parseFloat(value.toString()) || 0;
  166. case FieldTypeEnum.Date:
  167. if (typeof value === 'number') {
  168. // Excel date is days since 1900-01-01
  169. const excelEpoch = new Date(1900, 0, 1);
  170. return new Date(excelEpoch.getTime() + (value - 1) * 24 * 60 * 60 * 1000);
  171. }
  172. return new Date(value);
  173. case FieldTypeEnum.Numeric:
  174. return parseInt(value.toString()) || 0;
  175. case FieldTypeEnum.String:
  176. default:
  177. return value.toString();
  178. }
  179. }
  180. }