feat: Add user roles management with CRUD operations and permissions handling
This commit is contained in:
parent
1465f065b1
commit
59ddae0d46
@ -48,6 +48,14 @@ const menuItems = ref<MenuItem[]>([
|
||||
label: 'Configuración',
|
||||
icon: 'pi pi-cog',
|
||||
to: '/configuracion'
|
||||
},
|
||||
{
|
||||
label: 'Usuarios y Roles',
|
||||
icon: 'pi pi-users',
|
||||
items: [
|
||||
{ label: 'Usuarios', icon: 'pi pi-user', to: '/users' },
|
||||
{ label: 'Roles', icon: 'pi pi-shield', to: '/users/roles' }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
113
src/modules/users/components/RoleForm.vue
Normal file
113
src/modules/users/components/RoleForm.vue
Normal file
@ -0,0 +1,113 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Encabezado principal -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl md:text-3xl font-black text-slate-900 dark:text-white mb-1">Gestión de Roles y Permisos</h1>
|
||||
<p class="text-base text-slate-500 dark:text-slate-400">Asigna permisos específicos a cada rol de usuario.</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Bar -->
|
||||
<div class="sticky top-20 z-10 flex flex-wrap items-center justify-between gap-4 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md p-4 mb-6 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<p class="text-lg font-bold text-slate-900 dark:text-white">Permisos para <span class="text-primary">{{ activeRoleName }}</span></p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Hay cambios sin guardar</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button label="Cancelar" class="min-w-[84px]" outlined @click="onCancel" />
|
||||
<Button label="Guardar Cambios" class="min-w-[84px] px-5" @click="onSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permissions Content -->
|
||||
<div class="space-y-6">
|
||||
<div v-for="module in modules" :key="module.name" class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<div class="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
|
||||
<h4 class="text-base font-bold text-slate-800 dark:text-slate-200">{{ module.label }}</h4>
|
||||
<template v-if="module.name === 'reportes'">
|
||||
<Button label="DESELECCIONAR TODO" text class="text-xs font-bold text-primary" @click="deselectAll(module)" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button label="SELECCIONAR TODO" text class="text-xs font-bold" @click="selectAll(module)" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div v-for="perm in module.permissions" :key="perm.key" class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ perm.label }}</p>
|
||||
<InputSwitch v-model="perm.enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import InputSwitch from 'primevue/inputswitch';
|
||||
|
||||
// Mock roles list
|
||||
const rolesList = ref([
|
||||
{ id: 1, name: 'Administrador General', icon: 'shield_person', active: true },
|
||||
{ id: 2, name: 'Gerente de Tienda', icon: 'storefront', active: false },
|
||||
{ id: 3, name: 'Cajero', icon: 'point_of_sale', active: false },
|
||||
{ id: 4, name: 'Bodeguero', icon: 'inventory_2', active: false },
|
||||
]);
|
||||
|
||||
const activeRoleName = computed(() => {
|
||||
const active = rolesList.value.find(r => r.active);
|
||||
return active ? active.name + (active.id === 1 ? ' *' : '') : '';
|
||||
});
|
||||
|
||||
// Mock modules and permissions
|
||||
const modules = ref([
|
||||
{
|
||||
name: 'ventas',
|
||||
label: 'Módulo de Ventas (POS)',
|
||||
permissions: [
|
||||
{ key: 'acceso_tpv', label: 'Acceso al TPV', enabled: true },
|
||||
{ key: 'crear_ventas', label: 'Crear Ventas', enabled: true },
|
||||
{ key: 'aplicar_descuentos', label: 'Aplicar Descuentos', enabled: true },
|
||||
{ key: 'realizar_devoluciones', label: 'Realizar Devoluciones', enabled: true },
|
||||
{ key: 'anular_tickets', label: 'Anular Tickets', enabled: false },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'inventario',
|
||||
label: 'Módulo de Inventario',
|
||||
permissions: [
|
||||
{ key: 'ver_productos', label: 'Ver Productos', enabled: true },
|
||||
{ key: 'crear_editar_productos', label: 'Crear/Editar Productos', enabled: true },
|
||||
{ key: 'ajustes_stock', label: 'Ajustes de Stock', enabled: true },
|
||||
{ key: 'gestionar_proveedores', label: 'Gestionar Proveedores', enabled: false },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'reportes',
|
||||
label: 'Módulo de Reportes',
|
||||
permissions: [
|
||||
{ key: 'acceder_reportes', label: 'Acceder a Reportes', enabled: true },
|
||||
{ key: 'ver_dashboard', label: 'Ver Dashboard', enabled: true },
|
||||
{ key: 'exportar_datos', label: 'Exportar Datos', enabled: true },
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
function onAddRole() {
|
||||
// TODO: Acción para añadir nuevo rol
|
||||
}
|
||||
function onCancel() {
|
||||
// TODO: Acción para cancelar edición
|
||||
}
|
||||
function onSave() {
|
||||
// TODO: Acción para guardar cambios
|
||||
}
|
||||
function selectAll(module: any) {
|
||||
module.permissions.forEach((perm: any) => perm.enabled = true);
|
||||
}
|
||||
function deselectAll(module: any) {
|
||||
module.permissions.forEach((perm: any) => perm.enabled = false);
|
||||
}
|
||||
</script>
|
||||
403
src/modules/users/components/RoleIndex.vue
Normal file
403
src/modules/users/components/RoleIndex.vue
Normal file
@ -0,0 +1,403 @@
|
||||
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Toast Notifications -->
|
||||
<Toast position="bottom-right" />
|
||||
|
||||
<!-- Confirm Dialog -->
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-wrap justify-between gap-4 items-center">
|
||||
<div class="flex min-w-72 flex-col gap-1">
|
||||
<h1 class="text-surface-900 dark:text-white text-3xl md:text-4xl font-black leading-tight tracking-tight">
|
||||
Administración de Roles de Usuario
|
||||
</h1>
|
||||
<p class="text-surface-500 dark:text-surface-400 text-base font-normal leading-normal">
|
||||
Crea nuevos roles, edita los existentes y gestiona los permisos del sistema.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
label="Nuevo Rol"
|
||||
icon="pi pi-plus"
|
||||
@click="openCreateDialog"
|
||||
class="min-w-[180px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Table Card -->
|
||||
<Card class="shadow-sm">
|
||||
<template #content>
|
||||
<!-- Search and Filters -->
|
||||
<div class="flex flex-col md:flex-row justify-between gap-4 mb-4">
|
||||
<div class="flex-1">
|
||||
<IconField iconPosition="left">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText
|
||||
v-model="searchQuery"
|
||||
placeholder="Buscar roles por nombre..."
|
||||
class="w-full"
|
||||
/>
|
||||
</IconField>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<Dropdown
|
||||
v-model="selectedStatus"
|
||||
:options="statusOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Estado: Todos"
|
||||
class="w-40"
|
||||
/>
|
||||
<Dropdown
|
||||
v-model="selectedType"
|
||||
:options="typeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Tipo: Todos"
|
||||
class="w-40"
|
||||
/>
|
||||
<Button
|
||||
label="Limpiar"
|
||||
icon="pi pi-times"
|
||||
outlined
|
||||
@click="clearFilters"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<DataTable
|
||||
:value="roles"
|
||||
:rows="rowsPerPage"
|
||||
:totalRecords="totalRecords"
|
||||
:loading="loading"
|
||||
paginator
|
||||
lazy
|
||||
:rowsPerPageOptions="[5, 10, 20, 50]"
|
||||
@page="onPage"
|
||||
stripedRows
|
||||
responsiveLayout="scroll"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
|
||||
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} resultados"
|
||||
>
|
||||
<Column field="name" header="Nombre del Rol" sortable>
|
||||
<template #body="slotProps">
|
||||
<span class="font-medium">{{ slotProps.data.name }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="description" header="Descripción" sortable>
|
||||
<template #body="slotProps">
|
||||
<span class="text-slate-600 dark:text-slate-400">{{ slotProps.data.description }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="created_at" header="Fecha de Creación">
|
||||
<template #body="slotProps">
|
||||
{{ formatDate(slotProps.data.created_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right" style="min-width: 160px">
|
||||
<template #body="slotProps">
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button icon="pi pi-shield" size="small" text @click="handlePermissions(slotProps.data)" v-tooltip="'Gestionar Permisos'" />
|
||||
<Button icon="pi pi-pencil" size="small" text @click="handleEdit(slotProps.data)" v-tooltip="'Editar'" />
|
||||
<Button icon="pi pi-trash" size="small" text severity="danger" @click="handleDelete(slotProps.data)" v-tooltip="'Eliminar'" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<!-- Role Form Modal -->
|
||||
<Dialog
|
||||
v-model:visible="showDialog"
|
||||
modal
|
||||
:header="isEditing ? 'Editar Rol' : 'Crear Nuevo Rol'"
|
||||
:style="{ width: '90vw', maxWidth: '500px' }"
|
||||
:contentStyle="{ padding: '1.5rem' }"
|
||||
>
|
||||
<form @submit.prevent="handleSaveRole" class="space-y-4">
|
||||
<!-- Role Description -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="modal-role-description" class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
Descripción del Rol *
|
||||
</label>
|
||||
<InputText
|
||||
id="modal-role-description"
|
||||
v-model="formData.description"
|
||||
:invalid="showError('description')"
|
||||
placeholder="Ej: Administrador de almacén"
|
||||
class="w-full"
|
||||
/>
|
||||
<small v-if="showError('description')" class="text-red-500">
|
||||
{{ getErrorMessage('description') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-3 pt-4">
|
||||
<Button
|
||||
label="Cancelar"
|
||||
outlined
|
||||
@click="handleCancelForm"
|
||||
:disabled="modalLoading"
|
||||
/>
|
||||
<Button
|
||||
:label="isEditing ? 'Actualizar' : 'Crear Rol'"
|
||||
type="submit"
|
||||
:loading="modalLoading"
|
||||
:disabled="!isFormValid && modalTouched"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import Card from 'primevue/card';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import Toast from 'primevue/toast';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import IconField from 'primevue/iconfield';
|
||||
import InputIcon from 'primevue/inputicon';
|
||||
import { roleService } from '../services/roleService';
|
||||
import type { Role, CreateRoleData, UpdateRoleData } from '../types/role';
|
||||
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const router = useRouter();
|
||||
|
||||
const roles = ref<Role[]>([]);
|
||||
const searchQuery = ref('');
|
||||
const selectedStatus = ref('all');
|
||||
const selectedType = ref('all');
|
||||
const loading = ref(false);
|
||||
const totalRecords = ref(0);
|
||||
const rowsPerPage = ref(10);
|
||||
const currentPage = ref(1);
|
||||
|
||||
// Modal states
|
||||
const showDialog = ref(false);
|
||||
const isEditing = ref(false);
|
||||
const selectedRole = ref<Role | null>(null);
|
||||
const modalLoading = ref(false);
|
||||
const modalTouched = ref(false);
|
||||
const fieldErrors = ref<Record<string, string[]>>({});
|
||||
|
||||
// Form data
|
||||
const formData = ref<CreateRoleData>({
|
||||
description: ''
|
||||
});
|
||||
|
||||
const isFormValid = computed(() =>
|
||||
formData.value.description.trim() !== ''
|
||||
);
|
||||
|
||||
const statusOptions = [
|
||||
{ label: 'Todos', value: 'all' },
|
||||
{ label: 'Activo', value: 'active' },
|
||||
{ label: 'Inactivo', value: 'inactive' },
|
||||
];
|
||||
const typeOptions = [
|
||||
{ label: 'Todos', value: 'all' },
|
||||
{ label: 'Sistema', value: 'system' },
|
||||
{ label: 'Personalizado', value: 'custom' },
|
||||
];
|
||||
|
||||
function clearFilters() {
|
||||
searchQuery.value = '';
|
||||
selectedStatus.value = 'all';
|
||||
selectedType.value = 'all';
|
||||
}
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString('es-MX');
|
||||
}
|
||||
|
||||
// Fetch roles from API
|
||||
const fetchRoles = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await roleService.getAllRoles(currentPage.value, rowsPerPage.value);
|
||||
const data = response.data.data.models;
|
||||
roles.value = data.data;
|
||||
totalRecords.value = data.total;
|
||||
currentPage.value = data.current_page;
|
||||
} catch (error) {
|
||||
console.error('Error fetching roles:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'No se pudieron cargar los roles',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
function onPage(event: any) {
|
||||
currentPage.value = event.page + 1;
|
||||
rowsPerPage.value = event.rows;
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
selectedRole.value = null;
|
||||
isEditing.value = false;
|
||||
formData.value = { description: '' };
|
||||
modalTouched.value = false;
|
||||
fieldErrors.value = {};
|
||||
showDialog.value = true;
|
||||
}
|
||||
|
||||
function handlePermissions(role: Role) {
|
||||
router.push({ name: 'RolePermissions', params: { id: role.id } });
|
||||
}
|
||||
|
||||
function handleEdit(role: Role) {
|
||||
selectedRole.value = role;
|
||||
isEditing.value = true;
|
||||
formData.value = { description: role.description };
|
||||
modalTouched.value = false;
|
||||
fieldErrors.value = {};
|
||||
showDialog.value = true;
|
||||
}
|
||||
|
||||
function handleCancelForm() {
|
||||
showDialog.value = false;
|
||||
selectedRole.value = null;
|
||||
formData.value = { description: '' };
|
||||
modalTouched.value = false;
|
||||
fieldErrors.value = {};
|
||||
}
|
||||
|
||||
function showError(field: keyof CreateRoleData): boolean {
|
||||
// Mostrar error si hay errores del servidor para este campo
|
||||
if (fieldErrors.value[field] && fieldErrors.value[field].length > 0) {
|
||||
return true;
|
||||
}
|
||||
// Mostrar error de validación local solo después del primer intento
|
||||
if (!modalTouched.value) return false;
|
||||
return formData.value[field].trim() === '';
|
||||
}
|
||||
|
||||
function getErrorMessage(field: keyof CreateRoleData): string {
|
||||
// Priorizar errores del servidor
|
||||
if (fieldErrors.value[field] && fieldErrors.value[field].length > 0) {
|
||||
return fieldErrors.value[field][0] || '';
|
||||
}
|
||||
// Mensaje de validación local
|
||||
if (modalTouched.value && formData.value[field].trim() === '') {
|
||||
return 'La descripción del rol es obligatoria';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function handleSaveRole() {
|
||||
modalTouched.value = true;
|
||||
fieldErrors.value = {}; // Limpiar errores anteriores
|
||||
|
||||
if (!isFormValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
|
||||
try {
|
||||
if (isEditing.value && selectedRole.value) {
|
||||
await roleService.updateRole(selectedRole.value.id, formData.value);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Rol Actualizado',
|
||||
detail: `El rol "${formData.value.description}" ha sido actualizado exitosamente.`,
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
await roleService.createRole(formData.value);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Rol Creado',
|
||||
detail: `El rol "${formData.value.description}" ha sido creado exitosamente.`,
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
|
||||
await fetchRoles(); // Recargar la tabla
|
||||
handleCancelForm();
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error saving role:', error);
|
||||
|
||||
// Manejar errores específicos del backend
|
||||
if (error.response && error.response.data && error.response.data.errors) {
|
||||
fieldErrors.value = error.response.data.errors;
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error de Validación',
|
||||
detail: error.response.data.message || 'Revisa los campos marcados en rojo.',
|
||||
life: 4000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'No se pudo guardar el rol. Inténtalo de nuevo.',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(role: Role) {
|
||||
confirm.require({
|
||||
message: `¿Estás seguro de eliminar el rol "${role.description}"?`,
|
||||
header: 'Confirmar Eliminación',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectLabel: 'Cancelar',
|
||||
acceptLabel: 'Eliminar',
|
||||
rejectClass: 'p-button-secondary p-button-outlined',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
await roleService.deleteRole(role.id);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Rol Eliminado',
|
||||
detail: `El rol "${role.description}" ha sido eliminado exitosamente.`,
|
||||
life: 3000
|
||||
});
|
||||
await fetchRoles(); // Recargar la tabla
|
||||
} catch (error) {
|
||||
console.error('Error deleting role:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'No se pudo eliminar el rol. Inténtalo de nuevo.',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load roles on component mount
|
||||
onMounted(() => {
|
||||
fetchRoles();
|
||||
});
|
||||
</script>
|
||||
214
src/modules/users/components/index.html
Normal file
214
src/modules/users/components/index.html
Normal file
@ -0,0 +1,214 @@
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<!-- Left Column: Roles List -->
|
||||
<div class="lg:col-span-4 xl:col-span-3">
|
||||
<div class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-4 h-full">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-bold text-slate-800 dark:text-slate-200">Roles de Usuario</h3>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<a class="flex items-center gap-4 bg-primary/10 dark:bg-primary/20 text-primary dark:text-white p-3 rounded-lg border border-primary/50"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined">shield_person</span>
|
||||
<span class="text-sm font-bold flex-1 truncate">Administrador General</span>
|
||||
<span class="text-primary font-bold text-lg">*</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-4 hover:bg-slate-100 dark:hover:bg-slate-800/50 p-3 rounded-lg"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined text-slate-500 dark:text-slate-400">storefront</span>
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 flex-1 truncate">Gerente de
|
||||
Tienda</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-4 hover:bg-slate-100 dark:hover:bg-slate-800/50 p-3 rounded-lg"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined text-slate-500 dark:text-slate-400">point_of_sale</span>
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 flex-1 truncate">Cajero</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-4 hover:bg-slate-100 dark:hover:bg-slate-800/50 p-3 rounded-lg"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined text-slate-500 dark:text-slate-400">inventory_2</span>
|
||||
<span
|
||||
class="text-sm font-medium text-slate-700 dark:text-slate-300 flex-1 truncate">Bodeguero</span>
|
||||
</a>
|
||||
<div class="border-t border-slate-200 dark:border-slate-800 my-2"></div>
|
||||
<button
|
||||
class="w-full flex items-center justify-center gap-2 text-primary dark:text-primary hover:bg-primary/10 dark:hover:bg-primary/20 p-3 rounded-lg text-sm font-bold">
|
||||
<span class="material-symbols-outlined text-base">add_circle</span>
|
||||
<span>Añadir Nuevo Rol</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Column: Permissions Area -->
|
||||
<div class="lg:col-span-8 xl:col-span-9">
|
||||
<!-- Action Bar -->
|
||||
<div
|
||||
class="sticky top-20 z-10 flex flex-wrap items-center justify-between gap-4 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md p-4 mb-6 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<p class="text-lg font-bold text-slate-900 dark:text-white">Permisos para <span
|
||||
class="text-primary">Administrador General *</span></p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Hay cambios sin guardar</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 hover:bg-slate-200 dark:hover:bg-slate-700 text-sm font-bold">Cancelar</button>
|
||||
<button
|
||||
class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-5 bg-primary text-white text-sm font-bold hover:bg-primary/90">Guardar
|
||||
Cambios</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Permissions Content -->
|
||||
<div class="space-y-6">
|
||||
<!-- Module Card: Ventas (POS) -->
|
||||
<div class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<div class="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
|
||||
<h4 class="text-base font-bold text-slate-800 dark:text-slate-200">Módulo de Ventas (POS)</h4>
|
||||
<button class="text-primary dark:text-primary text-xs font-bold hover:underline">SELECCIONAR
|
||||
TODO</button>
|
||||
</div>
|
||||
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<!-- Permission Item -->
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Acceso al TPV</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Crear Ventas</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Aplicar Descuentos</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Realizar Devoluciones</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Anular Tickets</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Module Card: Inventario -->
|
||||
<div class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<div class="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
|
||||
<h4 class="text-base font-bold text-slate-800 dark:text-slate-200">Módulo de Inventario</h4>
|
||||
<button class="text-primary dark:text-primary text-xs font-bold hover:underline">SELECCIONAR
|
||||
TODO</button>
|
||||
</div>
|
||||
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Ver Productos</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Crear/Editar Productos</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Ajustes de Stock</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Gestionar Proveedores</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Module Card: Reportes -->
|
||||
<div class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<div class="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
|
||||
<h4 class="text-base font-bold text-slate-800 dark:text-slate-200">Módulo de Reportes</h4>
|
||||
<button class="text-primary dark:text-primary text-xs font-bold hover:underline">DESELECCIONAR
|
||||
TODO</button>
|
||||
</div>
|
||||
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Acceder a Reportes</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Ver Dashboard</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Exportar Datos</p>
|
||||
<label
|
||||
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
|
||||
<div
|
||||
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
|
||||
</div>
|
||||
<input checked="" class="invisible absolute" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
65
src/modules/users/services/roleService.ts
Normal file
65
src/modules/users/services/roleService.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import api from "../../../services/api"
|
||||
import type {
|
||||
Role,
|
||||
RolePagination,
|
||||
RoleResponse,
|
||||
CreateRoleData,
|
||||
UpdateRoleData,
|
||||
SingleRoleResponse,
|
||||
ApiError
|
||||
} from '../types/role'
|
||||
|
||||
export const roleService = {
|
||||
async getAllRoles(page = 1, perPage = 25) {
|
||||
try {
|
||||
console.log('🔍 Fetching roles from API...');
|
||||
const response = await api.get<RoleResponse>('/api/admin/roles', {
|
||||
params: {
|
||||
page,
|
||||
per_page: perPage
|
||||
}
|
||||
});
|
||||
console.log('✅ Roles fetched successfully:', response.data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching roles:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async createRole(data: CreateRoleData) {
|
||||
try {
|
||||
console.log('🔍 Creating role...', data);
|
||||
const response = await api.post<SingleRoleResponse>('/api/admin/roles', data);
|
||||
console.log('✅ Role created successfully:', response.data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating role:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async updateRole(id: number, data: UpdateRoleData) {
|
||||
try {
|
||||
console.log('🔍 Updating role...', { id, data });
|
||||
const response = await api.put<SingleRoleResponse>(`/api/admin/roles/${id}`, data);
|
||||
console.log('✅ Role updated successfully:', response.data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ Error updating role:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRole(id: number) {
|
||||
try {
|
||||
console.log('🔍 Deleting role...', { id });
|
||||
const response = await api.delete(`/api/admin/roles/${id}`);
|
||||
console.log('✅ Role deleted successfully:', response.data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('❌ Error deleting role:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/modules/users/types/role.d.ts
vendored
Normal file
59
src/modules/users/types/role.d.ts
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
// Base Role interface
|
||||
export interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
guard_name: string;
|
||||
description: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// Pagination interfaces
|
||||
export interface RolePagination {
|
||||
current_page: number;
|
||||
data: Role[];
|
||||
first_page_url: string;
|
||||
from: number;
|
||||
last_page: number;
|
||||
last_page_url: string;
|
||||
links: Array<{url: string | null; label: string; active: boolean}>;
|
||||
next_page_url: string | null;
|
||||
path: string;
|
||||
per_page: number;
|
||||
prev_page_url: string | null;
|
||||
to: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// API Response interfaces
|
||||
export interface RoleResponse {
|
||||
status: string;
|
||||
data: {
|
||||
models: RolePagination;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SingleRoleResponse {
|
||||
status: string;
|
||||
data: {
|
||||
model: Role;
|
||||
};
|
||||
}
|
||||
|
||||
// Form data interfaces
|
||||
export interface CreateRoleData {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface UpdateRoleData {
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Error handling interface
|
||||
export interface ApiError {
|
||||
status: string;
|
||||
message: string;
|
||||
errors: {
|
||||
[key: string]: string[];
|
||||
};
|
||||
}
|
||||
@ -14,6 +14,9 @@ import ProductsIndex from '../modules/products/components/ProductsIndex.vue';
|
||||
import ProductForm from '../modules/products/components/ProductForm.vue';
|
||||
import StoresIndex from '../modules/stores/components/StoresIndex.vue';
|
||||
|
||||
import RolesIndex from '../modules/users/components/RoleIndex.vue';
|
||||
import RoleForm from '../modules/users/components/RoleForm.vue';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
@ -151,6 +154,35 @@ const routes: RouteRecordRaw[] = [
|
||||
title: 'Tiendas',
|
||||
requiresAuth: true
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'Usuarios',
|
||||
|
||||
meta: {
|
||||
title: 'Roles',
|
||||
requiresAuth: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'roles',
|
||||
name: 'Roles',
|
||||
component: RolesIndex,
|
||||
meta: {
|
||||
title: 'Roles',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'roles/permissions/:id',
|
||||
name: 'RolePermissions',
|
||||
component: RoleForm,
|
||||
meta: {
|
||||
title: 'Administrar Permisos',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user