generated from corrad-software/corrad-af-2024
Added initial cabinet view for development
This commit is contained in:
parent
8e01631a4f
commit
b00a9f94c3
558
components/dms/CabinetNavigation.vue
Normal file
558
components/dms/CabinetNavigation.vue
Normal file
@ -0,0 +1,558 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
|
||||
const store = useDmsStore();
|
||||
const expandedSections = ref({
|
||||
'public-cabinet': true,
|
||||
'my-cabinets': true,
|
||||
'private-cabinets': true
|
||||
});
|
||||
|
||||
const showAccessRequestDialog = ref(false);
|
||||
const currentCabinetForAccess = ref(null);
|
||||
const accessRequestReason = ref('');
|
||||
|
||||
// Toggle expanded state of a section
|
||||
const toggleSection = (sectionId) => {
|
||||
expandedSections.value[sectionId] = !expandedSections.value[sectionId];
|
||||
};
|
||||
|
||||
// Navigate to a cabinet
|
||||
const navigateToCabinet = (cabinetId) => {
|
||||
// In a real implementation, this would navigate to the cabinet
|
||||
console.log(`Navigating to cabinet: ${cabinetId}`);
|
||||
};
|
||||
|
||||
// Request access to a private cabinet
|
||||
const requestAccess = (cabinet) => {
|
||||
currentCabinetForAccess.value = cabinet;
|
||||
showAccessRequestDialog.value = true;
|
||||
};
|
||||
|
||||
// Submit access request
|
||||
const submitAccessRequest = async () => {
|
||||
if (!accessRequestReason.value.trim()) {
|
||||
alert('Please provide a reason for your request');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await store.requestCabinetAccess(
|
||||
currentCabinetForAccess.value.id,
|
||||
accessRequestReason.value
|
||||
);
|
||||
|
||||
// Reset form
|
||||
accessRequestReason.value = '';
|
||||
currentCabinetForAccess.value = null;
|
||||
showAccessRequestDialog.value = false;
|
||||
|
||||
// Show success message
|
||||
alert('Access request submitted successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to submit access request:', error);
|
||||
alert('Failed to submit access request. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
// Get access status icon
|
||||
const getAccessStatusIcon = (cabinet) => {
|
||||
if (cabinet.accessType === 'public') {
|
||||
return { name: 'check-circle', color: 'text-green-500' };
|
||||
}
|
||||
|
||||
if (cabinet.accessType === 'personal') {
|
||||
return { name: 'check-circle', color: 'text-green-500' };
|
||||
}
|
||||
|
||||
if (cabinet.accessType === 'private') {
|
||||
if (cabinet.hasAccess) {
|
||||
return { name: 'check-circle', color: 'text-green-500' };
|
||||
}
|
||||
|
||||
if (cabinet.isLocked) {
|
||||
return { name: 'lock', color: 'text-red-500' };
|
||||
}
|
||||
|
||||
return { name: 'clock', color: 'text-amber-500' };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Render cabinet recursively
|
||||
const renderCabinet = (cabinet, level = 0) => {
|
||||
const paddingStyle = { paddingLeft: `${level * 16 + 8}px` };
|
||||
const hasChildren = cabinet.children && cabinet.children.length > 0;
|
||||
const isExpanded = expandedSections.value[cabinet.id];
|
||||
|
||||
return (
|
||||
<div class="cabinet-item">
|
||||
<div
|
||||
class="cabinet-header flex items-center py-2 px-2 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
style={paddingStyle}
|
||||
onClick={() => hasChildren ? toggleSection(cabinet.id) : navigateToCabinet(cabinet.id)}
|
||||
>
|
||||
{hasChildren && (
|
||||
<svg
|
||||
class={`mr-1 text-gray-500 transition-transform ${isExpanded ? 'transform rotate-90' : ''}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
<span class={`ml-${hasChildren ? '0' : '4'} flex-1`}>
|
||||
{cabinet.type === 'tag' ? (
|
||||
<span class="flex items-center">
|
||||
<span class="w-2 h-2 rounded-full bg-purple-500 mr-1"></span>
|
||||
<span class="text-sm">{cabinet.name}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span class="flex items-center">
|
||||
{cabinet.type === 'cabinet' && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="mr-1"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
<span class="text-sm">{cabinet.name}</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* Status icon */}
|
||||
{cabinet.accessType === 'private' && !cabinet.hasAccess && (
|
||||
<button
|
||||
class="text-blue-500 hover:text-blue-700 p-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
requestAccess(cabinet);
|
||||
}}
|
||||
disabled={cabinet.accessRequestStatus === 'rejected'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
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>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{getAccessStatusIcon(cabinet) && (
|
||||
<span class={getAccessStatusIcon(cabinet).color}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
{getAccessStatusIcon(cabinet).name === 'check-circle' && (
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
)}
|
||||
|
||||
{getAccessStatusIcon(cabinet).name === 'clock' && (
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
)}
|
||||
|
||||
{getAccessStatusIcon(cabinet).name === 'lock' && (
|
||||
<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>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasChildren && isExpanded && (
|
||||
<div class="cabinet-children">
|
||||
{cabinet.children.map(child => renderCabinet(child, level + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cabinet-navigation h-full overflow-y-auto">
|
||||
<!-- Public cabinets -->
|
||||
<div class="navigation-section">
|
||||
<div
|
||||
@click="toggleSection('public-cabinet')"
|
||||
class="section-header flex items-center p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="mr-1 text-gray-500"
|
||||
:class="expandedSections['public-cabinet'] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<span class="font-medium text-sm">📁 Public Cabinet</span>
|
||||
</div>
|
||||
|
||||
<div v-if="expandedSections['public-cabinet']" class="section-content">
|
||||
<div v-for="cabinet in store.cabinets.filter(c => c.accessType === 'public')" :key="cabinet.id">
|
||||
<div v-for="child in cabinet.children" :key="child.id" class="menu-item">
|
||||
<div
|
||||
class="flex items-center pl-8 pr-3 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
@click="navigateToCabinet(child.id)"
|
||||
>
|
||||
<svg class="mr-2 text-gray-500" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">{{ child.name }}</span>
|
||||
<span class="ml-auto text-green-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- My Cabinets -->
|
||||
<div class="navigation-section">
|
||||
<div
|
||||
@click="toggleSection('my-cabinets')"
|
||||
class="section-header flex items-center p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="mr-1 text-gray-500"
|
||||
:class="expandedSections['my-cabinets'] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<span class="font-medium text-sm">📁 My Cabinets</span>
|
||||
</div>
|
||||
|
||||
<div v-if="expandedSections['my-cabinets']" class="section-content">
|
||||
<template v-if="store.personalCabinets">
|
||||
<div v-for="cabinet in store.personalCabinets.children" :key="cabinet.id">
|
||||
<div
|
||||
class="flex items-center pl-8 pr-3 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
@click="() => {
|
||||
toggleSection(cabinet.id);
|
||||
navigateToCabinet(cabinet.id);
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
v-if="cabinet.children && cabinet.children.length > 0"
|
||||
class="mr-2 text-gray-500"
|
||||
:class="expandedSections[cabinet.id] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<svg v-else class="mr-2 text-gray-500" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">{{ cabinet.name }}</span>
|
||||
<span class="ml-auto text-green-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Recursive children -->
|
||||
<template v-if="expandedSections[cabinet.id] && cabinet.children && cabinet.children.length > 0">
|
||||
<div
|
||||
v-for="child in cabinet.children"
|
||||
:key="child.id"
|
||||
class="pl-4"
|
||||
>
|
||||
<div
|
||||
class="flex items-center pl-8 pr-3 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
@click="() => {
|
||||
if (child.children && child.children.length > 0) {
|
||||
toggleSection(child.id);
|
||||
}
|
||||
navigateToCabinet(child.id);
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
v-if="child.children && child.children.length > 0"
|
||||
class="mr-2 text-gray-500"
|
||||
:class="expandedSections[child.id] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<svg v-else class="mr-2 text-gray-500" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">{{ child.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Third level -->
|
||||
<template v-if="expandedSections[child.id] && child.children && child.children.length > 0">
|
||||
<div
|
||||
v-for="grandchild in child.children"
|
||||
:key="grandchild.id"
|
||||
class="pl-4"
|
||||
>
|
||||
<!-- Display tag groups -->
|
||||
<template v-if="grandchild.type === 'cabinet'">
|
||||
<div
|
||||
class="flex items-center pl-12 pr-3 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
@click="() => {
|
||||
if (grandchild.children && grandchild.children.length > 0) {
|
||||
toggleSection(grandchild.id);
|
||||
}
|
||||
navigateToCabinet(grandchild.id);
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
v-if="grandchild.children && grandchild.children.length > 0"
|
||||
class="mr-2 text-gray-500"
|
||||
:class="expandedSections[grandchild.id] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<svg v-else class="mr-2 text-gray-500" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">{{ grandchild.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<template v-if="expandedSections[grandchild.id] && grandchild.children && grandchild.children.length > 0">
|
||||
<div
|
||||
v-for="tag in grandchild.children"
|
||||
:key="tag.id"
|
||||
@click="navigateToCabinet(tag.id)"
|
||||
class="flex items-center pl-16 pr-3 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full bg-purple-500 mr-2"></span>
|
||||
<span class="text-sm">{{ tag.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Private Cabinets -->
|
||||
<div class="navigation-section">
|
||||
<div
|
||||
@click="toggleSection('private-cabinets')"
|
||||
class="section-header flex items-center p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="mr-1 text-gray-500"
|
||||
:class="expandedSections['private-cabinets'] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<span class="font-medium text-sm">🔒 Private Cabinets</span>
|
||||
</div>
|
||||
|
||||
<div v-if="expandedSections['private-cabinets']" class="section-content">
|
||||
<template v-if="store.privateCabinets">
|
||||
<div v-for="cabinet in store.privateCabinets.children" :key="cabinet.id">
|
||||
<div
|
||||
class="flex items-center pl-8 pr-3 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
@click="cabinet.hasAccess ? navigateToCabinet(cabinet.id) : null"
|
||||
>
|
||||
<svg class="mr-2 text-gray-500" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">{{ cabinet.name }}</span>
|
||||
|
||||
<!-- Status indicators -->
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<!-- Request access button -->
|
||||
<button
|
||||
v-if="!cabinet.hasAccess && cabinet.accessRequestStatus !== 'rejected'"
|
||||
@click.stop="requestAccess(cabinet)"
|
||||
class="text-blue-500 hover:text-blue-700 p-1"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Access status indicator -->
|
||||
<span
|
||||
v-if="cabinet.accessRequestStatus === 'pending'"
|
||||
class="text-amber-500"
|
||||
title="Access request pending"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-else-if="cabinet.isLocked"
|
||||
class="text-red-500"
|
||||
title="Access locked"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-else-if="cabinet.hasAccess"
|
||||
class="text-green-500"
|
||||
title="Access granted"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Access request dialog -->
|
||||
<rs-dialog v-model="showAccessRequestDialog" title="Request Access">
|
||||
<template v-if="currentCabinetForAccess">
|
||||
<p class="mb-4">You are requesting access to: <strong>{{ currentCabinetForAccess.name }}</strong></p>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Reason for Access Request
|
||||
</label>
|
||||
<textarea
|
||||
v-model="accessRequestReason"
|
||||
rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
placeholder="Please provide a reason for your request..."
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<rs-button color="secondary" @click="showAccessRequestDialog = false">
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button color="primary" @click="submitAccessRequest">
|
||||
Submit Request
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cabinet-navigation {
|
||||
width: 260px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
</style>
|
186
components/dms/dialogs/DMSAccessRequestDialog.vue
Normal file
186
components/dms/dialogs/DMSAccessRequestDialog.vue
Normal file
@ -0,0 +1,186 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
|
||||
const props = defineProps({
|
||||
document: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'requestSubmitted']);
|
||||
|
||||
// Store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Form state
|
||||
const selectedAccessLevel = ref('view');
|
||||
const justification = ref('');
|
||||
const isSubmitting = ref(false);
|
||||
const formError = ref('');
|
||||
|
||||
// Access level options
|
||||
const accessLevels = [
|
||||
{ id: 'view', label: 'View Only', description: 'Can only view the document' },
|
||||
{ id: 'download', label: 'Download', description: 'Can view and download the document' },
|
||||
{ id: 'print', label: 'Print', description: 'Can view, download, and print the document' },
|
||||
{ id: 'edit', label: 'Edit', description: 'Can view, download, print, and edit the document' },
|
||||
{ id: 'full', label: 'Full Access', description: 'Has complete control over the document' }
|
||||
];
|
||||
|
||||
// Computed properties
|
||||
const documentTitle = computed(() => {
|
||||
return props.document?.name || 'Document';
|
||||
});
|
||||
|
||||
const documentIconSvg = computed(() => {
|
||||
const extension = props.document?.extension?.toLowerCase();
|
||||
|
||||
if (extension === 'pdf')
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" 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="M9 15h6"></path><path d="M9 11h6"></path></svg>';
|
||||
|
||||
if (['doc', 'docx'].includes(extension))
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" 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>';
|
||||
|
||||
if (['xls', 'xlsx'].includes(extension))
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" 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="11" y1="12" x2="11" y2="18"></line></svg>';
|
||||
|
||||
if (['ppt', 'pptx'].includes(extension))
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" 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></svg>';
|
||||
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" 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>';
|
||||
});
|
||||
|
||||
const currentAccessLevel = computed(() => {
|
||||
// This would be determined from actual permissions in production
|
||||
return 'None';
|
||||
});
|
||||
|
||||
// Methods
|
||||
const closeDialog = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const submitRequest = async () => {
|
||||
// Validate form
|
||||
if (!selectedAccessLevel.value) {
|
||||
formError.value = 'Please select an access level';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!justification.value.trim()) {
|
||||
formError.value = 'Please provide a justification for your request';
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
formError.value = '';
|
||||
|
||||
try {
|
||||
// Submit the request to the store
|
||||
const request = await dmsStore.requestAccess(props.document.id, selectedAccessLevel.value);
|
||||
|
||||
// Emit success event
|
||||
emit('requestSubmitted', request);
|
||||
|
||||
// Close the dialog
|
||||
closeDialog();
|
||||
} catch (error) {
|
||||
formError.value = 'Failed to submit access request. Please try again.';
|
||||
console.error('Access request error:', error);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<rs-modal
|
||||
:visible="visible"
|
||||
@close="closeDialog"
|
||||
title="Request Document Access"
|
||||
size="md"
|
||||
>
|
||||
<template #body>
|
||||
<div class="p-4">
|
||||
<!-- Document info -->
|
||||
<div class="flex items-center mb-6 bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<span class="text-primary mr-4" v-html="documentIconSvg"></span>
|
||||
<div>
|
||||
<h3 class="font-medium text-lg">{{ documentTitle }}</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
Current Access Level: <span class="font-medium">{{ currentAccessLevel }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form error -->
|
||||
<div v-if="formError" class="mb-4 p-3 bg-red-100 text-red-800 rounded-md">
|
||||
{{ formError }}
|
||||
</div>
|
||||
|
||||
<!-- Access level selection -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">Request Access Level:</label>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="level in accessLevels"
|
||||
:key="level.id"
|
||||
class="flex items-start p-3 border rounded-md cursor-pointer"
|
||||
:class="selectedAccessLevel === level.id
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800'"
|
||||
@click="selectedAccessLevel = level.id"
|
||||
>
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<div class="w-4 h-4 rounded-full border-2 flex items-center justify-center"
|
||||
:class="selectedAccessLevel === level.id
|
||||
? 'border-primary'
|
||||
: 'border-gray-400'"
|
||||
>
|
||||
<div
|
||||
v-if="selectedAccessLevel === level.id"
|
||||
class="w-2 h-2 rounded-full bg-primary"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<div class="font-medium">{{ level.label }}</div>
|
||||
<p class="text-sm text-gray-500">{{ level.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Justification -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">Justification:</label>
|
||||
<textarea
|
||||
v-model="justification"
|
||||
rows="3"
|
||||
placeholder="Please explain why you need access to this document..."
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-800"
|
||||
></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Your request will be reviewed by the document owner or administrator.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<rs-button color="secondary" @click="closeDialog" :disabled="isSubmitting">
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button color="primary" @click="submitRequest" :disabled="isSubmitting">
|
||||
<svg v-if="isSubmitting" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin mr-2"><path d="M12 22C6.5 22 2 17.5 2 12S6.5 2 12 2s10 4.5 10 10"></path><path d="M12 2v4"></path><path d="M12 18v4"></path></svg>
|
||||
<span>Submit Request</span>
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-modal>
|
||||
</template>
|
445
components/dms/explorer/DMSExplorer.vue
Normal file
445
components/dms/explorer/DMSExplorer.vue
Normal file
@ -0,0 +1,445 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
// Props for the explorer
|
||||
const props = defineProps({
|
||||
initialPath: {
|
||||
type: String,
|
||||
default: '/'
|
||||
},
|
||||
viewMode: {
|
||||
type: String,
|
||||
default: 'list', // list, grid, details
|
||||
}
|
||||
});
|
||||
|
||||
// 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({});
|
||||
|
||||
// Mock data - replace with actual API calls
|
||||
const allItems = ref([]);
|
||||
const router = useRouter();
|
||||
|
||||
// Computed properties
|
||||
const currentItems = computed(() => {
|
||||
// Filter items based on current path
|
||||
return allItems.value.filter(item => item.parentPath === currentPath.value);
|
||||
});
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
// Split the path and create breadcrumb items
|
||||
if (currentPath.value === '/') return [{ name: 'Home', path: '/' }];
|
||||
|
||||
const paths = currentPath.value.split('/').filter(Boolean);
|
||||
let breadcrumbPath = '';
|
||||
|
||||
return [
|
||||
{ name: 'Home', path: '/' },
|
||||
...paths.map(segment => {
|
||||
breadcrumbPath += `/${segment}`;
|
||||
return {
|
||||
name: segment.charAt(0).toUpperCase() + segment.slice(1),
|
||||
path: breadcrumbPath
|
||||
};
|
||||
})
|
||||
];
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
// Example item structure
|
||||
allItems.value = [
|
||||
{ id: 'cabinet1', name: 'Cabinet 1', type: 'cabinet', parentPath: '/' },
|
||||
{ id: 'cabinet2', name: 'Cabinet 2', type: 'cabinet', parentPath: '/' },
|
||||
{ id: 'drawer1', name: 'Drawer 1', type: 'drawer', parentPath: '/cabinet1' },
|
||||
{ id: 'drawer2', name: 'Drawer 2', type: 'drawer', parentPath: '/cabinet1' },
|
||||
{ id: 'folder1', name: 'Folder 1', type: 'folder', parentPath: '/cabinet1/drawer1' },
|
||||
{ id: 'folder2', name: 'Folder 2', type: 'folder', parentPath: '/cabinet1/drawer1' },
|
||||
{ id: 'subfolder1', name: 'Subfolder 1', type: 'subfolder', parentPath: '/cabinet1/drawer1/folder1' },
|
||||
{ id: 'file1', name: 'Document 1.pdf', type: 'file', parentPath: '/cabinet1/drawer1/folder1/subfolder1', extension: 'pdf', size: '1.2 MB', modified: '2023-10-15' },
|
||||
{ id: 'file2', name: 'Spreadsheet.xlsx', type: 'file', parentPath: '/cabinet1/drawer1/folder1/subfolder1', extension: 'xlsx', size: '0.5 MB', modified: '2023-10-16' }
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('Failed to load items:', error);
|
||||
// Show error notification
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectItem = (item) => {
|
||||
selectedItem.value = item;
|
||||
emit('itemSelected', item);
|
||||
};
|
||||
|
||||
const navigateTo = (path) => {
|
||||
currentPath.value = path;
|
||||
selectedItem.value = null;
|
||||
emit('pathChanged', path);
|
||||
loadItems();
|
||||
};
|
||||
|
||||
const navigateToItem = (item) => {
|
||||
if (item.type === 'file') {
|
||||
selectItem(item);
|
||||
return;
|
||||
}
|
||||
|
||||
let newPath;
|
||||
if (item.type === 'cabinet') newPath = `/${item.id}`;
|
||||
else if (item.type === 'drawer') newPath = `/cabinet1/${item.id}`;
|
||||
else if (item.type === 'folder') newPath = `/cabinet1/drawer1/${item.id}`;
|
||||
else if (item.type === 'subfolder') newPath = `/cabinet1/drawer1/folder1/${item.id}`;
|
||||
else newPath = currentPath.value;
|
||||
|
||||
navigateTo(newPath);
|
||||
};
|
||||
|
||||
const toggleTreeItem = (path) => {
|
||||
treeExpanded.value[path] = !treeExpanded.value[path];
|
||||
};
|
||||
|
||||
const changeViewMode = (mode) => {
|
||||
viewMode.value = mode;
|
||||
emit('viewModeChanged', mode);
|
||||
};
|
||||
|
||||
// Format file size
|
||||
const formatSize = (size) => {
|
||||
return size;
|
||||
};
|
||||
|
||||
// Get icon for item type
|
||||
const getItemIcon = (item) => {
|
||||
switch (item.type) {
|
||||
case 'cabinet': return 'cabinet';
|
||||
case 'drawer': return 'folder';
|
||||
case 'folder': return 'folder';
|
||||
case 'subfolder': return 'folder';
|
||||
case 'file':
|
||||
if (item.extension === 'pdf') return 'pdf';
|
||||
if (['doc', 'docx'].includes(item.extension)) return 'doc';
|
||||
if (['xls', 'xlsx'].includes(item.extension)) return 'sheet';
|
||||
return 'file';
|
||||
default: return 'folder';
|
||||
}
|
||||
};
|
||||
|
||||
// Get SVG path based on icon type
|
||||
const getSvgIcon = (iconType) => {
|
||||
switch (iconType) {
|
||||
case 'cabinet':
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>`;
|
||||
case 'folder':
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>`;
|
||||
case 'pdf':
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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="M9 15h6"></path><path d="M9 11h6"></path></svg>`;
|
||||
case 'doc':
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>`;
|
||||
case 'sheet':
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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="11" y1="12" x2="11" y2="18"></line></svg>`;
|
||||
default:
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>`;
|
||||
}
|
||||
};
|
||||
|
||||
// Render SVG from string
|
||||
const renderSvgIcon = (iconType, size = 24) => {
|
||||
const svgString = getSvgIcon(iconType);
|
||||
return svgString.replace('width="24"', `width="${size}"`).replace('height="24"', `height="${size}"`);
|
||||
};
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
loadItems();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-explorer bg-gray-50 dark:bg-gray-900 h-full flex flex-col">
|
||||
<!-- Toolbar -->
|
||||
<div class="dms-toolbar p-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
|
||||
</button>
|
||||
<button class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</button>
|
||||
<div class="dms-breadcrumbs flex items-center space-x-1">
|
||||
<template v-for="(crumb, index) in breadcrumbs" :key="index">
|
||||
<span v-if="index > 0" class="text-gray-400">/</span>
|
||||
<button
|
||||
@click="navigateTo(crumb.path)"
|
||||
class="text-primary hover:underline p-1 rounded"
|
||||
:class="index === breadcrumbs.length - 1 ? 'font-medium' : ''"
|
||||
>
|
||||
{{ crumb.name }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="changeViewMode('list')"
|
||||
class="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
:class="viewMode === 'list' ? 'bg-gray-200 dark:bg-gray-700' : ''"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
:class="viewMode === 'grid' ? 'bg-gray-200 dark:bg-gray-700' : ''"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
:class="viewMode === 'details' ? 'bg-gray-200 dark:bg-gray-700' : ''"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
|
||||
|
||||
<!-- 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">
|
||||
<div class="tree-item p-1">
|
||||
<div
|
||||
@click="navigateTo('/')"
|
||||
class="flex items-center space-x-1 p-1 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
:class="currentPath === '/' ? 'bg-primary/20 text-primary' : ''"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
|
||||
<span>Home</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tree items, recursive component would be better in production -->
|
||||
<div v-for="cabinet in allItems.filter(i => i.type === 'cabinet')" :key="cabinet.id" class="tree-item p-1">
|
||||
<div
|
||||
@click="toggleTreeItem(cabinet.id)"
|
||||
class="flex items-center space-x-1 p-1 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg v-if="treeExpanded[cabinet.id]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>
|
||||
<span>{{ cabinet.name }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="treeExpanded[cabinet.id]" class="pl-5">
|
||||
<div
|
||||
v-for="drawer in allItems.filter(i => i.parentPath === `/${cabinet.id}`)"
|
||||
:key="drawer.id"
|
||||
class="tree-item"
|
||||
>
|
||||
<div
|
||||
@click="toggleTreeItem(drawer.id)"
|
||||
class="flex items-center space-x-1 p-1 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg v-if="treeExpanded[drawer.id]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
|
||||
<span>{{ drawer.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Further nesting would be handled with recursive components in production -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content area -->
|
||||
<div class="dms-content flex-1 overflow-y-auto">
|
||||
<!-- Loading indicator -->
|
||||
<div v-if="isLoading" class="flex justify-center items-center h-full">
|
||||
<Loading />
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="currentItems.length === 0" class="flex flex-col justify-center items-center h-full text-gray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" 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>
|
||||
<p class="mt-2">This folder is empty</p>
|
||||
</div>
|
||||
|
||||
<!-- List view -->
|
||||
<div v-else-if="viewMode === 'list'" class="p-4">
|
||||
<div
|
||||
v-for="item in currentItems"
|
||||
:key="item.id"
|
||||
@click="navigateToItem(item)"
|
||||
class="flex items-center p-2 border-b border-gray-100 dark:border-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="selectedItem?.id === item.id ? 'bg-primary/10 dark:bg-primary/20' : ''"
|
||||
>
|
||||
<span class="mr-3" v-html="renderSvgIcon(getItemIcon(item), 24)"></span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">{{ item.name }}</div>
|
||||
<div v-if="item.type === 'file'" class="text-xs text-gray-500">
|
||||
{{ item.extension.toUpperCase() }} · {{ item.size }} · Modified {{ item.modified }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid view -->
|
||||
<div v-else-if="viewMode === 'grid'" class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
|
||||
<div
|
||||
v-for="item in currentItems"
|
||||
:key="item.id"
|
||||
@click="navigateToItem(item)"
|
||||
class="flex flex-col items-center p-4 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 text-center"
|
||||
:class="selectedItem?.id === item.id ? 'bg-primary/10 dark:bg-primary/20' : ''"
|
||||
>
|
||||
<span class="mb-2" v-html="renderSvgIcon(getItemIcon(item), 48)"></span>
|
||||
<div class="w-full truncate text-sm font-medium">{{ item.name }}</div>
|
||||
<div v-if="item.type === 'file'" class="text-xs text-gray-500">{{ item.size }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details view -->
|
||||
<div v-else-if="viewMode === 'details'" class="p-4">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</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">Type</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">Size</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">Modified</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="navigateToItem(item)"
|
||||
class="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="selectedItem?.id === item.id ? 'bg-primary/10 dark:bg-primary/20' : ''"
|
||||
>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-3" v-html="renderSvgIcon(getItemIcon(item), 24)"></span>
|
||||
<div class="font-medium">{{ item.name }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 capitalize">
|
||||
{{ item.type }}
|
||||
</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.type === 'file' ? item.modified : '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details panel - shown when a file is selected -->
|
||||
<div v-if="selectedItem && selectedItem.type === 'file'" class="dms-details w-72 border-l border-gray-200 dark:border-gray-700 overflow-y-auto p-4">
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
<span class="mb-3" v-html="renderSvgIcon(getItemIcon(selectedItem), 64)"></span>
|
||||
<h3 class="text-lg font-medium text-center">{{ selectedItem.name }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ selectedItem.type }}</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Properties</h4>
|
||||
<div class="text-sm">
|
||||
<div class="flex justify-between py-1">
|
||||
<span class="text-gray-500">Type</span>
|
||||
<span>{{ selectedItem.extension.toUpperCase() }} File</span>
|
||||
</div>
|
||||
<div class="flex justify-between py-1">
|
||||
<span class="text-gray-500">Size</span>
|
||||
<span>{{ selectedItem.size }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between py-1">
|
||||
<span class="text-gray-500">Modified</span>
|
||||
<span>{{ selectedItem.modified }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Actions</h4>
|
||||
<div class="flex flex-col gap-2">
|
||||
<rs-button size="sm" block>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
Preview
|
||||
</rs-button>
|
||||
<rs-button size="sm" block>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
Download
|
||||
</rs-button>
|
||||
<rs-button size="sm" block>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
|
||||
Print
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Access Control</h4>
|
||||
<p class="text-sm text-gray-500 mb-2">You have full access to this document</p>
|
||||
<rs-button size="sm" block color="secondary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><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>
|
||||
Request More Access
|
||||
</rs-button>
|
||||
</div>
|
||||
</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>
|
146
components/dms/explorer/DMSTreeView.vue
Normal file
146
components/dms/explorer/DMSTreeView.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true
|
||||
},
|
||||
currentPath: {
|
||||
type: String,
|
||||
default: '/'
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
parentPath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['navigate']);
|
||||
|
||||
// Local state for expanded items
|
||||
const expandedItems = ref({});
|
||||
|
||||
// Toggle item expansion
|
||||
const toggleItem = (item) => {
|
||||
expandedItems.value[item.id] = !expandedItems.value[item.id];
|
||||
};
|
||||
|
||||
// Navigate to an item
|
||||
const navigateToItem = (item) => {
|
||||
let path;
|
||||
|
||||
// Build path based on item type
|
||||
if (item.type === 'file') {
|
||||
// For files, we navigate to the parent folder but select the file
|
||||
path = item.parentPath;
|
||||
} else {
|
||||
// For folders, we navigate to the folder itself
|
||||
path = item.path || `${props.parentPath}/${item.id}`;
|
||||
}
|
||||
|
||||
emit('navigate', { path, item });
|
||||
};
|
||||
|
||||
// Check if item is on the current path
|
||||
const isOnCurrentPath = (item) => {
|
||||
if (props.currentPath === '/') return item.parentPath === '/';
|
||||
|
||||
const fullItemPath = `${props.parentPath}/${item.id}`;
|
||||
return props.currentPath.startsWith(fullItemPath) || props.currentPath === fullItemPath;
|
||||
};
|
||||
|
||||
// Get icon for item type
|
||||
const getItemIcon = (item) => {
|
||||
switch (item.type) {
|
||||
case 'cabinet':
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>';
|
||||
case 'drawer':
|
||||
case 'folder':
|
||||
case 'subfolder':
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>';
|
||||
case 'file':
|
||||
if (item.extension === 'pdf')
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="M9 15h6"></path><path d="M9 11h6"></path></svg>';
|
||||
if (['doc', 'docx'].includes(item.extension))
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>';
|
||||
if (['xls', 'xlsx'].includes(item.extension))
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="11" y1="12" x2="11" y2="18"></line></svg>';
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>';
|
||||
default:
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>';
|
||||
}
|
||||
};
|
||||
|
||||
// Filter child items for the current parent
|
||||
const childItems = computed(() => {
|
||||
return props.items.filter(item => {
|
||||
// Root level items
|
||||
if (props.level === 0 && props.parentPath === '') {
|
||||
return item.parentPath === '/';
|
||||
}
|
||||
|
||||
// Other level items
|
||||
return item.parentPath === props.parentPath ||
|
||||
(props.level === 1 && item.parentPath === `/${props.items[0]?.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Get the next level items for a parent
|
||||
const getChildrenForParent = (parentItem) => {
|
||||
const parentPath = parentItem.path || `${props.parentPath}/${parentItem.id}`;
|
||||
return props.items.filter(item => item.parentPath === parentPath);
|
||||
};
|
||||
|
||||
// Check if an item has children
|
||||
const hasChildren = (item) => {
|
||||
const parentPath = item.path || `${props.parentPath}/${item.id}`;
|
||||
return props.items.some(i => i.parentPath === parentPath);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-tree-view" :style="{ paddingLeft: level > 0 ? '16px' : '0' }">
|
||||
<div
|
||||
v-for="item in childItems"
|
||||
:key="item.id"
|
||||
class="tree-item"
|
||||
>
|
||||
<div
|
||||
@click="hasChildren(item) ? toggleItem(item) : navigateToItem(item)"
|
||||
class="flex items-center gap-1 p-1 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
:class="isOnCurrentPath(item) ? 'bg-primary/20 text-primary' : ''"
|
||||
>
|
||||
<span v-if="hasChildren(item)" class="w-5">
|
||||
<svg v-if="expandedItems[item.id]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
</span>
|
||||
<span v-else class="w-5"></span> <!-- Spacer -->
|
||||
|
||||
<span class="w-5" v-html="getItemIcon(item)"></span>
|
||||
<span class="truncate">{{ item.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Recursive component for nested levels -->
|
||||
<DMSTreeView
|
||||
v-if="expandedItems[item.id] && hasChildren(item)"
|
||||
:items="props.items"
|
||||
:current-path="props.currentPath"
|
||||
:level="props.level + 1"
|
||||
:parent-path="item.path || `${props.parentPath}/${item.id}`"
|
||||
@navigate="(data) => emit('navigate', data)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tree-item {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
</style>
|
386
components/dms/navigation/DMSNavigation.vue
Normal file
386
components/dms/navigation/DMSNavigation.vue
Normal file
@ -0,0 +1,386 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
|
||||
const store = useDmsStore();
|
||||
const activeSection = ref('my-documents');
|
||||
const expandedSections = ref({
|
||||
'core-navigation': true,
|
||||
'organizational-tools': true,
|
||||
'document-actions': false,
|
||||
'access-permissions': false,
|
||||
'reports-analytics': false,
|
||||
'admin-settings': false,
|
||||
'advanced': false
|
||||
});
|
||||
|
||||
// Toggle expanded state of a section
|
||||
const toggleSection = (section) => {
|
||||
expandedSections.value[section] = !expandedSections.value[section];
|
||||
};
|
||||
|
||||
// Set active menu item
|
||||
const setActive = (section) => {
|
||||
activeSection.value = section;
|
||||
// You could emit an event or update a store here to change the main content
|
||||
};
|
||||
|
||||
// Check if item is active
|
||||
const isActive = (section) => {
|
||||
return activeSection.value === section;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-navigation h-full overflow-y-auto bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700">
|
||||
<!-- Core Navigation -->
|
||||
<div class="navigation-section">
|
||||
<div
|
||||
@click="toggleSection('core-navigation')"
|
||||
class="section-header flex items-center p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="mr-1 text-gray-500"
|
||||
:class="expandedSections['core-navigation'] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<span class="font-medium text-sm">📁 Core Navigation</span>
|
||||
</div>
|
||||
|
||||
<div v-if="expandedSections['core-navigation']" class="section-content">
|
||||
<div
|
||||
@click="setActive('my-documents')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('my-documents') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">My Documents</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('shared-with-me')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('shared-with-me') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
<span class="text-sm">Shared with Me</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('recent-documents')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('recent-documents') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
<span class="text-sm">Recent Documents</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('favorites')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('favorites') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
</svg>
|
||||
<span class="text-sm">Favorites</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('trash')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('trash') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
<span class="text-sm">Trash</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organizational Tools -->
|
||||
<div class="navigation-section">
|
||||
<div
|
||||
@click="toggleSection('organizational-tools')"
|
||||
class="section-header flex items-center p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="mr-1 text-gray-500"
|
||||
:class="expandedSections['organizational-tools'] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<span class="font-medium text-sm">🗂️ Organizational Tools</span>
|
||||
</div>
|
||||
|
||||
<div v-if="expandedSections['organizational-tools']" class="section-content">
|
||||
<div
|
||||
@click="setActive('folders')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('folders') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">Folders</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('tags')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('tags') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">Tags/Labels</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('categories')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('categories') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">Categories</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('workspaces')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('workspaces') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||
<line x1="9" y1="21" x2="9" y2="9"></line>
|
||||
</svg>
|
||||
<span class="text-sm">Workspaces/Projects</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document Actions -->
|
||||
<div class="navigation-section">
|
||||
<div
|
||||
@click="toggleSection('document-actions')"
|
||||
class="section-header flex items-center p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="mr-1 text-gray-500"
|
||||
:class="expandedSections['document-actions'] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<span class="font-medium text-sm">📄 Document Actions</span>
|
||||
</div>
|
||||
|
||||
<div v-if="expandedSections['document-actions']" class="section-content">
|
||||
<div
|
||||
@click="setActive('upload-document')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('upload-document') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">Upload Document</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('create-document')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('create-document') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">Create New Document</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('bulk-actions')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('bulk-actions') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
<span class="text-sm">Bulk Actions</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('version-history')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('version-history') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
<span class="text-sm">Version History</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('document-properties')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('document-properties') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
<span class="text-sm">Document Properties</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Access & Permissions -->
|
||||
<div class="navigation-section">
|
||||
<div
|
||||
@click="toggleSection('access-permissions')"
|
||||
class="section-header flex items-center p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<svg
|
||||
class="mr-1 text-gray-500"
|
||||
:class="expandedSections['access-permissions'] ? 'transform rotate-90' : ''"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
<span class="font-medium text-sm">🔒 Access & Permissions</span>
|
||||
</div>
|
||||
|
||||
<div v-if="expandedSections['access-permissions']" class="section-content">
|
||||
<div
|
||||
@click="setActive('shared-settings')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('shared-settings') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path>
|
||||
<polyline points="16 6 12 2 8 6"></polyline>
|
||||
<line x1="12" y1="2" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<span class="text-sm">Shared Settings</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('audit-logs')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('audit-logs') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">Audit Logs</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="setActive('access-requests')"
|
||||
class="menu-item flex items-center pl-8 pr-3 py-2 cursor-pointer"
|
||||
:class="isActive('access-requests') ? 'bg-primary/10 text-primary' : 'hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||
>
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="text-sm">Access Requests</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional sections would follow the same pattern -->
|
||||
<!-- For brevity, I'm not including all sections, but they would have the same structure -->
|
||||
|
||||
<!-- Footer with quick actions -->
|
||||
<div class="absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 p-3 bg-white dark:bg-gray-900">
|
||||
<!-- Removing the upload and create new buttons from here -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dms-navigation {
|
||||
width: 260px;
|
||||
position: relative;
|
||||
padding-bottom: 100px; /* Space for footer buttons */
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
</style>
|
265
components/dms/viewers/DMSDocumentViewer.vue
Normal file
265
components/dms/viewers/DMSDocumentViewer.vue
Normal file
@ -0,0 +1,265 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
document: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
viewMode: {
|
||||
type: String,
|
||||
default: 'preview' // preview, fullscreen
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'download', 'print', 'requestAccess']);
|
||||
|
||||
// State
|
||||
const isLoading = ref(true);
|
||||
const zoomLevel = ref(100); // percentage
|
||||
const currentPage = ref(1);
|
||||
const totalPages = ref(1);
|
||||
const viewerReady = ref(false);
|
||||
|
||||
// Computed properties
|
||||
const documentType = computed(() => {
|
||||
const extension = props.document.extension?.toLowerCase();
|
||||
|
||||
if (['pdf'].includes(extension)) return 'pdf';
|
||||
if (['doc', 'docx'].includes(extension)) return 'word';
|
||||
if (['xls', 'xlsx'].includes(extension)) return 'excel';
|
||||
if (['ppt', 'pptx'].includes(extension)) return 'powerpoint';
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) return 'image';
|
||||
if (['txt', 'md', 'json', 'csv'].includes(extension)) return 'text';
|
||||
|
||||
return 'generic';
|
||||
});
|
||||
|
||||
const documentUrl = computed(() => {
|
||||
// In production, this would come from the document API
|
||||
return props.document.url || `#`;
|
||||
});
|
||||
|
||||
const documentTitle = computed(() => {
|
||||
return props.document.name || 'Untitled Document';
|
||||
});
|
||||
|
||||
// Get SVG icon based on document type
|
||||
const getDocumentIcon = (type, size = 24) => {
|
||||
let svg = '';
|
||||
|
||||
switch (type) {
|
||||
case 'pdf':
|
||||
svg = `<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="M9 15h6"></path><path d="M9 11h6"></path></svg>`;
|
||||
break;
|
||||
case 'word':
|
||||
svg = `<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>`;
|
||||
break;
|
||||
case 'excel':
|
||||
svg = `<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="11" y1="12" x2="11" y2="18"></line></svg>`;
|
||||
break;
|
||||
case 'powerpoint':
|
||||
svg = `<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></svg>`;
|
||||
break;
|
||||
case 'image':
|
||||
svg = `<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>`;
|
||||
break;
|
||||
case 'text':
|
||||
svg = `<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>`;
|
||||
break;
|
||||
default:
|
||||
svg = `<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>`;
|
||||
}
|
||||
|
||||
return svg;
|
||||
};
|
||||
|
||||
// Methods
|
||||
const closeViewer = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const downloadDocument = () => {
|
||||
emit('download', props.document);
|
||||
};
|
||||
|
||||
const printDocument = () => {
|
||||
emit('print', props.document);
|
||||
};
|
||||
|
||||
const requestAccess = () => {
|
||||
emit('requestAccess', props.document);
|
||||
};
|
||||
|
||||
const zoomIn = () => {
|
||||
zoomLevel.value += 10;
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
if (zoomLevel.value > 50) {
|
||||
zoomLevel.value -= 10;
|
||||
}
|
||||
};
|
||||
|
||||
const resetZoom = () => {
|
||||
zoomLevel.value = 100;
|
||||
};
|
||||
|
||||
const goToPage = (page) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page;
|
||||
}
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage.value < totalPages.value) {
|
||||
currentPage.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage.value > 1) {
|
||||
currentPage.value--;
|
||||
}
|
||||
};
|
||||
|
||||
// Mock viewer loading for demonstration
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
isLoading.value = false;
|
||||
viewerReady.value = true;
|
||||
totalPages.value = 5; // Mock total pages
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dms-document-viewer bg-gray-900 text-white flex flex-col"
|
||||
:class="viewMode === 'fullscreen' ? 'fixed inset-0 z-50' : 'h-full'"
|
||||
>
|
||||
<!-- Toolbar -->
|
||||
<div class="viewer-toolbar p-2 flex items-center justify-between bg-gray-800">
|
||||
<div class="flex items-center gap-3">
|
||||
<button @click="closeViewer" class="p-2 rounded hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
|
||||
</button>
|
||||
<h3 class="font-medium truncate max-w-md">{{ documentTitle }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Page navigation (for multi-page documents) -->
|
||||
<div v-if="['pdf', 'word', 'powerpoint'].includes(documentType)" class="flex items-center gap-1">
|
||||
<button @click="prevPage" class="p-2 rounded hover:bg-gray-700" :disabled="currentPage <= 1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
||||
</button>
|
||||
<span class="text-sm">
|
||||
{{ currentPage }} / {{ totalPages }}
|
||||
</span>
|
||||
<button @click="nextPage" class="p-2 rounded hover:bg-gray-700" :disabled="currentPage >= totalPages">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Zoom controls -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button @click="zoomOut" class="p-2 rounded hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
|
||||
</button>
|
||||
<span class="text-sm">{{ zoomLevel }}%</span>
|
||||
<button @click="zoomIn" class="p-2 rounded hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
|
||||
</button>
|
||||
<button @click="resetZoom" class="p-2 rounded hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"></path><path d="M9 21H3v-6"></path><path d="M21 3l-7 7"></path><path d="M3 21l7-7"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button @click="downloadDocument" class="p-2 rounded hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
</button>
|
||||
<button @click="printDocument" class="p-2 rounded hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
|
||||
</button>
|
||||
<button @click="requestAccess" class="p-2 rounded hover:bg-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document content area -->
|
||||
<div class="document-content flex-1 overflow-auto flex justify-center items-start p-4">
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center h-full w-full">
|
||||
<Loading />
|
||||
<p class="mt-4 text-gray-400">Loading document...</p>
|
||||
</div>
|
||||
|
||||
<!-- PDF Viewer -->
|
||||
<div v-else-if="documentType === 'pdf'" class="document-display">
|
||||
<div class="bg-white p-4 shadow-lg" :style="{ transform: `scale(${zoomLevel/100})` }">
|
||||
<div class="w-[816px] h-[1056px] flex justify-center items-center border border-gray-300">
|
||||
<!-- This would be replaced with an actual PDF viewer like vue-pdf-embed or similar -->
|
||||
<div class="text-gray-800 text-center">
|
||||
<span v-html="getDocumentIcon('pdf', 64)" class="mx-auto mb-4 block"></span>
|
||||
<p class="font-medium">PDF Viewer</p>
|
||||
<p class="text-sm text-gray-500">Page {{ currentPage }} of {{ totalPages }}</p>
|
||||
<p class="text-sm text-gray-500 mt-2">{{ props.document.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Viewer -->
|
||||
<div v-else-if="documentType === 'image'" class="document-display">
|
||||
<div :style="{ transform: `scale(${zoomLevel/100})` }">
|
||||
<img src="https://via.placeholder.com/800x600" alt="Document preview" class="max-w-full shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Office Document Viewer -->
|
||||
<div v-else-if="['word', 'excel', 'powerpoint'].includes(documentType)" class="document-display">
|
||||
<div class="bg-white p-4 shadow-lg" :style="{ transform: `scale(${zoomLevel/100})` }">
|
||||
<div class="w-[816px] h-[1056px] flex justify-center items-center border border-gray-300">
|
||||
<!-- This would be replaced with an actual Office viewer -->
|
||||
<div class="text-gray-800 text-center">
|
||||
<span v-html="getDocumentIcon(documentType, 64)" class="mx-auto mb-4 block"></span>
|
||||
<p class="font-medium">{{ documentType.charAt(0).toUpperCase() + documentType.slice(1) }} Viewer</p>
|
||||
<p class="text-sm text-gray-500 mt-2">{{ props.document.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text Viewer -->
|
||||
<div v-else-if="documentType === 'text'" class="document-display">
|
||||
<div class="bg-white p-6 shadow-lg text-gray-800 w-[816px]" :style="{ transform: `scale(${zoomLevel/100})` }">
|
||||
<!-- This would be replaced with actual text content -->
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor ligula nec nisl consectetur, nec tincidunt nisl tincidunt. Duis vitae urna euismod, volutpat nisl ac, malesuada nunc.</p>
|
||||
<p>Sed vel lectus vel orci ultrices tincidunt. Nulla facilisi. Donec vitae nisi vel elit elementum tincidunt. Sed vel justo vel nisi volutpat tincidunt. Duis vitae urna euismod, volutpat nisl ac, malesuada nunc.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generic/Unsupported File -->
|
||||
<div v-else class="document-display flex flex-col items-center justify-center">
|
||||
<span v-html="getDocumentIcon('generic', 96)" class="mb-4"></span>
|
||||
<h3 class="text-lg font-medium mb-2">Preview not available</h3>
|
||||
<p class="text-gray-400 mb-6">This file type cannot be previewed</p>
|
||||
<rs-button @click="downloadDocument" color="primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
Download File
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.document-display {
|
||||
transition: transform 0.2s ease;
|
||||
transform-origin: center top;
|
||||
}
|
||||
</style>
|
@ -9,6 +9,33 @@ export default [
|
||||
"icon": "ic:outline-dashboard",
|
||||
"child": [],
|
||||
"meta": {}
|
||||
},
|
||||
|
||||
],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"header": "DMS",
|
||||
"description": "Document Management System",
|
||||
"child": [
|
||||
{
|
||||
"title": "Document Management",
|
||||
"path": "/dms",
|
||||
"icon": "ic:outline-folder",
|
||||
"child": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"title": "Access Requests",
|
||||
"path": "/dms/access-requests",
|
||||
"icon": "ic:outline-security",
|
||||
"child": []
|
||||
},
|
||||
{
|
||||
"title": "Settings",
|
||||
"path": "/dms/settings",
|
||||
"icon": "ic:outline-settings",
|
||||
"child": []
|
||||
}
|
||||
],
|
||||
"meta": {}
|
||||
|
364
pages/dms/document-properties.vue
Normal file
364
pages/dms/document-properties.vue
Normal file
@ -0,0 +1,364 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
|
||||
|
||||
// Define page metadata
|
||||
definePageMeta({
|
||||
title: "Document Properties",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "DMS",
|
||||
path: "/dms",
|
||||
},
|
||||
{
|
||||
name: "Document Properties",
|
||||
path: "/dms/document-properties",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Set up store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Local state
|
||||
const currentDocument = ref(null);
|
||||
const isEditing = ref(false);
|
||||
const documentForm = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
keywords: '',
|
||||
category: '',
|
||||
status: '',
|
||||
retention: '',
|
||||
owner: '',
|
||||
department: '',
|
||||
created: '',
|
||||
modified: '',
|
||||
fileSize: '',
|
||||
version: '',
|
||||
versionHistory: []
|
||||
});
|
||||
|
||||
// Mock document data
|
||||
const documentData = {
|
||||
id: 'doc123',
|
||||
title: 'Pembangunan Sistem IT 2021',
|
||||
description: 'Spesifikasi teknikal untuk pembangunan sistem IT baru di JKR Kota Bharu',
|
||||
keywords: 'IT, sistem, JKR, spesifikasi, pembangunan',
|
||||
category: 'Technical Specification',
|
||||
status: 'Approved',
|
||||
retention: '7 years',
|
||||
owner: 'Mohd Faizal bin Abdullah',
|
||||
department: 'IT Department',
|
||||
created: '2021-05-20',
|
||||
modified: '2021-05-25',
|
||||
fileSize: '4MB',
|
||||
filePath: '/JKR Cawangan Kota Bharu, Kelantan/Technical Documents/',
|
||||
fileName: 'Pembangunan_Sistem_IT_2021.pdf',
|
||||
fileType: 'PDF',
|
||||
version: '1.2',
|
||||
versionHistory: [
|
||||
{ version: '1.2', date: '2021-05-25', user: 'Mohd Faizal bin Abdullah', notes: 'Final approved version' },
|
||||
{ version: '1.1', date: '2021-05-22', user: 'Ahmad bin Ishak', notes: 'Updated requirements section' },
|
||||
{ version: '1.0', date: '2021-05-20', user: 'Mohd Faizal bin Abdullah', notes: 'Initial draft' }
|
||||
]
|
||||
};
|
||||
|
||||
// Enable editing
|
||||
const startEditing = () => {
|
||||
// Copy current document values to form
|
||||
documentForm.value = { ...currentDocument.value };
|
||||
isEditing.value = true;
|
||||
};
|
||||
|
||||
// Save changes
|
||||
const saveChanges = () => {
|
||||
// In a real app, we would send the updated data to the server
|
||||
// For now, we'll just update our local copy
|
||||
Object.assign(currentDocument.value, documentForm.value);
|
||||
isEditing.value = false;
|
||||
};
|
||||
|
||||
// Cancel editing
|
||||
const cancelEditing = () => {
|
||||
isEditing.value = false;
|
||||
};
|
||||
|
||||
// Load document on mount
|
||||
onMounted(() => {
|
||||
// In a real app, we would fetch the document from an API
|
||||
// For now, we'll use our mock data
|
||||
currentDocument.value = documentData;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-page">
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card class="h-full">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-xl font-bold text-primary">Document Properties</h1>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<rs-button v-if="!isEditing" color="primary" @click="startEditing">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
|
||||
Edit Properties
|
||||
</rs-button>
|
||||
<div v-else class="flex gap-2">
|
||||
<rs-button color="primary" @click="saveChanges">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
|
||||
Save
|
||||
</rs-button>
|
||||
<rs-button color="secondary" @click="cancelEditing">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
Cancel
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="explorer-layout h-full flex overflow-hidden">
|
||||
<!-- Left sidebar navigation -->
|
||||
<DMSNavigation />
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div v-if="currentDocument" class="max-w-4xl mx-auto">
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><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 Information
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">File Name</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.fileName }}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">File Type</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.fileType }}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">File Size</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.fileSize }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">File Path</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.filePath }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Created Date</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.created }}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Modified Date</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.modified }}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Current Version</label>
|
||||
<div class="text-gray-900 dark:text-gray-100">{{ currentDocument.version }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
Document Metadata
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.title }}</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="documentForm.title"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.description }}</div>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="documentForm.description"
|
||||
rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Keywords</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.keywords }}</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="documentForm.keywords"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Category</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.category }}</div>
|
||||
<select
|
||||
v-else
|
||||
v-model="documentForm.category"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option value="Technical Specification">Technical Specification</option>
|
||||
<option value="Project Proposal">Project Proposal</option>
|
||||
<option value="Contract">Contract</option>
|
||||
<option value="Report">Report</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Status</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.status }}</div>
|
||||
<select
|
||||
v-else
|
||||
v-model="documentForm.status"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option value="Draft">Draft</option>
|
||||
<option value="Under Review">Under Review</option>
|
||||
<option value="Approved">Approved</option>
|
||||
<option value="Rejected">Rejected</option>
|
||||
<option value="Archived">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Retention Period</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.retention }}</div>
|
||||
<select
|
||||
v-else
|
||||
v-model="documentForm.retention"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option value="1 year">1 year</option>
|
||||
<option value="3 years">3 years</option>
|
||||
<option value="5 years">5 years</option>
|
||||
<option value="7 years">7 years</option>
|
||||
<option value="10 years">10 years</option>
|
||||
<option value="Permanent">Permanent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||
Ownership Information
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Owner</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.owner }}</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="documentForm.owner"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Department</label>
|
||||
<div v-if="!isEditing" class="text-gray-900 dark:text-gray-100">{{ currentDocument.department }}</div>
|
||||
<input
|
||||
v-else
|
||||
v-model="documentForm.department"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
||||
Version History
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Version</th>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Date</th>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">User</th>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Notes</th>
|
||||
<th scope="col" class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="(version, index) in currentDocument.versionHistory" :key="index">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ version.version }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ version.date }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ version.user }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900 dark:text-gray-100">{{ version.notes }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<rs-button size="xs" color="secondary">View</rs-button>
|
||||
<rs-button v-if="index > 0" size="xs" color="primary" class="ml-2">Restore</rs-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dms-page {
|
||||
height: calc(100vh - 64px - 48px - 32px); /* Adjust based on your layout */
|
||||
}
|
||||
|
||||
.explorer-layout {
|
||||
height: calc(100vh - 200px); /* Adjust based on your layout */
|
||||
}
|
||||
</style>
|
465
pages/dms/index.vue
Normal file
465
pages/dms/index.vue
Normal file
@ -0,0 +1,465 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
|
||||
|
||||
// Define page metadata
|
||||
definePageMeta({
|
||||
title: "Document Management System",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "DMS",
|
||||
path: "/dms",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Set up store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Local state
|
||||
const showFileViewer = ref(false);
|
||||
const currentDocument = ref(null);
|
||||
const searchQuery = ref('');
|
||||
const isSearching = ref(false);
|
||||
const currentPath = ref('JKR Cawangan Kota Bharu, Kelantan');
|
||||
const viewMode = ref('explorer'); // explorer, cabinets, list
|
||||
|
||||
// File selection state
|
||||
const selectedFiles = ref([]);
|
||||
const isSelecting = ref(false);
|
||||
|
||||
// Toggle file selection
|
||||
const toggleFileSelection = (file) => {
|
||||
const index = selectedFiles.value.findIndex(f => f.id === file.id);
|
||||
if (index === -1) {
|
||||
selectedFiles.value.push(file);
|
||||
} else {
|
||||
selectedFiles.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Clear selection
|
||||
const clearSelection = () => {
|
||||
selectedFiles.value = [];
|
||||
isSelecting.value = false;
|
||||
};
|
||||
|
||||
// Select all files
|
||||
const selectAllFiles = () => {
|
||||
selectedFiles.value = [...dmsStore.currentItems];
|
||||
isSelecting.value = true;
|
||||
};
|
||||
|
||||
// Check if a file is selected
|
||||
const isFileSelected = (file) => {
|
||||
return selectedFiles.value.some(f => f.id === file.id);
|
||||
};
|
||||
|
||||
// Toggle view mode
|
||||
const changeViewMode = (mode) => {
|
||||
viewMode.value = mode;
|
||||
};
|
||||
|
||||
// View a file
|
||||
const viewFile = (file) => {
|
||||
if (isSelecting.value) {
|
||||
toggleFileSelection(file);
|
||||
return;
|
||||
}
|
||||
|
||||
currentDocument.value = file;
|
||||
showFileViewer.value = true;
|
||||
};
|
||||
|
||||
// Navigate to a location
|
||||
const navigateTo = (path) => {
|
||||
currentPath.value = path;
|
||||
// In a real app, we would fetch the contents of this location
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
// Search functionality
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.value.trim()) return;
|
||||
|
||||
isSearching.value = true;
|
||||
await dmsStore.searchDocuments(searchQuery.value);
|
||||
isSearching.value = false;
|
||||
};
|
||||
|
||||
// Clear search
|
||||
const clearSearch = () => {
|
||||
searchQuery.value = '';
|
||||
dmsStore.clearSearch();
|
||||
};
|
||||
|
||||
// Format file size
|
||||
const formatFileSize = (size) => {
|
||||
if (!size) return '0 B';
|
||||
|
||||
if (typeof size === 'string') {
|
||||
// If already formatted (like "4MB"), return as is
|
||||
if (size.endsWith('B')) return size;
|
||||
|
||||
// Try to parse the size if it's a number in string form
|
||||
const parsed = parseFloat(size);
|
||||
if (isNaN(parsed)) return size;
|
||||
size = parsed;
|
||||
}
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
while (size >= 1024 && i < units.length - 1) {
|
||||
size /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${size.toFixed(2)} ${units[i]}`;
|
||||
};
|
||||
|
||||
// Mock data for the example
|
||||
const mockFiles = [
|
||||
{
|
||||
id: 'file1',
|
||||
name: 'Pembangunan_Sistem_IT_2021.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '4MB',
|
||||
modified: '2021-05-20',
|
||||
status: 'locked',
|
||||
info: {
|
||||
title: 'Projek Pembangunan Sistem IT',
|
||||
subject: 'Dokumen spesifikasi sistem',
|
||||
state: 'Kelantan',
|
||||
date: '2021-05-20',
|
||||
user: 'Mohd Faizal bin Abdullah',
|
||||
storeDate: '2021-05-25'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'file2',
|
||||
name: 'Projek_Jalan_Raya_Kota_Bharu.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '5MB',
|
||||
modified: '2021-06-15',
|
||||
status: 'unlocked'
|
||||
},
|
||||
{
|
||||
id: 'file3',
|
||||
name: 'Anggaran_Kos_Projek_MRT3.xlsx',
|
||||
type: 'file',
|
||||
extension: 'xlsx',
|
||||
size: '3MB',
|
||||
modified: '2021-07-10',
|
||||
status: 'locked'
|
||||
},
|
||||
{
|
||||
id: 'file4',
|
||||
name: 'EIA_Empangan_Nenggiri.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '15MB',
|
||||
modified: '2021-04-18',
|
||||
status: 'locked'
|
||||
}
|
||||
];
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
// In a real app, we would load the initial data here
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-page">
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card class="h-full">
|
||||
<template #body>
|
||||
<div class="explorer-layout h-full flex flex-col">
|
||||
<!-- Address bar (Windows Explorer style) -->
|
||||
<div class="address-bar border-b border-gray-200 dark:border-gray-700 p-2 flex items-center gap-2">
|
||||
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
|
||||
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
|
||||
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
</button>
|
||||
|
||||
<div class="flex-1 flex items-center px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2 text-gray-500"><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>
|
||||
<span>{{ currentPath }}</span>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search in current folder..."
|
||||
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 w-56"
|
||||
/>
|
||||
<span class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View options toolbar -->
|
||||
<div class="view-toolbar border-b border-gray-200 dark:border-gray-700 p-2 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center bg-gray-100 dark:bg-gray-800 rounded-md overflow-hidden border border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
@click="changeViewMode('explorer')"
|
||||
class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center"
|
||||
:class="viewMode === 'explorer' ? 'bg-white dark:bg-gray-700 shadow-sm' : ''"
|
||||
>
|
||||
<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" class="mr-1"><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>
|
||||
<span class="text-sm">Explorer View</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="changeViewMode('cabinets')"
|
||||
class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center"
|
||||
:class="viewMode === 'cabinets' ? 'bg-white dark:bg-gray-700 shadow-sm' : ''"
|
||||
>
|
||||
<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" class="mr-1"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
|
||||
<span class="text-sm">Cabinets View</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="changeViewMode('list')"
|
||||
class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center"
|
||||
:class="viewMode === 'list' ? 'bg-white dark:bg-gray-700 shadow-sm' : ''"
|
||||
>
|
||||
<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" class="mr-1"><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>
|
||||
<span class="text-sm">List View</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<rs-button size="sm" color="primary" class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><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>
|
||||
Upload
|
||||
</rs-button>
|
||||
|
||||
<rs-button size="sm" color="secondary" class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
Create New
|
||||
</rs-button>
|
||||
|
||||
<div class="flex items-center border border-gray-200 dark:border-gray-700 rounded-md px-3 py-1 bg-white dark:bg-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M11 5h10"></path><path d="M11 9h10"></path><path d="M11 13h10"></path><path d="M3 17h18"></path><path d="M3 5l4 4"></path><path d="M3 13l4-4"></path></svg>
|
||||
<span class="text-sm">Sort</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Left sidebar navigation -->
|
||||
<DMSNavigation />
|
||||
|
||||
<!-- File explorer main content -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- List view -->
|
||||
<table class="min-w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-6">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
@click="selectAllFiles"
|
||||
>
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Nama
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Jenis
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Saiz
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Modified Date
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr
|
||||
v-for="file in mockFiles"
|
||||
:key="file.id"
|
||||
@click="viewFile(file)"
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20': isFileSelected(file) }"
|
||||
>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
:checked="isFileSelected(file)"
|
||||
@click.stop="toggleFileSelection(file)"
|
||||
>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-500 mr-2">
|
||||
<svg v-if="file.extension === 'pdf'" xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="M9 15h6"></path>
|
||||
<path d="M9 11h6"></path>
|
||||
</svg>
|
||||
<svg v-else-if="file.extension === 'xlsx'" xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="11" y1="12" x2="11" y2="18"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<span>{{ file.name }}</span>
|
||||
<span v-if="file.status === 'locked'" class="ml-2 text-red-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<span class="uppercase">{{ file.extension }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
{{ file.size }}
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
{{ file.modified }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Right sidebar for document details -->
|
||||
<div v-if="currentDocument" class="w-72 border-l border-gray-200 dark:border-gray-700 overflow-y-auto bg-gray-50 dark:bg-gray-800">
|
||||
<div class="p-4">
|
||||
<div class="text-center mb-4 bg-white dark:bg-gray-700 p-8 rounded shadow-sm">
|
||||
<div v-if="currentDocument.extension === 'pdf'" class="flex justify-center mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" 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="M9 15h6"></path>
|
||||
<path d="M9 11h6"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div v-else-if="currentDocument.extension === 'xlsx'" class="flex justify-center mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#1D6F42" stroke-width="1" 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" fill="white"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<rect x="8" y="12" width="8" height="6" fill="#1D6F42"></rect>
|
||||
<line x1="8" y1="16" x2="16" y2="16" stroke="white"></line>
|
||||
<line x1="11" y1="12" x2="11" y2="18" stroke="white"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-medium">{{ currentDocument.name }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<h3 class="text-lg font-medium mb-3">Document Information</h3>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 mb-2">
|
||||
<div class="col-span-1 text-gray-500 dark:text-gray-400">Title:</div>
|
||||
<div class="col-span-2 text-right">{{ currentDocument.info?.title || 'N/A' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 mb-2">
|
||||
<div class="col-span-1 text-gray-500 dark:text-gray-400">Subject:</div>
|
||||
<div class="col-span-2 text-right">{{ currentDocument.info?.subject || 'N/A' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 mb-2">
|
||||
<div class="col-span-1 text-gray-500 dark:text-gray-400">State:</div>
|
||||
<div class="col-span-2 text-right">{{ currentDocument.info?.state || 'N/A' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 mb-2">
|
||||
<div class="col-span-1 text-gray-500 dark:text-gray-400">Date:</div>
|
||||
<div class="col-span-2 text-right">{{ currentDocument.info?.date || currentDocument.modified }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
||||
<h3 class="text-lg font-medium mb-3">Document Details</h3>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 mb-2">
|
||||
<div class="col-span-1 text-gray-500 dark:text-gray-400">File Name:</div>
|
||||
<div class="col-span-2 text-right">{{ currentDocument.name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 mb-2">
|
||||
<div class="col-span-1 text-gray-500 dark:text-gray-400">Type:</div>
|
||||
<div class="col-span-2 text-right">{{ currentDocument.extension.toUpperCase() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 mb-2">
|
||||
<div class="col-span-1 text-gray-500 dark:text-gray-400">Size:</div>
|
||||
<div class="col-span-2 text-right">{{ currentDocument.size }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentDocument.info?.user" class="grid grid-cols-3 gap-2 mb-2">
|
||||
<div class="col-span-1 text-gray-500 dark:text-gray-400">Author:</div>
|
||||
<div class="col-span-2 text-right">{{ currentDocument.info.user }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentDocument.info?.storeDate" class="grid grid-cols-3 gap-2 mb-2">
|
||||
<div class="col-span-1 text-gray-500 dark:text-gray-400">Date modified:</div>
|
||||
<div class="col-span-2 text-right">{{ currentDocument.info.storeDate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-6">
|
||||
<rs-button size="sm" color="primary" block class="flex-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
Preview
|
||||
</rs-button>
|
||||
<rs-button size="sm" color="secondary" block class="flex-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
Download
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<rs-button v-if="currentDocument.status === 'locked'" size="sm" color="default" block class="mt-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><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>
|
||||
Request Access
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dms-page {
|
||||
height: calc(100vh - 64px - 48px - 32px); /* Adjust based on your layout */
|
||||
}
|
||||
|
||||
.explorer-layout {
|
||||
height: calc(100vh - 200px); /* Adjust based on your layout */
|
||||
}
|
||||
</style>
|
371
pages/dms/my-documents.vue
Normal file
371
pages/dms/my-documents.vue
Normal file
@ -0,0 +1,371 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
|
||||
|
||||
// Define page metadata
|
||||
definePageMeta({
|
||||
title: "My Documents",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "DMS",
|
||||
path: "/dms",
|
||||
},
|
||||
{
|
||||
name: "My Documents",
|
||||
path: "/dms/my-documents",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Set up store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Local state
|
||||
const searchQuery = ref('');
|
||||
const isSearching = ref(false);
|
||||
const currentPath = ref('My Documents');
|
||||
const viewMode = ref('explorer'); // explorer, cabinets, list
|
||||
const selectedFiles = ref([]);
|
||||
const isSelecting = ref(false);
|
||||
const currentDocument = ref(null);
|
||||
const showFileViewer = ref(false);
|
||||
|
||||
// Toggle file selection
|
||||
const toggleFileSelection = (file) => {
|
||||
const index = selectedFiles.value.findIndex(f => f.id === file.id);
|
||||
if (index === -1) {
|
||||
selectedFiles.value.push(file);
|
||||
} else {
|
||||
selectedFiles.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Clear selection
|
||||
const clearSelection = () => {
|
||||
selectedFiles.value = [];
|
||||
isSelecting.value = false;
|
||||
};
|
||||
|
||||
// Select all files
|
||||
const selectAllFiles = () => {
|
||||
selectedFiles.value = [...myFiles];
|
||||
isSelecting.value = true;
|
||||
};
|
||||
|
||||
// Check if a file is selected
|
||||
const isFileSelected = (file) => {
|
||||
return selectedFiles.value.some(f => f.id === file.id);
|
||||
};
|
||||
|
||||
// Toggle view mode
|
||||
const changeViewMode = (mode) => {
|
||||
viewMode.value = mode;
|
||||
};
|
||||
|
||||
// View a file
|
||||
const viewFile = (file) => {
|
||||
if (isSelecting.value) {
|
||||
toggleFileSelection(file);
|
||||
return;
|
||||
}
|
||||
|
||||
currentDocument.value = file;
|
||||
showFileViewer.value = true;
|
||||
};
|
||||
|
||||
// Search functionality
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.value.trim()) return;
|
||||
|
||||
isSearching.value = true;
|
||||
// In a real app, we would call an API to search
|
||||
setTimeout(() => {
|
||||
isSearching.value = false;
|
||||
}, 800);
|
||||
};
|
||||
|
||||
// Clear search
|
||||
const clearSearch = () => {
|
||||
searchQuery.value = '';
|
||||
};
|
||||
|
||||
// Mock data for the example
|
||||
const myFiles = [
|
||||
{
|
||||
id: 'file1',
|
||||
name: 'Project_Proposal_2023.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '2.4MB',
|
||||
modified: '2023-01-15',
|
||||
status: 'unlocked',
|
||||
createdBy: 'Current User'
|
||||
},
|
||||
{
|
||||
id: 'file2',
|
||||
name: 'Budget_Estimation_Q1.xlsx',
|
||||
type: 'file',
|
||||
extension: 'xlsx',
|
||||
size: '1.8MB',
|
||||
modified: '2023-02-10',
|
||||
status: 'unlocked',
|
||||
createdBy: 'Current User'
|
||||
},
|
||||
{
|
||||
id: 'file3',
|
||||
name: 'Meeting_Minutes_March.docx',
|
||||
type: 'file',
|
||||
extension: 'docx',
|
||||
size: '578KB',
|
||||
modified: '2023-03-22',
|
||||
status: 'locked',
|
||||
createdBy: 'Current User'
|
||||
},
|
||||
{
|
||||
id: 'file4',
|
||||
name: 'Technical_Specifications.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '3.2MB',
|
||||
modified: '2023-02-28',
|
||||
status: 'unlocked',
|
||||
createdBy: 'Current User'
|
||||
}
|
||||
];
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
// In a real app, we would fetch the user's documents here
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-page">
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card class="h-full">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-xl font-bold text-primary">My Documents</h1>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Search box -->
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@keyup.enter="handleSearch"
|
||||
type="text"
|
||||
placeholder="Search my documents..."
|
||||
class="pl-10 pr-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 w-64"
|
||||
/>
|
||||
<span
|
||||
class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
</span>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
@click="clearSearch"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="explorer-layout h-full flex flex-col">
|
||||
<!-- Address bar (Windows Explorer style) -->
|
||||
<div class="address-bar border-b border-gray-200 dark:border-gray-700 p-2 flex items-center gap-2">
|
||||
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
|
||||
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
|
||||
<button class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
</button>
|
||||
|
||||
<div class="flex-1 flex items-center px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2 text-gray-500"><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>
|
||||
<span>{{ currentPath }}</span>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter in current view..."
|
||||
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 w-56"
|
||||
/>
|
||||
<span class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View options toolbar -->
|
||||
<div class="view-toolbar border-b border-gray-200 dark:border-gray-700 p-2 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="changeViewMode('explorer')"
|
||||
class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="viewMode === 'explorer' ? 'bg-gray-100 dark:bg-gray-800' : ''"
|
||||
>
|
||||
<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>
|
||||
<span class="ml-1 text-sm">Explorer View</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="changeViewMode('cabinets')"
|
||||
class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="viewMode === 'cabinets' ? 'bg-gray-100 dark:bg-gray-800' : ''"
|
||||
>
|
||||
<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="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
|
||||
<span class="ml-1 text-sm">Cabinets View</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="changeViewMode('list')"
|
||||
class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="viewMode === 'list' ? 'bg-gray-100 dark:bg-gray-800' : ''"
|
||||
>
|
||||
<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>
|
||||
<span class="ml-1 text-sm">List View</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<rs-button size="sm" color="primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><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>
|
||||
Upload
|
||||
</rs-button>
|
||||
<rs-button size="sm" color="secondary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
New
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Left sidebar navigation -->
|
||||
<DMSNavigation />
|
||||
|
||||
<!-- File explorer main content -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- List view -->
|
||||
<table class="min-w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-6">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
@click="selectAllFiles"
|
||||
>
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Size
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Modified Date
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr
|
||||
v-for="file in myFiles"
|
||||
:key="file.id"
|
||||
@click="viewFile(file)"
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20': isFileSelected(file) }"
|
||||
>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
:checked="isFileSelected(file)"
|
||||
@click.stop="toggleFileSelection(file)"
|
||||
>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-500 mr-2">
|
||||
<svg v-if="file.extension === 'pdf'" xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="M9 15h6"></path>
|
||||
<path d="M9 11h6"></path>
|
||||
</svg>
|
||||
<svg v-else-if="file.extension === 'xlsx'" xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="11" y1="12" x2="11" y2="18"></line>
|
||||
</svg>
|
||||
<svg v-else-if="file.extension === 'docx'" xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<span>{{ file.name }}</span>
|
||||
<span v-if="file.status === 'locked'" class="ml-2 text-red-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<span class="uppercase">{{ file.extension }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
{{ file.size }}
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
{{ file.modified }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Right sidebar for document details - optional -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dms-page {
|
||||
height: calc(100vh - 64px - 48px - 32px); /* Adjust based on your layout */
|
||||
}
|
||||
|
||||
.explorer-layout {
|
||||
height: calc(100vh - 200px); /* Adjust based on your layout */
|
||||
}
|
||||
</style>
|
444
pages/dms/upload-document.vue
Normal file
444
pages/dms/upload-document.vue
Normal file
@ -0,0 +1,444 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useDmsStore } from '~/stores/dms';
|
||||
import DMSNavigation from '~/components/dms/navigation/DMSNavigation.vue';
|
||||
|
||||
// Define page metadata
|
||||
definePageMeta({
|
||||
title: "Upload Document",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "DMS",
|
||||
path: "/dms",
|
||||
},
|
||||
{
|
||||
name: "Upload Document",
|
||||
path: "/dms/upload-document",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Set up store
|
||||
const dmsStore = useDmsStore();
|
||||
|
||||
// Local state
|
||||
const isUploading = ref(false);
|
||||
const selectedFiles = ref([]);
|
||||
const uploadProgress = ref(0);
|
||||
const currentLocation = ref('/JKR Cawangan Kota Bharu, Kelantan');
|
||||
|
||||
// Document metadata form
|
||||
const documentForm = reactive({
|
||||
title: '',
|
||||
description: '',
|
||||
keywords: '',
|
||||
category: 'Technical Specification',
|
||||
status: 'Draft',
|
||||
retention: '7 years',
|
||||
department: '',
|
||||
accessLevel: 'private'
|
||||
});
|
||||
|
||||
// Available categories
|
||||
const categories = [
|
||||
'Technical Specification',
|
||||
'Project Proposal',
|
||||
'Contract',
|
||||
'Invoice',
|
||||
'Report',
|
||||
'Memo',
|
||||
'Letter',
|
||||
'Other'
|
||||
];
|
||||
|
||||
// Document statuses
|
||||
const statuses = [
|
||||
'Draft',
|
||||
'Under Review',
|
||||
'Approved',
|
||||
'Rejected',
|
||||
'Archived'
|
||||
];
|
||||
|
||||
// Retention periods
|
||||
const retentionPeriods = [
|
||||
'1 year',
|
||||
'3 years',
|
||||
'5 years',
|
||||
'7 years',
|
||||
'10 years',
|
||||
'Permanent'
|
||||
];
|
||||
|
||||
// Access levels
|
||||
const accessLevels = [
|
||||
{ value: 'private', label: 'Private (Only me)' },
|
||||
{ value: 'department', label: 'Department' },
|
||||
{ value: 'organization', label: 'Organization' },
|
||||
{ value: 'public', label: 'Public' }
|
||||
];
|
||||
|
||||
// Handle file selection
|
||||
const handleFileSelect = (event) => {
|
||||
const files = event.target.files;
|
||||
selectedFiles.value = Array.from(files);
|
||||
|
||||
// Auto-populate title based on filename (without extension)
|
||||
if (files.length === 1) {
|
||||
const fileName = files[0].name;
|
||||
const nameWithoutExtension = fileName.split('.').slice(0, -1).join('.');
|
||||
documentForm.title = nameWithoutExtension.replace(/_/g, ' ');
|
||||
}
|
||||
};
|
||||
|
||||
// Clear selected files
|
||||
const clearFiles = () => {
|
||||
selectedFiles.value = [];
|
||||
// Reset form
|
||||
Object.keys(documentForm).forEach(key => {
|
||||
documentForm[key] = key === 'category' ? 'Technical Specification' :
|
||||
key === 'status' ? 'Draft' :
|
||||
key === 'retention' ? '7 years' :
|
||||
key === 'accessLevel' ? 'private' : '';
|
||||
});
|
||||
};
|
||||
|
||||
// Simulate upload process
|
||||
const uploadFiles = async () => {
|
||||
if (selectedFiles.value.length === 0) return;
|
||||
|
||||
isUploading.value = true;
|
||||
uploadProgress.value = 0;
|
||||
|
||||
// Simulate upload progress
|
||||
const interval = setInterval(() => {
|
||||
uploadProgress.value += 10;
|
||||
if (uploadProgress.value >= 100) {
|
||||
clearInterval(interval);
|
||||
setTimeout(() => {
|
||||
isUploading.value = false;
|
||||
// Show success message
|
||||
alert('Files uploaded successfully!');
|
||||
// Clear form and selected files
|
||||
clearFiles();
|
||||
}, 500);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// Create dummy folder structure for folder picker
|
||||
const folderStructure = [
|
||||
{
|
||||
id: 'root',
|
||||
name: 'JKR Cawangan Kota Bharu, Kelantan',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan',
|
||||
children: [
|
||||
{
|
||||
id: 'tech',
|
||||
name: 'Technical Documents',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Technical Documents',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 'projects',
|
||||
name: 'Projects',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Projects',
|
||||
children: [
|
||||
{
|
||||
id: 'proj1',
|
||||
name: 'Project MRT3',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Projects/Project MRT3',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 'proj2',
|
||||
name: 'Empangan Nenggiri',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Projects/Empangan Nenggiri',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
name: 'Administrative',
|
||||
path: '/JKR Cawangan Kota Bharu, Kelantan/Administrative',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Change destination folder
|
||||
const selectFolder = (path) => {
|
||||
currentLocation.value = path;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dms-page">
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card class="h-full">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-xl font-bold text-primary">Upload Document</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="explorer-layout h-full flex overflow-hidden">
|
||||
<!-- Left sidebar navigation -->
|
||||
<DMSNavigation />
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- File upload section -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><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>
|
||||
Select Files
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div v-if="selectedFiles.length === 0" class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-12 text-center">
|
||||
<input
|
||||
type="file"
|
||||
id="file-upload"
|
||||
multiple
|
||||
class="hidden"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
<label
|
||||
for="file-upload"
|
||||
class="cursor-pointer flex flex-col items-center"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="mb-4 text-gray-400"><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>
|
||||
<span class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-1">Drag and drop files here</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 mb-4">or click to browse</span>
|
||||
<rs-button color="primary" size="sm">Select Files</rs-button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="mb-4 flex justify-between items-center">
|
||||
<h3 class="font-medium">Selected Files ({{ selectedFiles.length }})</h3>
|
||||
<rs-button size="sm" color="secondary" @click="clearFiles">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
Clear
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<li v-for="(file, index) in selectedFiles" :key="index" class="p-4 flex items-center">
|
||||
<div class="mr-3 text-gray-400">
|
||||
<svg v-if="file.type.includes('pdf')" xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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="M9 15h6"></path>
|
||||
<path d="M9 11h6"></path>
|
||||
</svg>
|
||||
<svg v-else-if="file.type.includes('spreadsheet') || file.type.includes('excel')" xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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="11" y1="12" x2="11" y2="18"></line>
|
||||
</svg>
|
||||
<svg v-else-if="file.type.includes('word') || file.type.includes('document')" xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{{ file.name }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ Math.round(file.size / 1024) }} KB
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="selectedFiles.splice(index, 1)"
|
||||
class="p-1 text-gray-400 hover:text-red-500"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload destination -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><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>
|
||||
Upload Destination
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex items-center p-2 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2 text-gray-400"><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>
|
||||
<span>{{ currentLocation }}</span>
|
||||
<rs-button size="sm" color="secondary" class="ml-auto">
|
||||
Change
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document metadata -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
Document Metadata
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
|
||||
<input
|
||||
v-model="documentForm.title"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
||||
<textarea
|
||||
v-model="documentForm.description"
|
||||
rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Keywords (comma separated)</label>
|
||||
<input
|
||||
v-model="documentForm.keywords"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Category</label>
|
||||
<select
|
||||
v-model="documentForm.category"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option v-for="category in categories" :key="category" :value="category">
|
||||
{{ category }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Status</label>
|
||||
<select
|
||||
v-model="documentForm.status"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option v-for="status in statuses" :key="status" :value="status">
|
||||
{{ status }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Retention Period</label>
|
||||
<select
|
||||
v-model="documentForm.retention"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option v-for="period in retentionPeriods" :key="period" :value="period">
|
||||
{{ period }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Department</label>
|
||||
<input
|
||||
v-model="documentForm.department"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Access Level</label>
|
||||
<select
|
||||
v-model="documentForm.accessLevel"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring focus:ring-primary focus:ring-opacity-50"
|
||||
>
|
||||
<option v-for="level in accessLevels" :key="level.value" :value="level.value">
|
||||
{{ level.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload progress -->
|
||||
<div v-if="isUploading" class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden mb-6">
|
||||
<div class="px-6 py-4">
|
||||
<h3 class="font-medium mb-2">Uploading Files...</h3>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-2">
|
||||
<div
|
||||
class="bg-primary h-2.5 rounded-full"
|
||||
:style="{ width: `${uploadProgress}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">{{ uploadProgress }}% complete</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3">
|
||||
<rs-button color="secondary" @click="clearFiles">Cancel</rs-button>
|
||||
<rs-button
|
||||
color="primary"
|
||||
:disabled="selectedFiles.length === 0 || isUploading"
|
||||
@click="uploadFiles"
|
||||
>
|
||||
<svg v-if="!isUploading" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><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>
|
||||
Upload {{ selectedFiles.length }} {{ selectedFiles.length === 1 ? 'File' : 'Files' }}
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dms-page {
|
||||
height: calc(100vh - 64px - 48px - 32px); /* Adjust based on your layout */
|
||||
}
|
||||
|
||||
.explorer-layout {
|
||||
height: calc(100vh - 200px); /* Adjust based on your layout */
|
||||
}
|
||||
</style>
|
@ -29,27 +29,35 @@ model lookup {
|
||||
}
|
||||
|
||||
model role {
|
||||
roleID Int @id @default(autoincrement())
|
||||
roleName String? @db.VarChar(255)
|
||||
roleDescription String? @db.VarChar(255)
|
||||
roleStatus String? @db.VarChar(255)
|
||||
roleCreatedDate DateTime? @db.DateTime(0)
|
||||
roleModifiedDate DateTime? @db.DateTime(0)
|
||||
roleID Int @id @default(autoincrement())
|
||||
roleName String? @db.VarChar(255)
|
||||
roleDescription String? @db.VarChar(255)
|
||||
roleStatus String? @db.VarChar(255)
|
||||
roleCreatedDate DateTime? @db.DateTime(0)
|
||||
roleModifiedDate DateTime? @db.DateTime(0)
|
||||
userrole userrole[]
|
||||
permissions AccessPermission[]
|
||||
}
|
||||
|
||||
model user {
|
||||
userID Int @id @default(autoincrement())
|
||||
userSecretKey String? @db.VarChar(255)
|
||||
userUsername String? @db.VarChar(255)
|
||||
userPassword String? @db.VarChar(255)
|
||||
userFullName String? @db.VarChar(255)
|
||||
userEmail String? @db.VarChar(255)
|
||||
userPhone String? @db.VarChar(255)
|
||||
userStatus String? @db.VarChar(255)
|
||||
userCreatedDate DateTime? @db.DateTime(0)
|
||||
userModifiedDate DateTime? @db.DateTime(0)
|
||||
userID Int @id @default(autoincrement())
|
||||
userSecretKey String? @db.VarChar(255)
|
||||
userUsername String? @db.VarChar(255)
|
||||
userPassword String? @db.VarChar(255)
|
||||
userFullName String? @db.VarChar(255)
|
||||
userEmail String? @db.VarChar(255)
|
||||
userPhone String? @db.VarChar(255)
|
||||
userStatus String? @db.VarChar(255)
|
||||
userCreatedDate DateTime? @db.DateTime(0)
|
||||
userModifiedDate DateTime? @db.DateTime(0)
|
||||
userrole userrole[]
|
||||
accessRequests AccessRequest[]
|
||||
permissions AccessPermission[]
|
||||
documents Document[] @relation("DocumentCreator")
|
||||
cabinets Cabinet[] @relation("CabinetCreator")
|
||||
drawers Drawer[] @relation("DrawerCreator")
|
||||
folders Folder[] @relation("FolderCreator")
|
||||
subfolders Subfolder[] @relation("SubfolderCreator")
|
||||
}
|
||||
|
||||
model userrole {
|
||||
@ -99,3 +107,164 @@ model site_settings {
|
||||
settingModifiedDate DateTime? @db.DateTime(0)
|
||||
siteLoginLogo String? @db.VarChar(500)
|
||||
}
|
||||
|
||||
// DMS Models
|
||||
|
||||
model Cabinet {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
createdAt DateTime @default(now()) @db.DateTime(0)
|
||||
updatedAt DateTime @updatedAt @db.DateTime(0)
|
||||
createdBy Int
|
||||
status String @default("active") @db.VarChar(50)
|
||||
user user @relation("CabinetCreator", fields: [createdBy], references: [userID])
|
||||
drawers Drawer[]
|
||||
permissions AccessPermission[]
|
||||
|
||||
@@index([createdBy])
|
||||
}
|
||||
|
||||
model Drawer {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
cabinetId Int
|
||||
createdAt DateTime @default(now()) @db.DateTime(0)
|
||||
updatedAt DateTime @updatedAt @db.DateTime(0)
|
||||
createdBy Int
|
||||
status String @default("active") @db.VarChar(50)
|
||||
cabinet Cabinet @relation(fields: [cabinetId], references: [id], onDelete: Cascade)
|
||||
user user @relation("DrawerCreator", fields: [createdBy], references: [userID])
|
||||
folders Folder[]
|
||||
permissions AccessPermission[]
|
||||
|
||||
@@index([cabinetId])
|
||||
@@index([createdBy])
|
||||
}
|
||||
|
||||
model Folder {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
drawerId Int
|
||||
createdAt DateTime @default(now()) @db.DateTime(0)
|
||||
updatedAt DateTime @updatedAt @db.DateTime(0)
|
||||
createdBy Int
|
||||
status String @default("active") @db.VarChar(50)
|
||||
drawer Drawer @relation(fields: [drawerId], references: [id], onDelete: Cascade)
|
||||
user user @relation("FolderCreator", fields: [createdBy], references: [userID])
|
||||
subfolders Subfolder[]
|
||||
documents Document[] @relation("FolderDocuments")
|
||||
permissions AccessPermission[]
|
||||
|
||||
@@index([drawerId])
|
||||
@@index([createdBy])
|
||||
}
|
||||
|
||||
model Subfolder {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
folderId Int
|
||||
createdAt DateTime @default(now()) @db.DateTime(0)
|
||||
updatedAt DateTime @updatedAt @db.DateTime(0)
|
||||
createdBy Int
|
||||
status String @default("active") @db.VarChar(50)
|
||||
folder Folder @relation(fields: [folderId], references: [id], onDelete: Cascade)
|
||||
user user @relation("SubfolderCreator", fields: [createdBy], references: [userID])
|
||||
documents Document[] @relation("SubfolderDocuments")
|
||||
permissions AccessPermission[]
|
||||
|
||||
@@index([folderId])
|
||||
@@index([createdBy])
|
||||
}
|
||||
|
||||
model Document {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
fileSize Int @default(0)
|
||||
fileType String @db.VarChar(100)
|
||||
fileExtension String @db.VarChar(20)
|
||||
filePath String @db.VarChar(500)
|
||||
version Int @default(1)
|
||||
isTemplate Boolean @default(false)
|
||||
isPublic Boolean @default(false)
|
||||
folderId Int?
|
||||
subfolderId Int?
|
||||
createdAt DateTime @default(now()) @db.DateTime(0)
|
||||
updatedAt DateTime @updatedAt @db.DateTime(0)
|
||||
createdBy Int
|
||||
status String @default("active") @db.VarChar(50)
|
||||
folder Folder? @relation("FolderDocuments", fields: [folderId], references: [id], onDelete: SetNull)
|
||||
subfolder Subfolder? @relation("SubfolderDocuments", fields: [subfolderId], references: [id], onDelete: SetNull)
|
||||
user user @relation("DocumentCreator", fields: [createdBy], references: [userID])
|
||||
accessRequests AccessRequest[]
|
||||
permissions AccessPermission[]
|
||||
versions DocumentVersion[]
|
||||
|
||||
@@index([folderId])
|
||||
@@index([subfolderId])
|
||||
@@index([createdBy])
|
||||
}
|
||||
|
||||
model DocumentVersion {
|
||||
id Int @id @default(autoincrement())
|
||||
documentId Int
|
||||
version Int
|
||||
filePath String @db.VarChar(500)
|
||||
fileSize Int @default(0)
|
||||
createdAt DateTime @default(now()) @db.DateTime(0)
|
||||
createdBy Int
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([documentId])
|
||||
}
|
||||
|
||||
model AccessRequest {
|
||||
id Int @id @default(autoincrement())
|
||||
documentId Int
|
||||
userId Int
|
||||
requestedLevel String @db.VarChar(50) // view, download, print, edit, full
|
||||
justification String? @db.Text
|
||||
status String @default("pending") @db.VarChar(50) // pending, approved, rejected
|
||||
responseNote String? @db.Text
|
||||
requestedAt DateTime @default(now()) @db.DateTime(0)
|
||||
respondedAt DateTime? @db.DateTime(0)
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
user user @relation(fields: [userId], references: [userID])
|
||||
|
||||
@@index([documentId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model AccessPermission {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int?
|
||||
roleId Int?
|
||||
documentId Int?
|
||||
cabinetId Int?
|
||||
drawerId Int?
|
||||
folderId Int?
|
||||
subfolderId Int?
|
||||
permissionLevel String @db.VarChar(50) // view, download, print, edit, full
|
||||
createdAt DateTime @default(now()) @db.DateTime(0)
|
||||
updatedAt DateTime @updatedAt @db.DateTime(0)
|
||||
expiresAt DateTime? @db.DateTime(0)
|
||||
user user? @relation(fields: [userId], references: [userID], onDelete: SetNull)
|
||||
role role? @relation(fields: [roleId], references: [roleID], onDelete: SetNull)
|
||||
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
cabinet Cabinet? @relation(fields: [cabinetId], references: [id], onDelete: Cascade)
|
||||
drawer Drawer? @relation(fields: [drawerId], references: [id], onDelete: Cascade)
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade)
|
||||
subfolder Subfolder? @relation(fields: [subfolderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([roleId])
|
||||
@@index([documentId])
|
||||
@@index([cabinetId])
|
||||
@@index([drawerId])
|
||||
@@index([folderId])
|
||||
@@index([subfolderId])
|
||||
}
|
||||
|
740
stores/dms.js
Normal file
740
stores/dms.js
Normal file
@ -0,0 +1,740 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useDmsStore = defineStore('dms', {
|
||||
state: () => ({
|
||||
// Navigation state
|
||||
currentPath: '/',
|
||||
pathHistory: ['/'],
|
||||
historyIndex: 0,
|
||||
|
||||
// User information
|
||||
currentUser: {
|
||||
id: 'user1',
|
||||
name: 'Aiman Fakhrullah',
|
||||
email: 'aiman@example.com',
|
||||
role: 'engineer', // engineer, admin, manager, etc.
|
||||
department: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu'
|
||||
},
|
||||
|
||||
// Cabinet access types
|
||||
cabinetAccessTypes: [
|
||||
{ id: 'public', name: 'Public Access', icon: 'check-circle', color: 'green' },
|
||||
{ id: 'personal', name: 'Personal Access', icon: 'circle-check', color: 'orange' },
|
||||
{ id: 'private', name: 'Private Access', icon: 'lock', color: 'red' }
|
||||
],
|
||||
|
||||
// Access request statuses
|
||||
accessRequestStatuses: [
|
||||
{ id: 'pending', name: 'Pending', color: 'orange' },
|
||||
{ id: 'approved', name: 'Approved', color: 'green' },
|
||||
{ id: 'rejected', name: 'Rejected', color: 'red' }
|
||||
],
|
||||
|
||||
// Items state - in production this would be loaded from API
|
||||
cabinets: [
|
||||
{
|
||||
id: 'public-cabinet',
|
||||
name: 'Public Cabinet',
|
||||
type: 'cabinet',
|
||||
accessType: 'public',
|
||||
children: [
|
||||
{
|
||||
id: 'public-cabinet-1',
|
||||
name: 'Public Cabinet',
|
||||
type: 'cabinet',
|
||||
accessType: 'public',
|
||||
parentId: 'public-cabinet'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'my-cabinets',
|
||||
name: 'My Cabinets',
|
||||
type: 'cabinet-group',
|
||||
accessType: 'personal',
|
||||
children: [
|
||||
{
|
||||
id: 'jkr-tebedu',
|
||||
name: 'JKR Cawangan Tebedu, Sarawak',
|
||||
type: 'cabinet',
|
||||
accessType: 'personal',
|
||||
parentId: 'my-cabinets',
|
||||
hasAccess: true
|
||||
},
|
||||
{
|
||||
id: 'jkr-kota-bharu',
|
||||
name: 'JKR Cawangan Kota Bharu, Kelantan',
|
||||
type: 'cabinet',
|
||||
accessType: 'personal',
|
||||
parentId: 'my-cabinets',
|
||||
hasAccess: true,
|
||||
children: [
|
||||
{
|
||||
id: 'jkr-kewangan-kb',
|
||||
name: 'JKR Bahagian Kewangan Cawangan Kota Bharu',
|
||||
type: 'cabinet',
|
||||
accessType: 'personal',
|
||||
parentId: 'jkr-kota-bharu',
|
||||
hasAccess: true
|
||||
},
|
||||
{
|
||||
id: 'jkr-kejuruteraan-kb',
|
||||
name: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
|
||||
type: 'cabinet',
|
||||
accessType: 'personal',
|
||||
parentId: 'jkr-kota-bharu',
|
||||
hasAccess: true,
|
||||
children: [
|
||||
{
|
||||
id: 'pembinaan-jambatan-kb',
|
||||
name: 'Pembinaan Jambatan Kota Bharu',
|
||||
type: 'cabinet',
|
||||
accessType: 'personal',
|
||||
parentId: 'jkr-kejuruteraan-kb',
|
||||
hasAccess: true,
|
||||
children: [
|
||||
{
|
||||
id: 'kewangan-tag',
|
||||
name: 'Kewangan',
|
||||
type: 'tag',
|
||||
color: 'purple',
|
||||
parentId: 'pembinaan-jambatan-kb'
|
||||
},
|
||||
{
|
||||
id: 'kejuruteraan-awam-tag',
|
||||
name: 'Kejuruteraan Awam',
|
||||
type: 'tag',
|
||||
color: 'purple',
|
||||
parentId: 'pembinaan-jambatan-kb'
|
||||
},
|
||||
{
|
||||
id: 'teknologi-maklumat-tag',
|
||||
name: 'Teknologi Maklumat',
|
||||
type: 'tag',
|
||||
color: 'purple',
|
||||
parentId: 'pembinaan-jambatan-kb'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'projek-jalan-raya-kb',
|
||||
name: 'Projek Jalan Raya Kota Bharu',
|
||||
type: 'cabinet',
|
||||
accessType: 'personal',
|
||||
parentId: 'jkr-kejuruteraan-kb',
|
||||
hasAccess: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'jkr-teknologi-kb',
|
||||
name: 'JKR Bahagian Teknologi Maklumat Cawangan Kota Bharu',
|
||||
type: 'cabinet',
|
||||
accessType: 'personal',
|
||||
parentId: 'jkr-kota-bharu',
|
||||
hasAccess: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'jkr-ipoh',
|
||||
name: 'JKR Cawangan Ipoh, Perak',
|
||||
type: 'cabinet',
|
||||
accessType: 'personal',
|
||||
parentId: 'my-cabinets',
|
||||
hasAccess: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'private-cabinets',
|
||||
name: 'Private Cabinets',
|
||||
type: 'cabinet-group',
|
||||
accessType: 'private',
|
||||
children: [
|
||||
{
|
||||
id: 'jkr-batu-kawan',
|
||||
name: 'JKR Cawangan Batu Kawan, Penang',
|
||||
type: 'cabinet',
|
||||
accessType: 'private',
|
||||
parentId: 'private-cabinets',
|
||||
hasAccess: false,
|
||||
accessRequestStatus: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'jkr-kuala-terengganu',
|
||||
name: 'JKR Cawangan Kuala Terengganu, Terengganu',
|
||||
type: 'cabinet',
|
||||
accessType: 'private',
|
||||
parentId: 'private-cabinets',
|
||||
hasAccess: false,
|
||||
accessRequestStatus: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'jkr-arkitek',
|
||||
name: 'JKR Cawangan Arkitek',
|
||||
type: 'cabinet',
|
||||
accessType: 'private',
|
||||
parentId: 'private-cabinets',
|
||||
hasAccess: false,
|
||||
accessRequestStatus: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'jkr-putrajaya',
|
||||
name: 'JKR Cawangan Putrajaya',
|
||||
type: 'cabinet',
|
||||
accessType: 'private',
|
||||
parentId: 'private-cabinets',
|
||||
hasAccess: false,
|
||||
accessRequestStatus: 'rejected',
|
||||
isLocked: true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// Files within cabinets
|
||||
items: [
|
||||
{
|
||||
id: 'file1',
|
||||
name: 'Pembangunan_Sistem_IT_2021.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '4MB',
|
||||
modified: '2021-05-20',
|
||||
cabinetId: 'jkr-kota-bharu',
|
||||
accessType: 'personal',
|
||||
status: 'locked',
|
||||
info: {
|
||||
title: 'Projek Pembangunan Sistem IT',
|
||||
subject: 'Dokumen spesifikasi sistem',
|
||||
state: 'Kelantan',
|
||||
date: '2021-05-20',
|
||||
user: 'Mohd Faizal bin Abdullah',
|
||||
storeDate: '2021-05-25'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'file2',
|
||||
name: 'Projek_Jalan_Raya_Kota_Bharu.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '5MB',
|
||||
modified: '2021-06-15',
|
||||
cabinetId: 'jkr-kejuruteraan-kb',
|
||||
accessType: 'personal',
|
||||
status: 'unlocked'
|
||||
},
|
||||
{
|
||||
id: 'file3',
|
||||
name: 'Anggaran_Kos_Projek_MRT3.xlsx',
|
||||
type: 'file',
|
||||
extension: 'xlsx',
|
||||
size: '3MB',
|
||||
modified: '2021-07-10',
|
||||
cabinetId: 'pembinaan-jambatan-kb',
|
||||
accessType: 'personal',
|
||||
status: 'locked'
|
||||
},
|
||||
{
|
||||
id: 'file4',
|
||||
name: 'EIA_Empangan_Nenggiri.pdf',
|
||||
type: 'file',
|
||||
extension: 'pdf',
|
||||
size: '15MB',
|
||||
modified: '2021-04-18',
|
||||
cabinetId: 'jkr-ipoh',
|
||||
accessType: 'personal',
|
||||
status: 'locked'
|
||||
},
|
||||
{
|
||||
id: 'file5',
|
||||
name: 'Borang_Maklumabalas.xlsx',
|
||||
type: 'file',
|
||||
extension: 'xlsx',
|
||||
size: '34.1 KB',
|
||||
modified: '2025-01-27',
|
||||
cabinetId: 'public-cabinet-1',
|
||||
accessType: 'public',
|
||||
createdBy: 'aimantasan',
|
||||
status: 'unlocked',
|
||||
info: {
|
||||
authors: 'aimantasan',
|
||||
lastSavedBy: 'aimantasan',
|
||||
dateAccessed: '2025-05-28',
|
||||
dateModified: '2025-01-27',
|
||||
contentCreated: '2025-01-27'
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// Access requests
|
||||
accessRequests: [
|
||||
{
|
||||
id: 'req1',
|
||||
userId: 'user1',
|
||||
userName: 'Aiman Fakhrullah',
|
||||
cabinetId: 'jkr-batu-kawan',
|
||||
cabinetName: 'JKR Cawangan Batu Kawan, Penang',
|
||||
requestDate: '2023-06-15',
|
||||
status: 'pending',
|
||||
reason: 'Need access for project collaboration'
|
||||
},
|
||||
{
|
||||
id: 'req2',
|
||||
userId: 'user1',
|
||||
userName: 'Aiman Fakhrullah',
|
||||
cabinetId: 'jkr-kuala-terengganu',
|
||||
cabinetName: 'JKR Cawangan Kuala Terengganu, Terengganu',
|
||||
requestDate: '2023-06-16',
|
||||
status: 'pending',
|
||||
reason: 'Required for cross-department coordination'
|
||||
},
|
||||
{
|
||||
id: 'req3',
|
||||
userId: 'user1',
|
||||
userName: 'Aiman Fakhrullah',
|
||||
cabinetId: 'jkr-arkitek',
|
||||
cabinetName: 'JKR Cawangan Arkitek',
|
||||
requestDate: '2023-06-17',
|
||||
status: 'pending',
|
||||
reason: 'Need architectural plans for current project'
|
||||
},
|
||||
{
|
||||
id: 'req4',
|
||||
userId: 'user1',
|
||||
userName: 'Aiman Fakhrullah',
|
||||
cabinetId: 'jkr-putrajaya',
|
||||
cabinetName: 'JKR Cawangan Putrajaya',
|
||||
requestDate: '2023-06-01',
|
||||
status: 'rejected',
|
||||
reason: 'Need access to headquarters documents',
|
||||
rejectionReason: 'Access restricted to headquarters staff only'
|
||||
}
|
||||
],
|
||||
|
||||
// Selected item
|
||||
selectedItem: null,
|
||||
|
||||
// View settings
|
||||
viewMode: 'list', // list, grid, details
|
||||
sortBy: 'name',
|
||||
sortDirection: 'asc',
|
||||
|
||||
// User permissions - would be loaded from auth service
|
||||
userPermissions: {
|
||||
canCreate: true,
|
||||
canEdit: true,
|
||||
canDelete: true,
|
||||
canManageAccess: true
|
||||
},
|
||||
|
||||
// Flags
|
||||
isLoading: false,
|
||||
showFileViewer: false,
|
||||
viewerDocument: null,
|
||||
searchQuery: '',
|
||||
searchResults: []
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// Get items at the current cabinet
|
||||
currentCabinetItems: (state) => {
|
||||
const currentCabinetId = state.currentPath.split('/').filter(Boolean).pop();
|
||||
return state.items.filter(item => item.cabinetId === currentCabinetId);
|
||||
},
|
||||
|
||||
// Get breadcrumbs for current path
|
||||
breadcrumbs: (state) => {
|
||||
if (state.currentPath === '/') return [{ name: 'Home', path: '/' }];
|
||||
|
||||
const paths = state.currentPath.split('/').filter(Boolean);
|
||||
let breadcrumbPath = '';
|
||||
|
||||
return [
|
||||
{ name: 'Home', path: '/' },
|
||||
...paths.map(segment => {
|
||||
breadcrumbPath += `/${segment}`;
|
||||
|
||||
// Find the actual cabinet name
|
||||
const findCabinetName = (cabinets, id) => {
|
||||
for (const cabinet of cabinets) {
|
||||
if (cabinet.id === id) return cabinet.name;
|
||||
if (cabinet.children) {
|
||||
const name = findCabinetName(cabinet.children, id);
|
||||
if (name) return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const name = findCabinetName(state.cabinets, segment) || segment;
|
||||
|
||||
return {
|
||||
name: name,
|
||||
path: breadcrumbPath
|
||||
};
|
||||
})
|
||||
];
|
||||
},
|
||||
|
||||
// Get public cabinets
|
||||
publicCabinets: (state) => {
|
||||
return state.cabinets.filter(cabinet => cabinet.accessType === 'public');
|
||||
},
|
||||
|
||||
// Get personal cabinets (ones the user has access to)
|
||||
personalCabinets: (state) => {
|
||||
return state.cabinets.find(cabinet => cabinet.id === 'my-cabinets') || null;
|
||||
},
|
||||
|
||||
// Get private cabinets
|
||||
privateCabinets: (state) => {
|
||||
return state.cabinets.find(cabinet => cabinet.id === 'private-cabinets') || null;
|
||||
},
|
||||
|
||||
// Check if we can navigate back
|
||||
canGoBack: (state) => {
|
||||
return state.historyIndex > 0;
|
||||
},
|
||||
|
||||
// Check if we can navigate forward
|
||||
canGoForward: (state) => {
|
||||
return state.historyIndex < state.pathHistory.length - 1;
|
||||
},
|
||||
|
||||
// Filter items by access level
|
||||
publicDocuments: (state) => {
|
||||
return state.items.filter(item => item.accessType === 'public');
|
||||
},
|
||||
|
||||
personalDocuments: (state) => {
|
||||
return state.items.filter(item => item.accessType === 'personal');
|
||||
},
|
||||
|
||||
privateDocuments: (state) => {
|
||||
return state.items.filter(item => item.accessType === 'private');
|
||||
},
|
||||
|
||||
// Get pending access requests
|
||||
pendingAccessRequests: (state) => {
|
||||
return state.accessRequests.filter(req => req.status === 'pending');
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// Navigation actions
|
||||
navigateTo(path) {
|
||||
// Add to history if it's a new path
|
||||
if (path !== this.currentPath) {
|
||||
// If we navigated back and then to a new path, truncate the forward history
|
||||
if (this.historyIndex < this.pathHistory.length - 1) {
|
||||
this.pathHistory = this.pathHistory.slice(0, this.historyIndex + 1);
|
||||
}
|
||||
|
||||
this.pathHistory.push(path);
|
||||
this.historyIndex = this.pathHistory.length - 1;
|
||||
}
|
||||
|
||||
this.currentPath = path;
|
||||
this.selectedItem = null;
|
||||
},
|
||||
|
||||
navigateBack() {
|
||||
if (this.canGoBack) {
|
||||
this.historyIndex--;
|
||||
this.currentPath = this.pathHistory[this.historyIndex];
|
||||
this.selectedItem = null;
|
||||
}
|
||||
},
|
||||
|
||||
navigateForward() {
|
||||
if (this.canGoForward) {
|
||||
this.historyIndex++;
|
||||
this.currentPath = this.pathHistory[this.historyIndex];
|
||||
this.selectedItem = null;
|
||||
}
|
||||
},
|
||||
|
||||
selectItem(item) {
|
||||
this.selectedItem = item;
|
||||
},
|
||||
|
||||
// View actions
|
||||
setViewMode(mode) {
|
||||
this.viewMode = mode;
|
||||
},
|
||||
|
||||
setSortBy(field) {
|
||||
if (this.sortBy === field) {
|
||||
// Toggle direction if clicking the same field
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortBy = field;
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
},
|
||||
|
||||
// File viewer actions
|
||||
openFileViewer(document) {
|
||||
this.viewerDocument = document;
|
||||
this.showFileViewer = true;
|
||||
},
|
||||
|
||||
closeFileViewer() {
|
||||
this.showFileViewer = false;
|
||||
this.viewerDocument = null;
|
||||
},
|
||||
|
||||
// Cabinet access actions
|
||||
async requestCabinetAccess(cabinetId, reason) {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// Find the cabinet
|
||||
let cabinetName = '';
|
||||
const findCabinet = (cabinets, id) => {
|
||||
for (const cabinet of cabinets) {
|
||||
if (cabinet.id === id) {
|
||||
cabinetName = cabinet.name;
|
||||
return cabinet;
|
||||
}
|
||||
if (cabinet.children) {
|
||||
const result = findCabinet(cabinet.children, id);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const cabinet = findCabinet(this.cabinets, cabinetId);
|
||||
if (!cabinet) throw new Error('Cabinet not found');
|
||||
|
||||
// Mock API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Create new access request
|
||||
const newRequest = {
|
||||
id: `req${Date.now()}`,
|
||||
userId: this.currentUser.id,
|
||||
userName: this.currentUser.name,
|
||||
cabinetId: cabinetId,
|
||||
cabinetName: cabinetName,
|
||||
requestDate: new Date().toISOString().split('T')[0],
|
||||
status: 'pending',
|
||||
reason: reason
|
||||
};
|
||||
|
||||
// Add to access requests
|
||||
this.accessRequests.push(newRequest);
|
||||
|
||||
// Update cabinet request status
|
||||
cabinet.accessRequestStatus = 'pending';
|
||||
|
||||
return newRequest;
|
||||
} catch (error) {
|
||||
console.error('Failed to request access:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async approveAccessRequest(requestId) {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// Mock API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Find the request
|
||||
const request = this.accessRequests.find(req => req.id === requestId);
|
||||
if (!request) throw new Error('Request not found');
|
||||
|
||||
// Update request status
|
||||
request.status = 'approved';
|
||||
|
||||
// Find the cabinet and update access
|
||||
const updateCabinetAccess = (cabinets, id) => {
|
||||
for (const cabinet of cabinets) {
|
||||
if (cabinet.id === id) {
|
||||
cabinet.hasAccess = true;
|
||||
cabinet.accessRequestStatus = 'approved';
|
||||
return true;
|
||||
}
|
||||
if (cabinet.children) {
|
||||
if (updateCabinetAccess(cabinet.children, id)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
updateCabinetAccess(this.cabinets, request.cabinetId);
|
||||
|
||||
return request;
|
||||
} catch (error) {
|
||||
console.error('Failed to approve access request:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async rejectAccessRequest(requestId, reason) {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// Mock API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Find the request
|
||||
const request = this.accessRequests.find(req => req.id === requestId);
|
||||
if (!request) throw new Error('Request not found');
|
||||
|
||||
// Update request status
|
||||
request.status = 'rejected';
|
||||
request.rejectionReason = reason;
|
||||
|
||||
// Find the cabinet and update access
|
||||
const updateCabinetAccess = (cabinets, id) => {
|
||||
for (const cabinet of cabinets) {
|
||||
if (cabinet.id === id) {
|
||||
cabinet.hasAccess = false;
|
||||
cabinet.accessRequestStatus = 'rejected';
|
||||
return true;
|
||||
}
|
||||
if (cabinet.children) {
|
||||
if (updateCabinetAccess(cabinet.children, id)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
updateCabinetAccess(this.cabinets, request.cabinetId);
|
||||
|
||||
return request;
|
||||
} catch (error) {
|
||||
console.error('Failed to reject access request:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// CRUD operations
|
||||
async createItem(item) {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// Mock API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Get current cabinet id
|
||||
const currentCabinetId = this.currentPath.split('/').filter(Boolean).pop();
|
||||
|
||||
// Generate a unique ID
|
||||
const newItem = {
|
||||
...item,
|
||||
id: `${item.type}${Date.now()}`,
|
||||
cabinetId: currentCabinetId,
|
||||
modified: new Date().toISOString().split('T')[0],
|
||||
createdBy: this.currentUser.name
|
||||
};
|
||||
|
||||
// Add to items
|
||||
this.items.push(newItem);
|
||||
|
||||
return newItem;
|
||||
} catch (error) {
|
||||
console.error('Failed to create item:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async updateItem(id, updates) {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// Mock API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Find the item
|
||||
const itemIndex = this.items.findIndex(item => item.id === id);
|
||||
if (itemIndex === -1) throw new Error('Item not found');
|
||||
|
||||
// Update the item
|
||||
const updatedItem = {
|
||||
...this.items[itemIndex],
|
||||
...updates,
|
||||
modified: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
|
||||
this.items.splice(itemIndex, 1, updatedItem);
|
||||
|
||||
return updatedItem;
|
||||
} catch (error) {
|
||||
console.error('Failed to update item:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteItem(id) {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// Mock API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Find the item
|
||||
const itemIndex = this.items.findIndex(item => item.id === id);
|
||||
if (itemIndex === -1) throw new Error('Item not found');
|
||||
|
||||
// Remove the item
|
||||
const deletedItem = this.items.splice(itemIndex, 1)[0];
|
||||
|
||||
return deletedItem;
|
||||
} catch (error) {
|
||||
console.error('Failed to delete item:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Search functionality
|
||||
async searchDocuments(query) {
|
||||
this.isLoading = true;
|
||||
this.searchQuery = query;
|
||||
|
||||
try {
|
||||
// Mock API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Simple search implementation - in production would be more sophisticated
|
||||
const results = this.items.filter(item =>
|
||||
item.type === 'file' &&
|
||||
(item.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
(item.info?.title && item.info.title.toLowerCase().includes(query.toLowerCase())) ||
|
||||
(item.info?.subject && item.info.subject.toLowerCase().includes(query.toLowerCase())))
|
||||
);
|
||||
|
||||
this.searchResults = results;
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Failed to search documents:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.searchQuery = '';
|
||||
this.searchResults = [];
|
||||
}
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user