+
+
+
Requesting access to:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ itemName }}
+
{{ itemPath }}
-
+
{{ formError }}
-
+
-
Access Type
-
+
+
-
-
+
- Access Duration
-
-
-
-
-
-
- Justification
+
+ Justification
*
-
+
+ Minimum 10 characters. Please provide a detailed explanation.
+
-
-
- Your request will be reviewed by the document owner or administrator.
-
+
+
+
+ Access Duration
+
+
+
+ How long do you need access to this resource?
+
+
-
-
- Cancel
-
-
-
- Submit Request
-
+
+
+
+ * Required fields
+
+
+
+
+ Cancel
+
+
+ Submit Request
+
+
diff --git a/components/dms/explorer/DMSExplorer.vue b/components/dms/explorer/DMSExplorer.vue
index 5f8cb9e..03860bc 100644
--- a/components/dms/explorer/DMSExplorer.vue
+++ b/components/dms/explorer/DMSExplorer.vue
@@ -5,6 +5,7 @@ import DMSDocumentViewer from '~/components/dms/viewers/DMSDocumentViewer.vue';
import DMSUploadDialog from '~/components/dms/dialogs/DMSUploadDialog.vue';
import DMSCreateNewDialog from '~/components/dms/dialogs/DMSCreateNewDialog.vue';
import DMSAccessRequestDialog from '~/components/dms/dialogs/DMSAccessRequestDialog.vue';
+import { useDmsStore } from '~/stores/dms';
// Props for the explorer
const props = defineProps({
@@ -36,6 +37,9 @@ const sortOrder = ref('asc'); // asc, desc
const searchQuery = ref('');
const showAccessRequestDialog = ref(false);
const accessRequestItem = ref(null);
+const isRequestingAccess = ref(false);
+const requestSuccess = ref(false);
+const requestError = ref(null);
// Navigation history for back/forward functionality
const navigationHistory = ref([props.initialPath]);
@@ -52,6 +56,7 @@ const currentDocument = ref(null);
// Mock data - replace with actual API calls
const allItems = ref([]);
const router = useRouter();
+const dmsStore = useDmsStore();
// Methods
const loadItems = async () => {
@@ -600,6 +605,39 @@ const closeAccessRequestDialog = () => {
accessRequestItem.value = null;
};
+const handleAccessRequest = async (item) => {
+ if (!item) return;
+
+ accessRequestItem.value = item;
+ showAccessRequestDialog.value = true;
+};
+
+const submitAccessRequest = async (requestData) => {
+ isRequestingAccess.value = true;
+ requestError.value = null;
+
+ try {
+ await dmsStore.requestAccess(
+ accessRequestItem.value.id,
+ requestData.accessType,
+ requestData.justification,
+ requestData.accessDuration
+ );
+
+ requestSuccess.value = true;
+ setTimeout(() => {
+ showAccessRequestDialog.value = false;
+ requestSuccess.value = false;
+ accessRequestItem.value = null;
+ }, 2000);
+ } catch (error) {
+ requestError.value = error.message || 'Failed to submit access request. Please try again.';
+ console.error('Access request error:', error);
+ } finally {
+ isRequestingAccess.value = false;
+ }
+};
+
// Computed properties
const currentItems = computed(() => {
let items = allItems.value.filter(item => item.parentPath === currentPath.value);
@@ -1119,6 +1157,33 @@ const parentType = computed(() => {
+
+
+
+
+
Restricted Access
+
+
+ Request Pending
+
+
+
@@ -1345,21 +1410,60 @@ const parentType = computed(() => {
/>
-
-
+
+
+
+
+
+
+
Access request submitted successfully!
+
+
+
+
+
+
+
+
+
+
{{ requestError }}
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
Access Request Metrics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Personal Metrics
+
Your access request performance
+
+
+
+
+
+
+
{{ personalMetrics.totalRequests }}
+
Total Requests
+
+
+
+
{{ personalMetrics.pendingRequests }}
+
Pending
+
+
+
+
{{ personalMetrics.approvedRequests }}
+
Approved
+
+
+
+
{{ personalMetrics.rejectedRequests }}
+
Rejected
+
+
+
+
+
Response Time Metrics
+
+
+
{{ formatDuration(personalMetrics.avgResponseTime) }}
+
Average Response
+
+
+
+
{{ formatDuration(personalMetrics.fastestResponse) }}
+
Fastest Response
+
+
+
+
{{ formatDuration(personalMetrics.slowestResponse) }}
+
Slowest Response
+
+
+
+
+
Overdue Metrics
+
+
+
{{ personalMetrics.overdueRequests }}
+
Overdue Requests
+
+
+
+
{{ formatPercentage(personalMetrics.overduePercentage) }}
+
Overdue Percentage
+
+
+
+
+
+
+
+
+
Department Metrics
+
Department-wide performance
+
+
+
+
+
+
+
{{ departmentMetrics.totalRequests }}
+
Total Requests
+
+
+
+
{{ departmentMetrics.pendingRequests }}
+
Pending
+
+
+
+
{{ departmentMetrics.approvedRequests }}
+
Approved
+
+
+
+
{{ departmentMetrics.rejectedRequests }}
+
Rejected
+
+
+
+
+
Response Time Metrics
+
+
+
{{ formatDuration(departmentMetrics.avgResponseTime) }}
+
Average Response
+
+
+
+
{{ formatDuration(departmentMetrics.fastestResponse) }}
+
Fastest Response
+
+
+
+
{{ formatDuration(departmentMetrics.slowestResponse) }}
+
Slowest Response
+
+
+
+
+
Overdue Metrics
+
+
+
{{ departmentMetrics.overdueRequests }}
+
Overdue Requests
+
+
+
+
{{ formatPercentage(departmentMetrics.overduePercentage) }}
+
Overdue Percentage
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
Access Requests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading access requests...
+
+
+
+
+
+
+
No access requests found
+
There are no access requests matching your current filters.
+
+ View all requests
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ Status
+ |
+
+ Actions
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ request.documentName }}
+
+
+ {{ request.documentPath }}
+
+
+
+ |
+
+
+
+
+ {{ request.requesterName }}
+
+
+ {{ request.requesterDepartment }}
+
+ |
+
+
+
+
+ {{ formatDate(request.requestDate) }}
+
+
+ {{ formatTimeAgo(request.requestDate) }}
+
+ |
+
+
+
+
+ {{ formatDate(request.targetResolutionTime) }}
+
+
+ {{ getDeadlineStatus(request).text }}
+
+ |
+
+
+
+
+ {{ request.status.charAt(0).toUpperCase() + request.status.slice(1) }}
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading access management...
+
+
+
+
+
+
+
⚠️
+
Error Loading Access Management
+
{{ errorMessage }}
+
+ Retry
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ dmsStore.currentUser.name }}
+
{{ dmsStore.currentUser.department }}
+
+
+
+
+
+ Approver
+
+
+
+ Administrator
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tabs.find(t => t.id === activeTab)?.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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();
+});
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading role management...
+
+
+
+
+
+
+
⚠️
+
Error Loading Data
+
{{ errorMessage }}
+
+ Retry
+
+
+
+
+
+
+
+
+
+
+
+
+
Authentik Integration
+
Role-Based Access Control Management
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ activeTab === 'users' ? 'Add User' : 'Add Role' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User
+ |
+
+ Department
+ |
+
+ Roles
+ |
+
+ Last Login
+ |
+
+ Status
+ |
+
+ Actions
+ |
+
+
+
+
+
+
+
+ {{ user.name.charAt(0) }}
+
+
+
+ {{ user.name }}
+
+
+ {{ user.email }}
+
+
+
+ |
+
+ {{ user.department }}
+ |
+
+
+
+ {{ role }}
+
+
+ |
+
+ {{ formatDateTime(user.lastLogin) }}
+ |
+
+
+ {{ user.status }}
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ role.name }}
+
{{ role.description }}
+
+
+
+
+
+
+
+
+
+
Document Permissions
+
+
+
+ {{ key }}
+
+
+
+
+
+
+
Cabinet Permissions
+
+
+
+ {{ key }}
+
+
+
+
+
+
+
Access Request Permissions
+
+
+
+ {{ key }}
+
+
+
+
+
+
+ {{ role.userCount }} {{ role.userCount === 1 ? 'user' : 'users' }} with this role
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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