Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-10 10:32:52 +00:00
committet von Server Deploy
Ursprung 7d67557be4
Commit ef153789cc
20 geänderte Dateien mit 13613 neuen und 333 gelöschten Zeilen

Datei anzeigen

@ -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 \"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 \"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 -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)"
] ]
} }
} }

Datei anzeigen

@ -1,6 +1,335 @@
TASKMATE - CHANGELOG 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 07.01.2025 - KONTAKTE-MODUL: VOLLSTÄNDIGE IMPLEMENTATION & FEHLERBEHEBUNG
================================================================================ ================================================================================

Datei anzeigen

@ -442,6 +442,53 @@ app.use(express.static(path.join(__dirname, 'public'), {
</div> </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) ### ⚠️ Erinnerung-Implementation Probleme (06.01.2026)
**FEHLER 1: Syntax-Fehler in JavaScript blockierte Login** **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 - **Lösung**: Aufgaben zuerst, dann Erinnerungen
- **Lesson**: UI-Reihenfolge muss Funktionalität folgen, nicht umgekehrt - **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 ### 🔧 TROUBLESHOOTING-WORKFLOW
**Bei JavaScript-Fehlern:** **Bei JavaScript-Fehlern:**
@ -530,6 +605,12 @@ app.use(express.static(path.join(__dirname, 'public'), {
3. clearSearch() Funktion ebenfalls erweitern 3. clearSearch() Funktion ebenfalls erweitern
4. Lokale Suchfelder entfernen - nur Header-Suche nutzen 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 ## 🐛 Troubleshooting
### Häufige Probleme ### Häufige Probleme

Datei anzeigen

@ -596,6 +596,23 @@ function createTables() {
logger.info('Migration: claude_instructions Spalte zu coding_directories hinzugefuegt'); 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 // Kontakte
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS contacts ( 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_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_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_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_company ON contacts(company);
CREATE INDEX IF NOT EXISTS idx_contacts_tags ON contacts(tags); CREATE INDEX IF NOT EXISTS idx_contacts_tags ON contacts(tags);
`); `);

Datei anzeigen

@ -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; module.exports = router;

Datei anzeigen

@ -753,7 +753,7 @@ router.get('/attachments/:entryId', (req, res) => {
* POST /api/knowledge/attachments/:entryId * POST /api/knowledge/attachments/:entryId
* Anhang hochladen * Anhang hochladen
*/ */
router.post('/attachments/:entryId', upload.single('file'), (req, res) => { router.post('/attachments/:entryId', upload.single('files'), (req, res) => {
try { try {
const entryId = req.params.entryId; const entryId = req.params.entryId;
const db = getDb(); const db = getDb();

Binäre Datei nicht angezeigt.

Datei anzeigen

@ -156,36 +156,99 @@
font-size: var(--text-sm); font-size: var(--text-sm);
} }
/* View Tabs */ /* View Tabs - Modern Design */
.view-tabs { .view-tabs {
display: flex; display: flex;
gap: 2px; gap: var(--spacing-1);
padding: 3px; padding: 8px 12px;
background: var(--bg-tertiary); background: rgba(0, 0, 0, 0.03);
border-radius: var(--radius-lg); 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 { .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-size: var(--text-sm);
font-weight: var(--font-medium); font-weight: var(--font-medium);
color: var(--text-tertiary); color: var(--text-tertiary);
background: none; background: none;
border: none; border: none;
border-radius: var(--radius-md); border-radius: 0;
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-default);
white-space: nowrap; white-space: nowrap;
} }
.view-tab:hover { /* Tab Icon */
color: var(--text-secondary); .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); color: var(--text-primary);
background: var(--bg-card); background: rgba(0, 0, 0, 0.05);
box-shadow: var(--shadow-sm); 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 */ /* Search */

Datei anzeigen

@ -691,12 +691,16 @@
position: fixed; position: fixed;
min-width: 280px; min-width: 280px;
max-width: 350px; max-width: 350px;
max-height: 80vh;
padding: var(--spacing-4); padding: var(--spacing-4);
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
z-index: var(--z-dropdown); z-index: var(--z-dropdown);
display: flex;
flex-direction: column;
overflow: hidden;
} }
.calendar-day-detail-header { .calendar-day-detail-header {
@ -717,8 +721,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-2); gap: var(--spacing-2);
max-height: 300px; flex: 1;
overflow-y: auto; overflow-y: auto;
margin-bottom: var(--spacing-3);
} }
.calendar-detail-task { .calendar-detail-task {

Datei anzeigen

@ -1,9 +1,29 @@
/** /**
* TASKMATE - Contacts Styles * 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 HEADER & CONTROLS
============================================================================= */ ============================================================================= */
@ -12,165 +32,299 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: var(--space-md); margin-bottom: var(--spacing-6);
flex-wrap: wrap; 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; display: flex;
gap: var(--space-sm); gap: var(--spacing-3);
align-items: center; 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; 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 { .contacts-filters {
display: flex; display: flex;
gap: var(--space-xs); gap: var(--spacing-4);
align-items: center;
flex-wrap: wrap;
} }
/* ============================================================================= .filter-group {
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;
position: relative; 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); border-color: var(--primary);
transform: translateY(-2px); box-shadow: var(--shadow-focus);
box-shadow: var(--shadow-md); 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; display: flex;
justify-content: space-between; align-items: center;
align-items: flex-start; gap: var(--spacing-3);
margin-bottom: var(--space-sm); min-width: 200px;
} }
.contact-avatar { .contact-avatar-small {
width: 48px; width: 32px;
height: 48px; height: 32px;
background: var(--primary); background: var(--primary);
color: white; color: white;
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-weight: bold; font-weight: var(--font-semibold);
font-size: 18px; font-size: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
.contact-actions { .contact-name-link {
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);
color: var(--text-primary); color: var(--text-primary);
font-weight: var(--font-medium);
text-decoration: none;
cursor: pointer;
} }
.contact-company { .contact-name-link:hover {
font-size: 14px;
color: var(--primary); color: var(--primary);
margin-bottom: 4px; text-decoration: underline;
} }
.contact-position { /* Tags cell */
font-size: 13px; .tags-cell {
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 {
display: flex; display: flex;
gap: var(--spacing-1);
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; max-width: 200px;
margin-top: var(--space-sm);
} }
.contact-tag { .contact-tag {
@ -178,8 +332,60 @@
color: var(--text-secondary); color: var(--text-secondary);
font-size: 11px; font-size: 11px;
padding: 2px 8px; padding: 2px 8px;
border-radius: var(--radius-sm); border-radius: var(--radius-full);
border: 1px solid var(--border-color); 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 { .contacts-empty {
text-align: center; text-align: center;
padding: var(--space-xl) var(--space-md); padding: var(--spacing-8) var(--spacing-4);
background: var(--bg-secondary); background: var(--bg-secondary);
border-radius: var(--radius); border-radius: var(--radius-xl);
border: 1px solid var(--border-color); border: 1px solid var(--border-default);
max-width: 500px;
margin: var(--spacing-8) auto;
} }
.contacts-empty i { .contacts-empty .empty-icon {
font-size: 48px;
color: var(--text-tertiary); 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 { .contacts-empty h3 {
font-size: 20px; font-size: var(--text-xl);
margin-bottom: var(--space-xs); font-weight: var(--font-semibold);
margin-bottom: var(--spacing-2);
color: var(--text-primary); color: var(--text-primary);
} }
.contacts-empty p { .contacts-empty p {
color: var(--text-secondary); color: var(--text-secondary);
margin-bottom: var(--space-md); margin-bottom: var(--spacing-4);
} }
/* ============================================================================= /* =============================================================================
@ -244,29 +458,105 @@
margin-top: 4px; 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 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 { .contacts-header {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.contacts-controls { .header-actions {
flex-direction: column; flex-direction: column;
width: 100%;
gap: var(--spacing-2);
} }
.contacts-search { .contacts-controls {
max-width: none; padding: var(--spacing-3);
} }
.contacts-grid { .contacts-controls-top {
grid-template-columns: 1fr; 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 { .form-row {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@ -280,4 +570,9 @@
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
} }
/* Mobile: Hide less important columns */
.hide-mobile {
display: none;
}
} }

Datei anzeigen

@ -112,11 +112,102 @@
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5); 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 { .advance-options {
display: flex; display: flex;
flex-direction: column; 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 { .checkbox-label {
@ -175,7 +266,7 @@
} }
/* ===================== /* =====================
CUSTOM SELECT (USER DROPDOWN) CUSTOM SELECT (USER DROPDOWN) - Aligned with Multi-Select Design
===================== */ ===================== */
.custom-select { .custom-select {
@ -187,35 +278,38 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px 12px; padding: 8px 14px;
background: var(--bg-primary); background: white;
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: 6px; border-radius: var(--radius-lg);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all var(--transition-fast);
min-height: 42px; min-height: 42px;
} }
.custom-select-trigger:hover { .custom-select-trigger:hover {
border-color: var(--border-hover); border-color: var(--border-dark);
} }
.custom-select-trigger.active { .custom-select-trigger.active {
border-color: var(--primary); 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 { .custom-select-value {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: var(--spacing-2);
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;
} }
.custom-select-arrow { .custom-select-arrow {
color: var(--text-secondary); width: 16px;
transition: transform 0.2s ease; height: 16px;
color: var(--text-tertiary);
transition: transform var(--transition-fast);
flex-shrink: 0;
} }
.custom-select.open .custom-select-arrow { .custom-select.open .custom-select-arrow {
@ -227,52 +321,48 @@
top: 100%; top: 100%;
left: 0; left: 0;
right: 0; right: 0;
background: var(--bg-card); margin-top: 4px;
background: #ffffff;
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: 6px; border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-xl);
z-index: 1000; z-index: 1001;
max-height: 200px; max-height: 240px;
overflow-y: auto; overflow-y: auto;
opacity: 0; min-width: 200px;
visibility: hidden; display: none;
transform: translateY(-10px); /* Ensure opaque background */
transition: all 0.2s ease; background-color: #ffffff !important;
} }
.custom-select.open .custom-select-options { .custom-select.open .custom-select-options {
opacity: 1; display: block;
visibility: visible;
transform: translateY(0);
} }
.custom-select-option { .custom-select-option {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: var(--spacing-3);
padding: 10px 12px; padding: 8px 12px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease; transition: background-color var(--transition-fast);
border-bottom: 1px solid var(--border-light); color: var(--text-primary);
} background-color: #ffffff;
position: relative;
.custom-select-option:last-child {
border-bottom: none;
} }
.custom-select-option:hover { .custom-select-option:hover {
background: var(--bg-hover); background-color: #f3f4f6 !important;
} }
.custom-select-option.selected { .custom-select-option.selected {
background: var(--primary-light); background-color: #e0e7ff !important;
color: var(--primary);
} }
.option-avatar { .option-avatar {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%; border-radius: var(--radius-full);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -284,7 +374,8 @@
.option-text { .option-text {
font-size: 14px; font-size: 14px;
color: #000000 !important; color: var(--text-primary);
flex: 1;
} }
.selected-user-avatar { .selected-user-avatar {
@ -325,11 +416,22 @@
transition: background-color 0.2s ease, color 0.2s ease; 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; background: transparent !important;
color: inherit !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 */ /* Stelle sicher, dass Text im Modal immer lesbar bleibt */
#reminder-modal .modal-content { #reminder-modal .modal-content {
background: var(--bg-card) !important; background: var(--bg-card) !important;
@ -395,10 +497,11 @@
===================== */ ===================== */
/* Reminder Button in Calendar Toolbar */ /* Reminder Button in Calendar Toolbar */
.btn-reminder { .btn-reminder,
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); .btn-secondary.btn-reminder {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important;
color: white !important; color: white !important;
border: 2px solid #ea580c; border: 2px solid #ea580c !important;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
@ -413,10 +516,12 @@
} }
.btn-reminder:hover, .btn-reminder:hover,
.btn-reminder:focus { .btn-reminder:focus,
background: linear-gradient(135deg, #ea580c 0%, #c2410c 100%); .btn-secondary.btn-reminder:hover,
.btn-secondary.btn-reminder:focus {
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%) !important;
color: white !important; color: white !important;
border-color: #c2410c; border-color: #dc2626 !important;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(234, 88, 12, 0.4); box-shadow: 0 4px 12px rgba(234, 88, 12, 0.4);
text-decoration: none; text-decoration: none;
@ -475,6 +580,66 @@
border: none; 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 */ /* Reminder Detail Popup */
.calendar-reminder-detail { .calendar-reminder-detail {
min-width: 300px; min-width: 300px;

Datei anzeigen

@ -173,13 +173,67 @@
<div class="header-center"> <div class="header-center">
<!-- View Tabs --> <!-- View Tabs -->
<nav class="view-tabs"> <nav class="view-tabs">
<button class="view-tab active" data-view="board">Board</button> <button class="view-tab active" data-view="board">
<button class="view-tab" data-view="list">Liste</button> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<button class="view-tab" data-view="calendar">Kalender</button> <rect x="3" y="3" width="7" height="7" rx="1"/>
<button class="view-tab" data-view="proposals">Genehmigung</button> <rect x="14" y="3" width="7" height="7" rx="1"/>
<button class="view-tab" data-view="coding">Coding</button> <rect x="3" y="14" width="7" height="7" rx="1"/>
<button class="view-tab" data-view="knowledge">Wissen</button> <rect x="14" y="14" width="7" height="7" rx="1"/>
<button class="view-tab" data-view="contacts">Kontakte</button> </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> </nav>
</div> </div>
@ -579,42 +633,103 @@
<!-- Contacts View --> <!-- Contacts View -->
<div id="view-contacts" class="view view-contacts hidden"> <div id="view-contacts" class="view view-contacts hidden">
<div class="contacts-header"> <div class="view-wrapper">
<h2>Kontakte</h2> <div class="contacts-header">
<button id="btn-new-contact" class="btn btn-primary"> <h2>Kontakte</h2>
<i class="fas fa-plus"></i> <div class="header-actions">
Neuer Kontakt <div class="contacts-stats">
</button> <span id="contacts-count">0 Kontakte</span>
</div> </div>
<button id="btn-export-contacts" class="btn btn-secondary btn-export">
<div class="contacts-controls"> <svg viewBox="0 0 24 24" width="16" height="16">
<div class="contacts-filters"> <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"/>
<select id="contacts-tag-filter"> </svg>
<option value="">Alle Tags</option> Exportieren
</select> </button>
<select id="contacts-sort"> <button id="btn-new-contact" class="btn btn-primary">
<option value="created_at-desc">Neueste zuerst</option> <svg viewBox="0 0 24 24" width="18" height="18">
<option value="created_at-asc">Älteste zuerst</option> <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
<option value="name-asc">Name (A-Z)</option> </svg>
<option value="name-desc">Name (Z-A)</option> Neuer Kontakt
<option value="company-asc">Firma (A-Z)</option> </button>
<option value="company-desc">Firma (Z-A)</option> </div>
</select>
</div> </div>
</div>
<div id="contacts-grid" class="contacts-grid"> <div class="contacts-controls">
<!-- Contact cards will be rendered here --> <div class="contacts-controls-top">
</div> <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"> <div class="contacts-table-container">
<i class="fas fa-address-book"></i> <table class="contacts-table" id="contacts-table">
<h3>Keine Kontakte vorhanden</h3> <thead>
<p>Erstellen Sie Ihren ersten Kontakt.</p> <tr>
<button class="btn btn-primary" onclick="document.getElementById('btn-new-contact').click()"> <th class="checkbox-cell">
<i class="fas fa-plus"></i> <input type="checkbox" class="table-checkbox" id="select-all-contacts">
Ersten Kontakt erstellen </th>
</button> <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>
</div> </div>
@ -1915,23 +2030,19 @@
<input type="hidden" id="reminder-color" value="#F59E0B"> <input type="hidden" id="reminder-color" value="#F59E0B">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="reminder-advance">Erinnerung</label> <label for="reminder-advance">Vorab-Erinnerung</label>
<div class="advance-options"> <div class="advance-options">
<label class="checklist-item"> <div class="reminder-advance-control">
<input type="checkbox" name="advance-days" value="1" checked> <div class="reminder-advance-inputs">
<span class="checklist-checkbox"></span> <input type="number" id="reminder-advance-number" min="1" max="9" value="1" class="form-control reminder-number-input">
<span class="checklist-text">1 Tag vorher</span> <select id="reminder-advance-unit" class="form-control reminder-unit-select">
</label> <option value="day">Tag(e)</option>
<label class="checklist-item"> <option value="week">Woche(n)</option>
<input type="checkbox" name="advance-days" value="2"> <option value="month">Monat(e)</option>
<span class="checklist-checkbox"></span> </select>
<span class="checklist-text">2 Tage vorher</span> <span class="reminder-advance-suffix">vorher</span>
</label> </div>
<label class="checklist-item"> </div>
<input type="checkbox" name="advance-days" value="3">
<span class="checklist-checkbox"></span>
<span class="checklist-text">3 Tage vorher</span>
</label>
</div> </div>
</div> </div>
</div> </div>

Datei anzeigen

@ -1284,6 +1284,14 @@ class ApiClient {
return this.post('/coding/validate-path', { path }); 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 // CONTACTS
// ============================================================================= // =============================================================================

Datei anzeigen

@ -733,7 +733,7 @@ class App {
users.forEach(user => { users.forEach(user => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = user.id; option.value = user.id;
option.textContent = user.username; option.textContent = user.displayName || user.email || 'Unbekannt';
select.appendChild(option); select.appendChild(option);
}); });
} }

Datei anzeigen

@ -524,9 +524,19 @@ class CalendarViewManager {
e.stopPropagation(); e.stopPropagation();
this.createTaskForDate(dateString); this.createTaskForDate(dateString);
} }
}, ['+ Aufgabe']); }, ['Aufgabe hinzufügen']);
dayEl.appendChild(addBtn); 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; return dayEl;
} }
@ -920,28 +930,57 @@ class CalendarViewManager {
className: 'btn btn-primary btn-block', className: 'btn btn-primary btn-block',
style: { marginTop: 'var(--spacing-md)' }, style: { marginTop: 'var(--spacing-md)' },
onclick: () => this.createTaskForDate(dateString) onclick: () => this.createTaskForDate(dateString)
}, ['+ Aufgabe hinzufügen'])); }, ['Aufgabe hinzufügen']));
// Add reminder button // Add reminder button
popup.appendChild(createElement('button', { popup.appendChild(createElement('button', {
className: 'btn btn-secondary btn-block', className: 'btn btn-reminder-secondary btn-block',
style: { marginTop: 'var(--spacing-sm)' }, style: { marginTop: 'var(--spacing-sm)' },
onclick: () => this.createReminderForDate(dateString) 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 // Position popup - different logic for week vs month view
const rect = anchorEl.getBoundingClientRect(); const rect = anchorEl.getBoundingClientRect();
const popupHeight = 400; // Estimated max height
const popupWidth = 350;
const padding = 8;
let popupTop, popupLeft; let popupTop, popupLeft;
if (this.viewMode === 'week') { if (this.viewMode === 'week') {
// For week view, position at the top of the day element // 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 popupTop = Math.max(150, rect.top + 50); // Ensure it's visible, minimum 150px from top
popupLeft = Math.min(rect.left, window.innerWidth - 350);
} else { } else {
// For month view, position below the day element // For month view, position below the day element
popupTop = rect.bottom + 8; popupTop = rect.bottom + padding;
popupLeft = Math.min(rect.left, window.innerWidth - 350);
// 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`; popup.style.top = `${popupTop}px`;
@ -1087,15 +1126,39 @@ class CalendarViewManager {
// Add reminder button // Add reminder button
popup.appendChild(createElement('button', { popup.appendChild(createElement('button', {
className: 'btn btn-secondary btn-block', className: 'btn btn-reminder-secondary btn-block',
style: { marginTop: 'var(--spacing-md)' }, style: { marginTop: 'var(--spacing-md)' },
onclick: () => this.createReminderForDate(dateString) onclick: () => this.createReminderForDate(dateString)
}, ['+ Weitere Erinnerung'])); }, ['Weitere Erinnerung']));
// Position popup // Position popup
const rect = anchorEl.getBoundingClientRect(); const rect = anchorEl.getBoundingClientRect();
let popupTop = rect.bottom + 8; const popupHeight = 400; // Estimated max height
let popupLeft = Math.min(rect.left, window.innerWidth - 350); 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.top = `${popupTop}px`;
popup.style.left = `${popupLeft}px`; popup.style.left = `${popupLeft}px`;

Datei anzeigen

@ -1,7 +1,7 @@
/** /**
* TASKMATE - Contacts Manager * TASKMATE - Contacts Manager
* =========================== * ===========================
* Kontaktverwaltung mit Kartenansicht * Kontaktverwaltung mit Tabellenansicht
*/ */
import api from './api.js'; import api from './api.js';
@ -12,11 +12,14 @@ class ContactsManager {
constructor() { constructor() {
this.contacts = []; this.contacts = [];
this.filteredContacts = []; this.filteredContacts = [];
this.selectedContacts = new Set();
this.allTags = new Set(); this.allTags = new Set();
this.searchQuery = ''; this.searchQuery = '';
this.filterTag = ''; this.filterTag = '';
this.sortBy = 'created_at'; this.sortBy = 'created_at';
this.sortOrder = 'desc'; this.sortOrder = 'desc';
this.currentPage = 1;
this.itemsPerPage = 25;
this.initialized = false; this.initialized = false;
} }
@ -30,16 +33,30 @@ class ContactsManager {
// DOM Elements // DOM Elements
this.contactsView = $('#view-contacts'); this.contactsView = $('#view-contacts');
this.contactsGrid = $('#contacts-grid'); this.contactsTable = $('#contacts-table');
this.contactsTbody = $('#contacts-tbody');
this.contactsEmpty = $('#contacts-empty'); this.contactsEmpty = $('#contacts-empty');
this.tagFilter = $('#contacts-tag-filter'); 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.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('[Contacts] DOM Elements check:');
console.log(' contactsView:', this.contactsView); console.log(' contactsView:', this.contactsView);
console.log(' newContactBtn:', this.newContactBtn); console.log(' newContactBtn:', this.newContactBtn);
console.log(' contactsGrid:', this.contactsGrid); console.log(' contactsTable:', this.contactsTable);
// Modal Elements // Modal Elements
this.contactModal = $('#contact-modal'); this.contactModal = $('#contact-modal');
@ -87,19 +104,46 @@ class ContactsManager {
if (this.tagFilter) { if (this.tagFilter) {
this.tagFilter.addEventListener('change', (e) => { this.tagFilter.addEventListener('change', (e) => {
this.filterTag = e.target.value; this.filterTag = e.target.value;
this.currentPage = 1;
this.filterContacts(); this.filterContacts();
}); });
} }
// Sort // Table sorting
if (this.sortSelect) { $$('.sortable').forEach(th => {
this.sortSelect.addEventListener('change', (e) => { th.addEventListener('click', (e) => {
const [sortBy, sortOrder] = e.target.value.split('-'); const sortField = th.dataset.sort;
this.sortBy = sortBy; if (this.sortBy === sortField) {
this.sortOrder = sortOrder; 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.sortContacts();
this.renderContacts(); 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 // New Contact Button
@ -114,6 +158,46 @@ class ContactsManager {
console.warn('[Contacts] newContactBtn not found!'); 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 // Modal Form
if (this.contactForm) { if (this.contactForm) {
this.contactForm.addEventListener('submit', (e) => { this.contactForm.addEventListener('submit', (e) => {
@ -161,6 +245,7 @@ class ContactsManager {
socket.on('contact:deleted', (data) => { socket.on('contact:deleted', (data) => {
console.log('[Contacts] Socket: contact deleted', data); console.log('[Contacts] Socket: contact deleted', data);
this.contacts = this.contacts.filter(c => c.id !== data.contactId); this.contacts = this.contacts.filter(c => c.id !== data.contactId);
this.selectedContacts.delete(data.contactId);
this.updateTagsList(); this.updateTagsList();
this.filterContacts(); this.filterContacts();
}); });
@ -216,6 +301,7 @@ class ContactsManager {
}); });
this.sortContacts(); this.sortContacts();
this.updateContactsCount();
this.renderContacts(); 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() { renderContacts() {
if (!this.contactsGrid) return; if (!this.contactsTbody) return;
if (this.filteredContacts.length === 0) { if (this.filteredContacts.length === 0) {
this.contactsGrid.classList.add('hidden'); this.contactsTable.parentElement.classList.add('hidden');
this.contactsEmpty.classList.remove('hidden'); this.contactsEmpty.classList.remove('hidden');
return; return;
} }
this.contactsGrid.classList.remove('hidden'); this.contactsTable.parentElement.classList.remove('hidden');
this.contactsEmpty.classList.add('hidden'); this.contactsEmpty.classList.add('hidden');
const html = this.filteredContacts.map(contact => this.createContactCard(contact)).join(''); const paginatedContacts = this.getPaginatedContacts();
this.contactsGrid.innerHTML = html; const html = paginatedContacts.map(contact => this.createContactRow(contact)).join('');
this.contactsTbody.innerHTML = html;
// Bind card events // Update pagination
this.bindCardEvents(); this.updatePagination();
// Bind row events
this.bindRowEvents();
} }
createContactCard(contact) { createContactRow(contact) {
const displayName = this.getContactDisplayName(contact); const displayName = this.getContactDisplayName(contact);
const initials = this.getContactInitials(contact); const initials = this.getContactInitials(contact);
const tags = contact.tags || []; const tags = contact.tags || [];
const isSelected = this.selectedContacts.has(contact.id);
return ` return `
<div class="contact-card" data-contact-id="${contact.id}"> <tr data-contact-id="${contact.id}" ${isSelected ? 'class="selected"' : ''}>
<div class="contact-card-header"> <td class="checkbox-cell">
<div class="contact-avatar"> <input type="checkbox" class="table-checkbox contact-checkbox" data-contact-id="${contact.id}" ${isSelected ? 'checked' : ''}>
${initials} </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>
<div class="contact-actions"> </td>
<button class="btn-icon btn-edit-contact" title="Bearbeiten"> <td>${contact.company || '-'}</td>
<i class="fas fa-edit"></i> <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> </button>
</div> </div>
</div> </td>
<div class="contact-card-body"> </tr>
<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>
`; `;
} }
@ -325,26 +432,97 @@ class ContactsManager {
return initials || '?'; 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 // Edit buttons
$$('.btn-edit-contact').forEach(btn => { $$('.btn-edit-contact').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
const card = btn.closest('.contact-card'); const contactId = parseInt(btn.dataset.contactId);
const contactId = parseInt(card.dataset.contactId);
this.editContact(contactId); this.editContact(contactId);
}); });
}); });
// Card click // Delete inline buttons
$$('.contact-card').forEach(card => { $$('.btn-delete-contact-inline').forEach(btn => {
card.addEventListener('click', () => { btn.addEventListener('click', (e) => {
const contactId = parseInt(card.dataset.contactId); 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); 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() { updateTagsList() {
// Collect all unique tags // Collect all unique tags
this.allTags.clear(); 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) { showContactModal(contact = null) {
console.log('[Contacts] showContactModal called with:', contact); console.log('[Contacts] showContactModal called with:', contact);
console.log('[Contacts] contactModal element:', this.contactModal); console.log('[Contacts] contactModal element:', this.contactModal);

Datei anzeigen

@ -18,6 +18,7 @@ class KnowledgeManager {
this.expandedEntries = new Set(); this.expandedEntries = new Set();
this.initialized = false; this.initialized = false;
this.searchDebounceTimer = null; this.searchDebounceTimer = null;
this.pendingFiles = null;
// Drag & Drop State // Drag & Drop State
this.draggedCategoryId = null; this.draggedCategoryId = null;
@ -140,6 +141,14 @@ class KnowledgeManager {
// File Upload // File Upload
this.fileInput?.addEventListener('change', (e) => this.handleFileSelect(e)); 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 // Drag & Drop for file upload
if (this.fileUploadArea) { if (this.fileUploadArea) {
@ -382,8 +391,8 @@ class KnowledgeManager {
<div class="knowledge-attachment-item" data-attachment-id="${att.id}"> <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> <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"> <div class="knowledge-attachment-info">
<span class="knowledge-attachment-name">${this.escapeHtml(att.original_name)}</span> <span class="knowledge-attachment-name">${this.escapeHtml(att.originalName || att.original_name || '')}</span>
<span class="knowledge-attachment-size">${this.formatFileSize(att.size_bytes)}</span> <span class="knowledge-attachment-size">${this.formatFileSize(att.sizeBytes || att.size_bytes || 0)}</span>
</div> </div>
<div class="knowledge-attachment-actions"> <div class="knowledge-attachment-actions">
<a href="${api.getKnowledgeAttachmentDownloadUrl(att.id)}" class="btn-icon" title="Herunterladen" download> <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.entryModalTitle.textContent = isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag';
this.entryForm?.reset(); this.entryForm?.reset();
this.deleteEntryBtn?.classList.toggle('hidden', !isEdit); 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) { if (isEdit) {
const entry = await this.loadEntryWithAttachments(entryId); const entry = await this.loadEntryWithAttachments(entryId);
@ -972,7 +982,7 @@ class KnowledgeManager {
} else { } else {
this.entryIdInput.value = ''; this.entryIdInput.value = '';
this.entryCategoryIdInput.value = this.selectedCategory?.id || ''; 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'); this.openModal(this.entryModal, 'knowledge-entry-modal');
@ -1010,8 +1020,15 @@ class KnowledgeManager {
await api.updateKnowledgeEntry(entryId, data); await api.updateKnowledgeEntry(entryId, data);
this.showToast('Eintrag aktualisiert', 'success'); this.showToast('Eintrag aktualisiert', 'success');
} else { } else {
await api.createKnowledgeEntry(data); const newEntry = await api.createKnowledgeEntry(data);
this.showToast('Eintrag erstellt', 'success'); 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(); this.closeEntryModal();
@ -1071,7 +1088,10 @@ class KnowledgeManager {
async uploadFiles(files) { async uploadFiles(files) {
const entryId = parseInt(this.entryIdInput?.value); const entryId = parseInt(this.entryIdInput?.value);
if (!entryId) { 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; return;
} }

Datei anzeigen

@ -237,9 +237,10 @@ class ReminderManager {
$('#reminder-color').value = '#F59E0B'; $('#reminder-color').value = '#F59E0B';
// Reset advance days // Reset advance days
$$('input[name="advance-days"]').forEach(cb => { const advanceNumberEl = $('#reminder-advance-number');
cb.checked = cb.value === '1'; 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 freshOptions = $('#reminder-assignee-options');
const freshValueDisplay = freshTrigger.querySelector('.custom-select-value'); 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 // Toggle dropdown
freshTrigger.addEventListener('click', (e) => { freshTrigger.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
wrapper.classList.toggle('open'); wrapper.classList.toggle('open');
// Simple toggle - CSS handles positioning
console.log('[Reminder] Dropdown toggled, open:', wrapper.classList.contains('open')); console.log('[Reminder] Dropdown toggled, open:', wrapper.classList.contains('open'));
}); });
@ -410,9 +420,23 @@ class ReminderManager {
// Set advance days // Set advance days
const advanceDays = reminder.advance_days || ['1']; const advanceDays = reminder.advance_days || ['1'];
$$('input[name="advance-days"]').forEach(cb => { const advanceNumberEl = $('#reminder-advance-number');
cb.checked = advanceDays.includes(cb.value); 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) { } catch (error) {
console.error('Error loading reminder:', error); console.error('Error loading reminder:', error);
@ -434,16 +458,19 @@ class ReminderManager {
try { try {
const formData = new FormData(this.form); const formData = new FormData(this.form);
// Get advance days // Get advance days from new inputs
const advanceDays = []; const advanceNumber = parseInt($('#reminder-advance-number').value) || 1;
$$('input[name="advance-days"]:checked').forEach(cb => { const advanceUnit = $('#reminder-advance-unit').value || 'day';
advanceDays.push(cb.value);
});
if (advanceDays.length === 0) { let advanceDaysValue = advanceNumber;
throw new Error('Bitte wählen Sie mindestens eine Erinnerungszeit aus'); if (advanceUnit === 'week') {
advanceDaysValue = advanceNumber * 7;
} else if (advanceUnit === 'month') {
advanceDaysValue = advanceNumber * 30;
} }
const advanceDays = [String(advanceDaysValue)];
const data = { const data = {
project_id: store.get('currentProjectId'), project_id: store.get('currentProjectId'),
title: formData.get('reminder-title') || $('#reminder-title').value, title: formData.get('reminder-title') || $('#reminder-title').value,

Datei anzeigen

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

11730
logs/app.log

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