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

@@ -337,3 +337,56 @@ El usuario esperaba verlo en abril porque el pago ocurrió en abril, pero el sis
- El endpoint `POST /emitir-factura-pago/:paymentId` requiere rol `platform_admin`
- La regla "primer pago no se factura automáticamente" sigue vigente; los subsecuentes sí son automáticos
- Los CFDIs importados por SAT sync ahora se asocian correctamente al `contribuyente_id` correspondiente
---
## 12. Rediseño del Estado de Resultados en `/reportes`
**Fecha:** 4 de mayo de 2026
### Problema
El tab "Estado de Resultados" mostraba solo 4 KPI cards y dos listas con títulos engañosos (decían "Cliente/Proveedor" pero mostraban regímenes fiscales). No había análisis horizontal, vertical, ni drill-down.
### Solución
Se reemplazó por un estado de resultados vertical contable con 7 líneas, análisis comparativo vs año anterior, análisis vertical (% de ventas), drill-down por RFC → CFDI, exportación a Excel y filtro por régimen fiscal.
### Backend (`apps/api/`)
**Nuevos archivos/modificaciones:**
| Archivo | Cambio |
|---|---|
| `src/services/reportes.service.ts` | **Nuevas funciones:** `getEstadoResultadosDetallado`, `getEstadoResultadosDrillDown`, `exportEstadoResultadosToExcel` |
| `src/controllers/reportes.controller.ts` | **3 nuevos handlers:** `getEstadoResultadosDetallado`, `getEstadoResultadosDrillDown`, `exportEstadoResultados` |
| `src/routes/reportes.routes.ts` | Registradas 3 rutas nuevas |
| `packages/shared/src/types/reportes.ts` | **Nuevo tipo:** `EstadoResultadosDetallado` |
**Endpoints nuevos:**
- `GET /reportes/estado-resultados-detallado` — Tabla vertical con año anterior
- `GET /reportes/estado-resultados/drill-down?categoria=X&rfc=Y` — Resumen por RFC o CFDIs individuales
- `GET /reportes/estado-resultados/export` — Descarga Excel con formato condicional
**Lógica de cálculo:**
| Línea | Fórmula | Filtros |
|---|---|---|
| Ventas | `subtotal_mxn - descuento_mxn` | Emitidas tipo I, PUE/PPD, vigentes, excluyendo anticipos (`uso_cfdi != 'P01'` ni concepto `84111506`) |
| Devoluciones | `subtotal_mxn - descuento_mxn` | Emitidas tipo E, relación `01` o `03`, vigentes |
| Costo de ventas | `subtotal_mxn - descuento_mxn` | Recibidas tipo I, PUE/PPD, `uso_cfdi = 'G01'`, vigentes |
| Gastos operativos | `subtotal_mxn - descuento_mxn` (recibidos) + `total_mxn` (nómina) | Recibidas tipo I excluyendo G01 + Emitidas tipo N, vigentes |
| Totales | Calculados | Ventas netas, Utilidad bruta, Utilidad de la operación |
### Frontend (`apps/web/`)
| Archivo | Cambio |
|---|---|
| `app/(dashboard)/reportes/components/estado-resultados-table.tsx` | **Nuevo** — Tabla vertical con concepto, monto, % vertical, año anterior, variación % |
| `app/(dashboard)/reportes/components/drill-down-modal.tsx` | **Nuevo** — Modal de dos niveles: RFC resumen → CFDIs individuales |
| `lib/api/reportes.ts` | Agregados wrappers para los 3 endpoints nuevos |
| `lib/hooks/use-reportes.ts` | Agregados `useEstadoResultadosDetallado` y `useEstadoResultadosDrillDown` |
| `app/(dashboard)/reportes/page.tsx` | Integrada tabla nueva; conectado `RegimenSelector` al reporte; mantenidos Top 10 Clientes/Proveedores debajo |
### Fix posterior: Total NaN en drill-down nivel 2
**Causa:** PostgreSQL devolvía `numeric` como string en el driver `pg`. Al sumar strings en el `reduce` del frontend, JavaScript concatenaba en lugar de sumar, generando `NaN` al formatear.
**Fix:** Se agregó `::float` en las 5 queries SQL de CFDIs individuales del drill-down, forzando que el backend devuelva números reales.