ADD: vista para tiers de clientes
This commit is contained in:
parent
eb7fc0de14
commit
fb2f7cb068
@ -460,6 +460,7 @@ export default {
|
|||||||
sales: 'Ventas',
|
sales: 'Ventas',
|
||||||
returns: 'Devoluciones',
|
returns: 'Devoluciones',
|
||||||
clients: 'Clientes',
|
clients: 'Clientes',
|
||||||
|
clientTiers: 'Niveles de Clientes',
|
||||||
billingRequests: 'Solicitudes de Facturación'
|
billingRequests: 'Solicitudes de Facturación'
|
||||||
},
|
},
|
||||||
cashRegister: {
|
cashRegister: {
|
||||||
@ -566,5 +567,9 @@ export default {
|
|||||||
clients: {
|
clients: {
|
||||||
title: 'Clientes',
|
title: 'Clientes',
|
||||||
description: 'Gestión de clientes',
|
description: 'Gestión de clientes',
|
||||||
|
},
|
||||||
|
clientTiers: {
|
||||||
|
title: 'Niveles de Clientes',
|
||||||
|
description: 'Gestión de niveles de clientes',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,6 +67,11 @@ onMounted(() => {
|
|||||||
name="pos.clients"
|
name="pos.clients"
|
||||||
to="pos.clients.index"
|
to="pos.clients.index"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
icon="leaderboard"
|
||||||
|
name="pos.clientTiers"
|
||||||
|
to="pos.client-tiers.index"
|
||||||
|
/>
|
||||||
<Link
|
<Link
|
||||||
icon="request_quote"
|
icon="request_quote"
|
||||||
name="pos.billingRequests"
|
name="pos.billingRequests"
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|||||||
import CreateModal from './Create.vue';
|
import CreateModal from './Create.vue';
|
||||||
import EditModal from './Edit.vue';
|
import EditModal from './Edit.vue';
|
||||||
import DeleteModal from './Delete.vue';
|
import DeleteModal from './Delete.vue';
|
||||||
|
import StatsModal from './Stats.vue';
|
||||||
|
|
||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
@ -16,8 +17,10 @@ const clients = ref([]);
|
|||||||
const showCreateModal = ref(false);
|
const showCreateModal = ref(false);
|
||||||
const showEditModal = ref(false);
|
const showEditModal = ref(false);
|
||||||
const showDeleteModal = ref(false);
|
const showDeleteModal = ref(false);
|
||||||
|
const showStatsModal = ref(false);
|
||||||
const editingClient = ref(null);
|
const editingClient = ref(null);
|
||||||
const deletingClient = ref(null);
|
const deletingClient = ref(null);
|
||||||
|
const statsClient = ref(null);
|
||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
const searcher = useSearcher({
|
const searcher = useSearcher({
|
||||||
@ -96,6 +99,16 @@ const closeDeleteModal = () => {
|
|||||||
deletingClient.value = null;
|
deletingClient.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openStatsModal = (client) => {
|
||||||
|
statsClient.value = client;
|
||||||
|
showStatsModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeStatsModal = () => {
|
||||||
|
showStatsModal.value = false;
|
||||||
|
statsClient.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
const onClientSaved = () => {
|
const onClientSaved = () => {
|
||||||
searcher.search();
|
searcher.search();
|
||||||
};
|
};
|
||||||
@ -130,6 +143,7 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<template #head>
|
<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">NOMBRE</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 DE CLIENTE</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">CORREO</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">CORREO</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TELEFONO</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TELEFONO</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">DIRECCIÓN</th>
|
||||||
@ -145,6 +159,9 @@ onMounted(() => {
|
|||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ client.name }}</p>
|
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ client.name }}</p>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-4 text-center">
|
||||||
|
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ client.client_number }}</p>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-300">{{ client.email }}</p>
|
<p class="text-sm text-gray-700 dark:text-gray-300">{{ client.email }}</p>
|
||||||
</td>
|
</td>
|
||||||
@ -166,6 +183,13 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<GoogleIcon name="content_copy" class="text-xl" />
|
<GoogleIcon name="content_copy" class="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
@click.stop="openStatsModal(client)"
|
||||||
|
class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
||||||
|
title="Estadísticas del cliente"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="bar_chart" class="text-xl" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="can('edit')"
|
v-if="can('edit')"
|
||||||
@click.stop="openEditModal(client)"
|
@click.stop="openEditModal(client)"
|
||||||
@ -226,5 +250,12 @@ onMounted(() => {
|
|||||||
@close="closeDeleteModal"
|
@close="closeDeleteModal"
|
||||||
@confirm="confirmDelete"
|
@confirm="confirmDelete"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Modal de Estadísticas del Cliente -->
|
||||||
|
<StatsModal
|
||||||
|
:show="showStatsModal"
|
||||||
|
:client="statsClient"
|
||||||
|
@close="closeStatsModal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
226
src/pages/POS/Clients/Stats.vue
Normal file
226
src/pages/POS/Clients/Stats.vue
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { api, apiURL } from '@Services/Api';
|
||||||
|
|
||||||
|
import Modal from '@Holos/Modal.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Loader from '@Shared/Loader.vue';
|
||||||
|
|
||||||
|
/** Props */
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Emits */
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
/** Estado */
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref(null);
|
||||||
|
const stats = ref(null);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const fetchStats = () => {
|
||||||
|
if (!props.client?.id) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
stats.value = null;
|
||||||
|
|
||||||
|
api.get(apiURL(`clients/${props.client.id}/stats`), {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
stats.value = data.stats;
|
||||||
|
loading.value = false;
|
||||||
|
},
|
||||||
|
onFail: (data) => {
|
||||||
|
error.value = data.message || 'Error al obtener estadísticas del cliente.';
|
||||||
|
loading.value = false;
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
error.value = 'Error de conexión. Por favor intente más tarde.';
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value) => {
|
||||||
|
return new Intl.NumberFormat('es-MX', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'MXN'
|
||||||
|
}).format(value || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return 'N/A';
|
||||||
|
return new Date(date).toLocaleDateString('es-MX', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Watchers */
|
||||||
|
watch(() => props.show, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
fetchStats();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :show="show" max-width="3xl" @close="close">
|
||||||
|
<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-10 h-10 rounded-full bg-indigo-100 dark:bg-indigo-900/30">
|
||||||
|
<GoogleIcon name="bar_chart" class="text-xl text-indigo-600 dark:text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Estadísticas del Cliente
|
||||||
|
</h3>
|
||||||
|
<p v-if="client" class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ client.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="close"
|
||||||
|
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="max-h-[70vh] overflow-y-auto">
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="flex justify-center py-12">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<div v-else-if="error" class="text-center py-8">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
|
||||||
|
<GoogleIcon name="error" class="text-3xl text-red-500" />
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div v-else-if="stats" class="space-y-6">
|
||||||
|
<!-- Tier Actual -->
|
||||||
|
<div v-if="stats.current_tier" class="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg p-6 text-white">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm opacity-90 mb-1">Nivel Actual</p>
|
||||||
|
<h3 class="text-2xl font-bold">{{ stats.current_tier.tier_name }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-sm opacity-90 mb-1">Descuento</p>
|
||||||
|
<h3 class="text-3xl font-bold">{{ stats.current_tier.discount_percentage }}%</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resumen de Compras -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<!-- Total Compras -->
|
||||||
|
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4 border border-green-200 dark:border-green-800">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-green-700 dark:text-green-400 mb-1">Total Compras</p>
|
||||||
|
<p class="text-2xl font-bold text-green-900 dark:text-green-300">
|
||||||
|
{{ formatCurrency(stats.total_purchases) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<GoogleIcon name="shopping_cart" class="text-2xl text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Devoluciones -->
|
||||||
|
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-4 border border-red-200 dark:border-red-800">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-red-700 dark:text-red-400 mb-1">Devoluciones</p>
|
||||||
|
<p class="text-2xl font-bold text-red-900 dark:text-red-300">
|
||||||
|
{{ formatCurrency(stats.lifetime_returns) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<GoogleIcon name="keyboard_return" class="text-2xl text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Compras Netas -->
|
||||||
|
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-blue-700 dark:text-blue-400 mb-1">Compras Netas</p>
|
||||||
|
<p class="text-2xl font-bold text-blue-900 dark:text-blue-300">
|
||||||
|
{{ formatCurrency(stats.net_purchases) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<GoogleIcon name="payments" class="text-2xl text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Descuentos Recibidos -->
|
||||||
|
<div class="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4 border border-purple-200 dark:border-purple-800">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-purple-700 dark:text-purple-400 mb-1">Descuentos Recibidos</p>
|
||||||
|
<p class="text-2xl font-bold text-purple-900 dark:text-purple-300">
|
||||||
|
{{ formatCurrency(stats.total_discounts_received) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<GoogleIcon name="discount" class="text-2xl text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Información Adicional -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Total de Transacciones</p>
|
||||||
|
<p class="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{{ stats.total_transactions }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">Promedio por Compra</p>
|
||||||
|
<p class="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{{ formatCurrency(stats.average_purchase) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Última Compra -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<GoogleIcon name="schedule" class="text-lg text-gray-600 dark:text-gray-400" />
|
||||||
|
<p class="text-sm font-semibold text-gray-600 dark:text-gray-400">Última Compra</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg text-gray-900 dark:text-gray-100">
|
||||||
|
{{ formatDate(stats.last_purchase_at) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
156
src/pages/POS/Tiers/Create.vue
Normal file
156
src/pages/POS/Tiers/Create.vue
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<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({
|
||||||
|
tier_name: '',
|
||||||
|
min_purchases_amount: '',
|
||||||
|
max_purchases_amount: '',
|
||||||
|
discount_percentage: '',
|
||||||
|
is_active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const createClient = () => {
|
||||||
|
form.post(apiURL('client-tiers'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
window.Notify.success('Nivel de cliente creado exitosamente');
|
||||||
|
emit('created');
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
window.Notify.error('Error al crear el nivel de cliente');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 Nivel de Cliente
|
||||||
|
</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="createClient" 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
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.tier_name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nombre del cliente"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.tier_name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Monto Mínimo-->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
MONTO MÍNIMO ACUMULADO
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.min_purchases_amount"
|
||||||
|
placeholder="Monto mínimo acumulado"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.min_purchases_amount" />
|
||||||
|
</div>
|
||||||
|
<!-- Monto Máximo-->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
MONTO MÁXIMO ACUMULADO
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.max_purchases_amount"
|
||||||
|
placeholder="Monto máximo acumulado"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.max_purchases_amount" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Descuento -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
DESCUENTO
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.discount_percentage"
|
||||||
|
type="text"
|
||||||
|
placeholder="Descuento"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.discount_percentage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estado -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
ESTADO
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.is_active"
|
||||||
|
type="checkbox"
|
||||||
|
placeholder="Estado"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.is_active" />
|
||||||
|
</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/Tiers/Delete.vue
Normal file
97
src/pages/POS/Tiers/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>
|
||||||
174
src/pages/POS/Tiers/Edit.vue
Normal file
174
src/pages/POS/Tiers/Edit.vue
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<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,
|
||||||
|
client: Object
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Formulario */
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
rfc: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const updateClient = () => {
|
||||||
|
form.put(apiURL(`clients/${props.client.id}`), {
|
||||||
|
onSuccess: () => {
|
||||||
|
window.Notify.success('Cliente actualizado exitosamente');
|
||||||
|
emit('updated');
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
window.Notify.error('Error al actualizar el cliente');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
form.reset();
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Observadores */
|
||||||
|
watch(() => props.client, (newClient) => {
|
||||||
|
if (newClient) {
|
||||||
|
form.name = newClient.name || '';
|
||||||
|
form.email = newClient.email || '';
|
||||||
|
form.phone = newClient.phone || '';
|
||||||
|
form.address = newClient.address || '';
|
||||||
|
form.rfc = newClient.rfc || '';
|
||||||
|
}
|
||||||
|
}, { 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 Cliente
|
||||||
|
</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="updateClient" 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
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nombre del cliente"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.name" />
|
||||||
|
</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="email"
|
||||||
|
placeholder="Correo electrónico"
|
||||||
|
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="tel"
|
||||||
|
placeholder="9922334455"
|
||||||
|
maxlength="10"
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.phone" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dirección -->
|
||||||
|
<div class="col-span-2">
|
||||||
|
<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 del cliente"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.address" />
|
||||||
|
</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"
|
||||||
|
maxlength="13"
|
||||||
|
placeholder="RFC del cliente"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.rfc" />
|
||||||
|
</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>
|
||||||
207
src/pages/POS/Tiers/Index.vue
Normal file
207
src/pages/POS/Tiers/Index.vue
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { api, 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 tiers = ref([]);
|
||||||
|
const showCreateModal = ref(false);
|
||||||
|
const showEditModal = ref(false);
|
||||||
|
const showDeleteModal = ref(false);
|
||||||
|
const editingClient = ref(null);
|
||||||
|
const deletingClient = ref(null);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const searcher = useSearcher({
|
||||||
|
url: apiURL('client-tiers'),
|
||||||
|
onSuccess: (r) => {
|
||||||
|
tiers.value = r.tiers;
|
||||||
|
},
|
||||||
|
onError: () => tiers.value = []
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos auxiliares */
|
||||||
|
const confirmDelete = (id) => {
|
||||||
|
api.delete(apiURL(`client-tiers/${id}`), {
|
||||||
|
onSuccess: () => {
|
||||||
|
window.Notify.success('Nivel de cliente eliminado exitosamente');
|
||||||
|
closeDeleteModal();
|
||||||
|
searcher.search();
|
||||||
|
},
|
||||||
|
onFail: () => {
|
||||||
|
window.Notify.error('Error al eliminar el nivel');
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
window.Notify.error('Error de conexión al eliminar el nivel');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
showCreateModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
showCreateModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (client) => {
|
||||||
|
editingClient.value = client;
|
||||||
|
showEditModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditModal = () => {
|
||||||
|
showEditModal.value = false;
|
||||||
|
editingClient.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeleteModal = (client) => {
|
||||||
|
deletingClient.value = client;
|
||||||
|
showDeleteModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDeleteModal = () => {
|
||||||
|
showDeleteModal.value = false;
|
||||||
|
deletingClient.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClientSaved = () => {
|
||||||
|
searcher.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Ciclo de vida */
|
||||||
|
onMounted(() => {
|
||||||
|
searcher.search();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<SearcherHead
|
||||||
|
:title="$t('clientTiers.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 Nivel
|
||||||
|
</button>
|
||||||
|
</SearcherHead>
|
||||||
|
<div class="pt-2 w-full">
|
||||||
|
|
||||||
|
<Table
|
||||||
|
:items="tiers"
|
||||||
|
@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">CANTIDAD MINIMA ACUMULADA</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">CANTIDAD MÁXIMA ACUMULADA</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">DESCUENTOS</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ESTADO</th>
|
||||||
|
</template>
|
||||||
|
<template #body="{items}">
|
||||||
|
<tr
|
||||||
|
v-for="tier in items"
|
||||||
|
:key="tier.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">{{ tier.name }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-center">
|
||||||
|
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ tier.minimum_purchases_amount }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-center">
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300">{{ tier.maximum_purchases_amount }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-center">
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300">{{ tier.discount_percentage }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-center">
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300">{{ tier.is_active ? 'Activo' : 'Inactivo' }}</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('create')"
|
||||||
|
@click.stop="openCreateModal(tier)"
|
||||||
|
class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
||||||
|
title="Editar cliente"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="edit" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="can('edit')"
|
||||||
|
@click.stop="openEditModal(tier)"
|
||||||
|
class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
||||||
|
title="Editar cliente"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="edit" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="can('destroy')"
|
||||||
|
@click.stop="openDeleteModal(tier)"
|
||||||
|
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||||
|
title="Eliminar cliente"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<td colspan="6" 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="onClientSaved"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Modal de Editar Cliente -->
|
||||||
|
<EditModal
|
||||||
|
v-if="can('edit')"
|
||||||
|
:show="showEditModal"
|
||||||
|
:client="editingClient"
|
||||||
|
@close="closeEditModal"
|
||||||
|
@updated="onClientSaved"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Modal de Eliminar Cliente -->
|
||||||
|
<DeleteModal
|
||||||
|
v-if="can('destroy')"
|
||||||
|
:show="showDeleteModal"
|
||||||
|
:client="deletingClient"
|
||||||
|
@close="closeDeleteModal"
|
||||||
|
@confirm="confirmDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
16
src/pages/POS/Tiers/Module.js
Normal file
16
src/pages/POS/Tiers/Module.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||||
|
|
||||||
|
// Ruta API
|
||||||
|
const apiTo = (name, params = {}) => route(`client-tiers.${name}`, params)
|
||||||
|
|
||||||
|
// Ruta visual
|
||||||
|
const viewTo = ({ name = '', params = {}, query = {} }) => ({ name: `pos.client-tiers.${name}`, params, query })
|
||||||
|
|
||||||
|
// Determina si un usuario puede hacer algo en base a los permisos
|
||||||
|
const can = (permission) => hasPermission(`client-tiers.${permission}`)
|
||||||
|
|
||||||
|
export {
|
||||||
|
can,
|
||||||
|
viewTo,
|
||||||
|
apiTo
|
||||||
|
}
|
||||||
@ -89,6 +89,11 @@ const router = createRouter({
|
|||||||
name: 'pos.clients.index',
|
name: 'pos.clients.index',
|
||||||
component: () => import('@Pages/POS/Clients/Index.vue')
|
component: () => import('@Pages/POS/Clients/Index.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'client-tiers',
|
||||||
|
name: 'pos.client-tiers.index',
|
||||||
|
component: () => import('@Pages/POS/Tiers/Index.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'billing-requests',
|
path: 'billing-requests',
|
||||||
name: 'pos.billingRequests.index',
|
name: 'pos.billingRequests.index',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user