All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
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
416 lines
13 KiB
Vue
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>
|