feat: bulk XML upload, period selector, and session persistence
- Add bulk XML CFDI upload support (up to 300MB) - Add period selector component for month/year navigation - Fix session persistence on page refresh (Zustand hydration) - Fix income/expense classification based on tenant RFC - Fix IVA calculation from XML (correct Impuestos element) - Add error handling to reportes page - Support multiple CORS origins - Update reportes service with proper Decimal/BigInt handling - Add RFC to tenant view store for proper CFDI classification - Update README with changelog and new features Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,26 +4,33 @@ import { useState } from 'react';
|
||||
import { DashboardShell } from '@/components/layouts/dashboard-shell';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { PeriodSelector } from '@/components/period-selector';
|
||||
import { useEstadoResultados, useFlujoEfectivo, useComparativo, useConcentradoRfc } from '@/lib/hooks/use-reportes';
|
||||
import { BarChart } from '@/components/charts/bar-chart';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import { FileText, TrendingUp, TrendingDown, Users } from 'lucide-react';
|
||||
|
||||
export default function ReportesPage() {
|
||||
const [año] = useState(new Date().getFullYear());
|
||||
const [año, setAño] = useState(new Date().getFullYear());
|
||||
const fechaInicio = `${año}-01-01`;
|
||||
const fechaFin = `${año}-12-31`;
|
||||
|
||||
const { data: estadoResultados, isLoading: loadingER } = useEstadoResultados(fechaInicio, fechaFin);
|
||||
const { data: flujoEfectivo, isLoading: loadingFE } = useFlujoEfectivo(fechaInicio, fechaFin);
|
||||
const { data: comparativo, isLoading: loadingComp } = useComparativo(año);
|
||||
const { data: clientes } = useConcentradoRfc('cliente', fechaInicio, fechaFin);
|
||||
const { data: proveedores } = useConcentradoRfc('proveedor', fechaInicio, fechaFin);
|
||||
const { data: estadoResultados, isLoading: loadingER, error: errorER } = useEstadoResultados(fechaInicio, fechaFin);
|
||||
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);
|
||||
|
||||
return (
|
||||
<DashboardShell
|
||||
title="Reportes"
|
||||
description="Analisis financiero y reportes fiscales"
|
||||
headerContent={
|
||||
<PeriodSelector
|
||||
año={año}
|
||||
onAñoChange={setAño}
|
||||
showMonth={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Tabs defaultValue="estado-resultados" className="space-y-4">
|
||||
<TabsList>
|
||||
@@ -36,7 +43,11 @@ export default function ReportesPage() {
|
||||
<TabsContent value="estado-resultados" className="space-y-4">
|
||||
{loadingER ? (
|
||||
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
|
||||
) : estadoResultados ? (
|
||||
) : 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>
|
||||
@@ -118,13 +129,17 @@ export default function ReportesPage() {
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="flujo-efectivo" className="space-y-4">
|
||||
{loadingFE ? (
|
||||
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
|
||||
) : flujoEfectivo ? (
|
||||
) : 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>
|
||||
@@ -159,28 +174,26 @@ export default function ReportesPage() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Flujo de Efectivo Mensual</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BarChart
|
||||
data={flujoEfectivo.entradas.map((e, i) => ({
|
||||
mes: e.concepto,
|
||||
ingresos: e.monto,
|
||||
egresos: flujoEfectivo.salidas[i]?.monto || 0,
|
||||
}))}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<BarChart
|
||||
title="Flujo de Efectivo Mensual"
|
||||
data={flujoEfectivo.entradas.map((e, i) => ({
|
||||
mes: e.concepto,
|
||||
ingresos: e.monto,
|
||||
egresos: flujoEfectivo.salidas[i]?.monto || 0,
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="comparativo" className="space-y-4">
|
||||
{loadingComp ? (
|
||||
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
|
||||
) : comparativo ? (
|
||||
) : 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>
|
||||
@@ -213,69 +226,77 @@ export default function ReportesPage() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Comparativo Mensual {año}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BarChart
|
||||
data={comparativo.periodos.map((mes, i) => ({
|
||||
mes,
|
||||
ingresos: comparativo.ingresos[i],
|
||||
egresos: comparativo.egresos[i],
|
||||
}))}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<BarChart
|
||||
title={`Comparativo Mensual ${año}`}
|
||||
data={comparativo.periodos.map((mes, i) => ({
|
||||
mes,
|
||||
ingresos: comparativo.ingresos[i],
|
||||
egresos: comparativo.egresos[i],
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="concentrado" className="space-y-4">
|
||||
<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?.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>
|
||||
</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?.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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{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>
|
||||
</Tabs>
|
||||
</DashboardShell>
|
||||
|
||||
Reference in New Issue
Block a user