'use client'; import { useState, useEffect } from 'react'; import { useCfdisConConciliacion, useConciliar, useDesconciliar } from '@/lib/hooks/use-conciliacion'; import { useBancos } from '@/lib/hooks/use-bancos'; import { useRegimenesDelPeriodo } from '@/lib/hooks/use-dashboard'; import { PeriodSelector, RegimenSelector, useDebounce } from '@horux/shared-ui'; import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal'; import { Header } from '@/components/layouts/header'; import { Card, CardContent, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Input, Label, Popover, PopoverTrigger, PopoverContent } from '@horux/shared-ui'; import { useAuthStore } from '@/stores/auth-store'; import { formatCurrency, toCfdiDate } from '@/lib/utils'; function formatCurrencyConciliacion(value: number): string { return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN', minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(value); } import { exportToExcel } from '@/lib/export-excel'; import { Eye, Download, X, CheckCircle, Search, ArrowUpDown, Filter } from 'lucide-react'; function getMonthRange(year: number, month: number) { const start = `${year}-${String(month).padStart(2, '0')}-01`; const lastDay = new Date(year, month, 0).getDate(); const end = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; return { start, end }; } function FilterHeader({ label, filterKey, filters, setFilters, openFilter, setOpenFilter, suggestions, }: { label: string; filterKey: string; filters: Record; setFilters: any; openFilter: string | null; setOpenFilter: (v: string | null) => void; suggestions: string[]; }) { const rawValue = (filters as any)[filterKey] || ''; const [localValue, setLocalValue] = useState(rawValue); const debouncedValue = useDebounce(localValue, 300); // Sync local state when popover opens or external filter changes useEffect(() => { setLocalValue(rawValue); }, [rawValue, openFilter === filterKey]); // Update parent filter only when debounced value changes useEffect(() => { if (debouncedValue !== rawValue) { setFilters((prev: any) => ({ ...prev, [filterKey]: debouncedValue })); } }, [debouncedValue]); const hasFilter = !!rawValue; const filteredSuggestions = localValue.length >= 1 ? suggestions.filter((s) => s.toLowerCase().includes(localValue.toLowerCase())).slice(0, 8) : suggestions.slice(0, 8); return (
{label} setOpenFilter(open ? filterKey : null)}>

Filtrar por {label}

setLocalValue(e.target.value)} autoFocus /> {filteredSuggestions.length > 0 && (
{filteredSuggestions.map((s, idx) => ( ))}
)}
{hasFilter && ( )}
); } export default function ConciliacionPage() { const now = new Date(); const defaultRange = getMonthRange(now.getFullYear(), now.getMonth() + 1); const [fechaInicio, setFechaInicio] = useState(defaultRange.start); const [fechaFin, setFechaFin] = useState(defaultRange.end); const [regimenSeleccionado, setRegimenSeleccionado] = useState(null); const [activeTab, setActiveTab] = useState<'EMITIDO' | 'RECIBIDO'>('EMITIDO'); const [selected, setSelected] = useState>(new Set()); const [fechaPago, setFechaPago] = useState(''); const [bancoId, setBancoId] = useState(''); const [selectedCfdi, setSelectedCfdi] = useState(null); // Ordenación — Por conciliar const [sortPendientes, setSortPendientes] = useState<{ field: 'fecha' | 'total'; dir: 'asc' | 'desc' } | null>(null); // Ordenación — Conciliadas const [sortConciliadas, setSortConciliadas] = useState<{ field: 'fecha' | 'total'; dir: 'asc' | 'desc' } | null>(null); // Filtros por columna — Por conciliar const [filtersPendientes, setFiltersPendientes] = useState({ rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '' }); const [openFilterPendientes, setOpenFilterPendientes] = useState(null); // Filtros por columna — Conciliadas const [filtersConciliadas, setFiltersConciliadas] = useState({ rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '', banco: '' }); const [openFilterConciliadas, setOpenFilterConciliadas] = useState(null); const { user } = useAuthStore(); const isVisor = user?.role === 'visor'; // Data const { data: regimenes, isLoading: regimenesLoading } = useRegimenesDelPeriodo(fechaInicio, fechaFin); const { data: cfdis, isLoading } = useCfdisConConciliacion({ tipo: activeTab, fechaInicio, fechaFin, ...(regimenSeleccionado && { regimen: regimenSeleccionado }), }); const { data: bancos } = useBancos(); const conciliarMut = useConciliar(); const desconciliarMut = useDesconciliar(); // Split data const pendientes = cfdis?.filter((c) => c.conciliado !== 'true') || []; const conciliadas = cfdis?.filter((c) => c.conciliado === 'true') || []; // Sugerencias únicas para filtros de columna (de todos los CFDIs cargados) const allCfdis = cfdis || []; const uniqueSuggestions = { rfcEmisor: [...new Set(allCfdis.map((c: any) => c.rfcEmisor).filter(Boolean))].sort(), nombreEmisor: [...new Set(allCfdis.map((c: any) => c.nombreEmisor).filter(Boolean))].sort(), rfcReceptor: [...new Set(allCfdis.map((c: any) => c.rfcReceptor).filter(Boolean))].sort(), nombreReceptor: [...new Set(allCfdis.map((c: any) => c.nombreReceptor).filter(Boolean))].sort(), banco: [...new Set(conciliadas.map((c: any) => c.conciliacion?.banco).filter(Boolean))].sort(), }; // Score cards — tipo P usa monto_pago_mxn, otros usan total_mxn // I+P-E: Ingresos y Pagos suman, Egresos restan const getMonto = (c: any) => { const monto = Number(c.montoMxn || c.totalMxn || 0); return c.tipoComprobante === 'E' ? -monto : monto; }; const montoConciliado = conciliadas.reduce((s, c) => s + getMonto(c), 0); const montoPendiente = pendientes.reduce((s, c) => s + getMonto(c), 0); // Reset selection + ordenación + filtros on tab/filter change useEffect(() => { setSelected(new Set()); setSortPendientes(null); setSortConciliadas(null); setFiltersPendientes({ rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '' }); setOpenFilterPendientes(null); setFiltersConciliadas({ rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '', banco: '' }); setOpenFilterConciliadas(null); }, [activeTab, fechaInicio, fechaFin, regimenSeleccionado]); // Handlers const toggleSelect = (id: number) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; const toggleSelectAll = () => { if (selected.size === pendientesOrdenados.length && pendientesOrdenados.length > 0) { setSelected(new Set()); } else { setSelected(new Set(pendientesOrdenados.map((c) => c.id))); } }; const handleConciliar = async () => { if (selected.size === 0 || !fechaPago || !bancoId) return; try { await conciliarMut.mutateAsync({ cfdiIds: Array.from(selected), fechaDePago: fechaPago, idBanco: parseInt(bancoId), }); setSelected(new Set()); setFechaPago(''); setBancoId(''); } catch (err: any) { alert(err.response?.data?.message || 'Error al conciliar'); } }; const handleDesconciliar = async (conciliacionId: number) => { if (!confirm('¿Desconciliar este CFDI?')) return; try { await desconciliarMut.mutateAsync(conciliacionId); } catch (err: any) { alert(err.response?.data?.message || 'Error al desconciliar'); } }; function matchesColumnFilters(c: any, filters: Record) { const rfcEmisorMatch = !filters.rfcEmisor || (c.rfcEmisor || '').toLowerCase().includes(filters.rfcEmisor.toLowerCase()); const nombreEmisorMatch = !filters.nombreEmisor || (c.nombreEmisor || '').toLowerCase().includes(filters.nombreEmisor.toLowerCase()); const rfcReceptorMatch = !filters.rfcReceptor || (c.rfcReceptor || '').toLowerCase().includes(filters.rfcReceptor.toLowerCase()); const nombreReceptorMatch = !filters.nombreReceptor || (c.nombreReceptor || '').toLowerCase().includes(filters.nombreReceptor.toLowerCase()); const bancoMatch = !filters.banco || ((c.conciliacion?.banco || '') + ' ****' + (c.conciliacion?.terminacionCuenta || '')).toLowerCase().includes(filters.banco.toLowerCase()); return rfcEmisorMatch && nombreEmisorMatch && rfcReceptorMatch && nombreReceptorMatch && bancoMatch; } function sortCfdis(list: any[], sort: { field: 'fecha' | 'total'; dir: 'asc' | 'desc' } | null) { if (!sort) return list; const sorted = [...list].sort((a, b) => { if (sort.field === 'fecha') { const da = toCfdiDate(a.fechaPagoP || a.fechaEmision).getTime(); const db = toCfdiDate(b.fechaPagoP || b.fechaEmision).getTime(); return sort.dir === 'asc' ? da - db : db - da; } if (sort.field === 'total') { const ta = getMonto(a); const tb = getMonto(b); return sort.dir === 'asc' ? ta - tb : tb - ta; } return 0; }); return sorted; } const pendientesOrdenados = sortCfdis( pendientes.filter((c) => matchesColumnFilters(c, filtersPendientes)), sortPendientes ); const conciliadasOrdenadas = sortCfdis( conciliadas.filter((c) => matchesColumnFilters(c, filtersConciliadas)), sortConciliadas ); const handleExport = () => { const allVisible = [...pendientesOrdenados, ...conciliadasOrdenadas]; if (!allVisible.length) return; exportToExcel( allVisible.map((c) => ({ ...c, _fecha: toCfdiDate(c.fechaPagoP || c.fechaEmision).toLocaleDateString('es-MX'), _totalMxn: getMonto(c), _estado: c.conciliado === 'true' ? 'Conciliado' : 'Pendiente', _fechaPago: c.conciliacion?.fechaDePago || '', _banco: c.conciliacion ? `${c.conciliacion.banco} ****${c.conciliacion.terminacionCuenta}` : '', })), [ { header: 'UUID', key: 'uuid', width: 40 }, { header: 'Fecha Emisión', key: '_fecha', width: 15 }, { header: 'RFC Emisor', key: 'rfcEmisor', width: 15 }, { header: 'Nombre Emisor', key: 'nombreEmisor', width: 30 }, { header: 'RFC Receptor', key: 'rfcReceptor', width: 15 }, { header: 'Total MXN', key: '_totalMxn', width: 15 }, { header: 'Estado', key: '_estado', width: 12 }, { header: 'Fecha Pago', key: '_fechaPago', width: 15 }, { header: 'Banco', key: '_banco', width: 20 }, ], `conciliacion-${activeTab.toLowerCase()}`, ); }; return ( <>
{ setFechaInicio(i); setFechaFin(f); }} />
{/* Regimen selector + Export button */}
{/* Score cards */}

Monto Conciliado

{formatCurrencyConciliacion(montoConciliado)}

{conciliadas.length} CFDIs

Monto Pendiente de Conciliar

{formatCurrencyConciliacion(montoPendiente)}

{pendientes.length} CFDIs

{/* Tabs */}
{(['EMITIDO', 'RECIBIDO'] as const).map((tab) => ( ))}
{isLoading ? (
Cargando...
) : ( <> {/* Por conciliar */}

Por conciliar ({pendientesOrdenados.length})

{!isVisor && ( )} {pendientesOrdenados.length === 0 ? ( ) : ( pendientesOrdenados.map((cfdi) => ( {!isVisor && ( )} )) )}
0 } onChange={toggleSelectAll} /> UUID setSortPendientes(prev => prev?.field === 'fecha' ? { field: 'fecha', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'fecha', dir: 'asc' })}> Fecha setSortPendientes(prev => prev?.field === 'total' ? { field: 'total', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'total', dir: 'asc' })}> Total MXN M. Pago
No hay CFDIs pendientes de conciliar
toggleSelect(cfdi.id)} /> {cfdi.uuid?.substring(0, 8)} {toCfdiDate(cfdi.fechaPagoP || cfdi.fechaEmision).toLocaleDateString('es-MX')} {cfdi.rfcEmisor} {cfdi.nombreEmisor} {cfdi.rfcReceptor} {cfdi.nombreReceptor} {formatCurrencyConciliacion(getMonto(cfdi))} {cfdi.metodoPago || '-'}
{/* Action bar - only when items selected */} {!isVisor && selected.size > 0 && (
{selected.size} seleccionados {(() => { const selectedBank = bancos?.find((x) => String(x.id) === bancoId); return ( ); })()} setFechaPago(e.target.value)} className="w-44" />
)} {/* Conciliadas */}

Conciliadas ({conciliadasOrdenadas.length})

{conciliadasOrdenadas.length === 0 ? ( ) : ( conciliadasOrdenadas.map((cfdi) => ( )) )}
UUID setSortConciliadas(prev => prev?.field === 'fecha' ? { field: 'fecha', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'fecha', dir: 'asc' })}> Fecha Emisión setSortConciliadas(prev => prev?.field === 'total' ? { field: 'total', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'total', dir: 'asc' })}> Total MXN Fecha Pago
No hay CFDIs conciliados
{cfdi.uuid?.substring(0, 8)} {toCfdiDate(cfdi.fechaPagoP || cfdi.fechaEmision).toLocaleDateString('es-MX')} {activeTab === 'EMITIDO' ? cfdi.rfcReceptor : cfdi.rfcEmisor} {activeTab === 'EMITIDO' ? cfdi.nombreReceptor : cfdi.nombreEmisor} {formatCurrencyConciliacion(getMonto(cfdi))} {cfdi.conciliacion?.fechaDePago ? new Date( (cfdi.conciliacion.fechaDePago.split('T')[0]) + 'T12:00:00', ).toLocaleDateString('es-MX') : '-'} {cfdi.conciliacion ? `${cfdi.conciliacion.banco} ****${cfdi.conciliacion.terminacionCuenta}` : '-'} {!isVisor && cfdi.conciliacion && ( )}
)}
setSelectedCfdi(null)} /> ); }