excel-reader.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import * as XLSX from 'xlsx';
  2. import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types';
  3. export class ExcelReaderService {
  4. async readExcelFile(
  5. fileData: Buffer | Uint8Array,
  6. layoutConfig: any,
  7. onProgress: (progress: ImportProgress) => void
  8. ): Promise<ReadSectionData[]> {
  9. try {
  10. const workbook = XLSX.read(fileData, { type: 'buffer' });
  11. const results: ReadSectionData[] = [];
  12. const totalSections = layoutConfig.sections?.length || 0;
  13. // Initialize progress
  14. onProgress({
  15. importId: 0, // Will be set by caller
  16. status: 'processing',
  17. currentSection: '',
  18. currentRow: 0,
  19. totalRows: 0,
  20. errors: [],
  21. processedSections: 0,
  22. totalSections
  23. });
  24. for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) {
  25. const section = layoutConfig.sections[sectionIndex];
  26. const worksheet = workbook.Sheets[section.sheetName];
  27. if (!worksheet) {
  28. const error = `Worksheet '${section.sheetName}' not found`;
  29. onProgress({
  30. importId: 0,
  31. status: 'processing',
  32. currentSection: section.name,
  33. currentRow: 0,
  34. totalRows: 0,
  35. errors: [error],
  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. } catch (error) {
  46. throw error;
  47. }
  48. }
  49. private async processSection(
  50. worksheet: XLSX.WorkSheet,
  51. section: any,
  52. sectionIndex: number,
  53. totalSections: number,
  54. onProgress: (progress: ImportProgress) => void
  55. ): Promise<ReadSectionData> {
  56. const startingRow = section.startingRow || 2; // Default to 2 to skip header
  57. const endingRow = section.endingRow || Infinity;
  58. // Convert worksheet to JSON array
  59. const worksheetData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
  60. // Process data rows
  61. const data: Record<string, any>[] = [];
  62. const totalRows = Math.min(endingRow, worksheetData.length) - startingRow + 1;
  63. for (let rowNum = startingRow; rowNum <= Math.min(endingRow, worksheetData.length); rowNum++) {
  64. const row = worksheetData[rowNum - 1]; // Convert to 0-based index
  65. if (!row || row.every(cell => cell === null || cell === undefined || cell === '')) {
  66. continue;
  67. }
  68. const rowData: Record<string, any> = {};
  69. // Map cell values based on field configuration
  70. for (const field of section.fields || []) {
  71. try {
  72. const cellAddress = this.parseCellAddress(field.cellPosition, rowNum);
  73. const cellValue = row[cellAddress.col - 1]; // Convert to 0-based index
  74. if (cellValue !== null && cellValue !== undefined && cellValue !== '') {
  75. const value = this.convertCellValue(
  76. field.dataType,
  77. field.dataTypeFormat,
  78. cellValue,
  79. field.parsedType || FieldTypeEnum.String
  80. );
  81. // Map to the correct column name for Prisma model
  82. const columnName = field.importTableColumnName;
  83. rowData[columnName] = value;
  84. }
  85. } catch (error) {
  86. console.log(`Error processing field ${field.name} at row ${rowNum}`, {
  87. error: error instanceof Error ? error.message : String(error),
  88. field,
  89. rowNum
  90. });
  91. }
  92. }
  93. // Only add non-empty rows
  94. if (Object.keys(rowData).length > 0) {
  95. data.push(rowData);
  96. }
  97. // Update progress every 100 rows
  98. if (rowNum % 100 === 0 || rowNum === Math.min(endingRow, worksheetData.length)) {
  99. onProgress({
  100. importId: 0,
  101. status: 'processing',
  102. currentSection: section.name,
  103. currentRow: rowNum - startingRow + 1,
  104. totalRows,
  105. errors: [],
  106. processedSections: sectionIndex,
  107. totalSections
  108. });
  109. }
  110. }
  111. const result = {
  112. id: section.id || 0,
  113. name: section.name || '',
  114. tableName: section.tableName || '',
  115. sheet: section.sheetName || '',
  116. type: section.type || '',
  117. startingRow,
  118. endingRow,
  119. parsedType: this.mapSectionType(section.type),
  120. fields: this.mapFields(section.fields || []),
  121. data
  122. };
  123. return result;
  124. }
  125. private parseCellAddress(cellPosition: string, rowNumber: number): { row: number; col: number } {
  126. let match = cellPosition.match(/([A-Z]+)(\d+)/);
  127. if (!match) {
  128. const appendedCellPosition = `${cellPosition}${rowNumber}`;
  129. match = appendedCellPosition.match(/([A-Z]+)(\d+)/);
  130. if (!match) {
  131. return { row: 1, col: 1 };
  132. }
  133. }
  134. const col = match[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1;
  135. const row = parseInt(match[2]);
  136. return { row, col };
  137. }
  138. private mapSectionType(type: string): SectionTypeEnum {
  139. const mappedType = (() => {
  140. switch (type?.toLowerCase()) {
  141. case 'grid':
  142. return SectionTypeEnum.Grid;
  143. case 'properties':
  144. return SectionTypeEnum.Properties;
  145. default:
  146. return SectionTypeEnum.Unknown;
  147. }
  148. })();
  149. return mappedType;
  150. }
  151. private mapFields(fields: any[]): LayoutSectionField[] {
  152. const mappedFields = fields.map((field, index) => {
  153. const mappedField = {
  154. id: field.id || index,
  155. cellPosition: field.cellPosition || '',
  156. name: field.name || '',
  157. dataType: field.dataType || 'string',
  158. dataTypeFormat: field.dataTypeFormat,
  159. importTableColumnName: field.importTableColumnName || field.name || `column_${index}`,
  160. importColumnOrderNumber: field.importColumnOrderNumber || index,
  161. parsedType: this.mapFieldType(field.dataType)
  162. };
  163. return mappedField;
  164. });
  165. return mappedFields;
  166. }
  167. private mapFieldType(dataType: string): FieldTypeEnum {
  168. const type = dataType?.toLowerCase();
  169. const mappedType = (() => {
  170. switch (type) {
  171. case 'time':
  172. return FieldTypeEnum.Time;
  173. case 'decimal':
  174. case 'number':
  175. case 'float':
  176. return FieldTypeEnum.Decimal;
  177. case 'date':
  178. return FieldTypeEnum.Date;
  179. case 'int':
  180. case 'integer':
  181. case 'numeric':
  182. return FieldTypeEnum.Numeric;
  183. default:
  184. return FieldTypeEnum.String;
  185. }
  186. })();
  187. return mappedType;
  188. }
  189. private convertCellValue(dataType: string, dataTypeFormat: string | undefined, value: any, fieldType: FieldTypeEnum): any {
  190. if (value === null || value === undefined) {
  191. return null;
  192. }
  193. const convertedValue = (() => {
  194. if (dataType === "DATE" && (typeof value === 'number' || value instanceof Date))
  195. return XLSX.SSF.format(dataTypeFormat || 'yyyy-mm-dd', value);
  196. switch (fieldType) {
  197. case FieldTypeEnum.Time:
  198. if (typeof value === 'number') {
  199. // Excel time is fraction of a day
  200. const result = value * 24 * 60 * 60 * 1000; // Convert to milliseconds
  201. return result;
  202. }
  203. return value;
  204. case FieldTypeEnum.Decimal:
  205. const decimalResult = parseFloat(value.toString()) || 0;
  206. return decimalResult;
  207. case FieldTypeEnum.Date:
  208. if (typeof value === 'number') {
  209. // Excel date is days since 1900-01-01
  210. const excelEpoch = new Date(1900, 0, 1);
  211. const dateResult = new Date(excelEpoch.getTime() + (value - 1) * 24 * 60 * 60 * 1000);
  212. return dateResult;
  213. }
  214. const dateResult = new Date(value);
  215. return dateResult;
  216. case FieldTypeEnum.Numeric:
  217. const numericResult = parseInt(value.toString()) || 0;
  218. return numericResult;
  219. case FieldTypeEnum.String:
  220. default:
  221. const stringResult = value.toString();
  222. return stringResult;
  223. }
  224. })();
  225. return convertedValue;
  226. }
  227. }