Files
analiticaNucleo/METABASE_QUERIES_INFORME_INGRESOS.md
josedario87 f8c53da6fc
All checks were successful
build-and-deploy / build (push) Successful in 43s
build-and-deploy / deploy (push) Successful in 4s
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
2025-10-14 10:34:27 -06:00

687 lines
23 KiB
Markdown

# 📊 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