feature-comercial-module-ts #13
@ -1,6 +1,7 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Toast />
|
||||
<!-- 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>
|
||||
@ -11,30 +12,61 @@
|
||||
<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>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
{{ getPermissionStats().enabled }} de {{ getPermissionStats().total }} permisos seleccionados
|
||||
<span v-if="hasUnsavedChanges" class="text-amber-600 dark:text-amber-400 font-medium"> • Cambios sin guardar</span>
|
||||
</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" />
|
||||
<Button
|
||||
label="Cancelar"
|
||||
class="min-w-[84px]"
|
||||
outlined
|
||||
:disabled="saving"
|
||||
@click="onCancel"
|
||||
/>
|
||||
<Button
|
||||
:label="saving ? 'Guardando...' : 'Guardar Cambios'"
|
||||
:icon="saving ? 'pi pi-spin pi-spinner' : ''"
|
||||
class="min-w-[84px] px-5"
|
||||
:disabled="saving || !hasUnsavedChanges"
|
||||
@click="onSave"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p class="text-slate-500 dark:text-slate-400">Cargando permisos...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="modules.length === 0" class="text-center py-12">
|
||||
<p class="text-slate-500 dark:text-slate-400">No se encontraron tipos de permisos</p>
|
||||
</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 v-else class="space-y-6">
|
||||
<div v-for="module in modules" :key="module.id" 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>
|
||||
<Button
|
||||
:label="areAllSelected(module) ? 'DESELECCIONAR TODO' : 'SELECCIONAR TODO'"
|
||||
text
|
||||
:class="areAllSelected(module) ? 'text-xs font-bold text-primary' : 'text-xs font-bold'"
|
||||
@click="toggleAllPermissions(module)"
|
||||
/>
|
||||
</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">
|
||||
<div v-for="perm in module.permissions" :key="perm.id" 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" />
|
||||
<InputSwitch
|
||||
:modelValue="rolePermissions[perm.id] || false"
|
||||
@update:modelValue="(value: boolean) => rolePermissions[perm.id] = value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,70 +76,291 @@
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Button from 'primevue/button';
|
||||
import InputSwitch from 'primevue/inputswitch';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { permissionsService } from '../services/permissionsService';
|
||||
import { roleService } from '../services/roleService';
|
||||
import type { PermissionType, UpdateRolePermissionsData } from '../types/permissions';
|
||||
import type { Role } from '../types/role';
|
||||
|
||||
// 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 toast = useToast();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const activeRoleName = computed(() => {
|
||||
const active = rolesList.value.find(r => r.active);
|
||||
return active ? active.name + (active.id === 1 ? ' *' : '') : '';
|
||||
// State management
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const permissionTypes = ref<PermissionType[]>([]);
|
||||
const rolePermissions = ref<{ [key: number]: boolean }>({});
|
||||
const currentRole = ref<Role | null>(null);
|
||||
const originalPermissions = ref<{ [key: number]: boolean }>({});
|
||||
|
||||
// Props para recibir el rol desde el componente padre (opcional, también funciona con route params)
|
||||
interface Props {
|
||||
roleId?: number;
|
||||
roleName?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
roleId: undefined,
|
||||
roleName: 'Rol sin nombre'
|
||||
});
|
||||
|
||||
// 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 },
|
||||
]
|
||||
}
|
||||
]);
|
||||
// Obtener roleId desde props o route params
|
||||
const currentRoleId = computed(() => {
|
||||
return props.roleId || (route.params.id ? Number(route.params.id) : undefined);
|
||||
});
|
||||
|
||||
function onAddRole() {
|
||||
// TODO: Acción para añadir nuevo rol
|
||||
const activeRoleName = computed(() => {
|
||||
if (currentRole.value) {
|
||||
return currentRole.value.description || currentRole.value.name;
|
||||
}
|
||||
return props.roleName;
|
||||
});
|
||||
|
||||
// Detectar si hay cambios sin guardar
|
||||
const hasUnsavedChanges = computed(() => {
|
||||
return JSON.stringify(rolePermissions.value) !== JSON.stringify(originalPermissions.value);
|
||||
});
|
||||
|
||||
// Computed para los módulos con permisos habilitados/deshabilitados
|
||||
const modules = computed(() => {
|
||||
return permissionTypes.value.map(permissionType => ({
|
||||
id: permissionType.id,
|
||||
name: permissionType.name.toLowerCase().replace(/\s+/g, '_'),
|
||||
label: `Módulo de ${permissionType.name}`,
|
||||
permissions: permissionType.permissions.map(permission => ({
|
||||
id: permission.id,
|
||||
key: permission.name,
|
||||
label: permission.description || permission.name,
|
||||
// Remover la propiedad enabled ya que usamos rolePermissions directamente en el template
|
||||
}))
|
||||
}));
|
||||
});
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(async () => {
|
||||
await loadInitialData();
|
||||
});
|
||||
|
||||
// Methods
|
||||
async function loadInitialData() {
|
||||
await Promise.all([
|
||||
loadPermissionTypes(),
|
||||
currentRoleId.value ? loadRoleData(currentRoleId.value) : Promise.resolve()
|
||||
]);
|
||||
}
|
||||
|
||||
async function loadRoleData(roleId: number) {
|
||||
try {
|
||||
console.log('Cargando datos del rol:', roleId);
|
||||
// Aquí podrías llamar a un endpoint para obtener los datos del rol si fuera necesario
|
||||
// Por ahora, solo simulamos que tenemos el rol
|
||||
currentRole.value = {
|
||||
id: roleId,
|
||||
name: `Rol ${roleId}`,
|
||||
description: `Rol ${roleId}`,
|
||||
guard_name: 'api',
|
||||
created_at: '',
|
||||
updated_at: ''
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error al cargar datos del rol:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPermissionTypes() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await permissionsService.getAllPermissionTypes();
|
||||
|
||||
if (response.data?.status === 'success') {
|
||||
permissionTypes.value = response.data.data.models;
|
||||
|
||||
// Inicializar todos los permisos como deshabilitados por defecto
|
||||
permissionTypes.value.forEach(permissionType => {
|
||||
permissionType.permissions.forEach(permission => {
|
||||
rolePermissions.value[permission.id] = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Si estamos editando un rol existente, cargar sus permisos específicos
|
||||
console.log('Props roleId:', props.roleId, 'Route params id:', route.params.id, 'Computed currentRoleId:', currentRoleId.value);
|
||||
if (currentRoleId.value) {
|
||||
await loadRolePermissions(currentRoleId.value);
|
||||
} else {
|
||||
console.log('No se cargaron permisos específicos porque roleId no está definido');
|
||||
}
|
||||
|
||||
console.log('Tipos de permisos cargados. Total de permisos disponibles:',
|
||||
permissionTypes.value.reduce((total, type) => total + type.permissions.length, 0));
|
||||
} else {
|
||||
throw new Error('Respuesta inválida de la API');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al cargar tipos de permisos:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'No se pudieron cargar los permisos del sistema',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRolePermissions(roleId: number) {
|
||||
try {
|
||||
console.log('Cargando permisos específicos del rol:', roleId);
|
||||
const response = await permissionsService.getRolePermissions(roleId);
|
||||
|
||||
if (response.data?.status === 'success') {
|
||||
const roleSpecificPermissions = response.data.data.permissions;
|
||||
|
||||
// Crear un Set con los IDs de permisos que tiene el rol para búsqueda rápida
|
||||
const rolePermissionIds = new Set(roleSpecificPermissions.map(perm => perm.id));
|
||||
|
||||
// Actualizar el estado de rolePermissions basado en la comparación
|
||||
permissionTypes.value.forEach(permissionType => {
|
||||
permissionType.permissions.forEach(permission => {
|
||||
// Si el permiso está en los permisos del rol, marcarlo como habilitado
|
||||
const isEnabled = rolePermissionIds.has(permission.id);
|
||||
rolePermissions.value[permission.id] = isEnabled;
|
||||
|
||||
if (isEnabled) {
|
||||
console.log(`Permiso habilitado: ${permission.name} (ID: ${permission.id})`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log('Permisos del rol cargados y comparados. Total permisos activos:', rolePermissionIds.size);
|
||||
console.log('Estado actual de rolePermissions:', JSON.stringify(rolePermissions.value, null, 2));
|
||||
|
||||
// Guardar el estado original para detectar cambios
|
||||
originalPermissions.value = { ...rolePermissions.value };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al cargar permisos del rol:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: `No se pudieron cargar los permisos del rol`,
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
// TODO: Acción para cancelar edición
|
||||
console.log('Cancelando edición de permisos');
|
||||
|
||||
// Si hay cambios sin guardar, mostrar advertencia
|
||||
if (hasUnsavedChanges.value) {
|
||||
// Podrías agregar un ConfirmDialog aquí para confirmar la cancelación
|
||||
console.log('Hay cambios sin guardar, pero procediendo con cancelación');
|
||||
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Cambios Descartados',
|
||||
detail: 'Los cambios no guardados se han descartado',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
|
||||
// Volver a la página de roles
|
||||
router.push({ name: 'Roles' });
|
||||
}
|
||||
function onSave() {
|
||||
// TODO: Acción para guardar cambios
|
||||
|
||||
async function onSave() {
|
||||
if (!currentRoleId.value) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'No se pudo identificar el rol a actualizar',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
saving.value = true;
|
||||
|
||||
// Obtener los IDs de permisos habilitados
|
||||
const enabledPermissions = Object.entries(rolePermissions.value)
|
||||
.filter(([, enabled]) => enabled)
|
||||
.map(([permissionId]) => parseInt(permissionId));
|
||||
|
||||
console.log('Guardando permisos para el rol:', currentRoleId.value, 'Permisos:', enabledPermissions);
|
||||
|
||||
// Llamar al servicio para actualizar permisos
|
||||
const response = await permissionsService.updateRolePermissions(currentRoleId.value, {
|
||||
permissions: enabledPermissions
|
||||
});
|
||||
|
||||
if (response.data?.status === 'success') {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Éxito',
|
||||
detail: `Permisos actualizados correctamente. ${enabledPermissions.length} permisos asignados.`,
|
||||
life: 3000
|
||||
});
|
||||
|
||||
// Actualizar el estado original para reflejar los cambios guardados
|
||||
originalPermissions.value = { ...rolePermissions.value };
|
||||
|
||||
} else {
|
||||
throw new Error('Respuesta inválida del servidor');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error al guardar permisos:', error);
|
||||
|
||||
let errorMessage = 'No se pudieron guardar los permisos';
|
||||
|
||||
// Manejar diferentes tipos de errores
|
||||
if (error.response?.data?.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
} else if (error.response?.data?.errors) {
|
||||
// Si hay errores de validación, mostrar el primero
|
||||
const firstError = Object.values(error.response.data.errors)[0];
|
||||
errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error al Guardar',
|
||||
detail: errorMessage,
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
function selectAll(module: any) {
|
||||
module.permissions.forEach((perm: any) => perm.enabled = true);
|
||||
|
||||
// Computed function to check if all permissions in a module are selected
|
||||
function areAllSelected(module: any) {
|
||||
return module.permissions.length > 0 && module.permissions.every((perm: any) => rolePermissions.value[perm.id] === true);
|
||||
}
|
||||
function deselectAll(module: any) {
|
||||
module.permissions.forEach((perm: any) => perm.enabled = false);
|
||||
|
||||
// Toggle function that handles both select all and deselect all
|
||||
function toggleAllPermissions(module: any) {
|
||||
const shouldSelectAll = !areAllSelected(module);
|
||||
|
||||
module.permissions.forEach((perm: any) => {
|
||||
rolePermissions.value[perm.id] = shouldSelectAll;
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to get permission statistics
|
||||
function getPermissionStats() {
|
||||
const totalPermissions = Object.keys(rolePermissions.value).length;
|
||||
const enabledPermissions = Object.values(rolePermissions.value).filter(Boolean).length;
|
||||
return { total: totalPermissions, enabled: enabledPermissions };
|
||||
}
|
||||
</script>
|
||||
|
||||
60
src/modules/users/services/permissionsService.ts
Normal file
60
src/modules/users/services/permissionsService.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import api from "../../../services/api"
|
||||
import type {
|
||||
PermissionTypesResponse,
|
||||
RolePermissionsResponse,
|
||||
UpdateRolePermissionsData,
|
||||
UpdateRolePermissionsResponse
|
||||
} from '../types/permissions'
|
||||
|
||||
export const permissionsService = {
|
||||
/**
|
||||
* Obtiene todos los tipos de permisos con sus permisos asociados
|
||||
* @returns Promise con la respuesta de la API conteniendo los tipos de permisos y permisos
|
||||
*/
|
||||
async getAllPermissionTypes() {
|
||||
try {
|
||||
console.log('Obteniendo todos los tipos de permisos...');
|
||||
const response = await api.get<PermissionTypesResponse>('/api/permission-types/all-with-permissions');
|
||||
console.log('Tipos de permisos obtenidos:', response.data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener tipos de permisos:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtiene los permisos asignados a un rol específico
|
||||
* @param roleId - ID del rol del cual obtener los permisos
|
||||
* @returns Promise con la respuesta de la API conteniendo los permisos del rol
|
||||
*/
|
||||
async getRolePermissions(roleId: number) {
|
||||
try {
|
||||
console.log(`Obteniendo permisos del rol ${roleId}...`);
|
||||
const response = await api.get<RolePermissionsResponse>(`/api/admin/roles/${roleId}/permissions`);
|
||||
console.log(`Permisos del rol ${roleId} obtenidos:`, response.data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`Error al obtener permisos del rol ${roleId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Actualiza los permisos asignados a un rol específico
|
||||
* @param roleId - ID del rol al cual asignar los permisos
|
||||
* @param data - Objeto con el array de IDs de permisos a asignar
|
||||
* @returns Promise con la respuesta de la API
|
||||
*/
|
||||
async updateRolePermissions(roleId: number, data: UpdateRolePermissionsData) {
|
||||
try {
|
||||
console.log(`Actualizando permisos del rol ${roleId}...`, data);
|
||||
const response = await api.put<UpdateRolePermissionsResponse>(`/api/admin/roles/${roleId}/permissions`, data);
|
||||
console.log(`Permisos del rol ${roleId} actualizados:`, response.data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`Error al actualizar permisos del rol ${roleId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
53
src/modules/users/types/permissions.d.ts
vendored
Normal file
53
src/modules/users/types/permissions.d.ts
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
// Base interfaces
|
||||
export interface Permission {
|
||||
id: number;
|
||||
name: string;
|
||||
guard_name: string;
|
||||
description: string;
|
||||
permission_type_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PermissionType {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
// Permission with pivot data for role-permission relationships
|
||||
export interface PermissionWithPivot extends Permission {
|
||||
pivot: {
|
||||
role_id: number;
|
||||
permission_id: number;
|
||||
};
|
||||
}
|
||||
|
||||
// API Response interfaces
|
||||
export interface PermissionTypesResponse {
|
||||
status: string;
|
||||
data: {
|
||||
models: PermissionType[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface RolePermissionsResponse {
|
||||
status: string;
|
||||
data: {
|
||||
permissions: PermissionWithPivot[];
|
||||
};
|
||||
}
|
||||
|
||||
// Interface for updating role permissions
|
||||
export interface UpdateRolePermissionsData {
|
||||
permissions: number[];
|
||||
}
|
||||
|
||||
export interface UpdateRolePermissionsResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data?: any;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user