diff --git a/METABASE_QUERIES_INFORME_INGRESOS.md b/METABASE_QUERIES_INFORME_INGRESOS.md deleted file mode 100644 index 5ac85c2..0000000 --- a/METABASE_QUERIES_INFORME_INGRESOS.md +++ /dev/null @@ -1,686 +0,0 @@ -# 📊 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 diff --git a/METABASE_QUERIES_PANORAMA.md b/METABASE_QUERIES_PANORAMA.md deleted file mode 100644 index 7fca425..0000000 --- a/METABASE_QUERIES_PANORAMA.md +++ /dev/null @@ -1,734 +0,0 @@ -# 📊 QUERIES DE METABASE PARA PANORAMA FACTURADOR - -## 🎯 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. Sin composables de métricas, sin cálculos en el frontend. - ---- - -## 📋 PARÁMETROS GLOBALES - -Todas las queries aceptan estos parámetros: - -| 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` | - -**Condición SQL estándar:** -```sql -WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}) -``` - ---- - -## 🎨 QUERIES POR COMPONENTE - -### Query 1: `panorama_totales_financieros_principales` -**Componente:** Card principal de "Totales Financieros" -**Devuelve:** 1 fila con 3 valores - -```sql -SELECT - -- Total Invertido en Café (pagado: uva + mojado + oreado) - COALESCE(SUM( - CASE - WHEN estado = 'pagado' AND tipo IN ('verde', 'uva') - THEN precio * peso_neto - WHEN estado = 'pagado' AND tipo IN ('oreado', 'mojado') - THEN (precio / 2) * peso_seco - ELSE 0 - END - ), 0) as total_invertido_cafe, - - -- Total Rechazos (traer de otra tabla) - COALESCE(( - SELECT SUM(total_cobrado) - FROM rechazos - WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}) - ), 0) as total_rechazos, - - -- Balance Neto (invertido - rechazos) - COALESCE(SUM( - CASE - WHEN estado = 'pagado' AND tipo IN ('verde', 'uva') - THEN precio * peso_neto - WHEN estado = 'pagado' AND tipo IN ('oreado', 'mojado') - THEN (precio / 2) * peso_seco - ELSE 0 - END - ), 0) - COALESCE(( - SELECT SUM(total_cobrado) - FROM rechazos - WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}) - ), 0) as balance_neto - -FROM vista_detalle_ingresos -WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}); -``` - -**Respuesta esperada:** -```json -{ - "total_invertido_cafe": 4567890.12, - "total_rechazos": 44750.00, - "balance_neto": 4523140.12 -} -``` - ---- - -### Query 2: `panorama_totales_ingreso_compra` -**Componente:** `TotalesIngresoCompra.vue` -**Devuelve:** 1 fila con TODOS los valores del componente - -```sql -SELECT - -- === TOTALES GENERALES (Pagado + Pendiente) === - - -- Uva - COALESCE(SUM(CASE WHEN tipo = 'uva' THEN peso_neto ELSE 0 END), 0) as total_lb_uva_ingresada, - COALESCE(SUM(CASE WHEN tipo = 'uva' THEN peso_seco ELSE 0 END), 0) as total_qq_seco_uva_ingresado, - - -- Mojado - COALESCE(SUM(CASE WHEN tipo = 'mojado' THEN peso_seco ELSE 0 END), 0) as total_qq_seco_mojado_ingresado, - - -- Oreado - COALESCE(SUM(CASE WHEN tipo = 'oreado' THEN peso_seco ELSE 0 END), 0) as total_qq_seco_oreado_ingresado, - - -- Total general - COALESCE(SUM(peso_seco), 0) as total_qq_seco_ingresado, - - -- === SOLO PAGADOS === - - -- Uva pagada - COALESCE(SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_neto ELSE 0 END), 0) as total_lb_uva_pagada, - COALESCE(SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_seco ELSE 0 END), 0) as total_qq_seco_uva_pagado, - - -- Mojado pagado - COALESCE(SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN peso_seco ELSE 0 END), 0) as total_qq_seco_mojado_pagado, - - -- Oreado pagado - COALESCE(SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN peso_seco ELSE 0 END), 0) as total_qq_seco_oreado_pagado, - - -- Total pagado - COALESCE(SUM(CASE WHEN estado = 'pagado' THEN peso_seco ELSE 0 END), 0) as total_qq_seco_comprado, - - -- === INVENTARIO EN DEPÓSITO (Pendiente) === - - -- Uva en depósito - COALESCE(SUM(CASE WHEN tipo = 'uva' AND estado = 'pendiente' THEN peso_neto ELSE 0 END), 0) as total_lb_uva_deposito, - - -- Mojado en depósito - COALESCE(SUM(CASE WHEN tipo = 'mojado' AND estado = 'pendiente' THEN peso_seco ELSE 0 END), 0) as total_qq_mojado_deposito, - - -- Oreado en depósito - COALESCE(SUM(CASE WHEN tipo = 'oreado' AND estado = 'pendiente' THEN peso_seco ELSE 0 END), 0) as total_qq_oreado_deposito, - - -- Total en depósito - COALESCE(SUM(CASE WHEN estado = 'pendiente' THEN peso_seco ELSE 0 END), 0) as total_qq_seco_deposito - -FROM vista_detalle_ingresos -WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}); -``` - -**Respuesta esperada:** -```json -{ - "total_lb_uva_ingresada": 123456.78, - "total_qq_seco_uva_ingresado": 246.91, - "total_qq_seco_mojado_ingresado": 500.00, - "total_qq_seco_oreado_ingresado": 300.00, - "total_qq_seco_ingresado": 1046.91, - "total_lb_uva_pagada": 100000.00, - "total_qq_seco_uva_pagado": 200.00, - "total_qq_seco_mojado_pagado": 450.00, - "total_qq_seco_oreado_pagado": 250.00, - "total_qq_seco_comprado": 900.00, - "total_lb_uva_deposito": 23456.78, - "total_qq_mojado_deposito": 50.00, - "total_qq_oreado_deposito": 50.00, - "total_qq_seco_deposito": 146.91 -} -``` - ---- - -### Query 3: `panorama_totales_monetarios` -**Componente:** `TotalesMonetarios.vue` -**Devuelve:** 1 fila con TODOS los valores - -```sql -SELECT - -- === INVERSIÓN HASTA LA FECHA === - - COALESCE(SUM( - CASE - WHEN estado = 'pagado' AND tipo = 'uva' - THEN precio * peso_neto - ELSE 0 - END - ), 0) as inversion_uva, - - COALESCE(SUM( - CASE - WHEN estado = 'pagado' AND tipo = 'mojado' - THEN (precio / 2) * peso_seco - ELSE 0 - END - ), 0) as inversion_mojado, - - COALESCE(SUM( - CASE - WHEN estado = 'pagado' AND tipo = 'oreado' - THEN (precio / 2) * peso_seco - ELSE 0 - END - ), 0) as inversion_oreado, - - COALESCE(SUM( - CASE - WHEN estado = 'pagado' AND tipo IN ('uva', 'mojado', 'oreado') - THEN - CASE - WHEN tipo = 'uva' THEN precio * peso_neto - ELSE (precio / 2) * peso_seco - END - ELSE 0 - END - ), 0) as total_invertido, - - -- === PRECIOS PROMEDIO PONDERADOS === - - -- Precio promedio uva por lb - CASE - WHEN SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_neto ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN precio * peso_neto ELSE 0 END) / - SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_neto ELSE 0 END) - ELSE 0 - END as precio_promedio_uva_por_lb, - - -- Precio promedio uva por qq (lb * 500) - CASE - WHEN SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_neto ELSE 0 END) > 0 - THEN (SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN precio * peso_neto ELSE 0 END) / - SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_neto ELSE 0 END)) * 500 - ELSE 0 - END as precio_promedio_uva_por_qq, - - -- Precio promedio mojado por qq - CASE - WHEN SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN (precio / 2) * peso_seco ELSE 0 END) / - SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) - ELSE 0 - END as precio_promedio_mojado_por_qq, - - -- Precio promedio oreado por qq - CASE - WHEN SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN (precio / 2) * peso_seco ELSE 0 END) / - SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) - ELSE 0 - END as precio_promedio_oreado_por_qq, - - -- Precio promedio global qq seco - CASE - WHEN SUM(CASE WHEN estado = 'pagado' THEN peso_seco ELSE 0 END) > 0 - THEN SUM( - CASE - WHEN estado = 'pagado' AND tipo IN ('verde', 'uva') - THEN precio * peso_neto - WHEN estado = 'pagado' AND tipo IN ('oreado', 'mojado') - THEN (precio / 2) * peso_seco - ELSE 0 - END - ) / SUM(CASE WHEN estado = 'pagado' THEN peso_seco ELSE 0 END) - ELSE 0 - END as precio_promedio_qq_seco, - - -- === INVERSIÓN RESTANTE A REALIZAR === - - -- Inversión restante uva (precio_promedio_uva_lb * lb_deposito) - (CASE - WHEN SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_neto ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN precio * peso_neto ELSE 0 END) / - SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_neto ELSE 0 END) - ELSE 0 - END) * COALESCE(SUM(CASE WHEN tipo = 'uva' AND estado = 'pendiente' THEN peso_neto ELSE 0 END), 0) as inversion_restante_uva, - - -- Inversión restante mojado (precio_promedio_mojado_qq * qq_deposito) - (CASE - WHEN SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN (precio / 2) * peso_seco ELSE 0 END) / - SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) - ELSE 0 - END) * COALESCE(SUM(CASE WHEN tipo = 'mojado' AND estado = 'pendiente' THEN peso_seco ELSE 0 END), 0) as inversion_restante_mojado, - - -- Inversión restante oreado (precio_promedio_oreado_qq * qq_deposito) - (CASE - WHEN SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN (precio / 2) * peso_seco ELSE 0 END) / - SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) - ELSE 0 - END) * COALESCE(SUM(CASE WHEN tipo = 'oreado' AND estado = 'pendiente' THEN peso_seco ELSE 0 END), 0) as inversion_restante_oreado, - - -- Inversión restante esperada (suma de las 3 anteriores) - (CASE - WHEN SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_neto ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN precio * peso_neto ELSE 0 END) / - SUM(CASE WHEN tipo = 'uva' AND estado = 'pagado' THEN peso_neto ELSE 0 END) - ELSE 0 - END) * COALESCE(SUM(CASE WHEN tipo = 'uva' AND estado = 'pendiente' THEN peso_neto ELSE 0 END), 0) + - (CASE - WHEN SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN (precio / 2) * peso_seco ELSE 0 END) / - SUM(CASE WHEN tipo = 'mojado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) - ELSE 0 - END) * COALESCE(SUM(CASE WHEN tipo = 'mojado' AND estado = 'pendiente' THEN peso_seco ELSE 0 END), 0) + - (CASE - WHEN SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN (precio / 2) * peso_seco ELSE 0 END) / - SUM(CASE WHEN tipo = 'oreado' AND estado = 'pagado' THEN peso_seco ELSE 0 END) - ELSE 0 - END) * COALESCE(SUM(CASE WHEN tipo = 'oreado' AND estado = 'pendiente' THEN peso_seco ELSE 0 END), 0) as inversion_restante_esperada - -FROM vista_detalle_ingresos -WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}); -``` - -**Respuesta esperada:** -```json -{ - "inversion_uva": 1234567.89, - "inversion_mojado": 567890.12, - "inversion_oreado": 345678.90, - "total_invertido": 2148136.91, - "precio_promedio_uva_por_lb": 4.50, - "precio_promedio_uva_por_qq": 2250.00, - "precio_promedio_mojado_por_qq": 2500.00, - "precio_promedio_oreado_por_qq": 2300.00, - "precio_promedio_qq_seco": 2400.00, - "inversion_restante_uva": 105555.51, - "inversion_restante_mojado": 125000.00, - "inversion_restante_oreado": 115000.00, - "inversion_restante_esperada": 345555.51 -} -``` - ---- - -### Query 4: `panorama_totales_verde` -**Componente:** `TotalesVerde.vue` -**Devuelve:** 1 fila con TODOS los valores - -```sql -SELECT - -- Total lb neto de verde (pagado + pendiente) - COALESCE(SUM(CASE WHEN tipo = 'verde' THEN peso_neto ELSE 0 END), 0) as total_lb_neto_verde, - - -- Precio promedio verde pagado (por lb) - CASE - WHEN SUM(CASE WHEN tipo = 'verde' AND estado = 'pagado' THEN peso_neto ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'verde' AND estado = 'pagado' THEN precio * peso_neto ELSE 0 END) / - SUM(CASE WHEN tipo = 'verde' AND estado = 'pagado' THEN peso_neto ELSE 0 END) - ELSE 0 - END as precio_promedio_verde_pagado, - - -- Total lb neto verde en depósito (pendiente) - COALESCE(SUM(CASE WHEN tipo = 'verde' AND estado = 'pendiente' THEN peso_neto ELSE 0 END), 0) as total_lb_neto_verde_deposito, - - -- Inversión verde hasta la fecha (pagado) - COALESCE(SUM( - CASE - WHEN tipo = 'verde' AND estado = 'pagado' - THEN precio * peso_neto - ELSE 0 - END - ), 0) as inversion_verde_hasta_fecha, - - -- Inversión restante verde (precio_promedio * lb_deposito) - (CASE - WHEN SUM(CASE WHEN tipo = 'verde' AND estado = 'pagado' THEN peso_neto ELSE 0 END) > 0 - THEN SUM(CASE WHEN tipo = 'verde' AND estado = 'pagado' THEN precio * peso_neto ELSE 0 END) / - SUM(CASE WHEN tipo = 'verde' AND estado = 'pagado' THEN peso_neto ELSE 0 END) - ELSE 0 - END) * COALESCE(SUM(CASE WHEN tipo = 'verde' AND estado = 'pendiente' THEN peso_neto ELSE 0 END), 0) as inversion_restante_verde, - - -- Total lb neto comprado verde (solo pagado) - COALESCE(SUM(CASE WHEN tipo = 'verde' AND estado = 'pagado' THEN peso_neto ELSE 0 END), 0) as total_lb_neto_comprado_verde - -FROM vista_detalle_ingresos -WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}); -``` - -**Respuesta esperada:** -```json -{ - "total_lb_neto_verde": 50000.00, - "precio_promedio_verde_pagado": 10.00, - "total_lb_neto_verde_deposito": 5000.00, - "inversion_verde_hasta_fecha": 450000.00, - "inversion_restante_verde": 50000.00, - "total_lb_neto_comprado_verde": 45000.00 -} -``` - ---- - -### Query 5: `panorama_secos_vendidos` -**Componente:** `SecosVendidos.vue` -**Devuelve:** 1 fila con TODOS los valores - -```sql -SELECT - -- Total qq seco por vender (en depósito) - COALESCE(SUM(CASE WHEN estado = 'pendiente' THEN peso_seco ELSE 0 END), 0) as total_qq_seco_por_vender, - - -- Precio de venta promedio por qq (TODO: necesita tabla de ventas, por ahora 0) - 0 as precio_venta_promedio_por_qq, - - -- Precio de compra promedio por qq - CASE - WHEN SUM(CASE WHEN estado = 'pagado' THEN peso_seco ELSE 0 END) > 0 - THEN SUM( - CASE - WHEN estado = 'pagado' AND tipo IN ('verde', 'uva') - THEN precio * peso_neto - WHEN estado = 'pagado' AND tipo IN ('oreado', 'mojado') - THEN (precio / 2) * peso_seco - ELSE 0 - END - ) / SUM(CASE WHEN estado = 'pagado' THEN peso_seco ELSE 0 END) - ELSE 0 - END as precio_compra_promedio_por_qq, - - -- Margen de ganancia por qq (venta - compra, por ahora será negativo) - 0 - CASE - WHEN SUM(CASE WHEN estado = 'pagado' THEN peso_seco ELSE 0 END) > 0 - THEN SUM( - CASE - WHEN estado = 'pagado' AND tipo IN ('verde', 'uva') - THEN precio * peso_neto - WHEN estado = 'pagado' AND tipo IN ('oreado', 'mojado') - THEN (precio / 2) * peso_seco - ELSE 0 - END - ) / SUM(CASE WHEN estado = 'pagado' THEN peso_seco ELSE 0 END) - ELSE 0 - END as margen_ganancia_por_qq - -FROM vista_detalle_ingresos -WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}); -``` - -**Respuesta esperada:** -```json -{ - "total_qq_seco_por_vender": 146.91, - "precio_venta_promedio_por_qq": 0, - "precio_compra_promedio_por_qq": 2400.00, - "margen_ganancia_por_qq": -2400.00 -} -``` - ---- - -### Query 6: `panorama_rechazos_subproductos` -**Componente:** `RechazosSubproductos.vue` -**Devuelve:** 6 filas (una por cada tipo de rechazo) - -```sql -SELECT - tipo, - COUNT(*) as num_registros, - COALESCE(SUM(cantidad), 0) as total_cantidad, - COALESCE(SUM(total_cobrado), 0) as total_cobrado, - - -- Precio promedio - CASE - WHEN SUM(cantidad) > 0 THEN SUM(total_cobrado) / SUM(cantidad) - ELSE 0 - END as precio_promedio - -FROM rechazos -WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}) -GROUP BY tipo -ORDER BY tipo; -``` - -**Respuesta esperada:** -```json -[ - { "tipo": "chibolita", "num_registros": 25, "total_cantidad": 1250.00, "total_cobrado": 12500.00, "precio_promedio": 10.00 }, - { "tipo": "magalla", "num_registros": 10, "total_cantidad": 50.00, "total_cobrado": 2500.00, "precio_promedio": 50.00 }, - { "tipo": "perico", "num_registros": 30, "total_cantidad": 1500.00, "total_cobrado": 15000.00, "precio_promedio": 10.00 }, - { "tipo": "picadillo", "num_registros": 15, "total_cantidad": 75.00, "total_cobrado": 3750.00, "precio_promedio": 50.00 }, - { "tipo": "pinta", "num_registros": 12, "total_cantidad": 600.00, "total_cobrado": 6000.00, "precio_promedio": 10.00 }, - { "tipo": "vano", "num_registros": 20, "total_cantidad": 100.00, "total_cobrado": 5000.00, "precio_promedio": 50.00 } -] -``` - ---- - -## 📈 QUERIES ADICIONALES (GRÁFICAS Y SERIES) - -### Query 7: `panorama_serie_temporal_diaria` -**Componente:** `GraficaSerieIngresos.vue`, `GraficaSerieInversion.vue` -**Devuelve:** Múltiples filas (una por día/tipo/estado) - -```sql -SELECT - DATE(created_at) as fecha, - tipo, - estado, - COUNT(*) as num_ingresos, - COALESCE(SUM(peso_seco), 0) as total_peso_seco_dia, - COALESCE(SUM(peso_neto), 0) as total_peso_neto_dia, - COALESCE(SUM( - CASE - WHEN tipo IN ('verde', 'uva') THEN precio * peso_neto - WHEN tipo IN ('oreado', 'mojado') THEN (precio / 2) * peso_seco - ELSE 0 - END - ), 0) as total_inversion_dia - -FROM vista_detalle_ingresos -WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}) -GROUP BY DATE(created_at), tipo, estado -ORDER BY fecha, tipo, estado; -``` - ---- - -### Query 8: `panorama_top_clientes` -**Componente:** `TopClientes.vue` -**Devuelve:** Múltiples filas (top 20 clientes por inversión) - -```sql -SELECT - cliente_id, - COUNT(*) as num_ingresos, - COALESCE(SUM(peso_seco), 0) as total_qq_seco, - COALESCE(SUM(peso_neto), 0) as total_lb_neto, - COALESCE(SUM( - CASE - WHEN tipo IN ('verde', 'uva') THEN precio * peso_neto - WHEN tipo IN ('oreado', 'mojado') THEN (precio / 2) * peso_seco - ELSE 0 - END - ), 0) as total_pagado - -FROM vista_detalle_ingresos -WHERE - estado = 'pagado' - AND ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}) -GROUP BY cliente_id -ORDER BY total_pagado DESC -LIMIT 20; -``` - ---- - -### Query 9: `panorama_conteo_registros` -**Propósito:** Para el footer que dice "Registros considerados: Ingresos X/Y · Rechazos A/B" -**Devuelve:** 1 fila con conteos - -```sql -SELECT - -- Ingresos filtrados - (SELECT COUNT(*) - FROM vista_detalle_ingresos - WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}) - ) as ingresos_filtrados, - - -- Total ingresos (sin filtros de fecha/anulados) - (SELECT COUNT(*) FROM vista_detalle_ingresos) as ingresos_total, - - -- Rechazos filtrados - (SELECT COUNT(*) - FROM rechazos - WHERE - ({{incluir_anulados}} OR (estado != 'anulado' AND fecha_anulado IS NULL)) - AND ({{fecha_desde}} IS NULL OR created_at >= {{fecha_desde}}) - AND ({{fecha_hasta}} IS NULL OR created_at <= {{fecha_hasta}}) - ) as rechazos_filtrados, - - -- Total rechazos - (SELECT COUNT(*) FROM rechazos) as rechazos_total; -``` - -**Respuesta esperada:** -```json -{ - "ingresos_filtrados": 230, - "ingresos_total": 350, - "rechazos_filtrados": 112, - "rechazos_total": 150 -} -``` - ---- - -## 📊 RESUMEN DE QUERIES - -| # | Nombre | Componente | Tipo de respuesta | -|---|--------|------------|-------------------| -| 1 | `panorama_totales_financieros_principales` | Card principal | 1 fila, 3 campos | -| 2 | `panorama_totales_ingreso_compra` | TotalesIngresoCompra | 1 fila, 14 campos | -| 3 | `panorama_totales_monetarios` | TotalesMonetarios | 1 fila, 14 campos | -| 4 | `panorama_totales_verde` | TotalesVerde | 1 fila, 6 campos | -| 5 | `panorama_secos_vendidos` | SecosVendidos | 1 fila, 4 campos | -| 6 | `panorama_rechazos_subproductos` | RechazosSubproductos | 6 filas (por tipo) | -| 7 | `panorama_serie_temporal_diaria` | Gráficas | Múltiples filas | -| 8 | `panorama_top_clientes` | TopClientes | 20 filas max | -| 9 | `panorama_conteo_registros` | Footer de filtros | 1 fila, 4 campos | - -**Total: 9 Queries** - ---- - -## 🚀 INTEGRACIÓN EN VUE - -```typescript -// panorama.vue - -const metrics = ref({ - financieros: null, - ingresoCompra: null, - monetarios: null, - verde: null, - secosVendidos: null, - rechazos: [], - serieTemporal: [], - topClientes: [], - conteos: null -}) - -async function loadAllData() { - const params = { - fecha_desde: fechaDesde.value, - fecha_hasta: fechaHasta.value, - incluir_anulados: includeAnulados.value - } - - const [ - financieros, - ingresoCompra, - monetarios, - verde, - secosVendidos, - rechazos, - serieTemporal, - topClientes, - conteos - ] = await Promise.all([ - $fetch('/api/metabase/question/1', { query: params }), - $fetch('/api/metabase/question/2', { query: params }), - $fetch('/api/metabase/question/3', { query: params }), - $fetch('/api/metabase/question/4', { query: params }), - $fetch('/api/metabase/question/5', { query: params }), - $fetch('/api/metabase/question/6', { query: params }), - $fetch('/api/metabase/question/7', { query: params }), - $fetch('/api/metabase/question/8', { query: params }), - $fetch('/api/metabase/question/9', { query: params }) - ]) - - // Asignar directamente, SIN procesamiento - metrics.value.financieros = financieros.data.rows[0] - metrics.value.ingresoCompra = ingresoCompra.data.rows[0] - metrics.value.monetarios = monetarios.data.rows[0] - metrics.value.verde = verde.data.rows[0] - metrics.value.secosVendidos = secosVendidos.data.rows[0] - metrics.value.rechazos = rechazos.data.rows - metrics.value.serieTemporal = serieTemporal.data.rows - metrics.value.topClientes = topClientes.data.rows - metrics.value.conteos = conteos.data.rows[0] -} -``` - -```vue - - - - -``` - ---- - -## ⚠️ NOTAS IMPORTANTES - -1. **Sin composables de métricas**: Los composables `useIngresosMetrics` y `useRechazosMetrics` se eliminan completamente. - -2. **Metabase hace TODO**: Cálculos, agregaciones, promedios ponderados, todo en SQL. - -3. **Vue solo renderiza**: Los componentes reciben props con los valores ya calculados. - -4. **Estructura de respuesta de Metabase**: - ```javascript - { - data: { - rows: [...], // Array de filas - cols: [...], // Metadata de columnas - rows_truncated: 1000 - } - } - ``` - -5. **Optimización**: Las 9 queries se ejecutan en **paralelo** con `Promise.all()`. - -6. **Filtros reactivos**: Cuando cambian los filtros, se vuelven a ejecutar las queries. - ---- - -**Documento creado:** 2025-10-14 -**Autor:** Claude Code -**Proyecto:** Analítica Núcleo - Panorama Facturador diff --git a/nuxt4-app/app/pages/informe-ingresos.vue b/nuxt4-app/app/pages/informe-ingresos.vue index a8ba9d7..5a0e22b 100644 --- a/nuxt4-app/app/pages/informe-ingresos.vue +++ b/nuxt4-app/app/pages/informe-ingresos.vue @@ -1,256 +1,443 @@