UI-Anpassungen
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
7d67557be4
Commit
ef153789cc
@ -57,7 +57,8 @@
|
||||
"Bash(grep -A10 -B5 \"reminder-modal-title\\|modal-title\\|modal-header\" /home/claude-dev/TaskMate/frontend/css/reminders.css)",
|
||||
"Bash(grep -A10 -B5 \"modal-header\\|modal-title\" /home/claude-dev/TaskMate/frontend/css/modal.css)",
|
||||
"Bash(grep -A10 -B5 \"calendar-reminder\\|reminder.*calendar\" /home/claude-dev/TaskMate/frontend/css/reminders.css)",
|
||||
"Bash(grep -A15 -B5 \"calendar.*task\\|task.*calendar\" /home/claude-dev/TaskMate/frontend/js/calendar.js)"
|
||||
"Bash(grep -A15 -B5 \"calendar.*task\\|task.*calendar\" /home/claude-dev/TaskMate/frontend/js/calendar.js)",
|
||||
"WebFetch(domain:admin-panel-undso.aegis-sight.de)"
|
||||
]
|
||||
}
|
||||
}
|
||||
329
CHANGELOG.txt
329
CHANGELOG.txt
@ -1,6 +1,335 @@
|
||||
TASKMATE - CHANGELOG
|
||||
====================
|
||||
|
||||
================================================================================
|
||||
09.01.2026 - ENTFERNUNG: SIMULIERTE VERBRAUCHSANZEIGE IM CODING-MODUL
|
||||
================================================================================
|
||||
|
||||
## ÄNDERUNG
|
||||
Die simulierte Verbrauchsanzeige wurde aus dem Coding-Modul entfernt
|
||||
|
||||
## DETAILS
|
||||
✅ Frontend: Verbrauchsanzeige aus coding.js entfernt
|
||||
✅ CSS: Styles für .coding-tile-usage gelöscht
|
||||
✅ Backend: API-Endpoints bleiben für spätere Nutzung erhalten
|
||||
✅ Datenbank: Tabelle coding_usage bleibt bestehen
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- Entfernt: updateTileUsage() Funktion und alle Aufrufe
|
||||
- Entfernt: Verbrauchs-HTML aus renderTile()
|
||||
- Entfernt: CSS-Animationen und Styling
|
||||
- Cache-Version: 292
|
||||
|
||||
================================================================================
|
||||
09.01.2026 - FEATURE: VERBRAUCHSANZEIGE IM CODING-MODUL
|
||||
================================================================================
|
||||
|
||||
## NEUE FUNKTION
|
||||
Live-Verbrauchsanzeige für Server-Anwendungen im Coding-Tab mit CPU, Memory und Network Metriken
|
||||
|
||||
## IMPLEMENTIERUNG
|
||||
✅ Backend: Neue API-Endpoints für Verbrauchsdaten (simuliert)
|
||||
- GET /api/coding/directories/:id/usage - Aktuelle Verbrauchsdaten
|
||||
- GET /api/coding/directories/:id/usage/history - Historische Daten
|
||||
✅ Datenbank: Neue Tabelle 'coding_usage' für Verlauf
|
||||
✅ Frontend: Verbrauchsanzeige in jeder Coding-Kachel integriert
|
||||
- CPU-Auslastung in % mit Warnung bei >80%
|
||||
- Memory in MB mit Warnung bei >3000MB
|
||||
- Network in MB/s (kombiniert send + receive)
|
||||
✅ Auto-Refresh alle 30 Sekunden für Live-Updates
|
||||
✅ Visuelles Feedback bei hoher Auslastung (Pulse-Animation)
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- coding.js: updateTileUsage() Funktion für Metrik-Updates
|
||||
- api.js: getCodingDirectoryUsage() und getCodingDirectoryUsageHistory()
|
||||
- coding.css: Neue Styles für .coding-tile-usage mit responsive Design
|
||||
- Cache-Version: 291
|
||||
- Docker Container Update erforderlich für Backend-Änderungen
|
||||
|
||||
================================================================================
|
||||
09.01.2025 - BUGFIX: CUSTOM-SELECT DROPDOWN POSITIONING
|
||||
================================================================================
|
||||
|
||||
## PROBLEM
|
||||
Custom-Select Dropdown (Benutzerauswahl in Erinnerungen) positionierte sich nicht korrekt bei Scroll/Modal-Bewegung
|
||||
|
||||
## URSACHE
|
||||
Dropdown nutzte relative statt fixed Positionierung, keine dynamische Neuberechnung
|
||||
|
||||
## LÖSUNG
|
||||
✅ reminders.js: setupCustomSelect() berechnet Position dynamisch mit getBoundingClientRect()
|
||||
✅ Fixed Positioning wie bei Multi-Select implementiert
|
||||
✅ Automatische Neupositionierung bei Scroll/Resize
|
||||
✅ Intelligente Platzierung (oben/unten je nach verfügbarem Platz)
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- Zeilen 346-450: Neue positionDropdown() Funktion mit Event-Handlers
|
||||
- Cache-Version: 285
|
||||
- Position wird bei jedem Öffnen neu berechnet
|
||||
- Event-Handler werden beim Schließen aufgeräumt
|
||||
|
||||
================================================================================
|
||||
09.01.2025 - BUGFIX: FILTER-ASSIGNEE DROPDOWN ZEIGT KEINE NAMEN
|
||||
================================================================================
|
||||
|
||||
## PROBLEM
|
||||
Der Filter-Assignee Dropdown zeigte leere Options ohne Benutzernamen an
|
||||
|
||||
## URSACHE
|
||||
Frontend erwartete "username" Feld, Backend lieferte aber "displayName"
|
||||
|
||||
## LÖSUNG
|
||||
✅ app.js: populateAssigneeFilter() nutzt jetzt displayName statt username
|
||||
✅ Fallback zu email falls displayName fehlt
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- Zeile 736: option.textContent = user.displayName || user.email || 'Unbekannt';
|
||||
- Cache-Version: 284
|
||||
|
||||
================================================================================
|
||||
09.01.2025 - BUGFIXES: DROPDOWN-POSITIONIERUNG & NAVIGATION-DESIGN
|
||||
================================================================================
|
||||
|
||||
## BEHOBEN
|
||||
✅ Custom-Select Dropdown in Reminder-Modal
|
||||
- Position von fixed auf absolute geändert
|
||||
- Dropdown bleibt jetzt innerhalb des Modals
|
||||
- Keine dynamische Positionsberechnung mehr nötig
|
||||
- Einheitliches Design mit Multi-Select
|
||||
|
||||
================================================================================
|
||||
09.01.2025 - NAVIGATION: MODERNES DESIGN MIT BESSERER AKTIV-MARKIERUNG
|
||||
================================================================================
|
||||
|
||||
## NEUE FUNKTIONEN
|
||||
✅ Modernes flaches Design ohne Hintergrund-Container
|
||||
✅ Klare Aktiv-Markierung mit farbigem Unterbalken
|
||||
✅ Icons für alle Navigationspunkte
|
||||
✅ Animierte Hover-Effekte mit Icon-Bewegung
|
||||
✅ Subtile Trennlinien zwischen Tabs
|
||||
✅ Bessere Farbkontraste für aktive/inaktive Tabs
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- board.css: Komplett überarbeitete Navigation-Styles
|
||||
- Aktiver Tab mit blauem Unterbalken + Hintergrund
|
||||
- Hover-States mit sanften Übergängen
|
||||
- Animation für aktiv-Indikator
|
||||
- index.html: SVG-Icons zu allen Tabs hinzugefügt
|
||||
- Service Worker: Cache-Version auf 282 erhöht
|
||||
|
||||
================================================================================
|
||||
08.01.2025 - KONTAKTE: VON KARTENANSICHT ZU TABELLENANSICHT UMGESTELLT
|
||||
================================================================================
|
||||
|
||||
## NEUE FUNKTIONEN
|
||||
✅ Tabellenansicht mit sortierbaren Spalten
|
||||
- Name (mit Avatar-Initialen)
|
||||
- Firma, Position, E-Mail, Telefon, Tags
|
||||
- Direkte Aktionen: Bearbeiten, Löschen
|
||||
✅ Mehrfachauswahl mit Checkboxen
|
||||
- Select All/Deselect All Funktionalität
|
||||
- Visuelles Feedback für ausgewählte Zeilen
|
||||
✅ Bulk-Actions für mehrere Kontakte
|
||||
- Mehrere Kontakte gleichzeitig löschen
|
||||
- Anzahl ausgewählter Kontakte wird angezeigt
|
||||
✅ Export-Funktionalität (CSV)
|
||||
- Alle oder gefilterte Kontakte exportieren
|
||||
- UTF-8 mit BOM für korrekte Umlaute
|
||||
✅ Pagination bei mehr als 25 Kontakten
|
||||
- Navigation zwischen Seiten
|
||||
- Seitenzahlen-Anzeige
|
||||
✅ Verbesserte Statistik
|
||||
- Kontakt-Anzahl im Header
|
||||
- Echtzeit-Updates bei Änderungen
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- contacts.css: Komplett neue Styles für Tabelle
|
||||
- index.html: HTML-Struktur für Tabellenlayout
|
||||
- contacts.js: Logik für Tabellen-Rendering und neue Features
|
||||
- Service Worker Cache-Version: 281
|
||||
- Responsive Design für mobile Geräte
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - WISSENSMANAGEMENT: ATTACHMENT ANZEIGE BEHOBEN (0KB & KEIN DATEINAME)
|
||||
================================================================================
|
||||
|
||||
## PROBLEMBEHEBUNG
|
||||
✅ Knowledge-Attachments zeigten 0KB und keinen Dateinamen
|
||||
- Backend sendet camelCase: originalName, sizeBytes
|
||||
- Frontend erwartete snake_case: original_name, size_bytes
|
||||
- Mismatch verursachte leere Anzeige in der UI
|
||||
|
||||
## TECHNISCHE LÖSUNG
|
||||
- knowledge.js renderAttachments(): Fallback für beide Schreibweisen
|
||||
- att.originalName || att.original_name || ''
|
||||
- att.sizeBytes || att.size_bytes || 0
|
||||
- Service Worker Cache-Version: 280
|
||||
- Frontend-Dateien in Docker Container kopiert
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - WISSENSMANAGEMENT: 500 ERROR BEI FILE UPLOAD BEHOBEN
|
||||
================================================================================
|
||||
|
||||
## PROBLEMBEHEBUNG
|
||||
✅ 500 Error bei Knowledge-Attachment Upload behoben
|
||||
- Frontend sendet Datei als 'files' (plural)
|
||||
- Knowledge-Backend erwartete 'file' (singular)
|
||||
- Task-Backend verwendet bereits 'files' (plural)
|
||||
- Mismatch verursachte "Unexpected field" Error
|
||||
|
||||
## TECHNISCHE LÖSUNG
|
||||
- knowledge.js: upload.single('files') statt 'file'
|
||||
- Konsistenz mit task file upload hergestellt
|
||||
- Frontend bleibt unverändert bei 'files'
|
||||
- Service Worker Cache-Version: 279
|
||||
- Docker Container neu gestartet
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - WISSENSMANAGEMENT: FILE UPLOAD FUNKTIONALITÄT REPARIERT
|
||||
================================================================================
|
||||
|
||||
## PROBLEMBEHEBUNG
|
||||
✅ File Upload funktioniert wieder im Knowledge-Modul
|
||||
- Click-Event für Upload-Bereich hinzugefügt
|
||||
- Klick auf gesamten Upload-Bereich öffnet Datei-Dialog
|
||||
✅ Verbesserte Handhabung bei neuen Einträgen
|
||||
- Klarere Meldung: "Speichern Sie zuerst den Eintrag"
|
||||
- Dateien werden temporär gespeichert
|
||||
- Nach Speichern automatisch hochgeladen
|
||||
✅ Upload-Bereich immer sichtbar
|
||||
- Auch bei neuen Einträgen angezeigt
|
||||
- Hilfstext erklärt Workflow
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- pendingFiles Property für temporäre Dateispeicherung
|
||||
- Auto-Upload nach createKnowledgeEntry
|
||||
- Service Worker Cache-Version: 277
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - KALENDER: INTELLIGENTE POPUP-POSITIONIERUNG
|
||||
================================================================================
|
||||
|
||||
## FUNKTIONALE VERBESSERUNGEN
|
||||
✅ Kalender-Popup bleibt immer im sichtbaren Bereich
|
||||
- Erkennt wenn Popup unten aus dem Viewport ragt
|
||||
- Positioniert sich automatisch oberhalb des Elements
|
||||
- Falls oben kein Platz: Am unteren Bildschirmrand fixiert
|
||||
✅ Horizontale Positionierung optimiert
|
||||
- Verhindert Überlauf rechts und links
|
||||
- Automatische Anpassung mit 8px Padding
|
||||
✅ Maximale Höhe auf 80% des Viewports begrenzt
|
||||
- Scrollbar bei vielen Aufgaben
|
||||
- Header bleibt immer sichtbar
|
||||
- Buttons bleiben am Ende fixiert
|
||||
✅ Konsistente Positionierung für beide Popup-Typen
|
||||
- Aufgaben-Popup
|
||||
- Erinnerungs-Popup
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- Position: fixed statt absolute für Viewport-Bezug
|
||||
- Viewport-Grenzen-Prüfung vor Positionierung
|
||||
- Flexbox-Layout für bessere Inhaltsverteilung
|
||||
- Service Worker Cache-Version: 271
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - ERINNERUNGEN: NEUE ZEITAUSWAHL MIT EINHEIT UND ZAHL
|
||||
================================================================================
|
||||
|
||||
## FUNKTIONALE ÄNDERUNGEN
|
||||
✅ Neue Zeitauswahl für Erinnerungen implementiert
|
||||
- Zahlenfeld (1-9) für die Anzahl
|
||||
- Dropdown für Zeiteinheit (Tag, Woche, Monat)
|
||||
- Ersetzt die vorherigen Checkboxen (1-3 Tage)
|
||||
✅ Flexiblere Erinnerungszeiten möglich:
|
||||
- 1-9 Tage vorher
|
||||
- 1-9 Wochen vorher (= 7-63 Tage)
|
||||
- 1-9 Monate vorher (= 30-270 Tage)
|
||||
✅ Automatische Umrechnung beim Laden bestehender Erinnerungen
|
||||
✅ Kompakteres und intuitiveres Design
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- HTML: Neues Input-Layout mit number input und select
|
||||
- JS: Umrechnung zwischen Tagen/Wochen/Monaten
|
||||
- CSS: Styling für horizontales Layout
|
||||
- Service Worker Cache-Version: 269
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - KALENDER: KORREKTUR DER ERINNERUNGS-BUTTON FARBEN
|
||||
================================================================================
|
||||
|
||||
## KORREKTUREN
|
||||
✅ Erinnerungs-Buttons haben jetzt korrekten orangenen Hintergrund
|
||||
- Hintergrund: #f97316 (Orange)
|
||||
- Schriftfarbe: Weiß
|
||||
- Hover: Dunkleres Orange (#ea580c)
|
||||
✅ Alle Erinnerungs-Buttons einheitlich gestaltet
|
||||
- "Erinnerung hinzufügen" in Kalendertagen
|
||||
- "Weitere Erinnerung" in Popups
|
||||
- Hauptbutton oberhalb Filter
|
||||
✅ Service Worker Cache-Version: 268
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - KALENDER: OPTIMIERUNG DER ERINNERUNGS-BUTTONS UND HOVER-EFFEKTE
|
||||
================================================================================
|
||||
|
||||
## UI-ANPASSUNGEN
|
||||
✅ Hover-Effekt für orangefarbene Erinnerungs-Buttons verbessert
|
||||
- Vorher: Wurde zu hell/weiß beim Hover
|
||||
- Jetzt: Dunkleres Orange mit sanftem Schatten-Effekt
|
||||
✅ "Erinnerung hinzufügen" Buttons in Kalendertagen jetzt orange
|
||||
- Konsistente Farbgebung für alle Erinnerungs-Funktionen
|
||||
- Gestrichelte orange Umrandung wie bei Aufgaben-Button
|
||||
✅ "+" Symbol bei "Aufgabe hinzufügen" entfernt
|
||||
- Klarerer Text ohne redundante Symbole
|
||||
✅ Neue CSS-Klasse "btn-reminder-secondary" für orange Buttons
|
||||
✅ Wochenansicht: Neuer Button "Erinnerung hinzufügen" hinzugefügt
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- Hover-Farbe: Von #ea580c (zu hell) zu #dc2626 (dunkler)
|
||||
- Transform-Effekt: translateY(-1px) für leichtes Anheben
|
||||
- Box-Shadow beim Hover für Tiefeneffekt
|
||||
- Service Worker Cache-Version: 267
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - ERINNERUNGS-BUTTON: SVG-ICON SICHTBARKEIT BEHOBEN
|
||||
================================================================================
|
||||
|
||||
## PROBLEMBEHEBUNG
|
||||
✅ SVG Glocken-Icon im Erinnerungs-Button wird jetzt korrekt angezeigt
|
||||
- Problem: createElement() unterstützt kein SVG-Namespace
|
||||
- Lösung: innerHTML für SVG-Erzeugung verwendet
|
||||
- Icon ist jetzt weiß und sichtbar auf orangem Hintergrund
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- calendar.js: SVG mit innerHTML statt createElement erzeugt
|
||||
- Konsistente Icon-Darstellung in allen Buttons
|
||||
- Service Worker Cache-Version: 275
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - KONTAKTE-MODUL: UI-REDESIGN FÜR KONSISTENZ
|
||||
================================================================================
|
||||
|
||||
## UI-ANPASSUNGEN
|
||||
✅ View-Container mit konsistentem Padding (spacing-6) wie andere Module
|
||||
✅ View-Wrapper für scrollbaren Inhalt hinzugefügt
|
||||
✅ Header-Styling vereinheitlicht (text-2xl, font-semibold)
|
||||
✅ Filter-Controls mit modernem Card-Design (bg-secondary, border-radius-xl)
|
||||
✅ Select-Dropdowns mit Custom-Styling und Hover-Effekten
|
||||
✅ Kontakt-Karten mit verbessertem Spacing und border-radius-xl
|
||||
✅ SVG-Icons statt Font Awesome für bessere Performance
|
||||
✅ Empty-State Design konsistent mit anderen Modulen
|
||||
✅ Responsive Breakpoints optimiert (968px, 768px)
|
||||
✅ CSS-Variablen durchgängig verwendet (spacing-*, text-*, radius-*)
|
||||
✅ Hover-Effekte verbessert (scale, shadow-lg)
|
||||
✅ Tag-Styling mit border-radius-full für moderne Optik
|
||||
|
||||
## TECHNISCHE DETAILS
|
||||
- Alle Abstände nutzen nun --spacing-* Variablen
|
||||
- Border-Radius konsistent mit --radius-xl
|
||||
- Filter-Select mit 40px Höhe wie andere Buttons
|
||||
- Max-Width 1400px für Content-Container
|
||||
- Service Worker Cache-Version: 266
|
||||
|
||||
================================================================================
|
||||
07.01.2025 - KONTAKTE-MODUL: VOLLSTÄNDIGE IMPLEMENTATION & FEHLERBEHEBUNG
|
||||
================================================================================
|
||||
|
||||
81
CLAUDE.md
81
CLAUDE.md
@ -442,6 +442,53 @@ app.use(express.static(path.join(__dirname, 'public'), {
|
||||
</div>
|
||||
```
|
||||
|
||||
### ⚠️ 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 = `<svg viewBox="0 0 24 24">...</svg>`;
|
||||
```
|
||||
- **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**
|
||||
@ -496,6 +543,34 @@ app.use(express.static(path.join(__dirname, 'public'), {
|
||||
- **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:**
|
||||
@ -530,6 +605,12 @@ app.use(express.static(path.join(__dirname, 'public'), {
|
||||
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
|
||||
|
||||
@ -596,6 +596,23 @@ function createTables() {
|
||||
logger.info('Migration: claude_instructions Spalte zu coding_directories hinzugefuegt');
|
||||
}
|
||||
|
||||
// Coding Verbrauchsdaten
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS coding_usage (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
directory_id INTEGER NOT NULL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
cpu_percent REAL,
|
||||
memory_mb REAL,
|
||||
disk_read_mb REAL,
|
||||
disk_write_mb REAL,
|
||||
network_recv_mb REAL,
|
||||
network_sent_mb REAL,
|
||||
process_count INTEGER,
|
||||
FOREIGN KEY (directory_id) REFERENCES coding_directories(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Kontakte
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
@ -641,6 +658,8 @@ function createTables() {
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_entries_category ON knowledge_entries(category_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_attachments_entry ON knowledge_attachments(entry_id);
|
||||
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);
|
||||
`);
|
||||
|
||||
@ -640,4 +640,75 @@ router.get('/directories/:id/commits', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/coding/directories/:id/usage
|
||||
* Aktuelle Verbrauchsdaten abrufen (simuliert)
|
||||
*/
|
||||
router.get('/directories/:id/usage', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const db = getDb();
|
||||
|
||||
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
|
||||
if (!directory) {
|
||||
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
|
||||
}
|
||||
|
||||
// Simulierte Verbrauchsdaten generieren
|
||||
const usage = {
|
||||
cpu_percent: Math.random() * 100,
|
||||
memory_mb: Math.floor(Math.random() * 4096),
|
||||
disk_read_mb: Math.random() * 100,
|
||||
disk_write_mb: Math.random() * 50,
|
||||
network_recv_mb: Math.random() * 10,
|
||||
network_sent_mb: Math.random() * 10,
|
||||
process_count: Math.floor(Math.random() * 20) + 1,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// Speichere in Datenbank für Historie
|
||||
db.prepare(`
|
||||
INSERT INTO coding_usage (directory_id, cpu_percent, memory_mb, disk_read_mb,
|
||||
disk_write_mb, network_recv_mb, network_sent_mb, process_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, usage.cpu_percent, usage.memory_mb, usage.disk_read_mb,
|
||||
usage.disk_write_mb, usage.network_recv_mb, usage.network_sent_mb, usage.process_count);
|
||||
|
||||
res.json({ usage });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Verbrauchsdaten:', error);
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/coding/directories/:id/usage/history
|
||||
* Historische Verbrauchsdaten abrufen
|
||||
*/
|
||||
router.get('/directories/:id/usage/history', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const hours = parseInt(req.query.hours) || 24;
|
||||
const db = getDb();
|
||||
|
||||
const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id);
|
||||
if (!directory) {
|
||||
return res.status(404).json({ error: 'Anwendung nicht gefunden' });
|
||||
}
|
||||
|
||||
const history = db.prepare(`
|
||||
SELECT * FROM coding_usage
|
||||
WHERE directory_id = ?
|
||||
AND timestamp > datetime('now', '-${hours} hours')
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100
|
||||
`).all(id);
|
||||
|
||||
res.json({ history });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Verbrauchshistorie:', error);
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@ -753,7 +753,7 @@ router.get('/attachments/:entryId', (req, res) => {
|
||||
* POST /api/knowledge/attachments/:entryId
|
||||
* Anhang hochladen
|
||||
*/
|
||||
router.post('/attachments/:entryId', upload.single('file'), (req, res) => {
|
||||
router.post('/attachments/:entryId', upload.single('files'), (req, res) => {
|
||||
try {
|
||||
const entryId = req.params.entryId;
|
||||
const db = getDb();
|
||||
|
||||
BIN
data/taskmate.db
BIN
data/taskmate.db
Binäre Datei nicht angezeigt.
@ -156,36 +156,99 @@
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* View Tabs */
|
||||
/* View Tabs - Modern Design */
|
||||
.view-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
background: var(--bg-tertiary);
|
||||
gap: var(--spacing-1);
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: var(--radius-lg);
|
||||
position: relative;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.view-tab {
|
||||
padding: 6px 12px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: 12px 20px;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-tertiary);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
transition: all var(--transition-default);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.view-tab:hover {
|
||||
color: var(--text-secondary);
|
||||
/* Tab Icon */
|
||||
.view-tab svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
transition: all var(--transition-default);
|
||||
}
|
||||
|
||||
.view-tab.active {
|
||||
/* Hover State */
|
||||
.view-tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-card);
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.view-tab:hover svg {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Active State with Underline */
|
||||
.view-tab.active {
|
||||
color: var(--primary);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.view-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--primary);
|
||||
border-radius: 3px 3px 0 0;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Animation for active indicator */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Subtle border between tabs */
|
||||
.view-tab:not(:last-child)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 25%;
|
||||
bottom: 25%;
|
||||
width: 1px;
|
||||
background: var(--border-light);
|
||||
opacity: 0.5;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.view-tab:hover::before,
|
||||
.view-tab:hover + .view-tab::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
|
||||
@ -691,12 +691,16 @@
|
||||
position: fixed;
|
||||
min-width: 280px;
|
||||
max-width: 350px;
|
||||
max-height: 80vh;
|
||||
padding: var(--spacing-4);
|
||||
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);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-day-detail-header {
|
||||
@ -717,8 +721,9 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
max-height: 300px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.calendar-detail-task {
|
||||
|
||||
@ -1,9 +1,29 @@
|
||||
/**
|
||||
* TASKMATE - Contacts Styles
|
||||
* ==========================
|
||||
* Kartenansicht für Kontakte
|
||||
* Tabellenansicht für Kontakte mit erweiterten Funktionen
|
||||
*/
|
||||
|
||||
/* =============================================================================
|
||||
VIEW CONTAINER
|
||||
============================================================================= */
|
||||
|
||||
.view-contacts {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
HEADER & CONTROLS
|
||||
============================================================================= */
|
||||
@ -12,165 +32,299 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-md);
|
||||
margin-bottom: var(--spacing-6);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.contacts-controls {
|
||||
.contacts-header h2 {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
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
|
||||
============================================================================= */
|
||||
|
||||
.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-controls-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bulk-actions.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bulk-actions-info {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.contacts-filters {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
gap: var(--spacing-4);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
GRID LAYOUT
|
||||
============================================================================= */
|
||||
|
||||
.contacts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: var(--space-md);
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
CONTACT CARD
|
||||
============================================================================= */
|
||||
|
||||
.contact-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-md);
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
.filter-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contact-card:hover {
|
||||
.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);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
box-shadow: var(--shadow-focus);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.contact-card-header {
|
||||
/* =============================================================================
|
||||
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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-sm);
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
.contact-avatar-small {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
font-weight: var(--font-semibold);
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.contact-card:hover .contact-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.contact-actions .btn-icon {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.contact-actions .btn-icon:hover {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
CONTACT INFO
|
||||
============================================================================= */
|
||||
|
||||
.contact-card-body {
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.contact-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 var(--space-xs);
|
||||
.contact-name-link {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-medium);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact-company {
|
||||
font-size: 14px;
|
||||
.contact-name-link:hover {
|
||||
color: var(--primary);
|
||||
margin-bottom: 4px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.contact-position {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.contact-email,
|
||||
.contact-phone,
|
||||
.contact-mobile {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-email i,
|
||||
.contact-phone i,
|
||||
.contact-mobile i {
|
||||
width: 14px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.contact-email:hover,
|
||||
.contact-phone:hover,
|
||||
.contact-mobile:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
TAGS
|
||||
============================================================================= */
|
||||
|
||||
.contact-tags {
|
||||
/* Tags cell */
|
||||
.tags-cell {
|
||||
display: flex;
|
||||
gap: var(--spacing-1);
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: var(--space-sm);
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.contact-tag {
|
||||
@ -178,8 +332,60 @@
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
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;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
@ -188,27 +394,35 @@
|
||||
|
||||
.contacts-empty {
|
||||
text-align: center;
|
||||
padding: var(--space-xl) var(--space-md);
|
||||
padding: var(--spacing-8) var(--spacing-4);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--border-default);
|
||||
max-width: 500px;
|
||||
margin: var(--spacing-8) auto;
|
||||
}
|
||||
|
||||
.contacts-empty i {
|
||||
font-size: 48px;
|
||||
.contacts-empty .empty-icon {
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: var(--space-md);
|
||||
margin-bottom: var(--spacing-4);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.contacts-empty .empty-icon svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.contacts-empty h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: var(--space-xs);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
margin-bottom: var(--spacing-2);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.contacts-empty p {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-md);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
@ -244,29 +458,105 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
PAGINATION
|
||||
============================================================================= */
|
||||
|
||||
.pagination {
|
||||
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);
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-default);
|
||||
color: var(--text-secondary);
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
RESPONSIVE
|
||||
============================================================================= */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@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);
|
||||
}
|
||||
|
||||
.contacts-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.contacts-controls {
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.contacts-search {
|
||||
max-width: none;
|
||||
.contacts-controls {
|
||||
padding: var(--spacing-3);
|
||||
}
|
||||
|
||||
.contacts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
.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);
|
||||
}
|
||||
|
||||
.contacts-table th,
|
||||
.contacts-table td {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@ -280,4 +570,9 @@
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Mobile: Hide less important columns */
|
||||
.hide-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -112,11 +112,102 @@
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Advance Options */
|
||||
/* Time Input Styling - Match Date Input */
|
||||
#reminder-modal input[type="time"] {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
font-family: var(--font-primary);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all var(--transition-fast);
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
#reminder-modal input[type="time"]:hover {
|
||||
border-color: var(--border-dark);
|
||||
}
|
||||
|
||||
#reminder-modal input[type="time"]:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--shadow-focus);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Advance Options - Redesigned */
|
||||
.advance-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: var(--spacing-3);
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--spacing-3);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
/* Reminder Advance Control */
|
||||
.reminder-advance-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.reminder-advance-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reminder-number-input {
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
padding: 10px 8px;
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-medium);
|
||||
background: white;
|
||||
border: 2px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.reminder-number-input:focus {
|
||||
border-color: var(--primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.reminder-unit-select {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
max-width: 180px;
|
||||
padding: 10px 16px;
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-medium);
|
||||
background: white;
|
||||
border: 2px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3e%3cpath d='M6 9l6 6 6-6'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.reminder-unit-select:focus {
|
||||
border-color: var(--primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.reminder-advance-suffix {
|
||||
font-size: var(--text-base);
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-semibold);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
@ -175,7 +266,7 @@
|
||||
}
|
||||
|
||||
/* =====================
|
||||
CUSTOM SELECT (USER DROPDOWN)
|
||||
CUSTOM SELECT (USER DROPDOWN) - Aligned with Multi-Select Design
|
||||
===================== */
|
||||
|
||||
.custom-select {
|
||||
@ -187,35 +278,38 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-primary);
|
||||
padding: 8px 14px;
|
||||
background: white;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all var(--transition-fast);
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.custom-select-trigger:hover {
|
||||
border-color: var(--border-hover);
|
||||
border-color: var(--border-dark);
|
||||
}
|
||||
|
||||
.custom-select-trigger.active {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.custom-select-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--spacing-2);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.custom-select-arrow {
|
||||
color: var(--text-secondary);
|
||||
transition: transform 0.2s ease;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--text-tertiary);
|
||||
transition: transform var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.custom-select.open .custom-select-arrow {
|
||||
@ -227,52 +321,48 @@
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-card);
|
||||
margin-top: 4px;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 1000;
|
||||
max-height: 200px;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
z-index: 1001;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
min-width: 200px;
|
||||
display: none;
|
||||
/* Ensure opaque background */
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.custom-select.open .custom-select-options {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.custom-select-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
gap: var(--spacing-3);
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.custom-select-option:last-child {
|
||||
border-bottom: none;
|
||||
transition: background-color var(--transition-fast);
|
||||
color: var(--text-primary);
|
||||
background-color: #ffffff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.custom-select-option:hover {
|
||||
background: var(--bg-hover);
|
||||
background-color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
.custom-select-option.selected {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
background-color: #e0e7ff !important;
|
||||
}
|
||||
|
||||
.option-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -284,7 +374,8 @@
|
||||
|
||||
.option-text {
|
||||
font-size: 14px;
|
||||
color: #000000 !important;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.selected-user-avatar {
|
||||
@ -325,11 +416,22 @@
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
#reminder-modal *:not(.btn):not(.custom-select-option):hover {
|
||||
#reminder-modal *:not(.btn):not(.custom-select-option):not(.option-avatar):not(.option-text):hover {
|
||||
background: transparent !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Ensure dropdown options have solid background */
|
||||
#reminder-modal .custom-select-options {
|
||||
background-color: #ffffff !important;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* Keep text elements transparent but allow avatars to have background */
|
||||
#reminder-modal .custom-select-option .option-text {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Stelle sicher, dass Text im Modal immer lesbar bleibt */
|
||||
#reminder-modal .modal-content {
|
||||
background: var(--bg-card) !important;
|
||||
@ -395,10 +497,11 @@
|
||||
===================== */
|
||||
|
||||
/* Reminder Button in Calendar Toolbar */
|
||||
.btn-reminder {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
.btn-reminder,
|
||||
.btn-secondary.btn-reminder {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important;
|
||||
color: white !important;
|
||||
border: 2px solid #ea580c;
|
||||
border: 2px solid #ea580c !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@ -413,10 +516,12 @@
|
||||
}
|
||||
|
||||
.btn-reminder:hover,
|
||||
.btn-reminder:focus {
|
||||
background: linear-gradient(135deg, #ea580c 0%, #c2410c 100%);
|
||||
.btn-reminder:focus,
|
||||
.btn-secondary.btn-reminder:hover,
|
||||
.btn-secondary.btn-reminder:focus {
|
||||
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%) !important;
|
||||
color: white !important;
|
||||
border-color: #c2410c;
|
||||
border-color: #dc2626 !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(234, 88, 12, 0.4);
|
||||
text-decoration: none;
|
||||
@ -475,6 +580,66 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Calendar Reminder Add Button - Orange styling */
|
||||
.calendar-week-add-reminder {
|
||||
margin: var(--spacing-2) var(--spacing-3) var(--spacing-3);
|
||||
padding: var(--spacing-2);
|
||||
background: #f97316;
|
||||
border: 1px solid #f97316;
|
||||
border-radius: var(--radius-lg);
|
||||
color: white !important;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.calendar-week-add-reminder:hover {
|
||||
background: #ea580c;
|
||||
border-color: #dc2626;
|
||||
color: white !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(249, 115, 22, 0.4);
|
||||
}
|
||||
|
||||
/* Secondary reminder button - orange variant */
|
||||
.btn-reminder-secondary,
|
||||
.btn.btn-reminder-secondary {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important;
|
||||
color: white !important;
|
||||
border: 2px solid #ea580c !important;
|
||||
font-weight: var(--font-medium);
|
||||
transition: all var(--transition-fast);
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
||||
.btn-reminder-secondary .icon,
|
||||
.btn.btn-reminder-secondary .icon {
|
||||
color: white !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
|
||||
.btn-reminder-secondary:hover,
|
||||
.btn-reminder-secondary:focus,
|
||||
.btn.btn-reminder-secondary:hover,
|
||||
.btn.btn-reminder-secondary:focus {
|
||||
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%) !important;
|
||||
color: white !important;
|
||||
border-color: #dc2626 !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 8px rgba(249, 115, 22, 0.4);
|
||||
}
|
||||
|
||||
.btn-reminder-secondary:active,
|
||||
.btn.btn-reminder-secondary:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 4px rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
|
||||
/* Reminder Detail Popup */
|
||||
.calendar-reminder-detail {
|
||||
min-width: 300px;
|
||||
|
||||
@ -173,13 +173,67 @@
|
||||
<div class="header-center">
|
||||
<!-- View Tabs -->
|
||||
<nav class="view-tabs">
|
||||
<button class="view-tab active" data-view="board">Board</button>
|
||||
<button class="view-tab" data-view="list">Liste</button>
|
||||
<button class="view-tab" data-view="calendar">Kalender</button>
|
||||
<button class="view-tab" data-view="proposals">Genehmigung</button>
|
||||
<button class="view-tab" data-view="coding">Coding</button>
|
||||
<button class="view-tab" data-view="knowledge">Wissen</button>
|
||||
<button class="view-tab" data-view="contacts">Kontakte</button>
|
||||
<button class="view-tab active" data-view="board">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
||||
</svg>
|
||||
Board
|
||||
</button>
|
||||
<button class="view-tab" data-view="list">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="8" y1="6" x2="21" y2="6"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18"/>
|
||||
<circle cx="3.5" cy="6" r="1" fill="currentColor"/>
|
||||
<circle cx="3.5" cy="12" r="1" fill="currentColor"/>
|
||||
<circle cx="3.5" cy="18" r="1" fill="currentColor"/>
|
||||
</svg>
|
||||
Liste
|
||||
</button>
|
||||
<button class="view-tab" data-view="calendar">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||
<circle cx="12" cy="16" r="1" fill="currentColor"/>
|
||||
</svg>
|
||||
Kalender
|
||||
</button>
|
||||
<button class="view-tab" data-view="proposals">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 11l3 3L22 4"/>
|
||||
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
|
||||
</svg>
|
||||
Genehmigung
|
||||
</button>
|
||||
<button class="view-tab" data-view="coding">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="16 18 22 12 16 6"/>
|
||||
<polyline points="8 6 2 12 8 18"/>
|
||||
<line x1="12" y1="20" x2="12" y2="4" transform="rotate(-15 12 12)"/>
|
||||
</svg>
|
||||
Coding
|
||||
</button>
|
||||
<button class="view-tab" data-view="knowledge">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
||||
</svg>
|
||||
Wissen
|
||||
</button>
|
||||
<button class="view-tab" data-view="contacts">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
Kontakte
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@ -579,42 +633,103 @@
|
||||
|
||||
<!-- Contacts View -->
|
||||
<div id="view-contacts" class="view view-contacts hidden">
|
||||
<div class="contacts-header">
|
||||
<h2>Kontakte</h2>
|
||||
<button id="btn-new-contact" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i>
|
||||
Neuer Kontakt
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="contacts-controls">
|
||||
<div class="contacts-filters">
|
||||
<select id="contacts-tag-filter">
|
||||
<option value="">Alle Tags</option>
|
||||
</select>
|
||||
<select id="contacts-sort">
|
||||
<option value="created_at-desc">Neueste zuerst</option>
|
||||
<option value="created_at-asc">Älteste zuerst</option>
|
||||
<option value="name-asc">Name (A-Z)</option>
|
||||
<option value="name-desc">Name (Z-A)</option>
|
||||
<option value="company-asc">Firma (A-Z)</option>
|
||||
<option value="company-desc">Firma (Z-A)</option>
|
||||
</select>
|
||||
<div class="view-wrapper">
|
||||
<div class="contacts-header">
|
||||
<h2>Kontakte</h2>
|
||||
<div class="header-actions">
|
||||
<div class="contacts-stats">
|
||||
<span id="contacts-count">0 Kontakte</span>
|
||||
</div>
|
||||
<button id="btn-export-contacts" class="btn btn-secondary btn-export">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Exportieren
|
||||
</button>
|
||||
<button id="btn-new-contact" class="btn btn-primary">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
Neuer Kontakt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="contacts-grid" class="contacts-grid">
|
||||
<!-- Contact cards will be rendered here -->
|
||||
</div>
|
||||
<div class="contacts-controls">
|
||||
<div class="contacts-controls-top">
|
||||
<div class="bulk-actions hidden" id="bulk-actions">
|
||||
<span class="bulk-actions-info"><span id="selected-count">0</span> ausgewählt</span>
|
||||
<button id="btn-bulk-delete" class="btn btn-danger btn-sm">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14M10 11v6M14 11v6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Löschen
|
||||
</button>
|
||||
<button id="btn-deselect-all" class="btn btn-secondary btn-sm">Abwählen</button>
|
||||
</div>
|
||||
<div class="contacts-filters">
|
||||
<div class="filter-group">
|
||||
<label for="contacts-tag-filter" class="sr-only">Tags filtern</label>
|
||||
<select id="contacts-tag-filter" class="filter-select">
|
||||
<option value="">Alle Tags</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="contacts-empty" class="contacts-empty hidden">
|
||||
<i class="fas fa-address-book"></i>
|
||||
<h3>Keine Kontakte vorhanden</h3>
|
||||
<p>Erstellen Sie Ihren ersten Kontakt.</p>
|
||||
<button class="btn btn-primary" onclick="document.getElementById('btn-new-contact').click()">
|
||||
<i class="fas fa-plus"></i>
|
||||
Ersten Kontakt erstellen
|
||||
</button>
|
||||
<div class="contacts-table-container">
|
||||
<table class="contacts-table" id="contacts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="checkbox-cell">
|
||||
<input type="checkbox" class="table-checkbox" id="select-all-contacts">
|
||||
</th>
|
||||
<th class="sortable" data-sort="name">Name</th>
|
||||
<th class="sortable" data-sort="company">Firma</th>
|
||||
<th class="sortable hide-mobile" data-sort="position">Position</th>
|
||||
<th class="sortable" data-sort="email">E-Mail</th>
|
||||
<th class="hide-mobile">Telefon</th>
|
||||
<th>Tags</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="contacts-tbody">
|
||||
<!-- Table rows will be rendered here -->
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination hidden" id="contacts-pagination">
|
||||
<button class="pagination-btn" id="btn-prev-page" disabled>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="pagination-info">
|
||||
Seite <span id="current-page">1</span> von <span id="total-pages">1</span>
|
||||
</span>
|
||||
<button class="pagination-btn" id="btn-next-page" disabled>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="contacts-empty" class="contacts-empty hidden">
|
||||
<div class="empty-icon">
|
||||
<svg viewBox="0 0 24 24" width="64" height="64" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Keine Kontakte vorhanden</h3>
|
||||
<p>Erstellen Sie Ihren ersten Kontakt.</p>
|
||||
<button class="btn btn-primary" onclick="document.getElementById('btn-new-contact').click()">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
Ersten Kontakt erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1915,23 +2030,19 @@
|
||||
<input type="hidden" id="reminder-color" value="#F59E0B">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reminder-advance">Erinnerung</label>
|
||||
<label for="reminder-advance">Vorab-Erinnerung</label>
|
||||
<div class="advance-options">
|
||||
<label class="checklist-item">
|
||||
<input type="checkbox" name="advance-days" value="1" checked>
|
||||
<span class="checklist-checkbox"></span>
|
||||
<span class="checklist-text">1 Tag vorher</span>
|
||||
</label>
|
||||
<label class="checklist-item">
|
||||
<input type="checkbox" name="advance-days" value="2">
|
||||
<span class="checklist-checkbox"></span>
|
||||
<span class="checklist-text">2 Tage vorher</span>
|
||||
</label>
|
||||
<label class="checklist-item">
|
||||
<input type="checkbox" name="advance-days" value="3">
|
||||
<span class="checklist-checkbox"></span>
|
||||
<span class="checklist-text">3 Tage vorher</span>
|
||||
</label>
|
||||
<div class="reminder-advance-control">
|
||||
<div class="reminder-advance-inputs">
|
||||
<input type="number" id="reminder-advance-number" min="1" max="9" value="1" class="form-control reminder-number-input">
|
||||
<select id="reminder-advance-unit" class="form-control reminder-unit-select">
|
||||
<option value="day">Tag(e)</option>
|
||||
<option value="week">Woche(n)</option>
|
||||
<option value="month">Monat(e)</option>
|
||||
</select>
|
||||
<span class="reminder-advance-suffix">vorher</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1284,6 +1284,14 @@ class ApiClient {
|
||||
return this.post('/coding/validate-path', { path });
|
||||
}
|
||||
|
||||
async getCodingDirectoryUsage(id) {
|
||||
return this.get(`/coding/directories/${id}/usage`);
|
||||
}
|
||||
|
||||
async getCodingDirectoryUsageHistory(id, hours = 24) {
|
||||
return this.get(`/coding/directories/${id}/usage/history?hours=${hours}`);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTACTS
|
||||
// =============================================================================
|
||||
|
||||
@ -733,7 +733,7 @@ class App {
|
||||
users.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.id;
|
||||
option.textContent = user.username;
|
||||
option.textContent = user.displayName || user.email || 'Unbekannt';
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
@ -524,9 +524,19 @@ class CalendarViewManager {
|
||||
e.stopPropagation();
|
||||
this.createTaskForDate(dateString);
|
||||
}
|
||||
}, ['+ Aufgabe']);
|
||||
}, ['Aufgabe hinzufügen']);
|
||||
dayEl.appendChild(addBtn);
|
||||
|
||||
// Add reminder button
|
||||
const addReminderBtn = createElement('button', {
|
||||
className: 'calendar-week-add-reminder',
|
||||
onclick: (e) => {
|
||||
e.stopPropagation();
|
||||
this.createReminderForDate(dateString);
|
||||
}
|
||||
}, ['Erinnerung hinzufügen']);
|
||||
dayEl.appendChild(addReminderBtn);
|
||||
|
||||
return dayEl;
|
||||
}
|
||||
|
||||
@ -920,28 +930,57 @@ class CalendarViewManager {
|
||||
className: 'btn btn-primary btn-block',
|
||||
style: { marginTop: 'var(--spacing-md)' },
|
||||
onclick: () => this.createTaskForDate(dateString)
|
||||
}, ['+ Aufgabe hinzufügen']));
|
||||
}, ['Aufgabe hinzufügen']));
|
||||
|
||||
// Add reminder button
|
||||
popup.appendChild(createElement('button', {
|
||||
className: 'btn btn-secondary btn-block',
|
||||
className: 'btn btn-reminder-secondary btn-block',
|
||||
style: { marginTop: 'var(--spacing-sm)' },
|
||||
onclick: () => this.createReminderForDate(dateString)
|
||||
}, ['🔔 Erinnerung hinzufügen']));
|
||||
}));
|
||||
|
||||
// Add HTML content with SVG icon
|
||||
const button = popup.lastElementChild;
|
||||
button.innerHTML = '<svg class="icon" viewBox="0 0 24 24" width="18" height="18" style="margin-right: 6px; flex-shrink: 0;"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 0 1-3.46 0" stroke="white" stroke-width="2" fill="none"></path></svg> Erinnerung hinzufügen';
|
||||
|
||||
// Position popup - different logic for week vs month view
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
const popupHeight = 400; // Estimated max height
|
||||
const popupWidth = 350;
|
||||
const padding = 8;
|
||||
|
||||
let popupTop, popupLeft;
|
||||
|
||||
if (this.viewMode === 'week') {
|
||||
// For week view, position at the top of the day element
|
||||
popupTop = Math.max(150, rect.top + 50); // Ensure it's visible, minimum 150px from top
|
||||
popupLeft = Math.min(rect.left, window.innerWidth - 350);
|
||||
} else {
|
||||
// For month view, position below the day element
|
||||
popupTop = rect.bottom + 8;
|
||||
popupLeft = Math.min(rect.left, window.innerWidth - 350);
|
||||
popupTop = rect.bottom + padding;
|
||||
|
||||
// Check if popup would go below viewport
|
||||
if (popupTop + popupHeight > window.innerHeight - padding) {
|
||||
// Position above the element instead
|
||||
popupTop = rect.top - popupHeight - padding;
|
||||
|
||||
// If still not enough space above, position at bottom of viewport
|
||||
if (popupTop < padding) {
|
||||
popupTop = window.innerHeight - popupHeight - padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal positioning
|
||||
popupLeft = rect.left;
|
||||
|
||||
// Check if popup would go beyond right edge
|
||||
if (popupLeft + popupWidth > window.innerWidth - padding) {
|
||||
popupLeft = window.innerWidth - popupWidth - padding;
|
||||
}
|
||||
|
||||
// Check if popup would go beyond left edge
|
||||
if (popupLeft < padding) {
|
||||
popupLeft = padding;
|
||||
}
|
||||
|
||||
popup.style.top = `${popupTop}px`;
|
||||
@ -1087,15 +1126,39 @@ class CalendarViewManager {
|
||||
|
||||
// Add reminder button
|
||||
popup.appendChild(createElement('button', {
|
||||
className: 'btn btn-secondary btn-block',
|
||||
className: 'btn btn-reminder-secondary btn-block',
|
||||
style: { marginTop: 'var(--spacing-md)' },
|
||||
onclick: () => this.createReminderForDate(dateString)
|
||||
}, ['+ Weitere Erinnerung']));
|
||||
}, ['Weitere Erinnerung']));
|
||||
|
||||
// Position popup
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
let popupTop = rect.bottom + 8;
|
||||
let popupLeft = Math.min(rect.left, window.innerWidth - 350);
|
||||
const popupHeight = 400; // Estimated max height
|
||||
const popupWidth = 350;
|
||||
const padding = 8;
|
||||
|
||||
let popupTop = rect.bottom + padding;
|
||||
let popupLeft = rect.left;
|
||||
|
||||
// Check if popup would go below viewport
|
||||
if (popupTop + popupHeight > window.innerHeight - padding) {
|
||||
// Position above the element instead
|
||||
popupTop = rect.top - popupHeight - padding;
|
||||
|
||||
// If still not enough space above, position at bottom of viewport
|
||||
if (popupTop < padding) {
|
||||
popupTop = window.innerHeight - popupHeight - padding;
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal positioning
|
||||
if (popupLeft + popupWidth > window.innerWidth - padding) {
|
||||
popupLeft = window.innerWidth - popupWidth - padding;
|
||||
}
|
||||
|
||||
if (popupLeft < padding) {
|
||||
popupLeft = padding;
|
||||
}
|
||||
|
||||
popup.style.top = `${popupTop}px`;
|
||||
popup.style.left = `${popupLeft}px`;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* TASKMATE - Contacts Manager
|
||||
* ===========================
|
||||
* Kontaktverwaltung mit Kartenansicht
|
||||
* Kontaktverwaltung mit Tabellenansicht
|
||||
*/
|
||||
|
||||
import api from './api.js';
|
||||
@ -12,11 +12,14 @@ class ContactsManager {
|
||||
constructor() {
|
||||
this.contacts = [];
|
||||
this.filteredContacts = [];
|
||||
this.selectedContacts = new Set();
|
||||
this.allTags = new Set();
|
||||
this.searchQuery = '';
|
||||
this.filterTag = '';
|
||||
this.sortBy = 'created_at';
|
||||
this.sortOrder = 'desc';
|
||||
this.currentPage = 1;
|
||||
this.itemsPerPage = 25;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
@ -30,16 +33,30 @@ class ContactsManager {
|
||||
|
||||
// DOM Elements
|
||||
this.contactsView = $('#view-contacts');
|
||||
this.contactsGrid = $('#contacts-grid');
|
||||
this.contactsTable = $('#contacts-table');
|
||||
this.contactsTbody = $('#contacts-tbody');
|
||||
this.contactsEmpty = $('#contacts-empty');
|
||||
this.tagFilter = $('#contacts-tag-filter');
|
||||
this.sortSelect = $('#contacts-sort');
|
||||
this.selectAllCheckbox = $('#select-all-contacts');
|
||||
this.bulkActions = $('#bulk-actions');
|
||||
this.selectedCountSpan = $('#selected-count');
|
||||
this.contactsCountSpan = $('#contacts-count');
|
||||
this.newContactBtn = $('#btn-new-contact');
|
||||
this.exportBtn = $('#btn-export-contacts');
|
||||
this.bulkDeleteBtn = $('#btn-bulk-delete');
|
||||
this.deselectAllBtn = $('#btn-deselect-all');
|
||||
|
||||
// Pagination
|
||||
this.pagination = $('#contacts-pagination');
|
||||
this.currentPageSpan = $('#current-page');
|
||||
this.totalPagesSpan = $('#total-pages');
|
||||
this.prevPageBtn = $('#btn-prev-page');
|
||||
this.nextPageBtn = $('#btn-next-page');
|
||||
|
||||
console.log('[Contacts] DOM Elements check:');
|
||||
console.log(' contactsView:', this.contactsView);
|
||||
console.log(' newContactBtn:', this.newContactBtn);
|
||||
console.log(' contactsGrid:', this.contactsGrid);
|
||||
console.log(' contactsTable:', this.contactsTable);
|
||||
|
||||
// Modal Elements
|
||||
this.contactModal = $('#contact-modal');
|
||||
@ -87,19 +104,46 @@ class ContactsManager {
|
||||
if (this.tagFilter) {
|
||||
this.tagFilter.addEventListener('change', (e) => {
|
||||
this.filterTag = e.target.value;
|
||||
this.currentPage = 1;
|
||||
this.filterContacts();
|
||||
});
|
||||
}
|
||||
|
||||
// Sort
|
||||
if (this.sortSelect) {
|
||||
this.sortSelect.addEventListener('change', (e) => {
|
||||
const [sortBy, sortOrder] = e.target.value.split('-');
|
||||
this.sortBy = sortBy;
|
||||
this.sortOrder = sortOrder;
|
||||
// Table sorting
|
||||
$$('.sortable').forEach(th => {
|
||||
th.addEventListener('click', (e) => {
|
||||
const sortField = th.dataset.sort;
|
||||
if (this.sortBy === sortField) {
|
||||
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortBy = sortField;
|
||||
this.sortOrder = 'asc';
|
||||
}
|
||||
|
||||
// Update UI
|
||||
$$('.sortable').forEach(el => el.classList.remove('sort-asc', 'sort-desc'));
|
||||
th.classList.add(this.sortOrder === 'asc' ? 'sort-asc' : 'sort-desc');
|
||||
|
||||
this.sortContacts();
|
||||
this.renderContacts();
|
||||
});
|
||||
});
|
||||
|
||||
// Select all checkbox
|
||||
if (this.selectAllCheckbox) {
|
||||
this.selectAllCheckbox.addEventListener('change', (e) => {
|
||||
const checked = e.target.checked;
|
||||
const visibleContacts = this.getPaginatedContacts();
|
||||
|
||||
if (checked) {
|
||||
visibleContacts.forEach(contact => this.selectedContacts.add(contact.id));
|
||||
} else {
|
||||
visibleContacts.forEach(contact => this.selectedContacts.delete(contact.id));
|
||||
}
|
||||
|
||||
this.updateBulkActions();
|
||||
this.renderContacts();
|
||||
});
|
||||
}
|
||||
|
||||
// New Contact Button
|
||||
@ -114,6 +158,46 @@ class ContactsManager {
|
||||
console.warn('[Contacts] newContactBtn not found!');
|
||||
}
|
||||
|
||||
// Export Button
|
||||
if (this.exportBtn) {
|
||||
this.exportBtn.addEventListener('click', () => this.exportContacts());
|
||||
}
|
||||
|
||||
// Bulk Delete Button
|
||||
if (this.bulkDeleteBtn) {
|
||||
this.bulkDeleteBtn.addEventListener('click', () => this.bulkDelete());
|
||||
}
|
||||
|
||||
// Deselect All Button
|
||||
if (this.deselectAllBtn) {
|
||||
this.deselectAllBtn.addEventListener('click', () => {
|
||||
this.selectedContacts.clear();
|
||||
this.selectAllCheckbox.checked = false;
|
||||
this.updateBulkActions();
|
||||
this.renderContacts();
|
||||
});
|
||||
}
|
||||
|
||||
// Pagination buttons
|
||||
if (this.prevPageBtn) {
|
||||
this.prevPageBtn.addEventListener('click', () => {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.renderContacts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.nextPageBtn) {
|
||||
this.nextPageBtn.addEventListener('click', () => {
|
||||
const totalPages = Math.ceil(this.filteredContacts.length / this.itemsPerPage);
|
||||
if (this.currentPage < totalPages) {
|
||||
this.currentPage++;
|
||||
this.renderContacts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Modal Form
|
||||
if (this.contactForm) {
|
||||
this.contactForm.addEventListener('submit', (e) => {
|
||||
@ -161,6 +245,7 @@ class ContactsManager {
|
||||
socket.on('contact:deleted', (data) => {
|
||||
console.log('[Contacts] Socket: contact deleted', data);
|
||||
this.contacts = this.contacts.filter(c => c.id !== data.contactId);
|
||||
this.selectedContacts.delete(data.contactId);
|
||||
this.updateTagsList();
|
||||
this.filterContacts();
|
||||
});
|
||||
@ -216,6 +301,7 @@ class ContactsManager {
|
||||
});
|
||||
|
||||
this.sortContacts();
|
||||
this.updateContactsCount();
|
||||
this.renderContacts();
|
||||
}
|
||||
|
||||
@ -243,56 +329,77 @@ class ContactsManager {
|
||||
});
|
||||
}
|
||||
|
||||
getPaginatedContacts() {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const end = start + this.itemsPerPage;
|
||||
return this.filteredContacts.slice(start, end);
|
||||
}
|
||||
|
||||
renderContacts() {
|
||||
if (!this.contactsGrid) return;
|
||||
if (!this.contactsTbody) return;
|
||||
|
||||
if (this.filteredContacts.length === 0) {
|
||||
this.contactsGrid.classList.add('hidden');
|
||||
this.contactsTable.parentElement.classList.add('hidden');
|
||||
this.contactsEmpty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.contactsGrid.classList.remove('hidden');
|
||||
this.contactsTable.parentElement.classList.remove('hidden');
|
||||
this.contactsEmpty.classList.add('hidden');
|
||||
|
||||
const html = this.filteredContacts.map(contact => this.createContactCard(contact)).join('');
|
||||
this.contactsGrid.innerHTML = html;
|
||||
const paginatedContacts = this.getPaginatedContacts();
|
||||
const html = paginatedContacts.map(contact => this.createContactRow(contact)).join('');
|
||||
this.contactsTbody.innerHTML = html;
|
||||
|
||||
// Bind card events
|
||||
this.bindCardEvents();
|
||||
// Update pagination
|
||||
this.updatePagination();
|
||||
|
||||
// Bind row events
|
||||
this.bindRowEvents();
|
||||
}
|
||||
|
||||
createContactCard(contact) {
|
||||
createContactRow(contact) {
|
||||
const displayName = this.getContactDisplayName(contact);
|
||||
const initials = this.getContactInitials(contact);
|
||||
const tags = contact.tags || [];
|
||||
const isSelected = this.selectedContacts.has(contact.id);
|
||||
|
||||
return `
|
||||
<div class="contact-card" data-contact-id="${contact.id}">
|
||||
<div class="contact-card-header">
|
||||
<div class="contact-avatar">
|
||||
${initials}
|
||||
<tr data-contact-id="${contact.id}" ${isSelected ? 'class="selected"' : ''}>
|
||||
<td class="checkbox-cell">
|
||||
<input type="checkbox" class="table-checkbox contact-checkbox" data-contact-id="${contact.id}" ${isSelected ? 'checked' : ''}>
|
||||
</td>
|
||||
<td>
|
||||
<div class="name-cell">
|
||||
<div class="contact-avatar-small">${initials}</div>
|
||||
<a href="#" class="contact-name-link" data-contact-id="${contact.id}">${displayName}</a>
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
<button class="btn-icon btn-edit-contact" title="Bearbeiten">
|
||||
<i class="fas fa-edit"></i>
|
||||
</td>
|
||||
<td>${contact.company || '-'}</td>
|
||||
<td class="hide-mobile">${contact.position || '-'}</td>
|
||||
<td>${contact.email ? `<a href="mailto:${contact.email}">${contact.email}</a>` : '-'}</td>
|
||||
<td class="hide-mobile">${contact.phone || '-'}</td>
|
||||
<td>
|
||||
<div class="tags-cell">
|
||||
${tags.length > 0 ? tags.map(tag => `<span class="contact-tag">${tag}</span>`).join('') : '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<div class="table-actions">
|
||||
<button class="btn-table-action btn-edit-contact" data-contact-id="${contact.id}" title="Bearbeiten">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-table-action btn-delete-contact-inline" data-contact-id="${contact.id}" title="Löschen">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14M10 11v6M14 11v6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-card-body">
|
||||
<h3 class="contact-name">${displayName}</h3>
|
||||
${contact.company ? `<div class="contact-company">${contact.company}</div>` : ''}
|
||||
${contact.position ? `<div class="contact-position">${contact.position}</div>` : ''}
|
||||
${contact.email ? `<div class="contact-email"><i class="fas fa-envelope"></i> ${contact.email}</div>` : ''}
|
||||
${contact.phone ? `<div class="contact-phone"><i class="fas fa-phone"></i> ${contact.phone}</div>` : ''}
|
||||
${contact.mobile ? `<div class="contact-mobile"><i class="fas fa-mobile"></i> ${contact.mobile}</div>` : ''}
|
||||
</div>
|
||||
${tags.length > 0 ? `
|
||||
<div class="contact-tags">
|
||||
${tags.map(tag => `<span class="contact-tag">${tag}</span>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -325,26 +432,97 @@ class ContactsManager {
|
||||
return initials || '?';
|
||||
}
|
||||
|
||||
bindCardEvents() {
|
||||
bindRowEvents() {
|
||||
// Contact checkboxes
|
||||
$$('.contact-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const contactId = parseInt(e.target.dataset.contactId);
|
||||
if (e.target.checked) {
|
||||
this.selectedContacts.add(contactId);
|
||||
} else {
|
||||
this.selectedContacts.delete(contactId);
|
||||
}
|
||||
this.updateBulkActions();
|
||||
this.updateRowSelection(contactId, e.target.checked);
|
||||
});
|
||||
});
|
||||
|
||||
// Edit buttons
|
||||
$$('.btn-edit-contact').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const card = btn.closest('.contact-card');
|
||||
const contactId = parseInt(card.dataset.contactId);
|
||||
const contactId = parseInt(btn.dataset.contactId);
|
||||
this.editContact(contactId);
|
||||
});
|
||||
});
|
||||
|
||||
// Card click
|
||||
$$('.contact-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const contactId = parseInt(card.dataset.contactId);
|
||||
// Delete inline buttons
|
||||
$$('.btn-delete-contact-inline').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const contactId = parseInt(btn.dataset.contactId);
|
||||
this.deleteContactInline(contactId);
|
||||
});
|
||||
});
|
||||
|
||||
// Name links
|
||||
$$('.contact-name-link').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const contactId = parseInt(link.dataset.contactId);
|
||||
this.editContact(contactId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateRowSelection(contactId, selected) {
|
||||
const row = $(`tr[data-contact-id="${contactId}"]`);
|
||||
if (row) {
|
||||
if (selected) {
|
||||
row.classList.add('selected');
|
||||
} else {
|
||||
row.classList.remove('selected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateBulkActions() {
|
||||
const count = this.selectedContacts.size;
|
||||
if (count > 0) {
|
||||
this.bulkActions.classList.remove('hidden');
|
||||
this.selectedCountSpan.textContent = count;
|
||||
} else {
|
||||
this.bulkActions.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Update select all checkbox state
|
||||
const visibleContacts = this.getPaginatedContacts();
|
||||
const allSelected = visibleContacts.length > 0 &&
|
||||
visibleContacts.every(contact => this.selectedContacts.has(contact.id));
|
||||
this.selectAllCheckbox.checked = allSelected;
|
||||
}
|
||||
|
||||
updateContactsCount() {
|
||||
const count = this.filteredContacts.length;
|
||||
this.contactsCountSpan.textContent = `${count} ${count === 1 ? 'Kontakt' : 'Kontakte'}`;
|
||||
}
|
||||
|
||||
updatePagination() {
|
||||
const totalPages = Math.ceil(this.filteredContacts.length / this.itemsPerPage);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
this.pagination.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.pagination.classList.remove('hidden');
|
||||
this.currentPageSpan.textContent = this.currentPage;
|
||||
this.totalPagesSpan.textContent = totalPages;
|
||||
|
||||
this.prevPageBtn.disabled = this.currentPage === 1;
|
||||
this.nextPageBtn.disabled = this.currentPage === totalPages;
|
||||
}
|
||||
|
||||
updateTagsList() {
|
||||
// Collect all unique tags
|
||||
this.allTags.clear();
|
||||
@ -370,6 +548,120 @@ class ContactsManager {
|
||||
}
|
||||
}
|
||||
|
||||
async bulkDelete() {
|
||||
const count = this.selectedContacts.size;
|
||||
if (count === 0) return;
|
||||
|
||||
const contactNames = Array.from(this.selectedContacts).map(id => {
|
||||
const contact = this.contacts.find(c => c.id === id);
|
||||
return contact ? this.getContactDisplayName(contact) : '';
|
||||
}).filter(Boolean);
|
||||
|
||||
const message = count === 1
|
||||
? `Möchten Sie den Kontakt "${contactNames[0]}" wirklich löschen?`
|
||||
: `Möchten Sie ${count} Kontakte wirklich löschen?`;
|
||||
|
||||
if (!confirm(message)) return;
|
||||
|
||||
try {
|
||||
// Delete contacts one by one
|
||||
for (const contactId of this.selectedContacts) {
|
||||
await api.deleteContact(contactId);
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: `${count} ${count === 1 ? 'Kontakt' : 'Kontakte'} gelöscht`, type: 'success' }
|
||||
}));
|
||||
|
||||
this.selectedContacts.clear();
|
||||
await this.loadContacts();
|
||||
} catch (error) {
|
||||
console.error('[Contacts] Error during bulk delete:', error);
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: 'Fehler beim Löschen der Kontakte', type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async deleteContactInline(contactId) {
|
||||
const contact = this.contacts.find(c => c.id === contactId);
|
||||
if (!contact) return;
|
||||
|
||||
const displayName = this.getContactDisplayName(contact);
|
||||
|
||||
if (!confirm(`Möchten Sie den Kontakt "${displayName}" wirklich löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.deleteContact(contactId);
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: 'Kontakt gelöscht', type: 'success' }
|
||||
}));
|
||||
await this.loadContacts();
|
||||
} catch (error) {
|
||||
console.error('[Contacts] Error deleting contact:', error);
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: 'Fehler beim Löschen', type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async exportContacts() {
|
||||
try {
|
||||
let contactsToExport = this.filteredContacts;
|
||||
|
||||
// Create CSV content
|
||||
const headers = ['Vorname', 'Nachname', 'Firma', 'Position', 'E-Mail', 'Telefon', 'Mobil', 'Adresse', 'PLZ', 'Stadt', 'Land', 'Website', 'Tags', 'Notizen'];
|
||||
const rows = [headers];
|
||||
|
||||
contactsToExport.forEach(contact => {
|
||||
const row = [
|
||||
contact.firstName || '',
|
||||
contact.lastName || '',
|
||||
contact.company || '',
|
||||
contact.position || '',
|
||||
contact.email || '',
|
||||
contact.phone || '',
|
||||
contact.mobile || '',
|
||||
contact.address || '',
|
||||
contact.postalCode || '',
|
||||
contact.city || '',
|
||||
contact.country || '',
|
||||
contact.website || '',
|
||||
(contact.tags || []).join(', '),
|
||||
contact.notes || ''
|
||||
];
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
// Convert to CSV
|
||||
const csvContent = rows.map(row =>
|
||||
row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',')
|
||||
).join('\n');
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `kontakte_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: `${contactsToExport.length} Kontakte exportiert`, type: 'success' }
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[Contacts] Error exporting contacts:', error);
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message: 'Fehler beim Exportieren', type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
showContactModal(contact = null) {
|
||||
console.log('[Contacts] showContactModal called with:', contact);
|
||||
console.log('[Contacts] contactModal element:', this.contactModal);
|
||||
|
||||
@ -18,6 +18,7 @@ class KnowledgeManager {
|
||||
this.expandedEntries = new Set();
|
||||
this.initialized = false;
|
||||
this.searchDebounceTimer = null;
|
||||
this.pendingFiles = null;
|
||||
|
||||
// Drag & Drop State
|
||||
this.draggedCategoryId = null;
|
||||
@ -140,6 +141,14 @@ class KnowledgeManager {
|
||||
|
||||
// File Upload
|
||||
this.fileInput?.addEventListener('change', (e) => this.handleFileSelect(e));
|
||||
|
||||
// Click on file upload area to trigger file input
|
||||
this.fileUploadArea?.addEventListener('click', (e) => {
|
||||
// Don't trigger if clicking on the label (it has its own handler)
|
||||
if (!e.target.closest('.file-input-label')) {
|
||||
this.fileInput?.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Drag & Drop for file upload
|
||||
if (this.fileUploadArea) {
|
||||
@ -382,8 +391,8 @@ class KnowledgeManager {
|
||||
<div class="knowledge-attachment-item" data-attachment-id="${att.id}">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="2" fill="none"/><path d="M14 2v6h6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
<div class="knowledge-attachment-info">
|
||||
<span class="knowledge-attachment-name">${this.escapeHtml(att.original_name)}</span>
|
||||
<span class="knowledge-attachment-size">${this.formatFileSize(att.size_bytes)}</span>
|
||||
<span class="knowledge-attachment-name">${this.escapeHtml(att.originalName || att.original_name || '')}</span>
|
||||
<span class="knowledge-attachment-size">${this.formatFileSize(att.sizeBytes || att.size_bytes || 0)}</span>
|
||||
</div>
|
||||
<div class="knowledge-attachment-actions">
|
||||
<a href="${api.getKnowledgeAttachmentDownloadUrl(att.id)}" class="btn-icon" title="Herunterladen" download>
|
||||
@ -957,7 +966,8 @@ class KnowledgeManager {
|
||||
this.entryModalTitle.textContent = isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag';
|
||||
this.entryForm?.reset();
|
||||
this.deleteEntryBtn?.classList.toggle('hidden', !isEdit);
|
||||
this.attachmentsSection.style.display = isEdit ? 'block' : 'none';
|
||||
// Show attachments section always, but with a note for new entries
|
||||
this.attachmentsSection.style.display = 'block';
|
||||
|
||||
if (isEdit) {
|
||||
const entry = await this.loadEntryWithAttachments(entryId);
|
||||
@ -972,7 +982,7 @@ class KnowledgeManager {
|
||||
} else {
|
||||
this.entryIdInput.value = '';
|
||||
this.entryCategoryIdInput.value = this.selectedCategory?.id || '';
|
||||
this.attachmentsContainer.innerHTML = '';
|
||||
this.attachmentsContainer.innerHTML = '<p class="text-muted" style="text-align: center; padding: 20px; color: var(--text-secondary); font-size: var(--text-sm);">Speichern Sie zuerst den Eintrag, um Dateien hochzuladen.</p>';
|
||||
}
|
||||
|
||||
this.openModal(this.entryModal, 'knowledge-entry-modal');
|
||||
@ -1010,8 +1020,15 @@ class KnowledgeManager {
|
||||
await api.updateKnowledgeEntry(entryId, data);
|
||||
this.showToast('Eintrag aktualisiert', 'success');
|
||||
} else {
|
||||
await api.createKnowledgeEntry(data);
|
||||
const newEntry = await api.createKnowledgeEntry(data);
|
||||
this.showToast('Eintrag erstellt', 'success');
|
||||
|
||||
// If there are pending files, upload them now
|
||||
if (this.pendingFiles && this.pendingFiles.length > 0) {
|
||||
this.showToast('Lade Dateien hoch...', 'info');
|
||||
await this.uploadFiles(this.pendingFiles);
|
||||
this.pendingFiles = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.closeEntryModal();
|
||||
@ -1071,7 +1088,10 @@ class KnowledgeManager {
|
||||
async uploadFiles(files) {
|
||||
const entryId = parseInt(this.entryIdInput?.value);
|
||||
if (!entryId) {
|
||||
this.showToast('Bitte zuerst den Eintrag speichern', 'error');
|
||||
// For new entries, show a more helpful message
|
||||
this.showToast('Speichern Sie zuerst den Eintrag, dann können Sie Dateien hochladen', 'info');
|
||||
// Store files temporarily to upload after save
|
||||
this.pendingFiles = files;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -237,9 +237,10 @@ class ReminderManager {
|
||||
$('#reminder-color').value = '#F59E0B';
|
||||
|
||||
// Reset advance days
|
||||
$$('input[name="advance-days"]').forEach(cb => {
|
||||
cb.checked = cb.value === '1';
|
||||
});
|
||||
const advanceNumberEl = $('#reminder-advance-number');
|
||||
const advanceUnitEl = $('#reminder-advance-unit');
|
||||
if (advanceNumberEl) advanceNumberEl.value = '1';
|
||||
if (advanceUnitEl) advanceUnitEl.value = 'day';
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,11 +343,20 @@ class ReminderManager {
|
||||
const freshOptions = $('#reminder-assignee-options');
|
||||
const freshValueDisplay = freshTrigger.querySelector('.custom-select-value');
|
||||
|
||||
// Clear any inline styles from dropdown
|
||||
freshOptions.style.width = '';
|
||||
freshOptions.style.left = '';
|
||||
freshOptions.style.top = '';
|
||||
freshOptions.style.bottom = '';
|
||||
|
||||
// Toggle dropdown
|
||||
freshTrigger.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
wrapper.classList.toggle('open');
|
||||
|
||||
// Simple toggle - CSS handles positioning
|
||||
|
||||
console.log('[Reminder] Dropdown toggled, open:', wrapper.classList.contains('open'));
|
||||
});
|
||||
|
||||
@ -410,9 +420,23 @@ class ReminderManager {
|
||||
|
||||
// Set advance days
|
||||
const advanceDays = reminder.advance_days || ['1'];
|
||||
$$('input[name="advance-days"]').forEach(cb => {
|
||||
cb.checked = advanceDays.includes(cb.value);
|
||||
});
|
||||
const advanceNumberEl = $('#reminder-advance-number');
|
||||
const advanceUnitEl = $('#reminder-advance-unit');
|
||||
|
||||
// Parse the first advance day value to set number and unit
|
||||
if (advanceDays.length > 0 && advanceNumberEl && advanceUnitEl) {
|
||||
const advanceValue = parseInt(advanceDays[0]);
|
||||
if (advanceValue <= 9) {
|
||||
advanceNumberEl.value = advanceValue;
|
||||
advanceUnitEl.value = 'day';
|
||||
} else if (advanceValue % 7 === 0 && advanceValue <= 63) {
|
||||
advanceNumberEl.value = advanceValue / 7;
|
||||
advanceUnitEl.value = 'week';
|
||||
} else if (advanceValue % 30 === 0 && advanceValue <= 270) {
|
||||
advanceNumberEl.value = advanceValue / 30;
|
||||
advanceUnitEl.value = 'month';
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading reminder:', error);
|
||||
@ -434,16 +458,19 @@ class ReminderManager {
|
||||
try {
|
||||
const formData = new FormData(this.form);
|
||||
|
||||
// Get advance days
|
||||
const advanceDays = [];
|
||||
$$('input[name="advance-days"]:checked').forEach(cb => {
|
||||
advanceDays.push(cb.value);
|
||||
});
|
||||
// Get advance days from new inputs
|
||||
const advanceNumber = parseInt($('#reminder-advance-number').value) || 1;
|
||||
const advanceUnit = $('#reminder-advance-unit').value || 'day';
|
||||
|
||||
if (advanceDays.length === 0) {
|
||||
throw new Error('Bitte wählen Sie mindestens eine Erinnerungszeit aus');
|
||||
let advanceDaysValue = advanceNumber;
|
||||
if (advanceUnit === 'week') {
|
||||
advanceDaysValue = advanceNumber * 7;
|
||||
} else if (advanceUnit === 'month') {
|
||||
advanceDaysValue = advanceNumber * 30;
|
||||
}
|
||||
|
||||
const advanceDays = [String(advanceDaysValue)];
|
||||
|
||||
const data = {
|
||||
project_id: store.get('currentProjectId'),
|
||||
title: formData.get('reminder-title') || $('#reminder-title').value,
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* Offline support and caching
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = '265';
|
||||
const CACHE_VERSION = '292';
|
||||
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
|
||||
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
|
||||
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
|
||||
|
||||
11730
logs/app.log
11730
logs/app.log
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
In neuem Issue referenzieren
Einen Benutzer sperren