264 lines
8.9 KiB
Vue
264 lines
8.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-[#c08040]" />
|
|
<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', [])
|
|
|
|
// 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
|
|
const colors = ['#c08040', '#d99a56', '#f0c07c', '#8b6f47', '#a0826e', '#b89968']
|
|
|
|
function getColor(index: number): string {
|
|
return colors[index % colors.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>
|