From 3824dd00bea1da8fa61144f2b892aa918df2e5c0 Mon Sep 17 00:00:00 2001 From: urieljareth Date: Wed, 8 Jul 2026 12:46:35 -0600 Subject: [PATCH] Nota de horas: datos de pago (bancarios) + periodo en el nombre del archivo - Las notas "por pagar" ahora incluyen los datos bancarios de cobro configurados (Transferencia Nacional/Internacional: cuenta, CLABE, beneficiario, RFC, banco, SWIFT), igual que el PDF de cotizacion. Solo se muestran en las notas por pagar. - El nombre del PDF incluye el periodo cobrado (fecha minima a maxima de los registros), formato DD-MM-YYYY_DD-MM-YYYY. Ej: "Nota de horas por pagar UJ2606AG777 - 24-06-2026_24-06-2026". Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cotizaciones/[id]/RegistroHorasPanel.tsx | 9 +++- .../api/cotizaciones/[id]/nota-horas/route.ts | 34 +++++++++++-- src/lib/nota-horas-pdf.ts | 48 +++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/app/(app)/cotizaciones/[id]/RegistroHorasPanel.tsx b/src/app/(app)/cotizaciones/[id]/RegistroHorasPanel.tsx index e84cfdf..d8481c7 100644 --- a/src/app/(app)/cotizaciones/[id]/RegistroHorasPanel.tsx +++ b/src/app/(app)/cotizaciones/[id]/RegistroHorasPanel.tsx @@ -271,10 +271,17 @@ export function RegistroHorasPanel({ return; } const blob = await res.blob(); + // Periodo cobrado para el nombre: fecha minima_maxima de los registros de la nota. + const fechas = filtrados.map((r) => r.fecha.slice(0, 10)).sort(); + const dd = (iso: string) => { + const [y, mo, d] = iso.split("-"); + return `${d}-${mo}-${y}`; + }; + const rango = fechas.length ? `${dd(fechas[0])}_${dd(fechas[fechas.length - 1])}` : ""; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `${TITULO_NOTA[estadoFiltro]} ${numero}.pdf`; + a.download = `${TITULO_NOTA[estadoFiltro]} ${numero}${rango ? ` - ${rango}` : ""}.pdf`; document.body.appendChild(a); a.click(); a.remove(); diff --git a/src/app/api/cotizaciones/[id]/nota-horas/route.ts b/src/app/api/cotizaciones/[id]/nota-horas/route.ts index 3f4207d..4e9d61d 100644 --- a/src/app/api/cotizaciones/[id]/nota-horas/route.ts +++ b/src/app/api/cotizaciones/[id]/nota-horas/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; -import { getConfigBranding } from "@/lib/config-helpers"; +import { getConfigBranding, getConfigBancaria } from "@/lib/config-helpers"; import { generateNotaHorasPDF, type NotaHorasGrupo } from "@/lib/nota-horas-pdf"; import { IVA_RATE, @@ -41,12 +41,13 @@ export async function POST( const from: string = ISO_RE.test(body?.from) ? body.from : ""; const to: string = ISO_RE.test(body?.to) ? body.to : ""; - const [cot, branding] = await Promise.all([ + const [cot, branding, bancaria] = await Promise.all([ prisma.cotizacion.findUnique({ where: { id }, include: { cliente: true, registrosHoras: true }, }), getConfigBranding(), + getConfigBancaria(), ]); if (!cot) return NextResponse.json({ error: "No encontrada" }, { status: 404 }); @@ -96,6 +97,32 @@ export async function POST( : "Periodo: todos los registros"; const modoLabel = MODOS_AGRUPACION.find((mm) => mm.value === modo)?.label ?? "Detalle"; + // Datos bancarios para el cobro (misma extraccion que el PDF de cotizacion). + // Solo se muestran en las notas "por pagar" (las que se envian para cobro). + const cfg = bancaria || {}; + const datosPago = + estado === "por_pagar" + ? { + razonSocial: cfg.razon_social || "URIEL JARETH ALVARADO ORTIZ", + rfc: cfg.rfc || "AAOU970201SU7", + cuentaNacional: cfg.cuenta_nacional || "", + clabeNacional: cfg.clabe_interbancaria || cfg.cuenta_nacional || "", + bancoNacional: cfg.cuenta_nacional ? "BBVA" : "", + clabeInternacional: cfg.cuenta_internacional_swift?.match(/CLABE[^:]*:\s*(\S+)/i)?.[1] || "", + swift: cfg.cuenta_internacional_swift?.match(/SWIFT[^:]*:\s*(\S+)/i)?.[1] || "BCMRMXMMPYM", + } + : undefined; + + // Periodo cobrado para el nombre del archivo: fecha minima_maxima (DD-MM-YYYY). + const fechasISO = regs.map((r) => r.fecha.toISOString().slice(0, 10)).sort(); + const ddmmyyyy = (iso: string) => { + const [yy, mo, dd] = iso.split("-"); + return `${dd}-${mo}-${yy}`; + }; + const rango = fechasISO.length + ? `${ddmmyyyy(fechasISO[0])}_${ddmmyyyy(fechasISO[fechasISO.length - 1])}` + : ""; + const buffer = await generateNotaHorasPDF({ numero: cot.numero, proyecto: cot.proyecto, @@ -123,10 +150,11 @@ export async function POST( iva, total, etiquetaTotal: ETIQUETA_TOTAL[estado], + datosPago, ...branding, }); - const nombre = sanitizeFilename(`${TITULO[estado]} ${cot.numero}`); + const nombre = sanitizeFilename(`${TITULO[estado]} ${cot.numero}${rango ? ` - ${rango}` : ""}`); return new NextResponse(new Uint8Array(buffer), { headers: { "Content-Type": "application/pdf", diff --git a/src/lib/nota-horas-pdf.ts b/src/lib/nota-horas-pdf.ts index 2c050fc..3cf93ad 100644 --- a/src/lib/nota-horas-pdf.ts +++ b/src/lib/nota-horas-pdf.ts @@ -37,6 +37,16 @@ export interface NotaHorasPDFData { iva: number; total: number; etiquetaTotal: string; // "Total a pagar" | "Total pagado" | "Total" + // Datos bancarios para el cobro (se muestran solo en las notas "por pagar"). + datosPago?: { + razonSocial: string; + rfc: string; + cuentaNacional: string; + clabeNacional: string; + bancoNacional: string; + clabeInternacional: string; + swift: string; + }; colorPrimario?: string; colorSecundario?: string; logoBase64?: string; @@ -212,6 +222,44 @@ export async function generateNotaHorasPDF(data: NotaHorasPDFData): Promise