'use client'; import { useState, useRef, useCallback, useEffect } from 'react'; import { useDebounce } from '@horux/shared-ui'; import { Header } from '@/components/layouts/header'; import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Popover, PopoverTrigger, PopoverContent } from '@horux/shared-ui'; import { useCfdis, useCfdiConceptos, useCreateCfdi, useDeleteCfdi } from '@/lib/hooks/use-cfdi'; import { getCfdis, createManyCfdis, searchEmisores, searchReceptores, getAllCfdiConceptos, type EmisorReceptor } from '@/lib/api/cfdi'; import { cancelarFactura, downloadPdf, downloadXml } from '@/lib/api/facturacion'; import type { CfdiFilters, CfdiConceptoFilters, TipoCfdi, Cfdi } from '@horux/shared'; import type { CreateCfdiData } from '@/lib/api/cfdi'; import { FileText, Search, ChevronLeft, ChevronRight, Plus, Upload, Trash2, X, FileUp, CheckCircle, AlertCircle, Loader2, Eye, Filter, XCircle, Calendar, User, Building2, Download, Printer, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; import * as XLSX from 'xlsx'; import { saveAs } from 'file-saver'; import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal'; import { getCfdiById } from '@/lib/api/cfdi'; import { useAuthStore } from '@/stores/auth-store'; import { useTenantViewStore } from '@/stores/tenant-view-store'; import { useContribuyenteStore } from '@/stores/contribuyente-store'; import { useQueryClient } from '@tanstack/react-query'; // Upload progress state interface UploadProgress { status: 'idle' | 'parsing' | 'uploading' | 'complete' | 'error'; totalFiles: number; parsedFiles: number; validFiles: number; currentBatch: number; totalBatches: number; uploaded: number; duplicates: number; errors: number; errorMessages: string[]; } type CfdiTipo = 'EMITIDO' | 'RECIBIDO'; const initialFormData: CreateCfdiData = { uuid: '', type: 'EMITIDO', serie: '', folio: '', fechaEmision: new Date().toISOString().split('T')[0], rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '', subtotal: 0, descuento: 0, ivaTraslado: 0, isrRetencion: 0, ivaRetencion: 0, total: 0, moneda: 'MXN', metodoPago: 'PUE', formaPago: '03', usoCfdi: 'G03', }; // Helper function to find element regardless of namespace prefix function findElement(doc: Document, localName: string): Element | null { // Try common prefixes first (most reliable for CFDI) const prefixes = ['cfdi', 'tfd', 'pago20', 'pago10', 'nomina12', '']; for (const prefix of prefixes) { const tagName = prefix ? `${prefix}:${localName}` : localName; const el = doc.getElementsByTagName(tagName)[0] as Element; if (el) return el; } // Try with wildcard - search all elements by localName const elements = doc.getElementsByTagName('*'); for (let i = 0; i < elements.length; i++) { if (elements[i].localName === localName) { return elements[i]; } } return null; } // Parse CFDI XML and extract data function parseCfdiXml(xmlString: string, tenantRfc: string): CreateCfdiData | null { try { const parser = new DOMParser(); const doc = parser.parseFromString(xmlString, 'text/xml'); // Check for parse errors const parseError = doc.querySelector('parsererror'); if (parseError) { console.error('XML parse error:', parseError.textContent); return null; } // Get the Comprobante element (root) const comprobante = findElement(doc, 'Comprobante'); if (!comprobante) { console.error('No se encontro elemento Comprobante'); return null; } // Get TimbreFiscalDigital for UUID const timbre = findElement(doc, 'TimbreFiscalDigital'); const uuid = timbre?.getAttribute('UUID') || ''; const fechaTimbradoRaw = timbre?.getAttribute('FechaTimbrado') || ''; // Get Emisor const emisor = findElement(doc, 'Emisor'); const rfcEmisor = emisor?.getAttribute('Rfc') || emisor?.getAttribute('rfc') || ''; const nombreEmisor = emisor?.getAttribute('Nombre') || emisor?.getAttribute('nombre') || ''; // Get Receptor const receptor = findElement(doc, 'Receptor'); const rfcReceptor = receptor?.getAttribute('Rfc') || receptor?.getAttribute('rfc') || ''; const nombreReceptor = receptor?.getAttribute('Nombre') || receptor?.getAttribute('nombre') || ''; const usoCfdi = receptor?.getAttribute('UsoCFDI') || ''; // Determine type based on tenant RFC // If tenant is emisor -> ingreso (we issued the invoice) // If tenant is receptor -> egreso (we received the invoice) const tenantRfcUpper = tenantRfc.toUpperCase(); let tipoFinal: CreateCfdiData['type']; if (rfcEmisor.toUpperCase() === tenantRfcUpper) { tipoFinal = 'EMITIDO'; } else { tipoFinal = 'RECIBIDO'; } // Get impuestos - search for the Impuestos element that is direct child of Comprobante // (not the ones inside Conceptos) let totalImpuestosTrasladados = 0; let totalImpuestosRetenidos = 0; // Try to get TotalImpuestosTrasladados from Comprobante's direct Impuestos child const allImpuestos = doc.getElementsByTagName('*'); for (let i = 0; i < allImpuestos.length; i++) { const el = allImpuestos[i]; if (el.localName === 'Impuestos' && el.parentElement?.localName === 'Comprobante') { totalImpuestosTrasladados = parseFloat(el.getAttribute('TotalImpuestosTrasladados') || '0'); totalImpuestosRetenidos = parseFloat(el.getAttribute('TotalImpuestosRetenidos') || '0'); break; } } // Fallback: calculate IVA from total - subtotal if not found const subtotal = parseFloat(comprobante.getAttribute('SubTotal') || '0'); const descuento = parseFloat(comprobante.getAttribute('Descuento') || '0'); const total = parseFloat(comprobante.getAttribute('Total') || '0'); if (totalImpuestosTrasladados === 0 && total > subtotal) { totalImpuestosTrasladados = Math.max(0, total - subtotal + descuento + totalImpuestosRetenidos); } // Get retenciones breakdown let isrRetenido = 0; let ivaRetenido = 0; const retenciones = doc.querySelectorAll('[localName="Retencion"], Retencion, cfdi\\:Retencion'); retenciones.forEach((ret: Element) => { const impuesto = ret.getAttribute('Impuesto'); const importe = parseFloat(ret.getAttribute('Importe') || '0'); if (impuesto === '001') isrRetenido = importe; // ISR if (impuesto === '002') ivaRetenido = importe; // IVA }); // Parse dates - handle both ISO format and datetime format const fechaEmisionRaw = comprobante.getAttribute('Fecha') || ''; const fechaEmision = fechaEmisionRaw.includes('T') ? fechaEmisionRaw.split('T')[0] : fechaEmisionRaw; const fechaTimbrado = fechaTimbradoRaw.includes('T') ? fechaTimbradoRaw.split('T')[0] : fechaTimbradoRaw; // Validate required fields if (!uuid) { console.error('UUID no encontrado en el XML'); return null; } if (!rfcEmisor || !rfcReceptor) { console.error('RFC emisor o receptor no encontrado'); return null; } if (!fechaEmision) { console.error('Fecha de emision no encontrada'); return null; } return { uuid: uuid.toUpperCase(), type: tipoFinal, serie: comprobante.getAttribute('Serie') || '', folio: comprobante.getAttribute('Folio') || '', fechaEmision, rfcEmisor, nombreEmisor: nombreEmisor || 'Sin nombre', rfcReceptor, nombreReceptor: nombreReceptor || 'Sin nombre', subtotal, descuento, ivaTraslado: totalImpuestosTrasladados, isrRetencion: isrRetenido, ivaRetencion: ivaRetenido, total, moneda: comprobante.getAttribute('Moneda') || 'MXN', tipoCambio: parseFloat(comprobante.getAttribute('TipoCambio') || '1'), tipoComprobante: comprobante.getAttribute('TipoDeComprobante') || '', metodoPago: comprobante.getAttribute('MetodoPago') || '', formaPago: comprobante.getAttribute('FormaPago') || '', usoCfdi, }; } catch (error) { console.error('Error parsing XML:', error); return null; } } const TIPO_COMPROBANTE_LABELS: Record = { I: 'Ingreso', E: 'Egreso', P: 'Pago', T: 'Traslado', N: 'Nómina', }; function formatTipoComprobante(value: string | null | undefined): string { if (!value) return ''; const upper = value.toUpperCase(); return TIPO_COMPROBANTE_LABELS[upper] ? `${upper} - ${TIPO_COMPROBANTE_LABELS[upper]}` : upper; } // Chunk size for batch uploads const PARSE_CHUNK_SIZE = 500; // Parse 500 files at a time const UPLOAD_CHUNK_SIZE = 200; // Upload 200 CFDIs per request export default function CfdiPage() { const { user } = useAuthStore(); const { viewingTenantRfc } = useTenantViewStore(); const { selectedContribuyenteId } = useContribuyenteStore(); const fileInputRef = useRef(null); const queryClient = useQueryClient(); // Get the effective tenant RFC (viewing tenant or user's tenant) const tenantRfc = viewingTenantRfc || user?.tenantRfc || ''; const [filters, setFilters] = useState({ page: 1, limit: 20, }); const [searchTerm, setSearchTerm] = useState(''); const [columnFilters, setColumnFilters] = useState({ fechaInicio: '', fechaFin: '', emisor: '', receptor: '', }); // Reset pagination and filters when contribuyente changes useEffect(() => { setFilters({ page: 1, limit: 20 }); setSearchTerm(''); setColumnFilters({ fechaInicio: '', fechaFin: '', emisor: '', receptor: '' }); }, [selectedContribuyenteId]); const [openFilter, setOpenFilter] = useState<'fecha' | 'emisor' | 'receptor' | null>(null); const [emisorSuggestions, setEmisorSuggestions] = useState([]); const [receptorSuggestions, setReceptorSuggestions] = useState([]); const [loadingEmisor, setLoadingEmisor] = useState(false); const [loadingReceptor, setLoadingReceptor] = useState(false); const [showForm, setShowForm] = useState(false); // Tabs: CFDIs vs Conceptos const [activeTab, setActiveTab] = useState<'cfdis' | 'conceptos'>('cfdis'); // Conceptos filters & state const [conceptoFilters, setConceptoFilters] = useState({ page: 1, limit: 20, }); const [conceptoSearch, setConceptoSearch] = useState(''); const [conceptoColumnFilters, setConceptoColumnFilters] = useState({ fechaInicio: '', fechaFin: '', uuid: '', claveProdServ: '', descripcion: '', }); const [conceptoOrder, setConceptoOrder] = useState<{ by: 'fecha' | 'importe'; dir: 'asc' | 'desc' }>({ by: 'fecha', dir: 'desc' }); const [openConceptoFilter, setOpenConceptoFilter] = useState<'fecha' | 'uuid' | 'clave' | 'descripcion' | null>(null); const hasConceptoDateFilter = !!conceptoColumnFilters.fechaInicio || !!conceptoColumnFilters.fechaFin; const hasConceptoUuidFilter = !!conceptoColumnFilters.uuid; const hasConceptoClaveFilter = !!conceptoColumnFilters.claveProdServ; const hasConceptoDescFilter = !!conceptoColumnFilters.descripcion; const { data: conceptosData, isLoading: conceptosLoading } = useCfdiConceptos(conceptoFilters); // Debounced values for autocomplete const debouncedEmisor = useDebounce(columnFilters.emisor, 300); const debouncedReceptor = useDebounce(columnFilters.receptor, 300); // Fetch emisor suggestions when debounced value changes useEffect(() => { if (debouncedEmisor.length < 2) { setEmisorSuggestions([]); return; } setLoadingEmisor(true); searchEmisores(debouncedEmisor) .then(setEmisorSuggestions) .catch(() => setEmisorSuggestions([])) .finally(() => setLoadingEmisor(false)); }, [debouncedEmisor]); // Fetch receptor suggestions when debounced value changes useEffect(() => { if (debouncedReceptor.length < 2) { setReceptorSuggestions([]); return; } setLoadingReceptor(true); searchReceptores(debouncedReceptor) .then(setReceptorSuggestions) .catch(() => setReceptorSuggestions([])) .finally(() => setLoadingReceptor(false)); }, [debouncedReceptor]); const [showBulkForm, setShowBulkForm] = useState(false); const [formData, setFormData] = useState(initialFormData); const [bulkData, setBulkData] = useState(''); const [uploadMode, setUploadMode] = useState<'xml' | 'json'>('xml'); const [jsonUploading, setJsonUploading] = useState(false); // Optimized upload state const [uploadProgress, setUploadProgress] = useState({ status: 'idle', totalFiles: 0, parsedFiles: 0, validFiles: 0, currentBatch: 0, totalBatches: 0, uploaded: 0, duplicates: 0, errors: 0, errorMessages: [] }); const [parsedCfdis, setParsedCfdis] = useState([]); const uploadAbortRef = useRef(false); const { data, isLoading } = useCfdis(filters); const createCfdi = useCreateCfdi(); const deleteCfdi = useDeleteCfdi(); // CFDI Viewer state const [viewingCfdi, setViewingCfdi] = useState(null); const [loadingCfdi, setLoadingCfdi] = useState(null); // Sync shared filters between CFDIs and Conceptos tabs useEffect(() => { setConceptoFilters(prev => ({ ...prev, fechaInicio: columnFilters.fechaInicio || undefined, fechaFin: columnFilters.fechaFin || undefined, tipo: filters.tipo, tipoComprobante: filters.tipoComprobante, estado: filters.estado, rfc: filters.rfc, search: searchTerm || undefined, page: 1, })); }, [filters.tipo, filters.tipoComprobante, filters.estado, filters.rfc, columnFilters.fechaInicio, columnFilters.fechaFin, searchTerm]); // Cancelación Facturapi state const [cancelTarget, setCancelTarget] = useState(null); const [cancelMotive, setCancelMotive] = useState<'01' | '02' | '03' | '04'>('02'); const [cancelSubstitution, setCancelSubstitution] = useState(''); const [cancelling, setCancelling] = useState(false); const handleViewCfdi = async (id: string | number) => { const idStr = String(id); setLoadingCfdi(idStr); try { const cfdi = await getCfdiById(idStr); setViewingCfdi(cfdi); } catch (error) { console.error('Error loading CFDI:', error); alert('Error al cargar el CFDI'); } finally { setLoadingCfdi(null); } }; const canEdit = user?.role === 'owner' || user?.role === 'cfo' || user?.role === 'contador' || user?.role === 'auxiliar'; const canDelete = user?.role === 'owner' || user?.role === 'contador'; const handleSearch = () => { setFilters({ ...filters, search: searchTerm, page: 1 }); }; // Conceptos column filters & sorting const applyConceptoColumnFilters = () => { setConceptoFilters(prev => ({ ...prev, fechaInicio: conceptoColumnFilters.fechaInicio || undefined, fechaFin: conceptoColumnFilters.fechaFin || undefined, uuid: conceptoColumnFilters.uuid || undefined, claveProdServ: conceptoColumnFilters.claveProdServ || undefined, descripcion: conceptoColumnFilters.descripcion || undefined, orderBy: conceptoOrder.by, orderDir: conceptoOrder.dir, page: 1, })); }; const toggleConceptoOrder = (by: 'fecha' | 'importe') => { setConceptoOrder(prev => { const dir = prev.by === by && prev.dir === 'desc' ? 'asc' : 'desc'; return { by, dir }; }); // Apply after state update using the new values setConceptoFilters(prev => ({ ...prev, orderBy: by, orderDir: by === conceptoOrder.by && conceptoOrder.dir === 'desc' ? 'asc' : 'desc', page: 1, })); }; const clearConceptoDateFilter = () => { setConceptoColumnFilters(prev => ({ ...prev, fechaInicio: '', fechaFin: '' })); setConceptoFilters(prev => ({ ...prev, fechaInicio: undefined, fechaFin: undefined, page: 1 })); }; const clearConceptoUuidFilter = () => { setConceptoColumnFilters(prev => ({ ...prev, uuid: '' })); setConceptoFilters(prev => ({ ...prev, uuid: undefined, page: 1 })); }; const clearConceptoClaveFilter = () => { setConceptoColumnFilters(prev => ({ ...prev, claveProdServ: '' })); setConceptoFilters(prev => ({ ...prev, claveProdServ: undefined, page: 1 })); }; const clearConceptoDescFilter = () => { setConceptoColumnFilters(prev => ({ ...prev, descripcion: '' })); setConceptoFilters(prev => ({ ...prev, descripcion: undefined, page: 1 })); }; // Export to Excel const [exporting, setExporting] = useState(false); const exportToExcel = async () => { if (!data?.data.length) return; setExporting(true); try { // Traer TODOS los CFDIs que coinciden con los filtros (sin paginación) const allFilters: CfdiFilters = { ...filters, page: 1, limit: 10000 }; const allData = await getCfdis(allFilters); const rows = allData.data; if (!rows.length) { alert('No hay datos para exportar'); return; } const exportData = rows.map(cfdi => ({ 'Fecha Emisión': new Date(cfdi.fechaEmision).toLocaleDateString('es-MX'), 'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante), 'Serie': cfdi.serie || '', 'Folio': cfdi.folio || '', 'RFC Emisor': cfdi.rfcEmisor, 'Nombre Emisor': cfdi.nombreEmisor, 'RFC Receptor': cfdi.rfcReceptor, 'Nombre Receptor': cfdi.nombreReceptor, 'Subtotal': cfdi.subtotal, 'Subtotal MXN': cfdi.subtotalMxn, 'IVA': cfdi.ivaTraslado, 'ISR Retención': cfdi.isrRetencion, 'IVA Retención': cfdi.ivaRetencion, 'Descuento': cfdi.descuento, 'Total': cfdi.total, 'Moneda': cfdi.moneda, 'Método de Pago': cfdi.metodoPago || '', 'Forma de Pago': cfdi.formaPago || '', 'Saldo Insoluto': cfdi.saldoInsoluto || '', 'Estatus': cfdi.status === 'Vigente' || cfdi.status === '1' ? 'Vigente' : 'Cancelado', 'Fecha Cancelación': cfdi.fechaCancelacion ? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX') : '', 'UUID': cfdi.uuid, })); const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'CFDIs'); // Auto-size columns const colWidths = Object.keys(exportData[0]).map(key => ({ wch: Math.max(key.length, ...exportData.map(row => String(row[key as keyof typeof row]).length)) })); ws['!cols'] = colWidths; const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }); const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const fileName = `cfdis_${new Date().toISOString().split('T')[0]}.xlsx`; saveAs(blob, fileName); } catch (error) { console.error('Error exporting:', error); alert('Error al exportar'); } finally { setExporting(false); } }; const exportConceptosToExcel = async () => { if (!conceptosData?.data.length) return; setExporting(true); try { const allFilters: CfdiConceptoFilters = { ...conceptoFilters, page: 1, limit: 10000 }; const allData = await getAllCfdiConceptos(allFilters); const rows = allData.data; if (!rows.length) { alert('No hay datos para exportar'); return; } const exportData = rows.map(c => ({ 'Fecha CFDI': c.cfdiFechaEmision ? new Date(c.cfdiFechaEmision).toLocaleDateString('es-MX') : '', 'UUID': c.cfdiUuid || '', 'Tipo Comprobante': formatTipoComprobante(c.cfdiTipoComprobante), 'Estatus CFDI': c.cfdiStatus === 'Vigente' || c.cfdiStatus === '1' ? 'Vigente' : 'Cancelado', 'RFC Emisor': c.cfdiRfcEmisor || '', 'Nombre Emisor': c.cfdiNombreEmisor || '', 'RFC Receptor': c.cfdiRfcReceptor || '', 'Nombre Receptor': c.cfdiNombreReceptor || '', 'Clave ProdServ': c.claveProdServ || '', 'No. Identificación': c.noIdentificacion || '', 'Descripción': c.descripcion, 'Cantidad': c.cantidad, 'Unidad': c.claveUnidad || c.unidad || '', 'Valor Unitario': c.valorUnitario, 'Importe': c.importe, 'Descuento': c.descuento, 'IVA Trasladado': c.ivaTraslado, 'IVA Retención': c.ivaRetencion, 'ISR Retención': c.isrRetencion, })); const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Conceptos'); const colWidths = Object.keys(exportData[0]).map(key => ({ wch: Math.max(key.length, ...exportData.map(row => String(row[key as keyof typeof row]).length)) })); ws['!cols'] = colWidths; const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }); const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const fileName = `conceptos_${new Date().toISOString().split('T')[0]}.xlsx`; saveAs(blob, fileName); } catch (error) { console.error('Error exporting conceptos:', error); alert('Error al exportar conceptos'); } finally { setExporting(false); } }; const handleDownloadPdf = async (facturapiId: string | null) => { if (!facturapiId) return; try { const blob = await downloadPdf(facturapiId); const url = window.URL.createObjectURL(new Blob([blob])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', `factura-${facturapiId}.pdf`); document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); } catch (error: any) { alert('Error al descargar PDF: ' + (error?.message || 'Desconocido')); } }; const handleDownloadXml = async (facturapiId: string | null) => { if (!facturapiId) return; try { const blob = await downloadXml(facturapiId); const url = window.URL.createObjectURL(new Blob([blob])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', `factura-${facturapiId}.xml`); document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); } catch (error: any) { alert('Error al descargar XML: ' + (error?.message || 'Desconocido')); } }; const exportSingleCfdiToExcel = (cfdi: Cfdi) => { const row = { 'Fecha Emisión': new Date(cfdi.fechaEmision).toLocaleDateString('es-MX'), 'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante), 'Serie': cfdi.serie || '', 'Folio': cfdi.folio || '', 'RFC Emisor': cfdi.rfcEmisor, 'Nombre Emisor': cfdi.nombreEmisor, 'RFC Receptor': cfdi.rfcReceptor, 'Nombre Receptor': cfdi.nombreReceptor, 'Subtotal': cfdi.subtotal, 'Subtotal MXN': cfdi.subtotalMxn, 'IVA': cfdi.ivaTraslado, 'ISR Retención': cfdi.isrRetencion, 'IVA Retención': cfdi.ivaRetencion, 'Descuento': cfdi.descuento, 'Total': cfdi.total, 'Moneda': cfdi.moneda, 'Método de Pago': cfdi.metodoPago || '', 'Forma de Pago': cfdi.formaPago || '', 'Saldo Insoluto': cfdi.saldoInsoluto || '', 'Estatus': cfdi.status === 'Vigente' || cfdi.status === '1' ? 'Vigente' : 'Cancelado', 'Fecha Cancelación': cfdi.fechaCancelacion ? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX') : '', 'UUID': cfdi.uuid, }; const ws = XLSX.utils.json_to_sheet([row]); ws['!cols'] = Object.keys(row).map((key) => ({ wch: Math.max(key.length, String(row[key as keyof typeof row] ?? '').length), })); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'CFDI'); const buffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); const idParte = [cfdi.serie, cfdi.folio].filter(Boolean).join('-') || cfdi.uuid.slice(0, 8); saveAs(blob, `cfdi_${idParte}.xlsx`); }; const selectEmisor = (emisor: EmisorReceptor) => { setColumnFilters(prev => ({ ...prev, emisor: emisor.nombre })); setEmisorSuggestions([]); }; const selectReceptor = (receptor: EmisorReceptor) => { setColumnFilters(prev => ({ ...prev, receptor: receptor.nombre })); setReceptorSuggestions([]); }; const applyDateFilter = () => { setFilters({ ...filters, fechaInicio: columnFilters.fechaInicio || undefined, fechaFin: columnFilters.fechaFin || undefined, page: 1, }); setOpenFilter(null); }; const applyEmisorFilter = () => { setFilters({ ...filters, emisor: columnFilters.emisor || undefined, page: 1, }); setOpenFilter(null); }; const applyReceptorFilter = () => { setFilters({ ...filters, receptor: columnFilters.receptor || undefined, page: 1, }); setOpenFilter(null); }; const clearDateFilter = () => { setColumnFilters({ ...columnFilters, fechaInicio: '', fechaFin: '' }); setFilters({ ...filters, fechaInicio: undefined, fechaFin: undefined, page: 1 }); setOpenFilter(null); }; const clearEmisorFilter = () => { setColumnFilters({ ...columnFilters, emisor: '' }); setFilters({ ...filters, emisor: undefined, page: 1 }); setOpenFilter(null); }; const clearReceptorFilter = () => { setColumnFilters({ ...columnFilters, receptor: '' }); setFilters({ ...filters, receptor: undefined, page: 1 }); setOpenFilter(null); }; const hasDateFilter = filters.fechaInicio || filters.fechaFin; const hasEmisorFilter = filters.emisor; const hasReceptorFilter = filters.receptor; const hasActiveColumnFilters = hasDateFilter || hasEmisorFilter || hasReceptorFilter; const handleFilterType = (tipo?: TipoCfdi) => { setFilters({ ...filters, tipo, page: 1 }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { await createCfdi.mutateAsync(formData); setFormData(initialFormData); setShowForm(false); } catch (error: any) { alert(error.response?.data?.message || 'Error al crear CFDI'); } }; const handleBulkSubmit = async (e: React.FormEvent) => { e.preventDefault(); setJsonUploading(true); try { const cfdis = JSON.parse(bulkData); if (!Array.isArray(cfdis)) { throw new Error('El formato debe ser un array de CFDIs'); } const result = await createManyCfdis(cfdis); alert(`Se crearon ${result.inserted} CFDIs exitosamente`); setBulkData(''); setShowBulkForm(false); queryClient.invalidateQueries({ queryKey: ['cfdis'] }); } catch (error: any) { alert(error.message || 'Error al procesar CFDIs'); } finally { setJsonUploading(false); } }; // Optimized: Parse files in chunks to prevent memory issues const handleXmlFilesChange = useCallback(async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length === 0) return; uploadAbortRef.current = false; setUploadProgress({ status: 'parsing', totalFiles: files.length, parsedFiles: 0, validFiles: 0, currentBatch: 0, totalBatches: 0, uploaded: 0, duplicates: 0, errors: 0, errorMessages: [] }); setParsedCfdis([]); const validCfdis: CreateCfdiData[] = []; let parsedCount = 0; let errorCount = 0; // Process in chunks to prevent memory issues for (let i = 0; i < files.length; i += PARSE_CHUNK_SIZE) { if (uploadAbortRef.current) break; const chunk = files.slice(i, i + PARSE_CHUNK_SIZE); // Parse chunk in parallel const results = await Promise.all( chunk.map(async (file) => { try { const text = await file.text(); const data = parseCfdiXml(text, tenantRfc); return data; } catch { return null; } }) ); // Collect valid results results.forEach((data) => { if (data && data.uuid) { validCfdis.push(data); } else { errorCount++; } }); parsedCount += chunk.length; setUploadProgress(prev => ({ ...prev, parsedFiles: parsedCount, validFiles: validCfdis.length, errors: errorCount })); // Small delay to allow UI to update await new Promise(r => setTimeout(r, 10)); } setParsedCfdis(validCfdis); setUploadProgress(prev => ({ ...prev, status: uploadAbortRef.current ? 'idle' : 'idle', totalBatches: Math.ceil(validCfdis.length / UPLOAD_CHUNK_SIZE) })); }, [tenantRfc]); // Optimized: Upload in batches with progress const handleXmlBulkSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (parsedCfdis.length === 0) { alert('No hay CFDIs validos para cargar'); return; } uploadAbortRef.current = false; const totalBatches = Math.ceil(parsedCfdis.length / UPLOAD_CHUNK_SIZE); setUploadProgress(prev => ({ ...prev, status: 'uploading', currentBatch: 0, totalBatches, uploaded: 0, duplicates: 0, errors: 0, errorMessages: [] })); let totalUploaded = 0; let totalDuplicates = 0; let totalErrors = 0; const allErrors: string[] = []; // Upload in batches for (let i = 0; i < parsedCfdis.length; i += UPLOAD_CHUNK_SIZE) { if (uploadAbortRef.current) break; const batchNumber = Math.floor(i / UPLOAD_CHUNK_SIZE) + 1; const chunk = parsedCfdis.slice(i, i + UPLOAD_CHUNK_SIZE); setUploadProgress(prev => ({ ...prev, currentBatch: batchNumber })); try { const result = await createManyCfdis(chunk, batchNumber, totalBatches, parsedCfdis.length); totalUploaded += result.inserted; totalDuplicates += result.duplicates; totalErrors += result.errors; if (result.errorMessages) { allErrors.push(...result.errorMessages); } setUploadProgress(prev => ({ ...prev, uploaded: totalUploaded, duplicates: totalDuplicates, errors: prev.errors + result.errors, errorMessages: allErrors.slice(0, 20) // Limit error messages })); } catch (error: any) { console.error(`Error en lote ${batchNumber}:`, error); totalErrors += chunk.length; allErrors.push(`Lote ${batchNumber}: ${error.message || 'Error desconocido'}`); setUploadProgress(prev => ({ ...prev, errors: prev.errors + chunk.length, errorMessages: allErrors.slice(0, 20) })); } // Small delay between batches await new Promise(r => setTimeout(r, 100)); } setUploadProgress(prev => ({ ...prev, status: 'complete' })); // Invalidate queries to refresh the list queryClient.invalidateQueries({ queryKey: ['cfdis'] }); }; const clearXmlFiles = () => { uploadAbortRef.current = true; setParsedCfdis([]); setUploadProgress({ status: 'idle', totalFiles: 0, parsedFiles: 0, validFiles: 0, currentBatch: 0, totalBatches: 0, uploaded: 0, duplicates: 0, errors: 0, errorMessages: [] }); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; // Keyboard shortcuts - Esc to close popovers and forms useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { // Close open filter popovers if (openFilter !== null) { setOpenFilter(null); return; } // Close forms if (showForm) { setShowForm(false); return; } if (showBulkForm) { setShowBulkForm(false); clearXmlFiles(); return; } } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [openFilter, showForm, showBulkForm]); const cancelUpload = () => { uploadAbortRef.current = true; setUploadProgress(prev => ({ ...prev, status: 'idle' })); }; const handleDelete = async (id: string | number) => { const idStr = String(id); console.log('[DeleteCFDI] Intentando eliminar ID:', idStr, 'tipo:', typeof id); if (!id || idStr === 'undefined' || idStr === 'null' || idStr === '0') { alert('ID de CFDI inválido: ' + idStr); return; } if (confirm('¿Eliminar este CFDI?')) { try { await deleteCfdi.mutateAsync(idStr); alert('CFDI eliminado correctamente'); } catch (error: any) { console.error('Error deleting CFDI:', error); const msg = error?.response?.data?.message || error?.message || 'Error al eliminar el CFDI'; alert('Error: ' + msg); } } }; const openCancelDialog = (cfdi: any) => { setCancelTarget(cfdi); setCancelMotive('02'); setCancelSubstitution(''); }; const handleCancelFactura = async () => { if (!cancelTarget) return; if (cancelMotive === '01' && cancelSubstitution.trim().length !== 36) { alert('El motivo 01 requiere el UUID completo (36 caracteres) de la factura que sustituye a esta.'); return; } setCancelling(true); try { await cancelarFactura(cancelTarget.uuid, cancelMotive, cancelMotive === '01' ? cancelSubstitution.trim() : undefined); await queryClient.invalidateQueries({ queryKey: ['cfdis'] }); setCancelTarget(null); alert('Factura cancelada. El estatus final depende del SAT (puede quedar en "pendiente" si requiere aceptación del receptor).'); } catch (err: any) { alert(err?.response?.data?.message || err?.message || 'Error al cancelar la factura'); } finally { setCancelling(false); } }; const calculateTotal = () => { const subtotal = formData.subtotal || 0; const descuento = formData.descuento || 0; const iva = formData.ivaTraslado || 0; const isrRetencion = formData.isrRetencion || 0; const ivaRetencion = formData.ivaRetencion || 0; return subtotal - descuento + iva - isrRetencion - ivaRetencion; }; 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: 'short', year: 'numeric', }); const generateUUID = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }).toUpperCase(); }; return ( <>
{/* Tabs */}
{/* Filters */}
setSearchTerm(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} />
{((activeTab === 'cfdis' && data && data.data.length > 0) || (activeTab === 'conceptos' && conceptosData && conceptosData.data.length > 0)) && ( )} {canEdit && ( <> )}
{activeTab === 'cfdis' && ( <> {/* Add CFDI Form */} {showForm && canEdit && (
Agregar CFDI Ingresa los datos del comprobante fiscal
setFormData({ ...formData, uuid: e.target.value.toUpperCase() })} placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" required />
setFormData({ ...formData, serie: e.target.value })} placeholder="A" />
setFormData({ ...formData, folio: e.target.value })} placeholder="001" />
setFormData({ ...formData, fechaEmision: e.target.value })} required />
setFormData({ ...formData, moneda: e.target.value.toUpperCase() })} placeholder="MXN" />

Emisor

setFormData({ ...formData, rfcEmisor: e.target.value.toUpperCase() })} placeholder="XAXX010101000" maxLength={13} required />
setFormData({ ...formData, nombreEmisor: e.target.value })} placeholder="Empresa Emisora SA de CV" required />

Receptor

setFormData({ ...formData, rfcReceptor: e.target.value.toUpperCase() })} placeholder="XAXX010101000" maxLength={13} required />
setFormData({ ...formData, nombreReceptor: e.target.value })} placeholder="Empresa Receptora SA de CV" required />
setFormData({ ...formData, subtotal: parseFloat(e.target.value) || 0 })} required />
setFormData({ ...formData, descuento: parseFloat(e.target.value) || 0 })} />
setFormData({ ...formData, ivaTraslado: parseFloat(e.target.value) || 0 })} />
setFormData({ ...formData, isrRetencion: parseFloat(e.target.value) || 0 })} />
setFormData({ ...formData, ivaRetencion: parseFloat(e.target.value) || 0 })} />
setFormData({ ...formData, total: parseFloat(e.target.value) || 0 })} required />
)} {/* Bulk Upload Form */} {showBulkForm && canEdit && (
Carga Masiva de CFDIs Sube archivos XML o pega datos en formato JSON
{/* Mode selector */}
{uploadMode === 'xml' ? (
{/* File input - only show when idle */} {uploadProgress.status === 'idle' && (
)} {/* Parsing progress */} {uploadProgress.status === 'parsing' && (

Analizando archivos XML...

{uploadProgress.parsedFiles.toLocaleString()} de {uploadProgress.totalFiles.toLocaleString()} archivos

{uploadProgress.validFiles.toLocaleString()} validos {Math.round((uploadProgress.parsedFiles / uploadProgress.totalFiles) * 100)}%
)} {/* Upload progress */} {uploadProgress.status === 'uploading' && (

Subiendo CFDIs al servidor...

Lote {uploadProgress.currentBatch} de {uploadProgress.totalBatches}

{uploadProgress.uploaded.toLocaleString()}

Cargados

{uploadProgress.duplicates.toLocaleString()}

Duplicados

{uploadProgress.errors.toLocaleString()}

Errores

)} {/* Upload complete */} {uploadProgress.status === 'complete' && (

Carga completada

Se procesaron {uploadProgress.validFiles.toLocaleString()} archivos

{uploadProgress.uploaded.toLocaleString()}

Cargados

{uploadProgress.duplicates.toLocaleString()}

Duplicados

{uploadProgress.errors.toLocaleString()}

Errores

{uploadProgress.errorMessages.length > 0 && (

Errores:

    {uploadProgress.errorMessages.map((err, i) => (
  • {err}
  • ))}
)}
)} {/* Ready to upload - show summary and upload button */} {uploadProgress.status === 'idle' && parsedCfdis.length > 0 && (

{parsedCfdis.length.toLocaleString()} CFDIs listos para cargar

Se enviaran en {Math.ceil(parsedCfdis.length / UPLOAD_CHUNK_SIZE)} lotes de {UPLOAD_CHUNK_SIZE} registros

)} {/* Initial state - no files */} {uploadProgress.status === 'idle' && parsedCfdis.length === 0 && uploadProgress.totalFiles === 0 && (
)} ) : (