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:
503
CHANGELOG.txt
503
CHANGELOG.txt
@@ -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
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|||||||
1004
CLAUDE.md
1004
CLAUDE.md
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -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');
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = ?'
|
||||||
|
|||||||
@@ -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(`
|
|
||||||
SELECT c.*, u.display_name as creator_name
|
|
||||||
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({
|
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info('Kontakt aktualisiert', { contactId, userId });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Fehler beim Aktualisieren des Kontakts:', { error: error.message, contactId: req.params.id });
|
|
||||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
db.prepare('UPDATE contacts SET avatar_url = ? WHERE id = ?')
|
||||||
|
.run(avatarUrl, id);
|
||||||
// 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());
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { avatar_url: avatarUrl },
|
||||||
|
message: 'Avatar erfolgreich hochgeladen'
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Fehler beim Abrufen der Tags:', { error: error.message });
|
logger.error('Fehler beim Avatar-Upload:', error);
|
||||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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' });
|
||||||
}
|
}
|
||||||
|
|
||||||
db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
// Bei Wiederherstellung: Prüfen ob Spalte vorhanden
|
||||||
.run(archived ? 1 : 0, taskId);
|
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 = ?')
|
||||||
|
.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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.view-list.active {
|
.view-list.active {
|
||||||
display: flex;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List Header */
|
/* List Header */
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo kleiner auf Mobile */
|
||||||
|
.logo {
|
||||||
|
font-size: var(--text-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-tab {
|
.filter-bar-actions {
|
||||||
flex: 1;
|
display: none;
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-bar {
|
|
||||||
flex-direction: column;
|
|
||||||
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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,77 +183,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-center">
|
|
||||||
<!-- View Tabs -->
|
|
||||||
<nav class="view-tabs">
|
|
||||||
<button class="view-tab active" data-view="board">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
|
||||||
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
|
||||||
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
|
||||||
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
|
||||||
</svg>
|
|
||||||
Board
|
|
||||||
</button>
|
|
||||||
<button class="view-tab" data-view="list">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<line x1="8" y1="6" x2="21" y2="6"/>
|
|
||||||
<line x1="8" y1="12" x2="21" y2="12"/>
|
|
||||||
<line x1="8" y1="18" x2="21" y2="18"/>
|
|
||||||
<circle cx="3.5" cy="6" r="1" fill="currentColor"/>
|
|
||||||
<circle cx="3.5" cy="12" r="1" fill="currentColor"/>
|
|
||||||
<circle cx="3.5" cy="18" r="1" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
Liste
|
|
||||||
</button>
|
|
||||||
<button class="view-tab" data-view="calendar">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<rect x="3" y="4" width="18" height="18" rx="2"/>
|
|
||||||
<line x1="16" y1="2" x2="16" y2="6"/>
|
|
||||||
<line x1="8" y1="2" x2="8" y2="6"/>
|
|
||||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
|
||||||
<circle cx="12" cy="16" r="1" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
Kalender
|
|
||||||
</button>
|
|
||||||
<button class="view-tab" data-view="proposals">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M9 11l3 3L22 4"/>
|
|
||||||
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
|
|
||||||
</svg>
|
|
||||||
Genehmigung
|
|
||||||
</button>
|
|
||||||
<button class="view-tab" data-view="coding">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="16 18 22 12 16 6"/>
|
|
||||||
<polyline points="8 6 2 12 8 18"/>
|
|
||||||
<line x1="12" y1="20" x2="12" y2="4" transform="rotate(-15 12 12)"/>
|
|
||||||
</svg>
|
|
||||||
Coding
|
|
||||||
</button>
|
|
||||||
<button class="view-tab" data-view="knowledge">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
|
||||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
|
||||||
</svg>
|
|
||||||
Wissen
|
|
||||||
</button>
|
|
||||||
<button class="view-tab" data-view="contacts">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
|
||||||
<circle cx="9" cy="7" r="4"/>
|
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
||||||
</svg>
|
|
||||||
Kontakte
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<div class="search-container">
|
<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...">
|
<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)">
|
<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>
|
<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>
|
||||||
@@ -260,12 +193,6 @@
|
|||||||
<div id="search-spinner" class="search-spinner hidden"></div>
|
<div id="search-spinner" class="search-spinner hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 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 -->
|
<!-- Notification Bell -->
|
||||||
<div class="notification-bell" id="notification-bell">
|
<div class="notification-bell" id="notification-bell">
|
||||||
<button id="notification-btn" class="btn btn-icon" title="Benachrichtigungen">
|
<button id="notification-btn" class="btn btn-icon" title="Benachrichtigungen">
|
||||||
@@ -321,6 +248,135 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<nav class="view-tabs-bar">
|
||||||
|
<nav class="view-tabs">
|
||||||
|
<button class="view-tab active" data-view="board">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
||||||
|
</svg>
|
||||||
|
Board
|
||||||
|
</button>
|
||||||
|
<button class="view-tab" data-view="list">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="8" y1="6" x2="21" y2="6"/>
|
||||||
|
<line x1="8" y1="12" x2="21" y2="12"/>
|
||||||
|
<line x1="8" y1="18" x2="21" y2="18"/>
|
||||||
|
<circle cx="3.5" cy="6" r="1" fill="currentColor"/>
|
||||||
|
<circle cx="3.5" cy="12" r="1" fill="currentColor"/>
|
||||||
|
<circle cx="3.5" cy="18" r="1" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
Liste
|
||||||
|
</button>
|
||||||
|
<button class="view-tab" data-view="calendar">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2"/>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||||
|
<circle cx="12" cy="16" r="1" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
Kalender
|
||||||
|
</button>
|
||||||
|
<button class="view-tab" data-view="proposals">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M9 11l3 3L22 4"/>
|
||||||
|
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
|
||||||
|
</svg>
|
||||||
|
Genehmigung
|
||||||
|
</button>
|
||||||
|
<button class="view-tab" data-view="coding">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="16 18 22 12 16 6"/>
|
||||||
|
<polyline points="8 6 2 12 8 18"/>
|
||||||
|
<line x1="12" y1="20" x2="12" y2="4" transform="rotate(-15 12 12)"/>
|
||||||
|
</svg>
|
||||||
|
Coding
|
||||||
|
</button>
|
||||||
|
<button class="view-tab" data-view="knowledge">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
||||||
|
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
||||||
|
</svg>
|
||||||
|
Wissen
|
||||||
|
</button>
|
||||||
|
<button class="view-tab" data-view="contacts">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||||
|
<circle cx="9" cy="7" r="4"/>
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||||
|
</svg>
|
||||||
|
Kontakte
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<div class="filter-bar-actions">
|
||||||
|
<button id="btn-filter-toggle" class="btn btn-outline filter-toggle-btn">
|
||||||
|
<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>
|
||||||
|
<span>Filter</span>
|
||||||
|
</button>
|
||||||
|
<button id="btn-show-archived" class="btn btn-outline filter-toggle-btn">
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="filter-popover" class="filter-popover hidden">
|
||||||
|
<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 class="filter-popover-row">
|
||||||
|
<label>Prioritaet</label>
|
||||||
|
<div id="filter-priority-list" class="filter-checkbox-list">
|
||||||
|
<label class="filter-checkbox-item">
|
||||||
|
<input type="checkbox" name="filter-priority" value="high">
|
||||||
|
<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 class="filter-popover-footer">
|
||||||
|
<button id="btn-clear-filters" class="btn btn-text">Filter zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<!-- Offline Banner -->
|
<!-- Offline Banner -->
|
||||||
<div id="offline-banner" class="offline-banner hidden">
|
<div id="offline-banner" class="offline-banner hidden">
|
||||||
<svg class="icon" viewBox="0 0 24 24"><path d="M1 1l22 22M16.72 11.06A10.94 10.94 0 0 1 19 12.55M5 12.55a10.94 10.94 0 0 1 5.17-2.39M10.71 5.05A16 16 0 0 1 22.58 9M1.42 9a15.91 15.91 0 0 1 4.7-2.88M8.53 16.11a6 6 0 0 1 6.95 0M12 20h.01" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
<svg class="icon" viewBox="0 0 24 24"><path d="M1 1l22 22M16.72 11.06A10.94 10.94 0 0 1 19 12.55M5 12.55a10.94 10.94 0 0 1 5.17-2.39M10.71 5.05A16 16 0 0 1 22.58 9M1.42 9a15.91 15.91 0 0 1 4.7-2.88M8.53 16.11a6 6 0 0 1 6.95 0M12 20h.01" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||||
@@ -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>×</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">
|
||||||
@@ -2093,6 +2134,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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
// =====================
|
// =====================
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
@@ -180,9 +157,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);
|
||||||
@@ -198,9 +172,9 @@ class BoardManager {
|
|||||||
'Statuskarte hinzufügen'
|
'Statuskarte hinzufügen'
|
||||||
]);
|
]);
|
||||||
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
|
||||||
|
|||||||
@@ -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
@@ -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('input') ||
|
||||||
target.closest('.knowledge-entry-list') ||
|
target.closest('textarea') ||
|
||||||
target.closest('.list-table') ||
|
|
||||||
target.closest('input') ||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -587,10 +587,38 @@ class TaskModalManager {
|
|||||||
this.close();
|
this.close();
|
||||||
this.showSuccess('Aufgabe wiederhergestellt');
|
this.showSuccess('Aufgabe wiederhergestellt');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showError('Fehler beim Wiederherstellen');
|
// Prüfen ob Spaltenauswahl erforderlich ist
|
||||||
|
if (error.data?.requiresColumn) {
|
||||||
|
this.showColumnSelectDialog();
|
||||||
|
} else {
|
||||||
|
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
|
||||||
await this.autoSaveTask();
|
await this.autoSaveTask();
|
||||||
|
|||||||
@@ -492,37 +492,55 @@ 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; }
|
||||||
const due = new Date(task.dueDate);
|
} else if (filterVal === 'today') {
|
||||||
const weekFromNow = new Date();
|
const status = getDueDateStatus(task.dueDate);
|
||||||
weekFromNow.setDate(weekFromNow.getDate() + 7);
|
if (status === 'today') { matches = true; break; }
|
||||||
if (!task.dueDate || due > weekFromNow) return false;
|
} else if (filterVal === 'week') {
|
||||||
|
if (task.dueDate) {
|
||||||
|
const due = new Date(task.dueDate);
|
||||||
|
const weekFromNow = new Date();
|
||||||
|
weekFromNow.setDate(weekFromNow.getDate() + 7);
|
||||||
|
if (due <= weekFromNow) { matches = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!matches) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren