1190 Zeilen
40 KiB
JavaScript
1190 Zeilen
40 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();
|
|
}
|
|
|
|
// ==========================================
|
|
// 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);
|
|
}
|
|
|
|
// ==========================================
|
|
// 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;
|