feat: agregar opción para permitir eliminación de seriales y mejorar gestión de estado en componentes

This commit is contained in:
Juan Felipe Zapata Moreno 2026-02-24 23:30:11 -06:00
parent 44d86af459
commit b2dea0785e
4 changed files with 98 additions and 37 deletions

View File

@ -10,6 +10,10 @@ const props = defineProps({
disabled: {
type: Boolean,
default: false
},
allowRemove: {
type: Boolean,
default: true
}
});
@ -133,17 +137,26 @@ watch(() => props.modelValue, (val) => {
}"
/>
<!-- Badge vendido -->
<!-- Badge estado -->
<span
v-if="item.locked"
class="shrink-0 text-xs px-1.5 py-0.5 rounded bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
class="shrink-0 text-xs px-1.5 py-0.5 rounded"
:class="item.lock_reason === 'traspasado'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
: item.lock_reason === 'salida'
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'"
>
Vendido
{{
item.lock_reason === 'traspasado' ? 'Traspasado' :
item.lock_reason === 'salida' ? 'Salida' :
'Vendido'
}}
</span>
<!-- Botón eliminar -->
<button
v-if="!item.locked && !props.disabled"
v-if="!item.locked && !props.disabled && props.allowRemove"
type="button"
@click="removeSerial(index)"
class="shrink-0 p-1 text-gray-400 hover:text-red-500 transition-colors"
@ -156,7 +169,7 @@ watch(() => props.modelValue, (val) => {
<!-- Botón agregar -->
<button
v-if="!props.disabled"
v-if="!props.disabled && props.allowRemove"
type="button"
@click="addAndFocus"
class="flex items-center gap-1.5 text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 transition-colors py-1"

View File

@ -84,9 +84,20 @@ const loadUnits = async () => {
};
const validateSerialsAndUnit = () => {
if (form.track_serials && selectedUnit.value && selectedUnit.value.allows_decimals) {
Notify.warning('No se pueden usar números de serie con esta unidad de medida.');
if (!selectedUnit.value) return;
if (selectedUnit.value.allows_decimals) {
if (form.track_serials) {
Notify.warning('No se pueden usar números de serie con esta unidad de medida.');
}
form.track_serials = false;
return;
}
const unitName = (selectedUnit.value.name || '').toLowerCase();
const unitAbbr = (selectedUnit.value.abbreviation || '').toLowerCase();
if (unitName.includes('serial') || unitAbbr.includes('serial')) {
form.track_serials = true;
}
};
@ -200,7 +211,7 @@ watch(() => form.track_serials, () => {
v-model="form.barcode"
type="text"
placeholder="1234567890123"
maxlength="100"
maxlength="14"
/>
<FormError :message="form.errors?.barcode" />
</div>

View File

@ -134,9 +134,20 @@ const loadEquivalences = () => {
};
const validateSerialsAndUnit = () => {
if (form.track_serials && selectedUnit.value && selectedUnit.value.allows_decimals) {
Notify.warning('No se pueden usar números de serie con esta unidad de medida.');
if (!selectedUnit.value) return;
if (selectedUnit.value.allows_decimals) {
if (form.track_serials) {
Notify.warning('No se pueden usar números de serie con esta unidad de medida.');
}
form.track_serials = false;
return;
}
const unitName = (selectedUnit.value.name || '').toLowerCase();
const unitAbbr = (selectedUnit.value.abbreviation || '').toLowerCase();
if (unitName.includes('serial') || unitAbbr.includes('serial')) {
form.track_serials = true;
}
};
@ -282,7 +293,7 @@ watch(() => props.show, (newValue) => {
}
});
watch(() => form.unit_of_measure_id, () => {
watch(selectedUnit, () => {
validateSerialsAndUnit();
});

View File

@ -222,12 +222,40 @@ watch(() => props.show, (isShown) => {
form.notes = props.movement.notes || '';
form.supplier_id = props.movement.supplier_id || null;
// Cargar números de serie si existen
if (props.movement.serials && props.movement.serials.length > 0) {
serialsList.value = props.movement.serials.map(s => ({
serial_number: s.serial_number,
locked: s.status === 'vendido'
}));
// Cargar números de serie según tipo de movimiento
const isTransfer = props.movement.movement_type === 'transfer';
const isExit = props.movement.movement_type === 'exit';
const rawSerials = isTransfer
? (props.movement.transferred_serials || [])
: isExit
? (props.movement.exited_serials || [])
: (props.movement.serials || []);
if (rawSerials.length > 0) {
const movementWarehouseId = props.movement.warehouse_id || props.movement.warehouse_to?.id;
serialsList.value = rawSerials.map(s => {
let locked = false;
let lock_reason = null;
// Seriales con status='salida' son propios de este movimiento editables
const isOwnExitSerial = isExit && s.status === 'salida';
if (!isOwnExitSerial && s.status !== 'disponible') {
locked = true;
lock_reason = s.status;
} else if (
!isTransfer && !isExit &&
s.warehouse_id &&
movementWarehouseId &&
s.warehouse_id !== movementWarehouseId
) {
locked = true;
lock_reason = 'traspasado';
}
return { serial_number: s.serial_number, locked, lock_reason };
});
} else if (props.movement.serial_numbers && props.movement.serial_numbers.length > 0) {
serialsList.value = props.movement.serial_numbers.map(sn => ({
serial_number: sn,
@ -241,10 +269,10 @@ watch(() => props.show, (isShown) => {
if (props.movement.movement_type === 'entry') {
form.destination_warehouse_id = props.movement.warehouse_id || '';
} else if (props.movement.movement_type === 'exit') {
form.origin_warehouse_id = props.movement.warehouse_id || '';
form.origin_warehouse_id = props.movement.warehouse_from_id || props.movement.warehouse_from?.id || '';
} else if (props.movement.movement_type === 'transfer') {
form.origin_warehouse_id = props.movement.origin_warehouse_id || '';
form.destination_warehouse_id = props.movement.destination_warehouse_id || '';
form.origin_warehouse_id = props.movement.warehouse_from_id || props.movement.warehouse_from?.id || '';
form.destination_warehouse_id = props.movement.warehouse_to_id || props.movement.warehouse_to?.id || '';
}
}
} else {
@ -337,7 +365,11 @@ watch(() => props.show, (isShown) => {
NÚMEROS DE SERIE
</label>
</div>
<SerialInputList v-model="serialsList" @update:model-value="updateQuantityFromSerials" />
<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">
@ -410,7 +442,7 @@ watch(() => props.show, (isShown) => {
</label>
<select
v-model="form.origin_warehouse_id"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
required
>
<option value="">Seleccionar almacén...</option>
@ -429,7 +461,8 @@ watch(() => props.show, (isShown) => {
</label>
<select
v-model="form.origin_warehouse_id"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
disabled
required
>
<option value="">Seleccionar...</option>
@ -437,7 +470,6 @@ watch(() => props.show, (isShown) => {
v-for="wh in warehouses"
:key="wh.id"
:value="wh.id"
:disabled="wh.id === form.destination_warehouse_id"
>
{{ wh.name }} ({{ wh.code }})
</option>
@ -450,8 +482,9 @@ watch(() => props.show, (isShown) => {
</label>
<select
v-model="form.destination_warehouse_id"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
required
disabled
>
<option value="">Seleccionar...</option>
<option
@ -469,21 +502,14 @@ watch(() => props.show, (isShown) => {
<!-- Proveedor y Referencia (solo para entradas) -->
<div v-if="movement?.movement_type === 'entry'" class="space-y-4">
<!-- Proveedor -->
<!-- Proveedor (solo lectura) -->
<div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
PROVEEDOR <span class="text-gray-400 text-xs normal-case">(opcional)</span>
PROVEEDOR
</label>
<select
v-model="form.supplier_id"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
>
<option :value="null">Seleccionar proveedor...</option>
<option v-for="supplier in suppliers" :key="supplier.id" :value="supplier.id">
{{ supplier.business_name }}
</option>
</select>
<FormError :message="form.errors?.supplier_id" />
<div class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 text-sm text-gray-600 dark:text-gray-400 cursor-not-allowed">
{{ movement?.supplier?.business_name || 'Sin proveedor' }}
</div>
</div>
<!-- Referencia de factura -->