Added File Preview, Hiearchy View, Access Type Display

This commit is contained in:
Aiman Fakhrullah Mantasan 2025-05-30 19:14:03 +08:00
parent 9b14213beb
commit 1aac9905e3
4 changed files with 1070 additions and 465 deletions

View File

@ -3,7 +3,7 @@ import { ref, computed } from 'vue';
import { useDmsStore } from '~/stores/dms';
const props = defineProps({
document: {
item: {
type: Object,
required: true
},
@ -13,63 +13,63 @@ const props = defineProps({
}
});
const emit = defineEmits(['close', 'requestSubmitted']);
const emit = defineEmits(['close', 'submit']);
// Store
const dmsStore = useDmsStore();
// Form state
const selectedAccessLevel = ref('view');
const selectedAccessType = ref('view');
const accessDuration = ref('7 days');
const justification = ref('');
const isSubmitting = ref(false);
const formError = ref('');
// Access level options
const accessLevels = [
// Access type options
const accessTypes = [
{ 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' }
{ id: 'download', label: 'Download', description: 'Can view and download' },
{ id: 'print', label: 'Print', description: 'Can view and print' },
{ id: 'full', label: 'Full Access', description: 'View, download and print' }
];
// Access duration options
const durationOptions = [
'7 days',
'14 days',
'30 days',
'60 days',
'90 days',
'Permanent'
];
// Computed properties
const documentTitle = computed(() => {
return props.document?.name || 'Document';
const itemTitle = computed(() => {
return props.item?.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';
const itemFileName = computed(() => {
if (props.item?.type === 'file') {
return props.item.name;
}
return '';
});
// Methods
const closeDialog = () => {
// Reset form
selectedAccessType.value = 'view';
accessDuration.value = '7 days';
justification.value = '';
formError.value = '';
emit('close');
};
const submitRequest = async () => {
// Validate form
if (!selectedAccessLevel.value) {
formError.value = 'Please select an access level';
if (!selectedAccessType.value) {
formError.value = 'Please select an access type';
return;
}
@ -83,12 +83,17 @@ const submitRequest = async () => {
try {
// Submit the request to the store
const request = await dmsStore.requestAccess(props.document.id, selectedAccessLevel.value);
const request = await dmsStore.requestAccess(
props.item.id,
selectedAccessType.value,
justification.value,
accessDuration.value
);
// Emit success event
emit('requestSubmitted', request);
emit('submit', request);
// Close the dialog
// Close the dialog (this will also reset the form)
closeDialog();
} catch (error) {
formError.value = 'Failed to submit access request. Please try again.';
@ -107,79 +112,121 @@ const submitRequest = async () => {
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 class="p-6">
<!-- Document Information Section -->
<div class="mb-6">
<div class="bg-blue-50 dark:bg-blue-900/10 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
<h3 class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">Document Information</h3>
<div class="space-y-1">
<div class="flex">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300 w-12">Title:</span>
<span class="text-sm text-gray-900 dark:text-gray-100">{{ itemTitle }}</span>
</div>
<div class="ml-3">
<div class="font-medium">{{ level.label }}</div>
<p class="text-sm text-gray-500">{{ level.description }}</p>
<div class="flex">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300 w-12">File:</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ itemFileName }}</span>
</div>
</div>
</div>
</div>
<!-- Justification -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Justification:</label>
<!-- Form error -->
<div v-if="formError" class="mb-4 p-3 bg-red-50 border border-red-200 text-red-800 rounded-md text-sm">
{{ formError }}
</div>
<!-- Access Type Section -->
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Access Type</h3>
<div class="grid grid-cols-2 gap-3">
<div
v-for="accessType in accessTypes"
:key="accessType.id"
class="relative"
>
<label
:for="accessType.id"
class="flex items-start p-3 border rounded-lg cursor-pointer transition-colors"
:class="selectedAccessType === accessType.id
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800'"
>
<div class="flex items-center h-5">
<input
:id="accessType.id"
v-model="selectedAccessType"
:value="accessType.id"
type="radio"
class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
</div>
<div class="ml-3">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ accessType.label }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ accessType.description }}
</div>
</div>
</label>
</div>
</div>
</div>
<!-- Access Duration Section -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Access Duration</label>
<select
v-model="accessDuration"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option v-for="duration in durationOptions" :key="duration" :value="duration">
{{ duration }}
</option>
</select>
</div>
<!-- Justification Section -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
Justification
<span class="text-red-500">*</span>
</label>
<textarea
v-model="justification"
rows="3"
rows="4"
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"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
></textarea>
<p class="text-xs text-gray-500 mt-1">Your request will be reviewed by the document owner or administrator.</p>
</div>
<!-- Footer Note -->
<p class="text-xs text-gray-500 dark:text-gray-400 mb-6">
Your request will be reviewed by the document owner or administrator.
</p>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-3">
<rs-button color="secondary" @click="closeDialog" :disabled="isSubmitting">
<div class="flex justify-end gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-800">
<button
@click="closeDialog"
:disabled="isSubmitting"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
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>
</button>
<button
@click="submitRequest"
:disabled="isSubmitting"
class="px-4 py-2 text-sm font-medium text-white bg-gray-900 dark:bg-gray-800 border border-transparent rounded-lg hover:bg-gray-800 dark:hover:bg-gray-700 focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center"
>
<svg v-if="isSubmitting" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Submit Request
</button>
</div>
</template>
</rs-modal>

File diff suppressed because it is too large Load Diff

View File

@ -194,6 +194,7 @@ onMounted(() => {
<DMSExplorer
:initial-path="'/'"
:view-mode="'list'"
:active-document-tab="activeTab"
@item-selected="handleItemSelected"
@view-mode-changed="handleViewModeChanged"
@path-changed="handlePathChanged"

View File

@ -735,6 +735,57 @@ export const useDmsStore = defineStore('dms', {
clearSearch() {
this.searchQuery = '';
this.searchResults = [];
},
// Access request functionality
async requestAccess(itemId, accessLevel, justification, duration = '7 days') {
this.isLoading = true;
try {
// Mock API delay
await new Promise(resolve => setTimeout(resolve, 500));
// Generate a unique request ID
const requestId = `req${Date.now()}`;
// Create new access request
const newRequest = {
id: requestId,
userId: 'current-user-id', // Would come from auth store
userName: 'Current User', // Would come from auth store
itemId: itemId,
accessLevel: accessLevel,
justification: justification,
duration: duration,
requestDate: new Date().toISOString().split('T')[0],
status: 'pending'
};
// Add to access requests
this.accessRequests.push(newRequest);
// Update the item's access request status (for mock data)
// In production, this would be handled server-side
const updateItemStatus = (items, id) => {
for (const item of items) {
if (item.id === id) {
item.accessRequestStatus = 'pending';
return true;
}
}
return false;
};
// Try to find and update the item in the mock data arrays
// This is a simplified approach for demo purposes
return newRequest;
} catch (error) {
console.error('Failed to submit access request:', error);
throw error;
} finally {
this.isLoading = false;
}
}
}
});