import { defineStore } from 'pinia'; export const useDmsStore = defineStore('dms', { state: () => ({ // Navigation state currentPath: '/', pathHistory: ['/'], historyIndex: 0, // Loading and error states isLoading: false, error: null, requestTimeouts: new Map(), // Track active requests for cleanup // User information currentUser: { id: 'user1', name: 'Aiman Fakhrullah', email: 'aiman@example.com', role: 'user', // Role can be 'superadmin', 'admin', or 'user' department: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu' }, // System roles systemRoles: [ { id: 'superadmin', name: 'Super Administrator', description: 'Full system access with ability to manage all settings, users, and content', color: 'purple' }, { id: 'admin', name: 'Administrator', description: 'Administrative access to manage content and some system settings', color: 'blue' }, { id: 'user', name: 'User', description: 'Standard user access for viewing and interacting with content based on permissions', color: 'green' } ], // 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' } ], // 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: [ { 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' } } ], // Search state searchResults: [], searchQuery: '', // 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'], blockedFileTypes: ['exe', 'bat', 'cmd'], maxFileSize: 100 * 1024 * 1024, // 100MB in bytes quotas: { perUser: 5000 * 1024 * 1024, // 5GB perGroup: 50000 * 1024 * 1024, // 50GB perProject: 100000 * 1024 * 1024 // 100GB } }, metadata: { customFields: [ { name: 'Department', type: 'dropdown', required: true, options: ['HR', 'Finance', 'IT', 'Legal'] }, { name: 'Priority', type: 'select', required: false, options: ['Low', 'Medium', 'High', 'Critical'] }, { name: 'Project Code', type: 'text', required: true }, { name: 'Review Date', type: 'date', required: false } ], predefinedTags: ['urgent', 'confidential', 'public', 'draft', 'final'], userGeneratedTags: true, tagSuggestions: true }, documents: { versionControl: { enabled: true, maxVersions: 10, autoVersioning: true }, namingConventions: { autoGenerate: true, pattern: '{department}_{title}_{date}' } }, access: { rbacEnabled: true, permissions: { view: true, edit: true, delete: false, download: true, share: true } } }, // Enhanced user permissions with detailed tracking userQuotas: { used: 1500 * 1024 * 1024, // 1.5GB used limit: 5000 * 1024 * 1024, // 5GB limit documents: 450, maxDocuments: 1000 }, // Document metadata templates metadataTemplates: { standard: { title: '', description: '', department: '', author: '', tags: [], priority: 'Medium', reviewDate: null, projectCode: '' }, contract: { title: '', description: '', department: 'Legal', contractType: '', vendor: '', startDate: null, endDate: null, value: '', tags: ['contract'], priority: 'High' }, report: { title: '', description: '', department: '', reportType: '', period: '', author: '', tags: ['report'], priority: 'Medium' } }, // Version tracking documentVersions: {}, // Enhanced search with metadata advancedSearchFilters: { fileType: [], department: [], tags: [], dateRange: { start: null, end: null }, author: [], priority: [], hasVersions: false } }), 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() { 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; } }, 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; }, // Enhanced file validation based on settings validateFile(file) { const errors = []; const settings = this.systemSettings.upload; // Check file type const fileExtension = file.name.split('.').pop().toLowerCase(); if (!settings.allowedFileTypes.includes(fileExtension)) { errors.push(`File type .${fileExtension} is not allowed`); } if (settings.blockedFileTypes.includes(fileExtension)) { errors.push(`File type .${fileExtension} is blocked`); } // Check file size if (file.size > settings.maxFileSize) { const maxSizeMB = settings.maxFileSize / (1024 * 1024); errors.push(`File size exceeds limit of ${maxSizeMB}MB`); } // Check user quota if (this.userQuotas.used + file.size > this.userQuotas.limit) { errors.push('Upload would exceed your storage quota'); } return { isValid: errors.length === 0, errors }; }, // Enhanced metadata validation validateMetadata(metadata, template = 'standard') { const errors = []; const templateConfig = this.metadataTemplates[template]; const customFields = this.systemSettings.metadata.customFields; // Validate required custom fields customFields.forEach(field => { if (field.required && (!metadata[field.name] || metadata[field.name].toString().trim() === '')) { errors.push(`${field.name} is required`); } // Validate field options for dropdown/select types if (field.options && metadata[field.name] && !field.options.includes(metadata[field.name])) { errors.push(`Invalid value for ${field.name}`); } }); // Validate document naming convention if (this.systemSettings.documents.namingConventions.autoGenerate) { const pattern = this.systemSettings.documents.namingConventions.pattern; const requiredFields = pattern.match(/{(\w+)}/g)?.map(match => match.slice(1, -1)) || []; requiredFields.forEach(field => { if (!metadata[field]) { errors.push(`${field} is required for auto-generated naming`); } }); } return { isValid: errors.length === 0, errors }; }, // Generate document name based on naming convention generateDocumentName(metadata, originalFileName) { if (!this.systemSettings.documents.namingConventions.autoGenerate) { return originalFileName; } let pattern = this.systemSettings.documents.namingConventions.pattern; const date = new Date().toISOString().split('T')[0]; // Replace placeholders pattern = pattern.replace(/{department}/g, metadata.department || 'unknown'); pattern = pattern.replace(/{title}/g, metadata.title || 'document'); pattern = pattern.replace(/{date}/g, date); pattern = pattern.replace(/{author}/g, metadata.author || 'unknown'); pattern = pattern.replace(/{projectCode}/g, metadata.projectCode || ''); // Get file extension const extension = originalFileName.split('.').pop(); return `${pattern}.${extension}`; }, // Enhanced upload with metadata and validation async uploadFileWithMetadata(file, metadata, currentPath) { // Validate file const fileValidation = this.validateFile(file); if (!fileValidation.isValid) { throw new Error(`File validation failed: ${fileValidation.errors.join(', ')}`); } // Validate metadata const metadataValidation = this.validateMetadata(metadata); if (!metadataValidation.isValid) { throw new Error(`Metadata validation failed: ${metadataValidation.errors.join(', ')}`); } this.isLoading = true; try { // Generate document name const documentName = this.generateDocumentName(metadata, file.name); // Create document object const newDocument = { id: `doc_${Date.now()}`, name: documentName, originalName: file.name, type: 'file', extension: file.name.split('.').pop().toLowerCase(), size: this.formatFileSize(file.size), modified: new Date().toLocaleDateString(), author: metadata.author || 'Current User', department: metadata.department, description: metadata.description, tags: metadata.tags || [], priority: metadata.priority, projectCode: metadata.projectCode, reviewDate: metadata.reviewDate, version: 1, hasAccess: true, accessType: metadata.accessType || 'private', metadata: { ...metadata } }; // Add to items (simulate upload) this.items.push(newDocument); // Update user quota this.userQuotas.used += file.size; this.userQuotas.documents += 1; // Initialize version tracking if enabled if (this.systemSettings.documents.versionControl.enabled) { this.documentVersions[newDocument.id] = [{ version: 1, uploadDate: new Date().toISOString(), author: metadata.author, changes: 'Initial upload', fileSize: file.size }]; } return newDocument; } catch (error) { console.error('Upload failed:', error); throw error; } finally { this.isLoading = false; } }, // Version control actions async createNewVersion(documentId, file, metadata) { if (!this.systemSettings.documents.versionControl.enabled) { throw new Error('Version control is not enabled'); } const document = this.items.find(item => item.id === documentId); if (!document) { throw new Error('Document not found'); } // Check version limit const versions = this.documentVersions[documentId] || []; if (versions.length >= this.systemSettings.documents.versionControl.maxVersions) { // Remove oldest version versions.shift(); } // Create new version const newVersion = { version: document.version + 1, uploadDate: new Date().toISOString(), author: metadata.author, changes: metadata.changes || 'Updated document', fileSize: file.size }; // Update document document.version = newVersion.version; document.modified = new Date().toLocaleDateString(); document.size = this.formatFileSize(file.size); // Add version to tracking versions.push(newVersion); this.documentVersions[documentId] = versions; return newVersion; }, // Get document versions getDocumentVersions(documentId) { return this.documentVersions[documentId] || []; }, // Enhanced search with metadata async advancedSearch(query, filters = {}) { this.isLoading = true; this.searchQuery = query; Object.assign(this.advancedSearchFilters, filters); try { // Mock API delay await new Promise(resolve => setTimeout(resolve, 300)); let results = this.items.filter(item => item.type === 'file'); // Text search if (query) { const searchLower = query.toLowerCase(); results = results.filter(item => item.name.toLowerCase().includes(searchLower) || item.description?.toLowerCase().includes(searchLower) || item.department?.toLowerCase().includes(searchLower) || item.author?.toLowerCase().includes(searchLower) || item.projectCode?.toLowerCase().includes(searchLower) || item.tags?.some(tag => tag.toLowerCase().includes(searchLower)) ); } // Filter by file type if (filters.fileType && filters.fileType.length > 0) { results = results.filter(item => filters.fileType.includes(item.extension)); } // Filter by department if (filters.department && filters.department.length > 0) { results = results.filter(item => filters.department.includes(item.department)); } // Filter by tags if (filters.tags && filters.tags.length > 0) { results = results.filter(item => item.tags?.some(tag => filters.tags.includes(tag)) ); } // Filter by priority if (filters.priority && filters.priority.length > 0) { results = results.filter(item => filters.priority.includes(item.priority)); } // Filter by date range if (filters.dateRange?.start || filters.dateRange?.end) { results = results.filter(item => { const itemDate = new Date(item.modified); const startDate = filters.dateRange.start ? new Date(filters.dateRange.start) : null; const endDate = filters.dateRange.end ? new Date(filters.dateRange.end) : null; if (startDate && itemDate < startDate) return false; if (endDate && itemDate > endDate) return false; return true; }); } // Filter by version existence if (filters.hasVersions) { results = results.filter(item => this.documentVersions[item.id]?.length > 1); } this.searchResults = results; return results; } catch (error) { console.error('Advanced search failed:', error); throw error; } finally { this.isLoading = false; } }, // Tag suggestions getTagSuggestions(input) { if (!this.systemSettings.metadata.tagSuggestions) return []; const allTags = [ ...this.systemSettings.metadata.predefinedTags, ...new Set(this.items.flatMap(item => item.tags || [])) ]; return allTags.filter(tag => tag.toLowerCase().includes(input.toLowerCase()) ).slice(0, 10); }, // Update system settings updateSystemSettings(category, settings) { if (this.systemSettings[category]) { Object.assign(this.systemSettings[category], settings); } }, // Format file size helper formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }, // 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 = []; }, // Access request functionality async requestAccess(itemId, accessType, justification, duration = '7 days') { this.isLoading = true; try { // Mock API delay await new Promise(resolve => setTimeout(resolve, 800)); // 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: `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 the request to the store this.accessRequests.push(newRequest); // 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) { console.error('Failed to submit access request:', error); throw error; } 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); }, // Clear request timeouts when component unmounts clearRequestTimeouts() { this.requestTimeouts.forEach(timeoutId => clearTimeout(timeoutId)); this.requestTimeouts.clear(); }, // Set loading state with error clearing setLoading(loading) { this.isLoading = loading; if (loading) { this.error = null; } }, // Set error state with loading clearing setError(error) { this.error = error; this.isLoading = false; console.error('DMS Store Error:', error); }, // Authentik integration placeholder - this would be replaced with actual Authentik API calls async authenticateWithAuthentik(username, password) { this.setLoading(true); try { // Simulate API delay with timeout handling const timeoutId = setTimeout(() => { throw new Error('Authentication timeout'); }, 10000); // 10 second timeout this.requestTimeouts.set('auth', timeoutId); await new Promise(resolve => setTimeout(resolve, 800)); clearTimeout(timeoutId); this.requestTimeouts.delete('auth'); // This is a placeholder for the actual Authentik integration // In a real implementation, this would make API calls to Authentik if (username === 'superadmin' && password === 'password') { this.setLoading(false); return { user: { id: 'superadmin1', name: 'Super Admin User', email: 'superadmin@example.com', role: 'superadmin', department: 'IT Department' }, token: 'sample-authentik-token' }; } if (username === 'admin' && password === 'password') { this.setLoading(false); 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') { this.setLoading(false); return { user: { id: 'user1', name: 'Aiman Fakhrullah', email: 'aiman@example.com', role: 'user', department: 'General Department' }, token: 'sample-authentik-token' }; } throw new Error('Authentication failed'); } catch (error) { this.setError(error.message); throw error; } }, // 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 = { 'superadmin1': { roles: ['superadmin'], 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 }, systemSettings: { manage: true }, users: { manage: true }, roles: { manage: true } } }, '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 }, systemSettings: { manage: false }, users: { manage: false }, roles: { manage: false } } }, 'user1': { roles: ['user'], 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 }, systemSettings: { manage: false }, users: { manage: false }, roles: { manage: 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 }, systemSettings: { manage: false }, users: { manage: false }, roles: { manage: false } } }; } } });