- La utilidad del dashboard ahora descuenta NCs emitidas de ingresos y NCs recibidas de gastos. - El margen se calcula sobre ingresos netos. - Solo afecta la UI del dashboard; no modifica el backend ni otros reportes.
490 lines
21 KiB
TypeScript
490 lines
21 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { Header } from '@/components/layouts/header';
|
||
import { KpiCard } from '@horux/shared-ui';
|
||
import { BarChart } from '@/components/charts/bar-chart';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@horux/shared-ui';
|
||
import { PeriodSelector, RegimenSelector } from '@horux/shared-ui';
|
||
import { useKpis, useIngresosEgresos, useAlertas, useRegimenesDelPeriodo } from '@/lib/hooks/use-dashboard';
|
||
import { useAuthStore } from '@/stores/auth-store';
|
||
import { useTenantViewStore } from '@/stores/tenant-view-store';
|
||
import { isGlobalAdminRfc } from '@horux/shared';
|
||
import {
|
||
TrendingUp,
|
||
TrendingDown,
|
||
Wallet,
|
||
Receipt,
|
||
AlertTriangle,
|
||
ShoppingCart,
|
||
CheckSquare,
|
||
FileMinus,
|
||
FilePlus,
|
||
} from 'lucide-react';
|
||
import { cn } from '@horux/shared-ui';
|
||
import { FiscalDisclaimer } from '@/components/fiscal-disclaimer';
|
||
|
||
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 shiftDatesOneYear(fechaInicio: string, fechaFin: string, delta: number) {
|
||
const s = new Date(fechaInicio + 'T00:00:00');
|
||
const e = new Date(fechaFin + 'T00:00:00');
|
||
s.setFullYear(s.getFullYear() + delta);
|
||
e.setFullYear(e.getFullYear() + delta);
|
||
// Ajustar último día del mes si cambió
|
||
const lastDay = new Date(e.getFullYear(), e.getMonth() + 1, 0).getDate();
|
||
if (e.getDate() > lastDay) e.setDate(lastDay);
|
||
return {
|
||
fechaInicio: s.toISOString().split('T')[0],
|
||
fechaFin: e.toISOString().split('T')[0],
|
||
};
|
||
}
|
||
|
||
export default function DashboardPage() {
|
||
const router = useRouter();
|
||
const { user } = useAuthStore();
|
||
const { viewingTenantId } = useTenantViewStore();
|
||
// Admin global no opera sobre datos de despacho propios — su home natural
|
||
// es `/clientes`. EXCEPCIÓN: si está impersonando un tenant (vía botón "Ver"
|
||
// en /clientes), sí entra al dashboard para validar lo que ve el cliente.
|
||
const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles);
|
||
useEffect(() => {
|
||
if (isGlobalAdmin && !viewingTenantId) router.replace('/clientes');
|
||
}, [isGlobalAdmin, viewingTenantId, router]);
|
||
|
||
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 [conciliacion, setConciliacion] = useState(false);
|
||
|
||
// Periodo anterior (mismo rango, un año atrás)
|
||
const anterior = shiftDatesOneYear(fechaInicio, fechaFin, -1);
|
||
|
||
// Año del inicio para el chart anual
|
||
const añoChart = new Date(fechaInicio + 'T00:00:00').getFullYear();
|
||
const mesResumen = new Date(fechaInicio + 'T00:00:00').getMonth() + 1;
|
||
|
||
const { data: kpis } = useKpis(fechaInicio, fechaFin, conciliacion);
|
||
const { data: kpisAnterior } = useKpis(anterior.fechaInicio, anterior.fechaFin, conciliacion);
|
||
const { data: chartData } = useIngresosEgresos(añoChart, conciliacion);
|
||
const { data: alertas, isLoading: alertasLoading } = useAlertas(5);
|
||
const { data: regimenesPeriodo, isLoading: regimenesLoading } = useRegimenesDelPeriodo(fechaInicio, fechaFin, conciliacion);
|
||
|
||
const handlePeriodChange = (inicio: string, fin: string) => {
|
||
setFechaInicio(inicio);
|
||
setFechaFin(fin);
|
||
};
|
||
|
||
// Filtrar ingresos por régimen seleccionado
|
||
const ingresosDisplay = regimenSeleccionado
|
||
? kpis?.ingresosPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
|
||
: kpis?.ingresos || 0;
|
||
|
||
const ingresosAnterior = regimenSeleccionado
|
||
? kpisAnterior?.ingresosPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
|
||
: kpisAnterior?.ingresos || 0;
|
||
|
||
const ingresosVariacion = ingresosAnterior > 0
|
||
? Math.round(((ingresosDisplay - ingresosAnterior) / ingresosAnterior) * 10000) / 100
|
||
: null;
|
||
|
||
// Filtrar egresos por régimen seleccionado
|
||
const egresosDisplay = regimenSeleccionado
|
||
? kpis?.egresosPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
|
||
: kpis?.egresos || 0;
|
||
|
||
const egresosAnterior = regimenSeleccionado
|
||
? kpisAnterior?.egresosPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
|
||
: kpisAnterior?.egresos || 0;
|
||
|
||
const egresosVariacion = egresosAnterior > 0
|
||
? Math.round(((egresosDisplay - egresosAnterior) / egresosAnterior) * 10000) / 100
|
||
: null;
|
||
|
||
// Adquisición de mercancías
|
||
const adquisicionDisplay = regimenSeleccionado
|
||
? kpis?.adquisicionMercanciasPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
|
||
: kpis?.adquisicionMercancias || 0;
|
||
|
||
// Filtrar IVA por régimen seleccionado
|
||
const ivaDisplay = regimenSeleccionado
|
||
? kpis?.ivaBalancePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
|
||
: kpis?.ivaBalance || 0;
|
||
|
||
// Notas de crédito
|
||
const ncsEmitidasDisplay = regimenSeleccionado
|
||
? kpis?.ncsEmitidasPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
|
||
: kpis?.ncsEmitidas || 0;
|
||
|
||
const ncsRecibidasDisplay = regimenSeleccionado
|
||
? kpis?.ncsRecibidasPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
|
||
: kpis?.ncsRecibidas || 0;
|
||
|
||
const ivaAnterior = regimenSeleccionado
|
||
? kpisAnterior?.ivaBalancePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
|
||
: kpisAnterior?.ivaBalance || 0;
|
||
|
||
const ivaVariacion = ivaAnterior !== 0
|
||
? Math.round(((ivaDisplay - ivaAnterior) / Math.abs(ivaAnterior)) * 10000) / 100
|
||
: null;
|
||
|
||
// Utilidad ajustada por notas de crédito:
|
||
// Ingresos netos = Ingresos − NCs emitidas
|
||
// Egresos netos = Gastos − NCs recibidas
|
||
// Utilidad neta = Ingresos netos − Egresos netos
|
||
const ingresosNetosDisplay = ingresosDisplay - ncsEmitidasDisplay;
|
||
const egresosNetosDisplay = egresosDisplay - ncsRecibidasDisplay;
|
||
const utilidadDisplay = ingresosNetosDisplay - egresosNetosDisplay;
|
||
const margenDisplay = ingresosNetosDisplay > 0
|
||
? Math.round((utilidadDisplay / ingresosNetosDisplay) * 10000) / 100
|
||
: 0;
|
||
|
||
const formatCurrency = (value: number) =>
|
||
new Intl.NumberFormat('es-MX', {
|
||
style: 'currency',
|
||
currency: 'MXN',
|
||
minimumFractionDigits: 0,
|
||
}).format(value);
|
||
|
||
// Helper para construir URLs de drill-down
|
||
const drillUrl = (titulo: string, filters: Record<string, string>) => {
|
||
const p = new URLSearchParams({ titulo, fechaInicio, fechaFin, status: 'vigente', ...filters });
|
||
// Dashboard no tiene toggles considerarActivos/considerarNCs — siempre
|
||
// pasa los defaults true (omitir = backend usa true). Si en el futuro se
|
||
// agregan toggles aquí, propagarlos como hace /impuestos.
|
||
if (regimenSeleccionado) {
|
||
if (filters.type === 'EMITIDO') p.set('regimenEmisor', regimenSeleccionado);
|
||
else if (filters.type === 'RECIBIDO') p.set('regimenReceptor', regimenSeleccionado);
|
||
// Por bucket — 605 es receptor en bucket=ingresos (nómina recibida).
|
||
else if (filters.bucket === 'ingresos') {
|
||
if (regimenSeleccionado === '605') p.set('regimenReceptor', '605');
|
||
else p.set('regimenEmisor', regimenSeleccionado);
|
||
}
|
||
else if (filters.bucket === 'causado') p.set('regimenEmisor', regimenSeleccionado);
|
||
else if (filters.bucket === 'gastos' || filters.bucket === 'acreditable') p.set('regimenReceptor', regimenSeleccionado);
|
||
}
|
||
return `/drill-down?${p}`;
|
||
};
|
||
|
||
// Año anterior para labels
|
||
const añoAnterior = new Date(anterior.fechaInicio + 'T00:00:00').getFullYear();
|
||
|
||
// Reset régimen si ya no existe en el periodo
|
||
const regimenesDisponibles = regimenesPeriodo || [];
|
||
if (regimenSeleccionado && regimenesDisponibles.length > 0 &&
|
||
!regimenesDisponibles.find(r => r.clave === regimenSeleccionado)) {
|
||
setRegimenSeleccionado(null);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Header title="Dashboard">
|
||
<PeriodSelector
|
||
fechaInicio={fechaInicio}
|
||
fechaFin={fechaFin}
|
||
onChange={handlePeriodChange}
|
||
/>
|
||
</Header>
|
||
<main className="p-6 space-y-6">
|
||
{/* Filtros */}
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<RegimenSelector
|
||
regimenes={regimenesDisponibles}
|
||
selected={regimenSeleccionado}
|
||
onChange={setRegimenSeleccionado}
|
||
isLoading={regimenesLoading}
|
||
/>
|
||
<button
|
||
onClick={() => setConciliacion(!conciliacion)}
|
||
className={cn(
|
||
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
||
conciliacion
|
||
? 'bg-primary/10 text-primary border border-primary/30'
|
||
: 'hover:bg-accent'
|
||
)}
|
||
>
|
||
<CheckSquare className="h-4 w-4" />
|
||
Conciliación
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* KPIs */}
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
<KpiCard
|
||
title={regimenSeleccionado ? `Ingresos del Mes (${regimenSeleccionado})` : 'Ingresos del Mes'}
|
||
value={ingresosDisplay}
|
||
icon={<TrendingUp className="h-4 w-4" />}
|
||
trend={ingresosVariacion !== null ? (ingresosVariacion >= 0 ? 'up' : 'down') : 'neutral'}
|
||
trendValue={
|
||
ingresosVariacion !== null
|
||
? `${ingresosVariacion >= 0 ? '+' : ''}${ingresosVariacion}% vs ${añoAnterior}`
|
||
: 'Sin datos del periodo anterior'
|
||
}
|
||
href={drillUrl('Ingresos del Mes - CFDIs', { bucket: 'ingresos' })}
|
||
/>
|
||
<KpiCard
|
||
title={regimenSeleccionado ? `NCs Emitidas (${regimenSeleccionado})` : 'NCs Emitidas'}
|
||
value={ncsEmitidasDisplay}
|
||
icon={<FileMinus className="h-4 w-4" />}
|
||
trend="neutral"
|
||
trendValue="Notas de crédito emitidas"
|
||
/>
|
||
<KpiCard
|
||
title={regimenSeleccionado ? `Gastos del Mes (${regimenSeleccionado})` : 'Gastos del Mes'}
|
||
value={egresosDisplay}
|
||
icon={<TrendingDown className="h-4 w-4" />}
|
||
trend={egresosVariacion !== null ? (egresosVariacion >= 0 ? 'up' : 'down') : 'neutral'}
|
||
trendValue={
|
||
egresosVariacion !== null
|
||
? `${egresosVariacion >= 0 ? '+' : ''}${egresosVariacion}% vs ${añoAnterior}`
|
||
: 'Sin datos del periodo anterior'
|
||
}
|
||
href={drillUrl('Gastos del Mes - CFDIs', { bucket: 'gastos' })}
|
||
/>
|
||
<KpiCard
|
||
title={regimenSeleccionado ? `NCs Recibidas (${regimenSeleccionado})` : 'NCs Recibidas'}
|
||
value={ncsRecibidasDisplay}
|
||
icon={<FilePlus className="h-4 w-4" />}
|
||
trend="neutral"
|
||
trendValue="Notas de crédito recibidas"
|
||
/>
|
||
<KpiCard
|
||
title={regimenSeleccionado ? `Utilidad Neta (${regimenSeleccionado})` : 'Utilidad Neta'}
|
||
value={utilidadDisplay}
|
||
icon={<Wallet className="h-4 w-4" />}
|
||
trend={utilidadDisplay > 0 ? 'up' : 'down'}
|
||
trendValue={`${margenDisplay}% margen · incluye NCs`}
|
||
/>
|
||
<KpiCard
|
||
title={regimenSeleccionado ? `Balance IVA (${regimenSeleccionado})` : 'Balance IVA'}
|
||
value={ivaDisplay}
|
||
icon={<Receipt className="h-4 w-4" />}
|
||
trend={ivaDisplay > 0 ? 'up' : ivaDisplay < 0 ? 'down' : 'neutral'}
|
||
trendValue={ivaDisplay > 0 ? 'Por pagar' : ivaDisplay < 0 ? 'A favor' : 'Neutro'}
|
||
subtitle={
|
||
ivaVariacion !== null
|
||
? `${ivaVariacion >= 0 ? '+' : ''}${ivaVariacion}% vs ${añoAnterior}`
|
||
: undefined
|
||
}
|
||
href={drillUrl('Balance IVA - CFDIs', {})}
|
||
/>
|
||
</div>
|
||
|
||
{/* Desglose por régimen */}
|
||
{!regimenSeleccionado && kpis && (
|
||
(kpis.ingresosPorRegimen.length > 1 || kpis.egresosPorRegimen.length > 1 || kpis.ivaBalancePorRegimen.length > 1 || kpis.ncsEmitidasPorRegimen.length > 1 || kpis.ncsRecibidasPorRegimen.length > 1) && (
|
||
<div className="grid gap-4 md:grid-cols-2 3xl:grid-cols-3">
|
||
{kpis.ingresosPorRegimen.length > 1 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base font-medium">Ingresos por Regimen</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-2">
|
||
{kpis.ingresosPorRegimen.map((r) => (
|
||
<div key={r.regimenClave} className="flex items-center justify-between py-2 border-b last:border-0">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xs font-mono font-bold bg-muted px-2 py-1 rounded">{r.regimenClave}</span>
|
||
<span className="text-sm">{r.regimenDescripcion}</span>
|
||
</div>
|
||
<span className="text-sm font-semibold">{formatCurrency(r.monto)}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
{kpis.egresosPorRegimen.length > 1 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base font-medium">Gastos por Regimen</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-2">
|
||
{kpis.egresosPorRegimen.map((r) => (
|
||
<div key={r.regimenClave} className="flex items-center justify-between py-2 border-b last:border-0">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xs font-mono font-bold bg-muted px-2 py-1 rounded">{r.regimenClave}</span>
|
||
<span className="text-sm">{r.regimenDescripcion}</span>
|
||
</div>
|
||
<span className="text-sm font-semibold">{formatCurrency(r.monto)}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
{kpis.ivaBalancePorRegimen.length > 1 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base font-medium">Balance IVA por Regimen</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-2">
|
||
{kpis.ivaBalancePorRegimen.map((r) => (
|
||
<div key={r.regimenClave} className="flex items-center justify-between py-2 border-b last:border-0">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xs font-mono font-bold bg-muted px-2 py-1 rounded">{r.regimenClave}</span>
|
||
<span className="text-sm">{r.regimenDescripcion}</span>
|
||
</div>
|
||
<span className={`text-sm font-semibold ${r.monto > 0 ? 'text-destructive' : 'text-success'}`}>
|
||
{formatCurrency(r.monto)}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
{kpis.ncsEmitidasPorRegimen.length > 1 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base font-medium">NCs Emitidas por Regimen</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-2">
|
||
{kpis.ncsEmitidasPorRegimen.map((r) => (
|
||
<div key={r.regimenClave} className="flex items-center justify-between py-2 border-b last:border-0">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xs font-mono font-bold bg-muted px-2 py-1 rounded">{r.regimenClave}</span>
|
||
<span className="text-sm">{r.regimenDescripcion}</span>
|
||
</div>
|
||
<span className="text-sm font-semibold">{formatCurrency(r.monto)}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
{kpis.ncsRecibidasPorRegimen.length > 1 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base font-medium">NCs Recibidas por Regimen</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-2">
|
||
{kpis.ncsRecibidasPorRegimen.map((r) => (
|
||
<div key={r.regimenClave} className="flex items-center justify-between py-2 border-b last:border-0">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xs font-mono font-bold bg-muted px-2 py-1 rounded">{r.regimenClave}</span>
|
||
<span className="text-sm">{r.regimenDescripcion}</span>
|
||
</div>
|
||
<span className="text-sm font-semibold">{formatCurrency(r.monto)}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
))}
|
||
|
||
{/* Charts and Alerts */}
|
||
<div className="grid gap-6 lg:grid-cols-3">
|
||
<div className="lg:col-span-2">
|
||
<BarChart
|
||
title="Ingresos vs Egresos"
|
||
data={chartData || []}
|
||
/>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-base font-medium">
|
||
<AlertTriangle className="h-4 w-4" />
|
||
Alertas
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
{alertasLoading ? (
|
||
<p className="text-sm text-muted-foreground">Cargando...</p>
|
||
) : alertas?.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground">No hay alertas pendientes</p>
|
||
) : (
|
||
alertas?.map((alerta) => (
|
||
<div
|
||
key={alerta.id}
|
||
className={`p-3 rounded-lg border ${
|
||
alerta.prioridad === 'alta'
|
||
? 'border-destructive/50 bg-destructive/10'
|
||
: 'border-border bg-muted/50'
|
||
}`}
|
||
>
|
||
<p className="text-sm font-medium">{alerta.titulo}</p>
|
||
<p className="text-xs text-muted-foreground mt-1">
|
||
{alerta.mensaje}
|
||
</p>
|
||
</div>
|
||
))
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Resumen Fiscal */}
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<p className="text-sm text-muted-foreground">CFDIs Emitidos</p>
|
||
<p className="text-2xl font-bold">{
|
||
regimenSeleccionado
|
||
? kpis?.cfdisEmitidosPorRegimen?.find(r => r.regimen === regimenSeleccionado)?.total || 0
|
||
: kpis?.cfdisEmitidos || 0
|
||
}</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<p className="text-sm text-muted-foreground">CFDIs Recibidos</p>
|
||
<p className="text-2xl font-bold">{
|
||
regimenSeleccionado
|
||
? kpis?.cfdisRecibidosPorRegimen?.find(r => r.regimen === regimenSeleccionado)?.total || 0
|
||
: kpis?.cfdisRecibidos || 0
|
||
}</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center gap-2">
|
||
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
|
||
<p className="text-sm text-muted-foreground">Adquisición de Mercancías</p>
|
||
</div>
|
||
<p className="text-2xl font-bold">{formatCurrency(adquisicionDisplay)}</p>
|
||
<p className="text-xs text-muted-foreground mt-1">Uso CFDI G01</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<p className="text-sm text-muted-foreground">IVA a Favor ({añoChart})</p>
|
||
<p className="text-2xl font-bold text-success">
|
||
{formatCurrency(kpis?.ivaAFavorAcumulado || 0)}
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<p className="text-sm text-muted-foreground">IVA a Favor Historico</p>
|
||
<p className="text-2xl font-bold text-success">
|
||
{formatCurrency(kpis?.ivaAFavorHistorico || 0)}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground mt-1">{añoChart - 5} — {añoChart}</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
<FiscalDisclaimer />
|
||
</main>
|
||
</>
|
||
);
|
||
}
|