generated from corrad-software/corrad-af-2024
398 lines
16 KiB
Vue
398 lines
16 KiB
Vue
<script setup>
|
|
import { ref, computed, watch } from 'vue';
|
|
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
currentPath: {
|
|
type: String,
|
|
default: '/'
|
|
},
|
|
parentType: {
|
|
type: String,
|
|
default: 'root' // root, cabinet, drawer, folder
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['close', 'create', 'update:visible']);
|
|
|
|
// Local state
|
|
const createType = ref('cabinet');
|
|
const itemName = ref('');
|
|
const itemDescription = ref('');
|
|
const itemCategory = ref('');
|
|
const itemDepartment = ref('');
|
|
const isPrivate = ref(false);
|
|
const selectedTags = ref([]);
|
|
const customTag = ref('');
|
|
const isCreating = ref(false);
|
|
|
|
// Available types based on parent
|
|
const availableTypes = computed(() => {
|
|
switch (props.parentType) {
|
|
case 'root':
|
|
return [{ value: 'cabinet', label: 'Cabinet', icon: 'cabinet' }];
|
|
case 'cabinet':
|
|
return [{ value: 'drawer', label: 'Drawer', icon: 'drawer' }];
|
|
case 'drawer':
|
|
return [{ value: 'folder', label: 'Folder', icon: 'folder' }];
|
|
case 'folder':
|
|
return [{ value: 'subfolder', label: 'Subfolder', icon: 'subfolder' }];
|
|
default:
|
|
return [];
|
|
}
|
|
});
|
|
|
|
// Global tags
|
|
const globalTags = ref([
|
|
'Important', 'Urgent', 'Confidential', 'Public', 'Archive',
|
|
'Financial', 'Legal', 'Technical', 'Administrative', 'Project'
|
|
]);
|
|
|
|
// Validation
|
|
const isValid = computed(() => {
|
|
return itemName.value.trim().length > 0;
|
|
});
|
|
|
|
const nameError = computed(() => {
|
|
if (!itemName.value.trim()) return 'Name is required';
|
|
if (itemName.value.length > 100) return 'Name must be less than 100 characters';
|
|
if (!/^[a-zA-Z0-9\s\-_()]+$/.test(itemName.value)) return 'Name contains invalid characters';
|
|
return null;
|
|
});
|
|
|
|
// Methods
|
|
const closeDialog = () => {
|
|
resetForm();
|
|
emit('update:visible', false);
|
|
emit('close');
|
|
};
|
|
|
|
const resetForm = () => {
|
|
itemName.value = '';
|
|
itemDescription.value = '';
|
|
itemCategory.value = '';
|
|
itemDepartment.value = '';
|
|
isPrivate.value = false;
|
|
selectedTags.value = [];
|
|
customTag.value = '';
|
|
createType.value = availableTypes.value[0]?.value || 'cabinet';
|
|
};
|
|
|
|
const addTag = (tag) => {
|
|
if (!selectedTags.value.includes(tag)) {
|
|
selectedTags.value.push(tag);
|
|
}
|
|
};
|
|
|
|
const removeTag = (tag) => {
|
|
const index = selectedTags.value.indexOf(tag);
|
|
if (index !== -1) {
|
|
selectedTags.value.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
const addCustomTag = () => {
|
|
const tag = customTag.value.trim();
|
|
if (tag && !selectedTags.value.includes(tag)) {
|
|
selectedTags.value.push(tag);
|
|
customTag.value = '';
|
|
}
|
|
};
|
|
|
|
const createItem = async () => {
|
|
if (!isValid.value || isCreating.value) return;
|
|
|
|
isCreating.value = true;
|
|
|
|
const itemData = {
|
|
type: createType.value,
|
|
name: itemName.value.trim(),
|
|
description: itemDescription.value.trim(),
|
|
category: itemCategory.value,
|
|
department: itemDepartment.value,
|
|
isPrivate: isPrivate.value,
|
|
tags: selectedTags.value,
|
|
parentPath: props.currentPath,
|
|
parentType: props.parentType
|
|
};
|
|
|
|
try {
|
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
|
emit('create', itemData);
|
|
closeDialog();
|
|
} catch (error) {
|
|
console.error('Failed to create item:', error);
|
|
} finally {
|
|
isCreating.value = false;
|
|
}
|
|
};
|
|
|
|
// Set initial type when available types change
|
|
watch(() => availableTypes.value, (newTypes) => {
|
|
if (newTypes.length > 0) {
|
|
createType.value = newTypes[0].value;
|
|
}
|
|
}, { immediate: true });
|
|
|
|
const getSvgIcon = (iconType, size = 24) => {
|
|
const icons = {
|
|
cabinet: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>`,
|
|
drawer: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/></svg>`,
|
|
folder: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
|
|
subfolder: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><path d="M8 13h8"></path></svg>`,
|
|
close: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
|
|
tag: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>`,
|
|
plus: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
|
|
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
|
|
unlock: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>`
|
|
};
|
|
return icons[iconType] || icons.folder;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="visible" class="create-dialog fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center">
|
|
<div class="dialog-container bg-white dark:bg-gray-900 rounded-lg shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col">
|
|
<!-- Header -->
|
|
<div class="dialog-header border-b border-gray-200 dark:border-gray-700 p-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-xl font-semibold">Create New Item</h2>
|
|
<button @click="closeDialog" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
|
|
<span v-html="getSvgIcon('close', 20)"></span>
|
|
</button>
|
|
</div>
|
|
<p class="text-sm text-gray-500 mt-1">Create in: {{ currentPath }}</p>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="dialog-content flex-1 p-6 overflow-y-auto">
|
|
<form @submit.prevent="createItem" class="space-y-6">
|
|
<!-- Item Type Selection -->
|
|
<div v-if="availableTypes.length > 1">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Type</label>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<button
|
|
v-for="type in availableTypes"
|
|
:key="type.value"
|
|
type="button"
|
|
@click="createType = type.value"
|
|
class="flex items-center space-x-3 p-3 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
:class="createType === type.value ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : ''"
|
|
>
|
|
<span v-html="getSvgIcon(type.icon, 24)" class="text-blue-600"></span>
|
|
<span class="font-medium">{{ type.label }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="availableTypes.length === 1" class="flex items-center space-x-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
<span v-html="getSvgIcon(availableTypes[0].icon, 24)" class="text-blue-600"></span>
|
|
<div>
|
|
<h3 class="font-medium">{{ availableTypes[0].label }}</h3>
|
|
<p class="text-sm text-gray-500">Creating a new {{ availableTypes[0].label.toLowerCase() }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Name Field -->
|
|
<div>
|
|
<label for="item-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Name <span class="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
id="item-name"
|
|
v-model="itemName"
|
|
type="text"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
:class="nameError ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''"
|
|
:placeholder="`Enter ${createType} name`"
|
|
required
|
|
/>
|
|
<p v-if="nameError" class="mt-1 text-sm text-red-600">{{ nameError }}</p>
|
|
</div>
|
|
|
|
<!-- Description Field -->
|
|
<div>
|
|
<label for="item-description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
id="item-description"
|
|
v-model="itemDescription"
|
|
rows="3"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
:placeholder="`Brief description of this ${createType}`"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Metadata Fields -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="item-category" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Category
|
|
</label>
|
|
<select
|
|
id="item-category"
|
|
v-model="itemCategory"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
>
|
|
<option value="">Select category</option>
|
|
<option value="administrative">Administrative</option>
|
|
<option value="financial">Financial</option>
|
|
<option value="legal">Legal</option>
|
|
<option value="technical">Technical</option>
|
|
<option value="project">Project</option>
|
|
<option value="operational">Operational</option>
|
|
<option value="other">Other</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="item-department" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Department
|
|
</label>
|
|
<input
|
|
id="item-department"
|
|
v-model="itemDepartment"
|
|
type="text"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
placeholder="Responsible department"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Privacy Setting -->
|
|
<div>
|
|
<label class="flex items-center space-x-3">
|
|
<input
|
|
v-model="isPrivate"
|
|
type="checkbox"
|
|
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span class="flex items-center space-x-2">
|
|
<span v-html="getSvgIcon(isPrivate ? 'lock' : 'unlock', 16)"></span>
|
|
<span class="text-sm font-medium">Private {{ createType }}</span>
|
|
</span>
|
|
</label>
|
|
<p class="ml-6 text-xs text-gray-500 mt-1">
|
|
{{ isPrivate ? 'Only authorized users can access this item' : 'This item will be publicly accessible' }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Tags</label>
|
|
|
|
<!-- Predefined Tags -->
|
|
<div class="flex flex-wrap gap-2 mb-3">
|
|
<button
|
|
v-for="tag in globalTags"
|
|
:key="tag"
|
|
type="button"
|
|
@click="addTag(tag)"
|
|
class="inline-flex items-center px-2 py-1 rounded-full text-xs border"
|
|
:class="selectedTags.includes(tag)
|
|
? 'bg-blue-100 text-blue-800 border-blue-200'
|
|
: 'bg-gray-100 text-gray-800 border-gray-200 hover:bg-gray-200'"
|
|
>
|
|
<span v-html="getSvgIcon('tag', 12)" class="mr-1"></span>
|
|
{{ tag }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Selected Tags -->
|
|
<div v-if="selectedTags.length" class="flex flex-wrap gap-2 mb-3">
|
|
<span
|
|
v-for="tag in selectedTags"
|
|
:key="tag"
|
|
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800"
|
|
>
|
|
{{ tag }}
|
|
<button type="button" @click="removeTag(tag)" class="ml-1 hover:text-blue-600">
|
|
<span v-html="getSvgIcon('close', 12)"></span>
|
|
</button>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Custom Tag Input -->
|
|
<div class="flex gap-2">
|
|
<input
|
|
v-model="customTag"
|
|
@keyup.enter="addCustomTag"
|
|
type="text"
|
|
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700"
|
|
placeholder="Add custom tag and press Enter"
|
|
/>
|
|
<button
|
|
type="button"
|
|
@click="addCustomTag"
|
|
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600"
|
|
>
|
|
<span v-html="getSvgIcon('plus', 16)"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="dialog-footer border-t border-gray-200 dark:border-gray-700 p-4">
|
|
<div class="flex justify-end space-x-3">
|
|
<rs-button variant="secondary" @click="closeDialog">Cancel</rs-button>
|
|
<rs-button
|
|
@click="createItem"
|
|
:disabled="!isValid || isCreating"
|
|
class="inline-flex items-center"
|
|
>
|
|
<span v-if="isCreating" class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></span>
|
|
<span v-html="getSvgIcon('plus', 16)" class="mr-2" v-else></span>
|
|
{{ isCreating ? 'Creating...' : `Create ${createType}` }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.create-dialog {
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.dialog-container {
|
|
max-height: 90vh;
|
|
}
|
|
|
|
.dialog-content {
|
|
min-height: 200px;
|
|
}
|
|
|
|
/* Custom scrollbar */
|
|
.dialog-content::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.dialog-content::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.dialog-content::-webkit-scrollbar-thumb {
|
|
background-color: rgba(156, 163, 175, 0.5);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.dialog-content::-webkit-scrollbar-thumb:hover {
|
|
background-color: rgba(156, 163, 175, 0.7);
|
|
}
|
|
|
|
/* Form validation styles */
|
|
input:invalid:not(:focus):not(:placeholder-shown) {
|
|
border-color: #ef4444;
|
|
}
|
|
|
|
input:valid:not(:focus):not(:placeholder-shown) {
|
|
border-color: #10b981;
|
|
}
|
|
</style> |