Files
analiticaNucleo/nuxt4-app/app/components/comparativa/CosechasTotales.vue
josedario87 aa76fea286 Refactor: Adaptar todos los componentes al sistema de temas
- Reemplazar colores hardcoded del tema café con variables --brand-*
  - #c08040 → var(--brand-primary-strong)
  - #d99a56 → var(--brand-primary)
  - #f0c07c → var(--brand-accent)
  - #1c140c → var(--brand-surface)
  - #3a2a16 → var(--brand-border)
  - #1b1209, #14100b → var(--brand-bg)

- Reemplazar colores de tipos de café con variables --coffee-*
  - #a855f7 → var(--coffee-uva)
  - #f97316 → var(--coffee-oreado)
  - #06b6d4 → var(--coffee-mojado)
  - #22c55e → var(--coffee-verde)

- Reemplazar clases gray-scale de Tailwind con variables de tema
  - text-gray-400, text-gray-500 → text-[var(--brand-text-muted)]
  - bg-gray-700/30 → bg-[var(--brand-surface)]

- Todos los componentes ahora responden dinámicamente a cambios de tema

Archivos adaptados:
- Páginas: error, informe-ingresos, panorama, explorer, metabase-debug, profile, notifications, settings
- Componentes de ingresos: GraficaSerieIngresos, GraficaSerieInversion, GraficaDinamicaPagadoDeposito, GraficaAcumuladoresUva, TotalesIngresoCompra, TotalesMonetarios, TotalesVerde, SecosVendidos, TopClientes, VistaTablaIngresos, VistaTablaIngresosConClientes, FiltrosActivos
- Componentes de comparativa: CosechasHeatmap, CosechasPorTipo, CosechasEvolucion, CosechasTotales
- Componentes de UI: ClienteSelector, DateRangeSelector, MetadatosCard, MaintenanceMode
- Componentes de auth: UserAvatar, UserMetadata
- Componentes de clientes: ClienteCard, VistaTablaClientes
- Componentes de rechazos: RechazoCard, RechazosRechazoCard, RechazosSubproductos
- Componentes de metabase: MetabaseCardDisplay, MetabaseCardsTable
2025-10-30 17:54:42 -06:00

294 lines
9.9 KiB
Vue

<template>
<UCard class="brand-card border border-transparent">
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-lucide-bar-chart-3" class="size-5 text-[var(--brand-primary-strong)]" />
<h3 class="text-base font-semibold text-[var(--brand-text)]">Totales por Cosecha</h3>
</div>
</template>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Gráfico de Peso Total -->
<div class="flex flex-col gap-2">
<h4 class="text-sm font-medium text-[var(--brand-text-muted)]">Peso Total (qq)</h4>
<svg :viewBox="`0 0 ${svgWidth} ${svgHeight}`" class="w-full h-64">
<!-- Eje Y -->
<line
:x1="padding.left"
:y1="padding.top"
:x2="padding.left"
:y2="svgHeight - padding.bottom"
stroke="var(--brand-border)"
stroke-width="1"
/>
<!-- Barras de peso -->
<g v-for="(cosecha, index) in datosCosechas" :key="cosecha.id">
<rect
:x="padding.left + index * barWidth + index * barGap"
:y="getBarY(cosecha.pesoTotal, maxPeso)"
:width="barWidth"
:height="getBarHeight(cosecha.pesoTotal, maxPeso)"
:fill="getColor(index)"
:opacity="0.8"
class="transition-all duration-300 hover:opacity-100"
/>
<!-- Valor sobre la barra -->
<text
:x="padding.left + index * barWidth + index * barGap + barWidth / 2"
:y="getBarY(cosecha.pesoTotal, maxPeso) - 5"
text-anchor="middle"
class="text-xs fill-[var(--brand-text)]"
>
{{ cosecha.pesoTotal.toLocaleString('es-HN', { maximumFractionDigits: 0 }) }}
</text>
<!-- Etiqueta -->
<text
:x="padding.left + index * barWidth + index * barGap + barWidth / 2"
:y="svgHeight - padding.bottom + 20"
text-anchor="middle"
class="text-xs fill-[var(--brand-text-muted)]"
>
{{ cosecha.label }}
</text>
</g>
</svg>
</div>
<!-- Gráfico de Inversión Total -->
<div class="flex flex-col gap-2">
<h4 class="text-sm font-medium text-[var(--brand-text-muted)]">Inversión Total (L)</h4>
<svg :viewBox="`0 0 ${svgWidth} ${svgHeight}`" class="w-full h-64">
<!-- Eje Y -->
<line
:x1="padding.left"
:y1="padding.top"
:x2="padding.left"
:y2="svgHeight - padding.bottom"
stroke="var(--brand-border)"
stroke-width="1"
/>
<!-- Barras de inversión -->
<g v-for="(cosecha, index) in datosCosechas" :key="cosecha.id">
<rect
:x="padding.left + index * barWidth + index * barGap"
:y="getBarY(cosecha.inversionTotal, maxInversion)"
:width="barWidth"
:height="getBarHeight(cosecha.inversionTotal, maxInversion)"
:fill="getColor(index)"
:opacity="0.8"
class="transition-all duration-300 hover:opacity-100"
/>
<!-- Valor sobre la barra -->
<text
:x="padding.left + index * barWidth + index * barGap + barWidth / 2"
:y="getBarY(cosecha.inversionTotal, maxInversion) - 5"
text-anchor="middle"
class="text-xs fill-[var(--brand-text)]"
>
{{ formatMoney(cosecha.inversionTotal) }}
</text>
<!-- Etiqueta -->
<text
:x="padding.left + index * barWidth + index * barGap + barWidth / 2"
:y="svgHeight - padding.bottom + 20"
text-anchor="middle"
class="text-xs fill-[var(--brand-text-muted)]"
>
{{ cosecha.label }}
</text>
</g>
</svg>
</div>
<!-- Gráfico de Cantidad de Ingresos -->
<div class="flex flex-col gap-2">
<h4 class="text-sm font-medium text-[var(--brand-text-muted)]">Cantidad de Ingresos</h4>
<svg :viewBox="`0 0 ${svgWidth} ${svgHeight}`" class="w-full h-64">
<!-- Eje Y -->
<line
:x1="padding.left"
:y1="padding.top"
:x2="padding.left"
:y2="svgHeight - padding.bottom"
stroke="var(--brand-border)"
stroke-width="1"
/>
<!-- Barras de cantidad -->
<g v-for="(cosecha, index) in datosCosechas" :key="cosecha.id">
<rect
:x="padding.left + index * barWidth + index * barGap"
:y="getBarY(cosecha.cantidad, maxCantidad)"
:width="barWidth"
:height="getBarHeight(cosecha.cantidad, maxCantidad)"
:fill="getColor(index)"
:opacity="0.8"
class="transition-all duration-300 hover:opacity-100"
/>
<!-- Valor sobre la barra -->
<text
:x="padding.left + index * barWidth + index * barGap + barWidth / 2"
:y="getBarY(cosecha.cantidad, maxCantidad) - 5"
text-anchor="middle"
class="text-xs fill-[var(--brand-text)]"
>
{{ cosecha.cantidad }}
</text>
<!-- Etiqueta -->
<text
:x="padding.left + index * barWidth + index * barGap + barWidth / 2"
:y="svgHeight - padding.bottom + 20"
text-anchor="middle"
class="text-xs fill-[var(--brand-text-muted)]"
>
{{ cosecha.label }}
</text>
</g>
</svg>
</div>
</div>
</UCard>
</template>
<script setup lang="ts">
import type { IngresoRecord } from '~/composables/useIngresosMetrics'
interface Props {
ingresos: IngresoRecord[]
cosechasSeleccionadas: string[]
}
const props = defineProps<Props>()
// Obtener definiciones de cosechas
const cosechasDisponibles = inject<any[]>('cosechasDisponibles', [])
// Obtener configuración de estilos
const estilosGraficas = inject<any>('estilosGraficas', ref({
coloresCosechas: ['var(--brand-primary-strong)', 'var(--brand-primary)', '#8b6f47', '#a0826e', '#b89968', 'var(--brand-accent)']
}))
// Dimensiones del SVG
const svgWidth = 400
const svgHeight = 300
const padding = { top: 20, right: 20, bottom: 40, left: 40 }
// Calcular datos por cosecha
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)
const inicio = new Date(cosechaDef.fechaInicio)
const fin = new Date(cosechaDef.fechaFin)
return fecha >= inicio && fecha <= fin
})
const pesoTotal = ingresosEnCosecha.reduce((sum, ing) => sum + ing.peso_neto, 0)
const inversionTotal = ingresosEnCosecha.reduce((sum, ing) => {
let inversion = 0
if (ing.tipo === 'uva' || ing.tipo === 'verde') {
inversion = ing.precio * ing.peso_neto
} else if (ing.tipo === 'oreado' || ing.tipo === 'mojado') {
inversion = (ing.precio / 2) * (ing.peso_seco || 0)
}
return sum + inversion
}, 0)
return {
id: cosechaId,
label: cosechaDef.label.replace('Cosecha ', ''),
pesoTotal,
inversionTotal,
cantidad: ingresosEnCosecha.length
}
}).filter(Boolean) as Array<{
id: string
label: string
pesoTotal: number
inversionTotal: number
cantidad: number
}>
})
// Máximos para escala
const maxPeso = computed(() => Math.max(...datosCosechas.value.map(c => c.pesoTotal), 1))
const maxInversion = computed(() => Math.max(...datosCosechas.value.map(c => c.inversionTotal), 1))
const maxCantidad = computed(() => Math.max(...datosCosechas.value.map(c => c.cantidad), 1))
// Dimensiones de barras
const chartWidth = computed(() => svgWidth - padding.left - padding.right)
const barWidth = computed(() => {
const count = datosCosechas.value.length
return count > 0 ? (chartWidth.value - (count - 1) * 10) / count : 0
})
const barGap = 10
// Función para obtener Y de la barra
function getBarY(value: number, max: number): number {
const chartHeight = svgHeight - padding.top - padding.bottom
const normalized = value / max
return padding.top + chartHeight * (1 - normalized)
}
// Función para obtener altura de la barra
function getBarHeight(value: number, max: number): number {
const chartHeight = svgHeight - padding.top - padding.bottom
return (value / max) * chartHeight
}
// Colores para las barras
// Mapeo de cosechas a índices fijos
const cosechaColorMap: Record<string, number> = {
'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 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 {
if (value >= 1000000) {
return `${(value / 1000000).toFixed(1)}M`
}
if (value >= 1000) {
return `${(value / 1000).toFixed(0)}K`
}
return value.toFixed(0)
}
</script>