excel-reader.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. import * as XLSX from 'xlsx';
  3. import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types';
  4. // Simple logger utility for debugging
  5. const logger = {
  6. debug: (message: string, ...args: any[]) => {
  7. console.debug(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
  8. },
  9. info: (message: string, ...args: any[]) => {
  10. console.info(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
  11. },
  12. warn: (message: string, ...args: any[]) => {
  13. console.warn(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
  14. },
  15. error: (message: string, ...args: any[]) => {
  16. console.error(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
  17. }
  18. };
  19. export class ExcelReaderService {
  20. async readExcelFile(
  21. filePath: string,
  22. layoutConfig: any,
  23. onProgress: (progress: ImportProgress) => void
  24. ): Promise<ReadSectionData[]> {
  25. logger.info('Starting Excel file import', {
  26. filePath,
  27. fileSize: require('fs').statSync(filePath).size,
  28. layoutConfigSections: layoutConfig.sections?.length || 0
  29. });
  30. const startTime = Date.now();
  31. try {
  32. logger.debug('Loading Excel workbook from file path...');
  33. const workbook = XLSX.readFile(filePath);
  34. logger.info('Excel workbook loaded successfully', {
  35. worksheets: workbook.SheetNames.map(name => ({ name, rowCount: XLSX.utils.sheet_to_json(workbook.Sheets[name]).length }))
  36. });
  37. const results: ReadSectionData[] = [];
  38. const totalSections = layoutConfig.sections?.length || 0;
  39. logger.info('Processing Excel import', { totalSections });
  40. // Initialize progress
  41. onProgress({
  42. importId: 0, // Will be set by caller
  43. status: 'processing',
  44. currentSection: '',
  45. currentRow: 0,
  46. totalRows: 0,
  47. errors: [],
  48. processedSections: 0,
  49. totalSections
  50. });
  51. for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) {
  52. const section = layoutConfig.sections[sectionIndex];
  53. logger.debug(`Processing section ${sectionIndex + 1}/${totalSections}`, {
  54. sectionName: section.name,
  55. sheetName: section.sheetName,
  56. startingRow: section.startingRow,
  57. endingRow: section.endingRow
  58. });
  59. const worksheet = workbook.Sheets[section.sheetName];
  60. if (!worksheet) {
  61. const error = `Worksheet '${section.sheetName}' not found`;
  62. logger.warn(error, { availableWorksheets: workbook.SheetNames });
  63. onProgress({
  64. importId: 0,
  65. status: 'processing',
  66. currentSection: section.name,
  67. currentRow: 0,
  68. totalRows: 0,
  69. errors: [error],
  70. processedSections: sectionIndex + 1,
  71. totalSections
  72. });
  73. continue;
  74. }
  75. const sectionData = await this.processSection(worksheet, section, sectionIndex, totalSections, onProgress);
  76. results.push(sectionData);
  77. logger.info(`Section ${section.name} processed successfully`, {
  78. rowsProcessed: sectionData.data.length,
  79. fields: sectionData.fields.length
  80. });
  81. }
  82. const totalTime = Date.now() - startTime;
  83. logger.info('Excel file import completed', {
  84. totalSections: results.length,
  85. totalRows: results.reduce((sum, section) => sum + section.data.length, 0),
  86. totalTimeMs: totalTime
  87. });
  88. return results;
  89. } catch (error) {
  90. logger.error('Error reading Excel file', {
  91. error: error instanceof Error ? error.message : String(error),
  92. stack: error instanceof Error ? error.stack : undefined
  93. });
  94. throw error;
  95. }
  96. }
  97. private async processSection(
  98. worksheet: XLSX.WorkSheet,
  99. section: any,
  100. sectionIndex: number,
  101. totalSections: number,
  102. onProgress: (progress: ImportProgress) => void
  103. ): Promise<ReadSectionData> {
  104. const sectionStartTime = Date.now();
  105. logger.info(`Starting section processing`, {
  106. sectionName: section.name,
  107. sheetName: section.sheetName,
  108. sectionIndex: sectionIndex + 1,
  109. totalSections
  110. });
  111. const startingRow = section.startingRow || 2; // Default to 2 to skip header
  112. const endingRow = section.endingRow || Infinity;
  113. logger.debug('Section configuration', {
  114. sectionName: section.name,
  115. startingRow,
  116. endingRow,
  117. fieldsCount: section.fields?.length || 0
  118. });
  119. // Convert worksheet to JSON array
  120. const worksheetData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
  121. // Process data rows
  122. const data: Record<string, any>[] = [];
  123. const totalRows = Math.min(endingRow, worksheetData.length) - startingRow + 1;
  124. let processedRows = 0;
  125. let skippedRows = 0;
  126. for (let rowNum = startingRow; rowNum <= Math.min(endingRow, worksheetData.length); rowNum++) {
  127. const row = worksheetData[rowNum - 1]; // Convert to 0-based index
  128. if (!row || row.every(cell => cell === null || cell === undefined || cell === '')) {
  129. skippedRows++;
  130. logger.debug(`Skipping empty row ${rowNum}`);
  131. continue;
  132. }
  133. const rowData: Record<string, any> = {};
  134. let fieldsProcessed = 0;
  135. // Map cell values based on field configuration
  136. for (const field of section.fields || []) {
  137. try {
  138. const cellAddress = this.parseCellAddress(field.cellPosition);
  139. const cellValue = row[cellAddress.col - 1]; // Convert to 0-based index
  140. logger.debug(`Processing field`, {
  141. fieldName: field.name,
  142. cellPosition: field.cellPosition,
  143. cellAddress,
  144. rawValue: cellValue,
  145. rowNum
  146. });
  147. if (cellValue !== null && cellValue !== undefined && cellValue !== '') {
  148. const value = this.convertCellValue(
  149. cellValue,
  150. field.parsedType || FieldTypeEnum.String
  151. );
  152. logger.debug(`Value converted`, {
  153. fieldName: field.name,
  154. originalValue: cellValue,
  155. convertedValue: value,
  156. fieldType: field.parsedType || FieldTypeEnum.String
  157. });
  158. // Map to the correct column name for Prisma model
  159. const columnName = field.importTableColumnName;
  160. rowData[columnName] = value;
  161. fieldsProcessed++;
  162. }
  163. } catch (error) {
  164. logger.error(`Error processing field ${field.name} at row ${rowNum}`, {
  165. error: error instanceof Error ? error.message : String(error),
  166. field,
  167. rowNum
  168. });
  169. }
  170. }
  171. // Only add non-empty rows
  172. if (Object.keys(rowData).length > 0) {
  173. data.push(rowData);
  174. processedRows++;
  175. if (processedRows <= 5 || processedRows % 100 === 0) {
  176. logger.debug(`Row processed`, {
  177. rowNum,
  178. fieldsProcessed,
  179. rowDataKeys: Object.keys(rowData),
  180. dataLength: data.length
  181. });
  182. }
  183. } else {
  184. logger.debug(`Skipping row with no valid data`, { rowNum });
  185. }
  186. // Update progress every 100 rows
  187. if (rowNum % 100 === 0 || rowNum === Math.min(endingRow, worksheetData.length)) {
  188. onProgress({
  189. importId: 0,
  190. status: 'processing',
  191. currentSection: section.name,
  192. currentRow: rowNum - startingRow + 1,
  193. totalRows,
  194. errors: [],
  195. processedSections: sectionIndex,
  196. totalSections
  197. });
  198. }
  199. }
  200. const sectionTime = Date.now() - sectionStartTime;
  201. logger.info(`Section processing completed`, {
  202. sectionName: section.name,
  203. processedRows,
  204. skippedRows,
  205. totalRows,
  206. dataRows: data.length,
  207. processingTimeMs: sectionTime
  208. });
  209. const result = {
  210. id: section.id || 0,
  211. name: section.name || '',
  212. tableName: section.tableName || '',
  213. sheet: section.sheetName || '',
  214. type: section.type || '',
  215. startingRow,
  216. endingRow,
  217. parsedType: this.mapSectionType(section.type),
  218. fields: this.mapFields(section.fields || []),
  219. data
  220. };
  221. logger.debug('Section result', {
  222. sectionName: section.name,
  223. resultSummary: {
  224. id: result.id,
  225. name: result.name,
  226. tableName: result.tableName,
  227. dataRows: result.data.length,
  228. fields: result.fields.length
  229. }
  230. });
  231. return result;
  232. }
  233. private parseCellAddress(cellPosition: string): { row: number; col: number } {
  234. logger.debug(`Parsing cell address: ${cellPosition}`);
  235. const match = cellPosition.match(/([A-Z]+)(\d+)/);
  236. if (!match) {
  237. logger.warn(`Invalid cell position format: ${cellPosition}, using default 1,1`);
  238. return { row: 1, col: 1 };
  239. }
  240. const col = match[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1;
  241. const row = parseInt(match[2]);
  242. logger.debug(`Parsed cell address`, {
  243. original: cellPosition,
  244. row,
  245. col
  246. });
  247. return { row, col };
  248. }
  249. private mapSectionType(type: string): SectionTypeEnum {
  250. logger.debug(`Mapping section type: ${type}`);
  251. const mappedType = (() => {
  252. switch (type?.toLowerCase()) {
  253. case 'grid':
  254. return SectionTypeEnum.Grid;
  255. case 'properties':
  256. return SectionTypeEnum.Properties;
  257. default:
  258. return SectionTypeEnum.Unknown;
  259. }
  260. })();
  261. logger.debug(`Section type mapped`, {
  262. originalType: type,
  263. mappedType: SectionTypeEnum[mappedType]
  264. });
  265. return mappedType;
  266. }
  267. private mapFields(fields: any[]): LayoutSectionField[] {
  268. logger.debug(`Mapping ${fields.length} fields`);
  269. const mappedFields = fields.map((field, index) => {
  270. const mappedField = {
  271. id: field.id || index,
  272. cellPosition: field.cellPosition || '',
  273. name: field.name || '',
  274. dataType: field.dataType || 'string',
  275. dataTypeFormat: field.dataTypeFormat,
  276. importTableColumnName: field.importTableColumnName || field.name || `column_${index}`,
  277. importColumnOrderNumber: field.importColumnOrderNumber || index,
  278. parsedType: this.mapFieldType(field.dataType)
  279. };
  280. logger.debug(`Field mapped`, {
  281. index,
  282. originalName: field.name,
  283. mappedName: mappedField.name,
  284. cellPosition: mappedField.cellPosition,
  285. parsedType: FieldTypeEnum[mappedField.parsedType]
  286. });
  287. return mappedField;
  288. });
  289. return mappedFields;
  290. }
  291. private mapFieldType(dataType: string): FieldTypeEnum {
  292. const type = dataType?.toLowerCase();
  293. const mappedType = (() => {
  294. switch (type) {
  295. case 'time':
  296. return FieldTypeEnum.Time;
  297. case 'decimal':
  298. case 'number':
  299. case 'float':
  300. return FieldTypeEnum.Decimal;
  301. case 'date':
  302. return FieldTypeEnum.Date;
  303. case 'int':
  304. case 'integer':
  305. case 'numeric':
  306. return FieldTypeEnum.Numeric;
  307. default:
  308. return FieldTypeEnum.String;
  309. }
  310. })();
  311. logger.debug(`Field type mapped`, {
  312. originalDataType: dataType,
  313. mappedType: FieldTypeEnum[mappedType]
  314. });
  315. return mappedType;
  316. }
  317. private convertCellValue(value: any, fieldType: FieldTypeEnum): any {
  318. if (value === null || value === undefined) {
  319. logger.debug(`Converting null/undefined value to null`, { fieldType: FieldTypeEnum[fieldType] });
  320. return null;
  321. }
  322. logger.debug(`Converting cell value`, {
  323. originalValue: value,
  324. originalType: typeof value,
  325. targetFieldType: FieldTypeEnum[fieldType]
  326. });
  327. const convertedValue = (() => {
  328. switch (fieldType) {
  329. case FieldTypeEnum.Time:
  330. if (typeof value === 'number') {
  331. // Excel time is fraction of a day
  332. const result = value * 24 * 60 * 60 * 1000; // Convert to milliseconds
  333. logger.debug(`Time conversion`, { original: value, converted: result });
  334. return result;
  335. }
  336. return value;
  337. case FieldTypeEnum.Decimal:
  338. const decimalResult = parseFloat(value.toString()) || 0;
  339. logger.debug(`Decimal conversion`, { original: value, converted: decimalResult });
  340. return decimalResult;
  341. case FieldTypeEnum.Date:
  342. if (typeof value === 'number') {
  343. // Excel date is days since 1900-01-01
  344. const excelEpoch = new Date(1900, 0, 1);
  345. const dateResult = new Date(excelEpoch.getTime() + (value - 1) * 24 * 60 * 60 * 1000);
  346. logger.debug(`Date conversion`, { original: value, converted: dateResult });
  347. return dateResult;
  348. }
  349. const dateResult = new Date(value);
  350. logger.debug(`Date conversion from string`, { original: value, converted: dateResult });
  351. return dateResult;
  352. case FieldTypeEnum.Numeric:
  353. const numericResult = parseInt(value.toString()) || 0;
  354. logger.debug(`Numeric conversion`, { original: value, converted: numericResult });
  355. return numericResult;
  356. case FieldTypeEnum.String:
  357. default:
  358. const stringResult = value.toString();
  359. logger.debug(`String conversion`, { original: value, converted: stringResult });
  360. return stringResult;
  361. }
  362. })();
  363. return convertedValue;
  364. }
  365. }