diff --git a/.env.example b/.env.example index f71d455..d5c3b24 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,16 @@ DATABASE_URL=mysql://jorgecuadros:jorgecuadros@localhost:3306/jorgecuadros SESSION_SECRET=change-me-to-a-random-string WEB_ORIGIN=http://localhost:3000 NEXT_PUBLIC_API_ORIGIN=http://localhost:3001 + +# Company info — printed in the header of every report (PDF + browser +# print). Leave blank to use the placeholders. COMPANY_LOGO_PATH is +# optional; when unset the API falls back to apps/api/assets/company_logo.png. +COMPANY_NAME=Jorge Cuadros & Asociados +COMPANY_ADDRESS_LINE1= +COMPANY_ADDRESS_LINE2= +COMPANY_CITY_STATE= +COMPANY_PHONE= +COMPANY_EMAIL= +COMPANY_TAX_ID= +COMPANY_WEBSITE= +COMPANY_LOGO_PATH= diff --git a/apps/api/assets/company_logo.png b/apps/api/assets/company_logo.png new file mode 100644 index 0000000..b591e6e Binary files /dev/null and b/apps/api/assets/company_logo.png differ diff --git a/apps/api/package.json b/apps/api/package.json index 05584eb..7dfd0ee 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -21,7 +21,9 @@ "argon2": "^0.41.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "exceljs": "^4.4.0", "express-session": "^1.18.0", + "pdfkit": "^0.15.1", "passport": "^0.7.0", "passport-local": "^1.0.0", "reflect-metadata": "^0.2.2", @@ -32,6 +34,7 @@ "@nestjs/testing": "^10.4.4", "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", + "@types/pdfkit": "^0.13.5", "@types/jest": "^29.5.13", "@types/node": "^20.16.11", "@types/passport": "^1.0.17", diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 478ce06..75d9f84 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -11,6 +11,7 @@ import { PropertiesModule } from "./properties/properties.module"; import { BillingModule } from "./billing/billing.module"; import { BankModule } from "./bank/bank.module"; import { OpsModule } from "./ops/ops.module"; +import { ReportsModule } from "./reports/reports.module"; import { AppController } from "./app.controller"; @Module({ @@ -27,6 +28,7 @@ import { AppController } from "./app.controller"; BillingModule, BankModule, OpsModule, + ReportsModule, ], controllers: [AppController], }) diff --git a/apps/api/src/reports/company.ts b/apps/api/src/reports/company.ts new file mode 100644 index 0000000..6bfc0ac --- /dev/null +++ b/apps/api/src/reports/company.ts @@ -0,0 +1,103 @@ +/** + * Company info used on every report header (PDF + print). Read from + * the environment so the office can edit it without a code change — + * the .env.example file lists the keys; defaults below are placeholders + * the office should override for production. + * + * Single source of truth: the API renders the header. The web header + * (login + AppShell) still reads the static "Jorge Cuadros & Asociados" + * strings for now — those are visual brand, the API's COMPANY_INFO + * block is the legal/locator block on printed documents. + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; + +export interface CompanyInfo { + name: string; + /** Street address — line 1. */ + addressLine1: string; + /** Street address — line 2 (suite, floor, etc.). Optional. */ + addressLine2: string; + /** "City, State, ZIP, Country" — single line. */ + cityState: string; + phone: string; + email: string; + /** Mexican tax ID ("RFC"). Optional. */ + taxId: string; + website: string; + /** Absolute path to the logo PNG. Null when missing — renderers fall + * back to a text mark. */ + logoPath: string | null; + /** Logo buffer + intrinsic size, eagerly loaded so the PDF renderer + * doesn't do a sync read on every report. Null when no logo. */ + logo: { buffer: Buffer; width: number; height: number } | null; +} + +function envOr(key: string, fallback: string): string { + const v = process.env[key]; + return v && v.trim() ? v : fallback; +} + +function resolveLogoPath(): string | null { + const explicit = process.env.COMPANY_LOGO_PATH; + if (explicit) { + return fs.existsSync(explicit) ? explicit : null; + } + // Default: look in apps/api/assets/company_logo.png (copied from + // apps/web/public/images/company_logo.png — single canonical image + // kept in lock-step; see .env.example for the override path). + const candidates = [ + path.resolve(__dirname, "..", "..", "assets", "company_logo.png"), + path.resolve(__dirname, "..", "..", "..", "web", "public", "images", "company_logo.png"), + ]; + for (const c of candidates) { + if (fs.existsSync(c)) return c; + } + return null; +} + +let cached: CompanyInfo | null = null; + +export function getCompanyInfo(): CompanyInfo { + if (cached) return cached; + const logoPath = resolveLogoPath(); + let logo: CompanyInfo["logo"] = null; + if (logoPath) { + try { + const buf = fs.readFileSync(logoPath); + // Intrinsic PNG size: read IHDR (bytes 16-23 of the file). + // Width = BE uint32 at offset 16, height = BE uint32 at offset 20. + const w = + logoPath.endsWith(".png") && buf.length >= 24 + ? buf.readUInt32BE(16) + : 0; + const h = + logoPath.endsWith(".png") && buf.length >= 24 + ? buf.readUInt32BE(20) + : 0; + logo = { buffer: buf, width: w, height: h }; + } catch { + logo = null; + } + } + cached = { + name: envOr("COMPANY_NAME", "Jorge Cuadros & Asociados"), + addressLine1: envOr( + "COMPANY_ADDRESS_LINE1", + "Av. Revolución 1234, Int. 5", + ), + addressLine2: envOr("COMPANY_ADDRESS_LINE2", ""), + cityState: envOr( + "COMPANY_CITY_STATE", + "Tijuana, Baja California 22000, México", + ), + phone: envOr("COMPANY_PHONE", "(664) 000-0000"), + email: envOr("COMPANY_EMAIL", "contacto@jorgecuadros.local"), + taxId: envOr("COMPANY_TAX_ID", ""), + website: envOr("COMPANY_WEBSITE", "jorgecuadros.local"), + logoPath, + logo, + }; + return cached; +} diff --git a/apps/api/src/reports/outputs.ts b/apps/api/src/reports/outputs.ts new file mode 100644 index 0000000..7c6dad9 --- /dev/null +++ b/apps/api/src/reports/outputs.ts @@ -0,0 +1,433 @@ +/** + * Output renderers for the reports module. + * + * Every report's `run` returns `{ columns, rows, totals?, subtitle? }`. + * CSV/XLSX/PDF all derive from the same shape so adding a report = one + * registry entry, no per-format template. + * + * PDF uses pdfkit. The statement format (edo-cuenta-datos) uses a + * different layout than the tabular one — handled inline. + */ + +// pdfkit exports its constructor via `module.exports = PDFDocument`, so a +// namespace import gets the type, and `import = require()` gets the value. +import PDFDocument = require("pdfkit"); +import * as ExcelJS from "exceljs"; +import type { ColumnDef, ReportResult } from "./reports.types"; +import { getCompanyInfo } from "./company"; + +type Doc = PDFKit.PDFDocument; + +/* ----------------------------------------------------------------- CSV */ + +function csvCell(v: unknown): string { + if (v === null || v === undefined) return ""; + const s = String(v); + if (s.includes(",") || s.includes('"') || s.includes("\n") || s.includes("\r")) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +export function renderCsv(columns: ColumnDef[], result: ReportResult): string { + const headers = columns.map((c) => csvCell(c.label)).join(","); + const lines = result.rows.map((r) => + columns + .map((c) => { + const v = r[c.key]; + if (typeof v === "number") return v; + return csvCell(v); + }) + .join(","), + ); + const totals: string[] = []; + if (result.totals) { + for (const [k, v] of Object.entries(result.totals)) { + totals.push(csvCell(k), csvCell(v)); + } + } + return [headers, ...lines, ...(totals.length ? [totals.join(",")] : [])].join( + "\n", + ); +} + +/* ----------------------------------------------------------------- XLSX */ + +export async function renderXlsx( + columns: ColumnDef[], + result: ReportResult, +): Promise { + const wb = new ExcelJS.Workbook(); + wb.creator = "Jorge Cuadros & Asociados"; + const ws = wb.addWorksheet("Reporte", { + views: [{ state: "frozen", ySplit: 1 }], + }); + ws.columns = columns.map((c) => ({ + header: c.label, + key: c.key, + width: Math.max(10, Math.min(40, (c.label.length + 2) * 1.2)), + })); + ws.getRow(1).font = { bold: true }; + ws.getRow(1).fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFE2EDE9" }, // brand-tint + }; + for (const row of result.rows) { + ws.addRow(row); + } + // Number formatting for money columns. + for (const col of columns) { + if (col.type === "money" || col.type === "number") { + ws.getColumn(col.key).numFmt = + col.type === "money" ? "#,##0.00" : "#,##0"; + ws.getColumn(col.key).alignment = { horizontal: "right" }; + } + } + if (result.totals) { + const last = ws.addRow({}); + let i = 1; + for (const [k, v] of Object.entries(result.totals)) { + const cell = ws.getCell(last.number, i); + cell.value = `${k}: ${v}`; + cell.font = { bold: true }; + i++; + } + } + const buf = await wb.xlsx.writeBuffer(); + return Buffer.from(buf); +} + +/* ----------------------------------------------------------------- PDF */ + +const BRAND = "#0c322d"; +const ACCENT = "#bf5a34"; +const MUTED = "#756c5c"; +const LINE = "#e4dccb"; + +function fmtMoney(v: unknown): string { + if (v === null || v === undefined || v === "") return ""; + const n = Number(v); + if (!Number.isFinite(n)) return String(v); + return n.toLocaleString("es-MX", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function pdfRow( + doc: Doc, + y: number, + cols: Array<{ label: string; width: number; align?: "left" | "right" }>, + values: Array<{ text: string; align?: "left" | "right" }>, + x: number, +): number { + let cx = x; + for (let i = 0; i < cols.length; i++) { + const c = cols[i]; + const v = values[i] ?? { text: "" }; + const align = v.align ?? c.align ?? "left"; + const w = c.width; + doc + .font("Helvetica") + .fontSize(9) + .fillColor("#211d17") + .text(v.text, cx, y, { + width: w - 4, + align, + ellipsis: true, + lineBreak: false, + height: 16, + }); + cx += w; + } + return y + 18; +} + +export function renderPdf( + columns: ColumnDef[], + result: ReportResult, + title: string, +): Promise { + return new Promise((resolve, reject) => { + const doc = new PDFDocument({ + size: "LETTER", + layout: "landscape", + margins: { top: 96, bottom: 56, left: 48, right: 48 }, + bufferPages: true, + info: { + Title: title, + Author: "Jorge Cuadros & Asociados", + Subject: "Reporte", + Creator: "Jorge Cuadros Platform — Reports module", + }, + }); + const chunks: Buffer[] = []; + doc.on("data", (c: Buffer) => chunks.push(c)); + doc.on("end", () => resolve(Buffer.concat(chunks))); + doc.on("error", reject); + + const company = getCompanyInfo(); + const pageW = doc.page.width - 96; + + /** The header is repeated on every page (via addPage + manual draw). */ + const drawHeader = () => { + // Background bar (brand pine) for the masthead. + doc.rect(0, 0, doc.page.width, 60).fill(BRAND); + + // Logo, fitted to a 40px box, with 8px padding. + let textX = 48; + if (company.logo) { + const targetH = 40; + const scale = targetH / company.logo.height; + const w = company.logo.width * scale; + doc.image(company.logo.buffer, 48, 10, { height: targetH }); + textX = 48 + w + 14; + } + + // Company name (large) + "Reporte" tag below it. + doc + .fillColor("#f5f1e8") + .font("Helvetica-Bold") + .fontSize(15) + .text(company.name, textX, 14, { width: pageW - (textX - 48), lineBreak: false }); + doc + .font("Helvetica") + .fontSize(8) + .fillColor("#cde0db") + .text("Reporte", textX, 36, { lineBreak: false }); + + // Right-aligned company locator (address + phone + email). + const rightLines = [ + company.addressLine1, + company.addressLine2, + [company.cityState].filter(Boolean).join(" · "), + [company.phone, company.email].filter(Boolean).join(" · "), + company.taxId ? `RFC: ${company.taxId}` : "", + ].filter(Boolean); + doc.font("Helvetica").fontSize(8).fillColor("#cde0db"); + let ry = 12; + for (const line of rightLines) { + doc.text(line, 48, ry, { + width: pageW, + align: "right", + lineBreak: false, + ellipsis: true, + }); + ry += 10; + } + + // Thin accent line under the masthead. + doc.rect(0, 60, doc.page.width, 2).fill(ACCENT); + + // Title + subtitle + printed-at. + doc + .font("Helvetica-Bold") + .fontSize(15) + .fillColor(BRAND) + .text(title, 48, 72, { lineBreak: false }); + let metaY = 92; + if (result.subtitle) { + doc + .font("Helvetica") + .fontSize(9) + .fillColor(MUTED) + .text(result.subtitle, 48, metaY, { lineBreak: false }); + metaY += 12; + } + const printedAt = new Date().toLocaleString("es-MX"); + doc + .font("Helvetica") + .fontSize(8) + .fillColor(MUTED) + .text(`Impreso: ${printedAt}`, 48, metaY, { lineBreak: false }); + }; + + drawHeader(); + + // Column widths: distribute page width minus margins, weighted. + const totalW = columns.reduce((s, c) => s + (c.width ?? 12), 0); + const cols = columns.map((c) => ({ + label: c.label, + width: ((c.width ?? 12) / totalW) * pageW, + align: c.align, + })); + + let y = 130; + const drawTableHeader = () => { + doc.rect(48, y, pageW, 18).fill("#faf6ee"); + y = pdfRow( + doc, + y + 4, + cols, + cols.map((c) => ({ text: c.label, align: c.align })), + 48, + ); + doc + .moveTo(48, y) + .lineTo(48 + pageW, y) + .strokeColor(LINE) + .lineWidth(0.5) + .stroke(); + }; + drawTableHeader(); + + // Body rows. + for (const r of result.rows) { + if (y > doc.page.height - 64) { + doc.addPage({ layout: "landscape", margins: { top: 96, bottom: 56, left: 48, right: 48 } }); + drawHeader(); + y = 130; + drawTableHeader(); + } + const vals = columns.map((c) => { + const v = r[c.key]; + const text = c.type === "money" ? fmtMoney(v) : v == null ? "" : String(v); + return { text, align: c.align }; + }); + y = pdfRow(doc, y + 4, cols, vals, 48); + doc + .moveTo(48, y) + .lineTo(48 + pageW, y) + .strokeColor("#e4dccb") + .lineWidth(0.4) + .stroke(); + } + + // Totals. + if (result.totals) { + y += 6; + doc.rect(48, y, pageW, 18).fill(ACCENT); + doc + .font("Helvetica-Bold") + .fontSize(9) + .fillColor("#f5f1e8") + .text( + Object.entries(result.totals) + .map(([k, v]) => `${k}: ${v}`) + .join(" · "), + 52, + y + 5, + { width: pageW - 8, align: "left" }, + ); + } + + doc.end(); + }); +} + +/* ----------------------------------------------------------------- print (HTML) */ + +/** + * Print-stylesheet-friendly HTML. The web app's print stylesheet hides + * nav, but otherwise this is a plain table the browser paginates itself. + * + * The header carries the company info (logo + name + locator) so printed + * pages stand alone — staff can hand one to a customer and the office + * identification is on every sheet, not buried in the cover page. + */ +export function renderPrintHtml( + columns: ColumnDef[], + result: ReportResult, + title: string, +): string { + const company = getCompanyInfo(); + const head = (label: string, align?: "left" | "right") => + `${escapeHtml(label)}`; + const cell = (v: unknown, c: ColumnDef) => { + const text = c.type === "money" ? fmtMoney(v) : v == null ? "" : String(v); + const align = c.align ?? "left"; + return `${escapeHtml(text)}`; + }; + const rows = result.rows + .map( + (r) => + `${columns + .map((c) => cell(r[c.key], c)) + .join("")}`, + ) + .join(""); + const totals = result.totals + ? `${Object.entries( + result.totals, + ) + .map(([k, v]) => `${escapeHtml(k)}: ${escapeHtml(String(v))}`) + .join("  ·  ")}` + : ""; + + // Logo embedded as base64 data URL — the print page is opened as a + // new tab and printed standalone, so a relative path to the web app + // wouldn't resolve when launched outside the web's origin. + const logoDataUrl = company.logo + ? `data:image/png;base64,${company.logo.buffer.toString("base64")}` + : null; + + const locatorLines = [ + company.addressLine1, + company.addressLine2, + company.cityState, + [company.phone, company.email].filter(Boolean).join(" · "), + company.taxId ? `RFC: ${company.taxId}` : "", + ].filter(Boolean); + + return ` + + +${escapeHtml(title)} — ${escapeHtml(company.name)} + + +
+
+ ${logoDataUrl ? `` : ""} +
+
${escapeHtml(company.name)}
+
Reporte
+
+
+ ${locatorLines.map((l) => escapeHtml(l)).join("
")} + ${company.website ? `
${escapeHtml(company.website)}` : ""} +
+
+
+
+

${escapeHtml(title)}

+ ${result.subtitle ? `

${escapeHtml(result.subtitle)}

` : ""} +

Impreso: ${new Date().toLocaleString("es-MX")}

+
+ + ${columns.map((c) => head(c.label, c.align)).join("")} + ${rows}${totals} +
+ +`; +} + +function escapeHtml(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} diff --git a/apps/api/src/reports/reports.controller.ts b/apps/api/src/reports/reports.controller.ts new file mode 100644 index 0000000..8dcd1b5 --- /dev/null +++ b/apps/api/src/reports/reports.controller.ts @@ -0,0 +1,130 @@ +import { + Body, + Controller, + Get, + Header, + Param, + Post, + Query, + Res, + UseGuards, +} from "@nestjs/common"; +import type { Response } from "express"; +import { AuthenticatedGuard } from "../auth/authenticated.guard"; +import { ReportsService } from "./reports.service"; +import { + renderCsv, + renderPdf, + renderPrintHtml, + renderXlsx, +} from "./outputs"; +import { findReport } from "./reports.registry"; + +/** + * Reports routes. Every report is dispatched by slug; outputs are + * differentiated by `?format=...` (default `json`). Reads only — gated + * by AuthenticatedGuard alone, like every other read in the app. + */ +@UseGuards(AuthenticatedGuard) +@Controller("reports") +export class ReportsController { + constructor(private readonly reports: ReportsService) {} + + /** Catalog of all registered reports (the /reportes index). */ + @Get() + catalog() { + return { items: this.reports.catalog() }; + } + + /** Run a report and return the JSON result (rows + totals + the def's columns). */ + @Get(":slug") + async runJson( + @Param("slug") slug: string, + @Query() query: Record, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + return { ...result, columns: def?.columns ?? [] }; + } + + /** CSV download. */ + @Get(":slug/csv") + @Header("Content-Type", "text/csv; charset=utf-8") + async runCsv( + @Param("slug") slug: string, + @Query() query: Record, + @Res() res: Response, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + const filename = `${def?.title ?? slug}-${new Date().toISOString().slice(0, 10)}.csv`; + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename.replace(/[^\wÀ-ſ .-]/g, "_")}"`, + ); + res.send(renderCsv(def?.columns ?? [], result)); + } + + /** XLSX download. */ + @Get(":slug/xlsx") + async runXlsx( + @Param("slug") slug: string, + @Query() query: Record, + @Res() res: Response, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + const filename = `${def?.title ?? slug}-${new Date().toISOString().slice(0, 10)}.xlsx`; + const buf = await renderXlsx(def?.columns ?? [], result); + res.setHeader( + "Content-Type", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ); + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename.replace(/[^\wÀ-ſ .-]/g, "_")}"`, + ); + res.send(buf); + } + + /** PDF download. */ + @Get(":slug/pdf") + async runPdf( + @Param("slug") slug: string, + @Query() query: Record, + @Res() res: Response, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + const buf = await renderPdf(def?.columns ?? [], result, def?.title ?? slug); + const filename = `${def?.title ?? slug}-${new Date().toISOString().slice(0, 10)}.pdf`; + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename.replace(/[^\wÀ-ſ .-]/g, "_")}"`, + ); + res.send(buf); + } + + /** Browser-printable HTML view (the user hits Print → Save as PDF). */ + @Get(":slug/print") + @Header("Content-Type", "text/html; charset=utf-8") + async runPrint( + @Param("slug") slug: string, + @Query() query: Record, + ) { + const def = findReport(slug); + const result = await this.reports.run(slug, query); + return renderPrintHtml(def?.columns ?? [], result, def?.title ?? slug); + } + + /** POST a customer-picker-driven report (statement). Mirrors GET to keep + * the param contract simple: same body shape, same response. */ + @Post(":slug") + async runPost( + @Param("slug") slug: string, + @Body() body: Record, + ) { + return this.reports.run(slug, body); + } +} diff --git a/apps/api/src/reports/reports.module.ts b/apps/api/src/reports/reports.module.ts new file mode 100644 index 0000000..e13cbd7 --- /dev/null +++ b/apps/api/src/reports/reports.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { ReportsController } from "./reports.controller"; +import { ReportsService } from "./reports.service"; + +@Module({ + controllers: [ReportsController], + providers: [ReportsService], +}) +export class ReportsModule {} diff --git a/apps/api/src/reports/reports.registry.ts b/apps/api/src/reports/reports.registry.ts new file mode 100644 index 0000000..d759e97 --- /dev/null +++ b/apps/api/src/reports/reports.registry.ts @@ -0,0 +1,763 @@ +/** + * The catalog. One entry per report. Adding a new report is one new entry + * here — no new route, no new page, no new component. + * + * Filter behavior to keep in mind: + * - Currency balances are NEVER summed across currencies (912 customers + * carry both MXN and USD; the legacy data has no FX per row, so any + * cross-currency total would be invented). Every report that touches + * the ledger accepts a `currency` filter and reports per currency. + * - Voided transactions must be excluded from totals (NOT_VOIDED). The + * UI still shows them struck-through; the SQL drops them. + * - The legacy `REPORTE DE EFECTIVO` covered cash receipts only. In the + * new schema those are `Transaction` rows with `legacySourceTable` in + * the EFECTIVO* set OR `checkNumber` null + amount > 0 (true cash). + */ + +import { Prisma } from "@jorgecuadros/database"; +import { + intParam, + NOT_VOIDED, + parseDate, + type ReportDef, +} from "./reports.types"; + +/* ------------------------------------------------------------------ helpers */ + +function nameOf(c: { name: string; nameMissing: boolean }): string { + return c.nameMissing ? "(sin nombre)" : c.name; +} + +/* ------------------------------------------------------------------ reports */ + +/** + * LISTADO EN ROJO — overdue customers worklist. + * Same data as the receivables worklist with balance=owing, but presented + * as a printable report rather than a paginated browser. + */ +const listadoEnRojo: ReportDef = { + slug: "listado-en-rojo", + title: "Clientes en rojo", + description: + "Cartera vencida: clientes con saldo deudor en la moneda seleccionada, " + + "ordenados del más antiguo al más reciente.", + domain: "estado-cuenta", + legacyName: "LISTADO EN ROJO", + format: "tabular", + params: [ + { + key: "currency", + label: "Moneda", + kind: "select", + options: [ + { value: "MXN", label: "MXN" }, + { value: "USD", label: "USD" }, + ], + defaultValue: "MXN", + }, + { + key: "query", + label: "Buscar (nombre o ciudad)", + kind: "text", + placeholder: "Ej. Pérez, Tijuana…", + }, + ], + columns: [ + { key: "id", label: "#", type: "text" }, + { key: "name", label: "Cliente", type: "text" }, + { key: "city", label: "Ciudad", type: "text" }, + { key: "movements", label: "Movs.", type: "number", align: "right" }, + { key: "balance", label: "Saldo", type: "money", align: "right" }, + { key: "lastMovement", label: "Último movimiento", type: "date" }, + ], + async run(prisma, p) { + const currency = (p.currency === "USD" ? "USD" : "MXN") as "MXN" | "USD"; + const q = p.query?.trim(); + const nameFilter = q + ? Prisma.sql`AND (c.name LIKE ${`%${q}%`} OR c.city LIKE ${`%${q}%`})` + : Prisma.empty; + + const bal = + currency === "USD" + ? Prisma.sql`SUM(CASE WHEN t.currency = 'USD' THEN t.amount ELSE 0 END)` + : Prisma.sql`SUM(CASE WHEN t.currency = 'MXN' THEN t.amount ELSE 0 END)`; + + const rows = await prisma.$queryRaw< + Array<{ + id: string; + name: string; + nameMissing: boolean; + city: string | null; + movements: bigint | number | string; + balance: Prisma.Decimal | null; + lastMovement: Date | null; + }> + >` + SELECT c.id, c.name, c.nameMissing, c.city, + COUNT(*) AS movements, + ${bal} AS balance, + MAX(t.transactionDate) AS lastMovement + FROM customers c + JOIN transactions t ON t.customerId = c.id + WHERE t.voidedAt IS NULL ${nameFilter} + GROUP BY c.id, c.name, c.nameMissing, c.city + HAVING ${bal} < -0.005 + ORDER BY MAX(t.transactionDate) ASC, c.nameMissing ASC, c.name ASC + `; + + let totalBalance = new Prisma.Decimal(0); + let totalMovs = 0; + const out = rows.map((r) => { + const b = r.balance ?? new Prisma.Decimal(0); + totalBalance = totalBalance.plus(b); + totalMovs += Number(r.movements); + return { + id: r.id.slice(0, 8), + name: nameOf(r), + city: r.city ?? "—", + movements: Number(r.movements), + balance: b.toFixed(2), + lastMovement: r.lastMovement + ? r.lastMovement.toISOString().slice(0, 10) + : "—", + }; + }); + + return { + rows: out, + totals: { + customers: out.length, + movements: totalMovs, + balance: totalBalance.toFixed(2), + currency, + }, + subtitle: `Moneda: ${currency} · ${out.length} clientes con saldo deudor`, + }; + }, +}; + +/** + * PAGOS NO EFECTUADOS (AGUA / LUZ / TEL). + * Customers enrolled in a service (by PropertyService.kind) with no + * related ledger charge in the last N days. Heuristic: any non-voided + * charge-type transaction in the window counts as "they paid". The + * report groups by property so the same customer with two water meters + * appears once per property. + */ +const pagosNoEfectuados: ReportDef = { + slug: "pagos-no-efectuados", + title: "Pagos no efectuados", + description: + "Clientes con un servicio contratado (agua, luz o teléfono) sin " + + "movimientos de cargo en los últimos N días. Heurística basada en el " + + "servicio registrado en la propiedad y la ausencia de cargos en el " + + "periodo seleccionado.", + domain: "servicios", + legacyName: "PAGOS NO EFECTUADOS AGUA/LUZ/TEL", + format: "tabular", + params: [ + { + key: "serviceKind", + label: "Servicio", + kind: "select", + options: [ + { value: "WATER", label: "Agua" }, + { value: "ELECTRICITY", label: "Luz" }, + { value: "PHONE", label: "Teléfono" }, + ], + defaultValue: "WATER", + }, + { + key: "days", + label: "Días sin movimiento", + kind: "number", + defaultValue: "60", + }, + { + key: "currency", + label: "Moneda", + kind: "select", + options: [ + { value: "MXN", label: "MXN" }, + { value: "USD", label: "USD" }, + ], + defaultValue: "MXN", + }, + ], + columns: [ + { key: "customerId", label: "Cliente #", type: "text" }, + { key: "customerName", label: "Cliente", type: "text" }, + { key: "propertyAddress", label: "Dirección", type: "text" }, + { key: "accountNumber", label: "Cuenta / Medidor", type: "text" }, + { key: "lastCharge", label: "Último cargo", type: "date" }, + { key: "balance", label: "Saldo", type: "money", align: "right" }, + ], + async run(prisma, p) { + const kind = (p.serviceKind ?? "WATER") as + | "WATER" + | "ELECTRICITY" + | "PHONE"; + const days = intParam(p, "days", 60, 1, 365); + const currency = (p.currency === "USD" ? "USD" : "MXN") as "MXN" | "USD"; + const cutoff = new Date(Date.now() - days * 86400000); + + // Customers enrolled in the service on a non-archived property, with no + // charge-type transaction in the window. The subquery picks up + // *anything* the customer paid (any domain, any type) — close enough + // for the staff's "who's overdue" view; the precise per-service match + // would need a per-service typeId taxonomy that doesn't exist in the + // legacy data. + const rows = await prisma.$queryRaw< + Array<{ + customerId: string; + customerName: string; + nameMissing: boolean; + propertyId: string; + propertyAddress: string | null; + accountNumber: string | null; + lastCharge: Date | null; + balance: Prisma.Decimal | null; + }> + >` + SELECT + c.id AS customerId, + c.name AS customerName, + c.nameMissing AS nameMissing, + pr.id AS propertyId, + pr.addressLine1 AS propertyAddress, + ps.accountNumber AS accountNumber, + (SELECT MAX(t.transactionDate) FROM transactions t + WHERE t.customerId = c.id AND t.voidedAt IS NULL + AND t.amount < 0 + AND t.transactionDate >= ${cutoff}) AS lastCharge, + (SELECT SUM(t.amount) FROM transactions t + WHERE t.customerId = c.id AND t.voidedAt IS NULL + AND t.currency = ${currency}) AS balance + FROM property_services ps + JOIN properties pr ON pr.id = ps.propertyId AND pr.archivedAt IS NULL + JOIN customers c ON c.id = pr.customerId AND c.archivedAt IS NULL + WHERE ps.kind = ${kind} AND ps.active = 1 + AND NOT EXISTS ( + SELECT 1 FROM transactions t + WHERE t.customerId = c.id AND t.voidedAt IS NULL + AND t.amount < 0 AND t.transactionDate >= ${cutoff} + ) + ORDER BY c.nameMissing ASC, c.name ASC, pr.addressLine1 ASC + `; + + let totalBalance = new Prisma.Decimal(0); + const out = rows.map((r) => { + const b = r.balance ?? new Prisma.Decimal(0); + totalBalance = totalBalance.plus(b); + return { + customerId: r.customerId.slice(0, 8), + customerName: nameOf({ + name: r.customerName, + nameMissing: r.nameMissing, + }), + propertyAddress: r.propertyAddress ?? "—", + accountNumber: r.accountNumber ?? "—", + lastCharge: r.lastCharge + ? r.lastCharge.toISOString().slice(0, 10) + : "—", + balance: b.toFixed(2), + }; + }); + + return { + rows: out, + totals: { + rows: out.length, + balance: totalBalance.toFixed(2), + currency, + }, + subtitle: `Servicio: ${ + kind === "WATER" ? "Agua" : kind === "ELECTRICITY" ? "Luz" : "Teléfono" + } · ${days} días · ${out.length} propiedades sin cargo reciente`, + }; + }, +}; + +/** + * FALTANTES DE (AGUA / LUZ / TEL). + * Data-quality report: properties enrolled in a service that are missing + * the key identifier the legacy system required (account/meter/route). + * Different `faltante` per service kind in the legacy because the + * service's identifier fields differ; here we flag any of the three + * common identifiers being blank. + */ +const faltantes: ReportDef = { + slug: "faltantes", + title: "Faltantes de datos por servicio", + description: + "Calidad de datos: propiedades con un servicio contratado que no " + + "tienen número de cuenta, medidor o ruta registrado. El reporte que " + + "en la legacy corría como FALTANTES DE AGUA / LUZ / TEL.", + domain: "servicios", + legacyName: "FALTANTES DE AGUA/LUZ/TEL", + format: "tabular", + params: [ + { + key: "serviceKind", + label: "Servicio", + kind: "select", + options: [ + { value: "WATER", label: "Agua" }, + { value: "ELECTRICITY", label: "Luz" }, + { value: "PHONE", label: "Teléfono" }, + ], + defaultValue: "WATER", + }, + ], + columns: [ + { key: "customerId", label: "Cliente #", type: "text" }, + { key: "customerName", label: "Cliente", type: "text" }, + { key: "propertyAddress", label: "Dirección", type: "text" }, + { key: "missing", label: "Faltante", type: "text" }, + { key: "dueDay", label: "Día de vencimiento", type: "text" }, + ], + async run(prisma, p) { + const kind = (p.serviceKind ?? "WATER") as + | "WATER" + | "ELECTRICITY" + | "PHONE"; + + // A row per (property, missing field). The "missing" string describes + // what's blank so the report is self-explanatory when printed. + const rows = await prisma.$queryRaw< + Array<{ + customerId: string; + customerName: string; + nameMissing: boolean; + propertyId: string; + propertyAddress: string | null; + dueDay: string | null; + missing: string; + }> + >` + SELECT + c.id AS customerId, + c.name AS customerName, + c.nameMissing AS nameMissing, + pr.id AS propertyId, + pr.addressLine1 AS propertyAddress, + ps.dueDay AS dueDay, + CASE + WHEN ps.accountNumber IS NULL OR ps.accountNumber = '' THEN 'Sin número de cuenta' + WHEN ps.meterNumber IS NULL OR ps.meterNumber = '' THEN 'Sin número de medidor' + WHEN ps.route IS NULL OR ps.route = '' THEN 'Sin ruta' + ELSE '' + END AS missing + FROM property_services ps + JOIN properties pr ON pr.id = ps.propertyId AND pr.archivedAt IS NULL + JOIN customers c ON c.id = pr.customerId AND c.archivedAt IS NULL + WHERE ps.kind = ${kind} AND ps.active = 1 + AND ( + ps.accountNumber IS NULL OR ps.accountNumber = '' + OR ps.meterNumber IS NULL OR ps.meterNumber = '' + OR ps.route IS NULL OR ps.route = '' + ) + ORDER BY c.nameMissing ASC, c.name ASC, pr.addressLine1 ASC + `; + + const out = rows.map((r) => ({ + customerId: r.customerId.slice(0, 8), + customerName: nameOf({ + name: r.customerName, + nameMissing: r.nameMissing, + }), + propertyAddress: r.propertyAddress ?? "—", + missing: r.missing, + dueDay: r.dueDay ?? "—", + })); + + return { + rows: out, + totals: { rows: out.length }, + subtitle: `Servicio: ${ + kind === "WATER" ? "Agua" : kind === "ELECTRICITY" ? "Luz" : "Teléfono" + } · ${out.length} propiedades con datos faltantes`, + }; + }, +}; + +/** + * REPORTE DE EFECTIVO — cash reconciliation. + * Credits in the EFECTIVO* legacy source tables OR with no cheque number + * (true cash) within a date range. Excludes voided rows. Matches the + * shape of the legacy REPORTE DE EFECTIVO report. + */ +const reporteDeEfectivo: ReportDef = { + slug: "reporte-de-efectivo", + title: "Reporte de efectivo", + description: + "Recibos de efectivo en el periodo seleccionado. Cubre los abonos " + + "provenientes de las tablas legacy EFECTIVO* y los créditos sin " + + "número de cheque (efectivo real). El match del reporte original.", + domain: "chequera", + legacyName: "REPORTE DE EFECTIVO", + format: "tabular", + params: [ + { key: "from", label: "Desde", kind: "date" }, + { key: "to", label: "Hasta", kind: "date", endOfDay: true }, + { + key: "currency", + label: "Moneda", + kind: "select", + options: [ + { value: "MXN", label: "MXN" }, + { value: "USD", label: "USD" }, + ], + defaultValue: "MXN", + }, + ], + columns: [ + { key: "date", label: "Fecha", type: "date" }, + { key: "customerName", label: "Cliente", type: "text" }, + { key: "concept", label: "Concepto", type: "text" }, + { key: "source", label: "Origen", type: "text" }, + { key: "amount", label: "Monto", type: "money", align: "right" }, + ], + async run(prisma, p) { + const from = parseDate(p.from); + const to = parseDate(p.to, true); + const currency = (p.currency === "USD" ? "USD" : "MXN") as "MXN" | "USD"; + + const ands: Prisma.TransactionWhereInput[] = [ + NOT_VOIDED, + { amount: { gt: 0 } }, + { currency }, + { + OR: [ + { legacySourceTable: { in: ["EFECTIVO", "EFECTIVO_BACKUP"] } }, + { + AND: [ + { checkNumber: null }, + { legacySourceTable: { not: "CHEQUE FM3" } }, + ], + }, + ], + }, + ]; + if (from || to) { + ands.push({ + transactionDate: { + ...(from ? { gte: from } : {}), + ...(to ? { lte: to } : {}), + }, + }); + } + + const rows = await prisma.transaction.findMany({ + where: { AND: ands }, + orderBy: { transactionDate: "asc" }, + select: { + transactionDate: true, + amount: true, + reference: true, + checkNumber: true, + message: true, + legacySourceTable: true, + type: { select: { nameEs: true, nameEn: true } }, + customer: { select: { name: true, nameMissing: true } }, + }, + }); + + let total = new Prisma.Decimal(0); + const out = rows.map((r) => { + total = total.plus(r.amount); + return { + date: r.transactionDate.toISOString().slice(0, 10), + customerName: nameOf(r.customer), + concept: r.message ?? r.type?.nameEs ?? r.type?.nameEn ?? "—", + source: r.legacySourceTable ?? "—", + amount: r.amount.toFixed(2), + }; + }); + + return { + rows: out, + totals: { + rows: out.length, + total: total.toFixed(2), + currency, + }, + subtitle: `Efectivo · ${currency} · ${out.length} recibos${ + from ? ` desde ${p.from}` : "" + }${to ? ` hasta ${p.to}` : ""}`, + }; + }, +}; + +/** + * VIGENTE (LIC / INCEN / MULT / …) — policies up for renewal. + * Wraps the policy listing with status=expiring and a policy-type filter, + * sorted by soonest expiry. The legacy VIGENTE LIC / INCEN / MULT + * reports are the same data; the new filter is a dropdown. + */ +const vigente: ReportDef = { + slug: "vigente", + title: "Pólizas por vencer", + description: + "Pólizas que vencen en los próximos N días, filtradas por ramo. " + + "Equivalente a los reportes VIGENTE LIC / INCEN / MULT de la legacy.", + domain: "polizas", + legacyName: "VIGENTE LIC/INCEN/MULT", + format: "tabular", + params: [ + { + key: "typeName", + label: "Ramo", + kind: "select", + options: [ + { value: "LICENCIAS", label: "Licencias" }, + { value: "INCENDIO", label: "Incendio" }, + { value: "MULT", label: "Multirriesgo" }, + { value: "MCA2", label: "MCA2 (auto)" }, + { value: "ME", label: "ME" }, + { value: "MF", label: "MF" }, + { value: "RC", label: "RC" }, + { value: "INCEN", label: "Incen" }, + { value: "TAMPL", label: "TAMPL" }, + { value: "FAMILIAR", label: "Familiar" }, + ], + defaultValue: "LICENCIAS", + }, + { + key: "days", + label: "Ventana (días)", + kind: "number", + defaultValue: "30", + }, + ], + columns: [ + { key: "policyNumber", label: "Póliza", type: "text" }, + { key: "customerName", label: "Cliente", type: "text" }, + { key: "provider", label: "Aseguradora", type: "text" }, + { key: "agent", label: "Agente", type: "text" }, + { key: "from", label: "Desde", type: "date" }, + { key: "to", label: "Vence", type: "date" }, + { key: "daysToExpire", label: "Días", type: "number", align: "right" }, + { key: "premium", label: "Prima neta", type: "money", align: "right" }, + ], + async run(prisma, p) { + const typeName = p.typeName ?? "LICENCIAS"; + const days = intParam(p, "days", 30, 1, 365); + const now = new Date(); + const today = new Date( + Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()), + ); + const soon = new Date(today.getTime() + days * 86400000); + + const rows = await prisma.policy.findMany({ + where: { + policyType: { name: typeName }, + archivedAt: null, + policyTo: { gte: today, lte: soon }, + }, + orderBy: { policyTo: "asc" }, + select: { + policyNumber: true, + policyFrom: true, + policyTo: true, + netPremium: true, + agentName: true, + customer: { select: { name: true, nameMissing: true } }, + insuranceProvider: { select: { name: true } }, + }, + }); + + let totalPremium = new Prisma.Decimal(0); + const out = rows.map((r) => { + const daysTo = r.policyTo + ? Math.round( + (r.policyTo.getTime() - today.getTime()) / 86400000, + ) + : 0; + if (r.netPremium) totalPremium = totalPremium.plus(r.netPremium); + return { + policyNumber: r.policyNumber, + customerName: nameOf(r.customer), + provider: r.insuranceProvider?.name ?? "—", + agent: r.agentName ?? "—", + from: r.policyFrom ? r.policyFrom.toISOString().slice(0, 10) : "—", + to: r.policyTo ? r.policyTo.toISOString().slice(0, 10) : "—", + daysToExpire: daysTo, + premium: r.netPremium ? r.netPremium.toFixed(2) : "0.00", + }; + }); + + return { + rows: out, + totals: { + rows: out.length, + premium: totalPremium.toFixed(2), + }, + subtitle: `Ramo: ${typeName} · ${days} días · ${out.length} pólizas por vencer`, + }; + }, +}; + +/** + * EDO CUENTA DATOS — per-customer account statement. + * Wraps the existing BillingService.statement() output. The full layout + * (header, balance per currency, by-domain split, by-type breakdown, + * full movement list with running balance) is rendered by the statement + * page; this report is the same data with print/PDF/CSV/XLSX outputs. + */ +const edoCuentaDatos: ReportDef = { + slug: "edo-cuenta-datos", + title: "Estado de cuenta", + description: + "Estado de cuenta de un cliente: saldos por moneda, desglose por " + + "ramo y concepto, y el historial completo de movimientos con saldo " + + "corrido. El reporte del cliente final.", + domain: "estado-cuenta", + legacyName: "EDO CUENTA DATOS", + format: "statement", + params: [ + { key: "customerId", label: "Cliente", kind: "customer-picker" }, + ], + columns: [ + // Statement rows carry synthetic `__kind` discriminators instead of + // column keys; the runner renders the special cases inline. These + // columns drive CSV/XLSX when the user wants a flat movement export. + { key: "date", label: "Fecha", type: "date" }, + { key: "concept", label: "Concepto", type: "text" }, + { key: "reference", label: "Referencia", type: "text" }, + { key: "amount", label: "Cargo / Abono", type: "money", align: "right" }, + { key: "balanceAfter", label: "Saldo", type: "money", align: "right" }, + ], + async run(prisma, p) { + const customerId = p.customerId; + if (!customerId) { + return { rows: [], subtitle: "Selecciona un cliente" }; + } + const customer = await prisma.customer.findUnique({ + where: { id: customerId }, + select: { + id: true, + name: true, + nameMissing: true, + addressLine1: true, + city: true, + state: true, + email: true, + phone: true, + }, + }); + if (!customer) return { rows: [], subtitle: "Cliente no encontrado" }; + + // Reuse the same NOT_VOIDED + STATEMENT_EXCLUDED_SOURCE_TABLES filter + // as BillingService.statement so the numbers match what the customer + // already sees in /estado-cuenta/[id]. + const rows = await prisma.transaction.findMany({ + where: { + customerId, + voidedAt: null, + legacySourceTable: { + notIn: [ + "EFECTIVO", + "EFECTIVO_BACKUP", + "EFECTIVO FM3", + "CHEQUE FM3", + "IVA 2015", + ], + }, + }, + orderBy: [{ transactionDate: "asc" }, { id: "asc" }], + select: { + id: true, + transactionDate: true, + domain: true, + amount: true, + currency: true, + reference: true, + period: true, + checkNumber: true, + message: true, + legacySourceTable: true, + type: { select: { nameEs: true, nameEn: true } }, + }, + }); + + // Compute running balance per currency, then return newest-first. + const running = new Map(); + const movements = rows.map((r) => { + const prev = running.get(r.currency) ?? new Prisma.Decimal(0); + const next = prev.plus(r.amount); + running.set(r.currency, next); + return { + date: r.transactionDate.toISOString().slice(0, 10), + domain: r.domain, + currency: r.currency, + reference: r.reference ?? "", + period: r.period ?? "", + checkNumber: r.checkNumber ?? "", + concept: r.type?.nameEs ?? r.type?.nameEn ?? "—", + amount: r.amount.toFixed(2), + balanceAfter: next.toFixed(2), + }; + }); + movements.reverse(); + + // Per-currency summary + per-domain breakdown. + const perCurrency = new Map< + string, + { currency: string; charges: Prisma.Decimal; credits: Prisma.Decimal; count: number } + >(); + for (const r of rows) { + const c = + perCurrency.get(r.currency) ?? + { + currency: r.currency, + charges: new Prisma.Decimal(0), + credits: new Prisma.Decimal(0), + count: 0, + }; + c.count += 1; + if (r.amount.lessThan(0)) c.charges = c.charges.plus(r.amount); + else c.credits = c.credits.plus(r.amount); + perCurrency.set(r.currency, c); + } + + return { + rows: [ + { + __kind: "header", + name: nameOf(customer), + address: customer.addressLine1 ?? "", + city: [customer.city, customer.state].filter(Boolean).join(", "), + phone: customer.phone ?? "", + email: customer.email ?? "", + }, + ...[...perCurrency.values()].map((c) => ({ + __kind: "summary", + currency: c.currency, + charges: c.charges.toFixed(2), + credits: c.credits.toFixed(2), + balance: c.charges.plus(c.credits).toFixed(2), + count: c.count, + })), + { __kind: "movements-header" }, + ...movements, + ], + subtitle: `${nameOf(customer)} · ${rows.length} movimientos`, + }; + }, +}; + +/* ------------------------------------------------------------------ export */ + +export const REPORTS: ReportDef[] = [ + listadoEnRojo, + pagosNoEfectuados, + faltantes, + reporteDeEfectivo, + vigente, + edoCuentaDatos, +]; + +export function findReport(slug: string): ReportDef | undefined { + return REPORTS.find((r) => r.slug === slug); +} diff --git a/apps/api/src/reports/reports.service.ts b/apps/api/src/reports/reports.service.ts new file mode 100644 index 0000000..535dab7 --- /dev/null +++ b/apps/api/src/reports/reports.service.ts @@ -0,0 +1,45 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { findReport, REPORTS } from "./reports.registry"; +import type { ReportDef } from "./reports.types"; + +/** + * The reports service. Two responsibilities: + * 1. Run a report by slug with the given params — just dispatch. + * 2. Return the catalog for the /reportes index page. + * + * Output rendering (CSV/XLSX/PDF/HTML print) lives in `outputs.ts`; this + * service is data only. The controller maps URLs to (slug, format) and + * hands the result to outputs. + */ +@Injectable() +export class ReportsService { + constructor(private readonly prisma: PrismaService) {} + + /** List every registered report, in display order. */ + catalog(): Array<{ + slug: string; + title: string; + description: string; + domain: string; + legacyName: string | null; + format: string; + params: ReportDef["params"]; + }> { + return REPORTS.map((r) => ({ + slug: r.slug, + title: r.title, + description: r.description, + domain: r.domain, + legacyName: r.legacyName, + format: r.format, + params: r.params, + })); + } + + async run(slug: string, params: Record) { + const def = findReport(slug); + if (!def) throw new NotFoundException(`Reporte "${slug}" no encontrado`); + return def.run(this.prisma, params); + } +} diff --git a/apps/api/src/reports/reports.types.ts b/apps/api/src/reports/reports.types.ts new file mode 100644 index 0000000..a59549e --- /dev/null +++ b/apps/api/src/reports/reports.types.ts @@ -0,0 +1,140 @@ +/** + * The reports module — plan step 10. + * + * Each "report" is one entry in `reports.registry.ts`. The entry declares + * its slug (URL id), title, what filters it accepts, what columns it + * returns, and a `run` function that produces the data from Prisma. The + * service dispatches on slug; the controller exposes JSON + CSV + XLSX + + * PDF + HTML print; the catalog endpoint exposes the registry itself so + * the `/reportes` page can render the same data. + * + * Output philosophy: a report returns a uniform shape — `columns` (typed + * schema) + `rows` (any[] of values matching the column types) + `totals` + * (record of column key → summary value). All three output formats + * (CSV/XLSX/PDF/print) derive from this same shape so adding a new + * report is one entry, never a per-format template. + */ + +import { Prisma } from "@jorgecuadros/database"; +import { PrismaService } from "../prisma/prisma.service"; + +/** Top-level grouping for the catalog page; matches the existing nav. */ +export type ReportDomain = + | "clientes" + | "polizas" + | "servicios" + | "estado-cuenta" + | "chequera"; + +/** How the runner should render rows: a grid, or a per-customer statement. */ +export type ReportFormat = "tabular" | "statement"; + +/** Filter controls the report's UI should render. */ +export type ParamDef = + | { + key: string; + label: string; + kind: "text" | "number"; + placeholder?: string; + defaultValue?: string; + } + | { + key: string; + label: string; + kind: "date"; + /** Inclusive bound, true for `to`, false for `from`. */ + endOfDay?: boolean; + defaultValue?: string; + } + | { + key: string; + label: string; + kind: "select"; + options: { value: string; label: string }[]; + defaultValue?: string; + } + | { + key: string; + label: string; + kind: "customer-picker"; + }; + +/** One column of the output table. */ +export interface ColumnDef { + key: string; + label: string; + /** Render hint for the on-screen + print table. */ + type: "text" | "number" | "money" | "date"; + /** Right-align numbers/money; default false (left). */ + align?: "left" | "right"; + /** Used for column-width hints in the print/PDF layout. */ + width?: number; +} + +/** Shape every report's `run` resolves to. Columns come from the def. */ +export interface ReportResult { + rows: Array>; + totals?: Record; + /** Optional free-form subtitle for print/PDF (e.g. date range, scope). */ + subtitle?: string; +} + +/** A report's static declaration. */ +export interface ReportDef { + slug: string; + title: string; + description: string; + domain: ReportDomain; + /** The original Access report name (per docs/LEGACY_DATABASES_OBJECTS.md) + * for traceability. Null when this is a new report with no legacy equiv. */ + legacyName: string | null; + format: ReportFormat; + params: ParamDef[]; + columns: ColumnDef[]; + /** + * Run the report. Receives the Prisma client and the validated params + * record (keys are the `key` from ParamDef, values are the strings the + * runner collected; numeric/date params arrive as strings — the report + * parses them). Must apply the same NOT_VOIDED filter on transactions as + * the billing module so totals match. + */ + run: ( + prisma: PrismaService, + params: Record, + ) => Promise; +} + +/** A typed bag of helpers for the report functions. */ +export interface ReportCtx { + prisma: PrismaService; + params: Record; +} + +/** Helper: a `YYYY-MM-DD` bound; unparseable is undefined. */ +export function parseDate( + v: string | undefined, + endOfDay = false, +): Date | undefined { + if (!v) return undefined; + const d = new Date(endOfDay ? `${v}T23:59:59.999Z` : `${v}T00:00:00.000Z`); + return Number.isNaN(d.getTime()) ? undefined : d; +} + +/** Helper: integer param with default. */ +export function intParam( + p: Record, + key: string, + def: number, + min = 1, + max = 1000, +): number { + const n = Number(p[key]); + if (!Number.isFinite(n)) return def; + return Math.min(max, Math.max(min, Math.round(n))); +} + +/** Helper: not-voided filter, shared with billing.service. */ +export const NOT_VOIDED: Prisma.TransactionWhereInput = { voidedAt: null }; +export const NOT_VOIDED_BANK: Prisma.BankTransactionWhereInput = { + voidedAt: null, +}; diff --git a/apps/web/src/app/banco/page.tsx b/apps/web/src/app/banco/page.tsx index 83028fb..6cf0935 100644 --- a/apps/web/src/app/banco/page.tsx +++ b/apps/web/src/app/banco/page.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { createBankMovement, getBankFacets, @@ -208,6 +209,13 @@ function BankBrowser() { parte del estado de cuenta de los clientes y sus cifras no se suman con las de ellos.

+
+ +
diff --git a/apps/web/src/app/clientes/[id]/page.tsx b/apps/web/src/app/clientes/[id]/page.tsx index 3e1b49b..3a21d60 100644 --- a/apps/web/src/app/clientes/[id]/page.tsx +++ b/apps/web/src/app/clientes/[id]/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { archiveCustomer, getCustomer, @@ -95,6 +96,15 @@ function Detail({ id }: { id: string }) {
+ getCustomer(id).then(setData).catch(() => {})} diff --git a/apps/web/src/app/clientes/page.tsx b/apps/web/src/app/clientes/page.tsx index 21ca29c..af89f0b 100644 --- a/apps/web/src/app/clientes/page.tsx +++ b/apps/web/src/app/clientes/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { getStats, listCustomers } from "@/lib/api"; import { useCan } from "@/lib/abilities"; import { formatNumber, SIN_NOMBRE } from "@/lib/labels"; @@ -99,6 +100,12 @@ function ClientesBrowser() {

Clientes

+ {canCreate && ( + Nuevo cliente diff --git a/apps/web/src/app/estado-cuenta/page.tsx b/apps/web/src/app/estado-cuenta/page.tsx index 853508c..d1b3259 100644 --- a/apps/web/src/app/estado-cuenta/page.tsx +++ b/apps/web/src/app/estado-cuenta/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { MovementForm } from "@/components/MovementForm"; import { getBillingFacets, @@ -256,6 +257,15 @@ function BillingBrowser() {
+
+ +
+
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index e442240..4340d75 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -2205,3 +2205,488 @@ button { padding: 20px; z-index: 50; } + +/* ============================================================================ + Home dashboard (/inicio) + ========================================================================== */ +.home { + display: flex; + flex-direction: column; + gap: 36px; + padding-top: 8px; +} +.home-last-seen { + margin-top: 14px; + font-size: 13px; +} +.home-section { + display: flex; + flex-direction: column; + gap: 14px; +} +.section-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} +.section-title { + font-family: var(--font-display); + font-size: 20px; + font-weight: 560; + margin: 0; + color: var(--ink); + letter-spacing: -0.005em; +} +.section-sub { + font-size: 12.5px; + color: var(--muted); +} + +/* KPI cards row — 4 across on wide screens, 2 on tablet, 1 on mobile */ +.home-section.kpi-row { + display: grid; +} +.home > .home-section:first-of-type { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; +} +@media (max-width: 1080px) { + .home > .home-section:first-of-type { + grid-template-columns: repeat(2, 1fr); + } +} +@media (max-width: 560px) { + .home > .home-section:first-of-type { + grid-template-columns: 1fr; + } +} + +.kpi-card { + display: flex; + flex-direction: column; + padding: 18px 18px 16px; + text-decoration: none; + color: inherit; + transition: transform 0.18s var(--ease-out-expo), box-shadow 0.18s; + position: relative; +} +.kpi-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} +.kpi-label { + font-size: 11.5px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted); + font-weight: 700; +} +.kpi-primary { + font-family: var(--font-display); + font-size: 32px; + font-weight: 560; + color: var(--ink); + margin-top: 8px; + font-feature-settings: "tnum" 1; + letter-spacing: -0.015em; + line-height: 1.1; + min-height: 36px; +} +.kpi-sub { + list-style: none; + padding: 0; + margin: 12px 0 0; + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12.5px; + color: var(--ink-soft); +} +.kpi-sub li { + line-height: 1.35; +} +.kpi-cta { + margin-top: 14px; + font-size: 12px; + font-weight: 700; + color: var(--brand-700); + letter-spacing: 0.02em; + text-transform: uppercase; +} +.kpi-card:hover .kpi-cta { + color: var(--brand-800); +} + +/* Attention grid — auto-fill so the row of cards stays balanced */ +.attention-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 14px; +} +.attention-card { + display: flex; + flex-direction: column; + gap: 6px; + padding: 16px 18px 18px; + text-decoration: none; + color: inherit; + border-left: 3px solid var(--line-strong); + transition: transform 0.18s var(--ease-out-expo), box-shadow 0.18s; + position: relative; +} +.attention-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} +.attention-card.tone-warn { + border-left-color: var(--seguros); +} +.attention-card.tone-info { + border-left-color: var(--brand-600); +} +.attention-card.tone-muted { + border-left-color: var(--muted-2); +} +.attention-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.attention-title { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted); + font-weight: 700; +} +.attention-arrow { + color: var(--muted-2); + font-size: 14px; + transition: transform 0.18s var(--ease-out-expo), color 0.18s; +} +.attention-card:hover .attention-arrow { + color: var(--brand-700); + transform: translateX(3px); +} +.attention-primary { + font-family: var(--font-display); + font-size: 26px; + font-weight: 560; + color: var(--ink); + font-feature-settings: "tnum" 1; + letter-spacing: -0.01em; + line-height: 1.1; + min-height: 30px; + margin-top: 2px; +} +.attention-sub { + font-size: 12.5px; + color: var(--ink-soft); + line-height: 1.4; +} +.attention-meta { + font-size: 11.5px; + color: var(--muted); + margin-top: 4px; +} + +/* Currency totals inside an attention card */ +.home-currency-totals { + display: inline-flex; + flex-wrap: wrap; + gap: 8px 14px; + align-items: baseline; +} +.home-currency-totals-row { + display: inline-flex; + align-items: baseline; + gap: 6px; +} +.home-currency-totals-num { + font-family: var(--font-display); + font-feature-settings: "tnum" 1; + font-size: 20px; + font-weight: 560; +} + +/* Quick links row */ +.quick-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 12px; +} +.quick-link { + display: flex; + flex-direction: column; + padding: 14px 16px; + text-decoration: none; + color: inherit; + position: relative; + transition: transform 0.18s var(--ease-out-expo), box-shadow 0.18s; +} +.quick-link:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} +.quick-label { + font-family: var(--font-display); + font-size: 17px; + font-weight: 560; + color: var(--ink); +} +.quick-sub { + font-size: 12px; + color: var(--muted); + margin-top: 2px; +} +.quick-arrow { + position: absolute; + top: 14px; + right: 16px; + color: var(--muted-2); + font-size: 14px; + transition: transform 0.18s var(--ease-out-expo), color 0.18s; +} +.quick-link:hover .quick-arrow { + color: var(--brand-700); + transform: translateX(3px); +} + +/* Money tone helpers (used in chequera card) */ +.money-pos { color: var(--positive); } +.money-neg { color: var(--negative); } + +/* ============================================================================ + Reports module + ========================================================================== */ + +.report-catalog { + display: flex; + flex-direction: column; + gap: 28px; + margin-top: 20px; +} +.report-catalog-group { + display: flex; + flex-direction: column; + gap: 10px; +} +.report-catalog-title { + font-family: var(--font-display); + font-weight: 500; + font-size: 20px; + color: var(--brand-800); + margin: 0; + padding-bottom: 6px; + border-bottom: 1px solid var(--line); +} +.report-catalog-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 12px; +} +.report-catalog-item { + margin: 0; +} +.report-catalog-link { + display: flex; + flex-direction: column; + gap: 6px; + padding: 14px 16px; + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; + color: inherit; + text-decoration: none; + transition: border-color 0.18s, transform 0.18s var(--ease-out-expo); +} +.report-catalog-link:hover { + border-color: var(--brand-500); + transform: translateY(-1px); +} +.report-catalog-item-title { + font-weight: 600; + font-size: 15px; + color: var(--ink); +} +.report-catalog-item-desc { + font-size: 13px; + color: var(--muted); + line-height: 1.4; +} +.report-catalog-item-meta { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 4px; +} +.report-catalog-tag { + font-size: 11px; + background: var(--brand-tint); + color: var(--brand-800); + padding: 2px 8px; + border-radius: 999px; + font-family: var(--font-mono); +} +.report-catalog-tag.muted { + background: var(--paper-2); + color: var(--muted); +} + +/* Runner */ +.report-runner { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 20px; +} +.report-filters { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: flex-end; + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; + padding: 14px 16px; +} +.report-filters-actions { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; +} +.report-error { + background: #fbeae3; + color: var(--negative); + border: 1px solid #e6b6a4; + border-radius: 6px; + padding: 10px 14px; + font-size: 13px; +} +.report-loading { + font-size: 12px; + color: var(--muted); + padding: 4px 0; +} +.report-result { + display: flex; + flex-direction: column; + gap: 12px; +} +.report-result-head { + display: flex; + align-items: flex-end; + gap: 16px; + flex-wrap: wrap; +} +.report-result-meta { + flex: 1; + min-width: 200px; +} +.report-output-buttons { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.btn-sm { + padding: 4px 10px; + font-size: 12px; +} +.report-table-wrap { + overflow-x: auto; + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; +} +.report-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.report-table th, +.report-table td { + padding: 8px 12px; + border-bottom: 1px solid var(--line); +} +.report-table th { + background: var(--paper-2); + color: var(--ink-soft); + font-weight: 600; + text-align: left; + position: sticky; + top: 0; + z-index: 1; +} +.report-table tbody tr:hover { + background: var(--paper-2); +} +.report-totals { + background: var(--accent); + color: #f5f1e8; + font-weight: 600; + padding: 10px 14px; +} +.statement { + display: flex; + flex-direction: column; + gap: 20px; +} +.statement-head { + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; + padding: 16px 18px; +} +.statement-name { + font-family: var(--font-display); + font-size: 22px; + font-weight: 600; + color: var(--ink); + margin: 0 0 4px; +} +.statement-section-title { + font-family: var(--font-display); + font-size: 16px; + font-weight: 500; + color: var(--brand-800); + margin: 0 0 8px; +} + +/* Context buttons (the inline shortcut on existing pages) */ +.context-reports { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + margin-left: auto; +} +.context-reports-label { + font-size: 11px; + color: var(--muted-2); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-right: 4px; + font-weight: 500; +} +.context-report-link { + font-size: 12px; + padding: 4px 10px; + border: 1px solid var(--line); + background: var(--surface); + color: var(--ink); + border-radius: 999px; + text-decoration: none; + transition: border-color 0.15s, color 0.15s; +} +.context-report-link:hover { + border-color: var(--brand-500); + color: var(--brand-700); +} diff --git a/apps/web/src/app/inicio/page.tsx b/apps/web/src/app/inicio/page.tsx new file mode 100644 index 0000000..db46d2f --- /dev/null +++ b/apps/web/src/app/inicio/page.tsx @@ -0,0 +1,455 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { AppShell } from "@/components/AppShell"; +import { + EXPIRY_WINDOW_DAYS, + getBankStats, + getBillingStats, + getPolicyStats, + getPropertyStats, + getStats, +} from "@/lib/api"; +import { useAuth } from "@/lib/abilities"; +import { + balancePhrase, + formatDate, + formatMoney, + formatNumber, + policyStatusLabel, + trustStatusLabel, +} from "@/lib/labels"; +import type { + BankStats, + BillingStats, + CustomerStats, + PolicyStats, + PropertyStats, +} from "@/lib/types"; + +export default function InicioPage() { + return ( + + + + ); +} + +interface DashboardData { + customers: CustomerStats | null; + policies: PolicyStats | null; + properties: PropertyStats | null; + billing: BillingStats | null; + bank: BankStats | null; +} + +function HomeDashboard() { + const user = useAuth(); + const [data, setData] = useState({ + customers: null, + policies: null, + properties: null, + billing: null, + bank: null, + }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let alive = true; + Promise.allSettled([ + getStats(), + getPolicyStats(), + getPropertyStats(), + getBillingStats(), + getBankStats(), + ]).then((results) => { + if (!alive) return; + setData({ + customers: results[0].status === "fulfilled" ? results[0].value : null, + policies: results[1].status === "fulfilled" ? results[1].value : null, + properties: results[2].status === "fulfilled" ? results[2].value : null, + billing: results[3].status === "fulfilled" ? results[3].value : null, + bank: results[4].status === "fulfilled" ? results[4].value : null, + }); + setLoading(false); + }); + return () => { + alive = false; + }; + }, []); + + const greeting = greetingFor(user?.name); + const lastBillingMovement = data.billing?.lastMovement ?? null; + const lastBankMovement = data.bank?.lastMovement ?? null; + + return ( +
+
+

Resumen general

+

{greeting}

+

+ Vista rápida del estado de la cartera de clientes, las pólizas + activas, los fideicomisos y los movimientos recientes. +

+ +
+ +
+ + + + +
+ +
+
+

Atención

+ + Lo que conviene revisar antes de cerrar el día + +
+ +
+ + + + + ) : ( + "—" + ) + } + sub={ + data.billing + ? `En ${formatNumber(data.billing.ledgerCustomers)} expedientes con cargo` + : undefined + } + meta={ + data.billing + ? `${formatNumber(data.billing.crossLineCustomers)} clientes con cargo en ambos ramos` + : undefined + } + /> + + {formatMoney(data.bank.net, "MXN")} + + ) : ( + "—" + ) + } + sub={ + data.bank + ? balancePhrase(data.bank.net) + : undefined + } + meta={ + data.bank + ? `${formatNumber(data.bank.movements)} movimientos · ${formatNumber(data.bank.pending)} pendientes` + : undefined + } + /> +
+
+ +
+
+

Accesos rápidos

+ + Ir directo a cada módulo + +
+
+ + + + + +
+
+
+ ); +} + +function greetingFor(name?: string): string { + const hour = new Date().getHours(); + const partOfDay = + hour < 12 ? "Buenos días" : hour < 19 ? "Buenas tardes" : "Buenas noches"; + const display = (name ?? "").trim().split(/\s+/)[0]; + return display ? `${partOfDay}, ${display}` : partOfDay; +} + +function LastSeenLine({ + billing, + bank, + loading, +}: { + billing: string | null; + bank: string | null; + loading: boolean; +}) { + if (loading) { + return ( + + ); + } + if (!billing && !bank) return null; + return ( +

+ {billing && <>Último movimiento de cartera: {formatDate(billing)}} + {billing && bank && } + {bank && <>Último movimiento de chequera: {formatDate(bank)}} +

+ ); +} + +function KpiCard({ + href, + label, + primary, + sub, + loading, +}: { + href: string; + label: string; + primary: React.ReactNode; + sub: string[]; + loading: boolean; +}) { + return ( + +
{label}
+
+ {loading ? ( + + ) : ( + primary + )} +
+
    + {loading + ? Array.from({ length: 3 }).map((_, i) => ( +
  • + )) + : sub.map((line) =>
  • {line}
  • )} +
+ + + ); +} + +function AttentionCard({ + href, + tone, + title, + primary, + sub, + meta, + loading, +}: { + href: string; + tone: "warn" | "info" | "muted"; + title: string; + primary: React.ReactNode; + sub?: string; + meta?: string; + loading: boolean; +}) { + return ( + +
+ {title} + +
+
+ {loading ? ( + + ) : ( + primary + )} +
+ {sub && !loading &&
{sub}
} + {meta && !loading &&
{meta}
} + + ); +} + +function QuickLink({ + href, + label, + sub, +}: { + href: string; + label: string; + sub: string; +}) { + return ( + + {label} + {sub} + + + ); +} + +function CurrencyTotals({ + totals, + field, +}: { + totals: BillingStats["byCurrency"]; + field: "owing" | "inCredit"; +}) { + if (!totals || totals.length === 0) return <>—; + return ( + + {totals.map((c) => ( + + {c.currency} + + {formatNumber(c[field])} + + + ))} + + ); +} diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx index 36f201b..7c23046 100644 --- a/apps/web/src/app/login/page.tsx +++ b/apps/web/src/app/login/page.tsx @@ -16,7 +16,7 @@ export default function LoginPage() { useEffect(() => { let alive = true; me() - .then(() => router.replace("/clientes")) + .then(() => router.replace("/inicio")) .catch(() => { if (alive) setBootChecking(false); }); @@ -31,7 +31,7 @@ export default function LoginPage() { setSubmitting(true); try { await login(email.trim(), password); - router.replace("/clientes"); + router.replace("/inicio"); } catch (err) { if (err instanceof ApiError && err.status === 401) { setError("Correo o contraseña incorrectos"); diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 70a6814..b027fc5 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function HomePage() { - redirect("/clientes"); + redirect("/inicio"); } diff --git a/apps/web/src/app/polizas/[id]/page.tsx b/apps/web/src/app/polizas/[id]/page.tsx index b875881..5c26b8f 100644 --- a/apps/web/src/app/polizas/[id]/page.tsx +++ b/apps/web/src/app/polizas/[id]/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { addPolicyChild, archivePolicy, @@ -92,6 +93,15 @@ function Detail({ id }: { id: string }) {
+
diff --git a/apps/web/src/app/polizas/page.tsx b/apps/web/src/app/polizas/page.tsx index e480f05..e16b548 100644 --- a/apps/web/src/app/polizas/page.tsx +++ b/apps/web/src/app/polizas/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { useCan } from "@/lib/abilities"; import { EXPIRY_WINDOW_DAYS, @@ -127,6 +128,13 @@ function PolizasBrowser() {

Pólizas

+ {canCreate && ( + Nueva póliza )} diff --git a/apps/web/src/app/reportes/[slug]/page.tsx b/apps/web/src/app/reportes/[slug]/page.tsx new file mode 100644 index 0000000..a71806f --- /dev/null +++ b/apps/web/src/app/reportes/[slug]/page.tsx @@ -0,0 +1,110 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { AppShell } from "@/components/AppShell"; +import { ReportRunner } from "@/components/ReportRunner"; +import { getReportCatalog } from "@/lib/api"; +import type { ReportDef } from "@/lib/types"; + +/** + * /reportes/[slug] — one report's runner page. The whole page is + * data-driven from the catalog; if a slug isn't in the registry the + * page shows a "not found" inline message. + */ +export default function ReporteRunnerPage({ + params, + searchParams, +}: { + params: { slug: string }; + searchParams?: Record; +}) { + return ( + + + + ); +} + +function Runner({ + slug, + searchParams, +}: { + slug: string; + searchParams?: Record; +}) { + const [def, setDef] = useState(null); + const [missing, setMissing] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + getReportCatalog() + .then((c) => { + const found = c.items.find((r) => r.slug === slug); + if (found) setDef(found); + else setMissing(true); + }) + .catch((e) => setError(e?.message ?? "No se pudo cargar el reporte.")); + }, [slug]); + + if (error) { + return ( + <> + +
{error}
+ + ); + } + if (missing) { + return ( + <> + +
Reporte "{slug}" no encontrado.
+ + ); + } + if (!def) { + return ( + <> + +
Cargando reporte…
+ + ); + } + + const initialParams: Record = {}; + if (searchParams) { + const allowed = new Set(def.params.map((p) => p.key)); + for (const [k, v] of Object.entries(searchParams)) { + if (!allowed.has(k)) continue; + const s = Array.isArray(v) ? v[0] : v; + if (s) initialParams[k] = s; + } + } + + return ( + <> +
+

Reportes

+

{def.title}

+

+ {def.description} +

+ {def.legacyName && ( +

+ Equivalente en Access: {def.legacyName} +

+ )} +
+ + + ); +} + +function BackLink() { + return ( + + ← Reportes + + ); +} diff --git a/apps/web/src/app/reportes/page.tsx b/apps/web/src/app/reportes/page.tsx new file mode 100644 index 0000000..1c9dcb3 --- /dev/null +++ b/apps/web/src/app/reportes/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { AppShell } from "@/components/AppShell"; +import { getReportCatalog } from "@/lib/api"; +import type { ReportCatalog, ReportDef, ReportDomain } from "@/lib/types"; + +/** + * The /reportes catalog. Index of every registered report, grouped by + * domain (matches the main nav). Discovery layer for the 280+ reports + * the system will eventually surface; for now we ship 6 in v1. + */ +export default function ReportesPage() { + return ( + + + + ); +} + +const DOMAIN_LABEL: Record = { + clientes: "Clientes", + polizas: "Pólizas", + servicios: "Servicios", + "estado-cuenta": "Estado de cuenta", + chequera: "Chequera", +}; + +const DOMAIN_ORDER: ReportDomain[] = [ + "clientes", + "estado-cuenta", + "polizas", + "servicios", + "chequera", +]; + +function ReportesCatalog() { + const [catalog, setCatalog] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + getReportCatalog() + .then(setCatalog) + .catch((e) => setError(e?.message ?? "No se pudo cargar el catálogo.")); + }, []); + + const grouped = new Map(); + if (catalog) { + for (const r of catalog.items) { + const list = grouped.get(r.domain) ?? []; + list.push(r); + grouped.set(r.domain, list); + } + } + + return ( + <> +
+

Reportes

+

Catálogo de reportes

+

+ {catalog + ? `${catalog.items.length} reportes disponibles. Cada uno corre como consulta sobre el esquema actual — los filtros y totales se recalculan en vivo.` + : "Cargando…"} +

+
+ + {error &&
{error}
} + +
+ {DOMAIN_ORDER.filter((d) => grouped.has(d)).map((d) => ( +
+

{DOMAIN_LABEL[d]}

+
    + {(grouped.get(d) ?? []).map((r) => ( +
  • + +
    {r.title}
    +
    {r.description}
    +
    + {r.legacyName && ( + + Legacy: {r.legacyName} + + )} + + {r.format === "statement" ? "Estado de cuenta" : "Tabular"} + +
    + +
  • + ))} +
+
+ ))} +
+ + ); +} diff --git a/apps/web/src/app/servicios/page.tsx b/apps/web/src/app/servicios/page.tsx index f47f08a..99ce739 100644 --- a/apps/web/src/app/servicios/page.tsx +++ b/apps/web/src/app/servicios/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { AppShell } from "@/components/AppShell"; +import { ContextReports } from "@/components/ContextReports"; import { useCan } from "@/lib/abilities"; import { EXPIRY_WINDOW_DAYS, @@ -169,6 +170,13 @@ function ServiciosBrowser() {

Propiedades

+ {canCreate && ( + Nueva propiedad )} diff --git a/apps/web/src/components/AppShell.tsx b/apps/web/src/components/AppShell.tsx index 5ff97bf..846bc4c 100644 --- a/apps/web/src/components/AppShell.tsx +++ b/apps/web/src/components/AppShell.tsx @@ -14,12 +14,14 @@ import type { AuthUser, Ability } from "@/lib/types"; * content. Provides the AuthContext so any page can read the user's * abilities. Used by every authenticated page. */ -const NAV: { href: string; label: string; ability?: Ability }[] = [ +const NAV: { href: string; label: string; ability?: Ability; exact?: boolean }[] = [ + { href: "/inicio", label: "Inicio", exact: true }, { href: "/clientes", label: "Clientes" }, { href: "/servicios", label: "Propiedades" }, { href: "/polizas", label: "Pólizas" }, { href: "/estado-cuenta", label: "Estado de cuenta" }, { href: "/banco", label: "Chequera" }, + { href: "/reportes", label: "Reportes" }, { href: "/catalogos", label: "Catálogos", ability: "lookup:manage" }, { href: "/usuarios", label: "Usuarios", ability: "user:manage" }, { href: "/operaciones", label: "Operaciones", ability: "db:manage" }, @@ -78,7 +80,7 @@ export function AppShell({ children }: { children: ReactNode }) {
- + {NAV.filter((item) => !item.ability || can(user, item.ability)).map( (item) => { - const active = pathname?.startsWith(item.href) ?? false; + const active = item.exact + ? pathname === item.href + : pathname?.startsWith(item.href) ?? false; return ( ; + /** When true, opens the report in a new tab (for "see the catalog" + * style entries where the user is going to look around). */ + external?: boolean; +} + +export function ContextReports({ + label = "Reportes", + entries, +}: { + label?: string; + entries: ReportLink[]; +}) { + if (entries.length === 0) return null; + return ( +
+ {label} + {entries.map((e) => { + const qs = e.params + ? "?" + + new URLSearchParams( + Object.entries(e.params).filter(([, v]) => v != null && v !== ""), + ).toString() + : ""; + const href = `/reportes/${e.slug}${qs}`; + return ( + + {e.label} + + ); + })} +
+ ); +} diff --git a/apps/web/src/components/ReportRunner.tsx b/apps/web/src/components/ReportRunner.tsx new file mode 100644 index 0000000..b27808e --- /dev/null +++ b/apps/web/src/components/ReportRunner.tsx @@ -0,0 +1,476 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + API_ORIGIN, + reportDownloadUrl, + runReport, +} from "@/lib/api"; +import { CustomerPicker } from "@/components/CustomerPicker"; +import { formatMoney, formatNumber } from "@/lib/labels"; +import type { + ReportDef, + ReportParam, + ReportRunResult, +} from "@/lib/types"; + +/** + * The shared runner. Renders the filter form, fetches the result, and + * shows the table + output buttons. One component, every report — the + * per-report shape comes entirely from the def the API returns. + */ +export function ReportRunner({ + def, + initialParams, +}: { + def: ReportDef; + /** Pre-filled param values (e.g. when launched with a customerId from + * a context button on a customer detail page). */ + initialParams?: Record; +}) { + // The form state, keyed by param.key. Initialised from defaults + + // initialParams (initialParams wins for explicitly-set keys). + const [params, setParams] = useState>(() => { + const seed: Record = {}; + for (const p of def.params) { + if (p.kind === "select" || p.kind === "text" || p.kind === "number" || p.kind === "date") { + if (p.defaultValue != null) seed[p.key] = p.defaultValue; + } + } + if (initialParams) Object.assign(seed, initialParams); + return seed; + }); + + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const run = useCallback( + (p: Record) => { + setLoading(true); + setError(null); + runReport(def.slug, p) + .then(setResult) + .catch((e) => { + setError(e?.message ?? "No se pudo correr el reporte."); + setResult(null); + }) + .finally(() => setLoading(false)); + }, + [def.slug], + ); + + // Auto-run on mount so the runner opens with results, not blank. + useEffect(() => { + run(params); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function updateParam(key: string, value: string) { + setParams((prev) => ({ ...prev, [key]: value })); + } + + function applyFilters(e?: React.FormEvent) { + e?.preventDefault(); + run(params); + } + + return ( +
+
+ {def.params.map((p) => ( + updateParam(p.key, v)} + /> + ))} +
+ +
+ + + {error &&
{error}
} + + {result && ( + + )} +
+ ); +} + +/* ---------------------------------------------------------- one param field */ + +function ParamField({ + param, + value, + onChange, +}: { + param: ReportParam; + value: string; + onChange: (v: string) => void; +}) { + const label = ( + + {param.label} + {param.kind === "customer-picker" && !value && ( + + (requerido) + + )} + + ); + + if (param.kind === "select") { + return ( + + ); + } + if (param.kind === "date") { + return ( + + ); + } + if (param.kind === "number") { + return ( + + ); + } + if (param.kind === "customer-picker") { + return ( +
+ {label} + onChange(id)} + /> +
+ ); + } + return ( + + ); +} + +/* ---------------------------------------------------------- results block */ + +function ResultBlock({ + def, + result, + params, + loading, +}: { + def: ReportDef; + result: ReportRunResult; + params: Record; + loading: boolean; +}) { + return ( +
+
+
+ {result.subtitle &&

{result.subtitle}

} +

+ {formatNumber(result.rows.length)} fila + {result.rows.length === 1 ? "" : "s"} + {result.totals && ( + <> + {" "}·{" "} + {Object.entries(result.totals) + .map(([k, v]) => `${k}: ${v}`) + .join(" · ")} + + )} +

+
+ +
+ + {loading &&
Actualizando…
} + + {def.format === "statement" ? ( + + ) : ( + + )} +
+ ); +} + +/* ---------------------------------------------------------- tabular layout */ + +function TabularLayout({ result }: { result: ReportRunResult }) { + if (result.rows.length === 0) { + return ( +
+ No se encontraron filas con los filtros actuales. +
+ ); + } + return ( +
+ + + + {result.columns.map((c) => ( + + ))} + + + + {result.rows.map((r, i) => ( + + {result.columns.map((c) => { + const v = r[c.key]; + return ( + + ); + })} + + ))} + + {result.totals && ( + + + + + + )} +
+ {c.label} +
+ {formatCell(v, c.type)} +
+ {Object.entries(result.totals) + .map(([k, v]) => `${k}: ${v}`) + .join(" · ")} +
+
+ ); +} + +function formatCell(v: unknown, type: string): string { + if (v === null || v === undefined || v === "") return "—"; + if (type === "money") { + const n = Number(v); + return Number.isFinite(n) + ? new Intl.NumberFormat("es-MX", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(n) + : String(v); + } + if (type === "number") { + const n = Number(v); + return Number.isFinite(n) ? formatNumber(n) : String(v); + } + return String(v); +} + +/* --------------------------------------------------------- statement layout */ + +function StatementLayout({ result }: { result: ReportRunResult }) { + // The edo-cuenta-datos report synthesises a list with __kind + // discriminators (header, summary, movements-header, plain movement). + // Group by kind and render each block inline. + const header = result.rows.find((r) => r.__kind === "header") as + | Record + | undefined; + const summaries = result.rows.filter((r) => r.__kind === "summary"); + const movements = result.rows.filter( + (r) => r.__kind !== "header" && r.__kind !== "summary" && r.__kind !== "movements-header", + ); + + if (!header) { + return ( +
+ Selecciona un cliente y corre el reporte para ver el estado de cuenta. +
+ ); + } + + return ( +
+
+

{String(header.name ?? "—")}

+ {Boolean(header.address) && ( +

{String(header.address)}

+ )} + {Boolean(header.city) &&

{String(header.city)}

} + {Boolean(header.phone || header.email) && ( +

+ {String(header.phone ?? "")} + {header.phone && header.email ? " · " : ""} + {String(header.email ?? "")} +

+ )} +
+ + {summaries.length > 0 && ( +
+

Resumen por moneda

+ + + + + + + + + + + + {summaries.map((s, i) => ( + + + + + + + + ))} + +
MonedaCargosAbonosSaldoMovs.
{String(s.currency)} + {formatCell(s.charges, "money")} + + {formatCell(s.credits, "money")} + + {formatCell(s.balance, "money")} + {formatCell(s.count, "number")}
+
+ )} + + {movements.length > 0 && ( +
+

Movimientos

+
+ + + + + + + + + + + + {movements.map((m, i) => ( + + + + + + + + ))} + +
FechaConceptoReferenciaCargo / AbonoSaldo
{String(m.date ?? "")}{String(m.concept ?? "")}{String(m.reference ?? "")} + {formatCell(m.amount, "money")} + + {formatCell(m.balanceAfter, "money")} +
+
+
+ )} +
+ ); +} + +// Hint to the bundler that API_ORIGIN is part of the API surface used here. +void API_ORIGIN; diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index e529c68..1721ba0 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -44,6 +44,8 @@ import type { PropertyListResponse, PropertySort, PropertyStats, + ReportCatalog, + ReportRunResult, ServiceInput, TrustInput, Role, @@ -730,3 +732,37 @@ export function startOpsJob(kind: OpsJobKind, file?: string): Promise { body: JSON.stringify({ kind, file }), }); } + +/* ----------------------------------------------------------- Reports module */ + +export function getReportCatalog(): Promise { + return apiFetch("/reports"); +} + +export function runReport( + slug: string, + params: Record, +): Promise { + const qs = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) { + if (v != null && v !== "") qs.set(k, v); + } + const tail = qs.toString(); + return apiFetch(`/reports/${slug}${tail ? `?${tail}` : ""}`); +} + +/** Build a download URL for a report's file output. The session cookie + * travels with the browser's same-origin navigation, so a plain `href` + * is enough — no fetch-with-credentials dance. */ +export function reportDownloadUrl( + slug: string, + format: "csv" | "xlsx" | "pdf" | "print", + params: Record, +): string { + const qs = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) { + if (v != null && v !== "") qs.set(k, v); + } + const tail = qs.toString(); + return `${API_ORIGIN}/reports/${slug}/${format}${tail ? `?${tail}` : ""}`; +} diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts index 9f37040..3440ae5 100644 --- a/apps/web/src/lib/types.ts +++ b/apps/web/src/lib/types.ts @@ -1025,3 +1025,58 @@ export interface BankSummary { /** Cumulative figure the selected year opened on. */ opening: string; } + +/* ----------------------------------------------------------- Reports module */ + +export type ReportDomain = + | "clientes" + | "polizas" + | "servicios" + | "estado-cuenta" + | "chequera"; + +export type ReportFormat = "tabular" | "statement"; + +/** Param declaration a report exposes to its filter form. */ +export type ReportParam = + | { key: string; label: string; kind: "text"; placeholder?: string; defaultValue?: string } + | { key: string; label: string; kind: "number"; defaultValue?: string } + | { key: string; label: string; kind: "date"; endOfDay?: boolean; defaultValue?: string } + | { + key: string; + label: string; + kind: "select"; + options: { value: string; label: string }[]; + defaultValue?: string; + } + | { key: string; label: string; kind: "customer-picker" }; + +export interface ReportColumn { + key: string; + label: string; + type: "text" | "number" | "money" | "date"; + align?: "left" | "right"; + width?: number; +} + +export interface ReportDef { + slug: string; + title: string; + description: string; + domain: ReportDomain; + legacyName: string | null; + format: ReportFormat; + params: ReportParam[]; + columns: ReportColumn[]; +} + +export interface ReportRunResult { + columns: ReportColumn[]; + rows: Array>; + totals?: Record; + subtitle?: string; +} + +export interface ReportCatalog { + items: ReportDef[]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81e0574..7ffc27e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ importers: class-validator: specifier: ^0.14.1 version: 0.14.4 + exceljs: + specifier: ^4.4.0 + version: 4.4.0 express-session: specifier: ^1.18.0 version: 1.19.0 @@ -49,6 +52,9 @@ importers: passport-local: specifier: ^1.0.0 version: 1.0.0 + pdfkit: + specifier: ^0.15.1 + version: 0.15.2 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -80,6 +86,9 @@ importers: '@types/passport-local': specifier: ^1.0.38 version: 1.0.38 + '@types/pdfkit': + specifier: ^0.13.5 + version: 0.13.9 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.19.43)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) @@ -396,6 +405,12 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@fast-csv/format@4.3.5': + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + + '@fast-csv/parse@4.3.6': + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -717,6 +732,9 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.3.17': + resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==} + '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} @@ -799,6 +817,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} @@ -811,6 +832,9 @@ packages: '@types/passport@1.0.17': resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/pdfkit@0.13.9': + resolution: {integrity: sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -975,6 +999,18 @@ packages: append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -988,12 +1024,23 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1022,6 +1069,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1030,13 +1081,23 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + body-parser@1.20.4: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1054,6 +1115,12 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + browserslist@4.28.7: resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1066,12 +1133,23 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1107,6 +1185,9 @@ packages: caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1174,6 +1255,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -1199,6 +1284,10 @@ packages: resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} engines: {node: '>= 6'} + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1243,6 +1332,15 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1255,9 +1353,15 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1283,6 +1387,10 @@ packages: babel-plugin-macros: optional: true + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -1294,6 +1402,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1306,6 +1418,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1326,6 +1441,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1349,6 +1467,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.24.3: resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} @@ -1364,6 +1485,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -1415,6 +1539,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + exceljs@4.4.0: + resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} + engines: {node: '>=8.3.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -1439,6 +1567,10 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-csv@4.3.6: + resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} + engines: {node: '>=10.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1477,6 +1609,13 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + fontkit@1.9.0: + resolution: {integrity: sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1496,6 +1635,9 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -1511,9 +1653,17 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1566,6 +1716,10 @@ packages: engines: {node: '>=0.4.7'} hasBin: true + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1581,6 +1735,10 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -1603,6 +1761,9 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1631,21 +1792,49 @@ packages: resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==} engines: {node: '>=18'} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1666,18 +1855,60 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1845,6 +2076,10 @@ packages: node-notifier: optional: true + jpeg-exif@1.1.4: + resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1884,10 +2119,17 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -1895,9 +2137,18 @@ packages: libphonenumber-js@1.13.9: resolution: {integrity: sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + loader-runner@4.3.2: resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} engines: {node: '>=6.11.5'} @@ -1906,9 +2157,49 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -1989,6 +2280,10 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -2100,6 +2395,18 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2142,6 +2449,12 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2198,6 +2511,9 @@ packages: pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + pdfkit@0.15.2: + resolution: {integrity: sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2221,6 +2537,13 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + png-js@1.1.0: + resolution: {integrity: sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -2234,6 +2557,9 @@ packages: engines: {node: '>=16.13'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -2277,10 +2603,16 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -2288,6 +2620,10 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -2325,6 +2661,14 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + restructure@2.0.1: + resolution: {integrity: sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -2339,12 +2683,23 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -2377,6 +2732,13 @@ packages: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -2447,6 +2809,10 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -2463,6 +2829,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2527,6 +2896,10 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + terser-webpack-plugin@5.6.1: resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} engines: {node: '>= 10.13.0'} @@ -2582,10 +2955,17 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -2604,6 +2984,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2709,6 +3092,12 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2717,6 +3106,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -2733,6 +3125,11 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -2782,6 +3179,18 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2809,6 +3218,9 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -2836,6 +3248,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + snapshots: '@angular-devkit/core@17.3.11(chokidar@3.6.0)': @@ -3233,6 +3649,25 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@fast-csv/format@4.3.5': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + + '@fast-csv/parse@4.3.6': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3670,6 +4105,10 @@ snapshots: '@swc/counter@0.1.3': {} + '@swc/helpers@0.3.17': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 @@ -3778,6 +4217,8 @@ snapshots: '@types/mime@1.3.5': {} + '@types/node@14.18.63': {} + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -3797,6 +4238,10 @@ snapshots: dependencies: '@types/express': 4.17.25 + '@types/pdfkit@0.13.9': + dependencies: + '@types/node': 20.19.43 + '@types/prop-types@15.7.15': {} '@types/qs@6.15.1': {} @@ -3991,6 +4436,42 @@ snapshots: append-field@1.0.0: {} + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + arg@4.1.3: {} argon2@0.41.1: @@ -4005,10 +4486,21 @@ snapshots: argparse@2.0.1: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + array-flatten@1.1.1: {} array-timsort@1.0.3: {} + async@3.2.6: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + babel-jest@29.7.0(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 @@ -4066,18 +4558,29 @@ snapshots: balanced-match@1.0.2: {} + base64-js@0.0.8: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.11.0: {} + big-integer@1.6.52: {} + binary-extensions@2.3.0: {} + binary@0.3.0: + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 + bluebird@3.4.7: {} + body-parser@1.20.4: dependencies: bytes: 3.1.2 @@ -4110,6 +4613,14 @@ snapshots: dependencies: fill-range: 7.1.1 + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + browserslist@4.28.7: dependencies: baseline-browser-mapping: 2.11.0 @@ -4126,13 +4637,19 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} + buffer-indexof-polyfill@1.0.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + buffers@0.1.1: {} + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -4164,6 +4681,10 @@ snapshots: caniuse-lite@1.0.30001806: {} + chainsaw@0.1.0: + dependencies: + traverse: 0.3.9 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4227,6 +4748,8 @@ snapshots: clone@1.0.4: {} + clone@2.1.2: {} + co@4.6.0: {} collect-v8-coverage@1.0.3: {} @@ -4249,6 +4772,13 @@ snapshots: has-own-prop: 2.0.0 repeat-string: 1.6.1 + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -4288,6 +4818,13 @@ snapshots: optionalDependencies: typescript: 5.7.2 + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + create-jest@29.7.0(@types/node@20.19.43)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 @@ -4311,8 +4848,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + csstype@3.2.3: {} + dayjs@1.11.21: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -4323,6 +4864,27 @@ snapshots: dedent@1.7.2: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.1 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + deepmerge@4.3.1: {} defaults@1.0.4: @@ -4335,12 +4897,20 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + depd@2.0.0: {} destroy@1.2.0: {} detect-newline@3.1.0: {} + dfa@1.2.0: {} + diff-sequences@29.6.3: {} diff@4.0.4: {} @@ -4355,6 +4925,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -4369,6 +4943,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 @@ -4382,6 +4960,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.9 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-module-lexer@1.7.0: {} es-object-atoms@1.1.2: @@ -4415,6 +5005,18 @@ snapshots: events@3.3.0: {} + exceljs@4.4.0: + dependencies: + archiver: 5.3.2 + dayjs: 1.11.21 + fast-csv: 4.3.6 + jszip: 3.10.1 + readable-stream: 3.6.2 + saxes: 5.0.1 + tmp: 0.2.7 + unzipper: 0.10.14 + uuid: 8.3.2 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -4492,6 +5094,11 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-csv@4.3.6: + dependencies: + '@fast-csv/format': 4.3.5 + '@fast-csv/parse': 4.3.6 + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -4540,6 +5147,22 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + fontkit@1.9.0: + dependencies: + '@swc/helpers': 0.3.17 + brotli: 1.3.3 + clone: 2.1.2 + deep-equal: 2.2.3 + dfa: 1.2.0 + restructure: 2.0.1 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -4566,6 +5189,8 @@ snapshots: fresh@0.5.2: {} + fs-constants@1.0.0: {} + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -4579,8 +5204,17 @@ snapshots: fsevents@2.3.3: optional: true + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + function-bind@1.1.2: {} + functions-have-names@1.2.3: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -4644,6 +5278,8 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 + has-bigints@1.1.0: {} + has-flag@4.0.0: {} has-own-prop@2.0.0: {} @@ -4654,6 +5290,10 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -4676,6 +5316,8 @@ snapshots: ieee754@1.2.1: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4731,18 +5373,51 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + ipaddr.js@1.9.1: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + is-core-module@2.16.2: dependencies: hasown: 2.0.4 + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4755,12 +5430,54 @@ snapshots: is-interactive@1.0.0: {} + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + is-stream@2.0.1: {} + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + is-unicode-supported@0.1.0: {} + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -5127,6 +5844,8 @@ snapshots: - supports-color - ts-node + jpeg-exif@1.1.4: {} + js-tokens@4.0.0: {} js-yaml@3.15.0: @@ -5158,22 +5877,70 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + kleur@3.0.3: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + leven@3.1.0: {} libphonenumber-js@1.13.9: {} + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + lines-and-columns@1.2.4: {} + listenercount@1.0.1: {} + loader-runner@4.3.2: {} locate-path@5.0.0: dependencies: p-locate: 4.1.0 + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + + lodash.groupby@4.6.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isequal@4.5.0: {} + + lodash.isfunction@3.0.9: {} + + lodash.isnil@4.0.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isundefined@3.0.1: {} + lodash.memoize@4.1.2: {} + lodash.union@4.6.0: {} + + lodash.uniq@4.5.0: {} + lodash@4.17.21: {} lodash@4.18.1: {} @@ -5240,6 +6007,10 @@ snapshots: dependencies: brace-expansion: 1.1.16 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.2 + minimatch@9.0.9: dependencies: brace-expansion: 2.1.2 @@ -5331,6 +6102,22 @@ snapshots: object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -5375,6 +6162,10 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@0.2.9: {} + + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5421,6 +6212,14 @@ snapshots: pause@0.0.1: {} + pdfkit@0.15.2: + dependencies: + crypto-js: 4.2.0 + fontkit: 1.9.0 + jpeg-exif: 1.1.4 + linebreak: 1.1.0 + png-js: 1.1.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5435,6 +6234,12 @@ snapshots: pluralize@8.0.0: {} + png-js@1.1.0: + dependencies: + browserify-zlib: 0.2.0 + + possible-typed-array-names@1.1.0: {} + postcss@8.4.31: dependencies: nanoid: 3.3.16 @@ -5453,6 +6258,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + process-nextick-args@2.0.1: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -5494,18 +6301,41 @@ snapshots: dependencies: loose-envify: 1.4.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + readdirp@3.6.0: dependencies: picomatch: 2.3.2 reflect-metadata@0.2.2: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + repeat-string@1.6.1: {} require-directory@2.1.1: {} @@ -5534,6 +6364,12 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restructure@2.0.1: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + run-async@2.4.1: {} run-async@3.0.0: {} @@ -5546,10 +6382,22 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -5607,6 +6455,15 @@ snapshots: gopd: 1.2.0 has-property-descriptors: 1.0.2 + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -5675,6 +6532,11 @@ snapshots: statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + streamsearch@1.1.0: {} string-length@4.0.2: @@ -5694,6 +6556,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -5737,6 +6603,14 @@ snapshots: tapable@2.3.3: {} + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + terser-webpack-plugin@5.6.1(webpack@5.97.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -5760,10 +6634,14 @@ snapshots: through@2.3.8: {} + tiny-inflate@1.0.3: {} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 + tmp@0.2.7: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -5780,6 +6658,8 @@ snapshots: tr46@0.0.3: {} + traverse@0.3.9: {} + tree-kill@1.2.2: {} ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.43)(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3): @@ -5867,10 +6747,33 @@ snapshots: undici-types@6.21.0: {} + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + universalify@2.0.1: {} unpipe@1.0.0: {} + unzipper@0.10.14: + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: browserslist: 4.28.7 @@ -5885,6 +6788,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@8.3.2: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: @@ -5959,6 +6864,31 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -5990,6 +6920,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + xmlchars@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -6011,3 +6943,9 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2