EDMS/components/dms/dialogs/CreateNewDialog.vue
2025-06-05 14:57:08 +08:00

716 lines
26 KiB
Vue

<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { useNotifications } from '~/composables/useNotifications';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
initialPath: {
type: String,
default: '/'
}
});
const emit = defineEmits(['update:visible', 'create-complete', 'cancel']);
// Notifications
const { success, error } = useNotifications();
// Creation state
const creationType = ref('folder'); // 'folder', 'subfolder', 'document'
const itemName = ref('');
const selectedPath = ref(props.initialPath);
const currentFolder = ref(null);
const showAdvancedOptions = ref(false);
// Navigation state
const expandedFolders = ref(new Set(['/']));
const breadcrumbs = ref([]);
const folderContents = ref([]);
const isLoading = ref(false);
// Advanced options
const accessPermissions = ref({
level: 'internal',
inheritFromParent: true
});
const templateOptions = ref([
{ value: 'blank', label: 'Blank Document', icon: 'mdi:file-document' },
{ value: 'template1', label: 'Report Template', icon: 'mdi:file-chart' },
{ value: 'template2', label: 'Memo Template', icon: 'mdi:file-document-edit' },
{ value: 'template3', label: 'Project Plan', icon: 'mdi:file-table' }
]);
const selectedTemplate = ref('blank');
// Creation types
const creationTypes = [
{
value: 'folder',
label: 'New Folder',
icon: 'mdi:folder-plus',
description: 'Create a new folder to organize documents'
},
{
value: 'subfolder',
label: 'New Subfolder',
icon: 'mdi:folder-multiple-plus',
description: 'Create a subfolder within the selected folder'
},
{
value: 'document',
label: 'New Document',
icon: 'mdi:file-plus',
description: 'Create a new document from template'
}
];
// Mock folder structure (in real implementation, this would come from an API)
const mockFolderStructure = ref([
{
id: '1',
name: 'JKR Documents',
path: '/jkr-documents',
type: 'folder',
hasChildren: true,
children: [
{
id: '1-1',
name: 'Kota Bharu Branch',
path: '/jkr-documents/kota-bharu',
type: 'folder',
hasChildren: true,
children: [
{ id: '1-1-1', name: 'Reports', path: '/jkr-documents/kota-bharu/reports', type: 'folder', hasChildren: false },
{ id: '1-1-2', name: 'Projects', path: '/jkr-documents/kota-bharu/projects', type: 'folder', hasChildren: false }
]
},
{
id: '1-2',
name: 'Headquarters',
path: '/jkr-documents/headquarters',
type: 'folder',
hasChildren: true,
children: [
{ id: '1-2-1', name: 'Admin', path: '/jkr-documents/headquarters/admin', type: 'folder', hasChildren: false },
{ id: '1-2-2', name: 'Finance', path: '/jkr-documents/headquarters/finance', type: 'folder', hasChildren: false }
]
}
]
},
{
id: '2',
name: 'Public Documents',
path: '/public-documents',
type: 'folder',
hasChildren: true,
children: [
{ id: '2-1', name: 'Announcements', path: '/public-documents/announcements', type: 'folder', hasChildren: false },
{ id: '2-2', name: 'Forms', path: '/public-documents/forms', type: 'folder', hasChildren: false },
{ id: '2-3', name: 'Policies', path: '/public-documents/policies', type: 'folder', hasChildren: false }
]
},
{
id: '3',
name: 'Archive',
path: '/archive',
type: 'folder',
hasChildren: true,
children: [
{ id: '3-1', name: '2023', path: '/archive/2023', type: 'folder', hasChildren: false },
{ id: '3-2', name: '2022', path: '/archive/2022', type: 'folder', hasChildren: false }
]
}
]);
// Computed
const canCreate = computed(() => {
return itemName.value.trim().length > 0 && selectedPath.value;
});
const currentPathName = computed(() => {
if (selectedPath.value === '/') return 'Root';
return selectedPath.value.split('/').pop();
});
// Navigation methods
const buildBreadcrumbs = (path) => {
if (path === '/') {
return [{ name: 'Root', path: '/' }];
}
const segments = path.split('/').filter(Boolean);
const crumbs = [{ name: 'Root', path: '/' }];
let currentPath = '';
segments.forEach(segment => {
currentPath += '/' + segment;
crumbs.push({
name: segment,
path: currentPath
});
});
return crumbs;
};
const navigateToPath = (path) => {
selectedPath.value = path;
breadcrumbs.value = buildBreadcrumbs(path);
loadFolderContents(path);
};
const loadFolderContents = async (path) => {
isLoading.value = true;
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 300));
// Mock folder contents
const contents = [
{
id: 'c1',
name: 'Annual Report 2024.pdf',
type: 'file',
size: '2.4 MB',
lastModified: '2024-01-15T10:30:00Z',
icon: 'mdi:file-pdf'
},
{
id: 'c2',
name: 'Budget Analysis',
type: 'folder',
itemCount: 12,
lastModified: '2024-01-14T15:45:00Z',
icon: 'mdi:folder'
},
{
id: 'c3',
name: 'Meeting Minutes.docx',
type: 'file',
size: '156 KB',
lastModified: '2024-01-13T09:15:00Z',
icon: 'mdi:file-word'
}
];
folderContents.value = contents;
} catch (err) {
error('Failed to load folder contents');
} finally {
isLoading.value = false;
}
};
// Tree navigation
const toggleFolderExpansion = (folder) => {
if (expandedFolders.value.has(folder.path)) {
expandedFolders.value.delete(folder.path);
} else {
expandedFolders.value.add(folder.path);
}
};
const selectFolder = (folder) => {
navigateToPath(folder.path);
currentFolder.value = folder;
};
// Creation process
const performCreation = async () => {
try {
const newItem = {
name: itemName.value.trim(),
type: creationType.value,
path: selectedPath.value,
permissions: accessPermissions.value,
template: creationType.value === 'document' ? selectedTemplate.value : null
};
// Here you would implement actual creation logic
console.log('Creating:', newItem);
success(`Successfully created ${creationType.value}: ${newItem.name}`);
emit('create-complete', newItem);
closeModal();
} catch (err) {
error('Failed to create item: ' + err.message);
}
};
// Modal controls
const closeModal = () => {
itemName.value = '';
creationType.value = 'folder';
selectedPath.value = props.initialPath;
showAdvancedOptions.value = false;
selectedTemplate.value = 'blank';
emit('update:visible', false);
};
const handleCancel = () => {
closeModal();
emit('cancel');
};
// Format file size
const formatFileSize = (size) => {
if (typeof size === 'string') return size;
// ... existing formatFileSize logic
return size;
};
// Format date
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString();
};
// Initialize
watch(() => props.visible, (visible) => {
if (visible) {
navigateToPath(props.initialPath);
}
});
onMounted(() => {
if (props.visible) {
navigateToPath(props.initialPath);
}
});
</script>
<template>
<Teleport to="body">
<div
v-if="visible"
class="fixed inset-0 z-50 overflow-y-auto"
@click.self="handleCancel"
>
<!-- Backdrop -->
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"></div>
<!-- Modal -->
<div class="relative min-h-screen flex items-center justify-center p-4">
<div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center space-x-3">
<Icon name="mdi:plus-circle" class="w-6 h-6 text-green-600 dark:text-green-400" />
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
Create New Item
</h2>
</div>
<button
@click="handleCancel"
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="flex h-[70vh]">
<!-- Left Panel: Navigation Tree -->
<div class="w-1/3 border-r border-gray-200 dark:border-gray-700 flex flex-col">
<!-- Tree Header -->
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100">
Choose Location
</h3>
</div>
<!-- Tree Navigation -->
<div class="flex-1 overflow-y-auto p-2">
<div class="space-y-1">
<!-- Root -->
<button
@click="selectFolder({ name: 'Root', path: '/' })"
:class="[
'w-full flex items-center px-3 py-2 text-sm rounded-md transition-colors',
selectedPath === '/'
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
]"
>
<Icon name="mdi:folder-home" class="w-4 h-4 mr-2 text-blue-600 dark:text-blue-400" />
<span class="font-medium">Root</span>
</button>
<!-- Folder Tree -->
<div
v-for="folder in mockFolderStructure"
:key="folder.id"
class="ml-2"
>
<div class="flex items-center">
<!-- Expand/Collapse Button -->
<button
v-if="folder.hasChildren"
@click="toggleFolderExpansion(folder)"
class="flex-shrink-0 w-4 h-4 mr-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
>
<Icon
:name="expandedFolders.has(folder.path) ? 'mdi:chevron-down' : 'mdi:chevron-right'"
class="w-4 h-4"
/>
</button>
<div v-else class="w-5"></div>
<!-- Folder Button -->
<button
@click="selectFolder(folder)"
:class="[
'flex-1 flex items-center px-2 py-1 text-sm rounded-md transition-colors text-left',
selectedPath === folder.path
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
]"
>
<Icon name="mdi:folder" class="w-4 h-4 mr-2 text-blue-600 dark:text-blue-400" />
<span class="truncate">{{ folder.name }}</span>
</button>
</div>
<!-- Nested Children -->
<div
v-if="folder.children && expandedFolders.has(folder.path)"
class="ml-4 mt-1 space-y-1"
>
<div
v-for="child in folder.children"
:key="child.id"
class="flex items-center"
>
<button
v-if="child.hasChildren"
@click="toggleFolderExpansion(child)"
class="flex-shrink-0 w-4 h-4 mr-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
>
<Icon
:name="expandedFolders.has(child.path) ? 'mdi:chevron-down' : 'mdi:chevron-right'"
class="w-4 h-4"
/>
</button>
<div v-else class="w-5"></div>
<button
@click="selectFolder(child)"
:class="[
'flex-1 flex items-center px-2 py-1 text-sm rounded-md transition-colors text-left',
selectedPath === child.path
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
]"
>
<Icon name="mdi:folder" class="w-4 h-4 mr-2 text-blue-600 dark:text-blue-400" />
<span class="truncate">{{ child.name }}</span>
</button>
</div>
<!-- Nested Children's Children -->
<div
v-if="child.children && expandedFolders.has(child.path)"
class="ml-4 mt-1 space-y-1"
>
<button
v-for="grandchild in child.children"
:key="grandchild.id"
@click="selectFolder(grandchild)"
:class="[
'w-full flex items-center px-2 py-1 text-sm rounded-md transition-colors text-left',
selectedPath === grandchild.path
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
]"
>
<div class="w-5"></div>
<Icon name="mdi:folder" class="w-4 h-4 mr-2 text-blue-600 dark:text-blue-400" />
<span class="truncate">{{ grandchild.name }}</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Panel: Folder Contents & Creation Form -->
<div class="flex-1 flex flex-col">
<!-- Breadcrumb Navigation -->
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<nav class="flex items-center space-x-1 text-sm">
<button
v-for="(crumb, index) in breadcrumbs"
:key="index"
@click="navigateToPath(crumb.path)"
:class="[
'px-2 py-1 rounded-md transition-colors',
index === breadcrumbs.length - 1
? 'text-gray-900 dark:text-gray-100 font-medium'
: 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300'
]"
>
{{ crumb.name }}
</button>
<Icon
v-if="index < breadcrumbs.length - 1"
name="mdi:chevron-right"
class="w-4 h-4 text-gray-400"
/>
</nav>
</div>
<!-- Creation Form -->
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<!-- Creation Type Selection -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
What would you like to create?
</label>
<div class="grid grid-cols-3 gap-3">
<button
v-for="type in creationTypes"
:key="type.value"
@click="creationType = type.value"
:class="[
'p-4 border-2 rounded-lg transition-all text-left',
creationType === type.value
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/10'
: 'border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500'
]"
>
<Icon
:name="type.icon"
:class="[
'w-8 h-8 mb-2',
creationType === type.value
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-400'
]"
/>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ type.label }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ type.description }}
</div>
</button>
</div>
</div>
<!-- Name Input -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ creationType === 'folder' ? 'Folder' : creationType === 'subfolder' ? 'Subfolder' : 'Document' }} Name
</label>
<input
v-model="itemName"
type="text"
:placeholder="`Enter ${creationType} name...`"
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
focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
@keyup.enter="canCreate && performCreation()"
/>
</div>
<!-- Template Selection (for documents) -->
<div v-if="creationType === 'document'" class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Document Template
</label>
<div class="grid grid-cols-2 gap-3">
<button
v-for="template in templateOptions"
:key="template.value"
@click="selectedTemplate = template.value"
:class="[
'p-3 border rounded-lg transition-all text-left flex items-center space-x-3',
selectedTemplate === template.value
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/10'
: 'border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500'
]"
>
<Icon
:name="template.icon"
:class="[
'w-6 h-6',
selectedTemplate === template.value
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-400'
]"
/>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ template.label }}
</span>
</button>
</div>
</div>
<!-- Advanced Options -->
<div class="mb-4">
<button
@click="showAdvancedOptions = !showAdvancedOptions"
class="flex items-center text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
<Icon
:name="showAdvancedOptions ? 'mdi:chevron-up' : 'mdi:chevron-down'"
class="w-4 h-4 mr-1"
/>
Advanced Options
</button>
<div v-if="showAdvancedOptions" class="mt-3 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg">
<div class="mb-4">
<label class="flex items-center">
<input
v-model="accessPermissions.inheritFromParent"
type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded"
/>
<span class="ml-2 text-sm text-gray-900 dark:text-gray-100">
Inherit permissions from parent folder
</span>
</label>
</div>
<div v-if="!accessPermissions.inheritFromParent">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Access Level
</label>
<select
v-model="accessPermissions.level"
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 value="public">Public - Accessible to everyone</option>
<option value="internal">Internal - Company employees only</option>
<option value="confidential">Confidential - Restricted access</option>
<option value="secret">Secret - Highly restricted</option>
</select>
</div>
</div>
</div>
<!-- Selected Location Display -->
<div class="p-3 bg-blue-50 dark:bg-blue-900/10 rounded-lg">
<div class="flex items-center text-sm">
<Icon name="mdi:map-marker" class="w-4 h-4 text-blue-600 dark:text-blue-400 mr-2" />
<span class="text-gray-700 dark:text-gray-300">
Creating in: <span class="font-medium text-blue-600 dark:text-blue-400">{{ currentPathName }}</span>
</span>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ selectedPath }}
</div>
</div>
</div>
<!-- Folder Contents Preview -->
<div class="flex-1 overflow-y-auto p-4">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
Current folder contents:
</h4>
<div v-if="isLoading" class="flex items-center justify-center py-8">
<Icon name="mdi:loading" class="w-6 h-6 animate-spin text-gray-400" />
<span class="ml-2 text-gray-500 dark:text-gray-400">Loading...</span>
</div>
<div v-else-if="folderContents.length === 0" class="text-center py-8">
<Icon name="mdi:folder-open" class="w-12 h-12 text-gray-300 mx-auto mb-2" />
<p class="text-gray-500 dark:text-gray-400">This folder is empty</p>
</div>
<div v-else class="space-y-2">
<div
v-for="item in folderContents"
:key="item.id"
class="flex items-center p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
>
<Icon
:name="item.icon"
:class="[
'w-5 h-5 mr-3',
item.type === 'folder' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-500 dark:text-gray-400'
]"
/>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{{ item.name }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ item.type === 'folder' ? `${item.itemCount} items` : item.size }} •
{{ formatDate(item.lastModified) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-500 dark:text-gray-400">
Location: {{ selectedPath }}
</div>
<div class="flex items-center space-x-3">
<RsButton
variant="secondary-outline"
@click="handleCancel"
>
Cancel
</RsButton>
<RsButton
variant="primary"
@click="performCreation"
:disabled="!canCreate"
>
<Icon :name="creationTypes.find(t => t.value === creationType)?.icon" class="w-4 h-4 mr-2" />
Create {{ creationType === 'folder' ? 'Folder' : creationType === 'subfolder' ? 'Subfolder' : 'Document' }}
</RsButton>
</div>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
/* Windows Explorer styling */
.tree-node {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
/* Smooth transitions */
.transition-all {
transition: all 0.2s ease-in-out;
}
/* Custom scrollbar for tree */
.overflow-y-auto::-webkit-scrollbar {
width: 6px;
}
.overflow-y-auto::-webkit-scrollbar-track {
background: transparent;
}
.overflow-y-auto::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>