EDMS/pages/dms/role-management.vue
2025-05-31 14:58:52 +08:00

585 lines
24 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted } from 'vue';
import { useDmsStore } from '~/stores/dms';
// Define page metadata
definePageMeta({
title: "Role Management",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "DMS",
path: "/dms",
},
{
name: "Role Management",
path: "/dms/role-management",
},
],
});
// Store
const dmsStore = useDmsStore();
// State variables
const isLoading = ref(true);
const users = ref([]);
const roles = ref([]);
const activeTab = ref('users');
const searchQuery = ref('');
const hasError = ref(false);
const errorMessage = ref('');
// Filtered users
const filteredUsers = computed(() => {
if (!searchQuery.value) return users.value;
const query = searchQuery.value.toLowerCase();
return users.value.filter(user =>
user.name.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query) ||
user.department.toLowerCase().includes(query) ||
user.roles.some(role => role.toLowerCase().includes(query))
);
});
// Methods
const loadUsers = async () => {
isLoading.value = true;
hasError.value = false;
try {
// This would be an actual API call in production
// For demo, we'll simulate it with mock data
await new Promise(resolve => setTimeout(resolve, 800));
users.value = [
{
id: 'user1',
name: 'Aiman Fakhrullah',
email: 'aiman@example.com',
department: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
roles: ['engineer'],
lastLogin: '2023-12-15T08:30:00Z',
status: 'active'
},
{
id: 'user2',
name: 'Ahmad Zaki',
email: 'ahmad@example.com',
department: 'JKR Bahagian Kewangan',
roles: ['finance', 'approver'],
lastLogin: '2023-12-14T14:45:00Z',
status: 'active'
},
{
id: 'user3',
name: 'Siti Aminah',
email: 'siti@example.com',
department: 'JKR Cawangan Kuala Terengganu',
roles: ['manager', 'approver'],
lastLogin: '2023-12-13T09:15:00Z',
status: 'active'
},
{
id: 'admin1',
name: 'Admin User',
email: 'admin@example.com',
department: 'IT Department',
roles: ['admin'],
lastLogin: '2023-12-15T10:00:00Z',
status: 'active'
}
];
} catch (error) {
console.error('Failed to load users:', error);
hasError.value = true;
errorMessage.value = 'Failed to load users. Please try again.';
} finally {
isLoading.value = false;
}
};
const loadRoles = async () => {
isLoading.value = true;
hasError.value = false;
try {
// This would be an actual API call to Authentik in production
// For demo, we'll simulate it with mock data
await new Promise(resolve => setTimeout(resolve, 600));
roles.value = [
{
id: 'admin',
name: 'Administrator',
description: 'Full system access',
userCount: 1,
permissions: {
documents: {
view: true,
edit: true,
delete: true,
approve: true,
reject: true,
download: true
},
cabinets: {
view: true,
create: true,
edit: true,
delete: true
},
accessRequests: {
approve: true,
reject: true,
viewAll: true
}
}
},
{
id: 'manager',
name: 'Manager',
description: 'Department management',
userCount: 1,
permissions: {
documents: {
view: true,
edit: true,
delete: false,
approve: true,
reject: true,
download: true
},
cabinets: {
view: true,
create: true,
edit: true,
delete: false
},
accessRequests: {
approve: true,
reject: true,
viewAll: true
}
}
},
{
id: 'engineer',
name: 'Engineer',
description: 'Engineering staff',
userCount: 1,
permissions: {
documents: {
view: true,
edit: true,
delete: false,
approve: false,
reject: false,
download: true
},
cabinets: {
view: true,
create: false,
edit: false,
delete: false
},
accessRequests: {
approve: false,
reject: false,
viewAll: false
}
}
},
{
id: 'finance',
name: 'Finance',
description: 'Finance department staff',
userCount: 1,
permissions: {
documents: {
view: true,
edit: true,
delete: false,
approve: false,
reject: false,
download: true
},
cabinets: {
view: true,
create: false,
edit: false,
delete: false
},
accessRequests: {
approve: false,
reject: false,
viewAll: false
}
}
},
{
id: 'approver',
name: 'Approver',
description: 'Can approve access requests',
userCount: 2,
permissions: {
documents: {
view: true,
edit: false,
delete: false,
approve: true,
reject: true,
download: false
},
cabinets: {
view: true,
create: false,
edit: false,
delete: false
},
accessRequests: {
approve: true,
reject: true,
viewAll: true
}
}
}
];
} catch (error) {
console.error('Failed to load roles:', error);
hasError.value = true;
errorMessage.value = 'Failed to load roles. Please try again.';
} finally {
isLoading.value = false;
}
};
const switchTab = (tab) => {
activeTab.value = tab;
if (tab === 'users' && users.value.length === 0) {
loadUsers();
} else if (tab === 'roles' && roles.value.length === 0) {
loadRoles();
}
};
const formatDateTime = (dateString) => {
if (!dateString) return 'Never';
const date = new Date(dateString);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Icons
const getSvgIcon = (iconName) => {
const icons = {
'users': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>`,
'roles': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>`,
'search': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
'add': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
'edit': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
'trash': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`
};
return icons[iconName] || '';
};
// Lifecycle hooks
onMounted(() => {
loadUsers();
});
</script>
<template>
<div class="dms-role-management">
<LayoutsBreadcrumb />
<rs-card class="h-full">
<template #body>
<div class="h-full flex flex-col">
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center h-full">
<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-gray-600 dark:text-gray-400">Loading role management...</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="hasError" class="flex items-center justify-center h-full">
<div class="text-center p-6">
<div class="text-red-500 text-5xl mb-4"></div>
<h2 class="text-xl font-semibold text-red-600 mb-2">Error Loading Data</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ errorMessage }}</p>
<rs-button @click="activeTab === 'users' ? loadUsers() : loadRoles()" variant="primary">
Retry
</rs-button>
</div>
</div>
<!-- Main Content -->
<div v-else class="h-full flex flex-col">
<!-- Header with authentik info -->
<div class="bg-indigo-50 dark:bg-indigo-900/20 border-b border-indigo-200 dark:border-indigo-800">
<div class="px-6 py-4">
<div class="flex items-center">
<div class="w-10 h-10 rounded bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-600 dark:text-indigo-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<div>
<h2 class="text-lg font-medium text-indigo-900 dark:text-indigo-100">Authentik Integration</h2>
<p class="text-sm text-indigo-600 dark:text-indigo-300">Role-Based Access Control Management</p>
</div>
</div>
</div>
</div>
<!-- Tabs Navigation -->
<div class="border-b border-gray-200 dark:border-gray-700">
<div class="px-6">
<nav class="-mb-px flex space-x-6" aria-label="Tabs">
<button
v-for="tab in [{ id: 'users', label: 'Users', icon: 'users' }, { id: 'roles', label: 'Roles', icon: 'roles' }]"
:key="tab.id"
@click="switchTab(tab.id)"
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors"
:class="[
activeTab === tab.id
? 'border-indigo-500 text-indigo-600 dark:border-indigo-400 dark:text-indigo-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600'
]"
>
<div class="flex items-center">
<span v-html="getSvgIcon(tab.icon)" class="mr-2"></span>
<span>{{ tab.label }}</span>
</div>
</button>
</nav>
</div>
</div>
<!-- Action Bar -->
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<!-- Search -->
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span v-html="getSvgIcon('search')" class="text-gray-400 dark:text-gray-500"></span>
</div>
<input
type="text"
v-model="searchQuery"
class="pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-indigo-500 focus:border-indigo-500 w-64"
placeholder="Search..."
/>
</div>
<!-- Actions -->
<div>
<rs-button variant="primary" class="flex items-center">
<span v-html="getSvgIcon('add')" class="mr-1.5"></span>
<span>{{ activeTab === 'users' ? 'Add User' : 'Add Role' }}</span>
</rs-button>
</div>
</div>
</div>
<!-- Tab Content -->
<div class="flex-1 p-4 overflow-auto">
<!-- Users Tab -->
<div v-if="activeTab === 'users'" class="h-full">
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900/10">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
User
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Department
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Roles
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Last Login
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="user in filteredUsers" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-900/10">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400">
{{ user.name.charAt(0) }}
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ user.name }}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ user.email }}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<div class="max-w-xs truncate">{{ user.department }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex flex-wrap gap-1">
<span
v-for="role in user.roles"
:key="role"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/20 dark:text-indigo-300': role === 'admin',
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300': role === 'manager',
'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300': role === 'engineer',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300': role === 'finance',
'bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300': role === 'approver'
}"
>
{{ role }}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ formatDateTime(user.lastLogin) }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300': user.status === 'active',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300': user.status === 'pending',
'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300': user.status === 'inactive'
}"
>
{{ user.status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-3">
<span v-html="getSvgIcon('edit')"></span>
</button>
<button class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
<span v-html="getSvgIcon('trash')"></span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Roles Tab -->
<div v-else-if="activeTab === 'roles'" class="h-full">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div
v-for="role in roles"
:key="role.id"
class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700"
>
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ role.name }}</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ role.description }}</p>
</div>
<div class="flex space-x-2">
<button class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
<span v-html="getSvgIcon('edit')"></span>
</button>
</div>
</div>
</div>
<div class="px-6 py-4">
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">Document Permissions</h4>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="(value, key) in role.permissions.documents"
:key="`doc-${key}`"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="value ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400 line-through'"
>
{{ key }}
</span>
</div>
</div>
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">Cabinet Permissions</h4>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="(value, key) in role.permissions.cabinets"
:key="`cab-${key}`"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="value ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400 line-through'"
>
{{ key }}
</span>
</div>
</div>
<div>
<div class="flex justify-between items-center mb-2">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">Access Request Permissions</h4>
</div>
<div class="flex flex-wrap gap-2">
<span
v-for="(value, key) in role.permissions.accessRequests"
:key="`acc-${key}`"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="value ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400 line-through'"
>
{{ key }}
</span>
</div>
</div>
</div>
<div class="px-6 py-3 bg-gray-50 dark:bg-gray-900/10 text-right">
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ role.userCount }} {{ role.userCount === 1 ? 'user' : 'users' }} with this role
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
</div>
</template>
<style scoped>
.dms-role-management {
height: calc(100vh - 64px);
}
</style>