Files
Horux360/apps/web/components/cfdi/cfdi-invoice.tsx
2026-02-17 02:36:18 +00:00

231 lines
8.2 KiB
TypeScript

'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<string, string> = {
ingreso: 'Ingreso',
egreso: 'Egreso',
traslado: 'Traslado',
pago: 'Pago',
nomina: 'Nomina',
};
const formaPagoLabels: Record<string, string> = {
'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<string, string> = {
PUE: 'Pago en una sola exhibición',
PPD: 'Pago en parcialidades o diferido',
};
export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
({ cfdi, conceptos }, ref) => {
return (
<div
ref={ref}
className="bg-white text-black p-8 max-w-[800px] mx-auto text-sm"
style={{ fontFamily: 'Arial, sans-serif' }}
>
{/* Header */}
<div className="flex justify-between items-start border-b-2 border-gray-800 pb-4 mb-4">
<div className="w-32 h-20 bg-gray-200 flex items-center justify-center text-gray-500 text-xs">
[LOGO]
</div>
<div className="text-right">
<h1 className="text-2xl font-bold text-gray-800">FACTURA</h1>
<p className="text-gray-600">
{cfdi.serie && `Serie: ${cfdi.serie} `}
{cfdi.folio && `Folio: ${cfdi.folio}`}
</p>
<p className="text-gray-600">Fecha: {formatDate(cfdi.fechaEmision)}</p>
<span
className={`inline-block px-2 py-1 text-xs font-semibold rounded mt-1 ${
cfdi.estado === 'vigente'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{cfdi.estado === 'vigente' ? 'VIGENTE' : 'CANCELADO'}
</span>
</div>
</div>
{/* Emisor / Receptor */}
<div className="grid grid-cols-2 gap-6 mb-6">
<div className="border border-gray-300 p-4 rounded">
<h3 className="font-bold text-gray-700 border-b border-gray-200 pb-1 mb-2">
EMISOR
</h3>
<p className="font-semibold">{cfdi.nombreEmisor}</p>
<p className="text-gray-600">RFC: {cfdi.rfcEmisor}</p>
</div>
<div className="border border-gray-300 p-4 rounded">
<h3 className="font-bold text-gray-700 border-b border-gray-200 pb-1 mb-2">
RECEPTOR
</h3>
<p className="font-semibold">{cfdi.nombreReceptor}</p>
<p className="text-gray-600">RFC: {cfdi.rfcReceptor}</p>
{cfdi.usoCfdi && (
<p className="text-gray-600">Uso CFDI: {cfdi.usoCfdi}</p>
)}
</div>
</div>
{/* Datos del Comprobante */}
<div className="border border-gray-300 p-4 rounded mb-6">
<h3 className="font-bold text-gray-700 border-b border-gray-200 pb-1 mb-2">
DATOS DEL COMPROBANTE
</h3>
<div className="grid grid-cols-4 gap-4 text-sm">
<div>
<span className="text-gray-500">Tipo:</span>
<p className="font-medium">{tipoLabels[cfdi.tipo] || cfdi.tipo}</p>
</div>
<div>
<span className="text-gray-500">Método de Pago:</span>
<p className="font-medium">
{cfdi.metodoPago ? metodoPagoLabels[cfdi.metodoPago] || cfdi.metodoPago : '-'}
</p>
</div>
<div>
<span className="text-gray-500">Forma de Pago:</span>
<p className="font-medium">
{cfdi.formaPago ? formaPagoLabels[cfdi.formaPago] || cfdi.formaPago : '-'}
</p>
</div>
<div>
<span className="text-gray-500">Moneda:</span>
<p className="font-medium">
{cfdi.moneda}
{cfdi.tipoCambio !== 1 && ` (TC: ${cfdi.tipoCambio})`}
</p>
</div>
</div>
</div>
{/* Conceptos */}
{conceptos && conceptos.length > 0 && (
<div className="mb-6">
<h3 className="font-bold text-gray-700 border-b border-gray-200 pb-1 mb-2">
CONCEPTOS
</h3>
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-100">
<th className="text-left p-2 border">Descripción</th>
<th className="text-center p-2 border w-20">Cant.</th>
<th className="text-right p-2 border w-28">P. Unit.</th>
<th className="text-right p-2 border w-28">Importe</th>
</tr>
</thead>
<tbody>
{conceptos.map((concepto, idx) => (
<tr key={idx} className="border-b">
<td className="p-2 border">{concepto.descripcion}</td>
<td className="text-center p-2 border">{concepto.cantidad}</td>
<td className="text-right p-2 border">
{formatCurrency(concepto.valorUnitario)}
</td>
<td className="text-right p-2 border">
{formatCurrency(concepto.importe)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Totales */}
<div className="flex justify-end mb-6">
<div className="w-64">
<div className="flex justify-between py-1 border-b">
<span className="text-gray-600">Subtotal:</span>
<span>{formatCurrency(cfdi.subtotal)}</span>
</div>
{cfdi.descuento > 0 && (
<div className="flex justify-between py-1 border-b">
<span className="text-gray-600">Descuento:</span>
<span className="text-red-600">-{formatCurrency(cfdi.descuento)}</span>
</div>
)}
{cfdi.iva > 0 && (
<div className="flex justify-between py-1 border-b">
<span className="text-gray-600">IVA (16%):</span>
<span>{formatCurrency(cfdi.iva)}</span>
</div>
)}
{cfdi.ivaRetenido > 0 && (
<div className="flex justify-between py-1 border-b">
<span className="text-gray-600">IVA Retenido:</span>
<span className="text-red-600">-{formatCurrency(cfdi.ivaRetenido)}</span>
</div>
)}
{cfdi.isrRetenido > 0 && (
<div className="flex justify-between py-1 border-b">
<span className="text-gray-600">ISR Retenido:</span>
<span className="text-red-600">-{formatCurrency(cfdi.isrRetenido)}</span>
</div>
)}
<div className="flex justify-between py-2 font-bold text-lg border-t-2 border-gray-800 mt-1">
<span>TOTAL:</span>
<span>{formatCurrency(cfdi.total)}</span>
</div>
</div>
</div>
{/* Timbre Fiscal */}
<div className="border-t-2 border-gray-800 pt-4">
<h3 className="font-bold text-gray-700 mb-2">TIMBRE FISCAL DIGITAL</h3>
<div className="grid grid-cols-2 gap-4 text-xs">
<div>
<p className="text-gray-500">UUID:</p>
<p className="font-mono break-all">{cfdi.uuidFiscal}</p>
</div>
<div>
<p className="text-gray-500">Fecha de Timbrado:</p>
<p>{cfdi.fechaTimbrado}</p>
</div>
</div>
</div>
</div>
);
}
);
CfdiInvoice.displayName = 'CfdiInvoice';