/** * TASKMATE - Knowledge Manager * ============================ * Wiki-Layout: Baum-Navigation + Content-Bereich */ import api from './api.js'; import { $, $$ } from './utils.js'; import store from './store.js'; class KnowledgeManager { constructor() { this.categories = []; this.allEntries = []; this.selectedEntryId = null; this.selectedCategoryId = null; this.expandedCategories = new Set(); this.isEditing = false; this.editingEntryId = null; this.filterQuery = ''; this.initialized = false; this.searchDebounceTimer = null; } async init() { console.log('[Knowledge] init() called, initialized:', this.initialized); if (this.initialized) { await this.loadData(); return; } // DOM Elements - Layout this.knowledgeView = $('#view-knowledge'); this.mobileCategories = $('#knowledge-mobile-categories'); // DOM Elements - Sidebar this.sidebarSearch = $('#knowledge-sidebar-search'); this.newCategoryBtn = $('#btn-new-category'); this.tree = $('#knowledge-tree'); this.treeEmpty = $('#knowledge-tree-empty'); // DOM Elements - Content this.emptyState = $('#knowledge-empty'); this.reader = $('#knowledge-reader'); this.editor = $('#knowledge-editor'); // Reader Elements this.readerTitle = $('#knowledge-reader-title'); this.readerMeta = $('#knowledge-reader-meta'); this.readerUrl = $('#knowledge-reader-url'); this.readerBody = $('#knowledge-reader-body'); this.readerAttachments = $('#knowledge-reader-attachments'); this.editEntryBtn = $('#btn-edit-entry'); this.deleteEntryReaderBtn = $('#btn-delete-entry-reader'); // Editor Elements this.editorForm = $('#knowledge-editor-form'); this.editorId = $('#knowledge-editor-id'); this.editorCategoryId = $('#knowledge-editor-category-id'); this.editorTitle = $('#knowledge-editor-title'); this.editorTitleInput = $('#knowledge-editor-title-input'); this.editorUrlInput = $('#knowledge-editor-url-input'); this.editorNotes = $('#knowledge-editor-notes'); this.editorAttachments = $('#knowledge-editor-attachments'); this.editorAttachmentsList = $('#knowledge-editor-attachments-list'); this.editorFileInput = $('#knowledge-editor-file-input'); this.btnUploadFile = $('#btn-upload-file'); this.btnCancelEditor = $('#btn-cancel-editor'); // 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'); this.bindEvents(); this.initialized = true; console.log('[Knowledge] Initialization complete'); await this.loadData(); } bindEvents() { // Sidebar search this.sidebarSearch?.addEventListener('input', (e) => { this.filterQuery = e.target.value.trim().toLowerCase(); this.renderTree(); }); // New Category Button this.newCategoryBtn?.addEventListener('click', () => { this.openCategoryModal(); }); // Category Form Submit this.categoryForm?.addEventListener('submit', (e) => this.handleCategorySubmit(e)); // Modal close buttons this.categoryModal?.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', () => this.closeCategoryModal()); }); // Icon Picker Events this.bindIconPickerEvents(); // Reader buttons this.editEntryBtn?.addEventListener('click', () => { if (this.selectedEntryId) { this.showEditor(this.selectedEntryId); } }); this.deleteEntryReaderBtn?.addEventListener('click', () => { if (this.selectedEntryId) { this.deleteEntry(this.selectedEntryId); } }); // Editor form this.editorForm?.addEventListener('submit', (e) => { e.preventDefault(); this.saveEntry(); }); this.btnCancelEditor?.addEventListener('click', () => { if (this.editingEntryId) { this.showEntry(this.editingEntryId); } else { this.showEmptyState(); } }); // File upload this.btnUploadFile?.addEventListener('click', () => { this.editorFileInput?.click(); }); this.editorFileInput?.addEventListener('change', (e) => { this.handleFileUpload(e); }); // Store subscription for realtime updates store.subscribe('knowledge', () => { this.loadData(); }); // Socket.io realtime events window.addEventListener('app:refresh', () => { if (this.knowledgeView && !this.knowledgeView.classList.contains('hidden')) { this.loadData(); } }); } // ========================================== // DATA LOADING // ========================================== async loadData() { try { const [categories, entriesResult] = await Promise.all([ api.getKnowledgeCategories(), api.getKnowledgeEntries() ]); this.categories = categories; this.categories.sort((a, b) => (a.position || 0) - (b.position || 0)); this.allEntries = Array.isArray(entriesResult) ? entriesResult : []; this.allEntries.sort((a, b) => (a.position || 0) - (b.position || 0)); this.renderTree(); this.renderMobileCategories(); // Refresh current view if an entry is selected if (this.selectedEntryId && !this.isEditing) { const entry = this.allEntries.find(e => e.id === this.selectedEntryId); if (entry) { this.showEntry(this.selectedEntryId); } else { this.selectedEntryId = null; this.showEmptyState(); } } } catch (error) { console.error('[Knowledge] Error loading data:', error); this.showToast('Fehler beim Laden der Wissensdatenbank', '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; } } // ========================================== // TREE RENDERING (Sidebar) // ========================================== getFilteredTree() { if (!this.filterQuery) { return this.categories.map(cat => ({ ...cat, entries: this.allEntries.filter(e => e.categoryId === cat.id) })); } const q = this.filterQuery; return this.categories .map(cat => { const matchingEntries = this.allEntries.filter(e => e.categoryId === cat.id && (e.title.toLowerCase().includes(q) || (e.notes && e.notes.toLowerCase().includes(q))) ); const catMatches = cat.name.toLowerCase().includes(q); if (catMatches || matchingEntries.length > 0) { return { ...cat, entries: catMatches ? this.allEntries.filter(e => e.categoryId === cat.id) : matchingEntries }; } return null; }) .filter(Boolean); } renderTree() { if (!this.tree) return; const treeData = this.getFilteredTree(); if (treeData.length === 0) { this.tree.innerHTML = ''; this.treeEmpty?.classList.remove('hidden'); return; } this.treeEmpty?.classList.add('hidden'); // If filtering, auto-expand all categories if (this.filterQuery) { treeData.forEach(cat => this.expandedCategories.add(cat.id)); } this.tree.innerHTML = treeData.map(cat => { const isExpanded = this.expandedCategories.has(cat.id); const entries = cat.entries || []; const icon = cat.icon || '\u{1F4C1}'; return `
`; }).join(''); this.bindTreeEvents(); } bindTreeEvents() { // Category headers - toggle expand this.tree.querySelectorAll('.knowledge-tree-category-header').forEach(header => { header.addEventListener('click', (e) => { if (e.target.closest('.knowledge-tree-category-actions')) return; const catId = parseInt(header.dataset.categoryId); this.toggleCategory(catId); }); }); // Category edit buttons this.tree.querySelectorAll('[data-action="edit-category"]').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); this.openCategoryModal(parseInt(btn.dataset.id)); }); }); // Category delete buttons this.tree.querySelectorAll('[data-action="delete-category"]').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); this.handleDeleteCategory(parseInt(btn.dataset.id)); }); }); // Entry items - show entry this.tree.querySelectorAll('.knowledge-tree-entry').forEach(item => { item.addEventListener('click', () => { const entryId = parseInt(item.dataset.entryId); this.showEntry(entryId); }); }); // New entry links this.tree.querySelectorAll('.knowledge-tree-new-entry').forEach(item => { item.addEventListener('click', () => { const categoryId = parseInt(item.dataset.categoryId); this.showEditor(null, categoryId); }); }); } toggleCategory(categoryId) { if (this.expandedCategories.has(categoryId)) { this.expandedCategories.delete(categoryId); } else { this.expandedCategories.add(categoryId); } this.renderTree(); } // ========================================== // MOBILE CATEGORIES // ========================================== renderMobileCategories() { if (!this.mobileCategories) return; const chips = this.categories.map(cat => `Kein Inhalt vorhanden.
'; } } // Attachments if (this.readerAttachments) { const attachments = entry.attachments || []; if (attachments.length > 0) { this.readerAttachments.classList.remove('hidden'); this.readerAttachments.innerHTML = `Keine Anhaenge vorhanden
'; return; } this.editorAttachments.classList.remove('hidden'); this.editorAttachmentsList.innerHTML = attachments.map(att => ` `).join(''); // Bind delete events this.editorAttachmentsList.querySelectorAll('[data-action="delete-attachment"]').forEach(btn => { btn.addEventListener('click', () => { this.handleDeleteAttachment(parseInt(btn.dataset.id)); }); }); } async handleFileUpload(e) { const files = e.target.files; if (!files || files.length === 0) return; const entryId = parseInt(this.editorId?.value); if (!entryId) { this.showToast('Speichern Sie zuerst den Eintrag, dann koennen Sie Dateien hochladen', 'info'); e.target.value = ''; 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'); } } e.target.value = ''; // Reload entry attachments const entry = await this.loadEntryWithAttachments(entryId); if (entry) { this.renderEditorAttachments(entry.attachments); } } async handleDeleteAttachment(attachmentId) { const confirmDelete = confirm('Anhang wirklich loeschen?'); if (!confirmDelete) return; try { await api.deleteKnowledgeAttachment(attachmentId); this.showToast('Anhang geloescht', 'success'); // Reload entry attachments const entryId = parseInt(this.editorId?.value); if (entryId) { const entry = await this.loadEntryWithAttachments(entryId); if (entry) { this.renderEditorAttachments(entry.attachments); } } } catch (error) { this.showToast(error.message || 'Fehler beim Loeschen', 'error'); } } // ========================================== // SEARCH (Header search integration) // ========================================== setSearchQuery(query) { clearTimeout(this.searchDebounceTimer); if (!query || !query.trim()) { this.filterQuery = ''; if (this.sidebarSearch) this.sidebarSearch.value = ''; this.renderTree(); return; } this.searchDebounceTimer = setTimeout(() => { this.filterQuery = query.trim().toLowerCase(); if (this.sidebarSearch) this.sidebarSearch.value = query.trim(); this.renderTree(); }, 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 || '\u{1F4C1}'; this.categoryIconInput.value = icon; if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = icon; } } } else { this.categoryIdInput.value = ''; this.categoryColorInput.value = '#3B82F6'; this.categoryIconInput.value = '\u{1F4C1}'; if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = '\u{1F4C1}'; } } 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.loadData(); } 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 count = category.entryCount || 0; const msg = count > 0 ? `Kategorie "${category.name}" mit ${count} Eintraegen loeschen? Dies kann nicht rueckgaengig gemacht werden.` : `Kategorie "${category.name}" loeschen? Dies kann nicht rueckgaengig gemacht werden.`; const confirmDelete = confirm(msg); if (!confirmDelete) return; try { await api.deleteKnowledgeCategory(categoryId); this.showToast('Kategorie geloescht', 'success'); // If any entry from this category was selected, clear if (this.selectedCategoryId === categoryId) { this.selectedCategoryId = null; this.selectedEntryId = null; this.showEmptyState(); } await this.loadData(); } catch (error) { this.showToast(error.message || 'Fehler beim Loeschen', 'error'); } } // ========================================== // ICON PICKER // ========================================== bindIconPickerEvents() { if (!this.iconPickerPreview || !this.iconPickerSection) return; this.iconPickerPreview.addEventListener('click', () => { this.iconPickerSection.classList.toggle('hidden'); }); const tabs = this.iconPickerSection.querySelectorAll('.icon-tab'); tabs.forEach(tab => { tab.addEventListener('click', () => { this.switchIconTab(tab.dataset.tab); }); }); const iconBtns = this.iconPickerSection.querySelectorAll('.icon-btn'); iconBtns.forEach(btn => { btn.addEventListener('click', () => { this.selectIcon(btn.dataset.icon); }); }); } switchIconTab(tabName) { if (!this.iconPickerSection) return; const tabs = this.iconPickerSection.querySelectorAll('.icon-tab'); tabs.forEach(tab => { tab.classList.toggle('active', tab.dataset.tab === tabName); }); const grids = this.iconPickerSection.querySelectorAll('.icon-grid'); grids.forEach(grid => { grid.classList.toggle('hidden', grid.dataset.tabContent !== tabName); }); } selectIcon(icon) { if (!icon) return; if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = icon; } if (this.categoryIconInput) { this.categoryIconInput.value = icon; } if (this.iconPickerSection) { this.iconPickerSection.classList.add('hidden'); } } // ========================================== // 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'); } 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); } // ========================================== // MARKDOWN RENDERING // ========================================== sanitizeHtml(html) { if (!html) return ''; let clean = html.replace(/<\s*(script|iframe|object|embed|form|style|link|meta|base)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, ''); clean = clean.replace(/<\s*(script|iframe|object|embed|form|style|link|meta|base)\b[^>]*\/?>/gi, ''); clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, ''); clean = clean.replace(/href\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'href="#"'); clean = clean.replace(/src\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'src=""'); return clean; } renderMarkdown(text) { if (!text) return ''; let html = text .replace(/&/g, '&') .replace(//g, '>'); // Code blocks html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, lang, code) => { return `${code.trim()}`;
});
// Inline code
html = html.replace(/`([^`\n]+)`/g, '$1');
// Headings
html = html.replace(/^### (.+)$/gm, ''); // Line breaks html = html.replace(/(?|<\/li>|<\/ul>|<\/ol>|<\/pre>|<\/p>|
)\n(?! )/g, ' ' + html + ' \s*<\/p>/g, '');
// Don't wrap block elements in
html = html.replace(/ \s*(<(?:h[123]|ul|ol|pre|blockquote)[\s>])/g, '$1');
html = html.replace(/(<\/(?:h[123]|ul|ol|pre|blockquote)>)\s*<\/p>/g, '$1');
return html;
}
// ==========================================
// 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;
|
|
|<\/p>|
');
// Wrap in paragraphs
html = '