Files
CRM_Montzar/apps/web/src/lib/excel-client.ts
T

177 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()] ?? '';
}
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<unknown[]>(sheet, {
header: 1,
defval: '',
raw: false,
}) as unknown[][];
}
/** خواندن اکسل در مرورگر — بدون آپلود فایل سنگین به API */
export async function previewExcelClient(
file: File,
aliases: Record<string, string>,
fields: ExcelFieldOption[],
options?: ClientExcelReadOptions,
): Promise<ExcelPreviewResult> {
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<string, string> = {};
const used = new Set<string>();
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<string, string>,
mapping?: Record<string, string>,
options?: ClientExcelReadOptions,
): Promise<Record<string, string>[]> {
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<number, string>();
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<string, string>[] = [];
for (let r = dataStart; 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;
}