diff --git a/components/dms/CabinetNavigation.vue b/components/dms/CabinetNavigation.vue new file mode 100644 index 0000000..b2ed6f7 --- /dev/null +++ b/components/dms/CabinetNavigation.vue @@ -0,0 +1,558 @@ + + + + + \ No newline at end of file diff --git a/components/dms/dialogs/DMSAccessRequestDialog.vue b/components/dms/dialogs/DMSAccessRequestDialog.vue new file mode 100644 index 0000000..69ba659 --- /dev/null +++ b/components/dms/dialogs/DMSAccessRequestDialog.vue @@ -0,0 +1,186 @@ + + + \ No newline at end of file diff --git a/components/dms/explorer/DMSExplorer.vue b/components/dms/explorer/DMSExplorer.vue new file mode 100644 index 0000000..31699dc --- /dev/null +++ b/components/dms/explorer/DMSExplorer.vue @@ -0,0 +1,445 @@ + + + + + \ No newline at end of file diff --git a/components/dms/explorer/DMSTreeView.vue b/components/dms/explorer/DMSTreeView.vue new file mode 100644 index 0000000..1463905 --- /dev/null +++ b/components/dms/explorer/DMSTreeView.vue @@ -0,0 +1,146 @@ + + + + + \ No newline at end of file diff --git a/components/dms/navigation/DMSNavigation.vue b/components/dms/navigation/DMSNavigation.vue new file mode 100644 index 0000000..0c47581 --- /dev/null +++ b/components/dms/navigation/DMSNavigation.vue @@ -0,0 +1,386 @@ + + + + + \ No newline at end of file diff --git a/components/dms/viewers/DMSDocumentViewer.vue b/components/dms/viewers/DMSDocumentViewer.vue new file mode 100644 index 0000000..da1db83 --- /dev/null +++ b/components/dms/viewers/DMSDocumentViewer.vue @@ -0,0 +1,265 @@ + + + + + \ No newline at end of file diff --git a/navigation/index.js b/navigation/index.js index ed66b1c..307749d 100644 --- a/navigation/index.js +++ b/navigation/index.js @@ -9,6 +9,33 @@ export default [ "icon": "ic:outline-dashboard", "child": [], "meta": {} + }, + + ], + "meta": {} + }, + { + "header": "DMS", + "description": "Document Management System", + "child": [ + { + "title": "Document Management", + "path": "/dms", + "icon": "ic:outline-folder", + "child": [], + "meta": {} + }, + { + "title": "Access Requests", + "path": "/dms/access-requests", + "icon": "ic:outline-security", + "child": [] + }, + { + "title": "Settings", + "path": "/dms/settings", + "icon": "ic:outline-settings", + "child": [] } ], "meta": {} diff --git a/pages/dms/document-properties.vue b/pages/dms/document-properties.vue new file mode 100644 index 0000000..84a43dc --- /dev/null +++ b/pages/dms/document-properties.vue @@ -0,0 +1,364 @@ + + + + + \ No newline at end of file diff --git a/pages/dms/index.vue b/pages/dms/index.vue new file mode 100644 index 0000000..3aa9f57 --- /dev/null +++ b/pages/dms/index.vue @@ -0,0 +1,465 @@ + + + + + \ No newline at end of file diff --git a/pages/dms/my-documents.vue b/pages/dms/my-documents.vue new file mode 100644 index 0000000..a7b25f4 --- /dev/null +++ b/pages/dms/my-documents.vue @@ -0,0 +1,371 @@ + + + + + \ No newline at end of file diff --git a/pages/dms/upload-document.vue b/pages/dms/upload-document.vue new file mode 100644 index 0000000..ecd62ca --- /dev/null +++ b/pages/dms/upload-document.vue @@ -0,0 +1,444 @@ + + + + + \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 34d2c01..4d234d3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,27 +29,35 @@ model lookup { } model role { - roleID Int @id @default(autoincrement()) - roleName String? @db.VarChar(255) - roleDescription String? @db.VarChar(255) - roleStatus String? @db.VarChar(255) - roleCreatedDate DateTime? @db.DateTime(0) - roleModifiedDate DateTime? @db.DateTime(0) + roleID Int @id @default(autoincrement()) + roleName String? @db.VarChar(255) + roleDescription String? @db.VarChar(255) + roleStatus String? @db.VarChar(255) + roleCreatedDate DateTime? @db.DateTime(0) + roleModifiedDate DateTime? @db.DateTime(0) userrole userrole[] + permissions AccessPermission[] } model user { - userID Int @id @default(autoincrement()) - userSecretKey String? @db.VarChar(255) - userUsername String? @db.VarChar(255) - userPassword String? @db.VarChar(255) - userFullName String? @db.VarChar(255) - userEmail String? @db.VarChar(255) - userPhone String? @db.VarChar(255) - userStatus String? @db.VarChar(255) - userCreatedDate DateTime? @db.DateTime(0) - userModifiedDate DateTime? @db.DateTime(0) + userID Int @id @default(autoincrement()) + userSecretKey String? @db.VarChar(255) + userUsername String? @db.VarChar(255) + userPassword String? @db.VarChar(255) + userFullName String? @db.VarChar(255) + userEmail String? @db.VarChar(255) + userPhone String? @db.VarChar(255) + userStatus String? @db.VarChar(255) + userCreatedDate DateTime? @db.DateTime(0) + userModifiedDate DateTime? @db.DateTime(0) userrole userrole[] + accessRequests AccessRequest[] + permissions AccessPermission[] + documents Document[] @relation("DocumentCreator") + cabinets Cabinet[] @relation("CabinetCreator") + drawers Drawer[] @relation("DrawerCreator") + folders Folder[] @relation("FolderCreator") + subfolders Subfolder[] @relation("SubfolderCreator") } model userrole { @@ -99,3 +107,164 @@ model site_settings { settingModifiedDate DateTime? @db.DateTime(0) siteLoginLogo String? @db.VarChar(500) } + +// DMS Models + +model Cabinet { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + description String? @db.Text + createdAt DateTime @default(now()) @db.DateTime(0) + updatedAt DateTime @updatedAt @db.DateTime(0) + createdBy Int + status String @default("active") @db.VarChar(50) + user user @relation("CabinetCreator", fields: [createdBy], references: [userID]) + drawers Drawer[] + permissions AccessPermission[] + + @@index([createdBy]) +} + +model Drawer { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + description String? @db.Text + cabinetId Int + createdAt DateTime @default(now()) @db.DateTime(0) + updatedAt DateTime @updatedAt @db.DateTime(0) + createdBy Int + status String @default("active") @db.VarChar(50) + cabinet Cabinet @relation(fields: [cabinetId], references: [id], onDelete: Cascade) + user user @relation("DrawerCreator", fields: [createdBy], references: [userID]) + folders Folder[] + permissions AccessPermission[] + + @@index([cabinetId]) + @@index([createdBy]) +} + +model Folder { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + description String? @db.Text + drawerId Int + createdAt DateTime @default(now()) @db.DateTime(0) + updatedAt DateTime @updatedAt @db.DateTime(0) + createdBy Int + status String @default("active") @db.VarChar(50) + drawer Drawer @relation(fields: [drawerId], references: [id], onDelete: Cascade) + user user @relation("FolderCreator", fields: [createdBy], references: [userID]) + subfolders Subfolder[] + documents Document[] @relation("FolderDocuments") + permissions AccessPermission[] + + @@index([drawerId]) + @@index([createdBy]) +} + +model Subfolder { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + description String? @db.Text + folderId Int + createdAt DateTime @default(now()) @db.DateTime(0) + updatedAt DateTime @updatedAt @db.DateTime(0) + createdBy Int + status String @default("active") @db.VarChar(50) + folder Folder @relation(fields: [folderId], references: [id], onDelete: Cascade) + user user @relation("SubfolderCreator", fields: [createdBy], references: [userID]) + documents Document[] @relation("SubfolderDocuments") + permissions AccessPermission[] + + @@index([folderId]) + @@index([createdBy]) +} + +model Document { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + description String? @db.Text + fileSize Int @default(0) + fileType String @db.VarChar(100) + fileExtension String @db.VarChar(20) + filePath String @db.VarChar(500) + version Int @default(1) + isTemplate Boolean @default(false) + isPublic Boolean @default(false) + folderId Int? + subfolderId Int? + createdAt DateTime @default(now()) @db.DateTime(0) + updatedAt DateTime @updatedAt @db.DateTime(0) + createdBy Int + status String @default("active") @db.VarChar(50) + folder Folder? @relation("FolderDocuments", fields: [folderId], references: [id], onDelete: SetNull) + subfolder Subfolder? @relation("SubfolderDocuments", fields: [subfolderId], references: [id], onDelete: SetNull) + user user @relation("DocumentCreator", fields: [createdBy], references: [userID]) + accessRequests AccessRequest[] + permissions AccessPermission[] + versions DocumentVersion[] + + @@index([folderId]) + @@index([subfolderId]) + @@index([createdBy]) +} + +model DocumentVersion { + id Int @id @default(autoincrement()) + documentId Int + version Int + filePath String @db.VarChar(500) + fileSize Int @default(0) + createdAt DateTime @default(now()) @db.DateTime(0) + createdBy Int + document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + + @@index([documentId]) +} + +model AccessRequest { + id Int @id @default(autoincrement()) + documentId Int + userId Int + requestedLevel String @db.VarChar(50) // view, download, print, edit, full + justification String? @db.Text + status String @default("pending") @db.VarChar(50) // pending, approved, rejected + responseNote String? @db.Text + requestedAt DateTime @default(now()) @db.DateTime(0) + respondedAt DateTime? @db.DateTime(0) + document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + user user @relation(fields: [userId], references: [userID]) + + @@index([documentId]) + @@index([userId]) +} + +model AccessPermission { + id Int @id @default(autoincrement()) + userId Int? + roleId Int? + documentId Int? + cabinetId Int? + drawerId Int? + folderId Int? + subfolderId Int? + permissionLevel String @db.VarChar(50) // view, download, print, edit, full + createdAt DateTime @default(now()) @db.DateTime(0) + updatedAt DateTime @updatedAt @db.DateTime(0) + expiresAt DateTime? @db.DateTime(0) + user user? @relation(fields: [userId], references: [userID], onDelete: SetNull) + role role? @relation(fields: [roleId], references: [roleID], onDelete: SetNull) + document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) + cabinet Cabinet? @relation(fields: [cabinetId], references: [id], onDelete: Cascade) + drawer Drawer? @relation(fields: [drawerId], references: [id], onDelete: Cascade) + folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade) + subfolder Subfolder? @relation(fields: [subfolderId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([roleId]) + @@index([documentId]) + @@index([cabinetId]) + @@index([drawerId]) + @@index([folderId]) + @@index([subfolderId]) +} diff --git a/stores/dms.js b/stores/dms.js new file mode 100644 index 0000000..b532ad8 --- /dev/null +++ b/stores/dms.js @@ -0,0 +1,740 @@ +import { defineStore } from 'pinia'; + +export const useDmsStore = defineStore('dms', { + state: () => ({ + // Navigation state + currentPath: '/', + pathHistory: ['/'], + historyIndex: 0, + + // User information + currentUser: { + id: 'user1', + name: 'Aiman Fakhrullah', + email: 'aiman@example.com', + role: 'engineer', // engineer, admin, manager, etc. + department: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu' + }, + + // Cabinet access types + cabinetAccessTypes: [ + { id: 'public', name: 'Public Access', icon: 'check-circle', color: 'green' }, + { id: 'personal', name: 'Personal Access', icon: 'circle-check', color: 'orange' }, + { id: 'private', name: 'Private Access', icon: 'lock', color: 'red' } + ], + + // Access request statuses + accessRequestStatuses: [ + { id: 'pending', name: 'Pending', color: 'orange' }, + { id: 'approved', name: 'Approved', color: 'green' }, + { id: 'rejected', name: 'Rejected', color: 'red' } + ], + + // Items state - in production this would be loaded from API + cabinets: [ + { + id: 'public-cabinet', + name: 'Public Cabinet', + type: 'cabinet', + accessType: 'public', + children: [ + { + id: 'public-cabinet-1', + name: 'Public Cabinet', + type: 'cabinet', + accessType: 'public', + parentId: 'public-cabinet' + } + ] + }, + { + id: 'my-cabinets', + name: 'My Cabinets', + type: 'cabinet-group', + accessType: 'personal', + children: [ + { + id: 'jkr-tebedu', + name: 'JKR Cawangan Tebedu, Sarawak', + type: 'cabinet', + accessType: 'personal', + parentId: 'my-cabinets', + hasAccess: true + }, + { + id: 'jkr-kota-bharu', + name: 'JKR Cawangan Kota Bharu, Kelantan', + type: 'cabinet', + accessType: 'personal', + parentId: 'my-cabinets', + hasAccess: true, + children: [ + { + id: 'jkr-kewangan-kb', + name: 'JKR Bahagian Kewangan Cawangan Kota Bharu', + type: 'cabinet', + accessType: 'personal', + parentId: 'jkr-kota-bharu', + hasAccess: true + }, + { + id: 'jkr-kejuruteraan-kb', + name: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu', + type: 'cabinet', + accessType: 'personal', + parentId: 'jkr-kota-bharu', + hasAccess: true, + children: [ + { + id: 'pembinaan-jambatan-kb', + name: 'Pembinaan Jambatan Kota Bharu', + type: 'cabinet', + accessType: 'personal', + parentId: 'jkr-kejuruteraan-kb', + hasAccess: true, + children: [ + { + id: 'kewangan-tag', + name: 'Kewangan', + type: 'tag', + color: 'purple', + parentId: 'pembinaan-jambatan-kb' + }, + { + id: 'kejuruteraan-awam-tag', + name: 'Kejuruteraan Awam', + type: 'tag', + color: 'purple', + parentId: 'pembinaan-jambatan-kb' + }, + { + id: 'teknologi-maklumat-tag', + name: 'Teknologi Maklumat', + type: 'tag', + color: 'purple', + parentId: 'pembinaan-jambatan-kb' + } + ] + }, + { + id: 'projek-jalan-raya-kb', + name: 'Projek Jalan Raya Kota Bharu', + type: 'cabinet', + accessType: 'personal', + parentId: 'jkr-kejuruteraan-kb', + hasAccess: true + } + ] + }, + { + id: 'jkr-teknologi-kb', + name: 'JKR Bahagian Teknologi Maklumat Cawangan Kota Bharu', + type: 'cabinet', + accessType: 'personal', + parentId: 'jkr-kota-bharu', + hasAccess: true + } + ] + }, + { + id: 'jkr-ipoh', + name: 'JKR Cawangan Ipoh, Perak', + type: 'cabinet', + accessType: 'personal', + parentId: 'my-cabinets', + hasAccess: true + } + ] + }, + { + id: 'private-cabinets', + name: 'Private Cabinets', + type: 'cabinet-group', + accessType: 'private', + children: [ + { + id: 'jkr-batu-kawan', + name: 'JKR Cawangan Batu Kawan, Penang', + type: 'cabinet', + accessType: 'private', + parentId: 'private-cabinets', + hasAccess: false, + accessRequestStatus: 'pending' + }, + { + id: 'jkr-kuala-terengganu', + name: 'JKR Cawangan Kuala Terengganu, Terengganu', + type: 'cabinet', + accessType: 'private', + parentId: 'private-cabinets', + hasAccess: false, + accessRequestStatus: 'pending' + }, + { + id: 'jkr-arkitek', + name: 'JKR Cawangan Arkitek', + type: 'cabinet', + accessType: 'private', + parentId: 'private-cabinets', + hasAccess: false, + accessRequestStatus: 'pending' + }, + { + id: 'jkr-putrajaya', + name: 'JKR Cawangan Putrajaya', + type: 'cabinet', + accessType: 'private', + parentId: 'private-cabinets', + hasAccess: false, + accessRequestStatus: 'rejected', + isLocked: true + } + ] + } + ], + + // Files within cabinets + items: [ + { + id: 'file1', + name: 'Pembangunan_Sistem_IT_2021.pdf', + type: 'file', + extension: 'pdf', + size: '4MB', + modified: '2021-05-20', + cabinetId: 'jkr-kota-bharu', + accessType: 'personal', + status: 'locked', + info: { + title: 'Projek Pembangunan Sistem IT', + subject: 'Dokumen spesifikasi sistem', + state: 'Kelantan', + date: '2021-05-20', + user: 'Mohd Faizal bin Abdullah', + storeDate: '2021-05-25' + } + }, + { + id: 'file2', + name: 'Projek_Jalan_Raya_Kota_Bharu.pdf', + type: 'file', + extension: 'pdf', + size: '5MB', + modified: '2021-06-15', + cabinetId: 'jkr-kejuruteraan-kb', + accessType: 'personal', + status: 'unlocked' + }, + { + id: 'file3', + name: 'Anggaran_Kos_Projek_MRT3.xlsx', + type: 'file', + extension: 'xlsx', + size: '3MB', + modified: '2021-07-10', + cabinetId: 'pembinaan-jambatan-kb', + accessType: 'personal', + status: 'locked' + }, + { + id: 'file4', + name: 'EIA_Empangan_Nenggiri.pdf', + type: 'file', + extension: 'pdf', + size: '15MB', + modified: '2021-04-18', + cabinetId: 'jkr-ipoh', + accessType: 'personal', + status: 'locked' + }, + { + id: 'file5', + name: 'Borang_Maklumabalas.xlsx', + type: 'file', + extension: 'xlsx', + size: '34.1 KB', + modified: '2025-01-27', + cabinetId: 'public-cabinet-1', + accessType: 'public', + createdBy: 'aimantasan', + status: 'unlocked', + info: { + authors: 'aimantasan', + lastSavedBy: 'aimantasan', + dateAccessed: '2025-05-28', + dateModified: '2025-01-27', + contentCreated: '2025-01-27' + } + } + ], + + // 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: '', + searchResults: [] + }), + + getters: { + // Get items at the current cabinet + currentCabinetItems: (state) => { + const currentCabinetId = state.currentPath.split('/').filter(Boolean).pop(); + return state.items.filter(item => item.cabinetId === currentCabinetId); + }, + + // Get breadcrumbs for current path + breadcrumbs: (state) => { + if (state.currentPath === '/') return [{ name: 'Home', path: '/' }]; + + const paths = state.currentPath.split('/').filter(Boolean); + let breadcrumbPath = ''; + + return [ + { name: 'Home', path: '/' }, + ...paths.map(segment => { + breadcrumbPath += `/${segment}`; + + // Find the actual cabinet name + const findCabinetName = (cabinets, id) => { + for (const cabinet of cabinets) { + if (cabinet.id === id) return cabinet.name; + if (cabinet.children) { + const name = findCabinetName(cabinet.children, id); + if (name) return name; + } + } + return null; + }; + + const name = findCabinetName(state.cabinets, segment) || segment; + + return { + name: name, + path: breadcrumbPath + }; + }) + ]; + }, + + // Get public cabinets + publicCabinets: (state) => { + return state.cabinets.filter(cabinet => cabinet.accessType === 'public'); + }, + + // Get personal cabinets (ones the user has access to) + personalCabinets: (state) => { + return state.cabinets.find(cabinet => cabinet.id === 'my-cabinets') || null; + }, + + // Get private cabinets + privateCabinets: (state) => { + return state.cabinets.find(cabinet => cabinet.id === 'private-cabinets') || null; + }, + + // Check if we can navigate back + canGoBack: (state) => { + return state.historyIndex > 0; + }, + + // Check if we can navigate forward + canGoForward: (state) => { + return state.historyIndex < state.pathHistory.length - 1; + }, + + // Filter items by access level + publicDocuments: (state) => { + return state.items.filter(item => item.accessType === 'public'); + }, + + personalDocuments: (state) => { + return state.items.filter(item => item.accessType === 'personal'); + }, + + privateDocuments: (state) => { + return state.items.filter(item => item.accessType === 'private'); + }, + + // Get pending access requests + pendingAccessRequests: (state) => { + return state.accessRequests.filter(req => req.status === 'pending'); + } + }, + + actions: { + // Navigation actions + navigateTo(path) { + // Add to history if it's a new path + if (path !== this.currentPath) { + // If we navigated back and then to a new path, truncate the forward history + if (this.historyIndex < this.pathHistory.length - 1) { + this.pathHistory = this.pathHistory.slice(0, this.historyIndex + 1); + } + + this.pathHistory.push(path); + this.historyIndex = this.pathHistory.length - 1; + } + + this.currentPath = path; + this.selectedItem = null; + }, + + navigateBack() { + if (this.canGoBack) { + this.historyIndex--; + this.currentPath = this.pathHistory[this.historyIndex]; + this.selectedItem = null; + } + }, + + navigateForward() { + if (this.canGoForward) { + this.historyIndex++; + this.currentPath = this.pathHistory[this.historyIndex]; + this.selectedItem = null; + } + }, + + selectItem(item) { + this.selectedItem = item; + }, + + // View actions + setViewMode(mode) { + this.viewMode = mode; + }, + + setSortBy(field) { + if (this.sortBy === field) { + // Toggle direction if clicking the same field + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortBy = field; + this.sortDirection = 'asc'; + } + }, + + // File viewer actions + openFileViewer(document) { + this.viewerDocument = document; + this.showFileViewer = true; + }, + + closeFileViewer() { + this.showFileViewer = false; + this.viewerDocument = null; + }, + + // Cabinet access actions + async requestCabinetAccess(cabinetId, reason) { + this.isLoading = true; + + try { + // Find the cabinet + let cabinetName = ''; + const findCabinet = (cabinets, id) => { + for (const cabinet of cabinets) { + if (cabinet.id === id) { + cabinetName = cabinet.name; + return cabinet; + } + if (cabinet.children) { + const result = findCabinet(cabinet.children, id); + if (result) return result; + } + } + return null; + }; + + const cabinet = findCabinet(this.cabinets, cabinetId); + if (!cabinet) throw new Error('Cabinet not found'); + + // Mock API delay + await new Promise(resolve => setTimeout(resolve, 500)); + + // Create new access request + const newRequest = { + id: `req${Date.now()}`, + userId: this.currentUser.id, + userName: this.currentUser.name, + cabinetId: cabinetId, + cabinetName: cabinetName, + requestDate: new Date().toISOString().split('T')[0], + status: 'pending', + reason: reason + }; + + // Add to access requests + this.accessRequests.push(newRequest); + + // Update cabinet request status + cabinet.accessRequestStatus = 'pending'; + + return newRequest; + } catch (error) { + console.error('Failed to request access:', error); + throw error; + } finally { + this.isLoading = false; + } + }, + + async approveAccessRequest(requestId) { + this.isLoading = true; + + try { + // Mock API delay + await new Promise(resolve => setTimeout(resolve, 500)); + + // Find the request + const request = this.accessRequests.find(req => req.id === requestId); + if (!request) throw new Error('Request not found'); + + // Update request status + request.status = 'approved'; + + // Find the cabinet and update access + const updateCabinetAccess = (cabinets, id) => { + for (const cabinet of cabinets) { + if (cabinet.id === id) { + cabinet.hasAccess = true; + cabinet.accessRequestStatus = 'approved'; + return true; + } + if (cabinet.children) { + if (updateCabinetAccess(cabinet.children, id)) return true; + } + } + return false; + }; + + updateCabinetAccess(this.cabinets, request.cabinetId); + + return request; + } catch (error) { + console.error('Failed to approve access request:', error); + throw error; + } finally { + this.isLoading = false; + } + }, + + async rejectAccessRequest(requestId, reason) { + this.isLoading = true; + + try { + // Mock API delay + await new Promise(resolve => setTimeout(resolve, 500)); + + // Find the request + const request = this.accessRequests.find(req => req.id === requestId); + if (!request) throw new Error('Request not found'); + + // Update request status + request.status = 'rejected'; + request.rejectionReason = reason; + + // Find the cabinet and update access + const updateCabinetAccess = (cabinets, id) => { + for (const cabinet of cabinets) { + if (cabinet.id === id) { + cabinet.hasAccess = false; + cabinet.accessRequestStatus = 'rejected'; + return true; + } + if (cabinet.children) { + if (updateCabinetAccess(cabinet.children, id)) return true; + } + } + return false; + }; + + updateCabinetAccess(this.cabinets, request.cabinetId); + + return request; + } catch (error) { + console.error('Failed to reject access request:', error); + throw error; + } finally { + this.isLoading = false; + } + }, + + // CRUD operations + async createItem(item) { + this.isLoading = true; + + try { + // Mock API delay + await new Promise(resolve => setTimeout(resolve, 500)); + + // Get current cabinet id + const currentCabinetId = this.currentPath.split('/').filter(Boolean).pop(); + + // Generate a unique ID + const newItem = { + ...item, + id: `${item.type}${Date.now()}`, + cabinetId: currentCabinetId, + modified: new Date().toISOString().split('T')[0], + createdBy: this.currentUser.name + }; + + // Add to items + this.items.push(newItem); + + return newItem; + } catch (error) { + console.error('Failed to create item:', error); + throw error; + } finally { + this.isLoading = false; + } + }, + + async updateItem(id, updates) { + this.isLoading = true; + + try { + // Mock API delay + await new Promise(resolve => setTimeout(resolve, 500)); + + // Find the item + const itemIndex = this.items.findIndex(item => item.id === id); + if (itemIndex === -1) throw new Error('Item not found'); + + // Update the item + const updatedItem = { + ...this.items[itemIndex], + ...updates, + modified: new Date().toISOString().split('T')[0] + }; + + this.items.splice(itemIndex, 1, updatedItem); + + return updatedItem; + } catch (error) { + console.error('Failed to update item:', error); + throw error; + } finally { + this.isLoading = false; + } + }, + + async deleteItem(id) { + this.isLoading = true; + + try { + // Mock API delay + await new Promise(resolve => setTimeout(resolve, 500)); + + // Find the item + const itemIndex = this.items.findIndex(item => item.id === id); + if (itemIndex === -1) throw new Error('Item not found'); + + // Remove the item + const deletedItem = this.items.splice(itemIndex, 1)[0]; + + return deletedItem; + } catch (error) { + console.error('Failed to delete item:', error); + throw error; + } finally { + this.isLoading = false; + } + }, + + // Search functionality + async searchDocuments(query) { + this.isLoading = true; + this.searchQuery = query; + + try { + // Mock API delay + await new Promise(resolve => setTimeout(resolve, 500)); + + // Simple search implementation - in production would be more sophisticated + const results = this.items.filter(item => + item.type === 'file' && + (item.name.toLowerCase().includes(query.toLowerCase()) || + (item.info?.title && item.info.title.toLowerCase().includes(query.toLowerCase())) || + (item.info?.subject && item.info.subject.toLowerCase().includes(query.toLowerCase()))) + ); + + this.searchResults = results; + return results; + } catch (error) { + console.error('Failed to search documents:', error); + throw error; + } finally { + this.isLoading = false; + } + }, + + clearSearch() { + this.searchQuery = ''; + this.searchResults = []; + } + } +}); \ No newline at end of file