UI-Redesign: AegisSight Design, Filter-Popover, Header-Umbau

- Session-Timeout auf 60 Minuten erhöht (ACCESS_TOKEN_EXPIRY + SESSION_TIMEOUT)
- AegisSight Light Theme: Gold-Akzent (#C8A851) statt Indigo
- Navigation-Tabs in eigene Zeile unter Header verschoben (HTML-Struktur)
- Filter-Bar durch kompaktes Popover mit Checkboxen ersetzt (Mehrfachauswahl)
- Archiv-Funktion repariert (lädt jetzt per API statt leerem Store)
- Filter-Bugs behoben: Reset-Button ID, Default-Werte, Ohne-Datum-Filter
- Mehrspalten-Layout Feature entfernt
- Online-Status vom Header an User-Avatar verschoben (grüner Punkt)
- Lupen-Icon entfernt
- CLAUDE.md: Docker-Deploy und CSS-Tricks Regeln aktualisiert

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Server Deploy
2026-03-19 18:49:38 +01:00
Ursprung 99a6b7437b
Commit 4bd57d653f
36 geänderte Dateien mit 5027 neuen und 2897 gelöschten Zeilen

Datei anzeigen

@@ -1,6 +1,509 @@
TASKMATE - CHANGELOG TASKMATE - CHANGELOG
==================== ====================
================================================================================
19.03.2026 - v390 - Filter-Buttons in die View-Tabs-Zeile verschoben
================================================================================
- Filter- und Archiv-Buttons aus separater filter-bar in die view-tabs-bar verschoben (rechts)
- Neue CSS-Klasse filter-bar-actions statt filter-bar/filter-bar-left
- Separate filter-bar Zeile entfernt (eine Zeile weniger im Layout)
- filter-popover bleibt in der view-tabs-bar (absolut positioniert)
- Responsive: filter-bar-actions wird bei 768px ausgeblendet
- board.css: view-tabs-bar auf space-between geaendert
- Cache-Version: 390
================================================================================
19.03.2026 - v389 - View-Tabs in eigene Zeile unter dem Header verschoben
================================================================================
- HTML: div.header-center entfernt, nav.view-tabs-bar nach dem Header eingefuegt
- Tabs sind jetzt ein eigenstaendiges Element unter dem Header (sticky)
- board.css: .header-center durch .view-tabs-bar ersetzt, flex-wrap entfernt
- board.css: overflow-x/scrollbar-hide von .view-tabs entfernt
- responsive.css: .header-center Regeln durch .view-tabs-bar ersetzt
- responsive.css: view-tabs-bar wird auf Mobile versteckt (Mobile Menu uebernimmt)
- Cache-Version auf 389 erhoeht
================================================================================
19.03.2026 - v387 - Connection-Status durch Avatar-Online-Punkt ersetzt
================================================================================
- Connection-Status Indikator (div#connection-status) aus dem Header entfernt
- Gruener Online-Punkt als ::after Pseudo-Element am User-Avatar hinzugefuegt
- Punkt wird rot bei Verbindungsverlust (CSS-Klasse .offline auf body)
- sync.js: Store-Subscriber fuer syncStatus setzt .offline Klasse auf body
- Cache-Version: 386 -> 387
================================================================================
19.03.2026 - v386 - Lupen-Icon aus Suchfeld entfernt
================================================================================
- SVG-Element mit class "search-icon" aus dem Header entfernt
- Suchfeld (search-input) und search-container bleiben erhalten
- Cache-Version: 385 -> 386
================================================================================
19.03.2026 - v385 - Filter-Checkboxen statt Dropdowns
- Filter-Popover oeffnet sich jetzt rechts statt links
- Filter-Bar Buttons rechtsbuendig ausgerichtet (justify-content: flex-end)
- Alle Select-Dropdowns durch Checkbox-Listen ersetzt (Mehrfachauswahl moeglich)
- Prioritaet: Hoch/Mittel/Niedrig als Checkboxen
- Faelligkeit: Ueberfaellig/Heute/Diese Woche/Ohne Datum als Checkboxen
- Bearbeiter und Labels werden dynamisch als Checkboxen befuellt
- filterTasks in utils.js unterstuetzt jetzt Array-basierte Filter
- Checkbox-Design im AegisSight-Stil mit Custom-Checkmark
- Geaenderte Dateien: index.html, board.css, app.js, utils.js, sw.js
================================================================================
19.03.2026 - v384 - Filter-Bar kompakt mit Popover
- Filter-Bar umgebaut: statt 4 Dropdowns in einer Zeile jetzt kompakter Filter-Button + Archiv-Button
- Filter oeffnet sich als Popover-Dropdown mit allen 4 Filter-Optionen
- Filter-Button zeigt Anzahl aktiver Filter an ("Filter (2)")
- Aktive Filter werden visuell hervorgehoben (Primary-Farbe)
- Klick ausserhalb des Popovers schliesst es automatisch
- Geaenderte Dateien: index.html, app.js, board.css, sw.js
================================================================================
19.03.2026 - v383 - Filter-System Bugfixes (3 Bugs)
- Bug 1: Reset-Button funktionierte nicht (ID-Mismatch btn-reset-filters vs btn-clear-filters)
- Bug 2: Default-Werte konsistent auf leeren String umgestellt (HTML, Store, Filter-Logik)
- Bug 3: "Ohne Datum" Filter (value=none) zeigt jetzt nur Tasks ohne Faelligkeitsdatum
- Geaenderte Dateien: app.js, store.js, utils.js, sw.js
================================================================================
19.03.2026 - v382 - AegisSight Light Theme implementiert
- CSS-Variablen in variables.css auf AegisSight-Farbpalette umgestellt
- Primary/Accent: Indigo (#4F46E5) ersetzt durch Gold (#C8A851)
- Hintergruende angepasst (bg-main, bg-tertiary, bg-hover, bg-active)
- Info-Farben auf gedaempftes Blaugrau (#7C8DB5) geaendert
- Scrollbar-Farben und Focus-Schatten aktualisiert
- Hardcodierte alte Farbreferenzen in 5 CSS-Dateien ersetzt:
gitea.css, knowledge.css, board.css, coding.css, reminders.css
- Status-Farben (success, warning, error) und Prioritaetsfarben unveraendert
================================================================================
19.03.2026 - v381 - Mehrspalten-Layout Feature komplett entfernt
- Button #btn-toggle-layout aus index.html entfernt
- Alle Layout-Methoden aus board.js entfernt (loadLayoutPreference, saveLayoutPreference, toggleLayout, applyLayoutClass, checkAndApplyDynamicLayout)
- Multi-Column CSS-Regeln aus board.css entfernt
- Resize-Event-Listener und setTimeout-Aufrufe fuer Layout entfernt
================================================================================
19.03.2026 - v380 - Archiv-Modal laedt archivierte Tasks jetzt per API
- Problem: Archiv-Modal war immer leer, weil archivierte Tasks nie vom Backend geladen wurden
- Ursache: openArchiveModal nutzte nur store.get("tasks").filter(t => t.archived), aber der Store enthielt nur aktive Tasks (Backend filtert mit archived=0)
- Loesung: openArchiveModal ruft jetzt api.getTasks(projectId, { archived: true }) auf und laedt archivierte Tasks per API
- renderArchiveList bekommt Tasks als Parameter statt nochmal den Store zu filtern
- restoreTask und deleteArchivedTask laden die Archivliste nach Aktion neu vom Server
- Ladeindikator waehrend API-Anfrage
- Geaenderte Dateien: frontend/js/app.js, frontend/sw.js (v380)
================================================================================
19.03.2026 - v379 - Header-Tabs immer in eigener zweiter Zeile
- Problem: .header-center (Tabs) rutschte bei breitem Fenster neben Avatar in Zeile 1
- Ursache: width:100% + flex-basis:100% reicht nicht, Flex-Container bricht nicht um wenn genug Platz
- Loesung: flex: 0 0 100% (nicht wachsen, nicht schrumpfen, Basis 100%) erzwingt Umbruch zuverlaessig
================================================================================
19.03.2026 - v378 - Search-Container Layout-Fix im Header
- Problem: width:100% im .search-container drueckte die Lupe aus header-right heraus
- Loesung: width:100% ersetzt durch flex:1 1 auto + min-width:150px
- Container nimmt jetzt nur verfuegbaren Platz ein, dehnt sich nicht ueber header-right hinaus
================================================================================
19.03.2026 - v376 - Navigation-Tabs in eigene Header-Zeile verschoben
================================================================================
- Navigation-Tabs werden jetzt in einer separaten zweiten Zeile im Header angezeigt
- Zeile 1: Logo + Projekt + Suche + Online + Timer + Avatar
- Zeile 2: Tabs (volle Breite, zentriert)
- Zeile 3: Filter-Bar (unveraendert)
- Nur CSS-Aenderungen in board.css (kein HTML geaendert)
- Geaenderte Dateien: frontend/css/board.css, frontend/sw.js
================================================================================
31.01.2026 - v374 - Mobile Board komplett ueberarbeitet
================================================================================
## VERBESSERUNG
### Board auf Smartphone deutlich groesser und besser lesbar
- Task-Titel: 14px -> 16px (bzw. 18px auf kleinen Screens)
- Task-Cards: mehr Padding, groessere Labels
- Task-Meta-Infos: 12px -> 14px, groessere Icons
- Priority-Sterne: 12px -> 16px
- Assignee-Avatare: 24px -> 32px
### Filter & Stats-Bar optimiert
- Filter-Selects: groesser (48px Hoehe), volle Breite auf kleinen Screens
- Stats-Bar: groessere Icons (40px), groessere Zahlen
- Horizontales Scrollen fuer Stats wenn noetig
### Week-Strip groesser
- Navigation-Buttons: 28px -> 40px
- Tages-Nummern: 14px -> 18px
### Spalten-Header groesser
- Titel: 14px -> 16px (18px auf kleinen Screens)
- Task-Count Badge: 24px -> 28px
- Mehr Padding
### Add-Task-Button groesser
- Min-Hoehe: 52px (56px auf kleinen Screens)
- Schrift: 14px -> 16px (18px auf kleinen Screens)
### Modal-Optimierungen
- Fullscreen auf sehr kleinen Screens (< 480px)
- Groessere Form-Labels
- Mehr Padding
### Geaenderte Dateien
- frontend/css/mobile.css - Umfangreiche Board-Optimierungen
================================================================================
31.01.2026 - v373 - Mobile UX Optimierung
================================================================================
## VERBESSERUNG
### Mobile Touch & iOS Kompatibilitaet
- Input-Schriftgroesse auf 16px erhoeht (verhindert iOS Zoom bei Focus)
- Modal Close Button auf 44px Touch Target vergroessert
- Dropdown Items mit min-height 44px fuer bessere Touch-Bedienung
- Safe Area Support fuer Mobile Column Indicator (iOS Notch/Home Bar)
- Safe Area Support fuer Toast Container auf Mobile
- Safe Area Support fuer Lightbox Close Button
### Geaenderte Dateien
- frontend/css/components.css - Input font-size, Dropdown min-height
- frontend/css/modal.css - Modal close 44px, Lightbox safe-area
- frontend/css/mobile.css - Column indicator safe-area
- frontend/css/responsive.css - Toast container safe-area
================================================================================
24.01.2026 - v372 - UI: Navigation-Tabs vertikal und zentriert
================================================================================
## VERBESSERUNG
### Vertikale Navigation-Tabs
- Tabs sind jetzt immer vertikal: Icon oben, Text darunter
- Kompakteres Layout, alle 7 Tabs passen besser nebeneinander
- Tabs bleiben bei allen Bildschirmbreiten zentriert (nicht nach rechts)
### Geaenderte Dateien
- frontend/css/board.css - Basis-Styles fuer .view-tab
- frontend/css/responsive.css - justify-content: center hinzugefuegt
================================================================================
24.01.2026 - v369 - Bugfix: Spalte loeschen mit archivierten Aufgaben
================================================================================
## BUG BEHOBEN
### Spalte loeschen funktioniert jetzt korrekt
- Problem: Spalten mit archivierten Aufgaben konnten nicht geloescht werden
- Ursache: Frontend pruefte nur aktive Tasks, Backend zaehlte alle (inkl. archivierte)
- Spalte wanderte nach rechts statt geloescht zu werden
- Loesung: Backend prueft jetzt nur aktive (nicht-archivierte) Tasks
### Archivierte Aufgaben bleiben erhalten
- Archivierte Aufgaben werden NICHT geloescht wenn Spalte geloescht wird
- Stattdessen: column_id wird auf NULL gesetzt
- Bei Wiederherstellung: Spaltenauswahl-Dialog erscheint
- Benutzer waehlt neue Spalte fuer die wiederhergestellte Aufgabe
### Neues Modal: Spaltenauswahl
- Neues Modal "column-select-modal" in index.html
- Event-Handler "column-select:show" in app.js
- Wird verwendet bei Wiederherstellung von Aufgaben ohne gueltige Spalte
================================================================================
23.01.2026 - v368 - Kontakte & Board: UI-Verbesserungen
================================================================================
## KONTAKTE-MODUL
### Filter-Buttons
- Text durch Icons ersetzt (Personen-Icon / Gebaeude-Icon)
- Inline-SVGs fuer bessere Kompatibilitaet
### Icons repariert
- Alle xlink:href Referenzen durch Inline-SVGs ersetzt
- Betroffene Icons: edit, trash, arrow-left, plus, x, mail, phone, users
- Kontaktdetails und Interaktionen nutzen jetzt Inline-SVGs
### Avatar-Darstellung
- Typ-Badges (Person/Institution) komplett entfernt
- Stattdessen Icons im Avatar-Kreis (Personen-Icon / Gebaeude-Icon)
- Weisse Icons auf farbigem Hintergrund
### Caching-Problem behoben
- Cache-Control Headers im Backend hinzugefuegt
- Kontakte werden jetzt immer frisch geladen (kein 304 Not Modified)
### Filter-Zaehlung korrigiert
- Von Server-seitiger zu Client-seitiger Filterung gewechselt
- Alle Kontakte werden einmal geladen, dann lokal gefiltert
- Zaehlung zeigt jetzt korrekte Werte (z.B. 7+5 statt 5+4)
### Neue Kontakte hinzugefuegt
- Barbara Banczyk (LfM)
- Torsten Giebel (LfM)
- Anna Heinzmann (LKA BW)
- Stephanie Kaschka (LKA HH)
- Kilian Bieker (ohne Institution)
- Marc Restemeyer (LAFP NRW)
## BOARD-MODUL
### Spalten-Buttons verbessert
- Edit/Delete-Buttons von absoluter zu Flexbox-Positionierung
- Automatische vertikale Zentrierung
- Keine Ueberlappung mehr mit Spaltentitel
- Lange Titel werden mit "..." abgeschnitten
## TECHNISCH
- Service Worker Cache-Version erhoeht auf v368
================================================================================
23.01.2026 - v362 - Institution: Felder entfernt
================================================================================
## ENTFERNT
- Mitarbeiteranzahl (employee_count) aus Institution entfernt
- Gruendungsjahr (founded_year) aus Institution entfernt
- Steuernummer (tax_number) aus Institution entfernt
- Spalten aus Datenbank entfernt
- Service Worker Cache-Version erhoeht auf v362
================================================================================
23.01.2026 - v361 - Kontakte: Umbenennung Organisation zu Institution
================================================================================
## GEAENDERT
- "Organisation" ueberall zu "Institution" umbenannt
- Service Worker Cache-Version erhoeht auf v361
================================================================================
23.01.2026 - v360 - Kontakte: Zwei Buttons und Umbenennung zu Organisation
================================================================================
## GEAENDERT
- Zwei separate Buttons: "Neuer Kontakt" (Person) und "Neue Organisation"
- "Unternehmen" ueberall zu "Organisation" umbenannt
- Filter-Tab "Unternehmen" -> "Organisationen"
- Formular-Titel passt sich dem Typ an
- Service Worker Cache-Version erhoeht auf v360
================================================================================
22.01.2026 - v359 - Geburtstag-Feld aus Kontakten entfernt
================================================================================
## ENTFERNT
- Geburtstag-Feld aus dem Kontakt-Formular (Person) entfernt
- Geburtstag-Anzeige aus der Kontakt-Detailansicht entfernt
- birth_date Spalte aus der Datenbank entfernt
- birth_date aus Backend-Routes (contacts.js, contacts-extended.js) entfernt
- birth_date aus Migrations-Dateien entfernt
- Service Worker Cache-Version erhoeht auf v359
================================================================================
21.01.2026 - v346 - BUGFIX: Kontakte Event-Binding repariert
================================================================================
## FEHLER BEHOBEN
- "Neuer Kontakt" Button: Event-Binding nach Modal-Schließen repariert
- Modal-Schließen optimiert: Kein komplettes Re-Rendering des Layouts mehr
- Robusteres Event-Binding mit Console-Logging für Debugging
- Service Worker Cache-Version erhöht auf v346
================================================================================
21.01.2026 - BUGFIX: Kontakte Modal-Overlay Fehler behoben
================================================================================
## FEHLER BEHOBEN
- Kontakte-Formular zeigte "undefined" beim Anlegen neuer Kontakte
- Modal-Overlay fehlte beim Kontakt-Formular
- Form-Container hatte keine korrekte Modal-Struktur
- "Neuer Kontakt" Button reagierte nicht - Event-Handling verbessert
- SVG-Icon durch Text-Symbol ersetzt für bessere Kompatibilität
- Modal-CSS-Klasse korrigiert: "visible" statt "show" für korrekte Anzeige
- Z-Index erhöht, um sicherzustellen dass Modal über allem liegt
- Modal-Struktur korrigiert: Overlay und Modal sind jetzt separate Elemente
- Form-Submit Problem behoben: Formular wurde als GET statt POST abgeschickt
- Event-Binding direkt auf Submit-Button statt Form-Submit für bessere Kontrolle
- Validierung für Pflichtfelder (Nachname/Firmenname) hinzugefügt
- Display-Name wird jetzt korrekt im Frontend generiert
- Nach Kontakt-Erstellung werden vollständige Details geladen
- Event-Delegation für Form-Buttons implementiert
- API-Request Syntax korrigiert (object statt separate Parameter)
- Kontakt-Löschung funktioniert jetzt korrekt
- Interaktionen-API ebenfalls korrigiert
- Filter-Sidebar entfernt und in Header-Leiste integriert
- Archiv/Aktiv Filter entfernt - nur noch Alle/Personen/Unternehmen
- Modernes Filter-Button-Design im Header
- Responsive Layout für mobile Geräte
- Service Worker Cache-Version erhöht auf v358
================================================================================
20.01.2026 - UPDATE: Kategoriesystem aus Kontaktmanagement entfernt
================================================================================
## ÄNDERUNGEN
- Kategoriesystem komplett entfernt (Backend und Frontend)
- Vereinfachtes Kontaktmanagement ohne Kategorie-Verknüpfungen
- Saubere UI ohne Kategorie-Filter und -Zuweisungen
================================================================================
20.01.2026 - FEATURE: Kontaktmanagement komplett neu implementiert
================================================================================
## NEUE FUNKTIONEN
- Modernes Kontaktmanagement-Modul (neu entwickelt)
- Unterstützung für Personen und Unternehmen
- Mehrere Kontaktdetails pro Person (E-Mail, Telefon, Adresse)
- Verknüpfung von Personen mit Unternehmen
- Interaktions-Historie (Anrufe, E-Mails, Meetings, Notizen)
- Avatar-Upload für Kontakte
## UI/UX
- Moderne Card-basierte Listenansicht mit alphabetischer Gruppierung
- Sidebar mit Filtern (Alle, Personen, Unternehmen, Aktiv, Archiviert)
- Detailansicht mit allen Kontaktinformationen
- Formular mit Type-Toggle (Person/Unternehmen)
- Dynamische Formularfelder je nach Kontakttyp
- Integration in die globale Suche
## DATENBANK
- Neue optimierte Tabellenstruktur:
- contacts: Haupttabelle für Personen und Unternehmen
- contact_details: Mehrere E-Mails, Telefone etc. pro Kontakt
- contact_categories: Kategorien zur Organisation
- contact_interactions: Interaktions-Historie
- Trigger für automatisches updated_at
- Performance-Indizes für schnelle Abfragen
## TECHNISCHE DETAILS
- Backend: Vollständige REST API in /api/contacts
- Frontend: ES6 Modul contacts.js (ersetzt contacts-basic.js)
- CSS: Komplett neues contacts.css mit responsivem Design
- Cache-Version auf 326 erhöht
================================================================================
19.01.2025 - BUGFIX: User-Dropdown Z-Index korrigiert
================================================================================
## FEHLERBEHEBUNG
- User-Dropdown erscheint jetzt korrekt über anderen Elementen
- Z-Index von 100 auf 300 erhöht (über sticky header mit z-index 200)
- Cache-Version auf 315 erhöht
## TECHNISCHE DETAILS
- Datei: frontend/css/variables.css
- Geändert: --z-dropdown: 300 (vorher 100)
================================================================================
19.01.2025 - FEATURE: Wissen-Modul elegant überarbeitet
================================================================================
## NEUE FUNKTIONEN
- Eleganter Ansichtstyp-Umschalter im Header (Liste/Karten/Kompakt)
- Template-Auswahl direkt im Entry-Modal als Tabs:
- Allgemein: Flexibler Eintrag mit allen Feldern
- Link: Optimiert für Weblinks (URL-Feld erforderlich)
- Dokument: Fokus auf Datei-Anhänge (URL ausgeblendet)
- Notiz: Erweiterte Notizen (URL ausgeblendet, größeres Textfeld)
- Verbessertes Karten-Design mit sauberen SVG-Icons statt Emojis
## UI/UX VERBESSERUNGEN
- View-Switcher nahtlos in Header integriert (TaskMate-Stil)
- Template-Tabs ändern dynamisch die Formular-Felder
- Karten zeigen URL-Domain statt voller URL
- Notizen-Vorschau in Karten auf 150 Zeichen
- View-Präferenz wird gespeichert
- Mobile: View-Switcher ausgeblendet, automatisch Karten
## TECHNISCHE DETAILS
- Template-System ohne separates Modal implementiert
- Dynamische Feld-Sichtbarkeit basierend auf Template-Typ
- Kompakte View-Switcher Komponente
- SVG-Icons für bessere Performance und Konsistenz
## BUGFIXES
✅ Sperrige UI-Elemente entfernt und durch elegante Lösungen ersetzt
✅ Template-Flow intuitiver gestaltet
✅ Visuelles Design konsistent mit TaskMate-Stil
================================================================================
17.01.2026 - FEATURE: Responsive Design-Verbesserungen
================================================================================
## NEUE FUNKTIONEN
- Anwendung passt sich automatisch an Browsergröße an
- Kein horizontales Scrollen mehr nötig
- Navigation zeigt bei mittleren Bildschirmen (1200px) nur Icons
- Tooltips beim Hover über Navigations-Icons
## IMPLEMENTIERUNG
✅ Globale responsive CSS-Regeln hinzugefügt (overflow-x: hidden)
✅ Navigation-Tabs zeigen ab 1200px nur noch Icons
✅ data-tooltip Attribute für alle View-Tabs hinzugefügt
✅ Header-Elemente optimiert für verschiedene Bildschirmgrößen
✅ Project Selector und Search Container werden bei kleineren Bildschirmen schmaler
✅ View-Tabs sind bei Bedarf horizontal scrollbar (ohne sichtbare Scrollbar)
## TECHNISCHE DETAILS
- Neue Media Query für max-width: 1200px
- Touch-optimiertes horizontales Scrollen für View-Tabs
- Minimale Touch-Target-Größe von 44px beibehalten
- Box-sizing: border-box global angewendet
## BUGFIXES
✅ Elemente verschwinden nicht mehr bei kleinen Bildschirmen
✅ Horizontales Scrollen komplett eliminiert
✅ Search Input hat keine feste Breite mehr (max-width statt width)
✅ Project Select nutzt responsive Breiten
✅ Board und Columns nutzen flexible Breiten
✅ Header-Layout für kleine Bildschirme optimiert (keine Wrapping-Probleme mehr)
✅ Mobile Board-Columns nutzen 100% Breite statt calc(100vw - 32px)
================================================================================
11.01.2025 - BUGFIX: Mobile Ansicht für Liste und Kalender repariert
================================================================================
## PROBLEM
Liste und Kalender wurden in der mobilen Ansicht nicht angezeigt
## LÖSUNG
✅ CSS-Konflikt zwischen hidden-Klasse und active-Display behoben
✅ Mobile-spezifische Responsive-Anpassungen für beide Views
✅ Verbesserte Touch-Scrolling für Tabellen in der Listenansicht
✅ Optimierte Kalender-Darstellung auf kleinen Bildschirmen
================================================================================
11.01.2025 - FEATURE: Verbesserte mobile Navigation mit Swipe-Gesten
================================================================================
## NEUE FUNKTIONEN
- Board-View zeigt jetzt nur eine Statuskarte auf einmal
- Swipe links/rechts wechselt zwischen den Statuskarten
- Swipe in der Header-Leiste wechselt zwischen den Views (Board, Liste, etc.)
- Visuelle Indikatoren zeigen aktuelle Spalte und verfügbare Navigationsoptionen
## IMPLEMENTIERUNG
✅ Neues mobile-swipe.js Modul für erweiterte Touch-Gesten
✅ Eine Spalte pro Bildschirm in mobiler Ansicht
✅ Spalten-Indikator mit Punkten und Spaltenname
✅ View-Hints beim Swipen in der Header-Leiste
✅ Responsive Anpassungen für optimale mobile Experience
================================================================================
11.01.2025 - BUGFIX: Wissenseinträge - Benachrichtigungen aktiviert
================================================================================
## PROBLEM
Beim Erstellen neuer Wissenseinträge erhielten andere Nutzer keine Benachrichtigung
## LÖSUNG
✅ Benachrichtigungen werden jetzt als persistent gespeichert
✅ Alle Nutzer erhalten eine Inbox-Benachrichtigung bei neuen Einträgen
✅ Klick auf Benachrichtigung navigiert direkt zum Wissenseintrag
================================================================================ ================================================================================
11.01.2025 - FEATURE: Coding-Modul - Freie Pfad-Eingabe 11.01.2025 - FEATURE: Coding-Modul - Freie Pfad-Eingabe
================================================================================ ================================================================================

940
CLAUDE.md
Datei anzeigen

@@ -1,777 +1,227 @@
# TaskMate - Entwicklerdokumentation # TaskMate - Projekt-Konfiguration
# YAML-Format fuer KI-Assistenten
## ⚠️ WICHTIGER HINWEIS FÜR KI-ASSISTENTEN PROJECT:
Der Anwender hat **KEINE Programmierkenntnisse**. Das bedeutet: name: TaskMate
- **DU übernimmst ALLE technischen Aufgaben vollständig** type: PRODUCTION
- **Erkläre in einfachen Worten**, was du tust und warum url: https://taskmate.aegis-sight.de
- **Frage NIEMALS nach technischen Details** oder Code-Schnipseln container: taskmate
- **Führe ALLE Schritte selbstständig aus** port_external: 3001
- Der Anwender kann nur bestätigen/ablehnen, nicht selbst coden port_internal: 3000
gitea: https://gitea-undso.aegis-sight.de/AegisSight/TaskMate
### Kommunikations-Regeln USER_PROFILE:
**RICHTIG**: "Ich werde jetzt die Benutzeroberfläche anpassen, damit..." programming_skills: NONE
**FALSCH**: "Kannst du mir den Code aus Zeile 42 zeigen?" support_level: FULL_SERVICE
communication: SIMPLE_LANGUAGE
**RICHTIG**: "Ich starte jetzt den Server neu. Das dauert etwa 30 Sekunden." COMMUNICATION_RULES:
**FALSCH**: "Führe bitte folgenden Befehl aus: docker restart..." do:
- "Alle technischen Aufgaben vollstaendig uebernehmen"
- "In einfachen Worten erklaeren was passiert"
- "Alle Schritte selbststaendig ausfuehren"
- "Status-Updates geben: Aenderung abgeschlossen, teste jetzt..."
dont:
- "NIEMALS nach technischen Details fragen"
- "NIEMALS nach Code-Schnipseln fragen"
- "NIEMALS Befehle zum Ausfuehren geben"
- "NIEMALS nach Logs oder Console fragen"
## 🚀 Quick Start CRITICAL_RULES:
1_cache_version: "Nach Frontend-Aenderungen: frontend/sw.js CACHE_VERSION erhoehen"
2_changelog_file: "Bei JEDER Aenderung: CHANGELOG.txt aktualisieren"
3_changelog_db: "Bei JEDER Aenderung: Wissensdatenbank Changelog aktualisieren (siehe WORKFLOWS)"
4_no_toISOString: "NIEMALS toISOString() fuer Datums-Operationen (UTC-Problem!)"
5_realtime_updates: "User darf NIE F5 druecken muessen"
6_docker_restart: "Nach Backend-Aenderungen: docker compose up -d --build (NICHT docker restart - das nutzt das alte Image!)"
7_no_css_tricks: "Layout-Aenderungen immer ueber HTML-Struktur loesen, NIEMALS mit CSS-Tricks wie flex-wrap+order, negative margins oder aehnlichem. Wenn ein Element in eine eigene Zeile soll, muss es ein eigenes HTML-Element sein."
### Wichtigste Befehle PROTECTED_DATA:
```bash project: "AegisSight"
# Docker Container neu starten (nach Backend-Änderungen) description: "Produktiv im Einsatz - NIEMALS loeschen oder aendern"
docker restart taskmate protected_users:
# Container neu bauen (bei Dependency-Änderungen)
docker build -t taskmate . && docker restart taskmate
# Logs anzeigen
docker logs taskmate -f
# Health Check
curl http://localhost:3000/api/health
```
### Kritische Regeln - NIEMALS VERGESSEN! ⚠️
1. **Cache-Version erhöhen** nach Frontend-Änderungen: `frontend/sw.js``CACHE_VERSION`
2. **CHANGELOG.txt** bei JEDER Änderung aktualisieren
3. **Keine `toISOString()`** für Datums-Operationen (UTC-Problem!)
4. **Echtzeit-Updates** - User darf NIE F5 drücken müssen
5. **Docker restart** nach Backend-Änderungen
### Datenschutz & Projektsicherheit 🔐
**ABSOLUT KRITISCH**: Das Projekt "AegisSight" ist produktiv im Einsatz!
- **Projekt "AegisSight" NIEMALS löschen, ändern oder beeinträchtigen**
- **Bestehende Benutzer NIEMALS zurücksetzen, löschen oder verändern**
- **Produktivdaten sind TABU** - keine Testdaten in echte Projekte
- **Keine Datenbank-Resets** ohne explizite Anweisung
- **JEDE Änderung MUSS umkehrbar sein** - Live-System!
- **Backup vor kritischen Änderungen** ist Pflicht
**⚠️ NUTZERDATEN-SCHUTZ - ABSOLUTES VERBOT**:
- **KEINE Änderungen an Nutzerdaten oder Kennwörtern**
- **Geschützte Benutzer (NICHT modifizieren)**:
- admin - admin
- Hendrik (hendrik_gebhardt@gmx.de) - "Hendrik (hendrik_gebhardt@gmx.de)"
- Monami (momohomma@googlemail.com) - "Monami (momohomma@googlemail.com)"
- **Diese Benutzer sind produktiv im Einsatz** forbidden_actions:
- **Keine Passwort-Resets oder Änderungen an diesen Accounts** - "Nutzerdaten aendern"
- **Bei Anmeldeproblemen: Nur Debugging, keine Datenänderung** - "Passwoerter aendern"
- "Datenbank-Resets ohne Anweisung"
- "Testdaten in Produktivdaten"
### Rollback-Strategie für Live-Betrieb COMMANDS:
Bei JEDER Änderung sicherstellen: restart: "docker restart taskmate"
rebuild: "cd /home/claude-dev/TaskMate && docker compose build --no-cache && docker compose up -d"
logs: "docker logs taskmate -f --tail 100"
health: "curl http://localhost:3000/api/health"
copy_frontend: "docker cp frontend/js/ taskmate:/app/public/js/"
copy_backend: "docker cp backend/routes/ taskmate:/app/routes/"
```bash FILE_STRUCTURE:
# 1. Vor Änderungen - Backup erstellen frontend:
cp data/taskmate.db data/taskmate.db.backup-$(date +%Y%m%d-%H%M%S) core:
docker commit taskmate taskmate-backup-$(date +%Y%m%d-%H%M%S) - "frontend/js/app.js - Hauptanwendung & View-Controller"
- "frontend/js/store.js - State Management (Pub-Sub)"
- "frontend/js/api.js - API Client mit Auth/CSRF"
- "frontend/js/auth.js - Login/Token-Verwaltung"
- "frontend/js/sync.js - Socket.io Echtzeit"
views:
- "frontend/js/board.js - Kanban-Board mit Drag&Drop"
- "frontend/js/calendar.js - Kalender (Monat/Woche)"
- "frontend/js/list.js - Listenansicht"
- "frontend/js/dashboard.js - Statistik-Dashboard"
- "frontend/js/proposals.js - Vorschlagssystem"
- "frontend/js/knowledge.js - Wissensdatenbank"
- "frontend/js/admin.js - Benutzerverwaltung"
important:
- "frontend/sw.js - Service Worker mit CACHE_VERSION"
- "frontend/index.html - Haupt-HTML"
backend:
core:
- "backend/server.js - Express Server mit Socket.io"
- "backend/database.js - Datenbankschema (20+ Tabellen)"
routes:
- "/api/auth - Login/Logout"
- "/api/tasks - Aufgaben CRUD"
- "/api/projects - Projekte"
- "/api/columns - Kanban-Spalten"
- "/api/comments - Kommentare"
- "/api/files - Datei-Upload"
- "/api/proposals - Vorschlaege"
- "/api/knowledge - Wissensdatenbank"
other:
- "CHANGELOG.txt - Aenderungsprotokoll"
# 2. Bei Problemen - Rollback durchführen DATABASE:
docker stop taskmate type: SQLite
docker run -d --name taskmate-temp taskmate-backup-TIMESTAMP path: "data/taskmate.db"
# Nach Test: docker rm -f taskmate && docker rename taskmate-temp taskmate tables:
- users
- projects
- columns
- tasks
- task_labels
- task_assignees
- comments
- attachments
- proposals
- notifications
- knowledge_categories
- knowledge_entries
# 3. Code-Rollback via Git CODE_PATTERNS:
git stash # Aktuelle Änderungen sichern date_formatting:
git checkout HEAD~1 # Zum vorherigen Commit correct: |
docker build -t taskmate . && docker restart taskmate
```
**Änderungs-Workflow für Live-System:**
1. Backup von Datenbank UND Docker-Image
2. Kleine, inkrementelle Änderungen
3. Sofortiger Test nach jeder Änderung
4. Rollback-Plan dokumentieren
5. Bei kritischen Änderungen: Wartungsfenster planen
### Arbeitsweise mit nicht-technischem Anwender
**Der Anwender versteht KEIN Coding!** Daher:
1. **Vollständige Übernahme**: Du führst ALLE technischen Schritte durch
2. **Einfache Erklärungen**: "Ich passe jetzt X an, damit Y funktioniert"
3. **Status-Updates**: "Änderung abgeschlossen, teste jetzt..."
4. **Keine technischen Fragen**: Niemals nach Code, Logs oder Befehlen fragen
5. **Proaktives Handeln**: Selbstständig debuggen und lösen
**Beispiel-Kommunikation:**
- ✅ "Ich habe ein Problem gefunden und behebe es jetzt..."
- ❌ "Welche Version von Node.js ist installiert?"
- ✅ "Die Änderung ist fertig. Bitte die Seite neu laden und testen."
- ❌ "Kannst du mal in die Console schauen?"
### Zugriff & Domains
- **Frontend**: https://taskmate.aegis-sight.de
- **Lokaler Port**: 3001 → Container Port 3000
- **Gitea**: https://gitea-undso.aegis-sight.de/AegisSight/TaskMate
- **Gitea-Token**: Siehe `.env` Datei (NIEMALS in Git einchecken!)
## 📁 Projektstruktur
### Wichtige Dateien - Hier starten!
```
frontend/js/app.js # Hauptanwendung & View-Controller
backend/server.js # Express Server mit Socket.io
backend/database.js # Datenbankschema (20+ Tabellen)
frontend/js/store.js # State Management (Pub-Sub)
frontend/js/api.js # API Client mit Auth/CSRF
frontend/sw.js # Service Worker → CACHE_VERSION!
CHANGELOG.txt # Änderungsprotokoll
```
### Frontend-Module (22 Dateien)
```
# Core
app.js # Hauptanwendung
store.js # State Management
api.js # Backend-Kommunikation
auth.js # Login/Token-Verwaltung
utils.js # Hilfsfunktionen
# Views
board.js # Kanban-Board mit Drag&Drop
calendar.js # Kalender (Monat/Woche)
list.js # Listenansicht
dashboard.js # Statistik-Dashboard
proposals.js # Vorschlagssystem
knowledge.js # Wissensdatenbank
admin.js # Benutzerverwaltung
# Features
task-modal.js # Aufgaben-Details
notifications.js # Benachrichtigungen
sync.js # Socket.io Echtzeit
mobile.js # Mobile Features
```
### Backend-Routes (22 Module)
```
/api/auth # Login/Logout
/api/tasks # Aufgaben CRUD
/api/projects # Projekte
/api/columns # Kanban-Spalten
/api/comments # Kommentare
/api/files # Datei-Upload
/api/proposals # Vorschläge
/api/gitea # Git-Integration
/api/knowledge # Wissensdatenbank
```
## 🔧 Entwicklung
### Neue View/Ansicht hinzufügen
```javascript
// 1. Datei erstellen: frontend/js/myview.js
export function initMyView() {
// KRITISCH: Echtzeit-Updates registrieren!
store.subscribe('tasks', updateView);
window.addEventListener('app:refresh', updateView);
function updateView() {
// UI aktualisieren
}
}
// 2. In index.html einbinden
<script type="module" src="js/myview.js"></script>
// 3. Navigation erweitern in navigation.js
```
### Neue API-Route erstellen
```javascript
// 1. Route-Datei: backend/routes/myroute.js
const router = require('express').Router();
const auth = require('../middleware/auth');
router.get('/', auth, (req, res) => {
// Implementation
});
module.exports = router;
// 2. In server.js registrieren
app.use('/api/myroute', require('./routes/myroute'));
// 3. Frontend API-Call in api.js
async myRouteCall() {
return this.request('/api/myroute');
}
```
### Datums-Formatierung (RICHTIG!)
```javascript
// ✅ RICHTIG - Lokale Zeit
const year = date.getFullYear(); const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`; const dateStr = `${year}-${month}-${day}`;
wrong: "date.toISOString().split('T')[0] // NIEMALS - UTC Problem!"
// ❌ FALSCH - UTC-Konvertierung toast_messages:
const dateStr = date.toISOString().split('T')[0]; // NIEMALS! correct: |
```
### Echtzeit-Updates implementieren
```javascript
// PFLICHT für ALLE Komponenten!
// 1. Store-Subscriptions
store.subscribe('tasks', () => renderTasks());
store.subscribe('columns', () => updateColumns());
store.subscribe('labels', () => refreshLabels());
// 2. Event-Listener
window.addEventListener('app:refresh', () => {
// Komplette UI aktualisieren
});
window.addEventListener('modal:close', () => {
// Nach Modal-Schließung
});
// 3. Socket.io Events in sync.js
socket.on('task:update', (data) => {
store.updateTask(data);
});
```
## 💾 Datenbank
### Wichtige Tabellen
```sql
users # Benutzer mit Rollen
projects # Projekte
columns # Kanban-Spalten
tasks # Aufgaben
task_labels # M:N Labels
task_assignees # M:N Zuweisungen
comments # Kommentare
attachments # Dateianhänge
proposals # Vorschläge
notifications # Benachrichtigungen
knowledge_* # Wissensdatenbank
```
### Schema ändern
```javascript
// 1. In backend/database.js anpassen
CREATE TABLE new_table (
id INTEGER PRIMARY KEY,
...
);
// 2. Datenbank neu initialisieren
rm data/taskmate.db*
docker restart taskmate
// 3. API & Frontend anpassen
```
## 🚢 Deployment
### Deployment-Checkliste
```bash
# 1. Vor Deployment
- [ ] Keine console.log() im Code
- [ ] Alle Features getestet
- [ ] Keine Testdaten in DB
# 2. Deployment durchführen
- [ ] Cache-Version erhöhen: frontend/sw.js
- [ ] CHANGELOG.txt aktualisieren
- [ ] Git commit & push
- [ ] docker build -t taskmate .
- [ ] docker restart taskmate
# 3. Nach Deployment
- [ ] https://taskmate.aegis-sight.de testen
- [ ] Browser-Cache leeren (Strg+F5)
- [ ] Login, Aufgabe erstellen, etc. testen
```
### Docker-Befehle
```bash
# Container Status
docker ps -a | grep taskmate
# Container stoppen/starten
docker stop taskmate
docker start taskmate
# Container neu erstellen
docker rm -f taskmate
docker-compose up -d
# In Container Shell
docker exec -it taskmate sh
# Logs live verfolgen
docker logs taskmate -f --tail 100
```
### KRITISCHES PROBLEM: Frontend-Änderungen werden nicht sichtbar
**Problem**: Frontend-Dateien (HTML, CSS, JS) werden beim Docker Build nach `/app/public/` kopiert und sind NICHT live gemountet. Änderungen in `/home/claude-dev/TaskMate/frontend/` werden daher nicht automatisch übernommen.
**Symptome**:
- CSS/JS-Änderungen funktionieren nicht trotz Browser-Cache-Löschung
- Service Worker Cache-Version Erhöhung hilft nicht
- Änderungen werden sporadisch nach längerer Zeit sichtbar
**Ursache**:
1. **Dockerfile kopiert Frontend-Dateien**: `COPY frontend/ ./public/`
2. **Express.js cached statische Dateien** mit ETags und Last-Modified Headers
3. **Mehrschichtiges Caching**: Service Worker + Browser + Express.js
**LÖSUNG A - Sofortige Änderungen (Development)**:
```bash
# Einzelne Datei kopieren
docker cp frontend/css/style.css taskmate:/app/public/css/style.css
# Mehrere Dateien kopieren
docker cp frontend/js/app.js taskmate:/app/public/js/app.js
docker cp frontend/index.html taskmate:/app/public/index.html
# CSS + JS zusammen kopieren
docker cp frontend/css/ taskmate:/app/public/css/
docker cp frontend/js/ taskmate:/app/public/js/
```
**LÖSUNG B - Vollständige Aktualisierung (Production)**:
```bash
# Docker Image neu bauen und Container ersetzen
docker build -t taskmate . && docker restart taskmate
```
**Express.js Caching deaktiviert**:
```javascript
// In server.js - statische Dateien ohne Caching
app.use(express.static(path.join(__dirname, 'public'), {
etag: false,
lastModified: false,
cacheControl: false,
setHeaders: (res, path) => {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
}
}));
```
**Debugging-Workflow**:
1. Prüfe Datei-Timestamps im Container: `docker exec taskmate ls -la /app/public/css/`
2. Vergleiche mit lokalen Dateien: `ls -la frontend/css/`
3. Bei Diskrepanz: Files mit `docker cp` aktualisieren
4. Bei JavaScript-Problemen: Browser-Console auf Fehler prüfen
**Warum passiert das**:
- Container-Pfad `/app/public/` = Static Files (nicht live)
- Container-Pfad `/app/taskmate-source/` = Git-Operationen (live gemountet)
- Frontend wird NUR beim Build-Time kopiert, nicht zur Laufzeit
## 🚨 KRITISCHE LEKTIONEN AUS PROBLEMEN
### ⚠️ Kontakte-Modul Implementation Probleme (07.01.2025)
**FEHLER 1: Backend API-Route nicht gefunden (404)**
- **Problem**: GET /api/contacts gibt 404 - Endpoint nicht gefunden
- **Ursache**: Backend-Dateien nicht im Docker-Container, Container nicht neu gestartet
- **Lösung**: Backend-Dateien kopieren und Container neu starten
- **Prävention**:
```bash
# Backend-Änderungen: Alle Dateien kopieren
docker cp backend/routes/contacts.js taskmate:/app/routes/
docker cp backend/middleware/validation.js taskmate:/app/middleware/
docker cp backend/server.js taskmate:/app/server.js
docker restart taskmate # IMMER nach Backend-Änderungen
```
**FEHLER 2: Database Table existiert nicht (500 Internal Server Error)**
- **Problem**: "no such table: contacts" - Tabelle wurde nicht erstellt
- **Ursache**: database.js Änderungen nicht übernommen, bestehende DB erweitert sich nicht automatisch
- **Lösung**: database.js kopieren + Tabelle manuell erstellen
- **Pattern für neue Tabellen**:
```bash
docker cp backend/database.js taskmate:/app/database.js
docker exec taskmate node -e "
const Database = require('better-sqlite3');
const db = new Database('/app/data/taskmate.db');
db.exec('CREATE TABLE IF NOT EXISTS new_table (...);');
console.log('Table created successfully');
"
```
**FEHLER 3: store.showMessage ist undefined**
- **Problem**: `store.showMessage()` Funktion existiert nicht
- **Ursache**: Falsche API für Toast-Nachrichten
- **Lösung**: Verwende `window.dispatchEvent` mit `toast:show`
- **Pattern für Toast-Messages**:
```javascript
// FALSCH: store.showMessage('Text', 'success')
// RICHTIG:
window.dispatchEvent(new CustomEvent('toast:show', { window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: 'Text', type: 'success' } detail: { message: 'Text', type: 'success' }
})); }));
``` wrong: "store.showMessage('Text', 'success') // existiert nicht"
**FEHLER 4: Event-Handler nicht gebunden** realtime_updates:
- **Problem**: Button-Clicks funktionieren nicht trotz Event-Listener required: |
- **Ursache**: Timing-Problem - DOM noch nicht bereit, Modal-Overlay fehlt store.subscribe('tasks', () => renderTasks());
- **Lösung**: Korrekte Modal-Struktur + Overlay-Management window.addEventListener('app:refresh', () => updateView());
- **Debugging-Pattern**:
```javascript
console.log('[Module] Element check:', this.buttonElement);
if (this.buttonElement) {
console.log('[Module] Binding event');
this.buttonElement.addEventListener('click', () => {
console.log('[Module] Button clicked!');
});
}
```
**FEHLER 5: Modal Design inkonsistent** TROUBLESHOOTING:
- **Problem**: Custom Modal-Styles passen nicht zum App-Design frontend_changes_not_visible:
- **Ursache**: Eigene CSS-Klassen statt Standard-Modal-Pattern problem: "Frontend-Dateien werden beim Build kopiert, nicht live gemountet"
- **Lösung**: Standard Modal-Struktur verwenden solution_dev: "docker cp frontend/js/ taskmate:/app/public/js/"
- **Standard Modal-Pattern**: solution_prod: "cd /home/claude-dev/TaskMate && docker compose build --no-cache && docker compose up -d"
```html
<div class="modal modal-medium hidden">
<div class="modal-header">
<h2>Title</h2>
<button class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body"><!-- Content --></div>
<div class="modal-footer"><!-- Buttons --></div>
</div>
```
### ⚠️ SVG Icon-Rendering Probleme (07.01.2025) 401_unauthorized:
problem: "Token abgelaufen"
solution: "Neu einloggen"
**FEHLER: SVG Icons werden nicht angezeigt** csrf_error:
- **Problem**: SVG-Icons verschwinden oder zeigen nicht korrekt an problem: "CSRF Token ungueltig"
- **Ursache**: `createElement()` unterstützt kein SVG-Namespace solution: "Browser-Cache/Cookies loeschen, neu einloggen"
- **Lösung**: SVG mit `innerHTML` oder als String-Template einfügen
- **Pattern**:
```javascript
// FALSCH - SVG wird nicht gerendert
const icon = createElement('svg', { viewBox: '0 0 24 24' });
// RICHTIG - SVG als HTML-String missing_data:
element.innerHTML = `<svg viewBox="0 0 24 24">...</svg>`; problem: "Daten scheinen verschwunden"
``` first_check: "docker logs taskmate - auf 401/403 Fehler pruefen"
- **Prävention**: Bei dynamischen SVGs immer innerHTML oder DOMParser nutzen note: "NIEMALS sofort Backup/Restore - erst Auth pruefen"
### ⚠️ API Field Name Mismatches (07.01.2025) WORKFLOWS:
changelog_entry:
description: "PFLICHT bei jeder Code-Aenderung"
step_1:
name: "CHANGELOG.txt aktualisieren"
file: "/home/claude-dev/TaskMate/CHANGELOG.txt"
format: "DD.MM.YYYY - vXXX - Kurzbeschreibung"
step_2:
name: "Wissensdatenbank Changelog aktualisieren"
command: |
docker exec taskmate node -e "
const Database = require('better-sqlite3');
const db = new Database('/app/data/taskmate.db');
db.prepare(\`
INSERT INTO knowledge_entries (category_id, title, notes, position, created_by)
VALUES (15, 'DD.MM.YYYY - Beschreibung', 'Markdown-Inhalt', 0, 1)
\`).run();
console.log('Changelog erstellt');
"
category_id: 15
title_format: "DD.MM.YYYY - Kurze Beschreibung"
notes_format: "Markdown mit Details (Problem, Loesung, geaenderte Dateien)"
**FEHLER: Frontend/Backend Feldnamen-Diskrepanz** deployment:
- **Problem**: Daten werden mit 0 Bytes oder leer angezeigt steps:
- **Ursache**: Backend sendet camelCase, Frontend erwartet snake_case - "Cache-Version erhoehen: frontend/sw.js"
- **Beispiel**: `originalName` vs `original_name`, `sizeBytes` vs `size_bytes` - "CHANGELOG.txt aktualisieren"
- **Lösung**: Fallback-Pattern für beide Schreibweisen - "Wissensdatenbank Changelog aktualisieren"
- **Pattern**: - "docker cp oder docker build"
```javascript - "docker compose up -d (Container mit neuem Image starten)"
// Robuste Feldabfrage mit Fallback - "Testen: https://taskmate.aegis-sight.de"
const fileName = data.originalName || data.original_name || ''; - "Browser-Cache leeren (Strg+F5)"
const fileSize = data.sizeBytes || data.size_bytes || 0;
```
- **Prävention**: API-Dokumentation prüfen, einheitliche Naming-Convention
### ⚠️ File Upload Field Names (07.01.2025) CONVENTIONS:
language_ui: Deutsch
language_code: Englisch
umlauts: "ae oe ue verwenden (ä ö ü)"
emojis: "Nur in Dokumentation, nicht in Code/UI"
auto_save: true
**FEHLER: Multer "Unexpected field" Error** SECURITY:
- **Problem**: 500 Error bei File-Upload auth:
- **Ursache**: Frontend sendet 'files' (plural), Backend erwartet 'file' (singular) type: JWT
- **Lösung**: Backend-Konsistenz herstellen validity: 24h
- **Pattern**: storage: localStorage
```javascript csrf:
// Backend - Konsistent 'files' verwenden header: "X-CSRF-Token"
upload.single('files') // NICHT 'file' generated_at: login
roles:
admin: "Nur Benutzerverwaltung"
user: "Alles ausser Admin-Bereich"
// Frontend - FormData immer mit 'files' KNOWN_ISSUES:
formData.append('files', file); docker_rebuild:
``` problem: "docker restart nutzt das ALTE Image - Code-Aenderungen werden NICHT uebernommen"
- **Prävention**: Einheitliche Field-Names über alle Upload-Endpoints wichtig: "docker build allein reicht auch nicht, da docker-compose den alten Container weiter nutzt"
solution: "cd /home/claude-dev/TaskMate && docker compose build --no-cache && docker compose up -d"
hinweis: "Immer mit --no-cache bauen, da Docker COPY-Layer cached auch wenn Dateien sich geaendert haben"
svg_rendering:
problem: "createElement() unterstuetzt kein SVG-Namespace"
solution: "SVG mit innerHTML oder String-Template einfuegen"
### ⚠️ Erinnerung-Implementation Probleme (06.01.2026) field_name_mismatch:
problem: "Backend camelCase vs Frontend snake_case"
solution: "Fallback-Pattern: data.originalName || data.original_name"
**FEHLER 1: Syntax-Fehler in JavaScript blockierte Login** file_upload:
- **Problem**: Missing closing brace in calendar.js verhinderte Login komplett problem: "Multer Unexpected field Error"
- **Ursache**: Unvollständige Code-Blöcke beim Multi-Edit solution: "Konsistent 'files' (plural) verwenden"
- **Lösung**: IMMER Syntax-Check nach JavaScript-Änderungen
- **Prävention**:
```bash
# Nach JS-Änderungen prüfen:
node -c frontend/js/calendar.js
docker logs taskmate --tail 20 # Auf Syntax-Fehler prüfen
```
**FEHLER 2: "Verschwundene" Projekte durch 401-Fehler** ROLLBACK:
- **Problem**: User dachte AegisSight-Projekt sei gelöscht backup_db: "cp data/taskmate.db data/taskmate.db.backup-$(date +%Y%m%d-%H%M%S)"
- **Ursache**: Authentifizierungs-Token abgelaufen, API gibt 401 zurück backup_docker: "docker commit taskmate taskmate-backup-$(date +%Y%m%d-%H%M%S)"
- **Diagnose**: `docker logs taskmate` zeigt 401-Fehler restore_code: "git stash && git checkout HEAD~1"
- **Lösung**: Einfach neu anmelden, Daten sind intakt
- **Prävention**: Bei "verschwundenen" Daten IMMER zuerst Auth prüfen
**FEHLER 3: Checkbox-Styling funktioniert nicht** Last-Updated: 2026-03-19
- **Problem**: CSS-Selektoren greifen nicht, komplexe Pseudo-Element-Struktur
- **Ursache**: Browser-CSS-Konflikte, CSS-Variable-Probleme
- **Lösung**: Direktes Styling nativer Checkboxes mit `appearance: none`
- **Lesson**: Bei CSS-Problemen: **Einfachster Ansatz zuerst**
```css
/* FALSCH: Komplexe Pseudo-Struktur */
input:checked + span::after { content: '✓'; }
/* RICHTIG: Direktes Styling */
input[type="checkbox"] {
appearance: none;
background: #3B82F6 when :checked;
}
```
**FEHLER 4: Event-Handler Konflikte bei Modal-Updates**
- **Problem**: Dropdown-Handler werden überschrieben
- **Ursache**: Mehrfache Event-Binding ohne Cleanup
- **Lösung**: Element-Kloning für saubere Event-Handler
- **Pattern**:
```javascript
// Event-Handler cleanup durch Klonen
const newElement = element.cloneNode(true);
element.parentNode.replaceChild(newElement, element);
// Dann neue Handler binden
```
**FEHLER 5: Visuelle Darstellung unterbricht Funktionalität**
- **Problem**: Erinnerungen unterbrachen Aufgaben-Balken
- **Ursache**: Falsche Render-Reihenfolge (Erinnerungen vor Aufgaben)
- **Lösung**: Aufgaben zuerst, dann Erinnerungen
- **Lesson**: UI-Reihenfolge muss Funktionalität folgen, nicht umgekehrt
### ⚠️ Dropdown-Transparenz-Probleme (09.01.2026)
**FEHLER: Dropdown-Menüs haben unerwünschte Transparenz**
- **Problem**: Dropdown-Menüs zeigen durchscheinenden Hintergrund, besonders bei dunklen Themes
- **Symptome**:
- Text schwer lesbar durch transparenten Hintergrund
- Inhalte dahinter scheinen durch
- Besonders auffällig bei Kontakte-Modul und anderen Dropdown-Menüs
- **Ursache**: CSS-Variablen für Hintergründe verwenden `rgba()` mit Transparenz
- **Lösung**: Explizite, nicht-transparente Hintergrundfarben für Dropdowns setzen
- **Pattern**:
```css
/* FALSCH - Transparente Hintergründe */
.dropdown {
background: var(--bg-secondary); /* rgba mit 0.95 opacity */
}
/* RICHTIG - Solide Hintergründe für Dropdowns */
.dropdown {
background: #ffffff; /* Hell-Theme */
background: #1a1a1a; /* Dunkel-Theme */
}
```
- **Prävention**:
- Dropdown-Komponenten immer mit soliden Hintergründen
- Keine CSS-Variablen mit Transparenz für interaktive Elemente
- Bei Theme-Support: Explizite Farben ohne Alpha-Kanal
### 🔧 TROUBLESHOOTING-WORKFLOW
**Bei JavaScript-Fehlern:**
1. `docker logs taskmate --tail 50` prüfen
2. Browser-Console auf Syntax-Fehler prüfen
3. Node.js Syntax-Check: `node -c datei.js`
**Bei "verschwundenen" Daten:**
1. **NIEMALS** sofort Backup/Restore - erst debuggen!
2. API-Logs prüfen auf 401/403 Fehler
3. Auth-Status prüfen: `localStorage.getItem('token')`
4. Datenbank direkt prüfen: `sqlite3 data/taskmate.db "SELECT COUNT(*) FROM projects"`
**Bei CSS-Problemen:**
1. Simplest approach first - keine komplexen Selektoren
2. `!important` nur als letzter Ausweg
3. Browser-DevTools: Computed Styles prüfen
4. Cache leeren: `CACHE_VERSION++` in sw.js
**Bei neuen Modulen mit globaler Suche:**
1. Module in app.js setupSearch() registrieren:
```javascript
} else if (currentView === 'mymodule') {
import('./mymodule.js').then(module => {
if (module.myManager) {
module.myManager.searchQuery = value;
module.myManager.filterData();
}
});
```
2. Manager-Instanz exportieren: `export { myManager }`
3. clearSearch() Funktion ebenfalls erweitern
4. Lokale Suchfelder entfernen - nur Header-Suche nutzen
**Bei File-Upload Problemen:**
1. Prüfe ob Entry/Task bereits gespeichert ist (ID vorhanden)
2. Bei neuen Einträgen: Erst speichern, dann Upload
3. Field-Name Konsistenz prüfen: 'files' (plural) überall
4. `docker logs taskmate` für Multer-Errors checken
## 🐛 Troubleshooting
### Häufige Probleme
**401 Unauthorized**
- Token abgelaufen → Neu einloggen
- Prüfen: localStorage.getItem('token')
**CSRF Token ungültig**
- Browser-Cache/Cookies löschen
- Neu einloggen
**Änderungen nicht sichtbar**
- Service Worker Cache → sw.js Version erhöhen!
- Browser: Strg+F5
- Prüfen: Echtzeit-Updates implementiert?
**Docker startet nicht**
```bash
docker logs taskmate
docker ps -a | grep taskmate
netstat -tulpn | grep 3001
```
**Datenbank-Fehler**
```bash
# Backup erstellen
cp data/taskmate.db data/taskmate.db.backup
# Integrität prüfen
sqlite3 data/taskmate.db "PRAGMA integrity_check;"
# Schema anzeigen
sqlite3 data/taskmate.db ".schema"
```
### Debug-Tipps
**Frontend Debugging**
```javascript
// Store-Status prüfen
console.log(store.getState());
// API-Calls tracken
window.api.debug = true;
// Socket-Events loggen
window.socket.on('*', console.log);
```
**Backend Debugging**
```javascript
// In server.js
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
// SQL Queries loggen
db.prepare(sql).run(); // Vorher console.log(sql)
```
## 📋 Code-Patterns
### API Response Format
```javascript
// Erfolg
res.json({
success: true,
data: result
});
// Fehler
res.status(400).json({
success: false,
error: 'Fehlermeldung'
});
```
### Store Update Pattern
```javascript
// Daten aktualisieren
store.updateTasks(tasks);
// Wird automatisch ausgelöst:
// - Alle task-Subscriber
// - Socket.io Broadcast
// - UI-Updates
```
### Modal Pattern
```javascript
// Modal öffnen
modal.show();
// WICHTIG: Bei Schließung
modal.addEventListener('close', () => {
window.dispatchEvent(new CustomEvent('modal:close'));
// Triggert UI-Updates!
});
```
## 🔒 Sicherheit
### Authentifizierung
- JWT Token mit 24h Gültigkeit
- Refresh bei jeder Aktivität
- Token in localStorage
### CSRF-Schutz
- Token bei Login generiert
- Bei jeder Mutation mitgesendet
- Header: `X-CSRF-Token`
### Berechtigungen
- Admin: Nur Benutzerverwaltung
- User: Alles außer Admin-Bereich
- Projekt-basierte Rechte
## 📝 Wichtige Konventionen
- **Sprache**: Deutsch für UI, Englisch für Code
- **Umlaute**: ä, ö, ü verwenden (keine ae, oe, ue)
- **CSS**: Variablen in `frontend/css/variables.css`
- **Keine Emojis** in Code/UI (nur Doku)
- **Auto-Save**: Änderungen werden automatisch gespeichert
## 🎯 Performance
### Frontend
- Lazy Loading für Views
- Debouncing bei Suche/Filter
- Virtual Scrolling bei langen Listen
- Service Worker Caching
### Backend
- SQLite mit WAL Mode
- Prepared Statements
- Index auf häufig gefilterte Spalten
- Pagination bei großen Datenmengen
## 🔄 Git Workflow
### Lokales Repository
```bash
# Status prüfen
git status
# Commit erstellen
git add .
git commit -m "Beschreibung der Änderung"
# Zu Gitea pushen
git push origin main
```
### Gitea Integration
- Automatischer Push bei Commits
- Repository-Projekt Verknüpfung
- Branch-Verwaltung in UI
---
**Hinweis**: Diese Dokumentation ist für die KI-gestützte Entwicklung optimiert. Bei Fragen die `ANWENDUNGSBESCHREIBUNG.txt` für Endnutzer-Dokumentation konsultieren.

Datei anzeigen

@@ -113,7 +113,7 @@ function createTables() {
CREATE TABLE IF NOT EXISTS tasks ( CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL, project_id INTEGER NOT NULL,
column_id INTEGER NOT NULL, column_id INTEGER,
title TEXT NOT NULL, title TEXT NOT NULL,
description TEXT, description TEXT,
priority TEXT DEFAULT 'medium', priority TEXT DEFAULT 'medium',
@@ -128,7 +128,7 @@ function createTables() {
created_by INTEGER, created_by INTEGER,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE, FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE SET NULL,
FOREIGN KEY (assigned_to) REFERENCES users(id), FOREIGN KEY (assigned_to) REFERENCES users(id),
FOREIGN KEY (depends_on) REFERENCES tasks(id) ON DELETE SET NULL, FOREIGN KEY (depends_on) REFERENCES tasks(id) ON DELETE SET NULL,
FOREIGN KEY (created_by) REFERENCES users(id) FOREIGN KEY (created_by) REFERENCES users(id)
@@ -613,31 +613,109 @@ function createTables() {
) )
`); `);
// Kontakte // Neues optimiertes Kontakt-Schema
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS contacts ( CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL CHECK(type IN ('person', 'company')),
-- Gemeinsame Felder
display_name TEXT NOT NULL,
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'inactive', 'archived')),
tags TEXT,
notes TEXT,
avatar_url TEXT,
-- Person-spezifische Felder
salutation TEXT,
first_name TEXT, first_name TEXT,
last_name TEXT, last_name TEXT,
company TEXT,
position TEXT, position TEXT,
email TEXT, department TEXT,
phone TEXT, parent_company_id INTEGER,
mobile TEXT,
address TEXT, -- Firma-spezifische Felder
postal_code TEXT, company_name TEXT,
city TEXT, company_type TEXT,
country TEXT, industry TEXT,
website TEXT, website TEXT,
notes TEXT,
tags TEXT, -- Meta
project_id INTEGER NOT NULL,
created_by INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER,
FOREIGN KEY (parent_company_id) REFERENCES contacts(id) ON DELETE SET NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) FOREIGN KEY (created_by) REFERENCES users(id)
) )
`); `);
// Kontaktdetails (E-Mails, Telefone, Adressen)
db.exec(`
CREATE TABLE IF NOT EXISTS contact_details (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_id INTEGER NOT NULL,
type TEXT NOT NULL CHECK(type IN ('email', 'phone', 'mobile', 'fax', 'address', 'social')),
label TEXT DEFAULT 'Arbeit',
value TEXT NOT NULL,
is_primary BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE
)
`);
// Kontakt-Kategorien
db.exec(`
CREATE TABLE IF NOT EXISTS contact_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
color TEXT DEFAULT '#6B7280',
icon TEXT,
project_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
)
`);
// Kontakt-Kategorie Zuordnungen
db.exec(`
CREATE TABLE IF NOT EXISTS contact_to_categories (
contact_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (contact_id, category_id),
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES contact_categories(id) ON DELETE CASCADE
)
`);
// Interaktionen/Historie
db.exec(`
CREATE TABLE IF NOT EXISTS contact_interactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_id INTEGER NOT NULL,
type TEXT NOT NULL CHECK(type IN ('call', 'email', 'meeting', 'note', 'task')),
subject TEXT,
content TEXT,
interaction_date DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
)
`);
// Trigger für updated_at auf Kontakte-Tabelle
db.exec(`
CREATE TRIGGER IF NOT EXISTS update_contacts_timestamp
AFTER UPDATE ON contacts
BEGIN
UPDATE contacts SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END
`);
// Indizes für Performance // Indizes für Performance
db.exec(` db.exec(`
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id); CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
@@ -660,8 +738,16 @@ function createTables() {
CREATE INDEX IF NOT EXISTS idx_coding_directories_position ON coding_directories(position); CREATE INDEX IF NOT EXISTS idx_coding_directories_position ON coding_directories(position);
CREATE INDEX IF NOT EXISTS idx_coding_usage_directory ON coding_usage(directory_id); CREATE INDEX IF NOT EXISTS idx_coding_usage_directory ON coding_usage(directory_id);
CREATE INDEX IF NOT EXISTS idx_coding_usage_timestamp ON coding_usage(timestamp); CREATE INDEX IF NOT EXISTS idx_coding_usage_timestamp ON coding_usage(timestamp);
CREATE INDEX IF NOT EXISTS idx_contacts_company ON contacts(company); -- Kontakt-Indizes für Performance
CREATE INDEX IF NOT EXISTS idx_contacts_tags ON contacts(tags); CREATE INDEX IF NOT EXISTS idx_contacts_type ON contacts(type);
CREATE INDEX IF NOT EXISTS idx_contacts_status ON contacts(status);
CREATE INDEX IF NOT EXISTS idx_contacts_project ON contacts(project_id);
CREATE INDEX IF NOT EXISTS idx_contacts_parent ON contacts(parent_company_id);
CREATE INDEX IF NOT EXISTS idx_contacts_display_name ON contacts(display_name);
CREATE INDEX IF NOT EXISTS idx_details_contact ON contact_details(contact_id);
CREATE INDEX IF NOT EXISTS idx_details_type ON contact_details(type);
CREATE INDEX IF NOT EXISTS idx_interactions_contact ON contact_interactions(contact_id);
CREATE INDEX IF NOT EXISTS idx_interactions_date ON contact_interactions(interaction_date);
`); `);
logger.info('Datenbank-Tabellen erstellt'); logger.info('Datenbank-Tabellen erstellt');

Datei anzeigen

@@ -13,7 +13,7 @@ const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET || JWT_SECRET.length < 32) { if (!JWT_SECRET || JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET muss in .env gesetzt und mindestens 32 Zeichen lang sein!'); throw new Error('JWT_SECRET muss in .env gesetzt und mindestens 32 Zeichen lang sein!');
} }
const ACCESS_TOKEN_EXPIRY = 15; // Minuten (kürzer für mehr Sicherheit) const ACCESS_TOKEN_EXPIRY = 60; // Minuten (kürzer für mehr Sicherheit)
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60; // 7 Tage in Minuten const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60; // 7 Tage in Minuten
const SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT) || 30; // Minuten const SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT) || 30; // Minuten

Datei anzeigen

@@ -17,7 +17,7 @@ const backup = require('../utils/backup');
*/ */
const DEFAULT_UPLOAD_SETTINGS = { const DEFAULT_UPLOAD_SETTINGS = {
maxFileSizeMB: 15, maxFileSizeMB: 15,
allowedExtensions: ['pdf', 'docx', 'txt'] allowedExtensions: ['pdf', 'docx', 'doc', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'xls', 'xlsx', 'ppt', 'pptx', 'rtf', 'csv', 'json', 'html']
}; };
// Alle Admin-Routes erfordern Authentifizierung und Admin-Rolle // Alle Admin-Routes erfordern Authentifizierung und Admin-Rolle

Datei anzeigen

@@ -252,17 +252,20 @@ router.delete('/:id', (req, res) => {
return res.status(404).json({ error: 'Spalte nicht gefunden' }); return res.status(404).json({ error: 'Spalte nicht gefunden' });
} }
// Prüfen ob Aufgaben in der Spalte sind // Prüfen ob AKTIVE (nicht-archivierte) Aufgaben in der Spalte sind
const taskCount = db.prepare( const activeTaskCount = db.prepare(
'SELECT COUNT(*) as count FROM tasks WHERE column_id = ?' 'SELECT COUNT(*) as count FROM tasks WHERE column_id = ? AND (archived IS NULL OR archived = 0)'
).get(columnId).count; ).get(columnId).count;
if (taskCount > 0) { if (activeTaskCount > 0) {
return res.status(400).json({ return res.status(400).json({
error: 'Spalte enthält noch Aufgaben. Verschiebe oder lösche diese zuerst.' error: 'Spalte enthält noch Aufgaben. Verschiebe oder lösche diese zuerst.'
}); });
} }
// Archivierte Aufgaben: column_id auf NULL setzen (bleiben im Archiv erhalten)
db.prepare('UPDATE tasks SET column_id = NULL WHERE column_id = ? AND archived = 1').run(columnId);
// Mindestens eine Spalte muss bleiben // Mindestens eine Spalte muss bleiben
const columnCount = db.prepare( const columnCount = db.prepare(
'SELECT COUNT(*) as count FROM columns WHERE project_id = ?' 'SELECT COUNT(*) as count FROM columns WHERE project_id = ?'

Datei anzeigen

@@ -1,438 +1,445 @@
/** /**
* TASKMATE - Contact Routes * TASKMATE - Kontakte API
* ========================= * ========================
* CRUD für Kontakte * REST API für Kontaktmanagement
*/ */
const express = require('express'); const router = require('express').Router();
const router = express.Router();
const { getDb } = require('../database'); const { getDb } = require('../database');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { validators } = require('../middleware/validation'); const { authenticateToken } = require('../middleware/auth');
const { upload } = require('../middleware/upload');
// =====================
// KONTAKTE CRUD
// =====================
// GET /api/contacts - Alle Kontakte abrufen
router.get('/', authenticateToken, (req, res) => {
// Kein Caching für Kontakte
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
res.set('Pragma', 'no-cache');
/**
* GET /api/contacts
* Alle Kontakte abrufen mit optionalem Filter
*/
router.get('/', (req, res) => {
try { try {
const db = getDb(); const db = getDb();
const { search, tag, sortBy = 'created_at', sortOrder = 'desc' } = req.query; const currentUser = req.user;
const { project_id, type, status, search } = req.query;
logger.info('Kontakte abrufen', { user_id: currentUser.id, filters: req.query });
let query = ` let query = `
SELECT c.*, u.display_name as creator_name SELECT
c.*,
u.display_name as created_by_name,
pc.display_name as parent_company_name
FROM contacts c FROM contacts c
LEFT JOIN users u ON c.created_by = u.id LEFT JOIN users u ON c.created_by = u.id
LEFT JOIN contacts pc ON c.parent_company_id = pc.id
WHERE 1=1 WHERE 1=1
`; `;
const params = []; const params = [];
// Suchfilter // Filter nach Projekt
if (project_id) {
query += ' AND c.project_id = ?';
params.push(project_id);
}
// Filter nach Typ
if (type && ['person', 'company'].includes(type)) {
query += ' AND c.type = ?';
params.push(type);
}
// Filter nach Status
if (status && ['active', 'inactive', 'archived'].includes(status)) {
query += ' AND c.status = ?';
params.push(status);
}
// Suche
if (search) { if (search) {
query += ` AND ( query += ` AND (
c.display_name LIKE ? OR
c.first_name LIKE ? OR c.first_name LIKE ? OR
c.last_name LIKE ? OR c.last_name LIKE ? OR
c.company LIKE ? OR c.company_name LIKE ? OR
c.email LIKE ? OR c.tags LIKE ? OR
c.phone LIKE ? OR c.notes LIKE ?
c.mobile LIKE ?
)`; )`;
const searchParam = `%${search}%`; const searchPattern = `%${search}%`;
params.push(searchParam, searchParam, searchParam, searchParam, searchParam, searchParam); params.push(searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern);
} }
// Tag-Filter query += ' ORDER BY c.display_name ASC';
if (tag) {
query += ` AND c.tags LIKE ?`;
params.push(`%${tag}%`);
}
// Sortierung const contacts = db.prepare(query).all(...params);
const validSortFields = ['first_name', 'last_name', 'company', 'created_at', 'updated_at'];
const sortField = validSortFields.includes(sortBy) ? sortBy : 'created_at';
const order = sortOrder.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
query += ` ORDER BY c.${sortField} ${order}`;
const contacts = db.prepare(query).all(params); // Details für jeden Kontakt abrufen
const contactsWithDetails = contacts.map(contact => {
// Kontaktdetails abrufen
const details = db.prepare(`
SELECT * FROM contact_details
WHERE contact_id = ?
ORDER BY is_primary DESC, type, label
`).all(contact.id);
res.json(contacts.map(c => ({ return {
id: c.id, ...contact,
firstName: c.first_name, details
lastName: c.last_name, };
company: c.company, });
position: c.position,
email: c.email, res.json({ success: true, data: contactsWithDetails });
phone: c.phone,
mobile: c.mobile,
address: c.address,
postalCode: c.postal_code,
city: c.city,
country: c.country,
website: c.website,
notes: c.notes,
tags: c.tags ? c.tags.split(',').map(t => t.trim()) : [],
createdAt: c.created_at,
updatedAt: c.updated_at,
createdBy: c.created_by,
creatorName: c.creator_name
})));
} catch (error) { } catch (error) {
logger.error('Fehler beim Abrufen der Kontakte:', { error: error.message }); logger.error('Fehler beim Abrufen der Kontakte:', error);
res.status(500).json({ error: 'Interner Serverfehler' }); res.status(500).json({ success: false, error: error.message });
} }
}); });
/** // GET /api/contacts/:id - Einzelnen Kontakt abrufen
* GET /api/contacts/:id router.get('/:id', authenticateToken, (req, res) => {
* Einzelnen Kontakt abrufen
*/
router.get('/:id', (req, res) => {
try { try {
const db = getDb(); const db = getDb();
const contactId = req.params.id; const { id } = req.params;
const contact = db.prepare(` const contact = db.prepare(`
SELECT c.*, u.display_name as creator_name SELECT
c.*,
u.display_name as created_by_name,
pc.display_name as parent_company_name
FROM contacts c FROM contacts c
LEFT JOIN users u ON c.created_by = u.id LEFT JOIN users u ON c.created_by = u.id
LEFT JOIN contacts pc ON c.parent_company_id = pc.id
WHERE c.id = ? WHERE c.id = ?
`).get(contactId); `).get(id);
if (!contact) { if (!contact) {
return res.status(404).json({ error: 'Kontakt nicht gefunden' }); return res.status(404).json({ success: false, error: 'Kontakt nicht gefunden' });
} }
res.json({ // Details abrufen
id: contact.id, contact.details = db.prepare(`
firstName: contact.first_name, SELECT * FROM contact_details
lastName: contact.last_name, WHERE contact_id = ?
company: contact.company, ORDER BY is_primary DESC, type, label
position: contact.position, `).all(id);
email: contact.email,
phone: contact.phone,
mobile: contact.mobile, // Interaktionen abrufen
address: contact.address, contact.interactions = db.prepare(`
postalCode: contact.postal_code, SELECT ci.*, u.display_name as created_by_name
city: contact.city, FROM contact_interactions ci
country: contact.country, LEFT JOIN users u ON ci.created_by = u.id
website: contact.website, WHERE ci.contact_id = ?
notes: contact.notes, ORDER BY ci.interaction_date DESC
tags: contact.tags ? contact.tags.split(',').map(t => t.trim()) : [], LIMIT 10
createdAt: contact.created_at, `).all(id);
updatedAt: contact.updated_at,
createdBy: contact.created_by, res.json({ success: true, data: contact });
creatorName: contact.creator_name
});
} catch (error) { } catch (error) {
logger.error('Fehler beim Abrufen des Kontakts:', { error: error.message, contactId: req.params.id }); logger.error('Fehler beim Abrufen des Kontakts:', error);
res.status(500).json({ error: 'Interner Serverfehler' }); res.status(500).json({ success: false, error: error.message });
} }
}); });
/** // POST /api/contacts - Neuen Kontakt erstellen
* POST /api/contacts router.post('/', authenticateToken, (req, res) => {
* Neuen Kontakt erstellen
*/
router.post('/', validators.contact, (req, res) => {
try { try {
const db = getDb(); const db = getDb();
const userId = req.user.id; const currentUser = req.user;
const { const { type, project_id, details = [], ...contactData } = req.body;
firstName,
lastName, // Validierung
company, if (!type || !['person', 'company'].includes(type)) {
position, return res.status(400).json({
email, success: false,
phone, error: 'Kontakttyp muss "person" oder "company" sein'
mobile, });
address, }
postalCode,
city, if (!project_id) {
country, return res.status(400).json({
website, success: false,
notes, error: 'Projekt-ID ist erforderlich'
tags });
} = req.body; }
// Display Name generieren falls nicht vorhanden
if (!contactData.display_name) {
if (type === 'person') {
contactData.display_name = `${contactData.first_name || ''} ${contactData.last_name || ''}`.trim();
} else {
contactData.display_name = contactData.company_name || '';
}
}
// Transaktion starten
const insertContact = db.prepare(`
INSERT INTO contacts (
type, project_id, created_by,
display_name, status, tags, notes, avatar_url,
salutation, first_name, last_name, position, department,
parent_company_id,
company_name, company_type, industry, website
) VALUES (
?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
?,
?, ?, ?, ?
)
`);
const insertDetail = db.prepare(`
INSERT INTO contact_details (contact_id, type, label, value, is_primary)
VALUES (?, ?, ?, ?, ?)
`);
const result = db.transaction(() => {
// Kontakt einfügen
const info = insertContact.run(
type, project_id, currentUser.id,
contactData.display_name,
contactData.status || 'active',
contactData.tags || null,
contactData.notes || null,
contactData.avatar_url || null,
// Person-Felder
type === 'person' ? contactData.salutation : null,
type === 'person' ? contactData.first_name : null,
type === 'person' ? contactData.last_name : null,
type === 'person' ? contactData.position : null,
type === 'person' ? contactData.department : null,
type === 'person' ? contactData.parent_company_id : null,
// Company-Felder
type === 'company' ? contactData.company_name : null,
type === 'company' ? contactData.company_type : null,
type === 'company' ? contactData.industry : null,
type === 'company' ? contactData.website : null
);
const contactId = info.lastInsertRowid;
// Details einfügen
for (const detail of details) {
if (detail.value) {
insertDetail.run(
contactId,
detail.type,
detail.label || 'Arbeit',
detail.value,
detail.is_primary ? 1 : 0
);
}
}
return contactId;
})();
// Neu erstellten Kontakt mit Details abrufen
const newContact = db.prepare(`
SELECT * FROM contacts WHERE id = ?
`).get(result);
newContact.details = db.prepare(`
SELECT * FROM contact_details WHERE contact_id = ?
`).all(result);
logger.info('Kontakt erstellt', { contact_id: result, user_id: currentUser.id });
res.json({
success: true,
data: newContact,
message: 'Kontakt erfolgreich erstellt'
});
} catch (error) {
logger.error('Fehler beim Erstellen des Kontakts:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// PUT /api/contacts/:id - Kontakt aktualisieren
router.put('/:id', authenticateToken, (req, res) => {
try {
const db = getDb();
const { id } = req.params;
const { details = [], ...contactData } = req.body;
// Prüfen ob Kontakt existiert
const existing = db.prepare('SELECT * FROM contacts WHERE id = ?').get(id);
if (!existing) {
return res.status(404).json({ success: false, error: 'Kontakt nicht gefunden' });
}
// Update-Query dynamisch aufbauen
const fields = [];
const values = [];
// Erlaubte Felder für Update
const allowedFields = [
'display_name', 'status', 'tags', 'notes', 'avatar_url',
'salutation', 'first_name', 'last_name', 'position', 'department',
'parent_company_id',
'company_name', 'company_type', 'industry', 'website'
];
for (const field of allowedFields) {
if (contactData.hasOwnProperty(field)) {
fields.push(`${field} = ?`);
values.push(contactData[field]);
}
}
values.push(id);
// Transaktion für Updates
db.transaction(() => {
// Kontakt aktualisieren
if (fields.length > 0) {
db.prepare(`
UPDATE contacts
SET ${fields.join(', ')}
WHERE id = ?
`).run(...values);
}
// Details aktualisieren - alte löschen und neue einfügen
db.prepare('DELETE FROM contact_details WHERE contact_id = ?').run(id);
const insertDetail = db.prepare(`
INSERT INTO contact_details (contact_id, type, label, value, is_primary)
VALUES (?, ?, ?, ?, ?)
`);
for (const detail of details) {
if (detail.value) {
insertDetail.run(
id,
detail.type,
detail.label || 'Arbeit',
detail.value,
detail.is_primary ? 1 : 0
);
}
}
})();
// Aktualisierten Kontakt abrufen
const updatedContact = db.prepare(`
SELECT * FROM contacts WHERE id = ?
`).get(id);
updatedContact.details = db.prepare(`
SELECT * FROM contact_details WHERE contact_id = ?
`).all(id);
logger.info('Kontakt aktualisiert', { contact_id: id });
res.json({
success: true,
data: updatedContact,
message: 'Kontakt erfolgreich aktualisiert'
});
} catch (error) {
logger.error('Fehler beim Aktualisieren des Kontakts:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// DELETE /api/contacts/:id - Kontakt löschen
router.delete('/:id', authenticateToken, (req, res) => {
try {
const db = getDb();
const { id } = req.params;
const result = db.prepare('DELETE FROM contacts WHERE id = ?').run(id);
if (result.changes === 0) {
return res.status(404).json({ success: false, error: 'Kontakt nicht gefunden' });
}
logger.info('Kontakt gelöscht', { contact_id: id });
res.json({ success: true, message: 'Kontakt erfolgreich gelöscht' });
} catch (error) {
logger.error('Fehler beim Löschen des Kontakts:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// =====================
// INTERAKTIONEN (nach /:id Routen)
// =====================
// POST /api/contacts/:id/interactions - Neue Interaktion hinzufügen
router.post('/:id/interactions', authenticateToken, (req, res) => {
try {
const db = getDb();
const currentUser = req.user;
const { id } = req.params;
const { type, subject, content } = req.body;
if (!type || !['call', 'email', 'meeting', 'note', 'task'].includes(type)) {
return res.status(400).json({
success: false,
error: 'Ungültiger Interaktionstyp'
});
}
const result = db.prepare(` const result = db.prepare(`
INSERT INTO contacts ( INSERT INTO contact_interactions (
first_name, last_name, company, position, contact_id, type, subject, content, created_by
email, phone, mobile, address, postal_code, ) VALUES (?, ?, ?, ?, ?)
city, country, website, notes, tags, `).run(id, type, subject, content, currentUser.id);
created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
firstName || null,
lastName || null,
company || null,
position || null,
email || null,
phone || null,
mobile || null,
address || null,
postalCode || null,
city || null,
country || null,
website || null,
notes || null,
Array.isArray(tags) ? tags.join(', ') : null,
userId
);
const newContact = db.prepare(` const newInteraction = db.prepare(`
SELECT c.*, u.display_name as creator_name SELECT ci.*, u.display_name as created_by_name
FROM contacts c FROM contact_interactions ci
LEFT JOIN users u ON c.created_by = u.id LEFT JOIN users u ON ci.created_by = u.id
WHERE c.id = ? WHERE ci.id = ?
`).get(result.lastInsertRowid); `).get(result.lastInsertRowid);
// Socket.io Event res.json({ success: true, data: newInteraction });
const io = req.app.get('io');
io.emit('contact:created', {
contact: {
id: newContact.id,
firstName: newContact.first_name,
lastName: newContact.last_name,
company: newContact.company,
position: newContact.position,
email: newContact.email,
phone: newContact.phone,
mobile: newContact.mobile,
address: newContact.address,
postalCode: newContact.postal_code,
city: newContact.city,
country: newContact.country,
website: newContact.website,
notes: newContact.notes,
tags: newContact.tags ? newContact.tags.split(',').map(t => t.trim()) : [],
createdAt: newContact.created_at,
updatedAt: newContact.updated_at,
createdBy: newContact.created_by,
creatorName: newContact.creator_name
},
userId
});
res.status(201).json({
id: newContact.id,
firstName: newContact.first_name,
lastName: newContact.last_name,
company: newContact.company,
position: newContact.position,
email: newContact.email,
phone: newContact.phone,
mobile: newContact.mobile,
address: newContact.address,
postalCode: newContact.postal_code,
city: newContact.city,
country: newContact.country,
website: newContact.website,
notes: newContact.notes,
tags: newContact.tags ? newContact.tags.split(',').map(t => t.trim()) : [],
createdAt: newContact.created_at,
updatedAt: newContact.updated_at,
createdBy: newContact.created_by,
creatorName: newContact.creator_name
});
logger.info('Kontakt erstellt', { contactId: newContact.id, userId });
} catch (error) { } catch (error) {
logger.error('Fehler beim Erstellen des Kontakts:', { error: error.message, body: req.body }); logger.error('Fehler beim Hinzufügen der Interaktion:', error);
res.status(500).json({ error: 'Interner Serverfehler' }); res.status(500).json({ success: false, error: error.message });
} }
}); });
/** // =====================
* PUT /api/contacts/:id // DATEI-UPLOAD (nach /:id Routen)
* Kontakt aktualisieren // =====================
*/
router.put('/:id', validators.contact, (req, res) => { // POST /api/contacts/:id/avatar - Avatar hochladen
router.post('/:id/avatar', authenticateToken, upload.single('files'), async (req, res) => {
try { try {
const db = getDb(); const db = getDb();
const contactId = req.params.id; const { id } = req.params;
const userId = req.user.id;
const {
firstName,
lastName,
company,
position,
email,
phone,
mobile,
address,
postalCode,
city,
country,
website,
notes,
tags
} = req.body;
// Prüfen ob Kontakt existiert if (!req.file) {
const existing = db.prepare('SELECT id FROM contacts WHERE id = ?').get(contactId); return res.status(400).json({
if (!existing) { success: false,
return res.status(404).json({ error: 'Kontakt nicht gefunden' }); error: 'Keine Datei hochgeladen'
});
} }
// Update // Avatar-URL speichern
db.prepare(` const avatarUrl = `/uploads/${req.file.filename}`;
UPDATE contacts SET
first_name = ?,
last_name = ?,
company = ?,
position = ?,
email = ?,
phone = ?,
mobile = ?,
address = ?,
postal_code = ?,
city = ?,
country = ?,
website = ?,
notes = ?,
tags = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(
firstName || null,
lastName || null,
company || null,
position || null,
email || null,
phone || null,
mobile || null,
address || null,
postalCode || null,
city || null,
country || null,
website || null,
notes || null,
Array.isArray(tags) ? tags.join(', ') : null,
contactId
);
const updatedContact = db.prepare(` db.prepare('UPDATE contacts SET avatar_url = ? WHERE id = ?')
SELECT c.*, u.display_name as creator_name .run(avatarUrl, id);
FROM contacts c
LEFT JOIN users u ON c.created_by = u.id
WHERE c.id = ?
`).get(contactId);
// Socket.io Event
const io = req.app.get('io');
io.emit('contact:updated', {
contact: {
id: updatedContact.id,
firstName: updatedContact.first_name,
lastName: updatedContact.last_name,
company: updatedContact.company,
position: updatedContact.position,
email: updatedContact.email,
phone: updatedContact.phone,
mobile: updatedContact.mobile,
address: updatedContact.address,
postalCode: updatedContact.postal_code,
city: updatedContact.city,
country: updatedContact.country,
website: updatedContact.website,
notes: updatedContact.notes,
tags: updatedContact.tags ? updatedContact.tags.split(',').map(t => t.trim()) : [],
createdAt: updatedContact.created_at,
updatedAt: updatedContact.updated_at,
createdBy: updatedContact.created_by,
creatorName: updatedContact.creator_name
},
userId
});
res.json({ res.json({
id: updatedContact.id, success: true,
firstName: updatedContact.first_name, data: { avatar_url: avatarUrl },
lastName: updatedContact.last_name, message: 'Avatar erfolgreich hochgeladen'
company: updatedContact.company,
position: updatedContact.position,
email: updatedContact.email,
phone: updatedContact.phone,
mobile: updatedContact.mobile,
address: updatedContact.address,
postalCode: updatedContact.postal_code,
city: updatedContact.city,
country: updatedContact.country,
website: updatedContact.website,
notes: updatedContact.notes,
tags: updatedContact.tags ? updatedContact.tags.split(',').map(t => t.trim()) : [],
createdAt: updatedContact.created_at,
updatedAt: updatedContact.updated_at,
createdBy: updatedContact.created_by,
creatorName: updatedContact.creator_name
}); });
logger.info('Kontakt aktualisiert', { contactId, userId });
} catch (error) { } catch (error) {
logger.error('Fehler beim Aktualisieren des Kontakts:', { error: error.message, contactId: req.params.id }); logger.error('Fehler beim Avatar-Upload:', error);
res.status(500).json({ error: 'Interner Serverfehler' }); res.status(500).json({ success: false, error: error.message });
}
});
/**
* DELETE /api/contacts/:id
* Kontakt löschen
*/
router.delete('/:id', (req, res) => {
try {
const db = getDb();
const contactId = req.params.id;
const userId = req.user.id;
// Prüfen ob Kontakt existiert
const existing = db.prepare('SELECT id FROM contacts WHERE id = ?').get(contactId);
if (!existing) {
return res.status(404).json({ error: 'Kontakt nicht gefunden' });
}
// Löschen
db.prepare('DELETE FROM contacts WHERE id = ?').run(contactId);
// Socket.io Event
const io = req.app.get('io');
io.emit('contact:deleted', { contactId, userId });
res.json({ success: true });
logger.info('Kontakt gelöscht', { contactId, userId });
} catch (error) {
logger.error('Fehler beim Löschen des Kontakts:', { error: error.message, contactId: req.params.id });
res.status(500).json({ error: 'Interner Serverfehler' });
}
});
/**
* GET /api/contacts/tags
* Alle verwendeten Tags abrufen
*/
router.get('/tags/all', (req, res) => {
try {
const db = getDb();
const contacts = db.prepare('SELECT DISTINCT tags FROM contacts WHERE tags IS NOT NULL').all();
// Alle Tags sammeln und deduplizieren
const allTags = new Set();
contacts.forEach(contact => {
if (contact.tags) {
contact.tags.split(',').forEach(tag => {
const trimmedTag = tag.trim();
if (trimmedTag) {
allTags.add(trimmedTag);
}
});
}
});
res.json(Array.from(allTags).sort());
} catch (error) {
logger.error('Fehler beim Abrufen der Tags:', { error: error.message });
res.status(500).json({ error: 'Interner Serverfehler' });
} }
}); });

Datei anzeigen

@@ -522,7 +522,7 @@ router.post('/entries', (req, res) => {
actorId: req.user.id actorId: req.user.id
}, },
io, io,
false // nicht persistent true // persistent, damit die Benachrichtigung in der Inbox bleibt
); );
}); });

Datei anzeigen

@@ -828,7 +828,7 @@ router.post('/:id/duplicate', (req, res) => {
router.put('/:id/archive', (req, res) => { router.put('/:id/archive', (req, res) => {
try { try {
const taskId = req.params.id; const taskId = req.params.id;
const { archived } = req.body; const { archived, columnId } = req.body;
const db = getDb(); const db = getDb();
@@ -837,16 +837,42 @@ router.put('/:id/archive', (req, res) => {
return res.status(404).json({ error: 'Aufgabe nicht gefunden' }); return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
} }
// Bei Wiederherstellung: Prüfen ob Spalte vorhanden
if (!archived && !task.column_id) {
// Task hat keine Spalte (wurde mit gelöschter Spalte archiviert)
if (!columnId) {
return res.status(400).json({
error: 'Spalte erforderlich',
requiresColumn: true
});
}
// Prüfen ob Spalte existiert und zum Projekt gehört
const column = db.prepare('SELECT * FROM columns WHERE id = ? AND project_id = ?')
.get(columnId, task.project_id);
if (!column) {
return res.status(400).json({ error: 'Ungültige Spalte' });
}
// Wiederherstellen mit neuer Spalte
db.prepare('UPDATE tasks SET archived = 0, column_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(columnId, taskId);
} else {
// Normales Archivieren/Wiederherstellen
db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?') db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(archived ? 1 : 0, taskId); .run(archived ? 1 : 0, taskId);
}
addHistory(db, taskId, req.user.id, archived ? 'archived' : 'restored'); addHistory(db, taskId, req.user.id, archived ? 'archived' : 'restored');
logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`); logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`);
// WebSocket // WebSocket - vollständige Task-Daten senden
const io = req.app.get('io'); const io = req.app.get('io');
io.to(`project:${task.project_id}`).emit('task:archived', { id: taskId, archived: !!archived }); const updatedTask = getFullTask(db, taskId);
io.to(`project:${task.project_id}`).emit('task:archived', {
id: taskId,
archived: !!archived,
columnId: updatedTask?.columnId
});
res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt' }); res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt' });
} catch (error) { } catch (error) {

Datei anzeigen

@@ -4,7 +4,7 @@
"namespace": "android_app", "namespace": "android_app",
"package_name": "de.aegissight.taskmate", "package_name": "de.aegissight.taskmate",
"sha256_cert_fingerprints": [ "sha256_cert_fingerprints": [
"TO_BE_REPLACED_WITH_YOUR_APP_SIGNING_KEY_FINGERPRINT" "75:8D:45:A0:FF:33:7E:39:69:A7:E2:13:CE:6A:3C:A4:C8:67:2F:6A:21:91:42:4C:74:B1:BC:B9:C5:B8:74:F2"
] ]
} }
}] }]

Datei anzeigen

@@ -112,13 +112,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: var(--header-height); min-height: var(--header-height);
padding: 0 var(--spacing-4); padding: 0 var(--spacing-4);
background: var(--bg-card); background: var(--bg-card);
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border-light);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: var(--z-sticky); z-index: var(--z-sticky);
width: 100%;
box-sizing: border-box;
} }
.header-left, .header-left,
@@ -126,11 +128,20 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-3); gap: var(--spacing-3);
flex-shrink: 0;
flex-wrap: nowrap;
} }
.header-center { .view-tabs-bar {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding: var(--spacing-1) var(--spacing-4);
background: var(--bg-card);
border-bottom: 1px solid var(--border-light);
position: sticky;
top: var(--header-height);
z-index: calc(var(--z-sticky) - 1);
} }
.logo { .logo {
@@ -150,7 +161,8 @@
} }
.project-select { .project-select {
min-width: 160px; width: 100%;
min-width: 120px;
max-width: 200px; max-width: 200px;
font-weight: var(--font-medium); font-weight: var(--font-medium);
font-size: var(--text-sm); font-size: var(--text-sm);
@@ -166,15 +178,17 @@
position: relative; position: relative;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
justify-content: center;
} }
.view-tab { .view-tab {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: var(--spacing-2); gap: 2px;
padding: 12px 20px; padding: 8px 12px;
font-size: var(--text-sm); font-size: 11px;
font-weight: var(--font-medium); font-weight: var(--font-medium);
color: var(--text-tertiary); color: var(--text-tertiary);
background: none; background: none;
@@ -183,6 +197,7 @@
cursor: pointer; cursor: pointer;
transition: all var(--transition-default); transition: all var(--transition-default);
white-space: nowrap; white-space: nowrap;
flex-shrink: 0;
} }
/* Tab Icon */ /* Tab Icon */
@@ -207,7 +222,7 @@
/* Active State with Underline */ /* Active State with Underline */
.view-tab.active { .view-tab.active {
color: var(--primary); color: var(--primary);
background: rgba(59, 130, 246, 0.1); background: rgba(200, 168, 81, 0.1);
border-radius: var(--radius-md); border-radius: var(--radius-md);
} }
@@ -256,6 +271,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-2); gap: var(--spacing-2);
flex: 1 1 auto;
min-width: 150px;
max-width: 450px;
} }
.search-icon { .search-icon {
@@ -266,8 +284,8 @@
} }
.search-input { .search-input {
width: 450px; width: 100%;
min-width: 350px; max-width: 450px;
padding: 10px 16px; padding: 10px 16px;
background: var(--bg-tertiary); background: var(--bg-tertiary);
border: 1px solid transparent; border: 1px solid transparent;
@@ -439,6 +457,23 @@
border-radius: var(--radius-full); border-radius: var(--radius-full);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
position: relative;
}
.user-avatar::after {
content: '';
position: absolute;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
background: var(--success);
border-radius: 50%;
border: 2px solid var(--bg-card);
}
.offline .user-avatar::after {
background: var(--error);
} }
.user-avatar:hover { .user-avatar:hover {
@@ -447,16 +482,16 @@
} }
.user-dropdown { .user-dropdown {
position: absolute; position: fixed;
top: calc(100% + 8px); top: 60px;
right: 0; right: 20px;
min-width: 220px; min-width: 220px;
padding: var(--spacing-2); padding: var(--spacing-2);
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
z-index: var(--z-dropdown); z-index: 9999;
} }
.user-info { .user-info {
@@ -524,34 +559,125 @@
FILTER BAR FILTER BAR
======================================== */ ======================================== */
.filter-bar { .filter-bar-actions {
display: flex; display: flex;
gap: var(--spacing-2);
align-items: center; align-items: center;
justify-content: flex-start;
gap: var(--spacing-4);
padding: var(--spacing-3) var(--spacing-4);
background: var(--bg-card);
border-bottom: 1px solid var(--border-light);
flex-shrink: 0; flex-shrink: 0;
width: 100%;
} }
.filter-group { .filter-toggle-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-2);
}
.filter-toggle-btn.has-filters {
color: var(--primary);
border-color: var(--primary);
background: var(--primary-light);
}
.filter-popover {
position: absolute;
top: 100%;
right: var(--spacing-4);
z-index: var(--z-dropdown);
min-width: 280px;
background: var(--bg-card);
border: 1px solid var(--border-light);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
padding: var(--spacing-4);
}
.filter-popover.hidden {
display: none;
}
.filter-popover-content {
display: flex;
flex-direction: column;
gap: var(--spacing-3); gap: var(--spacing-3);
} }
.filter-group label { .filter-popover-row {
font-size: var(--text-sm); display: flex;
font-weight: var(--font-medium); flex-direction: column;
color: var(--text-tertiary); gap: var(--spacing-1);
} }
.filter-actions { .filter-popover-row label {
font-size: var(--text-xs);
font-weight: var(--font-semibold);
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.filter-checkbox-list {
display: flex; display: flex;
flex-direction: column;
gap: 2px;
max-height: 200px;
overflow-y: auto;
}
.filter-checkbox-item {
display: flex;
align-items: center;
gap: var(--spacing-2); gap: var(--spacing-2);
margin-left: auto; padding: 6px 8px;
border-radius: var(--radius-md);
cursor: pointer;
font-size: var(--text-sm);
color: var(--text-primary);
}
.filter-checkbox-item:hover {
background: var(--bg-hover);
}
.filter-checkbox-item input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 20px;
height: 20px;
min-width: 20px;
border: 1px solid var(--border-default);
border-radius: 4px;
background: var(--bg-tertiary);
cursor: pointer;
position: relative;
}
.filter-checkbox-item input[type="checkbox"]:hover {
border-color: var(--primary);
}
.filter-checkbox-item input[type="checkbox"]:checked {
background: var(--primary);
border-color: var(--primary);
}
.filter-checkbox-item input[type="checkbox"]:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.filter-popover-footer {
padding-top: var(--spacing-2);
border-top: 1px solid var(--border-light);
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-2);
} }
/* ======================================== /* ========================================
@@ -637,26 +763,26 @@
background: var(--bg-main); background: var(--bg-main);
} }
.view-board .filter-bar {
flex-shrink: 0;
}
.board { .board {
display: flex; display: flex;
gap: var(--spacing-3); gap: var(--spacing-3);
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden;
padding: var(--spacing-4); padding: var(--spacing-4);
align-items: flex-start; align-items: flex-start;
width: 100%;
box-sizing: border-box;
} }
/* Column */ /* Column */
.column { .column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 280px; width: var(--column-width, 280px);
min-width: 260px; min-width: var(--column-min-width, 260px);
max-width: 100%;
max-height: calc(100vh - var(--header-height) - 160px); max-height: calc(100vh - var(--header-height) - 160px);
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border-light); border: 1px solid var(--border-light);
@@ -697,7 +823,10 @@
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
flex: 1; flex: 1;
padding-right: var(--spacing-8); min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.column-count { .column-count {
@@ -716,12 +845,12 @@
.column-actions { .column-actions {
display: flex; display: flex;
align-items: center;
gap: var(--spacing-1); gap: var(--spacing-1);
opacity: 0; opacity: 0;
transition: opacity var(--transition-fast); transition: opacity var(--transition-fast);
position: absolute; flex-shrink: 0;
top: var(--spacing-3); margin-left: var(--spacing-2);
right: var(--spacing-3);
} }
.column-actions .btn-icon { .column-actions .btn-icon {
@@ -1276,75 +1405,3 @@
gap: var(--spacing-2); gap: var(--spacing-2);
} }
/* Base Multi-Column Layout - aktiviert das Feature, aber zeigt noch einspaltig */
.board.multi-column-layout .column-body {
/* Bleibt erstmal bei flex layout bis Inhalt zu lang wird */
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
/* Dynamisch aktivierte 2-spaltige Ansicht (wenn Scrollen nötig wäre) */
.board.multi-column-layout .column-body.dynamic-2-columns {
display: grid;
grid-template-columns: 1fr 1fr;
grid-auto-flow: row;
gap: var(--spacing-2);
align-content: start;
overflow-x: hidden;
overflow-y: auto;
}
/* Dynamisch aktivierte 3-spaltige Ansicht (wenn viel Inhalt) */
.board.multi-column-layout .column-body.dynamic-3-columns {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-auto-flow: row;
gap: var(--spacing-2);
align-content: start;
overflow-x: hidden;
overflow-y: auto;
}
/* Spalten-Breite wenn erweitert */
.board.multi-column-layout .column {
transition: width var(--transition-default), min-width var(--transition-default);
}
.board.multi-column-layout .column.expanded-2x {
width: auto;
min-width: 560px;
max-width: 640px;
}
.board.multi-column-layout .column.expanded-3x {
width: auto;
min-width: 840px;
max-width: 960px;
}
/* Task Cards im Multi-Column Layout */
.board.multi-column-layout .task-card {
width: 100%;
box-sizing: border-box;
}
/* Hover-Effekt für Layout-Toggle Button */
#btn-toggle-layout {
transition: all var(--transition-fast);
}
#btn-toggle-layout:hover {
transform: rotate(90deg);
}
/* Active state indicator - korrigiert für richtige Selektion */
.view-board.active .board.multi-column-layout ~ * {
/* Dummy rule to ensure the layout class is applied */
}
/* Layout toggle button active state */
#btn-toggle-layout.active {
color: var(--primary);
background: var(--primary-light);
}

Datei anzeigen

@@ -9,7 +9,7 @@
======================================== */ ======================================== */
.view-calendar { .view-calendar {
display: flex; display: none;
flex-direction: column; flex-direction: column;
padding: var(--spacing-6); padding: var(--spacing-6);
gap: var(--spacing-4); gap: var(--spacing-4);
@@ -17,6 +17,10 @@
height: 100%; height: 100%;
} }
.view-calendar.active {
display: flex !important;
}
/* Calendar Header */ /* Calendar Header */
.calendar-header { .calendar-header {
display: flex; display: flex;

Datei anzeigen

@@ -175,8 +175,8 @@
} }
.git-status-badge.ahead { .git-status-badge.ahead {
background: rgba(59, 130, 246, 0.15); background: rgba(124, 141, 181, 0.15);
color: #3B82F6; color: var(--info);
} }
.git-status-badge.behind { .git-status-badge.behind {

Datei anzeigen

@@ -170,7 +170,7 @@ select {
width: 100%; width: 100%;
padding: 10px 14px; padding: 10px 14px;
font-family: var(--font-primary); font-family: var(--font-primary);
font-size: var(--text-sm); font-size: var(--text-base); /* 16px - verhindert iOS Zoom bei Focus */
color: var(--text-primary); color: var(--text-primary);
background: var(--bg-input); background: var(--bg-input);
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
@@ -604,7 +604,8 @@ input[type="color"] {
align-items: center; align-items: center;
gap: var(--spacing-3); gap: var(--spacing-3);
width: 100%; width: 100%;
padding: var(--spacing-2) var(--spacing-3); padding: var(--spacing-3) var(--spacing-4);
min-height: 44px;
font-size: var(--text-sm); font-size: var(--text-sm);
color: var(--text-secondary); color: var(--text-secondary);
background: none; background: none;

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

@@ -796,7 +796,7 @@
.drop-zone.drag-over { .drop-zone.drag-over {
border-color: var(--primary); border-color: var(--primary);
background: rgba(59, 130, 246, 0.05); background: rgba(200, 168, 81, 0.08);
color: var(--primary); color: var(--primary);
} }

Datei anzeigen

@@ -287,10 +287,10 @@
background-color: transparent; background-color: transparent;
} }
25%, 75% { 25%, 75% {
background-color: var(--primary-100, rgba(59, 130, 246, 0.1)); background-color: var(--primary-100, rgba(200, 168, 81, 0.1));
} }
50% { 50% {
background-color: var(--primary-200, rgba(59, 130, 246, 0.2)); background-color: var(--primary-200, rgba(200, 168, 81, 0.2));
} }
} }

Datei anzeigen

@@ -13,7 +13,7 @@
} }
.view-list.active { .view-list.active {
display: flex; display: flex !important;
} }
/* List Header */ /* List Header */

Datei anzeigen

@@ -71,6 +71,82 @@
transform: translateY(-8px) rotate(-45deg); transform: translateY(-8px) rotate(-45deg);
} }
/* ========================================
MOBILE COLUMN INDICATOR
======================================== */
.mobile-column-indicator {
position: fixed;
bottom: 20px;
bottom: calc(20px + env(safe-area-inset-bottom, 0px));
left: 50%;
transform: translateX(-50%);
z-index: 100;
text-align: center;
pointer-events: none;
}
.column-dots {
display: flex;
gap: 8px;
justify-content: center;
margin-bottom: 8px;
}
.column-dots .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-tertiary);
opacity: 0.3;
transition: all 0.3s ease;
}
.column-dots .dot.active {
background: var(--primary);
opacity: 1;
transform: scale(1.2);
}
.column-name {
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
/* Mobile view hint */
.mobile-view-hint {
position: fixed;
top: 50%;
transform: translateY(-50%);
z-index: 100;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 20px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
opacity: 0;
pointer-events: none;
transition: all 0.3s ease;
}
.mobile-view-hint.visible {
opacity: 1;
}
.mobile-view-hint.left {
left: 20px;
}
.mobile-view-hint.right {
right: 20px;
}
/* ======================================== /* ========================================
MOBILE SLIDE-IN MENU MOBILE SLIDE-IN MENU
======================================== */ ======================================== */
@@ -436,6 +512,221 @@
min-height: 44px; min-height: 44px;
min-width: 44px; min-width: 44px;
} }
/* ========================================
MOBILE BOARD OPTIMIERUNG
======================================== */
/* Task Cards - Groesser und besser lesbar */
.task-card {
padding: var(--spacing-4);
border-radius: var(--radius-xl);
}
.task-title {
font-size: 1rem;
line-height: 1.4;
font-weight: var(--font-semibold);
}
.task-card-meta {
font-size: var(--text-sm);
gap: var(--spacing-4);
margin-top: var(--spacing-4);
}
.task-meta-item {
gap: 6px;
}
.task-meta-item .icon {
width: 18px;
height: 18px;
}
.task-card-labels {
gap: 8px;
margin-top: var(--spacing-4);
}
.task-label {
padding: 6px 12px;
font-size: var(--text-sm);
}
.task-card-footer {
margin-top: var(--spacing-4);
padding-top: var(--spacing-4);
}
.task-assignee-avatar {
width: 32px;
height: 32px;
font-size: 12px;
}
.task-counts {
font-size: var(--text-sm);
gap: var(--spacing-4);
}
/* Priority Stars groesser */
.priority-stars {
font-size: 16px;
}
/* Column Header */
.column-header {
padding: var(--spacing-4);
}
.column-title {
font-size: var(--text-base);
}
.column-count {
min-width: 28px;
height: 28px;
font-size: var(--text-sm);
}
/* Column Body */
.column-body {
padding: var(--spacing-3);
gap: var(--spacing-3);
}
/* Add Task Button */
.btn-add-task {
padding: var(--spacing-4);
font-size: var(--text-base);
min-height: 52px;
}
/* Filter Bar */
.filter-bar {
padding: var(--spacing-4);
gap: var(--spacing-3);
}
.filter-select {
min-height: 44px;
font-size: var(--text-base);
padding: var(--spacing-3) var(--spacing-4);
}
/* Stats Bar */
.board-stats-bar {
padding: var(--spacing-3) var(--spacing-4);
gap: var(--spacing-4);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.board-stat {
flex-shrink: 0;
}
.board-stat-icon {
width: 36px;
height: 36px;
}
.board-stat-icon svg {
width: 20px;
height: 20px;
}
.board-stat-value {
font-size: var(--text-lg);
}
.board-stat-label {
font-size: var(--text-sm);
}
/* Week Strip */
.week-strip {
padding: var(--spacing-3) var(--spacing-4);
}
.week-strip-nav {
width: 40px;
height: 40px;
}
.week-strip-nav svg {
width: 20px;
height: 20px;
}
.week-strip-day {
padding: var(--spacing-2) var(--spacing-3);
}
.week-strip-day-name {
font-size: 12px;
}
.week-strip-day-number {
font-size: var(--text-lg);
}
.week-strip-today {
font-size: var(--text-sm);
padding: var(--spacing-2) var(--spacing-3);
min-height: 40px;
}
/* Board Padding */
.board {
padding: var(--spacing-3);
gap: var(--spacing-3);
}
/* Modal auf Mobile */
.modal-body {
padding: var(--spacing-5);
}
.form-group label {
font-size: var(--text-base);
margin-bottom: var(--spacing-3);
}
/* Subtasks groesser */
.subtask-item {
padding: var(--spacing-3) var(--spacing-4);
min-height: 48px;
}
.subtask-title {
font-size: var(--text-base);
}
/* Comments groesser */
.comment-content {
padding: var(--spacing-4);
}
.comment-text {
font-size: var(--text-base);
line-height: 1.6;
}
/* Attachments */
.attachment-name {
font-size: var(--text-base);
}
/* Links */
.link-item {
padding: var(--spacing-4);
}
.link-title {
font-size: var(--text-base);
}
} }
/* ======================================== /* ========================================
@@ -469,4 +760,150 @@
.hamburger-btn.active .hamburger-line:nth-child(3) { .hamburger-btn.active .hamburger-line:nth-child(3) {
transform: translateY(-7px) rotate(-45deg); transform: translateY(-7px) rotate(-45deg);
} }
/* ========================================
EXTRA SMALL: NOCH GROESSERE ELEMENTE
======================================== */
/* Task Cards noch groesser */
.task-card {
padding: var(--spacing-5);
}
.task-title {
font-size: 1.125rem;
line-height: 1.5;
}
.task-card-meta {
font-size: var(--text-base);
}
.task-label {
padding: 8px 14px;
font-size: var(--text-base);
}
/* Column */
.column-header {
padding: var(--spacing-4) var(--spacing-5);
}
.column-title {
font-size: var(--text-lg);
}
.column-body {
padding: var(--spacing-4);
gap: var(--spacing-4);
}
/* Filter - vertikal auf sehr kleinen Screens */
.filter-bar {
flex-direction: column;
align-items: stretch;
}
.filter-group {
flex-direction: column;
align-items: stretch;
gap: var(--spacing-3);
}
.filter-group label {
display: none;
}
.filter-select {
width: 100%;
min-height: 48px;
}
.filter-actions {
margin-left: 0;
justify-content: center;
flex-wrap: wrap;
}
/* Stats Bar - scrollbar horizontal */
.board-stats-bar {
justify-content: flex-start;
gap: var(--spacing-5);
padding: var(--spacing-4);
}
.board-stat-icon {
width: 40px;
height: 40px;
}
.board-stat-value {
font-size: var(--text-xl);
}
/* Week Strip kompakter */
.week-strip {
padding: var(--spacing-2) var(--spacing-3);
}
.week-strip-day {
padding: var(--spacing-2);
}
.week-strip-day-name {
font-size: 11px;
}
.week-strip-day-number {
font-size: var(--text-base);
}
/* Login Screen */
.login-container {
padding: var(--spacing-6);
}
.login-header h1 {
font-size: var(--text-xl);
}
.login-form .form-group label {
font-size: var(--text-base);
}
/* Modals */
.modal {
width: 100%;
max-width: 100%;
height: 100%;
max-height: 100%;
border-radius: 0;
}
.modal-header {
padding: var(--spacing-4);
}
.modal-header h2 {
font-size: var(--text-lg);
}
.modal-body {
padding: var(--spacing-4);
}
.modal-footer {
padding: var(--spacing-4);
}
/* Form Groups */
.form-row {
grid-template-columns: 1fr;
}
/* Add Task Button */
.btn-add-task {
min-height: 56px;
font-size: var(--text-lg);
}
} }

Datei anzeigen

@@ -77,8 +77,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 32px; width: 44px;
height: 32px; height: 44px;
font-size: 20px; font-size: 20px;
color: var(--text-muted); color: var(--text-muted);
background: none; background: none;
@@ -770,7 +770,9 @@
.lightbox-close { .lightbox-close {
position: absolute; position: absolute;
top: var(--spacing-6); top: var(--spacing-6);
top: calc(var(--spacing-6) + env(safe-area-inset-top, 0px));
right: var(--spacing-6); right: var(--spacing-6);
right: calc(var(--spacing-6) + env(safe-area-inset-right, 0px));
width: 48px; width: 48px;
height: 48px; height: 48px;
font-size: 32px; font-size: 32px;

Datei anzeigen

@@ -59,16 +59,16 @@
============================================================================= */ ============================================================================= */
.notification-dropdown { .notification-dropdown {
position: absolute; position: fixed;
top: calc(100% + 8px); top: 60px;
right: 0; right: 20px;
width: 380px; width: 380px;
max-height: 520px; max-height: 520px;
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
z-index: var(--z-dropdown); z-index: 10001;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

Datei anzeigen

@@ -175,7 +175,7 @@
.reminder-number-input:focus { .reminder-number-input:focus {
border-color: var(--primary); border-color: var(--primary);
outline: none; outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); box-shadow: var(--shadow-focus);
} }
.reminder-unit-select { .reminder-unit-select {
@@ -200,7 +200,7 @@
.reminder-unit-select:focus { .reminder-unit-select:focus {
border-color: var(--primary); border-color: var(--primary);
outline: none; outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); box-shadow: var(--shadow-focus);
} }
.reminder-advance-suffix { .reminder-advance-suffix {
@@ -293,7 +293,7 @@
.custom-select-trigger.active { .custom-select-trigger.active {
border-color: var(--primary); border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); box-shadow: var(--shadow-focus);
} }
.custom-select-value { .custom-select-value {
@@ -477,7 +477,7 @@
border-color: #1d4ed8; border-color: #1d4ed8;
color: white !important; color: white !important;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 3px 8px rgba(59, 130, 246, 0.4); box-shadow: 0 3px 8px rgba(200, 168, 81, 0.4);
} }
#reminder-modal .btn-secondary { #reminder-modal .btn-secondary {

Datei anzeigen

@@ -4,6 +4,57 @@
* Mobile und Tablet Anpassungen * Mobile und Tablet Anpassungen
*/ */
/* ========================================
GLOBALE RESPONSIVE ANPASSUNGEN
======================================== */
/* Verhindere horizontales Scrollen */
html {
overflow-x: hidden;
width: 100%;
}
body {
overflow-x: hidden;
width: 100%;
margin: 0;
padding: 0;
}
/* App Container */
.app {
width: 100%;
max-width: 100%;
overflow-x: hidden;
position: relative;
}
/* Hauptcontainer */
.main {
width: 100%;
max-width: 100%;
overflow-x: hidden;
position: relative;
}
/* Alle Views */
.view {
width: 100%;
max-width: 100%;
overflow-x: hidden;
box-sizing: border-box;
}
/* Verhindere Überlauf bei allen Elementen */
* {
max-width: 100%;
}
/* Responsive Box-Sizing für alle Elemente */
* {
box-sizing: border-box;
}
/* ======================================== /* ========================================
TABLET (max 1024px) TABLET (max 1024px)
======================================== */ ======================================== */
@@ -31,33 +82,91 @@
} }
} }
/* ========================================
MEDIUM SCREENS (max 1200px)
======================================== */
@media (max-width: 1200px) {
/* Header-Anpassungen für mittlere Bildschirme */
.header {
padding: var(--spacing-sm) var(--spacing-md);
}
.header-left {
gap: var(--spacing-sm);
}
.header-right {
gap: var(--spacing-sm);
}
/* Project Selector schmaler */
.project-select {
max-width: 200px;
}
/* Search Container schmaler */
.search-container {
max-width: 200px;
}
/* Navigation tabs - kompakter und zentriert bei mittleren Bildschirmen */
.view-tabs {
gap: var(--spacing-xs);
flex-wrap: nowrap;
justify-content: center;
}
.view-tab {
padding: 6px 8px;
font-size: 11px;
flex-shrink: 0;
}
/* Modals anpassen */
.modal {
margin: var(--spacing-md);
}
.modal-large {
max-width: calc(100vw - 32px);
}
/* Tabellen responsiv */
table {
display: block;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
/* ======================================== /* ========================================
SMALL TABLET (max 768px) SMALL TABLET (max 768px)
======================================== */ ======================================== */
@media (max-width: 768px) { @media (max-width: 768px) {
.header { .header {
flex-wrap: wrap; display: flex;
height: auto; align-items: center;
padding: var(--spacing-sm) var(--spacing-md); height: 60px;
gap: var(--spacing-sm); padding: 0 var(--spacing-sm);
gap: var(--spacing-xs);
overflow: hidden;
} }
.header-left { .header-left {
flex: 1; flex: 0 0 auto;
order: 1; display: flex;
align-items: center;
gap: var(--spacing-xs);
} }
.header-center { .view-tabs-bar {
order: 3; display: none; /* Navigation wird ins Mobile Menu verschoben */
width: 100%;
justify-content: center;
padding-top: var(--spacing-sm);
border-top: 1px solid var(--border-default);
} }
.header-right { .header-right {
order: 2; flex: 0 0 auto;
} }
.logo { .logo {
@@ -72,28 +181,18 @@
display: none; display: none;
} }
.view-tabs { /* Mobile Navigation wird über Hamburger Menu gesteuert */
width: 100%; .mobile-menu {
justify-content: center; display: block;
} }
.view-tab { /* Logo kleiner auf Mobile */
flex: 1; .logo {
text-align: center; font-size: var(--text-lg);
} }
.filter-bar { .filter-bar-actions {
flex-direction: column; display: none;
gap: var(--spacing-md);
}
.filter-group {
flex-wrap: wrap;
justify-content: center;
}
.filter-actions {
justify-content: center;
} }
.view-board { .view-board {
@@ -101,16 +200,119 @@
gap: var(--spacing-md); gap: var(--spacing-md);
} }
/* Mobile Board - One column at a time */
.board-container {
overflow-x: hidden !important;
scroll-snap-type: none !important;
width: 100%;
padding: 0;
}
.column { .column {
width: calc(100vw - 32px); width: 100%;
min-width: calc(100vw - 32px); min-width: unset;
max-height: none; max-width: 100%;
max-height: calc(100vh - 200px);
margin: 0;
padding: var(--spacing-sm);
box-sizing: border-box;
}
.column.mobile-active {
display: flex !important;
} }
.btn-add-column { .btn-add-column {
width: calc(100vw - 32px); width: 100%;
min-width: calc(100vw - 32px); min-width: unset;
min-height: 100px; min-height: 100px;
margin: 0;
box-sizing: border-box;
}
/* Hide scrollbar in mobile board */
.board-container::-webkit-scrollbar {
display: none;
}
/* Fix for views in mobile */
.view.active {
display: flex !important;
}
/* List view mobile adjustments */
.view-list.active {
padding: var(--spacing-sm);
}
.list-header {
flex-direction: column;
align-items: stretch;
gap: var(--spacing-md);
padding: var(--spacing-md);
}
.list-controls {
flex-direction: column;
align-items: stretch;
}
.list-view-toggle {
justify-content: center;
}
.list-sort {
margin-left: 0;
justify-content: space-between;
}
/* Table responsive */
.list-table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.list-table {
min-width: 600px;
}
/* Calendar view mobile adjustments */
.view-calendar {
padding: var(--spacing-sm);
}
.calendar-header {
flex-direction: column;
gap: var(--spacing-md);
padding: var(--spacing-md);
}
.calendar-nav {
width: 100%;
justify-content: space-between;
}
.calendar-actions {
width: 100%;
justify-content: center;
}
.calendar-grid {
font-size: var(--text-xs);
}
.calendar-day-header {
padding: var(--spacing-sm);
}
.calendar-day {
min-height: 60px;
padding: var(--spacing-xs);
}
.calendar-task {
font-size: 9px;
padding: 1px 2px;
} }
.modal { .modal {
@@ -264,6 +466,7 @@
left: var(--spacing-md); left: var(--spacing-md);
right: var(--spacing-md); right: var(--spacing-md);
bottom: var(--spacing-md); bottom: var(--spacing-md);
bottom: calc(var(--spacing-md) + env(safe-area-inset-bottom, 0px));
} }
.toast { .toast {
@@ -291,7 +494,7 @@
@media print { @media print {
.header, .header,
.filter-bar, .view-tabs-bar,
.btn-add-column, .btn-add-column,
.column-actions, .column-actions,
.btn-add-task, .btn-add-task,

Datei anzeigen

@@ -9,26 +9,26 @@
FARBEN - Modernes Light Theme FARBEN - Modernes Light Theme
======================================== */ ======================================== */
/* Primärfarben */ /* Primärfarben - AegisSight Light Theme */
--primary: #4F46E5; --primary: #C8A851;
--primary-hover: #4338CA; --primary-hover: #B5923E;
--primary-light: #EEF2FF; --primary-light: rgba(200, 168, 81, 0.12);
--accent: #06B6D4; --accent: #C8A851;
--accent-hover: #0891B2; --accent-hover: #B5923E;
/* Hintergründe */ /* Hintergründe */
--bg-main: #F8FAFC; --bg-main: #E8EDF6;
--bg-secondary: #FFFFFF; --bg-secondary: #FFFFFF;
--bg-tertiary: #F1F5F9; --bg-tertiary: #F1F3F9;
--bg-hover: #E2E8F0; --bg-hover: #F8F9FC;
--bg-active: #CBD5E1; --bg-active: #D0D6DE;
--bg-sidebar: #FFFFFF; --bg-sidebar: #FFFFFF;
--bg-card: #FFFFFF; --bg-card: #FFFFFF;
--bg-input: #FFFFFF; --bg-input: #FFFFFF;
/* Textfarben */ /* Textfarben */
--text-primary: #0F172A; --text-primary: #0A1832;
--text-secondary: #475569; --text-secondary: #64748B;
--text-tertiary: #64748B; --text-tertiary: #64748B;
--text-muted: #94A3B8; --text-muted: #94A3B8;
--text-placeholder: #94A3B8; --text-placeholder: #94A3B8;
@@ -44,9 +44,9 @@
--error: #EF4444; --error: #EF4444;
--error-bg: #FEE2E2; --error-bg: #FEE2E2;
--error-text: #991B1B; --error-text: #991B1B;
--info: #3B82F6; --info: #7C8DB5;
--info-bg: #DBEAFE; --info-bg: rgba(124, 141, 181, 0.12);
--info-text: #1E40AF; --info-text: #4A5568;
/* Prioritätsfarben */ /* Prioritätsfarben */
--priority-high: #EF4444; --priority-high: #EF4444;
@@ -60,7 +60,7 @@
--border-light: #F1F5F9; --border-light: #F1F5F9;
--border-default: #E2E8F0; --border-default: #E2E8F0;
--border-dark: #CBD5E1; --border-dark: #CBD5E1;
--border-focus: #4F46E5; --border-focus: #C8A851;
/* Schatten */ /* Schatten */
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05); --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
@@ -68,12 +68,12 @@
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
--shadow-focus: 0 0 0 3px rgba(79, 70, 229, 0.2); --shadow-focus: 0 0 0 3px rgba(200, 168, 81, 0.25);
/* Scrollbar */ /* Scrollbar */
--scrollbar-bg: #F1F5F9; --scrollbar-bg: #E8EDF6;
--scrollbar-thumb: #CBD5E1; --scrollbar-thumb: #94A3B8;
--scrollbar-thumb-hover: #94A3B8; --scrollbar-thumb-hover: #64748B;
/* Overlay */ /* Overlay */
--overlay-bg: rgba(15, 23, 42, 0.5); --overlay-bg: rgba(15, 23, 42, 0.5);
@@ -158,7 +158,7 @@
Z-INDEX Z-INDEX
======================================== */ ======================================== */
--z-dropdown: 100; --z-dropdown: 300;
--z-sticky: 200; --z-sticky: 200;
--z-modal-overlay: 900; --z-modal-overlay: 900;
--z-modal: 1000; --z-modal: 1000;

Datei anzeigen

@@ -38,6 +38,7 @@
<link rel="stylesheet" href="css/knowledge.css"> <link rel="stylesheet" href="css/knowledge.css">
<link rel="stylesheet" href="css/reminders.css"> <link rel="stylesheet" href="css/reminders.css">
<link rel="stylesheet" href="css/contacts.css"> <link rel="stylesheet" href="css/contacts.css">
<link rel="stylesheet" href="css/contacts-modern.css">
<link rel="stylesheet" href="css/responsive.css"> <link rel="stylesheet" href="css/responsive.css">
<link rel="stylesheet" href="css/mobile.css"> <link rel="stylesheet" href="css/mobile.css">
<link rel="stylesheet" href="css/pwa.css"> <link rel="stylesheet" href="css/pwa.css">
@@ -182,8 +183,72 @@
</div> </div>
</div> </div>
<div class="header-center"> <div class="header-right">
<!-- View Tabs --> <!-- Search -->
<div class="search-container">
<input type="text" id="search-input" class="search-input" placeholder="Suchen...">
<button type="button" id="search-clear" class="search-clear hidden" title="Suche zurücksetzen (Esc)">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<div id="search-spinner" class="search-spinner hidden"></div>
</div>
<!-- Notification Bell -->
<div class="notification-bell" id="notification-bell">
<button id="notification-btn" class="btn btn-icon" title="Benachrichtigungen">
<svg class="notification-icon" viewBox="0 0 24 24">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.73 21a2 2 0 0 1-3.46 0" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span id="notification-badge" class="notification-badge hidden">0</span>
</button>
<div id="notification-dropdown" class="notification-dropdown hidden">
<div class="notification-header">
<h3>Benachrichtigungen</h3>
<button id="btn-mark-all-read" class="btn btn-text">Alle gelesen</button>
</div>
<div id="notification-list" class="notification-list">
<!-- Notifications rendered here -->
</div>
<div id="notification-empty" class="notification-empty hidden">
<svg class="notification-empty-icon" viewBox="0 0 24 24">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="M13.73 21a2 2 0 0 1-3.46 0" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
<p>Keine Benachrichtigungen</p>
</div>
</div>
</div>
<!-- Session Timer -->
<div id="session-timer" class="session-timer" title="Verbleibende Sitzungszeit">
<svg class="session-timer-icon" viewBox="0 0 24 24" width="16" height="16">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/>
<polyline points="12 6 12 12 16 14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span id="session-countdown">--:--</span>
</div>
<!-- User Menu -->
<div class="user-menu">
<button id="user-menu-btn" class="user-avatar">
<span id="user-initial">U</span>
</button>
<div id="user-dropdown" class="user-dropdown hidden">
<div class="user-info">
<span id="user-name">Benutzer</span>
<span id="user-role">Angemeldet</span>
</div>
<div class="dropdown-divider"></div>
<button id="btn-settings" class="dropdown-item">Einstellungen</button>
<div class="dropdown-divider"></div>
<button id="btn-logout" class="dropdown-item text-danger">Abmelden</button>
</div>
</div>
</div>
</header>
<nav class="view-tabs-bar">
<nav class="view-tabs"> <nav class="view-tabs">
<button class="view-tab active" data-view="board"> <button class="view-tab active" data-view="board">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -247,79 +312,70 @@
Kontakte Kontakte
</button> </button>
</nav> </nav>
</div> <div class="filter-bar-actions">
<button id="btn-filter-toggle" class="btn btn-outline filter-toggle-btn">
<div class="header-right"> <svg class="icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>
<!-- Search --> <span>Filter</span>
<div class="search-container">
<svg class="search-icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2" fill="none"/><path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2"/></svg>
<input type="text" id="search-input" class="search-input" placeholder="Suchen...">
<button type="button" id="search-clear" class="search-clear hidden" title="Suche zurücksetzen (Esc)">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button> </button>
<div id="search-spinner" class="search-spinner hidden"></div> <button id="btn-show-archived" class="btn btn-outline filter-toggle-btn">
</div> <svg class="icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="5" rx="1"></rect><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"></path><path d="M10 12h4"></path></svg>
<span>Archiv</span>
<!-- Connection Status -->
<div id="connection-status" class="connection-status online" title="Verbunden">
<span class="status-dot"></span>
<span class="status-text">Online</span>
</div>
<!-- Notification Bell -->
<div class="notification-bell" id="notification-bell">
<button id="notification-btn" class="btn btn-icon" title="Benachrichtigungen">
<svg class="notification-icon" viewBox="0 0 24 24">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.73 21a2 2 0 0 1-3.46 0" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span id="notification-badge" class="notification-badge hidden">0</span>
</button> </button>
<div id="notification-dropdown" class="notification-dropdown hidden">
<div class="notification-header">
<h3>Benachrichtigungen</h3>
<button id="btn-mark-all-read" class="btn btn-text">Alle gelesen</button>
</div> </div>
<div id="notification-list" class="notification-list"> <div id="filter-popover" class="filter-popover hidden">
<!-- Notifications rendered here --> <div class="filter-popover-content">
<div class="filter-popover-row">
<label>Bearbeiter</label>
<div id="filter-assignee-list" class="filter-checkbox-list"></div>
</div> </div>
<div id="notification-empty" class="notification-empty hidden"> <div class="filter-popover-row">
<svg class="notification-empty-icon" viewBox="0 0 24 24"> <label>Prioritaet</label>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" stroke="currentColor" stroke-width="2" fill="none"/> <div id="filter-priority-list" class="filter-checkbox-list">
<path d="M13.73 21a2 2 0 0 1-3.46 0" stroke="currentColor" stroke-width="2" fill="none"/> <label class="filter-checkbox-item">
</svg> <input type="checkbox" name="filter-priority" value="high">
<p>Keine Benachrichtigungen</p> <span>Hoch</span>
</label>
<label class="filter-checkbox-item">
<input type="checkbox" name="filter-priority" value="medium">
<span>Mittel</span>
</label>
<label class="filter-checkbox-item">
<input type="checkbox" name="filter-priority" value="low">
<span>Niedrig</span>
</label>
</div>
</div>
<div class="filter-popover-row">
<label>Labels</label>
<div id="filter-labels-list" class="filter-checkbox-list"></div>
</div>
<div class="filter-popover-row">
<label>Faelligkeit</label>
<div id="filter-due-list" class="filter-checkbox-list">
<label class="filter-checkbox-item">
<input type="checkbox" name="filter-due" value="overdue">
<span>Ueberfaellig</span>
</label>
<label class="filter-checkbox-item">
<input type="checkbox" name="filter-due" value="today">
<span>Heute</span>
</label>
<label class="filter-checkbox-item">
<input type="checkbox" name="filter-due" value="week">
<span>Diese Woche</span>
</label>
<label class="filter-checkbox-item">
<input type="checkbox" name="filter-due" value="none">
<span>Ohne Datum</span>
</label>
</div> </div>
</div> </div>
</div> </div>
<div class="filter-popover-footer">
<!-- Session Timer --> <button id="btn-clear-filters" class="btn btn-text">Filter zurücksetzen</button>
<div id="session-timer" class="session-timer" title="Verbleibende Sitzungszeit">
<svg class="session-timer-icon" viewBox="0 0 24 24" width="16" height="16">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/>
<polyline points="12 6 12 12 16 14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span id="session-countdown">--:--</span>
</div>
<!-- User Menu -->
<div class="user-menu">
<button id="user-menu-btn" class="user-avatar">
<span id="user-initial">U</span>
</button>
<div id="user-dropdown" class="user-dropdown hidden">
<div class="user-info">
<span id="user-name">Benutzer</span>
<span id="user-role">Angemeldet</span>
</div>
<div class="dropdown-divider"></div>
<button id="btn-settings" class="dropdown-item">Einstellungen</button>
<div class="dropdown-divider"></div>
<button id="btn-logout" class="dropdown-item text-danger">Abmelden</button>
</div> </div>
</div> </div>
</div> </nav>
</header>
<!-- Offline Banner --> <!-- Offline Banner -->
<div id="offline-banner" class="offline-banner hidden"> <div id="offline-banner" class="offline-banner hidden">
@@ -332,43 +388,8 @@
<!-- Board View --> <!-- Board View -->
<div id="view-board" class="view view-board active"> <div id="view-board" class="view view-board active">
<!-- Filter Bar -->
<div class="filter-bar">
<div class="filter-group">
<label>Filter:</label>
<select id="filter-assignee" class="filter-select">
<option value="">Alle Benutzer</option>
</select>
<select id="filter-priority" class="filter-select">
<option value="">Alle Prioritäten</option>
<option value="high">Hoch</option>
<option value="medium">Mittel</option>
<option value="low">Niedrig</option>
</select>
<select id="filter-labels" class="filter-select">
<option value="">Alle Labels</option>
</select>
<select id="filter-due" class="filter-select">
<option value="">Alle Fälligkeiten</option>
<option value="overdue">Überfällig</option>
<option value="today">Heute</option>
<option value="week">Diese Woche</option>
<option value="none">Ohne Datum</option>
</select>
</div>
<div class="filter-actions">
<button id="btn-toggle-layout" class="btn btn-icon" title="Layout umschalten">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
</svg>
</button>
<button id="btn-clear-filters" class="btn btn-text">Filter zurücksetzen</button>
<button id="btn-show-archived" class="btn btn-text">Archiv anzeigen</button>
</div>
</div>
<!-- Week Strip Calendar --> <!-- Week Strip Calendar -->
<div id="week-strip" class="week-strip"> <div id="week-strip" class="week-strip">
@@ -1051,6 +1072,26 @@
</div> </div>
</div> </div>
<!-- Column Select Modal -->
<div id="column-select-modal" class="modal modal-small hidden">
<div class="modal-header">
<h2>Spalte auswählen</h2>
<button class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<p id="column-select-message"></p>
<div class="form-group" style="margin-top: 1rem;">
<select id="column-select-dropdown" class="form-control">
<!-- Spalten werden dynamisch eingefügt -->
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" id="column-select-cancel" class="btn btn-secondary">Abbrechen</button>
<button type="button" id="column-select-ok" class="btn btn-primary">Wiederherstellen</button>
</div>
</div>
<!-- Archive Modal --> <!-- Archive Modal -->
<div id="archive-modal" class="modal modal-large hidden"> <div id="archive-modal" class="modal modal-large hidden">
<div class="modal-header"> <div class="modal-header">
@@ -2094,6 +2135,9 @@
<!-- Socket.io Client --> <!-- Socket.io Client -->
<script src="/socket.io/socket.io.js"></script> <script src="/socket.io/socket.io.js"></script>
<!-- Auth Fix -->
<script src="js/auth-fix.js"></script>
<!-- Main App (ES Module) --> <!-- Main App (ES Module) -->
<script type="module" src="js/app.js"></script> <script type="module" src="js/app.js"></script>
<script type="module" src="js/reminders.js"></script> <script type="module" src="js/reminders.js"></script>

Datei anzeigen

@@ -570,8 +570,10 @@ class ApiClient {
return this.put(`/tasks/${taskId}/archive`, { archived: true }); return this.put(`/tasks/${taskId}/archive`, { archived: true });
} }
async restoreTask(projectId, taskId) { async restoreTask(projectId, taskId, columnId = null) {
return this.put(`/tasks/${taskId}/archive`, { archived: false }); const data = { archived: false };
if (columnId) data.columnId = columnId;
return this.put(`/tasks/${taskId}/archive`, data);
} }
async getTaskHistory(projectId, taskId) { async getTaskHistory(projectId, taskId) {

Datei anzeigen

@@ -263,27 +263,33 @@ class App {
// Search - Hybrid search with client-side filtering and server search // Search - Hybrid search with client-side filtering and server search
this.setupSearch(); this.setupSearch();
// Filter changes // Filter toggle popover
$('#filter-priority')?.addEventListener('change', (e) => { $('#btn-filter-toggle')?.addEventListener('click', (e) => {
store.setFilter('priority', e.target.value); e.stopPropagation();
$('#filter-popover')?.classList.toggle('hidden');
}); });
$('#filter-assignee')?.addEventListener('change', (e) => { // Close popover on click outside
store.setFilter('assignee', e.target.value); document.addEventListener('click', (e) => {
const popover = $('#filter-popover');
const toggleBtn = $('#btn-filter-toggle');
if (popover && !popover.contains(e.target) && !toggleBtn?.contains(e.target)) {
popover.classList.add('hidden');
}
}); });
$('#filter-labels')?.addEventListener('change', (e) => { // Filter changes - checkbox listeners for static filters
store.setFilter('label', e.target.value); $('#filter-priority-list')?.addEventListener('change', () => this.applyCheckboxFilters());
}); $('#filter-due-list')?.addEventListener('change', () => this.applyCheckboxFilters());
$('#filter-assignee-list')?.addEventListener('change', () => this.applyCheckboxFilters());
$('#filter-due')?.addEventListener('change', (e) => { $('#filter-labels-list')?.addEventListener('change', () => this.applyCheckboxFilters());
store.setFilter('dueDate', e.target.value);
});
// Reset filters // Reset filters
$('#btn-reset-filters')?.addEventListener('click', () => { $('#btn-clear-filters')?.addEventListener('click', () => {
store.resetFilters(); store.resetFilters();
this.resetFilterInputs(); this.resetFilterInputs();
this.updateFilterButtonState();
$('#filter-popover')?.classList.add('hidden');
}); });
// Open archive modal // Open archive modal
@@ -301,6 +307,9 @@ class App {
// Confirm dialog events // Confirm dialog events
window.addEventListener('confirm:show', (e) => this.showConfirmDialog(e.detail)); window.addEventListener('confirm:show', (e) => this.showConfirmDialog(e.detail));
// Column select dialog events
window.addEventListener('column-select:show', (e) => this.showColumnSelectDialog(e.detail));
// Lightbox events // Lightbox events
window.addEventListener('lightbox:open', (e) => this.openLightbox(e.detail)); window.addEventListener('lightbox:open', (e) => this.openLightbox(e.detail));
@@ -702,11 +711,13 @@ class App {
// Initialize contacts view when switching to it // Initialize contacts view when switching to it
if (view === 'contacts') { if (view === 'contacts') {
window.initContactsPromise = window.initContactsPromise || import('./contacts.js').then(module => { window.initContactsPromise = window.initContactsPromise || import('./contacts.js').then(module => {
if (module.initContacts) { if (module.contactsManager && !module.contactsManager.isInitialized) {
return module.initContacts(); return module.contactsManager.init();
} }
}); });
window.initContactsPromise.catch(console.error); window.initContactsPromise.catch(error => {
console.error('[App] Contacts module error:', error);
});
} }
// Render list view when switching to it (delayed to ensure store is updated) // Render list view when switching to it (delayed to ensure store is updated)
@@ -752,65 +763,114 @@ class App {
} }
populateAssigneeFilter() { populateAssigneeFilter() {
const select = $('#filter-assignee'); const list = $('#filter-assignee-list');
if (!select) return; if (!list) return;
const users = store.get('users'); const users = store.get('users');
select.innerHTML = '<option value="all">Alle Bearbeiter</option>'; list.innerHTML = '';
users.forEach(user => { users.forEach(user => {
const option = document.createElement('option'); const label = document.createElement('label');
option.value = user.id; label.className = 'filter-checkbox-item';
option.textContent = user.displayName || user.email || 'Unbekannt'; label.innerHTML = `<input type="checkbox" name="filter-assignee" value="${user.id}"><span>${user.displayName || user.email || 'Unbekannt'}</span>`;
select.appendChild(option); list.appendChild(label);
}); });
} }
populateLabelFilter() { populateLabelFilter() {
const select = $('#filter-labels'); const list = $('#filter-labels-list');
if (!select) return; if (!list) return;
const labels = store.get('labels'); const labels = store.get('labels');
select.innerHTML = '<option value="all">Alle Labels</option>'; list.innerHTML = '';
labels.forEach(label => { labels.forEach(label => {
const option = document.createElement('option'); const item = document.createElement('label');
option.value = label.id; item.className = 'filter-checkbox-item';
option.textContent = label.name; item.innerHTML = `<input type="checkbox" name="filter-labels" value="${label.id}"><span>${label.name}</span>`;
select.appendChild(option); list.appendChild(item);
}); });
} }
applyCheckboxFilters() {
const getCheckedValues = (name) => {
const checkboxes = document.querySelectorAll(`input[name="${name}"]:checked`);
return Array.from(checkboxes).map(cb => cb.value);
};
const priority = getCheckedValues('filter-priority');
const assignee = getCheckedValues('filter-assignee');
const label = getCheckedValues('filter-labels');
const dueDate = getCheckedValues('filter-due');
store.setFilter('priority', priority.length > 0 ? priority : '');
store.setFilter('assignee', assignee.length > 0 ? assignee : '');
store.setFilter('label', label.length > 0 ? label : '');
store.setFilter('dueDate', dueDate.length > 0 ? dueDate : '');
this.updateFilterButtonState();
}
resetFilterInputs() { resetFilterInputs() {
const priority = $('#filter-priority'); const checkboxes = document.querySelectorAll('#filter-popover input[type="checkbox"]');
const assignee = $('#filter-assignee'); checkboxes.forEach(cb => cb.checked = false);
const labels = $('#filter-labels');
const dueDate = $('#filter-due');
const search = $('#search-input'); const search = $('#search-input');
const searchClear = $('#search-clear'); const searchClear = $('#search-clear');
const searchContainer = $('.search-container'); const searchContainer = $('.search-container');
if (priority) priority.value = 'all';
if (assignee) assignee.value = 'all';
if (labels) labels.value = 'all';
if (dueDate) dueDate.value = '';
if (search) search.value = ''; if (search) search.value = '';
if (searchClear) searchClear.classList.add('hidden'); if (searchClear) searchClear.classList.add('hidden');
if (searchContainer) searchContainer.classList.remove('has-search'); if (searchContainer) searchContainer.classList.remove('has-search');
} }
openArchiveModal() { updateFilterButtonState() {
this.handleModalOpen({ modalId: 'archive-modal' }); const btn = $('#btn-filter-toggle');
this.renderArchiveList(); if (!btn) return;
const groups = ['filter-priority', 'filter-assignee', 'filter-labels', 'filter-due'];
const activeCount = groups.filter(name => {
return document.querySelectorAll(`input[name="${name}"]:checked`).length > 0;
}).length;
const label = btn.querySelector('span');
if (label) {
label.textContent = activeCount > 0 ? `Filter (${activeCount})` : 'Filter';
}
btn.classList.toggle('has-filters', activeCount > 0);
} }
renderArchiveList() { async openArchiveModal() {
this.handleModalOpen({ modalId: 'archive-modal' });
const archiveList = $('#archive-list');
const archiveEmpty = $('#archive-empty');
// Ladeindikator anzeigen
if (archiveList) {
archiveList.classList.remove('hidden');
archiveList.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)">Lade archivierte Aufgaben...</div>';
}
if (archiveEmpty) archiveEmpty.classList.add('hidden');
try {
const projectId = store.get('currentProjectId');
const allTasks = await api.getTasks(projectId, { archived: true });
const archivedTasks = allTasks.filter(t => t.archived);
this.renderArchiveList(archivedTasks);
} catch (error) {
console.error('Failed to load archived tasks:', error);
if (archiveList) {
archiveList.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--danger)">Fehler beim Laden der archivierten Aufgaben</div>';
}
}
}
renderArchiveList(tasks) {
const archiveList = $('#archive-list'); const archiveList = $('#archive-list');
const archiveEmpty = $('#archive-empty'); const archiveEmpty = $('#archive-empty');
if (!archiveList) return; if (!archiveList) return;
// Get all archived tasks
const tasks = store.get('tasks').filter(t => t.archived);
const columns = store.get('columns'); const columns = store.get('columns');
if (tasks.length === 0) { if (tasks.length === 0) {
@@ -855,13 +915,16 @@ class App {
const projectId = store.get('currentProjectId'); const projectId = store.get('currentProjectId');
await api.restoreTask(projectId, taskId); await api.restoreTask(projectId, taskId);
store.updateTask(taskId, { archived: false }); store.updateTask(taskId, { archived: false });
this.renderArchiveList();
this.showSuccess('Aufgabe wiederhergestellt'); this.showSuccess('Aufgabe wiederhergestellt');
// Re-render board // Re-render board
if (this.board) { if (this.board) {
this.board.render(); this.board.render();
} }
// Archivliste neu laden
const allTasks = await api.getTasks(projectId, { archived: true });
this.renderArchiveList(allTasks.filter(t => t.archived));
} catch (error) { } catch (error) {
console.error('Restore error:', error); console.error('Restore error:', error);
this.showError('Fehler beim Wiederherstellen'); this.showError('Fehler beim Wiederherstellen');
@@ -889,8 +952,11 @@ class App {
const projectId = store.get('currentProjectId'); const projectId = store.get('currentProjectId');
await api.deleteTask(projectId, taskId); await api.deleteTask(projectId, taskId);
store.removeTask(taskId); store.removeTask(taskId);
this.renderArchiveList();
this.showSuccess('Aufgabe gelöscht'); this.showSuccess('Aufgabe gelöscht');
// Archivliste neu laden
const allTasks = await api.getTasks(projectId, { archived: true });
this.renderArchiveList(allTasks.filter(t => t.archived));
} catch (error) { } catch (error) {
this.showError('Fehler beim Löschen'); this.showError('Fehler beim Löschen');
} }
@@ -944,9 +1010,9 @@ class App {
import('./contacts.js').then(module => { import('./contacts.js').then(module => {
if (module.contactsManager) { if (module.contactsManager) {
module.contactsManager.searchQuery = ''; module.contactsManager.searchQuery = '';
module.contactsManager.filterContacts(); module.contactsManager.loadContacts();
} }
}); }).catch(() => {});
// Cancel any pending server search // Cancel any pending server search
if (searchAbortController) { if (searchAbortController) {
@@ -1027,9 +1093,9 @@ class App {
import('./contacts.js').then(module => { import('./contacts.js').then(module => {
if (module.contactsManager) { if (module.contactsManager) {
module.contactsManager.searchQuery = value; module.contactsManager.searchQuery = value;
module.contactsManager.filterContacts(); module.contactsManager.loadContacts();
} }
}); }).catch(() => {});
} else { } else {
// Immediate client-side filtering for tasks // Immediate client-side filtering for tasks
store.setFilter('search', value); store.setFilter('search', value);
@@ -1283,6 +1349,55 @@ class App {
cancelBtn?.addEventListener('click', handleCancel); cancelBtn?.addEventListener('click', handleCancel);
} }
// =====================
// COLUMN SELECT DIALOG
// =====================
showColumnSelectDialog({ message, columns, onSelect }) {
const modal = $('#column-select-modal');
const messageEl = $('#column-select-message');
const dropdown = $('#column-select-dropdown');
const confirmBtn = $('#column-select-ok');
const cancelBtn = $('#column-select-cancel');
if (messageEl) messageEl.textContent = message;
// Spalten in Dropdown einfügen
if (dropdown) {
dropdown.innerHTML = '';
columns.forEach(column => {
const option = document.createElement('option');
option.value = column.id;
option.textContent = column.name;
dropdown.appendChild(option);
});
}
// Show modal
this.handleModalOpen({ modalId: 'column-select-modal' });
// One-time event handlers
const handleConfirm = () => {
const selectedColumnId = parseInt(dropdown?.value);
this.handleModalClose({ modalId: 'column-select-modal' });
if (onSelect && selectedColumnId) onSelect(selectedColumnId);
cleanup();
};
const handleCancel = () => {
this.handleModalClose({ modalId: 'column-select-modal' });
cleanup();
};
const cleanup = () => {
confirmBtn?.removeEventListener('click', handleConfirm);
cancelBtn?.removeEventListener('click', handleCancel);
};
confirmBtn?.addEventListener('click', handleConfirm);
cancelBtn?.addEventListener('click', handleCancel);
}
// ===================== // =====================
// LIGHTBOX // LIGHTBOX
// ===================== // =====================

Datei anzeigen

@@ -26,9 +26,6 @@ class BoardManager {
this.weekStripDate = this.getMonday(new Date()); this.weekStripDate = this.getMonday(new Date());
this.tooltip = null; this.tooltip = null;
// Layout preferences
this.multiColumnLayout = this.loadLayoutPreference();
this.init(); this.init();
} }
@@ -56,20 +53,6 @@ class BoardManager {
this.renderWeekStrip(); this.renderWeekStrip();
}); });
// Apply initial layout preference
setTimeout(() => {
this.applyLayoutClass();
if (this.multiColumnLayout) {
this.checkAndApplyDynamicLayout();
}
}, 100);
// Re-check layout on window resize
window.addEventListener('resize', debounce(() => {
if (this.multiColumnLayout) {
this.checkAndApplyDynamicLayout();
}
}, 250));
} }
// ===================== // =====================
@@ -163,12 +146,6 @@ class BoardManager {
// Keyboard navigation // Keyboard navigation
document.addEventListener('keydown', (e) => this.handleKeyboard(e)); document.addEventListener('keydown', (e) => this.handleKeyboard(e));
// Layout toggle button - use delegated event handling
document.addEventListener('click', (e) => {
if (e.target.closest('#btn-toggle-layout')) {
this.toggleLayout();
}
});
} }
// ===================== // =====================
@@ -181,9 +158,6 @@ class BoardManager {
const columns = store.get('columns'); const columns = store.get('columns');
clearElement(this.boardElement); clearElement(this.boardElement);
// Apply layout class
this.applyLayoutClass();
columns.forEach(column => { columns.forEach(column => {
const columnElement = this.createColumnElement(column); const columnElement = this.createColumnElement(column);
this.boardElement.appendChild(columnElement); this.boardElement.appendChild(columnElement);
@@ -199,8 +173,8 @@ class BoardManager {
]); ]);
this.boardElement.appendChild(addColumnBtn); this.boardElement.appendChild(addColumnBtn);
// Check dynamic layout after render // Emit event for mobile column navigation
setTimeout(() => this.checkAndApplyDynamicLayout(), 100); document.dispatchEvent(new CustomEvent('columns:updated'));
} }
createColumnElement(column) { createColumnElement(column) {
@@ -506,9 +480,6 @@ class BoardManager {
countElement.textContent = filteredTasks.length.toString(); countElement.textContent = filteredTasks.length.toString();
} }
}); });
// Check if dynamic layout adjustment is needed
setTimeout(() => this.checkAndApplyDynamicLayout(), 100);
} }
// ===================== // =====================
@@ -1496,102 +1467,6 @@ class BoardManager {
return div.innerHTML; return div.innerHTML;
} }
// =====================
// LAYOUT PREFERENCES
// =====================
loadLayoutPreference() {
const stored = localStorage.getItem('taskmate:boardLayout');
return stored === 'multiColumn';
}
saveLayoutPreference(multiColumn) {
localStorage.setItem('taskmate:boardLayout', multiColumn ? 'multiColumn' : 'single');
}
toggleLayout() {
this.multiColumnLayout = !this.multiColumnLayout;
this.saveLayoutPreference(this.multiColumnLayout);
this.applyLayoutClass();
this.checkAndApplyDynamicLayout();
this.showSuccess(this.multiColumnLayout
? 'Mehrspalten-Layout aktiviert'
: 'Einspalten-Layout aktiviert'
);
}
applyLayoutClass() {
const toggleBtn = $('#btn-toggle-layout');
if (this.multiColumnLayout) {
this.boardElement?.classList.add('multi-column-layout');
toggleBtn?.classList.add('active');
} else {
this.boardElement?.classList.remove('multi-column-layout');
toggleBtn?.classList.remove('active');
// Remove all dynamic classes when disabled
const columns = this.boardElement?.querySelectorAll('.column');
columns?.forEach(column => {
const columnBody = column.querySelector('.column-body');
columnBody?.classList.remove('dynamic-2-columns', 'dynamic-3-columns');
column.classList.remove('expanded-2x', 'expanded-3x');
});
}
}
checkAndApplyDynamicLayout() {
if (!this.multiColumnLayout || !this.boardElement) return;
// Debug logging
console.log('[Layout Check] Checking dynamic layout...');
// Check each column to see if scrolling is needed
const columns = this.boardElement.querySelectorAll('.column');
columns.forEach(column => {
const columnBody = column.querySelector('.column-body');
if (!columnBody) return;
// Remove dynamic classes first
columnBody.classList.remove('dynamic-2-columns', 'dynamic-3-columns');
column.classList.remove('expanded-2x', 'expanded-3x');
// Force reflow to get accurate measurements
columnBody.offsetHeight;
const scrollHeight = columnBody.scrollHeight;
const clientHeight = columnBody.clientHeight;
const hasOverflow = scrollHeight > clientHeight;
console.log('[Layout Check]', {
column: column.dataset.columnId,
scrollHeight,
clientHeight,
hasOverflow,
ratio: scrollHeight / clientHeight
});
// Check if content overflows
if (hasOverflow && clientHeight > 0) {
// Calculate how many columns we need based on content height
const ratio = scrollHeight / clientHeight;
if (ratio > 2.5 && window.innerWidth >= 1800) {
// Need 3 columns
console.log('[Layout] Applying 3 columns');
columnBody.classList.add('dynamic-3-columns');
column.classList.add('expanded-3x');
} else if (ratio > 1.1 && window.innerWidth >= 1400) {
// Need 2 columns (reduced threshold to 1.1)
console.log('[Layout] Applying 2 columns');
columnBody.classList.add('dynamic-2-columns');
column.classList.add('expanded-2x');
}
}
});
}
} }
// Create and export singleton // Create and export singleton

Datei anzeigen

@@ -712,7 +712,11 @@ class CalendarViewManager {
filteredTasks = filteredTasks.filter(task => { filteredTasks = filteredTasks.filter(task => {
const filterCategory = columnFilterMap[task.columnId]; const filterCategory = columnFilterMap[task.columnId];
// If no filter state exists for this category, default to showing "in_progress" // If column has 'in_progress' filter category, check if 'in_progress' filter is enabled
if (filterCategory === 'in_progress') {
return this.enabledFilters['in_progress'] !== false;
}
// For other categories, check their specific filter state
if (this.enabledFilters[filterCategory] === undefined) { if (this.enabledFilters[filterCategory] === undefined) {
// Default: show in_progress, hide open and completed // Default: show in_progress, hide open and completed
return filterCategory === 'in_progress'; return filterCategory === 'in_progress';

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

@@ -5,6 +5,7 @@
*/ */
import { $, $$ } from './utils.js'; import { $, $$ } from './utils.js';
import { enhanceMobileSwipe } from './mobile-swipe.js';
class MobileManager { class MobileManager {
constructor() { constructor() {
@@ -21,6 +22,8 @@ class MobileManager {
this.touchStartTime = 0; this.touchStartTime = 0;
this.isSwiping = false; this.isSwiping = false;
this.swipeDirection = null; this.swipeDirection = null;
this.currentColumnIndex = 0;
this.swipeTarget = null; // 'board' or 'header'
// Touch drag & drop state // Touch drag & drop state
this.touchDraggedElement = null; this.touchDraggedElement = null;
@@ -87,6 +90,9 @@ class MobileManager {
this.updateUserInfo(); this.updateUserInfo();
}); });
// Enhance with new swipe functionality
enhanceMobileSwipe(this);
console.log('[Mobile] Initialized'); console.log('[Mobile] Initialized');
} }
@@ -220,8 +226,18 @@ class MobileManager {
$$('.view').forEach(v => { $$('.view').forEach(v => {
const viewName = v.id.replace('view-', ''); const viewName = v.id.replace('view-', '');
const isActive = viewName === view; const isActive = viewName === view;
v.classList.toggle('active', isActive);
v.classList.toggle('hidden', !isActive); if (isActive) {
v.classList.add('active');
v.classList.remove('hidden');
// Force display for mobile
if (this.isMobile) {
v.style.display = '';
}
} else {
v.classList.remove('active');
v.classList.add('hidden');
}
}); });
// Update mobile nav // Update mobile nav
@@ -300,41 +316,42 @@ class MobileManager {
bindSwipeEvents() { bindSwipeEvents() {
if (!this.mainContent) return; if (!this.mainContent) return;
this.mainContent.addEventListener('touchstart', (e) => this.handleSwipeStart(e), { passive: true }); // Swipe für Board-Container (Spalten-Navigation)
this.mainContent.addEventListener('touchmove', (e) => this.handleSwipeMove(e), { passive: false }); const boardContainer = document.querySelector('.board-container');
this.mainContent.addEventListener('touchend', (e) => this.handleSwipeEnd(e), { passive: true }); if (boardContainer) {
this.mainContent.addEventListener('touchcancel', () => this.resetSwipe(), { passive: true }); boardContainer.addEventListener('touchstart', (e) => this.handleBoardSwipeStart(e), { passive: true });
boardContainer.addEventListener('touchmove', (e) => this.handleBoardSwipeMove(e), { passive: false });
boardContainer.addEventListener('touchend', (e) => this.handleBoardSwipeEnd(e), { passive: true });
boardContainer.addEventListener('touchcancel', () => this.resetSwipe(), { passive: true });
}
// Swipe für Header (View-Navigation)
const header = document.querySelector('.header');
if (header) {
header.addEventListener('touchstart', (e) => this.handleHeaderSwipeStart(e), { passive: true });
header.addEventListener('touchmove', (e) => this.handleHeaderSwipeMove(e), { passive: false });
header.addEventListener('touchend', (e) => this.handleHeaderSwipeEnd(e), { passive: true });
header.addEventListener('touchcancel', () => this.resetSwipe(), { passive: true });
}
} }
/** /**
* Handle swipe start * Handle board swipe start (für Spalten-Navigation)
*/ */
handleSwipeStart(e) { handleBoardSwipeStart(e) {
if (!this.isMobile) return; if (!this.isMobile || this.currentView !== 'board') return;
if (this.isMenuOpen || $('.modal-overlay:not(.hidden)')) return;
// Don't swipe if menu is open // Don't swipe on interactive elements
if (this.isMenuOpen) return;
// Don't swipe if modal is open
if ($('.modal-overlay:not(.hidden)')) return;
// Don't swipe on specific interactive elements, but allow swipe in column-body
const target = e.target; const target = e.target;
if (target.closest('.modal') || if (target.closest('button') ||
target.closest('.calendar-grid') ||
target.closest('.knowledge-entry-list') ||
target.closest('.list-table') ||
target.closest('input') || target.closest('input') ||
target.closest('textarea') || target.closest('textarea') ||
target.closest('select') || target.closest('select') ||
target.closest('button') || target.closest('.task-card')) {
target.closest('a[href]') ||
target.closest('.task-card .priority-stars') ||
target.closest('.task-card .task-counts')) {
return; return;
} }
// Only single touch
if (e.touches.length !== 1) return; if (e.touches.length !== 1) return;
this.touchStartX = e.touches[0].clientX; this.touchStartX = e.touches[0].clientX;
@@ -342,12 +359,79 @@ class MobileManager {
this.touchStartTime = Date.now(); this.touchStartTime = Date.now();
this.isSwiping = false; this.isSwiping = false;
this.swipeDirection = null; this.swipeDirection = null;
this.swipeTarget = 'board';
} }
/** /**
* Handle swipe move * Handle header swipe start (für View-Navigation)
*/ */
handleSwipeMove(e) { handleHeaderSwipeStart(e) {
if (!this.isMobile) return;
if (this.isMenuOpen || $('.modal-overlay:not(.hidden)')) return;
// Nur in der Header-Region swipen
if (!e.target.closest('.header')) return;
// Nicht auf Buttons swipen
if (e.target.closest('button') || e.target.closest('.view-tab')) return;
if (e.touches.length !== 1) return;
this.touchStartX = e.touches[0].clientX;
this.touchStartY = e.touches[0].clientY;
this.touchStartTime = Date.now();
this.isSwiping = false;
this.swipeDirection = null;
this.swipeTarget = 'header';
}
/**
* Handle board swipe move
*/
handleBoardSwipeMove(e) {
if (!this.isMobile || this.touchStartX === 0 || this.swipeTarget !== 'board') return;
const touch = e.touches[0];
this.touchCurrentX = touch.clientX;
this.touchCurrentY = touch.clientY;
const deltaX = this.touchCurrentX - this.touchStartX;
const deltaY = this.touchCurrentY - this.touchStartY;
// Determine direction
if (!this.swipeDirection && (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10)) {
if (Math.abs(deltaX) > Math.abs(deltaY) * 1.5) {
this.swipeDirection = 'horizontal';
this.isSwiping = true;
document.body.classList.add('is-swiping');
} else {
this.swipeDirection = 'vertical';
this.resetSwipe();
return;
}
}
if (this.swipeDirection !== 'horizontal') return;
e.preventDefault();
// Visual feedback für Spalten-Navigation
const columns = $$('.column');
if (deltaX > this.SWIPE_THRESHOLD && this.currentColumnIndex > 0) {
this.swipeIndicatorLeft?.classList.add('visible');
this.swipeIndicatorRight?.classList.remove('visible');
} else if (deltaX < -this.SWIPE_THRESHOLD && this.currentColumnIndex < columns.length - 1) {
this.swipeIndicatorRight?.classList.add('visible');
this.swipeIndicatorLeft?.classList.remove('visible');
} else {
this.swipeIndicatorLeft?.classList.remove('visible');
this.swipeIndicatorRight?.classList.remove('visible');
}
}
/**
* Handle header swipe move
*/
handleHeaderSwipeMove(e) {
if (!this.isMobile || this.touchStartX === 0) return; if (!this.isMobile || this.touchStartX === 0) return;
const touch = e.touches[0]; const touch = e.touches[0];
@@ -412,10 +496,11 @@ class MobileManager {
* Handle swipe end * Handle swipe end
*/ */
handleSwipeEnd() { handleSwipeEnd() {
if (!this.isSwiping || this.swipeDirection !== 'horizontal') { if (!this.isMobile || this.touchStartX === 0 || this.swipeTarget !== 'header') return;
this.resetSwipe();
return; const touch = e.touches[0];
} this.touchCurrentX = touch.clientX;
this.touchCurrentY = touch.clientY;
const deltaX = this.touchCurrentX - this.touchStartX; const deltaX = this.touchCurrentX - this.touchStartX;
const deltaTime = Date.now() - this.touchStartTime; const deltaTime = Date.now() - this.touchStartTime;

Datei anzeigen

@@ -32,10 +32,10 @@ class Store {
// Filters // Filters
filters: { filters: {
search: '', search: '',
priority: 'all', priority: '',
assignee: 'all', assignee: '',
label: 'all', label: '',
dueDate: 'all', dueDate: '',
archived: false archived: false
}, },
@@ -155,10 +155,10 @@ class Store {
labels: [], labels: [],
filters: { filters: {
search: '', search: '',
priority: 'all', priority: '',
assignee: 'all', assignee: '',
label: 'all', label: '',
dueDate: 'all', dueDate: '',
archived: false archived: false
}, },
searchResultIds: [], searchResultIds: [],
@@ -412,10 +412,10 @@ class Store {
this.setState({ this.setState({
filters: { filters: {
search: '', search: '',
priority: 'all', priority: '',
assignee: 'all', assignee: '',
label: 'all', label: '',
dueDate: 'all', dueDate: '',
archived: false archived: false
} }
}, 'RESET_FILTERS'); }, 'RESET_FILTERS');

Datei anzeigen

@@ -49,6 +49,16 @@ class SyncManager {
}); });
this.setupEventListeners(); this.setupEventListeners();
// Update body offline class based on sync status
store.subscribe('syncStatus', () => {
const status = store.get('syncStatus');
if (status === 'offline' || status === 'error') {
document.body.classList.add('offline');
} else {
document.body.classList.remove('offline');
}
});
} catch (error) { } catch (error) {
console.error('[Sync] Failed to connect:', error); console.error('[Sync] Failed to connect:', error);
store.setSyncStatus('error'); store.setSyncStatus('error');

Datei anzeigen

@@ -587,9 +587,37 @@ class TaskModalManager {
this.close(); this.close();
this.showSuccess('Aufgabe wiederhergestellt'); this.showSuccess('Aufgabe wiederhergestellt');
} catch (error) { } catch (error) {
// Prüfen ob Spaltenauswahl erforderlich ist
if (error.data?.requiresColumn) {
this.showColumnSelectDialog();
} else {
this.showError('Fehler beim Wiederherstellen'); this.showError('Fehler beim Wiederherstellen');
} }
} }
}
showColumnSelectDialog() {
const columns = store.get('columns');
// Modal für Spaltenauswahl erstellen
window.dispatchEvent(new CustomEvent('column-select:show', {
detail: {
message: 'Die ursprüngliche Spalte existiert nicht mehr. Bitte wählen Sie eine Spalte:',
columns: columns,
onSelect: async (columnId) => {
try {
const projectId = store.get('currentProjectId');
await api.restoreTask(projectId, this.taskId, columnId);
store.updateTask(this.taskId, { archived: false, columnId: columnId });
this.close();
this.showSuccess('Aufgabe wiederhergestellt');
} catch (error) {
this.showError('Fehler beim Wiederherstellen');
}
}
}
}));
}
async autoSaveDescription() { async autoSaveDescription() {
// Deprecated - use autoSaveTask instead // Deprecated - use autoSaveTask instead

Datei anzeigen

@@ -492,38 +492,56 @@ export function filterTasks(tasks, filters, searchResultIds = [], columns = [])
} }
} }
// Priority filter // Priority filter (string or array)
if (filters.priority && filters.priority !== 'all') { if (filters.priority && filters.priority.length > 0) {
if (task.priority !== filters.priority) return false; const priorityFilter = Array.isArray(filters.priority) ? filters.priority : [filters.priority];
if (!priorityFilter.includes(task.priority)) return false;
} }
// Assignee filter // Assignee filter (string or array)
if (filters.assignee && filters.assignee !== 'all') { if (filters.assignee && filters.assignee.length > 0) {
if (task.assignedTo !== parseInt(filters.assignee)) return false; const assigneeFilter = Array.isArray(filters.assignee) ? filters.assignee : [filters.assignee];
const assigneeIds = assigneeFilter.map(id => parseInt(id));
const taskAssigneeId = task.assignedTo;
const taskAssignees = task.assignees?.map(a => a.id || a) || [];
const matches = assigneeIds.some(id => id === taskAssigneeId || taskAssignees.includes(id));
if (!matches) return false;
} }
// Label filter // Label filter (string or array)
if (filters.label && filters.label !== 'all') { if (filters.label && filters.label.length > 0) {
const hasLabel = task.labels?.some(l => l.id === parseInt(filters.label)); const labelFilter = Array.isArray(filters.label) ? filters.label : [filters.label];
const labelIds = labelFilter.map(id => parseInt(id));
const hasLabel = task.labels?.some(l => labelIds.includes(l.id));
if (!hasLabel) return false; if (!hasLabel) return false;
} }
// Due date filter // Due date filter (string or array)
if (filters.dueDate && filters.dueDate !== 'all' && filters.dueDate !== '') { if (filters.dueDate && filters.dueDate.length > 0) {
const status = getDueDateStatus(task.dueDate); const dueDateFilter = Array.isArray(filters.dueDate) ? filters.dueDate : [filters.dueDate];
let matches = false;
// Bei "überfällig" erledigte Aufgaben ausschließen for (const filterVal of dueDateFilter) {
if (filters.dueDate === 'overdue') { if (filterVal === 'none') {
if (status !== 'overdue' || isTaskCompleted(task)) return false; if (!task.dueDate) { matches = true; break; }
} } else if (filterVal === 'overdue') {
if (filters.dueDate === 'today' && status !== 'today') return false; const status = getDueDateStatus(task.dueDate);
if (filters.dueDate === 'week') { if (status === 'overdue' && !isTaskCompleted(task)) { matches = true; break; }
} else if (filterVal === 'today') {
const status = getDueDateStatus(task.dueDate);
if (status === 'today') { matches = true; break; }
} else if (filterVal === 'week') {
if (task.dueDate) {
const due = new Date(task.dueDate); const due = new Date(task.dueDate);
const weekFromNow = new Date(); const weekFromNow = new Date();
weekFromNow.setDate(weekFromNow.getDate() + 7); weekFromNow.setDate(weekFromNow.getDate() + 7);
if (!task.dueDate || due > weekFromNow) return false; if (due <= weekFromNow) { matches = true; break; }
} }
} }
}
if (!matches) return false;
}
return true; return true;
}); });

Datei anzeigen

@@ -4,7 +4,7 @@
* Offline support and caching * Offline support and caching
*/ */
const CACHE_VERSION = '306'; const CACHE_VERSION = '390';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION; const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION; const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION; const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;