feature-comercial-module-ts #13
@ -8,12 +8,19 @@ import Dropdown from 'primevue/dropdown';
|
|||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import Badge from 'primevue/badge';
|
import Badge from 'primevue/badge';
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import DataTable from 'primevue/datatable';
|
||||||
|
import Column from 'primevue/column';
|
||||||
|
import IconField from 'primevue/iconfield';
|
||||||
|
import InputIcon from 'primevue/inputicon';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { purchaseServices } from '../../purchases/services/purchaseServices';
|
import { purchaseServices } from '../../purchases/services/purchaseServices';
|
||||||
import type { PurchaseDetailResponse } from '../../purchases/types/purchases';
|
import type { PurchaseDetailResponse } from '../../purchases/types/purchases';
|
||||||
import { useWarehouseStore } from '../../../stores/warehouseStore';
|
import { useWarehouseStore } from '../../../stores/warehouseStore';
|
||||||
|
import { useProductStore } from '../../products/stores/productStore';
|
||||||
import { inventoryWarehouseServices } from '../services/inventoryWarehouse.services';
|
import { inventoryWarehouseServices } from '../services/inventoryWarehouse.services';
|
||||||
import type { InventoryProductItem, CreateInventoryRequest } from '../types/warehouse.inventory';
|
import type { InventoryProductItem, CreateInventoryRequest } from '../types/warehouse.inventory';
|
||||||
|
import type { Product as ProductType } from '../../products/types/product';
|
||||||
|
|
||||||
interface SerialNumber {
|
interface SerialNumber {
|
||||||
serial: string;
|
serial: string;
|
||||||
@ -38,11 +45,22 @@ const toast = useToast();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const warehouseStore = useWarehouseStore();
|
const warehouseStore = useWarehouseStore();
|
||||||
|
const productStore = useProductStore();
|
||||||
|
|
||||||
// Data from API
|
// Data from API
|
||||||
const purchaseData = ref<PurchaseDetailResponse | null>(null);
|
const purchaseData = ref<PurchaseDetailResponse | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// Modo de operación: 'purchase' o 'manual'
|
||||||
|
const operationMode = computed(() => {
|
||||||
|
return route.query.purchaseId ? 'purchase' : 'manual';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Almacén de destino (para modo manual)
|
||||||
|
const targetWarehouseId = computed(() => {
|
||||||
|
return route.query.warehouse ? Number(route.query.warehouse) : null;
|
||||||
|
});
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
const purchaseOrderNumber = ref('ORD-2023-001');
|
const purchaseOrderNumber = ref('ORD-2023-001');
|
||||||
const totalItemsPO = ref(12);
|
const totalItemsPO = ref(12);
|
||||||
@ -92,6 +110,12 @@ const expandedRows = ref<any[]>([]);
|
|||||||
const newSerialNumber = ref('');
|
const newSerialNumber = ref('');
|
||||||
const newSerialWarehouse = ref<number>(1);
|
const newSerialWarehouse = ref<number>(1);
|
||||||
|
|
||||||
|
// Modal de productos
|
||||||
|
const showProductModal = ref(false);
|
||||||
|
const productSearch = ref('');
|
||||||
|
const selectedProducts = ref<ProductType[]>([]);
|
||||||
|
const loadingProducts = ref(false);
|
||||||
|
|
||||||
const totalReceived = computed(() => {
|
const totalReceived = computed(() => {
|
||||||
return products.value.reduce((sum, p) => sum + p.quantityReceived, 0);
|
return products.value.reduce((sum, p) => sum + p.quantityReceived, 0);
|
||||||
});
|
});
|
||||||
@ -123,11 +147,16 @@ const warehouseSummary = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
|
// En modo manual sin productos, no es válido
|
||||||
|
if (operationMode.value === 'manual' && products.value.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return products.value.every(product => {
|
return products.value.every(product => {
|
||||||
if (product.requiresSerial) {
|
if (product.requiresSerial) {
|
||||||
return product.serialNumbers.length === product.quantityOrdered;
|
return product.serialNumbers.length === product.quantityOrdered;
|
||||||
} else {
|
} else {
|
||||||
return product.warehouseId !== null;
|
return product.warehouseId !== null && product.quantityReceived > 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -183,12 +212,6 @@ async function confirmReceipt() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const purchaseId = route.query.purchaseId;
|
|
||||||
if (!purchaseId) {
|
|
||||||
toast.add({ severity: 'error', summary: 'Error', detail: 'ID de compra no válido', life: 3000 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
@ -229,8 +252,10 @@ async function confirmReceipt() {
|
|||||||
// Enviar al API
|
// Enviar al API
|
||||||
const response = await inventoryWarehouseServices.addInventory(requestData);
|
const response = await inventoryWarehouseServices.addInventory(requestData);
|
||||||
|
|
||||||
// Actualizar estado de la compra a "Ingresada a Inventario" (4)
|
// Si es desde una compra, actualizar estado
|
||||||
await purchaseServices.updatePurchaseStatus(Number(purchaseId), '4');
|
if (operationMode.value === 'purchase' && route.query.purchaseId) {
|
||||||
|
await purchaseServices.updatePurchaseStatus(Number(route.query.purchaseId), '4');
|
||||||
|
}
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
@ -239,7 +264,7 @@ async function confirmReceipt() {
|
|||||||
life: 4000
|
life: 4000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Regresar a la vista de compras
|
// Regresar a la vista anterior
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.back();
|
router.back();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@ -261,13 +286,7 @@ async function fetchPurchaseDetails() {
|
|||||||
const purchaseId = route.query.purchaseId;
|
const purchaseId = route.query.purchaseId;
|
||||||
|
|
||||||
if (!purchaseId) {
|
if (!purchaseId) {
|
||||||
toast.add({
|
// Si no hay purchaseId, estamos en modo manual
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error',
|
|
||||||
detail: 'No se proporcionó un ID de compra válido',
|
|
||||||
life: 3000
|
|
||||||
});
|
|
||||||
router.back();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,18 +328,136 @@ async function fetchPurchaseDetails() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializeManualMode() {
|
||||||
|
// En modo manual, limpiar productos de ejemplo y establecer almacén por defecto
|
||||||
|
products.value = [];
|
||||||
|
purchaseOrderNumber.value = 'Entrada Manual';
|
||||||
|
totalItemsPO.value = 0;
|
||||||
|
|
||||||
|
// Obtener el nombre del almacén si se especificó
|
||||||
|
if (targetWarehouseId.value) {
|
||||||
|
const warehouse = warehouseStore.warehouses.find(w => w.id === targetWarehouseId.value);
|
||||||
|
if (warehouse) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: 'Entrada Manual',
|
||||||
|
detail: `Agregando inventario al almacén: ${warehouse.name}`,
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await warehouseStore.fetchWarehouses();
|
await warehouseStore.fetchWarehouses();
|
||||||
// Establecer el primer almacén activo como predeterminado
|
// Establecer el primer almacén activo como predeterminado
|
||||||
if (warehouseStore.activeWarehouses.length > 0) {
|
if (warehouseStore.activeWarehouses.length > 0) {
|
||||||
newSerialWarehouse.value = warehouseStore.activeWarehouses[0]?.id || 1;
|
newSerialWarehouse.value = warehouseStore.activeWarehouses[0]?.id || 1;
|
||||||
}
|
}
|
||||||
await fetchPurchaseDetails();
|
|
||||||
|
// Cargar datos según el modo
|
||||||
|
if (operationMode.value === 'purchase') {
|
||||||
|
await fetchPurchaseDetails();
|
||||||
|
} else {
|
||||||
|
initializeManualMode();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
// Lógica para cancelar y regresar
|
router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Funciones para el modal de productos
|
||||||
|
const openProductModal = async () => {
|
||||||
|
showProductModal.value = true;
|
||||||
|
loadingProducts.value = true;
|
||||||
|
try {
|
||||||
|
await productStore.fetchProducts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar productos:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: 'No se pudieron cargar los productos',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loadingProducts.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeProductModal = () => {
|
||||||
|
showProductModal.value = false;
|
||||||
|
selectedProducts.value = [];
|
||||||
|
productSearch.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredProducts = computed(() => {
|
||||||
|
if (!productSearch.value) {
|
||||||
|
return productStore.activeProducts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = productSearch.value.toLowerCase();
|
||||||
|
return productStore.activeProducts.filter(p =>
|
||||||
|
p.name.toLowerCase().includes(search) ||
|
||||||
|
p.sku.toLowerCase().includes(search) ||
|
||||||
|
(p.code && p.code.toLowerCase().includes(search))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const addSelectedProducts = () => {
|
||||||
|
if (selectedProducts.value.length === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Selección Requerida',
|
||||||
|
detail: 'Por favor seleccione al menos un producto',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedProducts.value.forEach(product => {
|
||||||
|
// Verificar si el producto ya está en la lista
|
||||||
|
const exists = products.value.find(p => p.id === product.id);
|
||||||
|
if (!exists) {
|
||||||
|
products.value.push({
|
||||||
|
id: product.id,
|
||||||
|
name: product.name,
|
||||||
|
sku: product.sku,
|
||||||
|
category: product.description || 'Sin categoría',
|
||||||
|
quantityOrdered: 1, // Cantidad inicial
|
||||||
|
quantityReceived: 0,
|
||||||
|
warehouseId: targetWarehouseId.value || null,
|
||||||
|
requiresSerial: product.is_serial,
|
||||||
|
serialNumbers: [],
|
||||||
|
purchaseCost: 0, // El usuario puede ajustar esto
|
||||||
|
attributes: product.attributes || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Productos Agregados',
|
||||||
|
detail: `Se agregaron ${selectedProducts.value.length} producto(s)`,
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
closeProductModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeProduct = (productId: number) => {
|
||||||
|
const index = products.value.findIndex(p => p.id === productId);
|
||||||
|
if (index > -1) {
|
||||||
|
products.value.splice(index, 1);
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: 'Producto Eliminado',
|
||||||
|
detail: 'El producto ha sido eliminado de la lista',
|
||||||
|
life: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -343,13 +480,21 @@ function cancel() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="text-3xl font-black text-slate-900 tracking-tight">Registrar Entrada de Mercancía</h2>
|
<h2 class="text-3xl font-black text-slate-900 tracking-tight">Registrar Entrada de Mercancía</h2>
|
||||||
<p class="text-slate-500 mt-1 flex items-center gap-2">
|
<p class="text-slate-500 mt-1 flex items-center gap-2">
|
||||||
<i class="pi pi-receipt text-base"></i>
|
<i :class="operationMode === 'purchase' ? 'pi pi-receipt' : 'pi pi-warehouse'" class="text-base"></i>
|
||||||
No. Orden #{{ purchaseOrderNumber }} |
|
<template v-if="operationMode === 'purchase'">
|
||||||
<span class="text-primary font-semibold">Distribución Multi-Almacén</span>
|
No. Orden #{{ purchaseOrderNumber }} |
|
||||||
|
<span class="text-primary font-semibold">Distribución Multi-Almacén</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-primary font-semibold">Entrada Manual de Inventario</span>
|
||||||
|
<template v-if="targetWarehouseId">
|
||||||
|
| Almacén: {{ warehouseStore.warehouses.find(w => w.id === targetWarehouseId)?.name }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Card class="shadow-sm">
|
<Card class="shadow-sm" v-if="operationMode === 'purchase'">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex gap-4 px-2">
|
<div class="flex gap-4 px-2">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@ -364,7 +509,21 @@ function cancel() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
<Button icon="pi pi-eye" label="Ver Detalles" severity="secondary" outlined />
|
<Button
|
||||||
|
v-if="operationMode === 'manual'"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
label="Agregar Producto"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
@click="openProductModal"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="operationMode === 'purchase'"
|
||||||
|
icon="pi pi-eye"
|
||||||
|
label="Ver Detalles"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -372,7 +531,9 @@ function cancel() {
|
|||||||
<Card>
|
<Card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="px-6 py-4 border-b border-slate-200 bg-slate-50/50 flex justify-between items-center">
|
<div class="px-6 py-4 border-b border-slate-200 bg-slate-50/50 flex justify-between items-center">
|
||||||
<h3 class="font-bold text-slate-900">Productos de la Orden</h3>
|
<h3 class="font-bold text-slate-900">
|
||||||
|
{{ operationMode === 'purchase' ? 'Productos de la Orden' : 'Productos a Ingresar' }}
|
||||||
|
</h3>
|
||||||
<span class="text-xs text-slate-500 flex items-center gap-1">
|
<span class="text-xs text-slate-500 flex items-center gap-1">
|
||||||
<i class="pi pi-info-circle text-sm"></i>
|
<i class="pi pi-info-circle text-sm"></i>
|
||||||
Seleccione el almacén de destino por cada producto
|
Seleccione el almacén de destino por cada producto
|
||||||
@ -380,7 +541,20 @@ function cancel() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="overflow-x-auto -m-6">
|
<!-- Empty State para modo manual -->
|
||||||
|
<div v-if="operationMode === 'manual' && products.length === 0" class="text-center py-12">
|
||||||
|
<i class="pi pi-inbox text-6xl text-slate-300 mb-4"></i>
|
||||||
|
<h3 class="text-lg font-semibold text-slate-700 mb-2">No hay productos agregados</h3>
|
||||||
|
<p class="text-sm text-slate-500 mb-6">Comienza agregando productos para crear la entrada de inventario</p>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-plus"
|
||||||
|
label="Agregar Producto"
|
||||||
|
severity="primary"
|
||||||
|
@click="openProductModal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="overflow-x-auto -m-6">
|
||||||
<table class="w-full border-collapse">
|
<table class="w-full border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left text-xs font-bold text-slate-500 uppercase tracking-wider bg-slate-50">
|
<tr class="text-left text-xs font-bold text-slate-500 uppercase tracking-wider bg-slate-50">
|
||||||
@ -414,7 +588,22 @@ function cancel() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm text-slate-600">{{ product.sku }}</td>
|
<td class="px-6 py-4 text-sm text-slate-600">{{ product.sku }}</td>
|
||||||
<td class="px-6 py-4 text-center text-sm font-bold text-slate-900">{{ product.quantityOrdered }}</td>
|
<td class="px-6 py-4 text-center text-sm font-bold text-slate-900">
|
||||||
|
<template v-if="operationMode === 'manual'">
|
||||||
|
<InputNumber
|
||||||
|
v-model="product.quantityOrdered"
|
||||||
|
:min="1"
|
||||||
|
showButtons
|
||||||
|
buttonLayout="horizontal"
|
||||||
|
:step="1"
|
||||||
|
class="w-full max-w-[120px] mx-auto"
|
||||||
|
:inputStyle="{ textAlign: 'center', fontWeight: 'bold', fontSize: '0.875rem', width: '60px' }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ product.quantityOrdered }}
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
<InputNumber v-if="!product.requiresSerial" v-model="product.quantityReceived"
|
<InputNumber v-if="!product.requiresSerial" v-model="product.quantityReceived"
|
||||||
:max="product.quantityOrdered" :min="0"
|
:max="product.quantityOrdered" :min="0"
|
||||||
@ -440,14 +629,32 @@ function cancel() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
<Button v-if="product.requiresSerial"
|
<template v-if="product.requiresSerial">
|
||||||
:label="isRowExpanded(product) ? 'OCULTAR' : 'GESTIONAR SERIES'"
|
<Button
|
||||||
:icon="isRowExpanded(product) ? 'pi pi-chevron-up' : 'pi pi-qrcode'"
|
:label="isRowExpanded(product) ? 'OCULTAR' : 'GESTIONAR SERIES'"
|
||||||
:severity="isRowExpanded(product) ? 'secondary' : 'info'"
|
:icon="isRowExpanded(product) ? 'pi pi-chevron-up' : 'pi pi-qrcode'"
|
||||||
:outlined="!isRowExpanded(product)"
|
:severity="isRowExpanded(product) ? 'secondary' : 'info'"
|
||||||
size="small"
|
:outlined="!isRowExpanded(product)"
|
||||||
@click="toggleRow(product)" />
|
size="small"
|
||||||
<Badge v-else value="Estándar" severity="secondary" />
|
@click="toggleRow(product)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<Badge value="Estándar" severity="secondary" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Botón eliminar en modo manual -->
|
||||||
|
<Button
|
||||||
|
v-if="operationMode === 'manual'"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
severity="danger"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
@click="removeProduct(product.id)"
|
||||||
|
v-tooltip.top="'Eliminar producto'"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@ -606,12 +813,148 @@ function cancel() {
|
|||||||
<!-- Footer Actions -->
|
<!-- Footer Actions -->
|
||||||
<div class="flex items-center justify-end gap-4 pt-4 border-t border-slate-200">
|
<div class="flex items-center justify-end gap-4 pt-4 border-t border-slate-200">
|
||||||
<Button label="Cancelar" severity="secondary" text @click="cancel" />
|
<Button label="Cancelar" severity="secondary" text @click="cancel" />
|
||||||
<Button label="Confirmar Recepción Multi-Almacén"
|
<Button
|
||||||
|
:label="operationMode === 'purchase' ? 'Confirmar Recepción Multi-Almacén' : 'Confirmar Entrada de Inventario'"
|
||||||
icon="pi pi-check"
|
icon="pi pi-check"
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid || products.length === 0"
|
||||||
@click="confirmReceipt" />
|
@click="confirmReceipt"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Modal de Selección de Productos -->
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="showProductModal"
|
||||||
|
modal
|
||||||
|
header="Seleccionar Productos"
|
||||||
|
:style="{ width: '90vw', maxWidth: '1200px' }"
|
||||||
|
:contentStyle="{ padding: '0' }"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i class="pi pi-shopping-cart text-xl"></i>
|
||||||
|
<span class="font-bold text-lg">Seleccionar Productos del Catálogo</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<IconField iconPosition="left">
|
||||||
|
<InputIcon class="pi pi-search" />
|
||||||
|
<InputText
|
||||||
|
v-model="productSearch"
|
||||||
|
placeholder="Buscar por nombre, SKU o código..."
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</IconField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Products Table -->
|
||||||
|
<DataTable
|
||||||
|
v-model:selection="selectedProducts"
|
||||||
|
:value="filteredProducts"
|
||||||
|
:loading="loadingProducts"
|
||||||
|
selectionMode="multiple"
|
||||||
|
dataKey="id"
|
||||||
|
:paginator="true"
|
||||||
|
:rows="10"
|
||||||
|
:rowsPerPageOptions="[5, 10, 20, 50]"
|
||||||
|
stripedRows
|
||||||
|
responsiveLayout="scroll"
|
||||||
|
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} productos"
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
|
||||||
|
>
|
||||||
|
<Column selectionMode="multiple" headerStyle="width: 3rem"></Column>
|
||||||
|
|
||||||
|
<Column field="name" header="Producto" sortable style="min-width: 250px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-medium text-surface-900 dark:text-white">
|
||||||
|
{{ slotProps.data.name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-surface-500 dark:text-surface-400">
|
||||||
|
{{ slotProps.data.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="sku" header="SKU" sortable style="min-width: 120px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span class="font-mono text-sm text-surface-600 dark:text-surface-400">
|
||||||
|
{{ slotProps.data.sku }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="code" header="Código" sortable style="min-width: 100px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span class="font-mono text-sm text-primary-600 dark:text-primary-400">
|
||||||
|
{{ slotProps.data.code }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="is_serial" header="Tipo" style="min-width: 100px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Badge
|
||||||
|
:value="slotProps.data.is_serial ? 'Serial' : 'Estándar'"
|
||||||
|
:severity="slotProps.data.is_serial ? 'info' : 'secondary'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="suggested_sale_price" header="Precio Sugerido" sortable style="min-width: 130px">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span class="font-semibold text-green-600 dark:text-green-400">
|
||||||
|
${{ slotProps.data.suggested_sale_price.toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<i class="pi pi-inbox text-4xl text-surface-300 dark:text-surface-600 mb-4"></i>
|
||||||
|
<p class="text-surface-500 dark:text-surface-400">
|
||||||
|
No se encontraron productos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #loading>
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<i class="pi pi-spin pi-spinner text-4xl text-primary mb-4"></i>
|
||||||
|
<p class="text-surface-500 dark:text-surface-400">
|
||||||
|
Cargando productos...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm text-surface-600 dark:text-surface-400">
|
||||||
|
{{ selectedProducts.length }} producto(s) seleccionado(s)
|
||||||
|
</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
label="Cancelar"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
@click="closeProductModal"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Agregar Productos"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
:disabled="selectedProducts.length === 0"
|
||||||
|
@click="addSelectedProducts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -426,7 +426,11 @@ const getMovementTypeSeverity = (type: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openBatchAdd = () => {
|
const openBatchAdd = () => {
|
||||||
router.push({ name: 'BatchAddInventory' });
|
const warehouseId = route.params.id;
|
||||||
|
router.push({
|
||||||
|
name: 'WarehouseAddInventory',
|
||||||
|
query: { warehouse: warehouseId }
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewItem = (item: any) => {
|
const viewItem = (item: any) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user