- 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
656 lines
21 KiB
Vue
656 lines
21 KiB
Vue
<!-- Gráfica de Uva Pagada vs Uva en Depósito -->
|
|
<template>
|
|
<UCard class="brand-card border border-transparent">
|
|
<template #header>
|
|
<div class="flex flex-col gap-3">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-lg font-bold brand-section-title">Dinámica: Pagado vs Depósito</h3>
|
|
<p class="text-xs text-[var(--brand-text-muted)] mt-1">
|
|
Evolución de café pagado y en depósito
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<UCheckbox
|
|
v-for="tipo in tipos"
|
|
:key="tipo.value"
|
|
:model-value="tiposSeleccionados.includes(tipo.value)"
|
|
@update:model-value="(checked) => toggleTipo(tipo.value, checked)"
|
|
:label="getTipoLabel(tipo)"
|
|
:disabled="isTipoDisabled(tipo.value)"
|
|
size="xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selector de Granularidad Temporal -->
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide">Granularidad:</span>
|
|
<div class="flex items-center gap-1">
|
|
<button
|
|
v-for="option in granularityOptions"
|
|
:key="option.value"
|
|
@click="selectedGranularity = option.value"
|
|
:class="[
|
|
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded text-xs font-medium transition-all',
|
|
selectedGranularity === option.value
|
|
? 'bg-[var(--brand-primary)] text-white shadow-sm'
|
|
: 'bg-[var(--brand-surface)] text-[var(--brand-text-muted)] hover:bg-[var(--brand-primary)]/10 hover:text-[var(--brand-text)]'
|
|
]"
|
|
>
|
|
<UIcon :name="option.icon" class="w-3.5 h-3.5" />
|
|
<span>{{ option.label }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="relative w-full" style="height: 400px;">
|
|
<svg :viewBox="`0 0 ${width} ${height}`" class="w-full h-full" preserveAspectRatio="none">
|
|
<!-- Grid lines -->
|
|
<g v-for="i in 5" :key="`grid-${i}`">
|
|
<line
|
|
:x1="padding"
|
|
:y1="padding + (i - 1) * (chartHeight / 4)"
|
|
:x2="width - padding"
|
|
:y2="padding + (i - 1) * (chartHeight / 4)"
|
|
stroke="currentColor"
|
|
:stroke-opacity="0.1"
|
|
stroke-width="1"
|
|
/>
|
|
<text
|
|
:x="padding - 10"
|
|
:y="padding + (i - 1) * (chartHeight / 4) + 5"
|
|
text-anchor="end"
|
|
class="text-xs fill-[var(--brand-text-muted)]"
|
|
>
|
|
{{ formatValue(maxValue - (i - 1) * (maxValue / 4)) }}
|
|
</text>
|
|
</g>
|
|
|
|
<!-- Gráficas por cada tipo -->
|
|
<g v-for="tipo in tiposActivos" :key="`tipo-${tipo.value}`">
|
|
<!-- Area Pagado -->
|
|
<path
|
|
:d="getAreaPagadoPathForTipo(tipo.value)"
|
|
:fill="getTipoColor(tipo.value)"
|
|
fill-opacity="0.2"
|
|
/>
|
|
|
|
<!-- Area Deposito -->
|
|
<path
|
|
:d="getAreaDepositoPathForTipo(tipo.value)"
|
|
:fill="getTipoColor(tipo.value)"
|
|
fill-opacity="0.1"
|
|
stroke-dasharray="5,5"
|
|
/>
|
|
|
|
<!-- Line Pagado -->
|
|
<path
|
|
:d="getLinePagadoPathForTipo(tipo.value)"
|
|
:stroke="getTipoColor(tipo.value)"
|
|
stroke-width="3"
|
|
fill="none"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
|
|
<!-- Line Deposito -->
|
|
<path
|
|
:d="getLineDepositoPathForTipo(tipo.value)"
|
|
:stroke="getTipoColor(tipo.value)"
|
|
stroke-width="3"
|
|
fill="none"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-dasharray="8,4"
|
|
/>
|
|
|
|
<!-- Points Pagado -->
|
|
<circle
|
|
v-for="(point, i) in getPointsPagadoForTipo(tipo.value)"
|
|
:key="`pagado-${tipo.value}-${i}`"
|
|
:cx="point.x"
|
|
:cy="point.y"
|
|
r="4"
|
|
:fill="getTipoColor(tipo.value)"
|
|
class="cursor-pointer hover:r-6 transition-all"
|
|
>
|
|
<title>{{ tipo.label }} Pagado - {{ point.label }}: {{ point.value.toFixed(2) }} {{ getTipoUnidad(tipo) }}</title>
|
|
</circle>
|
|
|
|
<!-- Points Deposito -->
|
|
<circle
|
|
v-for="(point, i) in getPointsDepositoForTipo(tipo.value)"
|
|
:key="`deposito-${tipo.value}-${i}`"
|
|
:cx="point.x"
|
|
:cy="point.y"
|
|
r="4"
|
|
:fill="getTipoColor(tipo.value)"
|
|
fill-opacity="0.6"
|
|
class="cursor-pointer hover:r-6 transition-all"
|
|
>
|
|
<title>{{ tipo.label }} Depósito - {{ point.label }}: {{ point.value.toFixed(2) }} {{ getTipoUnidad(tipo) }}</title>
|
|
</circle>
|
|
</g>
|
|
|
|
<!-- X-axis labels (distribuidos uniformemente) -->
|
|
<g v-if="tiposActivos.length > 0">
|
|
<text
|
|
v-for="(point, i) in getPointsPagadoForTipo(tiposActivos[0].value)"
|
|
:key="`label-${i}`"
|
|
v-show="shouldShowLabel(i, getPointsPagadoForTipo(tiposActivos[0].value).length)"
|
|
:x="point.x"
|
|
:y="height - padding + 20"
|
|
text-anchor="middle"
|
|
class="text-xs fill-[var(--brand-text-muted)]"
|
|
>
|
|
{{ point.dateLabel }}
|
|
</text>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex items-center justify-between text-xs flex-wrap gap-2">
|
|
<div class="flex items-center gap-4 flex-wrap">
|
|
<div v-for="tipo in tiposActivos" :key="`totales-${tipo.value}`" class="flex items-center gap-3">
|
|
<div class="flex items-center gap-1.5">
|
|
<div class="w-3 h-3 rounded-full" :style="{ backgroundColor: getTipoColor(tipo.value) }"></div>
|
|
<span class="text-[var(--brand-text-muted)] text-xs">{{ tipo.label }}:</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-bold" :style="{ color: getTipoColor(tipo.value) }">
|
|
{{ formatValue(getTotalPagadoForTipo(tipo.value)) }} {{ getTipoUnidad(tipo) }}
|
|
</span>
|
|
<span class="text-[var(--brand-text-muted)]">pagado</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-bold opacity-60" :style="{ color: getTipoColor(tipo.value) }">
|
|
{{ formatValue(getTotalDepositoForTipo(tipo.value)) }} {{ getTipoUnidad(tipo) }}
|
|
</span>
|
|
<span class="text-[var(--brand-text-muted)]">depósito</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</UCard>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { IngresoRecord } from '~/composables/useIngresosMetrics'
|
|
|
|
interface Props {
|
|
ingresos: IngresoRecord[]
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const tipos = [
|
|
{ value: 'uva', label: 'Uva', unidad: 'lb' },
|
|
{ value: 'oreado', label: 'Oreado', unidad: 'qq' },
|
|
{ value: 'mojado', label: 'Mojado', unidad: 'qq' },
|
|
{ value: 'verde', label: 'Verde', unidad: 'lb' }
|
|
]
|
|
|
|
const tiposSeleccionados = ref(['uva'])
|
|
|
|
const tiposActivos = computed(() => tipos.filter(t => tiposSeleccionados.value.includes(t.value)))
|
|
|
|
// Selector de granularidad temporal
|
|
type GranularityMode = 'auto' | 'year' | 'month' | 'day' | 'hour'
|
|
const selectedGranularity = ref<GranularityMode>('auto')
|
|
|
|
const granularityOptions = [
|
|
{ value: 'auto' as GranularityMode, label: 'Auto', icon: 'i-lucide-wand-2' },
|
|
{ value: 'year' as GranularityMode, label: 'Años', icon: 'i-lucide-calendar' },
|
|
{ value: 'month' as GranularityMode, label: 'Meses', icon: 'i-lucide-calendar-days' },
|
|
{ value: 'day' as GranularityMode, label: 'Días', icon: 'i-lucide-calendar-clock' },
|
|
{ value: 'hour' as GranularityMode, label: 'Horas', icon: 'i-lucide-clock' }
|
|
]
|
|
|
|
// Determinar si estamos en modo seco (oreado o mojado seleccionado)
|
|
const modoSeco = computed(() => {
|
|
return tiposSeleccionados.value.includes('oreado') || tiposSeleccionados.value.includes('mojado')
|
|
})
|
|
|
|
// Determinar si un tipo está deshabilitado
|
|
function isTipoDisabled(tipo: string): boolean {
|
|
if (tipo === 'verde') {
|
|
return modoSeco.value
|
|
}
|
|
if (tipo === 'oreado' || tipo === 'mojado') {
|
|
return tiposSeleccionados.value.includes('verde')
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Obtener label dinámico con unidad correcta
|
|
function getTipoLabel(tipo: { value: string, label: string, unidad: string }): string {
|
|
if (tipo.value === 'uva') {
|
|
return modoSeco.value ? `${tipo.label} (qq)` : `${tipo.label} (lb)`
|
|
}
|
|
return `${tipo.label} (${tipo.unidad})`
|
|
}
|
|
|
|
// Obtener unidad dinámica según el modo
|
|
function getTipoUnidad(tipo: { value: string, label: string, unidad: string }): string {
|
|
if (tipo.value === 'uva') {
|
|
return modoSeco.value ? 'qq' : 'lb'
|
|
}
|
|
return tipo.unidad
|
|
}
|
|
|
|
function toggleTipo(value: string, checked: boolean) {
|
|
if (checked) {
|
|
if (!tiposSeleccionados.value.includes(value)) {
|
|
tiposSeleccionados.value.push(value)
|
|
}
|
|
// Si se activa verde, desactivar oreado y mojado
|
|
if (value === 'verde') {
|
|
tiposSeleccionados.value = tiposSeleccionados.value.filter(v => v !== 'oreado' && v !== 'mojado')
|
|
}
|
|
// Si se activa oreado o mojado, desactivar verde
|
|
if (value === 'oreado' || value === 'mojado') {
|
|
tiposSeleccionados.value = tiposSeleccionados.value.filter(v => v !== 'verde')
|
|
}
|
|
} else {
|
|
tiposSeleccionados.value = tiposSeleccionados.value.filter(v => v !== value)
|
|
}
|
|
}
|
|
|
|
const width = 1200
|
|
const height = 400
|
|
const padding = 60
|
|
|
|
const chartWidth = width - 2 * padding
|
|
const chartHeight = height - 2 * padding
|
|
|
|
interface DataPoint {
|
|
date: Date
|
|
value: number
|
|
x: number
|
|
y: number
|
|
label: string
|
|
dateLabel: string
|
|
}
|
|
|
|
// Función para detectar granularidad temporal
|
|
function getTimeGranularity(startDate: Date, endDate: Date): 'year' | 'month' | 'day' | 'hour' {
|
|
// Si no es auto, usar la selección manual
|
|
if (selectedGranularity.value !== 'auto') {
|
|
return selectedGranularity.value
|
|
}
|
|
|
|
// Auto: detectar según el rango
|
|
const diffMs = endDate.getTime() - startDate.getTime()
|
|
const diffDays = diffMs / (1000 * 60 * 60 * 24)
|
|
const diffMonths = diffDays / 30
|
|
const diffYears = diffDays / 365
|
|
|
|
if (diffYears > 2) return 'year'
|
|
if (diffMonths > 2) return 'month'
|
|
if (diffDays > 2) return 'day'
|
|
return 'hour'
|
|
}
|
|
|
|
// Función para generar timestamps completos según granularidad
|
|
function generateTimeRange(startDate: Date, endDate: Date, granularity: 'year' | 'month' | 'day' | 'hour'): Date[] {
|
|
const dates: Date[] = []
|
|
const current = new Date(startDate)
|
|
|
|
while (current <= endDate) {
|
|
dates.push(new Date(current))
|
|
|
|
switch (granularity) {
|
|
case 'year':
|
|
current.setFullYear(current.getFullYear() + 1)
|
|
break
|
|
case 'month':
|
|
current.setMonth(current.getMonth() + 1)
|
|
break
|
|
case 'day':
|
|
current.setDate(current.getDate() + 1)
|
|
break
|
|
case 'hour':
|
|
current.setHours(current.getHours() + 1)
|
|
break
|
|
}
|
|
}
|
|
|
|
return dates
|
|
}
|
|
|
|
// Función para formatear fecha según granularidad
|
|
function formatDateByGranularity(date: Date, granularity: 'year' | 'month' | 'day' | 'hour'): string {
|
|
switch (granularity) {
|
|
case 'year':
|
|
return date.getFullYear().toString()
|
|
case 'month':
|
|
return date.toLocaleDateString('es-HN', { year: 'numeric', month: 'short' })
|
|
case 'day':
|
|
return date.toLocaleDateString('es-HN', { day: 'numeric', month: 'short' })
|
|
case 'hour':
|
|
return date.toLocaleTimeString('es-HN', { hour: '2-digit', minute: '2-digit' })
|
|
}
|
|
}
|
|
|
|
// Función para agrupar fecha según granularidad (devuelve formato ISO parseable)
|
|
function getDateKey(date: Date, granularity: 'year' | 'month' | 'day' | 'hour'): string {
|
|
const year = date.getFullYear()
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
const hour = String(date.getHours()).padStart(2, '0')
|
|
|
|
switch (granularity) {
|
|
case 'year':
|
|
return `${year}`
|
|
case 'month':
|
|
return `${year}-${month}`
|
|
case 'day':
|
|
return `${year}-${month}-${day}`
|
|
case 'hour':
|
|
return `${year}-${month}-${day}T${hour}`
|
|
}
|
|
}
|
|
|
|
// Determinar si mostrar una etiqueta (máximo 10 etiquetas en el eje)
|
|
function shouldShowLabel(index: number, total: number): boolean {
|
|
if (total <= 10) return true
|
|
|
|
const maxLabels = 10
|
|
const step = Math.ceil(total / maxLabels)
|
|
|
|
// Siempre mostrar primera y última
|
|
if (index === 0 || index === total - 1) return true
|
|
|
|
// Mostrar cada N puntos
|
|
return index % step === 0
|
|
}
|
|
|
|
// Datos pagado por tipo
|
|
const dataPagadoByTipo = computed(() => {
|
|
const result: Record<string, DataPoint[]> = {}
|
|
|
|
// Encontrar rango temporal solo de los tipos seleccionados
|
|
const allDates = props.ingresos
|
|
.filter(i => tiposSeleccionados.value.includes(i.tipo))
|
|
.filter(i => i.created_at)
|
|
.map(i => new Date(i.created_at!))
|
|
|
|
if (allDates.length === 0) {
|
|
tiposSeleccionados.value.forEach(tipo => {
|
|
result[tipo] = []
|
|
})
|
|
return result
|
|
}
|
|
|
|
const minDate = new Date(Math.min(...allDates.map(d => d.getTime())))
|
|
const maxDate = new Date(Math.max(...allDates.map(d => d.getTime())))
|
|
const granularity = getTimeGranularity(minDate, maxDate)
|
|
const timeRange = generateTimeRange(minDate, maxDate, granularity)
|
|
|
|
tiposSeleccionados.value.forEach(tipo => {
|
|
const ingresosFiltrados = props.ingresos
|
|
.filter(i => i.tipo === tipo)
|
|
.filter(i => i.estado === 'pagado')
|
|
.filter(i => i.created_at)
|
|
|
|
const porPeriodo = new Map<string, number>()
|
|
|
|
ingresosFiltrados.forEach(ingreso => {
|
|
const fecha = new Date(ingreso.created_at!)
|
|
const key = getDateKey(fecha, granularity)
|
|
let valor = 0
|
|
|
|
if (tipo === 'uva') {
|
|
valor = modoSeco.value ? ingreso.peso_neto / 500 : ingreso.peso_neto
|
|
} else if (tipo === 'verde') {
|
|
valor = ingreso.peso_neto
|
|
} else {
|
|
valor = ingreso.peso_seco
|
|
}
|
|
|
|
porPeriodo.set(key, (porPeriodo.get(key) || 0) + valor)
|
|
})
|
|
|
|
let acumulado = 0
|
|
const puntos: DataPoint[] = []
|
|
|
|
timeRange.forEach(fecha => {
|
|
const key = getDateKey(fecha, granularity)
|
|
const valor = porPeriodo.get(key) || 0
|
|
acumulado += valor
|
|
|
|
puntos.push({
|
|
date: fecha,
|
|
value: acumulado,
|
|
x: 0,
|
|
y: 0,
|
|
label: key,
|
|
dateLabel: formatDateByGranularity(fecha, granularity)
|
|
})
|
|
})
|
|
|
|
result[tipo] = puntos
|
|
})
|
|
|
|
return result
|
|
})
|
|
|
|
// Datos depósito por tipo
|
|
const dataDepositoByTipo = computed(() => {
|
|
const result: Record<string, DataPoint[]> = {}
|
|
|
|
// Encontrar rango temporal solo de los tipos seleccionados
|
|
const allDates = props.ingresos
|
|
.filter(i => tiposSeleccionados.value.includes(i.tipo))
|
|
.filter(i => i.created_at)
|
|
.map(i => new Date(i.created_at!))
|
|
|
|
if (allDates.length === 0) {
|
|
tiposSeleccionados.value.forEach(tipo => {
|
|
result[tipo] = []
|
|
})
|
|
return result
|
|
}
|
|
|
|
const minDate = new Date(Math.min(...allDates.map(d => d.getTime())))
|
|
const maxDate = new Date(Math.max(...allDates.map(d => d.getTime())))
|
|
const granularity = getTimeGranularity(minDate, maxDate)
|
|
const timeRange = generateTimeRange(minDate, maxDate, granularity)
|
|
|
|
tiposSeleccionados.value.forEach(tipo => {
|
|
const todosFiltrados = props.ingresos
|
|
.filter(i => i.tipo === tipo)
|
|
.filter(i => i.created_at)
|
|
|
|
const pagadosFiltrados = props.ingresos
|
|
.filter(i => i.tipo === tipo)
|
|
.filter(i => i.estado === 'pagado')
|
|
.filter(i => i.created_at)
|
|
|
|
const totalPorPeriodo = new Map<string, number>()
|
|
const pagadoPorPeriodo = new Map<string, number>()
|
|
|
|
todosFiltrados.forEach(ingreso => {
|
|
const fecha = new Date(ingreso.created_at!)
|
|
const key = getDateKey(fecha, granularity)
|
|
let valor = 0
|
|
|
|
if (tipo === 'uva') {
|
|
valor = modoSeco.value ? ingreso.peso_neto / 500 : ingreso.peso_neto
|
|
} else if (tipo === 'verde') {
|
|
valor = ingreso.peso_neto
|
|
} else {
|
|
valor = ingreso.peso_seco
|
|
}
|
|
|
|
totalPorPeriodo.set(key, (totalPorPeriodo.get(key) || 0) + valor)
|
|
})
|
|
|
|
pagadosFiltrados.forEach(ingreso => {
|
|
const fecha = new Date(ingreso.created_at!)
|
|
const key = getDateKey(fecha, granularity)
|
|
let valor = 0
|
|
|
|
if (tipo === 'uva') {
|
|
valor = modoSeco.value ? ingreso.peso_neto / 500 : ingreso.peso_neto
|
|
} else if (tipo === 'verde') {
|
|
valor = ingreso.peso_neto
|
|
} else {
|
|
valor = ingreso.peso_seco
|
|
}
|
|
|
|
pagadoPorPeriodo.set(key, (pagadoPorPeriodo.get(key) || 0) + valor)
|
|
})
|
|
|
|
let acumuladoTotal = 0
|
|
let acumuladoPagado = 0
|
|
const puntos: DataPoint[] = []
|
|
|
|
timeRange.forEach(fecha => {
|
|
const key = getDateKey(fecha, granularity)
|
|
acumuladoTotal += totalPorPeriodo.get(key) || 0
|
|
acumuladoPagado += pagadoPorPeriodo.get(key) || 0
|
|
const deposito = acumuladoTotal - acumuladoPagado
|
|
|
|
puntos.push({
|
|
date: fecha,
|
|
value: deposito,
|
|
x: 0,
|
|
y: 0,
|
|
label: key,
|
|
dateLabel: formatDateByGranularity(fecha, granularity)
|
|
})
|
|
})
|
|
|
|
result[tipo] = puntos
|
|
})
|
|
|
|
return result
|
|
})
|
|
|
|
const maxValue = computed(() => {
|
|
let max = 0
|
|
|
|
Object.values(dataPagadoByTipo.value).forEach(puntos => {
|
|
if (puntos.length > 0) {
|
|
const maxPagado = Math.max(...puntos.map(p => p.value))
|
|
if (maxPagado > max) max = maxPagado
|
|
}
|
|
})
|
|
|
|
Object.values(dataDepositoByTipo.value).forEach(puntos => {
|
|
if (puntos.length > 0) {
|
|
const maxDeposito = Math.max(...puntos.map(p => p.value))
|
|
if (maxDeposito > max) max = maxDeposito
|
|
}
|
|
})
|
|
|
|
return max * 1.1 || 100
|
|
})
|
|
|
|
function getTipoColor(tipo: string) {
|
|
const tipoConfig = tipos.find(t => t.value === tipo)
|
|
return tipoConfig?.value === 'uva' ? 'var(--coffee-uva)' :
|
|
tipoConfig?.value === 'oreado' ? 'var(--coffee-oreado)' :
|
|
tipoConfig?.value === 'mojado' ? 'var(--coffee-mojado)' : 'var(--coffee-verde)'
|
|
}
|
|
|
|
function getPointsPagadoForTipo(tipo: string) {
|
|
const data = dataPagadoByTipo.value[tipo]
|
|
if (!data || data.length === 0) return []
|
|
|
|
return data.map((point, i) => {
|
|
const x = padding + (i / (data.length - 1 || 1)) * chartWidth
|
|
const y = height - padding - (point.value / maxValue.value) * chartHeight
|
|
|
|
return { ...point, x, y }
|
|
})
|
|
}
|
|
|
|
function getPointsDepositoForTipo(tipo: string) {
|
|
const data = dataDepositoByTipo.value[tipo]
|
|
if (!data || data.length === 0) return []
|
|
|
|
return data.map((point, i) => {
|
|
const x = padding + (i / (data.length - 1 || 1)) * chartWidth
|
|
const y = height - padding - (point.value / maxValue.value) * chartHeight
|
|
|
|
return { ...point, x, y }
|
|
})
|
|
}
|
|
|
|
function getLinePagadoPathForTipo(tipo: string) {
|
|
const points = getPointsPagadoForTipo(tipo)
|
|
if (points.length === 0) return ''
|
|
|
|
return points.map((point, i) => {
|
|
return i === 0 ? `M ${point.x} ${point.y}` : `L ${point.x} ${point.y}`
|
|
}).join(' ')
|
|
}
|
|
|
|
function getLineDepositoPathForTipo(tipo: string) {
|
|
const points = getPointsDepositoForTipo(tipo)
|
|
if (points.length === 0) return ''
|
|
|
|
return points.map((point, i) => {
|
|
return i === 0 ? `M ${point.x} ${point.y}` : `L ${point.x} ${point.y}`
|
|
}).join(' ')
|
|
}
|
|
|
|
function getAreaPagadoPathForTipo(tipo: string) {
|
|
const points = getPointsPagadoForTipo(tipo)
|
|
if (points.length === 0) return ''
|
|
|
|
const linePart = points.map((point, i) => {
|
|
return i === 0 ? `M ${point.x} ${point.y}` : `L ${point.x} ${point.y}`
|
|
}).join(' ')
|
|
|
|
const lastPoint = points[points.length - 1]
|
|
const firstPoint = points[0]
|
|
|
|
if (!lastPoint || !firstPoint) return ''
|
|
|
|
return `${linePart} L ${lastPoint.x} ${height - padding} L ${firstPoint.x} ${height - padding} Z`
|
|
}
|
|
|
|
function getAreaDepositoPathForTipo(tipo: string) {
|
|
const points = getPointsDepositoForTipo(tipo)
|
|
if (points.length === 0) return ''
|
|
|
|
const linePart = points.map((point, i) => {
|
|
return i === 0 ? `M ${point.x} ${point.y}` : `L ${point.x} ${point.y}`
|
|
}).join(' ')
|
|
|
|
const lastPoint = points[points.length - 1]
|
|
const firstPoint = points[0]
|
|
|
|
if (!lastPoint || !firstPoint) return ''
|
|
|
|
return `${linePart} L ${lastPoint.x} ${height - padding} L ${firstPoint.x} ${height - padding} Z`
|
|
}
|
|
|
|
function getTotalPagadoForTipo(tipo: string) {
|
|
const data = dataPagadoByTipo.value[tipo]
|
|
if (!data || data.length === 0) return 0
|
|
return data[data.length - 1]?.value || 0
|
|
}
|
|
|
|
function getTotalDepositoForTipo(tipo: string) {
|
|
const data = dataDepositoByTipo.value[tipo]
|
|
if (!data || data.length === 0) return 0
|
|
return data[data.length - 1]?.value || 0
|
|
}
|
|
|
|
function formatValue(value: number): string {
|
|
if (value >= 1000) {
|
|
return (value / 1000).toFixed(1) + 'k'
|
|
}
|
|
return value.toFixed(0)
|
|
}
|
|
</script>
|