diff --git a/nuxt4-app/app/components/comparativa/CosechasEvolucion.vue b/nuxt4-app/app/components/comparativa/CosechasEvolucion.vue index 8521981..3c557fe 100644 --- a/nuxt4-app/app/components/comparativa/CosechasEvolucion.vue +++ b/nuxt4-app/app/components/comparativa/CosechasEvolucion.vue @@ -180,6 +180,11 @@ const props = defineProps() // Obtener definiciones de cosechas const cosechasDisponibles = inject('cosechasDisponibles', []) +// Obtener configuración de estilos +const estilosGraficas = inject('estilosGraficas', ref({ + coloresCosechas: ['#c08040', '#d99a56', '#8b6f47', '#a0826e', '#b89968', '#f0c07c'] +})) + // Dimensiones del SVG const svgWidth = 900 const svgHeight = 450 @@ -303,11 +308,35 @@ function getLinePath(puntos: Array<{ diaRelativo: number, valor: number }>): str return path } -// Colores para las cosechas -const cosechaColors = ['#c08040', '#d99a56', '#f0c07c', '#8b6f47', '#a0826e', '#b89968'] +// Mapeo de cosechas a índices fijos +const cosechaColorMap: Record = { + 'cosecha-20-21': 0, + 'cosecha-21-22': 1, + 'cosecha-22-23': 2, + 'cosecha-23-24': 3, + 'cosecha-24-25': 4, + 'cosecha-25-26': 5 +} -function getCosechaColor(index: number): string { - return cosechaColors[index % cosechaColors.length] +// Función para obtener color fijo de cosecha según su ID (no su orden de selección) +function getCosechaColor(cosechaIdOrIndex: string | number): string { + const colores = estilosGraficas.value.coloresCosechas + + // Si es un string, es un ID de cosecha + if (typeof cosechaIdOrIndex === 'string') { + const index = cosechaColorMap[cosechaIdOrIndex] ?? 0 + return colores[index % colores.length] + } + + // Si es un número, buscar el ID de la cosecha en evolucionCosechas + const cosecha = evolucionCosechas.value[cosechaIdOrIndex] + if (cosecha?.id) { + const index = cosechaColorMap[cosecha.id] ?? cosechaIdOrIndex + return colores[index % colores.length] + } + + // Fallback al índice directo + return colores[cosechaIdOrIndex % colores.length] } function formatValue(value: number): string { diff --git a/nuxt4-app/app/components/comparativa/CosechasHeatmap.vue b/nuxt4-app/app/components/comparativa/CosechasHeatmap.vue index 76e72ec..35b2ff7 100644 --- a/nuxt4-app/app/components/comparativa/CosechasHeatmap.vue +++ b/nuxt4-app/app/components/comparativa/CosechasHeatmap.vue @@ -8,18 +8,56 @@
- +

Vista de Calor por Día

- - {{ isFullscreen ? 'Salir' : 'Pantalla completa' }} - +
+ +
+ + + {{ (nivelZoom * 100).toFixed(0) }}% + + +
+ +
+ + {{ isFullscreen ? 'Salir' : 'Pantalla completa' }} + +
@@ -230,7 +268,7 @@ class="flex flex-col items-center" :style="{ width: `${barMaxWidth}px` }" > -
+
{{ cosecha.label }}
@@ -291,7 +329,7 @@ :style="{ width: getBarWidth(cosecha.valoresPorDia[dia - 1] || 0), height: `${barHeight - 2}px`, - backgroundColor: getCosechaColor(cosechaIndex) + backgroundColor: getCosechaColor(cosecha.id) }" />
@@ -353,7 +391,7 @@ >
{{ cosecha.label }}
@@ -383,12 +421,12 @@
-
+
{{ formatTotal(cosecha.total) }}
@@ -398,7 +436,7 @@ class="h-full transition-all" :style="{ width: `${(cosecha.total / maxTotalEnRango) * 100}%`, - backgroundColor: getCosechaColor(index) + backgroundColor: getCosechaColor(cosecha.id) }" />
@@ -439,10 +477,23 @@ const props = defineProps() // Obtener definiciones de cosechas const cosechasDisponibles = inject('cosechasDisponibles', []) +// Obtener configuración de estilos +const estilosGraficas = inject('estilosGraficas', ref({ + coloresCosechas: ['#c08040', '#d99a56', '#8b6f47', '#a0826e', '#b89968', '#f0c07c'], + anchoCelda: 80, + altoCelda: 6, + anchoMaxBarra: 300, + altoBarra: 8, + opacidadVacias: 0.05 +})) + // Referencia al contenedor y estado de fullscreen const cardContainer = ref(null) const isFullscreen = ref(false) +// Estado de zoom (0.5x a 2x) +const nivelZoom = ref(1) + // Selección de vista, métrica y tipo const vistaMode = ref<'heatmap' | 'barras'>('barras') const metrica = ref<'peso' | 'cantidad' | 'inversion'>('peso') @@ -452,13 +503,27 @@ const tipoSeleccionado = ref('todos') const primerDiaSeleccionado = ref(null) const rangoSeleccionado = ref<{ diaDesde: number, diaHasta: number, diasTotales: number } | null>(null) -// Configuración de celdas (heatmap) -const cellWidth = 80 -const cellHeight = 6 - -// Configuración de barras -const barMaxWidth = 300 -const barHeight = 8 +// Configuración de celdas y barras (ahora reactivas desde la configuración y zoom) +const cellWidth = computed(() => { + const value = estilosGraficas.value.anchoCelda + const baseValue = typeof value === 'number' ? value : 80 + return Math.round(baseValue * nivelZoom.value) +}) +const cellHeight = computed(() => { + const value = estilosGraficas.value.altoCelda + const baseValue = typeof value === 'number' ? value : 6 + return Math.round(baseValue * nivelZoom.value) +}) +const barMaxWidth = computed(() => { + const value = estilosGraficas.value.anchoMaxBarra + const baseValue = typeof value === 'number' ? value : 300 + return Math.round(baseValue * nivelZoom.value) +}) +const barHeight = computed(() => { + const value = estilosGraficas.value.altoBarra + const baseValue = typeof value === 'number' ? value : 8 + return Math.round(baseValue * nivelZoom.value) +}) // Tooltip state const tooltipVisible = ref(false) @@ -630,7 +695,7 @@ function getCellColor(valor: number, max: number): string { // Escala de color desde transparente a naranja intenso if (intensity === 0) { - return 'rgba(192, 128, 64, 0.05)' // Muy suave para vacío + return `rgba(192, 128, 64, ${estilosGraficas.value.opacidadVacias})` // Opacidad configurable } // Gradiente de naranja con opacidad basada en intensidad @@ -642,26 +707,42 @@ function getCellColor(valor: number, max: number): string { return `rgba(${r}, ${g}, ${b}, ${alpha})` } -// Colores sólidos por cosecha (vista barras) -const cosechaColors = [ - '#c08040', // Naranja primario - '#d99a56', // Naranja claro - '#8b6f47', // Marrón - '#a0826e', // Café claro - '#b89968', // Beige - '#f0c07c' // Amarillo dorado -] +// Mapeo de cosechas a índices fijos +const cosechaColorMap: Record = { + 'cosecha-20-21': 0, + 'cosecha-21-22': 1, + 'cosecha-22-23': 2, + 'cosecha-23-24': 3, + 'cosecha-24-25': 4, + 'cosecha-25-26': 5 +} -// Función para obtener color fijo de cosecha -function getCosechaColor(index: number): string { - return cosechaColors[index % cosechaColors.length] +// Función para obtener color fijo de cosecha según su ID (no su orden de selección) +function getCosechaColor(cosechaIdOrIndex: string | number): string { + const colores = estilosGraficas.value.coloresCosechas + + // Si es un string, es un ID de cosecha + if (typeof cosechaIdOrIndex === 'string') { + const index = cosechaColorMap[cosechaIdOrIndex] ?? 0 + return colores[index % colores.length] + } + + // Si es un número, buscar el ID de la cosecha en datosCosechas + const cosecha = datosCosechas.value[cosechaIdOrIndex] + if (cosecha?.id) { + const index = cosechaColorMap[cosecha.id] ?? cosechaIdOrIndex + return colores[index % colores.length] + } + + // Fallback al índice directo + return colores[cosechaIdOrIndex % colores.length] } // Función para calcular ancho de barra según valor function getBarWidth(valor: number): string { if (maxValorDia.value === 0) return '0px' const percentage = (valor / maxValorDia.value) * 100 - const width = (percentage / 100) * barMaxWidth + const width = (percentage / 100) * barMaxWidth.value // .value es necesario en el script setup return `${width}px` } @@ -798,6 +879,23 @@ function formatRangoFecha(diaRelativo: number): string { }) } +// Funciones de zoom +function zoomIn() { + if (nivelZoom.value < 2) { + nivelZoom.value = Math.min(2, nivelZoom.value + 0.25) + } +} + +function zoomOut() { + if (nivelZoom.value > 0.5) { + nivelZoom.value = Math.max(0.5, nivelZoom.value - 0.25) + } +} + +function resetZoom() { + nivelZoom.value = 1 +} + // Funciones de pantalla completa async function toggleFullscreen() { if (!cardContainer.value) return @@ -822,13 +920,40 @@ function handleFullscreenChange() { isFullscreen.value = !!document.fullscreenElement } +// Zoom con rueda del mouse + Ctrl +function handleWheel(event: WheelEvent) { + if (event.ctrlKey || event.metaKey) { + event.preventDefault() + + if (event.deltaY < 0) { + // Scroll up = zoom in + zoomIn() + } else { + // Scroll down = zoom out + zoomOut() + } + } +} + // Montar y desmontar listeners onMounted(() => { document.addEventListener('fullscreenchange', handleFullscreenChange) + + // Agregar listener para zoom con rueda del mouse + const container = cardContainer.value?.$el + if (container) { + container.addEventListener('wheel', handleWheel, { passive: false }) + } }) onUnmounted(() => { document.removeEventListener('fullscreenchange', handleFullscreenChange) + + // Remover listener de zoom + const container = cardContainer.value?.$el + if (container) { + container.removeEventListener('wheel', handleWheel) + } }) diff --git a/nuxt4-app/app/components/comparativa/CosechasPorTipo.vue b/nuxt4-app/app/components/comparativa/CosechasPorTipo.vue index d6c8540..61552db 100644 --- a/nuxt4-app/app/components/comparativa/CosechasPorTipo.vue +++ b/nuxt4-app/app/components/comparativa/CosechasPorTipo.vue @@ -188,6 +188,11 @@ const props = defineProps() // Obtener definiciones de cosechas const cosechasDisponibles = inject('cosechasDisponibles', []) +// Obtener configuración de estilos +const estilosGraficas = inject('estilosGraficas', ref({ + coloresCosechas: ['#c08040', '#d99a56', '#8b6f47', '#a0826e', '#b89968', '#f0c07c'] +})) + // Dimensiones del SVG const svgWidth = 500 const svgHeight = 350 @@ -287,10 +292,34 @@ function getBarHeight(value: number, max: number): number { return (value / max) * chartHeight } -// Colores para las cosechas -const cosechaColors = ['#c08040', '#d99a56', '#f0c07c', '#8b6f47', '#a0826e', '#b89968'] +// Mapeo de cosechas a índices fijos +const cosechaColorMap: Record = { + 'cosecha-20-21': 0, + 'cosecha-21-22': 1, + 'cosecha-22-23': 2, + 'cosecha-23-24': 3, + 'cosecha-24-25': 4, + 'cosecha-25-26': 5 +} -function getCosechaColor(index: number): string { - return cosechaColors[index % cosechaColors.length] +// Función para obtener color fijo de cosecha según su ID (no su orden de selección) +function getCosechaColor(cosechaIdOrIndex: string | number): string { + const colores = estilosGraficas.value.coloresCosechas + + // Si es un string, es un ID de cosecha + if (typeof cosechaIdOrIndex === 'string') { + const index = cosechaColorMap[cosechaIdOrIndex] ?? 0 + return colores[index % colores.length] + } + + // Si es un número, buscar el ID de la cosecha en datosPorTipo + const cosecha = datosPorTipo.value[cosechaIdOrIndex] + if (cosecha?.id) { + const index = cosechaColorMap[cosecha.id] ?? cosechaIdOrIndex + return colores[index % colores.length] + } + + // Fallback al índice directo + return colores[cosechaIdOrIndex % colores.length] } diff --git a/nuxt4-app/app/components/comparativa/CosechasTotales.vue b/nuxt4-app/app/components/comparativa/CosechasTotales.vue index 73c12b1..ef1155b 100644 --- a/nuxt4-app/app/components/comparativa/CosechasTotales.vue +++ b/nuxt4-app/app/components/comparativa/CosechasTotales.vue @@ -171,6 +171,11 @@ const props = defineProps() // Obtener definiciones de cosechas const cosechasDisponibles = inject('cosechasDisponibles', []) +// Obtener configuración de estilos +const estilosGraficas = inject('estilosGraficas', ref({ + coloresCosechas: ['#c08040', '#d99a56', '#8b6f47', '#a0826e', '#b89968', '#f0c07c'] +})) + // Dimensiones del SVG const svgWidth = 400 const svgHeight = 300 @@ -245,10 +250,35 @@ function getBarHeight(value: number, max: number): number { } // Colores para las barras -const colors = ['#c08040', '#d99a56', '#f0c07c', '#8b6f47', '#a0826e', '#b89968'] +// Mapeo de cosechas a índices fijos +const cosechaColorMap: Record = { + 'cosecha-20-21': 0, + 'cosecha-21-22': 1, + 'cosecha-22-23': 2, + 'cosecha-23-24': 3, + 'cosecha-24-25': 4, + 'cosecha-25-26': 5 +} -function getColor(index: number): string { - return colors[index % colors.length] +// Función para obtener color fijo de cosecha según su ID (no su orden de selección) +function getColor(cosechaIdOrIndex: string | number): string { + const colores = estilosGraficas.value.coloresCosechas + + // Si es un string, es un ID de cosecha + if (typeof cosechaIdOrIndex === 'string') { + const index = cosechaColorMap[cosechaIdOrIndex] ?? 0 + return colores[index % colores.length] + } + + // Si es un número, buscar el ID de la cosecha en datosCosechas + const cosecha = datosCosechas.value[cosechaIdOrIndex] + if (cosecha?.id) { + const index = cosechaColorMap[cosecha.id] ?? cosechaIdOrIndex + return colores[index % colores.length] + } + + // Fallback al índice directo + return colores[cosechaIdOrIndex % colores.length] } function formatMoney(value: number): string { diff --git a/nuxt4-app/app/components/informe-ingresos/FiltrosPanel.vue b/nuxt4-app/app/components/informe-ingresos/FiltrosPanel.vue index 116f12a..7de1952 100644 --- a/nuxt4-app/app/components/informe-ingresos/FiltrosPanel.vue +++ b/nuxt4-app/app/components/informe-ingresos/FiltrosPanel.vue @@ -1,138 +1,193 @@ @@ -154,6 +209,7 @@ interface Props { ubicacionesOptions: any[] calidadesOptions: any[] includeAnulados: boolean + noFilter: boolean } const props = defineProps() @@ -168,6 +224,8 @@ const emit = defineEmits<{ 'update:selectedUbicaciones': [value: string[]] 'update:selectedCalidades': [value: string[]] 'update:includeAnulados': [value: boolean] + 'update:noFilter': [value: boolean] + 'hideFiltros': [] }>() const selectedTipos = computed({ @@ -195,9 +253,37 @@ const includeAnulados = computed({ set: (value) => emit('update:includeAnulados', value) }) +const noFilter = computed({ + get: () => props.noFilter, + set: (value) => emit('update:noFilter', value) +}) + +const hasAvailableAdvancedFilters = computed(() => { + return selectedTipos.value.length === 0 || + selectedEstados.value.length === 0 || + selectedUbicaciones.value.length === 0 || + selectedCalidades.value.length === 0 +}) + +const hasAnyActiveFilter = computed(() => { + return props.selectedClienteIds.length > 0 || + props.fechaDesde !== null || + props.fechaHasta !== null || + props.selectedTipos.length > 0 || + props.selectedEstados.length > 0 || + props.selectedUbicaciones.length > 0 || + props.selectedCalidades.length > 0 || + props.includeAnulados +}) + function clearDates() { emit('update:fechaDesde', null) emit('update:fechaHasta', null) emit('update:selectedPreset', '') } + +function handleNoFilter() { + emit('update:noFilter', true) + emit('hideFiltros') +} diff --git a/nuxt4-app/app/components/ingresos/FiltrosActivos.vue b/nuxt4-app/app/components/ingresos/FiltrosActivos.vue new file mode 100644 index 0000000..2d90686 --- /dev/null +++ b/nuxt4-app/app/components/ingresos/FiltrosActivos.vue @@ -0,0 +1,293 @@ + + + diff --git a/nuxt4-app/app/pages/comparativa-cosechas.vue b/nuxt4-app/app/pages/comparativa-cosechas.vue index c6499f1..c6654b2 100644 --- a/nuxt4-app/app/pages/comparativa-cosechas.vue +++ b/nuxt4-app/app/pages/comparativa-cosechas.vue @@ -10,6 +10,17 @@
+ + + + + + +
+ +
+

+ + Colores de Cosechas (por año) +

+

+ Los colores son fijos para cada cosecha, sin importar el orden en que se seleccionen +

+
+
+ + +
+
+
+ + +
+

+ + Dimensiones +

+
+ +
+ +
+ + + {{ estilosGraficas.anchoCelda }}px + +
+
+ + +
+ +
+ + + {{ estilosGraficas.altoCelda }}px + +
+
+ + +
+ +
+ + + {{ estilosGraficas.anchoMaxBarra }}px + +
+
+ + +
+ +
+ + + {{ estilosGraficas.altoBarra }}px + +
+
+
+
+ + +
+

+ + Opacidad +

+
+ +
+ + + {{ (estilosGraficas.opacidadVacias * 100).toFixed(0) }}% + +
+
+
+
+ + +
+
@@ -133,6 +326,62 @@ const pageSections = ref({ porTipo: false }) +// Estado del modal de configuración +const mostrarConfiguracion = ref(false) + +// Configuración de estilos por defecto +const estilosGraficasDefault = { + coloresCosechas: [ + '#c08040', // Cosecha 20-21 + '#d99a56', // Cosecha 21-22 + '#8b6f47', // Cosecha 22-23 + '#a0826e', // Cosecha 23-24 + '#b89968', // Cosecha 24-25 + '#f0c07c' // Cosecha 25-26 + ], + anchoCelda: 80, + altoCelda: 6, + anchoMaxBarra: 300, + altoBarra: 8, + opacidadVacias: 0.05 +} + +// Cargar configuración desde cookies +function cargarConfiguracionDesdeCookies() { + const cookie = useCookie('estilos-graficas', { + maxAge: 60 * 60 * 24 * 365, // 1 año + sameSite: 'lax' + }) + + if (cookie.value) { + try { + return { ...estilosGraficasDefault, ...cookie.value } + } catch (e) { + console.error('Error al cargar configuración desde cookies:', e) + return { ...estilosGraficasDefault } + } + } + + return { ...estilosGraficasDefault } +} + +// Configuración reactiva de estilos +const estilosGraficas = ref(cargarConfiguracionDesdeCookies()) + +// Guardar en cookies cuando cambie la configuración +watch(estilosGraficas, (newValue) => { + const cookie = useCookie('estilos-graficas', { + maxAge: 60 * 60 * 24 * 365, // 1 año + sameSite: 'lax' + }) + cookie.value = newValue +}, { deep: true }) + +// Función para resetear la configuración +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' }, @@ -156,6 +405,7 @@ const ingresos = computed(() => ingresosStore.allRecords as IngresoRecord[]) const loading = computed(() => ingresosStore.isLoading) const error = computed(() => ingresosStore.error) -// Exportar cosechas para los componentes +// Exportar cosechas y configuración para los componentes provide('cosechasDisponibles', cosechasDisponibles) +provide('estilosGraficas', estilosGraficas) diff --git a/nuxt4-app/app/pages/informe-ingresos.vue b/nuxt4-app/app/pages/informe-ingresos.vue index 793877d..5e29b7b 100644 --- a/nuxt4-app/app/pages/informe-ingresos.vue +++ b/nuxt4-app/app/pages/informe-ingresos.vue @@ -1,26 +1,7 @@