refactor
This commit is contained in:
parent
46b155c2c8
commit
83dd71f80f
@ -14,10 +14,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
quantity: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
excludeSerials: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@ -31,7 +27,6 @@ const emit = defineEmits(['close', 'confirm']);
|
||||
const loading = ref(false);
|
||||
const availableSerials = ref([]);
|
||||
const selectedSerials = ref([]);
|
||||
const selectionMode = ref('auto'); // 'auto' | 'manual'
|
||||
const searchQuery = ref('');
|
||||
|
||||
/** Computados */
|
||||
@ -47,22 +42,12 @@ const filteredSerials = computed(() => {
|
||||
|
||||
const selectedCount = computed(() => selectedSerials.value.length);
|
||||
|
||||
const isComplete = computed(() => {
|
||||
if (selectionMode.value === 'auto') {
|
||||
return availableSerials.value.length >= props.quantity;
|
||||
}
|
||||
return selectedSerials.value.length === props.quantity;
|
||||
const hasEnoughStock = computed(() => {
|
||||
return availableSerials.value.length > 0;
|
||||
});
|
||||
|
||||
const canConfirm = computed(() => {
|
||||
if (selectionMode.value === 'auto') {
|
||||
return availableSerials.value.length >= props.quantity;
|
||||
}
|
||||
return selectedSerials.value.length === props.quantity;
|
||||
});
|
||||
|
||||
const hasEnoughStock = computed(() => {
|
||||
return availableSerials.value.length >= props.quantity;
|
||||
return selectedSerials.value.length > 0;
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
@ -87,11 +72,7 @@ const toggleSerial = (serial) => {
|
||||
if (index > -1) {
|
||||
selectedSerials.value.splice(index, 1);
|
||||
} else {
|
||||
if (selectedSerials.value.length < props.quantity) {
|
||||
selectedSerials.value.push(serial);
|
||||
} else {
|
||||
Notify.warning(`Solo puedes seleccionar ${props.quantity} serial(es)`);
|
||||
}
|
||||
selectedSerials.value.push(serial);
|
||||
}
|
||||
};
|
||||
|
||||
@ -100,7 +81,7 @@ const isSelected = (serial) => {
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
selectedSerials.value = availableSerials.value.slice(0, props.quantity);
|
||||
selectedSerials.value = [...availableSerials.value];
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
@ -108,19 +89,9 @@ const clearSelection = () => {
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
let serialNumbers = [];
|
||||
|
||||
if (selectionMode.value === 'auto') {
|
||||
// En modo automático, el backend asignará los seriales
|
||||
serialNumbers = null;
|
||||
} else {
|
||||
// En modo manual, enviamos los seriales seleccionados
|
||||
serialNumbers = selectedSerials.value.map(s => s.serial_number);
|
||||
}
|
||||
|
||||
emit('confirm', {
|
||||
selectionMode: selectionMode.value,
|
||||
serialNumbers: serialNumbers
|
||||
serialNumbers: selectedSerials.value.map(s => s.serial_number),
|
||||
quantity: selectedSerials.value.length
|
||||
});
|
||||
};
|
||||
|
||||
@ -131,7 +102,6 @@ const handleClose = () => {
|
||||
const resetState = () => {
|
||||
selectedSerials.value = [];
|
||||
searchQuery.value = '';
|
||||
selectionMode.value = 'auto';
|
||||
};
|
||||
|
||||
/** Watchers */
|
||||
@ -140,13 +110,7 @@ watch(() => props.show, (isShown) => {
|
||||
resetState();
|
||||
loadSerials();
|
||||
}
|
||||
});
|
||||
|
||||
watch(selectionMode, (newMode) => {
|
||||
if (newMode === 'auto') {
|
||||
selectedSerials.value = [];
|
||||
}
|
||||
});
|
||||
},{ immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -163,7 +127,7 @@ watch(selectionMode, (newMode) => {
|
||||
Seleccionar Números de Serie
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ product.name }} - Cantidad: {{ quantity }}
|
||||
{{ product.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -199,66 +163,9 @@ watch(selectionMode, (newMode) => {
|
||||
|
||||
<!-- Content -->
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Modo de selección -->
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-3">
|
||||
Modo de asignación
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="relative flex items-center gap-3 p-4 rounded-xl border-2 transition-all"
|
||||
:class="{
|
||||
'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20 ring-2 ring-emerald-500': selectionMode === 'auto',
|
||||
'border-gray-200 dark:border-gray-700 hover:border-gray-300': selectionMode !== 'auto'
|
||||
}"
|
||||
@click="selectionMode = 'auto'"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-lg bg-emerald-500 flex items-center justify-center">
|
||||
<GoogleIcon name="auto_awesome" class="text-xl text-white" />
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<p class="text-sm font-bold text-gray-800 dark:text-gray-200">
|
||||
Automático
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Asignar primeros disponibles
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="selectionMode === 'auto'" class="absolute top-2 right-2">
|
||||
<GoogleIcon name="check_circle" class="text-emerald-500" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="relative flex items-center gap-3 p-4 rounded-xl border-2 transition-all"
|
||||
:class="{
|
||||
'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 ring-2 ring-indigo-500': selectionMode === 'manual',
|
||||
'border-gray-200 dark:border-gray-700 hover:border-gray-300': selectionMode !== 'manual'
|
||||
}"
|
||||
@click="selectionMode = 'manual'"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-lg bg-indigo-500 flex items-center justify-center">
|
||||
<GoogleIcon name="touch_app" class="text-xl text-white" />
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<p class="text-sm font-bold text-gray-800 dark:text-gray-200">
|
||||
Manual
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Elegir seriales específicos
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="selectionMode === 'manual'" class="absolute top-2 right-2">
|
||||
<GoogleIcon name="check_circle" class="text-indigo-500" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selección manual -->
|
||||
<div v-if="selectionMode === 'manual'" class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<!-- Buscador y acciones -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 relative">
|
||||
@ -274,11 +181,11 @@ watch(selectionMode, (newMode) => {
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="selectedCount < quantity"
|
||||
v-if="selectedCount < availableSerials.length"
|
||||
@click="selectAll"
|
||||
class="px-3 py-2 text-xs font-medium text-indigo-600 bg-indigo-50 rounded-lg hover:bg-indigo-100 transition-colors"
|
||||
>
|
||||
Seleccionar primeros {{ quantity }}
|
||||
Seleccionar todos
|
||||
</button>
|
||||
<button
|
||||
v-if="selectedCount > 0"
|
||||
@ -297,12 +204,11 @@ watch(selectionMode, (newMode) => {
|
||||
<span
|
||||
class="font-semibold"
|
||||
:class="{
|
||||
'text-green-600': selectedCount === quantity,
|
||||
'text-amber-600': selectedCount > 0 && selectedCount < quantity,
|
||||
'text-green-600': selectedCount > 0,
|
||||
'text-gray-500': selectedCount === 0
|
||||
}"
|
||||
>
|
||||
{{ selectedCount }} / {{ quantity }} seleccionado(s)
|
||||
{{ selectedCount }} seleccionado(s)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -349,21 +255,6 @@ watch(selectionMode, (newMode) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info modo automático -->
|
||||
<div v-else class="bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-lg p-4">
|
||||
<div class="flex gap-3">
|
||||
<GoogleIcon name="info" class="text-emerald-600 dark:text-emerald-400 text-xl shrink-0" />
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-emerald-800 dark:text-emerald-300">
|
||||
Asignación automática
|
||||
</p>
|
||||
<p class="text-xs text-emerald-700 dark:text-emerald-400 mt-1">
|
||||
Se asignarán automáticamente los primeros {{ quantity }} número(s) de serie disponible(s) al confirmar la venta.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
@ -154,51 +154,6 @@ const confirmDelete = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Importación masiva
|
||||
const openBulkModal = () => {
|
||||
bulkSerials.value = '';
|
||||
showBulkModal.value = true;
|
||||
};
|
||||
|
||||
const closeBulkModal = () => {
|
||||
showBulkModal.value = false;
|
||||
bulkSerials.value = '';
|
||||
};
|
||||
|
||||
const bulkImport = async () => {
|
||||
const lines = bulkSerials.value
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
|
||||
if (lines.length === 0) {
|
||||
Notify.warning('Ingresa al menos un número de serie');
|
||||
return;
|
||||
}
|
||||
|
||||
bulkProcessing.value = true;
|
||||
try {
|
||||
const response = await serialService.bulkImport(inventoryId.value, lines);
|
||||
Notify.success(`${response.count || lines.length} números de serie importados`);
|
||||
if (response.inventory) {
|
||||
inventory.value = response.inventory;
|
||||
}
|
||||
closeBulkModal();
|
||||
loadSerials();
|
||||
} catch (error) {
|
||||
Notify.error('Error al importar números de serie');
|
||||
} finally {
|
||||
bulkProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const bulkCount = computed(() => {
|
||||
return bulkSerials.value
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0).length;
|
||||
});
|
||||
|
||||
// Navegación
|
||||
const goBack = () => {
|
||||
router.push({ name: 'pos.inventory.index' });
|
||||
@ -276,15 +231,6 @@ onMounted(() => {
|
||||
<option value="disponible">Disponible</option>
|
||||
<option value="vendido">Vendido</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm"
|
||||
@click="openBulkModal"
|
||||
title="Importar múltiples números de serie"
|
||||
>
|
||||
<GoogleIcon name="upload" class="text-xl" />
|
||||
Importar
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm"
|
||||
@click="openCreateModal"
|
||||
@ -360,7 +306,7 @@ onMounted(() => {
|
||||
class="text-gray-400 text-xs"
|
||||
title="No se puede editar/eliminar un serial vendido"
|
||||
>
|
||||
Venta #{{ serial.sale_detail_id }}
|
||||
Venta {{ serial.sale_detail?.sale?.invoice_number || `#${serial.sale_detail_id}` }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
@ -554,56 +500,5 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- Modal Importar Múltiples -->
|
||||
<Modal :show="showBulkModal" max-width="md" @close="closeBulkModal">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
Importar Números de Serie
|
||||
</h3>
|
||||
<button
|
||||
@click="closeBulkModal"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<GoogleIcon name="close" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
NÚMEROS DE SERIE (UNO POR LÍNEA)
|
||||
</label>
|
||||
<textarea
|
||||
v-model="bulkSerials"
|
||||
class="w-full px-3 py-2 text-sm font-mono 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"
|
||||
rows="10"
|
||||
placeholder="ABC123456789 DEF987654321 GHI456123789 ..."
|
||||
></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ bulkCount }} número(s) de serie detectado(s)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 pt-4">
|
||||
<button
|
||||
@click="closeBulkModal"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
@click="bulkImport"
|
||||
:disabled="bulkProcessing || bulkCount === 0"
|
||||
class="px-4 py-2 text-sm font-semibold text-white bg-green-600 rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<span v-if="bulkProcessing">Importando...</span>
|
||||
<span v-else>Importar {{ bulkCount }} serial(es)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -34,7 +34,6 @@ const lastSaleData = ref(null);
|
||||
// Estado para selector de seriales
|
||||
const showSerialSelector = ref(false);
|
||||
const serialSelectorProduct = ref(null);
|
||||
const serialSelectorQuantity = ref(1);
|
||||
|
||||
/** Buscador de productos */
|
||||
const searcher = useSearcher({
|
||||
@ -69,7 +68,6 @@ const addToCart = (product) => {
|
||||
// Si el producto tiene seriales, mostrar selector
|
||||
if (product.has_serials) {
|
||||
serialSelectorProduct.value = product;
|
||||
serialSelectorQuantity.value = 1;
|
||||
showSerialSelector.value = true;
|
||||
return;
|
||||
}
|
||||
@ -84,7 +82,6 @@ const addToCart = (product) => {
|
||||
const closeSerialSelector = () => {
|
||||
showSerialSelector.value = false;
|
||||
serialSelectorProduct.value = null;
|
||||
serialSelectorQuantity.value = 1;
|
||||
};
|
||||
|
||||
const handleSerialConfirm = (serialConfig) => {
|
||||
@ -92,7 +89,7 @@ const handleSerialConfirm = (serialConfig) => {
|
||||
|
||||
cart.addProductWithSerials(
|
||||
serialSelectorProduct.value,
|
||||
serialSelectorQuantity.value,
|
||||
serialConfig.quantity,
|
||||
serialConfig
|
||||
);
|
||||
|
||||
@ -468,7 +465,6 @@ onMounted(() => {
|
||||
:show="showClientModal"
|
||||
:sale-data="lastSaleData"
|
||||
@close="closeClientModal"
|
||||
@save="handleClientSave"
|
||||
/>
|
||||
|
||||
<!-- Modal de Selección de Seriales -->
|
||||
@ -476,7 +472,6 @@ onMounted(() => {
|
||||
v-if="serialSelectorProduct"
|
||||
:show="showSerialSelector"
|
||||
:product="serialSelectorProduct"
|
||||
:quantity="serialSelectorQuantity"
|
||||
:exclude-serials="cart.getSelectedSerials()"
|
||||
@close="closeSerialSelector"
|
||||
@confirm="handleSerialConfirm"
|
||||
|
||||
@ -229,8 +229,9 @@ watch(() => props.show, () => {
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ item.product_name }}
|
||||
</p>
|
||||
<p v-if="item.inventory?.sku" class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
SKU: {{ item.inventory.sku }}
|
||||
<!-- Muestra los seriales si existen -->
|
||||
<p v-if="item.serials && item.serials.length > 0" class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-mono">
|
||||
{{ item.serials.map(s => s.serial_number).join(', ') }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
|
||||
@ -74,22 +74,28 @@ const useCart = defineStore('cart', {
|
||||
|
||||
// Agregar producto con seriales ya configurados
|
||||
addProductWithSerials(product, quantity, serialConfig) {
|
||||
// Eliminar item existente si hay
|
||||
this.removeProduct(product.id);
|
||||
const existingItem = this.items.find(item => item.inventory_id === product.id);
|
||||
const newSerials = serialConfig.serialNumbers || [];
|
||||
|
||||
// Agregar nuevo item con seriales
|
||||
this.items.push({
|
||||
inventory_id: product.id,
|
||||
product_name: product.name,
|
||||
sku: product.sku,
|
||||
quantity: quantity,
|
||||
unit_price: parseFloat(product.price?.retail_price || 0),
|
||||
tax_rate: parseFloat(product.price?.tax || 16),
|
||||
max_stock: product.stock,
|
||||
has_serials: true,
|
||||
serial_numbers: serialConfig.serialNumbers || [],
|
||||
serial_selection_mode: serialConfig.selectionMode
|
||||
});
|
||||
if (existingItem) {
|
||||
// Combinar seriales existentes con los nuevos
|
||||
const combinedSerials = [...existingItem.serial_numbers, ...newSerials];
|
||||
existingItem.serial_numbers = combinedSerials;
|
||||
existingItem.quantity = combinedSerials.length;
|
||||
} else {
|
||||
// Agregar nuevo item con seriales
|
||||
this.items.push({
|
||||
inventory_id: product.id,
|
||||
product_name: product.name,
|
||||
sku: product.sku,
|
||||
quantity: quantity,
|
||||
unit_price: parseFloat(product.price?.retail_price || 0),
|
||||
tax_rate: parseFloat(product.price?.tax || 16),
|
||||
max_stock: product.stock,
|
||||
has_serials: true,
|
||||
serial_numbers: newSerials
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Actualizar seriales de un item
|
||||
|
||||
@ -29,7 +29,10 @@ export const formatCurrency = (amount) => {
|
||||
* formatMoney(null) // "0.00"
|
||||
*/
|
||||
export const formatMoney = (amount) => {
|
||||
return parseFloat(amount || 0).toFixed(2);
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount || 0);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user