Factura Global & fecha_efectiva: - Migracion 045_factura_global.sql: periodicidad, meses_global, año_global, fecha_efectiva - sat-parser.service.ts: extrae InformacionGlobal del XML - sat.service.ts: calcFechaEfectiva con soporte bimestral (periodicidad 05) - metricas-compute, dashboard, impuestos, cfdi, export, conciliacion, alertas: reemplaza fecha_emision-1h por COALESCE(fecha_efectiva, fecha_emision-1h) - Script recalc-metricas.ts para recalculo manual Fallback datos fiscales tenant → contribuyente: - contribuyente.service.ts: fetchTenantFiscalData + mergeContribuyenteWithTenant rellena regimenFiscal, codigoPostal y domicilio cuando el contribuyente tiene el mismo RFC que el tenant y sus campos estan vacios - contribuyente.controller.ts y contribuyente-config.controller.ts: pasan req.user!.tenantId al servicio Fix critico SAT sync: - sat.service.ts: anio_global → año_global en INSERT/UPDATE de CFDIs (la migracion creo 'año_global' con tilde; el codigo usaba 'anio_global', causando fallo en 100% de inserciones de CFDI) - determineChunkMonths: salta sondeo si existe job previo con requestIds - MAX_POLL_ATTEMPTS: 45 → 500 (~8h) para syncs iniciales grandes Docs: - docs/sessions/2026-05-22-factura-global-contribuyente-fallback.md
121 lines
5.3 KiB
TypeScript
121 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { DashboardShell } from '@/components/layouts/dashboard-shell';
|
|
import { Card, CardContent, Button, SortableHeader } from '@horux/shared-ui';
|
|
import { apiClient } from '@/lib/api/client';
|
|
import { formatCurrency, toCfdiDate } from '@/lib/utils';
|
|
import { exportToExcel } from '@/lib/export-excel';
|
|
import { useTableSort } from '@horux/shared-ui';
|
|
import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal';
|
|
import { Eye, Download } from 'lucide-react';
|
|
import type { Cfdi } from '@horux/shared';
|
|
|
|
const EXCEL_COLUMNS = [
|
|
{ header: 'UUID', key: 'uuid', width: 40 },
|
|
{ header: 'Fecha Emision', key: '_fechaEmision', width: 15 },
|
|
{ header: 'Fecha Cancelacion', key: '_fechaCancelacion', width: 18 },
|
|
{ header: 'RFC Emisor', key: 'rfcEmisor', width: 15 },
|
|
{ header: 'Nombre Emisor', key: 'nombreEmisor', width: 30 },
|
|
{ header: 'RFC Receptor', key: 'rfcReceptor', width: 15 },
|
|
{ header: 'Nombre Receptor', key: 'nombreReceptor', width: 30 },
|
|
{ header: 'Total MXN', key: '_totalMxn', width: 15 },
|
|
];
|
|
|
|
function prepareRows(data: any[]) {
|
|
return data.map((c) => ({
|
|
...c,
|
|
_fechaEmision: toCfdiDate(c.fechaEmision).toLocaleDateString('es-MX'),
|
|
_fechaCancelacion: c.fechaCancelacion ? toCfdiDate(c.fechaCancelacion).toLocaleDateString('es-MX') : '',
|
|
_totalMxn: Number(c.totalMxn || 0),
|
|
}));
|
|
}
|
|
|
|
export default function CancelacionesPage() {
|
|
const [selectedCfdi, setSelectedCfdi] = useState<Cfdi | null>(null);
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['drilldown-cancelaciones'],
|
|
queryFn: async () => {
|
|
const res = await apiClient.get<Cfdi[]>('/alertas/drilldown/cancelaciones');
|
|
return res.data;
|
|
},
|
|
});
|
|
|
|
const { sortedData, toggleSort, getSortIndicator } = useTableSort<Cfdi, 'fecha' | 'cancelacion' | 'total'>(
|
|
data,
|
|
{
|
|
fecha: (c) => toCfdiDate(c.fechaEmision).getTime(),
|
|
cancelacion: (c: any) => c.fechaCancelacion ? toCfdiDate(c.fechaCancelacion).getTime() : 0,
|
|
total: (c) => Number(c.totalMxn || 0),
|
|
},
|
|
'cancelacion',
|
|
);
|
|
|
|
const handleExport = () => {
|
|
if (!sortedData || sortedData.length === 0) return;
|
|
exportToExcel(prepareRows(sortedData), EXCEL_COLUMNS, 'cfdis-cancelados');
|
|
};
|
|
|
|
return (
|
|
<DashboardShell title="CFDIs Cancelados (Ultimos 5 años)">
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
{isLoading ? (
|
|
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
|
|
) : !data || data.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">No hay CFDIs cancelados</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<p className="text-xs text-muted-foreground">{data.length} CFDIs cancelados</p>
|
|
<Button variant="outline" size="sm" onClick={handleExport}>
|
|
<Download className="h-4 w-4 mr-1" />
|
|
Excel
|
|
</Button>
|
|
</div>
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b text-left text-muted-foreground">
|
|
<th className="pb-3 font-medium">UUID</th>
|
|
<SortableHeader label="Fecha Emision" active={getSortIndicator('fecha')} onClick={() => toggleSort('fecha')} />
|
|
<SortableHeader label="Fecha Cancelacion" active={getSortIndicator('cancelacion')} onClick={() => toggleSort('cancelacion')} />
|
|
<th className="pb-3 font-medium">RFC Emisor</th>
|
|
<th className="pb-3 font-medium">RFC Receptor</th>
|
|
<SortableHeader label="Total MXN" align="right" active={getSortIndicator('total')} onClick={() => toggleSort('total')} />
|
|
<th className="pb-3 font-medium"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(sortedData || []).map((cfdi: any) => (
|
|
<tr key={cfdi.id} className="border-b hover:bg-muted/50">
|
|
<td className="py-3 font-mono text-xs">{cfdi.uuid?.substring(0, 8)}</td>
|
|
<td className="py-3">{toCfdiDate(cfdi.fechaEmision).toLocaleDateString('es-MX')}</td>
|
|
<td className="py-3">{cfdi.fechaCancelacion ? toCfdiDate(cfdi.fechaCancelacion).toLocaleDateString('es-MX') : '-'}</td>
|
|
<td className="py-3 font-mono text-xs">{cfdi.rfcEmisor}</td>
|
|
<td className="py-3 font-mono text-xs">{cfdi.rfcReceptor}</td>
|
|
<td className="py-3 text-right font-medium">{formatCurrency(Number(cfdi.totalMxn))}</td>
|
|
<td className="py-3">
|
|
<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>
|
|
|
|
<CfdiViewerModal
|
|
cfdi={selectedCfdi}
|
|
open={!!selectedCfdi}
|
|
onClose={() => setSelectedCfdi(null)}
|
|
/>
|
|
</DashboardShell>
|
|
);
|
|
}
|