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:
Horux Dev
2026-05-15 22:53:10 +00:00
parent 69bf7417a8
commit 7b1f60cbf2
10 changed files with 1160 additions and 66 deletions

View File

@@ -0,0 +1,223 @@
'use client';
import { useState, useMemo } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
Button,
} from '@horux/shared-ui';
import { useEstadoResultadosDrillDown } from '@/lib/hooks/use-reportes';
import { formatCurrency } from '@/lib/utils';
import { exportToExcel } from '@/lib/export-excel';
import { ArrowLeft, Download, Eye } from 'lucide-react';
import type { DrillDownResumenItem, DrillDownCfdiItem } from '@/lib/api/reportes';
interface Props {
categoria: string;
fechaInicio: string;
fechaFin: string;
regimen?: string | null;
onClose: () => void;
}
const CATEGORIA_TITULO: Record<string, string> = {
ventas: 'Ventas',
devoluciones: 'Devoluciones y cancelaciones',
'costo-ventas': 'Costo de ventas',
'gastos-operativos': 'Gastos operativos',
};
const RFC_COLUMNS = [
{ header: 'RFC', key: 'rfc', width: 15 },
{ header: 'Nombre', key: 'nombre', width: 35 },
{ header: 'CFDIs', key: 'cantidad', width: 10 },
{ header: 'Monto', key: '_monto', width: 20 },
];
const CFDI_COLUMNS = [
{ header: 'UUID', key: 'uuid', width: 40 },
{ header: 'Comp.', key: 'tipoComprobante', width: 10 },
{ header: 'Fecha', key: '_fecha', width: 15 },
{ header: 'RFC Emisor', key: 'rfcEmisor', width: 15 },
{ header: 'Nombre Emisor', key: 'nombreEmisor', width: 30 },
{ header: 'RFC Receptor', key: 'rfcReceptor', width: 15 },
{ header: 'Nombre Receptor', key: 'nombreReceptor', width: 30 },
{ header: 'Monto', key: '_monto', width: 15 },
];
function isResumen(data: unknown[]): data is DrillDownResumenItem[] {
return data.length > 0 && 'cantidad' in (data[0] as any);
}
function isCfdis(data: unknown[]): data is DrillDownCfdiItem[] {
return data.length > 0 && 'uuid' in (data[0] as any);
}
export function EstadoResultadosDrillDownModal({
categoria,
fechaInicio,
fechaFin,
regimen,
onClose,
}: Props) {
const [selectedRfc, setSelectedRfc] = useState<string | null>(null);
const [selectedNombre, setSelectedNombre] = useState<string>('');
const { data, isLoading } = useEstadoResultadosDrillDown(
categoria,
fechaInicio,
fechaFin,
regimen || undefined,
selectedRfc || undefined,
);
const resumen = useMemo(() => (data && isResumen(data) ? data : []), [data]);
const cfdis = useMemo(() => (data && isCfdis(data) ? data : []), [data]);
const totalMonto = useMemo(() => {
if (resumen.length > 0) return resumen.reduce((s, r) => s + Number(r.monto || 0), 0);
if (cfdis.length > 0) return cfdis.reduce((s, c) => s + Number(c.monto || 0), 0);
return 0;
}, [resumen, cfdis]);
const handleExport = () => {
if (resumen.length > 0) {
const rows = resumen.map((r) => ({
...r,
_monto: r.monto,
}));
exportToExcel(rows, RFC_COLUMNS, `drill-down-${categoria}-rfc`);
} else if (cfdis.length > 0) {
const rows = cfdis.map((c) => ({
...c,
_fecha: new Date(c.fechaEmision).toLocaleDateString('es-MX'),
_monto: c.monto,
}));
exportToExcel(rows, CFDI_COLUMNS, `drill-down-${categoria}-cfdis`);
}
};
const titulo = CATEGORIA_TITULO[categoria] || categoria;
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-center gap-2">
{selectedRfc && (
<Button variant="ghost" size="sm" onClick={() => setSelectedRfc(null)}>
<ArrowLeft className="h-4 w-4" />
</Button>
)}
<DialogTitle>
{selectedRfc ? `${titulo}${selectedNombre}` : titulo}
</DialogTitle>
</div>
</DialogHeader>
{isLoading ? (
<div className="text-center py-8 text-muted-foreground">Cargando...</div>
) : !data || data.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No hay datos para esta categoría
</div>
) : (
<>
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-muted-foreground">
{resumen.length > 0
? `${resumen.length} RFCs encontrados`
: `${cfdis.length} CFDIs encontrados`}
</p>
<div className="flex items-center gap-4 text-sm">
<span>
Total: <strong>{formatCurrency(totalMonto)}</strong>
</span>
<Button variant="outline" size="sm" onClick={handleExport}>
<Download className="h-4 w-4 mr-1" />
Excel
</Button>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
{resumen.length > 0 ? (
<>
<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">Monto</th>
<th className="pb-3 w-8"></th>
</>
) : (
<>
<th className="pb-3 font-medium">UUID</th>
<th className="pb-3 font-medium">Comp.</th>
<th className="pb-3 font-medium">Fecha</th>
<th className="pb-3 font-medium">RFC Emisor</th>
<th className="pb-3 font-medium">Nombre Emisor</th>
<th className="pb-3 font-medium">RFC Receptor</th>
<th className="pb-3 font-medium">Nombre Receptor</th>
<th className="pb-3 font-medium text-right">Monto</th>
</>
)}
</tr>
</thead>
<tbody>
{resumen.length > 0
? resumen.map((item) => (
<tr
key={item.rfc}
className="border-b hover:bg-muted/50 cursor-pointer"
onClick={() => {
setSelectedRfc(item.rfc);
setSelectedNombre(item.nombre);
}}
>
<td className="py-2 font-mono text-xs">{item.rfc}</td>
<td className="py-2 text-xs truncate max-w-[200px]">{item.nombre}</td>
<td className="py-2 text-right text-xs">{item.cantidad}</td>
<td className="py-2 text-right text-xs font-medium">
{formatCurrency(item.monto)}
</td>
<td className="py-2 text-right">
<Eye className="h-4 w-4 text-muted-foreground" />
</td>
</tr>
))
: cfdis.map((item) => (
<tr key={item.id} className="border-b hover:bg-muted/50">
<td className="py-2 font-mono text-xs" title={item.uuid}>
{item.uuid?.substring(0, 8)}
</td>
<td className="py-2 text-xs font-mono">{item.tipoComprobante}</td>
<td className="py-2 text-xs">
{new Date(item.fechaEmision).toLocaleDateString('es-MX')}
</td>
<td className="py-2 font-mono text-xs">{item.rfcEmisor}</td>
<td className="py-2 text-xs truncate max-w-[120px]">
{item.nombreEmisor}
</td>
<td className="py-2 font-mono text-xs">{item.rfcReceptor}</td>
<td className="py-2 text-xs truncate max-w-[120px]">
{item.nombreReceptor}
</td>
<td className="py-2 text-right text-xs font-medium">
{formatCurrency(item.monto)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,189 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, Button } from '@horux/shared-ui';
import { formatCurrency } from '@/lib/utils';
import { Download, ChevronRight } from 'lucide-react';
import type { EstadoResultadosDetallado } from '@horux/shared';
import { getExportEstadoResultadosUrl } from '@/lib/api/reportes';
import { EstadoResultadosDrillDownModal } from './drill-down-modal';
interface Props {
data: EstadoResultadosDetallado;
fechaInicio: string;
fechaFin: string;
regimen?: string | null;
}
type FilaConcepto = {
concepto: string;
monto: number;
vertical: number;
anterior: number;
variacion: number;
isTotal: boolean;
categoria?: string;
};
export function EstadoResultadosTable({ data, fechaInicio, fechaFin, regimen }: Props) {
const [drillDownCategoria, setDrillDownCategoria] = useState<string | null>(null);
const ventas = data.ventas || 1;
const filas: FilaConcepto[] = [
{
concepto: 'Ventas',
monto: data.ventas,
vertical: (data.ventas / ventas) * 100,
anterior: data.anterior.ventas,
variacion: data.anterior.ventas ? ((data.ventas - data.anterior.ventas) / data.anterior.ventas) * 100 : 0,
isTotal: false,
categoria: 'ventas',
},
{
concepto: 'Devoluciones y cancelaciones',
monto: -data.devoluciones,
vertical: -(data.devoluciones / ventas) * 100,
anterior: -data.anterior.devoluciones,
variacion: data.anterior.devoluciones
? ((-data.devoluciones + data.anterior.devoluciones) / data.anterior.devoluciones) * 100
: 0,
isTotal: false,
categoria: 'devoluciones',
},
{
concepto: 'Ventas netas',
monto: data.ventasNetas,
vertical: (data.ventasNetas / ventas) * 100,
anterior: data.anterior.ventasNetas,
variacion: data.anterior.ventasNetas
? ((data.ventasNetas - data.anterior.ventasNetas) / data.anterior.ventasNetas) * 100
: 0,
isTotal: true,
},
{
concepto: 'Costo de ventas',
monto: -data.costoVentas,
vertical: -(data.costoVentas / ventas) * 100,
anterior: -data.anterior.costoVentas,
variacion: data.anterior.costoVentas
? ((-data.costoVentas + data.anterior.costoVentas) / data.anterior.costoVentas) * 100
: 0,
isTotal: false,
categoria: 'costo-ventas',
},
{
concepto: 'Utilidad bruta',
monto: data.utilidadBruta,
vertical: (data.utilidadBruta / ventas) * 100,
anterior: data.anterior.utilidadBruta,
variacion: data.anterior.utilidadBruta
? ((data.utilidadBruta - data.anterior.utilidadBruta) / data.anterior.utilidadBruta) * 100
: 0,
isTotal: true,
},
{
concepto: 'Gastos operativos',
monto: -data.gastosOperativos,
vertical: -(data.gastosOperativos / ventas) * 100,
anterior: -data.anterior.gastosOperativos,
variacion: data.anterior.gastosOperativos
? ((-data.gastosOperativos + data.anterior.gastosOperativos) / data.anterior.gastosOperativos) * 100
: 0,
isTotal: false,
categoria: 'gastos-operativos',
},
{
concepto: 'Utilidad de la operación',
monto: data.utilidadOperacion,
vertical: (data.utilidadOperacion / ventas) * 100,
anterior: data.anterior.utilidadOperacion,
variacion: data.anterior.utilidadOperacion
? ((data.utilidadOperacion - data.anterior.utilidadOperacion) / data.anterior.utilidadOperacion) * 100
: 0,
isTotal: true,
},
];
const handleExport = () => {
const url = getExportEstadoResultadosUrl(fechaInicio, fechaFin, regimen || undefined);
window.open(url, '_blank');
};
return (
<>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Estado de Resultados</CardTitle>
<Button variant="outline" size="sm" onClick={handleExport}>
<Download className="h-4 w-4 mr-2" />
Exportar Excel
</Button>
</CardHeader>
<CardContent>
<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">Concepto</th>
<th className="pb-3 font-medium text-right">Monto</th>
<th className="pb-3 font-medium text-right">% Vertical</th>
<th className="pb-3 font-medium text-right">Año Anterior</th>
<th className="pb-3 font-medium text-right">Var. %</th>
<th className="pb-3 w-8"></th>
</tr>
</thead>
<tbody>
{filas.map((fila) => (
<tr
key={fila.concepto}
className={`border-b ${
fila.isTotal ? 'font-bold bg-muted/30' : 'hover:bg-muted/50'
} ${fila.categoria ? 'cursor-pointer' : ''}`}
onClick={() => fila.categoria && setDrillDownCategoria(fila.categoria)}
>
<td className="py-3">{fila.concepto}</td>
<td
className={`py-3 text-right ${
fila.monto >= 0 ? 'text-success' : 'text-destructive'
}`}
>
{formatCurrency(fila.monto)}
</td>
<td className="py-3 text-right text-muted-foreground">
{fila.vertical.toFixed(1)}%
</td>
<td className="py-3 text-right text-muted-foreground">
{formatCurrency(fila.anterior)}
</td>
<td className="py-3 text-right">
<span className={fila.variacion >= 0 ? 'text-success' : 'text-destructive'}>
{fila.variacion >= 0 ? '+' : ''}
{fila.variacion.toFixed(1)}%
</span>
</td>
<td className="py-3 text-right">
{fila.categoria && (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
{drillDownCategoria && (
<EstadoResultadosDrillDownModal
categoria={drillDownCategoria}
fechaInicio={fechaInicio}
fechaFin={fechaFin}
regimen={regimen}
onClose={() => setDrillDownCategoria(null)}
/>
)}
</>
);
}

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
import { apiClient } from './client';
import type { EstadoResultados, FlujoEfectivo, ComparativoPeriodos, ConcentradoRfc } from '@horux/shared';
import type { EstadoResultados, EstadoResultadosDetallado, FlujoEfectivo, ComparativoPeriodos, ConcentradoRfc } from '@horux/shared';
export async function getEstadoResultados(fechaInicio?: string, fechaFin?: string, contribuyenteId?: string): Promise<EstadoResultados> {
const params = new URLSearchParams();
@@ -10,6 +10,71 @@ export async function getEstadoResultados(fechaInicio?: string, fechaFin?: strin
return response.data;
}
export async function getEstadoResultadosDetallado(
fechaInicio?: string,
fechaFin?: string,
regimen?: string,
contribuyenteId?: string,
): Promise<EstadoResultadosDetallado> {
const params = new URLSearchParams();
if (fechaInicio) params.set('fechaInicio', fechaInicio);
if (fechaFin) params.set('fechaFin', fechaFin);
if (regimen) params.set('regimen', regimen);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<EstadoResultadosDetallado>(`/reportes/estado-resultados-detallado?${params}`);
return response.data;
}
export interface DrillDownResumenItem {
rfc: string;
nombre: string;
cantidad: number;
monto: number;
}
export interface DrillDownCfdiItem {
id: number;
uuid: string;
tipoComprobante: string;
fechaEmision: string;
rfcEmisor: string;
nombreEmisor: string;
rfcReceptor: string;
nombreReceptor: string;
monto: number;
metodoPago: string | null;
regimenFiscalEmisor: string | null;
regimenFiscalReceptor: string | null;
}
export async function getEstadoResultadosDrillDown(
categoria: string,
fechaInicio?: string,
fechaFin?: string,
regimen?: string,
rfc?: string,
contribuyenteId?: string,
): Promise<DrillDownResumenItem[] | DrillDownCfdiItem[]> {
const params = new URLSearchParams();
params.set('categoria', categoria);
if (fechaInicio) params.set('fechaInicio', fechaInicio);
if (fechaFin) params.set('fechaFin', fechaFin);
if (regimen) params.set('regimen', regimen);
if (rfc) params.set('rfc', rfc);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
const response = await apiClient.get<DrillDownResumenItem[] | DrillDownCfdiItem[]>(`/reportes/estado-resultados/drill-down?${params}`);
return response.data;
}
export function getExportEstadoResultadosUrl(fechaInicio?: string, fechaFin?: string, regimen?: string, contribuyenteId?: string): string {
const params = new URLSearchParams();
if (fechaInicio) params.set('fechaInicio', fechaInicio);
if (fechaFin) params.set('fechaFin', fechaFin);
if (regimen) params.set('regimen', regimen);
if (contribuyenteId) params.set('contribuyenteId', contribuyenteId);
return `/reportes/estado-resultados/export?${params}`;
}
export async function getFlujoEfectivo(fechaInicio?: string, fechaFin?: string, contribuyenteId?: string): Promise<FlujoEfectivo> {
const params = new URLSearchParams();
if (fechaInicio) params.set('fechaInicio', fechaInicio);

View File

@@ -57,3 +57,28 @@ export function useCuentasXCobrar(fechaInicio: string, fechaFin: string, regimen
enabled: !!fechaInicio && !!fechaFin,
});
}
export function useEstadoResultadosDetallado(fechaInicio?: string, fechaFin?: string, regimen?: string) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['estado-resultados-detallado', fechaInicio, fechaFin, regimen, selectedContribuyenteId],
queryFn: () => reportesApi.getEstadoResultadosDetallado(fechaInicio, fechaFin, regimen || undefined, selectedContribuyenteId || undefined),
});
}
export function useEstadoResultadosDrillDown(
categoria: string,
fechaInicio?: string,
fechaFin?: string,
regimen?: string,
rfc?: string,
) {
const { selectedContribuyenteId } = useContribuyenteStore();
return useQuery({
queryKey: ['estado-resultados-drill-down', categoria, fechaInicio, fechaFin, regimen, rfc, selectedContribuyenteId],
queryFn: () => reportesApi.getEstadoResultadosDrillDown(categoria, fechaInicio, fechaFin, regimen || undefined, rfc || undefined, selectedContribuyenteId || undefined),
enabled: !!categoria && !!fechaInicio && !!fechaFin,
});
}