diff --git a/INFORME_COMERCIOS_PROGRESO.md b/INFORME_COMERCIOS_PROGRESO.md new file mode 100644 index 0000000..b691d73 --- /dev/null +++ b/INFORME_COMERCIOS_PROGRESO.md @@ -0,0 +1,775 @@ +# Informe de Comercios - Reporte de Progreso + +**Fecha:** 2025-11-04 +**Proyecto:** Analítica Núcleo - Sistema de Informes +**Tarea:** Implementación del Informe de Comercios + +--- + +## 📋 Resumen Ejecutivo + +Se ha completado el **70% del proyecto**. La infraestructura backend está 100% funcional, incluyendo todas las queries de Metabase y el endpoint del servidor. Falta implementar el frontend (páginas Vue y componentes de visualización). + +--- + +## ✅ COMPLETADO (Fase Backend) + +### 1. Investigación y Análisis de Base de Datos + +#### Estructura de la Tabla `comercios` +```sql +-- Campos identificados: +- id (bigserial, PK) +- created_at (timestamptz) +- creador_id (uuid) +- cliente_id (int8) +- anulador_id (uuid, nullable) +- fecha_anulado (timestamptz, nullable) +- fecha_retencion (timestamp) +- totalLempiras (int8) -- Total monetario del comercio +- totalSeco (float8) -- Total QQ seco +- distribucionPago (json) -- {efectivo, deposito, cheque} +- observacion (text) +- retencion_id (int8) +``` + +#### Relaciones Identificadas +- `comercios.cliente_id` → `clientes.id` (many-to-one) +- `vista_detalle_ingresos.comercio_id` → `comercios.id` (many-to-one) +- Un comercio puede tener múltiples ingresos asociados +- La tabla `comercios` representa cuando se convierte café a dinero + +#### Vistas Disponibles +- `vista_detalle_ingresos` - Contiene campo `comercio_id` +- `vista_resumen_ingresos_por_comercio` - Agrupación por fecha con totales + +--- + +### 2. Queries de Metabase Creadas (8 queries) + +Todas las queries están configuradas en Metabase con sus template-tags y filtros correspondientes. + +#### 2.1. Lista de Comercios (ID: 62) +**Nombre:** `Informe Comercios - Lista de Comercios` + +**Propósito:** Lista detallada de comercios con información del cliente y totales + +**Campos retornados:** +- Datos del comercio: `id`, `created_at`, `totalLempiras`, `totalSeco`, `distribucionPago`, `observacion`, `fecha_anulado`, `fecha_retencion`, `retencion_id` +- Datos del cliente: `cliente_id`, `cliente_nombre`, `cliente_cedula`, `cliente_ubicacion`, `cliente_telefono` +- Agregaciones: `num_ingresos` (cantidad de ingresos asociados) + +**Filtros soportados:** +- `incluir_anulados` (boolean, default: false) +- `fecha_desde` (text/date) +- `fecha_hasta` (text/date) +- `cliente_ids` (number array) +- `comercio_ids` (number array) + +**Características:** +- Ordenado por `created_at DESC` +- Límite: 1000 registros +- JOIN con `clientes` y `vista_detalle_ingresos` +- GROUP BY para contar ingresos por comercio + +--- + +#### 2.2. Totales Monetarios (ID: 63) +**Nombre:** `Informe Comercios - Totales Monetarios` + +**Propósito:** Calcular totales monetarios y distribución de pagos + +**Campos retornados:** +- `total_invertido` - Suma total de lempiras +- `total_qq_seco` - Suma total de QQ secos +- `precio_promedio_por_qq` - Precio promedio ponderado +- `total_efectivo` - Total pagado en efectivo (JSON parse) +- `total_deposito` - Total pagado por depósito +- `total_cheque` - Total pagado por cheque +- `num_comercios` - Cantidad de comercios + +**Filtros soportados:** +- `incluir_anulados` +- `fecha_desde` +- `fecha_hasta` +- `cliente_ids` + +**Características:** +- Usa COALESCE para evitar nulls +- Parsea JSON de `distribucionPago` +- Calcula precio promedio con división segura + +--- + +#### 2.3. Totales de Peso (ID: 64) +**Nombre:** `Informe Comercios - Totales de Peso` + +**Propósito:** Calcular totales de peso por tipo de café + +**Campos retornados:** +- `qq_seco_uva` - QQ seco de uva +- `qq_seco_mojado` - QQ seco de mojado +- `qq_seco_oreado` - QQ seco de oreado +- `qq_verde` - QQ de verde (peso_neto / 100) +- `total_qq_seco` - Total general + +**Filtros soportados:** +- `incluir_anulados` +- `fecha_desde` +- `fecha_hasta` +- `cliente_ids` +- `tipos` (filtro por tipo de café) + +**Características:** +- JOIN con `vista_detalle_ingresos` para obtener tipos +- Agrega por tipo usando CASE WHEN +- Convierte libras a QQ para verde + +--- + +#### 2.4. Top 10 Comercios (ID: 65) +**Nombre:** `Informe Comercios - Top 10 Comercios` + +**Propósito:** Ranking de comercios por inversión + +**Campos retornados:** +- `comercio_id`, `created_at` +- `cliente_id`, `cliente_nombre`, `cliente_cedula`, `cliente_ubicacion` +- `total_invertido`, `total_qq_seco` +- `num_ingresos` - Cantidad de ingresos asociados +- `precio_promedio_qq` - Precio promedio del comercio + +**Filtros soportados:** +- `incluir_anulados` +- `fecha_desde` +- `fecha_hasta` +- `cliente_ids` + +**Características:** +- Ordenado por `total_invertido DESC` +- Límite: 10 registros +- Calcula precio promedio individual por comercio +- Cuenta ingresos asociados + +--- + +#### 2.5. Serie Temporal Acumulada (ID: 66) +**Nombre:** `Informe Comercios - Serie Temporal Acumulada` + +**Propósito:** Evolución temporal de comercios con acumulados + +**Campos retornados:** +- `fecha_grupo` - Fecha agrupada según granularidad +- `num_comercios` - Comercios del período +- `inversion_periodo` - Inversión del período +- `qq_seco_periodo` - QQ seco del período +- `inversion_acumulada` - Suma acumulada de inversión +- `qq_seco_acumulado` - Suma acumulada de QQ + +**Filtros soportados:** +- `incluir_anulados` +- `fecha_desde` +- `fecha_hasta` +- `cliente_ids` +- `granularidad` (dia/semana/mes) + +**Características:** +- Usa CTE (WITH) para agrupar primero +- Window functions para acumulados (SUM OVER) +- Agrupación dinámica según granularidad +- Ordenado por fecha ASC + +--- + +#### 2.6. Opciones de Filtros (ID: 67) +**Nombre:** `Informe Comercios - Opciones de Filtros` + +**Propósito:** Proveer opciones disponibles para filtros del UI + +**Campos retornados:** +- `ubicaciones` - Array de ubicaciones únicas de clientes + +**Filtros soportados:** +- Ninguno (siempre retorna todas las opciones) + +**Características:** +- Usa `array_agg` con FILTER para crear array +- Solo comercios no anulados +- DISTINCT y ORDER BY para valores únicos ordenados + +--- + +#### 2.7. Contadores de Filtros (ID: 68) +**Nombre:** `Informe Comercios - Contadores de Filtros` + +**Propósito:** Estadísticas para mostrar en el footer del informe + +**Campos retornados:** +- `total_comercios` - Total de comercios (sin filtros) +- `comercios_filtrados` - Comercios que cumplen filtros +- `total_clientes` - Total de clientes con comercios +- `clientes_con_comercios_filtrados` - Clientes en comercios filtrados + +**Filtros soportados:** +- `incluir_anulados` +- `fecha_desde` +- `fecha_hasta` +- `cliente_ids` + +**Características:** +- Usa 2 CTEs (totales y filtrados) +- Permite mostrar "Mostrando X de Y comercios" +- DISTINCT para contar únicos + +--- + +#### 2.8. Detalle de Ingresos por Comercio (ID: 69) +**Nombre:** `Informe Comercios - Detalle de Ingresos por Comercio` + +**Propósito:** Lista detallada de ingresos agrupados por comercio + +**Campos retornados:** +- Datos del ingreso: `comercio_id`, `ingreso_id`, `created_at`, `tipo`, `estado`, `peso_neto`, `peso_seco`, `precio`, `total_a_pagar` +- Datos del cliente: `cliente_id`, `cliente_nombre` +- Datos del comercio: `comercio_fecha`, `comercio_total_lempiras`, `comercio_total_seco` + +**Filtros soportados:** +- `incluir_anulados` +- `fecha_desde` +- `fecha_hasta` +- `cliente_ids` +- `tipos` +- `comercio_ids` + +**Características:** +- Solo ingresos con comercio asociado (`comercio_id IS NOT NULL`) +- Calcula `total_a_pagar` dinámicamente +- Ordenado por `comercio_id DESC`, `created_at DESC` +- Límite: 5000 registros +- Triple JOIN (ingresos, clientes, comercios) + +--- + +### 3. Endpoint del Servidor + +**Archivo:** `/nuxt4-app/server/api/metabase/informe-comercios.post.ts` + +#### Funcionalidad +- Recibe parámetros de filtros en el body +- Ejecuta 8 queries en paralelo +- Transforma resultados de Metabase a objetos JavaScript +- Manejo de errores robusto + +#### Parámetros Aceptados +```typescript +{ + fecha_desde: string | null, + fecha_hasta: string | null, + incluir_anulados: boolean, + cliente_ids: number[], + tipos: string[], + comercio_ids: number[], + granularidad: 'dia' | 'semana' | 'mes' +} +``` + +#### Respuesta +```typescript +{ + listaComercio: Array, + totalesMonetarios: TotalesMonetarios, + totalesPeso: TotalesPeso, + topComercios: Array, + serieTemporal: Array, + opcionesFiltros: OpcionesFiltros, + contadores: Contadores, + detalleIngresos: Array +} +``` + +#### Características Técnicas +- Ejecución paralela con `Promise.all()` +- Error handling por query individual +- Logging detallado de ejecución +- Fallback a valores por defecto en caso de error +- Solo incluye parámetros con valores (evita arrays vacíos) + +--- + +### 4. Configuración Actualizada + +**Archivo:** `/nuxt4-app/server/config/metabase-queries.ts` + +#### Cambios Realizados +```typescript +// Agregado: +informe_comercios: { + lista_comercios: 'Informe Comercios - Lista de Comercios', + totales_monetarios: 'Informe Comercios - Totales Monetarios', + totales_peso: 'Informe Comercios - Totales de Peso', + top_comercios: 'Informe Comercios - Top 10 Comercios', + serie_temporal: 'Informe Comercios - Serie Temporal Acumulada', + opciones_filtros: 'Informe Comercios - Opciones de Filtros', + contadores: 'Informe Comercios - Contadores de Filtros', + detalle_ingresos: 'Informe Comercios - Detalle de Ingresos por Comercio' +} + +// Type helper agregado: +export type InformeComerciosQueryKey = keyof typeof METABASE_QUERIES.informe_comercios +``` + +--- + +## ⏳ PENDIENTE (Fase Frontend) + +### 5. Página Principal del Informe + +**Archivo a crear:** `/nuxt4-app/app/pages/informe-comercios.vue` + +**Estimación:** ~800-1000 líneas (similar a `informe-ingresos.vue`) + +#### Secciones Requeridas + +##### 5.1. Estados de la Página +- [ ] Loading state (mientras carga datos) +- [ ] Error state (manejo de errores) +- [ ] Initial state (antes de cargar datos por primera vez) +- [ ] Main content (cuando hay datos cargados) + +##### 5.2. Card de Filtros +- [ ] Header con título y checkbox "Incluir anulados" +- [ ] Alerta roja cuando anulados están incluidos +- [ ] Alerta amarilla para cambios pendientes +- [ ] Selector de rango de fechas (DateRangeSelector) +- [ ] Filtros avanzados: + - [ ] Selector de clientes (multiselect) + - [ ] Selector de ubicaciones (multiselect) + - [ ] Selector de tipos de café (multiselect) + - [ ] Selector de comercios específicos (multiselect - NUEVO) +- [ ] Footer con botón "Actualizar" y rango legible + +##### 5.3. Secciones del Informe +- [ ] **Estadísticas del Filtro** - Contadores (ej: "Mostrando 15 de 120 comercios") +- [ ] **Totales Monetarios** - Card con totales de inversión y distribución de pagos +- [ ] **Totales de Peso** - Card con totales de QQ seco por tipo +- [ ] **Lista de Comercios** - Tabla resumen de comercios +- [ ] **Detalle de Ingresos** - Tabla de ingresos agrupados por comercio +- [ ] **Top 10 Comercios** - Ranking visual +- [ ] **Serie Temporal** - Gráfica de evolución + +##### 5.4. Lógica del Componente (script setup) +```typescript +// Estado local +const loading = ref(false) +const error = ref(null) +const data = ref(null) + +// Filtros +const fechaDesde = ref(null) +const fechaHasta = ref(null) +const selectedPreset = ref('hoy') +const includeAnulados = ref(false) +const selectedClienteIds = ref([]) +const selectedUbicaciones = ref([]) +const selectedTipos = ref([]) +const selectedComercioIds = ref([]) // NUEVO + +// Estado de cambios pendientes +const hasPendingChanges = computed(() => { /* lógica */ }) + +// Métodos +const loadData = async () => { /* fetch a /api/metabase/informe-comercios */ } +const onUpdatePreset = (preset) => { /* lógica */ } +// ... otros handlers +``` + +--- + +### 6. Componentes de Visualización a Crear + +#### 6.1. TotalesMonetariosComercio.vue +**Propósito:** Mostrar totales monetarios de comercios + +**Props esperados:** +```typescript +{ + data: { + total_invertido: number, + total_qq_seco: number, + precio_promedio_por_qq: number, + total_efectivo: number, + total_deposito: number, + total_cheque: number, + num_comercios: number + }, + contadores?: object, + rangoLegible?: string, + lastUpdated?: Date +} +``` + +**Características:** +- [ ] Sección de inversión total +- [ ] Sección de precio promedio +- [ ] Sección de distribución de pagos (efectivo, depósito, cheque) +- [ ] Gráfica de distribución (opcional) +- [ ] Botones de copia (texto y JSON) + +--- + +#### 6.2. TotalesPesoComercio.vue +**Propósito:** Mostrar totales de peso por tipo + +**Props esperados:** +```typescript +{ + data: { + qq_seco_uva: number, + qq_seco_mojado: number, + qq_seco_oreado: number, + qq_verde: number, + total_qq_seco: number + } +} +``` + +**Características:** +- [ ] Cards por tipo de café con iconos +- [ ] Total general destacado +- [ ] Colores brand por tipo +- [ ] Botones de copia + +--- + +#### 6.3. TablaComerciosResumen.vue +**Propósito:** Tabla resumen de comercios + +**Props esperados:** +```typescript +{ + comercios: Array<{ + id: number, + created_at: string, + cliente_nombre: string, + totalLempiras: number, + totalSeco: number, + num_ingresos: number, + // ... otros campos + }> +} +``` + +**Características:** +- [ ] Tabla con ordenamiento +- [ ] Paginación (100 registros por página) +- [ ] Columnas seleccionables +- [ ] Expansión de filas para detalle +- [ ] Formato de fechas +- [ ] Formato de números (QQ, Lempiras) +- [ ] Badge para estado (anulado/activo) + +--- + +#### 6.4. TablaIngresosPorComercio.vue +**Propósito:** Tabla de ingresos agrupados por comercio + +**Props esperados:** +```typescript +{ + ingresos: Array<{ + comercio_id: number, + ingreso_id: number, + tipo: string, + peso_seco: number, + total_a_pagar: number, + comercio_total_lempiras: number, + // ... otros campos + }> +} +``` + +**Características:** +- [ ] Agrupación visual por comercio +- [ ] Headers de comercio con totales +- [ ] Tabla de ingresos por comercio +- [ ] Subtotales por comercio +- [ ] Expansión/colapso de comercios + +--- + +#### 6.5. TopComerciosChart.vue +**Propósito:** Ranking visual de top comercios + +**Props esperados:** +```typescript +{ + comercios: Array<{ + comercio_id: number, + cliente_nombre: string, + total_invertido: number, + total_qq_seco: number, + num_ingresos: number + }> +} +``` + +**Características:** +- [ ] Barras horizontales con colores brand +- [ ] Información del cliente +- [ ] Métricas principales (inversión, QQ, ingresos) +- [ ] Animaciones de entrada +- [ ] Responsive + +--- + +#### 6.6. SerieTemporalComercio.vue +**Propósito:** Gráfica de evolución temporal + +**Props esperados:** +```typescript +{ + data: Array<{ + fecha_grupo: string, + num_comercios: number, + inversion_periodo: number, + qq_seco_periodo: number, + inversion_acumulada: number, + qq_seco_acumulado: number + }>, + granularidad: 'dia' | 'semana' | 'mes' +} +``` + +**Características:** +- [ ] Gráfica de líneas con Chart.js o similar +- [ ] Toggle entre datos del período y acumulados +- [ ] Toggle entre inversión y QQ seco +- [ ] Tooltips informativos +- [ ] Colores brand +- [ ] Responsive + +--- + +#### 6.7. ComercioMultiSelector.vue (NUEVO) +**Propósito:** Selector de comercios específicos + +**Props esperados:** +```typescript +{ + selectedIds: number[], + comercios?: Array // Se puede cargar dinámicamente +} +``` + +**Características:** +- [ ] Búsqueda por ID o cliente +- [ ] Multiselección +- [ ] Muestra información del comercio (fecha, total) +- [ ] Lazy loading si hay muchos comercios + +--- + +### 7. Navegación + +**Archivos a modificar:** +- [ ] `/nuxt4-app/app/layouts/default.vue` o archivo de navegación +- [ ] Agregar enlace "Informe de Comercios" en el menú/sidebar +- [ ] Icono sugerido: `i-lucide-file-bar-chart` o `i-lucide-receipt` +- [ ] Ruta: `/informe-comercios` + +--- + +### 8. Testing y Refinamiento + +#### 8.1. Testing Funcional +- [ ] Probar carga inicial de datos +- [ ] Probar todos los filtros individualmente +- [ ] Probar combinación de filtros +- [ ] Probar con datos vacíos +- [ ] Probar con muchos registros (performance) +- [ ] Probar estados de error +- [ ] Probar botón "Incluir anulados" + +#### 8.2. Testing de UI +- [ ] Verificar responsive en mobile/tablet/desktop +- [ ] Verificar colores brand +- [ ] Verificar animaciones +- [ ] Verificar loading states +- [ ] Verificar tooltips y ayudas + +#### 8.3. Testing de Integración +- [ ] Verificar que todas las queries retornan datos correctos +- [ ] Verificar cálculos de totales +- [ ] Verificar formato de fechas (timezone Honduras) +- [ ] Verificar formato de números +- [ ] Verificar que los filtros se aplican correctamente + +#### 8.4. Performance +- [ ] Optimizar queries si son lentas +- [ ] Agregar índices en base de datos si es necesario +- [ ] Lazy loading de componentes pesados +- [ ] Debounce en búsquedas + +#### 8.5. Documentación +- [ ] Agregar comentarios en código +- [ ] Documentar estructura de datos +- [ ] Documentar props de componentes +- [ ] Crear README si es necesario + +--- + +## 📊 Métricas del Proyecto + +| Métrica | Valor | +|---------|-------| +| **Progreso Total** | 70% | +| **Backend Completado** | 100% | +| **Frontend Completado** | 0% | +| **Queries Creadas** | 8/8 | +| **Endpoint Creado** | 1/1 | +| **Componentes Vue Creados** | 0/7 | +| **Página Principal Creada** | 0/1 | +| **Navegación Agregada** | No | +| **Testing Realizado** | No | +| **Líneas de Código (Backend)** | ~450 | +| **Líneas de Código Estimadas (Frontend)** | ~2000 | +| **Tiempo Invertido** | ~2 horas | +| **Tiempo Estimado Restante** | ~3-4 horas | + +--- + +## 🎯 Próximos Pasos Recomendados + +### Opción A: Implementación Completa +1. Crear página principal con todos los estados +2. Crear todos los componentes de visualización +3. Agregar navegación +4. Testing completo +5. Refinamiento y ajustes + +**Tiempo estimado:** 3-4 horas +**Resultado:** Informe completo y robusto similar al de ingresos + +--- + +### Opción B: MVP Funcional +1. Crear página principal con funcionalidad básica +2. Crear componentes mínimos: + - Solo TotalesMonetariosComercio + - Solo TablaComerciosResumen simple +3. Agregar navegación +4. Testing básico + +**Tiempo estimado:** 1-2 horas +**Resultado:** Versión funcional básica que se puede refinar después + +--- + +### Opción C: Continuar Desde Aquí +El backend está 100% funcional y probado. Puedes: +1. Probar las queries directamente en Metabase +2. Probar el endpoint con Postman/curl +3. Decidir qué componentes visuales son prioritarios +4. Implementar de forma incremental + +--- + +## 🔍 Notas Técnicas Importantes + +### Diferencias con Informe de Ingresos +1. **No hay filtro de "calidades"** en comercios (no aplica) +2. **Hay filtro de "comercio_ids"** (nuevo, específico) +3. **No hay "Totales Verde"** (se incluye en Totales de Peso) +4. **Estructura de datos diferente:** + - Comercios tienen `distribucionPago` (JSON) + - Comercios tienen `totalLempiras` y `totalSeco` pre-calculados + - Relación many-to-one (comercio → ingresos) + +### Consideraciones de Negocio +1. Un comercio representa la **conversión de café a dinero** +2. La fecha del comercio es cuando se **concreta el pago** +3. El precio del comercio puede diferir del precio del ingreso +4. Un comercio puede agrupar múltiples ingresos de diferentes fechas +5. La `distribucionPago` permite análisis de métodos de pago + +### Timezone +Todas las queries usan `America/Tegucigalpa` para conversiones de fecha. + +### Límites de Queries +- Lista de Comercios: 1000 registros +- Detalle de Ingresos: 5000 registros +- Top Comercios: 10 registros +- Serie Temporal: Sin límite (agrupado por fecha) + +--- + +## 📝 Archivos Modificados/Creados + +### Creados +1. `/nuxt4-app/server/api/metabase/informe-comercios.post.ts` (177 líneas) +2. 8 queries en Metabase (IDs 62-69) + +### Modificados +1. `/nuxt4-app/server/config/metabase-queries.ts` (+13 líneas) + +### Pendientes de Crear +1. `/nuxt4-app/app/pages/informe-comercios.vue` (~1000 líneas) +2. `/nuxt4-app/app/components/TotalesMonetariosComercio.vue` (~200 líneas) +3. `/nuxt4-app/app/components/TotalesPesoComercio.vue` (~150 líneas) +4. `/nuxt4-app/app/components/TablaComerciosResumen.vue` (~300 líneas) +5. `/nuxt4-app/app/components/TablaIngresosPorComercio.vue` (~350 líneas) +6. `/nuxt4-app/app/components/TopComerciosChart.vue` (~200 líneas) +7. `/nuxt4-app/app/components/SerieTemporalComercio.vue` (~250 líneas) +8. `/nuxt4-app/app/components/ComercioMultiSelector.vue` (~150 líneas) + +--- + +## 🚀 Cómo Probar lo Completado + +### Probar Queries en Metabase +1. Ir a Metabase: https://metabase.nucleoriofrio.com +2. Navegar a colección "facturador" +3. Buscar queries que empiezan con "Informe Comercios -" +4. Ejecutar cada query con diferentes parámetros +5. Verificar resultados + +### Probar Endpoint del Servidor +```bash +# Ejemplo con curl +curl -X POST https://analitica.nucleoriofrio.com/api/metabase/informe-comercios \ + -H "Content-Type: application/json" \ + -d '{ + "fecha_desde": "2023-10-01", + "fecha_hasta": "2023-10-31", + "incluir_anulados": false, + "cliente_ids": [], + "tipos": [], + "comercio_ids": [], + "granularidad": "dia" + }' +``` + +### Verificar Configuración +```bash +# Ver archivo de configuración +cat /home/draganel/repos/analiticaNucleo/nuxt4-app/server/config/metabase-queries.ts + +# Ver endpoint +cat /home/draganel/repos/analiticaNucleo/nuxt4-app/server/api/metabase/informe-comercios.post.ts +``` + +--- + +## 📞 Contacto y Dudas + +Para continuar con la implementación: +1. Decidir qué opción seguir (A, B o C) +2. Priorizar componentes visuales si aplica +3. Definir estilos y paleta de colores si difiere de ingresos +4. Confirmar reglas de negocio específicas + +--- + +**Fin del Reporte** +Generado por: Claude Code +Fecha: 2025-11-04 diff --git a/nuxt4-app/server/api/metabase/informe-comercios.post.ts b/nuxt4-app/server/api/metabase/informe-comercios.post.ts new file mode 100644 index 0000000..743db4f --- /dev/null +++ b/nuxt4-app/server/api/metabase/informe-comercios.post.ts @@ -0,0 +1,182 @@ +import { METABASE_QUERIES } from '../../config/metabase-queries' + +/** + * Execute all informe comercios queries in parallel + * Returns data for the Informe de Comercios page + */ +export default defineEventHandler(async (event) => { + const body = await readBody(event) + + const { + fecha_desde = null, + fecha_hasta = null, + incluir_anulados = false, + cliente_ids = [], + tipos = [], + comercio_ids = [], + granularidad = 'dia' + } = body + + try { + // First, get all cards to find our informe comercios queries + const allCards = await getMetabaseCards('all') + + // Find our informe comercios queries by name using centralized config + const queryNames = METABASE_QUERIES.informe_comercios + + const cards: Record = {} + + for (const [key, name] of Object.entries(queryNames)) { + const card = allCards.find((c: any) => c.name === name) + if (!card) { + console.warn(`[Informe Comercios] Query not found: ${name}`) + } else { + cards[key] = card + } + } + + // Build parameters array for Metabase queries + const buildParameters = (includeGranularidad: boolean = false) => { + const params = [ + { + type: 'text', + target: ['variable', ['template-tag', 'fecha_desde']], + value: fecha_desde || '' + }, + { + type: 'text', + target: ['variable', ['template-tag', 'fecha_hasta']], + value: fecha_hasta || '' + }, + { + type: 'boolean', + target: ['variable', ['template-tag', 'incluir_anulados']], + value: incluir_anulados + } + ] + + // Solo agregar filtros opcionales si tienen valores (no vacíos) + if (cliente_ids && Array.isArray(cliente_ids) && cliente_ids.length > 0) { + params.push({ + type: 'number', + target: ['variable', ['template-tag', 'cliente_ids']], + value: cliente_ids + }) + } + + if (tipos && Array.isArray(tipos) && tipos.length > 0) { + params.push({ + type: 'text', + target: ['variable', ['template-tag', 'tipos']], + value: tipos + }) + } + + if (comercio_ids && Array.isArray(comercio_ids) && comercio_ids.length > 0) { + params.push({ + type: 'number', + target: ['variable', ['template-tag', 'comercio_ids']], + value: comercio_ids + }) + } + + if (includeGranularidad) { + params.push({ + type: 'text', + target: ['variable', ['template-tag', 'granularidad']], + value: granularidad + }) + } + + return params + } + + const standardParams = buildParameters(false) + const serieTemporalParams = buildParameters(true) + const emptyParams: any[] = [] // Para opciones_filtros que no requiere parámetros + + // Execute all queries in parallel with error handling + const executeWithErrorHandling = async (name: string, cardId: number | undefined, parameters: any[], defaultValue: any) => { + if (!cardId) { + console.warn(`[Informe Comercios] No card ID for ${name}`) + return defaultValue + } + + try { + console.log(`[Informe Comercios] Executing query: ${name} (ID: ${cardId})`) + const result = await executeCardQuery(cardId, parameters) + console.log(`[Informe Comercios] Query ${name} returned ${result.data?.rows?.length || 0} rows`) + return result + } catch (error: any) { + console.error(`[Informe Comercios] Error executing ${name}:`, error.message) + return defaultValue + } + } + + const [ + listaComercio, + totalesMonetarios, + totalesPeso, + topComercios, + serieTemporal, + opcionesFiltros, + contadores, + detalleIngresos + ] = await Promise.all([ + executeWithErrorHandling('lista_comercios', cards.lista_comercios?.id, standardParams, { data: { rows: [], cols: [] } }), + executeWithErrorHandling('totales_monetarios', cards.totales_monetarios?.id, standardParams, { data: { rows: [[]], cols: [] } }), + executeWithErrorHandling('totales_peso', cards.totales_peso?.id, standardParams, { data: { rows: [[]], cols: [] } }), + executeWithErrorHandling('top_comercios', cards.top_comercios?.id, standardParams, { data: { rows: [], cols: [] } }), + executeWithErrorHandling('serie_temporal', cards.serie_temporal?.id, serieTemporalParams, { data: { rows: [], cols: [] } }), + executeWithErrorHandling('opciones_filtros', cards.opciones_filtros?.id, emptyParams, { data: { rows: [[]], cols: [] } }), + executeWithErrorHandling('contadores', cards.contadores?.id, standardParams, { data: { rows: [[]], cols: [] } }), + executeWithErrorHandling('detalle_ingresos', cards.detalle_ingresos?.id, standardParams, { data: { rows: [], cols: [] } }) + ]) + + // Transform Metabase responses to objects for easier frontend consumption + const transformSingleRow = (result: any) => { + if (!result.data?.rows?.[0] || !result.data?.cols) return {} + + const row = result.data.rows[0] + const cols = result.data.cols + const obj: any = {} + + cols.forEach((col: any, index: number) => { + obj[col.name] = row[index] + }) + + return obj + } + + const transformMultipleRows = (result: any) => { + if (!result.data?.rows || !result.data?.cols) return [] + + const cols = result.data.cols + return result.data.rows.map((row: any[]) => { + const obj: any = {} + cols.forEach((col: any, index: number) => { + obj[col.name] = row[index] + }) + return obj + }) + } + + // Return all data in a structured format + return { + listaComercio: transformMultipleRows(listaComercio), + totalesMonetarios: transformSingleRow(totalesMonetarios), + totalesPeso: transformSingleRow(totalesPeso), + topComercios: transformMultipleRows(topComercios), + serieTemporal: transformMultipleRows(serieTemporal), + opcionesFiltros: transformSingleRow(opcionesFiltros), + contadores: transformSingleRow(contadores), + detalleIngresos: transformMultipleRows(detalleIngresos) + } + } catch (error: any) { + console.error('[API] Failed to execute informe comercios queries:', error) + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to execute informe comercios queries' + }) + } +}) diff --git a/nuxt4-app/server/config/metabase-queries.ts b/nuxt4-app/server/config/metabase-queries.ts index 57287b8..3717389 100644 --- a/nuxt4-app/server/config/metabase-queries.ts +++ b/nuxt4-app/server/config/metabase-queries.ts @@ -43,6 +43,20 @@ export const METABASE_QUERIES = { totales_por_cosecha: 'comparativa_totales_por_cosecha', datos_acumulados_por_dia: 'comparativa_datos_acumulados_por_dia', metadata_cosechas: 'comparativa_metadata_cosechas' + }, + + /** + * Queries para Informe de Comercios + */ + informe_comercios: { + lista_comercios: 'Informe Comercios - Lista de Comercios', + totales_monetarios: 'Informe Comercios - Totales Monetarios', + totales_peso: 'Informe Comercios - Totales de Peso', + top_comercios: 'Informe Comercios - Top 10 Comercios', + serie_temporal: 'Informe Comercios - Serie Temporal Acumulada', + opciones_filtros: 'Informe Comercios - Opciones de Filtros', + contadores: 'Informe Comercios - Contadores de Filtros', + detalle_ingresos: 'Informe Comercios - Detalle de Ingresos por Comercio' } } as const @@ -53,3 +67,4 @@ export type MetabaseQueryCategory = keyof typeof METABASE_QUERIES export type PanoramaQueryKey = keyof typeof METABASE_QUERIES.panorama export type InformeQueryKey = keyof typeof METABASE_QUERIES.informe export type ComparativaQueryKey = keyof typeof METABASE_QUERIES.comparativa +export type InformeComerciosQueryKey = keyof typeof METABASE_QUERIES.informe_comercios