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