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(//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 parseAmount(raw?: string): number | null { const text = toAsciiDigits(raw ?? '') .replace(/[,،]/g, '') .replace(/\s+/g, '') .trim(); if (!text) return null; const n = Number(text.replace(/[^\d.-]/g, '')); return Number.isFinite(n) ? n : null; } 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, ): Promise[]> { const sheet = await workbookFromUpload(file); const headerRow = sheet.getRow(1); const map = new Map(); 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[] = []; sheet.eachRow((row, rowNumber) => { if (rowNumber === 1) return; const item: Record = {}; 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>, ): Promise { 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); }