All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
181 lines
5.8 KiB
Vue
181 lines
5.8 KiB
Vue
<template>
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h3 class="text-xl font-bold">Trazabilidad de Lote</h3>
|
|
<p class="text-sm text-gray-500">Historial completo desde los ingresos iniciales</p>
|
|
</div>
|
|
<UButton
|
|
icon="i-heroicons-x-mark"
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="$emit('close')"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-if="loading" class="flex justify-center py-8">
|
|
<UIcon name="i-heroicons-arrow-path" class="animate-spin w-8 h-8" />
|
|
</div>
|
|
|
|
<div v-else-if="trazabilidad" class="space-y-6">
|
|
<!-- Estadísticas -->
|
|
<div class="grid grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg">
|
|
<div class="text-center">
|
|
<p class="text-2xl font-bold text-blue-600">{{ trazabilidad.estadisticas.total_ancestros }}</p>
|
|
<p class="text-sm text-gray-600">Lotes ancestros</p>
|
|
</div>
|
|
<div class="text-center">
|
|
<p class="text-2xl font-bold text-green-600">{{ trazabilidad.estadisticas.profundidad_maxima }}</p>
|
|
<p class="text-sm text-gray-600">Niveles de profundidad</p>
|
|
</div>
|
|
<div class="text-center">
|
|
<p class="text-2xl font-bold text-orange-600">
|
|
{{ trazabilidad.estadisticas.kg_iniciales.toLocaleString('es-AR') }} kg
|
|
</p>
|
|
<p class="text-sm text-gray-600">Kilos iniciales</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Árbol de Trazabilidad -->
|
|
<div class="space-y-2">
|
|
<h4 class="font-semibold text-lg">Historial</h4>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="(item, index) in sortedHistorial"
|
|
:key="index"
|
|
class="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors"
|
|
:class="{
|
|
'bg-blue-50': item.profundidad === 0,
|
|
'border-l-4 border-blue-500': item.profundidad === 0,
|
|
}"
|
|
>
|
|
<!-- Indicador de profundidad -->
|
|
<div class="flex items-center gap-2 min-w-[100px]">
|
|
<UBadge :color="getProfundidadColor(item.profundidad)" variant="subtle" size="xs">
|
|
Nivel {{ item.profundidad }}
|
|
</UBadge>
|
|
</div>
|
|
|
|
<!-- Indentación visual -->
|
|
<div class="flex items-center">
|
|
<div :style="{ width: `${item.profundidad * 20}px` }" class="border-l-2 border-gray-300"></div>
|
|
<UIcon
|
|
v-if="item.profundidad > 0"
|
|
name="i-heroicons-arrow-turn-down-right"
|
|
class="w-4 h-4 text-gray-400 mx-1"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Información del lote -->
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-mono font-semibold">{{ item.codigo || item.lote_id.substring(0, 8) }}</span>
|
|
<UBadge :color="getTipoColor(item.tipo)" variant="subtle">
|
|
{{ getTipoLabel(item.tipo) }}
|
|
</UBadge>
|
|
<span v-if="item.cantidad_kg" class="text-sm text-gray-600">
|
|
{{ item.cantidad_kg.toLocaleString('es-AR') }} kg
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Operación que generó este lote -->
|
|
<div v-if="item.operacion_tipo" class="mt-1 text-sm text-gray-500">
|
|
<UIcon name="i-heroicons-beaker" class="w-3 h-3 inline" />
|
|
Generado por: <span class="font-medium">{{ getOperacionLabel(item.operacion_tipo) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Leyenda -->
|
|
<div class="pt-4 border-t">
|
|
<p class="text-xs text-gray-500">
|
|
<UIcon name="i-heroicons-information-circle" class="w-4 h-4 inline" />
|
|
Los lotes se muestran desde el más reciente (nivel 0) hasta los ingresos iniciales.
|
|
La profundidad indica cuántos pasos atrás se encuentra cada lote en la cadena de trazabilidad.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="text-center py-8 text-gray-500">
|
|
No se encontró información de trazabilidad
|
|
</div>
|
|
</UCard>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { TrazabilidadRow } from '~/composables/useLotes'
|
|
|
|
const props = defineProps<{
|
|
loteId: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
close: []
|
|
}>()
|
|
|
|
const { fetchTrazabilidad, TIPOS_LOTE, TIPOS_OPERACION } = useLotes()
|
|
|
|
const loading = ref(false)
|
|
const trazabilidad = ref<{
|
|
historial: TrazabilidadRow[]
|
|
estadisticas: {
|
|
total_ancestros: number
|
|
profundidad_maxima: number
|
|
kg_iniciales: number
|
|
}
|
|
} | null>(null)
|
|
|
|
const sortedHistorial = computed(() => {
|
|
if (!trazabilidad.value) return []
|
|
return [...trazabilidad.value.historial].sort((a, b) => a.profundidad - b.profundidad)
|
|
})
|
|
|
|
const loadTrazabilidad = async () => {
|
|
loading.value = true
|
|
try {
|
|
trazabilidad.value = await fetchTrazabilidad(props.loteId)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const getTipoLabel = (tipo: string) => {
|
|
const found = TIPOS_LOTE.find((t) => t.value === tipo)
|
|
return found?.label || tipo
|
|
}
|
|
|
|
const getTipoColor = (tipo: string): string => {
|
|
const colorMap: Record<string, string> = {
|
|
uva: 'purple',
|
|
despulpado_primera: 'green',
|
|
despulpado_segunda: 'yellow',
|
|
despulpado_rechazos: 'red',
|
|
oreado: 'orange',
|
|
presecado: 'amber',
|
|
reposo: 'blue',
|
|
secado: 'emerald',
|
|
}
|
|
return colorMap[tipo] || 'gray'
|
|
}
|
|
|
|
const getOperacionLabel = (tipo: string) => {
|
|
const found = TIPOS_OPERACION.find((t) => t.value === tipo)
|
|
return found?.label || tipo
|
|
}
|
|
|
|
const getProfundidadColor = (profundidad: number): string => {
|
|
if (profundidad === 0) return 'blue'
|
|
if (profundidad <= 2) return 'green'
|
|
if (profundidad <= 4) return 'orange'
|
|
return 'red'
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadTrazabilidad()
|
|
})
|
|
</script>
|