generated from corrad-software/corrad-af-2024
1924 lines
60 KiB
JavaScript
1924 lines
60 KiB
JavaScript
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
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
});
|