excel-reader.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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);
  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. cellValue,
  77. field.parsedType || FieldTypeEnum.String
  78. );
  79. // Map to the correct column name for Prisma model
  80. const columnName = field.importTableColumnName;
  81. rowData[columnName] = value;
  82. }
  83. } catch {
  84. // Error processing field, skip this field
  85. }
  86. }
  87. // Only add non-empty rows
  88. if (Object.keys(rowData).length > 0) {
  89. data.push(rowData);
  90. }
  91. // Update progress every 100 rows
  92. if (rowNum % 100 === 0 || rowNum === Math.min(endingRow, worksheetData.length)) {
  93. onProgress({
  94. importId: 0,
  95. status: 'processing',
  96. currentSection: section.name,
  97. currentRow: rowNum - startingRow + 1,
  98. totalRows,
  99. errors: [],
  100. processedSections: sectionIndex,
  101. totalSections
  102. });
  103. }
  104. }
  105. const result = {
  106. id: section.id || 0,
  107. name: section.name || '',
  108. tableName: section.tableName || '',
  109. sheet: section.sheetName || '',
  110. type: section.type || '',
  111. startingRow,
  112. endingRow,
  113. parsedType: this.mapSectionType(section.type),
  114. fields: this.mapFields(section.fields || []),
  115. data
  116. };
  117. return result;
  118. }
  119. private parseCellAddress(cellPosition: string): { row: number; col: number } {
  120. const match = cellPosition.match(/([A-Z]+)(\d+)/);
  121. if (!match) {
  122. return { row: 1, col: 1 };
  123. }
  124. const col = match[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1;
  125. const row = parseInt(match[2]);
  126. return { row, col };
  127. }
  128. private mapSectionType(type: string): SectionTypeEnum {
  129. const mappedType = (() => {
  130. switch (type?.toLowerCase()) {
  131. case 'grid':
  132. return SectionTypeEnum.Grid;
  133. case 'properties':
  134. return SectionTypeEnum.Properties;
  135. default:
  136. return SectionTypeEnum.Unknown;
  137. }
  138. })();
  139. return mappedType;
  140. }
  141. private mapFields(fields: any[]): LayoutSectionField[] {
  142. const mappedFields = fields.map((field, index) => {
  143. const mappedField = {
  144. id: field.id || index,
  145. cellPosition: field.cellPosition || '',
  146. name: field.name || '',
  147. dataType: field.dataType || 'string',
  148. dataTypeFormat: field.dataTypeFormat,
  149. importTableColumnName: field.importTableColumnName || field.name || `column_${index}`,
  150. importColumnOrderNumber: field.importColumnOrderNumber || index,
  151. parsedType: this.mapFieldType(field.dataType)
  152. };
  153. return mappedField;
  154. });
  155. return mappedFields;
  156. }
  157. private mapFieldType(dataType: string): FieldTypeEnum {
  158. const type = dataType?.toLowerCase();
  159. const mappedType = (() => {
  160. switch (type) {
  161. case 'time':
  162. return FieldTypeEnum.Time;
  163. case 'decimal':
  164. case 'number':
  165. case 'float':
  166. return FieldTypeEnum.Decimal;
  167. case 'date':
  168. return FieldTypeEnum.Date;
  169. case 'int':
  170. case 'integer':
  171. case 'numeric':
  172. return FieldTypeEnum.Numeric;
  173. default:
  174. return FieldTypeEnum.String;
  175. }
  176. })();
  177. return mappedType;
  178. }
  179. private convertCellValue(value: any, fieldType: FieldTypeEnum): any {
  180. if (value === null || value === undefined) {
  181. return null;
  182. }
  183. const convertedValue = (() => {
  184. switch (fieldType) {
  185. case FieldTypeEnum.Time:
  186. if (typeof value === 'number') {
  187. // Excel time is fraction of a day
  188. const result = value * 24 * 60 * 60 * 1000; // Convert to milliseconds
  189. return result;
  190. }
  191. return value;
  192. case FieldTypeEnum.Decimal:
  193. const decimalResult = parseFloat(value.toString()) || 0;
  194. return decimalResult;
  195. case FieldTypeEnum.Date:
  196. if (typeof value === 'number') {
  197. // Excel date is days since 1900-01-01
  198. const excelEpoch = new Date(1900, 0, 1);
  199. const dateResult = new Date(excelEpoch.getTime() + (value - 1) * 24 * 60 * 60 * 1000);
  200. return dateResult;
  201. }
  202. const dateResult = new Date(value);
  203. return dateResult;
  204. case FieldTypeEnum.Numeric:
  205. const numericResult = parseInt(value.toString()) || 0;
  206. return numericResult;
  207. case FieldTypeEnum.String:
  208. default:
  209. const stringResult = value.toString();
  210. return stringResult;
  211. }
  212. })();
  213. return convertedValue;
  214. }
  215. }