Support Didar activity Excel exports without header rows via positional columns.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -281,34 +281,76 @@ function resolveFieldKey(
|
||||
return aliases[compact] ?? aliases[raw.trim()] ?? aliases[normalizeHeader(raw)] ?? '';
|
||||
}
|
||||
|
||||
export type ExcelPositionalColumn = { key: string; label: string };
|
||||
|
||||
export type ExcelReadOptions = {
|
||||
/** اگر ردیف اول هدر نباشد، از این ترتیب ستون استفاده شود */
|
||||
positionalColumns?: ExcelPositionalColumn[];
|
||||
/** تشخیص فایل بدون هدر (مثل خروجی دیدار) */
|
||||
detectHeaderless?: (firstRowCells: string[]) => boolean;
|
||||
};
|
||||
|
||||
function readFirstRowCells(sheet: ExcelJS.Worksheet): string[] {
|
||||
const cells: string[] = [];
|
||||
const row = sheet.getRow(1);
|
||||
const max = Math.max(row.cellCount || 0, sheet.columnCount || 0, 22);
|
||||
for (let col = 1; col <= max; col += 1) {
|
||||
cells.push(cellStr(row.getCell(col).value));
|
||||
}
|
||||
while (cells.length && !cells[cells.length - 1]) cells.pop();
|
||||
return cells;
|
||||
}
|
||||
|
||||
export async function previewExcel(
|
||||
file: Express.Multer.File,
|
||||
aliases: Record<string, string>,
|
||||
fields: ExcelFieldOption[],
|
||||
options?: ExcelReadOptions,
|
||||
): Promise<ExcelPreviewResult> {
|
||||
const sheet = await workbookFromUpload(file);
|
||||
const headerRow = sheet.getRow(1);
|
||||
const firstCells = readFirstRowCells(sheet);
|
||||
const headerless =
|
||||
Boolean(options?.positionalColumns?.length) &&
|
||||
Boolean(options?.detectHeaderless?.(firstCells));
|
||||
|
||||
const columns: ExcelColumnPreview[] = [];
|
||||
const suggested: Record<string, string> = {};
|
||||
const used = new Set<string>();
|
||||
|
||||
headerRow.eachCell((cell, col) => {
|
||||
const header = cellStr(cell.value);
|
||||
if (!header) return;
|
||||
const samples: string[] = [];
|
||||
const last = Math.min(sheet.rowCount, 6);
|
||||
for (let r = 2; r <= last; r += 1) {
|
||||
const value = cellStr(sheet.getRow(r).getCell(col).value);
|
||||
if (value) samples.push(value);
|
||||
if (samples.length >= 3) break;
|
||||
}
|
||||
columns.push({ header, index: col, samples });
|
||||
const key = resolveFieldKey(header, aliases);
|
||||
if (key && !used.has(key)) {
|
||||
suggested[header] = key;
|
||||
used.add(key);
|
||||
}
|
||||
});
|
||||
if (headerless && options?.positionalColumns) {
|
||||
const last = Math.min(sheet.rowCount, 5);
|
||||
options.positionalColumns.forEach((colDef, idx) => {
|
||||
const col = idx + 1;
|
||||
const samples: string[] = [];
|
||||
for (let r = 1; r <= last; r += 1) {
|
||||
const value = cellStr(sheet.getRow(r).getCell(col).value);
|
||||
if (value) samples.push(value);
|
||||
if (samples.length >= 3) break;
|
||||
}
|
||||
columns.push({ header: colDef.label, index: col, samples });
|
||||
suggested[colDef.label] = colDef.key;
|
||||
used.add(colDef.key);
|
||||
});
|
||||
} else {
|
||||
const headerRow = sheet.getRow(1);
|
||||
headerRow.eachCell((cell, col) => {
|
||||
const header = cellStr(cell.value);
|
||||
if (!header) return;
|
||||
const samples: string[] = [];
|
||||
const last = Math.min(sheet.rowCount, 6);
|
||||
for (let r = 2; r <= last; r += 1) {
|
||||
const value = cellStr(sheet.getRow(r).getCell(col).value);
|
||||
if (value) samples.push(value);
|
||||
if (samples.length >= 3) break;
|
||||
}
|
||||
columns.push({ header, index: col, samples });
|
||||
const key = resolveFieldKey(header, aliases);
|
||||
if (key && !used.has(key)) {
|
||||
suggested[header] = key;
|
||||
used.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!columns.length) {
|
||||
throw new BadRequestException('ستون عنوان در ردیف اول فایل پیدا نشد');
|
||||
@@ -319,14 +361,16 @@ export async function previewExcel(
|
||||
const missingRequired = required.some((key) => !mapped.has(key));
|
||||
const unrecognized = columns.some((col) => !suggested[col.header]);
|
||||
|
||||
// برای فایلهای خیلی بزرگ (دهها هزار ردیف) پیمایش کامل کند است؛ rowCount شیت کافی است
|
||||
const rowCount = Math.max(0, (sheet.actualRowCount || sheet.rowCount || 1) - 1);
|
||||
const rawRows = Math.max(0, sheet.actualRowCount || sheet.rowCount || 0);
|
||||
const rowCount = headerless ? rawRows : Math.max(0, rawRows - 1);
|
||||
|
||||
return {
|
||||
columns,
|
||||
fields,
|
||||
suggested,
|
||||
needsMapping: unrecognized || missingRequired || !mapped.size,
|
||||
needsMapping: headerless
|
||||
? false
|
||||
: unrecognized || missingRequired || !mapped.size,
|
||||
rowCount,
|
||||
largeFile: rowCount >= EXCEL_IMPORT_LARGE_THRESHOLD,
|
||||
};
|
||||
@@ -336,16 +380,33 @@ export async function readMappedRows(
|
||||
file: Express.Multer.File,
|
||||
aliases: Record<string, string>,
|
||||
mapping?: Record<string, string>,
|
||||
options?: ExcelReadOptions,
|
||||
): Promise<Record<string, string>[]> {
|
||||
const sheet = await workbookFromUpload(file);
|
||||
const headerRow = sheet.getRow(1);
|
||||
const firstCells = readFirstRowCells(sheet);
|
||||
const headerless =
|
||||
!mapping &&
|
||||
Boolean(options?.positionalColumns?.length) &&
|
||||
Boolean(options?.detectHeaderless?.(firstCells));
|
||||
|
||||
const map = new Map<number, string>();
|
||||
headerRow.eachCell((cell, col) => {
|
||||
const raw = cellStr(cell.value);
|
||||
if (!raw) return;
|
||||
const key = resolveFieldKey(raw, aliases, mapping);
|
||||
if (key) map.set(col, key);
|
||||
});
|
||||
let dataStartRow = 2;
|
||||
|
||||
if (headerless && options?.positionalColumns) {
|
||||
options.positionalColumns.forEach((colDef, idx) => {
|
||||
map.set(idx + 1, colDef.key);
|
||||
});
|
||||
dataStartRow = 1;
|
||||
} else {
|
||||
const headerRow = sheet.getRow(1);
|
||||
headerRow.eachCell((cell, col) => {
|
||||
const raw = cellStr(cell.value);
|
||||
if (!raw) return;
|
||||
const key = resolveFieldKey(raw, aliases, mapping);
|
||||
if (key) map.set(col, key);
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.size) {
|
||||
throw new BadRequestException(
|
||||
'ستونهای فایل شناسایی نشد. ستونهای فایل را با فیلدهای سیستم تطبیق دهید.',
|
||||
@@ -354,7 +415,7 @@ export async function readMappedRows(
|
||||
|
||||
const rows: Record<string, string>[] = [];
|
||||
sheet.eachRow((row, rowNumber) => {
|
||||
if (rowNumber === 1) return;
|
||||
if (rowNumber < dataStartRow) return;
|
||||
const item: Record<string, string> = {};
|
||||
let any = false;
|
||||
for (const [col, key] of map) {
|
||||
|
||||
Reference in New Issue
Block a user