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

873 lines
46 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState } from 'react';
import { Header } from '@/components/layouts/header';
import { Card, CardContent, CardHeader, CardTitle, Button, Input } from '@horux/shared-ui';
import { KpiCard, PeriodSelector, RegimenSelector } from '@horux/shared-ui';
import { useIvaMensual, useIsrMensual, useResumenIva, useResumenIsr, useResumenIsrDesglosado, useCoeficiente } from '@/lib/hooks/use-impuestos';
import { setCoeficiente as setCoeficienteApi } from '@/lib/api/impuestos';
import { useQueryClient } from '@tanstack/react-query';
import { useRegimenesDelPeriodo } from '@/lib/hooks/use-dashboard';
import { Calculator, TrendingUp, TrendingDown, Receipt, Settings, Wallet, CheckSquare, Download } from 'lucide-react';
import { cn } from '@horux/shared-ui';
import { FiscalDisclaimer } from '@/components/fiscal-disclaimer';
import { exportToExcel } from '@/lib/export-excel';
import { ActivosFijosTab } from '@/components/impuestos/activos-fijos-tab';
const meses = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];
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 };
}
export default function ImpuestosPage() {
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 [activeTab, setActiveTab] = useState<'iva' | 'isr' | 'activos-fijos'>('iva');
const [regimenSeleccionado, setRegimenSeleccionado] = useState<string | null>(null);
const [conciliacion, setConciliacion] = useState(false);
const [considerarActivos, setConsiderarActivos] = useState(true);
const [considerarNCs, setConsiderarNCs] = useState(true);
const año = new Date(fechaInicio + 'T00:00:00').getFullYear();
const mes = new Date(fechaInicio + 'T00:00:00').getMonth() + 1;
const queryClient = useQueryClient();
const [coefInput, setCoefInput] = useState('');
const [savingCoef, setSavingCoef] = useState(false);
const { data: ivaMensual, isLoading: ivaLoading } = useIvaMensual(año, conciliacion, considerarActivos, considerarNCs);
const { data: isrMensual, isLoading: isrLoading } = useIsrMensual(año, conciliacion, regimenSeleccionado, considerarActivos, considerarNCs);
const { data: resumenIva } = useResumenIva(fechaInicio, fechaFin, conciliacion, considerarActivos, considerarNCs);
const { data: resumenIsr } = useResumenIsr(fechaInicio, fechaFin, conciliacion, considerarActivos, considerarNCs);
const { data: resumenIsrDesglose } = useResumenIsrDesglosado(fechaFin, conciliacion, considerarActivos, considerarNCs);
const { data: coefData } = useCoeficiente(año);
const { data: regimenesPeriodo, isLoading: regimenesLoading } = useRegimenesDelPeriodo(fechaInicio, fechaFin, conciliacion);
const regimenesDisponibles = regimenesPeriodo || [];
if (regimenSeleccionado && regimenesDisponibles.length > 0 &&
!regimenesDisponibles.find(r => r.clave === regimenSeleccionado)) {
setRegimenSeleccionado(null);
}
const formatCurrency = (value: number) =>
new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
const drillUrl = (titulo: string, filters: Record<string, string>) => {
const p = new URLSearchParams({ titulo, fechaInicio, fechaFin, status: 'vigente', ...filters });
// Propaga los toggles de "Considerar activos" / "Considerar NCs" para que
// el drill aplique los mismos filtros que las cards (sin esto, el drill
// mostraba CFDIs ya excluidos del total).
if (!considerarActivos) p.set('considerarActivos', '0');
if (!considerarNCs) p.set('considerarNCs', '0');
if (regimenSeleccionado) {
// Por type explícito (raros — IVA legacy paths)
if (filters.type === 'EMITIDO') p.set('regimenEmisor', regimenSeleccionado);
else if (filters.type === 'RECIBIDO') p.set('regimenReceptor', regimenSeleccionado);
// Por bucket (la mayoría de KPIs nuevos). 605 es receptor en bucket=ingresos
// (nómina recibida); el resto va por emisor para ingresos/causado, receptor
// para gastos/acreditable.
else if (filters.bucket === 'ingresos') {
if (regimenSeleccionado === '605') p.set('regimenReceptor', '605');
else p.set('regimenEmisor', regimenSeleccionado);
}
else if (filters.bucket === 'causado' || filters.bucket === 'ncs_emitidas') p.set('regimenEmisor', regimenSeleccionado);
else if (filters.bucket === 'gastos' || filters.bucket === 'acreditable' || filters.bucket === 'ncs_recibidas' || filters.bucket === 'no_deducibles_efectivo') p.set('regimenReceptor', regimenSeleccionado);
}
return `/drill-down?${p}`;
};
return (
<>
<Header title="Control de Impuestos">
<PeriodSelector
fechaInicio={fechaInicio}
fechaFin={fechaFin}
onChange={(fi, ff) => { setFechaInicio(fi); setFechaFin(ff); }}
/>
</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>
<button
onClick={() => setConsiderarActivos(!considerarActivos)}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
considerarActivos
? 'bg-primary/10 text-primary border border-primary/30'
: 'hover:bg-accent'
)}
title="Si está inactivo, no se consideran facturas tipo I con uso de CFDI I01-I08 (compras de activos fijos)."
>
<CheckSquare className="h-4 w-4" />
Considerar activos
</button>
<button
onClick={() => setConsiderarNCs(!considerarNCs)}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
considerarNCs
? 'bg-primary/10 text-primary border border-primary/30'
: 'hover:bg-accent'
)}
title="Si está inactivo, se excluyen TODAS las facturas tipo E (cualquier tipo de relación) y se omite la compensación I/07 PPD ↔ E."
>
<CheckSquare className="h-4 w-4" />
Considerar NCs
</button>
</div>
</div>
<div className="flex gap-2">
<Button
variant={activeTab === 'iva' ? 'default' : 'outline'}
onClick={() => setActiveTab('iva')}
>
<Receipt className="h-4 w-4 mr-2" />
IVA
</Button>
<Button
variant={activeTab === 'isr' ? 'default' : 'outline'}
onClick={() => setActiveTab('isr')}
>
<Calculator className="h-4 w-4 mr-2" />
ISR
</Button>
<Button
variant={activeTab === 'activos-fijos' ? 'default' : 'outline'}
onClick={() => setActiveTab('activos-fijos')}
>
<Wallet className="h-4 w-4 mr-2" />
Activos Fijos
</Button>
</div>
{activeTab === 'iva' && (
<>
{/* IVA KPIs */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
<KpiCard
title={regimenSeleccionado ? `IVA Trasladado (${regimenSeleccionado})` : 'IVA Trasladado'}
value={
regimenSeleccionado
? resumenIva?.trasladadoPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: resumenIva?.trasladado || 0
}
icon={<TrendingUp className="h-4 w-4" />}
subtitle="Cobrado a clientes"
href={drillUrl('IVA Trasladado - CFDIs Emitidos', { bucket: 'causado' })}
target="_blank"
rel="noopener noreferrer"
/>
<KpiCard
title={regimenSeleccionado ? `IVA Acreditable (${regimenSeleccionado})` : 'IVA Acreditable'}
value={
regimenSeleccionado
? resumenIva?.acreditablePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: resumenIva?.acreditable || 0
}
icon={<TrendingDown className="h-4 w-4" />}
subtitle="Pagado a proveedores"
href={drillUrl('IVA Acreditable - CFDIs Recibidos', { bucket: 'acreditable' })}
target="_blank"
rel="noopener noreferrer"
/>
{(() => {
const val = regimenSeleccionado
? resumenIva?.retenidoPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: resumenIva?.retenido || 0;
return (
<KpiCard
title={regimenSeleccionado ? `IVA Retenido (${regimenSeleccionado})` : 'IVA Retenido'}
value={val}
icon={<Receipt className="h-4 w-4" />}
trend={val > 0 ? 'up' : val < 0 ? 'down' : 'neutral'}
trendValue={val > 0 ? 'A favor' : val < 0 ? 'En contra' : 'Neutro'}
/>
);
})()}
{(() => {
const t = regimenSeleccionado
? resumenIva?.trasladadoPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: resumenIva?.trasladado || 0;
const a = regimenSeleccionado
? resumenIva?.acreditablePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: resumenIva?.acreditable || 0;
const ret = regimenSeleccionado
? resumenIva?.retenidoPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: resumenIva?.retenido || 0;
const res = t - a - ret;
return (
<KpiCard
title={regimenSeleccionado ? `Resultado (${regimenSeleccionado})` : 'Resultado del Periodo'}
value={res}
icon={<Calculator className="h-4 w-4" />}
trend={res > 0 ? 'up' : res < 0 ? 'down' : 'neutral'}
trendValue={res > 0 ? 'Por pagar' : res < 0 ? 'A favor' : 'Neutro'}
/>
);
})()}
<KpiCard
title="Acumulado Anual"
value={resumenIva?.acumuladoAnual || 0}
icon={<Receipt className="h-4 w-4" />}
trend={(resumenIva?.acumuladoAnual || 0) < 0 ? 'up' : 'neutral'}
trendValue={(resumenIva?.acumuladoAnual || 0) < 0 ? 'Saldo a favor' : ''}
/>
{(() => {
const v = regimenSeleccionado
? (resumenIva?.ivaNoAcreditableEfectivoPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto ?? 0)
: (resumenIva?.ivaNoAcreditableEfectivo ?? 0);
return (
<KpiCard
title={regimenSeleccionado ? `IVA No Acreditable (${regimenSeleccionado})` : 'IVA No Acreditable'}
value={v}
icon={<TrendingDown className="h-4 w-4" />}
subtitle="Efectivo > $2,000"
/>
);
})()}
</div>
{/* IVA Mensual Table */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">Histórico IVA {año}</CardTitle>
{ivaMensual && ivaMensual.length > 0 && (
<Button variant="outline" size="sm" onClick={() => exportToExcel(
ivaMensual.map(r => ({
Mes: meses[r.mes - 1],
Trasladado: r.ivaTrasladado,
Acreditable: r.ivaAcreditable,
Retenido: r.ivaRetenido,
Resultado: r.resultado,
Acumulado: r.acumulado,
})),
[
{ header: 'Mes', key: 'Mes', width: 12 },
{ header: 'Trasladado', key: 'Trasladado', width: 18 },
{ header: 'Acreditable', key: 'Acreditable', width: 18 },
{ header: 'Retenido', key: 'Retenido', width: 18 },
{ header: 'Resultado', key: 'Resultado', width: 18 },
{ header: 'Acumulado', key: 'Acumulado', width: 18 },
],
`iva-mensual-${año}`,
)}>
<Download className="h-4 w-4 mr-1" /> Excel
</Button>
)}
</CardHeader>
<CardContent>
{ivaLoading ? (
<div className="text-center py-8 text-muted-foreground">
Cargando...
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b text-left text-sm text-muted-foreground">
<th className="pb-3 font-medium">Mes</th>
<th className="pb-3 font-medium text-right">Trasladado</th>
<th className="pb-3 font-medium text-right">Acreditable</th>
<th className="pb-3 font-medium text-right">Retenido</th>
<th className="pb-3 font-medium text-right">Resultado</th>
<th className="pb-3 font-medium text-right">Acumulado</th>
<th className="pb-3 font-medium">Estado</th>
</tr>
</thead>
<tbody className="text-sm">
{ivaMensual?.map((row) => (
<tr key={row.mes} className="border-b hover:bg-muted/50">
<td className="py-3 font-medium">{meses[row.mes - 1]}</td>
<td className="py-3 text-right">
{formatCurrency(row.ivaTrasladado)}
</td>
<td className="py-3 text-right">
{formatCurrency(row.ivaAcreditable)}
</td>
<td className="py-3 text-right">
{formatCurrency(row.ivaRetenido)}
</td>
<td
className={`py-3 text-right font-medium ${
row.resultado > 0
? 'text-destructive'
: 'text-success'
}`}
>
{formatCurrency(row.resultado)}
</td>
<td
className={`py-3 text-right font-medium ${
row.acumulado > 0
? 'text-destructive'
: 'text-success'
}`}
>
{formatCurrency(row.acumulado)}
</td>
<td className="py-3">
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
row.estado === 'declarado'
? 'bg-success/10 text-success'
: 'bg-warning/10 text-warning'
}`}
>
{row.estado === 'declarado' ? 'Declarado' : 'Pendiente'}
</span>
</td>
</tr>
))}
{(!ivaMensual || ivaMensual.length === 0) && (
<tr>
<td colSpan={7} className="py-8 text-center text-muted-foreground">
No hay registros de IVA para este año
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</>
)}
{activeTab === 'isr' && (
<>
{/* ISR KPIs */}
{(() => {
const bg = regimenSeleccionado
? resumenIsr?.baseGravablePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)
: null;
const showUtilidad = true;
const ingSel = regimenSeleccionado
? resumenIsr?.ingresosPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: resumenIsr?.ingresosAcumulados || 0;
const dedSel = regimenSeleccionado
? resumenIsr?.deduccionesPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: resumenIsr?.deducciones || 0;
// ISR a pagar filtered by regime
const bgSelForKpi = regimenSeleccionado
? resumenIsr?.baseGravablePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)
: null;
const isrCausadoSel = regimenSeleccionado
? (bgSelForKpi?.isrCausado || 0)
: resumenIsr?.isrCausado || 0;
const isrRetenidoSel = regimenSeleccionado ? 0 : resumenIsr?.isrRetenido || 0;
const isrAPagarSel = Math.max(0, isrCausadoSel - isrRetenidoSel);
const ncsEmSel = regimenSeleccionado
? (resumenIsr?.ncsEmitidasPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto ?? 0)
: (resumenIsr?.ncsEmitidas ?? 0);
const ncsRecSel = regimenSeleccionado
? (resumenIsr?.ncsRecibidasPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto ?? 0)
: (resumenIsr?.ncsRecibidas ?? 0);
const noDedSel = regimenSeleccionado
? (resumenIsr?.gastosNoDeduciblesEfectivoPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto ?? 0)
: (resumenIsr?.gastosNoDeduciblesEfectivo ?? 0);
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
<KpiCard
title={regimenSeleccionado ? `Ingresos ISR (${regimenSeleccionado})` : 'Ingresos Nominales'}
value={ingSel}
icon={<TrendingUp className="h-4 w-4" />}
href={drillUrl('Ingresos ISR - CFDIs Emitidos', { bucket: 'ingresos' })}
target="_blank"
rel="noopener noreferrer"
/>
<KpiCard
title={regimenSeleccionado ? `NCs Emitidas (${regimenSeleccionado})` : 'NCs Emitidas'}
value={ncsEmSel}
icon={<TrendingDown className="h-4 w-4" />}
href={drillUrl('NCs Emitidas - CFDIs', { bucket: 'ncs_emitidas' })}
target="_blank"
rel="noopener noreferrer"
/>
<KpiCard
title={regimenSeleccionado ? `Deducciones (${regimenSeleccionado})` : 'Deducciones'}
value={dedSel}
icon={<TrendingDown className="h-4 w-4" />}
href={drillUrl('Deducciones - CFDIs Recibidos', { bucket: 'gastos' })}
target="_blank"
rel="noopener noreferrer"
/>
<KpiCard
title={regimenSeleccionado ? `NCs Recibidas (${regimenSeleccionado})` : 'NCs Recibidas'}
value={ncsRecSel}
icon={<TrendingUp className="h-4 w-4" />}
href={drillUrl('NCs Recibidas - CFDIs', { bucket: 'ncs_recibidas' })}
target="_blank"
rel="noopener noreferrer"
/>
<KpiCard
title={regimenSeleccionado ? `Base Gravable (${regimenSeleccionado})` : 'Base Gravable'}
value={regimenSeleccionado ? (bg?.baseGravable ?? 0) : resumenIsr?.baseGravable || 0}
icon={<Calculator className="h-4 w-4" />}
subtitle={bg ? (bg.formula === 'ingresos-deducciones' ? 'Ingresos - Deducciones' : 'Solo ingresos') : undefined}
/>
<KpiCard
title={regimenSeleccionado ? `ISR a Pagar (${regimenSeleccionado})` : 'ISR a Pagar'}
value={isrAPagarSel}
icon={<Receipt className="h-4 w-4" />}
trend={isrAPagarSel > 0 ? 'up' : 'neutral'}
/>
{(() => {
// Utilidad del Periodo: simétrica con la fórmula de base gravable.
// ingresoNeto = ingresos ncsEmitidas
// deducciónNeta = deducciones ncsRecibidas
// utilidad = ingresoNeto deducciónNeta
// = ingresos ncsEm ded + ncsRec
// Sin clamp a 0 — puede ser negativa (refleja pérdida operativa real).
const utilidad = ingSel - ncsEmSel - dedSel + ncsRecSel;
return (
<KpiCard
title={regimenSeleccionado ? `Utilidad del Periodo (${regimenSeleccionado})` : 'Utilidad del Periodo'}
value={utilidad}
icon={<Wallet className="h-4 w-4" />}
trend={utilidad > 0 ? 'up' : 'down'}
/>
);
})()}
<KpiCard
title={regimenSeleccionado ? `No Deducibles (${regimenSeleccionado})` : 'No Deducibles'}
value={noDedSel}
icon={<TrendingDown className="h-4 w-4" />}
subtitle="Efectivo > $2,000"
href={drillUrl('No Deducibles - Efectivo > $2,000', { bucket: 'no_deducibles_efectivo' })}
target="_blank"
rel="noopener noreferrer"
/>
</div>
);
})()}
{/* ISR Info + Coeficiente */}
{(() => {
// Regímenes PF no usan coeficiente de utilidad
const REGIMENES_PF = ['605', '606', '612', '621', '625'];
const isResicoPF = regimenSeleccionado === '626'; // RESICO PF also doesn't use coeficiente
const showCoeficiente = !regimenSeleccionado || (!REGIMENES_PF.includes(regimenSeleccionado) && !isResicoPF);
return (
<div className={`grid gap-4 ${showCoeficiente ? 'lg:grid-cols-3' : ''}`}>
<Card className={showCoeficiente ? 'lg:col-span-2' : ''}>
<CardHeader>
<CardTitle className="text-base">Cálculo de ISR del Periodo</CardTitle>
</CardHeader>
<CardContent>
{(() => {
const desglose = resumenIsrDesglose;
if (!desglose) {
return <div className="text-sm text-muted-foreground">Cargando</div>;
}
const { delPeriodo, anteriores, total, mesFinal, anio } = desglose;
const labelMesFinal = `${meses[mesFinal - 1]} ${anio}`;
const labelAnteriores =
mesFinal === 1
? '(sin meses anteriores)'
: mesFinal === 2
? `(${meses[0]})`
: `(${meses[0]}-${meses[mesFinal - 2]})`;
// Resolver per-régimen si hay régimen seleccionado, igual patrón que antes.
const ingPer = regimenSeleccionado
? delPeriodo.ingresosPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: delPeriodo.ingresosAcumulados || 0;
const ingAnt = regimenSeleccionado
? anteriores.ingresosPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: anteriores.ingresosAcumulados || 0;
const dedPer = regimenSeleccionado
? delPeriodo.deduccionesPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: delPeriodo.deducciones || 0;
const dedAnt = regimenSeleccionado
? anteriores.deduccionesPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: anteriores.deducciones || 0;
const ncsEmPer = regimenSeleccionado
? delPeriodo.ncsEmitidasPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: delPeriodo.ncsEmitidas || 0;
const ncsEmAnt = regimenSeleccionado
? anteriores.ncsEmitidasPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: anteriores.ncsEmitidas || 0;
const ncsRecPer = regimenSeleccionado
? delPeriodo.ncsRecibidasPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: delPeriodo.ncsRecibidas || 0;
const ncsRecAnt = regimenSeleccionado
? anteriores.ncsRecibidasPorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.monto || 0
: anteriores.ncsRecibidas || 0;
const bgTotal = regimenSeleccionado
? total.baseGravablePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.baseGravable || 0
: total.baseGravable || 0;
const causadoTotal = regimenSeleccionado
? total.baseGravablePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.isrCausado || 0
: total.isrCausado || 0;
const retenido = total.isrRetenido || 0;
const aPagar = Math.max(0, causadoTotal - (regimenSeleccionado ? 0 : retenido));
// Determina si las NCs aplican al cálculo de base gravable.
// Solo regímenes con formula='ingresos-deducciones' (606, 612,
// 626 RESICO PM) ajustan por NCs. Cuando no hay régimen
// seleccionado (vista agregada), mostramos si hay NCs > 0
// porque el total puede mezclar regímenes.
const formulaSel = regimenSeleccionado
? total.baseGravablePorRegimen?.find(r => r.regimenClave === regimenSeleccionado)?.formula
: null;
const showNcs = regimenSeleccionado
? formulaSel === 'ingresos-deducciones'
: (ncsEmPer + ncsEmAnt + ncsRecPer + ncsRecAnt) > 0;
return (
<div className="space-y-1">
{/* Bloque de Ingresos */}
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">Ingresos del periodo ({labelMesFinal})</span>
<span className="font-medium">{formatCurrency(ingPer)}</span>
</div>
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">(+) Ingresos acumulados anteriores {labelAnteriores}</span>
<span className="font-medium">{formatCurrency(ingAnt)}</span>
</div>
{showNcs && (
<>
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">() NCs Emitidas del periodo ({labelMesFinal})</span>
<span className="font-medium">{formatCurrency(ncsEmPer)}</span>
</div>
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">() NCs Emitidas acumuladas {labelAnteriores}</span>
<span className="font-medium">{formatCurrency(ncsEmAnt)}</span>
</div>
</>
)}
<div className="flex justify-between py-2 border-b bg-muted/30 px-2 rounded">
<span className="font-semibold">Total Ingresos</span>
<span className="font-semibold">{formatCurrency(ingPer + ingAnt - (showNcs ? (ncsEmPer + ncsEmAnt) : 0))}</span>
</div>
{/* Bloque de Deducciones */}
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">() Deducciones del periodo ({labelMesFinal})</span>
<span className="font-medium">{formatCurrency(dedPer)}</span>
</div>
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">() Deducciones acumuladas anteriores {labelAnteriores}</span>
<span className="font-medium">{formatCurrency(dedAnt)}</span>
</div>
{showNcs && (
<>
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">() NCs Recibidas del periodo ({labelMesFinal})</span>
<span className="font-medium">{formatCurrency(ncsRecPer)}</span>
</div>
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">() NCs Recibidas acumuladas {labelAnteriores}</span>
<span className="font-medium">{formatCurrency(ncsRecAnt)}</span>
</div>
</>
)}
<div className="flex justify-between py-2 border-b bg-muted/30 px-2 rounded">
<span className="font-semibold">Total Deducciones</span>
<span className="font-semibold">{formatCurrency(dedPer + dedAnt - (showNcs ? (ncsRecPer + ncsRecAnt) : 0))}</span>
</div>
<div className="flex justify-between py-2 border-b">
<span className="font-medium">(=) Base gravable acumulada</span>
<span className={cn('font-medium', bgTotal < 0 ? 'text-destructive' : '')}>
{formatCurrency(bgTotal)}
</span>
</div>
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">ISR causado (acumulado)</span>
<span className="font-medium">{formatCurrency(causadoTotal)}</span>
</div>
<div className="flex justify-between py-2 border-b">
<span className="text-muted-foreground">() ISR retenido (acumulado)</span>
<span className="font-medium">{formatCurrency(regimenSeleccionado ? 0 : retenido)}</span>
</div>
<div className="flex justify-between py-2 bg-muted/50 px-4 rounded-lg mt-2">
<span className="font-medium">ISR a pagar</span>
<span className="font-bold text-lg">{formatCurrency(aPagar)}</span>
</div>
</div>
);
})()}
</CardContent>
</Card>
{showCoeficiente && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Settings className="h-4 w-4" />
Coeficiente de Utilidad {año}
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Se utiliza para calcular el ISR de regimenes como General de Ley (601) y otros.
</p>
<div className="space-y-2">
<label className="text-sm font-medium">Coeficiente actual</label>
{coefData?.coeficiente !== null && coefData?.coeficiente !== undefined ? (
<p className="text-2xl font-bold">{coefData.coeficiente}</p>
) : (
<p className="text-sm text-destructive">No configurado</p>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Actualizar coeficiente</label>
<div className="flex gap-2">
<Input
type="number"
step="0.0001"
min="0"
max="1"
placeholder="Ej: 0.3521"
value={coefInput}
onChange={(e) => setCoefInput(e.target.value)}
className="h-9"
/>
<Button
size="sm"
disabled={!coefInput || savingCoef}
onClick={async () => {
const val = parseFloat(coefInput);
if (isNaN(val) || val < 0 || val > 1) return;
setSavingCoef(true);
try {
await setCoeficienteApi(año, val);
setCoefInput('');
queryClient.invalidateQueries({ queryKey: ['coeficiente'] });
queryClient.invalidateQueries({ queryKey: ['isr-resumen'] });
} catch {
alert('Error al guardar');
} finally {
setSavingCoef(false);
}
}}
>
{savingCoef ? 'Guardando...' : 'Guardar'}
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground">
Este valor se obtiene de la declaracion anual del ejercicio anterior. No se sobrescribe entre años.
</p>
</div>
</CardContent>
</Card>
)}
</div>
);
})()}
{/* Tabla 1: Histórico SIN NCs (datos brutos) */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">Histórico Ingresos y Deducciones sin NCs {año}</CardTitle>
{isrMensual && isrMensual.length > 0 && (
<Button variant="outline" size="sm" onClick={() => exportToExcel(
isrMensual.map(r => ({
Mes: meses[r.mes - 1],
Ingresos: r.ingresosAcumulados,
'Ingresos Acumulados': r.ingresosAcum,
Deducciones: r.deducciones,
'Deducciones Acumuladas': r.deduccionesAcum,
'Base Gravable Acumulada': r.baseGravableAcum,
})),
[
{ header: 'Mes', key: 'Mes', width: 12 },
{ header: 'Ingresos', key: 'Ingresos', width: 18 },
{ header: 'Ingresos Acumulados', key: 'Ingresos Acumulados', width: 22 },
{ header: 'Deducciones', key: 'Deducciones', width: 18 },
{ header: 'Deducciones Acumuladas', key: 'Deducciones Acumuladas', width: 22 },
{ header: 'Base Gravable Acumulada', key: 'Base Gravable Acumulada', width: 22 },
],
`isr-sin-ncs-${año}`,
)}>
<Download className="h-4 w-4 mr-1" /> Excel
</Button>
)}
</CardHeader>
<CardContent>
{isrLoading ? (
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b text-left text-sm text-muted-foreground">
<th className="pb-3 font-medium">Mes</th>
<th className="pb-3 font-medium text-right">Ingresos</th>
<th className="pb-3 font-medium text-right">Ingresos Acum.</th>
<th className="pb-3 font-medium text-right">Deducciones</th>
<th className="pb-3 font-medium text-right">Deducciones Acum.</th>
<th className="pb-3 font-medium text-right">Base Gravable Acum.</th>
</tr>
</thead>
<tbody className="text-sm">
{isrMensual?.map((row) => (
<tr key={row.mes} className="border-b hover:bg-muted/50">
<td className="py-3 font-medium">{meses[row.mes - 1]}</td>
<td className="py-3 text-right">{formatCurrency(row.ingresosAcumulados)}</td>
<td className="py-3 text-right">{formatCurrency(row.ingresosAcum)}</td>
<td className="py-3 text-right">{formatCurrency(row.deducciones)}</td>
<td className="py-3 text-right">{formatCurrency(row.deduccionesAcum)}</td>
<td className={cn(
'py-3 text-right font-medium',
row.baseGravableAcum < 0 ? 'text-destructive' : ''
)}>
{formatCurrency(row.baseGravableAcum)}
</td>
</tr>
))}
{(!isrMensual || isrMensual.length === 0) && (
<tr>
<td colSpan={6} className="py-8 text-center text-muted-foreground">
No hay registros de ISR para este año
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
{/* Tabla 2: Histórico CON NCs (valores netos: ing ncsEm, ded ncsRec) */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">Histórico Ingresos y Deducciones {año}</CardTitle>
{isrMensual && isrMensual.length > 0 && (
<Button variant="outline" size="sm" onClick={() => exportToExcel(
isrMensual.map(r => ({
Mes: meses[r.mes - 1],
Ingresos: r.ingresosAcumulados - (r.ncsEmitidas ?? 0),
'Ingresos Acumulados': r.ingresosAcum - (r.ncsEmitidasAcum ?? 0),
Deducciones: r.deducciones - (r.ncsRecibidas ?? 0),
'Deducciones Acumuladas': r.deduccionesAcum - (r.ncsRecibidasAcum ?? 0),
'Base Gravable Acumulada': (r.ingresosAcum - (r.ncsEmitidasAcum ?? 0)) - (r.deduccionesAcum - (r.ncsRecibidasAcum ?? 0)),
})),
[
{ header: 'Mes', key: 'Mes', width: 12 },
{ header: 'Ingresos', key: 'Ingresos', width: 18 },
{ header: 'Ingresos Acumulados', key: 'Ingresos Acumulados', width: 22 },
{ header: 'Deducciones', key: 'Deducciones', width: 18 },
{ header: 'Deducciones Acumuladas', key: 'Deducciones Acumuladas', width: 22 },
{ header: 'Base Gravable Acumulada', key: 'Base Gravable Acumulada', width: 22 },
],
`isr-con-ncs-${año}`,
)}>
<Download className="h-4 w-4 mr-1" /> Excel
</Button>
)}
</CardHeader>
<CardContent>
{isrLoading ? (
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b text-left text-sm text-muted-foreground">
<th className="pb-3 font-medium">Mes</th>
<th className="pb-3 font-medium text-right">Ingresos</th>
<th className="pb-3 font-medium text-right">Ingresos Acum.</th>
<th className="pb-3 font-medium text-right">Deducciones</th>
<th className="pb-3 font-medium text-right">Deducciones Acum.</th>
<th className="pb-3 font-medium text-right">Base Gravable Acum.</th>
</tr>
</thead>
<tbody className="text-sm">
{isrMensual?.map((row) => {
// `?? 0` defensivo: si el backend (en respuesta cacheada por react-query
// o por algún motivo de versión) no incluye estos campos, retornan 0
// en lugar de NaN al restar.
const ncsEm = row.ncsEmitidas ?? 0;
const ncsRec = row.ncsRecibidas ?? 0;
const ncsEmAcum = row.ncsEmitidasAcum ?? 0;
const ncsRecAcum = row.ncsRecibidasAcum ?? 0;
const ingNet = row.ingresosAcumulados - ncsEm;
const ingAcumNet = row.ingresosAcum - ncsEmAcum;
const dedNet = row.deducciones - ncsRec;
const dedAcumNet = row.deduccionesAcum - ncsRecAcum;
const baseNet = ingAcumNet - dedAcumNet;
return (
<tr key={row.mes} className="border-b hover:bg-muted/50">
<td className="py-3 font-medium">{meses[row.mes - 1]}</td>
<td className="py-3 text-right">{formatCurrency(ingNet)}</td>
<td className="py-3 text-right">{formatCurrency(ingAcumNet)}</td>
<td className="py-3 text-right">{formatCurrency(dedNet)}</td>
<td className="py-3 text-right">{formatCurrency(dedAcumNet)}</td>
<td className={cn(
'py-3 text-right font-medium',
baseNet < 0 ? 'text-destructive' : ''
)}>
{formatCurrency(baseNet)}
</td>
</tr>
);
})}
{(!isrMensual || isrMensual.length === 0) && (
<tr>
<td colSpan={6} className="py-8 text-center text-muted-foreground">
No hay registros de ISR para este año
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</>
)}
{activeTab === 'activos-fijos' && (
<ActivosFijosTab año={año} mes={mes} />
)}
<FiscalDisclaimer />
</main>
</>
);
}