Feat: Implementar visualizaciones completas en Informe de Ingresos
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 46s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 46s
Agrega tres secciones principales con datos reales de Metabase: 1. Lista de Ingresos (Query ID 50) - Tabla completa con detalles de cada ingreso - Campos: ID, fecha, cliente, tipo, peso, precio, total, estado - Badges de colores para tipos y estados - Soporte para 1000 registros 2. Top 10 Clientes (Query ID 51) - Ranking por monto total pagado - Muestra: nombre, cédula, ubicación - Métricas: total pagado, número de ingresos, quintales - Diseño en cards con numeración 3. Serie Temporal Acumulada (Query ID 52) - Datos desglosados por fecha, tipo y estado - Columnas del período: ingresos, peso seco, inversión - Columnas acumuladas: qq acumulados, inversión acumulada - Tabla scrollable con sticky header Correcciones técnicas: - Usar campos correctos de las queries de Metabase - created_at en lugar de fecha - cliente_nombre en lugar de nombre_cliente - cliente_id, cliente_cedula, cliente_ubicacion - fecha_grupo, peso_seco_periodo, inversion_acumulada
This commit is contained in:
@@ -367,35 +367,204 @@
|
||||
<TotalesMonetarios v-if="pageSections.totalesCafe" :data="data.totalesMonetarios" />
|
||||
<TotalesVerde v-if="pageSections.totalesVerde" :data="data.totalesVerde" />
|
||||
|
||||
<!-- Placeholder para tablas y gráficas futuras -->
|
||||
<!-- Lista de Ingresos -->
|
||||
<UCard v-if="pageSections.tablaIngresos" class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold brand-section-title">Lista de Ingresos</h2>
|
||||
<p class="text-xs text-[var(--brand-text-muted)] mt-1">
|
||||
Detalles completos de {{ data.listaIngresos?.length || 0 }} ingresos filtrados
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="py-8 text-center text-sm text-[var(--brand-text-muted)]">
|
||||
<p>Tabla detallada de ingresos próximamente</p>
|
||||
<p class="mt-2">{{ data.listaIngresos?.length || 0 }} registros disponibles</p>
|
||||
|
||||
<div v-if="data.listaIngresos && data.listaIngresos.length > 0" class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-[var(--brand-border)]">
|
||||
<th class="text-left py-3 px-2 font-semibold text-[var(--brand-text-muted)]">ID</th>
|
||||
<th class="text-left py-3 px-2 font-semibold text-[var(--brand-text-muted)]">Fecha</th>
|
||||
<th class="text-left py-3 px-2 font-semibold text-[var(--brand-text-muted)]">Cliente</th>
|
||||
<th class="text-left py-3 px-2 font-semibold text-[var(--brand-text-muted)]">Tipo</th>
|
||||
<th class="text-right py-3 px-2 font-semibold text-[var(--brand-text-muted)]">Peso Neto</th>
|
||||
<th class="text-right py-3 px-2 font-semibold text-[var(--brand-text-muted)]">Precio</th>
|
||||
<th class="text-right py-3 px-2 font-semibold text-[var(--brand-text-muted)]">Total</th>
|
||||
<th class="text-center py-3 px-2 font-semibold text-[var(--brand-text-muted)]">Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="ingreso in data.listaIngresos"
|
||||
:key="ingreso.id"
|
||||
class="border-b border-[var(--brand-border)]/50 hover:bg-[#1c140c] transition-colors"
|
||||
>
|
||||
<td class="py-2 px-2 text-[var(--brand-text-muted)]">{{ ingreso.id }}</td>
|
||||
<td class="py-2 px-2 text-[var(--brand-text)]">
|
||||
{{ ingreso.created_at ? new Date(ingreso.created_at).toLocaleDateString('es-ES') : '-' }}
|
||||
</td>
|
||||
<td class="py-2 px-2 text-[var(--brand-text)]">{{ ingreso.cliente_nombre || '-' }}</td>
|
||||
<td class="py-2 px-2">
|
||||
<span :class="[
|
||||
'inline-flex px-2 py-0.5 rounded text-xs font-medium',
|
||||
ingreso.tipo === 'uva' ? 'bg-purple-500/20 text-purple-300' :
|
||||
ingreso.tipo === 'verde' ? 'bg-green-500/20 text-green-300' :
|
||||
ingreso.tipo === 'mojado' ? 'bg-blue-500/20 text-blue-300' :
|
||||
ingreso.tipo === 'oreado' ? 'bg-yellow-500/20 text-yellow-300' :
|
||||
'bg-gray-500/20 text-gray-300'
|
||||
]">
|
||||
{{ ingreso.tipo || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 px-2 text-right text-[var(--brand-text)]">
|
||||
{{ ingreso.peso_neto ? ingreso.peso_neto.toFixed(2) : '-' }} lb
|
||||
</td>
|
||||
<td class="py-2 px-2 text-right text-[var(--brand-text)]">
|
||||
L {{ ingreso.precio ? ingreso.precio.toFixed(2) : '-' }}
|
||||
</td>
|
||||
<td class="py-2 px-2 text-right font-semibold text-[var(--brand-primary)]">
|
||||
L {{ ingreso.total_a_pagar ? ingreso.total_a_pagar.toFixed(2) : '-' }}
|
||||
</td>
|
||||
<td class="py-2 px-2 text-center">
|
||||
<span :class="[
|
||||
'inline-flex px-2 py-0.5 rounded text-xs font-medium',
|
||||
ingreso.estado === 'pagado' ? 'bg-green-500/20 text-green-300' :
|
||||
ingreso.estado === 'pendiente' ? 'bg-yellow-500/20 text-yellow-300' :
|
||||
ingreso.estado === 'anulado' ? 'bg-red-500/20 text-red-300' :
|
||||
'bg-gray-500/20 text-gray-300'
|
||||
]">
|
||||
{{ ingreso.estado || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="py-8 text-center text-sm text-[var(--brand-text-muted)]">
|
||||
No hay ingresos disponibles con los filtros actuales
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Top 10 Clientes -->
|
||||
<UCard v-if="pageSections.top10Clientes" class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold brand-section-title">Top 10 Clientes</h2>
|
||||
<p class="text-xs text-[var(--brand-text-muted)] mt-1">
|
||||
Clientes ordenados por monto total de ingresos
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="py-8 text-center text-sm text-[var(--brand-text-muted)]">
|
||||
<p>Ranking de clientes próximamente</p>
|
||||
<p class="mt-2">{{ data.listaClientes?.length || 0 }} clientes disponibles</p>
|
||||
|
||||
<div v-if="data.listaClientes && data.listaClientes.length > 0" class="space-y-3">
|
||||
<div
|
||||
v-for="(cliente, index) in data.listaClientes.slice(0, 10)"
|
||||
:key="cliente.cliente_id"
|
||||
class="flex items-center gap-3 p-3 rounded-lg border border-[var(--brand-border)] hover:border-[#c08040]/50 transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-[var(--brand-primary)]/20 text-[var(--brand-primary)] font-bold text-sm">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-[var(--brand-text)] truncate">
|
||||
{{ cliente.cliente_nombre || 'Sin nombre' }}
|
||||
</div>
|
||||
<div class="text-xs text-[var(--brand-text-muted)]">
|
||||
{{ cliente.cliente_cedula || 'Sin cédula' }} · {{ cliente.cliente_ubicacion || 'Sin ubicación' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm font-bold text-[var(--brand-primary)]">
|
||||
L {{ cliente.total_pagado ? cliente.total_pagado.toFixed(2) : '0.00' }}
|
||||
</div>
|
||||
<div class="text-xs text-[var(--brand-text-muted)]">
|
||||
{{ cliente.num_ingresos || 0 }} ingresos · {{ cliente.total_qq_seco ? cliente.total_qq_seco.toFixed(2) : '0.00' }} qq
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="py-8 text-center text-sm text-[var(--brand-text-muted)]">
|
||||
No hay clientes disponibles con los filtros actuales
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<div v-if="pageSections.graficas" class="space-y-6">
|
||||
<!-- Gráficas: Series Temporales -->
|
||||
<div v-if="pageSections.graficas && data.serieTemporal && data.serieTemporal.length > 0" class="space-y-6">
|
||||
<UCard class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
<h2 class="text-xl font-bold brand-section-title">Gráficas y Análisis</h2>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold brand-section-title">Serie Temporal Acumulada</h2>
|
||||
<p class="text-xs text-[var(--brand-text-muted)] mt-1">
|
||||
Evolución de ingresos en el tiempo ({{ data.serieTemporal.length }} puntos de datos)
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="py-8 text-center text-sm text-[var(--brand-text-muted)]">
|
||||
<p>Gráficas de series temporales próximamente</p>
|
||||
<p class="mt-2">{{ data.serieTemporal?.length || 0 }} puntos de datos disponibles</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Tabla de serie temporal -->
|
||||
<div class="overflow-x-auto max-h-96">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="sticky top-0 bg-[#1c140c]">
|
||||
<tr class="border-b border-[var(--brand-border)]">
|
||||
<th class="text-left py-2 px-2 font-semibold text-[var(--brand-text-muted)]">Fecha</th>
|
||||
<th class="text-left py-2 px-2 font-semibold text-[var(--brand-text-muted)]">Tipo</th>
|
||||
<th class="text-left py-2 px-2 font-semibold text-[var(--brand-text-muted)]">Estado</th>
|
||||
<th class="text-right py-2 px-2 font-semibold text-[var(--brand-text-muted)]">Ingresos</th>
|
||||
<th class="text-right py-2 px-2 font-semibold text-[var(--brand-text-muted)]">Peso Seco (qq)</th>
|
||||
<th class="text-right py-2 px-2 font-semibold text-[var(--brand-text-muted)]">Inversión</th>
|
||||
<th class="text-right py-2 px-2 font-semibold text-[var(--brand-text-muted)]">Acum. qq</th>
|
||||
<th class="text-right py-2 px-2 font-semibold text-[var(--brand-text-muted)]">Acum. L</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(punto, idx) in data.serieTemporal"
|
||||
:key="`${punto.fecha_grupo}-${punto.tipo}-${punto.estado}-${idx}`"
|
||||
class="border-b border-[var(--brand-border)]/30 hover:bg-[#1c140c] transition-colors"
|
||||
>
|
||||
<td class="py-2 px-2 text-[var(--brand-text)]">
|
||||
{{ punto.fecha_grupo ? new Date(punto.fecha_grupo).toLocaleDateString('es-ES') : '-' }}
|
||||
</td>
|
||||
<td class="py-2 px-2">
|
||||
<span :class="[
|
||||
'inline-flex px-2 py-0.5 rounded text-xs font-medium',
|
||||
punto.tipo === 'uva' ? 'bg-purple-500/20 text-purple-300' :
|
||||
punto.tipo === 'verde' ? 'bg-green-500/20 text-green-300' :
|
||||
punto.tipo === 'mojado' ? 'bg-blue-500/20 text-blue-300' :
|
||||
punto.tipo === 'oreado' ? 'bg-yellow-500/20 text-yellow-300' :
|
||||
'bg-gray-500/20 text-gray-300'
|
||||
]">
|
||||
{{ punto.tipo || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 px-2">
|
||||
<span :class="[
|
||||
'inline-flex px-2 py-0.5 rounded text-xs font-medium',
|
||||
punto.estado === 'pagado' ? 'bg-green-500/20 text-green-300' :
|
||||
punto.estado === 'pendiente' ? 'bg-yellow-500/20 text-yellow-300' :
|
||||
'bg-gray-500/20 text-gray-300'
|
||||
]">
|
||||
{{ punto.estado || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 px-2 text-right text-[var(--brand-text)]">
|
||||
{{ punto.num_ingresos_periodo || 0 }}
|
||||
</td>
|
||||
<td class="py-2 px-2 text-right text-[var(--brand-text)]">
|
||||
{{ punto.peso_seco_periodo ? punto.peso_seco_periodo.toFixed(2) : '0.00' }}
|
||||
</td>
|
||||
<td class="py-2 px-2 text-right font-semibold text-[var(--brand-primary)]">
|
||||
L {{ punto.inversion_periodo ? punto.inversion_periodo.toFixed(2) : '0.00' }}
|
||||
</td>
|
||||
<td class="py-2 px-2 text-right text-cyan-400">
|
||||
{{ punto.peso_seco_acumulado ? punto.peso_seco_acumulado.toFixed(2) : '0.00' }}
|
||||
</td>
|
||||
<td class="py-2 px-2 text-right font-bold text-cyan-400">
|
||||
L {{ punto.inversion_acumulada ? punto.inversion_acumulada.toFixed(2) : '0.00' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user