generated from corrad-software/corrad-af-2024
468 lines
15 KiB
Vue
468 lines
15 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useDmsStore } from '~/stores/dms';
|
|
|
|
const props = defineProps({
|
|
documentId: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
showToolbar: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
allowRollback: {
|
|
type: Boolean,
|
|
default: true
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
'version-created',
|
|
'version-restored',
|
|
'version-selected',
|
|
'version-compared'
|
|
]);
|
|
|
|
// Store
|
|
const dmsStore = useDmsStore();
|
|
|
|
// Local state
|
|
const selectedVersions = ref([]);
|
|
const showUploadDialog = ref(false);
|
|
const uploadFile = ref(null);
|
|
const versionNotes = ref('');
|
|
const isCreatingVersion = ref(false);
|
|
const isRestoring = ref(false);
|
|
const compareMode = ref(false);
|
|
|
|
// Get document data
|
|
const document = computed(() => {
|
|
return dmsStore.items.find(item => item.id === props.documentId);
|
|
});
|
|
|
|
// Get version history
|
|
const versions = computed(() => {
|
|
return dmsStore.getDocumentVersions(props.documentId).sort((a, b) => b.version - a.version);
|
|
});
|
|
|
|
// Get current version
|
|
const currentVersion = computed(() => {
|
|
return document.value?.version || 1;
|
|
});
|
|
|
|
// Check if versioning is enabled
|
|
const versioningEnabled = computed(() => {
|
|
return dmsStore.systemSettings.documents.versionControl.enabled;
|
|
});
|
|
|
|
// Format file size
|
|
const formatFileSize = (bytes) => {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
};
|
|
|
|
// Format date
|
|
const formatDate = (dateString) => {
|
|
return new Date(dateString).toLocaleString();
|
|
};
|
|
|
|
// Get time ago
|
|
const getTimeAgo = (dateString) => {
|
|
const now = new Date();
|
|
const date = new Date(dateString);
|
|
const diffInSeconds = Math.floor((now - date) / 1000);
|
|
|
|
if (diffInSeconds < 60) return 'Just now';
|
|
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
|
|
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
|
return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
|
};
|
|
|
|
// Handle file upload for new version
|
|
const handleFileUpload = (event) => {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
uploadFile.value = file;
|
|
}
|
|
};
|
|
|
|
// Create new version
|
|
const createNewVersion = async () => {
|
|
if (!uploadFile.value || !versionNotes.value.trim()) {
|
|
return;
|
|
}
|
|
|
|
isCreatingVersion.value = true;
|
|
|
|
try {
|
|
const metadata = {
|
|
author: dmsStore.currentUser.name,
|
|
changes: versionNotes.value.trim()
|
|
};
|
|
|
|
const newVersion = await dmsStore.createNewVersion(
|
|
props.documentId,
|
|
uploadFile.value,
|
|
metadata
|
|
);
|
|
|
|
emit('version-created', newVersion);
|
|
|
|
// Reset form
|
|
uploadFile.value = null;
|
|
versionNotes.value = '';
|
|
showUploadDialog.value = false;
|
|
|
|
// Reset file input
|
|
const fileInput = document.querySelector('#version-file-input');
|
|
if (fileInput) fileInput.value = '';
|
|
|
|
} catch (error) {
|
|
console.error('Failed to create version:', error);
|
|
// Handle error (show toast, etc.)
|
|
} finally {
|
|
isCreatingVersion.value = false;
|
|
}
|
|
};
|
|
|
|
// Restore to specific version
|
|
const restoreToVersion = async (version) => {
|
|
if (!props.allowRollback) return;
|
|
|
|
isRestoring.value = true;
|
|
|
|
try {
|
|
// Simulate version restoration
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
|
|
// Update document version
|
|
if (document.value) {
|
|
document.value.version = version.version;
|
|
document.value.modified = new Date().toLocaleDateString();
|
|
}
|
|
|
|
emit('version-restored', version);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to restore version:', error);
|
|
} finally {
|
|
isRestoring.value = false;
|
|
}
|
|
};
|
|
|
|
// Toggle version selection for comparison
|
|
const toggleVersionSelection = (version) => {
|
|
const index = selectedVersions.value.findIndex(v => v.version === version.version);
|
|
|
|
if (index > -1) {
|
|
selectedVersions.value.splice(index, 1);
|
|
} else if (selectedVersions.value.length < 2) {
|
|
selectedVersions.value.push(version);
|
|
} else {
|
|
// Replace oldest selection
|
|
selectedVersions.value.shift();
|
|
selectedVersions.value.push(version);
|
|
}
|
|
|
|
emit('version-selected', selectedVersions.value);
|
|
};
|
|
|
|
// Compare selected versions
|
|
const compareVersions = () => {
|
|
if (selectedVersions.value.length === 2) {
|
|
compareMode.value = true;
|
|
emit('version-compared', selectedVersions.value);
|
|
}
|
|
};
|
|
|
|
// Clear selection
|
|
const clearSelection = () => {
|
|
selectedVersions.value = [];
|
|
compareMode.value = false;
|
|
};
|
|
|
|
// Download specific version
|
|
const downloadVersion = (version) => {
|
|
// Simulate download
|
|
console.log('Downloading version', version.version);
|
|
// In real implementation, this would trigger file download
|
|
};
|
|
|
|
// Get version status badge
|
|
const getVersionStatus = (version) => {
|
|
if (version.version === currentVersion.value) {
|
|
return { text: 'Current', color: 'green' };
|
|
} else if (version.version === Math.max(...versions.value.map(v => v.version))) {
|
|
return { text: 'Latest', color: 'blue' };
|
|
} else {
|
|
return { text: 'Previous', color: 'gray' };
|
|
}
|
|
};
|
|
|
|
// Cancel upload dialog
|
|
const cancelUpload = () => {
|
|
uploadFile.value = null;
|
|
versionNotes.value = '';
|
|
showUploadDialog.value = false;
|
|
const fileInput = document.querySelector('#version-file-input');
|
|
if (fileInput) fileInput.value = '';
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="document-version-manager">
|
|
<!-- Toolbar -->
|
|
<div v-if="showToolbar" class="toolbar mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-4">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
|
Version History
|
|
</h3>
|
|
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
<span v-if="document">{{ document.name }}</span>
|
|
<span class="mx-2">•</span>
|
|
<span>{{ versions.length }} version{{ versions.length !== 1 ? 's' : '' }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2">
|
|
<!-- Compare button -->
|
|
<button
|
|
v-if="selectedVersions.length === 2"
|
|
@click="compareVersions"
|
|
class="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
|
|
>
|
|
Compare Selected
|
|
</button>
|
|
|
|
<!-- Clear selection -->
|
|
<button
|
|
v-if="selectedVersions.length > 0"
|
|
@click="clearSelection"
|
|
class="px-3 py-1 bg-gray-600 text-white rounded hover:bg-gray-700 text-sm"
|
|
>
|
|
Clear Selection
|
|
</button>
|
|
|
|
<!-- Upload new version -->
|
|
<button
|
|
v-if="versioningEnabled"
|
|
@click="showUploadDialog = true"
|
|
class="px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700 text-sm"
|
|
>
|
|
Upload New Version
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selected versions info -->
|
|
<div v-if="selectedVersions.length > 0" class="mt-3 p-2 bg-blue-50 dark:bg-blue-900/20 rounded">
|
|
<p class="text-sm text-blue-800 dark:text-blue-200">
|
|
{{ selectedVersions.length }} version{{ selectedVersions.length !== 1 ? 's' : '' }} selected
|
|
<span v-if="selectedVersions.length === 2">(Ready to compare)</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Version list -->
|
|
<div class="version-list space-y-4">
|
|
<div
|
|
v-for="version in versions"
|
|
:key="version.version"
|
|
class="version-item border border-gray-200 dark:border-gray-600 rounded-lg p-4"
|
|
:class="{
|
|
'border-blue-500 bg-blue-50 dark:bg-blue-900/20': selectedVersions.some(v => v.version === version.version),
|
|
'border-green-500 bg-green-50 dark:bg-green-900/20': version.version === currentVersion
|
|
}"
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex items-start space-x-4 flex-1">
|
|
<!-- Selection checkbox -->
|
|
<div class="flex items-center mt-1">
|
|
<input
|
|
type="checkbox"
|
|
:checked="selectedVersions.some(v => v.version === version.version)"
|
|
@change="toggleVersionSelection(version)"
|
|
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Version info -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center space-x-3 mb-2">
|
|
<h4 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
|
Version {{ version.version }}
|
|
</h4>
|
|
|
|
<!-- Status badge -->
|
|
<span
|
|
class="px-2 py-1 text-xs font-medium rounded-full"
|
|
:class="`bg-${getVersionStatus(version).color}-100 text-${getVersionStatus(version).color}-800 dark:bg-${getVersionStatus(version).color}-900/20 dark:text-${getVersionStatus(version).color}-200`"
|
|
>
|
|
{{ getVersionStatus(version).text }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600 dark:text-gray-400">
|
|
<div>
|
|
<p><strong>Author:</strong> {{ version.author }}</p>
|
|
<p><strong>Upload Date:</strong> {{ formatDate(version.uploadDate) }}</p>
|
|
<p><strong>Time Ago:</strong> {{ getTimeAgo(version.uploadDate) }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<p><strong>File Size:</strong> {{ formatFileSize(version.fileSize) }}</p>
|
|
<p v-if="version.changes"><strong>Changes:</strong> {{ version.changes }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex items-center space-x-2 ml-4">
|
|
<!-- Download button -->
|
|
<button
|
|
@click="downloadVersion(version)"
|
|
class="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900/20 rounded"
|
|
title="Download this version"
|
|
>
|
|
<Icon name="mdi:download" class="w-4 h-4" />
|
|
</button>
|
|
|
|
<!-- View button -->
|
|
<button
|
|
class="p-2 text-gray-400 hover:text-green-600 hover:bg-green-100 dark:hover:bg-green-900/20 rounded"
|
|
title="View this version"
|
|
>
|
|
<Icon name="mdi:eye" class="w-4 h-4" />
|
|
</button>
|
|
|
|
<!-- Restore button -->
|
|
<button
|
|
v-if="allowRollback && version.version !== currentVersion"
|
|
@click="restoreToVersion(version)"
|
|
:disabled="isRestoring"
|
|
class="p-2 text-gray-400 hover:text-orange-600 hover:bg-orange-100 dark:hover:bg-orange-900/20 rounded disabled:opacity-50"
|
|
title="Restore to this version"
|
|
>
|
|
<Icon name="mdi:restore" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-if="versions.length === 0" class="text-center py-8">
|
|
<Icon name="mdi:file-clock" class="w-16 h-16 mx-auto text-gray-400 mb-4" />
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
No Version History
|
|
</h3>
|
|
<p class="text-gray-500 dark:text-gray-400">
|
|
This document doesn't have any version history yet.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Upload new version dialog -->
|
|
<rs-modal :visible="showUploadDialog" @close="cancelUpload" size="lg">
|
|
<template #header>
|
|
<h3 class="text-lg font-semibold">Upload New Version</h3>
|
|
</template>
|
|
|
|
<template #body>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Select File
|
|
</label>
|
|
<input
|
|
id="version-file-input"
|
|
type="file"
|
|
@change="handleFileUpload"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700"
|
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Version Notes *
|
|
</label>
|
|
<textarea
|
|
v-model="versionNotes"
|
|
rows="3"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700"
|
|
placeholder="Describe what changed in this version..."
|
|
required
|
|
></textarea>
|
|
</div>
|
|
|
|
<div v-if="uploadFile" class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
|
|
<div class="flex items-center space-x-3">
|
|
<Icon name="mdi:file" class="w-5 h-5 text-gray-400" />
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ uploadFile.name }}
|
|
</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ formatFileSize(uploadFile.size) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-end space-x-2">
|
|
<button
|
|
@click="cancelUpload"
|
|
class="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
>
|
|
Cancel
|
|
</button>
|
|
|
|
<button
|
|
@click="createNewVersion"
|
|
:disabled="!uploadFile || !versionNotes.trim() || isCreatingVersion"
|
|
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span v-if="isCreatingVersion">Uploading...</span>
|
|
<span v-else>Create Version</span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</rs-modal>
|
|
|
|
<!-- Restore confirmation -->
|
|
<div v-if="isRestoring" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm mx-4">
|
|
<div class="text-center">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
Restoring Version
|
|
</h3>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Please wait while we restore the document to the selected version...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.version-item {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.version-item:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
}
|
|
</style> |