diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ebb126d..02e2571 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,509 @@ 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 ================================================================================ diff --git a/CLAUDE.md b/CLAUDE.md index 1570b60..3f26ed3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,777 +1,227 @@ -# TaskMate - Entwicklerdokumentation - -## ⚠️ WICHTIGER HINWEIS FÜR KI-ASSISTENTEN -Der Anwender hat **KEINE Programmierkenntnisse**. Das bedeutet: -- **DU übernimmst ALLE technischen Aufgaben vollständig** -- **Erkläre in einfachen Worten**, was du tust und warum -- **Frage NIEMALS nach technischen Details** oder Code-Schnipseln -- **Führe ALLE Schritte selbstständig aus** -- Der Anwender kann nur bestätigen/ablehnen, nicht selbst coden - -### Kommunikations-Regeln -✅ **RICHTIG**: "Ich werde jetzt die Benutzeroberfläche anpassen, damit..." -❌ **FALSCH**: "Kannst du mir den Code aus Zeile 42 zeigen?" - -✅ **RICHTIG**: "Ich starte jetzt den Server neu. Das dauert etwa 30 Sekunden." -❌ **FALSCH**: "Führe bitte folgenden Befehl aus: docker restart..." - -## 🚀 Quick Start - -### Wichtigste Befehle -```bash -# Docker Container neu starten (nach Backend-Änderungen) -docker restart taskmate - -# Container neu bauen (bei Dependency-Änderungen) -docker build -t taskmate . && docker restart taskmate - -# Logs anzeigen -docker logs taskmate -f - -# Health Check -curl http://localhost:3000/api/health -``` - -### Kritische Regeln - NIEMALS VERGESSEN! ⚠️ -1. **Cache-Version erhöhen** nach Frontend-Änderungen: `frontend/sw.js` → `CACHE_VERSION` -2. **CHANGELOG.txt** bei JEDER Änderung aktualisieren -3. **Keine `toISOString()`** für Datums-Operationen (UTC-Problem!) -4. **Echtzeit-Updates** - User darf NIE F5 drücken müssen -5. **Docker restart** nach Backend-Änderungen - -### Datenschutz & Projektsicherheit 🔐 -**ABSOLUT KRITISCH**: Das Projekt "AegisSight" ist produktiv im Einsatz! - -- **Projekt "AegisSight" NIEMALS löschen, ändern oder beeinträchtigen** -- **Bestehende Benutzer NIEMALS zurücksetzen, löschen oder verändern** -- **Produktivdaten sind TABU** - keine Testdaten in echte Projekte -- **Keine Datenbank-Resets** ohne explizite Anweisung -- **JEDE Änderung MUSS umkehrbar sein** - Live-System! -- **Backup vor kritischen Änderungen** ist Pflicht - -**⚠️ NUTZERDATEN-SCHUTZ - ABSOLUTES VERBOT**: -- **KEINE Änderungen an Nutzerdaten oder Kennwörtern** -- **Geschützte Benutzer (NICHT modifizieren)**: - - admin - - Hendrik (hendrik_gebhardt@gmx.de) - - Monami (momohomma@googlemail.com) -- **Diese Benutzer sind produktiv im Einsatz** -- **Keine Passwort-Resets oder Änderungen an diesen Accounts** -- **Bei Anmeldeproblemen: Nur Debugging, keine Datenänderung** - -### Rollback-Strategie für Live-Betrieb -Bei JEDER Änderung sicherstellen: - -```bash -# 1. Vor Änderungen - Backup erstellen -cp data/taskmate.db data/taskmate.db.backup-$(date +%Y%m%d-%H%M%S) -docker commit taskmate taskmate-backup-$(date +%Y%m%d-%H%M%S) - -# 2. Bei Problemen - Rollback durchführen -docker stop taskmate -docker run -d --name taskmate-temp taskmate-backup-TIMESTAMP -# Nach Test: docker rm -f taskmate && docker rename taskmate-temp taskmate - -# 3. Code-Rollback via Git -git stash # Aktuelle Änderungen sichern -git checkout HEAD~1 # Zum vorherigen Commit -docker build -t taskmate . && docker restart taskmate -``` - -**Änderungs-Workflow für Live-System:** -1. Backup von Datenbank UND Docker-Image -2. Kleine, inkrementelle Änderungen -3. Sofortiger Test nach jeder Änderung -4. Rollback-Plan dokumentieren -5. Bei kritischen Änderungen: Wartungsfenster planen - -### Arbeitsweise mit nicht-technischem Anwender -**Der Anwender versteht KEIN Coding!** Daher: - -1. **Vollständige Übernahme**: Du führst ALLE technischen Schritte durch -2. **Einfache Erklärungen**: "Ich passe jetzt X an, damit Y funktioniert" -3. **Status-Updates**: "Änderung abgeschlossen, teste jetzt..." -4. **Keine technischen Fragen**: Niemals nach Code, Logs oder Befehlen fragen -5. **Proaktives Handeln**: Selbstständig debuggen und lösen - -**Beispiel-Kommunikation:** -- ✅ "Ich habe ein Problem gefunden und behebe es jetzt..." -- ❌ "Welche Version von Node.js ist installiert?" -- ✅ "Die Änderung ist fertig. Bitte die Seite neu laden und testen." -- ❌ "Kannst du mal in die Console schauen?" - -### Zugriff & Domains -- **Frontend**: https://taskmate.aegis-sight.de -- **Lokaler Port**: 3001 → Container Port 3000 -- **Gitea**: https://gitea-undso.aegis-sight.de/AegisSight/TaskMate -- **Gitea-Token**: Siehe `.env` Datei (NIEMALS in Git einchecken!) - -## 📁 Projektstruktur - -### Wichtige Dateien - Hier starten! -``` -frontend/js/app.js # Hauptanwendung & View-Controller -backend/server.js # Express Server mit Socket.io -backend/database.js # Datenbankschema (20+ Tabellen) -frontend/js/store.js # State Management (Pub-Sub) -frontend/js/api.js # API Client mit Auth/CSRF -frontend/sw.js # Service Worker → CACHE_VERSION! -CHANGELOG.txt # Änderungsprotokoll -``` - -### Frontend-Module (22 Dateien) -``` -# Core -app.js # Hauptanwendung -store.js # State Management -api.js # Backend-Kommunikation -auth.js # Login/Token-Verwaltung -utils.js # Hilfsfunktionen - -# Views -board.js # Kanban-Board mit Drag&Drop -calendar.js # Kalender (Monat/Woche) -list.js # Listenansicht -dashboard.js # Statistik-Dashboard -proposals.js # Vorschlagssystem -knowledge.js # Wissensdatenbank -admin.js # Benutzerverwaltung - -# Features -task-modal.js # Aufgaben-Details -notifications.js # Benachrichtigungen -sync.js # Socket.io Echtzeit -mobile.js # Mobile Features -``` - -### Backend-Routes (22 Module) -``` -/api/auth # Login/Logout -/api/tasks # Aufgaben CRUD -/api/projects # Projekte -/api/columns # Kanban-Spalten -/api/comments # Kommentare -/api/files # Datei-Upload -/api/proposals # Vorschläge -/api/gitea # Git-Integration -/api/knowledge # Wissensdatenbank -``` - -## 🔧 Entwicklung - -### Neue View/Ansicht hinzufügen -```javascript -// 1. Datei erstellen: frontend/js/myview.js -export function initMyView() { - // KRITISCH: Echtzeit-Updates registrieren! - store.subscribe('tasks', updateView); - window.addEventListener('app:refresh', updateView); - - function updateView() { - // UI aktualisieren - } -} - -// 2. In index.html einbinden - - -// 3. Navigation erweitern in navigation.js -``` - -### Neue API-Route erstellen -```javascript -// 1. Route-Datei: backend/routes/myroute.js -const router = require('express').Router(); -const auth = require('../middleware/auth'); - -router.get('/', auth, (req, res) => { - // Implementation -}); - -module.exports = router; - -// 2. In server.js registrieren -app.use('/api/myroute', require('./routes/myroute')); - -// 3. Frontend API-Call in api.js -async myRouteCall() { - return this.request('/api/myroute'); -} -``` - -### Datums-Formatierung (RICHTIG!) -```javascript -// ✅ RICHTIG - Lokale Zeit -const year = date.getFullYear(); -const month = String(date.getMonth() + 1).padStart(2, '0'); -const day = String(date.getDate()).padStart(2, '0'); -const dateStr = `${year}-${month}-${day}`; - -// ❌ FALSCH - UTC-Konvertierung -const dateStr = date.toISOString().split('T')[0]; // NIEMALS! -``` - -### Echtzeit-Updates implementieren -```javascript -// PFLICHT für ALLE Komponenten! -// 1. Store-Subscriptions -store.subscribe('tasks', () => renderTasks()); -store.subscribe('columns', () => updateColumns()); -store.subscribe('labels', () => refreshLabels()); - -// 2. Event-Listener -window.addEventListener('app:refresh', () => { - // Komplette UI aktualisieren -}); - -window.addEventListener('modal:close', () => { - // Nach Modal-Schließung -}); - -// 3. Socket.io Events in sync.js -socket.on('task:update', (data) => { - store.updateTask(data); -}); -``` - -## 💾 Datenbank - -### Wichtige Tabellen -```sql -users # Benutzer mit Rollen -projects # Projekte -columns # Kanban-Spalten -tasks # Aufgaben -task_labels # M:N Labels -task_assignees # M:N Zuweisungen -comments # Kommentare -attachments # Dateianhänge -proposals # Vorschläge -notifications # Benachrichtigungen -knowledge_* # Wissensdatenbank -``` - -### Schema ändern -```javascript -// 1. In backend/database.js anpassen -CREATE TABLE new_table ( - id INTEGER PRIMARY KEY, - ... -); - -// 2. Datenbank neu initialisieren -rm data/taskmate.db* -docker restart taskmate - -// 3. API & Frontend anpassen -``` - -## 🚢 Deployment - -### Deployment-Checkliste -```bash -# 1. Vor Deployment -- [ ] Keine console.log() im Code -- [ ] Alle Features getestet -- [ ] Keine Testdaten in DB - -# 2. Deployment durchführen -- [ ] Cache-Version erhöhen: frontend/sw.js -- [ ] CHANGELOG.txt aktualisieren -- [ ] Git commit & push -- [ ] docker build -t taskmate . -- [ ] docker restart taskmate - -# 3. Nach Deployment -- [ ] https://taskmate.aegis-sight.de testen -- [ ] Browser-Cache leeren (Strg+F5) -- [ ] Login, Aufgabe erstellen, etc. testen -``` - -### Docker-Befehle -```bash -# Container Status -docker ps -a | grep taskmate - -# Container stoppen/starten -docker stop taskmate -docker start taskmate - -# Container neu erstellen -docker rm -f taskmate -docker-compose up -d - -# In Container Shell -docker exec -it taskmate sh - -# Logs live verfolgen -docker logs taskmate -f --tail 100 -``` - -### KRITISCHES PROBLEM: Frontend-Änderungen werden nicht sichtbar - -**Problem**: Frontend-Dateien (HTML, CSS, JS) werden beim Docker Build nach `/app/public/` kopiert und sind NICHT live gemountet. Änderungen in `/home/claude-dev/TaskMate/frontend/` werden daher nicht automatisch übernommen. - -**Symptome**: -- CSS/JS-Änderungen funktionieren nicht trotz Browser-Cache-Löschung -- Service Worker Cache-Version Erhöhung hilft nicht -- Änderungen werden sporadisch nach längerer Zeit sichtbar - -**Ursache**: -1. **Dockerfile kopiert Frontend-Dateien**: `COPY frontend/ ./public/` -2. **Express.js cached statische Dateien** mit ETags und Last-Modified Headers -3. **Mehrschichtiges Caching**: Service Worker + Browser + Express.js - -**LÖSUNG A - Sofortige Änderungen (Development)**: -```bash -# Einzelne Datei kopieren -docker cp frontend/css/style.css taskmate:/app/public/css/style.css - -# Mehrere Dateien kopieren -docker cp frontend/js/app.js taskmate:/app/public/js/app.js -docker cp frontend/index.html taskmate:/app/public/index.html - -# CSS + JS zusammen kopieren -docker cp frontend/css/ taskmate:/app/public/css/ -docker cp frontend/js/ taskmate:/app/public/js/ -``` - -**LÖSUNG B - Vollständige Aktualisierung (Production)**: -```bash -# Docker Image neu bauen und Container ersetzen -docker build -t taskmate . && docker restart taskmate -``` - -**Express.js Caching deaktiviert**: -```javascript -// In server.js - statische Dateien ohne Caching -app.use(express.static(path.join(__dirname, 'public'), { - etag: false, - lastModified: false, - cacheControl: false, - setHeaders: (res, path) => { - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); - } -})); -``` - -**Debugging-Workflow**: -1. Prüfe Datei-Timestamps im Container: `docker exec taskmate ls -la /app/public/css/` -2. Vergleiche mit lokalen Dateien: `ls -la frontend/css/` -3. Bei Diskrepanz: Files mit `docker cp` aktualisieren -4. Bei JavaScript-Problemen: Browser-Console auf Fehler prüfen - -**Warum passiert das**: -- Container-Pfad `/app/public/` = Static Files (nicht live) -- Container-Pfad `/app/taskmate-source/` = Git-Operationen (live gemountet) -- Frontend wird NUR beim Build-Time kopiert, nicht zur Laufzeit - -## 🚨 KRITISCHE LEKTIONEN AUS PROBLEMEN - -### ⚠️ Kontakte-Modul Implementation Probleme (07.01.2025) - -**FEHLER 1: Backend API-Route nicht gefunden (404)** -- **Problem**: GET /api/contacts gibt 404 - Endpoint nicht gefunden -- **Ursache**: Backend-Dateien nicht im Docker-Container, Container nicht neu gestartet -- **Lösung**: Backend-Dateien kopieren und Container neu starten -- **Prävention**: - ```bash - # Backend-Änderungen: Alle Dateien kopieren - docker cp backend/routes/contacts.js taskmate:/app/routes/ - docker cp backend/middleware/validation.js taskmate:/app/middleware/ - docker cp backend/server.js taskmate:/app/server.js - docker restart taskmate # IMMER nach Backend-Änderungen - ``` - -**FEHLER 2: Database Table existiert nicht (500 Internal Server Error)** -- **Problem**: "no such table: contacts" - Tabelle wurde nicht erstellt -- **Ursache**: database.js Änderungen nicht übernommen, bestehende DB erweitert sich nicht automatisch -- **Lösung**: database.js kopieren + Tabelle manuell erstellen -- **Pattern für neue Tabellen**: - ```bash - docker cp backend/database.js taskmate:/app/database.js - docker exec taskmate node -e " - const Database = require('better-sqlite3'); - const db = new Database('/app/data/taskmate.db'); - db.exec('CREATE TABLE IF NOT EXISTS new_table (...);'); - console.log('Table created successfully'); - " - ``` - -**FEHLER 3: store.showMessage ist undefined** -- **Problem**: `store.showMessage()` Funktion existiert nicht -- **Ursache**: Falsche API für Toast-Nachrichten -- **Lösung**: Verwende `window.dispatchEvent` mit `toast:show` -- **Pattern für Toast-Messages**: - ```javascript - // FALSCH: store.showMessage('Text', 'success') - // RICHTIG: - window.dispatchEvent(new CustomEvent('toast:show', { - detail: { message: 'Text', type: 'success' } - })); - ``` - -**FEHLER 4: Event-Handler nicht gebunden** -- **Problem**: Button-Clicks funktionieren nicht trotz Event-Listener -- **Ursache**: Timing-Problem - DOM noch nicht bereit, Modal-Overlay fehlt -- **Lösung**: Korrekte Modal-Struktur + Overlay-Management -- **Debugging-Pattern**: - ```javascript - console.log('[Module] Element check:', this.buttonElement); - if (this.buttonElement) { - console.log('[Module] Binding event'); - this.buttonElement.addEventListener('click', () => { - console.log('[Module] Button clicked!'); - }); - } - ``` - -**FEHLER 5: Modal Design inkonsistent** -- **Problem**: Custom Modal-Styles passen nicht zum App-Design -- **Ursache**: Eigene CSS-Klassen statt Standard-Modal-Pattern -- **Lösung**: Standard Modal-Struktur verwenden -- **Standard Modal-Pattern**: - ```html -
- ``` - -### ⚠️ SVG Icon-Rendering Probleme (07.01.2025) - -**FEHLER: SVG Icons werden nicht angezeigt** -- **Problem**: SVG-Icons verschwinden oder zeigen nicht korrekt an -- **Ursache**: `createElement()` unterstützt kein SVG-Namespace -- **Lösung**: SVG mit `innerHTML` oder als String-Template einfügen -- **Pattern**: - ```javascript - // FALSCH - SVG wird nicht gerendert - const icon = createElement('svg', { viewBox: '0 0 24 24' }); - - // RICHTIG - SVG als HTML-String - element.innerHTML = ``; - ``` -- **Prävention**: Bei dynamischen SVGs immer innerHTML oder DOMParser nutzen - -### ⚠️ API Field Name Mismatches (07.01.2025) - -**FEHLER: Frontend/Backend Feldnamen-Diskrepanz** -- **Problem**: Daten werden mit 0 Bytes oder leer angezeigt -- **Ursache**: Backend sendet camelCase, Frontend erwartet snake_case -- **Beispiel**: `originalName` vs `original_name`, `sizeBytes` vs `size_bytes` -- **Lösung**: Fallback-Pattern für beide Schreibweisen -- **Pattern**: - ```javascript - // Robuste Feldabfrage mit Fallback - const fileName = data.originalName || data.original_name || ''; - const fileSize = data.sizeBytes || data.size_bytes || 0; - ``` -- **Prävention**: API-Dokumentation prüfen, einheitliche Naming-Convention - -### ⚠️ File Upload Field Names (07.01.2025) - -**FEHLER: Multer "Unexpected field" Error** -- **Problem**: 500 Error bei File-Upload -- **Ursache**: Frontend sendet 'files' (plural), Backend erwartet 'file' (singular) -- **Lösung**: Backend-Konsistenz herstellen -- **Pattern**: - ```javascript - // Backend - Konsistent 'files' verwenden - upload.single('files') // NICHT 'file' - - // Frontend - FormData immer mit 'files' - formData.append('files', file); - ``` -- **Prävention**: Einheitliche Field-Names über alle Upload-Endpoints - -### ⚠️ Erinnerung-Implementation Probleme (06.01.2026) - -**FEHLER 1: Syntax-Fehler in JavaScript blockierte Login** -- **Problem**: Missing closing brace in calendar.js verhinderte Login komplett -- **Ursache**: Unvollständige Code-Blöcke beim Multi-Edit -- **Lösung**: IMMER Syntax-Check nach JavaScript-Änderungen -- **Prävention**: - ```bash - # Nach JS-Änderungen prüfen: - node -c frontend/js/calendar.js - docker logs taskmate --tail 20 # Auf Syntax-Fehler prüfen - ``` - -**FEHLER 2: "Verschwundene" Projekte durch 401-Fehler** -- **Problem**: User dachte AegisSight-Projekt sei gelöscht -- **Ursache**: Authentifizierungs-Token abgelaufen, API gibt 401 zurück -- **Diagnose**: `docker logs taskmate` zeigt 401-Fehler -- **Lösung**: Einfach neu anmelden, Daten sind intakt -- **Prävention**: Bei "verschwundenen" Daten IMMER zuerst Auth prüfen - -**FEHLER 3: Checkbox-Styling funktioniert nicht** -- **Problem**: CSS-Selektoren greifen nicht, komplexe Pseudo-Element-Struktur -- **Ursache**: Browser-CSS-Konflikte, CSS-Variable-Probleme -- **Lösung**: Direktes Styling nativer Checkboxes mit `appearance: none` -- **Lesson**: Bei CSS-Problemen: **Einfachster Ansatz zuerst** - ```css - /* FALSCH: Komplexe Pseudo-Struktur */ - input:checked + span::after { content: '✓'; } - - /* RICHTIG: Direktes Styling */ - input[type="checkbox"] { - appearance: none; - background: #3B82F6 when :checked; - } - ``` - -**FEHLER 4: Event-Handler Konflikte bei Modal-Updates** -- **Problem**: Dropdown-Handler werden überschrieben -- **Ursache**: Mehrfache Event-Binding ohne Cleanup -- **Lösung**: Element-Kloning für saubere Event-Handler -- **Pattern**: - ```javascript - // Event-Handler cleanup durch Klonen - const newElement = element.cloneNode(true); - element.parentNode.replaceChild(newElement, element); - // Dann neue Handler binden - ``` - -**FEHLER 5: Visuelle Darstellung unterbricht Funktionalität** -- **Problem**: Erinnerungen unterbrachen Aufgaben-Balken -- **Ursache**: Falsche Render-Reihenfolge (Erinnerungen vor Aufgaben) -- **Lösung**: Aufgaben zuerst, dann Erinnerungen -- **Lesson**: UI-Reihenfolge muss Funktionalität folgen, nicht umgekehrt - -### ⚠️ Dropdown-Transparenz-Probleme (09.01.2026) - -**FEHLER: Dropdown-Menüs haben unerwünschte Transparenz** -- **Problem**: Dropdown-Menüs zeigen durchscheinenden Hintergrund, besonders bei dunklen Themes -- **Symptome**: - - Text schwer lesbar durch transparenten Hintergrund - - Inhalte dahinter scheinen durch - - Besonders auffällig bei Kontakte-Modul und anderen Dropdown-Menüs -- **Ursache**: CSS-Variablen für Hintergründe verwenden `rgba()` mit Transparenz -- **Lösung**: Explizite, nicht-transparente Hintergrundfarben für Dropdowns setzen -- **Pattern**: - ```css - /* FALSCH - Transparente Hintergründe */ - .dropdown { - background: var(--bg-secondary); /* rgba mit 0.95 opacity */ - } - - /* RICHTIG - Solide Hintergründe für Dropdowns */ - .dropdown { - background: #ffffff; /* Hell-Theme */ - background: #1a1a1a; /* Dunkel-Theme */ - } - ``` -- **Prävention**: - - Dropdown-Komponenten immer mit soliden Hintergründen - - Keine CSS-Variablen mit Transparenz für interaktive Elemente - - Bei Theme-Support: Explizite Farben ohne Alpha-Kanal - -### 🔧 TROUBLESHOOTING-WORKFLOW - -**Bei JavaScript-Fehlern:** -1. `docker logs taskmate --tail 50` prüfen -2. Browser-Console auf Syntax-Fehler prüfen -3. Node.js Syntax-Check: `node -c datei.js` - -**Bei "verschwundenen" Daten:** -1. **NIEMALS** sofort Backup/Restore - erst debuggen! -2. API-Logs prüfen auf 401/403 Fehler -3. Auth-Status prüfen: `localStorage.getItem('token')` -4. Datenbank direkt prüfen: `sqlite3 data/taskmate.db "SELECT COUNT(*) FROM projects"` - -**Bei CSS-Problemen:** -1. Simplest approach first - keine komplexen Selektoren -2. `!important` nur als letzter Ausweg -3. Browser-DevTools: Computed Styles prüfen -4. Cache leeren: `CACHE_VERSION++` in sw.js - -**Bei neuen Modulen mit globaler Suche:** -1. Module in app.js setupSearch() registrieren: - ```javascript - } else if (currentView === 'mymodule') { - import('./mymodule.js').then(module => { - if (module.myManager) { - module.myManager.searchQuery = value; - module.myManager.filterData(); - } - }); - ``` -2. Manager-Instanz exportieren: `export { myManager }` -3. clearSearch() Funktion ebenfalls erweitern -4. Lokale Suchfelder entfernen - nur Header-Suche nutzen - -**Bei File-Upload Problemen:** -1. Prüfe ob Entry/Task bereits gespeichert ist (ID vorhanden) -2. Bei neuen Einträgen: Erst speichern, dann Upload -3. Field-Name Konsistenz prüfen: 'files' (plural) überall -4. `docker logs taskmate` für Multer-Errors checken - -## 🐛 Troubleshooting - -### Häufige Probleme - -**401 Unauthorized** -- Token abgelaufen → Neu einloggen -- Prüfen: localStorage.getItem('token') - -**CSRF Token ungültig** -- Browser-Cache/Cookies löschen -- Neu einloggen - -**Änderungen nicht sichtbar** -- Service Worker Cache → sw.js Version erhöhen! -- Browser: Strg+F5 -- Prüfen: Echtzeit-Updates implementiert? - -**Docker startet nicht** -```bash -docker logs taskmate -docker ps -a | grep taskmate -netstat -tulpn | grep 3001 -``` - -**Datenbank-Fehler** -```bash -# Backup erstellen -cp data/taskmate.db data/taskmate.db.backup - -# Integrität prüfen -sqlite3 data/taskmate.db "PRAGMA integrity_check;" - -# Schema anzeigen -sqlite3 data/taskmate.db ".schema" -``` - -### Debug-Tipps - -**Frontend Debugging** -```javascript -// Store-Status prüfen -console.log(store.getState()); - -// API-Calls tracken -window.api.debug = true; - -// Socket-Events loggen -window.socket.on('*', console.log); -``` - -**Backend Debugging** -```javascript -// In server.js -app.use((req, res, next) => { - console.log(`${req.method} ${req.path}`); - next(); -}); - -// SQL Queries loggen -db.prepare(sql).run(); // Vorher console.log(sql) -``` - -## 📋 Code-Patterns - -### API Response Format -```javascript -// Erfolg -res.json({ - success: true, - data: result -}); - -// Fehler -res.status(400).json({ - success: false, - error: 'Fehlermeldung' -}); -``` - -### Store Update Pattern -```javascript -// Daten aktualisieren -store.updateTasks(tasks); - -// Wird automatisch ausgelöst: -// - Alle task-Subscriber -// - Socket.io Broadcast -// - UI-Updates -``` - -### Modal Pattern -```javascript -// Modal öffnen -modal.show(); - -// WICHTIG: Bei Schließung -modal.addEventListener('close', () => { - window.dispatchEvent(new CustomEvent('modal:close')); - // Triggert UI-Updates! -}); -``` - -## 🔒 Sicherheit - -### Authentifizierung -- JWT Token mit 24h Gültigkeit -- Refresh bei jeder Aktivität -- Token in localStorage - -### CSRF-Schutz -- Token bei Login generiert -- Bei jeder Mutation mitgesendet -- Header: `X-CSRF-Token` - -### Berechtigungen -- Admin: Nur Benutzerverwaltung -- User: Alles außer Admin-Bereich -- Projekt-basierte Rechte - -## 📝 Wichtige Konventionen - -- **Sprache**: Deutsch für UI, Englisch für Code -- **Umlaute**: ä, ö, ü verwenden (keine ae, oe, ue) -- **CSS**: Variablen in `frontend/css/variables.css` -- **Keine Emojis** in Code/UI (nur Doku) -- **Auto-Save**: Änderungen werden automatisch gespeichert - -## 🎯 Performance - -### Frontend -- Lazy Loading für Views -- Debouncing bei Suche/Filter -- Virtual Scrolling bei langen Listen -- Service Worker Caching - -### Backend -- SQLite mit WAL Mode -- Prepared Statements -- Index auf häufig gefilterte Spalten -- Pagination bei großen Datenmengen - -## 🔄 Git Workflow - -### Lokales Repository -```bash -# Status prüfen -git status - -# Commit erstellen -git add . -git commit -m "Beschreibung der Änderung" - -# Zu Gitea pushen -git push origin main -``` - -### Gitea Integration -- Automatischer Push bei Commits -- Repository-Projekt Verknüpfung -- Branch-Verwaltung in UI - ---- - -**Hinweis**: Diese Dokumentation ist für die KI-gestützte Entwicklung optimiert. Bei Fragen die `ANWENDUNGSBESCHREIBUNG.txt` für Endnutzer-Dokumentation konsultieren. \ No newline at end of file +# TaskMate - Projekt-Konfiguration +# YAML-Format fuer KI-Assistenten + +PROJECT: + name: TaskMate + type: PRODUCTION + url: https://taskmate.aegis-sight.de + container: taskmate + port_external: 3001 + port_internal: 3000 + gitea: https://gitea-undso.aegis-sight.de/AegisSight/TaskMate + +USER_PROFILE: + programming_skills: NONE + support_level: FULL_SERVICE + communication: SIMPLE_LANGUAGE + +COMMUNICATION_RULES: + do: + - "Alle technischen Aufgaben vollstaendig uebernehmen" + - "In einfachen Worten erklaeren was passiert" + - "Alle Schritte selbststaendig ausfuehren" + - "Status-Updates geben: Aenderung abgeschlossen, teste jetzt..." + dont: + - "NIEMALS nach technischen Details fragen" + - "NIEMALS nach Code-Schnipseln fragen" + - "NIEMALS Befehle zum Ausfuehren geben" + - "NIEMALS nach Logs oder Console fragen" + +CRITICAL_RULES: + 1_cache_version: "Nach Frontend-Aenderungen: frontend/sw.js CACHE_VERSION erhoehen" + 2_changelog_file: "Bei JEDER Aenderung: CHANGELOG.txt aktualisieren" + 3_changelog_db: "Bei JEDER Aenderung: Wissensdatenbank Changelog aktualisieren (siehe WORKFLOWS)" + 4_no_toISOString: "NIEMALS toISOString() fuer Datums-Operationen (UTC-Problem!)" + 5_realtime_updates: "User darf NIE F5 druecken muessen" + 6_docker_restart: "Nach Backend-Aenderungen: docker compose up -d --build (NICHT docker restart - das nutzt das alte Image!)" + 7_no_css_tricks: "Layout-Aenderungen immer ueber HTML-Struktur loesen, NIEMALS mit CSS-Tricks wie flex-wrap+order, negative margins oder aehnlichem. Wenn ein Element in eine eigene Zeile soll, muss es ein eigenes HTML-Element sein." + +PROTECTED_DATA: + project: "AegisSight" + description: "Produktiv im Einsatz - NIEMALS loeschen oder aendern" + protected_users: + - admin + - "Hendrik (hendrik_gebhardt@gmx.de)" + - "Monami (momohomma@googlemail.com)" + forbidden_actions: + - "Nutzerdaten aendern" + - "Passwoerter aendern" + - "Datenbank-Resets ohne Anweisung" + - "Testdaten in Produktivdaten" + +COMMANDS: + restart: "docker restart taskmate" + rebuild: "cd /home/claude-dev/TaskMate && docker compose build --no-cache && docker compose up -d" + logs: "docker logs taskmate -f --tail 100" + health: "curl http://localhost:3000/api/health" + copy_frontend: "docker cp frontend/js/ taskmate:/app/public/js/" + copy_backend: "docker cp backend/routes/ taskmate:/app/routes/" + +FILE_STRUCTURE: + frontend: + core: + - "frontend/js/app.js - Hauptanwendung & View-Controller" + - "frontend/js/store.js - State Management (Pub-Sub)" + - "frontend/js/api.js - API Client mit Auth/CSRF" + - "frontend/js/auth.js - Login/Token-Verwaltung" + - "frontend/js/sync.js - Socket.io Echtzeit" + views: + - "frontend/js/board.js - Kanban-Board mit Drag&Drop" + - "frontend/js/calendar.js - Kalender (Monat/Woche)" + - "frontend/js/list.js - Listenansicht" + - "frontend/js/dashboard.js - Statistik-Dashboard" + - "frontend/js/proposals.js - Vorschlagssystem" + - "frontend/js/knowledge.js - Wissensdatenbank" + - "frontend/js/admin.js - Benutzerverwaltung" + important: + - "frontend/sw.js - Service Worker mit CACHE_VERSION" + - "frontend/index.html - Haupt-HTML" + backend: + core: + - "backend/server.js - Express Server mit Socket.io" + - "backend/database.js - Datenbankschema (20+ Tabellen)" + routes: + - "/api/auth - Login/Logout" + - "/api/tasks - Aufgaben CRUD" + - "/api/projects - Projekte" + - "/api/columns - Kanban-Spalten" + - "/api/comments - Kommentare" + - "/api/files - Datei-Upload" + - "/api/proposals - Vorschlaege" + - "/api/knowledge - Wissensdatenbank" + other: + - "CHANGELOG.txt - Aenderungsprotokoll" + +DATABASE: + type: SQLite + path: "data/taskmate.db" + tables: + - users + - projects + - columns + - tasks + - task_labels + - task_assignees + - comments + - attachments + - proposals + - notifications + - knowledge_categories + - knowledge_entries + +CODE_PATTERNS: + date_formatting: + correct: | + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const dateStr = `${year}-${month}-${day}`; + wrong: "date.toISOString().split('T')[0] // NIEMALS - UTC Problem!" + + toast_messages: + correct: | + window.dispatchEvent(new CustomEvent('toast:show', { + detail: { message: 'Text', type: 'success' } + })); + wrong: "store.showMessage('Text', 'success') // existiert nicht" + + realtime_updates: + required: | + store.subscribe('tasks', () => renderTasks()); + window.addEventListener('app:refresh', () => updateView()); + +TROUBLESHOOTING: + frontend_changes_not_visible: + problem: "Frontend-Dateien werden beim Build kopiert, nicht live gemountet" + solution_dev: "docker cp frontend/js/ taskmate:/app/public/js/" + solution_prod: "cd /home/claude-dev/TaskMate && docker compose build --no-cache && docker compose up -d" + + 401_unauthorized: + problem: "Token abgelaufen" + solution: "Neu einloggen" + + csrf_error: + problem: "CSRF Token ungueltig" + solution: "Browser-Cache/Cookies loeschen, neu einloggen" + + missing_data: + problem: "Daten scheinen verschwunden" + first_check: "docker logs taskmate - auf 401/403 Fehler pruefen" + note: "NIEMALS sofort Backup/Restore - erst Auth pruefen" + +WORKFLOWS: + changelog_entry: + description: "PFLICHT bei jeder Code-Aenderung" + step_1: + name: "CHANGELOG.txt aktualisieren" + file: "/home/claude-dev/TaskMate/CHANGELOG.txt" + format: "DD.MM.YYYY - vXXX - Kurzbeschreibung" + step_2: + name: "Wissensdatenbank Changelog aktualisieren" + command: | + docker exec taskmate node -e " + const Database = require('better-sqlite3'); + const db = new Database('/app/data/taskmate.db'); + db.prepare(\` + INSERT INTO knowledge_entries (category_id, title, notes, position, created_by) + VALUES (15, 'DD.MM.YYYY - Beschreibung', 'Markdown-Inhalt', 0, 1) + \`).run(); + console.log('Changelog erstellt'); + " + category_id: 15 + title_format: "DD.MM.YYYY - Kurze Beschreibung" + notes_format: "Markdown mit Details (Problem, Loesung, geaenderte Dateien)" + + deployment: + steps: + - "Cache-Version erhoehen: frontend/sw.js" + - "CHANGELOG.txt aktualisieren" + - "Wissensdatenbank Changelog aktualisieren" + - "docker cp oder docker build" + - "docker compose up -d (Container mit neuem Image starten)" + - "Testen: https://taskmate.aegis-sight.de" + - "Browser-Cache leeren (Strg+F5)" + +CONVENTIONS: + language_ui: Deutsch + language_code: Englisch + umlauts: "ae oe ue verwenden (ä ö ü)" + emojis: "Nur in Dokumentation, nicht in Code/UI" + auto_save: true + +SECURITY: + auth: + type: JWT + validity: 24h + storage: localStorage + csrf: + header: "X-CSRF-Token" + generated_at: login + roles: + admin: "Nur Benutzerverwaltung" + user: "Alles ausser Admin-Bereich" + +KNOWN_ISSUES: + docker_rebuild: + problem: "docker restart nutzt das ALTE Image - Code-Aenderungen werden NICHT uebernommen" + wichtig: "docker build allein reicht auch nicht, da docker-compose den alten Container weiter nutzt" + solution: "cd /home/claude-dev/TaskMate && docker compose build --no-cache && docker compose up -d" + hinweis: "Immer mit --no-cache bauen, da Docker COPY-Layer cached auch wenn Dateien sich geaendert haben" + svg_rendering: + problem: "createElement() unterstuetzt kein SVG-Namespace" + solution: "SVG mit innerHTML oder String-Template einfuegen" + + field_name_mismatch: + problem: "Backend camelCase vs Frontend snake_case" + solution: "Fallback-Pattern: data.originalName || data.original_name" + + file_upload: + problem: "Multer Unexpected field Error" + solution: "Konsistent 'files' (plural) verwenden" + +ROLLBACK: + backup_db: "cp data/taskmate.db data/taskmate.db.backup-$(date +%Y%m%d-%H%M%S)" + backup_docker: "docker commit taskmate taskmate-backup-$(date +%Y%m%d-%H%M%S)" + restore_code: "git stash && git checkout HEAD~1" + +Last-Updated: 2026-03-19 diff --git a/backend/database.js b/backend/database.js index 3e55c62..d16f7cf 100644 --- a/backend/database.js +++ b/backend/database.js @@ -113,7 +113,7 @@ function createTables() { CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, - column_id INTEGER NOT NULL, + column_id INTEGER, title TEXT NOT NULL, description TEXT, priority TEXT DEFAULT 'medium', @@ -128,7 +128,7 @@ function createTables() { created_by INTEGER, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, 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 (depends_on) REFERENCES tasks(id) ON DELETE SET NULL, FOREIGN KEY (created_by) REFERENCES users(id) @@ -613,31 +613,109 @@ function createTables() { ) `); - // Kontakte + // Neues optimiertes Kontakt-Schema db.exec(` CREATE TABLE IF NOT EXISTS contacts ( 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, last_name TEXT, - company TEXT, position TEXT, - email TEXT, - phone TEXT, - mobile TEXT, - address TEXT, - postal_code TEXT, - city TEXT, - country TEXT, + department TEXT, + parent_company_id INTEGER, + + -- Firma-spezifische Felder + company_name TEXT, + company_type TEXT, + industry TEXT, website TEXT, - notes TEXT, - tags TEXT, + + -- Meta + project_id INTEGER NOT NULL, + created_by INTEGER NOT NULL, created_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) ) `); + // 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 db.exec(` 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_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_contacts_company ON contacts(company); - CREATE INDEX IF NOT EXISTS idx_contacts_tags ON contacts(tags); + -- Kontakt-Indizes für Performance + 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'); diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index a89b675..33f31cf 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -13,7 +13,7 @@ const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET || JWT_SECRET.length < 32) { 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 SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT) || 30; // Minuten diff --git a/backend/routes/admin.js b/backend/routes/admin.js index abdaee7..d8e8146 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -17,7 +17,7 @@ const backup = require('../utils/backup'); */ const DEFAULT_UPLOAD_SETTINGS = { 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 diff --git a/backend/routes/columns.js b/backend/routes/columns.js index 67640e9..d94aa35 100644 --- a/backend/routes/columns.js +++ b/backend/routes/columns.js @@ -252,17 +252,20 @@ router.delete('/:id', (req, res) => { return res.status(404).json({ error: 'Spalte nicht gefunden' }); } - // Prüfen ob Aufgaben in der Spalte sind - const taskCount = db.prepare( - 'SELECT COUNT(*) as count FROM tasks WHERE column_id = ?' + // Prüfen ob AKTIVE (nicht-archivierte) Aufgaben in der Spalte sind + const activeTaskCount = db.prepare( + 'SELECT COUNT(*) as count FROM tasks WHERE column_id = ? AND (archived IS NULL OR archived = 0)' ).get(columnId).count; - if (taskCount > 0) { + if (activeTaskCount > 0) { return res.status(400).json({ 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 const columnCount = db.prepare( 'SELECT COUNT(*) as count FROM columns WHERE project_id = ?' diff --git a/backend/routes/contacts.js b/backend/routes/contacts.js index 3209c7d..4809d72 100644 --- a/backend/routes/contacts.js +++ b/backend/routes/contacts.js @@ -1,438 +1,445 @@ /** - * TASKMATE - Contact Routes - * ========================= - * CRUD für Kontakte + * TASKMATE - Kontakte API + * ======================== + * REST API für Kontaktmanagement */ -const express = require('express'); -const router = express.Router(); +const router = require('express').Router(); const { getDb } = require('../database'); 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 { 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 = ` - 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 LEFT JOIN users u ON c.created_by = u.id + LEFT JOIN contacts pc ON c.parent_company_id = pc.id WHERE 1=1 `; 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) { query += ` AND ( + c.display_name LIKE ? OR c.first_name LIKE ? OR c.last_name LIKE ? OR - c.company LIKE ? OR - c.email LIKE ? OR - c.phone LIKE ? OR - c.mobile LIKE ? + c.company_name LIKE ? OR + c.tags LIKE ? OR + c.notes LIKE ? )`; - const searchParam = `%${search}%`; - params.push(searchParam, searchParam, searchParam, searchParam, searchParam, searchParam); + const searchPattern = `%${search}%`; + params.push(searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern); } - // Tag-Filter - if (tag) { - query += ` AND c.tags LIKE ?`; - params.push(`%${tag}%`); - } + query += ' ORDER BY c.display_name ASC'; - // Sortierung - 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); - 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 => ({ - id: c.id, - firstName: c.first_name, - lastName: c.last_name, - company: c.company, - position: c.position, - email: c.email, - 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 - }))); + return { + ...contact, + details + }; + }); + + res.json({ success: true, data: contactsWithDetails }); } catch (error) { - logger.error('Fehler beim Abrufen der Kontakte:', { error: error.message }); - res.status(500).json({ error: 'Interner Serverfehler' }); + logger.error('Fehler beim Abrufen der Kontakte:', error); + res.status(500).json({ success: false, error: error.message }); } }); -/** - * GET /api/contacts/:id - * Einzelnen Kontakt abrufen - */ -router.get('/:id', (req, res) => { +// GET /api/contacts/:id - Einzelnen Kontakt abrufen +router.get('/:id', authenticateToken, (req, res) => { try { const db = getDb(); - const contactId = req.params.id; + const { id } = req.params; 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 LEFT JOIN users u ON c.created_by = u.id + LEFT JOIN contacts pc ON c.parent_company_id = pc.id WHERE c.id = ? - `).get(contactId); + `).get(id); if (!contact) { - return res.status(404).json({ error: 'Kontakt nicht gefunden' }); + return res.status(404).json({ success: false, error: 'Kontakt nicht gefunden' }); } - res.json({ - id: contact.id, - firstName: contact.first_name, - lastName: contact.last_name, - company: contact.company, - position: contact.position, - email: contact.email, - phone: contact.phone, - mobile: contact.mobile, - address: contact.address, - postalCode: contact.postal_code, - city: contact.city, - country: contact.country, - website: contact.website, - notes: contact.notes, - tags: contact.tags ? contact.tags.split(',').map(t => t.trim()) : [], - createdAt: contact.created_at, - updatedAt: contact.updated_at, - createdBy: contact.created_by, - creatorName: contact.creator_name - }); + // Details abrufen + contact.details = db.prepare(` + SELECT * FROM contact_details + WHERE contact_id = ? + ORDER BY is_primary DESC, type, label + `).all(id); + + + // Interaktionen abrufen + contact.interactions = db.prepare(` + SELECT ci.*, u.display_name as created_by_name + FROM contact_interactions ci + LEFT JOIN users u ON ci.created_by = u.id + WHERE ci.contact_id = ? + ORDER BY ci.interaction_date DESC + LIMIT 10 + `).all(id); + + res.json({ success: true, data: contact }); } catch (error) { - logger.error('Fehler beim Abrufen des Kontakts:', { error: error.message, contactId: req.params.id }); - res.status(500).json({ error: 'Interner Serverfehler' }); + logger.error('Fehler beim Abrufen des Kontakts:', error); + res.status(500).json({ success: false, error: error.message }); } }); -/** - * POST /api/contacts - * Neuen Kontakt erstellen - */ -router.post('/', validators.contact, (req, res) => { +// POST /api/contacts - Neuen Kontakt erstellen +router.post('/', authenticateToken, (req, res) => { try { const db = getDb(); - const userId = req.user.id; - const { - firstName, - lastName, - company, - position, - email, - phone, - mobile, - address, - postalCode, - city, - country, - website, - notes, - tags - } = req.body; + const currentUser = req.user; + const { type, project_id, details = [], ...contactData } = req.body; + + // Validierung + if (!type || !['person', 'company'].includes(type)) { + return res.status(400).json({ + success: false, + error: 'Kontakttyp muss "person" oder "company" sein' + }); + } + + if (!project_id) { + return res.status(400).json({ + success: false, + error: 'Projekt-ID ist erforderlich' + }); + } + + // 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(` - INSERT INTO contacts ( - first_name, last_name, company, position, - email, phone, mobile, address, postal_code, - city, country, website, notes, tags, - 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 - ); + INSERT INTO contact_interactions ( + contact_id, type, subject, content, created_by + ) VALUES (?, ?, ?, ?, ?) + `).run(id, type, subject, content, currentUser.id); - const newContact = 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 = ? + const newInteraction = db.prepare(` + SELECT ci.*, u.display_name as created_by_name + FROM contact_interactions ci + LEFT JOIN users u ON ci.created_by = u.id + WHERE ci.id = ? `).get(result.lastInsertRowid); - // Socket.io Event - 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 }); + res.json({ success: true, data: newInteraction }); } catch (error) { - logger.error('Fehler beim Erstellen des Kontakts:', { error: error.message, body: req.body }); - res.status(500).json({ error: 'Interner Serverfehler' }); + logger.error('Fehler beim Hinzufügen der Interaktion:', error); + res.status(500).json({ success: false, error: error.message }); } }); -/** - * PUT /api/contacts/:id - * Kontakt aktualisieren - */ -router.put('/:id', validators.contact, (req, res) => { +// ===================== +// DATEI-UPLOAD (nach /:id Routen) +// ===================== + +// POST /api/contacts/:id/avatar - Avatar hochladen +router.post('/:id/avatar', authenticateToken, upload.single('files'), async (req, res) => { try { const db = getDb(); - const contactId = req.params.id; - const userId = req.user.id; - const { - firstName, - lastName, - company, - position, - email, - phone, - mobile, - address, - postalCode, - city, - country, - website, - notes, - tags - } = req.body; + const { id } = req.params; - // 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' }); + if (!req.file) { + return res.status(400).json({ + success: false, + error: 'Keine Datei hochgeladen' + }); } - // Update - db.prepare(` - 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(); + // Avatar-URL speichern + const avatarUrl = `/uploads/${req.file.filename}`; - const contacts = db.prepare('SELECT DISTINCT tags FROM contacts WHERE tags IS NOT NULL').all(); - - // Alle Tags sammeln und deduplizieren - const allTags = new Set(); - contacts.forEach(contact => { - if (contact.tags) { - contact.tags.split(',').forEach(tag => { - const trimmedTag = tag.trim(); - if (trimmedTag) { - allTags.add(trimmedTag); - } - }); - } - }); + db.prepare('UPDATE contacts SET avatar_url = ? WHERE id = ?') + .run(avatarUrl, id); - res.json(Array.from(allTags).sort()); + res.json({ + success: true, + data: { avatar_url: avatarUrl }, + message: 'Avatar erfolgreich hochgeladen' + }); } catch (error) { - logger.error('Fehler beim Abrufen der Tags:', { error: error.message }); - res.status(500).json({ error: 'Interner Serverfehler' }); + logger.error('Fehler beim Avatar-Upload:', error); + res.status(500).json({ success: false, error: error.message }); } }); diff --git a/backend/routes/knowledge.js b/backend/routes/knowledge.js index f18cb30..1dd0f86 100644 --- a/backend/routes/knowledge.js +++ b/backend/routes/knowledge.js @@ -522,7 +522,7 @@ router.post('/entries', (req, res) => { actorId: req.user.id }, io, - false // nicht persistent + true // persistent, damit die Benachrichtigung in der Inbox bleibt ); }); diff --git a/backend/routes/tasks.js b/backend/routes/tasks.js index 782afb3..6017fc1 100644 --- a/backend/routes/tasks.js +++ b/backend/routes/tasks.js @@ -828,7 +828,7 @@ router.post('/:id/duplicate', (req, res) => { router.put('/:id/archive', (req, res) => { try { const taskId = req.params.id; - const { archived } = req.body; + const { archived, columnId } = req.body; const db = getDb(); @@ -837,16 +837,42 @@ router.put('/:id/archive', (req, res) => { return res.status(404).json({ error: 'Aufgabe nicht gefunden' }); } - db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?') - .run(archived ? 1 : 0, taskId); + // Bei Wiederherstellung: Prüfen ob Spalte vorhanden + if (!archived && !task.column_id) { + // Task hat keine Spalte (wurde mit gelöschter Spalte archiviert) + if (!columnId) { + return res.status(400).json({ + error: 'Spalte erforderlich', + requiresColumn: true + }); + } + // Prüfen ob Spalte existiert und zum Projekt gehört + const column = db.prepare('SELECT * FROM columns WHERE id = ? AND project_id = ?') + .get(columnId, task.project_id); + if (!column) { + return res.status(400).json({ error: 'Ungültige Spalte' }); + } + // Wiederherstellen mit neuer Spalte + db.prepare('UPDATE tasks SET archived = 0, column_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?') + .run(columnId, taskId); + } else { + // Normales Archivieren/Wiederherstellen + db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?') + .run(archived ? 1 : 0, taskId); + } addHistory(db, taskId, req.user.id, archived ? 'archived' : 'restored'); logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`); - // WebSocket + // WebSocket - vollständige Task-Daten senden 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' }); } catch (error) { diff --git a/frontend/.well-known/assetlinks.json b/frontend/.well-known/assetlinks.json index b38a55f..b33c045 100644 --- a/frontend/.well-known/assetlinks.json +++ b/frontend/.well-known/assetlinks.json @@ -4,7 +4,7 @@ "namespace": "android_app", "package_name": "de.aegissight.taskmate", "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" ] } -}] \ No newline at end of file +}] diff --git a/frontend/css/board.css b/frontend/css/board.css index 0c6c714..8d1fd0d 100644 --- a/frontend/css/board.css +++ b/frontend/css/board.css @@ -112,13 +112,15 @@ display: flex; align-items: center; justify-content: space-between; - height: var(--header-height); + min-height: var(--header-height); padding: 0 var(--spacing-4); background: var(--bg-card); border-bottom: 1px solid var(--border-light); position: sticky; top: 0; z-index: var(--z-sticky); + width: 100%; + box-sizing: border-box; } .header-left, @@ -126,11 +128,20 @@ display: flex; align-items: center; gap: var(--spacing-3); + flex-shrink: 0; + flex-wrap: nowrap; } -.header-center { +.view-tabs-bar { display: flex; + justify-content: space-between; 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 { @@ -150,7 +161,8 @@ } .project-select { - min-width: 160px; + width: 100%; + min-width: 120px; max-width: 200px; font-weight: var(--font-medium); font-size: var(--text-sm); @@ -166,15 +178,17 @@ position: relative; backdrop-filter: blur(10px); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + justify-content: center; } .view-tab { position: relative; display: flex; + flex-direction: column; align-items: center; - gap: var(--spacing-2); - padding: 12px 20px; - font-size: var(--text-sm); + gap: 2px; + padding: 8px 12px; + font-size: 11px; font-weight: var(--font-medium); color: var(--text-tertiary); background: none; @@ -183,6 +197,7 @@ cursor: pointer; transition: all var(--transition-default); white-space: nowrap; + flex-shrink: 0; } /* Tab Icon */ @@ -207,7 +222,7 @@ /* Active State with Underline */ .view-tab.active { color: var(--primary); - background: rgba(59, 130, 246, 0.1); + background: rgba(200, 168, 81, 0.1); border-radius: var(--radius-md); } @@ -256,6 +271,9 @@ display: flex; align-items: center; gap: var(--spacing-2); + flex: 1 1 auto; + min-width: 150px; + max-width: 450px; } .search-icon { @@ -266,8 +284,8 @@ } .search-input { - width: 450px; - min-width: 350px; + width: 100%; + max-width: 450px; padding: 10px 16px; background: var(--bg-tertiary); border: 1px solid transparent; @@ -439,6 +457,23 @@ border-radius: var(--radius-full); cursor: pointer; 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 { @@ -447,16 +482,16 @@ } .user-dropdown { - position: absolute; - top: calc(100% + 8px); - right: 0; + position: fixed; + top: 60px; + right: 20px; min-width: 220px; padding: var(--spacing-2); background: var(--bg-card); border: 1px solid var(--border-default); border-radius: var(--radius-xl); box-shadow: var(--shadow-lg); - z-index: var(--z-dropdown); + z-index: 9999; } .user-info { @@ -524,34 +559,125 @@ FILTER BAR ======================================== */ -.filter-bar { +.filter-bar-actions { display: flex; + gap: var(--spacing-2); 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; - width: 100%; } -.filter-group { +.filter-toggle-btn { display: flex; 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); } -.filter-group label { - font-size: var(--text-sm); - font-weight: var(--font-medium); - color: var(--text-tertiary); +.filter-popover-row { + display: flex; + flex-direction: column; + 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; + flex-direction: column; + gap: 2px; + max-height: 200px; + overflow-y: auto; +} + +.filter-checkbox-item { + display: flex; + align-items: center; 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); } -.view-board .filter-bar { - flex-shrink: 0; -} - .board { display: flex; gap: var(--spacing-3); flex: 1; min-height: 0; overflow-x: auto; + overflow-y: hidden; padding: var(--spacing-4); align-items: flex-start; + width: 100%; + box-sizing: border-box; } /* Column */ .column { display: flex; flex-direction: column; - width: 280px; - min-width: 260px; + width: var(--column-width, 280px); + min-width: var(--column-min-width, 260px); + max-width: 100%; max-height: calc(100vh - var(--header-height) - 160px); background: var(--bg-card); border: 1px solid var(--border-light); @@ -697,7 +823,10 @@ text-transform: uppercase; letter-spacing: 0.05em; flex: 1; - padding-right: var(--spacing-8); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .column-count { @@ -716,12 +845,12 @@ .column-actions { display: flex; + align-items: center; gap: var(--spacing-1); opacity: 0; transition: opacity var(--transition-fast); - position: absolute; - top: var(--spacing-3); - right: var(--spacing-3); + flex-shrink: 0; + margin-left: var(--spacing-2); } .column-actions .btn-icon { @@ -1276,75 +1405,3 @@ 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); -} diff --git a/frontend/css/calendar.css b/frontend/css/calendar.css index e5acad2..f5c7638 100644 --- a/frontend/css/calendar.css +++ b/frontend/css/calendar.css @@ -9,7 +9,7 @@ ======================================== */ .view-calendar { - display: flex; + display: none; flex-direction: column; padding: var(--spacing-6); gap: var(--spacing-4); @@ -17,6 +17,10 @@ height: 100%; } +.view-calendar.active { + display: flex !important; +} + /* Calendar Header */ .calendar-header { display: flex; diff --git a/frontend/css/coding.css b/frontend/css/coding.css index abcef9d..ed012e6 100644 --- a/frontend/css/coding.css +++ b/frontend/css/coding.css @@ -175,8 +175,8 @@ } .git-status-badge.ahead { - background: rgba(59, 130, 246, 0.15); - color: #3B82F6; + background: rgba(124, 141, 181, 0.15); + color: var(--info); } .git-status-badge.behind { diff --git a/frontend/css/components.css b/frontend/css/components.css index b23248a..036d4e0 100644 --- a/frontend/css/components.css +++ b/frontend/css/components.css @@ -170,7 +170,7 @@ select { width: 100%; padding: 10px 14px; 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); background: var(--bg-input); border: 1px solid var(--border-default); @@ -604,7 +604,8 @@ input[type="color"] { align-items: center; gap: var(--spacing-3); width: 100%; - padding: var(--spacing-2) var(--spacing-3); + padding: var(--spacing-3) var(--spacing-4); + min-height: 44px; font-size: var(--text-sm); color: var(--text-secondary); background: none; diff --git a/frontend/css/contacts.css b/frontend/css/contacts.css index 06cd45f..0ac82ba 100644 --- a/frontend/css/contacts.css +++ b/frontend/css/contacts.css @@ -1,578 +1,1186 @@ /** - * TASKMATE - Contacts Styles + * TASKMATE - Kontakte Styles * ========================== - * Tabellenansicht für Kontakte mit erweiterten Funktionen + * Styles für das Kontaktmanagement-Modul */ /* ============================================================================= - VIEW CONTAINER + LAYOUT ============================================================================= */ -.view-contacts { - height: 100%; - overflow: hidden; +.contacts-layout { display: flex; - flex-direction: column; -} - -.view-contacts .view-wrapper { - flex: 1; - overflow-y: auto; - padding: var(--spacing-6); - max-width: 1400px; - width: 100%; - margin: 0 auto; + height: 100%; + gap: var(--spacing-4); } /* ============================================================================= - HEADER & CONTROLS + HEADER FILTER BUTTONS ============================================================================= */ +.header-center { + display: flex; + align-items: center; + flex: 1; + justify-content: center; +} + +.filter-buttons { + display: flex; + gap: var(--spacing-2); + background: var(--bg-tertiary); + padding: 4px; + border-radius: var(--radius-lg); +} + +.filter-btn { + display: flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-2) var(--spacing-3); + border: none; + background: transparent; + border-radius: var(--radius-md); + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + white-space: nowrap; +} + +.filter-btn:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.filter-btn.active { + background: var(--bg-primary); + color: var(--primary); + box-shadow: var(--shadow-sm); +} + +.filter-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + background: var(--bg-tertiary); + border-radius: var(--radius-full); + font-size: var(--text-xs); + font-weight: var(--font-semibold); +} + +.filter-btn.active .filter-badge { + background: var(--primary); + color: white; +} + + +/* ============================================================================= + MAIN CONTENT + ============================================================================= */ + +.contacts-main { + flex: 1; + display: flex; + flex-direction: column; + background: var(--bg-primary); + border-radius: var(--radius-xl); + border: 1px solid var(--border-default); + overflow: hidden; +} + +.contacts-main.full-width { + width: 100%; +} + .contacts-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: var(--spacing-6); - flex-wrap: wrap; + padding: var(--spacing-5); + border-bottom: 1px solid var(--border-default); + background: var(--bg-secondary); gap: var(--spacing-4); } +.header-left { + display: flex; + align-items: baseline; + gap: var(--spacing-3); +} + .contacts-header h2 { - font-size: var(--text-2xl); + font-size: var(--text-xl); font-weight: var(--font-semibold); color: var(--text-primary); margin: 0; } +.contact-count { + font-size: var(--text-sm); + color: var(--text-secondary); +} + .header-actions { display: flex; gap: var(--spacing-3); - align-items: center; -} - -.contacts-stats { - display: flex; - align-items: center; - gap: var(--spacing-2); - color: var(--text-secondary); - font-size: var(--text-sm); - padding: var(--spacing-2) var(--spacing-3); - background: var(--bg-secondary); - border-radius: var(--radius-lg); - border: 1px solid var(--border-default); } /* ============================================================================= - CONTROLS BAR + CONTACT LIST ============================================================================= */ -.contacts-controls { - background: var(--bg-secondary); - border: 1px solid var(--border-default); - border-radius: var(--radius-xl); - padding: var(--spacing-4); - margin-bottom: var(--spacing-5); +.contacts-content { + flex: 1; + overflow-y: auto; + padding: var(--spacing-5); } -.contacts-controls-top { +.contacts-list { display: flex; - justify-content: space-between; - align-items: center; - gap: var(--spacing-4); - flex-wrap: wrap; + flex-direction: column; + gap: var(--spacing-6); } -.bulk-actions { +.contact-group { display: flex; + flex-direction: column; gap: var(--spacing-3); - align-items: center; } -.bulk-actions.hidden { - display: none; -} - -.bulk-actions-info { +.group-letter { font-size: var(--text-sm); - color: var(--text-primary); - font-weight: var(--font-medium); -} - -.contacts-filters { - display: flex; - gap: var(--spacing-4); - align-items: center; - flex-wrap: wrap; -} - -.filter-group { - position: relative; -} - -.filter-select { - min-width: 180px; - height: 36px; - padding: 0 var(--spacing-4); - padding-right: 36px; - background: var(--bg-primary); - border: 1px solid var(--border-default); - border-radius: var(--radius-lg); - color: var(--text-primary); - font-size: var(--text-sm); - font-weight: var(--font-medium); - appearance: none; - cursor: pointer; - transition: all var(--transition-fast); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right var(--spacing-2) center; - background-size: 14px; -} - -.filter-select:hover { - border-color: var(--border-dark); - background-color: var(--bg-hover); -} - -.filter-select:focus { - border-color: var(--primary); - box-shadow: var(--shadow-focus); - outline: none; -} - -/* ============================================================================= - TABLE LAYOUT - ============================================================================= */ - -.contacts-table-container { - background: var(--bg-primary); - border: 1px solid var(--border-default); - border-radius: var(--radius-xl); - overflow: hidden; -} - -.contacts-table { - width: 100%; - border-collapse: separate; - border-spacing: 0; -} - -.contacts-table th, -.contacts-table td { - padding: var(--spacing-3) var(--spacing-4); - text-align: left; - white-space: nowrap; -} - -.contacts-table th { - background: var(--bg-secondary); font-weight: var(--font-semibold); - color: var(--text-primary); - font-size: var(--text-sm); - position: sticky; - top: 0; - z-index: 10; - border-bottom: 2px solid var(--border-default); -} - -.contacts-table th:first-child { - padding-left: var(--spacing-4); - width: 40px; -} - -.contacts-table th:last-child { - padding-right: var(--spacing-4); - text-align: right; -} - -/* Sortable headers */ -.sortable { - cursor: pointer; - user-select: none; - position: relative; - padding-right: 24px; -} - -.sortable:hover { - color: var(--primary); -} - -.sortable::after { - content: ''; - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - width: 0; - height: 0; - border-left: 4px solid transparent; - border-right: 4px solid transparent; - opacity: 0.3; -} - -.sortable.sort-asc::after { - border-bottom: 6px solid var(--primary); - opacity: 1; -} - -.sortable.sort-desc::after { - border-top: 6px solid var(--primary); - opacity: 1; -} - -/* Table rows */ -.contacts-table tbody tr { - border-bottom: 1px solid var(--border-default); - transition: background-color var(--transition-fast); -} - -.contacts-table tbody tr:hover { - background-color: var(--bg-hover); -} - -.contacts-table tbody tr.selected { - background-color: var(--bg-tertiary); -} - -.contacts-table td { color: var(--text-secondary); - font-size: var(--text-sm); + margin: 0; + padding: 0 var(--spacing-2); + text-transform: uppercase; } -.contacts-table td:first-child { - padding-left: var(--spacing-4); -} - -.contacts-table td:last-child { - padding-right: var(--spacing-4); -} - -/* Checkbox column */ -.checkbox-cell { - width: 40px; - text-align: center !important; -} - -.table-checkbox { - width: 18px; - height: 18px; - cursor: pointer; - appearance: none; - border: 2px solid var(--border-default); - border-radius: var(--radius-sm); - background: var(--bg-primary); - position: relative; - transition: all var(--transition-fast); -} - -.table-checkbox:hover { - border-color: var(--primary); -} - -.table-checkbox:checked { - background: var(--primary); - border-color: var(--primary); -} - -.table-checkbox:checked::after { - content: '✓'; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: white; - font-size: 12px; - font-weight: bold; -} - -/* Name column with avatar */ -.name-cell { +.contact-cards { display: flex; - align-items: center; - gap: var(--spacing-3); - min-width: 200px; + flex-direction: column; + gap: var(--spacing-2); } -.contact-avatar-small { - width: 32px; - height: 32px; - background: var(--primary); - color: white; +/* ============================================================================= + CONTACT CARD + ============================================================================= */ + +.contact-card { + display: flex; + align-items: flex-start; + gap: var(--spacing-4); + padding: var(--spacing-4); + background: var(--bg-secondary); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + cursor: pointer; + transition: all var(--transition-fast); + position: relative; +} + +.contact-card:hover { + border-color: var(--primary); + box-shadow: var(--shadow-sm); + transform: translateY(-1px); +} + +.contact-avatar { + width: 48px; + height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: var(--font-semibold); - font-size: 12px; + color: white; flex-shrink: 0; + overflow: hidden; } -.contact-name-link { - color: var(--text-primary); +.contact-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.contact-avatar svg { + stroke: white; +} + +.contact-info { + flex: 1; + min-width: 0; +} + +.contact-name { + font-size: var(--text-base); font-weight: var(--font-medium); - text-decoration: none; - cursor: pointer; + color: var(--text-primary); + margin: 0 0 var(--spacing-1) 0; } -.contact-name-link:hover { - color: var(--primary); - text-decoration: underline; -} - -/* Tags cell */ -.tags-cell { - display: flex; - gap: var(--spacing-1); - flex-wrap: wrap; - max-width: 200px; -} - -.contact-tag { - background: var(--bg-tertiary); +.contact-position, +.contact-industry { + font-size: var(--text-sm); color: var(--text-secondary); - font-size: 11px; + margin: 0 0 var(--spacing-2) 0; +} + +.contact-details { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-3); + font-size: var(--text-xs); + color: var(--text-tertiary); +} + +.detail-item { + display: flex; + align-items: center; + gap: var(--spacing-1); +} + +.detail-item .icon { + width: 14px; + height: 14px; + opacity: 0.6; +} + +.contact-tags { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-1); + margin-top: var(--spacing-2); +} + +.contact-tags .tag { + font-size: var(--text-xs); padding: 2px 8px; border-radius: var(--radius-full); - border: 1px solid var(--border-default); font-weight: var(--font-medium); - white-space: nowrap; } -/* Actions column */ -.actions-cell { - text-align: right !important; +.contact-type { + position: absolute; + top: var(--spacing-4); + right: var(--spacing-4); } -.table-actions { - display: flex; - gap: var(--spacing-2); - justify-content: flex-end; +.type-badge { + font-size: var(--text-xs); + font-weight: var(--font-medium); + padding: 2px 10px; + border-radius: var(--radius-full); + text-transform: uppercase; } -.btn-table-action { - background: transparent; - border: 1px solid var(--border-default); - color: var(--text-secondary); - width: 32px; - height: 32px; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-md); - transition: all var(--transition-fast); - cursor: pointer; +.type-badge.type-person { + background: #1a1a1a15; + color: #1a1a1a; } -.btn-table-action:hover { - background: var(--primary); - color: white; - border-color: var(--primary); - transform: scale(1.05); -} - -.btn-table-action svg { - width: 16px; - height: 16px; -} - -/* Export button */ -.btn-export { - display: flex; - align-items: center; - gap: var(--spacing-2); -} - -.btn-export svg { - width: 16px; - height: 16px; +.type-badge.type-company { + background: #6b728015; + color: #6b7280; } /* ============================================================================= EMPTY STATE ============================================================================= */ -.contacts-empty { +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; text-align: center; - padding: var(--spacing-8) var(--spacing-4); - background: var(--bg-secondary); - border-radius: var(--radius-xl); - border: 1px solid var(--border-default); - max-width: 500px; - margin: var(--spacing-8) auto; -} - -.contacts-empty .empty-icon { - color: var(--text-tertiary); - margin-bottom: var(--spacing-4); - opacity: 0.5; -} - -.contacts-empty .empty-icon svg { - width: 64px; - height: 64px; -} - -.contacts-empty h3 { - font-size: var(--text-xl); - font-weight: var(--font-semibold); - margin-bottom: var(--spacing-2); - color: var(--text-primary); -} - -.contacts-empty p { + padding: var(--spacing-8); color: var(--text-secondary); +} + +.empty-state .empty-icon { + opacity: 0.2; margin-bottom: var(--spacing-4); } +.empty-state h3 { + font-size: var(--text-lg); + font-weight: var(--font-semibold); + color: var(--text-primary); + margin: 0 0 var(--spacing-2) 0; +} + +.empty-state p { + margin: 0; + color: var(--text-secondary); +} + /* ============================================================================= - CONTACT MODAL + DETAIL VIEW ============================================================================= */ -#contact-form { - display: grid; - gap: var(--space-md); +.contact-detail { + padding: var(--spacing-6); + max-width: 800px; + margin: 0 auto; } -#contact-form .form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-md); +.detail-header { + display: flex; + align-items: flex-start; + gap: var(--spacing-5); + margin-bottom: var(--spacing-6); + padding-bottom: var(--spacing-6); + border-bottom: 1px solid var(--border-default); } -#contact-form .form-group.full-width { - grid-column: 1 / -1; -} - -#contact-form .tags-input-wrapper { +.detail-avatar { + width: 120px; + height: 120px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--text-3xl); + font-weight: var(--font-bold); + color: white; + flex-shrink: 0; + overflow: hidden; position: relative; } -#contact-form .tags-input-wrapper input { - padding-right: 32px; +.detail-avatar img { + width: 100%; + height: 100%; + object-fit: cover; } -#contact-form .tags-help { - font-size: 11px; +.avatar-upload { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.7); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity var(--transition-fast); + cursor: pointer; +} + +.detail-avatar:hover .avatar-upload { + opacity: 1; +} + +.avatar-upload .icon { + color: white; +} + +.detail-info { + flex: 1; +} + +.detail-name { + font-size: var(--text-2xl); + font-weight: var(--font-semibold); + color: var(--text-primary); + margin: 0 0 var(--spacing-2) 0; +} + +.detail-subtitle { + font-size: var(--text-base); + color: var(--text-secondary); + margin: 0 0 var(--spacing-4) 0; +} + +.detail-actions { + display: flex; + gap: var(--spacing-2); + flex-wrap: wrap; +} + +/* Detail Sections */ +.detail-section { + margin-bottom: var(--spacing-6); +} + +.section-title { + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0 0 var(--spacing-3) 0; + display: flex; + align-items: center; + gap: var(--spacing-2); +} + +.section-content { + background: var(--bg-secondary); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + padding: var(--spacing-4); +} + +.info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-4); +} + +.info-item { + display: flex; + flex-direction: column; + gap: var(--spacing-1); +} + +.info-label { + font-size: var(--text-xs); + font-weight: var(--font-medium); color: var(--text-tertiary); - margin-top: 4px; + text-transform: uppercase; +} + +.info-value { + font-size: var(--text-sm); + color: var(--text-primary); +} + +/* Contact Details List */ +.details-list { + display: flex; + flex-direction: column; + gap: var(--spacing-3); +} + +.detail-entry { + display: flex; + align-items: center; + gap: var(--spacing-3); +} + +.detail-entry .icon { + width: 20px; + height: 20px; + color: var(--text-tertiary); +} + +.detail-content { + flex: 1; +} + +.detail-value { + font-size: var(--text-sm); + color: var(--text-primary); +} + +.detail-label { + font-size: var(--text-xs); + color: var(--text-tertiary); +} + +/* Interactions */ +.interactions-list { + display: flex; + flex-direction: column; + gap: var(--spacing-2); +} + +.interaction-item { + padding: var(--spacing-3); + background: var(--bg-tertiary); + border-radius: var(--radius-md); + display: flex; + gap: var(--spacing-3); + align-items: flex-start; +} + +.interaction-icon { + width: 32px; + height: 32px; + background: var(--bg-secondary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.interaction-content { + flex: 1; +} + +.interaction-header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: var(--spacing-1); +} + +.interaction-title { + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--text-primary); +} + +.interaction-time { + font-size: var(--text-xs); + color: var(--text-tertiary); +} + +.interaction-body { + font-size: var(--text-sm); + color: var(--text-secondary); } /* ============================================================================= - PAGINATION + FORM ============================================================================= */ -.pagination { +.modal .contact-form-container { + background: var(--bg-primary); + max-width: 900px; + width: 90vw; + max-height: 90vh; + overflow: hidden; + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); display: flex; - justify-content: center; - align-items: center; - gap: var(--spacing-2); - padding: var(--spacing-4); - border-top: 1px solid var(--border-default); - background: var(--bg-secondary); + flex-direction: column; } -.pagination-btn { +.contact-form-container { background: var(--bg-primary); - border: 1px solid var(--border-default); + max-width: 800px; + margin: 0 auto; + border-radius: var(--radius-xl); + overflow: hidden; + box-shadow: var(--shadow-lg); +} + +.contact-form { + padding: var(--spacing-6); + overflow-y: auto; + max-height: calc(90vh - 150px); +} + +.form-header { + margin-bottom: var(--spacing-6); + text-align: center; +} + +.form-title { + font-size: var(--text-xl); + font-weight: var(--font-semibold); + color: var(--text-primary); + margin: 0; +} + +.form-section { + margin-bottom: var(--spacing-6); +} + +.form-section-title { + font-size: var(--text-sm); + font-weight: var(--font-semibold); color: var(--text-secondary); - padding: var(--spacing-2) var(--spacing-3); + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0 0 var(--spacing-4) 0; +} + +/* Contact Type Toggle */ +.contact-type-toggle { + display: flex; + gap: var(--spacing-2); + margin-bottom: var(--spacing-6); + background: var(--bg-secondary); + padding: var(--spacing-1); + border-radius: var(--radius-lg); +} + +.type-option { + flex: 1; + padding: var(--spacing-3); + border: none; + background: transparent; border-radius: var(--radius-md); font-size: var(--text-sm); font-weight: var(--font-medium); + color: var(--text-secondary); cursor: pointer; transition: all var(--transition-fast); } -.pagination-btn:hover:not(:disabled) { - background: var(--bg-hover); - border-color: var(--primary); +.type-option.active { + background: var(--bg-primary); color: var(--primary); + box-shadow: var(--shadow-sm); } -.pagination-btn:disabled { - opacity: 0.5; - cursor: not-allowed; +/* Form Fields */ +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-4); + margin-bottom: var(--spacing-4); } -.pagination-info { - color: var(--text-secondary); +.form-group.full-width { + grid-column: 1 / -1; +} + +/* Contact Details Fields */ +.contact-details-fields { + display: flex; + flex-direction: column; + gap: var(--spacing-3); +} + +.detail-field-group { + display: flex; + gap: var(--spacing-2); + align-items: flex-end; +} + +.detail-field-group .form-group { + flex: 1; + margin-bottom: 0; +} + +.detail-field-group .form-group:first-child { + flex: 0 0 120px; +} + +.btn-remove-detail { + width: 36px; + height: 36px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + background: var(--bg-secondary); + border: 1px solid var(--border-default); + color: var(--danger); + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-remove-detail:hover { + background: var(--danger); + color: white; + border-color: var(--danger); +} + +.btn-add-detail { + align-self: flex-start; font-size: var(--text-sm); - font-weight: var(--font-medium); + display: flex; + align-items: center; + gap: var(--spacing-2); } /* ============================================================================= RESPONSIVE ============================================================================= */ -@media (max-width: 1200px) { - .contacts-table-container { - overflow-x: auto; - } - - .contacts-table { - min-width: 900px; - } -} - -@media (max-width: 968px) { - .view-contacts .view-wrapper { - padding: var(--spacing-4); - } - +@media (max-width: 1024px) { .contacts-header { - flex-direction: column; - align-items: stretch; - } - - .header-actions { - flex-direction: column; - width: 100%; - gap: var(--spacing-2); - } - - .contacts-controls { - padding: var(--spacing-3); - } - - .contacts-controls-top { - flex-direction: column; - align-items: stretch; - } - - .bulk-actions { - width: 100%; - justify-content: space-between; - } - - .filter-select { - min-width: 120px; - font-size: var(--text-xs); + flex-wrap: wrap; } - .contacts-table th, - .contacts-table td { - padding: var(--spacing-2) var(--spacing-3); + .header-center { + order: 3; + width: 100%; + margin-top: var(--spacing-3); + } + + .filter-buttons { + width: 100%; + justify-content: center; } } @media (max-width: 768px) { + .contacts-header { + flex-direction: column; + gap: var(--spacing-3); + align-items: stretch; + } + + .header-left { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-1); + } + + .header-actions { + width: 100%; + } + + .header-actions .btn { + flex: 1; + justify-content: center; + } + + .detail-header { + flex-direction: column; + align-items: center; + text-align: center; + } + + .detail-actions { + justify-content: center; + } + .form-row { grid-template-columns: 1fr; } - - .modal-actions { - flex-direction: column-reverse; - gap: var(--space-sm); - } - - .modal-actions-left { - width: 100%; - justify-content: space-between; + + .detail-field-group { + flex-wrap: wrap; } - /* Mobile: Hide less important columns */ - .hide-mobile { - display: none; + .detail-field-group .form-group:first-child { + flex: 1 1 100%; } +} + +/* ============================================================================= + ADDITIONAL STYLES + ============================================================================= */ + +/* Detail View Styles */ +.btn-back { + display: flex; + align-items: center; + gap: var(--spacing-2); + background: transparent; + border: none; + color: var(--text-secondary); + font-size: var(--text-sm); + cursor: pointer; + padding: var(--spacing-2); + margin-left: calc(var(--spacing-2) * -1); + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +.btn-back:hover { + background: var(--bg-secondary); + color: var(--primary); +} + +.detail-content { + display: flex; + gap: var(--spacing-6); + margin-top: var(--spacing-6); +} + +.detail-main { + flex: 1; + min-width: 0; +} + +.detail-sidebar { + width: 300px; + flex-shrink: 0; +} + +.contact-header-detail { + display: flex; + align-items: center; + gap: var(--spacing-5); + margin-bottom: var(--spacing-6); + padding-bottom: var(--spacing-6); + border-bottom: 1px solid var(--border-default); +} + +.avatar-large { + width: 100px; + height: 100px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--text-3xl); + font-weight: var(--font-bold); + color: white; + flex-shrink: 0; + overflow: hidden; +} + +.avatar-large img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.contact-title h1 { + font-size: var(--text-2xl); + font-weight: var(--font-semibold); + margin: 0 0 var(--spacing-2) 0; + color: var(--text-primary); +} + +.contact-title .subtitle { + font-size: var(--text-base); + color: var(--text-secondary); + margin: 0; +} + +/* Details Grid */ +.details-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: var(--spacing-4); +} + +.detail-item { + display: flex; + gap: var(--spacing-3); + align-items: flex-start; +} + +.detail-icon { + color: var(--text-tertiary); + flex-shrink: 0; +} + +.detail-content { + display: flex; + flex-direction: column; + gap: var(--spacing-1); + flex: 1; +} + +.detail-label { + font-size: var(--text-xs); + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.detail-value { + font-size: var(--text-sm); + color: var(--text-primary); +} + +/* Info List */ +.info-list { + display: grid; + grid-template-columns: 120px 1fr; + gap: var(--spacing-3) var(--spacing-4); + margin: 0; +} + +.info-list dt { + font-size: var(--text-sm); + color: var(--text-tertiary); + font-weight: var(--font-medium); +} + +.info-list dd { + font-size: var(--text-sm); + color: var(--text-primary); + margin: 0; +} + +.info-list dd a { + color: var(--primary); + text-decoration: none; +} + +.info-list dd a:hover { + text-decoration: underline; +} + +/* Notes */ +.notes-content { + background: var(--bg-secondary); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + padding: var(--spacing-4); + font-size: var(--text-sm); + line-height: 1.6; + white-space: pre-wrap; +} + +/* Meta List */ +.meta-list { + display: flex; + flex-direction: column; + gap: var(--spacing-3); + margin: 0; +} + +.meta-list dt { + font-size: var(--text-xs); + color: var(--text-tertiary); + font-weight: var(--font-medium); + text-transform: uppercase; + margin-bottom: var(--spacing-1); +} + +.meta-list dd { + font-size: var(--text-sm); + color: var(--text-primary); + margin: 0 0 var(--spacing-3) 0; +} + +.meta-list dd:last-child { + margin-bottom: 0; +} + +/* Form Styles */ +.form-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-5); + border-bottom: 1px solid var(--border-default); + background: var(--bg-secondary); +} + +.form-header h2 { + font-size: var(--text-xl); + font-weight: var(--font-semibold); + color: var(--text-primary); + margin: 0; +} + +.btn-close { + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: var(--spacing-2); + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +.btn-close:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.radio-group { + display: flex; + gap: var(--spacing-4); +} + +.radio-item { + display: flex; + align-items: center; + gap: var(--spacing-2); + cursor: pointer; +} + +.radio-item input[type="radio"] { + margin: 0; +} + +.checkbox-item { + display: flex; + align-items: center; + gap: var(--spacing-2); + cursor: pointer; + padding: var(--spacing-2); + border-radius: var(--radius-md); + transition: background var(--transition-fast); +} + +.checkbox-item:hover { + background: var(--bg-hover); +} + +.checkbox-item input[type="checkbox"] { + margin: 0; +} + +/* Detail Controls */ +.detail-controls { + display: flex; + gap: var(--spacing-2); + align-items: center; + margin-bottom: var(--spacing-3); +} + +.detail-controls .form-control { + flex: 1; +} + +.detail-type { + flex: 0 0 120px; +} + +.detail-label { + flex: 0 0 120px; +} + +.detail-value { + flex: 2; +} + +.checkbox-primary { + display: flex; + align-items: center; + gap: var(--spacing-1); + font-size: var(--text-sm); + white-space: nowrap; +} + +.btn-icon { + background: transparent; + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + padding: var(--spacing-2); + cursor: pointer; + color: var(--text-secondary); + transition: all var(--transition-fast); +} + +.btn-icon:hover { + border-color: var(--danger); + color: var(--danger); + background: var(--danger)10; +} + +.btn-remove-detail { + flex-shrink: 0; +} + +.details-list { + display: flex; + flex-direction: column; + gap: var(--spacing-2); +} + +/* Interactions */ +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-3); +} + +.interactions-list { + display: flex; + flex-direction: column; + gap: var(--spacing-3); +} + +.interaction-item { + display: flex; + gap: var(--spacing-3); + padding: var(--spacing-3); + background: var(--bg-secondary); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); +} + +.interaction-icon { + width: 36px; + height: 36px; + background: var(--bg-tertiary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + flex-shrink: 0; +} + +.interaction-content { + flex: 1; + min-width: 0; +} + +.interaction-header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: var(--spacing-1); +} + +.interaction-type { + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--text-secondary); +} + +.interaction-date { + font-size: var(--text-xs); + color: var(--text-tertiary); +} + +.interaction-content h4 { + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--text-primary); + margin: var(--spacing-1) 0; +} + +.interaction-content p { + font-size: var(--text-sm); + color: var(--text-secondary); + margin: var(--spacing-1) 0; + line-height: 1.5; +} + +.interaction-author { + font-size: var(--text-xs); + color: var(--text-tertiary); + margin-top: var(--spacing-2); +} + +.text-muted { + color: var(--text-tertiary); + font-size: var(--text-sm); + font-style: italic; +} + +/* Tags List */ +.tags-list { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-2); +} + +.sidebar-section { + background: var(--bg-secondary); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + padding: var(--spacing-4); + margin-bottom: var(--spacing-4); +} + +.sidebar-section h4 { + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: var(--text-primary); + margin: 0 0 var(--spacing-3) 0; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: var(--spacing-3); + padding: var(--spacing-4); + border-top: 1px solid var(--border-default); + background: var(--bg-secondary); + margin: var(--spacing-6) calc(var(--spacing-6) * -1) calc(var(--spacing-6) * -1); +} + +.required { + color: var(--danger); } \ No newline at end of file diff --git a/frontend/css/gitea.css b/frontend/css/gitea.css index 3d69c9e..512440b 100644 --- a/frontend/css/gitea.css +++ b/frontend/css/gitea.css @@ -796,7 +796,7 @@ .drop-zone.drag-over { border-color: var(--primary); - background: rgba(59, 130, 246, 0.05); + background: rgba(200, 168, 81, 0.08); color: var(--primary); } diff --git a/frontend/css/knowledge.css b/frontend/css/knowledge.css index 5ae914e..8d5e1ea 100644 --- a/frontend/css/knowledge.css +++ b/frontend/css/knowledge.css @@ -287,10 +287,10 @@ background-color: transparent; } 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% { - background-color: var(--primary-200, rgba(59, 130, 246, 0.2)); + background-color: var(--primary-200, rgba(200, 168, 81, 0.2)); } } diff --git a/frontend/css/list.css b/frontend/css/list.css index d0b75a2..b3a4146 100644 --- a/frontend/css/list.css +++ b/frontend/css/list.css @@ -13,7 +13,7 @@ } .view-list.active { - display: flex; + display: flex !important; } /* List Header */ diff --git a/frontend/css/mobile.css b/frontend/css/mobile.css index 73ab1b8..03b18c3 100644 --- a/frontend/css/mobile.css +++ b/frontend/css/mobile.css @@ -71,6 +71,82 @@ 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 ======================================== */ @@ -436,6 +512,221 @@ min-height: 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) { 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); + } } diff --git a/frontend/css/modal.css b/frontend/css/modal.css index 1c11d1d..afc6069 100644 --- a/frontend/css/modal.css +++ b/frontend/css/modal.css @@ -77,8 +77,8 @@ display: flex; align-items: center; justify-content: center; - width: 32px; - height: 32px; + width: 44px; + height: 44px; font-size: 20px; color: var(--text-muted); background: none; @@ -770,7 +770,9 @@ .lightbox-close { position: absolute; top: var(--spacing-6); + top: calc(var(--spacing-6) + env(safe-area-inset-top, 0px)); right: var(--spacing-6); + right: calc(var(--spacing-6) + env(safe-area-inset-right, 0px)); width: 48px; height: 48px; font-size: 32px; diff --git a/frontend/css/notifications.css b/frontend/css/notifications.css index ad76245..6eb748b 100644 --- a/frontend/css/notifications.css +++ b/frontend/css/notifications.css @@ -59,16 +59,16 @@ ============================================================================= */ .notification-dropdown { - position: absolute; - top: calc(100% + 8px); - right: 0; + position: fixed; + top: 60px; + right: 20px; width: 380px; max-height: 520px; background: var(--bg-card); border: 1px solid var(--border-default); border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); - z-index: var(--z-dropdown); + z-index: 10001; overflow: hidden; display: flex; flex-direction: column; diff --git a/frontend/css/reminders.css b/frontend/css/reminders.css index 05ca789..c93fb08 100644 --- a/frontend/css/reminders.css +++ b/frontend/css/reminders.css @@ -175,7 +175,7 @@ .reminder-number-input:focus { border-color: var(--primary); outline: none; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + box-shadow: var(--shadow-focus); } .reminder-unit-select { @@ -200,7 +200,7 @@ .reminder-unit-select:focus { border-color: var(--primary); outline: none; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + box-shadow: var(--shadow-focus); } .reminder-advance-suffix { @@ -293,7 +293,7 @@ .custom-select-trigger.active { border-color: var(--primary); - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + box-shadow: var(--shadow-focus); } .custom-select-value { @@ -477,7 +477,7 @@ border-color: #1d4ed8; color: white !important; 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 { diff --git a/frontend/css/responsive.css b/frontend/css/responsive.css index 4afd8fd..4fc1d67 100644 --- a/frontend/css/responsive.css +++ b/frontend/css/responsive.css @@ -4,6 +4,57 @@ * 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) ======================================== */ @@ -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) ======================================== */ @media (max-width: 768px) { .header { - flex-wrap: wrap; - height: auto; - padding: var(--spacing-sm) var(--spacing-md); - gap: var(--spacing-sm); + display: flex; + align-items: center; + height: 60px; + padding: 0 var(--spacing-sm); + gap: var(--spacing-xs); + overflow: hidden; } .header-left { - flex: 1; - order: 1; + flex: 0 0 auto; + display: flex; + align-items: center; + gap: var(--spacing-xs); } - .header-center { - order: 3; - width: 100%; - justify-content: center; - padding-top: var(--spacing-sm); - border-top: 1px solid var(--border-default); + .view-tabs-bar { + display: none; /* Navigation wird ins Mobile Menu verschoben */ } .header-right { - order: 2; + flex: 0 0 auto; } .logo { @@ -72,28 +181,18 @@ display: none; } - .view-tabs { - width: 100%; - justify-content: center; + /* Mobile Navigation wird über Hamburger Menu gesteuert */ + .mobile-menu { + display: block; + } + + /* Logo kleiner auf Mobile */ + .logo { + font-size: var(--text-lg); } - .view-tab { - flex: 1; - 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; + .filter-bar-actions { + display: none; } .view-board { @@ -101,16 +200,119 @@ 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 { - width: calc(100vw - 32px); - min-width: calc(100vw - 32px); - max-height: none; + width: 100%; + min-width: unset; + 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 { - width: calc(100vw - 32px); - min-width: calc(100vw - 32px); + width: 100%; + min-width: unset; 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 { @@ -264,6 +466,7 @@ left: var(--spacing-md); right: var(--spacing-md); bottom: var(--spacing-md); + bottom: calc(var(--spacing-md) + env(safe-area-inset-bottom, 0px)); } .toast { @@ -291,7 +494,7 @@ @media print { .header, - .filter-bar, + .view-tabs-bar, .btn-add-column, .column-actions, .btn-add-task, diff --git a/frontend/css/variables.css b/frontend/css/variables.css index 81cf623..579b27c 100644 --- a/frontend/css/variables.css +++ b/frontend/css/variables.css @@ -9,26 +9,26 @@ FARBEN - Modernes Light Theme ======================================== */ - /* Primärfarben */ - --primary: #4F46E5; - --primary-hover: #4338CA; - --primary-light: #EEF2FF; - --accent: #06B6D4; - --accent-hover: #0891B2; + /* Primärfarben - AegisSight Light Theme */ + --primary: #C8A851; + --primary-hover: #B5923E; + --primary-light: rgba(200, 168, 81, 0.12); + --accent: #C8A851; + --accent-hover: #B5923E; /* Hintergründe */ - --bg-main: #F8FAFC; + --bg-main: #E8EDF6; --bg-secondary: #FFFFFF; - --bg-tertiary: #F1F5F9; - --bg-hover: #E2E8F0; - --bg-active: #CBD5E1; + --bg-tertiary: #F1F3F9; + --bg-hover: #F8F9FC; + --bg-active: #D0D6DE; --bg-sidebar: #FFFFFF; --bg-card: #FFFFFF; --bg-input: #FFFFFF; /* Textfarben */ - --text-primary: #0F172A; - --text-secondary: #475569; + --text-primary: #0A1832; + --text-secondary: #64748B; --text-tertiary: #64748B; --text-muted: #94A3B8; --text-placeholder: #94A3B8; @@ -44,9 +44,9 @@ --error: #EF4444; --error-bg: #FEE2E2; --error-text: #991B1B; - --info: #3B82F6; - --info-bg: #DBEAFE; - --info-text: #1E40AF; + --info: #7C8DB5; + --info-bg: rgba(124, 141, 181, 0.12); + --info-text: #4A5568; /* Prioritätsfarben */ --priority-high: #EF4444; @@ -60,7 +60,7 @@ --border-light: #F1F5F9; --border-default: #E2E8F0; --border-dark: #CBD5E1; - --border-focus: #4F46E5; + --border-focus: #C8A851; /* Schatten */ --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-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-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-bg: #F1F5F9; - --scrollbar-thumb: #CBD5E1; - --scrollbar-thumb-hover: #94A3B8; + --scrollbar-bg: #E8EDF6; + --scrollbar-thumb: #94A3B8; + --scrollbar-thumb-hover: #64748B; /* Overlay */ --overlay-bg: rgba(15, 23, 42, 0.5); @@ -158,7 +158,7 @@ Z-INDEX ======================================== */ - --z-dropdown: 100; + --z-dropdown: 300; --z-sticky: 200; --z-modal-overlay: 900; --z-modal: 1000; diff --git a/frontend/index.html b/frontend/index.html index af53c46..8c5d1a6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -38,6 +38,7 @@ + @@ -182,77 +183,9 @@ -