This reverts commit d3b326e.
The deployment caused reports of blank screens and 400 errors. Reverting to restore stable state while investigating root cause.
248 lines
7.3 KiB
TypeScript
248 lines
7.3 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useState, useEffect } from 'react';
|
|
import type { Cfdi } from '@horux/shared';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, Button } from '@horux/shared-ui';
|
|
import { CfdiInvoice } from './cfdi-invoice';
|
|
import { getCfdiXml, getCfdiConceptos } from '@/lib/api/cfdi';
|
|
import { Download, FileText, Loader2, Printer } from 'lucide-react';
|
|
|
|
interface CfdiConcepto {
|
|
descripcion: string;
|
|
cantidad: number;
|
|
valorUnitario: number;
|
|
importe: number;
|
|
claveProdServ?: string;
|
|
claveUnidad?: string;
|
|
noIdentificacion?: string;
|
|
}
|
|
|
|
interface CfdiViewerModalProps {
|
|
cfdi: Cfdi | null;
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function parseConceptosFromXml(xmlString: string): CfdiConcepto[] {
|
|
try {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(xmlString, 'text/xml');
|
|
const conceptos: CfdiConcepto[] = [];
|
|
|
|
const elements = doc.getElementsByTagName('*');
|
|
for (let i = 0; i < elements.length; i++) {
|
|
if (elements[i].localName === 'Concepto') {
|
|
const el = elements[i];
|
|
conceptos.push({
|
|
descripcion: el.getAttribute('Descripcion') || '',
|
|
cantidad: parseFloat(el.getAttribute('Cantidad') || '1'),
|
|
valorUnitario: parseFloat(el.getAttribute('ValorUnitario') || '0'),
|
|
importe: parseFloat(el.getAttribute('Importe') || '0'),
|
|
claveProdServ: el.getAttribute('ClaveProdServ') || undefined,
|
|
claveUnidad: el.getAttribute('ClaveUnidad') || undefined,
|
|
noIdentificacion: el.getAttribute('NoIdentificacion') || undefined,
|
|
});
|
|
}
|
|
}
|
|
|
|
return conceptos;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function CfdiViewerModal({ cfdi, open, onClose }: CfdiViewerModalProps) {
|
|
const invoiceRef = useRef<HTMLDivElement>(null);
|
|
const [conceptos, setConceptos] = useState<CfdiConcepto[]>([]);
|
|
const [downloading, setDownloading] = useState<'pdf' | 'xml' | null>(null);
|
|
const [xmlContent, setXmlContent] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!cfdi) {
|
|
setXmlContent(null);
|
|
setConceptos([]);
|
|
return;
|
|
}
|
|
|
|
if (cfdi.xmlOriginal) setXmlContent(cfdi.xmlOriginal);
|
|
|
|
// Fetch conceptos from DB, fallback to XML parsing
|
|
getCfdiConceptos(cfdi.id)
|
|
.then((dbConceptos) => {
|
|
if (dbConceptos.length > 0) {
|
|
setConceptos(dbConceptos.map((c: any) => ({
|
|
descripcion: c.descripcion,
|
|
cantidad: Number(c.cantidad),
|
|
valorUnitario: Number(c.valorUnitario),
|
|
importe: Number(c.importe),
|
|
claveProdServ: c.claveProdServ || undefined,
|
|
claveUnidad: c.claveUnidad || undefined,
|
|
noIdentificacion: c.noIdentificacion || undefined,
|
|
})));
|
|
} else if (cfdi.xmlOriginal) {
|
|
setConceptos(parseConceptosFromXml(cfdi.xmlOriginal));
|
|
} else {
|
|
setConceptos([]);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (cfdi.xmlOriginal) {
|
|
setConceptos(parseConceptosFromXml(cfdi.xmlOriginal));
|
|
}
|
|
});
|
|
}, [cfdi]);
|
|
|
|
const handleDownloadPdf = async () => {
|
|
if (!invoiceRef.current || !cfdi) return;
|
|
|
|
setDownloading('pdf');
|
|
try {
|
|
const html2pdf = (await import('html2pdf.js')).default;
|
|
|
|
const opt = {
|
|
margin: 10,
|
|
filename: `factura-${cfdi.uuid.substring(0, 8)}.pdf`,
|
|
image: { type: 'jpeg' as const, quality: 0.98 },
|
|
html2canvas: { scale: 2, useCORS: true },
|
|
jsPDF: { unit: 'mm' as const, format: 'a4' as const, orientation: 'portrait' as const },
|
|
};
|
|
|
|
await html2pdf().set(opt).from(invoiceRef.current).save();
|
|
} catch (error) {
|
|
console.error('Error generating PDF:', error);
|
|
alert('Error al generar el PDF');
|
|
} finally {
|
|
setDownloading(null);
|
|
}
|
|
};
|
|
|
|
const handleDownloadXml = async () => {
|
|
if (!cfdi) return;
|
|
|
|
setDownloading('xml');
|
|
try {
|
|
let xml = xmlContent;
|
|
|
|
if (!xml) {
|
|
xml = await getCfdiXml(String(cfdi.id));
|
|
}
|
|
|
|
if (!xml) {
|
|
alert('No hay XML disponible para este CFDI');
|
|
return;
|
|
}
|
|
|
|
const blob = new Blob([xml], { type: 'application/xml' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `cfdi-${cfdi.uuid.substring(0, 8)}.xml`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
} catch (error) {
|
|
console.error('Error downloading XML:', error);
|
|
alert('Error al descargar el XML');
|
|
} finally {
|
|
setDownloading(null);
|
|
}
|
|
};
|
|
|
|
const handlePrint = () => {
|
|
if (!invoiceRef.current) return;
|
|
|
|
// Create a print-specific stylesheet
|
|
const printStyles = document.createElement('style');
|
|
printStyles.innerHTML = `
|
|
@media print {
|
|
body * {
|
|
visibility: hidden;
|
|
}
|
|
#cfdi-print-area, #cfdi-print-area * {
|
|
visibility: visible;
|
|
}
|
|
#cfdi-print-area {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
padding: 20px;
|
|
}
|
|
@page {
|
|
size: A4;
|
|
margin: 15mm;
|
|
}
|
|
}
|
|
`;
|
|
document.head.appendChild(printStyles);
|
|
|
|
// Add ID to the invoice container for print targeting
|
|
invoiceRef.current.id = 'cfdi-print-area';
|
|
|
|
// Trigger print
|
|
window.print();
|
|
|
|
// Clean up
|
|
document.head.removeChild(printStyles);
|
|
invoiceRef.current.removeAttribute('id');
|
|
};
|
|
|
|
if (!cfdi) return null;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<div className="flex items-center justify-between">
|
|
<DialogTitle>Vista de Factura</DialogTitle>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleDownloadPdf}
|
|
disabled={downloading !== null}
|
|
>
|
|
{downloading === 'pdf' ? (
|
|
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
|
) : (
|
|
<Download className="h-4 w-4 mr-1" />
|
|
)}
|
|
PDF
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleDownloadXml}
|
|
disabled={downloading !== null || !xmlContent}
|
|
title={!xmlContent ? 'XML no disponible' : 'Descargar XML'}
|
|
>
|
|
{downloading === 'xml' ? (
|
|
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
|
) : (
|
|
<FileText className="h-4 w-4 mr-1" />
|
|
)}
|
|
XML
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handlePrint}
|
|
disabled={downloading !== null}
|
|
title="Imprimir factura"
|
|
>
|
|
<Printer className="h-4 w-4 mr-1" />
|
|
Imprimir
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<div className="border rounded-lg overflow-hidden bg-gray-50 p-4">
|
|
<CfdiInvoice ref={invoiceRef} cfdi={cfdi} conceptos={conceptos} />
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|