Permiso vistas implementados
This commit is contained in:
@@ -1,88 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { INTERNAL_ROUTES } from '$lib/routes-registry';
|
||||
import { auth } from '$lib/stores/auth.js';
|
||||
import { navPerms } from '$lib/stores/navPermissions';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let open = false;
|
||||
|
||||
// Navigation items based on user role
|
||||
$: navigation = getNavigationForRole($auth.user?.role);
|
||||
const NAV_ICONS: Record<string, string> = {
|
||||
'/': 'M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z',
|
||||
'/tickets':
|
||||
'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2',
|
||||
'/users':
|
||||
'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z',
|
||||
'/tenants':
|
||||
'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4',
|
||||
'/categories':
|
||||
'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10',
|
||||
'/systems':
|
||||
'M5 12a1 1 0 102 0V6.414l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L5 6.414V12zm14 0a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L19 17.586V12z',
|
||||
'/sla': 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'/audit':
|
||||
'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
|
||||
'/test-report':
|
||||
'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z'
|
||||
};
|
||||
|
||||
function getNavigationForRole(role: string | undefined) {
|
||||
const baseNavigation = [
|
||||
onMount(() => {
|
||||
navPerms.load();
|
||||
});
|
||||
|
||||
$: navigation = (() => {
|
||||
const role = $auth.user?.role;
|
||||
const isAdmin = role === 'ADMIN';
|
||||
|
||||
const items: { name: string; href: string; icon: string }[] = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
href: '/',
|
||||
icon: 'M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z'
|
||||
},
|
||||
{
|
||||
name: 'Tickets',
|
||||
href: '/tickets',
|
||||
icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2'
|
||||
icon: NAV_ICONS['/']
|
||||
}
|
||||
];
|
||||
|
||||
if (role === 'ADMIN' || role === 'SUPPORT_MANAGER') {
|
||||
baseNavigation.push(
|
||||
{
|
||||
name: 'Clientes',
|
||||
href: '/tenants',
|
||||
icon: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4'
|
||||
},
|
||||
{
|
||||
name: 'Usuarios',
|
||||
href: '/users',
|
||||
icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z'
|
||||
},
|
||||
{
|
||||
name: 'Categorías',
|
||||
href: '/categories',
|
||||
icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10'
|
||||
},
|
||||
{
|
||||
name: 'Sistemas',
|
||||
href: '/systems',
|
||||
icon: 'M5 12a1 1 0 102 0V6.414l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L5 6.414V12zm14 0a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L19 17.586V12z'
|
||||
}
|
||||
);
|
||||
}
|
||||
INTERNAL_ROUTES.forEach(route => {
|
||||
const hasPermission = isAdmin || $navPerms[route.key] === true;
|
||||
if (hasPermission) {
|
||||
items.push({
|
||||
name: route.label,
|
||||
href: route.path,
|
||||
icon: NAV_ICONS[route.path] || NAV_ICONS['/tickets']
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (role === 'ADMIN' || role === 'SUPPORT_MANAGER') {
|
||||
baseNavigation.push(
|
||||
{
|
||||
name: 'SLA Management',
|
||||
href: '/sla',
|
||||
icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z'
|
||||
},
|
||||
{
|
||||
name: 'Reportes',
|
||||
href: '/reports',
|
||||
icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (role === 'ADMIN') {
|
||||
baseNavigation.push(
|
||||
{
|
||||
name: 'Auditoría',
|
||||
href: '/audit',
|
||||
icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z'
|
||||
},
|
||||
{
|
||||
name: 'Seguridad',
|
||||
href: '/audit/security',
|
||||
icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z'
|
||||
},
|
||||
{
|
||||
name: 'Reporte Endpoints',
|
||||
href: '/test-report',
|
||||
icon: 'M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return baseNavigation;
|
||||
}
|
||||
return items;
|
||||
})();
|
||||
|
||||
function isCurrentPage(href: string) {
|
||||
return $page.url.pathname === href || ($page.url.pathname.startsWith(href) && href !== '/');
|
||||
@@ -132,7 +104,6 @@
|
||||
<p class="text-xs text-primary-100">Panel Interno</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={() => (open = false)}
|
||||
class="p-2 rounded-md text-primary-100 hover:text-white hover:bg-primary-500 lg:hidden"
|
||||
|
||||
18
frontend-internal/src/lib/routes-registry.ts
Normal file
18
frontend-internal/src/lib/routes-registry.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface RouteDefinition {
|
||||
key: string;
|
||||
label: string;
|
||||
path: string;
|
||||
icon?: string;
|
||||
adminOnly?: boolean;
|
||||
}
|
||||
|
||||
export const INTERNAL_ROUTES: RouteDefinition[] = [
|
||||
{ key: 'nav:tickets', label: 'Tickets', path: '/tickets' },
|
||||
{ key: 'nav:users', label: 'Usuarios', path: '/users' },
|
||||
{ key: 'nav:tenants', label: 'Clientes', path: '/tenants' },
|
||||
{ key: 'nav:categories', label: 'Categorías', path: '/categories' },
|
||||
{ key: 'nav:systems', label: 'Sistemas', path: '/systems' },
|
||||
{ key: 'nav:sla', label: 'SLA', path: '/sla' },
|
||||
{ key: 'nav:audit', label: 'Auditoría', path: '/audit' },
|
||||
{ key: 'nav:reports', label: 'Reportes', path: '/test-report' },
|
||||
]
|
||||
21
frontend-internal/src/lib/stores/navPermissions.ts
Normal file
21
frontend-internal/src/lib/stores/navPermissions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { api } from '$lib/utils/api';
|
||||
|
||||
function createNavPermissionsStore() {
|
||||
const { subscribe, set } = writable<Record<string, boolean>>({});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
load: async () => {
|
||||
try {
|
||||
const perms = await api.get<Record<string, boolean>>('/permissions/my');
|
||||
set(perms);
|
||||
} catch (e) {
|
||||
set({});
|
||||
}
|
||||
},
|
||||
clear: () => set({})
|
||||
};
|
||||
}
|
||||
|
||||
export const navPerms = createNavPermissionsStore();
|
||||
@@ -1,14 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/utils/api';
|
||||
import { auth } from '$lib/stores/auth';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import { INTERNAL_ROUTES } from '$lib/routes-registry';
|
||||
|
||||
let users = [];
|
||||
let tenants = [];
|
||||
let isLoading = false;
|
||||
let showModal = false;
|
||||
let editingUser = null;
|
||||
let navPermissions: Record<string, boolean> = {};
|
||||
let isLoadingPerms = false;
|
||||
|
||||
let formData = {
|
||||
email: '',
|
||||
@@ -29,6 +33,12 @@
|
||||
{ value: 'CLIENT_USER', label: 'Usuario Cliente' }
|
||||
];
|
||||
|
||||
// Solo ADMIN y SUPPORT_MANAGER pueden gestionar permisos de navegación
|
||||
$: canManageNav = $auth.user?.role === 'ADMIN' || $auth.user?.role === 'SUPPORT_MANAGER';
|
||||
|
||||
// Rutas relevantes según el rol del usuario que se está editando
|
||||
$: relevantRoutes = INTERNAL_ROUTES;
|
||||
|
||||
async function loadData() {
|
||||
isLoading = true;
|
||||
try {
|
||||
@@ -45,6 +55,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
function initNavPermissions() {
|
||||
// Inicializar todos en false por defecto
|
||||
navPermissions = {};
|
||||
INTERNAL_ROUTES.forEach(r => {
|
||||
navPermissions[r.key] = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadUserNavPermissions(userId: string) {
|
||||
isLoadingPerms = true;
|
||||
try {
|
||||
const perms = await api.get(`/permissions/user/${userId}`);
|
||||
// perms es un objeto { permission: granted }
|
||||
INTERNAL_ROUTES.forEach(r => {
|
||||
navPermissions[r.key] = perms[r.key] ?? false;
|
||||
});
|
||||
} catch (e) {
|
||||
// Si falla, dejar defaults
|
||||
initNavPermissions();
|
||||
} finally {
|
||||
isLoadingPerms = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
editingUser = null;
|
||||
formData = {
|
||||
@@ -56,40 +90,61 @@
|
||||
tenant_id: '',
|
||||
is_active: true
|
||||
};
|
||||
initNavPermissions();
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
function openEditModal(user) {
|
||||
async function openEditModal(user) {
|
||||
editingUser = user;
|
||||
formData = {
|
||||
email: user.email,
|
||||
password: '', // Don't show password
|
||||
password: '',
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
role: user.role,
|
||||
tenant_id: user.tenant_id || '',
|
||||
is_active: user.is_active
|
||||
};
|
||||
initNavPermissions();
|
||||
showModal = true;
|
||||
if (canManageNav) {
|
||||
await loadUserNavPermissions(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const payload = { ...formData };
|
||||
if (!payload.password) delete payload.password; // Don't send empty password on edit
|
||||
if (!payload.tenant_id) payload.tenant_id = null; // Send null if empty string
|
||||
if (!payload.password) delete payload.password;
|
||||
if (!payload.tenant_id) payload.tenant_id = null;
|
||||
|
||||
let userId: string;
|
||||
|
||||
if (editingUser) {
|
||||
await api.put(`/users/${editingUser.id}`, payload);
|
||||
userId = editingUser.id;
|
||||
toast.success('Usuario actualizado');
|
||||
} else {
|
||||
if (!payload.password) {
|
||||
toast.error('La contraseña es requerida para nuevos usuarios');
|
||||
return;
|
||||
toast.error('La contraseña es requerida para nuevos usuarios');
|
||||
return;
|
||||
}
|
||||
await api.post('/users/', payload);
|
||||
const newUser = await api.post('/users/', payload);
|
||||
userId = newUser.id;
|
||||
toast.success('Usuario creado');
|
||||
}
|
||||
|
||||
// Guardar permisos de navegación
|
||||
if (canManageNav && userId) {
|
||||
try {
|
||||
await api.put(`/permissions/user/${userId}`, {
|
||||
permissions: navPermissions
|
||||
});
|
||||
} catch (e) {
|
||||
toast.error('Usuario guardado pero error al guardar permisos de navegación');
|
||||
}
|
||||
}
|
||||
|
||||
showModal = false;
|
||||
loadData();
|
||||
} catch (e) {
|
||||
@@ -130,20 +185,18 @@
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Usuario</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Rol</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Cliente (Tenant)</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Estado</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">Acciones</span>
|
||||
</th>
|
||||
<th class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Usuario</th>
|
||||
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Rol</th>
|
||||
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Cliente (Tenant)</th>
|
||||
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Estado</th>
|
||||
<th class="relative py-3.5 pl-3 pr-4 sm:pr-6"><span class="sr-only">Acciones</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#if isLoading}
|
||||
<tr><td colspan="5" class="text-center py-4">Cargando...</td></tr>
|
||||
<tr><td colspan="5" class="text-center py-4">Cargando...</td></tr>
|
||||
{:else if users.length === 0}
|
||||
<tr><td colspan="5" class="text-center py-4">No hay usuarios registrados</td></tr>
|
||||
<tr><td colspan="5" class="text-center py-4">No hay usuarios registrados</td></tr>
|
||||
{:else}
|
||||
{#each users as user}
|
||||
<tr>
|
||||
@@ -177,27 +230,34 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="first_name" class="block text-sm font-medium text-gray-700">Nombre</label>
|
||||
<input type="text" id="first_name" bind:value={formData.first_name} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
<input type="text" id="first_name" bind:value={formData.first_name} required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
</div>
|
||||
<div>
|
||||
<label for="last_name" class="block text-sm font-medium text-gray-700">Apellido</label>
|
||||
<input type="text" id="last_name" bind:value={formData.last_name} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
<input type="text" id="last_name" bind:value={formData.last_name} required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input type="email" id="email" bind:value={formData.email} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
<input type="email" id="email" bind:value={formData.email} required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Contraseña {editingUser ? '(dejar en blanco para mantener)' : ''}</label>
|
||||
<input type="password" id="password" bind:value={formData.password} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">
|
||||
Contraseña {editingUser ? '(dejar en blanco para mantener)' : ''}
|
||||
</label>
|
||||
<input type="password" id="password" bind:value={formData.password}
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="role" class="block text-sm font-medium text-gray-700">Rol</label>
|
||||
<select id="role" bind:value={formData.role} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
<select id="role" bind:value={formData.role}
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
{#each ROLES as role}
|
||||
<option value={role.value}>{role.label}</option>
|
||||
{/each}
|
||||
@@ -205,8 +265,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="tenant" class="block text-sm font-medium text-gray-700">Cliente (Opcional - solo para usuarios externos)</label>
|
||||
<select id="tenant" bind:value={formData.tenant_id} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
<label for="tenant" class="block text-sm font-medium text-gray-700">Cliente (Opcional)</label>
|
||||
<select id="tenant" bind:value={formData.tenant_id}
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm border p-2">
|
||||
<option value="">-- Ninguno (Usuario Interno) --</option>
|
||||
{#each tenants as tenant}
|
||||
<option value={tenant.id}>{tenant.name}</option>
|
||||
@@ -215,17 +276,49 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="is_active" bind:checked={formData.is_active} class="h-4 w-4 rounded border-gray-300 text-blue-700 focus:ring-blue-500">
|
||||
<input type="checkbox" id="is_active" bind:checked={formData.is_active}
|
||||
class="h-4 w-4 rounded border-gray-300 text-blue-700 focus:ring-blue-500">
|
||||
<label for="is_active" class="ml-2 block text-sm text-gray-900">Activo</label>
|
||||
</div>
|
||||
|
||||
<!-- Sección de vistas — solo para admins -->
|
||||
{#if canManageNav}
|
||||
<div class="pt-4 border-t border-gray-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="text-sm font-semibold text-gray-900">Vistas disponibles</h4>
|
||||
{#if isLoadingPerms}
|
||||
<span class="text-xs text-gray-400">Cargando permisos...</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each INTERNAL_ROUTES as route}
|
||||
<label class="flex items-center gap-2 p-2 rounded-md border cursor-pointer hover:bg-gray-50
|
||||
{navPermissions[route.key] ? 'border-blue-300 bg-blue-50' : 'border-gray-200'}">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={navPermissions[route.key]}
|
||||
disabled={isLoadingPerms}
|
||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="text-sm text-gray-700">{route.label}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-2">
|
||||
Las vistas desactivadas no aparecerán en el menú del usuario.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
|
||||
<button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-700 text-base font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:col-start-2 sm:text-sm">
|
||||
<button type="submit"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-700 text-base font-medium text-white hover:bg-blue-800 sm:col-start-2 sm:text-sm">
|
||||
Guardar
|
||||
</button>
|
||||
<button type="button" on:click={() => showModal = false} class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:col-start-1 sm:text-sm">
|
||||
<button type="button" on:click={() => showModal = false}
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 sm:mt-0 sm:col-start-1 sm:text-sm">
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Modal>
|
||||
Reference in New Issue
Block a user