Files
HoruxDespachosNuevo/apps/web/app/(dashboard)/conciliacion/page.tsx

607 lines
29 KiB
TypeScript

'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<string, string>;
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 (
<div className="flex items-center justify-center gap-1">
{label}
<Popover open={openFilter === filterKey} onOpenChange={(open) => setOpenFilter(open ? filterKey : null)}>
<PopoverTrigger asChild>
<button className={`p-1 rounded hover:bg-muted ${hasFilter ? 'text-primary' : ''}`}>
<Filter className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-72" align="start">
<div className="space-y-3">
<h4 className="font-medium text-sm">Filtrar por {label}</h4>
<div className="relative">
<Label className="text-xs">Contiene</Label>
<Input
placeholder={`Buscar ${label.toLowerCase()}...`}
className="h-8 text-sm"
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
autoFocus
/>
{filteredSuggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-background border rounded-md shadow-lg max-h-40 overflow-y-auto z-50">
{filteredSuggestions.map((s, idx) => (
<button
key={idx}
className="w-full text-left px-3 py-1.5 text-sm hover:bg-muted truncate"
onClick={() => {
setLocalValue(s);
setFilters((prev: any) => ({ ...prev, [filterKey]: s }));
setOpenFilter(null);
}}
>
{s}
</button>
))}
</div>
)}
</div>
<div className="flex gap-2">
<Button size="sm" onClick={() => setOpenFilter(null)}>Aplicar</Button>
{hasFilter && (
<Button size="sm" variant="outline" onClick={() => { setFilters((prev: any) => ({ ...prev, [filterKey]: '' })); setOpenFilter(null); }}>
Limpiar
</Button>
)}
</div>
</div>
</PopoverContent>
</Popover>
</div>
);
}
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<string | null>(null);
const [activeTab, setActiveTab] = useState<'EMITIDO' | 'RECIBIDO'>('EMITIDO');
const [selected, setSelected] = useState<Set<number>>(new Set());
const [fechaPago, setFechaPago] = useState('');
const [bancoId, setBancoId] = useState<string>('');
const [selectedCfdi, setSelectedCfdi] = useState<any>(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<string | null>(null);
// Filtros por columna — Conciliadas
const [filtersConciliadas, setFiltersConciliadas] = useState({ rfcEmisor: '', nombreEmisor: '', rfcReceptor: '', nombreReceptor: '', banco: '' });
const [openFilterConciliadas, setOpenFilterConciliadas] = useState<string | null>(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<string, string>) {
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 (
<>
<Header title="Conciliación">
<PeriodSelector
fechaInicio={fechaInicio}
fechaFin={fechaFin}
onChange={(i, f) => {
setFechaInicio(i);
setFechaFin(f);
}}
/>
</Header>
<main className="p-6 space-y-6">
{/* Regimen selector + Export button */}
<div className="flex items-center justify-between">
<RegimenSelector
regimenes={regimenes || []}
selected={regimenSeleccionado}
onChange={setRegimenSeleccionado}
isLoading={regimenesLoading}
/>
<Button variant="outline" size="sm" onClick={handleExport} disabled={!cfdis?.length}>
<Download className="h-4 w-4 mr-1" /> Excel
</Button>
</div>
{/* Score cards */}
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardContent className="p-4">
<p className="text-sm text-muted-foreground">Monto Conciliado</p>
<p className="text-2xl font-bold text-success">{formatCurrencyConciliacion(montoConciliado)}</p>
<p className="text-xs text-muted-foreground mt-1">{conciliadas.length} CFDIs</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<p className="text-sm text-muted-foreground">Monto Pendiente de Conciliar</p>
<p className="text-2xl font-bold text-destructive">{formatCurrencyConciliacion(montoPendiente)}</p>
<p className="text-xs text-muted-foreground mt-1">{pendientes.length} CFDIs</p>
</CardContent>
</Card>
</div>
{/* Tabs */}
<div className="flex gap-2">
{(['EMITIDO', 'RECIBIDO'] as const).map((tab) => (
<Button
key={tab}
variant={activeTab === tab ? 'default' : 'outline'}
onClick={() => {
setActiveTab(tab);
setSelected(new Set());
}}
>
{tab === 'EMITIDO' ? 'Emitidas' : 'Recibidas'}
</Button>
))}
</div>
{isLoading ? (
<div className="text-sm text-muted-foreground">Cargando...</div>
) : (
<>
{/* Por conciliar */}
<Card>
<CardContent className="pt-6">
<h3 className="font-medium mb-4">Por conciliar ({pendientesOrdenados.length})</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-center text-muted-foreground">
{!isVisor && (
<th className="pb-3 w-8">
<input
type="checkbox"
checked={
selected.size === pendientesOrdenados.length && pendientesOrdenados.length > 0
}
onChange={toggleSelectAll}
/>
</th>
)}
<th className="pb-3 font-medium">UUID</th>
<th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortPendientes(prev => prev?.field === 'fecha' ? { field: 'fecha', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'fecha', dir: 'asc' })}>
<span className="flex items-center justify-center gap-1">Fecha <ArrowUpDown className="h-3 w-3" /></span>
</th>
<th className="pb-3 font-medium"><FilterHeader label="RFC Emisor" filterKey="rfcEmisor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} suggestions={uniqueSuggestions.rfcEmisor} /></th>
<th className="pb-3 font-medium"><FilterHeader label="Nombre Emisor" filterKey="nombreEmisor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} suggestions={uniqueSuggestions.nombreEmisor} /></th>
<th className="pb-3 font-medium"><FilterHeader label="RFC Receptor" filterKey="rfcReceptor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} suggestions={uniqueSuggestions.rfcReceptor} /></th>
<th className="pb-3 font-medium"><FilterHeader label="Nombre Receptor" filterKey="nombreReceptor" filters={filtersPendientes} setFilters={setFiltersPendientes} openFilter={openFilterPendientes} setOpenFilter={setOpenFilterPendientes} suggestions={uniqueSuggestions.nombreReceptor} /></th>
<th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortPendientes(prev => prev?.field === 'total' ? { field: 'total', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'total', dir: 'asc' })}>
<span className="flex items-center justify-center gap-1">Total MXN <ArrowUpDown className="h-3 w-3" /></span>
</th>
<th className="pb-3 font-medium">M. Pago</th>
<th className="pb-3"></th>
</tr>
</thead>
<tbody>
{pendientesOrdenados.length === 0 ? (
<tr>
<td colSpan={isVisor ? 9 : 10} className="py-4 text-sm text-muted-foreground text-center">
No hay CFDIs pendientes de conciliar
</td>
</tr>
) : (
pendientesOrdenados.map((cfdi) => (
<tr key={cfdi.id} className="border-b hover:bg-muted/50">
{!isVisor && (
<td className="py-2">
<input
type="checkbox"
checked={selected.has(cfdi.id)}
onChange={() => toggleSelect(cfdi.id)}
/>
</td>
)}
<td className="py-2 font-mono text-xs text-center" title={cfdi.uuid}>
{cfdi.uuid?.substring(0, 8)}
</td>
<td className="py-2 text-xs text-center">
{toCfdiDate(cfdi.fechaPagoP || cfdi.fechaEmision).toLocaleDateString('es-MX')}
</td>
<td className="py-2 font-mono text-xs text-center">{cfdi.rfcEmisor}</td>
<td className="py-2 text-xs truncate max-w-[120px] text-center">
{cfdi.nombreEmisor}
</td>
<td className="py-2 font-mono text-xs text-center">{cfdi.rfcReceptor}</td>
<td className="py-2 text-xs truncate max-w-[120px] text-center">
{cfdi.nombreReceptor}
</td>
<td className="py-2 text-xs font-medium text-center">
{formatCurrencyConciliacion(getMonto(cfdi))}
</td>
<td className="py-2 text-xs text-center">{cfdi.metodoPago || '-'}</td>
<td className="py-2 text-center">
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedCfdi(cfdi)}
title="Ver factura"
>
<Eye className="h-4 w-4" />
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* Action bar - only when items selected */}
{!isVisor && selected.size > 0 && (
<div className="sticky bottom-4 z-10 bg-card border rounded-lg shadow-lg p-4 flex items-center gap-4">
<span className="text-sm font-medium">{selected.size} seleccionados</span>
{(() => {
const selectedBank = bancos?.find((x) => String(x.id) === bancoId);
return (
<Select value={bancoId} onValueChange={setBancoId}>
<SelectTrigger className="w-48">
<span className="truncate">
{selectedBank
? `${selectedBank.banco} ****${selectedBank.terminacionCuenta}`
: 'Seleccionar banco'}
</span>
</SelectTrigger>
<SelectContent>
{bancos?.map((b) => (
<SelectItem key={b.id} value={String(b.id)}>
{b.banco} ****{b.terminacionCuenta}
</SelectItem>
))}
</SelectContent>
</Select>
);
})()}
<Input
type="date"
value={fechaPago}
onChange={(e) => setFechaPago(e.target.value)}
className="w-44"
/>
<Button
onClick={handleConciliar}
disabled={!fechaPago || !bancoId || conciliarMut.isPending}
>
<CheckCircle className="h-4 w-4 mr-1" />
Conciliar {selected.size} facturas
</Button>
<Button variant="ghost" size="sm" onClick={() => setSelected(new Set())}>
<X className="h-4 w-4" />
</Button>
</div>
)}
{/* Conciliadas */}
<Card>
<CardContent className="pt-6">
<h3 className="font-medium mb-4">Conciliadas ({conciliadasOrdenadas.length})</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-center text-muted-foreground">
<th className="pb-3 font-medium">UUID</th>
<th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortConciliadas(prev => prev?.field === 'fecha' ? { field: 'fecha', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'fecha', dir: 'asc' })}>
<span className="flex items-center justify-center gap-1">Fecha Emisión <ArrowUpDown className="h-3 w-3" /></span>
</th>
<th className="pb-3 font-medium"><FilterHeader label={activeTab === 'EMITIDO' ? 'RFC Receptor' : 'RFC Emisor'} filterKey={activeTab === 'EMITIDO' ? 'rfcReceptor' : 'rfcEmisor'} filters={filtersConciliadas} setFilters={setFiltersConciliadas} openFilter={openFilterConciliadas} setOpenFilter={setOpenFilterConciliadas} suggestions={activeTab === 'EMITIDO' ? uniqueSuggestions.rfcReceptor : uniqueSuggestions.rfcEmisor} /></th>
<th className="pb-3 font-medium"><FilterHeader label={activeTab === 'EMITIDO' ? 'Nombre Receptor' : 'Nombre Emisor'} filterKey={activeTab === 'EMITIDO' ? 'nombreReceptor' : 'nombreEmisor'} filters={filtersConciliadas} setFilters={setFiltersConciliadas} openFilter={openFilterConciliadas} setOpenFilter={setOpenFilterConciliadas} suggestions={activeTab === 'EMITIDO' ? uniqueSuggestions.nombreReceptor : uniqueSuggestions.nombreEmisor} /></th>
<th className="pb-3 font-medium cursor-pointer select-none" onClick={() => setSortConciliadas(prev => prev?.field === 'total' ? { field: 'total', dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: 'total', dir: 'asc' })}>
<span className="flex items-center justify-center gap-1">Total MXN <ArrowUpDown className="h-3 w-3" /></span>
</th>
<th className="pb-3 font-medium">Fecha Pago</th>
<th className="pb-3 font-medium"><FilterHeader label="Banco" filterKey="banco" filters={filtersConciliadas} setFilters={setFiltersConciliadas} openFilter={openFilterConciliadas} setOpenFilter={setOpenFilterConciliadas} suggestions={uniqueSuggestions.banco} /></th>
<th className="pb-3"></th>
</tr>
</thead>
<tbody>
{conciliadasOrdenadas.length === 0 ? (
<tr>
<td colSpan={8} className="py-4 text-sm text-muted-foreground text-center">
No hay CFDIs conciliados
</td>
</tr>
) : (
conciliadasOrdenadas.map((cfdi) => (
<tr key={cfdi.id} className="border-b hover:bg-muted/50">
<td className="py-2 font-mono text-xs text-center" title={cfdi.uuid}>
{cfdi.uuid?.substring(0, 8)}
</td>
<td className="py-2 text-xs text-center">
{toCfdiDate(cfdi.fechaPagoP || cfdi.fechaEmision).toLocaleDateString('es-MX')}
</td>
<td className="py-2 font-mono text-xs text-center">{activeTab === 'EMITIDO' ? cfdi.rfcReceptor : cfdi.rfcEmisor}</td>
<td className="py-2 text-xs truncate max-w-[120px] text-center">
{activeTab === 'EMITIDO' ? cfdi.nombreReceptor : cfdi.nombreEmisor}
</td>
<td className="py-2 text-xs font-medium text-center">
{formatCurrencyConciliacion(getMonto(cfdi))}
</td>
<td className="py-2 text-xs text-center">
{cfdi.conciliacion?.fechaDePago
? new Date(
(cfdi.conciliacion.fechaDePago.split('T')[0]) + 'T12:00:00',
).toLocaleDateString('es-MX')
: '-'}
</td>
<td className="py-2 text-xs text-center">
{cfdi.conciliacion
? `${cfdi.conciliacion.banco} ****${cfdi.conciliacion.terminacionCuenta}`
: '-'}
</td>
<td className="py-2 flex gap-1 justify-center">
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedCfdi(cfdi)}
title="Ver factura"
>
<Eye className="h-4 w-4" />
</Button>
{!isVisor && cfdi.conciliacion && (
<Button
variant="ghost"
size="sm"
onClick={() => handleDesconciliar(cfdi.conciliacion!.id)}
title="Desconciliar"
className="text-destructive hover:text-destructive"
>
<X className="h-4 w-4" />
</Button>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
</>
)}
</main>
<CfdiViewerModal
cfdi={selectedCfdi}
open={!!selectedCfdi}
onClose={() => setSelectedCfdi(null)}
/>
</>
);
}