234 lines
8.7 KiB
Svelte
234 lines
8.7 KiB
Svelte
<!-- [DEF:AdminRolesPage:Component] -->
|
|
<!--
|
|
@TIER: STANDARD
|
|
@SEMANTICS: admin, role-management, rbac
|
|
@PURPOSE: UI for managing system roles and their permissions.
|
|
@LAYER: Domain
|
|
@RELATION: DEPENDS_ON -> frontend.src.services.adminService
|
|
@RELATION: DEPENDS_ON -> frontend.src.components.auth.ProtectedRoute
|
|
|
|
@INVARIANT: Only accessible by users with Admin role.
|
|
-->
|
|
|
|
<script lang="ts">
|
|
// [SECTION: IMPORTS]
|
|
import { onMount } from 'svelte';
|
|
import { t } from '$lib/i18n';
|
|
import ProtectedRoute from '../../../components/auth/ProtectedRoute.svelte';
|
|
import { adminService } from '../../../services/adminService';
|
|
// [/SECTION: IMPORTS]
|
|
|
|
let roles = [];
|
|
let permissions = [];
|
|
let loading = true;
|
|
let error = null;
|
|
|
|
let showModal = false;
|
|
let isEditing = false;
|
|
let currentRoleId = null;
|
|
let roleForm = {
|
|
name: '',
|
|
description: '',
|
|
permissions: []
|
|
};
|
|
|
|
// [DEF:loadData:Function]
|
|
/**
|
|
* @purpose Fetches roles and available permissions.
|
|
* @pre Component mounted.
|
|
* @post roles and permissions arrays populated.
|
|
*/
|
|
async function loadData() {
|
|
console.log('[AdminRolesPage][loadData][Entry]');
|
|
loading = true;
|
|
try {
|
|
[roles, permissions] = await Promise.all([
|
|
adminService.getRoles(),
|
|
adminService.getPermissions()
|
|
]);
|
|
console.log('[AdminRolesPage][loadData][Coherence:OK]');
|
|
} catch (e) {
|
|
error = "Failed to load roles data.";
|
|
console.error('[AdminRolesPage][loadData][Coherence:Failed]', e);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
// [/DEF:loadData:Function]
|
|
|
|
// [DEF:openCreateModal:Function]
|
|
/**
|
|
* @purpose Initializes state for creating a new role.
|
|
* @pre None.
|
|
* @post showModal is true, roleForm is reset.
|
|
*/
|
|
function openCreateModal() {
|
|
console.log("[openCreateModal][Action] Opening create modal");
|
|
isEditing = false;
|
|
currentRoleId = null;
|
|
roleForm = { name: '', description: '', permissions: [] };
|
|
showModal = true;
|
|
}
|
|
// [/DEF:openCreateModal:Function]
|
|
|
|
// [DEF:openEditModal:Function]
|
|
/**
|
|
* @purpose Initializes state for editing an existing role.
|
|
* @pre role object is provided.
|
|
* @post showModal is true, roleForm is populated.
|
|
*/
|
|
function openEditModal(role) {
|
|
console.log(`[openEditModal][Action] Opening edit modal for role ${role.id}`);
|
|
isEditing = true;
|
|
currentRoleId = role.id;
|
|
roleForm = {
|
|
name: role.name,
|
|
description: role.description || '',
|
|
permissions: role.permissions.map(p => p.id)
|
|
};
|
|
showModal = true;
|
|
}
|
|
// [/DEF:openEditModal:Function]
|
|
|
|
// [DEF:handleSaveRole:Function]
|
|
/**
|
|
* @purpose Submits role data (create or update).
|
|
* @pre roleForm contains valid data.
|
|
* @post Role is saved, modal closed, data reloaded.
|
|
*/
|
|
async function handleSaveRole() {
|
|
console.log('[AdminRolesPage][handleSaveRole][Entry]');
|
|
try {
|
|
if (isEditing) {
|
|
await adminService.updateRole(currentRoleId, roleForm);
|
|
} else {
|
|
await adminService.createRole(roleForm);
|
|
}
|
|
showModal = false;
|
|
await loadData();
|
|
console.log('[AdminRolesPage][handleSaveRole][Coherence:OK]');
|
|
} catch (e) {
|
|
alert("Failed to save role: " + e.message);
|
|
console.error('[AdminRolesPage][handleSaveRole][Coherence:Failed]', e);
|
|
}
|
|
}
|
|
// [/DEF:handleSaveRole:Function]
|
|
|
|
// [DEF:handleDeleteRole:Function]
|
|
/**
|
|
* @purpose Deletes a role after confirmation.
|
|
* @pre role object is provided.
|
|
* @post Role is deleted if confirmed, data reloaded.
|
|
*/
|
|
async function handleDeleteRole(role) {
|
|
if (!confirm($t.admin.roles.confirm_delete.replace('{name}', role.name))) return;
|
|
|
|
console.log('[AdminRolesPage][handleDeleteRole][Entry]');
|
|
try {
|
|
await adminService.deleteRole(role.id);
|
|
await loadData();
|
|
console.log('[AdminRolesPage][handleDeleteRole][Coherence:OK]');
|
|
} catch (e) {
|
|
alert("Failed to delete role: " + e.message);
|
|
console.error('[AdminRolesPage][handleDeleteRole][Coherence:Failed]', e);
|
|
}
|
|
}
|
|
// [/DEF:handleDeleteRole:Function]
|
|
|
|
onMount(loadData);
|
|
</script>
|
|
|
|
<ProtectedRoute requiredPermission="admin:roles">
|
|
<!-- [SECTION: TEMPLATE] -->
|
|
<div class="container mx-auto p-4">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-2xl font-bold">{$t.admin.roles.title}</h1>
|
|
<button
|
|
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
|
on:click={openCreateModal}
|
|
>
|
|
{$t.admin.roles.create}
|
|
</button>
|
|
</div>
|
|
|
|
{#if loading}
|
|
<p>{$t.admin.roles.loading}</p>
|
|
{:else if error}
|
|
<div class="bg-red-100 text-red-700 p-4 rounded">{error}</div>
|
|
{:else}
|
|
<div class="bg-white shadow rounded-lg overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.admin.roles.name}</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.admin.roles.description}</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.admin.roles.permissions}</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.common.actions}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
{#each roles as role}
|
|
<tr>
|
|
<td class="px-6 py-4 whitespace-nowrap font-medium">{role.name}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{role.description || '-'}</td>
|
|
<td class="px-6 py-4">
|
|
<div class="flex flex-wrap gap-1">
|
|
{#each role.permissions as perm}
|
|
<span class="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded border border-blue-100">
|
|
{perm.resource}:{perm.action}
|
|
</span>
|
|
{/each}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<button on:click={() => openEditModal(role)} class="text-blue-600 hover:text-blue-900 mr-3">{$t.common.edit}</button>
|
|
<button on:click={() => handleDeleteRole(role)} class="text-red-600 hover:text-red-900">{$t.common.delete}</button>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if showModal}
|
|
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
|
<div class="bg-white rounded-lg p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
<h2 class="text-xl font-bold mb-4">
|
|
{isEditing ? $t.admin.roles.modal_edit_title : $t.admin.roles.modal_create_title}
|
|
</h2>
|
|
<form on:submit|preventDefault={handleSaveRole}>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium mb-1">{$t.admin.roles.name}</label>
|
|
<input type="text" bind:value={roleForm.name} class="w-full border p-2 rounded" required readonly={isEditing} />
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium mb-1">{$t.admin.roles.description}</label>
|
|
<textarea bind:value={roleForm.description} class="w-full border p-2 rounded h-20"></textarea>
|
|
</div>
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium mb-2">{$t.admin.roles.permissions}</label>
|
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 border p-3 rounded bg-gray-50">
|
|
{#each permissions as perm}
|
|
<label class="flex items-center space-x-2 p-1 hover:bg-white rounded cursor-pointer">
|
|
<input type="checkbox" value={perm.id} bind:group={roleForm.permissions} class="rounded text-blue-600" />
|
|
<span class="text-xs">{perm.resource}:{perm.action}</span>
|
|
</label>
|
|
{/each}
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-2">{$t.admin.roles.permissions_hint}</p>
|
|
</div>
|
|
<div class="flex justify-end gap-2 pt-4 border-t">
|
|
<button type="button" class="px-4 py-2 text-gray-600" on:click={() => showModal = false}>{$t.common.cancel}</button>
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">{$t.common.save}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<!-- [/SECTION: TEMPLATE] -->
|
|
</ProtectedRoute>
|
|
|
|
|
|
<!-- [/DEF:AdminRolesPage:Component] --> |