diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9e3c040..2d24c22 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,25 @@ TASKMATE - CHANGELOG ==================== +================================================================================ +19.03.2026 - v393 - Wissensdatenbank: Wiki-Layout Redesign + +WISSENSDATENBANK KOMPLETT UMGEBAUT: +- Neues Wiki-Layout mit Baum-Navigation (links) und Content-Bereich (rechts) +- Sidebar: Aufklappbare Kategorien mit Eintraegen als Kinder, Suchfeld, Neue Kategorie Button +- Content: 3 Zustaende (Leer, Lese-Ansicht, Editor) +- Lese-Ansicht: Titel, Meta-Info, Markdown-Body, Anhaenge, Bearbeiten/Loeschen Buttons +- Editor: Inline-Bearbeitung mit Titel, URL, Markdown-Textarea, Datei-Upload +- Alle Eintraege auf einen Blick im Baum sichtbar +- Suche filtert den Baum in Echtzeit +- Alte Ansicht (Expand/Collapse Liste, Drag&Drop, Resize Handle) entfernt + +Geaenderte Dateien: +- frontend/index.html (view-knowledge HTML-Struktur) +- frontend/css/knowledge.css (komplett neu) +- frontend/js/knowledge.js (komplett umgebaut) +- frontend/sw.js (CACHE_VERSION 392 -> 393) + ================================================================================ 19.03.2026 - v392 - Wissensdatenbank: Markdown, Volltextsuche, Sanitizing diff --git a/frontend/css/knowledge.css b/frontend/css/knowledge.css index f672f7f..cdd70d4 100644 --- a/frontend/css/knowledge.css +++ b/frontend/css/knowledge.css @@ -1,992 +1,419 @@ /** - * TASKMATE - Knowledge Management Styles - * ====================================== - * Sidebar + Main Layout mit Drag & Drop + * TASKMATE - Knowledge Wiki Styles + * ================================= + * Wiki-Layout: Sidebar-Baum + Content-Bereich */ -/* ============================================ - VIEW CONTAINER - ============================================ */ - .view-knowledge { - padding: var(--spacing-md); - height: calc(100vh - 120px); + padding: 0; + height: calc(100vh - 60px); display: flex; flex-direction: column; } -/* ============================================ - MAIN LAYOUT (Sidebar + Main) - ============================================ */ - .knowledge-layout { display: grid; - grid-template-columns: 450px 1fr; - gap: var(--spacing-lg); + grid-template-columns: 300px 1fr; flex: 1; min-height: 0; overflow: hidden; - position: relative; } -/* ============================================ - SIDEBAR (Kategorien) - ============================================ */ - +/* Sidebar */ .knowledge-sidebar { background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); + border-right: 1px solid var(--border-color); display: flex; flex-direction: column; overflow: hidden; - position: relative; } .knowledge-sidebar-header { display: flex; - justify-content: space-between; align-items: center; - padding: var(--spacing-md); + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); border-bottom: 1px solid var(--border-color); flex-shrink: 0; } -.knowledge-sidebar-title { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - margin: 0; +.knowledge-sidebar-search { + flex: 1; + display: flex; + align-items: center; + gap: var(--spacing-xs); + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 6px var(--spacing-sm); } -.knowledge-category-list { +.knowledge-sidebar-search svg { flex-shrink: 0; color: var(--text-muted); } + +.knowledge-sidebar-search input { + flex: 1; border: none; background: transparent; + font-size: 0.85rem; color: var(--text-primary); outline: none; min-width: 0; +} + +.knowledge-sidebar-search input::placeholder { color: var(--text-muted); } + +.knowledge-sidebar-header .btn-sm { + flex-shrink: 0; width: 32px; height: 32px; padding: 0; + display: flex; align-items: center; justify-content: center; +} + +/* Baum-Navigation */ +.knowledge-tree { flex: 1; overflow-y: auto; padding: var(--spacing-xs) 0; } +.knowledge-tree::-webkit-scrollbar { width: 6px; } +.knowledge-tree::-webkit-scrollbar-track { background: transparent; } +.knowledge-tree::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; } + +.knowledge-tree-category { margin-bottom: 2px; } + +.knowledge-tree-category-header { + display: flex; align-items: center; gap: var(--spacing-xs); + padding: 6px var(--spacing-md); cursor: pointer; + user-select: none; transition: background 0.1s; position: relative; +} + +.knowledge-tree-category-header:hover { background: var(--bg-hover); } + +.knowledge-tree-chevron { + width: 16px; height: 16px; flex-shrink: 0; color: var(--text-muted); + transition: transform 0.15s ease; display: flex; align-items: center; justify-content: center; +} + +.knowledge-tree-category.expanded .knowledge-tree-chevron { transform: rotate(90deg); } + +.knowledge-tree-category-icon { font-size: 1rem; flex-shrink: 0; width: 20px; text-align: center; } + +.knowledge-tree-category-name { + flex: 1; font-size: 0.85rem; font-weight: 600; color: var(--text-primary); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; +} + +.knowledge-tree-category-count { + font-size: 0.7rem; color: var(--text-muted); background: var(--bg-tertiary); + padding: 1px 6px; border-radius: var(--radius-full); flex-shrink: 0; +} + +.knowledge-tree-category-actions { + display: flex; gap: 2px; opacity: 0; transition: opacity 0.15s; flex-shrink: 0; +} + +.knowledge-tree-category-header:hover .knowledge-tree-category-actions { opacity: 1; } + +.knowledge-tree-category-actions .btn-icon { + width: 22px; height: 22px; padding: 0; background: transparent; + border: none; border-radius: var(--radius-sm); cursor: pointer; color: var(--text-muted); + display: flex; align-items: center; justify-content: center; transition: all 0.15s; +} + +.knowledge-tree-category-actions .btn-icon:hover { background: var(--bg-secondary); color: var(--text-primary); } +.knowledge-tree-category-actions .btn-icon.btn-danger-hover:hover { background: var(--danger-bg); color: var(--danger); } + +.knowledge-tree-entries { display: none; padding-left: 20px; } +.knowledge-tree-category.expanded .knowledge-tree-entries { display: block; } + +.knowledge-tree-entry { + display: flex; align-items: center; gap: var(--spacing-xs); + padding: 5px var(--spacing-md) 5px var(--spacing-sm); + cursor: pointer; font-size: 0.83rem; color: var(--text-secondary); + transition: all 0.1s; border-left: 2px solid transparent; +} + +.knowledge-tree-entry:hover { background: var(--bg-hover); color: var(--text-primary); } + +.knowledge-tree-entry.active { + background: var(--primary-light); color: var(--text-primary); + border-left-color: var(--primary); font-weight: 500; +} + +.knowledge-tree-entry-icon { flex-shrink: 0; color: var(--text-muted); display: flex; align-items: center; } +.knowledge-tree-entry-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } + +.knowledge-tree-new-entry { + display: flex; align-items: center; gap: var(--spacing-xs); + padding: 5px var(--spacing-md) 5px var(--spacing-sm); + cursor: pointer; font-size: 0.8rem; color: var(--text-muted); transition: all 0.1s; +} + +.knowledge-tree-new-entry:hover { color: var(--primary); background: var(--bg-hover); } +.knowledge-tree-new-entry svg { flex-shrink: 0; } + .knowledge-sidebar-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--spacing-xl); - color: var(--text-muted); - text-align: center; + display: flex; flex-direction: column; align-items: center; justify-content: center; + padding: var(--spacing-xl); color: var(--text-muted); text-align: center; flex: 1; } -.knowledge-sidebar-empty svg { - opacity: 0.4; - margin-bottom: var(--spacing-sm); +.knowledge-sidebar-empty svg { opacity: 0.4; margin-bottom: var(--spacing-sm); } +.knowledge-sidebar-empty p { margin: 0; font-size: 0.85rem; } + +/* Content */ +.knowledge-content { + overflow-y: auto; display: flex; flex-direction: column; min-height: 0; } -.knowledge-sidebar-empty p { - margin: 0; - font-size: 0.9rem; +.knowledge-empty-state { + display: flex; flex-direction: column; align-items: center; justify-content: center; + flex: 1; padding: var(--spacing-xxl); text-align: center; color: var(--text-muted); } -/* ============================================ - KATEGORIE-ITEMS (Sidebar) - ============================================ */ +.knowledge-empty-state svg { opacity: 0.3; margin-bottom: var(--spacing-md); } +.knowledge-empty-state h3 { font-size: 1.2rem; font-weight: 600; color: var(--text-secondary); margin: 0 0 var(--spacing-xs) 0; } +.knowledge-empty-state p { font-size: 0.95rem; margin: 0; } -.knowledge-category-item { - display: flex; - align-items: center; - gap: var(--spacing-sm); +/* Reader */ +.knowledge-reader { + padding: var(--spacing-xl) var(--spacing-xxl); + max-width: 800px; width: 100%; margin: 0 auto; +} + +.knowledge-reader-header { + display: flex; justify-content: space-between; align-items: flex-start; + gap: var(--spacing-md); margin-bottom: var(--spacing-md); +} + +.knowledge-reader-header h1 { + font-size: 1.75rem; font-weight: 700; color: var(--text-primary); + margin: 0; line-height: 1.3; flex: 1; +} + +.knowledge-reader-actions { display: flex; gap: var(--spacing-sm); flex-shrink: 0; } + +.knowledge-reader-meta { + font-size: 0.85rem; color: var(--text-muted); margin-bottom: var(--spacing-lg); + display: flex; align-items: center; gap: var(--spacing-md); flex-wrap: wrap; +} + +.knowledge-reader-meta-item { display: flex; align-items: center; gap: 4px; } + +.knowledge-reader-meta-badge { + display: inline-flex; align-items: center; gap: 4px; + padding: 2px 10px; border-radius: var(--radius-full); + font-size: 0.75rem; font-weight: 500; color: white; +} + +.knowledge-reader-url { margin-bottom: var(--spacing-lg); } + +.knowledge-reader-url a { + display: inline-flex; align-items: center; gap: var(--spacing-xs); + color: var(--primary); text-decoration: none; font-size: 0.9rem; word-break: break-all; +} + +.knowledge-reader-url a:hover { text-decoration: underline; } +.knowledge-reader-url a svg { flex-shrink: 0; color: var(--text-muted); } + +.knowledge-reader-body { font-size: 0.95rem; color: var(--text-secondary); line-height: 1.7; } + +.knowledge-reader-body h1 { + font-size: 1.4rem; font-weight: 700; color: var(--text-primary); + margin: 1.5rem 0 0.5rem 0; padding-bottom: 0.3rem; border-bottom: 1px solid var(--border-color); +} + +.knowledge-reader-body h2 { font-size: 1.2rem; font-weight: 600; color: var(--text-primary); margin: 1.2rem 0 0.4rem 0; } +.knowledge-reader-body h3 { font-size: 1.05rem; font-weight: 600; color: var(--text-primary); margin: 1rem 0 0.3rem 0; } +.knowledge-reader-body p { margin: 0.5rem 0; } + +.knowledge-reader-body code { + background: var(--bg-tertiary); padding: 2px 6px; + border-radius: var(--radius-sm); font-family: 'Courier New', monospace; font-size: 0.85em; +} + +.knowledge-reader-body pre { + background: var(--bg-tertiary); padding: var(--spacing-md); + border-radius: var(--radius-md); overflow-x: auto; margin: 0.75rem 0; +} + +.knowledge-reader-body pre code { background: transparent; padding: 0; border-radius: 0; font-size: 0.85rem; line-height: 1.5; } +.knowledge-reader-body ul, .knowledge-reader-body ol { padding-left: 1.5rem; margin: 0.5rem 0; } +.knowledge-reader-body li { margin: 0.2rem 0; } +.knowledge-reader-body a { color: var(--primary); text-decoration: underline; } +.knowledge-reader-body a:hover { opacity: 0.8; } +.knowledge-reader-body strong { font-weight: 700; color: var(--text-primary); } +.knowledge-reader-body em { font-style: italic; } + +.knowledge-reader-attachments { + margin-top: var(--spacing-xl); padding-top: var(--spacing-lg); border-top: 1px solid var(--border-color); +} + +.knowledge-reader-attachments h3 { font-size: 0.95rem; font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; } +.knowledge-reader-attachment-list { display: flex; flex-direction: column; gap: var(--spacing-xs); } + +.knowledge-reader-attachment-item { + display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); - cursor: pointer; - border-left: 3px solid transparent; - transition: all 0.15s ease; - user-select: none; + background: var(--bg-secondary); border: 1px solid var(--border-color); + border-radius: var(--radius-md); text-decoration: none; color: var(--text-primary); transition: all 0.15s; } -.knowledge-category-item:hover { - background: var(--bg-hover); -} - -.knowledge-category-item.active { - border-left-color: var(--category-color, var(--primary)); - background: var(--bg-hover); -} - -.knowledge-category-item.dragging { - opacity: 0.5; - background: var(--primary-light); -} - -.knowledge-category-item.drag-over { - border-top: 2px solid var(--primary); -} - -.knowledge-category-icon { - font-size: 1.2rem; - flex-shrink: 0; - width: 28px; - text-align: center; -} - -.knowledge-category-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 2px; -} - -.knowledge-category-name { - font-size: 0.9rem; - font-weight: 500; - color: var(--text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.knowledge-category-count { - font-size: 0.75rem; - color: var(--text-muted); -} - -.knowledge-category-actions { - display: flex; - gap: 2px; - opacity: 0; - transition: opacity 0.15s; -} - -.knowledge-category-item:hover .knowledge-category-actions { - opacity: 1; -} - -.knowledge-category-actions .btn-icon { - width: 24px; - height: 24px; - padding: 0; - background: transparent; - border: none; - border-radius: var(--radius-sm); - cursor: pointer; - color: var(--text-muted); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.15s; -} - -.knowledge-category-actions .btn-icon:hover { - background: var(--bg-secondary); - color: var(--text-primary); -} - -.knowledge-category-actions .btn-icon.btn-danger-hover:hover { - background: var(--danger-bg); - color: var(--danger); -} - -.knowledge-drag-handle { - cursor: grab; - opacity: 0; - color: var(--text-muted); - font-size: 1rem; - padding: 0 var(--spacing-xs); - transition: opacity 0.15s; -} - -.knowledge-category-item:hover .knowledge-drag-handle { - opacity: 0.5; -} - -.knowledge-drag-handle:hover { - opacity: 1 !important; -} - -.knowledge-drag-handle:active { - cursor: grabbing; -} - -/* ============================================ - HAUPTBEREICH (Einträge) - ============================================ */ - -.knowledge-main { - display: flex; - flex-direction: column; - min-height: 0; - overflow: hidden; -} - -.knowledge-main-header { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: var(--spacing-md); - border-bottom: 1px solid var(--border-color); - margin-bottom: var(--spacing-md); - flex-shrink: 0; -} - -.knowledge-main-title { - font-size: 1.25rem; - font-weight: 600; - color: var(--text-primary); - margin: 0; - display: flex; - align-items: center; - gap: var(--spacing-sm); -} - -.knowledge-main-title .category-icon { - font-size: 1.3rem; -} - -/* ============================================ - EINTRÄGE-LISTE - ============================================ */ - -.knowledge-entry-list { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} - -/* ============================================ - EINTRAG-ITEMS (Kompakt + Expand) - ============================================ */ - -.knowledge-entry-item { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - transition: all 0.15s ease; - user-select: none; -} - -.knowledge-entry-item:hover { - border-color: var(--primary); - box-shadow: var(--shadow-sm); -} - -.knowledge-entry-item.dragging { - opacity: 0.5; - box-shadow: var(--shadow-lg); -} - -.knowledge-entry-item.drag-over { - border-top: 2px solid var(--primary); - margin-top: -2px; -} - -.knowledge-entry-item.highlight { - animation: highlightPulse 2s ease-in-out; -} - -@keyframes highlightPulse { - 0%, 100% { - background-color: transparent; - } - 25%, 75% { - background-color: var(--primary-100, rgba(200, 168, 81, 0.1)); - } - 50% { - background-color: var(--primary-200, rgba(200, 168, 81, 0.2)); - } -} - -/* Header (immer sichtbar) */ -.knowledge-entry-header { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm) var(--spacing-md); - cursor: pointer; -} - -.knowledge-entry-expand { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - flex-shrink: 0; - color: var(--text-muted); - transition: transform 0.2s ease; -} - -.knowledge-entry-item.expanded .knowledge-entry-expand { - transform: rotate(90deg); -} - -.knowledge-entry-title { - flex: 1; - font-size: 0.95rem; - font-weight: 500; - color: var(--text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.knowledge-entry-indicators { - display: flex; - align-items: center; - gap: var(--spacing-xs); - color: var(--text-muted); - font-size: 0.8rem; -} - -.knowledge-entry-indicator { - display: flex; - align-items: center; - gap: 2px; -} - -.knowledge-entry-actions { - display: flex; - gap: 2px; - opacity: 0; - transition: opacity 0.15s; -} - -.knowledge-entry-item:hover .knowledge-entry-actions { - opacity: 1; -} - -.knowledge-entry-actions .btn-icon { - width: 28px; - height: 28px; - padding: 0; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - cursor: pointer; - color: var(--text-secondary); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.15s; -} - -.knowledge-entry-actions .btn-icon:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.knowledge-entry-actions .btn-icon.btn-danger-hover:hover { - background: var(--danger-bg); - color: var(--danger); - border-color: var(--danger); -} - -.knowledge-entry-drag-handle { - cursor: grab; - opacity: 0; - color: var(--text-muted); - padding: 0 var(--spacing-xs); - transition: opacity 0.15s; -} - -.knowledge-entry-item:hover .knowledge-entry-drag-handle { - opacity: 0.5; -} +.knowledge-reader-attachment-item:hover { border-color: var(--primary); background: var(--bg-hover); } +.knowledge-reader-attachment-item svg { flex-shrink: 0; color: var(--text-muted); } +.knowledge-reader-attachment-info { flex: 1; min-width: 0; } +.knowledge-reader-attachment-name { font-size: 0.9rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.knowledge-reader-attachment-size { font-size: 0.75rem; color: var(--text-muted); } -.knowledge-entry-drag-handle:hover { - opacity: 1 !important; +/* Editor */ +.knowledge-editor { + padding: var(--spacing-xl) var(--spacing-xxl); + max-width: 800px; width: 100%; margin: 0 auto; } -.knowledge-entry-drag-handle:active { - cursor: grabbing; -} +.knowledge-editor-header { margin-bottom: var(--spacing-lg); } +.knowledge-editor-header h2 { font-size: 1.3rem; font-weight: 600; color: var(--text-primary); margin: 0; } +.knowledge-editor-form .form-group { margin-bottom: var(--spacing-md); } +.knowledge-editor-form label { display: block; font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); margin-bottom: var(--spacing-xs); } -/* Details (aufklappbar) */ -.knowledge-entry-details { - display: none; - padding: var(--spacing-md); - padding-top: 0; - border-top: 1px solid var(--border-light); - margin-top: var(--spacing-xs); - animation: fadeIn 0.15s ease; +.knowledge-editor-form .form-control-lg { + font-size: 1.2rem; padding: var(--spacing-sm) var(--spacing-md); font-weight: 600; } -.knowledge-entry-item.expanded .knowledge-entry-details { - display: block; +.knowledge-editor-textarea { + min-height: 400px; font-family: 'Courier New', monospace; + font-size: 0.9rem; line-height: 1.6; resize: vertical; } -.knowledge-entry-url { - display: flex; - align-items: center; - gap: var(--spacing-xs); - font-size: 0.9rem; - color: var(--primary); - text-decoration: none; - margin-bottom: var(--spacing-sm); - word-break: break-all; -} +.knowledge-editor-buttons { display: flex; gap: var(--spacing-sm); margin-top: var(--spacing-lg); } -.knowledge-entry-url:hover { - text-decoration: underline; -} - -.knowledge-entry-url svg { - flex-shrink: 0; - color: var(--text-muted); -} - -.knowledge-entry-notes { - font-size: 0.9rem; - color: var(--text-secondary); - line-height: 1.5; - margin-bottom: var(--spacing-sm); - white-space: pre-wrap; -} - -/* Markdown Styles in Notizen */ -.knowledge-entry-notes h1 { - font-size: 1.2rem; - font-weight: 700; - color: var(--text-primary); - margin: 0.5rem 0 0.25rem 0; -} - -.knowledge-entry-notes h2 { - font-size: 1.1rem; - font-weight: 600; - color: var(--text-primary); - margin: 0.5rem 0 0.25rem 0; -} - -.knowledge-entry-notes h3 { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - margin: 0.4rem 0 0.2rem 0; -} - -.knowledge-entry-notes code { - background: var(--bg-tertiary); - padding: 2px 6px; - border-radius: var(--radius-sm); - font-family: 'Courier New', monospace; - font-size: 0.85em; -} - -.knowledge-entry-notes pre { - background: var(--bg-tertiary); - padding: var(--spacing-sm) var(--spacing-md); - border-radius: var(--radius-md); - overflow-x: auto; - margin: 0.5rem 0; -} +.knowledge-editor-attachments { margin-top: var(--spacing-md); padding-top: var(--spacing-md); border-top: 1px solid var(--border-color); } +.knowledge-editor-attachments > label { display: block; font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); margin-bottom: var(--spacing-sm); } -.knowledge-entry-notes pre code { - background: transparent; - padding: 0; - border-radius: 0; - font-size: 0.85rem; - line-height: 1.5; +.knowledge-editor-attachment-item { + display: flex; align-items: center; gap: var(--spacing-sm); + padding: var(--spacing-xs) var(--spacing-sm); + background: var(--bg-secondary); border: 1px solid var(--border-color); + border-radius: var(--radius-sm); margin-bottom: var(--spacing-xs); } -.knowledge-entry-notes ul, -.knowledge-entry-notes ol { - padding-left: 1.5rem; - margin: 0.25rem 0; -} - -.knowledge-entry-notes li { - margin: 0.15rem 0; -} - -.knowledge-entry-notes a { - color: var(--primary); - text-decoration: underline; -} - -.knowledge-entry-notes a:hover { - opacity: 0.8; -} - -.knowledge-entry-notes strong { - font-weight: 700; - color: var(--text-primary); -} - -.knowledge-entry-notes em { - font-style: italic; -} - -.knowledge-entry-notes p { - margin: 0.25rem 0; -} - -.knowledge-entry-attachments-info { - display: flex; - align-items: center; - gap: var(--spacing-xs); - font-size: 0.85rem; - color: var(--text-muted); -} - -/* ============================================ - EMPTY STATES - ============================================ */ - -.knowledge-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--spacing-xxl) var(--spacing-lg); - text-align: center; - color: var(--text-muted); - flex: 1; -} - -.knowledge-empty svg { - margin-bottom: var(--spacing-md); - opacity: 0.4; -} - -.knowledge-empty h3 { - font-size: 1.1rem; - font-weight: 600; - color: var(--text-secondary); - margin: 0 0 var(--spacing-xs) 0; -} - -.knowledge-empty p { - font-size: 0.95rem; - margin: 0; -} - -/* ============================================ - SUCHERGEBNISSE - ============================================ */ - -.knowledge-search-results { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.knowledge-search-header { - display: flex; - align-items: center; - gap: var(--spacing-md); - padding-bottom: var(--spacing-md); - border-bottom: 1px solid var(--border-color); - margin-bottom: var(--spacing-md); -} - -.knowledge-search-info { - font-size: 0.95rem; - color: var(--text-secondary); -} - -.knowledge-search-info span { - font-weight: 500; - color: var(--text-primary); -} +.knowledge-editor-attachment-item svg { flex-shrink: 0; color: var(--text-muted); } +.knowledge-editor-attachment-info { flex: 1; min-width: 0; } +.knowledge-editor-attachment-name { font-size: 0.85rem; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.knowledge-editor-attachment-size { font-size: 0.75rem; color: var(--text-muted); } -/* Kategorie-Badge in Suchergebnissen */ -.knowledge-entry-category-badge { - font-size: 0.7rem; - padding: 2px 8px; - border-radius: var(--radius-full); - background: var(--category-color, var(--primary)); - color: white; - font-weight: 500; +.knowledge-editor-attachment-delete { + width: 24px; height: 24px; padding: 0; background: transparent; border: none; + cursor: pointer; color: var(--text-muted); display: flex; align-items: center; justify-content: center; + border-radius: var(--radius-sm); transition: all 0.15s; } -/* ============================================ - MOBILE KATEGORIEN (Chips) - ============================================ */ +.knowledge-editor-attachment-delete:hover { background: var(--danger-bg); color: var(--danger); } +.knowledge-editor-file-upload { margin-top: var(--spacing-sm); } +/* Mobile Chips */ .knowledge-mobile-categories { - display: none; - gap: var(--spacing-sm); - padding: var(--spacing-sm) 0; - overflow-x: auto; - flex-shrink: 0; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; + display: none; gap: var(--spacing-sm); padding: var(--spacing-sm); + overflow-x: auto; flex-shrink: 0; -webkit-overflow-scrolling: touch; scrollbar-width: none; } -.knowledge-mobile-categories::-webkit-scrollbar { - display: none; -} +.knowledge-mobile-categories::-webkit-scrollbar { display: none; } .knowledge-mobile-chip { - display: flex; - align-items: center; - gap: var(--spacing-xs); + display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-md); - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-full); - cursor: pointer; - white-space: nowrap; - font-size: 0.85rem; - color: var(--text-secondary); - transition: all 0.15s; - flex-shrink: 0; + background: var(--bg-secondary); border: 1px solid var(--border-color); + border-radius: var(--radius-full); cursor: pointer; white-space: nowrap; + font-size: 0.85rem; color: var(--text-secondary); transition: all 0.15s; flex-shrink: 0; } -.knowledge-mobile-chip:hover { - background: var(--bg-hover); - border-color: var(--primary); -} +.knowledge-mobile-chip:hover { background: var(--bg-hover); border-color: var(--primary); } +.knowledge-mobile-chip.active { background: var(--primary); border-color: var(--primary); color: white; } +.knowledge-mobile-chip .chip-icon { font-size: 1rem; } +.knowledge-mobile-chip .chip-count { font-size: 0.75rem; opacity: 0.8; } +.knowledge-mobile-add { background: var(--primary); border-color: var(--primary); color: white; } -.knowledge-mobile-chip.active { - background: var(--primary); - border-color: var(--primary); - color: white; -} - -.knowledge-mobile-chip .chip-icon { - font-size: 1rem; -} - -.knowledge-mobile-chip .chip-count { - font-size: 0.75rem; - opacity: 0.8; -} - -.knowledge-mobile-add { - background: var(--primary); - border-color: var(--primary); - color: white; -} - -/* ============================================ - ATTACHMENTS (Modal) - ============================================ */ - -.knowledge-attachments-container { - display: flex; - flex-direction: column; - gap: var(--spacing-xs); - margin-bottom: var(--spacing-sm); -} +/* Attachments (Modal - beibehalten fuer Kategorie-Modal) */ +.knowledge-attachments-container { display: flex; flex-direction: column; gap: var(--spacing-xs); margin-bottom: var(--spacing-sm); } .knowledge-attachment-item { - display: flex; - align-items: center; - gap: var(--spacing-sm); + display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm); - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); + background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-sm); } -.knowledge-attachment-item svg { - flex-shrink: 0; - color: var(--text-muted); -} - -.knowledge-attachment-info { - flex: 1; - min-width: 0; -} - -.knowledge-attachment-name { - font-size: 0.9rem; - color: var(--text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.knowledge-attachment-size { - font-size: 0.8rem; - color: var(--text-muted); -} - -.knowledge-attachment-actions { - display: flex; - gap: var(--spacing-xs); -} +.knowledge-attachment-item svg { flex-shrink: 0; color: var(--text-muted); } +.knowledge-attachment-info { flex: 1; min-width: 0; } +.knowledge-attachment-name { font-size: 0.9rem; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.knowledge-attachment-size { font-size: 0.8rem; color: var(--text-muted); } +.knowledge-attachment-actions { display: flex; gap: var(--spacing-xs); } .knowledge-attachment-actions .btn-icon { - width: 24px; - height: 24px; - padding: 0; - background: transparent; - border: none; - cursor: pointer; - color: var(--text-muted); - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-sm); - transition: all 0.15s; + width: 24px; height: 24px; padding: 0; background: transparent; border: none; + cursor: pointer; color: var(--text-muted); display: flex; align-items: center; justify-content: center; + border-radius: var(--radius-sm); transition: all 0.15s; } -.knowledge-attachment-actions .btn-icon:hover { - background: var(--bg-hover); - color: var(--text-primary); -} +.knowledge-attachment-actions .btn-icon:hover { background: var(--bg-hover); color: var(--text-primary); } +.knowledge-attachment-actions .btn-icon.btn-danger-hover:hover { background: var(--danger-bg); color: var(--danger); } -.knowledge-attachment-actions .btn-icon.btn-danger-hover:hover { - background: var(--danger-bg); - color: var(--danger); -} - -/* ============================================ - DRAG & DROP INDICATOR - ============================================ */ - -.knowledge-drop-indicator { - height: 2px; - background: var(--primary); - border-radius: 1px; - margin: var(--spacing-xs) 0; -} - -/* ============================================ - ANIMATIONS - ============================================ */ - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -/* ============================================ - RESPONSIVE - ============================================ */ - -@media (max-width: 900px) { - .knowledge-layout { - grid-template-columns: 450px 1fr; - } -} - -@media (max-width: 768px) { - .view-knowledge { - padding: var(--spacing-sm); - } - - .knowledge-layout { - grid-template-columns: 1fr; - } - - .knowledge-sidebar { - display: none; - } - - .knowledge-mobile-categories { - display: flex; - } - - .knowledge-main-header { - flex-wrap: wrap; - gap: var(--spacing-sm); - } - - .knowledge-main-title { - font-size: 1.1rem; - } - - /* Aktionen immer sichtbar auf Mobile */ - .knowledge-entry-actions, - .knowledge-category-actions { - opacity: 1; - } - - .knowledge-entry-drag-handle, - .knowledge-drag-handle { - opacity: 0.5; - } -} - -@media (max-width: 480px) { - .knowledge-entry-header { - padding: var(--spacing-xs) var(--spacing-sm); - } - - .knowledge-entry-details { - padding: var(--spacing-sm); - } - - .knowledge-main-header { - padding-bottom: var(--spacing-sm); - margin-bottom: var(--spacing-sm); - } -} - -/* ========================================== - ICON PICKER STYLES - ========================================== */ - -.icon-picker-container { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} +/* Icon Picker */ +.icon-picker-container { display: flex; align-items: center; gap: var(--spacing-sm); } .icon-picker-preview { - display: flex; - align-items: center; - gap: var(--spacing-sm); + display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); - background: var(--bg-secondary); - border: 2px solid var(--border-color); - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s ease; + background: var(--bg-secondary); border: 2px solid var(--border-color); + border-radius: var(--radius-md); cursor: pointer; transition: all 0.2s ease; } -.icon-picker-preview:hover { - border-color: var(--primary); - background: var(--bg-hover); -} - -.icon-preview-emoji { - font-size: 1.75rem; - line-height: 1; -} - -.icon-preview-label { - font-size: 0.8rem; - color: var(--text-secondary); -} - -.icon-picker-section { - margin-top: var(--spacing-md); - padding-top: var(--spacing-md); - border-top: 1px solid var(--border-color); -} - -.icon-picker-tabs { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - margin-bottom: var(--spacing-md); -} +.icon-picker-preview:hover { border-color: var(--primary); background: var(--bg-hover); } +.icon-preview-emoji { font-size: 1.75rem; line-height: 1; } +.icon-preview-label { font-size: 0.8rem; color: var(--text-secondary); } +.icon-picker-section { margin-top: var(--spacing-md); padding-top: var(--spacing-md); border-top: 1px solid var(--border-color); } +.icon-picker-tabs { display: flex; flex-wrap: wrap; gap: var(--spacing-xs); margin-bottom: var(--spacing-md); } .icon-tab { - padding: var(--spacing-xs) var(--spacing-sm); - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - font-size: 0.8rem; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; + padding: var(--spacing-xs) var(--spacing-sm); background: var(--bg-secondary); + border: 1px solid var(--border-color); border-radius: var(--radius-sm); + font-size: 0.8rem; cursor: pointer; transition: all 0.2s ease; white-space: nowrap; } -.icon-tab:hover { - background: var(--bg-hover); - border-color: var(--primary); -} - -.icon-tab.active { - background: var(--primary); - color: white; - border-color: var(--primary); -} +.icon-tab:hover { background: var(--bg-hover); border-color: var(--primary); } +.icon-tab.active { background: var(--primary); color: white; border-color: var(--primary); } .icon-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); - gap: var(--spacing-xs); - max-height: 200px; - overflow-y: auto; - padding: var(--spacing-xs); - background: var(--bg-tertiary); - border-radius: var(--radius-md); + display: grid; grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); + gap: var(--spacing-xs); max-height: 200px; overflow-y: auto; + padding: var(--spacing-xs); background: var(--bg-tertiary); border-radius: var(--radius-md); } .icon-btn { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: var(--bg-primary); - border: 1px solid transparent; - border-radius: var(--radius-sm); - font-size: 1.25rem; - cursor: pointer; - transition: all 0.15s ease; + display: flex; align-items: center; justify-content: center; + width: 40px; height: 40px; background: var(--bg-primary); + border: 1px solid transparent; border-radius: var(--radius-sm); + font-size: 1.25rem; cursor: pointer; transition: all 0.15s ease; } -.icon-btn:hover { - background: var(--bg-hover); - border-color: var(--primary); - transform: scale(1.1); +.icon-btn:hover { background: var(--bg-hover); border-color: var(--primary); transform: scale(1.1); } +.icon-btn.selected { background: var(--primary-light); border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-light); } + +.icon-grid::-webkit-scrollbar { width: 6px; } +.icon-grid::-webkit-scrollbar-track { background: var(--bg-tertiary); border-radius: 3px; } +.icon-grid::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; } +.icon-grid::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); } + +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + +/* Responsive */ +@media (max-width: 768px) { + .view-knowledge { height: calc(100vh - 60px); } + .knowledge-layout { grid-template-columns: 1fr; } + .knowledge-sidebar { display: none; } + .knowledge-mobile-categories { display: flex; } + .knowledge-reader { padding: var(--spacing-md); } + .knowledge-editor { padding: var(--spacing-md); } + .knowledge-reader-header { flex-direction: column; gap: var(--spacing-sm); } + .knowledge-reader-header h1 { font-size: 1.3rem; } + .knowledge-reader-actions { align-self: flex-start; } + .knowledge-editor-textarea { min-height: 250px; } } -.icon-btn.selected { - background: var(--primary-light); - border-color: var(--primary); - box-shadow: 0 0 0 2px var(--primary-light); -} - -/* Scrollbar für Icon Grid */ -.icon-grid::-webkit-scrollbar { - width: 6px; -} - -.icon-grid::-webkit-scrollbar-track { - background: var(--bg-tertiary); - border-radius: 3px; -} - -.icon-grid::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 3px; -} - -.icon-grid::-webkit-scrollbar-thumb:hover { - background: var(--text-secondary); -} - -/* ========================================== - SIDEBAR RESIZE HANDLE - ========================================== */ - -.knowledge-resize-handle { - position: absolute; - top: 0; - right: -4px; - bottom: 0; - width: 8px; - cursor: col-resize; - z-index: 10; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; -} - -.knowledge-resize-handle::before { - content: ''; - width: 1px; - height: 30px; - background: transparent; - border-radius: 1px; - transition: all 0.2s ease; -} - -.knowledge-resize-handle:hover::before { - background: var(--border-color); - width: 2px; - height: 40px; -} - -.knowledge-resize-handle.dragging::before { - background: var(--primary); - width: 3px; - height: 80px; -} - -.knowledge-layout.resizing { - user-select: none; +@media (max-width: 480px) { + .knowledge-reader { padding: var(--spacing-sm); } + .knowledge-editor { padding: var(--spacing-sm); } + .knowledge-reader-header h1 { font-size: 1.1rem; } } diff --git a/frontend/index.html b/frontend/index.html index 91806b2..fc360b2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -580,91 +580,107 @@
- +
- +
- + - -
-
-

Kategorie wählen

- -
+ +
- -
- -
- - -
+ +
-

Kategorie auswählen

-

Wählen Sie links eine Kategorie aus, um Einträge anzuzeigen.

+

Wissensdatenbank

+

Waehlen Sie links einen Eintrag aus, um ihn hier anzuzeigen.

- -
diff --git a/frontend/js/knowledge.js b/frontend/js/knowledge.js index 2ea23c4..fb496d5 100644 --- a/frontend/js/knowledge.js +++ b/frontend/js/knowledge.js @@ -1,7 +1,7 @@ /** * TASKMATE - Knowledge Manager * ============================ - * Wissensmanagement mit Sidebar-Layout, Drag & Drop und kompakten Einträgen + * Wiki-Layout: Baum-Navigation + Content-Bereich */ import api from './api.js'; @@ -11,25 +11,22 @@ import store from './store.js'; class KnowledgeManager { constructor() { this.categories = []; - this.entries = []; - this.selectedCategory = null; - this.searchQuery = ''; - this.searchResults = []; - this.expandedEntries = new Set(); + 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; - this.pendingFiles = 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(); + await this.loadData(); return; } @@ -38,25 +35,38 @@ class KnowledgeManager { 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.sidebarSearch = $('#knowledge-sidebar-search'); this.newCategoryBtn = $('#btn-new-category'); - this.newEntryBtn = $('#btn-new-entry'); - this.clearSearchBtn = $('#btn-clear-search'); + 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'); @@ -73,143 +83,118 @@ class KnowledgeManager { 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(); + await this.loadData(); } bindEvents() { - console.log('[Knowledge] bindEvents() called'); + // 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(); }); - // 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)); - - // Click on file upload area to trigger file input - this.fileUploadArea?.addEventListener('click', (e) => { - // Don't trigger if clicking on the label (it has its own handler) - if (!e.target.closest('.file-input-label')) { - this.fileInput?.click(); + // Reader buttons + this.editEntryBtn?.addEventListener('click', () => { + if (this.selectedEntryId) { + this.showEditor(this.selectedEntryId); } }); - // Drag & Drop for file upload - if (this.fileUploadArea) { - this.fileUploadArea.addEventListener('dragover', (e) => { - e.preventDefault(); - this.fileUploadArea.classList.add('drag-over'); - }); + this.deleteEntryReaderBtn?.addEventListener('click', () => { + if (this.selectedEntryId) { + this.deleteEntry(this.selectedEntryId); + } + }); - this.fileUploadArea.addEventListener('dragleave', () => { - this.fileUploadArea.classList.remove('drag-over'); - }); + // Editor form + this.editorForm?.addEventListener('submit', (e) => { + e.preventDefault(); + this.saveEntry(); + }); - 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); - } - }); - } + this.btnCancelEditor?.addEventListener('click', () => { + if (this.editingEntryId) { + this.showEntry(this.editingEntryId); + } else { + this.showEmptyState(); + } + }); - // Drag & Drop for categories - this.bindCategoryDragEvents(); + // File upload + this.btnUploadFile?.addEventListener('click', () => { + this.editorFileInput?.click(); + }); - // Drag & Drop for entries - this.bindEntryDragEvents(); + this.editorFileInput?.addEventListener('change', (e) => { + this.handleFileUpload(e); + }); - // Sidebar resize functionality - this.bindResizeEvents(); + // 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 loadCategories() { + async loadData() { try { - this.categories = await api.getKnowledgeCategories(); - // Sort by position + 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.renderCategories(); + + this.allEntries = Array.isArray(entriesResult) ? entriesResult : []; + this.allEntries.sort((a, b) => (a.position || 0) - (b.position || 0)); + + this.renderTree(); this.renderMobileCategories(); - // Update UI state - this.updateMainState(); + // 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('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'); + console.error('[Knowledge] Error loading data:', error); + this.showToast('Fehler beim Laden der Wissensdatenbank', 'error'); } } @@ -224,52 +209,158 @@ class KnowledgeManager { } // ========================================== - // RENDERING - CATEGORIES (Sidebar) + // TREE RENDERING (Sidebar) // ========================================== - renderCategories() { - if (!this.categoriesList) return; + getFilteredTree() { + if (!this.filterQuery) { + return this.categories.map(cat => ({ + ...cat, + entries: this.allEntries.filter(e => e.categoryId === cat.id) + })); + } - if (this.categories.length === 0) { - this.categoriesList.innerHTML = ''; - this.categoriesEmpty?.classList.remove('hidden'); + 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.categoriesEmpty?.classList.add('hidden'); + this.treeEmpty?.classList.add('hidden'); - this.categoriesList.innerHTML = this.categories.map(cat => ` -
- ${cat.icon || '📁'} -
- ${this.escapeHtml(cat.name)} - ${cat.entryCount || 0} Einträge -
-
- - -
- ⋮⋮ -
- `).join(''); + // If filtering, auto-expand all categories + if (this.filterQuery) { + treeData.forEach(cat => this.expandedCategories.add(cat.id)); + } - this.bindCategoryClickEvents(); + this.tree.innerHTML = treeData.map(cat => { + const isExpanded = this.expandedCategories.has(cat.id); + const entries = cat.entries || []; + const icon = cat.icon || '\u{1F4C1}'; + + return ` +
+
+ + ${icon} + ${this.escapeHtml(cat.name)} + ${entries.length} +
+ + +
+
+
+ ${entries.map(entry => ` +
+ ${this.escapeHtml(entry.title)} +
+ `).join('')} +
+ + Neuer Eintrag +
+
+
+ `; + }).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 => ` -
- ${cat.icon || '📁'} + ${cat.icon || '\u{1F4C1}'} ${this.escapeHtml(cat.name)} (${cat.entryCount || 0})
@@ -285,577 +376,343 @@ class KnowledgeManager { this.bindMobileChipEvents(); } - // ========================================== - // RENDERING - ENTRIES (Main) - // ========================================== - - renderEntries() { - if (!this.entriesList) return; - - // Hide no-selection message - this.noSelection?.classList.add('hidden'); - - if (this.entries.length === 0) { - this.entriesList.innerHTML = ''; - this.entriesEmpty?.classList.remove('hidden'); - return; - } - - this.entriesEmpty?.classList.add('hidden'); - - this.entriesList.innerHTML = this.entries.map(entry => this.renderEntryItem(entry)).join(''); - this.bindEntryClickEvents(); - } - - renderEntryItem(entry, showCategory = false) { - const isExpanded = this.expandedEntries.has(entry.id); - const hasUrl = entry.url && entry.url.trim(); - const hasNotes = entry.notes && entry.notes.trim(); - const hasAttachments = entry.attachmentCount > 0; - - return ` -
-
- - - - ${this.escapeHtml(entry.title)} -
- ${hasUrl ? '🔗' : ''} - ${hasNotes ? '📝' : ''} - ${hasAttachments ? `📎${entry.attachmentCount}` : ''} -
- ${showCategory && entry.categoryName ? ` - - ` : ''} - ⋮⋮ -
- - -
-
-
- ${hasUrl ? ` - - - ${this.escapeHtml(entry.url)} - - ` : ''} - ${hasNotes ? ` -
${this.renderMarkdown(this.sanitizeHtml(entry.notes))}
- ` : ''} - ${hasAttachments ? ` -
- - ${entry.attachmentCount} Anhang${entry.attachmentCount > 1 ? 'e' : ''} -
- ` : ''} -
-
- `; - } - - renderSearchResults() { - if (!this.searchResultsList) return; - - if (this.searchResults.length === 0) { - this.searchResultsList.innerHTML = ''; - this.searchResultsEmpty?.classList.remove('hidden'); - return; - } - - this.searchResultsEmpty?.classList.add('hidden'); - - this.searchResultsList.innerHTML = this.searchResults.map(entry => - this.renderEntryItem(entry, true) - ).join(''); - - this.bindSearchResultEvents(); - } - - renderAttachments(attachments) { - if (!this.attachmentsContainer) return; - - if (!attachments || attachments.length === 0) { - this.attachmentsContainer.innerHTML = '

Keine Anhänge vorhanden

'; - return; - } - - this.attachmentsContainer.innerHTML = attachments.map(att => ` -
- -
- ${this.escapeHtml(att.originalName || att.original_name || '')} - ${this.formatFileSize(att.sizeBytes || att.size_bytes || 0)} -
-
- - - - -
-
- `).join(''); - - this.bindAttachmentEvents(); - } - - updateMainState() { - if (!this.selectedCategory) { - this.mainTitle.textContent = 'Kategorie wählen'; - this.newEntryBtn.disabled = true; - this.entriesList.innerHTML = ''; - this.entriesEmpty?.classList.add('hidden'); - this.noSelection?.classList.remove('hidden'); - } else { - const icon = this.selectedCategory.icon || '📁'; - this.mainTitle.innerHTML = `${icon} ${this.escapeHtml(this.selectedCategory.name)}`; - this.newEntryBtn.disabled = false; - this.noSelection?.classList.add('hidden'); - } - } - - // ========================================== - // EVENT BINDING - CATEGORIES - // ========================================== - - bindCategoryClickEvents() { - this.categoriesList?.querySelectorAll('.knowledge-category-item').forEach(item => { - const categoryId = parseInt(item.dataset.categoryId); - - // Click on category (not on actions) - select category - item.addEventListener('click', (e) => { - if (!e.target.closest('.knowledge-category-actions') && !e.target.closest('.knowledge-drag-handle')) { - this.selectCategory(categoryId); - } - }); - - // Edit button - const editBtn = item.querySelector('[data-action="edit"]'); - editBtn?.addEventListener('click', (e) => { - e.stopPropagation(); - this.openCategoryModal(categoryId); - }); - - // Delete button - const deleteBtn = item.querySelector('[data-action="delete"]'); - deleteBtn?.addEventListener('click', (e) => { - e.stopPropagation(); - this.handleDeleteCategory(categoryId); - }); - }); - } - bindMobileChipEvents() { this.mobileCategories?.querySelectorAll('.knowledge-mobile-chip').forEach(chip => { if (chip.dataset.action === 'add-category') { chip.addEventListener('click', () => this.openCategoryModal()); } else { const categoryId = parseInt(chip.dataset.categoryId); - chip.addEventListener('click', () => this.selectCategory(categoryId)); - } - }); - } - - bindCategoryDragEvents() { - if (!this.categoriesList) return; - - this.categoriesList.addEventListener('dragstart', (e) => { - const item = e.target.closest('.knowledge-category-item'); - if (!item) return; - - this.draggedCategoryId = parseInt(item.dataset.categoryId); - item.classList.add('dragging'); - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', this.draggedCategoryId); - }); - - this.categoriesList.addEventListener('dragend', (e) => { - const item = e.target.closest('.knowledge-category-item'); - if (item) { - item.classList.remove('dragging'); - } - this.draggedCategoryId = null; - this.categoriesList.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); - }); - - this.categoriesList.addEventListener('dragover', (e) => { - e.preventDefault(); - const item = e.target.closest('.knowledge-category-item'); - if (item && parseInt(item.dataset.categoryId) !== this.draggedCategoryId) { - this.categoriesList.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); - item.classList.add('drag-over'); - } - }); - - this.categoriesList.addEventListener('dragleave', (e) => { - const item = e.target.closest('.knowledge-category-item'); - if (item) { - item.classList.remove('drag-over'); - } - }); - - this.categoriesList.addEventListener('drop', async (e) => { - e.preventDefault(); - const targetItem = e.target.closest('.knowledge-category-item'); - if (!targetItem || !this.draggedCategoryId) return; - - const targetId = parseInt(targetItem.dataset.categoryId); - if (targetId === this.draggedCategoryId) return; - - targetItem.classList.remove('drag-over'); - - // Calculate new position - const targetIndex = this.categories.findIndex(c => c.id === targetId); - const draggedIndex = this.categories.findIndex(c => c.id === this.draggedCategoryId); - - if (targetIndex === -1 || draggedIndex === -1) return; - - // Optimistic update - const [moved] = this.categories.splice(draggedIndex, 1); - this.categories.splice(targetIndex, 0, moved); - - // Update positions in array - this.categories.forEach((cat, idx) => cat.position = idx); - - this.renderCategories(); - this.renderMobileCategories(); - - // Server update - try { - await api.updateKnowledgeCategoryPosition(this.draggedCategoryId, targetIndex); - } catch (error) { - console.error('Error updating category position:', error); - this.showToast('Fehler beim Speichern der Reihenfolge', 'error'); - await this.loadCategories(); + chip.addEventListener('click', () => { + this.selectedCategoryId = categoryId; + this.expandedCategories.add(categoryId); + this.renderTree(); + this.renderMobileCategories(); + }); } }); } // ========================================== - // EVENT BINDING - ENTRIES + // CONTENT STATES // ========================================== - 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); - }); - }); + showEmptyState() { + this.isEditing = false; + this.editingEntryId = null; + this.selectedEntryId = null; + this.emptyState?.classList.remove('hidden'); + this.reader?.classList.add('hidden'); + this.editor?.classList.add('hidden'); + this.renderTree(); } - bindSearchResultEvents() { - this.searchResultsList?.querySelectorAll('.knowledge-entry-item').forEach(item => { - const entryId = parseInt(item.dataset.entryId); + async showEntry(entryId) { + this.isEditing = false; + this.editingEntryId = null; + this.selectedEntryId = 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; + // Load full entry with attachments + const entry = await this.loadEntryWithAttachments(entryId); + if (!entry) { + this.showEmptyState(); + return; } - // Update hidden input - if (this.categoryIconInput) { - this.categoryIconInput.value = icon; + // Expand parent category + if (entry.categoryId) { + this.expandedCategories.add(entry.categoryId); + this.selectedCategoryId = entry.categoryId; } - // Hide the icon picker - if (this.iconPickerSection) { - this.iconPickerSection.classList.add('hidden'); - } - } + // Update content area + this.emptyState?.classList.add('hidden'); + this.editor?.classList.add('hidden'); + this.reader?.classList.remove('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'); + // Title + if (this.readerTitle) { + this.readerTitle.textContent = entry.title; } - // Update sidebar selection - this.renderCategories(); + // Meta + if (this.readerMeta) { + const cat = this.categories.find(c => c.id === entry.categoryId); + const catName = cat ? cat.name : 'Unbekannt'; + const catIcon = cat ? (cat.icon || '\u{1F4C1}') : '\u{1F4C1}'; + const date = entry.updatedAt || entry.createdAt; + let dateStr = ''; + if (date) { + const d = new Date(date); + const day = String(d.getDate()).padStart(2, '0'); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const year = d.getFullYear(); + dateStr = `${day}.${month}.${year}`; + } + const creator = entry.creatorName || entry.creator_name || ''; + this.readerMeta.innerHTML = ` + ${catIcon} ${this.escapeHtml(catName)} + ${creator ? `von ${this.escapeHtml(creator)}` : ''} + ${dateStr ? `${dateStr}` : ''} + `; + } + + // URL + if (this.readerUrl) { + if (entry.url && entry.url.trim()) { + this.readerUrl.classList.remove('hidden'); + this.readerUrl.innerHTML = ` + + ${this.escapeHtml(entry.url)} + `; + } else { + this.readerUrl.classList.add('hidden'); + this.readerUrl.innerHTML = ''; + } + } + + // Body (Markdown) + if (this.readerBody) { + if (entry.notes && entry.notes.trim()) { + this.readerBody.innerHTML = this.renderMarkdown(this.sanitizeHtml(entry.notes)); + } else { + this.readerBody.innerHTML = '

Kein Inhalt vorhanden.

'; + } + } + + // Attachments + if (this.readerAttachments) { + const attachments = entry.attachments || []; + if (attachments.length > 0) { + this.readerAttachments.classList.remove('hidden'); + this.readerAttachments.innerHTML = ` +

Anhaenge (${attachments.length})

+ ${attachments.map(att => ` + + + ${this.escapeHtml(att.originalName || att.original_name || 'Datei')} + ${this.formatFileSize(att.sizeBytes || att.size_bytes || 0)} + + `).join('')} + `; + } else { + this.readerAttachments.classList.add('hidden'); + this.readerAttachments.innerHTML = ''; + } + } + + // Update tree to highlight active entry + this.renderTree(); this.renderMobileCategories(); - - // Update main header - this.updateMainState(); - - // Load entries - await this.loadEntries(categoryId); } - toggleEntry(entryId) { - if (this.expandedEntries.has(entryId)) { - this.expandedEntries.delete(entryId); + async showEditor(entryId = null, categoryId = null) { + this.isEditing = true; + this.editingEntryId = entryId; + + this.emptyState?.classList.add('hidden'); + this.reader?.classList.add('hidden'); + this.editor?.classList.remove('hidden'); + + // Reset form + this.editorForm?.reset(); + if (this.editorId) this.editorId.value = ''; + if (this.editorCategoryId) this.editorCategoryId.value = ''; + + if (entryId) { + // Edit existing + const entry = await this.loadEntryWithAttachments(entryId); + if (!entry) return; + + if (this.editorTitle) this.editorTitle.textContent = 'Eintrag bearbeiten'; + if (this.editorId) this.editorId.value = entry.id; + if (this.editorCategoryId) this.editorCategoryId.value = entry.categoryId; + if (this.editorTitleInput) this.editorTitleInput.value = entry.title; + if (this.editorUrlInput) this.editorUrlInput.value = entry.url || ''; + if (this.editorNotes) this.editorNotes.value = entry.notes || ''; + + // Show attachments + this.renderEditorAttachments(entry.attachments || []); } else { - this.expandedEntries.add(entryId); + // New entry + if (this.editorTitle) this.editorTitle.textContent = 'Neuer Eintrag'; + if (this.editorCategoryId) this.editorCategoryId.value = categoryId || this.selectedCategoryId || ''; + + // Hide attachments for new entries + if (this.editorAttachments) this.editorAttachments.classList.add('hidden'); } - // Update just the affected entry - const entryItem = this.entriesList?.querySelector(`[data-entry-id="${entryId}"]`); - if (entryItem) { - entryItem.classList.toggle('expanded', this.expandedEntries.has(entryId)); - } + this.editorTitleInput?.focus(); } // ========================================== - // SEARCH + // ENTRY CRUD // ========================================== - async handleSearch(query) { - this.searchQuery = query.trim(); + async saveEntry() { + const entryId = this.editorId?.value ? parseInt(this.editorId.value) : null; + const categoryId = parseInt(this.editorCategoryId?.value); + const data = { + categoryId: categoryId, + title: this.editorTitleInput?.value.trim(), + url: this.editorUrlInput?.value.trim() || null, + notes: this.editorNotes?.value.trim() || null + }; - if (!this.searchQuery) { - this.clearSearch(); + if (!data.title) { + this.showToast('Bitte einen Titel eingeben', 'error'); + return; + } + + if (!data.categoryId) { + this.showToast('Keine Kategorie ausgewaehlt', 'error'); return; } try { - const results = await api.searchKnowledge(this.searchQuery); - this.searchResults = results.entries || []; - this.searchQuerySpan.textContent = this.searchQuery; + let savedEntry; + if (entryId) { + savedEntry = await api.updateKnowledgeEntry(entryId, data); + this.showToast('Eintrag aktualisiert', 'success'); + } else { + savedEntry = await api.createKnowledgeEntry(data); + this.showToast('Eintrag erstellt', 'success'); + } - // Show search results, hide other content - this.noSelection?.classList.add('hidden'); - this.entriesEmpty?.classList.add('hidden'); - this.entriesList.innerHTML = ''; - this.searchResultsSection?.classList.remove('hidden'); + // Reload data + await this.loadData(); - this.renderSearchResults(); + // Show the saved entry + const id = savedEntry?.id || entryId; + if (id) { + this.showEntry(id); + } else { + this.showEmptyState(); + } } catch (error) { - console.error('Error searching:', error); - this.showToast('Fehler bei der Suche', 'error'); + this.showToast(error.message || 'Fehler beim Speichern', 'error'); } } - clearSearch() { - this.searchQuery = ''; - this.searchResults = []; + async deleteEntry(entryId) { + const entry = this.allEntries.find(e => e.id === entryId); + const title = entry?.title || 'Dieser Eintrag'; - // Hide search results - this.searchResultsSection?.classList.add('hidden'); + const confirmDelete = confirm(`"${title}" wirklich loeschen?`); + if (!confirmDelete) return; - // Show normal view - if (this.selectedCategory) { - this.loadEntries(this.selectedCategory.id); - } else { - this.updateMainState(); + try { + await api.deleteKnowledgeEntry(entryId); + this.showToast('Eintrag geloescht', 'success'); + + this.selectedEntryId = null; + await this.loadData(); + this.showEmptyState(); + } catch (error) { + this.showToast(error.message || 'Fehler beim Loeschen', 'error'); } } - /** - * Public method for global search from header - */ + // ========================================== + // EDITOR ATTACHMENTS + // ========================================== + + renderEditorAttachments(attachments) { + if (!this.editorAttachments || !this.editorAttachmentsList) return; + + if (!attachments || attachments.length === 0) { + this.editorAttachments.classList.remove('hidden'); + this.editorAttachmentsList.innerHTML = '

Keine Anhaenge vorhanden

'; + return; + } + + this.editorAttachments.classList.remove('hidden'); + this.editorAttachmentsList.innerHTML = attachments.map(att => ` +
+ + ${this.escapeHtml(att.originalName || att.original_name || 'Datei')} + ${this.formatFileSize(att.sizeBytes || att.size_bytes || 0)} + +
+ `).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.clearSearch(); + this.filterQuery = ''; + if (this.sidebarSearch) this.sidebarSearch.value = ''; + this.renderTree(); return; } this.searchDebounceTimer = setTimeout(() => { - this.handleSearch(query); + this.filterQuery = query.trim().toLowerCase(); + if (this.sidebarSearch) this.sidebarSearch.value = query.trim(); + this.renderTree(); }, 300); } @@ -878,7 +735,7 @@ class KnowledgeManager { this.categoryNameInput.value = category.name; this.categoryDescriptionInput.value = category.description || ''; this.categoryColorInput.value = category.color || '#3B82F6'; - const icon = category.icon || '📁'; + const icon = category.icon || '\u{1F4C1}'; this.categoryIconInput.value = icon; if (this.iconPreviewEmoji) { this.iconPreviewEmoji.textContent = icon; @@ -887,9 +744,9 @@ class KnowledgeManager { } else { this.categoryIdInput.value = ''; this.categoryColorInput.value = '#3B82F6'; - this.categoryIconInput.value = '📁'; + this.categoryIconInput.value = '\u{1F4C1}'; if (this.iconPreviewEmoji) { - this.iconPreviewEmoji.textContent = '📁'; + this.iconPreviewEmoji.textContent = '\u{1F4C1}'; } } @@ -927,7 +784,7 @@ class KnowledgeManager { } this.closeCategoryModal(); - await this.loadCategories(); + await this.loadData(); } catch (error) { this.showToast(error.message || 'Fehler beim Speichern', 'error'); } @@ -939,200 +796,81 @@ class KnowledgeManager { const count = category.entryCount || 0; const msg = count > 0 - ? `Kategorie "${category.name}" mit ${count} Einträgen löschen? Dies kann nicht rückgängig gemacht werden.` - : `Kategorie "${category.name}" löschen? Dies kann nicht rückgängig gemacht werden.`; + ? `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 gelöscht', 'success'); + this.showToast('Kategorie geloescht', 'success'); - // If deleted category was selected, clear selection - if (this.selectedCategory?.id === categoryId) { - this.selectedCategory = null; - this.entries = []; - this.updateMainState(); + // If any entry from this category was selected, clear + if (this.selectedCategoryId === categoryId) { + this.selectedCategoryId = null; + this.selectedEntryId = null; + this.showEmptyState(); } - await this.loadCategories(); + await this.loadData(); } catch (error) { - this.showToast(error.message || 'Fehler beim Löschen', 'error'); + this.showToast(error.message || 'Fehler beim Loeschen', 'error'); } } // ========================================== - // ENTRY CRUD + // ICON PICKER // ========================================== - async openEntryModal(entryId = null) { - const isEdit = !!entryId; - this.entryModalTitle.textContent = isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag'; - this.entryForm?.reset(); - this.deleteEntryBtn?.classList.toggle('hidden', !isEdit); - // Show attachments section always, but with a note for new entries - this.attachmentsSection.style.display = 'block'; + bindIconPickerEvents() { + if (!this.iconPickerPreview || !this.iconPickerSection) return; - 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 = '

Speichern Sie zuerst den Eintrag, um Dateien hochzuladen.

'; - } + this.iconPickerPreview.addEventListener('click', () => { + this.iconPickerSection.classList.toggle('hidden'); + }); - this.openModal(this.entryModal, 'knowledge-entry-modal'); - this.entryTitleInput?.focus(); + 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); + }); + }); } - closeEntryModal() { - this.closeModal(this.entryModal, 'knowledge-entry-modal'); + 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); + }); } - async handleEntrySubmit(e) { - e.preventDefault(); + selectIcon(icon) { + if (!icon) return; - 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 (this.iconPreviewEmoji) { + this.iconPreviewEmoji.textContent = icon; } - if (!data.categoryId) { - this.showToast('Keine Kategorie ausgewählt', 'error'); - return; + if (this.categoryIconInput) { + this.categoryIconInput.value = icon; } - try { - if (entryId) { - await api.updateKnowledgeEntry(entryId, data); - this.showToast('Eintrag aktualisiert', 'success'); - } else { - const newEntry = await api.createKnowledgeEntry(data); - this.showToast('Eintrag erstellt', 'success'); - - // If there are pending files, upload them now - if (this.pendingFiles && this.pendingFiles.length > 0) { - this.showToast('Lade Dateien hoch...', 'info'); - await this.uploadFiles(this.pendingFiles); - this.pendingFiles = null; - } - } - - 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) { - // For new entries, show a more helpful message - this.showToast('Speichern Sie zuerst den Eintrag, dann können Sie Dateien hochladen', 'info'); - // Store files temporarily to upload after save - this.pendingFiles = files; - 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'); + if (this.iconPickerSection) { + this.iconPickerSection.classList.add('hidden'); } } @@ -1158,7 +896,6 @@ class KnowledgeManager { 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'); @@ -1170,161 +907,75 @@ class KnowledgeManager { 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`; - } - } - // ========================================== // MARKDOWN RENDERING // ========================================== - /** - * Sanitize HTML - entfernt gefaehrliche Tags und Attribute - * Muss VOR renderMarkdown aufgerufen werden - */ sanitizeHtml(html) { if (!html) return ''; - // Entferne script, iframe, object, embed, form, style Tags let clean = html.replace(/<\s*(script|iframe|object|embed|form|style|link|meta|base)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, ''); - // Entferne selbstschliessende gefaehrliche Tags clean = clean.replace(/<\s*(script|iframe|object|embed|form|style|link|meta|base)\b[^>]*\/?>/gi, ''); - // Entferne on*-Event-Attribute (onclick, onerror, onload etc.) clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, ''); - // Entferne javascript: URLs clean = clean.replace(/href\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'href="#"'); clean = clean.replace(/src\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, 'src=""'); return clean; } - /** - * Einfacher Markdown-Renderer - * Unterstuetzt: fett, kursiv, Ueberschriften, Listen, Links, Code, Zeilenumbrueche - */ renderMarkdown(text) { if (!text) return ''; - // Zuerst HTML-Entities escapen fuer Sicherheit let html = text .replace(/&/g, '&') .replace(//g, '>'); - // Code-Bloecke (dreifache Backticks) - vor anderen Regeln + // Code blocks html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, lang, code) => { return `
${code.trim()}
`; }); - // Inline-Code (einfache Backticks) + // Inline code html = html.replace(/`([^`\n]+)`/g, '$1'); - // Ueberschriften (# bis ###) + // Headings html = html.replace(/^### (.+)$/gm, '

$1

'); html = html.replace(/^## (.+)$/gm, '

$1

'); html = html.replace(/^# (.+)$/gm, '

$1

'); - // Fett (**text** oder __text__) + // Bold html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/__(.+?)__/g, '$1'); - // Kursiv (*text* oder _text_) + // Italic html = html.replace(/(?$1'); html = html.replace(/(?$1'); - // Links [text](url) + // Links html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); - // Ungeordnete Listen (- oder * am Zeilenanfang) + // Unordered lists html = html.replace(/^(?:- |\* )(.+)$/gm, '
  • $1
  • '); html = html.replace(/((?:
  • .*<\/li>\n?)+)/g, ''); - // Geordnete Listen (1. am Zeilenanfang) + // Ordered lists html = html.replace(/^\d+\. (.+)$/gm, '$1'); html = html.replace(/((?:.*<\/oli>\n?)+)/g, (match) => { return '
      ' + match.replace(/<\/?oli>/g, (tag) => tag.replace('oli', 'li')) + '
    '; }); - // Leere Zeilen als Absatz-Trennung + // Paragraphs html = html.replace(/\n\n+/g, '

    '); - // Einfache Zeilenumbrueche (ausser in pre/code und nach Block-Elementen) + // Line breaks html = html.replace(/(?|<\/li>|<\/ul>|<\/ol>|<\/pre>|<\/p>|

    )\n(?!|