From 837831ccd4bd47dff1571127ade243aea51a7cdc Mon Sep 17 00:00:00 2001 From: Consultoria AS Date: Tue, 17 Feb 2026 02:36:18 +0000 Subject: [PATCH] feat(web): add CfdiInvoice component for PDF-like rendering Co-Authored-By: Claude Opus 4.5 --- apps/web/components/cfdi/cfdi-invoice.tsx | 230 ++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 apps/web/components/cfdi/cfdi-invoice.tsx diff --git a/apps/web/components/cfdi/cfdi-invoice.tsx b/apps/web/components/cfdi/cfdi-invoice.tsx new file mode 100644 index 0000000..6e2d538 --- /dev/null +++ b/apps/web/components/cfdi/cfdi-invoice.tsx @@ -0,0 +1,230 @@ +'use client'; + +import { forwardRef } from 'react'; +import type { Cfdi } from '@horux/shared'; + +interface CfdiConcepto { + descripcion: string; + cantidad: number; + valorUnitario: number; + importe: number; + claveUnidad?: string; + claveProdServ?: string; +} + +interface CfdiInvoiceProps { + cfdi: Cfdi; + conceptos?: CfdiConcepto[]; +} + +const formatCurrency = (value: number) => + new Intl.NumberFormat('es-MX', { + style: 'currency', + currency: 'MXN', + }).format(value); + +const formatDate = (dateString: string) => + new Date(dateString).toLocaleDateString('es-MX', { + day: '2-digit', + month: 'long', + year: 'numeric', + }); + +const tipoLabels: Record = { + ingreso: 'Ingreso', + egreso: 'Egreso', + traslado: 'Traslado', + pago: 'Pago', + nomina: 'Nomina', +}; + +const formaPagoLabels: Record = { + '01': 'Efectivo', + '02': 'Cheque nominativo', + '03': 'Transferencia electrónica', + '04': 'Tarjeta de crédito', + '28': 'Tarjeta de débito', + '99': 'Por definir', +}; + +const metodoPagoLabels: Record = { + PUE: 'Pago en una sola exhibición', + PPD: 'Pago en parcialidades o diferido', +}; + +export const CfdiInvoice = forwardRef( + ({ cfdi, conceptos }, ref) => { + return ( +
+ {/* Header */} +
+
+ [LOGO] +
+
+

FACTURA

+

+ {cfdi.serie && `Serie: ${cfdi.serie} `} + {cfdi.folio && `Folio: ${cfdi.folio}`} +

+

Fecha: {formatDate(cfdi.fechaEmision)}

+ + {cfdi.estado === 'vigente' ? 'VIGENTE' : 'CANCELADO'} + +
+
+ + {/* Emisor / Receptor */} +
+
+

+ EMISOR +

+

{cfdi.nombreEmisor}

+

RFC: {cfdi.rfcEmisor}

+
+
+

+ RECEPTOR +

+

{cfdi.nombreReceptor}

+

RFC: {cfdi.rfcReceptor}

+ {cfdi.usoCfdi && ( +

Uso CFDI: {cfdi.usoCfdi}

+ )} +
+
+ + {/* Datos del Comprobante */} +
+

+ DATOS DEL COMPROBANTE +

+
+
+ Tipo: +

{tipoLabels[cfdi.tipo] || cfdi.tipo}

+
+
+ Método de Pago: +

+ {cfdi.metodoPago ? metodoPagoLabels[cfdi.metodoPago] || cfdi.metodoPago : '-'} +

+
+
+ Forma de Pago: +

+ {cfdi.formaPago ? formaPagoLabels[cfdi.formaPago] || cfdi.formaPago : '-'} +

+
+
+ Moneda: +

+ {cfdi.moneda} + {cfdi.tipoCambio !== 1 && ` (TC: ${cfdi.tipoCambio})`} +

+
+
+
+ + {/* Conceptos */} + {conceptos && conceptos.length > 0 && ( +
+

+ CONCEPTOS +

+ + + + + + + + + + + {conceptos.map((concepto, idx) => ( + + + + + + + ))} + +
DescripciónCant.P. Unit.Importe
{concepto.descripcion}{concepto.cantidad} + {formatCurrency(concepto.valorUnitario)} + + {formatCurrency(concepto.importe)} +
+
+ )} + + {/* Totales */} +
+
+
+ Subtotal: + {formatCurrency(cfdi.subtotal)} +
+ {cfdi.descuento > 0 && ( +
+ Descuento: + -{formatCurrency(cfdi.descuento)} +
+ )} + {cfdi.iva > 0 && ( +
+ IVA (16%): + {formatCurrency(cfdi.iva)} +
+ )} + {cfdi.ivaRetenido > 0 && ( +
+ IVA Retenido: + -{formatCurrency(cfdi.ivaRetenido)} +
+ )} + {cfdi.isrRetenido > 0 && ( +
+ ISR Retenido: + -{formatCurrency(cfdi.isrRetenido)} +
+ )} +
+ TOTAL: + {formatCurrency(cfdi.total)} +
+
+
+ + {/* Timbre Fiscal */} +
+

TIMBRE FISCAL DIGITAL

+
+
+

UUID:

+

{cfdi.uuidFiscal}

+
+
+

Fecha de Timbrado:

+

{cfdi.fechaTimbrado}

+
+
+
+
+ ); + } +); + +CfdiInvoice.displayName = 'CfdiInvoice';