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',
|
sales: 'Ventas',
|
||||||
returns: 'Devoluciones',
|
returns: 'Devoluciones',
|
||||||
clients: 'Clientes',
|
clients: 'Clientes',
|
||||||
|
suppliers: 'Proveedores',
|
||||||
clientTiers: 'Niveles de Clientes',
|
clientTiers: 'Niveles de Clientes',
|
||||||
billingRequests: 'Solicitudes de Facturación',
|
billingRequests: 'Solicitudes de Facturación',
|
||||||
warehouses: 'Almacenes',
|
warehouses: 'Almacenes',
|
||||||
@ -581,5 +582,9 @@ export default {
|
|||||||
movements: {
|
movements: {
|
||||||
title: 'Movimientos de Inventario',
|
title: 'Movimientos de Inventario',
|
||||||
description: 'Historial de entradas, salidas y traspasos de productos',
|
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"
|
name="pos.clients"
|
||||||
to="pos.clients.index"
|
to="pos.clients.index"
|
||||||
/>
|
/>
|
||||||
|
<SubLink
|
||||||
|
icon="support_agent"
|
||||||
|
name="pos.suppliers"
|
||||||
|
to="pos.suppliers.index"
|
||||||
|
/>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Link
|
<Link
|
||||||
v-if="hasPermission('warehouses.index')"
|
v-if="hasPermission('warehouses.index')"
|
||||||
|
|||||||
@ -257,7 +257,7 @@ watch(() => form.track_serials, () => {
|
|||||||
<!-- Precio de Venta -->
|
<!-- Precio de Venta -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
<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>
|
</label>
|
||||||
<FormInput
|
<FormInput
|
||||||
v-model.number="form.retail_price"
|
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>
|
<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>
|
<p class="font-semibold text-gray-900 dark:text-gray-100">{{ formatDate(movement.created_at) }}</p>
|
||||||
</div>
|
</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">
|
<div v-if="movement.notes" class="col-span-2">
|
||||||
<span class="text-gray-500 dark:text-gray-400">Notas:</span>
|
<span class="text-gray-500 dark:text-gray-400">Notas:</span>
|
||||||
<p class="text-gray-900 dark:text-gray-100 italic">{{ movement.notes }}</p>
|
<p class="text-gray-900 dark:text-gray-100 italic">{{ movement.notes }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|||||||
@ -19,6 +19,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
const warehouses = ref([]);
|
const warehouses = ref([]);
|
||||||
|
const suppliers = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ const form = useForm({
|
|||||||
quantity: 0,
|
quantity: 0,
|
||||||
unit_cost: 0,
|
unit_cost: 0,
|
||||||
warehouse_id: '',
|
warehouse_id: '',
|
||||||
|
supplier_id: null,
|
||||||
origin_warehouse_id: '',
|
origin_warehouse_id: '',
|
||||||
destination_warehouse_id: '',
|
destination_warehouse_id: '',
|
||||||
invoice_reference: '',
|
invoice_reference: '',
|
||||||
@ -72,13 +74,19 @@ const allowsDecimals = computed(() => {
|
|||||||
/** Métodos */
|
/** Métodos */
|
||||||
const loadWarehouses = () => {
|
const loadWarehouses = () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
Promise.all([
|
||||||
api.get(apiURL('almacenes'), {
|
api.get(apiURL('almacenes'), {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
warehouses.value = data.warehouses?.data || data.data || [];
|
warehouses.value = data.warehouses?.data || data.data || [];
|
||||||
},
|
|
||||||
onFinish: () => {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
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') {
|
if (props.movement.movement_type === 'entry') {
|
||||||
data.unit_cost = Number(form.unit_cost);
|
data.unit_cost = Number(form.unit_cost);
|
||||||
data.warehouse_to_id = form.destination_warehouse_id;
|
data.warehouse_to_id = form.destination_warehouse_id;
|
||||||
|
data.supplier_id = form.supplier_id || null;
|
||||||
data.invoice_reference = form.invoice_reference || null;
|
data.invoice_reference = form.invoice_reference || null;
|
||||||
} else if (props.movement.movement_type === 'exit') {
|
} else if (props.movement.movement_type === 'exit') {
|
||||||
data.warehouse_from_id = form.origin_warehouse_id;
|
data.warehouse_from_id = form.origin_warehouse_id;
|
||||||
@ -122,11 +131,6 @@ const closeModal = () => {
|
|||||||
emit('close');
|
emit('close');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWarehouseName = (warehouseId) => {
|
|
||||||
const warehouse = warehouses.value.find(w => w.id === warehouseId);
|
|
||||||
return warehouse ? `${warehouse.name} (${warehouse.code})` : 'N/A';
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Observadores */
|
/** Observadores */
|
||||||
watch(() => props.show, (isShown) => {
|
watch(() => props.show, (isShown) => {
|
||||||
if (isShown) {
|
if (isShown) {
|
||||||
@ -136,6 +140,7 @@ watch(() => props.show, (isShown) => {
|
|||||||
// Cargar datos del movimiento
|
// Cargar datos del movimiento
|
||||||
form.quantity = props.movement.quantity || 0;
|
form.quantity = props.movement.quantity || 0;
|
||||||
form.unit_cost = props.movement.unit_cost || 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.invoice_reference = props.movement.invoice_reference || '';
|
||||||
form.notes = props.movement.notes || '';
|
form.notes = props.movement.notes || '';
|
||||||
|
|
||||||
@ -323,8 +328,27 @@ watch(() => props.show, (isShown) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Referencia de factura (solo para entradas) -->
|
<!-- Proveedor y Referencia (solo para entradas) -->
|
||||||
<div v-if="movement?.movement_type === 'entry'">
|
<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">
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
REFERENCIA DE FACTURA
|
REFERENCIA DE FACTURA
|
||||||
</label>
|
</label>
|
||||||
@ -335,6 +359,7 @@ watch(() => props.show, (isShown) => {
|
|||||||
/>
|
/>
|
||||||
<FormError :message="form.errors?.invoice_reference" />
|
<FormError :message="form.errors?.invoice_reference" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Notas -->
|
<!-- Notas -->
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ const props = defineProps({
|
|||||||
/** Estado */
|
/** Estado */
|
||||||
const products = ref([]);
|
const products = ref([]);
|
||||||
const warehouses = ref([]);
|
const warehouses = ref([]);
|
||||||
|
const suppliers = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const selectedProducts = ref([]);
|
const selectedProducts = ref([]);
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ const api = useApi();
|
|||||||
/** Formulario */
|
/** Formulario */
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
warehouse_id: '',
|
warehouse_id: '',
|
||||||
|
supplier_id: null,
|
||||||
invoice_reference: '',
|
invoice_reference: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
products: []
|
products: []
|
||||||
@ -74,6 +76,11 @@ const loadData = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
api.get(apiURL('proveedores'), {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
suppliers.value = data.suppliers?.data || data.data || [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
api.get(apiURL('inventario'), {
|
api.get(apiURL('inventario'), {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
products.value = data.products?.data || data.data || [];
|
products.value = data.products?.data || data.data || [];
|
||||||
@ -331,6 +338,22 @@ watch(() => form.warehouse_id, (newWarehouseId, oldWarehouseId) => {
|
|||||||
<FormError :message="form.errors?.warehouse_id" />
|
<FormError :message="form.errors?.warehouse_id" />
|
||||||
</div>
|
</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 -->
|
<!-- Lista de productos -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
|||||||
@ -100,13 +100,11 @@ const movementTypes = [
|
|||||||
|
|
||||||
const getTypeBadge = (type) => {
|
const getTypeBadge = (type) => {
|
||||||
const badges = {
|
const badges = {
|
||||||
entry: { label: 'Entrada', class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-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', class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-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', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' },
|
transfer: { label: 'Traspaso', icon: 'swap_horiz', 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' }, */
|
|
||||||
};
|
};
|
||||||
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 */
|
/** Searcher */
|
||||||
@ -295,6 +293,7 @@ onMounted(() => {
|
|||||||
<template #head>
|
<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-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">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">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">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>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<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 }}
|
{{ getTypeBadge(movement.movement_type).label }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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">
|
<td class="px-6 py-4 text-center">
|
||||||
<!-- Cantidad total para múltiples productos -->
|
<!-- 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">
|
<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>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<template #empty>
|
<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">
|
<div class="flex flex-col items-center justify-center py-8 text-gray-500">
|
||||||
<GoogleIcon
|
<GoogleIcon
|
||||||
name="swap_horiz"
|
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',
|
name: 'pos.billingRequests.index',
|
||||||
beforeEnter: (to, from, next) => can(next, 'invoice-requests.index'),
|
beforeEnter: (to, from, next) => can(next, 'invoice-requests.index'),
|
||||||
component: () => import('@Pages/POS/Clients/BillingRequests.vue')
|
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