Files
seguidorDeLotes/nuxt4/app/components/lotes/Card.vue
josedario87 ce8bad68d5
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m46s
Agregar sistema de vinculaciones con registros externos de Metabase
- Nuevo schema BD para vinculaciones_externas con constraint único por período
- Cliente Metabase para consultar Ingresos, Carretas, Salidas y Rechazos
- Endpoints API para registros externos (/api/externos/*) y vinculaciones (/api/vinculaciones/*)
- Composable useRegistrosExternos con lógica de vinculación individual y masiva
- Componentes: TablaRegistros, ModalAsignar, ProgressDashboard
- Tab "Externos" en app.vue con sub-tabs y dashboard de progreso
- LotesCard.vue ahora muestra registros vinculados al lote
2025-11-29 15:25:26 -06:00

241 lines
7.6 KiB
Vue

<template>
<UCard>
<template #header>
<div class="flex justify-between items-center">
<div>
<h3 class="text-xl font-bold">{{ lote.codigo || 'Sin código' }}</h3>
<UBadge :color="getTipoColor(lote.tipo)" variant="subtle" class="mt-1">
{{ getTipoLabel(lote.tipo) }}
</UBadge>
</div>
<div class="flex gap-2">
<UButton
icon="i-heroicons-pencil"
variant="outline"
size="sm"
label="Editar"
@click="$emit('edit')"
/>
<UButton
icon="i-heroicons-chart-bar"
variant="outline"
size="sm"
color="green"
label="Ver Trazabilidad"
@click="$emit('trazabilidad')"
/>
</div>
</div>
</template>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm text-gray-500">ID</p>
<p class="font-mono text-xs">{{ lote.id }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Fecha de Creación</p>
<p class="font-medium">{{ formatDate(lote.fecha_creado) }}</p>
</div>
<div>
<p class="text-sm text-gray-500">Cantidad</p>
<p class="font-medium text-lg">
{{ lote.cantidad_kg ? `${lote.cantidad_kg.toLocaleString('es-AR')} kg` : '-' }}
</p>
</div>
<div>
<p class="text-sm text-gray-500">Lugar ID</p>
<p class="font-medium">{{ lote.lugar_id || '-' }}</p>
</div>
</div>
<!-- Sección de Vinculaciones Externas -->
<div class="pt-4 border-t">
<div class="flex items-center justify-between mb-3">
<p class="text-sm text-gray-500 font-medium">Registros Externos Vinculados</p>
<UButton
v-if="!vinculacionesLoading && !vinculacionesCargadas"
icon="i-heroicons-link"
size="xs"
variant="outline"
label="Cargar vinculaciones"
@click="cargarVinculaciones"
/>
<UButton
v-if="vinculacionesCargadas"
icon="i-heroicons-arrow-path"
size="xs"
variant="ghost"
@click="cargarVinculaciones"
/>
</div>
<!-- Loading -->
<div v-if="vinculacionesLoading" class="flex items-center gap-2 text-gray-500">
<UIcon name="i-heroicons-arrow-path" class="w-4 h-4 animate-spin" />
<span class="text-sm">Cargando vinculaciones...</span>
</div>
<!-- Contenido de vinculaciones -->
<div v-else-if="vinculacionesCargadas">
<!-- Sin vinculaciones -->
<div v-if="vinculacionesMeta?.total === 0" class="text-center py-4 text-gray-400">
<UIcon name="i-heroicons-link-slash" class="w-8 h-8 mx-auto mb-2" />
<p class="text-sm">No hay registros externos vinculados</p>
</div>
<!-- Con vinculaciones -->
<div v-else class="space-y-3">
<!-- Resumen por tipo -->
<div class="grid grid-cols-4 gap-2">
<div
v-for="(count, tipo) in vinculacionesMeta?.por_tipo"
:key="tipo"
class="text-center p-2 rounded-lg bg-gray-50 dark:bg-gray-800"
>
<UIcon :name="getTipoRegistroIcon(tipo as string)" class="w-5 h-5 mx-auto mb-1" :class="`text-${getTipoRegistroColor(tipo as string)}-500`" />
<p class="text-lg font-bold">{{ count }}</p>
<p class="text-xs text-gray-500 capitalize">{{ tipo }}</p>
</div>
</div>
<!-- Lista de vinculaciones agrupadas -->
<div class="space-y-2">
<template v-for="(items, tipo) in vinculacionesAgrupados" :key="tipo">
<div v-if="items && items.length > 0" class="border rounded-lg p-2">
<p class="text-xs font-medium text-gray-500 mb-1 capitalize flex items-center gap-1">
<UIcon :name="getTipoRegistroIcon(tipo as string)" class="w-3 h-3" />
{{ tipo }} ({{ items.length }})
</p>
<div class="flex flex-wrap gap-1">
<UBadge
v-for="v in items.slice(0, 5)"
:key="v.id"
:color="getTipoRegistroColor(tipo as string)"
variant="subtle"
size="xs"
>
#{{ v.registro_id }}
</UBadge>
<UBadge v-if="items.length > 5" color="gray" variant="subtle" size="xs">
+{{ items.length - 5 }} más
</UBadge>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
<div v-if="lote.meta" class="pt-4 border-t">
<p class="text-sm text-gray-500 mb-2">Información Adicional</p>
<UCard class="bg-gray-50 dark:bg-slate-900/70 border border-gray-200 dark:border-slate-800">
<pre class="text-xs overflow-x-auto text-gray-800 dark:text-slate-200 whitespace-pre-wrap">
{{ JSON.stringify(lote.meta, null, 2) }}
</pre>
</UCard>
</div>
</div>
</UCard>
</template>
<script setup lang="ts">
import type { Lote } from '~/composables/useLotes'
import type { Vinculacion } from '~/composables/useRegistrosExternos'
const props = defineProps<{
lote: Lote
}>()
const emit = defineEmits<{
edit: []
trazabilidad: []
}>()
const { TIPOS_LOTE } = useLotes()
const { fetchVinculacionesByLote, getTipoIcon, getTipoColor: getTipoRegistroColor } = useRegistrosExternos()
// Estado de vinculaciones
const vinculacionesLoading = ref(false)
const vinculacionesCargadas = ref(false)
const vinculacionesAgrupados = ref<{
ingresos: Vinculacion[]
carretas: Vinculacion[]
salidas: Vinculacion[]
rechazos: Vinculacion[]
} | null>(null)
const vinculacionesMeta = ref<{
lote_id: string
total: number
por_tipo: {
ingresos: number
carretas: number
salidas: number
rechazos: number
}
} | null>(null)
const cargarVinculaciones = async () => {
vinculacionesLoading.value = true
try {
const result = await fetchVinculacionesByLote(props.lote.id)
vinculacionesAgrupados.value = result.agrupados || null
vinculacionesMeta.value = result.meta || null
vinculacionesCargadas.value = true
} catch (error) {
console.error('Error cargando vinculaciones:', error)
} finally {
vinculacionesLoading.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 getTipoRegistroIcon = (tipo: string): string => {
const iconMap: Record<string, string> = {
ingresos: 'i-heroicons-inbox-arrow-down',
carretas: 'i-heroicons-truck',
salidas: 'i-heroicons-arrow-up-tray',
rechazos: 'i-heroicons-x-circle',
}
return iconMap[tipo] || 'i-heroicons-document'
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('es-AR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
// Cargar vinculaciones automáticamente al montar
onMounted(() => {
cargarVinculaciones()
})
</script>