Nota de horas: generar PDF en el servidor (sin URL del navegador)
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) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
89d7028044
commit
feb88de203
@@ -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<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(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({
|
||||
<h3 className="font-semibold text-sm truncate">{TITULO_NOTA[estadoFiltro]} — {numero}</h3>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={imprimirDesdePreview}
|
||||
className="flex items-center gap-2 px-3 py-2 border border-border rounded-lg text-sm hover:bg-gray-50"
|
||||
onClick={descargarNotaPDF}
|
||||
disabled={descargando}
|
||||
className="flex items-center gap-2 px-3 py-2 border border-border rounded-lg text-sm hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
<Printer className="w-4 h-4 text-primary" />
|
||||
Imprimir / Guardar PDF
|
||||
<Download className="w-4 h-4 text-primary" />
|
||||
{descargando ? "Generando…" : "Descargar PDF"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPreviewHtml(null)}
|
||||
|
||||
Reference in New Issue
Block a user