Initial commit: Horux Despachos project
This commit is contained in:
324
apps/web/components/cfdi/cfdi-invoice.tsx
Normal file
324
apps/web/components/cfdi/cfdi-invoice.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
'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 formatDateTime = (dateString: string) =>
|
||||
new Date(dateString).toLocaleString('es-MX', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
EMITIDO: 'Emitido',
|
||||
RECIBIDO: 'Recibido',
|
||||
};
|
||||
|
||||
const tipoCompLabels: Record<string, string> = {
|
||||
I: 'Ingreso',
|
||||
E: 'Egreso',
|
||||
T: 'Traslado',
|
||||
P: 'Pago',
|
||||
N: 'Nómina',
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
const usoCfdiLabels: Record<string, string> = {
|
||||
G01: 'Adquisición de mercancías',
|
||||
G02: 'Devoluciones, descuentos o bonificaciones',
|
||||
G03: 'Gastos en general',
|
||||
I01: 'Construcciones',
|
||||
I02: 'Mobilario y equipo de oficina',
|
||||
I03: 'Equipo de transporte',
|
||||
I04: 'Equipo de cómputo',
|
||||
I05: 'Dados, troqueles, moldes',
|
||||
I06: 'Comunicaciones telefónicas',
|
||||
I07: 'Comunicaciones satelitales',
|
||||
I08: 'Otra maquinaria y equipo',
|
||||
D01: 'Honorarios médicos',
|
||||
D02: 'Gastos médicos por incapacidad',
|
||||
D03: 'Gastos funerales',
|
||||
D04: 'Donativos',
|
||||
D05: 'Intereses por créditos hipotecarios',
|
||||
D06: 'Aportaciones voluntarias SAR',
|
||||
D07: 'Primas por seguros de gastos médicos',
|
||||
D08: 'Gastos de transportación escolar',
|
||||
D09: 'Depósitos en cuentas para el ahorro',
|
||||
D10: 'Pagos por servicios educativos',
|
||||
P01: 'Por definir',
|
||||
S01: 'Sin efectos fiscales',
|
||||
CP01: 'Pagos',
|
||||
CN01: 'Nómina',
|
||||
};
|
||||
|
||||
export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
|
||||
({ cfdi, conceptos }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="bg-white text-gray-800 max-w-[850px] mx-auto text-sm shadow-lg"
|
||||
style={{ fontFamily: 'Segoe UI, Roboto, Arial, sans-serif' }}
|
||||
>
|
||||
{/* Header con gradiente */}
|
||||
<div className="bg-gradient-to-r from-blue-700 to-blue-900 text-white p-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold opacity-90">Emisor</h2>
|
||||
<p className="text-xl font-bold mt-1">{cfdi.nombreEmisor}</p>
|
||||
<p className="text-blue-200 text-sm mt-1">RFC: {cfdi.rfcEmisor}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="flex items-center justify-end gap-3 mb-2">
|
||||
<span
|
||||
className={`px-3 py-1 text-xs font-bold rounded-full ${
|
||||
cfdi.status === 'Vigente' || cfdi.status === '1'
|
||||
? 'bg-green-400 text-green-900'
|
||||
: 'bg-red-400 text-red-900'
|
||||
}`}
|
||||
>
|
||||
{cfdi.status === 'Vigente' || cfdi.status === '1' ? 'VIGENTE' : 'CANCELADO'}
|
||||
</span>
|
||||
<span className="px-3 py-1 text-xs font-bold rounded-full bg-white/20">
|
||||
{typeLabels[cfdi.type] || cfdi.type} {cfdi.tipoComprobante ? `(${tipoCompLabels[cfdi.tipoComprobante] || cfdi.tipoComprobante})` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-3xl font-bold tracking-tight">
|
||||
{cfdi.serie && <span className="text-blue-300">{cfdi.serie}-</span>}
|
||||
{cfdi.folio || 'S/N'}
|
||||
</div>
|
||||
<p className="text-blue-200 text-sm mt-1">{formatDate(cfdi.fechaEmision)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{/* Receptor */}
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-5 border-l-4 border-blue-600">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Receptor</p>
|
||||
<p className="text-lg font-semibold text-gray-800 mt-1">{cfdi.nombreReceptor}</p>
|
||||
<p className="text-gray-600 text-sm">RFC: {cfdi.rfcReceptor}</p>
|
||||
</div>
|
||||
{cfdi.usoCfdi && (
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Uso CFDI</p>
|
||||
<p className="text-sm font-medium text-gray-700 mt-1">
|
||||
{cfdi.usoCfdi} - {usoCfdiLabels[cfdi.usoCfdi] || ''}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Datos del Comprobante */}
|
||||
<div className="grid grid-cols-4 gap-3 mb-5">
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide">Método Pago</p>
|
||||
<p className="text-sm font-semibold text-gray-800 mt-1">
|
||||
{cfdi.metodoPago || '-'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{cfdi.metodoPago ? metodoPagoLabels[cfdi.metodoPago] || '' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide">Forma Pago</p>
|
||||
<p className="text-sm font-semibold text-gray-800 mt-1">
|
||||
{cfdi.formaPago || '-'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{cfdi.formaPago ? formaPagoLabels[cfdi.formaPago] || '' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide">Moneda</p>
|
||||
<p className="text-sm font-semibold text-gray-800 mt-1">{cfdi.moneda || 'MXN'}</p>
|
||||
{cfdi.typeCambio && cfdi.typeCambio !== 1 && (
|
||||
<p className="text-xs text-gray-500">TC: {cfdi.typeCambio}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide">Versión</p>
|
||||
<p className="text-sm font-semibold text-gray-800 mt-1">CFDI 4.0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conceptos */}
|
||||
{conceptos && conceptos.length > 0 && (
|
||||
<div className="mb-5">
|
||||
<h3 className="text-xs text-gray-500 uppercase tracking-wide font-medium mb-2 flex items-center gap-2">
|
||||
<span className="w-1 h-4 bg-blue-600 rounded-full"></span>
|
||||
Conceptos
|
||||
</h3>
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-700">Descripción</th>
|
||||
<th className="text-center py-3 px-3 font-semibold text-gray-700 w-20">Cant.</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-700 w-32">P. Unitario</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-700 w-32">Importe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{conceptos.map((concepto, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
className={`border-t border-gray-100 ${idx % 2 === 1 ? 'bg-gray-50/50' : ''}`}
|
||||
>
|
||||
<td className="py-3 px-4">
|
||||
<p className="text-gray-800">{concepto.descripcion}</p>
|
||||
{concepto.claveProdServ && (
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
Clave: {concepto.claveProdServ}
|
||||
{concepto.claveUnidad && ` | Unidad: ${concepto.claveUnidad}`}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center py-3 px-3 text-gray-700">{concepto.cantidad}</td>
|
||||
<td className="text-right py-3 px-4 text-gray-700">
|
||||
{formatCurrency(concepto.valorUnitario)}
|
||||
</td>
|
||||
<td className="text-right py-3 px-4 font-medium text-gray-800">
|
||||
{formatCurrency(concepto.importe)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Totales */}
|
||||
<div className="flex justify-end mb-5">
|
||||
<div className="w-80 bg-gray-50 rounded-lg overflow-hidden">
|
||||
<div className="divide-y divide-gray-200">
|
||||
<div className="flex justify-between py-2.5 px-4">
|
||||
<span className="text-gray-600">Subtotal</span>
|
||||
<span className="font-medium">{formatCurrency(cfdi.subtotal)}</span>
|
||||
</div>
|
||||
{cfdi.descuento > 0 && (
|
||||
<div className="flex justify-between py-2.5 px-4">
|
||||
<span className="text-gray-600">Descuento</span>
|
||||
<span className="font-medium text-red-600">-{formatCurrency(cfdi.descuento)}</span>
|
||||
</div>
|
||||
)}
|
||||
{cfdi.ivaTraslado > 0 && (
|
||||
<div className="flex justify-between py-2.5 px-4">
|
||||
<span className="text-gray-600">IVA (16%)</span>
|
||||
<span className="font-medium">{formatCurrency(cfdi.ivaTraslado)}</span>
|
||||
</div>
|
||||
)}
|
||||
{cfdi.ivaRetencion > 0 && (
|
||||
<div className="flex justify-between py-2.5 px-4">
|
||||
<span className="text-gray-600">IVA Retenido</span>
|
||||
<span className="font-medium text-red-600">-{formatCurrency(cfdi.ivaRetencion)}</span>
|
||||
</div>
|
||||
)}
|
||||
{cfdi.isrRetencion > 0 && (
|
||||
<div className="flex justify-between py-2.5 px-4">
|
||||
<span className="text-gray-600">ISR Retenido</span>
|
||||
<span className="font-medium text-red-600">-{formatCurrency(cfdi.isrRetencion)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-blue-700 text-white py-3 px-4 flex justify-between items-center">
|
||||
<span className="font-semibold">TOTAL</span>
|
||||
<span className="text-xl font-bold">{formatCurrency(cfdi.total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timbre Fiscal Digital */}
|
||||
<div className="bg-gradient-to-r from-gray-100 to-gray-50 rounded-lg p-4 border border-gray-200">
|
||||
<div className="flex gap-4">
|
||||
{/* QR Placeholder */}
|
||||
<div className="w-24 h-24 bg-white border-2 border-gray-300 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<div className="text-center">
|
||||
<svg className="w-12 h-12 text-gray-400 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h2M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
|
||||
</svg>
|
||||
<span className="text-[10px] text-gray-400 mt-1 block">QR</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info del Timbre */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xs text-gray-500 uppercase tracking-wide font-semibold mb-2 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
Timbre Fiscal Digital
|
||||
</h3>
|
||||
<div className="space-y-1.5">
|
||||
<div>
|
||||
<span className="text-xs text-gray-500">UUID: </span>
|
||||
<span className="text-xs font-mono text-blue-700 font-medium">{cfdi.uuid}</span>
|
||||
</div>
|
||||
<div>
|
||||
{cfdi.fechaCertSat && (<>
|
||||
<span className="text-xs text-gray-500">Fecha Certificación SAT: </span>
|
||||
<span className="text-xs font-medium text-gray-700">{formatDateTime(cfdi.fechaCertSat)}</span>
|
||||
</>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leyenda */}
|
||||
<p className="text-[10px] text-gray-400 mt-3 text-center border-t border-gray-200 pt-2">
|
||||
Este documento es una representación impresa de un CFDI • Verificable en: https://verificacfdi.facturaelectronica.sat.gob.mx
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CfdiInvoice.displayName = 'CfdiInvoice';
|
||||
244
apps/web/components/cfdi/cfdi-viewer-modal.tsx
Normal file
244
apps/web/components/cfdi/cfdi-viewer-modal.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
'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;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})));
|
||||
} 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(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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user