mejoras ui/ux
This commit is contained in:
@@ -37,10 +37,10 @@
|
||||
<div class="flex items-end">
|
||||
<UButton
|
||||
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
|
||||
@click="clearPreset"
|
||||
@click="setToday"
|
||||
size="sm"
|
||||
>
|
||||
Limpiar
|
||||
Resetear a Hoy
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,11 +131,9 @@ function onManualDateChange(type: 'desde' | 'hasta', event: Event) {
|
||||
console.log(`Manual date change (${type}):`, value, 'preset set to custom')
|
||||
}
|
||||
|
||||
function clearPreset() {
|
||||
emit('update:selectedPreset', '')
|
||||
emit('update:fechaDesde', null)
|
||||
emit('update:fechaHasta', null)
|
||||
console.log('Preset cleared')
|
||||
function setToday() {
|
||||
selectPreset('hoy')
|
||||
console.log('Preset reset to today')
|
||||
}
|
||||
|
||||
// Watch para aplicar el preset cuando cambia (incluyendo el valor inicial)
|
||||
|
||||
@@ -102,10 +102,13 @@
|
||||
<span class="text-xs text-blue-400">
|
||||
(Día {{ primerDiaSeleccionado }})
|
||||
</span>
|
||||
<span class="text-xs text-[var(--brand-text-muted)] italic ml-2">
|
||||
• Click otra celda para completar
|
||||
</span>
|
||||
</div>
|
||||
<!-- Estado inicial -->
|
||||
<span v-else class="text-xs text-[var(--brand-text-muted)] italic">
|
||||
Click en una celda para iniciar selección
|
||||
Click en una celda para iniciar selección • Click derecho para cancelar
|
||||
</span>
|
||||
<UButton
|
||||
v-if="rangoSeleccionado || primerDiaSeleccionado !== null"
|
||||
@@ -118,57 +121,69 @@
|
||||
</div>
|
||||
|
||||
<label class="text-xs text-[var(--brand-text-muted)] ml-4">Métrica:</label>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button
|
||||
@click="metrica = 'peso'"
|
||||
@click="metrica = 'total_peso_seco'"
|
||||
class="px-3 py-1 rounded text-xs transition-all"
|
||||
:class="metrica === 'peso'
|
||||
:class="metrica === 'total_peso_seco'
|
||||
? 'bg-[#c08040] text-[#1b1209]'
|
||||
: 'bg-[var(--brand-bg-secondary)] text-[var(--brand-text-muted)] hover:text-[var(--brand-text)]'"
|
||||
>
|
||||
Peso (qq)
|
||||
Peso Seco (qq)
|
||||
</button>
|
||||
<button
|
||||
@click="metrica = 'cantidad'"
|
||||
@click="metrica = 'peso_neto_uva'"
|
||||
class="px-3 py-1 rounded text-xs transition-all"
|
||||
:class="metrica === 'cantidad'
|
||||
:class="metrica === 'peso_neto_uva'
|
||||
? 'bg-[#c08040] text-[#1b1209]'
|
||||
: 'bg-[var(--brand-bg-secondary)] text-[var(--brand-text-muted)] hover:text-[var(--brand-text)]'"
|
||||
>
|
||||
Cantidad
|
||||
Peso Neto Uva (qq)
|
||||
</button>
|
||||
<button
|
||||
@click="metrica = 'inversion'"
|
||||
@click="metrica = 'peso_neto_verde'"
|
||||
class="px-3 py-1 rounded text-xs transition-all"
|
||||
:class="metrica === 'inversion'
|
||||
:class="metrica === 'peso_neto_verde'
|
||||
? 'bg-[#c08040] text-[#1b1209]'
|
||||
: 'bg-[var(--brand-bg-secondary)] text-[var(--brand-text-muted)] hover:text-[var(--brand-text)]'"
|
||||
>
|
||||
Inversión (L)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="text-xs text-[var(--brand-text-muted)] ml-4">Tipo:</label>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="tipoSeleccionado = 'todos'"
|
||||
class="px-3 py-1 rounded text-xs transition-all"
|
||||
:class="tipoSeleccionado === 'todos'
|
||||
? 'bg-[#c08040] text-[#1b1209]'
|
||||
: 'bg-[var(--brand-bg-secondary)] text-[var(--brand-text-muted)] hover:text-[var(--brand-text)]'"
|
||||
>
|
||||
Todos
|
||||
Peso Neto Verde (qq)
|
||||
</button>
|
||||
<button
|
||||
v-for="tipo in ['uva', 'verde', 'oreado', 'mojado']"
|
||||
:key="tipo"
|
||||
@click="tipoSeleccionado = tipo"
|
||||
class="px-3 py-1 rounded text-xs transition-all capitalize"
|
||||
:class="tipoSeleccionado === tipo
|
||||
@click="metrica = 'sacos_total_dia'"
|
||||
class="px-3 py-1 rounded text-xs transition-all"
|
||||
:class="metrica === 'sacos_total_dia'
|
||||
? 'bg-[#c08040] text-[#1b1209]'
|
||||
: 'bg-[var(--brand-bg-secondary)] text-[var(--brand-text-muted)] hover:text-[var(--brand-text)]'"
|
||||
>
|
||||
{{ tipo }}
|
||||
Sacos Total
|
||||
</button>
|
||||
<button
|
||||
@click="metrica = 'total_lempiras_uva'"
|
||||
class="px-3 py-1 rounded text-xs transition-all"
|
||||
:class="metrica === 'total_lempiras_uva'
|
||||
? 'bg-[#c08040] text-[#1b1209]'
|
||||
: 'bg-[var(--brand-bg-secondary)] text-[var(--brand-text-muted)] hover:text-[var(--brand-text)]'"
|
||||
>
|
||||
Lempiras Uva
|
||||
</button>
|
||||
<button
|
||||
@click="metrica = 'total_lempiras_verde'"
|
||||
class="px-3 py-1 rounded text-xs transition-all"
|
||||
:class="metrica === 'total_lempiras_verde'
|
||||
? 'bg-[#c08040] text-[#1b1209]'
|
||||
: 'bg-[var(--brand-bg-secondary)] text-[var(--brand-text-muted)] hover:text-[var(--brand-text)]'"
|
||||
>
|
||||
Lempiras Verde
|
||||
</button>
|
||||
<button
|
||||
@click="metrica = 'total_lempiras_mojado_oreado'"
|
||||
class="px-3 py-1 rounded text-xs transition-all"
|
||||
:class="metrica === 'total_lempiras_mojado_oreado'
|
||||
? 'bg-[#c08040] text-[#1b1209]'
|
||||
: 'bg-[var(--brand-bg-secondary)] text-[var(--brand-text-muted)] hover:text-[var(--brand-text)]'"
|
||||
>
|
||||
Lempiras Mojado+Oreado
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,20 +194,49 @@
|
||||
<div class="min-w-max">
|
||||
<!-- Vista Heatmap -->
|
||||
<template v-if="vistaMode === 'heatmap'">
|
||||
<!-- Leyenda de totales -->
|
||||
<div class="flex items-center gap-3 mb-3 px-2 py-2 rounded-lg bg-[var(--brand-bg-secondary)]/50 border border-[var(--brand-border)]/30">
|
||||
<span class="text-[10px] text-[var(--brand-text-muted)] uppercase font-semibold">Leyenda:</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="w-2 h-2 rounded-full bg-[#c08040]"></div>
|
||||
<span class="text-[10px] text-[var(--brand-text-muted)]">Total completo</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="w-2 h-2 rounded-full bg-blue-400"></div>
|
||||
<span class="text-[10px] text-[var(--brand-text-muted)]">Acumulado hasta hoy (día {{ diaActualRelativo }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header con labels de cosechas -->
|
||||
<div class="flex gap-1 mb-2">
|
||||
<div class="w-16 flex-shrink-0"></div>
|
||||
<div
|
||||
v-for="(cosecha, index) in datosCosechas"
|
||||
:key="cosecha.id"
|
||||
class="flex flex-col items-center"
|
||||
class="flex flex-col items-center gap-1 p-2 rounded-lg bg-[var(--brand-bg-secondary)]"
|
||||
:style="{ width: `${cellWidth}px` }"
|
||||
>
|
||||
<div class="text-xs font-medium text-[var(--brand-text)] mb-1">
|
||||
<div class="text-xs font-semibold text-[var(--brand-text)]">
|
||||
{{ cosecha.label }}
|
||||
</div>
|
||||
<div class="text-xs text-[var(--brand-text-muted)]">
|
||||
{{ formatTotal(cosecha.total) }}
|
||||
<div class="flex flex-col items-center gap-0.5">
|
||||
<div class="text-xs font-medium text-[#c08040]" :title="`Total completo de ${cosecha.label}`">
|
||||
{{ formatTotal(cosecha.total) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="cosecha.totalALaFecha !== null"
|
||||
class="text-[10px] text-blue-400 font-medium"
|
||||
:title="`Acumulado hasta el día ${diaActualRelativo} (equivalente a hoy)`"
|
||||
>
|
||||
↗ {{ formatTotal(cosecha.totalALaFecha) }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-[10px] text-gray-500 italic"
|
||||
title="Esta cosecha aún no ha llegado a este día del año"
|
||||
>
|
||||
Sin datos
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,6 +272,10 @@
|
||||
:key="`${cosecha.id}-${dia}`"
|
||||
class="border-r border-b transition-all cursor-pointer relative"
|
||||
:class="[
|
||||
// Marcador del día actual
|
||||
dia - 1 === diaActualRelativo
|
||||
? 'border-l-2 border-l-blue-400'
|
||||
: '',
|
||||
// Rango seleccionado (naranja)
|
||||
isInSelectedRange(dia - 1)
|
||||
? 'ring-2 ring-[#c08040] border-[#c08040] z-10'
|
||||
@@ -243,6 +291,7 @@
|
||||
backgroundColor: getCellColor(cosecha.valoresPorDia[dia - 1] || 0, maxValorDia)
|
||||
}"
|
||||
@click="handleCellClick(dia - 1)"
|
||||
@contextmenu.prevent="cancelarSeleccion"
|
||||
@mouseenter="showTooltip($event, cosecha, dia - 1)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
@@ -251,6 +300,11 @@
|
||||
v-if="primerDiaSeleccionado === dia - 1 && rangoSeleccionado === null"
|
||||
class="absolute inset-0 bg-blue-500/30 pointer-events-none"
|
||||
/>
|
||||
<!-- Indicador del día actual -->
|
||||
<div
|
||||
v-if="dia - 1 === diaActualRelativo"
|
||||
class="absolute left-0 top-0 bottom-0 w-0.5 bg-blue-400 pointer-events-none z-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,20 +313,49 @@
|
||||
|
||||
<!-- Vista Barras -->
|
||||
<template v-else-if="vistaMode === 'barras'">
|
||||
<!-- Leyenda de totales -->
|
||||
<div class="flex items-center gap-3 mb-3 px-2 py-2 rounded-lg bg-[var(--brand-bg-secondary)]/50 border border-[var(--brand-border)]/30">
|
||||
<span class="text-[10px] text-[var(--brand-text-muted)] uppercase font-semibold">Leyenda:</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="w-2 h-2 rounded-full bg-[#c08040]"></div>
|
||||
<span class="text-[10px] text-[var(--brand-text-muted)]">Total completo</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="w-2 h-2 rounded-full bg-blue-400"></div>
|
||||
<span class="text-[10px] text-[var(--brand-text-muted)]">Acumulado hasta hoy (día {{ diaActualRelativo }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header con labels de cosechas -->
|
||||
<div class="flex gap-1 mb-2">
|
||||
<div class="w-16 flex-shrink-0"></div>
|
||||
<div
|
||||
v-for="(cosecha, index) in datosCosechas"
|
||||
:key="cosecha.id"
|
||||
class="flex flex-col items-center"
|
||||
class="flex flex-col items-center gap-1 p-2 rounded-lg bg-[var(--brand-bg-secondary)]"
|
||||
:style="{ width: `${barMaxWidth}px` }"
|
||||
>
|
||||
<div class="text-xs font-medium mb-1" :style="{ color: getCosechaColor(cosecha.id) }">
|
||||
<div class="text-xs font-semibold" :style="{ color: getCosechaColor(cosecha.id) }">
|
||||
{{ cosecha.label }}
|
||||
</div>
|
||||
<div class="text-xs text-[var(--brand-text-muted)]">
|
||||
{{ formatTotal(cosecha.total) }}
|
||||
<div class="flex flex-col items-center gap-0.5">
|
||||
<div class="text-xs font-medium text-[#c08040]" :title="`Total completo de ${cosecha.label}`">
|
||||
{{ formatTotal(cosecha.total) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="cosecha.totalALaFecha !== null"
|
||||
class="text-[10px] text-blue-400 font-medium"
|
||||
:title="`Acumulado hasta el día ${diaActualRelativo} (equivalente a hoy)`"
|
||||
>
|
||||
↗ {{ formatTotal(cosecha.totalALaFecha) }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-[10px] text-gray-500 italic"
|
||||
title="Esta cosecha aún no ha llegado a este día del año"
|
||||
>
|
||||
Sin datos
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -309,6 +392,10 @@
|
||||
:key="`${cosecha.id}-${dia}`"
|
||||
class="relative border-b cursor-pointer group"
|
||||
:class="[
|
||||
// Marcador del día actual
|
||||
dia - 1 === diaActualRelativo
|
||||
? 'border-l-2 border-l-blue-400'
|
||||
: '',
|
||||
// Rango seleccionado (naranja)
|
||||
isInSelectedRange(dia - 1)
|
||||
? 'border-[#c08040] border-2 bg-[#c08040]/10'
|
||||
@@ -320,6 +407,7 @@
|
||||
]"
|
||||
:style="{ height: `${barHeight}px` }"
|
||||
@click="handleCellClick(dia - 1)"
|
||||
@contextmenu.prevent="cancelarSeleccion"
|
||||
@mouseenter="showTooltip($event, cosecha, dia - 1)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
@@ -332,6 +420,11 @@
|
||||
backgroundColor: getCosechaColor(cosecha.id)
|
||||
}"
|
||||
/>
|
||||
<!-- Indicador del día actual -->
|
||||
<div
|
||||
v-if="dia - 1 === diaActualRelativo"
|
||||
class="absolute left-0 top-0 bottom-0 w-0.5 bg-blue-400 pointer-events-none z-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -465,10 +558,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IngresoRecord } from '~/composables/useIngresosMetrics'
|
||||
// Tipo para los registros de vista_resumen_ingresos
|
||||
interface ResumenIngresoRecord {
|
||||
fecha: string
|
||||
created_at?: string
|
||||
total_peso_seco: number
|
||||
peso_neto_uva: number
|
||||
peso_neto_verde: number
|
||||
sacos_total_dia: number
|
||||
total_lempiras_uva: number
|
||||
total_lempiras_verde: number
|
||||
total_lempiras_mojado: number
|
||||
total_lempiras_oreado: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ingresos: IngresoRecord[]
|
||||
ingresos: ResumenIngresoRecord[]
|
||||
cosechasSeleccionadas: string[]
|
||||
}
|
||||
|
||||
@@ -494,10 +599,10 @@ const isFullscreen = ref(false)
|
||||
// Estado de zoom (0.5x a 2x)
|
||||
const nivelZoom = ref(1)
|
||||
|
||||
// Selección de vista, métrica y tipo
|
||||
// Selección de vista y métrica
|
||||
const vistaMode = ref<'heatmap' | 'barras'>('barras')
|
||||
const metrica = ref<'peso' | 'cantidad' | 'inversion'>('peso')
|
||||
const tipoSeleccionado = ref<string>('todos')
|
||||
type MetricaType = 'total_peso_seco' | 'peso_neto_uva' | 'peso_neto_verde' | 'sacos_total_dia' | 'total_lempiras_uva' | 'total_lempiras_verde' | 'total_lempiras_mojado_oreado'
|
||||
const metrica = ref<MetricaType>('total_peso_seco')
|
||||
|
||||
// Sistema de selección interactiva de rango
|
||||
const primerDiaSeleccionado = ref<number | null>(null)
|
||||
@@ -531,35 +636,57 @@ const tooltipX = ref(0)
|
||||
const tooltipY = ref(0)
|
||||
const tooltipData = ref({ cosecha: '', dia: 0, valor: '', fecha: '' })
|
||||
|
||||
// Calcular datos por cosecha
|
||||
// Calcular el día actual relativo desde el inicio del año de cosecha (8 de septiembre)
|
||||
const diaActualRelativo = computed(() => {
|
||||
const hoy = new Date()
|
||||
|
||||
// Fecha de inicio del año de cosecha actual (8 de septiembre del año correspondiente)
|
||||
let anioInicioCosecha = hoy.getFullYear()
|
||||
const inicioCosechaEsteAnio = new Date(anioInicioCosecha, 8, 8) // 8 de septiembre
|
||||
|
||||
// Si aún no hemos llegado al 8 de septiembre de este año, el inicio fue el año pasado
|
||||
if (hoy < inicioCosechaEsteAnio) {
|
||||
anioInicioCosecha--
|
||||
}
|
||||
|
||||
const inicioCosecha = new Date(anioInicioCosecha, 8, 8) // 8 de septiembre
|
||||
const diaRelativo = Math.floor((hoy.getTime() - inicioCosecha.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
return diaRelativo
|
||||
})
|
||||
|
||||
// Calcular datos por cosecha usando vista_resumen_ingresos
|
||||
const datosCosechas = computed(() => {
|
||||
return props.cosechasSeleccionadas.map(cosechaId => {
|
||||
const cosechaDef = cosechasDisponibles.find(c => c.id === cosechaId)
|
||||
if (!cosechaDef) return null
|
||||
|
||||
const ingresosEnCosecha = props.ingresos.filter(ingreso => {
|
||||
if (!ingreso.created_at) return false
|
||||
const fecha = new Date(ingreso.created_at)
|
||||
// Filtrar registros que pertenecen a esta cosecha
|
||||
const registrosEnCosecha = props.ingresos.filter(registro => {
|
||||
const fechaStr = registro.fecha || registro.created_at
|
||||
if (!fechaStr) return false
|
||||
|
||||
const fecha = new Date(fechaStr)
|
||||
const inicio = new Date(cosechaDef.fechaInicio)
|
||||
const fin = new Date(cosechaDef.fechaFin)
|
||||
|
||||
// Filtrar por tipo si no es "todos"
|
||||
const cumpleTipo = tipoSeleccionado.value === 'todos' || ingreso.tipo === tipoSeleccionado.value
|
||||
|
||||
return fecha >= inicio && fecha <= fin && cumpleTipo
|
||||
return fecha >= inicio && fecha <= fin
|
||||
})
|
||||
|
||||
// Ordenar por fecha
|
||||
const ingresosOrdenados = ingresosEnCosecha.sort((a, b) => {
|
||||
return new Date(a.created_at!).getTime() - new Date(b.created_at!).getTime()
|
||||
const registrosOrdenados = registrosEnCosecha.sort((a, b) => {
|
||||
const fechaA = new Date(a.fecha || a.created_at!)
|
||||
const fechaB = new Date(b.fecha || b.created_at!)
|
||||
return fechaA.getTime() - fechaB.getTime()
|
||||
})
|
||||
|
||||
// Calcular valores por día
|
||||
// Calcular valores por día (ya son 1 registro por día)
|
||||
const fechaInicio = new Date(cosechaDef.fechaInicio)
|
||||
const valoresPorDia: number[] = []
|
||||
|
||||
ingresosOrdenados.forEach(ingreso => {
|
||||
const fecha = new Date(ingreso.created_at!)
|
||||
registrosOrdenados.forEach(registro => {
|
||||
const fechaStr = registro.fecha || registro.created_at!
|
||||
const fecha = new Date(fechaStr)
|
||||
const diaRelativo = Math.floor((fecha.getTime() - fechaInicio.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
// Asegurar que el array tenga suficiente espacio
|
||||
@@ -567,21 +694,26 @@ const datosCosechas = computed(() => {
|
||||
valoresPorDia.push(0)
|
||||
}
|
||||
|
||||
// Calcular valor según métrica
|
||||
// Obtener valor según métrica (ya están agregados por día)
|
||||
let valor = 0
|
||||
if (metrica.value === 'peso') {
|
||||
valor = ingreso.peso_neto
|
||||
} else if (metrica.value === 'cantidad') {
|
||||
valor = 1
|
||||
} else if (metrica.value === 'inversion') {
|
||||
if (ingreso.tipo === 'uva' || ingreso.tipo === 'verde') {
|
||||
valor = ingreso.precio * ingreso.peso_neto
|
||||
} else if (ingreso.tipo === 'oreado' || ingreso.tipo === 'mojado') {
|
||||
valor = (ingreso.precio / 2) * (ingreso.peso_seco || 0)
|
||||
}
|
||||
if (metrica.value === 'total_peso_seco') {
|
||||
valor = registro.total_peso_seco || 0
|
||||
} else if (metrica.value === 'peso_neto_uva') {
|
||||
valor = registro.peso_neto_uva || 0
|
||||
} else if (metrica.value === 'peso_neto_verde') {
|
||||
valor = registro.peso_neto_verde || 0
|
||||
} else if (metrica.value === 'sacos_total_dia') {
|
||||
valor = registro.sacos_total_dia || 0
|
||||
} else if (metrica.value === 'total_lempiras_uva') {
|
||||
valor = registro.total_lempiras_uva || 0
|
||||
} else if (metrica.value === 'total_lempiras_verde') {
|
||||
valor = registro.total_lempiras_verde || 0
|
||||
} else if (metrica.value === 'total_lempiras_mojado_oreado') {
|
||||
// Combinar mojado + oreado
|
||||
valor = (registro.total_lempiras_mojado || 0) + (registro.total_lempiras_oreado || 0)
|
||||
}
|
||||
|
||||
valoresPorDia[diaRelativo] += valor
|
||||
valoresPorDia[diaRelativo] = valor
|
||||
})
|
||||
|
||||
// Calcular total considerando el rango seleccionado
|
||||
@@ -615,17 +747,30 @@ const datosCosechas = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Calcular total acumulado hasta la fecha actual (día relativo de hoy)
|
||||
let totalALaFecha: number | null = null
|
||||
|
||||
// Solo calcular si el día actual está dentro del rango de datos disponibles
|
||||
if (diaActualRelativo.value >= 0 && diaActualRelativo.value < valoresPorDia.length) {
|
||||
totalALaFecha = 0
|
||||
for (let i = 0; i <= diaActualRelativo.value; i++) {
|
||||
totalALaFecha += valoresPorDia[i] || 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: cosechaId,
|
||||
label: cosechaDef.label.replace('Cosecha ', ''),
|
||||
valoresPorDia,
|
||||
total
|
||||
total,
|
||||
totalALaFecha
|
||||
}
|
||||
}).filter(Boolean) as Array<{
|
||||
id: string
|
||||
label: string
|
||||
valoresPorDia: number[]
|
||||
total: number
|
||||
totalALaFecha: number | null
|
||||
}>
|
||||
})
|
||||
|
||||
@@ -784,22 +929,28 @@ function hideTooltip() {
|
||||
}
|
||||
|
||||
function formatTooltipValue(valor: number): string {
|
||||
if (metrica.value === 'peso') {
|
||||
return `${valor.toLocaleString('es-HN', { maximumFractionDigits: 2, minimumFractionDigits: 2 })} qq`
|
||||
} else if (metrica.value === 'cantidad') {
|
||||
return `${valor.toLocaleString('es-HN', { maximumFractionDigits: 0 })} ingresos`
|
||||
const formatted = valor.toLocaleString('es-HN', { maximumFractionDigits: 2, minimumFractionDigits: 2 })
|
||||
|
||||
if (metrica.value === 'total_peso_seco' || metrica.value === 'peso_neto_uva' || metrica.value === 'peso_neto_verde') {
|
||||
return `${formatted} qq`
|
||||
} else if (metrica.value === 'sacos_total_dia') {
|
||||
return `${Math.round(valor).toLocaleString('es-HN')} sacos`
|
||||
} else {
|
||||
return `L ${valor.toLocaleString('es-HN', { maximumFractionDigits: 2, minimumFractionDigits: 2 })}`
|
||||
// Lempiras
|
||||
return `L ${formatted}`
|
||||
}
|
||||
}
|
||||
|
||||
function formatTotal(total: number): string {
|
||||
if (metrica.value === 'peso') {
|
||||
return `${total.toLocaleString('es-HN', { maximumFractionDigits: 2, minimumFractionDigits: 2 })} qq`
|
||||
} else if (metrica.value === 'cantidad') {
|
||||
return `${total.toLocaleString('es-HN', { maximumFractionDigits: 0 })} ing.`
|
||||
const formatted = total.toLocaleString('es-HN', { maximumFractionDigits: 2, minimumFractionDigits: 2 })
|
||||
|
||||
if (metrica.value === 'total_peso_seco' || metrica.value === 'peso_neto_uva' || metrica.value === 'peso_neto_verde') {
|
||||
return `${formatted} qq`
|
||||
} else if (metrica.value === 'sacos_total_dia') {
|
||||
return `${Math.round(total).toLocaleString('es-HN')} sac.`
|
||||
} else {
|
||||
return `L ${total.toLocaleString('es-HN', { maximumFractionDigits: 2, minimumFractionDigits: 2 })}`
|
||||
// Lempiras
|
||||
return `L ${formatted}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,6 +1014,19 @@ function clearRangoSeleccionado() {
|
||||
primerDiaSeleccionado.value = null
|
||||
}
|
||||
|
||||
// Función para cancelar la selección con click derecho
|
||||
function cancelarSeleccion(event: MouseEvent) {
|
||||
// Prevenir el menú contextual del navegador
|
||||
event.preventDefault()
|
||||
|
||||
// Resetear completamente la selección
|
||||
rangoSeleccionado.value = null
|
||||
primerDiaSeleccionado.value = null
|
||||
|
||||
// Ocultar tooltip si está visible
|
||||
hideTooltip()
|
||||
}
|
||||
|
||||
// Función para formatear fecha en formato legible
|
||||
function formatRangoFecha(diaRelativo: number): string {
|
||||
// Obtener la primera cosecha para calcular la fecha
|
||||
|
||||
@@ -22,28 +22,19 @@
|
||||
</Transition>
|
||||
|
||||
<!-- Fila 2: Selector de Rango de Fechas -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<div v-if="!fechaDesde && !fechaHasta" class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text-muted)] uppercase tracking-wide">
|
||||
Rango de Fechas
|
||||
</h3>
|
||||
<DateRangeSelector
|
||||
:selected-preset="selectedPreset"
|
||||
:fecha-desde="fechaDesde"
|
||||
:fecha-hasta="fechaHasta"
|
||||
@update:selected-preset="emit('update:selectedPreset', $event)"
|
||||
@update:fecha-desde="emit('update:fechaDesde', $event)"
|
||||
@update:fecha-hasta="emit('update:fechaHasta', $event)"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-semibold text-[var(--brand-text-muted)] uppercase tracking-wide">
|
||||
Rango de Fechas
|
||||
</h3>
|
||||
<DateRangeSelector
|
||||
:selected-preset="selectedPreset"
|
||||
:fecha-desde="fechaDesde"
|
||||
:fecha-hasta="fechaHasta"
|
||||
@update:selected-preset="emit('update:selectedPreset', $event)"
|
||||
@update:fecha-desde="emit('update:fechaDesde', $event)"
|
||||
@update:fecha-hasta="emit('update:fechaHasta', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Fila 3: Filtros Avanzados (grid de 4 columnas) -->
|
||||
<div v-if="hasAvailableAdvancedFilters" class="flex flex-col gap-3">
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
@click="limpiarTodos"
|
||||
icon="i-lucide-x"
|
||||
@click="resetearAHoy"
|
||||
icon="i-lucide-rotate-ccw"
|
||||
>
|
||||
Limpiar todos
|
||||
Resetear a Hoy
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +69,7 @@
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-purple-500/10 text-purple-400 border border-purple-500/30 transition-all hover:bg-purple-500/20"
|
||||
>
|
||||
<UIcon name="i-lucide-calendar" class="size-3" />
|
||||
<span>{{ fechaDesde || '—' }} → {{ fechaHasta || '—' }}</span>
|
||||
<span>{{ getPresetLabel() }}</span>
|
||||
<button
|
||||
@click="removeFechas"
|
||||
class="ml-1 hover:text-purple-300 transition-colors"
|
||||
@@ -179,6 +179,7 @@
|
||||
interface Props {
|
||||
clientes: any[]
|
||||
selectedClienteIds: number[]
|
||||
selectedPreset: string
|
||||
fechaDesde: string | null
|
||||
fechaHasta: string | null
|
||||
selectedTipos: string[]
|
||||
@@ -244,6 +245,29 @@ function getLabelEstado(value: string): string {
|
||||
return estado?.label || value
|
||||
}
|
||||
|
||||
function getPresetLabel(): string {
|
||||
// Si es un preset conocido, mostrar su nombre
|
||||
const presetLabels: Record<string, string> = {
|
||||
'hoy': 'Hoy',
|
||||
'semana': 'Esta Semana',
|
||||
'mes': 'Este Mes',
|
||||
'ytd': 'YTD',
|
||||
'cosecha-20-21': 'Cosecha 20-21',
|
||||
'cosecha-21-22': 'Cosecha 21-22',
|
||||
'cosecha-22-23': 'Cosecha 22-23',
|
||||
'cosecha-23-24': 'Cosecha 23-24',
|
||||
'cosecha-24-25': 'Cosecha 24-25',
|
||||
'cosecha-25-26': 'Cosecha 25-26'
|
||||
}
|
||||
|
||||
if (props.selectedPreset && props.selectedPreset !== 'custom' && presetLabels[props.selectedPreset]) {
|
||||
return presetLabels[props.selectedPreset]
|
||||
}
|
||||
|
||||
// Si es personalizado, mostrar las fechas
|
||||
return `${props.fechaDesde || '—'} → ${props.fechaHasta || '—'}`
|
||||
}
|
||||
|
||||
function removeCliente(id: number) {
|
||||
emit('update:selectedClienteIds', props.selectedClienteIds.filter(cid => cid !== id))
|
||||
}
|
||||
@@ -278,11 +302,9 @@ function removeNoFilter() {
|
||||
emit('update:noFilter', false)
|
||||
}
|
||||
|
||||
function limpiarTodos() {
|
||||
function resetearAHoy() {
|
||||
emit('update:selectedClienteIds', [])
|
||||
emit('update:fechaDesde', null)
|
||||
emit('update:fechaHasta', null)
|
||||
emit('update:selectedPreset', '')
|
||||
emit('update:selectedPreset', 'hoy')
|
||||
emit('update:selectedTipos', [])
|
||||
emit('update:selectedEstados', [])
|
||||
emit('update:selectedUbicaciones', [])
|
||||
|
||||
@@ -36,11 +36,6 @@
|
||||
size="xs"
|
||||
label="Evolución"
|
||||
/>
|
||||
<USwitch
|
||||
v-model="pageSections.porTipo"
|
||||
size="xs"
|
||||
label="Por Tipo"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
@@ -76,43 +71,79 @@
|
||||
<label
|
||||
v-for="cosecha in cosechasDisponibles"
|
||||
:key="cosecha.id"
|
||||
class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer transition-all"
|
||||
:class="cosechasSeleccionadas.includes(cosecha.id)
|
||||
? 'border-[#c08040] bg-[#c08040]/10'
|
||||
: 'border-[var(--brand-border)] hover:border-[#c08040]/50'"
|
||||
class="flex items-center gap-2 p-3 rounded-lg border transition-all"
|
||||
:class="[
|
||||
cosecha.disabled
|
||||
? 'border-gray-600/20 bg-gray-800/20 cursor-not-allowed opacity-50'
|
||||
: cosechasSeleccionadas.includes(cosecha.id)
|
||||
? 'border-[#c08040] bg-[#c08040]/10 cursor-pointer'
|
||||
: 'border-[var(--brand-border)] hover:border-[#c08040]/50 cursor-pointer'
|
||||
]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="cosecha.id"
|
||||
v-model="cosechasSeleccionadas"
|
||||
class="rounded border-[var(--brand-border)] text-[#c08040] focus:ring-[#c08040]"
|
||||
:disabled="cosecha.disabled"
|
||||
class="rounded border-[var(--brand-border)] text-[#c08040] focus:ring-[#c08040] disabled:cursor-not-allowed"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-medium text-[var(--brand-text)]">{{ cosecha.label }}</span>
|
||||
<span class="text-xs text-[var(--brand-text-muted)]">{{ cosecha.periodo }}</span>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-medium text-[var(--brand-text)]">{{ cosecha.label }}</span>
|
||||
<span
|
||||
v-if="!cosecha.disabled"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded-full bg-[#c08040]/20 text-[#c08040] font-semibold"
|
||||
>
|
||||
{{ cosecha.registros }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-[var(--brand-text-muted)]">
|
||||
{{ cosecha.disabled ? 'Sin datos' : cosecha.periodo }}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Metadatos de Resumen de Ingresos -->
|
||||
<div v-if="cosechasSeleccionadas.length > 0" class="grid grid-cols-1 gap-5">
|
||||
<UCard class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-lucide-database" class="size-5 text-[#c08040]" />
|
||||
<h3 class="text-base font-semibold text-[var(--brand-text)]">Fuente de Datos: Resumen Diario de Ingresos</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<MetadatosCard v-if="resumenIngresosMetadata" :metadata="resumenIngresosMetadata" :compact="true" />
|
||||
<div class="text-xs text-[var(--brand-text-muted)] p-3 rounded-lg bg-[var(--brand-bg-secondary)] border border-[var(--brand-border)]">
|
||||
<p class="font-semibold mb-2">Métricas utilizadas (registros diarios):</p>
|
||||
<ul class="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>total_peso_seco</strong> (qq)</li>
|
||||
<li><strong>peso_neto_uva</strong> (qq)</li>
|
||||
<li><strong>peso_neto_verde</strong> (qq)</li>
|
||||
<li><strong>sacos_total_dia</strong></li>
|
||||
<li><strong>Lempiras por tipo:</strong> uva, verde, mojado+oreado (combinados)</li>
|
||||
</ul>
|
||||
<p class="mt-2 text-[10px] italic">Todas las cosechas inician el 8 de septiembre y terminan el 7 de septiembre del siguiente año.</p>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<!-- Vista Heatmap -->
|
||||
<div v-if="cosechasSeleccionadas.length > 0 && pageSections.heatmap">
|
||||
<ComparativaCosechasHeatmap :ingresos="ingresos" :cosechas-seleccionadas="cosechasSeleccionadas" />
|
||||
<ComparativaCosechasHeatmap :ingresos="resumenIngresos" :cosechas-seleccionadas="cosechasSeleccionadas" />
|
||||
</div>
|
||||
|
||||
<!-- Resumen General por Cosecha -->
|
||||
<div v-if="cosechasSeleccionadas.length > 0 && pageSections.totales">
|
||||
<ComparativaCosechasTotales :ingresos="ingresos" :cosechas-seleccionadas="cosechasSeleccionadas" />
|
||||
</div>
|
||||
|
||||
<!-- Comparativa por Tipo de Café -->
|
||||
<div v-if="cosechasSeleccionadas.length > 0 && pageSections.porTipo">
|
||||
<ComparativaCosechasPorTipo :ingresos="ingresos" :cosechas-seleccionadas="cosechasSeleccionadas" />
|
||||
<ComparativaCosechasTotales :ingresos="resumenIngresos" :cosechas-seleccionadas="cosechasSeleccionadas" />
|
||||
</div>
|
||||
|
||||
<!-- Evolución Temporal Comparada -->
|
||||
<div v-if="cosechasSeleccionadas.length > 0 && pageSections.evolucion">
|
||||
<ComparativaCosechasEvolucion :ingresos="ingresos" :cosechas-seleccionadas="cosechasSeleccionadas" />
|
||||
<ComparativaCosechasEvolucion :ingresos="resumenIngresos" :cosechas-seleccionadas="cosechasSeleccionadas" />
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
@@ -126,8 +157,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Modal de Configuración de Estilos -->
|
||||
<UModal v-model:open="mostrarConfiguracion">
|
||||
<UCard class="brand-card">
|
||||
<UCard class="brand-card" v-if="mostrarConfiguracion">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -161,7 +191,7 @@
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="(cosecha, index) in cosechasDisponibles"
|
||||
v-for="(cosecha, index) in cosechasDefiniciones"
|
||||
:key="`color-${cosecha.id}`"
|
||||
class="flex items-center gap-3 p-3 rounded-lg border border-[var(--brand-border)] bg-[var(--brand-bg-secondary)]"
|
||||
>
|
||||
@@ -305,12 +335,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTableDataStore } from '~/stores/tableDataFactory'
|
||||
import { useMetadataStore } from '~/stores/metadata'
|
||||
import type { IngresoRecord } from '~/composables/useIngresosMetrics'
|
||||
|
||||
definePageMeta({
|
||||
@@ -322,8 +352,7 @@ definePageMeta({
|
||||
const pageSections = ref({
|
||||
heatmap: true,
|
||||
totales: false,
|
||||
evolucion: false,
|
||||
porTipo: false
|
||||
evolucion: false
|
||||
})
|
||||
|
||||
// Estado del modal de configuración
|
||||
@@ -382,30 +411,131 @@ function resetearConfiguracion() {
|
||||
estilosGraficas.value = { ...estilosGraficasDefault }
|
||||
}
|
||||
|
||||
// Definición de cosechas disponibles
|
||||
const cosechasDisponibles = [
|
||||
{ id: 'cosecha-20-21', label: 'Cosecha 20-21', periodo: 'Sep 2020 - Sep 2021', fechaInicio: '2020-09-25', fechaFin: '2021-09-24' },
|
||||
{ id: 'cosecha-21-22', label: 'Cosecha 21-22', periodo: 'Sep 2021 - Sep 2022', fechaInicio: '2021-09-25', fechaFin: '2022-09-24' },
|
||||
{ id: 'cosecha-22-23', label: 'Cosecha 22-23', periodo: 'Sep 2022 - Sep 2023', fechaInicio: '2022-09-25', fechaFin: '2023-09-24' },
|
||||
{ id: 'cosecha-23-24', label: 'Cosecha 23-24', periodo: 'Sep 2023 - Sep 2024', fechaInicio: '2023-09-25', fechaFin: '2024-09-24' },
|
||||
{ id: 'cosecha-24-25', label: 'Cosecha 24-25', periodo: 'Sep 2024 - Sep 2025', fechaInicio: '2024-09-25', fechaFin: '2025-09-09' },
|
||||
{ id: 'cosecha-25-26', label: 'Cosecha 25-26', periodo: 'Sep 2025 - Hoy', fechaInicio: '2025-09-10', fechaFin: new Date().toISOString().split('T')[0] }
|
||||
// Store de resumen de ingresos (registros por día con métricas agregadas)
|
||||
const resumenIngresosStore = useTableDataStore<any>('vista_resumen_ingresos')
|
||||
|
||||
// Datos de resumen desde el store
|
||||
const resumenIngresos = computed(() => resumenIngresosStore.allRecords)
|
||||
|
||||
// Definición de cosechas disponibles (8 sep - 7 sep)
|
||||
const cosechasDefiniciones = [
|
||||
{ id: 'cosecha-20-21', label: 'Cosecha 20-21', periodo: '8 Sep 2020 - 7 Sep 2021', fechaInicio: '2020-09-08', fechaFin: '2021-09-07' },
|
||||
{ id: 'cosecha-21-22', label: 'Cosecha 21-22', periodo: '8 Sep 2021 - 7 Sep 2022', fechaInicio: '2021-09-08', fechaFin: '2022-09-07' },
|
||||
{ id: 'cosecha-22-23', label: 'Cosecha 22-23', periodo: '8 Sep 2022 - 7 Sep 2023', fechaInicio: '2022-09-08', fechaFin: '2023-09-07' },
|
||||
{ id: 'cosecha-23-24', label: 'Cosecha 23-24', periodo: '8 Sep 2023 - 7 Sep 2024', fechaInicio: '2023-09-08', fechaFin: '2024-09-07' },
|
||||
{ id: 'cosecha-24-25', label: 'Cosecha 24-25', periodo: '8 Sep 2024 - 7 Sep 2025', fechaInicio: '2024-09-08', fechaFin: '2025-09-07' },
|
||||
{ id: 'cosecha-25-26', label: 'Cosecha 25-26', periodo: '8 Sep 2025 - Hoy', fechaInicio: '2025-09-08', fechaFin: new Date().toISOString().split('T')[0] }
|
||||
]
|
||||
|
||||
// Cosechas seleccionadas (por defecto las 3 más recientes)
|
||||
const cosechasSeleccionadas = ref<string[]>(['cosecha-23-24', 'cosecha-24-25'])
|
||||
// Calcular cuántos registros tiene cada cosecha
|
||||
const registrosPorCosecha = computed(() => {
|
||||
const counts: Record<string, number> = {}
|
||||
|
||||
// Store de ingresos
|
||||
const ingresosStore = useTableDataStore<IngresoRecord>('ingresos')
|
||||
cosechasDefiniciones.forEach(cosecha => {
|
||||
const registros = resumenIngresos.value.filter((record: any) => {
|
||||
const fecha = new Date(record.fecha || record.created_at)
|
||||
const inicio = new Date(cosecha.fechaInicio)
|
||||
const fin = new Date(cosecha.fechaFin)
|
||||
return fecha >= inicio && fecha <= fin
|
||||
})
|
||||
counts[cosecha.id] = registros.length
|
||||
})
|
||||
|
||||
// Datos de ingresos desde el store
|
||||
const ingresos = computed(() => ingresosStore.allRecords as IngresoRecord[])
|
||||
return counts
|
||||
})
|
||||
|
||||
// Cosechas disponibles con información de registros
|
||||
const cosechasDisponibles = computed(() => {
|
||||
return cosechasDefiniciones.map(cosecha => ({
|
||||
...cosecha,
|
||||
registros: registrosPorCosecha.value[cosecha.id] || 0,
|
||||
disabled: (registrosPorCosecha.value[cosecha.id] || 0) === 0
|
||||
}))
|
||||
})
|
||||
|
||||
// Cosechas seleccionadas (por defecto las más recientes con datos)
|
||||
const cosechasSeleccionadas = ref<string[]>([])
|
||||
|
||||
// Watch para filtrar cosechas deshabilitadas y seleccionar las más recientes con datos
|
||||
watch(cosechasDisponibles, (disponibles) => {
|
||||
// Remover cosechas deshabilitadas de la selección
|
||||
cosechasSeleccionadas.value = cosechasSeleccionadas.value.filter(id => {
|
||||
const cosecha = disponibles.find(c => c.id === id)
|
||||
return cosecha && !cosecha.disabled
|
||||
})
|
||||
|
||||
// Si no hay ninguna seleccionada, seleccionar las 2 más recientes con datos
|
||||
if (cosechasSeleccionadas.value.length === 0) {
|
||||
const conDatos = disponibles.filter(c => !c.disabled)
|
||||
cosechasSeleccionadas.value = conDatos.slice(-2).map(c => c.id)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Loading and error states
|
||||
const loading = computed(() => ingresosStore.isLoading)
|
||||
const error = computed(() => ingresosStore.error)
|
||||
const loading = computed(() => resumenIngresosStore.isLoading)
|
||||
const error = computed(() => resumenIngresosStore.error)
|
||||
|
||||
// Exportar cosechas y configuración para los componentes
|
||||
provide('cosechasDisponibles', cosechasDisponibles)
|
||||
// Metadatos desde el store de metadata
|
||||
const metadataStore = useMetadataStore()
|
||||
|
||||
const resumenIngresosMetadata = computed(() => {
|
||||
const meta = metadataStore.metadata.find((t: any) => t.table === 'vista_resumen_ingresos')
|
||||
return meta ? { ...meta, name: 'vista_resumen_ingresos' } : null
|
||||
})
|
||||
|
||||
// Definir las métricas disponibles para los componentes
|
||||
const metricasDisponibles = {
|
||||
pesoSeco: {
|
||||
key: 'total_peso_seco',
|
||||
label: 'Peso Seco Total',
|
||||
unidad: 'qq',
|
||||
descripcion: 'Peso seco total del día'
|
||||
},
|
||||
pesoNetoUva: {
|
||||
key: 'peso_neto_uva',
|
||||
label: 'Peso Neto Uva',
|
||||
unidad: 'qq',
|
||||
descripcion: 'Peso neto de café uva'
|
||||
},
|
||||
pesoNetoVerde: {
|
||||
key: 'peso_neto_verde',
|
||||
label: 'Peso Neto Verde',
|
||||
unidad: 'qq',
|
||||
descripcion: 'Peso neto de café verde'
|
||||
},
|
||||
sacos: {
|
||||
key: 'sacos_total_dia',
|
||||
label: 'Sacos Totales',
|
||||
unidad: 'sacos',
|
||||
descripcion: 'Total de sacos del día'
|
||||
},
|
||||
lempirasUva: {
|
||||
key: 'total_lempiras_uva',
|
||||
label: 'Lempiras Uva',
|
||||
unidad: 'L',
|
||||
descripcion: 'Total en lempiras de café uva'
|
||||
},
|
||||
lempirasVerde: {
|
||||
key: 'total_lempiras_verde',
|
||||
label: 'Lempiras Verde',
|
||||
unidad: 'L',
|
||||
descripcion: 'Total en lempiras de café verde'
|
||||
},
|
||||
lempirasMojadoOreado: {
|
||||
key: 'total_lempiras_mojado_oreado',
|
||||
label: 'Lempiras Mojado+Oreado',
|
||||
unidad: 'L',
|
||||
descripcion: 'Total en lempiras de café mojado y oreado (combinados)',
|
||||
// Esta métrica se calcula combinando total_lempiras_mojado + total_lempiras_oreado
|
||||
computed: true,
|
||||
keys: ['total_lempiras_mojado', 'total_lempiras_oreado']
|
||||
}
|
||||
}
|
||||
|
||||
// Exportar definiciones de cosechas, métricas y configuración para los componentes
|
||||
// Usamos cosechasDefiniciones (array estático) en lugar de cosechasDisponibles (computed)
|
||||
// porque los componentes hijos no necesitan la info de disabled/registros
|
||||
provide('cosechasDisponibles', cosechasDefiniciones)
|
||||
provide('metricasDisponibles', metricasDisponibles)
|
||||
provide('estilosGraficas', estilosGraficas)
|
||||
</script>
|
||||
|
||||
@@ -61,6 +61,31 @@
|
||||
|
||||
<!-- Main Content -->
|
||||
<template v-else>
|
||||
<!-- Loading overlay when filtering data -->
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-200"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-opacity duration-200"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="isFilteringData"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||
>
|
||||
<UCard class="brand-card border border-[#c08040]/30">
|
||||
<div class="flex flex-col items-center justify-center gap-4 py-6 px-8">
|
||||
<span class="inline-flex h-12 w-12 animate-spin rounded-full border-4 border-[#c08040] border-t-transparent" />
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-lg font-semibold text-[var(--brand-text)]">Filtrando datos...</span>
|
||||
<span class="text-sm text-[var(--brand-text-muted)]">Por favor espere</span>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Metadatos Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<MetadatosCard v-if="ingresosMetadata" :metadata="ingresosMetadata" :compact="true" />
|
||||
@@ -80,6 +105,7 @@
|
||||
v-if="hasFiltrosActivos"
|
||||
:clientes="clientes"
|
||||
:selected-cliente-ids="selectedClienteIds"
|
||||
:selected-preset="selectedPreset"
|
||||
:fecha-desde="fechaDesde"
|
||||
:fecha-hasta="fechaHasta"
|
||||
:selected-tipos="selectedTipos"
|
||||
@@ -598,7 +624,7 @@ type PresetValue =
|
||||
| 'cosecha-20-21' | 'cosecha-21-22' | 'cosecha-22-23'
|
||||
| 'cosecha-23-24' | 'cosecha-24-25' | 'cosecha-25-26'
|
||||
|
||||
const selectedPreset = useCookie<PresetValue>('informe-ingresos-preset', { default: () => 'cosecha-25-26' })
|
||||
const selectedPreset = useCookie<PresetValue>('informe-ingresos-preset', { default: () => 'hoy' })
|
||||
const fechaDesde = useCookie<string | null>('informe-ingresos-fecha-desde', { default: () => null })
|
||||
const fechaHasta = useCookie<string | null>('informe-ingresos-fecha-hasta', { default: () => null })
|
||||
const selectedClienteIds = useCookie<number[]>('informe-ingresos-clientes', { default: () => [] })
|
||||
@@ -609,8 +635,17 @@ const selectedEstados = useCookie<string[]>('informe-ingresos-estados', { defaul
|
||||
const selectedUbicaciones = useCookie<string[]>('informe-ingresos-ubicaciones', { default: () => [] })
|
||||
const selectedCalidades = useCookie<string[]>('informe-ingresos-calidades', { default: () => [] })
|
||||
|
||||
// Watch para sobrescribir noFilter cuando se agregue cualquier filtro
|
||||
watch([selectedClienteIds, fechaDesde, fechaHasta, selectedTipos, selectedEstados, selectedUbicaciones, selectedCalidades, includeAnulados], () => {
|
||||
// Debounce para manejar cambios de filtros
|
||||
let filterChangeTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// Watch para actualizar filtrados cuando cambien los filtros o los datos
|
||||
watch([ingresos, selectedClienteIds, fechaDesde, fechaHasta, selectedTipos, selectedEstados, selectedUbicaciones, selectedCalidades, includeAnulados], () => {
|
||||
// Cancelar timeout previo si existe
|
||||
if (filterChangeTimeout) {
|
||||
clearTimeout(filterChangeTimeout)
|
||||
}
|
||||
|
||||
// Actualizar noFilter si es necesario
|
||||
const hasAnyFilter = selectedClienteIds.value.length > 0 ||
|
||||
fechaDesde.value !== null ||
|
||||
fechaHasta.value !== null ||
|
||||
@@ -623,7 +658,12 @@ watch([selectedClienteIds, fechaDesde, fechaHasta, selectedTipos, selectedEstado
|
||||
if (hasAnyFilter && noFilter.value) {
|
||||
noFilter.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Debounce para evitar múltiples actualizaciones rápidas
|
||||
filterChangeTimeout = setTimeout(() => {
|
||||
updateFiltrados()
|
||||
}, 100)
|
||||
}, { immediate: true })
|
||||
|
||||
// Opciones para filtros avanzados
|
||||
const tiposCafeOptions = [
|
||||
@@ -789,8 +829,8 @@ function removeCliente(clienteId: number) {
|
||||
selectedClienteIds.value = selectedClienteIds.value.filter(id => id !== clienteId)
|
||||
}
|
||||
|
||||
// Filtrados que alimentan los métricos
|
||||
const ingresosFiltrados = computed(() => {
|
||||
// Helper para calcular ingresos filtrados
|
||||
function calculateIngresosFiltrados() {
|
||||
return (ingresos.value ?? [])
|
||||
.filter(r => (includeAnulados.value ? true : !isAnulado(r)))
|
||||
.filter(r => isWithinDate(r, fechaDesde.value, fechaHasta.value))
|
||||
@@ -799,7 +839,33 @@ const ingresosFiltrados = computed(() => {
|
||||
.filter(r => matchesEstado(r))
|
||||
.filter(r => matchesUbicacion(r))
|
||||
.filter(r => matchesCalidad(r))
|
||||
})
|
||||
}
|
||||
|
||||
// Usar shallowRef para controlar cuando se actualiza
|
||||
const ingresosFiltrados = shallowRef<IngresoRecord[]>([])
|
||||
|
||||
// Función para actualizar filtrados de forma controlada
|
||||
async function updateFiltrados() {
|
||||
// Activar loading
|
||||
isFilteringData.value = true
|
||||
|
||||
// Esperar a que el DOM se actualice
|
||||
await nextTick()
|
||||
|
||||
// Dar dos frames para asegurar que el loading se renderice
|
||||
await new Promise(resolve => requestAnimationFrame(() => {
|
||||
requestAnimationFrame(resolve)
|
||||
}))
|
||||
|
||||
// Ahora sí, calcular los filtrados
|
||||
ingresosFiltrados.value = calculateIngresosFiltrados()
|
||||
|
||||
// Esperar un frame más para que Vue procese los cambios
|
||||
await new Promise(resolve => requestAnimationFrame(resolve))
|
||||
|
||||
// Desactivar loading
|
||||
isFilteringData.value = false
|
||||
}
|
||||
|
||||
const clientesFiltrados = computed((): ClienteRecord[] => {
|
||||
// Si hay clientes seleccionados, filtrar por ellos
|
||||
@@ -907,6 +973,7 @@ function getViewCount(view: ViewMode): number {
|
||||
const loading = computed(() => ingresosStore.isLoading || clientesStore.isLoading)
|
||||
const error = computed(() => ingresosStore.error || clientesStore.error)
|
||||
const loadingProgress = ref(0)
|
||||
const isFilteringData = ref(false)
|
||||
|
||||
// Metadatos desde el store de metadata
|
||||
const metadataStore = useMetadataStore()
|
||||
@@ -981,11 +1048,25 @@ onMounted(async () => {
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err)
|
||||
} finally {
|
||||
// Default preset: cosecha 25-26
|
||||
selectedPreset.value = 'cosecha-25-26'
|
||||
// Re-evaluar presets dinámicos (hoy, semana, mes, ytd) al montar
|
||||
const dynamicPresets = ['hoy', 'semana', 'mes', 'ytd']
|
||||
if (selectedPreset.value && dynamicPresets.includes(selectedPreset.value)) {
|
||||
// Forzar re-evaluación del preset para obtener fechas actualizadas
|
||||
const currentPreset = selectedPreset.value
|
||||
selectedPreset.value = ''
|
||||
nextTick(() => {
|
||||
selectedPreset.value = currentPreset as PresetValue
|
||||
})
|
||||
}
|
||||
|
||||
// Si no hay preset, usar 'hoy' por defecto
|
||||
if (!selectedPreset.value) {
|
||||
selectedPreset.value = 'hoy'
|
||||
}
|
||||
|
||||
includeAnulados.value = false
|
||||
|
||||
// Clear advanced filters
|
||||
// Clear advanced filters on mount
|
||||
selectedTipos.value = []
|
||||
selectedEstados.value = []
|
||||
selectedUbicaciones.value = []
|
||||
|
||||
Reference in New Issue
Block a user