import * as XLSX from 'xlsx'; import type { ExcelFieldOption, ExcelPreviewResult } from '@mento/shared'; function normalizeHeader(raw: string) { return raw .trim() .toLowerCase() .replace(/[_-]+/g, '') .replace(/‌/g, '') .replace(/\s+/g, '') .replace(/[()]/g, ''); } function cellToString(value: unknown): string { if (value == null) return ''; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return String(value).trim(); } if (value instanceof Date) { return Number.isNaN(value.getTime()) ? '' : value.toISOString(); } return String(value).trim(); } function resolveFieldKey( raw: string, aliases: Record, mapping?: Record, ) { if (mapping) { return mapping[raw] || mapping[normalizeHeader(raw)] || ''; } const compact = normalizeHeader(raw); return aliases[compact] ?? aliases[raw.trim()] ?? ''; } export type ClientExcelPositionalColumn = { key: string; label: string }; export type ClientExcelReadOptions = { positionalColumns?: ClientExcelPositionalColumn[]; detectHeaderless?: (firstRowCells: string[]) => boolean; }; function readMatrix(fileBuffer: ArrayBuffer) { const wb = XLSX.read(fileBuffer, { type: 'array', cellDates: true }); const sheetName = wb.SheetNames[0]; if (!sheetName) throw new Error('شیت اکسل پیدا نشد'); const sheet = wb.Sheets[sheetName]!; return XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', raw: false, }) as unknown[][]; } /** خواندن اکسل در مرورگر — بدون آپلود فایل سنگین به API */ export async function previewExcelClient( file: File, aliases: Record, fields: ExcelFieldOption[], options?: ClientExcelReadOptions, ): Promise { const matrix = readMatrix(await file.arrayBuffer()); const headerRow = (matrix[0] ?? []).map((c) => cellToString(c)); const headerless = Boolean(options?.positionalColumns?.length) && Boolean(options?.detectHeaderless?.(headerRow)); const columns: ExcelPreviewResult['columns'] = []; const suggested: Record = {}; const used = new Set(); if (headerless && options?.positionalColumns) { options.positionalColumns.forEach((colDef, idx) => { const samples: string[] = []; for (let r = 0; r < Math.min(matrix.length, 5); r += 1) { const value = cellToString(matrix[r]?.[idx]); if (value) samples.push(value); if (samples.length >= 3) break; } columns.push({ header: colDef.label, index: idx + 1, samples }); suggested[colDef.label] = colDef.key; used.add(colDef.key); }); } else { headerRow.forEach((header, idx) => { if (!header) return; const samples: string[] = []; for (let r = 1; r < Math.min(matrix.length, 6); r += 1) { const value = cellToString(matrix[r]?.[idx]); if (value) samples.push(value); if (samples.length >= 3) break; } columns.push({ header, index: idx + 1, samples }); const key = resolveFieldKey(header, aliases); if (key && !used.has(key)) { suggested[header] = key; used.add(key); } }); } if (!columns.length) throw new Error('ستون عنوان در ردیف اول فایل پیدا نشد'); let rowCount = 0; const start = headerless ? 0 : 1; for (let r = start; r < matrix.length; r += 1) { const row = matrix[r] ?? []; if (row.some((c) => cellToString(c))) rowCount += 1; } const required = fields.filter((f) => f.required).map((f) => f.key); const mapped = new Set(Object.values(suggested)); const missingRequired = required.some((key) => !mapped.has(key)); const unrecognized = columns.some((col) => !suggested[col.header]); return { columns, fields, suggested, needsMapping: headerless ? false : unrecognized || missingRequired || !mapped.size, rowCount, largeFile: rowCount >= 80, }; } export async function readMappedRowsClient( file: File, aliases: Record, mapping?: Record, options?: ClientExcelReadOptions, ): Promise[]> { const matrix = readMatrix(await file.arrayBuffer()); const headerRow = (matrix[0] ?? []).map((c) => cellToString(c)); const headerless = !mapping && Boolean(options?.positionalColumns?.length) && Boolean(options?.detectHeaderless?.(headerRow)); const colMap = new Map(); let dataStart = 1; if (headerless && options?.positionalColumns) { options.positionalColumns.forEach((colDef, idx) => { colMap.set(idx, colDef.key); }); dataStart = 0; } else { headerRow.forEach((header, idx) => { if (!header) return; const key = resolveFieldKey(header, aliases, mapping); if (key) colMap.set(idx, key); }); } if (!colMap.size) { throw new Error('ستون‌های فایل شناسایی نشد. ستون‌های فایل را با فیلدهای سیستم تطبیق دهید.'); } const rows: Record[] = []; for (let r = dataStart; r < matrix.length; r += 1) { const line = matrix[r] ?? []; const item: Record = {}; let any = false; for (const [idx, key] of colMap) { const value = cellToString(line[idx]); if (value) any = true; item[key] = value; } if (any) rows.push(item); } if (!rows.length) throw new Error('هیچ ردیف داده‌ای در فایل نیست'); return rows; }