Initial commit: Horux Despachos project
This commit is contained in:
442
apps/web/app/(dashboard)/reportes/page.tsx
Normal file
442
apps/web/app/(dashboard)/reportes/page.tsx
Normal file
@@ -0,0 +1,442 @@
|
||||
'use client';
|
||||
|
||||
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 { 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 { FiscalDisclaimer } from '@/components/fiscal-disclaimer';
|
||||
|
||||
export default function ReportesPage() {
|
||||
const now = new Date();
|
||||
const [fechaInicio, setFechaInicio] = useState(`${now.getFullYear()}-01-01`);
|
||||
const [fechaFin, setFechaFin] = useState(`${now.getFullYear()}-12-31`);
|
||||
|
||||
const [regimenSeleccionado, setRegimenSeleccionado] = useState<string | null>(null);
|
||||
|
||||
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 regimenesDisponibles = regimenesPeriodo || [];
|
||||
if (regimenSeleccionado && regimenesDisponibles.length > 0 &&
|
||||
!regimenesDisponibles.find(r => r.clave === regimenSeleccionado)) {
|
||||
setRegimenSeleccionado(null);
|
||||
}
|
||||
const { data: flujoEfectivo, isLoading: loadingFE, error: errorFE } = useFlujoEfectivo(fechaInicio, fechaFin);
|
||||
const { data: comparativo, isLoading: loadingComp, error: errorComp } = useComparativo(año);
|
||||
const { data: clientes, error: errorClientes } = useConcentradoRfc('cliente', fechaInicio, fechaFin);
|
||||
const { data: proveedores, error: errorProveedores } = useConcentradoRfc('proveedor', fechaInicio, fechaFin);
|
||||
const { data: cxp, isLoading: loadingCXP } = useCuentasXPagar(fechaInicio, fechaFin, regimenSeleccionado || undefined);
|
||||
const { data: cxc, isLoading: loadingCXC } = useCuentasXCobrar(fechaInicio, fechaFin, regimenSeleccionado || undefined);
|
||||
|
||||
return (
|
||||
<DashboardShell
|
||||
title="Reportes"
|
||||
headerContent={
|
||||
<PeriodSelector
|
||||
fechaInicio={fechaInicio}
|
||||
fechaFin={fechaFin}
|
||||
onChange={(fi, ff) => { setFechaInicio(fi); setFechaFin(ff); }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<RegimenSelector
|
||||
regimenes={regimenesDisponibles}
|
||||
selected={regimenSeleccionado}
|
||||
onChange={setRegimenSeleccionado}
|
||||
isLoading={regimenesLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="estado-resultados" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="estado-resultados">Estado de Resultados</TabsTrigger>
|
||||
<TabsTrigger value="flujo-efectivo">Flujo de Efectivo</TabsTrigger>
|
||||
<TabsTrigger value="comparativo">Comparativo</TabsTrigger>
|
||||
<TabsTrigger value="concentrado">Concentrado RFC</TabsTrigger>
|
||||
<TabsTrigger value="cxp">Cuentas X Pagar</TabsTrigger>
|
||||
<TabsTrigger value="cxc">Cuentas X Cobrar</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="estado-resultados" className="space-y-4">
|
||||
{loadingER ? (
|
||||
<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 ? (
|
||||
<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>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top 10 Ingresos por Cliente</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>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top 10 Egresos por Proveedor</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>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="flujo-efectivo" className="space-y-4">
|
||||
{loadingFE ? (
|
||||
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
|
||||
) : errorFE ? (
|
||||
<div className="text-center py-8 text-destructive">Error: {(errorFE as Error).message}</div>
|
||||
) : !flujoEfectivo ? (
|
||||
<div className="text-center py-8 text-muted-foreground">No hay datos de flujo de efectivo</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Entradas</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-success">
|
||||
{formatCurrency(flujoEfectivo.totalEntradas)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Salidas</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-destructive">
|
||||
{formatCurrency(flujoEfectivo.totalSalidas)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Flujo Neto</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${flujoEfectivo.flujoNeto >= 0 ? 'text-success' : 'text-destructive'}`}>
|
||||
{formatCurrency(flujoEfectivo.flujoNeto)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<BarChart
|
||||
title="Flujo de Efectivo Mensual"
|
||||
data={flujoEfectivo.entradas.map((e, i) => ({
|
||||
mes: e.concepto,
|
||||
ingresos: e.monto,
|
||||
egresos: flujoEfectivo.salidas[i]?.monto || 0,
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="comparativo" className="space-y-4">
|
||||
{loadingComp ? (
|
||||
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
|
||||
) : errorComp ? (
|
||||
<div className="text-center py-8 text-destructive">Error: {(errorComp as Error).message}</div>
|
||||
) : !comparativo ? (
|
||||
<div className="text-center py-8 text-muted-foreground">No hay datos comparativos</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Var. Ingresos vs Año Anterior</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${comparativo.variacionIngresos >= 0 ? 'text-success' : 'text-destructive'}`}>
|
||||
{comparativo.variacionIngresos >= 0 ? '+' : ''}{comparativo.variacionIngresos.toFixed(1)}%
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Var. Egresos vs Año Anterior</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${comparativo.variacionEgresos <= 0 ? 'text-success' : 'text-destructive'}`}>
|
||||
{comparativo.variacionEgresos >= 0 ? '+' : ''}{comparativo.variacionEgresos.toFixed(1)}%
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Año Actual</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{año}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<BarChart
|
||||
title={`Comparativo Mensual ${año}`}
|
||||
data={comparativo.periodos.map((mes, i) => ({
|
||||
mes,
|
||||
ingresos: comparativo.ingresos[i],
|
||||
egresos: comparativo.egresos[i],
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="concentrado" className="space-y-4">
|
||||
{errorClientes || errorProveedores ? (
|
||||
<div className="text-center py-8 text-destructive">
|
||||
Error: {((errorClientes || errorProveedores) as Error).message}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
Clientes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{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 className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
Proveedores
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="cxp" className="space-y-4">
|
||||
{loadingCXP ? (
|
||||
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<KpiCard
|
||||
title="CFDIs con Saldo Pendiente"
|
||||
value={String(cxp?.cantidadCfdis || 0)}
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
/>
|
||||
<KpiCard
|
||||
title="Saldo Pendiente Total"
|
||||
value={cxp?.saldoTotal || 0}
|
||||
icon={<CreditCard className="h-4 w-4" />}
|
||||
trend={(cxp?.saldoTotal || 0) > 0 ? 'up' : 'neutral'}
|
||||
trendValue="Por pagar"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Saldo por Proveedor</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{cxp?.detalle.length === 0 ? (
|
||||
<p className="text-center py-8 text-muted-foreground">No hay cuentas por pagar en el periodo</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="pb-3 font-medium">RFC</th>
|
||||
<th className="pb-3 font-medium">Nombre</th>
|
||||
<th className="pb-3 font-medium text-right">CFDIs</th>
|
||||
<th className="pb-3 font-medium text-right">Saldo Pendiente</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cxp?.detalle.map((d) => (
|
||||
<tr key={d.rfc} className="border-b hover:bg-muted/50">
|
||||
<td className="py-3 font-mono text-xs">{d.rfc}</td>
|
||||
<td className="py-3">{d.nombre}</td>
|
||||
<td className="py-3 text-right">{d.cantidad}</td>
|
||||
<td className="py-3 text-right font-medium">{formatCurrency(d.saldo)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="cxc" className="space-y-4">
|
||||
{loadingCXC ? (
|
||||
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<KpiCard
|
||||
title="CFDIs con Saldo Pendiente"
|
||||
value={String(cxc?.cantidadCfdis || 0)}
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
/>
|
||||
<KpiCard
|
||||
title="Saldo Pendiente Total"
|
||||
value={cxc?.saldoTotal || 0}
|
||||
icon={<Banknote className="h-4 w-4" />}
|
||||
trend={(cxc?.saldoTotal || 0) > 0 ? 'up' : 'neutral'}
|
||||
trendValue="Por cobrar"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Saldo por Cliente</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{cxc?.detalle.length === 0 ? (
|
||||
<p className="text-center py-8 text-muted-foreground">No hay cuentas por cobrar en el periodo</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="pb-3 font-medium">RFC</th>
|
||||
<th className="pb-3 font-medium">Nombre</th>
|
||||
<th className="pb-3 font-medium text-right">CFDIs</th>
|
||||
<th className="pb-3 font-medium text-right">Saldo Pendiente</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cxc?.detalle.map((d) => (
|
||||
<tr key={d.rfc} className="border-b hover:bg-muted/50">
|
||||
<td className="py-3 font-mono text-xs">{d.rfc}</td>
|
||||
<td className="py-3">{d.nombre}</td>
|
||||
<td className="py-3 text-right">{d.cantidad}</td>
|
||||
<td className="py-3 text-right font-medium">{formatCurrency(d.saldo)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<FiscalDisclaimer />
|
||||
</DashboardShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user