feat: agregar selección de seriales en movimientos y mejorar gestión de categorías y subcategorías
This commit is contained in:
parent
1909ebec68
commit
fccb425781
@ -21,6 +21,10 @@ const props = defineProps({
|
||||
warehouseId: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
preSelectedSerials: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
@ -59,10 +63,16 @@ const loadSerials = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await serialService.getAvailableSerials(props.product.id, props.warehouseId);
|
||||
// Filtrar seriales que ya están en el carrito
|
||||
availableSerials.value = (response.serials?.data || []).filter(
|
||||
const fetched = (response.serials?.data || []).filter(
|
||||
serial => !props.excludeSerials.includes(serial.serial_number)
|
||||
);
|
||||
const fetchedIds = new Set(fetched.map(s => s.id));
|
||||
const extras = props.preSelectedSerials.filter(s => !fetchedIds.has(s.id));
|
||||
availableSerials.value = [...extras, ...fetched];
|
||||
|
||||
selectedSerials.value = props.preSelectedSerials.filter(s =>
|
||||
availableSerials.value.some(a => a.id === s.id)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error loading serials:', error);
|
||||
availableSerials.value = [];
|
||||
@ -94,6 +104,7 @@ const clearSelection = () => {
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', {
|
||||
serials: selectedSerials.value,
|
||||
serialNumbers: selectedSerials.value.map(s => s.serial_number),
|
||||
quantity: selectedSerials.value.length
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useForm, apiURL } from '@Services/Api';
|
||||
import { useForm, useApi, apiURL } from '@Services/Api';
|
||||
|
||||
import Modal from '@Holos/Modal.vue';
|
||||
import FormInput from '@Holos/Form/Input.vue';
|
||||
@ -24,6 +24,8 @@ const units = ref([]);
|
||||
const showCategoryCreate = ref(false);
|
||||
const showSubcategoryCreate = ref(false);
|
||||
|
||||
const api = useApi();
|
||||
|
||||
/** Formulario */
|
||||
const form = useForm({
|
||||
name: '',
|
||||
@ -68,9 +70,14 @@ const loadCategories = async () => {
|
||||
};
|
||||
|
||||
const onCategoryChange = () => {
|
||||
const selected = categories.value.find(c => c.id == form.category_id);
|
||||
subcategories.value = selected?.subcategories ?? [];
|
||||
form.subcategory_id = '';
|
||||
subcategories.value = [];
|
||||
if (!form.category_id) return;
|
||||
api.get(apiURL(`categorias/${form.category_id}/subcategorias`), {
|
||||
onSuccess: (data) => {
|
||||
subcategories.value = data.subcategories?.data || data.subcategories || [];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const loadUnits = async () => {
|
||||
@ -117,7 +124,9 @@ const validateSerialsAndUnit = () => {
|
||||
const createProduct = () => {
|
||||
form.transform((data) => ({
|
||||
...data,
|
||||
track_serials: selectedUnit.value ? !selectedUnit.value.allows_decimals && !!data.track_serials : false
|
||||
track_serials: selectedUnit.value ? !selectedUnit.value.allows_decimals && !!data.track_serials : false,
|
||||
category_id: data.category_id || null,
|
||||
subcategory_id: data.category_id ? (data.subcategory_id || null) : null,
|
||||
})).post(apiURL('inventario'), {
|
||||
onSuccess: () => {
|
||||
Notify.success('Producto creado exitosamente');
|
||||
@ -253,7 +262,6 @@ watch(() => form.track_serials, () => {
|
||||
<select
|
||||
v-model="form.category_id"
|
||||
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||
required
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
<option value="">Seleccionar clasificación</option>
|
||||
@ -286,9 +294,12 @@ watch(() => form.track_serials, () => {
|
||||
<select
|
||||
v-model="form.subcategory_id"
|
||||
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="!subcategories.length"
|
||||
:disabled="!form.category_id || !subcategories.length"
|
||||
:required="!!form.category_id"
|
||||
>
|
||||
<option value="">{{ subcategories.length ? 'Sin subclasificación' : 'Selecciona una clasificación primero' }}</option>
|
||||
<option value="">
|
||||
{{ !form.category_id ? 'Selecciona una clasificación primero' : subcategories.length ? 'Seleccionar subclasificación...' : 'Sin subclasificaciones disponibles' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="subcategory in subcategories"
|
||||
:key="subcategory.id"
|
||||
|
||||
@ -7,6 +7,8 @@ import Modal from '@Holos/Modal.vue';
|
||||
import FormInput from '@Holos/Form/Input.vue';
|
||||
import FormError from '@Holos/Form/Elements/Error.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
import CategoryCreateModal from '@Pages/POS/Category/CreateModal.vue';
|
||||
import SubcategoryCreateModal from '@Pages/POS/Category/Subcategories/CreateModal.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@ -24,6 +26,8 @@ const categories = ref([]);
|
||||
const subcategories = ref([]);
|
||||
const units = ref([]);
|
||||
const activeTab = ref('general');
|
||||
const showCategoryCreate = ref(false);
|
||||
const showSubcategoryCreate = ref(false);
|
||||
|
||||
// Estado de equivalencias
|
||||
const equivalences = ref([]);
|
||||
@ -71,6 +75,16 @@ const canHaveEquivalences = computed(() => {
|
||||
return props.product && !props.product.track_serials;
|
||||
});
|
||||
|
||||
// La unidad se bloquea si el producto ya tiene movimientos de inventario
|
||||
const unitLocked = computed(() => {
|
||||
if (!props.product?.id) return false;
|
||||
return (
|
||||
Number(props.product?.movements_count || 0) > 0 ||
|
||||
Number(props.product?.stock || 0) > 0 ||
|
||||
Number(props.product?.serials_count || 0) > 0
|
||||
);
|
||||
});
|
||||
|
||||
// Unidades disponibles para agregar equivalencia (excluir la base y las ya usadas)
|
||||
const availableUnitsForEquivalence = computed(() => {
|
||||
const usedIds = equivalences.value.map(e => e.unit_of_measure_id);
|
||||
@ -78,6 +92,9 @@ const availableUnitsForEquivalence = computed(() => {
|
||||
return units.value.filter(u => {
|
||||
if (u.id === form.unit_of_measure_id) return false; // excluir unidad base
|
||||
if (usedIds.includes(u.id) && u.id !== editingId) return false; // excluir ya usadas (excepto la que se edita)
|
||||
const name = (u.name || '').toLowerCase();
|
||||
const abbr = (u.abbreviation || '').toLowerCase();
|
||||
if (name.includes('serial') || abbr.includes('serial')) return false; // no aplica como equivalencia
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@ -94,10 +111,13 @@ const loadCategories = async () => {
|
||||
const result = await response.json();
|
||||
if (result.data && result.data.categories && result.data.categories.data) {
|
||||
categories.value = result.data.categories.data;
|
||||
// Actualizar subcategorías si ya hay una categoría seleccionada
|
||||
// Cargar subcategorías si ya hay una categoría seleccionada (producto existente)
|
||||
if (form.category_id) {
|
||||
const selected = categories.value.find(c => c.id == form.category_id);
|
||||
subcategories.value = selected?.subcategories ?? [];
|
||||
api.get(apiURL(`categorias/${form.category_id}/subcategorias`), {
|
||||
onSuccess: (data) => {
|
||||
subcategories.value = data.subcategories?.data || data.subcategories || [];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -106,9 +126,14 @@ const loadCategories = async () => {
|
||||
};
|
||||
|
||||
const onCategoryChange = () => {
|
||||
const selected = categories.value.find(c => c.id == form.category_id);
|
||||
subcategories.value = selected?.subcategories ?? [];
|
||||
form.subcategory_id = '';
|
||||
subcategories.value = [];
|
||||
if (!form.category_id) return;
|
||||
api.get(apiURL(`categorias/${form.category_id}/subcategorias`), {
|
||||
onSuccess: (data) => {
|
||||
subcategories.value = data.subcategories?.data || data.subcategories || [];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const loadUnits = async () => {
|
||||
@ -171,7 +196,9 @@ const updateProduct = () => {
|
||||
...data,
|
||||
track_serials: selectedUnit.value && !selectedUnit.value.allows_decimals
|
||||
? (hasSerials || !!data.track_serials)
|
||||
: false
|
||||
: false,
|
||||
category_id: data.category_id || null,
|
||||
subcategory_id: data.category_id ? (data.subcategory_id || null) : null,
|
||||
})).put(apiURL(`inventario/${props.product.id}`), {
|
||||
onSuccess: () => {
|
||||
Notify.success('Producto actualizado exitosamente');
|
||||
@ -194,6 +221,21 @@ const closeModal = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const onCategoryCreated = async (newCategory) => {
|
||||
await loadCategories();
|
||||
if (newCategory) {
|
||||
form.category_id = newCategory.id;
|
||||
onCategoryChange();
|
||||
}
|
||||
};
|
||||
|
||||
const onSubcategoryCreated = (newSubcategory) => {
|
||||
if (newSubcategory) {
|
||||
subcategories.value.push(newSubcategory);
|
||||
form.subcategory_id = newSubcategory.id;
|
||||
}
|
||||
};
|
||||
|
||||
const openSerials = () => {
|
||||
if (selectedUnit.value && selectedUnit.value.allows_decimals) {
|
||||
Notify.warning('No se pueden usar números de serie con esta unidad de medida.');
|
||||
@ -440,21 +482,30 @@ watch(activeTab, (tab) => {
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
CLASIFICACIÓN
|
||||
</label>
|
||||
<select
|
||||
v-model="form.category_id"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||
required
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
<option value="">Seleccionar clasificación</option>
|
||||
<option
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:value="category.id"
|
||||
<div class="flex gap-2">
|
||||
<select
|
||||
v-model="form.category_id"
|
||||
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
{{ category.name }}
|
||||
</option>
|
||||
</select>
|
||||
<option value="">Seleccionar clasificación</option>
|
||||
<option
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:value="category.id"
|
||||
>
|
||||
{{ category.name }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
@click="showCategoryCreate = true"
|
||||
class="flex items-center justify-center w-9 h-9 rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-600 border border-indigo-200 dark:bg-indigo-900/20 dark:hover:bg-indigo-900/40 dark:text-indigo-400 dark:border-indigo-800 transition-colors shrink-0"
|
||||
title="Crear nueva clasificación"
|
||||
>
|
||||
<GoogleIcon name="add" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
<FormError :message="form.errors?.category_id" />
|
||||
</div>
|
||||
|
||||
@ -463,20 +514,34 @@ watch(activeTab, (tab) => {
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
SUBCLASIFICACIÓN
|
||||
</label>
|
||||
<select
|
||||
v-model="form.subcategory_id"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="!subcategories.length"
|
||||
>
|
||||
<option value="">{{ subcategories.length ? 'Sin subclasificación' : 'Selecciona una clasificación primero' }}</option>
|
||||
<option
|
||||
v-for="subcategory in subcategories"
|
||||
:key="subcategory.id"
|
||||
:value="subcategory.id"
|
||||
<div class="flex gap-2">
|
||||
<select
|
||||
v-model="form.subcategory_id"
|
||||
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="!form.category_id || !subcategories.length"
|
||||
:required="!!form.category_id"
|
||||
>
|
||||
{{ subcategory.name }}
|
||||
</option>
|
||||
</select>
|
||||
<option value="">
|
||||
{{ !form.category_id ? 'Selecciona una clasificación primero' : subcategories.length ? 'Seleccionar subclasificación...' : 'Sin subclasificaciones disponibles' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="subcategory in subcategories"
|
||||
:key="subcategory.id"
|
||||
:value="subcategory.id"
|
||||
>
|
||||
{{ subcategory.name }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
@click="showSubcategoryCreate = true"
|
||||
:disabled="!form.category_id"
|
||||
class="flex items-center justify-center w-9 h-9 rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-600 border border-indigo-200 dark:bg-indigo-900/20 dark:hover:bg-indigo-900/40 dark:text-indigo-400 dark:border-indigo-800 transition-colors shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title="Crear nueva subclasificación"
|
||||
>
|
||||
<GoogleIcon name="add" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
<FormError :message="form.errors?.subcategory_id" />
|
||||
</div>
|
||||
|
||||
@ -487,7 +552,8 @@ watch(activeTab, (tab) => {
|
||||
</label>
|
||||
<select
|
||||
v-model="form.unit_of_measure_id"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="unitLocked"
|
||||
required
|
||||
>
|
||||
<option :value="null">Seleccionar unidad</option>
|
||||
@ -500,7 +566,11 @@ watch(activeTab, (tab) => {
|
||||
</option>
|
||||
</select>
|
||||
<FormError :message="form.errors?.unit_of_measure_id" />
|
||||
<p v-if="selectedUnit" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p v-if="unitLocked" class="mt-1 text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1">
|
||||
<GoogleIcon name="lock" class="text-xs" />
|
||||
No se puede cambiar: el producto ya tiene movimientos de inventario
|
||||
</p>
|
||||
<p v-else-if="selectedUnit" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span v-if="selectedUnit.allows_decimals">Esta unidad permite cantidades decimales (ej: 25.750)</span>
|
||||
<span v-else>Esta unidad solo permite cantidades enteras</span>
|
||||
</p>
|
||||
@ -775,4 +845,17 @@ watch(activeTab, (tab) => {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<CategoryCreateModal
|
||||
:show="showCategoryCreate"
|
||||
@close="showCategoryCreate = false"
|
||||
@created="onCategoryCreated"
|
||||
/>
|
||||
|
||||
<SubcategoryCreateModal
|
||||
:show="showSubcategoryCreate"
|
||||
:category-id="form.category_id"
|
||||
@close="showSubcategoryCreate = false"
|
||||
@created="onSubcategoryCreated"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -8,6 +8,7 @@ import FormInput from '@Holos/Form/Input.vue';
|
||||
import FormError from '@Holos/Form/Elements/Error.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
import SerialInputList from '@Components/POS/SerialInputList.vue';
|
||||
import SerialSelector from '@Components/POS/SerialSelector.vue';
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(['close', 'updated']);
|
||||
@ -23,7 +24,8 @@ const warehouses = ref([]);
|
||||
const suppliers = ref([]);
|
||||
const loading = ref(false);
|
||||
const serialsText = ref('');
|
||||
const serialNumbers = ref([]);
|
||||
const showSerialSelector = ref(false);
|
||||
const selectedSerialObjects = ref([]);
|
||||
const api = useApi();
|
||||
|
||||
/** Formulario */
|
||||
@ -126,6 +128,21 @@ const updateQuantityFromSerials = () => {
|
||||
form.quantity = count > 0 ? count : 1;
|
||||
};
|
||||
|
||||
// Seriales pre-seleccionados para SerialSelector (objetos completos con id)
|
||||
const serialSelectorPreSelected = computed(() => selectedSerialObjects.value);
|
||||
|
||||
/** Métodos de selección de seriales (para traspasos y salidas) */
|
||||
const openSerialSelector = () => {
|
||||
showSerialSelector.value = true;
|
||||
};
|
||||
|
||||
const handleSerialsConfirmed = ({ serials, serialNumbers }) => {
|
||||
selectedSerialObjects.value = serials;
|
||||
serialsList.value = serialNumbers.map(sn => ({ serial_number: sn, locked: false }));
|
||||
updateQuantityFromSerials();
|
||||
showSerialSelector.value = false;
|
||||
};
|
||||
|
||||
/** Métodos */
|
||||
const loadWarehouses = () => {
|
||||
loading.value = true;
|
||||
@ -190,7 +207,7 @@ const updateMovement = () => {
|
||||
|
||||
api.put(apiURL(`movimientos/${props.movement.id}`), {
|
||||
data,
|
||||
onSuccess: (response) => {
|
||||
onSuccess: () => {
|
||||
window.Notify.success('Movimiento actualizado correctamente');
|
||||
emit('updated');
|
||||
closeModal();
|
||||
@ -206,6 +223,7 @@ const updateMovement = () => {
|
||||
|
||||
const closeModal = () => {
|
||||
form.reset();
|
||||
showSerialSelector.value = false;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
@ -231,6 +249,11 @@ watch(() => props.show, (isShown) => {
|
||||
? (props.movement.exited_serials || [])
|
||||
: (props.movement.serials || []);
|
||||
|
||||
// Para traspasos/salidas: inicializar objetos de seriales para SerialSelector
|
||||
if (isTransfer || isExit) {
|
||||
selectedSerialObjects.value = rawSerials;
|
||||
}
|
||||
|
||||
if (rawSerials.length > 0) {
|
||||
const movementWarehouseId = props.movement.warehouse_id || props.movement.warehouse_to?.id;
|
||||
|
||||
@ -357,8 +380,65 @@ watch(() => props.show, (isShown) => {
|
||||
<FormError :message="form.errors?.quantity" />
|
||||
</div>
|
||||
|
||||
<!-- Números de Serie (solo si el producto los tiene) -->
|
||||
<div v-if="hasSerials" class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
|
||||
<!-- Números de Serie: traspasos y salidas usan selector -->
|
||||
<div
|
||||
v-if="hasSerials && (movement?.movement_type === 'transfer' || movement?.movement_type === 'exit')"
|
||||
class="p-3 bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2 flex-1">
|
||||
<GoogleIcon name="qr_code_2" class="text-blue-600 dark:text-blue-400 text-lg" />
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-blue-900 dark:text-blue-100">
|
||||
Este producto requiere números de serie
|
||||
</p>
|
||||
<p class="text-xs text-blue-700 dark:text-blue-300">
|
||||
{{ serialsArray.length }} serial(es) seleccionado(s)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="openSerialSelector"
|
||||
class="flex items-center gap-1 px-3 py-1.5 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors"
|
||||
>
|
||||
<GoogleIcon name="qr_code_scanner" class="text-sm" />
|
||||
{{ serialsArray.length > 0 ? 'Cambiar' : 'Seleccionar' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Badges de seriales seleccionados -->
|
||||
<div v-if="serialsArray.length > 0" class="mt-2 pt-2 border-t border-blue-200 dark:border-blue-800">
|
||||
<p class="text-xs font-medium text-blue-900 dark:text-blue-100 mb-1">Seriales seleccionados:</p>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="(serial, idx) in serialsArray.slice(0, 5)"
|
||||
:key="idx"
|
||||
class="inline-flex items-center px-2 py-0.5 text-xs font-mono bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 rounded"
|
||||
>
|
||||
{{ serial }}
|
||||
</span>
|
||||
<span
|
||||
v-if="serialsArray.length > 5"
|
||||
class="inline-flex items-center px-2 py-0.5 text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded"
|
||||
>
|
||||
+{{ serialsArray.length - 5 }} más
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Validación -->
|
||||
<div v-if="serialsArray.length > 0" class="mt-2 flex items-center justify-end">
|
||||
<p v-if="!serialsValidation.valid" class="text-xs text-red-600 dark:text-red-400 font-medium">
|
||||
{{ serialsValidation.message }}
|
||||
</p>
|
||||
<p v-else class="text-xs text-green-600 dark:text-green-400 font-medium flex items-center gap-1">
|
||||
<GoogleIcon name="check_circle" class="text-sm" />
|
||||
Válido
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Números de Serie: entradas usan lista de inputs -->
|
||||
<div v-else-if="hasSerials" class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<GoogleIcon name="qr_code_scanner" class="text-amber-600 dark:text-amber-400" />
|
||||
<label class="text-xs font-semibold text-amber-700 dark:text-amber-300 uppercase">
|
||||
@ -367,10 +447,8 @@ watch(() => props.show, (isShown) => {
|
||||
</div>
|
||||
<SerialInputList
|
||||
v-model="serialsList"
|
||||
:allow-remove="movement?.movement_type !== 'transfer'"
|
||||
@update:model-value="updateQuantityFromSerials"
|
||||
/>
|
||||
|
||||
<!-- Validación -->
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400">
|
||||
@ -567,5 +645,16 @@ watch(() => props.show, (isShown) => {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Selector de seriales (para traspasos y salidas) -->
|
||||
<SerialSelector
|
||||
v-if="movement?.inventory && showSerialSelector"
|
||||
:show="showSerialSelector"
|
||||
:product="{ id: movement.inventory?.id || movement.inventory_id, name: movement.inventory?.name, sku: movement.inventory?.sku, track_serials: movement.inventory?.track_serials }"
|
||||
:warehouse-id="Number(form.origin_warehouse_id) || null"
|
||||
:pre-selected-serials="serialSelectorPreSelected"
|
||||
@close="showSerialSelector = false"
|
||||
@confirm="handleSerialsConfirmed"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user