EDMS/components/dms/explorer/DMSExplorer.vue
2025-05-31 14:58:52 +08:00

1493 lines
67 KiB
Vue

<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import DMSDocumentViewer from '~/components/dms/viewers/DMSDocumentViewer.vue';
import DMSUploadDialog from '~/components/dms/dialogs/DMSUploadDialog.vue';
import DMSCreateNewDialog from '~/components/dms/dialogs/DMSCreateNewDialog.vue';
import DMSAccessRequestDialog from '~/components/dms/dialogs/DMSAccessRequestDialog.vue';
import { useDmsStore } from '~/stores/dms';
// Props for the explorer
const props = defineProps({
initialPath: {
type: String,
default: '/'
},
viewMode: {
type: String,
default: 'list', // list, grid, details
},
activeDocumentTab: {
type: String,
default: 'all' // all, public, private, personal
}
});
// Emits for events
const emit = defineEmits(['itemSelected', 'viewModeChanged', 'pathChanged']);
// Component state
const currentPath = ref(props.initialPath);
const selectedItem = ref(null);
const viewMode = ref(props.viewMode);
const isLoading = ref(true);
const treeExpanded = ref({});
const sortBy = ref('name'); // name, modified, size, type
const sortOrder = ref('asc'); // asc, desc
const searchQuery = ref('');
const showAccessRequestDialog = ref(false);
const accessRequestItem = ref(null);
const isRequestingAccess = ref(false);
const requestSuccess = ref(false);
const requestError = ref(null);
// Navigation history for back/forward functionality
const navigationHistory = ref([props.initialPath]);
const historyIndex = ref(0);
const canGoBack = computed(() => historyIndex.value > 0);
const canGoForward = computed(() => historyIndex.value < navigationHistory.value.length - 1);
// Dialog states
const showDocumentViewer = ref(false);
const showUploadDialog = ref(false);
const showCreateDialog = ref(false);
const currentDocument = ref(null);
// Mock data - replace with actual API calls
const allItems = ref([]);
const router = useRouter();
const dmsStore = useDmsStore();
// Methods
const loadItems = async () => {
isLoading.value = true;
try {
// Replace with actual API call
// const { data } = await useFetch('/api/dms/items', {
// params: { path: currentPath.value }
// });
// allItems.value = data.value;
// Mock data for development
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API delay
// Enhanced mock data with examples for each access type
allItems.value = [
// PUBLIC ACCESS EXAMPLES
{
id: 'public-cabinet1',
name: 'Public Documents',
type: 'cabinet',
parentPath: '/',
description: 'Publicly accessible documents and policies',
tags: ['Public', 'General'],
accessType: 'public',
created: '2023-01-15',
modified: '2023-10-15',
icon: 'unlock'
},
{
id: 'public-drawer1',
name: 'Government Policies',
type: 'drawer',
parentPath: '/public-cabinet1',
description: 'Public government policies and regulations',
tags: ['Policy', 'Government', 'Public'],
accessType: 'public',
created: '2023-03-01',
modified: '2023-09-30'
},
{
id: 'public-file1',
name: 'National_Infrastructure_Policy_2023.pdf',
type: 'file',
extension: 'pdf',
size: '2.1MB',
parentPath: '/public-cabinet1/public-drawer1',
description: 'National infrastructure development policy',
tags: ['Infrastructure', 'Policy', 'Public'],
accessType: 'public',
author: 'Ministry of Works',
department: 'Policy Division',
created: '2023-01-10',
modified: '2023-03-15'
},
{
id: 'public-file2',
name: 'Public_Tender_Guidelines.docx',
type: 'file',
extension: 'docx',
size: '890KB',
parentPath: '/public-cabinet1/public-drawer1',
description: 'Guidelines for public tender processes',
tags: ['Tender', 'Guidelines', 'Public'],
accessType: 'public',
author: 'Procurement Department',
department: 'Finance Division',
created: '2023-02-05',
modified: '2023-08-20'
},
// PERSONAL ACCESS EXAMPLES
{
id: 'personal-cabinet1',
name: 'JKR Cawangan Kota Bharu',
type: 'cabinet',
parentPath: '/',
description: 'Documents for Kota Bharu branch staff',
tags: ['Personal', 'Kota Bharu'],
accessType: 'personal',
created: '2023-02-20',
modified: '2023-10-10',
hasAccess: true
},
{
id: 'personal-drawer1',
name: 'Engineering Projects',
type: 'drawer',
parentPath: '/personal-cabinet1',
description: 'Civil engineering project documents',
tags: ['Engineering', 'Projects', 'Personal'],
accessType: 'personal',
created: '2023-04-01',
modified: '2023-08-15'
},
{
id: 'personal-file1',
name: 'Bridge_Construction_KB_2023.pdf',
type: 'file',
extension: 'pdf',
size: '5.2MB',
parentPath: '/personal-cabinet1/personal-drawer1',
description: 'Kota Bharu bridge construction project plan',
tags: ['Bridge', 'Construction', 'Kota Bharu'],
accessType: 'personal',
author: 'Ahmad bin Rahman',
department: 'Civil Engineering',
created: '2023-05-15',
modified: '2023-09-10'
},
{
id: 'personal-file2',
name: 'Staff_Meeting_Minutes_Oct2023.docx',
type: 'file',
extension: 'docx',
size: '156KB',
parentPath: '/personal-cabinet1/personal-drawer1',
description: 'Monthly staff meeting minutes',
tags: ['Meeting', 'Minutes', 'Internal'],
accessType: 'personal',
author: 'Fatimah binti Ali',
department: 'Administration',
created: '2023-10-05',
modified: '2023-10-05'
},
// PRIVATE ACCESS EXAMPLES
{
id: 'private-cabinet1',
name: 'JKR Headquarters Confidential',
type: 'cabinet',
parentPath: '/',
description: 'Confidential headquarters documents',
tags: ['Private', 'Confidential', 'HQ'],
accessType: 'private',
created: '2023-03-10',
modified: '2023-10-01',
hasAccess: false,
accessRequestStatus: 'pending',
isLocked: true
},
{
id: 'private-drawer1',
name: 'Budget & Financial Reports',
type: 'drawer',
parentPath: '/private-cabinet1',
description: 'Confidential financial documents',
tags: ['Financial', 'Budget', 'Confidential'],
accessType: 'private',
created: '2023-06-01',
modified: '2023-09-25',
hasAccess: false,
isLocked: true
},
{
id: 'private-file1',
name: 'Annual_Budget_2024_CONFIDENTIAL.xlsx',
type: 'file',
extension: 'xlsx',
size: '3.8MB',
parentPath: '/private-cabinet1/private-drawer1',
description: 'Confidential annual budget allocation',
tags: ['Budget', 'Confidential', '2024'],
accessType: 'private',
author: 'Director General',
department: 'Finance',
created: '2023-08-01',
modified: '2023-09-15',
hasAccess: false,
isLocked: true
},
// MIXED ACCESS EXAMPLES FOR DEMONSTRATION
{
id: 'folder1',
name: 'IT Department',
type: 'folder',
parentPath: '/personal-cabinet1',
description: 'Information technology department files',
tags: ['IT', 'Technology'],
accessType: 'personal',
created: '2023-04-10',
modified: '2023-09-01'
},
{
id: 'file3',
name: 'Network_Infrastructure_Plan.pdf',
type: 'file',
extension: 'pdf',
size: '1.9MB',
parentPath: '/personal-cabinet1/folder1',
description: 'Network infrastructure upgrade plan',
tags: ['Network', 'Infrastructure', 'IT'],
accessType: 'personal',
author: 'Omar bin Hassan',
department: 'IT Department',
created: '2023-07-12',
modified: '2023-08-30'
}
];
} catch (error) {
console.error('Error loading items:', error);
} finally {
isLoading.value = false;
}
};
const parseFileSize = (size) => {
if (typeof size === 'number') return size;
if (!size || typeof size !== 'string') return 0;
const units = { 'B': 1, 'KB': 1024, 'MB': 1024*1024, 'GB': 1024*1024*1024 };
const match = size.match(/^([\d.]+)\s*([A-Z]+)$/i);
if (match) {
const value = parseFloat(match[1]);
const unit = match[2].toUpperCase();
return value * (units[unit] || 1);
}
return parseFloat(size) || 0;
};
const selectItem = (item) => {
selectedItem.value = item;
emit('itemSelected', item);
};
const goBack = () => {
if (canGoBack.value) {
historyIndex.value--;
currentPath.value = navigationHistory.value[historyIndex.value];
autoExpandTreeForPath(currentPath.value);
emit('pathChanged', currentPath.value);
}
};
const goForward = () => {
if (canGoForward.value) {
historyIndex.value++;
currentPath.value = navigationHistory.value[historyIndex.value];
autoExpandTreeForPath(currentPath.value);
emit('pathChanged', currentPath.value);
}
};
const navigateTo = (path) => {
if (path !== currentPath.value) {
// Remove any forward history if we're navigating to a new path
if (historyIndex.value < navigationHistory.value.length - 1) {
navigationHistory.value = navigationHistory.value.slice(0, historyIndex.value + 1);
}
// Add new path to history
navigationHistory.value.push(path);
historyIndex.value = navigationHistory.value.length - 1;
currentPath.value = path;
// Auto-expand tree to show current path
autoExpandTreeForPath(path);
emit('pathChanged', path);
}
};
// Auto-expand tree items to show the current path
const autoExpandTreeForPath = (path) => {
const segments = path.split('/').filter(Boolean);
segments.forEach((segment, index) => {
const itemId = segments.slice(0, index + 1).join('-');
if (itemId) {
treeExpanded.value[itemId] = true;
}
});
};
const navigateToItem = (item) => {
// Navigate to the item's location
const newPath = item.parentPath + '/' + item.id;
navigateTo(newPath);
selectItem(item);
};
const toggleTreeItem = (itemId) => {
treeExpanded.value[itemId] = !treeExpanded.value[itemId];
};
const changeViewMode = (mode) => {
viewMode.value = mode;
emit('viewModeChanged', mode);
};
const setSortBy = (field) => {
if (sortBy.value === field) {
// Toggle sort order if clicking the same field
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
// Set new field with ascending order
sortBy.value = field;
sortOrder.value = 'asc';
}
};
const openFileViewer = (file) => {
currentDocument.value = file;
showDocumentViewer.value = true;
};
const closeFileViewer = () => {
showDocumentViewer.value = false;
currentDocument.value = null;
};
const handleUpload = (fileData) => {
console.log('Uploading file:', fileData);
// Handle file upload logic here
// Add to allItems.value or refresh from API
loadItems();
};
const handleCreateNew = (itemData) => {
console.log('Creating new item:', itemData);
// Handle create new item logic here
// Add to allItems.value or refresh from API
loadItems();
};
const formatFileSize = (size) => {
if (!size) return '0 B';
const bytes = parseFileSize(size);
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
let formattedSize = bytes;
while (formattedSize >= 1024 && i < units.length - 1) {
formattedSize /= 1024;
i++;
}
return `${formattedSize.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
};
const requestAccess = (item) => {
// In a real app, this would make an API call to request access
console.log('Requesting access for:', item.name);
// For demo purposes, just update the status
item.accessRequestStatus = 'pending';
};
// Get icon for item type
const getItemIcon = (item) => {
if (item.accessType === 'private' && item.isLocked) {
return 'lock';
}
if (item.accessType === 'public') {
switch (item.type) {
case 'cabinet': return 'unlock';
case 'drawer': return 'folder';
case 'folder': return 'folder';
case 'file': return getFileIcon(item.extension);
default: return 'folder';
}
}
if (item.accessType === 'personal') {
switch (item.type) {
case 'cabinet': return 'user';
case 'drawer': return 'folder';
case 'folder': return 'folder';
case 'file': return getFileIcon(item.extension);
default: return 'folder';
}
}
// Default for private access
switch (item.type) {
case 'cabinet': return 'lock';
case 'drawer': return 'folderLock';
case 'folder': return 'folderLock';
case 'file': return 'fileLock';
default: return 'lock';
}
};
const getFileIcon = (extension) => {
switch (extension?.toLowerCase()) {
case 'pdf': return 'pdf';
case 'doc':
case 'docx': return 'doc';
case 'xls':
case 'xlsx': return 'excel';
case 'ppt':
case 'pptx': return 'powerpoint';
case 'jpg':
case 'jpeg':
case 'png':
case 'gif': return 'image';
case 'txt': return 'text';
default: return 'file';
}
};
// Get SVG path based on icon type
const getSvgIcon = (iconType, size = 24) => {
const icons = {
back: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5m7-7-7 7 7 7"/></svg>`,
forward: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14m-7-7 7 7-7 7"/></svg>`,
home: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`,
search: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="M21 21l-4.35-4.35"></path></svg>`,
x: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
upload: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>`,
plus: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
sortAsc: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18l-6 6H9L3 6z"/><path d="m7 17 5-5 5 5"/></svg>`,
sortDesc: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18l-6 6H9L3 6z"/><path d="m7 7 5 5 5-5"/></svg>`,
// Access type specific icons
unlock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>`,
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
user: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`,
folder: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
folderLock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><rect x="8" y="11" width="8" height="5" rx="1" ry="1"></rect><path d="M10 11V9a2 2 0 0 1 4 0v2"></path></svg>`,
// File type icons
pdf: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><path d="M16 13a2 2 0 0 1-2 2h-2v4"></path><path d="M12 17v-4h2a2 2 0 0 1 2 2z"></path></svg>`,
doc: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>`,
excel: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><rect x="8" y="12" width="8" height="6"></rect><line x1="8" y1="16" x2="16" y2="16"></line><line x1="12" y1="12" x2="12" y2="18"></line></svg>`,
image: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
text: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
file: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
fileLock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><rect x="8" y="12" width="8" height="6" rx="1" ry="1"></rect><path d="M10 12V10a2 2 0 0 1 4 0v2"></path></svg>`,
// Other icons
tag: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>`,
cabinet: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2" ry="2"></rect><circle cx="17" cy="12" r="1"></circle><path d="M7 6V4a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2"></path></svg>`,
chevronDown: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>`,
chevronRight: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>`
};
return icons[iconType] || icons.file;
};
const getAccessTypeCount = (accessType) => {
if (accessType === 'all') {
return allItems.value.length;
}
return allItems.value.filter(item => item.accessType === accessType).length;
};
// Lifecycle hooks
onMounted(() => {
loadItems();
// Auto-expand tree for initial path
autoExpandTreeForPath(currentPath.value);
});
const getTabClasses = (tab) => {
if (props.activeDocumentTab === tab.id) {
switch (tab.color) {
case 'green':
return 'bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-300 shadow-sm';
case 'blue':
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300 shadow-sm';
case 'red':
return 'bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-300 shadow-sm';
default:
return 'bg-gray-100 text-gray-700 dark:bg-gray-900/20 dark:text-gray-300 shadow-sm';
}
}
return 'text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700';
};
const getTabIconClasses = (tab) => {
if (props.activeDocumentTab === tab.id) {
switch (tab.color) {
case 'green':
return 'text-green-600 dark:text-green-400';
case 'blue':
return 'text-blue-600 dark:text-blue-400';
case 'red':
return 'text-red-600 dark:text-red-400';
default:
return 'text-gray-600 dark:text-gray-400';
}
}
return '';
};
const getTabBadgeClasses = (tab) => {
if (props.activeDocumentTab === tab.id) {
switch (tab.color) {
case 'green':
return 'bg-green-200 text-green-800 dark:bg-green-800 dark:text-green-200';
case 'blue':
return 'bg-blue-200 text-blue-800 dark:bg-blue-800 dark:text-blue-200';
case 'red':
return 'bg-red-200 text-red-800 dark:bg-red-800 dark:text-red-200';
default:
return 'bg-gray-200 text-gray-800 dark:bg-gray-800 dark:text-gray-200';
}
}
return 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400';
};
// Methods for access control
const checkItemAccess = (item) => {
// Check if item is private and user doesn't have access
if (item.accessType === 'private' && item.hasAccess === false) {
return false;
}
return true;
};
const handleItemClick = (item) => {
// Check access before allowing navigation
if (!checkItemAccess(item)) {
// Show access request dialog for private items
accessRequestItem.value = item;
showAccessRequestDialog.value = true;
return;
}
// Allow normal navigation for accessible items
if (item.type === 'file') {
selectItem(item);
currentDocument.value = item;
showDocumentViewer.value = true;
} else {
navigateToItem(item);
}
};
const handleAccessRequestSubmitted = (request) => {
// Handle successful access request
showAccessRequestDialog.value = false;
accessRequestItem.value = null;
// Could show a success message or update the item status
console.log('Access request submitted:', request);
};
const closeAccessRequestDialog = () => {
showAccessRequestDialog.value = false;
accessRequestItem.value = null;
};
const handleAccessRequest = async (item) => {
if (!item) return;
accessRequestItem.value = item;
showAccessRequestDialog.value = true;
};
const submitAccessRequest = async (requestData) => {
isRequestingAccess.value = true;
requestError.value = null;
try {
await dmsStore.requestAccess(
accessRequestItem.value.id,
requestData.accessType,
requestData.justification,
requestData.accessDuration
);
requestSuccess.value = true;
setTimeout(() => {
showAccessRequestDialog.value = false;
requestSuccess.value = false;
accessRequestItem.value = null;
}, 2000);
} catch (error) {
requestError.value = error.message || 'Failed to submit access request. Please try again.';
console.error('Access request error:', error);
} finally {
isRequestingAccess.value = false;
}
};
// Computed properties
const currentItems = computed(() => {
let items = allItems.value.filter(item => item.parentPath === currentPath.value);
// Apply access type filter from parent component
if (props.activeDocumentTab !== 'all') {
items = items.filter(item => item.accessType === props.activeDocumentTab);
}
// Apply search filter
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase();
items = items.filter(item =>
item.name.toLowerCase().includes(query) ||
item.type.toLowerCase().includes(query) ||
(item.tags && item.tags.some(tag => tag.toLowerCase().includes(query)))
);
}
// Apply sorting
items.sort((a, b) => {
let aValue, bValue;
switch (sortBy.value) {
case 'name':
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
break;
case 'modified':
aValue = new Date(a.modified || 0);
bValue = new Date(b.modified || 0);
break;
case 'size':
aValue = parseFileSize(a.size || '0');
bValue = parseFileSize(b.size || '0');
break;
case 'type':
aValue = a.type;
bValue = b.type;
break;
default:
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
}
if (aValue < bValue) return sortOrder.value === 'asc' ? -1 : 1;
if (aValue > bValue) return sortOrder.value === 'asc' ? 1 : -1;
return 0;
});
return items;
});
// Filtered tree items based on access type from parent
const filteredTreeItems = computed(() => {
if (props.activeDocumentTab === 'all') {
return allItems.value;
}
return allItems.value.filter(item => item.accessType === props.activeDocumentTab);
});
const breadcrumbs = computed(() => {
if (currentPath.value === '/') {
return [{ name: 'Home', path: '/' }];
}
// Build breadcrumbs by looking up actual item names
const breadcrumbItems = [{ name: 'Home', path: '/' }];
const pathSegments = currentPath.value.split('/').filter(Boolean);
let currentBreadcrumbPath = '';
pathSegments.forEach((segmentId, index) => {
currentBreadcrumbPath += `/${segmentId}`;
// Find the item with this ID at the current path level
const item = allItems.value.find(item =>
item.id === segmentId &&
item.parentPath === (index === 0 ? '/' : breadcrumbItems[index].path)
);
if (item) {
breadcrumbItems.push({
name: item.name,
path: currentBreadcrumbPath,
type: item.type,
accessType: item.accessType
});
} else {
// Fallback to ID if item not found
breadcrumbItems.push({
name: segmentId.charAt(0).toUpperCase() + segmentId.slice(1),
path: currentBreadcrumbPath
});
}
});
return breadcrumbItems;
});
const parentType = computed(() => {
const pathParts = currentPath.value.split('/').filter(Boolean);
if (pathParts.length === 0) return 'root';
if (pathParts.length === 1) return 'cabinet';
if (pathParts.length === 2) return 'drawer';
if (pathParts.length === 3) return 'folder';
return 'subfolder';
});
</script>
<template>
<div class="dms-explorer h-full flex flex-col bg-white dark:bg-gray-900">
<!-- Enhanced Toolbar -->
<div class="dms-toolbar p-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<!-- Top row: Navigation Controls, Breadcrumbs, and Action Buttons -->
<div class="flex items-center justify-between mb-3">
<!-- Navigation Controls -->
<div class="flex items-center space-x-2">
<button
@click="goBack"
:disabled="!canGoBack"
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
:class="canGoBack ? 'text-gray-700 dark:text-gray-300' : 'text-gray-400'"
title="Back"
>
<span v-html="getSvgIcon('back', 18)"></span>
</button>
<button
@click="goForward"
:disabled="!canGoForward"
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
:class="canGoForward ? 'text-gray-700 dark:text-gray-300' : 'text-gray-400'"
title="Forward"
>
<span v-html="getSvgIcon('forward', 18)"></span>
</button>
<button @click="navigateTo('/')" class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700" title="Home">
<span v-html="getSvgIcon('home', 18)"></span>
</button>
</div>
<!-- Breadcrumbs with border -->
<div class="flex items-center space-x-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700/50 flex-1 mx-4 max-w-2xl">
<template v-for="(crumb, index) in breadcrumbs" :key="index">
<span v-if="index > 0" class="text-gray-400 mx-1">/</span>
<button
@click="navigateTo(crumb.path)"
class="text-primary hover:underline px-2 py-1 rounded text-sm transition-colors"
:class="index === breadcrumbs.length - 1 ? 'font-medium bg-white dark:bg-gray-600 text-blue-700 dark:text-blue-300 shadow-sm' : 'hover:bg-white/50 dark:hover:bg-gray-600/50'"
>
{{ crumb.name }}
</button>
</template>
</div>
<!-- Action Buttons -->
<div class="flex items-center space-x-2">
<rs-button @click="showUploadDialog = true" size="sm">
<span v-html="getSvgIcon('upload', 16)" class="mr-2"></span>
Upload
</rs-button>
<rs-button @click="showCreateDialog = true" size="sm" variant="secondary">
<span v-html="getSvgIcon('plus', 16)" class="mr-2"></span>
Create New
</rs-button>
</div>
</div>
<!-- Enhanced Search and View Controls -->
<div class="flex items-center justify-between gap-4">
<!-- Enhanced Search Section -->
<div class="flex items-center space-x-3 flex-1">
<div class="relative flex-1 max-w-lg">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span v-html="getSvgIcon('search', 20)" class="text-gray-400"></span>
</div>
<input
v-model="searchQuery"
type="text"
placeholder="Search documents, folders, and tags..."
class="block w-full pl-10 pr-4 py-2.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-blue-500
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
placeholder-gray-500 dark:placeholder-gray-400
transition-all duration-200"
/>
<div v-if="searchQuery" class="absolute inset-y-0 right-0 pr-3 flex items-center">
<button
@click="searchQuery = ''"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
<span v-html="getSvgIcon('x', 16)"></span>
</button>
</div>
</div>
<!-- Enhanced Sort Controls -->
<div class="flex items-center space-x-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-600">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 whitespace-nowrap">Sort by:</span>
<select
v-model="sortBy"
@change="setSortBy(sortBy)"
class="text-sm border-0 bg-transparent focus:ring-0 text-gray-700 dark:text-gray-300 font-medium min-w-0"
>
<option value="name">Name</option>
<option value="modified">Modified</option>
<option value="size">Size</option>
<option value="type">Type</option>
</select>
<button
@click="setSortBy(sortBy)"
class="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors"
:title="sortOrder === 'asc' ? 'Sort Descending' : 'Sort Ascending'"
>
<span v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 16)"
:class="sortOrder === 'asc' ? 'text-blue-600 dark:text-blue-400' : 'text-orange-600 dark:text-orange-400'"></span>
</button>
</div>
</div>
<!-- Enhanced View Mode Controls -->
<div class="flex items-center space-x-1 bg-gray-50 dark:bg-gray-700/50 rounded-lg p-1 border border-gray-200 dark:border-gray-600">
<button
@click="changeViewMode('list')"
class="p-2 rounded transition-all duration-200"
:class="viewMode === 'list'
? 'bg-white dark:bg-gray-600 text-blue-600 dark:text-blue-400 shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600'"
title="List View"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
</button>
<button
@click="changeViewMode('grid')"
class="p-2 rounded transition-all duration-200"
:class="viewMode === 'grid'
? 'bg-white dark:bg-gray-600 text-blue-600 dark:text-blue-400 shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600'"
title="Grid View"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
</button>
<button
@click="changeViewMode('details')"
class="p-2 rounded transition-all duration-200"
:class="viewMode === 'details'
? 'bg-white dark:bg-gray-600 text-blue-600 dark:text-blue-400 shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600'"
title="Details View"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg>
</button>
</div>
</div>
</div>
<!-- Main content area -->
<div class="flex-1 flex overflow-hidden">
<!-- Tree navigation -->
<div class="dms-tree w-64 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-2 bg-white dark:bg-gray-800">
<div class="tree-item p-1">
<div
@click="navigateTo('/')"
class="flex items-center space-x-2 p-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
:class="currentPath === '/' ? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : ''"
>
<span v-html="getSvgIcon('home', 18)"></span>
<span class="font-medium">Home</span>
</div>
</div>
<!-- Tree items -->
<div v-for="cabinet in filteredTreeItems.filter(i => i.type === 'cabinet' && i.parentPath === '/')" :key="cabinet.id" class="tree-item">
<div
@click="handleItemClick(cabinet)"
class="flex items-center space-x-2 p-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 relative"
:class="currentPath === `/${cabinet.id}` ? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : ''"
>
<button
@click.stop="toggleTreeItem(cabinet.id)"
class="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded"
>
<span v-html="getSvgIcon(treeExpanded[cabinet.id] ? 'chevronDown' : 'chevronRight', 14)"></span>
</button>
<span v-html="getSvgIcon(getItemIcon(cabinet), 18)"
:class="{
'text-green-600': cabinet.accessType === 'public',
'text-blue-600': cabinet.accessType === 'personal',
'text-red-600': cabinet.accessType === 'private'
}"></span>
<span class="text-sm truncate">{{ cabinet.name }}</span>
<!-- Lock indicator for private items without access -->
<span v-if="cabinet.accessType === 'private' && !checkItemAccess(cabinet)"
v-html="getSvgIcon('lock', 14)"
class="text-red-500 ml-auto"></span>
</div>
<div v-if="treeExpanded[cabinet.id]" class="ml-6">
<div
v-for="drawer in filteredTreeItems.filter(i => i.parentPath === `/${cabinet.id}`)"
:key="drawer.id"
class="tree-item"
>
<div
@click="handleItemClick(drawer)"
class="flex items-center space-x-2 p-1 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 relative"
:class="currentPath === `/${cabinet.id}/${drawer.id}` ? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : ''"
>
<button
v-if="filteredTreeItems.some(i => i.parentPath === `/${cabinet.id}/${drawer.id}`)"
@click.stop="toggleTreeItem(drawer.id)"
class="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded"
>
<span v-html="getSvgIcon(treeExpanded[drawer.id] ? 'chevronDown' : 'chevronRight', 12)"></span>
</button>
<span v-else class="w-4"></span>
<span v-html="getSvgIcon(getItemIcon(drawer), 16)"
:class="{
'text-green-600': drawer.accessType === 'public',
'text-blue-600': drawer.accessType === 'personal',
'text-red-600': drawer.accessType === 'private'
}"></span>
<span class="text-xs truncate">{{ drawer.name }}</span>
<!-- Lock indicator for private items without access -->
<span v-if="drawer.accessType === 'private' && !checkItemAccess(drawer)"
v-html="getSvgIcon('lock', 12)"
class="text-red-500 ml-auto"></span>
</div>
<!-- Sub-folders -->
<div v-if="treeExpanded[drawer.id]" class="ml-4">
<div
v-for="folder in filteredTreeItems.filter(i => i.parentPath === `/${cabinet.id}/${drawer.id}`)"
:key="folder.id"
class="tree-item"
>
<div
@click="handleItemClick(folder)"
class="flex items-center space-x-2 p-1 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 relative"
:class="currentPath === `/${cabinet.id}/${drawer.id}/${folder.id}` ? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : ''"
>
<span class="w-3"></span>
<span v-html="getSvgIcon(getItemIcon(folder), 14)"
:class="{
'text-green-600': folder.accessType === 'public',
'text-blue-600': folder.accessType === 'personal',
'text-red-600': folder.accessType === 'private'
}"></span>
<span class="text-xs truncate">{{ folder.name }}</span>
<!-- Lock indicator for private items without access -->
<span v-if="folder.accessType === 'private' && !checkItemAccess(folder)"
v-html="getSvgIcon('lock', 12)"
class="text-red-500 ml-auto"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content area -->
<div class="dms-content flex-1 overflow-y-auto bg-white dark:bg-gray-900">
<!-- Loading indicator -->
<div v-if="isLoading" class="flex justify-center items-center h-full">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<p class="text-gray-500">Loading documents...</p>
</div>
</div>
<!-- Empty state -->
<div v-else-if="currentItems.length === 0" class="flex flex-col justify-center items-center h-full text-gray-500">
<span v-html="getSvgIcon('folder', 64)" class="mb-4 text-gray-300"></span>
<p class="text-lg font-medium">{{ searchQuery ? 'No results found' : 'This folder is empty' }}</p>
<p class="text-sm">{{ searchQuery ? 'Try adjusting your search terms' : 'Upload documents or create new folders to get started' }}</p>
</div>
<!-- List view -->
<div v-else-if="viewMode === 'list'" class="p-4">
<div
v-for="item in currentItems"
:key="item.id"
@click="handleItemClick(item)"
@dblclick="item.type === 'file' ? openFileViewer(item) : null"
class="flex items-center p-3 border-b border-gray-100 dark:border-gray-800 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg mb-1 transition-all duration-200"
:class="[
selectedItem?.id === item.id ? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-700' : '',
item.accessType === 'private' && !checkItemAccess(item) ? 'opacity-60' : ''
]"
>
<!-- Icon with Access Type Indicator -->
<div class="relative mr-3 flex-shrink-0">
<span class="block" v-html="getSvgIcon(getItemIcon(item), 24)"
:class="{
'text-green-600': item.accessType === 'public',
'text-blue-600': item.accessType === 'personal',
'text-red-600': item.accessType === 'private'
}"></span>
<!-- Access Type Badge -->
<div class="absolute -top-1 -right-1 w-3 h-3 rounded-full border border-white dark:border-gray-800"
:class="{
'bg-green-500': item.accessType === 'public',
'bg-blue-500': item.accessType === 'personal',
'bg-red-500': item.accessType === 'private'
}"
:title="item.accessType ? item.accessType.charAt(0).toUpperCase() + item.accessType.slice(1) + ' Access' : ''">
</div>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2 mb-1">
<h4 class="font-medium truncate"
:class="item.accessType === 'private' && !checkItemAccess(item) ? 'text-gray-400' : ''">
{{ item.name }}
</h4>
<!-- Access Type Badge -->
<span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300': item.accessType === 'public',
'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300': item.accessType === 'personal',
'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300': item.accessType === 'private'
}">
{{ item.accessType === 'public' ? 'Public' : item.accessType === 'personal' ? 'Personal' : 'Private' }}
</span>
<!-- Lock Status for Private Items -->
<span v-if="item.accessType === 'private' && !checkItemAccess(item)"
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
<span v-html="getSvgIcon('lock', 10)" class="mr-1"></span>
{{ item.accessRequestStatus === 'pending' ? 'Pending' : item.accessRequestStatus === 'rejected' ? 'Denied' : 'Locked' }}
</span>
<!-- Tags -->
<div v-if="item.tags" class="flex space-x-1">
<span
v-for="tag in item.tags.slice(0, 2)"
:key="tag"
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
<span v-html="getSvgIcon('tag', 10)" class="mr-1"></span>
{{ tag }}
</span>
<span v-if="item.tags.length > 2" class="text-xs text-gray-400">+{{ item.tags.length - 2 }}</span>
</div>
</div>
<div class="flex items-center space-x-4 text-xs text-gray-500">
<span v-if="item.type === 'file'">{{ formatFileSize(item.size) }}</span>
<span>{{ item.modified ? new Date(item.modified).toLocaleDateString() : 'No date' }}</span>
<span v-if="item.author" class="flex items-center">
<span v-html="getSvgIcon('user', 12)" class="mr-1"></span>
{{ item.author }}
</span>
<span v-if="item.department" class="text-gray-400">{{ item.department }}</span>
</div>
<div v-if="item.description" class="text-sm text-gray-600 dark:text-gray-400 mt-1 truncate">
{{ item.description }}
</div>
</div>
<!-- Access Request Button for Private Items -->
<div v-if="item.accessType === 'private' && !checkItemAccess(item)" class="ml-2">
<rs-button
size="sm"
variant="outline"
:disabled="item.accessRequestStatus === 'pending' || item.accessRequestStatus === 'rejected'"
@click.stop="accessRequestItem = item; showAccessRequestDialog = true"
>
<span v-html="getSvgIcon('lock', 14)" class="mr-1"></span>
{{ item.accessRequestStatus === 'pending' ? 'Pending' : item.accessRequestStatus === 'rejected' ? 'Denied' : 'Request Access' }}
</rs-button>
</div>
</div>
</div>
<!-- Grid view -->
<div v-else-if="viewMode === 'grid'" class="p-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
<div
v-for="item in currentItems"
:key="item.id"
@click="handleItemClick(item)"
@dblclick="item.type === 'file' ? openFileViewer(item) : null"
class="flex flex-col items-center p-4 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 text-center group relative"
:class="selectedItem?.id === item.id ? 'bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-200 dark:ring-blue-700' : ''"
>
<span class="mb-2 group-hover:scale-110 transition-transform"
v-html="getSvgIcon(getItemIcon(item), 48)"
:class="{
'text-green-600': item.accessType === 'public',
'text-blue-600': item.accessType === 'personal',
'text-red-600': item.accessType === 'private'
}"></span>
<div class="w-full">
<h4 class="text-sm font-medium truncate mb-1"
:class="item.accessType === 'private' && !checkItemAccess(item) ? 'text-gray-400' : ''">
{{ item.name }}
</h4>
<div v-if="item.type === 'file'" class="text-xs text-gray-500">
<div>{{ item.extension?.toUpperCase() }}</div>
<div>{{ item.size }}</div>
</div>
<div v-else class="text-xs text-gray-500 capitalize">{{ item.type }}</div>
<div v-if="item.tags" class="flex flex-wrap gap-1 mt-2 justify-center">
<span
v-for="tag in item.tags.slice(0, 1)"
:key="tag"
class="inline-block px-1 py-0.5 rounded text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
{{ tag }}
</span>
</div>
</div>
<!-- Lock indicator for private items without access -->
<span v-if="item.accessType === 'private' && !checkItemAccess(item)"
v-html="getSvgIcon('lock', 16)"
class="absolute top-2 right-2 text-red-500 bg-white dark:bg-gray-800 rounded-full p-1"></span>
<!-- Locked Item Overlay -->
<div
v-if="item.isLocked || (item.accessType === 'private' && !item.hasAccess)"
class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-60 rounded-md z-10"
>
<div class="text-center p-2">
<div class="bg-red-500 rounded-full p-2 inline-flex mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
</svg>
</div>
<p class="text-white text-xs mb-2">Restricted Access</p>
<button
v-if="!item.accessRequestStatus || item.accessRequestStatus === 'rejected'"
@click.stop="handleAccessRequest(item)"
class="px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded"
>
Request Access
</button>
<div
v-else-if="item.accessRequestStatus === 'pending'"
class="px-2 py-1 bg-yellow-500 text-white text-xs rounded"
>
Request Pending
</div>
</div>
</div>
</div>
</div>
<!-- Details view -->
<div v-else-if="viewMode === 'details'" class="overflow-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800 sticky top-0">
<tr>
<th @click="setSortBy('name')" scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center space-x-1">
<span>Name</span>
<span v-if="sortBy === 'name'" v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 12)"></span>
</div>
</th>
<th @click="setSortBy('type')" scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center space-x-1">
<span>Type</span>
<span v-if="sortBy === 'type'" v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 12)"></span>
</div>
</th>
<th @click="setSortBy('size')" scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center space-x-1">
<span>Size</span>
<span v-if="sortBy === 'size'" v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 12)"></span>
</div>
</th>
<th @click="setSortBy('modified')" scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center space-x-1">
<span>Modified</span>
<span v-if="sortBy === 'modified'" v-html="getSvgIcon(sortOrder === 'asc' ? 'sortAsc' : 'sortDesc', 12)"></span>
</div>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Access</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tags</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
<tr
v-for="item in currentItems"
:key="item.id"
@click="handleItemClick(item)"
@dblclick="item.type === 'file' ? openFileViewer(item) : null"
class="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800"
:class="selectedItem?.id === item.id ? 'bg-blue-50 dark:bg-blue-900/20' : ''"
>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="mr-3"
v-html="getSvgIcon(getItemIcon(item), 20)"
:class="{
'text-green-600': item.accessType === 'public',
'text-blue-600': item.accessType === 'personal',
'text-red-600': item.accessType === 'private'
}"></span>
<div>
<div class="font-medium flex items-center space-x-2">
<span :class="item.accessType === 'private' && !checkItemAccess(item) ? 'text-gray-400' : ''">
{{ item.name }}
</span>
<span v-if="item.accessType === 'private' && !checkItemAccess(item)"
v-html="getSvgIcon('lock', 14)"
class="text-red-500"></span>
</div>
<div v-if="item.description" class="text-sm text-gray-500 truncate max-w-xs">{{ item.description }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
:class="{
'bg-blue-100 text-blue-800': item.type === 'cabinet',
'bg-green-100 text-green-800': item.type === 'drawer',
'bg-yellow-100 text-yellow-800': item.type === 'folder',
'bg-orange-100 text-orange-800': item.type === 'subfolder',
'bg-purple-100 text-purple-800': item.type === 'file'
}">
{{ item.type }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ item.type === 'file' ? item.size : '—' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ item.modified || '—' }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="{
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300': item.accessType === 'public',
'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300': item.accessType === 'personal',
'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300': item.accessType === 'private'
}">
<span v-html="getSvgIcon(item.accessType === 'public' ? 'unlock' : item.accessType === 'personal' ? 'user' : 'lock', 12)" class="mr-1"></span>
{{ item.accessType === 'public' ? 'Public' : item.accessType === 'personal' ? 'Personal' : 'Private' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div v-if="item.tags" class="flex flex-wrap gap-1">
<span
v-for="tag in item.tags.slice(0, 3)"
:key="tag"
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
{{ tag }}
</span>
<span v-if="item.tags.length > 3" class="text-xs text-gray-400">+{{ item.tags.length - 3 }}</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Windows-style Details panel -->
<div v-if="currentDocument" class="dms-details w-80 border-l border-gray-200 dark:border-gray-700 overflow-y-auto bg-white dark:bg-gray-800">
<div class="p-4">
<!-- Document Preview -->
<div class="text-center mb-6">
<div class="bg-gray-100 dark:bg-gray-700 rounded-lg p-8 mb-4">
<span v-html="getSvgIcon(getItemIcon(currentDocument), 64)" class="text-gray-400"></span>
</div>
<h3 class="text-lg font-semibold truncate mb-1">{{ currentDocument.name }}</h3>
<p class="text-sm text-gray-500">{{ currentDocument.extension?.toUpperCase() }} File</p>
</div>
<!-- Quick Actions -->
<div class="space-y-2 mb-6">
<rs-button @click="openFileViewer(currentDocument)" block size="sm">
Preview
</rs-button>
<rs-button variant="secondary" block size="sm">
Download
</rs-button>
<rs-button variant="secondary" block size="sm">
Share
</rs-button>
</div>
<!-- Properties -->
<div class="space-y-4">
<div>
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Properties</h4>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-gray-500">Size</dt>
<dd class="font-medium">{{ currentDocument.size }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">Modified</dt>
<dd class="font-medium">{{ currentDocument.modified }}</dd>
</div>
<div v-if="currentDocument.author" class="flex justify-between">
<dt class="text-gray-500">Author</dt>
<dd class="font-medium">{{ currentDocument.author }}</dd>
</div>
<div v-if="currentDocument.department" class="flex justify-between">
<dt class="text-gray-500">Department</dt>
<dd class="font-medium">{{ currentDocument.department }}</dd>
</div>
</dl>
</div>
<!-- Description -->
<div v-if="currentDocument.description">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Description</h4>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ currentDocument.description }}</p>
</div>
<!-- Tags -->
<div v-if="currentDocument.tags">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Tags</h4>
<div class="flex flex-wrap gap-1">
<span
v-for="tag in currentDocument.tags"
:key="tag"
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
>
<span v-html="getSvgIcon('tag', 10)" class="mr-1"></span>
{{ tag }}
</span>
</div>
</div>
<!-- Metadata -->
<div v-if="currentDocument.metadata">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Metadata</h4>
<dl class="space-y-2 text-sm">
<div v-if="currentDocument.metadata.title" class="flex justify-between">
<dt class="text-gray-500">Title</dt>
<dd class="font-medium text-right">{{ currentDocument.metadata.title }}</dd>
</div>
<div v-if="currentDocument.metadata.subject" class="flex justify-between">
<dt class="text-gray-500">Subject</dt>
<dd class="font-medium text-right">{{ currentDocument.metadata.subject }}</dd>
</div>
<div v-if="currentDocument.metadata.category" class="flex justify-between">
<dt class="text-gray-500">Category</dt>
<dd class="font-medium text-right">{{ currentDocument.metadata.category }}</dd>
</div>
</dl>
</div>
</div>
</div>
</div>
</div>
<!-- Document Viewer Modal -->
<DMSDocumentViewer
:visible="showDocumentViewer"
:document="currentDocument"
@close="closeFileViewer"
@download="(doc) => console.log('Download:', doc)"
@print="(doc) => console.log('Print:', doc)"
@edit="(doc) => console.log('Edit:', doc)"
/>
<!-- Upload Dialog -->
<DMSUploadDialog
v-model:visible="showUploadDialog"
:current-path="currentPath"
@upload="handleUpload"
@close="showUploadDialog = false"
/>
<!-- Create New Dialog -->
<DMSCreateNewDialog
v-if="showCreateDialog"
@close="showCreateDialog = false"
@create="handleCreateNew"
/>
<!-- Access Request Dialog -->
<DMSAccessRequestDialog
v-if="accessRequestItem"
:item="accessRequestItem"
:visible="showAccessRequestDialog"
@close="showAccessRequestDialog = false"
@submit="submitAccessRequest"
/>
<!-- Success Notification -->
<div
v-if="requestSuccess"
class="fixed bottom-4 right-4 bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded shadow-lg z-50"
>
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm">Access request submitted successfully!</p>
</div>
</div>
</div>
<!-- Error Notification -->
<div
v-if="requestError"
class="fixed bottom-4 right-4 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-lg z-50"
>
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm">{{ requestError }}</p>
<button
@click="requestError = null"
class="text-xs text-red-600 font-medium hover:text-red-500 mt-1"
>
Dismiss
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dms-explorer {
height: calc(100vh - 64px);
}
/* Ensure scrollbars look nice across browsers */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.7);
}
</style>