generated from corrad-software/corrad-af-2024
437 lines
15 KiB
Vue
437 lines
15 KiB
Vue
<template>
|
|
<div
|
|
v-if="visible"
|
|
class="fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center"
|
|
@click="closeModal"
|
|
>
|
|
<div
|
|
class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-hidden"
|
|
@click.stop
|
|
>
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
Document Information
|
|
</h2>
|
|
<button
|
|
@click="closeModal"
|
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
<Icon name="mdi:close" class="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="p-6 overflow-y-auto max-h-[60vh]">
|
|
<!-- Document Details -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<!-- Basic Information -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Basic Information</h3>
|
|
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
|
|
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ document.name }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Type</label>
|
|
<div class="mt-1 flex items-center space-x-2">
|
|
<Icon
|
|
:name="getFileTypeIcon(document.name)"
|
|
:class="['w-4 h-4', getFileTypeColor(document.name)]"
|
|
/>
|
|
<span class="text-sm text-gray-900 dark:text-gray-100">
|
|
{{ getFileTypeName(document.name) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Size</label>
|
|
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ formatFileSize(document.size) }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Last Modified</label>
|
|
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ formatDate(document.lastModified) }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Owner</label>
|
|
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ document.owner || 'Unknown' }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access & Security -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Access & Security</h3>
|
|
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Access Level</label>
|
|
<div class="mt-1">
|
|
<span :class="getAccessLevelBadge(document.accessLevel).class"
|
|
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium">
|
|
{{ getAccessLevelBadge(document.accessLevel).text }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="document.permissions">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Permissions</label>
|
|
<div class="mt-1 flex flex-wrap gap-1">
|
|
<span
|
|
v-for="permission in document.permissions"
|
|
:key="permission"
|
|
class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
|
|
>
|
|
{{ permission }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="document.encryption">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Encryption</label>
|
|
<div class="mt-1 flex items-center space-x-2">
|
|
<Icon name="mdi:shield-check" class="w-4 h-4 text-green-500" />
|
|
<span class="text-sm text-gray-900 dark:text-gray-100">{{ document.encryption }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags Section -->
|
|
<div class="mt-6">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">Tags</h3>
|
|
|
|
<div class="space-y-3">
|
|
<!-- Existing Tags -->
|
|
<div v-if="documentTags.length > 0" class="flex flex-wrap gap-2">
|
|
<span
|
|
v-for="tag in documentTags"
|
|
:key="tag"
|
|
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
|
|
>
|
|
{{ tag }}
|
|
<button
|
|
@click="removeTag(tag)"
|
|
class="ml-2 text-gray-500 hover:text-red-500"
|
|
>
|
|
<Icon name="mdi:close" class="w-3 h-3" />
|
|
</button>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Add New Tag -->
|
|
<div class="flex items-center space-x-2">
|
|
<input
|
|
v-model="newTag"
|
|
@keydown.enter="addTag"
|
|
type="text"
|
|
placeholder="Add a tag..."
|
|
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm
|
|
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100
|
|
focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
<button
|
|
@click="addTag"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm"
|
|
>
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Annotations Summary -->
|
|
<div v-if="annotations.length > 0" class="mt-6">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">Annotations</h3>
|
|
|
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
|
<div class="grid grid-cols-3 gap-4 text-center">
|
|
<div>
|
|
<div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
|
|
{{ getAnnotationCount('highlight') }}
|
|
</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Highlights</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
|
{{ getAnnotationCount('comment') }}
|
|
</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Comments</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
|
|
{{ getAnnotationCount('draw') }}
|
|
</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Drawings</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Version History (if available) -->
|
|
<div v-if="document.versions" class="mt-6">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">Version History</h3>
|
|
|
|
<div class="space-y-2">
|
|
<div
|
|
v-for="version in document.versions.slice(0, 5)"
|
|
:key="version.id"
|
|
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
|
|
>
|
|
<div class="flex items-center space-x-3">
|
|
<Icon name="mdi:history" class="w-4 h-4 text-gray-500" />
|
|
<div>
|
|
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
Version {{ version.number }}
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ formatDate(version.timestamp) }} by {{ version.author }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ formatFileSize(version.size) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Location -->
|
|
<div class="mt-6">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">Location</h3>
|
|
|
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
|
<div class="flex items-center space-x-2 text-sm text-gray-700 dark:text-gray-300">
|
|
<Icon name="mdi:folder" class="w-4 h-4" />
|
|
<span>{{ document.path || '/Documents/' + document.name }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="flex items-center justify-end space-x-3 p-6 border-t border-gray-200 dark:border-gray-700">
|
|
<button
|
|
@click="closeModal"
|
|
class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"
|
|
>
|
|
Close
|
|
</button>
|
|
<button
|
|
@click="downloadDocument"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
>
|
|
Download
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch } from 'vue';
|
|
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
document: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
annotations: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
'update:visible',
|
|
'tag-added',
|
|
'tag-removed'
|
|
]);
|
|
|
|
// State
|
|
const newTag = ref('');
|
|
const documentTags = ref([]);
|
|
|
|
// Computed
|
|
const isVisible = computed({
|
|
get() {
|
|
return props.visible;
|
|
},
|
|
set(value) {
|
|
emit('update:visible', value);
|
|
}
|
|
});
|
|
|
|
// Methods
|
|
const closeModal = () => {
|
|
isVisible.value = false;
|
|
};
|
|
|
|
const getFileTypeIcon = (fileName) => {
|
|
if (!fileName) return 'mdi:file-document';
|
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
const iconMap = {
|
|
pdf: 'mdi:file-pdf-box',
|
|
doc: 'mdi:file-word-box',
|
|
docx: 'mdi:file-word-box',
|
|
xls: 'mdi:file-excel-box',
|
|
xlsx: 'mdi:file-excel-box',
|
|
ppt: 'mdi:file-powerpoint-box',
|
|
pptx: 'mdi:file-powerpoint-box',
|
|
txt: 'mdi:file-document-outline',
|
|
md: 'mdi:language-markdown',
|
|
jpg: 'mdi:file-image',
|
|
jpeg: 'mdi:file-image',
|
|
png: 'mdi:file-image',
|
|
gif: 'mdi:file-image',
|
|
default: 'mdi:file-document'
|
|
};
|
|
return iconMap[extension] || iconMap.default;
|
|
};
|
|
|
|
const getFileTypeColor = (fileName) => {
|
|
if (!fileName) return 'text-gray-400';
|
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
const colorMap = {
|
|
pdf: 'text-red-400',
|
|
doc: 'text-blue-400',
|
|
docx: 'text-blue-400',
|
|
xls: 'text-green-400',
|
|
xlsx: 'text-green-400',
|
|
ppt: 'text-orange-400',
|
|
pptx: 'text-orange-400',
|
|
txt: 'text-gray-400',
|
|
md: 'text-purple-400',
|
|
jpg: 'text-purple-400',
|
|
jpeg: 'text-purple-400',
|
|
png: 'text-purple-400',
|
|
gif: 'text-purple-400',
|
|
default: 'text-gray-400'
|
|
};
|
|
return colorMap[extension] || colorMap.default;
|
|
};
|
|
|
|
const getFileTypeName = (fileName) => {
|
|
if (!fileName) return 'Unknown';
|
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
const nameMap = {
|
|
pdf: 'PDF Document',
|
|
doc: 'Word Document',
|
|
docx: 'Word Document',
|
|
xls: 'Excel Spreadsheet',
|
|
xlsx: 'Excel Spreadsheet',
|
|
ppt: 'PowerPoint Presentation',
|
|
pptx: 'PowerPoint Presentation',
|
|
txt: 'Text File',
|
|
md: 'Markdown Document',
|
|
jpg: 'JPEG Image',
|
|
jpeg: 'JPEG Image',
|
|
png: 'PNG Image',
|
|
gif: 'GIF Image',
|
|
default: 'Document'
|
|
};
|
|
return nameMap[extension] || nameMap.default;
|
|
};
|
|
|
|
const getAccessLevelBadge = (accessLevel) => {
|
|
switch (accessLevel) {
|
|
case 'public':
|
|
return { class: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400', text: 'Public' };
|
|
case 'department':
|
|
return { class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400', text: 'Department' };
|
|
case 'private':
|
|
return { class: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400', text: 'Private' };
|
|
case 'restricted':
|
|
return { class: 'bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-400', text: 'Restricted' };
|
|
default:
|
|
return { class: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400', text: 'Unknown' };
|
|
}
|
|
};
|
|
|
|
const formatFileSize = (size) => {
|
|
if (typeof size === 'string') return size;
|
|
if (!size) return '0 B';
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let index = 0;
|
|
while (size >= 1024 && index < units.length - 1) {
|
|
size /= 1024;
|
|
index++;
|
|
}
|
|
return `${size.toFixed(1)} ${units[index]}`;
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
};
|
|
|
|
const getAnnotationCount = (type) => {
|
|
return props.annotations.filter(annotation => annotation.type === type).length;
|
|
};
|
|
|
|
const addTag = () => {
|
|
if (newTag.value.trim() && !documentTags.value.includes(newTag.value.trim())) {
|
|
const tag = newTag.value.trim();
|
|
documentTags.value.push(tag);
|
|
emit('tag-added', tag);
|
|
newTag.value = '';
|
|
}
|
|
};
|
|
|
|
const removeTag = (tag) => {
|
|
const index = documentTags.value.indexOf(tag);
|
|
if (index > -1) {
|
|
documentTags.value.splice(index, 1);
|
|
emit('tag-removed', tag);
|
|
}
|
|
};
|
|
|
|
const downloadDocument = () => {
|
|
// Create download link
|
|
const link = document.createElement('a');
|
|
link.href = `/api/documents/${props.document.id}/download`;
|
|
link.download = props.document.name;
|
|
link.click();
|
|
};
|
|
|
|
// Watch for document changes
|
|
watch(() => props.document, (newDocument) => {
|
|
if (newDocument) {
|
|
documentTags.value = [...(newDocument.tags || [])];
|
|
}
|
|
}, { immediate: true });
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Modal animations */
|
|
.modal-enter-active,
|
|
.modal-leave-active {
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.modal-enter-from,
|
|
.modal-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.modal-content-enter-active,
|
|
.modal-content-leave-active {
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.modal-content-enter-from,
|
|
.modal-content-leave-to {
|
|
transform: scale(0.95) translateY(-20px);
|
|
}
|
|
</style> |