From 8cff207c45bc1404164b79f08d65e345fd96f8f6 Mon Sep 17 00:00:00 2001 From: Aiman Fakhrullah Mantasan Date: Sat, 31 May 2025 14:58:52 +0800 Subject: [PATCH] Added Access Management and Role Management --- .../dms/dialogs/DMSAccessApprovalDialog.vue | 296 +++++++ .../dms/dialogs/DMSAccessRequestDialog.vue | 351 ++++---- components/dms/explorer/DMSExplorer.vue | 122 ++- .../dms/workflows/DMSAccessRequestTracker.vue | 435 ++++++++++ components/dms/workflows/DMSApprovalQueue.vue | 539 ++++++++++++ navigation/index.js | 6 - pages/dms/access-management.vue | 236 ++++++ pages/dms/index.vue | 35 + pages/dms/role-management.vue | 585 +++++++++++++ stores/dms.js | 774 +++++++++++++++--- 10 files changed, 3120 insertions(+), 259 deletions(-) create mode 100644 components/dms/dialogs/DMSAccessApprovalDialog.vue create mode 100644 components/dms/workflows/DMSAccessRequestTracker.vue create mode 100644 components/dms/workflows/DMSApprovalQueue.vue create mode 100644 pages/dms/access-management.vue create mode 100644 pages/dms/role-management.vue diff --git a/components/dms/dialogs/DMSAccessApprovalDialog.vue b/components/dms/dialogs/DMSAccessApprovalDialog.vue new file mode 100644 index 0000000..dc6e581 --- /dev/null +++ b/components/dms/dialogs/DMSAccessApprovalDialog.vue @@ -0,0 +1,296 @@ + + + \ No newline at end of file diff --git a/components/dms/dialogs/DMSAccessRequestDialog.vue b/components/dms/dialogs/DMSAccessRequestDialog.vue index 171a67a..67c29c1 100644 --- a/components/dms/dialogs/DMSAccessRequestDialog.vue +++ b/components/dms/dialogs/DMSAccessRequestDialog.vue @@ -1,5 +1,5 @@ diff --git a/components/dms/workflows/DMSAccessRequestTracker.vue b/components/dms/workflows/DMSAccessRequestTracker.vue new file mode 100644 index 0000000..471513d --- /dev/null +++ b/components/dms/workflows/DMSAccessRequestTracker.vue @@ -0,0 +1,435 @@ + + + + + \ No newline at end of file diff --git a/components/dms/workflows/DMSApprovalQueue.vue b/components/dms/workflows/DMSApprovalQueue.vue new file mode 100644 index 0000000..7bb13c2 --- /dev/null +++ b/components/dms/workflows/DMSApprovalQueue.vue @@ -0,0 +1,539 @@ + + + + + \ No newline at end of file diff --git a/navigation/index.js b/navigation/index.js index 838fbc3..4cfe22e 100644 --- a/navigation/index.js +++ b/navigation/index.js @@ -10,12 +10,6 @@ export default [ "child": [], "meta": {} }, - { - "title": "Access Requests", - "path": "/dms/access-requests", - "icon": "ic:outline-security", - "child": [] - }, { "title": "Settings", "path": "/dms/settings", diff --git a/pages/dms/access-management.vue b/pages/dms/access-management.vue new file mode 100644 index 0000000..400838a --- /dev/null +++ b/pages/dms/access-management.vue @@ -0,0 +1,236 @@ + + + + + \ No newline at end of file diff --git a/pages/dms/index.vue b/pages/dms/index.vue index cfde3ab..93861f5 100644 --- a/pages/dms/index.vue +++ b/pages/dms/index.vue @@ -181,6 +181,41 @@ onMounted(() => {
+ +
+
+ +
+ +
+ + + + + + + Access Management + + + + + + + + + + + Role Management + +
+
+ +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': ``, + 'roles': ``, + 'search': ``, + 'add': ``, + 'edit': ``, + 'trash': `` + }; + + return icons[iconName] || ''; +}; + +// Lifecycle hooks +onMounted(() => { + loadUsers(); +}); + + + + + \ No newline at end of file diff --git a/stores/dms.js b/stores/dms.js index 51e30d1..4ce3dd4 100644 --- a/stores/dms.js +++ b/stores/dms.js @@ -30,6 +30,224 @@ export const useDmsStore = defineStore('dms', { { id: 'rejected', name: 'Rejected', color: 'red' } ], + // Access requests tracking + accessRequests: [ + { + id: 'req1', + documentId: 'private-file1', + documentName: 'Annual_Budget_2024_CONFIDENTIAL.xlsx', + documentPath: '/private-cabinet1/private-drawer1/Annual_Budget_2024_CONFIDENTIAL.xlsx', + requesterId: 'user1', + requesterName: 'Aiman Fakhrullah', + requesterEmail: 'aiman@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: 'user2', + approverName: 'Ahmad Zaki', + approverDepartment: 'JKR Bahagian Kewangan', + requestDate: '2023-12-01T09:30:00Z', + targetResolutionTime: '2023-12-03T09:30:00Z', // 48 hours SLA + responseDate: '2023-12-02T14:15:00Z', + status: 'approved', + accessType: 'view', + accessDuration: '7 days', + justification: 'Need to review budget allocations for the upcoming bridge construction project', + notes: 'Approved for view-only access. Please ensure confidentiality is maintained.' + }, + { + id: 'req2', + documentId: 'private-drawer1', + documentName: 'Budget & Financial Reports', + documentPath: '/private-cabinet1/Budget & Financial Reports', + requesterId: 'user1', + requesterName: 'Aiman Fakhrullah', + requesterEmail: 'aiman@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: null, + approverName: null, + approverDepartment: null, + requestDate: '2023-12-10T11:45:00Z', + targetResolutionTime: '2023-12-12T11:45:00Z', // 48 hours SLA + responseDate: null, + status: 'pending', + accessType: 'download', + accessDuration: '30 days', + justification: 'Need to analyze financial reports for the department annual review', + notes: null + }, + { + id: 'req3', + documentId: 'jkr-kuala-terengganu', + documentName: 'JKR Cawangan Kuala Terengganu, Terengganu', + documentPath: '/private-cabinets/JKR Cawangan Kuala Terengganu, Terengganu', + requesterId: 'user1', + requesterName: 'Aiman Fakhrullah', + requesterEmail: 'aiman@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: 'user3', + approverName: 'Siti Aminah', + approverDepartment: 'JKR Cawangan Kuala Terengganu', + requestDate: '2023-12-05T15:20:00Z', + targetResolutionTime: '2023-12-07T15:20:00Z', // 48 hours SLA + responseDate: '2023-12-08T10:05:00Z', // Overdue response + status: 'rejected', + accessType: 'full', + accessDuration: '90 days', + justification: 'Need full access for the joint infrastructure project between Kota Bharu and Kuala Terengganu branches', + notes: 'Request denied. Please have your department head contact our director for special authorization.' + }, + { + id: 'req4', + documentId: 'jkr-batu-kawan', + documentName: 'JKR Cawangan Batu Kawan, Penang', + documentPath: '/private-cabinets/JKR Cawangan Batu Kawan, Penang', + requesterId: 'user1', + requesterName: 'Aiman Fakhrullah', + requesterEmail: 'aiman@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: null, + approverName: null, + approverDepartment: null, + requestDate: '2023-11-28T09:15:00Z', + targetResolutionTime: '2023-11-30T09:15:00Z', // 48 hours SLA + responseDate: null, + status: 'pending', + accessType: 'view', + accessDuration: '14 days', + justification: 'Need to review similar projects in Batu Kawan for reference', + notes: null + }, + { + id: 'req5', + documentId: 'jkr-arkitek', + documentName: 'JKR Cawangan Arkitek', + documentPath: '/private-cabinets/JKR Cawangan Arkitek', + requesterId: 'user1', + requesterName: 'Aiman Fakhrullah', + requesterEmail: 'aiman@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: null, + approverName: null, + approverDepartment: null, + requestDate: '2023-12-15T14:30:00Z', + targetResolutionTime: '2023-12-17T14:30:00Z', // 48 hours SLA + responseDate: null, + status: 'pending', + accessType: 'view', + accessDuration: '30 days', + justification: 'Need to consult architectural plans for the new government complex', + notes: null + } + ], + + // Department access requests (for KPI tracking) + departmentAccessRequests: [ + { + id: 'dept-req1', + documentId: 'finance-reports-2023', + documentName: 'Finance Reports 2023', + documentPath: '/private-cabinet1/finance-reports-2023', + requesterId: 'user4', + requesterName: 'Ibrahim Hassan', + requesterEmail: 'ibrahim@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: 'user5', + approverName: 'Fatimah Abdullah', + approverDepartment: 'JKR Bahagian Kewangan', + requestDate: '2023-12-02T10:30:00Z', + targetResolutionTime: '2023-12-04T10:30:00Z', + responseDate: '2023-12-03T11:45:00Z', + status: 'approved', + accessType: 'view', + accessDuration: '14 days', + justification: 'Need to review financial reports for project planning', + notes: 'Approved with standard view-only restrictions' + }, + { + id: 'dept-req2', + documentId: 'hr-policies-2023', + documentName: 'HR Policies 2023', + documentPath: '/private-cabinet2/hr-policies-2023', + requesterId: 'user6', + requesterName: 'Nurul Huda', + requesterEmail: 'nurul@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: 'user7', + approverName: 'Omar Ali', + approverDepartment: 'JKR Bahagian HR', + requestDate: '2023-12-05T09:15:00Z', + targetResolutionTime: '2023-12-07T09:15:00Z', + responseDate: '2023-12-09T16:30:00Z', // Overdue + status: 'approved', + accessType: 'download', + accessDuration: '30 days', + justification: 'Need to implement new HR policies in our department', + notes: 'Approved with delayed response due to verification requirements' + }, + { + id: 'dept-req3', + documentId: 'strategic-plan-2024', + documentName: 'Strategic Plan 2024', + documentPath: '/private-cabinet3/strategic-plan-2024', + requesterId: 'user8', + requesterName: 'Hassan Ibrahim', + requesterEmail: 'hassan@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: 'user9', + approverName: 'Zainab Mohamed', + approverDepartment: 'JKR Pengarah', + requestDate: '2023-12-08T11:00:00Z', + targetResolutionTime: '2023-12-10T11:00:00Z', + responseDate: '2023-12-09T09:45:00Z', + status: 'rejected', + accessType: 'full', + accessDuration: '90 days', + justification: 'Need full access to implement strategic initiatives', + notes: 'Rejected due to insufficient clearance level. Please request through department head.' + }, + { + id: 'dept-req4', + documentId: 'project-tendering-guidelines', + documentName: 'Project Tendering Guidelines', + documentPath: '/private-cabinet4/project-tendering-guidelines', + requesterId: 'user10', + requesterName: 'Razak Ismail', + requesterEmail: 'razak@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: null, + approverName: null, + approverDepartment: null, + requestDate: '2023-12-12T14:15:00Z', + targetResolutionTime: '2023-12-14T14:15:00Z', + responseDate: null, + status: 'pending', + accessType: 'view', + accessDuration: '7 days', + justification: 'Need to review tendering guidelines for upcoming project', + notes: null + }, + { + id: 'dept-req5', + documentId: 'audit-reports-2023', + documentName: 'Audit Reports 2023', + documentPath: '/private-cabinet5/audit-reports-2023', + requesterId: 'user11', + requesterName: 'Aminah Zainal', + requesterEmail: 'aminah@example.com', + requesterDepartment: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + approverId: null, + approverName: null, + approverDepartment: null, + requestDate: '2023-12-14T10:30:00Z', + targetResolutionTime: '2023-12-16T10:30:00Z', + responseDate: null, + status: 'pending', + accessType: 'download', + accessDuration: '14 days', + justification: 'Need to review audit findings for compliance', + notes: null + } + ], + // Items state - in production this would be loaded from API cabinets: [ { @@ -268,75 +486,24 @@ export const useDmsStore = defineStore('dms', { } ], - // Access requests - accessRequests: [ - { - id: 'req1', - userId: 'user1', - userName: 'Aiman Fakhrullah', - cabinetId: 'jkr-batu-kawan', - cabinetName: 'JKR Cawangan Batu Kawan, Penang', - requestDate: '2023-06-15', - status: 'pending', - reason: 'Need access for project collaboration' - }, - { - id: 'req2', - userId: 'user1', - userName: 'Aiman Fakhrullah', - cabinetId: 'jkr-kuala-terengganu', - cabinetName: 'JKR Cawangan Kuala Terengganu, Terengganu', - requestDate: '2023-06-16', - status: 'pending', - reason: 'Required for cross-department coordination' - }, - { - id: 'req3', - userId: 'user1', - userName: 'Aiman Fakhrullah', - cabinetId: 'jkr-arkitek', - cabinetName: 'JKR Cawangan Arkitek', - requestDate: '2023-06-17', - status: 'pending', - reason: 'Need architectural plans for current project' - }, - { - id: 'req4', - userId: 'user1', - userName: 'Aiman Fakhrullah', - cabinetId: 'jkr-putrajaya', - cabinetName: 'JKR Cawangan Putrajaya', - requestDate: '2023-06-01', - status: 'rejected', - reason: 'Need access to headquarters documents', - rejectionReason: 'Access restricted to headquarters staff only' - } - ], - - // Selected item - selectedItem: null, - - // View settings - viewMode: 'list', // list, grid, details - sortBy: 'name', - sortDirection: 'asc', - - // User permissions - would be loaded from auth service - userPermissions: { - canCreate: true, - canEdit: true, - canDelete: true, - canManageAccess: true - }, - - // Flags - isLoading: false, - showFileViewer: false, - viewerDocument: null, - searchQuery: '', + // Search state searchResults: [], + searchQuery: '', - // Settings integration + // Upload state + uploadProgress: 0, + currentUploads: [], + + // View state + viewMode: 'list', // list, grid, details + sortBy: 'name', // name, modified, size, type + sortDirection: 'asc', // asc, desc + + // File viewer state + fileViewerOpen: false, + currentDocument: null, + + // System settings systemSettings: { upload: { allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'png'], @@ -521,8 +688,44 @@ export const useDmsStore = defineStore('dms', { }, // Get pending access requests - pendingAccessRequests: (state) => { - return state.accessRequests.filter(req => req.status === 'pending'); + pendingAccessRequests() { + return this.accessRequests.filter(req => req.status === 'pending'); + }, + + approvedAccessRequests() { + return this.accessRequests.filter(req => req.status === 'approved'); + }, + + rejectedAccessRequests() { + return this.accessRequests.filter(req => req.status === 'rejected'); + }, + + overdueAccessRequests() { + const now = new Date(); + return this.accessRequests.filter(req => { + if (req.status !== 'pending') return false; + + const deadline = new Date(req.targetResolutionTime); + return now > deadline; + }); + }, + + // Getter for average response time + averageResponseTime() { + const resolvedRequests = this.accessRequests.filter( + req => req.status === 'approved' || req.status === 'rejected' + ); + + if (resolvedRequests.length === 0) return 0; + + const totalResponseTime = resolvedRequests.reduce((total, req) => { + const requestDate = new Date(req.requestDate); + const responseDate = new Date(req.responseDate); + const responseTimeHours = (responseDate - requestDate) / (1000 * 60 * 60); + return total + responseTimeHours; + }, 0); + + return totalResponseTime / resolvedRequests.length; } }, @@ -1151,46 +1354,55 @@ export const useDmsStore = defineStore('dms', { }, // Access request functionality - async requestAccess(itemId, accessLevel, justification, duration = '7 days') { + async requestAccess(itemId, accessType, justification, duration = '7 days') { this.isLoading = true; try { // Mock API delay - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise(resolve => setTimeout(resolve, 800)); - // Generate a unique request ID - const requestId = `req${Date.now()}`; + // Find the item being requested + const item = this.findItemById(itemId); + + if (!item) { + throw new Error(`Item with id ${itemId} not found`); + } + + // Calculate target resolution time (48 hours SLA) + const requestDate = new Date(); + const targetResolutionTime = new Date(requestDate); + targetResolutionTime.setHours(targetResolutionTime.getHours() + 48); // Create new access request const newRequest = { - id: requestId, - userId: 'current-user-id', // Would come from auth store - userName: 'Current User', // Would come from auth store - itemId: itemId, - accessLevel: accessLevel, - justification: justification, - duration: duration, - requestDate: new Date().toISOString().split('T')[0], - status: 'pending' + id: `req-${Date.now()}`, + documentId: item.id, + documentName: item.name, + documentPath: this.getItemPath(item), + requesterId: this.currentUser.id, + requesterName: this.currentUser.name, + requesterEmail: this.currentUser.email, + requesterDepartment: this.currentUser.department, + approverId: null, + approverName: null, + approverDepartment: null, + requestDate: requestDate.toISOString(), + targetResolutionTime: targetResolutionTime.toISOString(), + responseDate: null, + status: 'pending', + accessType, + accessDuration: duration, + justification, + notes: null }; - // Add to access requests + // Add the request to the store this.accessRequests.push(newRequest); - // Update the item's access request status (for mock data) - // In production, this would be handled server-side - const updateItemStatus = (items, id) => { - for (const item of items) { - if (item.id === id) { - item.accessRequestStatus = 'pending'; - return true; - } - } - return false; - }; - - // Try to find and update the item in the mock data arrays - // This is a simplified approach for demo purposes + // Update the item's access request status if it's a cabinet + if (item.type === 'cabinet' || item.type === 'cabinet-group') { + this.updateCabinetAccessStatus(itemId, 'pending'); + } return newRequest; } catch (error) { @@ -1199,6 +1411,374 @@ export const useDmsStore = defineStore('dms', { } finally { this.isLoading = false; } + }, + + // Get access requests (can filter by user) + async getAccessRequests(userId = null) { + // Simulate API delay for realistic testing + await new Promise(resolve => setTimeout(resolve, 500)); + + if (userId) { + return this.accessRequests.filter(req => req.requesterId === userId); + } + + return this.accessRequests; + }, + + // Get department access requests + async getDepartmentAccessRequests() { + // Simulate API delay for realistic testing + await new Promise(resolve => setTimeout(resolve, 600)); + + return this.departmentAccessRequests; + }, + + // Approve an access request + async approveAccessRequest(requestId, notes = '') { + // Simulate API delay for realistic testing + await new Promise(resolve => setTimeout(resolve, 1000)); + + const requestIndex = this.accessRequests.findIndex(req => req.id === requestId); + + if (requestIndex === -1) { + throw new Error(`Request with id ${requestId} not found`); + } + + // Update the request + const request = this.accessRequests[requestIndex]; + const updatedRequest = { + ...request, + approverId: this.currentUser.id, + approverName: this.currentUser.name, + approverDepartment: this.currentUser.department, + responseDate: new Date().toISOString(), + status: 'approved', + notes + }; + + // Update in store + this.accessRequests[requestIndex] = updatedRequest; + + // Update the item's access status if it's a cabinet + if (request.documentId) { + const item = this.findItemById(request.documentId); + if (item && (item.type === 'cabinet' || item.type === 'cabinet-group')) { + this.updateCabinetAccessStatus(request.documentId, 'approved'); + } + } + + return updatedRequest; + }, + + // Reject an access request + async rejectAccessRequest(requestId, notes = '') { + // Simulate API delay for realistic testing + await new Promise(resolve => setTimeout(resolve, 1000)); + + const requestIndex = this.accessRequests.findIndex(req => req.id === requestId); + + if (requestIndex === -1) { + throw new Error(`Request with id ${requestId} not found`); + } + + // Update the request + const request = this.accessRequests[requestIndex]; + const updatedRequest = { + ...request, + approverId: this.currentUser.id, + approverName: this.currentUser.name, + approverDepartment: this.currentUser.department, + responseDate: new Date().toISOString(), + status: 'rejected', + notes + }; + + // Update in store + this.accessRequests[requestIndex] = updatedRequest; + + // Update the item's access status if it's a cabinet + if (request.documentId) { + const item = this.findItemById(request.documentId); + if (item && (item.type === 'cabinet' || item.type === 'cabinet-group')) { + this.updateCabinetAccessStatus(request.documentId, 'rejected'); + } + } + + return updatedRequest; + }, + + // Get KPI metrics for access requests + async getAccessRequestMetrics(timeRange = '30days', userId = null) { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 700)); + + // Filter requests by time range + const now = new Date(); + let daysToLookBack = 30; + + switch (timeRange) { + case '7days': + daysToLookBack = 7; + break; + case '30days': + daysToLookBack = 30; + break; + case '90days': + daysToLookBack = 90; + break; + case '365days': + daysToLookBack = 365; + break; + } + + const cutoffDate = new Date(now); + cutoffDate.setDate(cutoffDate.getDate() - daysToLookBack); + + let requestsToAnalyze = this.accessRequests; + + // Filter by user if specified + if (userId) { + requestsToAnalyze = requestsToAnalyze.filter(req => req.requesterId === userId); + } + + // Filter by date range + requestsToAnalyze = requestsToAnalyze.filter(req => { + const requestDate = new Date(req.requestDate); + return requestDate >= cutoffDate; + }); + + // Calculate metrics + const totalRequests = requestsToAnalyze.length; + const pendingRequests = requestsToAnalyze.filter(req => req.status === 'pending').length; + const approvedRequests = requestsToAnalyze.filter(req => req.status === 'approved').length; + const rejectedRequests = requestsToAnalyze.filter(req => req.status === 'rejected').length; + + // Calculate response times + const resolvedRequests = requestsToAnalyze.filter( + req => req.status === 'approved' || req.status === 'rejected' + ); + + let totalResponseTime = 0; + let fastestResponse = Infinity; + let slowestResponse = 0; + + resolvedRequests.forEach(req => { + const requestDate = new Date(req.requestDate); + const responseDate = new Date(req.responseDate); + const responseTimeHours = (responseDate - requestDate) / (1000 * 60 * 60); + + totalResponseTime += responseTimeHours; + fastestResponse = Math.min(fastestResponse, responseTimeHours); + slowestResponse = Math.max(slowestResponse, responseTimeHours); + }); + + const avgResponseTime = resolvedRequests.length > 0 + ? totalResponseTime / resolvedRequests.length + : 0; + + // Calculate overdue metrics + const overdueRequests = requestsToAnalyze.filter(req => { + if (req.status !== 'pending') return false; + + const deadline = new Date(req.targetResolutionTime); + return now > deadline; + }).length; + + const overduePercentage = totalRequests > 0 + ? overdueRequests / totalRequests + : 0; + + return { + totalRequests, + pendingRequests, + approvedRequests, + rejectedRequests, + avgResponseTime, + fastestResponse: fastestResponse === Infinity ? 0 : fastestResponse, + slowestResponse, + overdueRequests, + overduePercentage + }; + }, + + // Helper function to get the path for an item + getItemPath(item) { + if (!item.parentPath) return `/${item.name}`; + return `${item.parentPath}/${item.name}`; + }, + + // Find an item by ID + findItemById(id) { + // Look through cabinets recursively + const findInCabinets = (cabinets, targetId) => { + for (const cabinet of cabinets) { + if (cabinet.id === targetId) { + return cabinet; + } + + if (cabinet.children) { + const found = findInCabinets(cabinet.children, targetId); + if (found) return found; + } + } + return null; + }; + + // First check cabinets + const cabinetItem = findInCabinets(this.cabinets, id); + if (cabinetItem) return cabinetItem; + + // Then check regular items + return this.items.find(item => item.id === id); + }, + + // Update cabinet access status + updateCabinetAccessStatus(cabinetId, status) { + const updateCabinet = (cabinets, id, newStatus) => { + return cabinets.map(cabinet => { + if (cabinet.id === id) { + return { + ...cabinet, + accessRequestStatus: newStatus, + hasAccess: newStatus === 'approved', + isLocked: newStatus === 'rejected' + }; + } + + if (cabinet.children) { + return { + ...cabinet, + children: updateCabinet(cabinet.children, id, newStatus) + }; + } + + return cabinet; + }); + }; + + this.cabinets = updateCabinet(this.cabinets, cabinetId, status); + }, + + // Authentik integration placeholder - this would be replaced with actual Authentik API calls + async authenticateWithAuthentik(username, password) { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 800)); + + // This is a placeholder for the actual Authentik integration + // In a real implementation, this would make API calls to Authentik + + if (username === 'admin' && password === 'password') { + return { + user: { + id: 'admin1', + name: 'Admin User', + email: 'admin@example.com', + role: 'admin', + department: 'IT Department' + }, + token: 'sample-authentik-token' + }; + } + + if (username === 'user' && password === 'password') { + return { + user: { + id: 'user1', + name: 'Aiman Fakhrullah', + email: 'aiman@example.com', + role: 'engineer', + department: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu' + }, + token: 'sample-authentik-token' + }; + } + + throw new Error('Authentication failed'); + }, + + // Get RBAC permissions from Authentik + async getRbacPermissions(userId) { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 600)); + + // This is a placeholder for the actual Authentik integration + // In a real implementation, this would fetch RBAC permissions from Authentik + + const permissions = { + 'admin1': { + roles: ['admin'], + 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 + } + } + }, + 'user1': { + roles: ['engineer'], + 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 + } + } + } + }; + + return permissions[userId] || { + roles: [], + permissions: { + documents: { + view: false, + edit: false, + delete: false, + approve: false, + reject: false, + download: false + }, + cabinets: { + view: false, + create: false, + edit: false, + delete: false + }, + accessRequests: { + approve: false, + reject: false, + viewAll: false + } + } + }; } } }); \ No newline at end of file