/** * TASKMATE - Knowledge Manager * ============================ * Wissensmanagement mit Sidebar-Layout, Drag & Drop und kompakten Einträgen */ import api from './api.js'; import { $, $$ } from './utils.js'; import store from './store.js'; class KnowledgeManager { constructor() { this.categories = []; this.entries = []; this.selectedCategory = null; this.searchQuery = ''; this.searchResults = []; this.expandedEntries = new Set(); this.initialized = false; this.searchDebounceTimer = null; // Drag & Drop State this.draggedCategoryId = null; this.draggedEntryId = null; } async init() { console.log('[Knowledge] init() called, initialized:', this.initialized); if (this.initialized) { await this.loadCategories(); return; } // DOM Elements - Layout this.knowledgeView = $('#view-knowledge'); this.mobileCategories = $('#knowledge-mobile-categories'); // DOM Elements - Sidebar this.categoriesList = $('#knowledge-categories'); this.categoriesEmpty = $('#knowledge-categories-empty'); // DOM Elements - Main this.mainTitle = $('#knowledge-category-title'); this.entriesList = $('#knowledge-entries'); this.entriesEmpty = $('#knowledge-entries-empty'); this.noSelection = $('#knowledge-no-selection'); // DOM Elements - Search this.searchResultsSection = $('#knowledge-search-results'); this.searchResultsList = $('#knowledge-search-list'); this.searchResultsEmpty = $('#knowledge-search-empty'); this.searchQuerySpan = $('#knowledge-search-query'); // Buttons this.newCategoryBtn = $('#btn-new-category'); this.newEntryBtn = $('#btn-new-entry'); this.clearSearchBtn = $('#btn-clear-search'); // Category Modal Elements this.categoryModal = $('#knowledge-category-modal'); this.categoryForm = $('#knowledge-category-form'); this.categoryModalTitle = $('#knowledge-category-modal-title'); this.categoryIdInput = $('#knowledge-category-id'); this.categoryNameInput = $('#knowledge-category-name'); this.categoryDescriptionInput = $('#knowledge-category-description'); this.categoryColorInput = $('#knowledge-category-color'); this.categoryIconInput = $('#knowledge-category-icon'); // Icon Picker Elements this.iconPickerPreview = $('#icon-picker-preview'); this.iconPreviewEmoji = $('#icon-preview-emoji'); this.iconPickerSection = $('#icon-picker-section'); // Entry Modal Elements this.entryModal = $('#knowledge-entry-modal'); this.entryForm = $('#knowledge-entry-form'); this.entryModalTitle = $('#knowledge-entry-modal-title'); this.entryIdInput = $('#knowledge-entry-id'); this.entryCategoryIdInput = $('#knowledge-entry-category-id'); this.entryTitleInput = $('#knowledge-entry-title'); this.entryUrlInput = $('#knowledge-entry-url'); this.entryNotesInput = $('#knowledge-entry-notes'); this.attachmentsSection = $('#knowledge-attachments-section'); this.attachmentsContainer = $('#knowledge-attachments-container'); this.fileUploadArea = $('#knowledge-file-upload-area'); this.fileInput = $('#knowledge-file-input'); this.deleteEntryBtn = $('#btn-delete-entry'); this.bindEvents(); this.initialized = true; console.log('[Knowledge] Initialization complete'); await this.loadCategories(); } bindEvents() { console.log('[Knowledge] bindEvents() called'); // New Category Button this.newCategoryBtn?.addEventListener('click', () => { this.openCategoryModal(); }); // New Entry Button this.newEntryBtn?.addEventListener('click', () => { this.openEntryModal(); }); // Clear search this.clearSearchBtn?.addEventListener('click', () => { this.clearSearch(); }); // Category Form Submit this.categoryForm?.addEventListener('submit', (e) => this.handleCategorySubmit(e)); // Entry Form Submit this.entryForm?.addEventListener('submit', (e) => this.handleEntrySubmit(e)); // Delete Entry Button this.deleteEntryBtn?.addEventListener('click', () => { const entryId = parseInt(this.entryIdInput?.value); if (entryId) { this.handleDeleteEntry(entryId); } }); // Modal close buttons this.categoryModal?.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', () => this.closeCategoryModal()); }); this.entryModal?.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', () => this.closeEntryModal()); }); // Icon Picker Events this.bindIconPickerEvents(); // File Upload this.fileInput?.addEventListener('change', (e) => this.handleFileSelect(e)); // Drag & Drop for file upload if (this.fileUploadArea) { this.fileUploadArea.addEventListener('dragover', (e) => { e.preventDefault(); this.fileUploadArea.classList.add('drag-over'); }); this.fileUploadArea.addEventListener('dragleave', () => { this.fileUploadArea.classList.remove('drag-over'); }); this.fileUploadArea.addEventListener('drop', (e) => { e.preventDefault(); this.fileUploadArea.classList.remove('drag-over'); const files = e.dataTransfer.files; if (files.length > 0) { this.uploadFiles(files); } }); } // Drag & Drop for categories this.bindCategoryDragEvents(); // Drag & Drop for entries this.bindEntryDragEvents(); // Sidebar resize functionality this.bindResizeEvents(); } // ========================================== // DATA LOADING // ========================================== async loadCategories() { try { this.categories = await api.getKnowledgeCategories(); // Sort by position this.categories.sort((a, b) => (a.position || 0) - (b.position || 0)); this.renderCategories(); this.renderMobileCategories(); // Update UI state this.updateMainState(); } catch (error) { console.error('Error loading categories:', error); this.showToast('Fehler beim Laden der Kategorien', 'error'); } } async loadEntries(categoryId) { try { this.entries = await api.getKnowledgeEntries(categoryId); // Sort by position this.entries.sort((a, b) => (a.position || 0) - (b.position || 0)); this.renderEntries(); } catch (error) { console.error('Error loading entries:', error); this.showToast('Fehler beim Laden der Einträge', 'error'); } } async loadEntryWithAttachments(entryId) { try { return await api.getKnowledgeEntry(entryId); } catch (error) { console.error('Error loading entry:', error); this.showToast('Fehler beim Laden des Eintrags', 'error'); return null; } } // ========================================== // RENDERING - CATEGORIES (Sidebar) // ========================================== renderCategories() { if (!this.categoriesList) return; if (this.categories.length === 0) { this.categoriesList.innerHTML = ''; this.categoriesEmpty?.classList.remove('hidden'); return; } this.categoriesEmpty?.classList.add('hidden'); this.categoriesList.innerHTML = this.categories.map(cat => `
${cat.icon || '📁'}
${this.escapeHtml(cat.name)} ${cat.entryCount || 0} Einträge
⋮⋮
`).join(''); this.bindCategoryClickEvents(); } renderMobileCategories() { if (!this.mobileCategories) return; const chips = this.categories.map(cat => `
${cat.icon || '📁'} ${this.escapeHtml(cat.name)} (${cat.entryCount || 0})
`).join(''); const addChip = `
+ Neu
`; this.mobileCategories.innerHTML = chips + addChip; this.bindMobileChipEvents(); } // ========================================== // RENDERING - ENTRIES (Main) // ========================================== renderEntries() { if (!this.entriesList) return; // Hide no-selection message this.noSelection?.classList.add('hidden'); if (this.entries.length === 0) { this.entriesList.innerHTML = ''; this.entriesEmpty?.classList.remove('hidden'); return; } this.entriesEmpty?.classList.add('hidden'); this.entriesList.innerHTML = this.entries.map(entry => this.renderEntryItem(entry)).join(''); this.bindEntryClickEvents(); } renderEntryItem(entry, showCategory = false) { const isExpanded = this.expandedEntries.has(entry.id); const hasUrl = entry.url && entry.url.trim(); const hasNotes = entry.notes && entry.notes.trim(); const hasAttachments = entry.attachmentCount > 0; return `
${this.escapeHtml(entry.title)}
${hasUrl ? '🔗' : ''} ${hasNotes ? '📝' : ''} ${hasAttachments ? `📎${entry.attachmentCount}` : ''}
${showCategory && entry.categoryName ? ` ` : ''} ⋮⋮
${hasUrl ? ` ${this.escapeHtml(entry.url)} ` : ''} ${hasNotes ? `
${this.escapeHtml(entry.notes)}
` : ''} ${hasAttachments ? `
${entry.attachmentCount} Anhang${entry.attachmentCount > 1 ? 'e' : ''}
` : ''}
`; } renderSearchResults() { if (!this.searchResultsList) return; if (this.searchResults.length === 0) { this.searchResultsList.innerHTML = ''; this.searchResultsEmpty?.classList.remove('hidden'); return; } this.searchResultsEmpty?.classList.add('hidden'); this.searchResultsList.innerHTML = this.searchResults.map(entry => this.renderEntryItem(entry, true) ).join(''); this.bindSearchResultEvents(); } renderAttachments(attachments) { if (!this.attachmentsContainer) return; if (!attachments || attachments.length === 0) { this.attachmentsContainer.innerHTML = '

Keine Anhänge vorhanden

'; return; } this.attachmentsContainer.innerHTML = attachments.map(att => `
${this.escapeHtml(att.original_name)} ${this.formatFileSize(att.size_bytes)}
`).join(''); this.bindAttachmentEvents(); } updateMainState() { if (!this.selectedCategory) { this.mainTitle.textContent = 'Kategorie wählen'; this.newEntryBtn.disabled = true; this.entriesList.innerHTML = ''; this.entriesEmpty?.classList.add('hidden'); this.noSelection?.classList.remove('hidden'); } else { const icon = this.selectedCategory.icon || '📁'; this.mainTitle.innerHTML = `${icon} ${this.escapeHtml(this.selectedCategory.name)}`; this.newEntryBtn.disabled = false; this.noSelection?.classList.add('hidden'); } } // ========================================== // EVENT BINDING - CATEGORIES // ========================================== bindCategoryClickEvents() { this.categoriesList?.querySelectorAll('.knowledge-category-item').forEach(item => { const categoryId = parseInt(item.dataset.categoryId); // Click on category (not on actions) - select category item.addEventListener('click', (e) => { if (!e.target.closest('.knowledge-category-actions') && !e.target.closest('.knowledge-drag-handle')) { this.selectCategory(categoryId); } }); // Edit button const editBtn = item.querySelector('[data-action="edit"]'); editBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.openCategoryModal(categoryId); }); // Delete button const deleteBtn = item.querySelector('[data-action="delete"]'); deleteBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.handleDeleteCategory(categoryId); }); }); } bindMobileChipEvents() { this.mobileCategories?.querySelectorAll('.knowledge-mobile-chip').forEach(chip => { if (chip.dataset.action === 'add-category') { chip.addEventListener('click', () => this.openCategoryModal()); } else { const categoryId = parseInt(chip.dataset.categoryId); chip.addEventListener('click', () => this.selectCategory(categoryId)); } }); } bindCategoryDragEvents() { if (!this.categoriesList) return; this.categoriesList.addEventListener('dragstart', (e) => { const item = e.target.closest('.knowledge-category-item'); if (!item) return; this.draggedCategoryId = parseInt(item.dataset.categoryId); item.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', this.draggedCategoryId); }); this.categoriesList.addEventListener('dragend', (e) => { const item = e.target.closest('.knowledge-category-item'); if (item) { item.classList.remove('dragging'); } this.draggedCategoryId = null; this.categoriesList.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); }); this.categoriesList.addEventListener('dragover', (e) => { e.preventDefault(); const item = e.target.closest('.knowledge-category-item'); if (item && parseInt(item.dataset.categoryId) !== this.draggedCategoryId) { this.categoriesList.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); item.classList.add('drag-over'); } }); this.categoriesList.addEventListener('dragleave', (e) => { const item = e.target.closest('.knowledge-category-item'); if (item) { item.classList.remove('drag-over'); } }); this.categoriesList.addEventListener('drop', async (e) => { e.preventDefault(); const targetItem = e.target.closest('.knowledge-category-item'); if (!targetItem || !this.draggedCategoryId) return; const targetId = parseInt(targetItem.dataset.categoryId); if (targetId === this.draggedCategoryId) return; targetItem.classList.remove('drag-over'); // Calculate new position const targetIndex = this.categories.findIndex(c => c.id === targetId); const draggedIndex = this.categories.findIndex(c => c.id === this.draggedCategoryId); if (targetIndex === -1 || draggedIndex === -1) return; // Optimistic update const [moved] = this.categories.splice(draggedIndex, 1); this.categories.splice(targetIndex, 0, moved); // Update positions in array this.categories.forEach((cat, idx) => cat.position = idx); this.renderCategories(); this.renderMobileCategories(); // Server update try { await api.updateKnowledgeCategoryPosition(this.draggedCategoryId, targetIndex); } catch (error) { console.error('Error updating category position:', error); this.showToast('Fehler beim Speichern der Reihenfolge', 'error'); await this.loadCategories(); } }); } // ========================================== // EVENT BINDING - ENTRIES // ========================================== bindEntryClickEvents() { this.entriesList?.querySelectorAll('.knowledge-entry-item').forEach(item => { const entryId = parseInt(item.dataset.entryId); // Click on header to expand/collapse const header = item.querySelector('.knowledge-entry-header'); header?.addEventListener('click', (e) => { if (!e.target.closest('.knowledge-entry-actions') && !e.target.closest('.knowledge-entry-drag-handle')) { this.toggleEntry(entryId); } }); // Edit button const editBtn = item.querySelector('[data-action="edit"]'); editBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.openEntryModal(entryId); }); // Delete button const deleteBtn = item.querySelector('[data-action="delete"]'); deleteBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.handleDeleteEntry(entryId); }); }); } bindSearchResultEvents() { this.searchResultsList?.querySelectorAll('.knowledge-entry-item').forEach(item => { const entryId = parseInt(item.dataset.entryId); // Click on header to expand/collapse const header = item.querySelector('.knowledge-entry-header'); header?.addEventListener('click', (e) => { if (!e.target.closest('.knowledge-entry-actions') && !e.target.closest('.knowledge-entry-drag-handle')) { this.toggleEntry(entryId); // Re-render to reflect change this.renderSearchResults(); } }); // Edit button const editBtn = item.querySelector('[data-action="edit"]'); editBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.openEntryModal(entryId); }); // Delete button const deleteBtn = item.querySelector('[data-action="delete"]'); deleteBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.handleDeleteEntry(entryId); }); }); } bindEntryDragEvents() { if (!this.entriesList) return; this.entriesList.addEventListener('dragstart', (e) => { const item = e.target.closest('.knowledge-entry-item'); if (!item) return; this.draggedEntryId = parseInt(item.dataset.entryId); item.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', this.draggedEntryId); }); this.entriesList.addEventListener('dragend', (e) => { const item = e.target.closest('.knowledge-entry-item'); if (item) { item.classList.remove('dragging'); } this.draggedEntryId = null; this.entriesList.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); }); this.entriesList.addEventListener('dragover', (e) => { e.preventDefault(); const item = e.target.closest('.knowledge-entry-item'); if (item && parseInt(item.dataset.entryId) !== this.draggedEntryId) { this.entriesList.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); item.classList.add('drag-over'); } }); this.entriesList.addEventListener('dragleave', (e) => { const item = e.target.closest('.knowledge-entry-item'); if (item) { item.classList.remove('drag-over'); } }); this.entriesList.addEventListener('drop', async (e) => { e.preventDefault(); const targetItem = e.target.closest('.knowledge-entry-item'); if (!targetItem || !this.draggedEntryId) return; const targetId = parseInt(targetItem.dataset.entryId); if (targetId === this.draggedEntryId) return; targetItem.classList.remove('drag-over'); // Calculate new position const targetIndex = this.entries.findIndex(en => en.id === targetId); const draggedIndex = this.entries.findIndex(en => en.id === this.draggedEntryId); if (targetIndex === -1 || draggedIndex === -1) return; // Optimistic update const [moved] = this.entries.splice(draggedIndex, 1); this.entries.splice(targetIndex, 0, moved); // Update positions in array this.entries.forEach((entry, idx) => entry.position = idx); this.renderEntries(); // Server update try { await api.updateKnowledgeEntryPosition(this.draggedEntryId, targetIndex); } catch (error) { console.error('Error updating entry position:', error); this.showToast('Fehler beim Speichern der Reihenfolge', 'error'); if (this.selectedCategory) { await this.loadEntries(this.selectedCategory.id); } } }); } bindAttachmentEvents() { this.attachmentsContainer?.querySelectorAll('[data-action="delete-attachment"]').forEach(btn => { btn.addEventListener('click', () => { const item = btn.closest('.knowledge-attachment-item'); const attachmentId = parseInt(item?.dataset.attachmentId); if (attachmentId) { this.handleDeleteAttachment(attachmentId); } }); }); } // ========================================== // ICON PICKER // ========================================== bindIconPickerEvents() { if (!this.iconPickerPreview || !this.iconPickerSection) return; // Toggle icon picker visibility this.iconPickerPreview.addEventListener('click', () => { this.iconPickerSection.classList.toggle('hidden'); }); // Tab switching const tabs = this.iconPickerSection.querySelectorAll('.icon-tab'); tabs.forEach(tab => { tab.addEventListener('click', () => { const tabName = tab.dataset.tab; this.switchIconTab(tabName); }); }); // Icon selection const iconBtns = this.iconPickerSection.querySelectorAll('.icon-btn'); iconBtns.forEach(btn => { btn.addEventListener('click', () => { const icon = btn.dataset.icon; this.selectIcon(icon); }); }); } switchIconTab(tabName) { if (!this.iconPickerSection) return; // Update tab buttons const tabs = this.iconPickerSection.querySelectorAll('.icon-tab'); tabs.forEach(tab => { tab.classList.toggle('active', tab.dataset.tab === tabName); }); // Show/hide icon grids const grids = this.iconPickerSection.querySelectorAll('.icon-grid'); grids.forEach(grid => { grid.classList.toggle('hidden', grid.dataset.tabContent !== tabName); }); } selectIcon(icon) { if (!icon) return; // Update preview if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = icon; } // Update hidden input if (this.categoryIconInput) { this.categoryIconInput.value = icon; } // Hide the icon picker if (this.iconPickerSection) { this.iconPickerSection.classList.add('hidden'); } } // ========================================== // CATEGORY SELECTION // ========================================== async selectCategory(categoryId) { const category = this.categories.find(c => c.id === categoryId); if (!category) return; this.selectedCategory = category; // Clear search if active if (this.searchQuery) { this.searchQuery = ''; this.searchResults = []; this.searchResultsSection?.classList.add('hidden'); } // Update sidebar selection this.renderCategories(); this.renderMobileCategories(); // Update main header this.updateMainState(); // Load entries await this.loadEntries(categoryId); } toggleEntry(entryId) { if (this.expandedEntries.has(entryId)) { this.expandedEntries.delete(entryId); } else { this.expandedEntries.add(entryId); } // Update just the affected entry const entryItem = this.entriesList?.querySelector(`[data-entry-id="${entryId}"]`); if (entryItem) { entryItem.classList.toggle('expanded', this.expandedEntries.has(entryId)); } } // ========================================== // SEARCH // ========================================== async handleSearch(query) { this.searchQuery = query.trim(); if (!this.searchQuery) { this.clearSearch(); return; } try { const results = await api.searchKnowledge(this.searchQuery); this.searchResults = results.entries || []; this.searchQuerySpan.textContent = this.searchQuery; // Show search results, hide other content this.noSelection?.classList.add('hidden'); this.entriesEmpty?.classList.add('hidden'); this.entriesList.innerHTML = ''; this.searchResultsSection?.classList.remove('hidden'); this.renderSearchResults(); } catch (error) { console.error('Error searching:', error); this.showToast('Fehler bei der Suche', 'error'); } } clearSearch() { this.searchQuery = ''; this.searchResults = []; // Hide search results this.searchResultsSection?.classList.add('hidden'); // Show normal view if (this.selectedCategory) { this.loadEntries(this.selectedCategory.id); } else { this.updateMainState(); } } /** * Public method for global search from header */ setSearchQuery(query) { clearTimeout(this.searchDebounceTimer); if (!query || !query.trim()) { this.clearSearch(); return; } this.searchDebounceTimer = setTimeout(() => { this.handleSearch(query); }, 300); } // ========================================== // CATEGORY CRUD // ========================================== openCategoryModal(categoryId = null) { const isEdit = !!categoryId; this.categoryModalTitle.textContent = isEdit ? 'Kategorie bearbeiten' : 'Neue Kategorie'; this.categoryForm?.reset(); // Hide icon picker section this.iconPickerSection?.classList.add('hidden'); if (isEdit) { const category = this.categories.find(c => c.id === categoryId); if (category) { this.categoryIdInput.value = category.id; this.categoryNameInput.value = category.name; this.categoryDescriptionInput.value = category.description || ''; this.categoryColorInput.value = category.color || '#3B82F6'; const icon = category.icon || '📁'; this.categoryIconInput.value = icon; if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = icon; } } } else { this.categoryIdInput.value = ''; this.categoryColorInput.value = '#3B82F6'; this.categoryIconInput.value = '📁'; if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = '📁'; } } this.openModal(this.categoryModal, 'knowledge-category-modal'); this.categoryNameInput?.focus(); } closeCategoryModal() { this.closeModal(this.categoryModal, 'knowledge-category-modal'); } async handleCategorySubmit(e) { e.preventDefault(); const categoryId = this.categoryIdInput?.value ? parseInt(this.categoryIdInput.value) : null; const data = { name: this.categoryNameInput?.value.trim(), description: this.categoryDescriptionInput?.value.trim() || null, color: this.categoryColorInput?.value || '#3B82F6', icon: this.categoryIconInput?.value.trim() || null }; if (!data.name) { this.showToast('Bitte einen Namen eingeben', 'error'); return; } try { if (categoryId) { await api.updateKnowledgeCategory(categoryId, data); this.showToast('Kategorie aktualisiert', 'success'); } else { await api.createKnowledgeCategory(data); this.showToast('Kategorie erstellt', 'success'); } this.closeCategoryModal(); await this.loadCategories(); } catch (error) { this.showToast(error.message || 'Fehler beim Speichern', 'error'); } } async handleDeleteCategory(categoryId) { const category = this.categories.find(c => c.id === categoryId); if (!category) return; const confirmDelete = confirm(`Kategorie "${category.name}" und alle Einträge wirklich löschen?`); if (!confirmDelete) return; try { await api.deleteKnowledgeCategory(categoryId); this.showToast('Kategorie gelöscht', 'success'); // If deleted category was selected, clear selection if (this.selectedCategory?.id === categoryId) { this.selectedCategory = null; this.entries = []; this.updateMainState(); } await this.loadCategories(); } catch (error) { this.showToast(error.message || 'Fehler beim Löschen', 'error'); } } // ========================================== // ENTRY CRUD // ========================================== async openEntryModal(entryId = null) { const isEdit = !!entryId; this.entryModalTitle.textContent = isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag'; this.entryForm?.reset(); this.deleteEntryBtn?.classList.toggle('hidden', !isEdit); this.attachmentsSection.style.display = isEdit ? 'block' : 'none'; if (isEdit) { const entry = await this.loadEntryWithAttachments(entryId); if (entry) { this.entryIdInput.value = entry.id; this.entryCategoryIdInput.value = entry.categoryId; this.entryTitleInput.value = entry.title; this.entryUrlInput.value = entry.url || ''; this.entryNotesInput.value = entry.notes || ''; this.renderAttachments(entry.attachments); } } else { this.entryIdInput.value = ''; this.entryCategoryIdInput.value = this.selectedCategory?.id || ''; this.attachmentsContainer.innerHTML = ''; } this.openModal(this.entryModal, 'knowledge-entry-modal'); this.entryTitleInput?.focus(); } closeEntryModal() { this.closeModal(this.entryModal, 'knowledge-entry-modal'); } async handleEntrySubmit(e) { e.preventDefault(); const entryId = this.entryIdInput?.value ? parseInt(this.entryIdInput.value) : null; const categoryId = parseInt(this.entryCategoryIdInput?.value) || this.selectedCategory?.id; const data = { categoryId: categoryId, title: this.entryTitleInput?.value.trim(), url: this.entryUrlInput?.value.trim() || null, notes: this.entryNotesInput?.value.trim() || null }; if (!data.title) { this.showToast('Bitte einen Titel eingeben', 'error'); return; } if (!data.categoryId) { this.showToast('Keine Kategorie ausgewählt', 'error'); return; } try { if (entryId) { await api.updateKnowledgeEntry(entryId, data); this.showToast('Eintrag aktualisiert', 'success'); } else { await api.createKnowledgeEntry(data); this.showToast('Eintrag erstellt', 'success'); } this.closeEntryModal(); // Refresh view if (this.searchQuery) { await this.handleSearch(this.searchQuery); } else if (this.selectedCategory) { await this.loadEntries(this.selectedCategory.id); } await this.loadCategories(); // Update entry counts } catch (error) { this.showToast(error.message || 'Fehler beim Speichern', 'error'); } } async handleDeleteEntry(entryId) { const entry = this.entries.find(e => e.id === entryId) || this.searchResults.find(e => e.id === entryId); const title = entry?.title || 'Dieser Eintrag'; const confirmDelete = confirm(`${title} wirklich löschen?`); if (!confirmDelete) return; try { await api.deleteKnowledgeEntry(entryId); this.showToast('Eintrag gelöscht', 'success'); this.closeEntryModal(); // Remove from expanded set this.expandedEntries.delete(entryId); // Refresh view if (this.searchQuery) { await this.handleSearch(this.searchQuery); } else if (this.selectedCategory) { await this.loadEntries(this.selectedCategory.id); } await this.loadCategories(); // Update entry counts } catch (error) { this.showToast(error.message || 'Fehler beim Löschen', 'error'); } } // ========================================== // ATTACHMENTS // ========================================== handleFileSelect(e) { const files = e.target.files; if (files.length > 0) { this.uploadFiles(files); } e.target.value = ''; } async uploadFiles(files) { const entryId = parseInt(this.entryIdInput?.value); if (!entryId) { this.showToast('Bitte zuerst den Eintrag speichern', 'error'); return; } for (const file of files) { try { await api.uploadKnowledgeAttachment(entryId, file); this.showToast(`${file.name} hochgeladen`, 'success'); } catch (error) { this.showToast(`Fehler beim Hochladen von ${file.name}`, 'error'); } } // Reload entry to show new attachments const entry = await this.loadEntryWithAttachments(entryId); if (entry) { this.renderAttachments(entry.attachments); } } async handleDeleteAttachment(attachmentId) { const confirmDelete = confirm('Anhang wirklich löschen?'); if (!confirmDelete) return; try { await api.deleteKnowledgeAttachment(attachmentId); this.showToast('Anhang gelöscht', 'success'); // Reload entry to refresh attachments const entryId = parseInt(this.entryIdInput?.value); if (entryId) { const entry = await this.loadEntryWithAttachments(entryId); if (entry) { this.renderAttachments(entry.attachments); } } } catch (error) { this.showToast(error.message || 'Fehler beim Löschen', 'error'); } } // ========================================== // MODAL HELPERS // ========================================== openModal(modal, modalId) { if (modal) { modal.classList.remove('hidden'); modal.classList.add('visible'); } const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('hidden'); overlay.classList.add('visible'); } store.openModal(modalId); } closeModal(modal, modalId) { if (modal) { modal.classList.remove('visible'); modal.classList.add('hidden'); } // Only hide overlay if no other modals are open const openModals = store.get('openModals').filter(id => id !== modalId); if (openModals.length === 0) { const overlay = $('#modal-overlay'); if (overlay) { overlay.classList.remove('visible'); overlay.classList.add('hidden'); } } store.closeModal(modalId); } // ========================================== // SIDEBAR RESIZE // ========================================== bindResizeEvents() { // Use native DOM methods instead of $ utility this.resizeHandle = document.getElementById('knowledge-resize-handle'); this.knowledgeLayoutContainer = document.querySelector('.knowledge-layout'); if (!this.resizeHandle || !this.knowledgeLayoutContainer) return; // Load saved width from localStorage const savedWidth = localStorage.getItem('knowledge-sidebar-width'); if (savedWidth) { this.setSidebarWidth(parseInt(savedWidth)); } let isResizing = false; let startX = 0; let startWidth = 0; this.resizeHandle.addEventListener('mousedown', (e) => { isResizing = true; startX = e.clientX; startWidth = this.getCurrentSidebarWidth(); this.resizeHandle.classList.add('dragging'); this.knowledgeLayoutContainer.classList.add('resizing'); document.addEventListener('mousemove', this.handleResize); document.addEventListener('mouseup', this.handleResizeEnd); e.preventDefault(); }); this.handleResize = (e) => { if (!isResizing) return; const deltaX = e.clientX - startX; const newWidth = Math.max(220, Math.min(800, startWidth + deltaX)); this.setSidebarWidth(newWidth); }; this.handleResizeEnd = () => { if (!isResizing) return; isResizing = false; this.resizeHandle.classList.remove('dragging'); this.knowledgeLayoutContainer.classList.remove('resizing'); document.removeEventListener('mousemove', this.handleResize); document.removeEventListener('mouseup', this.handleResizeEnd); // Save current width to localStorage const currentWidth = this.getCurrentSidebarWidth(); localStorage.setItem('knowledge-sidebar-width', currentWidth.toString()); }; } getCurrentSidebarWidth() { const computedStyle = getComputedStyle(this.knowledgeLayoutContainer); const gridColumns = computedStyle.gridTemplateColumns; const match = gridColumns.match(/(\d+)px/); return match ? parseInt(match[1]) : 450; } setSidebarWidth(width) { if (this.knowledgeLayoutContainer) { this.knowledgeLayoutContainer.style.gridTemplateColumns = `${width}px 1fr`; } } // ========================================== // UTILITIES // ========================================== formatFileSize(bytes) { if (!bytes) return '0 B'; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; } escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } showToast(message, type = 'info') { window.dispatchEvent(new CustomEvent('toast:show', { detail: { message, type } })); } // ========================================== // SHOW/HIDE // ========================================== show() { this.knowledgeView?.classList.remove('hidden'); this.knowledgeView?.classList.add('active'); } hide() { this.knowledgeView?.classList.add('hidden'); this.knowledgeView?.classList.remove('active'); } } // Create singleton instance const knowledgeManager = new KnowledgeManager(); export { knowledgeManager }; export default knowledgeManager;