generated from corrad-software/corrad-af-2024
543 lines
20 KiB
Vue
543 lines
20 KiB
Vue
<script setup>
|
|
import { ref, computed, watch } from 'vue';
|
|
import { useDmsStore } from '~/stores/dms';
|
|
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['close', 'search']);
|
|
|
|
// Store
|
|
const dmsStore = useDmsStore();
|
|
|
|
// Search state
|
|
const searchQuery = ref('');
|
|
const isLoading = ref(false);
|
|
const searchResults = ref([]);
|
|
const hasSearched = ref(false);
|
|
|
|
// Advanced filters
|
|
const filters = ref({
|
|
fileType: [],
|
|
department: [],
|
|
tags: [],
|
|
priority: [],
|
|
author: [],
|
|
dateRange: { start: null, end: null },
|
|
hasVersions: false,
|
|
accessType: []
|
|
});
|
|
|
|
// Filter options
|
|
const fileTypeOptions = computed(() => {
|
|
const types = new Set();
|
|
dmsStore.items.filter(item => item.type === 'file').forEach(item => {
|
|
if (item.extension) types.add(item.extension);
|
|
});
|
|
return Array.from(types).map(type => ({ value: type, label: type.toUpperCase() }));
|
|
});
|
|
|
|
const departmentOptions = computed(() => {
|
|
const departments = new Set();
|
|
dmsStore.items.forEach(item => {
|
|
if (item.department) departments.add(item.department);
|
|
});
|
|
return Array.from(departments).map(dept => ({ value: dept, label: dept }));
|
|
});
|
|
|
|
const priorityOptions = computed(() => [
|
|
{ value: 'Low', label: 'Low' },
|
|
{ value: 'Medium', label: 'Medium' },
|
|
{ value: 'High', label: 'High' },
|
|
{ value: 'Critical', label: 'Critical' }
|
|
]);
|
|
|
|
const authorOptions = computed(() => {
|
|
const authors = new Set();
|
|
dmsStore.items.forEach(item => {
|
|
if (item.author) authors.add(item.author);
|
|
});
|
|
return Array.from(authors).map(author => ({ value: author, label: author }));
|
|
});
|
|
|
|
const accessTypeOptions = computed(() => [
|
|
{ value: 'public', label: 'Public' },
|
|
{ value: 'private', label: 'Private' },
|
|
{ value: 'personal', label: 'Personal' }
|
|
]);
|
|
|
|
const availableTags = computed(() => {
|
|
const tags = new Set();
|
|
dmsStore.items.forEach(item => {
|
|
if (item.tags) {
|
|
item.tags.forEach(tag => tags.add(tag));
|
|
}
|
|
});
|
|
return Array.from(tags);
|
|
});
|
|
|
|
// Methods
|
|
const performSearch = async () => {
|
|
isLoading.value = true;
|
|
hasSearched.value = true;
|
|
|
|
try {
|
|
const results = await dmsStore.advancedSearch(searchQuery.value, filters.value);
|
|
searchResults.value = results;
|
|
emit('search', { query: searchQuery.value, filters: filters.value, results });
|
|
} catch (error) {
|
|
console.error('Search failed:', error);
|
|
searchResults.value = [];
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const clearSearch = () => {
|
|
searchQuery.value = '';
|
|
filters.value = {
|
|
fileType: [],
|
|
department: [],
|
|
tags: [],
|
|
priority: [],
|
|
author: [],
|
|
dateRange: { start: null, end: null },
|
|
hasVersions: false,
|
|
accessType: []
|
|
};
|
|
searchResults.value = [];
|
|
hasSearched.value = false;
|
|
dmsStore.clearSearch();
|
|
};
|
|
|
|
const closeDialog = () => {
|
|
emit('close');
|
|
};
|
|
|
|
const selectDocument = (document) => {
|
|
dmsStore.selectItem(document);
|
|
closeDialog();
|
|
};
|
|
|
|
const formatFileSize = (size) => {
|
|
return dmsStore.formatFileSize(size);
|
|
};
|
|
|
|
const getFileIcon = (extension) => {
|
|
const iconMap = {
|
|
pdf: 'text-red-500',
|
|
doc: 'text-blue-500',
|
|
docx: 'text-blue-500',
|
|
xls: 'text-green-500',
|
|
xlsx: 'text-green-500',
|
|
ppt: 'text-orange-500',
|
|
pptx: 'text-orange-500',
|
|
jpg: 'text-purple-500',
|
|
jpeg: 'text-purple-500',
|
|
png: 'text-purple-500'
|
|
};
|
|
return iconMap[extension] || 'text-gray-500';
|
|
};
|
|
|
|
// Tag input handling
|
|
const tagInput = ref('');
|
|
const addTag = () => {
|
|
const tag = tagInput.value.trim();
|
|
if (tag && !filters.value.tags.includes(tag)) {
|
|
filters.value.tags.push(tag);
|
|
tagInput.value = '';
|
|
}
|
|
};
|
|
|
|
const removeTag = (index) => {
|
|
filters.value.tags.splice(index, 1);
|
|
};
|
|
|
|
// Quick search presets
|
|
const applyQuickFilter = (type) => {
|
|
clearSearch();
|
|
|
|
switch (type) {
|
|
case 'recent':
|
|
// Documents modified in last 7 days
|
|
const lastWeek = new Date();
|
|
lastWeek.setDate(lastWeek.getDate() - 7);
|
|
filters.value.dateRange.start = lastWeek.toISOString().split('T')[0];
|
|
break;
|
|
case 'confidential':
|
|
filters.value.tags = ['confidential'];
|
|
break;
|
|
case 'contracts':
|
|
filters.value.tags = ['contract'];
|
|
filters.value.department = ['Legal'];
|
|
break;
|
|
case 'versioned':
|
|
filters.value.hasVersions = true;
|
|
break;
|
|
}
|
|
|
|
performSearch();
|
|
};
|
|
|
|
// Auto-search when Enter is pressed
|
|
const handleKeyup = (event) => {
|
|
if (event.key === 'Enter') {
|
|
performSearch();
|
|
}
|
|
};
|
|
|
|
// File size conversion helper
|
|
const fileSizeToNumber = (sizeStr) => {
|
|
if (typeof sizeStr === 'number') return sizeStr;
|
|
const match = sizeStr.match(/^([\d.]+)\s*(B|KB|MB|GB)$/i);
|
|
if (!match) return 0;
|
|
|
|
const value = parseFloat(match[1]);
|
|
const unit = match[2].toUpperCase();
|
|
|
|
switch (unit) {
|
|
case 'GB': return value * 1024 * 1024 * 1024;
|
|
case 'MB': return value * 1024 * 1024;
|
|
case 'KB': return value * 1024;
|
|
default: return value;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<rs-modal :visible="visible" @close="closeDialog" size="5xl">
|
|
<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">
|
|
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
|
|
<path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2"/>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Advanced Search</h3>
|
|
</div>
|
|
</template>
|
|
|
|
<template #body>
|
|
<div class="space-y-6">
|
|
<!-- Search Input -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Search Query
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
v-model="searchQuery"
|
|
@keyup="handleKeyup"
|
|
type="text"
|
|
class="w-full pl-10 pr-4 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"
|
|
placeholder="Search documents, descriptions, authors..."
|
|
/>
|
|
<svg class="absolute left-3 top-2.5 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Filters -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Quick Filters
|
|
</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button @click="applyQuickFilter('recent')"
|
|
class="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full hover:bg-blue-200 dark:hover:bg-blue-900/50">
|
|
Recent (7 days)
|
|
</button>
|
|
<button @click="applyQuickFilter('confidential')"
|
|
class="px-3 py-1 text-sm bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 rounded-full hover:bg-red-200 dark:hover:bg-red-900/50">
|
|
Confidential
|
|
</button>
|
|
<button @click="applyQuickFilter('contracts')"
|
|
class="px-3 py-1 text-sm bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full hover:bg-green-200 dark:hover:bg-green-900/50">
|
|
Contracts
|
|
</button>
|
|
<button @click="applyQuickFilter('versioned')"
|
|
class="px-3 py-1 text-sm bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300 rounded-full hover:bg-purple-200 dark:hover:bg-purple-900/50">
|
|
Versioned Documents
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Filters -->
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 space-y-4">
|
|
<h4 class="text-md font-semibold text-gray-900 dark:text-gray-100">Advanced Filters</h4>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<!-- File Type Filter -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
File Type
|
|
</label>
|
|
<select v-model="filters.fileType" multiple
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
|
<option v-for="option in fileTypeOptions" :key="option.value" :value="option.value">
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Department Filter -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Department
|
|
</label>
|
|
<select v-model="filters.department" multiple
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
|
<option v-for="option in departmentOptions" :key="option.value" :value="option.value">
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Priority Filter -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Priority
|
|
</label>
|
|
<select v-model="filters.priority" multiple
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
|
<option v-for="option in priorityOptions" :key="option.value" :value="option.value">
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Author Filter -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Author
|
|
</label>
|
|
<select v-model="filters.author" multiple
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
|
<option v-for="option in authorOptions" :key="option.value" :value="option.value">
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Access Type Filter -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Access Type
|
|
</label>
|
|
<select v-model="filters.accessType" multiple
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
|
<option v-for="option in accessTypeOptions" :key="option.value" :value="option.value">
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Has Versions Filter -->
|
|
<div class="flex items-center">
|
|
<label class="flex items-center space-x-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
<input type="checkbox" v-model="filters.hasVersions"
|
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
<span>Has Multiple Versions</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Date Range Filter -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
From Date
|
|
</label>
|
|
<input v-model="filters.dateRange.start" type="date"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
To Date
|
|
</label>
|
|
<input v-model="filters.dateRange.end" type="date"
|
|
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags Filter -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Tags
|
|
</label>
|
|
<div class="space-y-2">
|
|
<!-- Tag Input -->
|
|
<div class="flex space-x-2">
|
|
<input v-model="tagInput"
|
|
@keyup.enter="addTag"
|
|
type="text"
|
|
class="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
|
|
placeholder="Type tag and press Enter">
|
|
<button @click="addTag"
|
|
class="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700">
|
|
Add
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Selected Tags -->
|
|
<div v-if="filters.tags.length > 0" class="flex flex-wrap gap-2">
|
|
<span v-for="(tag, index) in filters.tags" :key="index"
|
|
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(index)"
|
|
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>
|
|
|
|
<!-- Available Tags -->
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
Available tags:
|
|
<button v-for="tag in availableTags.slice(0, 10)" :key="tag"
|
|
@click="filters.tags.includes(tag) ? null : filters.tags.push(tag)"
|
|
class="mx-1 px-1 hover:text-blue-600 dark:hover:text-blue-400"
|
|
:class="filters.tags.includes(tag) ? 'text-blue-600 dark:text-blue-400' : ''">
|
|
{{ tag }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Results -->
|
|
<div v-if="hasSearched" class="space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h4 class="text-md font-semibold text-gray-900 dark:text-gray-100">
|
|
Search Results ({{ searchResults.length }})
|
|
</h4>
|
|
<button @click="clearSearch"
|
|
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
|
Clear Results
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
|
<div class="flex items-center space-x-2">
|
|
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
|
<span class="text-gray-500">Searching...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results List -->
|
|
<div v-else-if="searchResults.length > 0" class="max-h-96 overflow-y-auto space-y-2">
|
|
<div v-for="document in searchResults" :key="document.id"
|
|
@click="selectDocument(document)"
|
|
class="p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer">
|
|
<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"
|
|
:class="getFileIcon(document.extension)">
|
|
<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 class="flex-1 min-w-0">
|
|
<h5 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
{{ document.name }}
|
|
</h5>
|
|
<div class="flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
|
|
<span>{{ document.size }}</span>
|
|
<span v-if="document.department">{{ document.department }}</span>
|
|
<span v-if="document.author">{{ document.author }}</span>
|
|
<span>{{ document.modified }}</span>
|
|
<span v-if="document.version > 1" class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 px-1 rounded">
|
|
v{{ document.version }}
|
|
</span>
|
|
</div>
|
|
<div v-if="document.description" class="text-xs text-gray-600 dark:text-gray-400 mt-1 truncate">
|
|
{{ document.description }}
|
|
</div>
|
|
<div v-if="document.tags && document.tags.length > 0" class="flex flex-wrap gap-1 mt-1">
|
|
<span v-for="tag in document.tags.slice(0, 3)" :key="tag"
|
|
class="inline-block px-1 py-0.5 text-xs bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded">
|
|
{{ tag }}
|
|
</span>
|
|
<span v-if="document.tags.length > 3" class="text-xs text-gray-500">
|
|
+{{ document.tags.length - 3 }} more
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No Results -->
|
|
<div v-else class="text-center py-8">
|
|
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">No documents found</h3>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Try adjusting your search criteria</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-between items-center">
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
{{ dmsStore.items.filter(item => item.type === 'file').length }} total documents
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<rs-button @click="clearSearch" variant="secondary">
|
|
Clear
|
|
</rs-button>
|
|
<rs-button @click="performSearch" :disabled="isLoading">
|
|
<svg v-if="isLoading" 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>
|
|
{{ isLoading ? 'Searching...' : 'Search' }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-modal>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Multi-select styling */
|
|
select[multiple] {
|
|
height: auto;
|
|
min-height: 2.5rem;
|
|
}
|
|
|
|
select[multiple] option {
|
|
padding: 0.25rem 0.5rem;
|
|
}
|
|
|
|
select[multiple] option:checked {
|
|
background: linear-gradient(#3b82f6, #3b82f6);
|
|
color: white;
|
|
}
|
|
|
|
/* 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> |