Added initial cabinet view for development

This commit is contained in:
Aiman Fakhrullah Mantasan 2025-05-30 16:16:59 +08:00
parent 8e01631a4f
commit b00a9f94c3
13 changed files with 4582 additions and 16 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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": {}

View 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
View 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
View 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>

View 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>

View File

@ -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
View 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 = [];
}
}
});