Files
TaskMate/frontend/js/knowledge.js
hendrik_gebhardt@gmx.de 7d67557be4 Kontakt-Modul
2026-01-06 21:49:26 +00:00

1266 Zeilen
42 KiB
JavaScript

/**
* 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 => `
<div class="knowledge-category-item ${cat.id === this.selectedCategory?.id ? 'active' : ''}"
data-category-id="${cat.id}"
draggable="true"
style="--category-color: ${cat.color || '#3B82F6'}">
<span class="knowledge-category-icon">${cat.icon || '📁'}</span>
<div class="knowledge-category-info">
<span class="knowledge-category-name">${this.escapeHtml(cat.name)}</span>
<span class="knowledge-category-count">${cat.entryCount || 0} Einträge</span>
</div>
<div class="knowledge-category-actions">
<button class="btn-icon" data-action="edit" title="Bearbeiten">
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" fill="none"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
<button class="btn-icon btn-danger-hover" data-action="delete" title="Löschen">
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
</div>
<span class="knowledge-drag-handle" title="Ziehen zum Sortieren">⋮⋮</span>
</div>
`).join('');
this.bindCategoryClickEvents();
}
renderMobileCategories() {
if (!this.mobileCategories) return;
const chips = this.categories.map(cat => `
<div class="knowledge-mobile-chip ${cat.id === this.selectedCategory?.id ? 'active' : ''}"
data-category-id="${cat.id}">
<span class="chip-icon">${cat.icon || '📁'}</span>
<span class="chip-name">${this.escapeHtml(cat.name)}</span>
<span class="chip-count">(${cat.entryCount || 0})</span>
</div>
`).join('');
const addChip = `
<div class="knowledge-mobile-chip knowledge-mobile-add" data-action="add-category">
<span>+ Neu</span>
</div>
`;
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 `
<div class="knowledge-entry-item ${isExpanded ? 'expanded' : ''}"
data-entry-id="${entry.id}"
draggable="true">
<div class="knowledge-entry-header">
<span class="knowledge-entry-expand">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<span class="knowledge-entry-title">${this.escapeHtml(entry.title)}</span>
<div class="knowledge-entry-indicators">
${hasUrl ? '<span class="knowledge-entry-indicator" title="Hat Link">🔗</span>' : ''}
${hasNotes ? '<span class="knowledge-entry-indicator" title="Hat Notizen">📝</span>' : ''}
${hasAttachments ? `<span class="knowledge-entry-indicator" title="${entry.attachmentCount} Anhänge">📎${entry.attachmentCount}</span>` : ''}
</div>
${showCategory && entry.categoryName ? `
<span class="knowledge-entry-category-badge" style="--category-color: ${entry.categoryColor || '#3B82F6'}; background-color: ${entry.categoryColor || '#3B82F6'}">${this.escapeHtml(entry.categoryName)}</span>
` : ''}
<span class="knowledge-entry-drag-handle" title="Ziehen zum Sortieren">⋮⋮</span>
<div class="knowledge-entry-actions">
<button class="btn-icon" data-action="edit" title="Bearbeiten">
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" fill="none"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
<button class="btn-icon btn-danger-hover" data-action="delete" title="Löschen">
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
</div>
</div>
<div class="knowledge-entry-details">
${hasUrl ? `
<a href="${this.escapeHtml(entry.url)}" target="_blank" rel="noopener noreferrer" class="knowledge-entry-url">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2" fill="none"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" fill="none"/></svg>
${this.escapeHtml(entry.url)}
</a>
` : ''}
${hasNotes ? `
<div class="knowledge-entry-notes">${this.escapeHtml(entry.notes)}</div>
` : ''}
${hasAttachments ? `
<div class="knowledge-entry-attachments-info">
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" stroke="currentColor" stroke-width="2" fill="none"/></svg>
${entry.attachmentCount} Anhang${entry.attachmentCount > 1 ? 'e' : ''}
</div>
` : ''}
</div>
</div>
`;
}
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 = '<p class="text-muted">Keine Anhänge vorhanden</p>';
return;
}
this.attachmentsContainer.innerHTML = attachments.map(att => `
<div class="knowledge-attachment-item" data-attachment-id="${att.id}">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="2" fill="none"/><path d="M14 2v6h6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
<div class="knowledge-attachment-info">
<span class="knowledge-attachment-name">${this.escapeHtml(att.original_name)}</span>
<span class="knowledge-attachment-size">${this.formatFileSize(att.size_bytes)}</span>
</div>
<div class="knowledge-attachment-actions">
<a href="${api.getKnowledgeAttachmentDownloadUrl(att.id)}" class="btn-icon" title="Herunterladen" download>
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</a>
<button class="btn-icon btn-danger-hover" data-action="delete-attachment" title="Löschen">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
</div>
</div>
`).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 = `<span class="category-icon">${icon}</span> ${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;