generated from corrad-software/corrad-af-2024
1493 lines
67 KiB
Vue
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> |