generated from corrad-software/corrad-af-2024
479 lines
19 KiB
Vue
479 lines
19 KiB
Vue
<script setup>
|
|
import { ref, computed, watch, onMounted } from 'vue';
|
|
import { useDmsStore } from '~/stores/dms';
|
|
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
currentPath: {
|
|
type: String,
|
|
default: '/'
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['close', 'upload']);
|
|
|
|
// Store
|
|
const dmsStore = useDmsStore();
|
|
|
|
// Local state
|
|
const isUploading = ref(false);
|
|
const uploadProgress = ref(0);
|
|
const selectedFiles = ref([]);
|
|
const dragCounter = ref(0);
|
|
const isDragOver = ref(false);
|
|
const metadataTemplate = ref('standard');
|
|
const validationErrors = ref({});
|
|
|
|
// File metadata - using enhanced system
|
|
const fileMetadata = ref({});
|
|
|
|
// Get metadata template options
|
|
const templateOptions = computed(() => [
|
|
{ value: 'standard', label: 'Standard Document' },
|
|
{ value: 'contract', label: 'Contract/Agreement' },
|
|
{ value: 'report', label: 'Report/Analysis' }
|
|
]);
|
|
|
|
// Get custom fields from settings
|
|
const customFields = computed(() => {
|
|
return dmsStore.systemSettings.metadata.customFields || [];
|
|
});
|
|
|
|
// Get tag suggestions
|
|
const tagSuggestions = ref([]);
|
|
|
|
// Methods
|
|
const openFileDialog = () => {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.multiple = true;
|
|
input.accept = dmsStore.systemSettings.upload.allowedFileTypes.map(ext => `.${ext}`).join(',');
|
|
input.onchange = (e) => handleFiles(Array.from(e.target.files));
|
|
input.click();
|
|
};
|
|
|
|
const handleFiles = (files) => {
|
|
const validFiles = [];
|
|
const errors = {};
|
|
|
|
files.forEach(file => {
|
|
const validation = dmsStore.validateFile(file);
|
|
if (validation.isValid) {
|
|
validFiles.push(file);
|
|
|
|
// Initialize metadata for each file
|
|
const template = dmsStore.metadataTemplates[metadataTemplate.value];
|
|
fileMetadata.value[file.name] = {
|
|
...template,
|
|
title: file.name.split('.')[0],
|
|
author: 'Current User' // Get from auth store
|
|
};
|
|
} else {
|
|
errors[file.name] = validation.errors;
|
|
}
|
|
});
|
|
|
|
selectedFiles.value = [...selectedFiles.value, ...validFiles];
|
|
validationErrors.value = { ...validationErrors.value, ...errors };
|
|
};
|
|
|
|
const removeFile = (index) => {
|
|
const file = selectedFiles.value[index];
|
|
delete fileMetadata.value[file.name];
|
|
delete validationErrors.value[file.name];
|
|
selectedFiles.value.splice(index, 1);
|
|
};
|
|
|
|
const validateMetadata = () => {
|
|
const errors = {};
|
|
|
|
selectedFiles.value.forEach(file => {
|
|
const metadata = fileMetadata.value[file.name];
|
|
const validation = dmsStore.validateMetadata(metadata, metadataTemplate.value);
|
|
|
|
if (!validation.isValid) {
|
|
errors[file.name] = validation.errors;
|
|
}
|
|
});
|
|
|
|
validationErrors.value = { ...validationErrors.value, ...errors };
|
|
return Object.keys(errors).length === 0;
|
|
};
|
|
|
|
const uploadFiles = async () => {
|
|
if (!validateMetadata()) {
|
|
return;
|
|
}
|
|
|
|
isUploading.value = true;
|
|
uploadProgress.value = 0;
|
|
|
|
try {
|
|
for (let i = 0; i < selectedFiles.value.length; i++) {
|
|
const file = selectedFiles.value[i];
|
|
const metadata = fileMetadata.value[file.name];
|
|
|
|
// Upload with enhanced metadata
|
|
await dmsStore.uploadFileWithMetadata(file, metadata, props.currentPath);
|
|
|
|
uploadProgress.value = Math.round(((i + 1) / selectedFiles.value.length) * 100);
|
|
}
|
|
|
|
emit('upload', selectedFiles.value);
|
|
closeDialog();
|
|
} catch (error) {
|
|
console.error('Upload failed:', error);
|
|
// Show error to user
|
|
} finally {
|
|
isUploading.value = false;
|
|
}
|
|
};
|
|
|
|
const closeDialog = () => {
|
|
selectedFiles.value = [];
|
|
fileMetadata.value = {};
|
|
validationErrors.value = {};
|
|
uploadProgress.value = 0;
|
|
emit('close');
|
|
};
|
|
|
|
// Drag and drop handlers
|
|
const handleDragEnter = (e) => {
|
|
e.preventDefault();
|
|
dragCounter.value++;
|
|
isDragOver.value = true;
|
|
};
|
|
|
|
const handleDragLeave = (e) => {
|
|
e.preventDefault();
|
|
dragCounter.value--;
|
|
if (dragCounter.value === 0) {
|
|
isDragOver.value = false;
|
|
}
|
|
};
|
|
|
|
const handleDragOver = (e) => {
|
|
e.preventDefault();
|
|
};
|
|
|
|
const handleDrop = (e) => {
|
|
e.preventDefault();
|
|
dragCounter.value = 0;
|
|
isDragOver.value = false;
|
|
|
|
const files = Array.from(e.dataTransfer.files);
|
|
handleFiles(files);
|
|
};
|
|
|
|
// Tag input handling
|
|
const handleTagInput = (fileName, input) => {
|
|
if (input.length > 2) {
|
|
tagSuggestions.value = dmsStore.getTagSuggestions(input);
|
|
} else {
|
|
tagSuggestions.value = [];
|
|
}
|
|
};
|
|
|
|
const addTag = (fileName, tag) => {
|
|
if (!fileMetadata.value[fileName].tags.includes(tag)) {
|
|
fileMetadata.value[fileName].tags.push(tag);
|
|
}
|
|
tagSuggestions.value = [];
|
|
};
|
|
|
|
const removeTag = (fileName, tagIndex) => {
|
|
fileMetadata.value[fileName].tags.splice(tagIndex, 1);
|
|
};
|
|
|
|
// Watch for template changes
|
|
watch(metadataTemplate, (newTemplate) => {
|
|
selectedFiles.value.forEach(file => {
|
|
const currentMetadata = fileMetadata.value[file.name] || {};
|
|
const template = dmsStore.metadataTemplates[newTemplate];
|
|
|
|
// Merge current values with new template
|
|
fileMetadata.value[file.name] = {
|
|
...template,
|
|
...currentMetadata,
|
|
title: currentMetadata.title || file.name.split('.')[0]
|
|
};
|
|
});
|
|
});
|
|
|
|
// Format file size
|
|
const formatFileSize = (bytes) => {
|
|
return dmsStore.formatFileSize(bytes);
|
|
};
|
|
|
|
// Get field component type
|
|
const getFieldComponent = (fieldType) => {
|
|
switch (fieldType) {
|
|
case 'dropdown':
|
|
case 'select':
|
|
return 'select';
|
|
case 'date':
|
|
return 'date';
|
|
case 'number':
|
|
return 'number';
|
|
default:
|
|
return 'text';
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<rs-modal :visible="visible" @close="closeDialog" size="4xl">
|
|
<template #header>
|
|
<div class="flex items-center space-x-2">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<polyline points="10,9 9,9 8,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Upload Documents</h3>
|
|
</div>
|
|
</template>
|
|
|
|
<template #body>
|
|
<div class="space-y-6">
|
|
<!-- Template Selection -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Document Template
|
|
</label>
|
|
<select v-model="metadataTemplate"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
|
|
<option v-for="template in templateOptions" :key="template.value" :value="template.value">
|
|
{{ template.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- File Upload Area -->
|
|
<div class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center"
|
|
:class="{
|
|
'border-blue-500 bg-blue-50 dark:bg-blue-900/10': isDragOver,
|
|
'hover:border-gray-400': !isDragOver
|
|
}"
|
|
@dragenter="handleDragEnter"
|
|
@dragleave="handleDragLeave"
|
|
@dragover="handleDragOver"
|
|
@drop="handleDrop">
|
|
|
|
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
|
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
</svg>
|
|
|
|
<div v-if="isDragOver" class="text-blue-600 dark:text-blue-400">
|
|
<p class="text-lg font-medium">Drop files here</p>
|
|
</div>
|
|
<div v-else>
|
|
<p class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
Drag and drop files here, or
|
|
<button @click="openFileDialog" class="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
|
|
browse
|
|
</button>
|
|
</p>
|
|
<p class="text-sm text-gray-500">
|
|
Supported: {{ dmsStore.systemSettings.upload.allowedFileTypes.join(', ').toUpperCase() }}
|
|
</p>
|
|
<p class="text-sm text-gray-500">
|
|
Max size: {{ Math.round(dmsStore.systemSettings.upload.maxFileSize / (1024 * 1024)) }}MB per file
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selected Files -->
|
|
<div v-if="selectedFiles.length > 0" class="space-y-4">
|
|
<h4 class="text-md font-semibold text-gray-900 dark:text-gray-100">
|
|
Selected Files ({{ selectedFiles.length }})
|
|
</h4>
|
|
|
|
<div class="space-y-4 max-h-96 overflow-y-auto">
|
|
<div v-for="(file, index) in selectedFiles" :key="file.name"
|
|
class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
|
|
|
<!-- File Header -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center space-x-3">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2"/>
|
|
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2"/>
|
|
</svg>
|
|
<div>
|
|
<p class="font-medium text-gray-900 dark:text-gray-100">{{ file.name }}</p>
|
|
<p class="text-sm text-gray-500">{{ formatFileSize(file.size) }}</p>
|
|
</div>
|
|
</div>
|
|
<button @click="removeFile(index)"
|
|
class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Validation Errors -->
|
|
<div v-if="validationErrors[file.name]" class="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
|
|
<p class="text-sm font-medium text-red-800 dark:text-red-200 mb-1">Validation Errors:</p>
|
|
<ul class="text-sm text-red-700 dark:text-red-300 list-disc list-inside">
|
|
<li v-for="error in validationErrors[file.name]" :key="error">{{ error }}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Metadata Form -->
|
|
<div v-if="fileMetadata[file.name]" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
<!-- Dynamic Custom Fields -->
|
|
<div v-for="field in customFields" :key="field.name"
|
|
:class="field.type === 'text' && field.name === 'Description' ? 'md:col-span-2' : ''">
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
{{ field.name }}
|
|
<span v-if="field.required" class="text-red-500">*</span>
|
|
</label>
|
|
|
|
<!-- Dropdown/Select Field -->
|
|
<select v-if="field.type === 'dropdown' || field.type === 'select'"
|
|
v-model="fileMetadata[file.name][field.name]"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700">
|
|
<option value="">Select {{ field.name }}</option>
|
|
<option v-for="option in field.options" :key="option" :value="option">
|
|
{{ option }}
|
|
</option>
|
|
</select>
|
|
|
|
<!-- Date Field -->
|
|
<input v-else-if="field.type === 'date'"
|
|
v-model="fileMetadata[file.name][field.name]"
|
|
type="date"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700">
|
|
|
|
<!-- Number Field -->
|
|
<input v-else-if="field.type === 'number'"
|
|
v-model="fileMetadata[file.name][field.name]"
|
|
type="number"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700">
|
|
|
|
<!-- Text Field -->
|
|
<input v-else
|
|
v-model="fileMetadata[file.name][field.name]"
|
|
type="text"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700"
|
|
:placeholder="`Enter ${field.name.toLowerCase()}`">
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="md:col-span-2">
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
|
<textarea v-model="fileMetadata[file.name].description"
|
|
rows="2"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700"
|
|
placeholder="Brief description of the document"></textarea>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div class="md:col-span-2">
|
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Tags</label>
|
|
<div class="space-y-2">
|
|
<!-- Tag Input -->
|
|
<div class="relative">
|
|
<input type="text"
|
|
@input="handleTagInput(file.name, $event.target.value)"
|
|
@keyup.enter="addTag(file.name, $event.target.value); $event.target.value = ''"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:ring-1 focus:ring-blue-500 dark:bg-gray-700"
|
|
placeholder="Type and press Enter to add tags">
|
|
|
|
<!-- Tag Suggestions -->
|
|
<div v-if="tagSuggestions.length > 0"
|
|
class="absolute z-10 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg max-h-40 overflow-y-auto">
|
|
<button v-for="suggestion in tagSuggestions" :key="suggestion"
|
|
@click="addTag(file.name, suggestion)"
|
|
class="w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 focus:bg-gray-100 dark:focus:bg-gray-700">
|
|
{{ suggestion }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selected Tags -->
|
|
<div v-if="fileMetadata[file.name].tags && fileMetadata[file.name].tags.length > 0"
|
|
class="flex flex-wrap gap-2">
|
|
<span v-for="(tag, tagIndex) in fileMetadata[file.name].tags" :key="tagIndex"
|
|
class="inline-flex items-center px-2 py-1 text-xs bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">
|
|
{{ tag }}
|
|
<button @click="removeTag(file.name, tagIndex)"
|
|
class="ml-1 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
</button>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Progress -->
|
|
<div v-if="isUploading" class="space-y-2">
|
|
<div class="flex justify-between text-sm text-gray-600 dark:text-gray-400">
|
|
<span>Uploading...</span>
|
|
<span>{{ uploadProgress }}%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
:style="{ width: `${uploadProgress}%` }"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-between items-center">
|
|
<div class="text-sm text-gray-500">
|
|
Storage: {{ Math.round(dmsStore.userQuotas.used / (1024 * 1024)) }}MB /
|
|
{{ Math.round(dmsStore.userQuotas.limit / (1024 * 1024)) }}MB used
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<rs-button @click="closeDialog" variant="secondary" :disabled="isUploading">
|
|
Cancel
|
|
</rs-button>
|
|
<rs-button @click="uploadFiles"
|
|
:disabled="selectedFiles.length === 0 || isUploading || Object.keys(validationErrors).length > 0">
|
|
<svg v-if="isUploading" 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>
|
|
{{ isUploading ? 'Uploading...' : `Upload ${selectedFiles.length} File${selectedFiles.length !== 1 ? 's' : ''}` }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-modal>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Enhanced scrollbar styling */
|
|
.overflow-y-auto::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.overflow-y-auto::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.overflow-y-auto::-webkit-scrollbar-thumb {
|
|
background: #d1d5db;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
|
background: #9ca3af;
|
|
}
|
|
</style> |