feat: gestión de proveedores e integración en inventario
- Implementa CRUD completo, rutas y permisos para proveedores. - Integra datos de proveedor en movimientos (creación, edición y detalles). - Actualiza navegación principal y agrega traducciones.
This commit is contained in:
parent
99f190f61b
commit
d521f0b2c2
@ -460,6 +460,7 @@ export default {
|
||||
sales: 'Ventas',
|
||||
returns: 'Devoluciones',
|
||||
clients: 'Clientes',
|
||||
suppliers: 'Proveedores',
|
||||
clientTiers: 'Niveles de Clientes',
|
||||
billingRequests: 'Solicitudes de Facturación',
|
||||
warehouses: 'Almacenes',
|
||||
@ -581,5 +582,9 @@ export default {
|
||||
movements: {
|
||||
title: 'Movimientos de Inventario',
|
||||
description: 'Historial de entradas, salidas y traspasos de productos',
|
||||
}
|
||||
},
|
||||
suppliers: {
|
||||
title: 'Proveedores',
|
||||
description: 'Gestión de proveedores',
|
||||
},
|
||||
}
|
||||
@ -58,6 +58,11 @@ onMounted(() => {
|
||||
name="pos.clients"
|
||||
to="pos.clients.index"
|
||||
/>
|
||||
<SubLink
|
||||
icon="support_agent"
|
||||
name="pos.suppliers"
|
||||
to="pos.suppliers.index"
|
||||
/>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
v-if="hasPermission('warehouses.index')"
|
||||
|
||||
@ -257,7 +257,7 @@ watch(() => form.track_serials, () => {
|
||||
<!-- Precio de Venta -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
PRECIO VENTA
|
||||
PRECIO VENTA <span class="text-gray-500 dark:text-gray-400">(Subtotal)</span>
|
||||
</label>
|
||||
<FormInput
|
||||
v-model.number="form.retail_price"
|
||||
|
||||
@ -300,16 +300,54 @@ watch(() => props.show, (isShown) => {
|
||||
<span class="text-gray-500 dark:text-gray-400">Fecha:</span>
|
||||
<p class="font-semibold text-gray-900 dark:text-gray-100">{{ formatDate(movement.created_at) }}</p>
|
||||
</div>
|
||||
<div v-if="movement.invoice_reference" class="col-span-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Referencia factura:</span>
|
||||
<p class="font-mono font-semibold text-gray-900 dark:text-gray-100">{{ movement.invoice_reference }}</p>
|
||||
</div>
|
||||
<div v-if="movement.notes" class="col-span-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Notas:</span>
|
||||
<p class="text-gray-900 dark:text-gray-100 italic">{{ movement.notes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Referencia de factura -->
|
||||
<div v-if="movement.invoice_reference" class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 rounded-xl p-5 border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<GoogleIcon name="receipt" class="text-lg text-gray-600 dark:text-gray-400" />
|
||||
<h4 class="text-xs font-bold text-gray-700 dark:text-gray-300 uppercase">Referencia de Factura</h4>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ movement.invoice_reference }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Proveedor -->
|
||||
<div v-if="movement.supplier" class="bg-gradient-to-br from-indigo-50 to-indigo-100 dark:from-indigo-900/20 dark:to-indigo-800/20 rounded-xl p-5 border border-indigo-200 dark:border-indigo-700">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<GoogleIcon name="store" class="text-lg text-indigo-600 dark:text-indigo-400" />
|
||||
<h4 class="text-xs font-bold text-indigo-700 dark:text-indigo-300 uppercase">Proveedor</h4>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ movement.supplier.business_name }}</p>
|
||||
<p v-if="movement.supplier.rfc" class="text-xs text-gray-500 dark:text-gray-400 mt-1">RFC: {{ movement.supplier.rfc }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Almacenes -->
|
||||
<div v-if="movement.movement_type === 'transfer' && (movement.warehouse_from || movement.warehouse_to)" class="grid grid-cols-2 gap-3">
|
||||
<!-- Origen -->
|
||||
<div v-if="movement.warehouse_from" class="bg-red-50 dark:bg-red-900/10 rounded-xl p-4 border border-red-200 dark:border-red-800">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<GoogleIcon name="logout" class="text-lg text-red-600 dark:text-red-400" />
|
||||
<h4 class="text-xs font-bold text-red-700 dark:text-red-300 uppercase">Origen</h4>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ movement.warehouse_from.name }}</p>
|
||||
<p class="text-xs font-mono text-gray-500 dark:text-gray-400">{{ movement.warehouse_from.code }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Destino -->
|
||||
<div v-if="movement.warehouse_to" class="bg-green-50 dark:bg-green-900/10 rounded-xl p-4 border border-green-200 dark:border-green-800">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<GoogleIcon name="login" class="text-lg text-green-600 dark:text-green-400" />
|
||||
<h4 class="text-xs font-bold text-green-700 dark:text-green-300 uppercase">Destino</h4>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ movement.warehouse_to.name }}</p>
|
||||
<p class="text-xs font-mono text-gray-500 dark:text-gray-400">{{ movement.warehouse_to.code }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
@ -19,6 +19,7 @@ const props = defineProps({
|
||||
|
||||
/** Estado */
|
||||
const warehouses = ref([]);
|
||||
const suppliers = ref([]);
|
||||
const loading = ref(false);
|
||||
const api = useApi();
|
||||
|
||||
@ -27,6 +28,7 @@ const form = useForm({
|
||||
quantity: 0,
|
||||
unit_cost: 0,
|
||||
warehouse_id: '',
|
||||
supplier_id: null,
|
||||
origin_warehouse_id: '',
|
||||
destination_warehouse_id: '',
|
||||
invoice_reference: '',
|
||||
@ -72,13 +74,19 @@ const allowsDecimals = computed(() => {
|
||||
/** Métodos */
|
||||
const loadWarehouses = () => {
|
||||
loading.value = true;
|
||||
api.get(apiURL('almacenes'), {
|
||||
onSuccess: (data) => {
|
||||
warehouses.value = data.warehouses?.data || data.data || [];
|
||||
},
|
||||
onFinish: () => {
|
||||
loading.value = false;
|
||||
}
|
||||
Promise.all([
|
||||
api.get(apiURL('almacenes'), {
|
||||
onSuccess: (data) => {
|
||||
warehouses.value = data.warehouses?.data || data.data || [];
|
||||
}
|
||||
}),
|
||||
api.get(apiURL('proveedores'), {
|
||||
onSuccess: (data) => {
|
||||
suppliers.value = data.suppliers?.data || data.suppliers || [];
|
||||
}
|
||||
})
|
||||
]).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
@ -93,6 +101,7 @@ const updateMovement = () => {
|
||||
if (props.movement.movement_type === 'entry') {
|
||||
data.unit_cost = Number(form.unit_cost);
|
||||
data.warehouse_to_id = form.destination_warehouse_id;
|
||||
data.supplier_id = form.supplier_id || null;
|
||||
data.invoice_reference = form.invoice_reference || null;
|
||||
} else if (props.movement.movement_type === 'exit') {
|
||||
data.warehouse_from_id = form.origin_warehouse_id;
|
||||
@ -122,11 +131,6 @@ const closeModal = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const getWarehouseName = (warehouseId) => {
|
||||
const warehouse = warehouses.value.find(w => w.id === warehouseId);
|
||||
return warehouse ? `${warehouse.name} (${warehouse.code})` : 'N/A';
|
||||
};
|
||||
|
||||
/** Observadores */
|
||||
watch(() => props.show, (isShown) => {
|
||||
if (isShown) {
|
||||
@ -136,6 +140,7 @@ watch(() => props.show, (isShown) => {
|
||||
// Cargar datos del movimiento
|
||||
form.quantity = props.movement.quantity || 0;
|
||||
form.unit_cost = props.movement.unit_cost || 0;
|
||||
form.supplier_id = props.movement.supplier_id || null;
|
||||
form.invoice_reference = props.movement.invoice_reference || '';
|
||||
form.notes = props.movement.notes || '';
|
||||
|
||||
@ -323,17 +328,37 @@ watch(() => props.show, (isShown) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Referencia de factura (solo para entradas) -->
|
||||
<div v-if="movement?.movement_type === 'entry'">
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
REFERENCIA DE FACTURA
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.invoice_reference"
|
||||
type="text"
|
||||
placeholder="Ej: FAC-2026-001"
|
||||
/>
|
||||
<FormError :message="form.errors?.invoice_reference" />
|
||||
<!-- Proveedor y Referencia (solo para entradas) -->
|
||||
<div v-if="movement?.movement_type === 'entry'" class="space-y-4">
|
||||
<!-- Proveedor -->
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<!-- Referencia de factura -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
REFERENCIA DE FACTURA
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.invoice_reference"
|
||||
type="text"
|
||||
placeholder="Ej: FAC-2026-001"
|
||||
/>
|
||||
<FormError :message="form.errors?.invoice_reference" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notas -->
|
||||
|
||||
@ -19,6 +19,7 @@ const props = defineProps({
|
||||
/** Estado */
|
||||
const products = ref([]);
|
||||
const warehouses = ref([]);
|
||||
const suppliers = ref([]);
|
||||
const loading = ref(false);
|
||||
const selectedProducts = ref([]);
|
||||
|
||||
@ -36,6 +37,7 @@ const api = useApi();
|
||||
/** Formulario */
|
||||
const form = useForm({
|
||||
warehouse_id: '',
|
||||
supplier_id: null,
|
||||
invoice_reference: '',
|
||||
notes: '',
|
||||
products: []
|
||||
@ -74,6 +76,11 @@ const loadData = () => {
|
||||
}
|
||||
}
|
||||
}),
|
||||
api.get(apiURL('proveedores'), {
|
||||
onSuccess: (data) => {
|
||||
suppliers.value = data.suppliers?.data || data.data || [];
|
||||
}
|
||||
}),
|
||||
api.get(apiURL('inventario'), {
|
||||
onSuccess: (data) => {
|
||||
products.value = data.products?.data || data.data || [];
|
||||
@ -331,6 +338,22 @@ watch(() => form.warehouse_id, (newWarehouseId, oldWarehouseId) => {
|
||||
<FormError :message="form.errors?.warehouse_id" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<!-- Lista de productos -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
|
||||
@ -100,13 +100,11 @@ const movementTypes = [
|
||||
|
||||
const getTypeBadge = (type) => {
|
||||
const badges = {
|
||||
entry: { label: 'Entrada', class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' },
|
||||
exit: { label: 'Salida', class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300' },
|
||||
transfer: { label: 'Traspaso', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' },
|
||||
/* sale: { label: 'Venta', class: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300' },
|
||||
return: { label: 'Devolución', class: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300' }, */
|
||||
entry: { label: 'Entrada', icon: 'add_circle', class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' },
|
||||
exit: { label: 'Salida', icon: 'remove_circle', class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300' },
|
||||
transfer: { label: 'Traspaso', icon: 'swap_horiz', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' },
|
||||
};
|
||||
return badges[type] || { label: type, class: 'bg-gray-100 text-gray-800' };
|
||||
return badges[type] || { label: type, icon: 'help_outline', class: 'bg-gray-100 text-gray-800' };
|
||||
};
|
||||
|
||||
/** Searcher */
|
||||
@ -295,6 +293,7 @@ onMounted(() => {
|
||||
<template #head>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">PRODUCTO</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TIPO</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">PROVEEDOR</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">CANTIDAD</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ORIGEN</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">DESTINO</th>
|
||||
@ -328,10 +327,22 @@ onMounted(() => {
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<span :class="['inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium', getTypeBadge(movement.movement_type).class]">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-semibold',
|
||||
getTypeBadge(movement.movement_type).class
|
||||
]"
|
||||
>
|
||||
<GoogleIcon :name="getTypeBadge(movement.movement_type).icon" class="text-sm" />
|
||||
{{ getTypeBadge(movement.movement_type).label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<p v-if="movement.supplier" class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ movement.supplier.business_name }}
|
||||
</p>
|
||||
<span v-else class="text-sm text-gray-400 dark:text-gray-500">—</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<!-- Cantidad total para múltiples productos -->
|
||||
<p v-if="movement.products && movement.products.length > 0" class="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||||
@ -357,7 +368,7 @@ onMounted(() => {
|
||||
</tr>
|
||||
</template>
|
||||
<template #empty>
|
||||
<td colspan="7" class="table-cell text-center">
|
||||
<td colspan="8" class="table-cell text-center">
|
||||
<div class="flex flex-col items-center justify-center py-8 text-gray-500">
|
||||
<GoogleIcon
|
||||
name="swap_horiz"
|
||||
|
||||
191
src/pages/POS/Suppliers/Create.vue
Normal file
191
src/pages/POS/Suppliers/Create.vue
Normal file
@ -0,0 +1,191 @@
|
||||
<script setup>
|
||||
import { useForm, apiURL } from '@Services/Api';
|
||||
|
||||
import Modal from '@Holos/Modal.vue';
|
||||
import FormInput from '@Holos/Form/Input.vue';
|
||||
import FormError from '@Holos/Form/Elements/Error.vue';
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(['close', 'created']);
|
||||
|
||||
/** Propiedades */
|
||||
const props = defineProps({
|
||||
show: Boolean
|
||||
});
|
||||
|
||||
/** Formulario */
|
||||
const form = useForm({
|
||||
business_name: '',
|
||||
rfc: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
postal_code: '',
|
||||
notes: ''
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
const createSupplier = () => {
|
||||
form.post(apiURL('proveedores'), {
|
||||
onSuccess: () => {
|
||||
window.Notify.success('Proveedor creado exitosamente');
|
||||
emit('created');
|
||||
closeModal();
|
||||
},
|
||||
onError: () => {
|
||||
window.Notify.error('Error al crear el proveedor');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
form.reset();
|
||||
emit('close');
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :show="show" max-width="md" @close="closeModal">
|
||||
<div class="p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
Crear Proveedor
|
||||
</h3>
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Formulario -->
|
||||
<form @submit.prevent="createSupplier" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Nombre -->
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
NOMBRE (del negocio)
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.business_name"
|
||||
type="text"
|
||||
placeholder="Nombre del proveedor"
|
||||
required
|
||||
/>
|
||||
<FormError :message="form.errors?.name" />
|
||||
</div>
|
||||
|
||||
<!-- RFC -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
RFC
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.rfc"
|
||||
type="text"
|
||||
minlength="12"
|
||||
maxlength="13"
|
||||
placeholder="RFC"
|
||||
required
|
||||
/>
|
||||
<FormError :message="form.errors?.rfc" />
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
EMAIL
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.email"
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
required
|
||||
/>
|
||||
<FormError :message="form.errors?.email" />
|
||||
</div>
|
||||
<!-- Teléfono -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
TELÉFONO
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.phone"
|
||||
type="text"
|
||||
placeholder="9933428818"
|
||||
maxlength="10"
|
||||
/>
|
||||
<FormError :message="form.errors?.phone" />
|
||||
</div>
|
||||
|
||||
<!-- Dirección -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
DIRECCIÓN
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.address"
|
||||
type="text"
|
||||
placeholder="Dirección"
|
||||
required
|
||||
/>
|
||||
<FormError :message="form.errors?.address" />
|
||||
</div>
|
||||
|
||||
<!-- Código Postal -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
Código Postal
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.postal_code"
|
||||
type="text"
|
||||
maxlength="5"
|
||||
placeholder="Código Postal"
|
||||
required
|
||||
/>
|
||||
<FormError :message="form.errors?.postal_code" />
|
||||
</div>
|
||||
|
||||
<!-- Notas -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
Notas
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.notes"
|
||||
type="text"
|
||||
placeholder="Notas"
|
||||
required
|
||||
/>
|
||||
<FormError :message="form.errors?.notes" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botones -->
|
||||
<div class="flex items-center justify-end gap-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="px-4 py-2 text-sm font-semibold text-white bg-gray-900 rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<span v-if="form.processing">Guardando...</span>
|
||||
<span v-else>Guardar</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
97
src/pages/POS/Suppliers/Delete.vue
Normal file
97
src/pages/POS/Suppliers/Delete.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import Modal from '@Holos/Modal.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
|
||||
/** Props */
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
client: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
/** Emits */
|
||||
const emit = defineEmits(['close', 'confirm']);
|
||||
|
||||
/** Métodos */
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', props.client.id);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :show="show" max-width="md" @close="handleClose">
|
||||
<div class="p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30">
|
||||
<GoogleIcon name="delete_forever" class="text-2xl text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
Eliminar Cliente
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="space-y-5">
|
||||
<p class="text-gray-700 dark:text-gray-300 text-base">
|
||||
¿Estás seguro de que deseas eliminar este cliente?
|
||||
</p>
|
||||
|
||||
<div v-if="client" class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl p-5 space-y-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="text-base font-bold text-gray-900 dark:text-gray-100 mb-1">
|
||||
{{ client.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<GoogleIcon name="info" class="text-red-600 dark:text-red-400 text-xl flex-shrink-0 mt-0.5" />
|
||||
<p class="text-sm text-red-800 dark:text-red-300 font-medium">
|
||||
Esta acción es permanente y no se puede deshacer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
@click="handleClose"
|
||||
class="px-5 py-2.5 text-sm font-semibold text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleConfirm"
|
||||
class="flex items-center gap-2 px-5 py-2.5 bg-red-600 hover:bg-red-700 text-white text-sm font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-600 shadow-lg shadow-red-600/30 transition-all"
|
||||
>
|
||||
<GoogleIcon name="delete" class="text-xl" />
|
||||
Eliminar Cliente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
201
src/pages/POS/Suppliers/Edit.vue
Normal file
201
src/pages/POS/Suppliers/Edit.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<script setup>
|
||||
import { watch } from 'vue';
|
||||
import { useForm, apiURL } from '@Services/Api';
|
||||
|
||||
import Modal from '@Holos/Modal.vue';
|
||||
import FormInput from '@Holos/Form/Input.vue';
|
||||
import FormError from '@Holos/Form/Elements/Error.vue';
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(['close', 'updated']);
|
||||
|
||||
/** Propiedades */
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
supplier: Object
|
||||
});
|
||||
|
||||
/** Formulario */
|
||||
const form = useForm({
|
||||
business_name: '',
|
||||
rfc: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
postal_code: '',
|
||||
notes: ''
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
const updateSuppliers = () => {
|
||||
form.put(apiURL(`proveedores/${props.supplier.id}`), {
|
||||
onSuccess: () => {
|
||||
window.Notify.success('Proveedor actualizado exitosamente');
|
||||
emit('updated');
|
||||
closeModal();
|
||||
},
|
||||
onError: () => {
|
||||
window.Notify.error('Error al actualizar el proveedor');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
form.reset();
|
||||
emit('close');
|
||||
};
|
||||
|
||||
/** Observadores */
|
||||
watch(() => props.supplier, (newSuppliers) => {
|
||||
if (newSuppliers) {
|
||||
form.business_name = newSuppliers.business_name || '';
|
||||
form.rfc = newSuppliers.rfc || '';
|
||||
form.email = newSuppliers.email || '';
|
||||
form.phone = newSuppliers.phone || '';
|
||||
form.address = newSuppliers.address || '';
|
||||
form.postal_code = newSuppliers.postal_code || '';
|
||||
form.notes = newSuppliers.notes || '';
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :show="show" max-width="md" @close="closeModal">
|
||||
<div class="p-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
Editar Proveedor
|
||||
</h3>
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Formulario -->
|
||||
<form @submit.prevent="updateSuppliers" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Nombre -->
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
NOMBRE (del negocio)
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.business_name"
|
||||
type="text"
|
||||
placeholder="Nombre del proveedor"
|
||||
/>
|
||||
<FormError :message="form.errors?.name" />
|
||||
</div>
|
||||
|
||||
<!-- RFC -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
RFC
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.rfc"
|
||||
type="text"
|
||||
minlength="12"
|
||||
maxlength="13"
|
||||
placeholder="RFC"
|
||||
/>
|
||||
<FormError :message="form.errors?.rfc" />
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
EMAIL
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.email"
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<FormError :message="form.errors?.email" />
|
||||
</div>
|
||||
<!-- Teléfono -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
TELÉFONO
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.phone"
|
||||
type="text"
|
||||
placeholder="9933428818"
|
||||
maxlength="10"
|
||||
/>
|
||||
<FormError :message="form.errors?.phone" />
|
||||
</div>
|
||||
|
||||
<!-- Dirección -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
DIRECCIÓN
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.address"
|
||||
type="text"
|
||||
placeholder="Dirección"
|
||||
/>
|
||||
<FormError :message="form.errors?.address" />
|
||||
</div>
|
||||
|
||||
<!-- Código Postal -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
Código Postal
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.postal_code"
|
||||
type="text"
|
||||
maxlength="5"
|
||||
placeholder="Código Postal"
|
||||
/>
|
||||
<FormError :message="form.errors?.postal_code" />
|
||||
</div>
|
||||
|
||||
<!-- Notas -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
Notas
|
||||
</label>
|
||||
<FormInput
|
||||
v-model="form.notes"
|
||||
type="text"
|
||||
placeholder="Notas"
|
||||
/>
|
||||
<FormError :message="form.errors?.notes" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botones -->
|
||||
<div class="flex items-center justify-between mt-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="px-4 py-2 text-sm font-semibold text-white bg-gray-900 rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<span v-if="form.processing">Actualizando...</span>
|
||||
<span v-else>Actualizar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
215
src/pages/POS/Suppliers/Index.vue
Normal file
215
src/pages/POS/Suppliers/Index.vue
Normal file
@ -0,0 +1,215 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useSearcher, apiURL } from '@Services/Api';
|
||||
import { can } from './Module.js';
|
||||
|
||||
import SearcherHead from '@Holos/Searcher.vue';
|
||||
import Table from '@Holos/Table.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
import CreateModal from './Create.vue';
|
||||
import EditModal from './Edit.vue';
|
||||
import DeleteModal from './Delete.vue';
|
||||
|
||||
|
||||
/** Estado */
|
||||
const suppliers = ref([]);
|
||||
const showCreateModal = ref(false);
|
||||
const showEditModal = ref(false);
|
||||
const showDeleteModal = ref(false);
|
||||
const editingSupplier = ref(null);
|
||||
const deletingSupplier = ref(null);
|
||||
|
||||
/** Métodos */
|
||||
const searcher = useSearcher({
|
||||
url: apiURL('proveedores'),
|
||||
onSuccess: (r) => {
|
||||
suppliers.value = r.suppliers;
|
||||
},
|
||||
onError: () => suppliers.value = []
|
||||
});
|
||||
|
||||
/** Métodos auxiliares */
|
||||
const confirmDelete = async (id) => {
|
||||
try {
|
||||
const response = await fetch(apiURL(`proveedores/${id}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${sessionStorage.token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.Notify.success('Cliente eliminado exitosamente');
|
||||
closeDeleteModal();
|
||||
searcher.search();
|
||||
} else {
|
||||
window.Notify.error('Error al eliminar el cliente');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
window.Notify.error('Error al eliminar el cliente');
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
showCreateModal.value = true;
|
||||
};
|
||||
|
||||
const closeCreateModal = () => {
|
||||
showCreateModal.value = false;
|
||||
};
|
||||
|
||||
const openEditModal = (supplier) => {
|
||||
editingSupplier.value = supplier;
|
||||
showEditModal.value = true;
|
||||
};
|
||||
|
||||
const closeEditModal = () => {
|
||||
showEditModal.value = false;
|
||||
editingSupplier.value = null;
|
||||
};
|
||||
|
||||
const openDeleteModal = (supplier) => {
|
||||
deletingSupplier.value = supplier;
|
||||
showDeleteModal.value = true;
|
||||
};
|
||||
|
||||
const closeDeleteModal = () => {
|
||||
showDeleteModal.value = false;
|
||||
deletingSupplier.value = null;
|
||||
};
|
||||
|
||||
const onSupplierSaved = () => {
|
||||
searcher.search();
|
||||
};
|
||||
|
||||
/** Ciclo de vida */
|
||||
onMounted(() => {
|
||||
searcher.search();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SearcherHead
|
||||
:title="$t('suppliers.title')"
|
||||
placeholder="Buscar por nombre..."
|
||||
@search="(x) => searcher.search(x)"
|
||||
>
|
||||
<button
|
||||
v-if="can('create')"
|
||||
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"
|
||||
>
|
||||
<GoogleIcon name="add" class="text-xl" />
|
||||
Nuevo Proveedor
|
||||
</button>
|
||||
</SearcherHead>
|
||||
<div class="pt-2 w-full">
|
||||
|
||||
<Table
|
||||
:items="suppliers"
|
||||
@send-pagination="(page) => searcher.pagination(page)"
|
||||
>
|
||||
<template #head>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">NOMBRE</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">RFC</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">EMAIL</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TELÉFONO</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">DIRECCIÓN</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">CÓDIGO POSTAL</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">NOTAS</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ACCIONES</th>
|
||||
</template>
|
||||
<template #body="{items}">
|
||||
<tr
|
||||
v-for="supplier in items"
|
||||
:key="supplier.id"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ supplier.business_name }}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ supplier.rfc }}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">{{ supplier.email }}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">{{ supplier.phone }}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">{{ supplier.address }}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">{{ supplier.postal_code }}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">{{ supplier.notes }}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button
|
||||
v-if="can('edit')"
|
||||
@click.stop="openEditModal(supplier)"
|
||||
class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
||||
title="Editar proveedor"
|
||||
>
|
||||
<GoogleIcon name="edit" class="text-xl" />
|
||||
</button>
|
||||
<button
|
||||
v-if="can('destroy')"
|
||||
@click.stop="openDeleteModal(supplier)"
|
||||
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
title="Eliminar proveedor"
|
||||
>
|
||||
<GoogleIcon name="delete" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template #empty>
|
||||
<td colspan="11" class="table-cell text-center">
|
||||
<div class="flex flex-col items-center justify-center py-8 text-gray-500">
|
||||
<GoogleIcon
|
||||
name="person"
|
||||
class="text-6xl mb-2 opacity-50"
|
||||
/>
|
||||
<p class="font-semibold">
|
||||
{{ $t('registers.empty') }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
<!-- Modal de Crear Cliente -->
|
||||
<CreateModal
|
||||
v-if="can('create')"
|
||||
:show="showCreateModal"
|
||||
@close="closeCreateModal"
|
||||
@created="onSupplierSaved"
|
||||
/>
|
||||
|
||||
<!-- Modal de Editar Cliente -->
|
||||
<EditModal
|
||||
v-if="can('edit')"
|
||||
:show="showEditModal"
|
||||
:supplier="editingSupplier"
|
||||
@close="closeEditModal"
|
||||
@updated="onSupplierSaved"
|
||||
/>
|
||||
|
||||
<!-- Modal de Eliminar Proveedor -->
|
||||
<DeleteModal
|
||||
v-if="can('destroy')"
|
||||
:show="showDeleteModal"
|
||||
:supplier="deletingSupplier"
|
||||
@close="closeDeleteModal"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
16
src/pages/POS/Suppliers/Module.js
Normal file
16
src/pages/POS/Suppliers/Module.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||
|
||||
// Ruta API
|
||||
const apiTo = (name, params = {}) => route(`suppliers.${name}`, params)
|
||||
|
||||
// Ruta visual
|
||||
const viewTo = ({ name = '', params = {}, query = {} }) => ({ name: `pos.suppliers.${name}`, params, query })
|
||||
|
||||
// Determina si un usuario puede hacer algo en base a los permisos
|
||||
const can = (permission) => hasPermission(`suppliers.${permission}`)
|
||||
|
||||
export {
|
||||
can,
|
||||
viewTo,
|
||||
apiTo
|
||||
}
|
||||
@ -118,6 +118,12 @@ const router = createRouter({
|
||||
name: 'pos.billingRequests.index',
|
||||
beforeEnter: (to, from, next) => can(next, 'invoice-requests.index'),
|
||||
component: () => import('@Pages/POS/Clients/BillingRequests.vue')
|
||||
},
|
||||
{
|
||||
path: 'suppliers',
|
||||
name: 'pos.suppliers.index',
|
||||
beforeEnter: (to, from, next) => can(next, 'suppliers.index'),
|
||||
component: () => import('@Pages/POS/Suppliers/Index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user