feat(reportes): rediseño Estado de Resultados vertical con drill-down, análisis horizontal/vertical y export Excel
- Nuevo endpoint GET /reportes/estado-resultados-detallado con cálculo contable:
* Ventas, Devoluciones, Ventas netas, Costo de ventas, Utilidad bruta,
Gastos operativos, Utilidad de la operación
* Fórmula: subtotal_mxn - descuento_mxn (sin impuestos), nómina usa total_mxn
* Excluye anticipos (uso_cfdi=P01 o clave_prod_serv=84111506)
* Filtro por régimen fiscal opcional
* Año anterior calculado automáticamente
- Nuevo endpoint GET /reportes/estado-resultados/drill-down:
* Nivel 1: resumen agrupado por RFC
* Nivel 2: CFDIs individuales filtrados por categoría
* Categorías: ventas, devoluciones, costo-ventas, gastos-operativos
- Nuevo endpoint GET /reportes/estado-resultados/export:
* Genera Excel con formato condicional (verde/rojo, negritas)
- Frontend:
* Tabla vertical con % vertical, año anterior y variación %
* Filas clickeables para drill-down modal de 2 niveles
* Top 10 Clientes/Proveedores mantenidos debajo
* Selector de régimen conectado al reporte
- Fix: NaN en total de drill-down nivel 2 por numeric como string en pg
* Agregado ::float en queries SQL de CFDIs individuales
This commit is contained in:
@@ -4,11 +4,12 @@ import { useState } from 'react';
|
||||
import { DashboardShell } from '@/components/layouts/dashboard-shell';
|
||||
import { Card, CardContent, CardHeader, CardTitle, Tabs, TabsContent, TabsList, TabsTrigger } from '@horux/shared-ui';
|
||||
import { PeriodSelector, RegimenSelector, KpiCard } from '@horux/shared-ui';
|
||||
import { useEstadoResultados, useFlujoEfectivo, useComparativo, useConcentradoRfc, useCuentasXPagar, useCuentasXCobrar } from '@/lib/hooks/use-reportes';
|
||||
import { useEstadoResultadosDetallado, useFlujoEfectivo, useComparativo, useConcentradoRfc, useCuentasXPagar, useCuentasXCobrar } from '@/lib/hooks/use-reportes';
|
||||
import { EstadoResultadosTable } from './components/estado-resultados-table';
|
||||
import { useRegimenesDelPeriodo } from '@/lib/hooks/use-dashboard';
|
||||
import { BarChart } from '@/components/charts/bar-chart';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import { FileText, TrendingUp, TrendingDown, Users, CreditCard, Banknote } from 'lucide-react';
|
||||
import { FileText, Users, CreditCard, Banknote } from 'lucide-react';
|
||||
import { FiscalDisclaimer } from '@/components/fiscal-disclaimer';
|
||||
|
||||
export default function ReportesPage() {
|
||||
@@ -21,7 +22,7 @@ export default function ReportesPage() {
|
||||
const año = new Date(fechaInicio + 'T00:00:00').getFullYear();
|
||||
|
||||
const { data: regimenesPeriodo, isLoading: regimenesLoading } = useRegimenesDelPeriodo(fechaInicio, fechaFin);
|
||||
const { data: estadoResultados, isLoading: loadingER, error: errorER } = useEstadoResultados(fechaInicio, fechaFin);
|
||||
const { data: estadoResultadosDetallado, isLoading: loadingER, error: errorER } = useEstadoResultadosDetallado(fechaInicio, fechaFin, regimenSeleccionado || undefined);
|
||||
|
||||
const regimenesDisponibles = regimenesPeriodo || [];
|
||||
if (regimenSeleccionado && regimenesDisponibles.length > 0 &&
|
||||
@@ -70,85 +71,65 @@ export default function ReportesPage() {
|
||||
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
|
||||
) : errorER ? (
|
||||
<div className="text-center py-8 text-destructive">Error: {(errorER as Error).message}</div>
|
||||
) : !estadoResultados ? (
|
||||
) : !estadoResultadosDetallado ? (
|
||||
<div className="text-center py-8 text-muted-foreground">No hay datos disponibles para el período seleccionado</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Ingresos</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-success" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-success">
|
||||
{formatCurrency(estadoResultados.totalIngresos)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Egresos</CardTitle>
|
||||
<TrendingDown className="h-4 w-4 text-destructive" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-destructive">
|
||||
{formatCurrency(estadoResultados.totalEgresos)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Utilidad Bruta</CardTitle>
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${estadoResultados.utilidadBruta >= 0 ? 'text-success' : 'text-destructive'}`}>
|
||||
{formatCurrency(estadoResultados.utilidadBruta)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Utilidad Neta</CardTitle>
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${estadoResultados.utilidadNeta >= 0 ? 'text-success' : 'text-destructive'}`}>
|
||||
{formatCurrency(estadoResultados.utilidadNeta)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<EstadoResultadosTable
|
||||
data={estadoResultadosDetallado}
|
||||
fechaInicio={fechaInicio}
|
||||
fechaFin={fechaFin}
|
||||
regimen={regimenSeleccionado}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top 10 Ingresos por Cliente</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
Top 10 Clientes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{estadoResultados.ingresos.map((item, i) => (
|
||||
<div key={i} className="flex justify-between items-center py-2 border-b last:border-0">
|
||||
<span className="text-sm truncate max-w-[200px]">{item.concepto}</span>
|
||||
<span className="font-medium">{formatCurrency(item.monto)}</span>
|
||||
</div>
|
||||
))}
|
||||
{clientes && clientes.length > 0 ? (
|
||||
clientes.slice(0, 10).map((c, i) => (
|
||||
<div key={i} className="flex justify-between items-center py-2 border-b last:border-0">
|
||||
<div>
|
||||
<div className="font-medium text-sm">{c.nombre}</div>
|
||||
<div className="text-xs text-muted-foreground">{c.rfc} - {c.cantidadCfdis} CFDIs</div>
|
||||
</div>
|
||||
<span className="font-medium">{formatCurrency(c.totalFacturado)}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-4 text-muted-foreground text-sm">Sin clientes</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top 10 Egresos por Proveedor</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
Top 10 Proveedores
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{estadoResultados.egresos.map((item, i) => (
|
||||
<div key={i} className="flex justify-between items-center py-2 border-b last:border-0">
|
||||
<span className="text-sm truncate max-w-[200px]">{item.concepto}</span>
|
||||
<span className="font-medium">{formatCurrency(item.monto)}</span>
|
||||
</div>
|
||||
))}
|
||||
{proveedores && proveedores.length > 0 ? (
|
||||
proveedores.slice(0, 10).map((p, i) => (
|
||||
<div key={i} className="flex justify-between items-center py-2 border-b last:border-0">
|
||||
<div>
|
||||
<div className="font-medium text-sm">{p.nombre}</div>
|
||||
<div className="text-xs text-muted-foreground">{p.rfc} - {p.cantidadCfdis} CFDIs</div>
|
||||
</div>
|
||||
<span className="font-medium">{formatCurrency(p.totalFacturado)}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-4 text-muted-foreground text-sm">Sin proveedores</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user