feat: restaurar panorama facturador con nueva arquitectura basada en Metabase
- Crear endpoint /api/metabase/panorama.post.ts que ejecuta las 9 queries en paralelo - Restaurar y adaptar panorama.vue para usar el nuevo endpoint - Crear componentes auxiliares: SecosVendidos, TotalesIngresoCompra, TotalesMonetarios, TotalesVerde, MetricBox, RechazosRechazoCard - Adaptar RechazosSubproductos para recibir data directamente de Metabase - Toda la transformación de datos ocurre en las queries SQL de Metabase - Sin uso de stores ni composables de métricas - Agregar documentación de queries en archivos MD
This commit is contained in:
686
METABASE_QUERIES_INFORME_INGRESOS.md
Normal file
686
METABASE_QUERIES_INFORME_INGRESOS.md
Normal file
@@ -0,0 +1,686 @@
|
||||
# 📊 QUERIES DE METABASE PARA INFORME INGRESOS
|
||||
|
||||
## 🎯 FILOSOFÍA
|
||||
|
||||
**Metabase calcula TODO. Vue solo renderiza.**
|
||||
|
||||
Cada query devuelve exactamente los valores que un componente necesita mostrar, ya calculados y listos para usar.
|
||||
|
||||
---
|
||||
|
||||
## 📋 PARÁMETROS GLOBALES
|
||||
|
||||
Todas las queries aceptan estos parámetros (con soporte para múltiples valores):
|
||||
|
||||
| Parámetro | Tipo | Default | Descripción |
|
||||
|-----------|------|---------|-------------|
|
||||
| `fecha_desde` | Date | `null` | Fecha inicio (filtra por `created_at`) |
|
||||
| `fecha_hasta` | Date | `null` | Fecha fin (filtra por `created_at`) |
|
||||
| `incluir_anulados` | Boolean | `false` | Si `false`, excluye `estado='anulado'` o `fecha_anulado IS NOT NULL` |
|
||||
| `cliente_ids` | Array[Number] | `[]` | Array de IDs de clientes. Si vacío, incluye todos |
|
||||
| `tipos` | Array[String] | `[]` | Array de tipos: 'uva', 'mojado', 'oreado', 'verde'. Si vacío, todos |
|
||||
| `estados` | Array[String] | `[]` | Array de estados: 'pagado', 'pendiente'. Si vacío, todos |
|
||||
| `ubicaciones` | Array[String] | `[]` | Array de ubicaciones de clientes. Si vacío, todas |
|
||||
| `calidades` | Array[String] | `[]` | Array de calidades de café. Si vacío, todas |
|
||||
|
||||
**Condición SQL estándar:**
|
||||
```sql
|
||||
WHERE
|
||||
-- Filtro de anulados
|
||||
({{incluir_anulados}} OR (i.estado != 'anulado' AND i.fecha_anulado IS NULL))
|
||||
|
||||
-- Filtro de fechas
|
||||
AND ({{fecha_desde}} IS NULL OR i.created_at >= {{fecha_desde}})
|
||||
AND ({{fecha_hasta}} IS NULL OR i.created_at <= {{fecha_hasta}})
|
||||
|
||||
-- Filtro de clientes (si se proporciona)
|
||||
AND (CARDINALITY({{cliente_ids}}) = 0 OR i.cliente_id = ANY({{cliente_ids}}))
|
||||
|
||||
-- Filtro de tipos (si se proporciona)
|
||||
AND (CARDINALITY({{tipos}}) = 0 OR i.tipo = ANY({{tipos}}))
|
||||
|
||||
-- Filtro de estados (si se proporciona)
|
||||
AND (CARDINALITY({{estados}}) = 0 OR i.estado = ANY({{estados}}))
|
||||
|
||||
-- Filtro de ubicaciones vía join con clientes (si se proporciona)
|
||||
AND (CARDINALITY({{ubicaciones}}) = 0 OR c.ubicacion = ANY({{ubicaciones}}))
|
||||
|
||||
-- Filtro de calidades (si se proporciona)
|
||||
AND (CARDINALITY({{calidades}}) = 0 OR i.calidad = ANY({{calidades}}))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔁 QUERIES REUTILIZADAS DE PANORAMA
|
||||
|
||||
Las siguientes queries son **idénticas** a las de panorama, solo cambia que aceptan más parámetros de filtrado:
|
||||
|
||||
### Query 1: `informe_totales_ingreso_compra`
|
||||
**Reutiliza:** `panorama_totales_ingreso_compra`
|
||||
**Componente:** `TotalesIngresoCompra.vue`
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
-- === TOTALES GENERALES (Pagado + Pendiente) ===
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'uva' THEN i.peso_neto ELSE 0 END), 0) as total_lb_uva_ingresada,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'uva' THEN i.peso_seco ELSE 0 END), 0) as total_qq_seco_uva_ingresado,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'mojado' THEN i.peso_seco ELSE 0 END), 0) as total_qq_seco_mojado_ingresado,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'oreado' THEN i.peso_seco ELSE 0 END), 0) as total_qq_seco_oreado_ingresado,
|
||||
COALESCE(SUM(i.peso_seco), 0) as total_qq_seco_ingresado,
|
||||
|
||||
-- === SOLO PAGADOS ===
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'uva' AND i.estado = 'pagado' THEN i.peso_neto ELSE 0 END), 0) as total_lb_uva_pagada,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'uva' AND i.estado = 'pagado' THEN i.peso_seco ELSE 0 END), 0) as total_qq_seco_uva_pagado,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'mojado' AND i.estado = 'pagado' THEN i.peso_seco ELSE 0 END), 0) as total_qq_seco_mojado_pagado,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'oreado' AND i.estado = 'pagado' THEN i.peso_seco ELSE 0 END), 0) as total_qq_seco_oreado_pagado,
|
||||
COALESCE(SUM(CASE WHEN i.estado = 'pagado' THEN i.peso_seco ELSE 0 END), 0) as total_qq_seco_comprado,
|
||||
|
||||
-- === INVENTARIO EN DEPÓSITO (Pendiente) ===
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'uva' AND i.estado = 'pendiente' THEN i.peso_neto ELSE 0 END), 0) as total_lb_uva_deposito,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'mojado' AND i.estado = 'pendiente' THEN i.peso_seco ELSE 0 END), 0) as total_qq_mojado_deposito,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'oreado' AND i.estado = 'pendiente' THEN i.peso_seco ELSE 0 END), 0) as total_qq_oreado_deposito,
|
||||
COALESCE(SUM(CASE WHEN i.estado = 'pendiente' THEN i.peso_seco ELSE 0 END), 0) as total_qq_seco_deposito
|
||||
|
||||
FROM vista_detalle_ingresos i
|
||||
LEFT JOIN clientes c ON i.cliente_id = c.id
|
||||
WHERE
|
||||
({{incluir_anulados}} OR (i.estado != 'anulado' AND i.fecha_anulado IS NULL))
|
||||
AND ({{fecha_desde}} IS NULL OR i.created_at >= {{fecha_desde}})
|
||||
AND ({{fecha_hasta}} IS NULL OR i.created_at <= {{fecha_hasta}})
|
||||
AND (CARDINALITY({{cliente_ids}}) = 0 OR i.cliente_id = ANY({{cliente_ids}}))
|
||||
AND (CARDINALITY({{tipos}}) = 0 OR i.tipo = ANY({{tipos}}))
|
||||
AND (CARDINALITY({{estados}}) = 0 OR i.estado = ANY({{estados}}))
|
||||
AND (CARDINALITY({{ubicaciones}}) = 0 OR c.ubicacion = ANY({{ubicaciones}}))
|
||||
AND (CARDINALITY({{calidades}}) = 0 OR i.calidad = ANY({{calidades}}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Query 2: `informe_totales_monetarios`
|
||||
**Reutiliza:** `panorama_totales_monetarios`
|
||||
**Componente:** `TotalesMonetarios.vue`
|
||||
|
||||
*(SQL idéntico a Query 3 de panorama, pero con los filtros adicionales como en Query 1)*
|
||||
|
||||
---
|
||||
|
||||
### Query 3: `informe_totales_verde`
|
||||
**Reutiliza:** `panorama_totales_verde`
|
||||
**Componente:** `TotalesVerde.vue`
|
||||
|
||||
*(SQL idéntico a Query 4 de panorama, pero con los filtros adicionales)*
|
||||
|
||||
---
|
||||
|
||||
## 📊 QUERIES NUEVAS PARA INFORME INGRESOS
|
||||
|
||||
### Query 4: `informe_lista_ingresos`
|
||||
**Componente:** `IngresosVistaTablaIngresos.vue`
|
||||
**Devuelve:** Múltiples filas (todos los ingresos filtrados con info del cliente)
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
i.id,
|
||||
i.cliente_id,
|
||||
i.created_at,
|
||||
i.fecha,
|
||||
i.tipo,
|
||||
i.estado,
|
||||
i.peso_neto,
|
||||
i.peso_seco,
|
||||
i.precio,
|
||||
i.calidad,
|
||||
i.observaciones,
|
||||
i.fecha_anulado,
|
||||
|
||||
-- Calcular total a pagar
|
||||
CASE
|
||||
WHEN i.tipo IN ('verde', 'uva') THEN i.precio * i.peso_neto
|
||||
WHEN i.tipo IN ('oreado', 'mojado') THEN (i.precio / 2) * i.peso_seco
|
||||
ELSE 0
|
||||
END as total_a_pagar,
|
||||
|
||||
-- Datos del cliente (para mostrar en la tabla)
|
||||
c.name as cliente_nombre,
|
||||
c.cedula as cliente_cedula,
|
||||
c.ubicacion as cliente_ubicacion,
|
||||
c.telefono as cliente_telefono
|
||||
|
||||
FROM vista_detalle_ingresos i
|
||||
LEFT JOIN clientes c ON i.cliente_id = c.id
|
||||
WHERE
|
||||
({{incluir_anulados}} OR (i.estado != 'anulado' AND i.fecha_anulado IS NULL))
|
||||
AND ({{fecha_desde}} IS NULL OR i.created_at >= {{fecha_desde}})
|
||||
AND ({{fecha_hasta}} IS NULL OR i.created_at <= {{fecha_hasta}})
|
||||
AND (CARDINALITY({{cliente_ids}}) = 0 OR i.cliente_id = ANY({{cliente_ids}}))
|
||||
AND (CARDINALITY({{tipos}}) = 0 OR i.tipo = ANY({{tipos}}))
|
||||
AND (CARDINALITY({{estados}}) = 0 OR i.estado = ANY({{estados}}))
|
||||
AND (CARDINALITY({{ubicaciones}}) = 0 OR c.ubicacion = ANY({{ubicaciones}}))
|
||||
AND (CARDINALITY({{calidades}}) = 0 OR i.calidad = ANY({{calidades}}))
|
||||
ORDER BY i.created_at DESC, i.id DESC
|
||||
LIMIT 1000;
|
||||
```
|
||||
|
||||
**Respuesta esperada:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1234,
|
||||
"cliente_id": 42,
|
||||
"created_at": "2025-01-15T10:30:00Z",
|
||||
"fecha": "2025-01-15",
|
||||
"tipo": "uva",
|
||||
"estado": "pagado",
|
||||
"peso_neto": 2550.00,
|
||||
"peso_seco": 25.50,
|
||||
"precio": 4.50,
|
||||
"calidad": "primera",
|
||||
"observaciones": null,
|
||||
"fecha_anulado": null,
|
||||
"total_a_pagar": 11475.00,
|
||||
"cliente_nombre": "Juan Pérez",
|
||||
"cliente_cedula": "0801199012345",
|
||||
"cliente_ubicacion": "El Rosario",
|
||||
"cliente_telefono": "99887766"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Query 5: `informe_lista_clientes_con_totales`
|
||||
**Componente:** `ClientesVistaTablaClientes.vue`, `IngresosTopClientes.vue`
|
||||
**Devuelve:** Múltiples filas (un cliente por fila con sus totales agregados)
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
c.id as cliente_id,
|
||||
c.name as cliente_nombre,
|
||||
c.cedula as cliente_cedula,
|
||||
c.ubicacion as cliente_ubicacion,
|
||||
c.telefono as cliente_telefono,
|
||||
c.avatar_url as cliente_avatar,
|
||||
c.grupo_estudio as cliente_grupo_estudio,
|
||||
|
||||
-- === TOTALES POR CLIENTE ===
|
||||
|
||||
-- Contadores
|
||||
COUNT(i.id) as num_ingresos,
|
||||
|
||||
-- Totales de peso
|
||||
COALESCE(SUM(i.peso_seco), 0) as total_qq_seco,
|
||||
COALESCE(SUM(i.peso_neto), 0) as total_lb_neto,
|
||||
|
||||
-- Totales por tipo
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'uva' THEN i.peso_seco ELSE 0 END), 0) as total_qq_uva,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'mojado' THEN i.peso_seco ELSE 0 END), 0) as total_qq_mojado,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'oreado' THEN i.peso_seco ELSE 0 END), 0) as total_qq_oreado,
|
||||
COALESCE(SUM(CASE WHEN i.tipo = 'verde' THEN i.peso_neto ELSE 0 END), 0) as total_lb_verde,
|
||||
|
||||
-- Total pagado (inversión)
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN i.estado = 'pagado' AND i.tipo IN ('verde', 'uva') THEN i.precio * i.peso_neto
|
||||
WHEN i.estado = 'pagado' AND i.tipo IN ('oreado', 'mojado') THEN (i.precio / 2) * i.peso_seco
|
||||
ELSE 0
|
||||
END
|
||||
), 0) as total_pagado,
|
||||
|
||||
-- Total pendiente de pagar
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN i.estado = 'pendiente' AND i.tipo IN ('verde', 'uva') THEN i.precio * i.peso_neto
|
||||
WHEN i.estado = 'pendiente' AND i.tipo IN ('oreado', 'mojado') THEN (i.precio / 2) * i.peso_seco
|
||||
ELSE 0
|
||||
END
|
||||
), 0) as total_pendiente,
|
||||
|
||||
-- Precio promedio ponderado por qq
|
||||
CASE
|
||||
WHEN SUM(CASE WHEN i.estado = 'pagado' THEN i.peso_seco ELSE 0 END) > 0
|
||||
THEN SUM(
|
||||
CASE
|
||||
WHEN i.estado = 'pagado' AND i.tipo IN ('verde', 'uva') THEN i.precio * i.peso_neto
|
||||
WHEN i.estado = 'pagado' AND i.tipo IN ('oreado', 'mojado') THEN (i.precio / 2) * i.peso_seco
|
||||
ELSE 0
|
||||
END
|
||||
) / SUM(CASE WHEN i.estado = 'pagado' THEN i.peso_seco ELSE 0 END)
|
||||
ELSE 0
|
||||
END as precio_promedio_qq,
|
||||
|
||||
-- Fecha primer ingreso
|
||||
MIN(i.created_at) as primer_ingreso,
|
||||
|
||||
-- Fecha último ingreso
|
||||
MAX(i.created_at) as ultimo_ingreso
|
||||
|
||||
FROM clientes c
|
||||
LEFT JOIN vista_detalle_ingresos i ON c.id = i.cliente_id
|
||||
AND ({{incluir_anulados}} OR (i.estado != 'anulado' AND i.fecha_anulado IS NULL))
|
||||
AND ({{fecha_desde}} IS NULL OR i.created_at >= {{fecha_desde}})
|
||||
AND ({{fecha_hasta}} IS NULL OR i.created_at <= {{fecha_hasta}})
|
||||
AND (CARDINALITY({{tipos}}) = 0 OR i.tipo = ANY({{tipos}}))
|
||||
AND (CARDINALITY({{estados}}) = 0 OR i.estado = ANY({{estados}}))
|
||||
AND (CARDINALITY({{calidades}}) = 0 OR i.calidad = ANY({{calidades}}))
|
||||
|
||||
WHERE
|
||||
-- Filtro de clientes seleccionados
|
||||
(CARDINALITY({{cliente_ids}}) = 0 OR c.id = ANY({{cliente_ids}}))
|
||||
|
||||
-- Filtro de ubicaciones de clientes
|
||||
AND (CARDINALITY({{ubicaciones}}) = 0 OR c.ubicacion = ANY({{ubicaciones}}))
|
||||
|
||||
GROUP BY c.id, c.name, c.cedula, c.ubicacion, c.telefono, c.avatar_url, c.grupo_estudio
|
||||
HAVING COUNT(i.id) > 0
|
||||
ORDER BY total_pagado DESC;
|
||||
```
|
||||
|
||||
**Respuesta esperada:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"cliente_id": 42,
|
||||
"cliente_nombre": "Juan Pérez",
|
||||
"cliente_cedula": "0801199012345",
|
||||
"cliente_ubicacion": "El Rosario",
|
||||
"cliente_telefono": "99887766",
|
||||
"cliente_avatar": null,
|
||||
"cliente_grupo_estudio": null,
|
||||
"num_ingresos": 45,
|
||||
"total_qq_seco": 450.00,
|
||||
"total_lb_neto": 45000.00,
|
||||
"total_qq_uva": 200.00,
|
||||
"total_qq_mojado": 150.00,
|
||||
"total_qq_oreado": 100.00,
|
||||
"total_lb_verde": 5000.00,
|
||||
"total_pagado": 1234567.89,
|
||||
"total_pendiente": 50000.00,
|
||||
"precio_promedio_qq": 2743.71,
|
||||
"primer_ingreso": "2024-11-01T08:00:00Z",
|
||||
"ultimo_ingreso": "2025-01-15T14:30:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Query 6: `informe_serie_temporal_acumulada`
|
||||
**Componente:** `GraficaAcumuladoresUva.vue`, `GraficaDinamicaPagadoDeposito.vue`
|
||||
**Devuelve:** Múltiples filas (agrupadas por fecha y tipo, con acumulación)
|
||||
**Parámetros adicionales:** `granularidad` (valores: 'dia', 'semana', 'mes')
|
||||
|
||||
```sql
|
||||
WITH datos_agrupados AS (
|
||||
SELECT
|
||||
-- Agrupar por granularidad
|
||||
CASE
|
||||
WHEN {{granularidad}} = 'dia' THEN DATE(i.created_at)
|
||||
WHEN {{granularidad}} = 'semana' THEN DATE_TRUNC('week', i.created_at)::date
|
||||
WHEN {{granularidad}} = 'mes' THEN DATE_TRUNC('month', i.created_at)::date
|
||||
ELSE DATE(i.created_at)
|
||||
END as fecha_grupo,
|
||||
|
||||
i.tipo,
|
||||
i.estado,
|
||||
|
||||
-- Totales del período
|
||||
SUM(i.peso_seco) as peso_seco_periodo,
|
||||
SUM(i.peso_neto) as peso_neto_periodo,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN i.tipo IN ('verde', 'uva') THEN i.precio * i.peso_neto
|
||||
WHEN i.tipo IN ('oreado', 'mojado') THEN (i.precio / 2) * i.peso_seco
|
||||
ELSE 0
|
||||
END
|
||||
) as inversion_periodo,
|
||||
COUNT(*) as num_ingresos_periodo
|
||||
|
||||
FROM vista_detalle_ingresos i
|
||||
LEFT JOIN clientes c ON i.cliente_id = c.id
|
||||
WHERE
|
||||
({{incluir_anulados}} OR (i.estado != 'anulado' AND i.fecha_anulado IS NULL))
|
||||
AND ({{fecha_desde}} IS NULL OR i.created_at >= {{fecha_desde}})
|
||||
AND ({{fecha_hasta}} IS NULL OR i.created_at <= {{fecha_hasta}})
|
||||
AND (CARDINALITY({{cliente_ids}}) = 0 OR i.cliente_id = ANY({{cliente_ids}}))
|
||||
AND (CARDINALITY({{tipos}}) = 0 OR i.tipo = ANY({{tipos}}))
|
||||
AND (CARDINALITY({{estados}}) = 0 OR i.estado = ANY({{estados}}))
|
||||
AND (CARDINALITY({{ubicaciones}}) = 0 OR c.ubicacion = ANY({{ubicaciones}}))
|
||||
AND (CARDINALITY({{calidades}}) = 0 OR i.calidad = ANY({{calidades}}))
|
||||
GROUP BY fecha_grupo, i.tipo, i.estado
|
||||
)
|
||||
SELECT
|
||||
fecha_grupo,
|
||||
tipo,
|
||||
estado,
|
||||
peso_seco_periodo,
|
||||
peso_neto_periodo,
|
||||
inversion_periodo,
|
||||
num_ingresos_periodo,
|
||||
|
||||
-- Acumulados hasta esta fecha (por tipo y estado)
|
||||
SUM(peso_seco_periodo) OVER (
|
||||
PARTITION BY tipo, estado
|
||||
ORDER BY fecha_grupo
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) as peso_seco_acumulado,
|
||||
|
||||
SUM(peso_neto_periodo) OVER (
|
||||
PARTITION BY tipo, estado
|
||||
ORDER BY fecha_grupo
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) as peso_neto_acumulado,
|
||||
|
||||
SUM(inversion_periodo) OVER (
|
||||
PARTITION BY tipo, estado
|
||||
ORDER BY fecha_grupo
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) as inversion_acumulada
|
||||
|
||||
FROM datos_agrupados
|
||||
ORDER BY fecha_grupo, tipo, estado;
|
||||
```
|
||||
|
||||
**Respuesta esperada:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"fecha_grupo": "2025-01-15",
|
||||
"tipo": "uva",
|
||||
"estado": "pagado",
|
||||
"peso_seco_periodo": 25.50,
|
||||
"peso_neto_periodo": 2550.00,
|
||||
"inversion_periodo": 11475.00,
|
||||
"num_ingresos_periodo": 5,
|
||||
"peso_seco_acumulado": 450.00,
|
||||
"peso_neto_acumulado": 45000.00,
|
||||
"inversion_acumulada": 202500.00
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Query 7: `informe_opciones_filtros`
|
||||
**Propósito:** Obtener listas de valores únicos para los filtros dinámicos
|
||||
**Devuelve:** 1 objeto con arrays de opciones
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
-- Ubicaciones únicas de clientes que tienen ingresos
|
||||
(
|
||||
SELECT JSON_AGG(DISTINCT c.ubicacion ORDER BY c.ubicacion)
|
||||
FROM clientes c
|
||||
WHERE c.ubicacion IS NOT NULL
|
||||
) as ubicaciones_disponibles,
|
||||
|
||||
-- Calidades únicas de ingresos
|
||||
(
|
||||
SELECT JSON_AGG(DISTINCT i.calidad ORDER BY i.calidad)
|
||||
FROM vista_detalle_ingresos i
|
||||
WHERE i.calidad IS NOT NULL
|
||||
) as calidades_disponibles,
|
||||
|
||||
-- Tipos de café (hardcoded pero por consistencia)
|
||||
ARRAY['uva', 'mojado', 'oreado', 'verde']::text[] as tipos_cafe,
|
||||
|
||||
-- Estados (hardcoded)
|
||||
ARRAY['pagado', 'pendiente']::text[] as estados_disponibles;
|
||||
```
|
||||
|
||||
**Respuesta esperada:**
|
||||
```json
|
||||
{
|
||||
"ubicaciones_disponibles": ["El Rosario", "Las Flores", "Belén", "Santa Rosa"],
|
||||
"calidades_disponibles": ["primera", "segunda", "tercera", "especial"],
|
||||
"tipos_cafe": ["uva", "mojado", "oreado", "verde"],
|
||||
"estados_disponibles": ["pagado", "pendiente"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Query 8: `informe_contadores_filtros`
|
||||
**Propósito:** Para el footer que muestra "X ingresos filtrados de Y totales"
|
||||
**Devuelve:** 1 fila con contadores
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
-- Total ingresos (sin filtros)
|
||||
(SELECT COUNT(*) FROM vista_detalle_ingresos) as total_ingresos,
|
||||
|
||||
-- Ingresos filtrados
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM vista_detalle_ingresos i
|
||||
LEFT JOIN clientes c ON i.cliente_id = c.id
|
||||
WHERE
|
||||
({{incluir_anulados}} OR (i.estado != 'anulado' AND i.fecha_anulado IS NULL))
|
||||
AND ({{fecha_desde}} IS NULL OR i.created_at >= {{fecha_desde}})
|
||||
AND ({{fecha_hasta}} IS NULL OR i.created_at <= {{fecha_hasta}})
|
||||
AND (CARDINALITY({{cliente_ids}}) = 0 OR i.cliente_id = ANY({{cliente_ids}}))
|
||||
AND (CARDINALITY({{tipos}}) = 0 OR i.tipo = ANY({{tipos}}))
|
||||
AND (CARDINALITY({{estados}}) = 0 OR i.estado = ANY({{estados}}))
|
||||
AND (CARDINALITY({{ubicaciones}}) = 0 OR c.ubicacion = ANY({{ubicaciones}}))
|
||||
AND (CARDINALITY({{calidades}}) = 0 OR i.calidad = ANY({{calidades}}))
|
||||
) as ingresos_filtrados,
|
||||
|
||||
-- Total clientes (sin filtros)
|
||||
(SELECT COUNT(*) FROM clientes) as total_clientes,
|
||||
|
||||
-- Clientes con ingresos filtrados
|
||||
(
|
||||
SELECT COUNT(DISTINCT i.cliente_id)
|
||||
FROM vista_detalle_ingresos i
|
||||
LEFT JOIN clientes c ON i.cliente_id = c.id
|
||||
WHERE
|
||||
({{incluir_anulados}} OR (i.estado != 'anulado' AND i.fecha_anulado IS NULL))
|
||||
AND ({{fecha_desde}} IS NULL OR i.created_at >= {{fecha_desde}})
|
||||
AND ({{fecha_hasta}} IS NULL OR i.created_at <= {{fecha_hasta}})
|
||||
AND (CARDINALITY({{cliente_ids}}) = 0 OR i.cliente_id = ANY({{cliente_ids}}))
|
||||
AND (CARDINALITY({{tipos}}) = 0 OR i.tipo = ANY({{tipos}}))
|
||||
AND (CARDINALITY({{estados}}) = 0 OR i.estado = ANY({{estados}}))
|
||||
AND (CARDINALITY({{ubicaciones}}) = 0 OR c.ubicacion = ANY({{ubicaciones}}))
|
||||
AND (CARDINALITY({{calidades}}) = 0 OR i.calidad = ANY({{calidades}}))
|
||||
) as clientes_con_ingresos_filtrados;
|
||||
```
|
||||
|
||||
**Respuesta esperada:**
|
||||
```json
|
||||
{
|
||||
"total_ingresos": 1250,
|
||||
"ingresos_filtrados": 230,
|
||||
"total_clientes": 85,
|
||||
"clientes_con_ingresos_filtrados": 42
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 RESUMEN DE QUERIES
|
||||
|
||||
| # | Nombre | Componente | Tipo de respuesta | Reutiliza? |
|
||||
|---|--------|------------|-------------------|------------|
|
||||
| 1 | `informe_totales_ingreso_compra` | TotalesIngresoCompra | 1 fila | ✅ Panorama Q2 |
|
||||
| 2 | `informe_totales_monetarios` | TotalesMonetarios | 1 fila | ✅ Panorama Q3 |
|
||||
| 3 | `informe_totales_verde` | TotalesVerde | 1 fila | ✅ Panorama Q4 |
|
||||
| 4 | `informe_lista_ingresos` | VistaTablaIngresos | Múltiples filas | ❌ Nueva |
|
||||
| 5 | `informe_lista_clientes_con_totales` | VistaTablaClientes, TopClientes | Múltiples filas | ❌ Nueva |
|
||||
| 6 | `informe_serie_temporal_acumulada` | Gráficas acumuladas | Múltiples filas | ❌ Nueva |
|
||||
| 7 | `informe_opciones_filtros` | Selectores de filtros | 1 objeto JSON | ❌ Nueva |
|
||||
| 8 | `informe_contadores_filtros` | Footer de filtros | 1 fila | ❌ Nueva |
|
||||
|
||||
**Total: 8 Queries (3 reutilizadas + 5 nuevas)**
|
||||
|
||||
---
|
||||
|
||||
## 🔧 CONFIGURACIÓN DE PARÁMETROS EN METABASE
|
||||
|
||||
### Parámetros Simples (igual que panorama):
|
||||
- `fecha_desde`: Date, opcional
|
||||
- `fecha_hasta`: Date, opcional
|
||||
- `incluir_anulados`: Boolean, default `false`
|
||||
|
||||
### Parámetros de Array (NUEVO):
|
||||
|
||||
#### `cliente_ids`
|
||||
- **Tipo:** Text (se parsea como array)
|
||||
- **Widget Type:** Text
|
||||
- **Default:** `[]`
|
||||
- **Required:** No
|
||||
- **Variable Name:** `cliente_ids`
|
||||
- **SQL Type:** `INTEGER[]`
|
||||
|
||||
#### `tipos`
|
||||
- **Tipo:** Text
|
||||
- **Widget Type:** Text o Dropdown (multi-select)
|
||||
- **Default:** `[]`
|
||||
- **Required:** No
|
||||
- **Options:** `['uva', 'mojado', 'oreado', 'verde']`
|
||||
- **SQL Type:** `TEXT[]`
|
||||
|
||||
#### `estados`
|
||||
- **Tipo:** Text
|
||||
- **Widget Type:** Text o Dropdown (multi-select)
|
||||
- **Default:** `[]`
|
||||
- **Required:** No
|
||||
- **Options:** `['pagado', 'pendiente']`
|
||||
- **SQL Type:** `TEXT[]`
|
||||
|
||||
#### `ubicaciones`
|
||||
- **Tipo:** Text
|
||||
- **Widget Type:** Text
|
||||
- **Default:** `[]`
|
||||
- **Required:** No
|
||||
- **SQL Type:** `TEXT[]`
|
||||
|
||||
#### `calidades`
|
||||
- **Tipo:** Text
|
||||
- **Widget Type:** Text
|
||||
- **Default:** `[]`
|
||||
- **Required:** No
|
||||
- **SQL Type:** `TEXT[]`
|
||||
|
||||
#### `granularidad` (solo Query 6)
|
||||
- **Tipo:** Text
|
||||
- **Widget Type:** Dropdown
|
||||
- **Default:** `'dia'`
|
||||
- **Required:** Yes
|
||||
- **Options:** `['dia', 'semana', 'mes']`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 INTEGRACIÓN EN VUE
|
||||
|
||||
```typescript
|
||||
// informe-ingresos.vue
|
||||
|
||||
const filters = ref({
|
||||
fecha_desde: null,
|
||||
fecha_hasta: null,
|
||||
incluir_anulados: false,
|
||||
cliente_ids: [], // Array de números
|
||||
tipos: [], // Array de strings
|
||||
estados: [], // Array de strings
|
||||
ubicaciones: [], // Array de strings
|
||||
calidades: [], // Array de strings
|
||||
granularidad: 'dia' // Solo para Query 6
|
||||
})
|
||||
|
||||
async function loadAllData() {
|
||||
const [
|
||||
totalesIngresoCompra,
|
||||
totalesMonetarios,
|
||||
totalesVerde,
|
||||
listaIngresos,
|
||||
listaClientes,
|
||||
serieAcumulada,
|
||||
opcionesFiltros,
|
||||
contadores
|
||||
] = await Promise.all([
|
||||
$fetch('/api/metabase/question/101', { query: filters.value }),
|
||||
$fetch('/api/metabase/question/102', { query: filters.value }),
|
||||
$fetch('/api/metabase/question/103', { query: filters.value }),
|
||||
$fetch('/api/metabase/question/104', { query: filters.value }),
|
||||
$fetch('/api/metabase/question/105', { query: filters.value }),
|
||||
$fetch('/api/metabase/question/106', { query: filters.value }),
|
||||
$fetch('/api/metabase/question/107'), // Sin filtros, siempre trae todo
|
||||
$fetch('/api/metabase/question/108', { query: filters.value })
|
||||
])
|
||||
|
||||
// Asignar directamente
|
||||
metrics.value = {
|
||||
totalesIngresoCompra: totalesIngresoCompra.data.rows[0],
|
||||
totalesMonetarios: totalesMonetarios.data.rows[0],
|
||||
totalesVerde: totalesVerde.data.rows[0],
|
||||
ingresos: listaIngresos.data.rows,
|
||||
clientes: listaClientes.data.rows,
|
||||
serieAcumulada: serieAcumulada.data.rows,
|
||||
opcionesFiltros: opcionesFiltros.data.rows[0],
|
||||
contadores: contadores.data.rows[0]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ NOTAS IMPORTANTES
|
||||
|
||||
### 1. Soporte de Arrays en Metabase
|
||||
|
||||
Metabase no tiene soporte nativo para arrays en parámetros. Hay dos opciones:
|
||||
|
||||
**Opción A: Usar PostgreSQL arrays con CARDINALITY**
|
||||
```sql
|
||||
WHERE (CARDINALITY({{cliente_ids}}) = 0 OR i.cliente_id = ANY({{cliente_ids}}))
|
||||
```
|
||||
El parámetro se pasa como: `{1,2,3}` o `ARRAY[1,2,3]`
|
||||
|
||||
**Opción B: Usar múltiples OR con unnest**
|
||||
```sql
|
||||
WHERE ({{cliente_ids}} = '' OR i.cliente_id IN (SELECT unnest(string_to_array({{cliente_ids}}, ','))))
|
||||
```
|
||||
El parámetro se pasa como string CSV: `"1,2,3"`
|
||||
|
||||
**Recomendación:** Usar Opción A si Metabase/PostgreSQL lo soporta, caso contrario usar Opción B.
|
||||
|
||||
### 2. Performance
|
||||
|
||||
- Query 4 (lista de ingresos) tiene LIMIT 1000 para evitar cargas masivas
|
||||
- Query 5 (clientes con totales) puede ser lenta si hay muchos ingresos
|
||||
- Query 6 (serie acumulada) usa window functions, puede requerir índices en `created_at`
|
||||
|
||||
### 3. Índices Recomendados
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_ingresos_created_at ON vista_detalle_ingresos(created_at);
|
||||
CREATE INDEX idx_ingresos_cliente_id ON vista_detalle_ingresos(cliente_id);
|
||||
CREATE INDEX idx_ingresos_tipo ON vista_detalle_ingresos(tipo);
|
||||
CREATE INDEX idx_ingresos_estado ON vista_detalle_ingresos(estado);
|
||||
CREATE INDEX idx_clientes_ubicacion ON clientes(ubicacion);
|
||||
```
|
||||
|
||||
### 4. Diferencias con Panorama
|
||||
|
||||
- Panorama usa filtros simples (un valor por parámetro)
|
||||
- Informe usa filtros múltiples (arrays de valores)
|
||||
- Informe tiene query de lista completa de ingresos (para tabla)
|
||||
- Informe tiene agregación por cliente (para ranking y tabla de clientes)
|
||||
|
||||
---
|
||||
|
||||
## 📝 PRÓXIMOS PASOS
|
||||
|
||||
1. ✅ Crear estas 8 queries en Metabase
|
||||
2. ✅ Configurar parámetros (especialmente los de tipo array)
|
||||
3. ✅ Probar queries con diferentes combinaciones de filtros
|
||||
4. ✅ Obtener los Question IDs
|
||||
5. ✅ Adaptar `informe-ingresos.vue` para usar las nuevas queries
|
||||
6. ✅ Eliminar composables de métricas (reutilizar estructura de panorama)
|
||||
7. ✅ Adaptar componentes para recibir props directamente
|
||||
8. ✅ Probar performance con datasets grandes
|
||||
|
||||
---
|
||||
|
||||
**Documento creado:** 2025-10-14
|
||||
**Autor:** Claude Code
|
||||
**Proyecto:** Analítica Núcleo - Informe Ingresos
|
||||
Reference in New Issue
Block a user