cambio heavys
This commit is contained in:
@@ -2,23 +2,46 @@
|
||||
<template>
|
||||
<UCard class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
<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 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>
|
||||
<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"
|
||||
/>
|
||||
|
||||
<!-- 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-gray-700/30 text-gray-400 hover:bg-gray-700/50 hover:text-gray-300'
|
||||
]"
|
||||
>
|
||||
<UIcon :name="option.icon" class="w-3.5 h-3.5" />
|
||||
<span>{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -112,10 +135,12 @@
|
||||
</circle>
|
||||
</g>
|
||||
|
||||
<!-- X-axis labels -->
|
||||
<g v-if="tiposActivos.length > 0" v-for="(point, i) in getPointsPagadoForTipo(tiposActivos[0].value)" :key="`label-${i}`">
|
||||
<!-- X-axis labels (distribuidos uniformemente) -->
|
||||
<g v-if="tiposActivos.length > 0">
|
||||
<text
|
||||
v-if="i % Math.ceil(getPointsPagadoForTipo(tiposActivos[0].value).length / 6) === 0 || i === getPointsPagadoForTipo(tiposActivos[0].value).length - 1"
|
||||
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"
|
||||
@@ -174,6 +199,18 @@ 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')
|
||||
@@ -240,30 +277,135 @@ interface DataPoint {
|
||||
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)
|
||||
.sort((a, b) => new Date(a.created_at!).getTime() - new Date(b.created_at!).getTime())
|
||||
|
||||
if (ingresosFiltrados.length === 0) {
|
||||
result[tipo] = []
|
||||
return
|
||||
}
|
||||
|
||||
const porDia = new Map<string, number>()
|
||||
const porPeriodo = new Map<string, number>()
|
||||
|
||||
ingresosFiltrados.forEach(ingreso => {
|
||||
const fecha = new Date(ingreso.created_at!).toLocaleDateString('es-HN')
|
||||
const fecha = new Date(ingreso.created_at!)
|
||||
const key = getDateKey(fecha, granularity)
|
||||
let valor = 0
|
||||
|
||||
if (tipo === 'uva') {
|
||||
// Si estamos en modo seco, convertir uva a qq (dividir entre 500)
|
||||
valor = modoSeco.value ? ingreso.peso_neto / 500 : ingreso.peso_neto
|
||||
} else if (tipo === 'verde') {
|
||||
valor = ingreso.peso_neto
|
||||
@@ -271,21 +413,24 @@ const dataPagadoByTipo = computed(() => {
|
||||
valor = ingreso.peso_seco
|
||||
}
|
||||
|
||||
porDia.set(fecha, (porDia.get(fecha) || 0) + valor)
|
||||
porPeriodo.set(key, (porPeriodo.get(key) || 0) + valor)
|
||||
})
|
||||
|
||||
let acumulado = 0
|
||||
const puntos: DataPoint[] = []
|
||||
|
||||
Array.from(porDia.entries()).forEach(([fecha, valor]) => {
|
||||
timeRange.forEach(fecha => {
|
||||
const key = getDateKey(fecha, granularity)
|
||||
const valor = porPeriodo.get(key) || 0
|
||||
acumulado += valor
|
||||
|
||||
puntos.push({
|
||||
date: new Date(fecha),
|
||||
date: fecha,
|
||||
value: acumulado,
|
||||
x: 0,
|
||||
y: 0,
|
||||
label: fecha,
|
||||
dateLabel: new Date(fecha).toLocaleDateString('es-HN', { day: 'numeric', month: 'short' })
|
||||
label: key,
|
||||
dateLabel: formatDateByGranularity(fecha, granularity)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -299,27 +444,40 @@ const dataPagadoByTipo = computed(() => {
|
||||
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)
|
||||
.sort((a, b) => new Date(a.created_at!).getTime() - new Date(b.created_at!).getTime())
|
||||
|
||||
const pagadosFiltrados = props.ingresos
|
||||
.filter(i => i.tipo === tipo)
|
||||
.filter(i => i.estado === 'pagado')
|
||||
.filter(i => i.created_at)
|
||||
|
||||
if (todosFiltrados.length === 0) {
|
||||
result[tipo] = []
|
||||
return
|
||||
}
|
||||
|
||||
const totalPorDia = new Map<string, number>()
|
||||
const pagadoPorDia = new Map<string, number>()
|
||||
const totalPorPeriodo = new Map<string, number>()
|
||||
const pagadoPorPeriodo = new Map<string, number>()
|
||||
|
||||
todosFiltrados.forEach(ingreso => {
|
||||
const fecha = new Date(ingreso.created_at!).toLocaleDateString('es-HN')
|
||||
const fecha = new Date(ingreso.created_at!)
|
||||
const key = getDateKey(fecha, granularity)
|
||||
let valor = 0
|
||||
|
||||
if (tipo === 'uva') {
|
||||
@@ -330,11 +488,12 @@ const dataDepositoByTipo = computed(() => {
|
||||
valor = ingreso.peso_seco
|
||||
}
|
||||
|
||||
totalPorDia.set(fecha, (totalPorDia.get(fecha) || 0) + valor)
|
||||
totalPorPeriodo.set(key, (totalPorPeriodo.get(key) || 0) + valor)
|
||||
})
|
||||
|
||||
pagadosFiltrados.forEach(ingreso => {
|
||||
const fecha = new Date(ingreso.created_at!).toLocaleDateString('es-HN')
|
||||
const fecha = new Date(ingreso.created_at!)
|
||||
const key = getDateKey(fecha, granularity)
|
||||
let valor = 0
|
||||
|
||||
if (tipo === 'uva') {
|
||||
@@ -345,28 +504,26 @@ const dataDepositoByTipo = computed(() => {
|
||||
valor = ingreso.peso_seco
|
||||
}
|
||||
|
||||
pagadoPorDia.set(fecha, (pagadoPorDia.get(fecha) || 0) + valor)
|
||||
pagadoPorPeriodo.set(key, (pagadoPorPeriodo.get(key) || 0) + valor)
|
||||
})
|
||||
|
||||
let acumuladoTotal = 0
|
||||
let acumuladoPagado = 0
|
||||
const puntos: DataPoint[] = []
|
||||
|
||||
const fechasUnicas = new Set([...totalPorDia.keys(), ...pagadoPorDia.keys()])
|
||||
const fechasOrdenadas = Array.from(fechasUnicas).sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
|
||||
|
||||
fechasOrdenadas.forEach(fecha => {
|
||||
acumuladoTotal += totalPorDia.get(fecha) || 0
|
||||
acumuladoPagado += pagadoPorDia.get(fecha) || 0
|
||||
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: new Date(fecha),
|
||||
date: fecha,
|
||||
value: deposito,
|
||||
x: 0,
|
||||
y: 0,
|
||||
label: fecha,
|
||||
dateLabel: new Date(fecha).toLocaleDateString('es-HN', { day: 'numeric', month: 'short' })
|
||||
label: key,
|
||||
dateLabel: formatDateByGranularity(fecha, granularity)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -456,6 +613,8 @@ function getAreaPagadoPathForTipo(tipo: string) {
|
||||
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`
|
||||
}
|
||||
|
||||
@@ -470,6 +629,8 @@ function getAreaDepositoPathForTipo(tipo: string) {
|
||||
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`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user