Files
seguidorDeLotes/nuxt4/app/components/vinculaciones/ModalAsignar.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

258 lines
7.2 KiB
Vue

<template>
<UModal v-model:open="isOpen" :title="titulo" class="max-w-2xl">
<template #body>
<div class="space-y-4">
<!-- Resumen de registros a vincular -->
<UCard v-if="registros.length > 0" class="bg-gray-50 dark:bg-gray-800">
<div class="text-sm">
<p class="font-medium mb-2">
Registros a vincular ({{ registros.length }}):
</p>
<div class="flex flex-wrap gap-2">
<UBadge
v-for="reg in registros.slice(0, 5)"
:key="reg.id"
:color="tipoColor"
variant="subtle"
>
#{{ reg.id }}
</UBadge>
<UBadge v-if="registros.length > 5" color="gray" variant="subtle">
+{{ registros.length - 5 }} más
</UBadge>
</div>
</div>
</UCard>
<!-- Selector de lote -->
<div>
<label class="block text-sm font-medium mb-2">Seleccionar lote destino</label>
<USelect
v-model="loteSeleccionado"
:items="lotesOptions"
label-key="label"
value-key="value"
searchable
search-placeholder="Buscar por código..."
placeholder="Selecciona un lote"
class="w-full"
>
<template #option="{ option }">
<div class="flex items-center gap-2">
<UBadge :color="getLoteColor(option.tipo)" size="xs" variant="subtle">
{{ option.tipo }}
</UBadge>
<span class="font-mono">{{ option.codigo || 'Sin código' }}</span>
<span v-if="option.cantidad_kg" class="text-gray-500 text-xs">
({{ option.cantidad_kg }} kg)
</span>
</div>
</template>
</USelect>
</div>
<!-- O crear nuevo lote -->
<div class="text-center text-sm text-gray-500">
<span>O</span>
</div>
<UButton
icon="i-heroicons-plus"
label="Crear nuevo lote"
variant="outline"
block
@click="mostrarFormNuevoLote = !mostrarFormNuevoLote"
/>
<!-- Form para nuevo lote -->
<div v-if="mostrarFormNuevoLote" class="border rounded-lg p-4 space-y-3">
<div class="grid grid-cols-2 gap-3">
<UFormField label="Código (opcional)">
<UInput v-model="nuevoLote.codigo" placeholder="ej: LOTE-001" />
</UFormField>
<UFormField label="Tipo" required>
<USelect
v-model="nuevoLote.tipo"
:items="TIPOS_LOTE"
label-key="label"
value-key="value"
placeholder="Selecciona tipo"
/>
</UFormField>
</div>
<UFormField label="Cantidad (kg)">
<UInput v-model.number="nuevoLote.cantidad_kg" type="number" placeholder="0.00" />
</UFormField>
</div>
<!-- Observaciones -->
<UFormField label="Observaciones (opcional)">
<UTextarea
v-model="observaciones"
placeholder="Notas sobre esta vinculación..."
rows="2"
/>
</UFormField>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<UButton
label="Cancelar"
variant="outline"
@click="isOpen = false"
/>
<UButton
icon="i-heroicons-link"
:label="registros.length > 1 ? `Vincular ${registros.length} registros` : 'Vincular'"
color="primary"
:loading="guardando"
:disabled="!puedeGuardar"
@click="guardar"
/>
</div>
</template>
</UModal>
</template>
<script setup lang="ts">
import type { TipoRegistro, Lote } from '~/composables/useRegistrosExternos'
const props = defineProps<{
tipo: TipoRegistro
registros: any[]
}>()
const emit = defineEmits<{
'vinculado': []
}>()
const isOpen = defineModel<boolean>('open', { default: false })
const { vincular, vincularMasivo, getTipoColor } = useRegistrosExternos()
const { fetchLotes, createLote, TIPOS_LOTE } = useLotes()
// Estado
const lotes = ref<Lote[]>([])
const loteSeleccionado = ref<string | null>(null)
const mostrarFormNuevoLote = ref(false)
const nuevoLote = ref({
codigo: '',
tipo: '',
cantidad_kg: null as number | null,
})
const observaciones = ref('')
const guardando = ref(false)
// Computed
const titulo = computed(() => {
const tipoLabel = props.tipo.charAt(0).toUpperCase() + props.tipo.slice(1)
return props.registros.length > 1
? `Vincular ${props.registros.length} ${tipoLabel}s a lote`
: `Vincular ${tipoLabel} a lote`
})
const tipoColor = computed(() => getTipoColor(props.tipo))
const lotesOptions = computed(() =>
lotes.value.map(l => ({
value: l.id,
label: l.codigo || `Lote ${l.id.slice(0, 8)}...`,
codigo: l.codigo,
tipo: l.tipo,
cantidad_kg: l.cantidad_kg,
}))
)
const puedeGuardar = computed(() => {
if (mostrarFormNuevoLote.value) {
return nuevoLote.value.tipo !== ''
}
return loteSeleccionado.value !== null
})
// Funciones
const getLoteColor = (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 cargarLotes = async () => {
const resultado = await fetchLotes({ limit: 100 })
lotes.value = resultado
}
const guardar = async () => {
guardando.value = true
try {
let loteId = loteSeleccionado.value
// Si se quiere crear un nuevo lote
if (mostrarFormNuevoLote.value && nuevoLote.value.tipo) {
const nuevoLoteCreado = await createLote({
codigo: nuevoLote.value.codigo || undefined,
tipo: nuevoLote.value.tipo,
cantidad_kg: nuevoLote.value.cantidad_kg || undefined,
})
if (!nuevoLoteCreado) {
throw new Error('No se pudo crear el lote')
}
loteId = nuevoLoteCreado.id
}
if (!loteId) {
throw new Error('Debes seleccionar o crear un lote')
}
// Vincular registros
if (props.registros.length === 1) {
await vincular(props.tipo, props.registros[0].id, loteId, {
observaciones: observaciones.value || undefined,
datosCache: props.registros[0],
})
} else {
await vincularMasivo(
props.registros.map(r => ({
tipo: props.tipo,
registroId: r.id,
loteId: loteId!,
observaciones: observaciones.value || undefined,
datosCache: r,
}))
)
}
emit('vinculado')
isOpen.value = false
} catch (error: any) {
console.error('Error vinculando:', error)
} finally {
guardando.value = false
}
}
// Reset al abrir
watch(isOpen, (open) => {
if (open) {
loteSeleccionado.value = null
mostrarFormNuevoLote.value = false
nuevoLote.value = { codigo: '', tipo: '', cantidad_kg: null }
observaciones.value = ''
cargarLotes()
}
})
</script>