Files
seguidorDeLotes/nuxt4/app/components/operaciones/Form.vue
josedario87 cb0261dad3
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
Fix: múltiples correcciones de UI y funcionalidad
Cambios realizados:

1. Favicon:
   - Actualizar configuración en app.vue para usar iconos PNG correctos
   - Agregar links con tamaños 32x32 y 16x16
   - Actualizar theme-color a #16a34a (verde del proyecto)

2. Modal de Crear Operación:
   - Reestructurar con slots #header y #body para scroll correcto
   - Extraer header del Form.vue y moverlo al modal
   - Eliminar UCard del componente Form.vue
   - Agregar max-h-[80vh] para limitar altura
   - Ahora muestra scrollbar vertical cuando el contenido excede el espacio

3. USelect de filtro de operaciones:
   - Corregir de :options a :items (API correcta de NuxtUI v4)
   - Agregar label-key y value-key
   - Agregar computed selectOptions (igual que en Lotes)
   - Cambiar filtroTipo de ref('') a ref<string | null>(null)
   - Ahora el filtro funciona correctamente

Archivos modificados:
- app/app.vue: Configuración favicon y modal operaciones
- app/components/operaciones/Form.vue: Eliminar UCard
- app/components/operaciones/Table.vue: Corregir USelect
2025-11-22 03:29:27 -06:00

311 lines
9.4 KiB
Vue

<template>
<div class="space-y-6">
<!-- Paso 1: Tipo de Operación -->
<div v-if="step === 1" class="space-y-4">
<div class="flex items-center justify-between">
<div>
<h4 class="font-medium text-lg">1. Tipo de operación</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">Selecciona el proceso que ejecutarás.</p>
</div>
<UBadge color="gray" variant="soft">Selecciona uno</UBadge>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<button
v-for="tipo in TIPOS_OPERACION"
:key="tipo.value"
@click="formState.tipo = tipo.value"
class="p-4 border-2 rounded-lg transition-all text-left hover:-translate-y-0.5 hover:shadow-md"
:class="{
'border-primary bg-primary/10 ring-2 ring-primary/30': formState.tipo === tipo.value,
'border-gray-200 dark:border-slate-700': formState.tipo !== tipo.value,
}"
>
<div class="flex items-center gap-3">
<UIcon :name="tipo.icon" class="w-6 h-6" />
<div>
<p class="font-semibold">{{ tipo.label }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Seleccionar</p>
</div>
</div>
</button>
</div>
<div class="flex gap-2 justify-end pt-4">
<UButton
label="Cancelar"
variant="outline"
@click="$emit('cancel')"
/>
<UButton
label="Siguiente"
color="primary"
:disabled="!formState.tipo"
@click="step = 2"
/>
</div>
</div>
<!-- Paso 2: Seleccionar Lotes de Entrada (Inputs) -->
<div v-else-if="step === 2" class="space-y-4">
<div class="flex items-center justify-between">
<div>
<h4 class="font-medium text-lg">2. Lotes de entrada</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">
Usa lotes para la operación de <strong>{{ getTipoLabel(formState.tipo) }}</strong>.
</p>
</div>
<UBadge color="blue" variant="soft">{{ formState.inputs.length }} seleccionados</UBadge>
</div>
<div v-if="loadingLotes" class="text-center py-4">
<UIcon name="i-heroicons-arrow-path" class="animate-spin w-6 h-6" />
</div>
<div v-else class="space-y-2">
<div
v-for="lote in lotesDisponibles"
:key="lote.id"
class="p-3 border rounded-lg cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-sm"
:class="{
'border-primary bg-primary/10 ring-1 ring-primary/30': isLoteSeleccionado(lote.id),
'border-gray-200 dark:border-slate-700': !isLoteSeleccionado(lote.id),
}"
@click="toggleLoteInput(lote)"
>
<div class="flex items-center justify-between">
<div>
<span class="font-mono font-semibold">{{ lote.codigo || lote.id.substring(0, 8) }}</span>
<UBadge :color="getTipoColor(lote.tipo)" variant="subtle" class="ml-2">
{{ getTipoLabel(lote.tipo) }}
</UBadge>
<span v-if="lote.cantidad_kg" class="ml-2 text-sm text-gray-600 dark:text-gray-400">
{{ lote.cantidad_kg.toLocaleString('es-AR') }} kg
</span>
</div>
<UIcon
v-if="isLoteSeleccionado(lote.id)"
name="i-heroicons-check-circle"
class="w-5 h-5 text-primary"
/>
</div>
</div>
</div>
<div class="flex gap-2 justify-end pt-4">
<UButton
label="Anterior"
variant="outline"
@click="step = 1"
/>
<UButton
label="Siguiente"
color="primary"
:disabled="formState.inputs.length === 0"
@click="step = 3"
/>
</div>
</div>
<!-- Paso 3: Definir Lotes de Salida (Outputs) -->
<div v-else-if="step === 3" class="space-y-4">
<div class="flex items-center justify-between">
<div>
<h4 class="font-medium text-lg">3. Lotes de salida</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">Define los lotes resultantes de la operación.</p>
</div>
<UBadge color="emerald" variant="soft">{{ formState.outputs.length }} outputs</UBadge>
</div>
<div class="space-y-3">
<div
v-for="(output, index) in formState.outputs"
:key="index"
class="p-4 border rounded-lg space-y-3 bg-white/80 dark:bg-slate-900/60"
>
<div class="flex justify-between items-center">
<h5 class="font-medium">Lote de salida {{ index + 1 }}</h5>
<UButton
icon="i-heroicons-trash"
size="xs"
variant="ghost"
color="red"
@click="removeOutput(index)"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<UFormGroup label="Código (opcional)">
<UInput v-model="output.codigo" placeholder="Ej: PRIM-001" icon="i-heroicons-hashtag" />
</UFormGroup>
<UFormGroup label="Tipo" required>
<USelect
v-model="output.tipo"
:items="TIPOS_LOTE"
label-key="label"
value-key="value"
placeholder="Selecciona tipo"
searchable
/>
</UFormGroup>
<UFormGroup label="Cantidad (kg)">
<UInput v-model.number="output.cantidad_kg" type="number" step="0.01" placeholder="0.00" icon="i-heroicons-scale" />
</UFormGroup>
</div>
</div>
</div>
<UButton
icon="i-heroicons-plus"
label="Agregar lote de salida"
variant="outline"
block
@click="addOutput"
/>
<div class="flex gap-2 justify-end pt-4">
<UButton
label="Anterior"
variant="outline"
@click="step = 2"
/>
<UButton
label="Crear Operación"
color="primary"
:loading="loading"
:disabled="formState.outputs.length === 0 || !allOutputsValid"
@click="handleSubmit"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Lote } from '~/composables/useLotes'
const emit = defineEmits<{
cancel: []
success: []
}>()
const { fetchLotes, createOperacion, TIPOS_OPERACION, TIPOS_LOTE } = useLotes()
const step = ref(1)
const loading = ref(false)
const loadingLotes = ref(false)
const lotesDisponibles = ref<Lote[]>([])
const formState = ref({
tipo: '',
inputs: [] as Array<{ lote_id: string; cantidad_kg?: number; _lote?: Lote }>,
outputs: [] as Array<{ codigo?: string; tipo: string; cantidad_kg?: number }>,
})
const allOutputsValid = computed(() => {
return formState.value.outputs.every((output) => output.tipo)
})
const loadLotes = async () => {
loadingLotes.value = true
try {
lotesDisponibles.value = await fetchLotes()
} finally {
loadingLotes.value = false
}
}
const isLoteSeleccionado = (loteId: string) => {
return formState.value.inputs.some((input) => input.lote_id === loteId)
}
const toggleLoteInput = (lote: Lote) => {
const index = formState.value.inputs.findIndex((input) => input.lote_id === lote.id)
if (index >= 0) {
formState.value.inputs.splice(index, 1)
} else {
formState.value.inputs.push({
lote_id: lote.id,
cantidad_kg: lote.cantidad_kg || undefined,
_lote: lote,
})
}
}
const addOutput = () => {
formState.value.outputs.push({
codigo: '',
tipo: '',
cantidad_kg: undefined,
})
}
const removeOutput = (index: number) => {
formState.value.outputs.splice(index, 1)
}
const getTipoLabel = (tipo: string) => {
const foundOp = TIPOS_OPERACION.find((t) => t.value === tipo)
if (foundOp) return foundOp.label
const foundLote = TIPOS_LOTE.find((t) => t.value === tipo)
return foundLote?.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 handleSubmit = async () => {
loading.value = true
try {
const result = await createOperacion({
tipo: formState.value.tipo,
inputs: formState.value.inputs.map((input) => ({
lote_id: input.lote_id,
cantidad_kg: input.cantidad_kg,
})),
outputs: formState.value.outputs.map((output) => ({
codigo: output.codigo || undefined,
tipo: output.tipo,
cantidad_kg: output.cantidad_kg,
})),
})
if (result) {
emit('success')
}
} finally {
loading.value = false
}
}
// Cargar lotes cuando se monta el componente
onMounted(() => {
loadLotes()
})
// Agregar un output por defecto al inicio
watch(
() => step.value,
(newStep) => {
if (newStep === 3 && formState.value.outputs.length === 0) {
addOutput()
}
}
)
</script>