feature-comercial-module-ts #13

Merged
edgar.mendez merged 38 commits from feature-comercial-module-ts into develop 2026-03-04 15:07:09 +00:00
3 changed files with 432 additions and 66 deletions
Showing only changes of commit 29d4f5c9c7 - Show all commits

View File

@ -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>

View 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;
}
}
};

View 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;
}