Files
seguidorDeLotes/nuxt4/app/components/operaciones/Form.vue
josedario87 599a7999c9
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
Feat: agregar creación de lotes de input en formulario de operación
Ahora el formulario de crear operación permite crear nuevos lotes de
entrada directamente desde el paso 2, sin necesidad de salir del modal.

Cambios:
- Agregar botón "Nuevo lote de entrada" en el paso 2
- Mostrar formulario inline para crear lote con código, tipo y cantidad
- Al crear el lote, se agrega automáticamente a la lista de disponibles
- Se selecciona automáticamente como input de la operación
- Importar createLote del composable useLotes
- Agregar estado showCreateInputForm y creatingInput
- Implementar funciones cancelCreateInput y handleCreateInput

Beneficios:
- Flujo más ágil sin interrupciones
- Consistencia con la creación de lotes de output
- Mejor experiencia de usuario
2025-11-22 03:33:19 -06:00

416 lines
13 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">
<!-- Lotes existentes -->
<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>
<!-- Formulario para crear nuevo lote de entrada -->
<div v-if="showCreateInputForm" class="p-4 border-2 border-dashed border-primary/50 rounded-lg space-y-3 bg-primary/5">
<div class="flex justify-between items-center">
<h5 class="font-medium text-primary">Nuevo lote de entrada</h5>
<UButton
icon="i-heroicons-x-mark"
size="xs"
variant="ghost"
@click="cancelCreateInput"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<UFormGroup label="Código (opcional)">
<UInput v-model="newInputLote.codigo" placeholder="Ej: UVA-001" icon="i-heroicons-hashtag" />
</UFormGroup>
<UFormGroup label="Tipo" required>
<USelect
v-model="newInputLote.tipo"
:items="TIPOS_LOTE"
label-key="label"
value-key="value"
placeholder="Selecciona tipo"
searchable
/>
</UFormGroup>
<UFormGroup label="Cantidad (kg)">
<UInput v-model.number="newInputLote.cantidad_kg" type="number" step="0.01" placeholder="0.00" icon="i-heroicons-scale" />
</UFormGroup>
</div>
<div class="flex gap-2 justify-end">
<UButton
label="Cancelar"
variant="outline"
size="sm"
@click="cancelCreateInput"
/>
<UButton
label="Crear y Seleccionar"
color="primary"
size="sm"
:loading="creatingInput"
:disabled="!newInputLote.tipo"
@click="handleCreateInput"
/>
</div>
</div>
</div>
<UButton
v-if="!showCreateInputForm"
icon="i-heroicons-plus"
label="Nuevo lote de entrada"
variant="outline"
block
@click="showCreateInputForm = true"
/>
<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, createLote, createOperacion, TIPOS_OPERACION, TIPOS_LOTE } = useLotes()
const step = ref(1)
const loading = ref(false)
const loadingLotes = ref(false)
const lotesDisponibles = ref<Lote[]>([])
const showCreateInputForm = ref(false)
const creatingInput = ref(false)
const newInputLote = ref({
codigo: '',
tipo: '',
cantidad_kg: undefined as number | undefined,
})
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 cancelCreateInput = () => {
showCreateInputForm.value = false
newInputLote.value = {
codigo: '',
tipo: '',
cantidad_kg: undefined,
}
}
const handleCreateInput = async () => {
creatingInput.value = true
try {
const createdLote = await createLote({
codigo: newInputLote.value.codigo || undefined,
tipo: newInputLote.value.tipo,
cantidad_kg: newInputLote.value.cantidad_kg,
})
if (createdLote) {
// Agregar a la lista de lotes disponibles
lotesDisponibles.value.unshift(createdLote)
// Seleccionarlo automáticamente como input
formState.value.inputs.push({
lote_id: createdLote.id,
cantidad_kg: createdLote.cantidad_kg || undefined,
_lote: createdLote,
})
// Cerrar el formulario
cancelCreateInput()
}
} finally {
creatingInput.value = false
}
}
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>