Implementierung Wissensmanagement
Dieser Commit ist enthalten in:
853
frontend/css/knowledge.css
Normale Datei
853
frontend/css/knowledge.css
Normale Datei
@ -0,0 +1,853 @@
|
||||
/**
|
||||
* TASKMATE - Knowledge Management Styles
|
||||
* ======================================
|
||||
* Sidebar + Main Layout mit Drag & Drop
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
VIEW CONTAINER
|
||||
============================================ */
|
||||
|
||||
.view-knowledge {
|
||||
padding: var(--spacing-md);
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MAIN LAYOUT (Sidebar + Main)
|
||||
============================================ */
|
||||
|
||||
.knowledge-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: var(--spacing-lg);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SIDEBAR (Kategorien)
|
||||
============================================ */
|
||||
|
||||
.knowledge-sidebar {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.knowledge-sidebar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 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-category-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-xs) 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;
|
||||
}
|
||||
|
||||
.knowledge-sidebar-empty svg {
|
||||
opacity: 0.4;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.knowledge-sidebar-empty p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
KATEGORIE-ITEMS (Sidebar)
|
||||
============================================ */
|
||||
|
||||
.knowledge-category-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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 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-entry-drag-handle:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.knowledge-entry-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* 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-entry-item.expanded .knowledge-entry-details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.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-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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MOBILE KATEGORIEN (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;
|
||||
}
|
||||
|
||||
.knowledge-mobile-categories::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.knowledge-mobile-chip {
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ATTACHMENTS (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);
|
||||
padding: var(--spacing-xs) var(--spacing-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-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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
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: 220px 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-preview {
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
@ -25,6 +25,7 @@
|
||||
<link rel="stylesheet" href="css/proposals.css">
|
||||
<link rel="stylesheet" href="css/notifications.css">
|
||||
<link rel="stylesheet" href="css/gitea.css">
|
||||
<link rel="stylesheet" href="css/knowledge.css">
|
||||
<link rel="stylesheet" href="css/responsive.css">
|
||||
|
||||
<!-- Favicon -->
|
||||
@ -235,6 +236,7 @@
|
||||
<button class="view-tab" data-view="calendar">Kalender</button>
|
||||
<button class="view-tab" data-view="proposals">Genehmigung</button>
|
||||
<button class="view-tab" data-view="gitea">Gitea</button>
|
||||
<button class="view-tab" data-view="knowledge">Wissen</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@ -886,6 +888,100 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Knowledge View (Wissensmanagement) -->
|
||||
<div id="view-knowledge" class="view view-knowledge hidden">
|
||||
|
||||
<!-- Mobile: Kategorien als horizontale Chips -->
|
||||
<div id="knowledge-mobile-categories" class="knowledge-mobile-categories">
|
||||
<!-- Wird dynamisch befüllt -->
|
||||
</div>
|
||||
|
||||
<!-- Haupt-Layout: Sidebar + Main -->
|
||||
<div class="knowledge-layout">
|
||||
|
||||
<!-- Sidebar (links) - Kategorien -->
|
||||
<aside class="knowledge-sidebar">
|
||||
<div class="knowledge-sidebar-header">
|
||||
<h3 class="knowledge-sidebar-title">Kategorien</h3>
|
||||
<button id="btn-new-category" class="btn btn-primary btn-sm">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||
Neu
|
||||
</button>
|
||||
</div>
|
||||
<div id="knowledge-categories" class="knowledge-category-list">
|
||||
<!-- Kategorien werden dynamisch geladen -->
|
||||
</div>
|
||||
<div id="knowledge-categories-empty" class="knowledge-sidebar-empty hidden">
|
||||
<svg viewBox="0 0 24 24" width="48" height="48">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
<p>Keine Kategorien</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Hauptbereich (rechts) - Einträge -->
|
||||
<main class="knowledge-main">
|
||||
<div class="knowledge-main-header">
|
||||
<h2 id="knowledge-category-title" class="knowledge-main-title">Kategorie wählen</h2>
|
||||
<button id="btn-new-entry" class="btn btn-primary" disabled>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||
Neuer Eintrag
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Einträge-Liste -->
|
||||
<div id="knowledge-entries" class="knowledge-entry-list">
|
||||
<!-- Einträge werden dynamisch geladen -->
|
||||
</div>
|
||||
|
||||
<!-- Leerer Zustand: Keine Kategorie ausgewählt -->
|
||||
<div id="knowledge-no-selection" class="knowledge-empty">
|
||||
<svg viewBox="0 0 24 24" width="64" height="64">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
<h3>Kategorie auswählen</h3>
|
||||
<p>Wählen Sie links eine Kategorie aus, um Einträge anzuzeigen.</p>
|
||||
</div>
|
||||
|
||||
<!-- Leerer Zustand: Keine Einträge -->
|
||||
<div id="knowledge-entries-empty" class="knowledge-empty hidden">
|
||||
<svg viewBox="0 0 24 24" width="64" height="64">
|
||||
<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 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
<h3>Keine Einträge</h3>
|
||||
<p>Erstellen Sie den ersten Eintrag in dieser Kategorie.</p>
|
||||
</div>
|
||||
|
||||
<!-- Suchergebnisse -->
|
||||
<div id="knowledge-search-results" class="knowledge-search-results hidden">
|
||||
<div class="knowledge-search-header">
|
||||
<button id="btn-clear-search" class="btn btn-text">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M6 18L18 6M6 6l12 12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||
Suche beenden
|
||||
</button>
|
||||
<span class="knowledge-search-info">Ergebnisse für "<span id="knowledge-search-query"></span>"</span>
|
||||
</div>
|
||||
<div id="knowledge-search-list" class="knowledge-entry-list">
|
||||
<!-- Suchergebnisse werden hier angezeigt -->
|
||||
</div>
|
||||
<div id="knowledge-search-empty" class="knowledge-empty hidden">
|
||||
<svg viewBox="0 0 24 24" width="64" height="64">
|
||||
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
<h3>Keine Ergebnisse</h3>
|
||||
<p>Keine Einträge gefunden.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -1479,6 +1575,339 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Knowledge Category Modal -->
|
||||
<div id="knowledge-category-modal" class="modal modal-small hidden">
|
||||
<div class="modal-header">
|
||||
<h2 id="knowledge-category-modal-title">Neue Kategorie</h2>
|
||||
<button class="modal-close" data-close-modal>×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="knowledge-category-form">
|
||||
<input type="hidden" id="knowledge-category-id">
|
||||
<div class="form-group">
|
||||
<label for="knowledge-category-name">Name *</label>
|
||||
<input type="text" id="knowledge-category-name" required maxlength="100" placeholder="z.B. Technik">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="knowledge-category-description">Beschreibung</label>
|
||||
<textarea id="knowledge-category-description" rows="2" placeholder="Optionale Beschreibung..."></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="knowledge-category-color">Farbe</label>
|
||||
<input type="color" id="knowledge-category-color" value="#3B82F6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Icon</label>
|
||||
<div class="icon-picker-container">
|
||||
<div class="icon-picker-preview" id="icon-picker-preview">
|
||||
<span class="icon-preview-emoji" id="icon-preview-emoji">📁</span>
|
||||
<span class="icon-preview-label">Klicken zum Ändern</span>
|
||||
</div>
|
||||
<input type="hidden" id="knowledge-category-icon" value="📁">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Icon Picker Grid -->
|
||||
<div class="icon-picker-section" id="icon-picker-section">
|
||||
<div class="icon-picker-tabs">
|
||||
<button type="button" class="icon-tab active" data-tab="tech">💻 Technik</button>
|
||||
<button type="button" class="icon-tab" data-tab="network">🌐 Netzwerk</button>
|
||||
<button type="button" class="icon-tab" data-tab="office">📋 Büro</button>
|
||||
<button type="button" class="icon-tab" data-tab="finance">💰 Finanzen</button>
|
||||
<button type="button" class="icon-tab" data-tab="home">🏠 Haus</button>
|
||||
<button type="button" class="icon-tab" data-tab="misc">⭐ Sonstige</button>
|
||||
</div>
|
||||
|
||||
<!-- Technik Icons -->
|
||||
<div class="icon-grid" data-tab-content="tech">
|
||||
<button type="button" class="icon-btn" data-icon="💻">💻</button>
|
||||
<button type="button" class="icon-btn" data-icon="🖥️">🖥️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🖨️">🖨️</button>
|
||||
<button type="button" class="icon-btn" data-icon="⌨️">⌨️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🖱️">🖱️</button>
|
||||
<button type="button" class="icon-btn" data-icon="💾">💾</button>
|
||||
<button type="button" class="icon-btn" data-icon="💿">💿</button>
|
||||
<button type="button" class="icon-btn" data-icon="📀">📀</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔌">🔌</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔋">🔋</button>
|
||||
<button type="button" class="icon-btn" data-icon="📱">📱</button>
|
||||
<button type="button" class="icon-btn" data-icon="📲">📲</button>
|
||||
<button type="button" class="icon-btn" data-icon="☎️">☎️</button>
|
||||
<button type="button" class="icon-btn" data-icon="📞">📞</button>
|
||||
<button type="button" class="icon-btn" data-icon="📟">📟</button>
|
||||
<button type="button" class="icon-btn" data-icon="📠">📠</button>
|
||||
<button type="button" class="icon-btn" data-icon="📺">📺</button>
|
||||
<button type="button" class="icon-btn" data-icon="📷">📷</button>
|
||||
<button type="button" class="icon-btn" data-icon="📹">📹</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎥">🎥</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔦">🔦</button>
|
||||
<button type="button" class="icon-btn" data-icon="💡">💡</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔧">🔧</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔨">🔨</button>
|
||||
<button type="button" class="icon-btn" data-icon="⚙️">⚙️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🛠️">🛠️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔩">🔩</button>
|
||||
<button type="button" class="icon-btn" data-icon="⛏️">⛏️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🧰">🧰</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎮">🎮</button>
|
||||
<button type="button" class="icon-btn" data-icon="🕹️">🕹️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎧">🎧</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎤">🎤</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔊">🔊</button>
|
||||
<button type="button" class="icon-btn" data-icon="📡">📡</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔬">🔬</button>
|
||||
</div>
|
||||
|
||||
<!-- Netzwerk Icons -->
|
||||
<div class="icon-grid hidden" data-tab-content="network">
|
||||
<button type="button" class="icon-btn" data-icon="🌐">🌐</button>
|
||||
<button type="button" class="icon-btn" data-icon="🌍">🌍</button>
|
||||
<button type="button" class="icon-btn" data-icon="🌎">🌎</button>
|
||||
<button type="button" class="icon-btn" data-icon="🌏">🌏</button>
|
||||
<button type="button" class="icon-btn" data-icon="📡">📡</button>
|
||||
<button type="button" class="icon-btn" data-icon="📶">📶</button>
|
||||
<button type="button" class="icon-btn" data-icon="🛜">🛜</button>
|
||||
<button type="button" class="icon-btn" data-icon="📤">📤</button>
|
||||
<button type="button" class="icon-btn" data-icon="📥">📥</button>
|
||||
<button type="button" class="icon-btn" data-icon="📧">📧</button>
|
||||
<button type="button" class="icon-btn" data-icon="✉️">✉️</button>
|
||||
<button type="button" class="icon-btn" data-icon="📨">📨</button>
|
||||
<button type="button" class="icon-btn" data-icon="📩">📩</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔗">🔗</button>
|
||||
<button type="button" class="icon-btn" data-icon="⛓️">⛓️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔐">🔐</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔒">🔒</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔓">🔓</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔑">🔑</button>
|
||||
<button type="button" class="icon-btn" data-icon="🗝️">🗝️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🛡️">🛡️</button>
|
||||
<button type="button" class="icon-btn" data-icon="⚡">⚡</button>
|
||||
<button type="button" class="icon-btn" data-icon="☁️">☁️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🖧">🖧</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔄">🔄</button>
|
||||
<button type="button" class="icon-btn" data-icon="♻️">♻️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔃">🔃</button>
|
||||
<button type="button" class="icon-btn" data-icon="🌀">🌀</button>
|
||||
<button type="button" class="icon-btn" data-icon="💠">💠</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔷">🔷</button>
|
||||
</div>
|
||||
|
||||
<!-- Büro Icons -->
|
||||
<div class="icon-grid hidden" data-tab-content="office">
|
||||
<button type="button" class="icon-btn" data-icon="📁">📁</button>
|
||||
<button type="button" class="icon-btn" data-icon="📂">📂</button>
|
||||
<button type="button" class="icon-btn" data-icon="🗂️">🗂️</button>
|
||||
<button type="button" class="icon-btn" data-icon="📋">📋</button>
|
||||
<button type="button" class="icon-btn" data-icon="📄">📄</button>
|
||||
<button type="button" class="icon-btn" data-icon="📃">📃</button>
|
||||
<button type="button" class="icon-btn" data-icon="📑">📑</button>
|
||||
<button type="button" class="icon-btn" data-icon="📊">📊</button>
|
||||
<button type="button" class="icon-btn" data-icon="📈">📈</button>
|
||||
<button type="button" class="icon-btn" data-icon="📉">📉</button>
|
||||
<button type="button" class="icon-btn" data-icon="📝">📝</button>
|
||||
<button type="button" class="icon-btn" data-icon="✏️">✏️</button>
|
||||
<button type="button" class="icon-btn" data-icon="✒️">✒️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🖊️">🖊️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🖋️">🖋️</button>
|
||||
<button type="button" class="icon-btn" data-icon="📌">📌</button>
|
||||
<button type="button" class="icon-btn" data-icon="📍">📍</button>
|
||||
<button type="button" class="icon-btn" data-icon="📎">📎</button>
|
||||
<button type="button" class="icon-btn" data-icon="🖇️">🖇️</button>
|
||||
<button type="button" class="icon-btn" data-icon="📏">📏</button>
|
||||
<button type="button" class="icon-btn" data-icon="📐">📐</button>
|
||||
<button type="button" class="icon-btn" data-icon="✂️">✂️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🗃️">🗃️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🗄️">🗄️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🗑️">🗑️</button>
|
||||
<button type="button" class="icon-btn" data-icon="📆">📆</button>
|
||||
<button type="button" class="icon-btn" data-icon="📅">📅</button>
|
||||
<button type="button" class="icon-btn" data-icon="🗓️">🗓️</button>
|
||||
<button type="button" class="icon-btn" data-icon="⏰">⏰</button>
|
||||
<button type="button" class="icon-btn" data-icon="🕐">🕐</button>
|
||||
<button type="button" class="icon-btn" data-icon="📰">📰</button>
|
||||
<button type="button" class="icon-btn" data-icon="🗞️">🗞️</button>
|
||||
<button type="button" class="icon-btn" data-icon="📓">📓</button>
|
||||
<button type="button" class="icon-btn" data-icon="📔">📔</button>
|
||||
<button type="button" class="icon-btn" data-icon="📒">📒</button>
|
||||
<button type="button" class="icon-btn" data-icon="📕">📕</button>
|
||||
<button type="button" class="icon-btn" data-icon="📗">📗</button>
|
||||
<button type="button" class="icon-btn" data-icon="📘">📘</button>
|
||||
<button type="button" class="icon-btn" data-icon="📙">📙</button>
|
||||
<button type="button" class="icon-btn" data-icon="📚">📚</button>
|
||||
</div>
|
||||
|
||||
<!-- Finanzen Icons -->
|
||||
<div class="icon-grid hidden" data-tab-content="finance">
|
||||
<button type="button" class="icon-btn" data-icon="💰">💰</button>
|
||||
<button type="button" class="icon-btn" data-icon="💵">💵</button>
|
||||
<button type="button" class="icon-btn" data-icon="💴">💴</button>
|
||||
<button type="button" class="icon-btn" data-icon="💶">💶</button>
|
||||
<button type="button" class="icon-btn" data-icon="💷">💷</button>
|
||||
<button type="button" class="icon-btn" data-icon="💸">💸</button>
|
||||
<button type="button" class="icon-btn" data-icon="💳">💳</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏦">🏦</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏧">🏧</button>
|
||||
<button type="button" class="icon-btn" data-icon="💹">💹</button>
|
||||
<button type="button" class="icon-btn" data-icon="🧾">🧾</button>
|
||||
<button type="button" class="icon-btn" data-icon="💲">💲</button>
|
||||
<button type="button" class="icon-btn" data-icon="💱">💱</button>
|
||||
<button type="button" class="icon-btn" data-icon="🪙">🪙</button>
|
||||
<button type="button" class="icon-btn" data-icon="💎">💎</button>
|
||||
<button type="button" class="icon-btn" data-icon="⚖️">⚖️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏛️">🏛️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏢">🏢</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏪">🏪</button>
|
||||
<button type="button" class="icon-btn" data-icon="🛒">🛒</button>
|
||||
<button type="button" class="icon-btn" data-icon="🛍️">🛍️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎁">🎁</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎫">🎫</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎟️">🎟️</button>
|
||||
</div>
|
||||
|
||||
<!-- Haus/Wohnen Icons -->
|
||||
<div class="icon-grid hidden" data-tab-content="home">
|
||||
<button type="button" class="icon-btn" data-icon="🏠">🏠</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏡">🏡</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏘️">🏘️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏚️">🏚️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🚪">🚪</button>
|
||||
<button type="button" class="icon-btn" data-icon="🪟">🪟</button>
|
||||
<button type="button" class="icon-btn" data-icon="🛏️">🛏️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🛋️">🛋️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🪑">🪑</button>
|
||||
<button type="button" class="icon-btn" data-icon="🚿">🚿</button>
|
||||
<button type="button" class="icon-btn" data-icon="🛁">🛁</button>
|
||||
<button type="button" class="icon-btn" data-icon="🚽">🚽</button>
|
||||
<button type="button" class="icon-btn" data-icon="🧹">🧹</button>
|
||||
<button type="button" class="icon-btn" data-icon="🧺">🧺</button>
|
||||
<button type="button" class="icon-btn" data-icon="🧴">🧴</button>
|
||||
<button type="button" class="icon-btn" data-icon="🪥">🪥</button>
|
||||
<button type="button" class="icon-btn" data-icon="🍳">🍳</button>
|
||||
<button type="button" class="icon-btn" data-icon="🍴">🍴</button>
|
||||
<button type="button" class="icon-btn" data-icon="🥄">🥄</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔪">🔪</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏺">🏺</button>
|
||||
<button type="button" class="icon-btn" data-icon="🌡️">🌡️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🌿">🌿</button>
|
||||
<button type="button" class="icon-btn" data-icon="🪴">🪴</button>
|
||||
<button type="button" class="icon-btn" data-icon="🚗">🚗</button>
|
||||
<button type="button" class="icon-btn" data-icon="🚙">🚙</button>
|
||||
<button type="button" class="icon-btn" data-icon="🚕">🚕</button>
|
||||
<button type="button" class="icon-btn" data-icon="🛵">🛵</button>
|
||||
<button type="button" class="icon-btn" data-icon="🚲">🚲</button>
|
||||
<button type="button" class="icon-btn" data-icon="⛽">⛽</button>
|
||||
</div>
|
||||
|
||||
<!-- Sonstige Icons -->
|
||||
<div class="icon-grid hidden" data-tab-content="misc">
|
||||
<button type="button" class="icon-btn" data-icon="⭐">⭐</button>
|
||||
<button type="button" class="icon-btn" data-icon="🌟">🌟</button>
|
||||
<button type="button" class="icon-btn" data-icon="✨">✨</button>
|
||||
<button type="button" class="icon-btn" data-icon="💫">💫</button>
|
||||
<button type="button" class="icon-btn" data-icon="❤️">❤️</button>
|
||||
<button type="button" class="icon-btn" data-icon="💙">💙</button>
|
||||
<button type="button" class="icon-btn" data-icon="💚">💚</button>
|
||||
<button type="button" class="icon-btn" data-icon="💛">💛</button>
|
||||
<button type="button" class="icon-btn" data-icon="💜">💜</button>
|
||||
<button type="button" class="icon-btn" data-icon="🧡">🧡</button>
|
||||
<button type="button" class="icon-btn" data-icon="✅">✅</button>
|
||||
<button type="button" class="icon-btn" data-icon="❌">❌</button>
|
||||
<button type="button" class="icon-btn" data-icon="⚠️">⚠️</button>
|
||||
<button type="button" class="icon-btn" data-icon="❗">❗</button>
|
||||
<button type="button" class="icon-btn" data-icon="❓">❓</button>
|
||||
<button type="button" class="icon-btn" data-icon="ℹ️">ℹ️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔔">🔔</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔕">🔕</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎯">🎯</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎲">🎲</button>
|
||||
<button type="button" class="icon-btn" data-icon="🧩">🧩</button>
|
||||
<button type="button" class="icon-btn" data-icon="🏆">🏆</button>
|
||||
<button type="button" class="icon-btn" data-icon="🥇">🥇</button>
|
||||
<button type="button" class="icon-btn" data-icon="🥈">🥈</button>
|
||||
<button type="button" class="icon-btn" data-icon="🥉">🥉</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎵">🎵</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎶">🎶</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎬">🎬</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎨">🎨</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎭">🎭</button>
|
||||
<button type="button" class="icon-btn" data-icon="👤">👤</button>
|
||||
<button type="button" class="icon-btn" data-icon="👥">👥</button>
|
||||
<button type="button" class="icon-btn" data-icon="👨💼">👨💼</button>
|
||||
<button type="button" class="icon-btn" data-icon="👩💼">👩💼</button>
|
||||
<button type="button" class="icon-btn" data-icon="👨💻">👨💻</button>
|
||||
<button type="button" class="icon-btn" data-icon="👩💻">👩💻</button>
|
||||
<button type="button" class="icon-btn" data-icon="🧑🔧">🧑🔧</button>
|
||||
<button type="button" class="icon-btn" data-icon="👷">👷</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔴">🔴</button>
|
||||
<button type="button" class="icon-btn" data-icon="🟠">🟠</button>
|
||||
<button type="button" class="icon-btn" data-icon="🟡">🟡</button>
|
||||
<button type="button" class="icon-btn" data-icon="🟢">🟢</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔵">🔵</button>
|
||||
<button type="button" class="icon-btn" data-icon="🟣">🟣</button>
|
||||
<button type="button" class="icon-btn" data-icon="⚪">⚪</button>
|
||||
<button type="button" class="icon-btn" data-icon="⚫">⚫</button>
|
||||
<button type="button" class="icon-btn" data-icon="🟤">🟤</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔶">🔶</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔸">🔸</button>
|
||||
<button type="button" class="icon-btn" data-icon="🔹">🔹</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
||||
<button type="submit" form="knowledge-category-form" class="btn btn-primary" id="btn-save-category">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Knowledge Entry Modal -->
|
||||
<div id="knowledge-entry-modal" class="modal modal-medium hidden">
|
||||
<div class="modal-header">
|
||||
<h2 id="knowledge-entry-modal-title">Neuer Eintrag</h2>
|
||||
<button class="modal-close" data-close-modal>×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="knowledge-entry-form">
|
||||
<input type="hidden" id="knowledge-entry-id">
|
||||
<input type="hidden" id="knowledge-entry-category-id">
|
||||
<div class="form-group">
|
||||
<label for="knowledge-entry-title">Titel *</label>
|
||||
<input type="text" id="knowledge-entry-title" required maxlength="200" placeholder="z.B. Fritzbox Router">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="knowledge-entry-url">Link (URL)</label>
|
||||
<input type="url" id="knowledge-entry-url" placeholder="https://...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="knowledge-entry-notes">Notizen</label>
|
||||
<textarea id="knowledge-entry-notes" rows="5" placeholder="Notizen, Anleitungen, Tipps... (Markdown unterstützt)"></textarea>
|
||||
</div>
|
||||
<!-- Anhänge (nur im Bearbeitungsmodus) -->
|
||||
<div class="form-group" id="knowledge-attachments-section" style="display: none;">
|
||||
<label>Anhänge</label>
|
||||
<div id="knowledge-attachments-container" class="knowledge-attachments-container"></div>
|
||||
<div class="file-upload-area" id="knowledge-file-upload-area">
|
||||
<svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
<span>Dateien hierher ziehen oder <label for="knowledge-file-input" class="file-input-label">auswählen</label></span>
|
||||
<input type="file" id="knowledge-file-input" multiple hidden>
|
||||
<span class="file-hint">Max. 15 MB pro Datei</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer-left">
|
||||
<button type="button" id="btn-delete-entry" class="btn btn-text text-danger hidden">Löschen</button>
|
||||
</div>
|
||||
<div class="modal-footer-right">
|
||||
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
||||
<button type="submit" form="knowledge-entry-form" class="btn btn-primary" id="btn-save-entry">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
|
||||
@ -14,19 +14,23 @@ class ApiClient {
|
||||
|
||||
// Token Management
|
||||
setToken(token) {
|
||||
console.log('[API] setToken:', token ? token.substring(0, 20) + '...' : 'NULL');
|
||||
this.token = token;
|
||||
if (token) {
|
||||
localStorage.setItem('auth_token', token);
|
||||
} else {
|
||||
this.token = null;
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('current_user');
|
||||
}
|
||||
}
|
||||
|
||||
getToken() {
|
||||
if (!this.token) {
|
||||
this.token = localStorage.getItem('auth_token');
|
||||
}
|
||||
return this.token;
|
||||
// IMMER aus localStorage lesen um Synchronisationsprobleme zu vermeiden
|
||||
// (z.B. wenn Token nach Login gesetzt wird während andere Requests laufen)
|
||||
const token = localStorage.getItem('auth_token');
|
||||
this.token = token; // Cache aktualisieren
|
||||
return token;
|
||||
}
|
||||
|
||||
setCsrfToken(token) {
|
||||
@ -39,10 +43,10 @@ class ApiClient {
|
||||
}
|
||||
|
||||
getCsrfToken() {
|
||||
if (!this.csrfToken) {
|
||||
this.csrfToken = sessionStorage.getItem('csrf_token');
|
||||
}
|
||||
return this.csrfToken;
|
||||
// IMMER aus sessionStorage lesen um Synchronisationsprobleme zu vermeiden
|
||||
const token = sessionStorage.getItem('csrf_token');
|
||||
this.csrfToken = token; // Cache aktualisieren
|
||||
return token;
|
||||
}
|
||||
|
||||
// Base Request Method
|
||||
@ -56,6 +60,7 @@ class ApiClient {
|
||||
|
||||
// Add auth token
|
||||
const token = this.getToken();
|
||||
console.log('[API] Request:', endpoint, 'Token:', token ? token.substring(0, 20) + '...' : 'NULL');
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
@ -98,8 +103,22 @@ class ApiClient {
|
||||
|
||||
// Handle 401 Unauthorized
|
||||
if (response.status === 401) {
|
||||
this.setToken(null);
|
||||
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||
// Token der für diesen Request verwendet wurde
|
||||
const requestToken = token;
|
||||
const currentToken = localStorage.getItem('auth_token');
|
||||
|
||||
console.log('[API] 401 received for:', endpoint);
|
||||
console.log('[API] Request token:', requestToken ? requestToken.substring(0, 20) + '...' : 'NULL');
|
||||
console.log('[API] Current token:', currentToken ? currentToken.substring(0, 20) + '...' : 'NULL');
|
||||
|
||||
// Nur ausloggen wenn der Token der gleiche ist (kein neuer Login in der Zwischenzeit)
|
||||
if (!currentToken || currentToken === requestToken) {
|
||||
console.log('[API] Token invalid, triggering logout');
|
||||
this.setToken(null);
|
||||
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||
} else {
|
||||
console.log('[API] 401 ignored - new login occurred while request was in flight');
|
||||
}
|
||||
throw new ApiError('Sitzung abgelaufen', 401);
|
||||
}
|
||||
|
||||
@ -274,7 +293,9 @@ class ApiClient {
|
||||
// =====================
|
||||
|
||||
async login(username, password) {
|
||||
console.log('[API] login() called');
|
||||
const response = await this.post('/auth/login', { username, password });
|
||||
console.log('[API] login() response:', response ? 'OK' : 'NULL', 'token:', response?.token ? 'EXISTS' : 'MISSING');
|
||||
this.setToken(response.token);
|
||||
// Store CSRF token from login response
|
||||
if (response.csrfToken) {
|
||||
@ -977,6 +998,79 @@ class ApiClient {
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
// =====================
|
||||
// KNOWLEDGE ENDPOINTS (Wissensmanagement)
|
||||
// =====================
|
||||
|
||||
// Kategorien
|
||||
async getKnowledgeCategories() {
|
||||
return this.get('/knowledge/categories');
|
||||
}
|
||||
|
||||
async createKnowledgeCategory(data) {
|
||||
return this.post('/knowledge/categories', data);
|
||||
}
|
||||
|
||||
async updateKnowledgeCategory(id, data) {
|
||||
return this.put(`/knowledge/categories/${id}`, data);
|
||||
}
|
||||
|
||||
async deleteKnowledgeCategory(id) {
|
||||
return this.delete(`/knowledge/categories/${id}`);
|
||||
}
|
||||
|
||||
async updateKnowledgeCategoryPosition(id, newPosition) {
|
||||
return this.put(`/knowledge/categories/${id}/position`, { newPosition });
|
||||
}
|
||||
|
||||
// Einträge
|
||||
async getKnowledgeEntries(categoryId = null) {
|
||||
const params = categoryId ? `?categoryId=${categoryId}` : '';
|
||||
return this.get(`/knowledge/entries${params}`);
|
||||
}
|
||||
|
||||
async getKnowledgeEntry(id) {
|
||||
return this.get(`/knowledge/entries/${id}`);
|
||||
}
|
||||
|
||||
async createKnowledgeEntry(data) {
|
||||
return this.post('/knowledge/entries', data);
|
||||
}
|
||||
|
||||
async updateKnowledgeEntry(id, data) {
|
||||
return this.put(`/knowledge/entries/${id}`, data);
|
||||
}
|
||||
|
||||
async deleteKnowledgeEntry(id) {
|
||||
return this.delete(`/knowledge/entries/${id}`);
|
||||
}
|
||||
|
||||
async updateKnowledgeEntryPosition(id, newPosition, newCategoryId = null) {
|
||||
return this.put(`/knowledge/entries/${id}/position`, { newPosition, newCategoryId });
|
||||
}
|
||||
|
||||
// Anhänge
|
||||
async getKnowledgeAttachments(entryId) {
|
||||
return this.get(`/knowledge/attachments/${entryId}`);
|
||||
}
|
||||
|
||||
async uploadKnowledgeAttachment(entryId, file, onProgress) {
|
||||
return this.uploadFile(`/knowledge/attachments/${entryId}`, file, onProgress);
|
||||
}
|
||||
|
||||
async deleteKnowledgeAttachment(id) {
|
||||
return this.delete(`/knowledge/attachments/${id}`);
|
||||
}
|
||||
|
||||
getKnowledgeAttachmentDownloadUrl(id) {
|
||||
return `${this.baseUrl}/knowledge/attachments/download/${id}`;
|
||||
}
|
||||
|
||||
// Suche
|
||||
async searchKnowledge(query) {
|
||||
return this.get(`/knowledge/search?q=${encodeURIComponent(query)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom API Error Class
|
||||
|
||||
@ -20,6 +20,7 @@ import adminManager from './admin.js';
|
||||
import proposalsManager from './proposals.js';
|
||||
import notificationManager from './notifications.js';
|
||||
import giteaManager from './gitea.js';
|
||||
import knowledgeManager from './knowledge.js';
|
||||
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
|
||||
|
||||
class App {
|
||||
@ -79,6 +80,9 @@ class App {
|
||||
// Initialize gitea manager
|
||||
await giteaManager.init();
|
||||
|
||||
// Initialize knowledge manager
|
||||
await knowledgeManager.init();
|
||||
|
||||
// Update UI
|
||||
this.updateUserMenu();
|
||||
}
|
||||
@ -596,6 +600,18 @@ class App {
|
||||
v.classList.toggle('hidden', !isActive);
|
||||
});
|
||||
|
||||
// Clear search field when switching views
|
||||
const searchInput = $('#search-input');
|
||||
if (searchInput && searchInput.value) {
|
||||
searchInput.value = '';
|
||||
store.setFilter('search', '');
|
||||
store.setState({ searchResultIds: [] }, 'CLEAR_SEARCH_RESULTS');
|
||||
proposalsManager.setSearchQuery('');
|
||||
knowledgeManager.setSearchQuery('');
|
||||
$('#search-clear')?.classList.add('hidden');
|
||||
$('.search-container')?.classList.remove('has-search');
|
||||
}
|
||||
|
||||
// Load proposals when switching to proposals view - reset to active (non-archived)
|
||||
if (view === 'proposals') {
|
||||
proposalsManager.resetToActiveView();
|
||||
@ -607,6 +623,13 @@ class App {
|
||||
} else {
|
||||
giteaManager.hide();
|
||||
}
|
||||
|
||||
// Show/hide knowledge manager
|
||||
if (view === 'knowledge') {
|
||||
knowledgeManager.show();
|
||||
} else {
|
||||
knowledgeManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
@ -823,8 +846,9 @@ class App {
|
||||
updateSearchUI('');
|
||||
searchInput.focus();
|
||||
|
||||
// Clear proposals search as well
|
||||
// Clear view-specific search
|
||||
proposalsManager.setSearchQuery('');
|
||||
knowledgeManager.setSearchQuery('');
|
||||
|
||||
// Cancel any pending server search
|
||||
if (searchAbortController) {
|
||||
@ -897,6 +921,9 @@ class App {
|
||||
if (currentView === 'proposals') {
|
||||
// Search proposals only
|
||||
proposalsManager.setSearchQuery(value);
|
||||
} else if (currentView === 'knowledge') {
|
||||
// Search knowledge base
|
||||
knowledgeManager.setSearchQuery(value);
|
||||
} else {
|
||||
// Immediate client-side filtering for tasks
|
||||
store.setFilter('search', value);
|
||||
|
||||
@ -19,11 +19,14 @@ class AuthManager {
|
||||
// Initialize authentication state
|
||||
async init() {
|
||||
const token = api.getToken();
|
||||
console.log('[Auth] init() - Token exists:', !!token);
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
// Verify token by making a request
|
||||
console.log('[Auth] Verifying token...');
|
||||
const users = await api.getUsers();
|
||||
console.log('[Auth] Token valid, users loaded');
|
||||
this.isAuthenticated = true;
|
||||
|
||||
// Get current user from stored data
|
||||
@ -35,11 +38,13 @@ class AuthManager {
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Token invalid
|
||||
console.log('[Auth] Token invalid, logging out');
|
||||
this.logout();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Auth] No token found');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -471,8 +476,18 @@ class SessionTimerHandler {
|
||||
}
|
||||
}
|
||||
} else if (response.status === 401) {
|
||||
// Token ungültig - ausloggen
|
||||
this.auth.logout();
|
||||
// Token ungültig - aber nur ausloggen wenn kein neuer Login stattfand
|
||||
// (Race-Condition: Alter Refresh-Request kann 401 zurückgeben nachdem
|
||||
// ein neuer Login erfolgreich war)
|
||||
const currentToken = localStorage.getItem('auth_token');
|
||||
if (currentToken === token) {
|
||||
// Gleicher Token, wirklich ungültig, ausloggen
|
||||
console.log('[Auth] Refresh returned 401, logging out');
|
||||
this.auth.logout();
|
||||
} else {
|
||||
// Token hat sich geändert (neuer Login oder bereits ausgeloggt)
|
||||
console.log('[Auth] Refresh 401 ignored - token changed (new login occurred)');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session refresh error:', error);
|
||||
|
||||
1189
frontend/js/knowledge.js
Normale Datei
1189
frontend/js/knowledge.js
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@ -21,7 +21,9 @@ class SyncManager {
|
||||
|
||||
// Initialize Socket.io connection
|
||||
async connect() {
|
||||
if (this.socket?.connected) {
|
||||
// Verhindere doppelte Verbindungen (auch während des Verbindungsaufbaus)
|
||||
if (this.socket) {
|
||||
console.log('[Sync] Socket already exists, skipping connect');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -102,8 +104,15 @@ class SyncManager {
|
||||
console.error('[Sync] Socket error:', error);
|
||||
|
||||
if (error.type === 'auth') {
|
||||
// Auth failed, logout
|
||||
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||
// Nur ausloggen wenn wir wirklich nicht eingeloggt sind
|
||||
// (verhindert Logout durch alte Socket-Verbindungen nach neuem Login)
|
||||
const currentToken = localStorage.getItem('auth_token');
|
||||
if (!currentToken) {
|
||||
console.log('[Sync] Auth error and no token, triggering logout');
|
||||
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||
} else {
|
||||
console.log('[Sync] Auth error ignored - new login occurred');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -546,9 +555,8 @@ class SyncManager {
|
||||
const syncManager = new SyncManager();
|
||||
|
||||
// Listen for auth events
|
||||
window.addEventListener('auth:login', () => {
|
||||
syncManager.connect();
|
||||
});
|
||||
// Hinweis: syncManager.connect() wird NICHT hier aufgerufen,
|
||||
// sondern in app.js initializeApp() um doppelte Verbindungen zu vermeiden
|
||||
|
||||
window.addEventListener('auth:logout', () => {
|
||||
syncManager.disconnect();
|
||||
|
||||
@ -1321,7 +1321,8 @@ class TaskModalManager {
|
||||
try {
|
||||
const projectId = store.get('currentProjectId');
|
||||
const subtask = await api.createSubtask(projectId, this.taskId, { title });
|
||||
this.subtasks.push(subtask);
|
||||
// Neue Subtask an erster Stelle einfügen
|
||||
this.subtasks.unshift(subtask);
|
||||
this.renderSubtasks();
|
||||
input.value = '';
|
||||
|
||||
@ -1331,8 +1332,8 @@ class TaskModalManager {
|
||||
this.showError('Fehler beim Hinzufügen');
|
||||
}
|
||||
} else {
|
||||
// For new tasks, store locally
|
||||
this.subtasks.push({
|
||||
// For new tasks, store locally - an erster Stelle
|
||||
this.subtasks.unshift({
|
||||
id: generateTempId(),
|
||||
title,
|
||||
completed: false
|
||||
@ -1346,6 +1347,7 @@ class TaskModalManager {
|
||||
const subtask = this.subtasks.find(s => s.id === subtaskId);
|
||||
if (!subtask) return;
|
||||
|
||||
const wasCompleted = subtask.completed;
|
||||
subtask.completed = !subtask.completed;
|
||||
|
||||
if (this.mode === 'edit' && this.taskId) {
|
||||
@ -1355,10 +1357,26 @@ class TaskModalManager {
|
||||
completed: subtask.completed
|
||||
});
|
||||
|
||||
// Wenn abgehakt: ans Ende der Liste verschieben
|
||||
if (subtask.completed && !wasCompleted) {
|
||||
const currentIndex = this.subtasks.findIndex(s => s.id === subtaskId);
|
||||
const lastPosition = this.subtasks.length - 1;
|
||||
|
||||
if (currentIndex < lastPosition) {
|
||||
// Aus aktueller Position entfernen
|
||||
const [moved] = this.subtasks.splice(currentIndex, 1);
|
||||
// Ans Ende anfügen
|
||||
this.subtasks.push(moved);
|
||||
|
||||
// API-Call für neue Position
|
||||
await api.reorderSubtasks(projectId, this.taskId, subtaskId, lastPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// Update subtask progress in store for immediate board update
|
||||
this.updateSubtaskProgressInStore();
|
||||
} catch (error) {
|
||||
subtask.completed = !subtask.completed;
|
||||
subtask.completed = wasCompleted;
|
||||
this.showError('Fehler beim Aktualisieren');
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* Offline support and caching
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = '138';
|
||||
const CACHE_VERSION = '152';
|
||||
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
|
||||
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
|
||||
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
|
||||
@ -38,11 +38,13 @@ const STATIC_ASSETS = [
|
||||
'/js/proposals.js',
|
||||
'/js/notifications.js',
|
||||
'/js/gitea.js',
|
||||
'/js/knowledge.js',
|
||||
'/css/list.css',
|
||||
'/css/admin.css',
|
||||
'/css/proposals.css',
|
||||
'/css/notifications.css',
|
||||
'/css/gitea.css'
|
||||
'/css/gitea.css',
|
||||
'/css/knowledge.css'
|
||||
];
|
||||
|
||||
// API routes to cache
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren