Import large activity Excel in the browser via JSON chunks to avoid 502.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-03 18:28:47 +03:30
co-authored by Cursor
parent ac57b8ac33
commit cbfe470f33
11 changed files with 6451 additions and 31 deletions
+138
View File
@@ -0,0 +1,138 @@
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<string, string>,
mapping?: Record<string, string>,
) {
if (mapping) {
return mapping[raw] || mapping[normalizeHeader(raw)] || '';
}
const compact = normalizeHeader(raw);
return aliases[compact] ?? aliases[raw.trim()] ?? '';
}
/** خواندن اکسل در مرورگر — بدون آپلود فایل سنگین به API */
export async function previewExcelClient(
file: File,
aliases: Record<string, string>,
fields: ExcelFieldOption[],
): Promise<ExcelPreviewResult> {
const buffer = await file.arrayBuffer();
const wb = XLSX.read(buffer, { type: 'array', cellDates: true });
const sheetName = wb.SheetNames[0];
if (!sheetName) throw new Error('شیت اکسل پیدا نشد');
const sheet = wb.Sheets[sheetName]!;
const matrix = XLSX.utils.sheet_to_json<(string | number | boolean | Date | null)[]>(
sheet,
{ header: 1, defval: '', raw: false },
) as unknown[][];
const headerRow = (matrix[0] ?? []).map((c) => cellToString(c));
const columns: ExcelPreviewResult['columns'] = [];
const suggested: Record<string, string> = {};
const used = new Set<string>();
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;
for (let r = 1; 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: unrecognized || missingRequired || !mapped.size,
rowCount,
largeFile: rowCount >= 80,
};
}
export async function readMappedRowsClient(
file: File,
aliases: Record<string, string>,
mapping?: Record<string, string>,
): Promise<Record<string, string>[]> {
const buffer = await file.arrayBuffer();
const wb = XLSX.read(buffer, { type: 'array', cellDates: true });
const sheetName = wb.SheetNames[0];
if (!sheetName) throw new Error('شیت اکسل پیدا نشد');
const sheet = wb.Sheets[sheetName]!;
const matrix = XLSX.utils.sheet_to_json<unknown[]>(sheet, {
header: 1,
defval: '',
raw: false,
}) as unknown[][];
const headerRow = (matrix[0] ?? []).map((c) => cellToString(c));
const colMap = new Map<number, string>();
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<string, string>[] = [];
for (let r = 1; r < matrix.length; r += 1) {
const line = matrix[r] ?? [];
const item: Record<string, string> = {};
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;
}