From feb88de2031c2e6068526907bce62574afe48220 Mon Sep 17 00:00:00 2001 From: urieljareth Date: Wed, 8 Jul 2026 10:59:04 -0600 Subject: [PATCH] Nota de horas: generar PDF en el servidor (sin URL del navegador) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Imprimir el HTML de la nota desde el navegador siempre inserta encabezado/pie con la URL de la cotizacion, fecha y no. de pagina (el @page{margin:0} no lo evita al imprimir un iframe). Se reemplaza por un PDF generado en el servidor con pdfkit, que no lleva ningun encabezado del navegador. - src/lib/nota-horas-pdf.ts: generateNotaHorasPDF (logo, titulo segun estado, tabla detalle/agrupada, columna Estado en "todas", totales con IVA opcional). - API POST /api/cotizaciones/[id]/nota-horas: filtra por estado/periodo, agrupa y calcula totales del lado servidor y devuelve el PDF. - Panel: el boton del modal pasa de "Imprimir" a "Descargar PDF" (baja el PDF del servidor); se conserva la vista previa HTML en pantalla. - pdf-generator: toPngBuffer ahora convierte webp/otros a PNG con sharp, así el logo (webp) tambien aparece en el PDF de la cotizacion. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cotizaciones/[id]/RegistroHorasPanel.tsx | 47 +++- .../api/cotizaciones/[id]/nota-horas/route.ts | 140 +++++++++++ src/lib/nota-horas-pdf.ts | 224 ++++++++++++++++++ src/lib/pdf-generator.ts | 10 +- 4 files changed, 405 insertions(+), 16 deletions(-) create mode 100644 src/app/api/cotizaciones/[id]/nota-horas/route.ts create mode 100644 src/lib/nota-horas-pdf.ts diff --git a/src/app/(app)/cotizaciones/[id]/RegistroHorasPanel.tsx b/src/app/(app)/cotizaciones/[id]/RegistroHorasPanel.tsx index 6550ac2..e84cfdf 100644 --- a/src/app/(app)/cotizaciones/[id]/RegistroHorasPanel.tsx +++ b/src/app/(app)/cotizaciones/[id]/RegistroHorasPanel.tsx @@ -1,7 +1,7 @@ "use client"; import { useMemo, useRef, useState } from "react"; -import { Clock, Plus, Trash2, Pencil, Printer, Eye, X, CheckCircle2, CircleDashed } from "lucide-react"; +import { Clock, Plus, Trash2, Pencil, Download, Eye, X, CheckCircle2, CircleDashed } from "lucide-react"; import { formatCurrency, formatFechaRegistro, @@ -97,6 +97,7 @@ export function RegistroHorasPanel({ const [from, setFrom] = useState(""); const [to, setTo] = useState(""); const [saving, setSaving] = useState(false); + const [descargando, setDescargando] = useState(false); const [togglingId, setTogglingId] = useState(null); const [error, setError] = useState(null); // HTML de la nota congelado al abrir la previsualización (null = modal cerrado). @@ -254,12 +255,35 @@ export function RegistroHorasPanel({ // Congela el HTML de la nota (con branding) y abre el modal de previsualización. const abrirPreview = () => setPreviewHtml(construirHTMLNota()); - // Imprime / guarda como PDF solo el contenido de la nota previsualizada. - const imprimirDesdePreview = () => { - const w = iframeRef.current?.contentWindow; - if (!w) return; - w.focus(); - w.print(); + // Descarga la nota como PDF generado en el SERVIDOR (pdfkit). A diferencia de imprimir + // el HTML desde el navegador, este PDF no lleva encabezados/pies del navegador (URL, + // fecha, no. de pagina). Respeta el filtro de estado, periodo y desglose actuales. + const descargarNotaPDF = async () => { + setDescargando(true); + try { + const res = await fetch(`/api/cotizaciones/${cotizacionId}/nota-horas`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ estado: estadoFiltro, modo, from, to }), + }); + if (!res.ok) { + alert("No se pudo generar el PDF"); + return; + } + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${TITULO_NOTA[estadoFiltro]} ${numero}.pdf`; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 2000); + } catch { + alert("Error de conexion al generar el PDF"); + } finally { + setDescargando(false); + } }; const badgeEstado = (estadoPago: string) => { @@ -715,11 +739,12 @@ export function RegistroHorasPanel({

{TITULO_NOTA[estadoFiltro]} — {numero}