From 3beee1c17431e9c6ea5bf22487e7970a3bfc8515 Mon Sep 17 00:00:00 2001 From: Consultoria AS Date: Tue, 17 Feb 2026 02:37:20 +0000 Subject: [PATCH] feat(web): add CfdiViewerModal with PDF and XML download Co-Authored-By: Claude Opus 4.5 --- .../web/components/cfdi/cfdi-viewer-modal.tsx | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 apps/web/components/cfdi/cfdi-viewer-modal.tsx diff --git a/apps/web/components/cfdi/cfdi-viewer-modal.tsx b/apps/web/components/cfdi/cfdi-viewer-modal.tsx new file mode 100644 index 0000000..27943db --- /dev/null +++ b/apps/web/components/cfdi/cfdi-viewer-modal.tsx @@ -0,0 +1,169 @@ +'use client'; + +import { useRef, useState, useEffect } from 'react'; +import type { Cfdi } from '@horux/shared'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { CfdiInvoice } from './cfdi-invoice'; +import { getCfdiXml } from '@/lib/api/cfdi'; +import { Download, FileText, Loader2 } from 'lucide-react'; + +interface CfdiConcepto { + descripcion: string; + cantidad: number; + valorUnitario: number; + importe: number; +} + +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[] = []; + + // Find all Concepto elements + 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'), + }); + } + } + + return conceptos; + } catch { + return []; + } +} + +export function CfdiViewerModal({ cfdi, open, onClose }: CfdiViewerModalProps) { + const invoiceRef = useRef(null); + const [conceptos, setConceptos] = useState([]); + const [downloading, setDownloading] = useState<'pdf' | 'xml' | null>(null); + const [xmlContent, setXmlContent] = useState(null); + + useEffect(() => { + if (cfdi?.xmlOriginal) { + setXmlContent(cfdi.xmlOriginal); + setConceptos(parseConceptosFromXml(cfdi.xmlOriginal)); + } else { + setXmlContent(null); + setConceptos([]); + } + }, [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.uuidFiscal.substring(0, 8)}.pdf`, + image: { type: 'jpeg', quality: 0.98 }, + html2canvas: { scale: 2, useCORS: true }, + jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, + }; + + 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(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.uuidFiscal.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); + } + }; + + if (!cfdi) return null; + + return ( + !isOpen && onClose()}> + + +
+ Vista de Factura +
+ + +
+
+
+ +
+ +
+
+
+ ); +}