Creating import&export excel
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import ExcelJS from 'exceljs';
|
||||
|
||||
export function cellStr(value: ExcelJS.CellValue | undefined): string {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
return String(value).trim();
|
||||
}
|
||||
if (typeof value === 'object' && 'richText' in value && Array.isArray(value.richText)) {
|
||||
return value.richText.map((part) => part.text).join('').trim();
|
||||
}
|
||||
if (typeof value === 'object' && 'text' in value && typeof value.text === 'string') {
|
||||
return value.text.trim();
|
||||
}
|
||||
if (typeof value === 'object' && 'result' in value && value.result != null) {
|
||||
return String(value.result).trim();
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
return String(value).trim();
|
||||
}
|
||||
|
||||
export function normalizeHeader(raw: string) {
|
||||
return raw
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[_-]+/g, '')
|
||||
.replace(//g, '')
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/[()]/g, '');
|
||||
}
|
||||
|
||||
export function stripHtml(raw: string) {
|
||||
return raw
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function toAsciiDigits(value: string) {
|
||||
return value
|
||||
.replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d)))
|
||||
.replace(/[٠-٩]/g, (d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d)));
|
||||
}
|
||||
|
||||
function div(a: number, b: number) {
|
||||
return Math.trunc(a / b);
|
||||
}
|
||||
|
||||
function mod(a: number, b: number) {
|
||||
return a - Math.trunc(a / b) * b;
|
||||
}
|
||||
|
||||
export function toJalali(gy: number, gm: number, gd: number) {
|
||||
const gdm = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
||||
const gy2 = gm > 2 ? gy + 1 : gy;
|
||||
let days =
|
||||
355666 +
|
||||
365 * gy +
|
||||
div(gy2 + 3, 4) -
|
||||
div(gy2 + 99, 100) +
|
||||
div(gy2 + 399, 400) +
|
||||
gd +
|
||||
gdm[gm - 1];
|
||||
let jy = -1595 + 33 * div(days, 12053);
|
||||
days %= 12053;
|
||||
jy += 4 * div(days, 1461);
|
||||
days %= 1461;
|
||||
if (days > 365) {
|
||||
jy += div(days - 1, 365);
|
||||
days = (days - 1) % 365;
|
||||
}
|
||||
const jm = days < 186 ? 1 + div(days, 31) : 7 + div(days - 186, 30);
|
||||
const jd = 1 + (days < 186 ? days % 31 : (days - 186) % 30);
|
||||
return { jy, jm, jd };
|
||||
}
|
||||
|
||||
export function toGregorian(jy: number, jm: number, jd: number) {
|
||||
const jy2 = jy + 1595;
|
||||
let days =
|
||||
-355668 +
|
||||
365 * jy2 +
|
||||
div(jy2, 33) * 8 +
|
||||
div(mod(jy2, 33) + 3, 4) +
|
||||
jd +
|
||||
(jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186);
|
||||
let gy = 400 * div(days, 146097);
|
||||
days %= 146097;
|
||||
if (days > 36524) {
|
||||
gy += 100 * div(--days, 36524);
|
||||
days %= 36524;
|
||||
if (days >= 365) days += 1;
|
||||
}
|
||||
gy += 4 * div(days, 1461);
|
||||
days %= 1461;
|
||||
if (days > 365) {
|
||||
gy += div(days - 1, 365);
|
||||
days = (days - 1) % 365;
|
||||
}
|
||||
const gd = days + 1;
|
||||
const salA = [
|
||||
0,
|
||||
31,
|
||||
(gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0 ? 29 : 28,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
];
|
||||
let gm = 0;
|
||||
let gDay = gd;
|
||||
for (let i = 1; i <= 12; i += 1) {
|
||||
const v = salA[i]!;
|
||||
if (gDay <= v) {
|
||||
gm = i;
|
||||
break;
|
||||
}
|
||||
gDay -= v;
|
||||
}
|
||||
return { gy, gm, gd: gDay };
|
||||
}
|
||||
|
||||
export function formatJalaliDate(d: Date | string | null | undefined) {
|
||||
if (!d) return '';
|
||||
const date = typeof d === 'string' ? new Date(d) : d;
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
const { jy, jm, jd } = toJalali(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||
return `${jy}/${String(jm).padStart(2, '0')}/${String(jd).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function formatClock(d: Date | string | null | undefined) {
|
||||
if (!d) return '';
|
||||
const date = typeof d === 'string' ? new Date(d) : d;
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function parseJalaliDateTime(dateRaw?: string, timeRaw?: string): Date | null {
|
||||
const dateText = toAsciiDigits(dateRaw ?? '').trim();
|
||||
if (!dateText) return null;
|
||||
const match = /^(\d{3,4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/.exec(dateText);
|
||||
if (!match) {
|
||||
const fallback = new Date(dateText);
|
||||
return Number.isNaN(fallback.getTime()) ? null : fallback;
|
||||
}
|
||||
const jy = Number(match[1]);
|
||||
const jm = Number(match[2]);
|
||||
const jd = Number(match[3]);
|
||||
const { gy, gm, gd } = toGregorian(jy, jm, jd);
|
||||
let hh = 0;
|
||||
let mm = 0;
|
||||
const timeText = toAsciiDigits(timeRaw ?? '').trim();
|
||||
const tm = /^(\d{1,2}):(\d{2})/.exec(timeText);
|
||||
if (tm) {
|
||||
hh = Number(tm[1]);
|
||||
mm = Number(tm[2]);
|
||||
}
|
||||
return new Date(gy, gm - 1, gd, hh, mm, 0, 0);
|
||||
}
|
||||
|
||||
export async function workbookFromUpload(file: Express.Multer.File | undefined) {
|
||||
if (!file?.buffer?.length) {
|
||||
throw new BadRequestException('فایل اکسل انتخاب نشده است');
|
||||
}
|
||||
const wb = new ExcelJS.Workbook();
|
||||
await wb.xlsx.load(Uint8Array.from(file.buffer) as unknown as ExcelJS.Buffer);
|
||||
const sheet = wb.worksheets[0];
|
||||
if (!sheet) throw new BadRequestException('شیت اکسل پیدا نشد');
|
||||
return sheet;
|
||||
}
|
||||
|
||||
export async function readMappedRows(
|
||||
file: Express.Multer.File,
|
||||
aliases: Record<string, string>,
|
||||
): Promise<Record<string, string>[]> {
|
||||
const sheet = await workbookFromUpload(file);
|
||||
const headerRow = sheet.getRow(1);
|
||||
const map = new Map<number, string>();
|
||||
headerRow.eachCell((cell, col) => {
|
||||
const raw = cellStr(cell.value);
|
||||
if (!raw) return;
|
||||
const compact = normalizeHeader(raw);
|
||||
const key = aliases[compact] ?? aliases[raw.trim()] ?? aliases[normalizeHeader(raw)];
|
||||
if (key) map.set(col, key);
|
||||
});
|
||||
if (!map.size) {
|
||||
throw new BadRequestException(
|
||||
'ستونهای فایل شناسایی نشد. از قالب خروجی همین سیستم یا خروجی دیدار استفاده کنید.',
|
||||
);
|
||||
}
|
||||
|
||||
const rows: Record<string, string>[] = [];
|
||||
sheet.eachRow((row, rowNumber) => {
|
||||
if (rowNumber === 1) return;
|
||||
const item: Record<string, string> = {};
|
||||
let any = false;
|
||||
for (const [col, key] of map) {
|
||||
const value = cellStr(row.getCell(col).value);
|
||||
if (value) any = true;
|
||||
if (value || item[key] == null) item[key] = value;
|
||||
}
|
||||
if (any) rows.push(item);
|
||||
});
|
||||
if (!rows.length) {
|
||||
throw new BadRequestException('هیچ ردیف دادهای در فایل نیست');
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function writeXlsx(
|
||||
sheetName: string,
|
||||
columns: Array<{ header: string; key: string; width?: number }>,
|
||||
rows: Array<Record<string, string | number | null | undefined>>,
|
||||
): Promise<Buffer> {
|
||||
const wb = new ExcelJS.Workbook();
|
||||
const sheet = wb.addWorksheet(sheetName);
|
||||
sheet.columns = columns;
|
||||
for (const row of rows) sheet.addRow(row);
|
||||
const buf = await wb.xlsx.writeBuffer();
|
||||
return Buffer.from(buf);
|
||||
}
|
||||
Reference in New Issue
Block a user