Kontakt-Modul
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
623bbdf5dd
Commit
7d67557be4
@ -49,7 +49,15 @@
|
|||||||
"Bash(docker system prune:*)",
|
"Bash(docker system prune:*)",
|
||||||
"Bash(docker cp:*)",
|
"Bash(docker cp:*)",
|
||||||
"Bash(mv:*)",
|
"Bash(mv:*)",
|
||||||
"Bash(docker-compose up:*)"
|
"Bash(docker-compose up:*)",
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(./update-css.sh)",
|
||||||
|
"Bash(./check-resize.sh)",
|
||||||
|
"Bash(grep -n \"orange\\|#f97316\\|#ea580c\" /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 \"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)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3008
CHANGELOG.txt
3008
CHANGELOG.txt
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
232
CLAUDE.md
232
CLAUDE.md
@ -49,6 +49,16 @@ curl http://localhost:3000/api/health
|
|||||||
- **JEDE Änderung MUSS umkehrbar sein** - Live-System!
|
- **JEDE Änderung MUSS umkehrbar sein** - Live-System!
|
||||||
- **Backup vor kritischen Änderungen** ist Pflicht
|
- **Backup vor kritischen Änderungen** ist Pflicht
|
||||||
|
|
||||||
|
**⚠️ NUTZERDATEN-SCHUTZ - ABSOLUTES VERBOT**:
|
||||||
|
- **KEINE Änderungen an Nutzerdaten oder Kennwörtern**
|
||||||
|
- **Geschützte Benutzer (NICHT modifizieren)**:
|
||||||
|
- admin
|
||||||
|
- Hendrik (hendrik_gebhardt@gmx.de)
|
||||||
|
- Monami (momohomma@googlemail.com)
|
||||||
|
- **Diese Benutzer sind produktiv im Einsatz**
|
||||||
|
- **Keine Passwort-Resets oder Änderungen an diesen Accounts**
|
||||||
|
- **Bei Anmeldeproblemen: Nur Debugging, keine Datenänderung**
|
||||||
|
|
||||||
### Rollback-Strategie für Live-Betrieb
|
### Rollback-Strategie für Live-Betrieb
|
||||||
Bei JEDER Änderung sicherstellen:
|
Bei JEDER Änderung sicherstellen:
|
||||||
|
|
||||||
@ -298,6 +308,228 @@ docker exec -it taskmate sh
|
|||||||
docker logs taskmate -f --tail 100
|
docker logs taskmate -f --tail 100
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### KRITISCHES PROBLEM: Frontend-Änderungen werden nicht sichtbar
|
||||||
|
|
||||||
|
**Problem**: Frontend-Dateien (HTML, CSS, JS) werden beim Docker Build nach `/app/public/` kopiert und sind NICHT live gemountet. Änderungen in `/home/claude-dev/TaskMate/frontend/` werden daher nicht automatisch übernommen.
|
||||||
|
|
||||||
|
**Symptome**:
|
||||||
|
- CSS/JS-Änderungen funktionieren nicht trotz Browser-Cache-Löschung
|
||||||
|
- Service Worker Cache-Version Erhöhung hilft nicht
|
||||||
|
- Änderungen werden sporadisch nach längerer Zeit sichtbar
|
||||||
|
|
||||||
|
**Ursache**:
|
||||||
|
1. **Dockerfile kopiert Frontend-Dateien**: `COPY frontend/ ./public/`
|
||||||
|
2. **Express.js cached statische Dateien** mit ETags und Last-Modified Headers
|
||||||
|
3. **Mehrschichtiges Caching**: Service Worker + Browser + Express.js
|
||||||
|
|
||||||
|
**LÖSUNG A - Sofortige Änderungen (Development)**:
|
||||||
|
```bash
|
||||||
|
# Einzelne Datei kopieren
|
||||||
|
docker cp frontend/css/style.css taskmate:/app/public/css/style.css
|
||||||
|
|
||||||
|
# Mehrere Dateien kopieren
|
||||||
|
docker cp frontend/js/app.js taskmate:/app/public/js/app.js
|
||||||
|
docker cp frontend/index.html taskmate:/app/public/index.html
|
||||||
|
|
||||||
|
# CSS + JS zusammen kopieren
|
||||||
|
docker cp frontend/css/ taskmate:/app/public/css/
|
||||||
|
docker cp frontend/js/ taskmate:/app/public/js/
|
||||||
|
```
|
||||||
|
|
||||||
|
**LÖSUNG B - Vollständige Aktualisierung (Production)**:
|
||||||
|
```bash
|
||||||
|
# Docker Image neu bauen und Container ersetzen
|
||||||
|
docker build -t taskmate . && docker restart taskmate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Express.js Caching deaktiviert**:
|
||||||
|
```javascript
|
||||||
|
// In server.js - statische Dateien ohne Caching
|
||||||
|
app.use(express.static(path.join(__dirname, 'public'), {
|
||||||
|
etag: false,
|
||||||
|
lastModified: false,
|
||||||
|
cacheControl: false,
|
||||||
|
setHeaders: (res, path) => {
|
||||||
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debugging-Workflow**:
|
||||||
|
1. Prüfe Datei-Timestamps im Container: `docker exec taskmate ls -la /app/public/css/`
|
||||||
|
2. Vergleiche mit lokalen Dateien: `ls -la frontend/css/`
|
||||||
|
3. Bei Diskrepanz: Files mit `docker cp` aktualisieren
|
||||||
|
4. Bei JavaScript-Problemen: Browser-Console auf Fehler prüfen
|
||||||
|
|
||||||
|
**Warum passiert das**:
|
||||||
|
- Container-Pfad `/app/public/` = Static Files (nicht live)
|
||||||
|
- Container-Pfad `/app/taskmate-source/` = Git-Operationen (live gemountet)
|
||||||
|
- Frontend wird NUR beim Build-Time kopiert, nicht zur Laufzeit
|
||||||
|
|
||||||
|
## 🚨 KRITISCHE LEKTIONEN AUS PROBLEMEN
|
||||||
|
|
||||||
|
### ⚠️ Kontakte-Modul Implementation Probleme (07.01.2025)
|
||||||
|
|
||||||
|
**FEHLER 1: Backend API-Route nicht gefunden (404)**
|
||||||
|
- **Problem**: GET /api/contacts gibt 404 - Endpoint nicht gefunden
|
||||||
|
- **Ursache**: Backend-Dateien nicht im Docker-Container, Container nicht neu gestartet
|
||||||
|
- **Lösung**: Backend-Dateien kopieren und Container neu starten
|
||||||
|
- **Prävention**:
|
||||||
|
```bash
|
||||||
|
# Backend-Änderungen: Alle Dateien kopieren
|
||||||
|
docker cp backend/routes/contacts.js taskmate:/app/routes/
|
||||||
|
docker cp backend/middleware/validation.js taskmate:/app/middleware/
|
||||||
|
docker cp backend/server.js taskmate:/app/server.js
|
||||||
|
docker restart taskmate # IMMER nach Backend-Änderungen
|
||||||
|
```
|
||||||
|
|
||||||
|
**FEHLER 2: Database Table existiert nicht (500 Internal Server Error)**
|
||||||
|
- **Problem**: "no such table: contacts" - Tabelle wurde nicht erstellt
|
||||||
|
- **Ursache**: database.js Änderungen nicht übernommen, bestehende DB erweitert sich nicht automatisch
|
||||||
|
- **Lösung**: database.js kopieren + Tabelle manuell erstellen
|
||||||
|
- **Pattern für neue Tabellen**:
|
||||||
|
```bash
|
||||||
|
docker cp backend/database.js taskmate:/app/database.js
|
||||||
|
docker exec taskmate node -e "
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const db = new Database('/app/data/taskmate.db');
|
||||||
|
db.exec('CREATE TABLE IF NOT EXISTS new_table (...);');
|
||||||
|
console.log('Table created successfully');
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**FEHLER 3: store.showMessage ist undefined**
|
||||||
|
- **Problem**: `store.showMessage()` Funktion existiert nicht
|
||||||
|
- **Ursache**: Falsche API für Toast-Nachrichten
|
||||||
|
- **Lösung**: Verwende `window.dispatchEvent` mit `toast:show`
|
||||||
|
- **Pattern für Toast-Messages**:
|
||||||
|
```javascript
|
||||||
|
// FALSCH: store.showMessage('Text', 'success')
|
||||||
|
// RICHTIG:
|
||||||
|
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||||
|
detail: { message: 'Text', type: 'success' }
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
**FEHLER 4: Event-Handler nicht gebunden**
|
||||||
|
- **Problem**: Button-Clicks funktionieren nicht trotz Event-Listener
|
||||||
|
- **Ursache**: Timing-Problem - DOM noch nicht bereit, Modal-Overlay fehlt
|
||||||
|
- **Lösung**: Korrekte Modal-Struktur + Overlay-Management
|
||||||
|
- **Debugging-Pattern**:
|
||||||
|
```javascript
|
||||||
|
console.log('[Module] Element check:', this.buttonElement);
|
||||||
|
if (this.buttonElement) {
|
||||||
|
console.log('[Module] Binding event');
|
||||||
|
this.buttonElement.addEventListener('click', () => {
|
||||||
|
console.log('[Module] Button clicked!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**FEHLER 5: Modal Design inkonsistent**
|
||||||
|
- **Problem**: Custom Modal-Styles passen nicht zum App-Design
|
||||||
|
- **Ursache**: Eigene CSS-Klassen statt Standard-Modal-Pattern
|
||||||
|
- **Lösung**: Standard Modal-Struktur verwenden
|
||||||
|
- **Standard Modal-Pattern**:
|
||||||
|
```html
|
||||||
|
<div class="modal modal-medium hidden">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Title</h2>
|
||||||
|
<button class="modal-close" data-close-modal>×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body"><!-- Content --></div>
|
||||||
|
<div class="modal-footer"><!-- Buttons --></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Erinnerung-Implementation Probleme (06.01.2026)
|
||||||
|
|
||||||
|
**FEHLER 1: Syntax-Fehler in JavaScript blockierte Login**
|
||||||
|
- **Problem**: Missing closing brace in calendar.js verhinderte Login komplett
|
||||||
|
- **Ursache**: Unvollständige Code-Blöcke beim Multi-Edit
|
||||||
|
- **Lösung**: IMMER Syntax-Check nach JavaScript-Änderungen
|
||||||
|
- **Prävention**:
|
||||||
|
```bash
|
||||||
|
# Nach JS-Änderungen prüfen:
|
||||||
|
node -c frontend/js/calendar.js
|
||||||
|
docker logs taskmate --tail 20 # Auf Syntax-Fehler prüfen
|
||||||
|
```
|
||||||
|
|
||||||
|
**FEHLER 2: "Verschwundene" Projekte durch 401-Fehler**
|
||||||
|
- **Problem**: User dachte AegisSight-Projekt sei gelöscht
|
||||||
|
- **Ursache**: Authentifizierungs-Token abgelaufen, API gibt 401 zurück
|
||||||
|
- **Diagnose**: `docker logs taskmate` zeigt 401-Fehler
|
||||||
|
- **Lösung**: Einfach neu anmelden, Daten sind intakt
|
||||||
|
- **Prävention**: Bei "verschwundenen" Daten IMMER zuerst Auth prüfen
|
||||||
|
|
||||||
|
**FEHLER 3: Checkbox-Styling funktioniert nicht**
|
||||||
|
- **Problem**: CSS-Selektoren greifen nicht, komplexe Pseudo-Element-Struktur
|
||||||
|
- **Ursache**: Browser-CSS-Konflikte, CSS-Variable-Probleme
|
||||||
|
- **Lösung**: Direktes Styling nativer Checkboxes mit `appearance: none`
|
||||||
|
- **Lesson**: Bei CSS-Problemen: **Einfachster Ansatz zuerst**
|
||||||
|
```css
|
||||||
|
/* FALSCH: Komplexe Pseudo-Struktur */
|
||||||
|
input:checked + span::after { content: '✓'; }
|
||||||
|
|
||||||
|
/* RICHTIG: Direktes Styling */
|
||||||
|
input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
background: #3B82F6 when :checked;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**FEHLER 4: Event-Handler Konflikte bei Modal-Updates**
|
||||||
|
- **Problem**: Dropdown-Handler werden überschrieben
|
||||||
|
- **Ursache**: Mehrfache Event-Binding ohne Cleanup
|
||||||
|
- **Lösung**: Element-Kloning für saubere Event-Handler
|
||||||
|
- **Pattern**:
|
||||||
|
```javascript
|
||||||
|
// Event-Handler cleanup durch Klonen
|
||||||
|
const newElement = element.cloneNode(true);
|
||||||
|
element.parentNode.replaceChild(newElement, element);
|
||||||
|
// Dann neue Handler binden
|
||||||
|
```
|
||||||
|
|
||||||
|
**FEHLER 5: Visuelle Darstellung unterbricht Funktionalität**
|
||||||
|
- **Problem**: Erinnerungen unterbrachen Aufgaben-Balken
|
||||||
|
- **Ursache**: Falsche Render-Reihenfolge (Erinnerungen vor Aufgaben)
|
||||||
|
- **Lösung**: Aufgaben zuerst, dann Erinnerungen
|
||||||
|
- **Lesson**: UI-Reihenfolge muss Funktionalität folgen, nicht umgekehrt
|
||||||
|
|
||||||
|
### 🔧 TROUBLESHOOTING-WORKFLOW
|
||||||
|
|
||||||
|
**Bei JavaScript-Fehlern:**
|
||||||
|
1. `docker logs taskmate --tail 50` prüfen
|
||||||
|
2. Browser-Console auf Syntax-Fehler prüfen
|
||||||
|
3. Node.js Syntax-Check: `node -c datei.js`
|
||||||
|
|
||||||
|
**Bei "verschwundenen" Daten:**
|
||||||
|
1. **NIEMALS** sofort Backup/Restore - erst debuggen!
|
||||||
|
2. API-Logs prüfen auf 401/403 Fehler
|
||||||
|
3. Auth-Status prüfen: `localStorage.getItem('token')`
|
||||||
|
4. Datenbank direkt prüfen: `sqlite3 data/taskmate.db "SELECT COUNT(*) FROM projects"`
|
||||||
|
|
||||||
|
**Bei CSS-Problemen:**
|
||||||
|
1. Simplest approach first - keine komplexen Selektoren
|
||||||
|
2. `!important` nur als letzter Ausweg
|
||||||
|
3. Browser-DevTools: Computed Styles prüfen
|
||||||
|
4. Cache leeren: `CACHE_VERSION++` in sw.js
|
||||||
|
|
||||||
|
**Bei neuen Modulen mit globaler Suche:**
|
||||||
|
1. Module in app.js setupSearch() registrieren:
|
||||||
|
```javascript
|
||||||
|
} else if (currentView === 'mymodule') {
|
||||||
|
import('./mymodule.js').then(module => {
|
||||||
|
if (module.myManager) {
|
||||||
|
module.myManager.searchQuery = value;
|
||||||
|
module.myManager.filterData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
2. Manager-Instanz exportieren: `export { myManager }`
|
||||||
|
3. clearSearch() Funktion ebenfalls erweitern
|
||||||
|
4. Lokale Suchfelder entfernen - nur Header-Suche nutzen
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
### Häufige Probleme
|
### Häufige Probleme
|
||||||
|
|||||||
@ -485,6 +485,41 @@ function createTables() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Erinnerungen
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS reminders (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
project_id INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
reminder_date DATE NOT NULL,
|
||||||
|
reminder_time TIME DEFAULT '09:00',
|
||||||
|
color TEXT DEFAULT '#F59E0B',
|
||||||
|
advance_days TEXT DEFAULT '1',
|
||||||
|
repeat_type TEXT DEFAULT 'none',
|
||||||
|
repeat_interval INTEGER DEFAULT 1,
|
||||||
|
is_active INTEGER DEFAULT 1,
|
||||||
|
created_by INTEGER NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Erinnerungs-Benachrichtigungen (für Tracking welche bereits gesendet wurden)
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS reminder_notifications (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
reminder_id INTEGER NOT NULL,
|
||||||
|
notification_date DATE NOT NULL,
|
||||||
|
sent INTEGER DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(reminder_id, notification_date)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
// Wissensmanagement - Kategorien
|
// Wissensmanagement - Kategorien
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS knowledge_categories (
|
CREATE TABLE IF NOT EXISTS knowledge_categories (
|
||||||
@ -561,6 +596,31 @@ function createTables() {
|
|||||||
logger.info('Migration: claude_instructions Spalte zu coding_directories hinzugefuegt');
|
logger.info('Migration: claude_instructions Spalte zu coding_directories hinzugefuegt');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kontakte
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS contacts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
first_name TEXT,
|
||||||
|
last_name TEXT,
|
||||||
|
company TEXT,
|
||||||
|
position TEXT,
|
||||||
|
email TEXT,
|
||||||
|
phone TEXT,
|
||||||
|
mobile TEXT,
|
||||||
|
address TEXT,
|
||||||
|
postal_code TEXT,
|
||||||
|
city TEXT,
|
||||||
|
country TEXT,
|
||||||
|
website TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
tags TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by INTEGER,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
// Indizes für Performance
|
// Indizes für Performance
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
||||||
@ -581,6 +641,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_contacts_company ON contacts(company);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contacts_tags ON contacts(tags);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
logger.info('Datenbank-Tabellen erstellt');
|
logger.info('Datenbank-Tabellen erstellt');
|
||||||
|
|||||||
@ -305,6 +305,46 @@ function sanitizeMiddleware(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kontakt-Validierung Middleware
|
||||||
|
*/
|
||||||
|
validators.contact = function(req, res, next) {
|
||||||
|
const errors = [];
|
||||||
|
const { firstName, lastName, company, email, phone, mobile, website } = req.body;
|
||||||
|
|
||||||
|
// Mindestens ein Name oder Firma muss vorhanden sein
|
||||||
|
if (!firstName && !lastName && !company) {
|
||||||
|
errors.push('Mindestens Vorname, Nachname oder Firma muss angegeben werden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validieren
|
||||||
|
if (email) {
|
||||||
|
const emailError = validators.email(email, 'E-Mail');
|
||||||
|
if (emailError) errors.push(emailError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Website URL validieren
|
||||||
|
if (website) {
|
||||||
|
const urlError = validators.url(website, 'Website');
|
||||||
|
if (urlError) errors.push(urlError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telefonnummer Format (optional)
|
||||||
|
if (phone && !/^[\d\s\-\+\(\)]+$/.test(phone)) {
|
||||||
|
errors.push('Telefonnummer enthält ungültige Zeichen');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobile && !/^[\d\s\-\+\(\)]+$/.test(mobile)) {
|
||||||
|
errors.push('Mobilnummer enthält ungültige Zeichen');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return res.status(400).json({ errors });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stripHtml,
|
stripHtml,
|
||||||
sanitizeMarkdown,
|
sanitizeMarkdown,
|
||||||
|
|||||||
439
backend/routes/contacts.js
Normale Datei
439
backend/routes/contacts.js
Normale Datei
@ -0,0 +1,439 @@
|
|||||||
|
/**
|
||||||
|
* TASKMATE - Contact Routes
|
||||||
|
* =========================
|
||||||
|
* CRUD für Kontakte
|
||||||
|
*/
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getDb } = require('../database');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
const { validators } = require('../middleware/validation');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/contacts
|
||||||
|
* Alle Kontakte abrufen mit optionalem Filter
|
||||||
|
*/
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const { search, tag, sortBy = 'created_at', sortOrder = 'desc' } = req.query;
|
||||||
|
|
||||||
|
let query = `
|
||||||
|
SELECT c.*, u.display_name as creator_name
|
||||||
|
FROM contacts c
|
||||||
|
LEFT JOIN users u ON c.created_by = u.id
|
||||||
|
WHERE 1=1
|
||||||
|
`;
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
// Suchfilter
|
||||||
|
if (search) {
|
||||||
|
query += ` AND (
|
||||||
|
c.first_name LIKE ? OR
|
||||||
|
c.last_name LIKE ? OR
|
||||||
|
c.company LIKE ? OR
|
||||||
|
c.email LIKE ? OR
|
||||||
|
c.phone LIKE ? OR
|
||||||
|
c.mobile LIKE ?
|
||||||
|
)`;
|
||||||
|
const searchParam = `%${search}%`;
|
||||||
|
params.push(searchParam, searchParam, searchParam, searchParam, searchParam, searchParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag-Filter
|
||||||
|
if (tag) {
|
||||||
|
query += ` AND c.tags LIKE ?`;
|
||||||
|
params.push(`%${tag}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortierung
|
||||||
|
const validSortFields = ['first_name', 'last_name', 'company', 'created_at', 'updated_at'];
|
||||||
|
const sortField = validSortFields.includes(sortBy) ? sortBy : 'created_at';
|
||||||
|
const order = sortOrder.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||||
|
query += ` ORDER BY c.${sortField} ${order}`;
|
||||||
|
|
||||||
|
const contacts = db.prepare(query).all(params);
|
||||||
|
|
||||||
|
res.json(contacts.map(c => ({
|
||||||
|
id: c.id,
|
||||||
|
firstName: c.first_name,
|
||||||
|
lastName: c.last_name,
|
||||||
|
company: c.company,
|
||||||
|
position: c.position,
|
||||||
|
email: c.email,
|
||||||
|
phone: c.phone,
|
||||||
|
mobile: c.mobile,
|
||||||
|
address: c.address,
|
||||||
|
postalCode: c.postal_code,
|
||||||
|
city: c.city,
|
||||||
|
country: c.country,
|
||||||
|
website: c.website,
|
||||||
|
notes: c.notes,
|
||||||
|
tags: c.tags ? c.tags.split(',').map(t => t.trim()) : [],
|
||||||
|
createdAt: c.created_at,
|
||||||
|
updatedAt: c.updated_at,
|
||||||
|
createdBy: c.created_by,
|
||||||
|
creatorName: c.creator_name
|
||||||
|
})));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Fehler beim Abrufen der Kontakte:', { error: error.message });
|
||||||
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/contacts/:id
|
||||||
|
* Einzelnen Kontakt abrufen
|
||||||
|
*/
|
||||||
|
router.get('/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const contactId = req.params.id;
|
||||||
|
|
||||||
|
const contact = db.prepare(`
|
||||||
|
SELECT c.*, u.display_name as creator_name
|
||||||
|
FROM contacts c
|
||||||
|
LEFT JOIN users u ON c.created_by = u.id
|
||||||
|
WHERE c.id = ?
|
||||||
|
`).get(contactId);
|
||||||
|
|
||||||
|
if (!contact) {
|
||||||
|
return res.status(404).json({ error: 'Kontakt nicht gefunden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: contact.id,
|
||||||
|
firstName: contact.first_name,
|
||||||
|
lastName: contact.last_name,
|
||||||
|
company: contact.company,
|
||||||
|
position: contact.position,
|
||||||
|
email: contact.email,
|
||||||
|
phone: contact.phone,
|
||||||
|
mobile: contact.mobile,
|
||||||
|
address: contact.address,
|
||||||
|
postalCode: contact.postal_code,
|
||||||
|
city: contact.city,
|
||||||
|
country: contact.country,
|
||||||
|
website: contact.website,
|
||||||
|
notes: contact.notes,
|
||||||
|
tags: contact.tags ? contact.tags.split(',').map(t => t.trim()) : [],
|
||||||
|
createdAt: contact.created_at,
|
||||||
|
updatedAt: contact.updated_at,
|
||||||
|
createdBy: contact.created_by,
|
||||||
|
creatorName: contact.creator_name
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Fehler beim Abrufen des Kontakts:', { error: error.message, contactId: req.params.id });
|
||||||
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/contacts
|
||||||
|
* Neuen Kontakt erstellen
|
||||||
|
*/
|
||||||
|
router.post('/', validators.contact, (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const userId = req.user.id;
|
||||||
|
const {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
company,
|
||||||
|
position,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
mobile,
|
||||||
|
address,
|
||||||
|
postalCode,
|
||||||
|
city,
|
||||||
|
country,
|
||||||
|
website,
|
||||||
|
notes,
|
||||||
|
tags
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const result = db.prepare(`
|
||||||
|
INSERT INTO contacts (
|
||||||
|
first_name, last_name, company, position,
|
||||||
|
email, phone, mobile, address, postal_code,
|
||||||
|
city, country, website, notes, tags,
|
||||||
|
created_by
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(
|
||||||
|
firstName || null,
|
||||||
|
lastName || null,
|
||||||
|
company || null,
|
||||||
|
position || null,
|
||||||
|
email || null,
|
||||||
|
phone || null,
|
||||||
|
mobile || null,
|
||||||
|
address || null,
|
||||||
|
postalCode || null,
|
||||||
|
city || null,
|
||||||
|
country || null,
|
||||||
|
website || null,
|
||||||
|
notes || null,
|
||||||
|
Array.isArray(tags) ? tags.join(', ') : null,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
const newContact = db.prepare(`
|
||||||
|
SELECT c.*, u.display_name as creator_name
|
||||||
|
FROM contacts c
|
||||||
|
LEFT JOIN users u ON c.created_by = u.id
|
||||||
|
WHERE c.id = ?
|
||||||
|
`).get(result.lastInsertRowid);
|
||||||
|
|
||||||
|
// Socket.io Event
|
||||||
|
const io = req.app.get('io');
|
||||||
|
io.emit('contact:created', {
|
||||||
|
contact: {
|
||||||
|
id: newContact.id,
|
||||||
|
firstName: newContact.first_name,
|
||||||
|
lastName: newContact.last_name,
|
||||||
|
company: newContact.company,
|
||||||
|
position: newContact.position,
|
||||||
|
email: newContact.email,
|
||||||
|
phone: newContact.phone,
|
||||||
|
mobile: newContact.mobile,
|
||||||
|
address: newContact.address,
|
||||||
|
postalCode: newContact.postal_code,
|
||||||
|
city: newContact.city,
|
||||||
|
country: newContact.country,
|
||||||
|
website: newContact.website,
|
||||||
|
notes: newContact.notes,
|
||||||
|
tags: newContact.tags ? newContact.tags.split(',').map(t => t.trim()) : [],
|
||||||
|
createdAt: newContact.created_at,
|
||||||
|
updatedAt: newContact.updated_at,
|
||||||
|
createdBy: newContact.created_by,
|
||||||
|
creatorName: newContact.creator_name
|
||||||
|
},
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
id: newContact.id,
|
||||||
|
firstName: newContact.first_name,
|
||||||
|
lastName: newContact.last_name,
|
||||||
|
company: newContact.company,
|
||||||
|
position: newContact.position,
|
||||||
|
email: newContact.email,
|
||||||
|
phone: newContact.phone,
|
||||||
|
mobile: newContact.mobile,
|
||||||
|
address: newContact.address,
|
||||||
|
postalCode: newContact.postal_code,
|
||||||
|
city: newContact.city,
|
||||||
|
country: newContact.country,
|
||||||
|
website: newContact.website,
|
||||||
|
notes: newContact.notes,
|
||||||
|
tags: newContact.tags ? newContact.tags.split(',').map(t => t.trim()) : [],
|
||||||
|
createdAt: newContact.created_at,
|
||||||
|
updatedAt: newContact.updated_at,
|
||||||
|
createdBy: newContact.created_by,
|
||||||
|
creatorName: newContact.creator_name
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Kontakt erstellt', { contactId: newContact.id, userId });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Fehler beim Erstellen des Kontakts:', { error: error.message, body: req.body });
|
||||||
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/contacts/:id
|
||||||
|
* Kontakt aktualisieren
|
||||||
|
*/
|
||||||
|
router.put('/:id', validators.contact, (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const contactId = req.params.id;
|
||||||
|
const userId = req.user.id;
|
||||||
|
const {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
company,
|
||||||
|
position,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
mobile,
|
||||||
|
address,
|
||||||
|
postalCode,
|
||||||
|
city,
|
||||||
|
country,
|
||||||
|
website,
|
||||||
|
notes,
|
||||||
|
tags
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Prüfen ob Kontakt existiert
|
||||||
|
const existing = db.prepare('SELECT id FROM contacts WHERE id = ?').get(contactId);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: 'Kontakt nicht gefunden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE contacts SET
|
||||||
|
first_name = ?,
|
||||||
|
last_name = ?,
|
||||||
|
company = ?,
|
||||||
|
position = ?,
|
||||||
|
email = ?,
|
||||||
|
phone = ?,
|
||||||
|
mobile = ?,
|
||||||
|
address = ?,
|
||||||
|
postal_code = ?,
|
||||||
|
city = ?,
|
||||||
|
country = ?,
|
||||||
|
website = ?,
|
||||||
|
notes = ?,
|
||||||
|
tags = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(
|
||||||
|
firstName || null,
|
||||||
|
lastName || null,
|
||||||
|
company || null,
|
||||||
|
position || null,
|
||||||
|
email || null,
|
||||||
|
phone || null,
|
||||||
|
mobile || null,
|
||||||
|
address || null,
|
||||||
|
postalCode || null,
|
||||||
|
city || null,
|
||||||
|
country || null,
|
||||||
|
website || null,
|
||||||
|
notes || null,
|
||||||
|
Array.isArray(tags) ? tags.join(', ') : null,
|
||||||
|
contactId
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedContact = db.prepare(`
|
||||||
|
SELECT c.*, u.display_name as creator_name
|
||||||
|
FROM contacts c
|
||||||
|
LEFT JOIN users u ON c.created_by = u.id
|
||||||
|
WHERE c.id = ?
|
||||||
|
`).get(contactId);
|
||||||
|
|
||||||
|
// Socket.io Event
|
||||||
|
const io = req.app.get('io');
|
||||||
|
io.emit('contact:updated', {
|
||||||
|
contact: {
|
||||||
|
id: updatedContact.id,
|
||||||
|
firstName: updatedContact.first_name,
|
||||||
|
lastName: updatedContact.last_name,
|
||||||
|
company: updatedContact.company,
|
||||||
|
position: updatedContact.position,
|
||||||
|
email: updatedContact.email,
|
||||||
|
phone: updatedContact.phone,
|
||||||
|
mobile: updatedContact.mobile,
|
||||||
|
address: updatedContact.address,
|
||||||
|
postalCode: updatedContact.postal_code,
|
||||||
|
city: updatedContact.city,
|
||||||
|
country: updatedContact.country,
|
||||||
|
website: updatedContact.website,
|
||||||
|
notes: updatedContact.notes,
|
||||||
|
tags: updatedContact.tags ? updatedContact.tags.split(',').map(t => t.trim()) : [],
|
||||||
|
createdAt: updatedContact.created_at,
|
||||||
|
updatedAt: updatedContact.updated_at,
|
||||||
|
createdBy: updatedContact.created_by,
|
||||||
|
creatorName: updatedContact.creator_name
|
||||||
|
},
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: updatedContact.id,
|
||||||
|
firstName: updatedContact.first_name,
|
||||||
|
lastName: updatedContact.last_name,
|
||||||
|
company: updatedContact.company,
|
||||||
|
position: updatedContact.position,
|
||||||
|
email: updatedContact.email,
|
||||||
|
phone: updatedContact.phone,
|
||||||
|
mobile: updatedContact.mobile,
|
||||||
|
address: updatedContact.address,
|
||||||
|
postalCode: updatedContact.postal_code,
|
||||||
|
city: updatedContact.city,
|
||||||
|
country: updatedContact.country,
|
||||||
|
website: updatedContact.website,
|
||||||
|
notes: updatedContact.notes,
|
||||||
|
tags: updatedContact.tags ? updatedContact.tags.split(',').map(t => t.trim()) : [],
|
||||||
|
createdAt: updatedContact.created_at,
|
||||||
|
updatedAt: updatedContact.updated_at,
|
||||||
|
createdBy: updatedContact.created_by,
|
||||||
|
creatorName: updatedContact.creator_name
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Kontakt aktualisiert', { contactId, userId });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Fehler beim Aktualisieren des Kontakts:', { error: error.message, contactId: req.params.id });
|
||||||
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/contacts/:id
|
||||||
|
* Kontakt löschen
|
||||||
|
*/
|
||||||
|
router.delete('/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const contactId = req.params.id;
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// Prüfen ob Kontakt existiert
|
||||||
|
const existing = db.prepare('SELECT id FROM contacts WHERE id = ?').get(contactId);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ error: 'Kontakt nicht gefunden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Löschen
|
||||||
|
db.prepare('DELETE FROM contacts WHERE id = ?').run(contactId);
|
||||||
|
|
||||||
|
// Socket.io Event
|
||||||
|
const io = req.app.get('io');
|
||||||
|
io.emit('contact:deleted', { contactId, userId });
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
|
||||||
|
logger.info('Kontakt gelöscht', { contactId, userId });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Fehler beim Löschen des Kontakts:', { error: error.message, contactId: req.params.id });
|
||||||
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/contacts/tags
|
||||||
|
* Alle verwendeten Tags abrufen
|
||||||
|
*/
|
||||||
|
router.get('/tags/all', (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
const contacts = db.prepare('SELECT DISTINCT tags FROM contacts WHERE tags IS NOT NULL').all();
|
||||||
|
|
||||||
|
// Alle Tags sammeln und deduplizieren
|
||||||
|
const allTags = new Set();
|
||||||
|
contacts.forEach(contact => {
|
||||||
|
if (contact.tags) {
|
||||||
|
contact.tags.split(',').forEach(tag => {
|
||||||
|
const trimmedTag = tag.trim();
|
||||||
|
if (trimmedTag) {
|
||||||
|
allTags.add(trimmedTag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(Array.from(allTags).sort());
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Fehler beim Abrufen der Tags:', { error: error.message });
|
||||||
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -110,13 +110,13 @@ router.post('/categories', (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Eine Kategorie mit diesem Namen existiert bereits' });
|
return res.status(400).json({ error: 'Eine Kategorie mit diesem Namen existiert bereits' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position ermitteln
|
// Alle bestehenden Kategorien um 1 nach unten verschieben
|
||||||
const lastPosition = db.prepare(
|
db.prepare(`
|
||||||
'SELECT MAX(position) as max_pos FROM knowledge_categories'
|
UPDATE knowledge_categories
|
||||||
).get();
|
SET position = position + 1
|
||||||
const position = (lastPosition.max_pos ?? -1) + 1;
|
`).run();
|
||||||
|
|
||||||
// Einfügen
|
// Neue Kategorie an Position 0 (ganz oben) einfügen
|
||||||
const result = db.prepare(`
|
const result = db.prepare(`
|
||||||
INSERT INTO knowledge_categories (name, description, color, icon, position, created_by)
|
INSERT INTO knowledge_categories (name, description, color, icon, position, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
@ -125,7 +125,7 @@ router.post('/categories', (req, res) => {
|
|||||||
description ? stripHtml(description) : null,
|
description ? stripHtml(description) : null,
|
||||||
color || '#3B82F6',
|
color || '#3B82F6',
|
||||||
icon || null,
|
icon || null,
|
||||||
position,
|
0, // Neue Kategorien immer an Position 0 (oben)
|
||||||
req.user.id
|
req.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -473,13 +473,14 @@ router.post('/entries', (req, res) => {
|
|||||||
return res.status(404).json({ error: 'Kategorie nicht gefunden' });
|
return res.status(404).json({ error: 'Kategorie nicht gefunden' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position ermitteln
|
// Alle bestehenden Einträge um 1 nach unten verschieben
|
||||||
const lastPosition = db.prepare(
|
db.prepare(`
|
||||||
'SELECT MAX(position) as max_pos FROM knowledge_entries WHERE category_id = ?'
|
UPDATE knowledge_entries
|
||||||
).get(categoryId);
|
SET position = position + 1
|
||||||
const position = (lastPosition.max_pos ?? -1) + 1;
|
WHERE category_id = ?
|
||||||
|
`).run(categoryId);
|
||||||
|
|
||||||
// Einfügen
|
// Neuen Eintrag an Position 0 (ganz oben) einfügen
|
||||||
const result = db.prepare(`
|
const result = db.prepare(`
|
||||||
INSERT INTO knowledge_entries (category_id, title, url, notes, position, created_by)
|
INSERT INTO knowledge_entries (category_id, title, url, notes, position, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
@ -488,7 +489,7 @@ router.post('/entries', (req, res) => {
|
|||||||
stripHtml(title),
|
stripHtml(title),
|
||||||
url || null,
|
url || null,
|
||||||
notes || null,
|
notes || null,
|
||||||
position,
|
0, // Neue Einträge immer an Position 0 (oben)
|
||||||
req.user.id
|
req.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
360
backend/routes/reminders.js
Normale Datei
360
backend/routes/reminders.js
Normale Datei
@ -0,0 +1,360 @@
|
|||||||
|
/**
|
||||||
|
* TASKMATE - Reminders API
|
||||||
|
* ========================
|
||||||
|
* API endpoints für Erinnerungen
|
||||||
|
*/
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getDb } = require('../database');
|
||||||
|
const reminderService = require('../services/reminderService');
|
||||||
|
|
||||||
|
// GET /api/reminders - Alle Erinnerungen für ein Projekt abrufen
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { project_id } = req.query;
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
if (!project_id) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'project_id ist erforderlich'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const reminders = db.prepare(`
|
||||||
|
SELECT r.*, u.display_name as creator_name
|
||||||
|
FROM reminders r
|
||||||
|
LEFT JOIN users u ON r.created_by = u.id
|
||||||
|
WHERE r.project_id = ? AND r.is_active = 1
|
||||||
|
ORDER BY r.reminder_date ASC, r.reminder_time ASC
|
||||||
|
`).all(project_id);
|
||||||
|
|
||||||
|
// Advance days von String zu Array konvertieren
|
||||||
|
reminders.forEach(reminder => {
|
||||||
|
reminder.advance_days = reminder.advance_days ? reminder.advance_days.split(',') : ['1'];
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: reminders
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching reminders:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Interner Server-Fehler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/reminders/:id - Einzelne Erinnerung abrufen
|
||||||
|
router.get('/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const reminder = db.prepare(`
|
||||||
|
SELECT r.*, u.display_name as creator_name
|
||||||
|
FROM reminders r
|
||||||
|
LEFT JOIN users u ON r.created_by = u.id
|
||||||
|
WHERE r.id = ?
|
||||||
|
`).get(req.params.id);
|
||||||
|
|
||||||
|
if (!reminder) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Erinnerung nicht gefunden'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance days von String zu Array konvertieren
|
||||||
|
reminder.advance_days = reminder.advance_days ? reminder.advance_days.split(',') : ['1'];
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: reminder
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching reminder:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Interner Server-Fehler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/reminders - Neue Erinnerung erstellen
|
||||||
|
router.post('/', (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
project_id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
reminder_date,
|
||||||
|
reminder_time,
|
||||||
|
color,
|
||||||
|
advance_days,
|
||||||
|
repeat_type,
|
||||||
|
repeat_interval
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!project_id || !title || !reminder_date) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'project_id, title und reminder_date sind erforderlich'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
// Advance days Array zu String konvertieren
|
||||||
|
const advanceDaysStr = Array.isArray(advance_days) ? advance_days.join(',') : '1';
|
||||||
|
|
||||||
|
const result = db.prepare(`
|
||||||
|
INSERT INTO reminders (
|
||||||
|
project_id, title, description, reminder_date, reminder_time,
|
||||||
|
color, advance_days, repeat_type, repeat_interval, created_by
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(
|
||||||
|
project_id,
|
||||||
|
title,
|
||||||
|
description || null,
|
||||||
|
reminder_date,
|
||||||
|
reminder_time || '09:00',
|
||||||
|
color || '#F59E0B',
|
||||||
|
advanceDaysStr,
|
||||||
|
repeat_type || 'none',
|
||||||
|
repeat_interval || 1,
|
||||||
|
req.user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Benachrichtigungs-Termine mit ReminderService erstellen
|
||||||
|
if (advance_days && Array.isArray(advance_days)) {
|
||||||
|
const serviceInstance = reminderService.getInstance();
|
||||||
|
serviceInstance.createNotificationSchedule(result.lastInsertRowid, reminder_date, advance_days);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Erinnerung abrufen
|
||||||
|
const newReminder = db.prepare(`
|
||||||
|
SELECT r.*, u.display_name as creator_name
|
||||||
|
FROM reminders r
|
||||||
|
LEFT JOIN users u ON r.created_by = u.id
|
||||||
|
WHERE r.id = ?
|
||||||
|
`).get(result.lastInsertRowid);
|
||||||
|
|
||||||
|
newReminder.advance_days = newReminder.advance_days ? newReminder.advance_days.split(',') : ['1'];
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: newReminder
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating reminder:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Interner Server-Fehler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/reminders/:id - Erinnerung bearbeiten
|
||||||
|
router.put('/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
reminder_date,
|
||||||
|
reminder_time,
|
||||||
|
color,
|
||||||
|
advance_days,
|
||||||
|
repeat_type,
|
||||||
|
repeat_interval,
|
||||||
|
is_active
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
// Prüfen ob Erinnerung existiert
|
||||||
|
const existing = db.prepare('SELECT * FROM reminders WHERE id = ?').get(req.params.id);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Erinnerung nicht gefunden'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance days Array zu String konvertieren
|
||||||
|
const advanceDaysStr = Array.isArray(advance_days) ? advance_days.join(',') : existing.advance_days;
|
||||||
|
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE reminders SET
|
||||||
|
title = ?,
|
||||||
|
description = ?,
|
||||||
|
reminder_date = ?,
|
||||||
|
reminder_time = ?,
|
||||||
|
color = ?,
|
||||||
|
advance_days = ?,
|
||||||
|
repeat_type = ?,
|
||||||
|
repeat_interval = ?,
|
||||||
|
is_active = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(
|
||||||
|
title || existing.title,
|
||||||
|
description !== undefined ? description : existing.description,
|
||||||
|
reminder_date || existing.reminder_date,
|
||||||
|
reminder_time || existing.reminder_time,
|
||||||
|
color || existing.color,
|
||||||
|
advanceDaysStr,
|
||||||
|
repeat_type || existing.repeat_type,
|
||||||
|
repeat_interval || existing.repeat_interval,
|
||||||
|
is_active !== undefined ? is_active : existing.is_active,
|
||||||
|
req.params.id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Benachrichtigungs-Termine neu berechnen wenn sich das Datum geändert hat
|
||||||
|
if (reminder_date || advance_days) {
|
||||||
|
const finalAdvanceDays = advance_days || existing.advance_days.split(',');
|
||||||
|
const finalReminderDate = reminder_date || existing.reminder_date;
|
||||||
|
|
||||||
|
const serviceInstance = reminderService.getInstance();
|
||||||
|
serviceInstance.createNotificationSchedule(req.params.id, finalReminderDate, finalAdvanceDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisierte Erinnerung abrufen
|
||||||
|
const updatedReminder = db.prepare(`
|
||||||
|
SELECT r.*, u.display_name as creator_name
|
||||||
|
FROM reminders r
|
||||||
|
LEFT JOIN users u ON r.created_by = u.id
|
||||||
|
WHERE r.id = ?
|
||||||
|
`).get(req.params.id);
|
||||||
|
|
||||||
|
updatedReminder.advance_days = updatedReminder.advance_days ? updatedReminder.advance_days.split(',') : ['1'];
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: updatedReminder
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating reminder:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Interner Server-Fehler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/reminders/:id - Erinnerung löschen
|
||||||
|
router.delete('/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
const result = db.prepare('DELETE FROM reminders WHERE id = ?').run(req.params.id);
|
||||||
|
|
||||||
|
if (result.changes === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Erinnerung nicht gefunden'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Erinnerung erfolgreich gelöscht'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting reminder:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Interner Server-Fehler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/reminders/due/check - Fällige Erinnerungen prüfen (für Cron-Job)
|
||||||
|
router.get('/due/check', (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Fällige Benachrichtigungen finden
|
||||||
|
const dueNotifications = db.prepare(`
|
||||||
|
SELECT rn.*, r.title, r.description, r.project_id, r.created_by, r.reminder_date, r.color
|
||||||
|
FROM reminder_notifications rn
|
||||||
|
JOIN reminders r ON rn.reminder_id = r.id
|
||||||
|
WHERE rn.notification_date <= ? AND rn.sent = 0 AND r.is_active = 1
|
||||||
|
ORDER BY rn.notification_date ASC
|
||||||
|
`).all(today);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: dueNotifications
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking due reminders:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Interner Server-Fehler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/reminders/due/mark-sent - Benachrichtigung als gesendet markieren
|
||||||
|
router.post('/due/mark-sent', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { notification_id } = req.body;
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
db.prepare('UPDATE reminder_notifications SET sent = 1 WHERE id = ?').run(notification_id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Benachrichtigung als gesendet markiert'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error marking notification as sent:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Interner Server-Fehler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/reminders/stats - Debug-Statistiken für Reminder Service
|
||||||
|
router.get('/stats', (req, res) => {
|
||||||
|
try {
|
||||||
|
const serviceInstance = reminderService.getInstance();
|
||||||
|
const stats = serviceInstance.getStats();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: stats
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting reminder stats:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Interner Server-Fehler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/reminders/check-now - Manuelle Prüfung fälliger Erinnerungen
|
||||||
|
router.post('/check-now', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const serviceInstance = reminderService.getInstance();
|
||||||
|
await serviceInstance.manualCheck();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Manuelle Reminder-Prüfung durchgeführt'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during manual reminder check:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Interner Server-Fehler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -41,11 +41,13 @@ const adminRoutes = require('./routes/admin');
|
|||||||
const proposalRoutes = require('./routes/proposals');
|
const proposalRoutes = require('./routes/proposals');
|
||||||
const notificationRoutes = require('./routes/notifications');
|
const notificationRoutes = require('./routes/notifications');
|
||||||
const notificationService = require('./services/notificationService');
|
const notificationService = require('./services/notificationService');
|
||||||
|
const reminderService = require('./services/reminderService');
|
||||||
const gitRoutes = require('./routes/git');
|
const gitRoutes = require('./routes/git');
|
||||||
const applicationsRoutes = require('./routes/applications');
|
const applicationsRoutes = require('./routes/applications');
|
||||||
const giteaRoutes = require('./routes/gitea');
|
const giteaRoutes = require('./routes/gitea');
|
||||||
const knowledgeRoutes = require('./routes/knowledge');
|
const knowledgeRoutes = require('./routes/knowledge');
|
||||||
const codingRoutes = require('./routes/coding');
|
const codingRoutes = require('./routes/coding');
|
||||||
|
const reminderRoutes = require('./routes/reminders');
|
||||||
|
|
||||||
// Express App erstellen
|
// Express App erstellen
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -106,8 +108,17 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Statische Dateien (Frontend)
|
// Statische Dateien (Frontend) - ohne Caching für Development
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public'), {
|
||||||
|
etag: false,
|
||||||
|
lastModified: false,
|
||||||
|
cacheControl: false,
|
||||||
|
setHeaders: (res, path) => {
|
||||||
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// Uploads-Ordner
|
// Uploads-Ordner
|
||||||
app.use('/uploads', authenticateToken, express.static(process.env.UPLOAD_DIR || path.join(__dirname, 'uploads')));
|
app.use('/uploads', authenticateToken, express.static(process.env.UPLOAD_DIR || path.join(__dirname, 'uploads')));
|
||||||
@ -160,6 +171,12 @@ app.use('/api/knowledge', authenticateToken, csrfProtection, knowledgeRoutes);
|
|||||||
// Coding-Routes (Entwicklungsverzeichnisse mit Claude/Codex)
|
// Coding-Routes (Entwicklungsverzeichnisse mit Claude/Codex)
|
||||||
app.use('/api/coding', authenticateToken, csrfProtection, codingRoutes);
|
app.use('/api/coding', authenticateToken, csrfProtection, codingRoutes);
|
||||||
|
|
||||||
|
// Reminder-Routes (Erinnerungen)
|
||||||
|
app.use('/api/reminders', authenticateToken, csrfProtection, reminderRoutes);
|
||||||
|
|
||||||
|
// Contacts-Routes (Kontakte)
|
||||||
|
app.use('/api/contacts', authenticateToken, csrfProtection, require('./routes/contacts'));
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// SOCKET.IO
|
// SOCKET.IO
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@ -296,6 +313,10 @@ database.initialize()
|
|||||||
notificationService.checkDueTasks(io);
|
notificationService.checkDueTasks(io);
|
||||||
logger.info('Fälligkeits-Check für Benachrichtigungen gestartet');
|
logger.info('Fälligkeits-Check für Benachrichtigungen gestartet');
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
|
// Reminder Service starten
|
||||||
|
const reminderServiceInstance = reminderService.getInstance(io);
|
||||||
|
reminderServiceInstance.start();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -306,6 +327,11 @@ database.initialize()
|
|||||||
// Graceful Shutdown
|
// Graceful Shutdown
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
logger.info('SIGTERM empfangen, fahre herunter...');
|
logger.info('SIGTERM empfangen, fahre herunter...');
|
||||||
|
|
||||||
|
// Reminder Service stoppen
|
||||||
|
const reminderServiceInstance = reminderService.getInstance();
|
||||||
|
reminderServiceInstance.stop();
|
||||||
|
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
database.close();
|
database.close();
|
||||||
logger.info('Server beendet');
|
logger.info('Server beendet');
|
||||||
@ -315,6 +341,11 @@ process.on('SIGTERM', () => {
|
|||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
logger.info('SIGINT empfangen, fahre herunter...');
|
logger.info('SIGINT empfangen, fahre herunter...');
|
||||||
|
|
||||||
|
// Reminder Service stoppen
|
||||||
|
const reminderServiceInstance = reminderService.getInstance();
|
||||||
|
reminderServiceInstance.stop();
|
||||||
|
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
database.close();
|
database.close();
|
||||||
logger.info('Server beendet');
|
logger.info('Server beendet');
|
||||||
|
|||||||
@ -47,6 +47,10 @@ const NOTIFICATION_TYPES = {
|
|||||||
title: (data) => 'Genehmigung erforderlich',
|
title: (data) => 'Genehmigung erforderlich',
|
||||||
message: (data) => `Neue Genehmigung: "${data.proposalTitle}"`
|
message: (data) => `Neue Genehmigung: "${data.proposalTitle}"`
|
||||||
},
|
},
|
||||||
|
'reminder:due': {
|
||||||
|
title: (data) => 'Erinnerung',
|
||||||
|
message: (data) => `${data.reminderTitle} - ${data.daysAdvance === '0' ? 'Heute' : `in ${data.daysAdvance} Tag${data.daysAdvance > 1 ? 'en' : ''}`}`
|
||||||
|
},
|
||||||
'approval:granted': {
|
'approval:granted': {
|
||||||
title: (data) => 'Genehmigung erteilt',
|
title: (data) => 'Genehmigung erteilt',
|
||||||
message: (data) => `"${data.proposalTitle}" wurde genehmigt`
|
message: (data) => `"${data.proposalTitle}" wurde genehmigt`
|
||||||
@ -284,6 +288,24 @@ const notificationService = {
|
|||||||
if (result) results.push(result);
|
if (result) results.push(result);
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reminder-Benachrichtigung erstellen
|
||||||
|
*/
|
||||||
|
createReminderNotification(reminder, daysAdvance, io) {
|
||||||
|
return this.create(
|
||||||
|
reminder.created_by,
|
||||||
|
'reminder:due',
|
||||||
|
{
|
||||||
|
reminderTitle: reminder.title,
|
||||||
|
daysAdvance: daysAdvance.toString(),
|
||||||
|
projectId: reminder.project_id,
|
||||||
|
reminderId: reminder.id
|
||||||
|
},
|
||||||
|
io,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
242
backend/services/reminderService.js
Normale Datei
242
backend/services/reminderService.js
Normale Datei
@ -0,0 +1,242 @@
|
|||||||
|
/**
|
||||||
|
* TASKMATE - Reminder Service
|
||||||
|
* ===========================
|
||||||
|
* Service für Erinnerungsbenachrichtigungen und Scheduling
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { getDb } = require('../database');
|
||||||
|
const notificationService = require('./notificationService');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
|
class ReminderService {
|
||||||
|
constructor(io = null) {
|
||||||
|
this.io = io;
|
||||||
|
this.intervalId = null;
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet den Reminder-Check-Service
|
||||||
|
* Läuft alle 5 Minuten (kann für Produktion auf 1 Stunde erhöht werden)
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
logger.warn('Reminder Service ist bereits gestartet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
// Sofort prüfen
|
||||||
|
this.checkDueReminders();
|
||||||
|
|
||||||
|
// Dann alle 5 Minuten (300000 ms)
|
||||||
|
// In Produktion könnte das auf 1 Stunde (3600000 ms) erhöht werden
|
||||||
|
this.intervalId = setInterval(() => {
|
||||||
|
this.checkDueReminders();
|
||||||
|
}, 300000); // 5 Minuten
|
||||||
|
|
||||||
|
logger.info('Reminder Service gestartet - prüft alle 5 Minuten');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stoppt den Reminder-Check-Service
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRunning = false;
|
||||||
|
logger.info('Reminder Service gestoppt');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft fällige Erinnerungen und sendet Benachrichtigungen
|
||||||
|
*/
|
||||||
|
async checkDueReminders() {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Finde alle fälligen Benachrichtigungen die noch nicht gesendet wurden
|
||||||
|
const dueNotifications = db.prepare(`
|
||||||
|
SELECT
|
||||||
|
rn.*,
|
||||||
|
r.id as reminder_id,
|
||||||
|
r.title as reminder_title,
|
||||||
|
r.description as reminder_description,
|
||||||
|
r.reminder_date,
|
||||||
|
r.reminder_time,
|
||||||
|
r.color,
|
||||||
|
r.project_id,
|
||||||
|
r.created_by,
|
||||||
|
r.advance_days
|
||||||
|
FROM reminder_notifications rn
|
||||||
|
JOIN reminders r ON rn.reminder_id = r.id
|
||||||
|
WHERE rn.notification_date <= ?
|
||||||
|
AND rn.sent = 0
|
||||||
|
AND r.is_active = 1
|
||||||
|
ORDER BY rn.notification_date ASC, r.reminder_time ASC
|
||||||
|
`).all(today);
|
||||||
|
|
||||||
|
if (dueNotifications.length === 0) {
|
||||||
|
logger.debug('Keine fälligen Erinnerungen gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${dueNotifications.length} fällige Erinnerung(en) gefunden`);
|
||||||
|
|
||||||
|
// Verarbeite jede fällige Benachrichtigung
|
||||||
|
for (const notification of dueNotifications) {
|
||||||
|
await this.processReminderNotification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Fehler beim Prüfen fälliger Erinnerungen:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet eine einzelne fällige Erinnerungs-Benachrichtigung
|
||||||
|
*/
|
||||||
|
async processReminderNotification(notification) {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
// Berechne wie viele Tage im Voraus diese Benachrichtigung ist
|
||||||
|
const reminderDate = new Date(notification.reminder_date);
|
||||||
|
const notificationDate = new Date(notification.notification_date);
|
||||||
|
const daysDiff = Math.ceil((reminderDate - notificationDate) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
const reminder = {
|
||||||
|
id: notification.reminder_id,
|
||||||
|
title: notification.reminder_title,
|
||||||
|
description: notification.reminder_description,
|
||||||
|
project_id: notification.project_id,
|
||||||
|
created_by: notification.created_by,
|
||||||
|
color: notification.color
|
||||||
|
};
|
||||||
|
|
||||||
|
// Erstelle Benachrichtigung
|
||||||
|
const createdNotification = notificationService.createReminderNotification(
|
||||||
|
reminder,
|
||||||
|
daysDiff,
|
||||||
|
this.io
|
||||||
|
);
|
||||||
|
|
||||||
|
if (createdNotification) {
|
||||||
|
// Markiere als gesendet
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE reminder_notifications
|
||||||
|
SET sent = 1
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(notification.id);
|
||||||
|
|
||||||
|
logger.info(`Reminder-Benachrichtigung gesendet: "${notification.reminder_title}" (${daysDiff} Tage vorher)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Fehler beim Verarbeiten der Reminder-Benachrichtigung ${notification.id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt Benachrichtigungstermine für eine neue Erinnerung
|
||||||
|
*/
|
||||||
|
createNotificationSchedule(reminderId, reminderDate, advanceDays) {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const baseDate = new Date(reminderDate);
|
||||||
|
|
||||||
|
// Lösche alte Termine falls vorhanden
|
||||||
|
db.prepare('DELETE FROM reminder_notifications WHERE reminder_id = ?').run(reminderId);
|
||||||
|
|
||||||
|
// Erstelle neue Termine für jeden advance day
|
||||||
|
advanceDays.forEach(days => {
|
||||||
|
const notificationDate = new Date(baseDate);
|
||||||
|
notificationDate.setDate(notificationDate.getDate() - parseInt(days));
|
||||||
|
|
||||||
|
const notificationDateStr = notificationDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Nur zukünftige Termine erstellen
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
if (notificationDateStr >= today) {
|
||||||
|
db.prepare(`
|
||||||
|
INSERT OR IGNORE INTO reminder_notifications (reminder_id, notification_date)
|
||||||
|
VALUES (?, ?)
|
||||||
|
`).run(reminderId, notificationDateStr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(`Benachrichtigungstermine erstellt für Reminder ${reminderId}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Fehler beim Erstellen der Benachrichtigungstermine für Reminder ${reminderId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manuelle Prüfung für API-Endpoint
|
||||||
|
*/
|
||||||
|
async manualCheck() {
|
||||||
|
logger.info('Manuelle Reminder-Prüfung ausgelöst');
|
||||||
|
return await this.checkDueReminders();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistiken für Debugging
|
||||||
|
*/
|
||||||
|
getStats() {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
isRunning: this.isRunning,
|
||||||
|
activeReminders: db.prepare('SELECT COUNT(*) as count FROM reminders WHERE is_active = 1').get().count,
|
||||||
|
pendingNotifications: db.prepare('SELECT COUNT(*) as count FROM reminder_notifications WHERE sent = 0').get().count,
|
||||||
|
nextDueDate: db.prepare(`
|
||||||
|
SELECT MIN(notification_date) as next_date
|
||||||
|
FROM reminder_notifications
|
||||||
|
WHERE sent = 0 AND notification_date >= date('now')
|
||||||
|
`).get().next_date
|
||||||
|
};
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Fehler beim Abrufen der Reminder-Statistiken:', error);
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socket.io Instanz setzen/aktualisieren
|
||||||
|
*/
|
||||||
|
setSocketIO(io) {
|
||||||
|
this.io = io;
|
||||||
|
logger.debug('Socket.IO Instanz für Reminder Service aktualisiert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton Export
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getInstance(io = null) {
|
||||||
|
if (!instance) {
|
||||||
|
instance = new ReminderService(io);
|
||||||
|
} else if (io) {
|
||||||
|
instance.setSocketIO(io);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Für Tests und Debugging
|
||||||
|
createInstance(io = null) {
|
||||||
|
return new ReminderService(io);
|
||||||
|
}
|
||||||
|
};
|
||||||
BIN
data/taskmate.db
BIN
data/taskmate.db
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
43
fix_passwords.js
Normale Datei
43
fix_passwords.js
Normale Datei
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Password Fix Script
|
||||||
|
* Setzt die Passwort-Hashes für alle Benutzer zurück
|
||||||
|
*/
|
||||||
|
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
async function fixPasswords() {
|
||||||
|
const db = new Database(path.join(__dirname, 'data/taskmate.db'));
|
||||||
|
|
||||||
|
console.log('Setze Passwort-Hashes zurück...');
|
||||||
|
|
||||||
|
// Standard-Passwörter
|
||||||
|
const passwords = {
|
||||||
|
'admin': 'admin123',
|
||||||
|
'hendrik_gebhardt@gmx.de': 'Hzfne313!fdEF34',
|
||||||
|
'momohomma@googlemail.com': 'Hzfne313!fdEF34'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [username, password] of Object.entries(passwords)) {
|
||||||
|
const hash = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
|
// Update basierend auf E-Mail oder Username
|
||||||
|
const result = db.prepare(`
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = ?, failed_attempts = 0, locked_until = NULL
|
||||||
|
WHERE email = ? OR username = ?
|
||||||
|
`).run(hash, username, username);
|
||||||
|
|
||||||
|
if (result.changes > 0) {
|
||||||
|
console.log(`✅ Passwort für ${username} aktualisiert`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Benutzer ${username} nicht gefunden`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
console.log('Passwort-Fix abgeschlossen!');
|
||||||
|
}
|
||||||
|
|
||||||
|
fixPasswords().catch(console.error);
|
||||||
@ -604,13 +604,13 @@
|
|||||||
.column-header {
|
.column-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
padding: var(--spacing-3);
|
padding: var(--spacing-3);
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header:active {
|
.column-header:active {
|
||||||
@ -631,6 +631,8 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
flex: 1;
|
||||||
|
padding-right: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-count {
|
.column-count {
|
||||||
@ -652,6 +654,9 @@
|
|||||||
gap: var(--spacing-1);
|
gap: var(--spacing-1);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity var(--transition-fast);
|
transition: opacity var(--transition-fast);
|
||||||
|
position: absolute;
|
||||||
|
top: var(--spacing-3);
|
||||||
|
right: var(--spacing-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-actions .btn-icon {
|
.column-actions .btn-icon {
|
||||||
|
|||||||
@ -586,34 +586,46 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Week view multi-day task range styles */
|
/* Week view multi-day task range styles - continuous bars like month view */
|
||||||
.calendar-week-task.has-range {
|
.calendar-week-task.has-range {
|
||||||
|
border-radius: 0;
|
||||||
|
/* Extend beyond cell boundaries for seamless connection */
|
||||||
|
margin-left: calc(-1 * var(--spacing-3));
|
||||||
|
margin-right: calc(-1 * var(--spacing-3));
|
||||||
|
padding: 4px var(--spacing-3);
|
||||||
|
/* Fixed height for consistent bar thickness */
|
||||||
|
height: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
line-height: 16px;
|
||||||
|
/* Transparent border for middle/end - color set via inline style on start */
|
||||||
|
border-left: 3px solid transparent;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-week-task.has-range.range-start::after {
|
.calendar-week-task.has-range.range-start {
|
||||||
content: '▶';
|
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
||||||
position: absolute;
|
/* Start doesn't extend left into previous cell */
|
||||||
right: 8px;
|
margin-left: 0;
|
||||||
top: 50%;
|
/* Compensate for width difference */
|
||||||
transform: translateY(-50%);
|
padding-left: calc(var(--spacing-3) + 3px);
|
||||||
font-size: 8px;
|
/* Border color set via inline style */
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-week-task.has-range.range-end::after {
|
.calendar-week-task.has-range.range-end {
|
||||||
content: '◀';
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||||
position: absolute;
|
/* End doesn't extend right into next cell */
|
||||||
left: 28px;
|
margin-right: 0;
|
||||||
top: 50%;
|
padding-right: calc(var(--spacing-3) + 3px);
|
||||||
transform: translateY(-50%);
|
|
||||||
font-size: 8px;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-week-task.has-range.range-middle {
|
.calendar-week-task.has-range.range-middle:empty {
|
||||||
opacity: 0.7;
|
/* Hide empty middle segments that have no content */
|
||||||
background: var(--bg-hover);
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-week-task.has-range.range-middle:not(:empty) {
|
||||||
|
/* Show middle segments that have content (first day of week) */
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Week Add Task Button */
|
/* Week Add Task Button */
|
||||||
@ -635,6 +647,42 @@
|
|||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
REMINDER ITEMS
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.calendar-reminder-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-reminder-bell {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Week view reminder styling */
|
||||||
|
.calendar-week-task.calendar-reminder-item {
|
||||||
|
padding-right: 20px; /* Space for bell icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-week-task.calendar-reminder-item .calendar-reminder-bell {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Month view reminder styling */
|
||||||
|
.calendar-task.calendar-reminder-item {
|
||||||
|
padding-right: 16px; /* Space for bell icon */
|
||||||
|
}
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
DAY DETAIL POPUP
|
DAY DETAIL POPUP
|
||||||
======================================== */
|
======================================== */
|
||||||
|
|||||||
283
frontend/css/contacts.css
Normale Datei
283
frontend/css/contacts.css
Normale Datei
@ -0,0 +1,283 @@
|
|||||||
|
/**
|
||||||
|
* TASKMATE - Contacts Styles
|
||||||
|
* ==========================
|
||||||
|
* Kartenansicht für Kontakte
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
HEADER & CONTROLS
|
||||||
|
============================================================================= */
|
||||||
|
|
||||||
|
.contacts-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-actions {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card:hover .contact-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-actions .btn-icon {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-actions .btn-icon:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
CONTACT INFO
|
||||||
|
============================================================================= */
|
||||||
|
|
||||||
|
.contact-card-body {
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 var(--space-xs);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-company {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-position {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-email,
|
||||||
|
.contact-phone,
|
||||||
|
.contact-mobile {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-email i,
|
||||||
|
.contact-phone i,
|
||||||
|
.contact-mobile i {
|
||||||
|
width: 14px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-email:hover,
|
||||||
|
.contact-phone:hover,
|
||||||
|
.contact-mobile:hover {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
TAGS
|
||||||
|
============================================================================= */
|
||||||
|
|
||||||
|
.contact-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-tag {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
EMPTY STATE
|
||||||
|
============================================================================= */
|
||||||
|
|
||||||
|
.contacts-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-xl) var(--space-md);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-empty i {
|
||||||
|
font-size: 48px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-empty h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-empty p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
CONTACT MODAL
|
||||||
|
============================================================================= */
|
||||||
|
|
||||||
|
#contact-form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact-form .form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact-form .form-group.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact-form .tags-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact-form .tags-input-wrapper input {
|
||||||
|
padding-right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact-form .tags-help {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
RESPONSIVE
|
||||||
|
============================================================================= */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.contacts-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-search {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contacts-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions-left {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,11 +21,12 @@
|
|||||||
|
|
||||||
.knowledge-layout {
|
.knowledge-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 280px 1fr;
|
grid-template-columns: 450px 1fr;
|
||||||
gap: var(--spacing-lg);
|
gap: var(--spacing-lg);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
@ -39,6 +40,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-sidebar-header {
|
.knowledge-sidebar-header {
|
||||||
@ -667,7 +669,7 @@
|
|||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.knowledge-layout {
|
.knowledge-layout {
|
||||||
grid-template-columns: 220px 1fr;
|
grid-template-columns: 450px 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -851,3 +853,46 @@
|
|||||||
.icon-grid::-webkit-scrollbar-thumb:hover {
|
.icon-grid::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--text-secondary);
|
background: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
SIDEBAR RESIZE HANDLE
|
||||||
|
========================================== */
|
||||||
|
|
||||||
|
.knowledge-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -4px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 8px;
|
||||||
|
cursor: col-resize;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-resize-handle::before {
|
||||||
|
content: '';
|
||||||
|
width: 1px;
|
||||||
|
height: 30px;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-resize-handle:hover::before {
|
||||||
|
background: var(--border-color);
|
||||||
|
width: 2px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-resize-handle.dragging::before {
|
||||||
|
background: var(--primary);
|
||||||
|
width: 3px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-layout.resizing {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|||||||
654
frontend/css/reminders.css
Normale Datei
654
frontend/css/reminders.css
Normale Datei
@ -0,0 +1,654 @@
|
|||||||
|
/**
|
||||||
|
* TASKMATE - Reminders CSS
|
||||||
|
* =========================
|
||||||
|
* Styling für Erinnerungen
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* =====================
|
||||||
|
REMINDER MODAL
|
||||||
|
===================== */
|
||||||
|
|
||||||
|
#reminder-modal .modal-content {
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Actions with Delete Button */
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions-left {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-reminder {
|
||||||
|
/* Lösch-Button rechts */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kompakte Color Picker */
|
||||||
|
.color-picker-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-trigger {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--border-default);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-trigger:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-icon {
|
||||||
|
color: white;
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 8px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-dropdown.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-dropdown .color-option {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-dropdown .color-option:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
border-color: var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-dropdown .color-option.selected {
|
||||||
|
border-color: var(--text-primary);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-dropdown .color-option.selected::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Advance Options */
|
||||||
|
.advance-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 28px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label .checkmark {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 2px solid #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label:hover .checkmark {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input:checked ~ .checkmark {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label .checkmark::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
left: 5px;
|
||||||
|
top: 2px;
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input:checked ~ .checkmark::after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================
|
||||||
|
CUSTOM SELECT (USER DROPDOWN)
|
||||||
|
===================== */
|
||||||
|
|
||||||
|
.custom-select {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-trigger:hover {
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-trigger.active {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-arrow {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select.open .custom-select-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-options {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
z-index: 1000;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select.open .custom-select-options {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-option:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-option.selected {
|
||||||
|
background: var(--primary-light);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-avatar {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-user-avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================
|
||||||
|
MODAL HEADER FIX
|
||||||
|
===================== */
|
||||||
|
|
||||||
|
#reminder-modal .modal-header h3 {
|
||||||
|
color: #000000 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
#reminder-modal .modal-header h3:hover {
|
||||||
|
color: #000000 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reminder-modal .modal-header {
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verhindere Orange-Hover-Effekte auf Modal-Inhalten */
|
||||||
|
#reminder-modal * {
|
||||||
|
transition: background-color 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reminder-modal *:not(.btn):not(.custom-select-option):hover {
|
||||||
|
background: transparent !important;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stelle sicher, dass Text im Modal immer lesbar bleibt */
|
||||||
|
#reminder-modal .modal-content {
|
||||||
|
background: var(--bg-card) !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reminder-modal label,
|
||||||
|
#reminder-modal .form-control,
|
||||||
|
#reminder-modal input,
|
||||||
|
#reminder-modal textarea,
|
||||||
|
#reminder-modal p,
|
||||||
|
#reminder-modal span:not(.btn):not(.option-avatar):not(.selected-user-avatar) {
|
||||||
|
color: #000000 !important;
|
||||||
|
background: var(--bg-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reminder-modal .form-group label {
|
||||||
|
color: #000000 !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reminder-modal .checkbox-label {
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================
|
||||||
|
MODAL BUTTONS
|
||||||
|
===================== */
|
||||||
|
|
||||||
|
#reminder-modal .btn-primary {
|
||||||
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||||
|
color: white !important;
|
||||||
|
border: 1px solid #3b82f6;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reminder-modal .btn-primary:hover,
|
||||||
|
#reminder-modal .btn-primary:focus {
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||||
|
border-color: #1d4ed8;
|
||||||
|
color: white !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 3px 8px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#reminder-modal .btn-secondary {
|
||||||
|
background: #6b7280;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reminder-modal .btn-secondary:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
border-color: #4b5563;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================
|
||||||
|
CALENDAR INTEGRATION
|
||||||
|
===================== */
|
||||||
|
|
||||||
|
/* Reminder Button in Calendar Toolbar */
|
||||||
|
.btn-reminder {
|
||||||
|
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||||
|
color: white !important;
|
||||||
|
border: 2px solid #ea580c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reminder:hover,
|
||||||
|
.btn-reminder:focus {
|
||||||
|
background: linear-gradient(135deg, #ea580c 0%, #c2410c 100%);
|
||||||
|
color: white !important;
|
||||||
|
border-color: #c2410c;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(234, 88, 12, 0.4);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reminder:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 6px rgba(234, 88, 12, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reminder .icon {
|
||||||
|
animation: reminderBell 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes reminderBell {
|
||||||
|
0%, 50%, 100% { transform: rotate(0deg); }
|
||||||
|
10%, 30% { transform: rotate(-10deg); }
|
||||||
|
20%, 40% { transform: rotate(10deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reminder Icons in Calendar - DEPRECATED (reminders now shown as tasks) */
|
||||||
|
/*
|
||||||
|
.calendar-reminder {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--reminder-color, #F59E0B);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
color: white;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-reminder::before {
|
||||||
|
content: '🔔';
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-reminder:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Day Detail Popup - Reminder Button */
|
||||||
|
.calendar-day-detail .btn:not(.btn-primary) {
|
||||||
|
background: linear-gradient(135deg, #6B7280 0%, #4B5563 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reminder Detail Popup */
|
||||||
|
.calendar-reminder-detail {
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-reminders {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin: var(--spacing-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-reminder {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid var(--reminder-color, #F59E0B);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-content {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 0; /* Allows text truncation */
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-reminder:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reminder Delete Button */
|
||||||
|
.reminder-delete-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-delete-btn:hover {
|
||||||
|
background: var(--error-bg);
|
||||||
|
border-color: var(--error);
|
||||||
|
color: var(--error);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-delete-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-reminder .reminder-time {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--reminder-color, #F59E0B);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-reminder .reminder-title {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail-reminder .reminder-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#reminder-modal .modal-content {
|
||||||
|
max-width: none;
|
||||||
|
width: 95vw;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advance-options {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
font-size: 16px;
|
||||||
|
padding-left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label .checkmark {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================
|
||||||
|
EINHEITLICHE CHECKBOX STYLES
|
||||||
|
===================== */
|
||||||
|
|
||||||
|
/* ALTERNATIVER ANSATZ: Direktes Checkbox-Styling */
|
||||||
|
.advance-options .checklist-item {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 12px !important;
|
||||||
|
padding: 8px 0 !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advance-options input[type="checkbox"] {
|
||||||
|
width: 20px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
appearance: none !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
-moz-appearance: none !important;
|
||||||
|
border: 2px solid #d1d5db !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
background-color: white !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
position: relative !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advance-options input[type="checkbox"]:checked {
|
||||||
|
background-color: #3B82F6 !important;
|
||||||
|
border-color: #3B82F6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advance-options input[type="checkbox"]:checked::after {
|
||||||
|
content: '✓' !important;
|
||||||
|
position: absolute !important;
|
||||||
|
top: -3px !important;
|
||||||
|
left: 2px !important;
|
||||||
|
color: white !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
line-height: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advance-options .checklist-text {
|
||||||
|
font-size: 14px !important;
|
||||||
|
color: #374151 !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verstecke die fake checkbox spans */
|
||||||
|
.advance-options .checklist-checkbox {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.checkbox-label .checkmark {
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
border-color: var(--border-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-reminder {
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,6 +27,8 @@
|
|||||||
<link rel="stylesheet" href="css/gitea.css">
|
<link rel="stylesheet" href="css/gitea.css">
|
||||||
<link rel="stylesheet" href="css/coding.css">
|
<link rel="stylesheet" href="css/coding.css">
|
||||||
<link rel="stylesheet" href="css/knowledge.css">
|
<link rel="stylesheet" href="css/knowledge.css">
|
||||||
|
<link rel="stylesheet" href="css/reminders.css">
|
||||||
|
<link rel="stylesheet" href="css/contacts.css">
|
||||||
<link rel="stylesheet" href="css/responsive.css">
|
<link rel="stylesheet" href="css/responsive.css">
|
||||||
<link rel="stylesheet" href="css/mobile.css">
|
<link rel="stylesheet" href="css/mobile.css">
|
||||||
|
|
||||||
@ -43,7 +45,7 @@
|
|||||||
<p>Melden Sie sich an, um fortzufahren</p>
|
<p>Melden Sie sich an, um fortzufahren</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="login-form" class="login-form">
|
<form id="login-form" class="login-form" method="POST" action="/api/auth/login" onsubmit="return false;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="login-username">E-Mail</label>
|
<label for="login-username">E-Mail</label>
|
||||||
<input type="text" id="login-username" name="username" required autocomplete="email" autofocus placeholder="benutzer@beispiel.de">
|
<input type="text" id="login-username" name="username" required autocomplete="email" autofocus placeholder="benutzer@beispiel.de">
|
||||||
@ -177,6 +179,7 @@
|
|||||||
<button class="view-tab" data-view="proposals">Genehmigung</button>
|
<button class="view-tab" data-view="proposals">Genehmigung</button>
|
||||||
<button class="view-tab" data-view="coding">Coding</button>
|
<button class="view-tab" data-view="coding">Coding</button>
|
||||||
<button class="view-tab" data-view="knowledge">Wissen</button>
|
<button class="view-tab" data-view="knowledge">Wissen</button>
|
||||||
|
<button class="view-tab" data-view="contacts">Kontakte</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -402,6 +405,10 @@
|
|||||||
<!-- Dynamisch gefüllt -->
|
<!-- Dynamisch gefüllt -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="btn-new-reminder" class="btn btn-secondary btn-reminder" title="Erinnerung hinzufügen">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24"><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="currentColor" stroke-width="2" fill="none"/></svg>
|
||||||
|
Erinnerung
|
||||||
|
</button>
|
||||||
<button id="btn-today" class="btn btn-secondary">Heute</button>
|
<button id="btn-today" class="btn btn-secondary">Heute</button>
|
||||||
<div class="calendar-view-toggle">
|
<div class="calendar-view-toggle">
|
||||||
<button class="btn btn-toggle active" data-calendar-view="month">Monat</button>
|
<button class="btn btn-toggle active" data-calendar-view="month">Monat</button>
|
||||||
@ -504,6 +511,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<p>Keine Kategorien</p>
|
<p>Keine Kategorien</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Resize Handle -->
|
||||||
|
<div id="knowledge-resize-handle" class="knowledge-resize-handle"></div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Hauptbereich (rechts) - Einträge -->
|
<!-- Hauptbereich (rechts) - Einträge -->
|
||||||
@ -568,6 +577,47 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Contacts View -->
|
||||||
|
<div id="view-contacts" class="view view-contacts hidden">
|
||||||
|
<div class="contacts-header">
|
||||||
|
<h2>Kontakte</h2>
|
||||||
|
<button id="btn-new-contact" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
Neuer Kontakt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contacts-controls">
|
||||||
|
<div class="contacts-filters">
|
||||||
|
<select id="contacts-tag-filter">
|
||||||
|
<option value="">Alle Tags</option>
|
||||||
|
</select>
|
||||||
|
<select id="contacts-sort">
|
||||||
|
<option value="created_at-desc">Neueste zuerst</option>
|
||||||
|
<option value="created_at-asc">Älteste zuerst</option>
|
||||||
|
<option value="name-asc">Name (A-Z)</option>
|
||||||
|
<option value="name-desc">Name (Z-A)</option>
|
||||||
|
<option value="company-asc">Firma (A-Z)</option>
|
||||||
|
<option value="company-desc">Firma (Z-A)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="contacts-grid" class="contacts-grid">
|
||||||
|
<!-- Contact cards will be rendered here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="contacts-empty" class="contacts-empty hidden">
|
||||||
|
<i class="fas fa-address-book"></i>
|
||||||
|
<h3>Keine Kontakte vorhanden</h3>
|
||||||
|
<p>Erstellen Sie Ihren ersten Kontakt.</p>
|
||||||
|
<button class="btn btn-primary" onclick="document.getElementById('btn-new-contact').click()">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
Ersten Kontakt erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1579,6 +1629,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Modal -->
|
||||||
|
<div id="contact-modal" class="modal modal-medium hidden">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="contact-modal-title">Neuer Kontakt</h2>
|
||||||
|
<button class="modal-close" data-close-modal>×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="contact-form">
|
||||||
|
<input type="hidden" id="contact-id" />
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-first-name">Vorname</label>
|
||||||
|
<input type="text" id="contact-first-name" placeholder="Max" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-last-name">Nachname</label>
|
||||||
|
<input type="text" id="contact-last-name" placeholder="Mustermann" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-company">Firma</label>
|
||||||
|
<input type="text" id="contact-company" placeholder="Musterfirma GmbH" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-position">Position</label>
|
||||||
|
<input type="text" id="contact-position" placeholder="Geschäftsführer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-email">E-Mail</label>
|
||||||
|
<input type="email" id="contact-email" placeholder="max@musterfirma.de" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-website">Website</label>
|
||||||
|
<input type="url" id="contact-website" placeholder="https://www.musterfirma.de" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-phone">Telefon</label>
|
||||||
|
<input type="tel" id="contact-phone" placeholder="+49 30 12345678" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-mobile">Mobil</label>
|
||||||
|
<input type="tel" id="contact-mobile" placeholder="+49 170 1234567" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="contact-address">Adresse</label>
|
||||||
|
<input type="text" id="contact-address" placeholder="Musterstraße 123" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-postal-code">PLZ</label>
|
||||||
|
<input type="text" id="contact-postal-code" placeholder="12345" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-city">Stadt</label>
|
||||||
|
<input type="text" id="contact-city" placeholder="Berlin" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contact-country">Land</label>
|
||||||
|
<input type="text" id="contact-country" placeholder="Deutschland" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="contact-notes">Notizen</label>
|
||||||
|
<textarea id="contact-notes" rows="3" placeholder="Zusätzliche Informationen..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="contact-tags">Tags</label>
|
||||||
|
<input type="text" id="contact-tags" placeholder="Kunde, Partner, Lieferant" />
|
||||||
|
<div class="tags-help">Mehrere Tags mit Komma trennen</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" id="btn-delete-contact" class="btn btn-danger hidden">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary modal-cancel">Abbrechen</button>
|
||||||
|
<button type="submit" form="contact-form" class="btn btn-primary">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div id="toast-container" class="toast-container"></div>
|
<div id="toast-container" class="toast-container"></div>
|
||||||
|
|
||||||
@ -1627,6 +1774,10 @@
|
|||||||
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" fill="none"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" fill="none"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||||
<span>Wissen</span>
|
<span>Wissen</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="mobile-nav-item" data-view="contacts">
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="8.5" cy="7" r="4" stroke="currentColor" stroke-width="2" fill="none"/><line x1="20" y1="8" x2="20" y2="14" stroke="currentColor" stroke-width="2"/><line x1="23" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="2"/></svg>
|
||||||
|
<span>Kontakte</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1692,11 +1843,231 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Reminder Modal -->
|
||||||
|
<div id="reminder-modal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="reminder-modal-title">Neue Erinnerung</h3>
|
||||||
|
<button class="modal-close" aria-label="Schließen">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="reminder-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reminder-title">Titel *</label>
|
||||||
|
<input type="text" id="reminder-title" class="form-control" required maxlength="255" placeholder="z.B. Meeting mit Kunde">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reminder-description">Beschreibung</label>
|
||||||
|
<textarea id="reminder-description" class="form-control" rows="3" placeholder="Optionale Beschreibung..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reminder-assignee">Zugewiesen an</label>
|
||||||
|
<div class="custom-select" id="reminder-assignee-wrapper">
|
||||||
|
<div class="custom-select-trigger" id="reminder-assignee-trigger">
|
||||||
|
<span class="custom-select-value">Alle Benutzer</span>
|
||||||
|
<svg class="custom-select-arrow" viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="custom-select-options" id="reminder-assignee-options">
|
||||||
|
<div class="custom-select-option" data-value="">
|
||||||
|
<span class="option-text">Alle Benutzer</span>
|
||||||
|
</div>
|
||||||
|
<!-- Wird dynamisch gefüllt -->
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="reminder-assignee" name="reminder-assignee" value="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reminder-date">Datum *</label>
|
||||||
|
<input type="date" id="reminder-date" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reminder-time">Uhrzeit</label>
|
||||||
|
<input type="time" id="reminder-time" class="form-control" value="09:00">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reminder-color">Farbe</label>
|
||||||
|
<div class="color-picker-wrapper">
|
||||||
|
<div class="color-picker-trigger" id="color-picker-trigger" style="background-color: #F59E0B">
|
||||||
|
<svg class="color-picker-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="color-picker-dropdown hidden" id="color-picker-dropdown">
|
||||||
|
<div class="color-option" data-color="#F59E0B" style="background: #F59E0B" title="Amber"></div>
|
||||||
|
<div class="color-option" data-color="#EF4444" style="background: #EF4444" title="Rot"></div>
|
||||||
|
<div class="color-option" data-color="#10B981" style="background: #10B981" title="Grün"></div>
|
||||||
|
<div class="color-option" data-color="#3B82F6" style="background: #3B82F6" title="Blau"></div>
|
||||||
|
<div class="color-option" data-color="#8B5CF6" style="background: #8B5CF6" title="Lila"></div>
|
||||||
|
<div class="color-option" data-color="#F97316" style="background: #F97316" title="Orange"></div>
|
||||||
|
<div class="color-option" data-color="#06B6D4" style="background: #06B6D4" title="Cyan"></div>
|
||||||
|
<div class="color-option" data-color="#84CC16" style="background: #84CC16" title="Lime"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="reminder-color" value="#F59E0B">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reminder-advance">Erinnerung</label>
|
||||||
|
<div class="advance-options">
|
||||||
|
<label class="checklist-item">
|
||||||
|
<input type="checkbox" name="advance-days" value="1" checked>
|
||||||
|
<span class="checklist-checkbox"></span>
|
||||||
|
<span class="checklist-text">1 Tag vorher</span>
|
||||||
|
</label>
|
||||||
|
<label class="checklist-item">
|
||||||
|
<input type="checkbox" name="advance-days" value="2">
|
||||||
|
<span class="checklist-checkbox"></span>
|
||||||
|
<span class="checklist-text">2 Tage vorher</span>
|
||||||
|
</label>
|
||||||
|
<label class="checklist-item">
|
||||||
|
<input type="checkbox" name="advance-days" value="3">
|
||||||
|
<span class="checklist-checkbox"></span>
|
||||||
|
<span class="checklist-text">3 Tage vorher</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<div class="form-actions-left">
|
||||||
|
<button type="button" class="btn btn-secondary" id="btn-cancel-reminder">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn btn-primary" id="btn-save-reminder">
|
||||||
|
<span class="btn-text">Speichern</span>
|
||||||
|
<span class="btn-loading hidden">Speichere...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-danger btn-delete-reminder hidden" id="btn-delete-reminder">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Socket.io Client -->
|
<!-- Socket.io Client -->
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
|
|
||||||
<!-- Main App (ES Module) -->
|
<!-- Main App (ES Module) -->
|
||||||
<script type="module" src="js/app.js"></script>
|
<script type="module" src="js/app.js"></script>
|
||||||
|
<script type="module" src="js/reminders.js"></script>
|
||||||
|
<script type="module" src="js/contacts.js"></script>
|
||||||
|
|
||||||
|
<!-- Emergency Login Handler -->
|
||||||
|
<script>
|
||||||
|
console.log('[Login] Fallback handler wird geladen...');
|
||||||
|
|
||||||
|
function setupLoginHandler() {
|
||||||
|
const loginForm = document.getElementById('login-form');
|
||||||
|
const submitButton = loginForm?.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
if (!loginForm) {
|
||||||
|
console.error('[Login] Login-Formular nicht gefunden!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Login] Login-Formular gefunden, Handler wird eingerichtet...');
|
||||||
|
|
||||||
|
// Force override - entferne alle existierenden Handler
|
||||||
|
const newForm = loginForm.cloneNode(true);
|
||||||
|
loginForm.parentNode.replaceChild(newForm, loginForm);
|
||||||
|
|
||||||
|
newForm.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
console.log('[Login] Submit Event ausgelöst');
|
||||||
|
|
||||||
|
const username = document.getElementById('login-username').value.trim();
|
||||||
|
const password = document.getElementById('login-password').value;
|
||||||
|
const errorEl = document.getElementById('login-error');
|
||||||
|
const newSubmitButton = newForm.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
console.log('[Login] Username:', username);
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
errorEl.textContent = 'Bitte Benutzername und Passwort eingeben';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (newSubmitButton) {
|
||||||
|
newSubmitButton.disabled = true;
|
||||||
|
newSubmitButton.textContent = 'Anmelden...';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[Login] Sende Login-Request...');
|
||||||
|
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('[Login] Response Status:', response.status);
|
||||||
|
console.log('[Login] Response Data:', data);
|
||||||
|
|
||||||
|
if (response.ok && data.token) {
|
||||||
|
console.log('[Login] Login erfolgreich!');
|
||||||
|
localStorage.setItem('auth_token', data.token);
|
||||||
|
localStorage.setItem('current_user', JSON.stringify(data.user));
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
console.log('[Login] Login fehlgeschlagen:', data.error);
|
||||||
|
errorEl.textContent = data.error || 'Anmeldung fehlgeschlagen';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Login] Netzwerkfehler:', err);
|
||||||
|
errorEl.textContent = 'Netzwerkfehler - bitte versuchen Sie es erneut';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
if (newSubmitButton) {
|
||||||
|
newSubmitButton.disabled = false;
|
||||||
|
newSubmitButton.textContent = 'Anmelden';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Additional click handler for button
|
||||||
|
const newSubmitButton = newForm.querySelector('button[type="submit"]');
|
||||||
|
if (newSubmitButton) {
|
||||||
|
newSubmitButton.addEventListener('click', function(e) {
|
||||||
|
console.log('[Login] Button clicked');
|
||||||
|
// Let form submit handle it
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Login] Handler eingerichtet');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try multiple times to ensure DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', setupLoginHandler);
|
||||||
|
} else {
|
||||||
|
setupLoginHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup after 1 second
|
||||||
|
setTimeout(setupLoginHandler, 1000);
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -645,6 +645,31 @@ class ApiClient {
|
|||||||
return token ? `${url}?token=${encodeURIComponent(token)}` : url;
|
return token ? `${url}?token=${encodeURIComponent(token)}` : url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// REMINDER ENDPOINTS
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
async getReminders(projectId) {
|
||||||
|
const response = await this.get(`/reminders?project_id=${projectId}`);
|
||||||
|
return response.data || response; // Extract data property or fallback to response
|
||||||
|
}
|
||||||
|
|
||||||
|
async createReminder(data) {
|
||||||
|
return this.post('/reminders', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateReminder(reminderId, data) {
|
||||||
|
return this.put(`/reminders/${reminderId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteReminder(reminderId) {
|
||||||
|
return this.delete(`/reminders/${reminderId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDueReminders() {
|
||||||
|
return this.get('/reminders/due/check');
|
||||||
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// LINK ENDPOINTS
|
// LINK ENDPOINTS
|
||||||
// =====================
|
// =====================
|
||||||
@ -1258,6 +1283,35 @@ class ApiClient {
|
|||||||
async validateCodingPath(path) {
|
async validateCodingPath(path) {
|
||||||
return this.post('/coding/validate-path', { path });
|
return this.post('/coding/validate-path', { path });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTACTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
async getContacts(params = {}) {
|
||||||
|
const queryString = new URLSearchParams(params).toString();
|
||||||
|
return this.get(`/contacts${queryString ? '?' + queryString : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContact(id) {
|
||||||
|
return this.get(`/contacts/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createContact(data) {
|
||||||
|
return this.post('/contacts', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateContact(id, data) {
|
||||||
|
return this.put(`/contacts/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteContact(id) {
|
||||||
|
return this.delete(`/contacts/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContactTags() {
|
||||||
|
return this.get('/contacts/tags/all');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom API Error Class
|
// Custom API Error Class
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import giteaManager from './gitea.js';
|
|||||||
import knowledgeManager from './knowledge.js';
|
import knowledgeManager from './knowledge.js';
|
||||||
import codingManager from './coding.js';
|
import codingManager from './coding.js';
|
||||||
import mobileManager from './mobile.js';
|
import mobileManager from './mobile.js';
|
||||||
|
import reminderManager from './reminders.js';
|
||||||
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
|
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
@ -152,15 +153,17 @@ class App {
|
|||||||
store.setCurrentProject(projectId);
|
store.setCurrentProject(projectId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [columns, tasks, labels] = await Promise.all([
|
const [columns, tasks, labels, reminders] = await Promise.all([
|
||||||
api.getColumns(projectId),
|
api.getColumns(projectId),
|
||||||
api.getTasks(projectId),
|
api.getTasks(projectId),
|
||||||
api.getLabels(projectId)
|
api.getLabels(projectId),
|
||||||
|
api.getReminders(projectId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
store.setColumns(columns);
|
store.setColumns(columns);
|
||||||
store.setTasks(tasks);
|
store.setTasks(tasks);
|
||||||
store.setLabels(labels);
|
store.setLabels(labels);
|
||||||
|
store.setReminders(reminders);
|
||||||
|
|
||||||
// Update project selector
|
// Update project selector
|
||||||
const projectSelect = $('#project-select');
|
const projectSelect = $('#project-select');
|
||||||
@ -667,6 +670,28 @@ class App {
|
|||||||
} else {
|
} else {
|
||||||
knowledgeManager.hide();
|
knowledgeManager.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize contacts view when switching to it
|
||||||
|
if (view === 'contacts') {
|
||||||
|
window.initContactsPromise = window.initContactsPromise || import('./contacts.js').then(module => {
|
||||||
|
if (module.initContacts) {
|
||||||
|
return module.initContacts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.initContactsPromise.catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render list view when switching to it (delayed to ensure store is updated)
|
||||||
|
if (view === 'list') {
|
||||||
|
setTimeout(() => listViewManager.render(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render calendar view when switching to it
|
||||||
|
if (view === 'calendar') {
|
||||||
|
setTimeout(() => {
|
||||||
|
calendarViewManager.render();
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
@ -887,6 +912,14 @@ class App {
|
|||||||
proposalsManager.setSearchQuery('');
|
proposalsManager.setSearchQuery('');
|
||||||
knowledgeManager.setSearchQuery('');
|
knowledgeManager.setSearchQuery('');
|
||||||
|
|
||||||
|
// Clear contacts search
|
||||||
|
import('./contacts.js').then(module => {
|
||||||
|
if (module.contactsManager) {
|
||||||
|
module.contactsManager.searchQuery = '';
|
||||||
|
module.contactsManager.filterContacts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Cancel any pending server search
|
// Cancel any pending server search
|
||||||
if (searchAbortController) {
|
if (searchAbortController) {
|
||||||
searchAbortController.abort();
|
searchAbortController.abort();
|
||||||
@ -961,6 +994,14 @@ class App {
|
|||||||
} else if (currentView === 'knowledge') {
|
} else if (currentView === 'knowledge') {
|
||||||
// Search knowledge base
|
// Search knowledge base
|
||||||
knowledgeManager.setSearchQuery(value);
|
knowledgeManager.setSearchQuery(value);
|
||||||
|
} else if (currentView === 'contacts') {
|
||||||
|
// Search contacts
|
||||||
|
import('./contacts.js').then(module => {
|
||||||
|
if (module.contactsManager) {
|
||||||
|
module.contactsManager.searchQuery = value;
|
||||||
|
module.contactsManager.filterContacts();
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Immediate client-side filtering for tasks
|
// Immediate client-side filtering for tasks
|
||||||
store.setFilter('search', value);
|
store.setFilter('search', value);
|
||||||
|
|||||||
@ -372,7 +372,14 @@ class BoardManager {
|
|||||||
const currentUser = users.find(u => u.id === assignee.id);
|
const currentUser = users.find(u => u.id === assignee.id);
|
||||||
const color = currentUser?.color || assignee.color || '#888';
|
const color = currentUser?.color || assignee.color || '#888';
|
||||||
const name = currentUser?.display_name || assignee.display_name || assignee.username || 'Benutzer';
|
const name = currentUser?.display_name || assignee.display_name || assignee.username || 'Benutzer';
|
||||||
const initials = currentUser?.initials || assignee.initials || getInitials(name);
|
|
||||||
|
// Initialen berechnen - currentUser hat immer die korrekten initials
|
||||||
|
let initials = currentUser?.initials || getInitials(name) || '?';
|
||||||
|
|
||||||
|
// Sicherheit: Falls initials undefined ist, Fallback verwenden
|
||||||
|
if (!initials || initials === 'undefined') {
|
||||||
|
initials = getInitials(currentUser?.email || name) || '?';
|
||||||
|
}
|
||||||
|
|
||||||
const avatar = createElement('span', {
|
const avatar = createElement('span', {
|
||||||
className: 'avatar task-assignee-avatar stacked',
|
className: 'avatar task-assignee-avatar stacked',
|
||||||
@ -401,14 +408,20 @@ class BoardManager {
|
|||||||
const users = store.get('users');
|
const users = store.get('users');
|
||||||
const assignedUser = users.find(u => u.id === task.assignedTo);
|
const assignedUser = users.find(u => u.id === task.assignedTo);
|
||||||
const currentColor = assignedUser?.color || task.assignedColor || '#888';
|
const currentColor = assignedUser?.color || task.assignedColor || '#888';
|
||||||
const currentName = assignedUser?.username || task.assignedName || 'Benutzer';
|
const currentName = assignedUser?.display_name || assignedUser?.username || task.assignedName || 'Benutzer';
|
||||||
|
|
||||||
|
// Initialen berechnen
|
||||||
|
let initials = assignedUser?.initials || getInitials(currentName) || '?';
|
||||||
|
if (!initials || initials === 'undefined') {
|
||||||
|
initials = getInitials(assignedUser?.email || currentName) || '?';
|
||||||
|
}
|
||||||
|
|
||||||
const assignee = createElement('div', { className: 'task-assignees' }, [
|
const assignee = createElement('div', { className: 'task-assignees' }, [
|
||||||
createElement('span', {
|
createElement('span', {
|
||||||
className: 'avatar task-assignee-avatar',
|
className: 'avatar task-assignee-avatar',
|
||||||
style: { backgroundColor: currentColor },
|
style: { backgroundColor: currentColor },
|
||||||
title: currentName
|
title: currentName
|
||||||
}, [getInitials(currentName)])
|
}, [initials])
|
||||||
]);
|
]);
|
||||||
footer.appendChild(assignee);
|
footer.appendChild(assignee);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -32,10 +32,17 @@ class CalendarViewManager {
|
|||||||
init() {
|
init() {
|
||||||
this.container = $('#view-calendar');
|
this.container = $('#view-calendar');
|
||||||
|
|
||||||
|
// Set initial view mode on calendar grid
|
||||||
|
const grid = $('#calendar-grid');
|
||||||
|
if (grid) {
|
||||||
|
grid.classList.add('calendar-month-view');
|
||||||
|
}
|
||||||
|
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
|
|
||||||
// Subscribe to store changes
|
// Subscribe to store changes
|
||||||
store.subscribe('tasks', () => this.render());
|
store.subscribe('tasks', () => this.render());
|
||||||
|
store.subscribe('reminders', () => this.render());
|
||||||
store.subscribe('filters', (filters) => {
|
store.subscribe('filters', (filters) => {
|
||||||
// Calendar-specific search behavior
|
// Calendar-specific search behavior
|
||||||
if (store.get('currentView') === 'calendar') {
|
if (store.get('currentView') === 'calendar') {
|
||||||
@ -95,7 +102,7 @@ class CalendarViewManager {
|
|||||||
// Close popup on outside click
|
// Close popup on outside click
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (this.dayDetailPopup && !this.dayDetailPopup.contains(e.target) &&
|
if (this.dayDetailPopup && !this.dayDetailPopup.contains(e.target) &&
|
||||||
!e.target.closest('.calendar-day')) {
|
!e.target.closest('.calendar-day') && !e.target.closest('.calendar-week-day')) {
|
||||||
this.closeDayDetail();
|
this.closeDayDetail();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -120,6 +127,12 @@ class CalendarViewManager {
|
|||||||
grid.classList.add(`calendar-${mode}-view`);
|
grid.classList.add(`calendar-${mode}-view`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show/hide weekday headers based on view mode
|
||||||
|
const weekdaysHeader = $('#calendar-weekdays');
|
||||||
|
if (weekdaysHeader) {
|
||||||
|
weekdaysHeader.style.display = mode === 'month' ? 'grid' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,13 +243,29 @@ class CalendarViewManager {
|
|||||||
render() {
|
render() {
|
||||||
if (store.get('currentView') !== 'calendar') return;
|
if (store.get('currentView') !== 'calendar') return;
|
||||||
|
|
||||||
|
// Ensure calendar grid exists and has correct class
|
||||||
|
const grid = $('#calendar-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
grid.classList.remove('calendar-month-view', 'calendar-week-view');
|
||||||
|
grid.classList.add(`calendar-${this.viewMode}-view`);
|
||||||
|
|
||||||
|
// Ensure weekday headers visibility matches current view mode
|
||||||
|
const weekdaysHeader = $('#calendar-weekdays');
|
||||||
|
if (weekdaysHeader) {
|
||||||
|
weekdaysHeader.style.display = this.viewMode === 'month' ? 'grid' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
this.updateHeader();
|
this.updateHeader();
|
||||||
|
|
||||||
|
// Force a small delay to ensure DOM is ready
|
||||||
|
setTimeout(() => {
|
||||||
if (this.viewMode === 'month') {
|
if (this.viewMode === 'month') {
|
||||||
this.renderMonthView();
|
this.renderMonthView();
|
||||||
} else {
|
} else {
|
||||||
this.renderWeekView();
|
this.renderWeekView();
|
||||||
}
|
}
|
||||||
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeader() {
|
updateHeader() {
|
||||||
@ -264,7 +293,14 @@ class CalendarViewManager {
|
|||||||
|
|
||||||
renderMonthView() {
|
renderMonthView() {
|
||||||
const daysContainer = $('#calendar-grid');
|
const daysContainer = $('#calendar-grid');
|
||||||
if (!daysContainer) return;
|
if (!daysContainer) {
|
||||||
|
console.warn('[Calendar] calendar-grid not found for month view');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure correct classes are set
|
||||||
|
daysContainer.classList.remove('calendar-week-view');
|
||||||
|
daysContainer.classList.add('calendar-month-view');
|
||||||
|
|
||||||
clearElement(daysContainer);
|
clearElement(daysContainer);
|
||||||
|
|
||||||
@ -334,18 +370,20 @@ class CalendarViewManager {
|
|||||||
const dateString = this.getDateString(date);
|
const dateString = this.getDateString(date);
|
||||||
const isToday = dateString === todayString;
|
const isToday = dateString === todayString;
|
||||||
|
|
||||||
daysContainer.appendChild(this.createWeekDayElement(date, tasksByDate, isToday));
|
daysContainer.appendChild(this.createWeekDayElement(date, tasksByDate, isToday, weekStart));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createWeekDayElement(date, tasksByDate, isToday = false) {
|
createWeekDayElement(date, tasksByDate, isToday = false, weekStart = null) {
|
||||||
const dateString = this.getDateString(date);
|
const dateString = this.getDateString(date);
|
||||||
const dayTasks = tasksByDate[dateString] || [];
|
const dayTasks = tasksByDate[dateString] || [];
|
||||||
|
const remindersByDate = this.getRemindersByDate();
|
||||||
|
const dayReminders = remindersByDate[dateString] || [];
|
||||||
const hasOverdue = dayTasks.some(t => this.isTaskOverdue(t));
|
const hasOverdue = dayTasks.some(t => this.isTaskOverdue(t));
|
||||||
|
|
||||||
const classes = ['calendar-week-day'];
|
const classes = ['calendar-week-day'];
|
||||||
if (isToday) classes.push('today');
|
if (isToday) classes.push('today');
|
||||||
if (dayTasks.length > 0) classes.push('has-tasks');
|
if (dayTasks.length > 0 || dayReminders.length > 0) classes.push('has-tasks');
|
||||||
if (hasOverdue) classes.push('has-overdue');
|
if (hasOverdue) classes.push('has-overdue');
|
||||||
|
|
||||||
const dayEl = createElement('div', {
|
const dayEl = createElement('div', {
|
||||||
@ -367,12 +405,58 @@ class CalendarViewManager {
|
|||||||
// Tasks container
|
// Tasks container
|
||||||
const tasksContainer = createElement('div', { className: 'calendar-week-day-tasks' });
|
const tasksContainer = createElement('div', { className: 'calendar-week-day-tasks' });
|
||||||
|
|
||||||
if (dayTasks.length === 0) {
|
// Combined array of reminders and tasks
|
||||||
|
const allItems = [];
|
||||||
|
|
||||||
|
// Add tasks first (wichtig für kontinuierliche Balken)
|
||||||
|
dayTasks.forEach(task => {
|
||||||
|
allItems.push({
|
||||||
|
type: 'task',
|
||||||
|
...task
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add reminders after tasks
|
||||||
|
dayReminders.forEach(reminder => {
|
||||||
|
allItems.push({
|
||||||
|
type: 'reminder',
|
||||||
|
id: reminder.id,
|
||||||
|
title: reminder.title,
|
||||||
|
color: reminder.color || '#F59E0B',
|
||||||
|
time: reminder.reminder_time
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allItems.length === 0) {
|
||||||
tasksContainer.appendChild(createElement('div', {
|
tasksContainer.appendChild(createElement('div', {
|
||||||
className: 'calendar-week-empty'
|
className: 'calendar-week-empty'
|
||||||
}, ['Keine Aufgaben']));
|
}, ['Keine Aufgaben']));
|
||||||
} else {
|
} else {
|
||||||
dayTasks.forEach(task => {
|
allItems.forEach(item => {
|
||||||
|
if (item.type === 'reminder') {
|
||||||
|
// Create reminder element
|
||||||
|
const reminderEl = createElement('div', {
|
||||||
|
className: 'calendar-week-task calendar-reminder-item',
|
||||||
|
dataset: { reminderId: item.id },
|
||||||
|
title: `Erinnerung: ${item.title}${item.time ? ' um ' + item.time : ''}`,
|
||||||
|
onclick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.editReminder(item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add title and bell icon
|
||||||
|
reminderEl.appendChild(createElement('span', { className: 'calendar-week-task-title' }, [item.title]));
|
||||||
|
reminderEl.appendChild(createElement('span', { className: 'calendar-reminder-bell' }, ['🔔']));
|
||||||
|
|
||||||
|
// Apply color
|
||||||
|
reminderEl.style.backgroundColor = `${item.color}25`;
|
||||||
|
reminderEl.style.borderLeftColor = item.color;
|
||||||
|
|
||||||
|
tasksContainer.appendChild(reminderEl);
|
||||||
|
} else {
|
||||||
|
// Existing task rendering code
|
||||||
|
const task = item;
|
||||||
// Get column color and user badge info
|
// Get column color and user badge info
|
||||||
const columnColor = this.getColumnColor(task);
|
const columnColor = this.getColumnColor(task);
|
||||||
const userBadge = this.getUserBadgeInfo(task);
|
const userBadge = this.getUserBadgeInfo(task);
|
||||||
@ -391,13 +475,20 @@ class CalendarViewManager {
|
|||||||
taskClasses.push('search-highlight');
|
taskClasses.push('search-highlight');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build task element children
|
// Check if this is the first day of the week or the start of the task
|
||||||
const children = [
|
const isFirstDayOfWeek = weekStart && this.getDateString(date) === this.getDateString(weekStart);
|
||||||
createElement('span', { className: 'calendar-week-task-title' }, [task.title])
|
const showContent = task.isRangeStart || !task.hasRange || isFirstDayOfWeek;
|
||||||
];
|
|
||||||
|
|
||||||
// Add user badges if assigned (supports multiple)
|
// Build task element children
|
||||||
if (userBadge && userBadge.length > 0) {
|
const children = [];
|
||||||
|
|
||||||
|
// Add title (only at start of task or first day of week)
|
||||||
|
if (showContent) {
|
||||||
|
children.push(createElement('span', { className: 'calendar-week-task-title' }, [task.title]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user badges if assigned (only at start of task or first day of week, supports multiple)
|
||||||
|
if (userBadge && userBadge.length > 0 && showContent) {
|
||||||
const badgeContainer = createElement('span', { className: 'calendar-task-badges' });
|
const badgeContainer = createElement('span', { className: 'calendar-task-badges' });
|
||||||
userBadge.forEach(badge => {
|
userBadge.forEach(badge => {
|
||||||
const badgeEl = createElement('span', { className: 'calendar-task-user-badge' }, [badge.initials]);
|
const badgeEl = createElement('span', { className: 'calendar-task-user-badge' }, [badge.initials]);
|
||||||
@ -420,6 +511,7 @@ class CalendarViewManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasksContainer.appendChild(taskEl);
|
tasksContainer.appendChild(taskEl);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,12 +544,14 @@ class CalendarViewManager {
|
|||||||
createDayElement(date, dayNumber, isOtherMonth, tasksByDate, isToday = false) {
|
createDayElement(date, dayNumber, isOtherMonth, tasksByDate, isToday = false) {
|
||||||
const dateString = this.getDateString(date);
|
const dateString = this.getDateString(date);
|
||||||
const dayTasks = tasksByDate[dateString] || [];
|
const dayTasks = tasksByDate[dateString] || [];
|
||||||
|
const remindersByDate = this.getRemindersByDate();
|
||||||
|
const dayReminders = remindersByDate[dateString] || [];
|
||||||
const hasOverdue = dayTasks.some(t => this.isTaskOverdue(t));
|
const hasOverdue = dayTasks.some(t => this.isTaskOverdue(t));
|
||||||
|
|
||||||
const classes = ['calendar-day'];
|
const classes = ['calendar-day'];
|
||||||
if (isOtherMonth) classes.push('other-month');
|
if (isOtherMonth) classes.push('other-month');
|
||||||
if (isToday) classes.push('today');
|
if (isToday) classes.push('today');
|
||||||
if (dayTasks.length > 0) classes.push('has-tasks');
|
if (dayTasks.length > 0 || dayReminders.length > 0) classes.push('has-tasks');
|
||||||
if (hasOverdue) classes.push('has-overdue');
|
if (hasOverdue) classes.push('has-overdue');
|
||||||
|
|
||||||
const dayEl = createElement('div', {
|
const dayEl = createElement('div', {
|
||||||
@ -470,11 +564,57 @@ class CalendarViewManager {
|
|||||||
className: 'calendar-day-number'
|
className: 'calendar-day-number'
|
||||||
}, [dayNumber.toString()]));
|
}, [dayNumber.toString()]));
|
||||||
|
|
||||||
// Tasks preview (show max 3)
|
// Combined container for reminders and tasks
|
||||||
if (dayTasks.length > 0) {
|
const allItems = [];
|
||||||
|
|
||||||
|
// Add tasks first (wichtig für kontinuierliche Balken)
|
||||||
|
dayTasks.forEach(task => {
|
||||||
|
allItems.push({
|
||||||
|
type: 'task',
|
||||||
|
...task
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add reminders after tasks
|
||||||
|
dayReminders.forEach(reminder => {
|
||||||
|
allItems.push({
|
||||||
|
type: 'reminder',
|
||||||
|
id: reminder.id,
|
||||||
|
title: reminder.title,
|
||||||
|
color: reminder.color || '#F59E0B',
|
||||||
|
time: reminder.reminder_time
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show max 3 items
|
||||||
|
if (allItems.length > 0) {
|
||||||
const tasksContainer = createElement('div', { className: 'calendar-day-tasks' });
|
const tasksContainer = createElement('div', { className: 'calendar-day-tasks' });
|
||||||
|
|
||||||
dayTasks.slice(0, 3).forEach(task => {
|
allItems.slice(0, 3).forEach(item => {
|
||||||
|
if (item.type === 'reminder') {
|
||||||
|
// Create reminder element
|
||||||
|
const reminderEl = createElement('div', {
|
||||||
|
className: 'calendar-task calendar-reminder-item',
|
||||||
|
dataset: { reminderId: item.id },
|
||||||
|
title: `Erinnerung: ${item.title}${item.time ? ' um ' + item.time : ''}`,
|
||||||
|
onclick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.editReminder(item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add title and bell icon
|
||||||
|
reminderEl.appendChild(createElement('span', { className: 'calendar-task-title' }, [item.title]));
|
||||||
|
reminderEl.appendChild(createElement('span', { className: 'calendar-reminder-bell' }, ['🔔']));
|
||||||
|
|
||||||
|
// Apply color
|
||||||
|
reminderEl.style.backgroundColor = `${item.color}40`;
|
||||||
|
reminderEl.style.borderLeftColor = item.color;
|
||||||
|
|
||||||
|
tasksContainer.appendChild(reminderEl);
|
||||||
|
} else {
|
||||||
|
// Existing task rendering code
|
||||||
|
const task = item;
|
||||||
// Get column color and user badge info
|
// Get column color and user badge info
|
||||||
const columnColor = this.getColumnColor(task);
|
const columnColor = this.getColumnColor(task);
|
||||||
const userBadge = this.getUserBadgeInfo(task);
|
const userBadge = this.getUserBadgeInfo(task);
|
||||||
@ -523,12 +663,13 @@ class CalendarViewManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasksContainer.appendChild(taskEl);
|
tasksContainer.appendChild(taskEl);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dayTasks.length > 3) {
|
if (allItems.length > 3) {
|
||||||
tasksContainer.appendChild(createElement('div', {
|
tasksContainer.appendChild(createElement('div', {
|
||||||
className: 'calendar-more'
|
className: 'calendar-more'
|
||||||
}, [`+${dayTasks.length - 3} weitere`]));
|
}, [`+${allItems.length - 3} weitere`]));
|
||||||
}
|
}
|
||||||
|
|
||||||
dayEl.appendChild(tasksContainer);
|
dayEl.appendChild(tasksContainer);
|
||||||
@ -570,18 +711,32 @@ class CalendarViewManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort tasks: earliest start date first (so they stay on top throughout their duration),
|
// Sort tasks: Range tasks first (to ensure continuous bars), then single-day tasks
|
||||||
// then alphabetically by title for same start date
|
|
||||||
filteredTasks.sort((a, b) => {
|
filteredTasks.sort((a, b) => {
|
||||||
const startA = a.startDate || a.dueDate || '';
|
const startA = a.startDate || a.dueDate || '';
|
||||||
const startB = b.startDate || b.dueDate || '';
|
const startB = b.startDate || b.dueDate || '';
|
||||||
|
const endA = a.dueDate || '';
|
||||||
|
const endB = b.dueDate || '';
|
||||||
|
|
||||||
// Compare start dates (earliest first = ascending)
|
// Check if task has range (both start and end date, and they're different)
|
||||||
|
const hasRangeA = a.startDate && a.dueDate && a.startDate !== a.dueDate;
|
||||||
|
const hasRangeB = b.startDate && b.dueDate && b.startDate !== b.dueDate;
|
||||||
|
|
||||||
|
// Range tasks come first
|
||||||
|
if (hasRangeA && !hasRangeB) return -1;
|
||||||
|
if (!hasRangeA && hasRangeB) return 1;
|
||||||
|
|
||||||
|
// If both have ranges or both are single-day, sort by start date
|
||||||
if (startA !== startB) {
|
if (startA !== startB) {
|
||||||
return startA.localeCompare(startB);
|
return startA.localeCompare(startB);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same start date: sort alphabetically by title
|
// Same start date: sort by end date (longer ranges first)
|
||||||
|
if (endA !== endB) {
|
||||||
|
return endB.localeCompare(endA); // Descending to put longer ranges first
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same dates: sort alphabetically by title
|
||||||
return (a.title || '').localeCompare(b.title || '', 'de');
|
return (a.title || '').localeCompare(b.title || '', 'de');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -629,6 +784,32 @@ class CalendarViewManager {
|
|||||||
return tasksByDate;
|
return tasksByDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRemindersByDate() {
|
||||||
|
const reminders = store.get('reminders') || [];
|
||||||
|
if (!Array.isArray(reminders)) {
|
||||||
|
console.warn('[Calendar] Reminders not available or not an array');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeReminders = reminders.filter(r => r.is_active);
|
||||||
|
const remindersByDate = {};
|
||||||
|
|
||||||
|
activeReminders.forEach(reminder => {
|
||||||
|
const date = reminder.reminder_date;
|
||||||
|
if (!remindersByDate[date]) {
|
||||||
|
remindersByDate[date] = [];
|
||||||
|
}
|
||||||
|
remindersByDate[date].push(reminder);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort reminders by time within each date
|
||||||
|
Object.keys(remindersByDate).forEach(date => {
|
||||||
|
remindersByDate[date].sort((a, b) => (a.reminder_time || '09:00').localeCompare(b.reminder_time || '09:00'));
|
||||||
|
});
|
||||||
|
|
||||||
|
return remindersByDate;
|
||||||
|
}
|
||||||
|
|
||||||
getDateString(date) {
|
getDateString(date) {
|
||||||
// Use local date components instead of toISOString() which converts to UTC
|
// Use local date components instead of toISOString() which converts to UTC
|
||||||
// This fixes the issue where the date is off by one day due to timezone differences
|
// This fixes the issue where the date is off by one day due to timezone differences
|
||||||
@ -643,9 +824,12 @@ class CalendarViewManager {
|
|||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
handleDayClick(e) {
|
handleDayClick(e) {
|
||||||
|
console.log('[Calendar] Day click event:', e.target, 'View mode:', this.viewMode);
|
||||||
|
|
||||||
// Check if clicked on a specific task
|
// Check if clicked on a specific task
|
||||||
const taskEl = e.target.closest('.calendar-task, .calendar-week-task');
|
const taskEl = e.target.closest('.calendar-task, .calendar-week-task');
|
||||||
if (taskEl) {
|
if (taskEl) {
|
||||||
|
console.log('[Calendar] Task clicked:', taskEl.dataset.taskId);
|
||||||
const taskId = parseInt(taskEl.dataset.taskId);
|
const taskId = parseInt(taskEl.dataset.taskId);
|
||||||
this.openTaskModal(taskId);
|
this.openTaskModal(taskId);
|
||||||
return;
|
return;
|
||||||
@ -653,24 +837,36 @@ class CalendarViewManager {
|
|||||||
|
|
||||||
// Check if clicked on add button (week view)
|
// Check if clicked on add button (week view)
|
||||||
if (e.target.closest('.calendar-week-add-task')) {
|
if (e.target.closest('.calendar-week-add-task')) {
|
||||||
|
console.log('[Calendar] Add task button clicked');
|
||||||
return; // Already handled by onclick
|
return; // Already handled by onclick
|
||||||
}
|
}
|
||||||
|
|
||||||
// For month view, show day detail popup
|
// Show day detail popup for both month and week view
|
||||||
if (this.viewMode === 'month') {
|
if (this.viewMode === 'month') {
|
||||||
const dayEl = e.target.closest('.calendar-day');
|
const dayEl = e.target.closest('.calendar-day');
|
||||||
if (!dayEl) return;
|
if (!dayEl) return;
|
||||||
|
|
||||||
|
console.log('[Calendar] Month day clicked:', dayEl.dataset.date);
|
||||||
|
const dateString = dayEl.dataset.date;
|
||||||
|
this.showDayDetail(dateString, dayEl);
|
||||||
|
} else if (this.viewMode === 'week') {
|
||||||
|
const dayEl = e.target.closest('.calendar-week-day');
|
||||||
|
console.log('[Calendar] Week day element found:', !!dayEl);
|
||||||
|
if (!dayEl) return;
|
||||||
|
|
||||||
|
console.log('[Calendar] Week day clicked:', dayEl.dataset.date);
|
||||||
const dateString = dayEl.dataset.date;
|
const dateString = dayEl.dataset.date;
|
||||||
this.showDayDetail(dateString, dayEl);
|
this.showDayDetail(dateString, dayEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showDayDetail(dateString, anchorEl) {
|
showDayDetail(dateString, anchorEl) {
|
||||||
|
console.log('[Calendar] showDayDetail called:', dateString, anchorEl);
|
||||||
this.closeDayDetail();
|
this.closeDayDetail();
|
||||||
|
|
||||||
const tasksByDate = this.getTasksByDate();
|
const tasksByDate = this.getTasksByDate();
|
||||||
const dayTasks = tasksByDate[dateString] || [];
|
const dayTasks = tasksByDate[dateString] || [];
|
||||||
|
console.log('[Calendar] Tasks for date:', dayTasks.length);
|
||||||
|
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
const dateDisplay = date.toLocaleDateString('de-DE', {
|
const dateDisplay = date.toLocaleDateString('de-DE', {
|
||||||
@ -726,13 +922,35 @@ class CalendarViewManager {
|
|||||||
onclick: () => this.createTaskForDate(dateString)
|
onclick: () => this.createTaskForDate(dateString)
|
||||||
}, ['+ Aufgabe hinzufügen']));
|
}, ['+ Aufgabe hinzufügen']));
|
||||||
|
|
||||||
// Position popup
|
// Add reminder button
|
||||||
|
popup.appendChild(createElement('button', {
|
||||||
|
className: 'btn btn-secondary btn-block',
|
||||||
|
style: { marginTop: 'var(--spacing-sm)' },
|
||||||
|
onclick: () => this.createReminderForDate(dateString)
|
||||||
|
}, ['🔔 Erinnerung hinzufügen']));
|
||||||
|
|
||||||
|
// Position popup - different logic for week vs month view
|
||||||
const rect = anchorEl.getBoundingClientRect();
|
const rect = anchorEl.getBoundingClientRect();
|
||||||
popup.style.top = `${rect.bottom + 8}px`;
|
|
||||||
popup.style.left = `${Math.min(rect.left, window.innerWidth - 350)}px`;
|
let popupTop, popupLeft;
|
||||||
|
|
||||||
|
if (this.viewMode === 'week') {
|
||||||
|
// For week view, position at the top of the day element
|
||||||
|
popupTop = Math.max(150, rect.top + 50); // Ensure it's visible, minimum 150px from top
|
||||||
|
popupLeft = Math.min(rect.left, window.innerWidth - 350);
|
||||||
|
} else {
|
||||||
|
// For month view, position below the day element
|
||||||
|
popupTop = rect.bottom + 8;
|
||||||
|
popupLeft = Math.min(rect.left, window.innerWidth - 350);
|
||||||
|
}
|
||||||
|
|
||||||
|
popup.style.top = `${popupTop}px`;
|
||||||
|
popup.style.left = `${popupLeft}px`;
|
||||||
|
console.log('[Calendar] Popup positioning for', this.viewMode, '- Top:', popup.style.top, 'Left:', popup.style.left);
|
||||||
|
|
||||||
document.body.appendChild(popup);
|
document.body.appendChild(popup);
|
||||||
this.dayDetailPopup = popup;
|
this.dayDetailPopup = popup;
|
||||||
|
console.log('[Calendar] Popup created and appended to body');
|
||||||
}
|
}
|
||||||
|
|
||||||
closeDayDetail() {
|
closeDayDetail() {
|
||||||
@ -778,6 +996,155 @@ class CalendarViewManager {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createReminderForDate(dateString) {
|
||||||
|
this.closeDayDetail();
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('modal:open', {
|
||||||
|
detail: {
|
||||||
|
modalId: 'reminder-modal',
|
||||||
|
mode: 'create',
|
||||||
|
data: {
|
||||||
|
prefill: {
|
||||||
|
date: dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
showRemindersForDate(dateString, anchorEl) {
|
||||||
|
const reminders = this.getRemindersByDate();
|
||||||
|
const dayReminders = reminders[dateString] || [];
|
||||||
|
|
||||||
|
if (dayReminders.length === 0) return;
|
||||||
|
|
||||||
|
// Close existing popup
|
||||||
|
this.closeDayDetail();
|
||||||
|
|
||||||
|
// Create popup
|
||||||
|
const popup = createElement('div', {
|
||||||
|
className: 'calendar-day-detail calendar-reminder-detail',
|
||||||
|
onclick: (e) => e.stopPropagation()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dayDetailPopup = popup;
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const header = createElement('h3', {}, [`Erinnerungen für ${formatDate(dateString)}`]);
|
||||||
|
popup.appendChild(header);
|
||||||
|
|
||||||
|
// Reminders list
|
||||||
|
const remindersList = createElement('div', { className: 'calendar-detail-reminders' });
|
||||||
|
|
||||||
|
dayReminders.forEach(reminder => {
|
||||||
|
// Create reminder content (clickable for edit)
|
||||||
|
const reminderContent = createElement('div', {
|
||||||
|
className: 'reminder-content',
|
||||||
|
onclick: () => this.editReminder(reminder.id)
|
||||||
|
}, [
|
||||||
|
createElement('div', {
|
||||||
|
className: 'reminder-time',
|
||||||
|
style: { color: reminder.color || '#F59E0B' }
|
||||||
|
}, [reminder.reminder_time || '09:00']),
|
||||||
|
createElement('div', { className: 'reminder-title' }, [reminder.title]),
|
||||||
|
reminder.description ? createElement('div', { className: 'reminder-description' }, [reminder.description]) : null
|
||||||
|
].filter(Boolean));
|
||||||
|
|
||||||
|
// Create delete button
|
||||||
|
const deleteBtn = createElement('button', {
|
||||||
|
className: 'reminder-delete-btn',
|
||||||
|
title: 'Erinnerung löschen',
|
||||||
|
onclick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.deleteReminder(reminder.id, reminder.title);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
createElement('svg', {
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
width: '16',
|
||||||
|
height: '16'
|
||||||
|
}, [
|
||||||
|
createElement('path', {
|
||||||
|
d: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '2',
|
||||||
|
fill: 'none',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Container with content and delete button
|
||||||
|
const reminderItem = createElement('div', {
|
||||||
|
className: 'calendar-detail-reminder'
|
||||||
|
}, [reminderContent, deleteBtn]);
|
||||||
|
|
||||||
|
remindersList.appendChild(reminderItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
popup.appendChild(remindersList);
|
||||||
|
|
||||||
|
// Add reminder button
|
||||||
|
popup.appendChild(createElement('button', {
|
||||||
|
className: 'btn btn-secondary btn-block',
|
||||||
|
style: { marginTop: 'var(--spacing-md)' },
|
||||||
|
onclick: () => this.createReminderForDate(dateString)
|
||||||
|
}, ['+ Weitere Erinnerung']));
|
||||||
|
|
||||||
|
// Position popup
|
||||||
|
const rect = anchorEl.getBoundingClientRect();
|
||||||
|
let popupTop = rect.bottom + 8;
|
||||||
|
let popupLeft = Math.min(rect.left, window.innerWidth - 350);
|
||||||
|
|
||||||
|
popup.style.top = `${popupTop}px`;
|
||||||
|
popup.style.left = `${popupLeft}px`;
|
||||||
|
|
||||||
|
document.body.appendChild(popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
editReminder(reminderId) {
|
||||||
|
this.closeDayDetail();
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('modal:open', {
|
||||||
|
detail: {
|
||||||
|
modalId: 'reminder-modal',
|
||||||
|
mode: 'edit',
|
||||||
|
data: { reminderId }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteReminder(reminderId, reminderTitle) {
|
||||||
|
if (!confirm(`Erinnerung "${reminderTitle}" wirklich löschen?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Import reminder manager if needed
|
||||||
|
if (typeof reminderManager !== 'undefined') {
|
||||||
|
await reminderManager.deleteReminder(reminderId);
|
||||||
|
} else {
|
||||||
|
// Fallback direct API call
|
||||||
|
await api.request(`/reminders/${reminderId}`, { method: 'DELETE' });
|
||||||
|
|
||||||
|
// Update store
|
||||||
|
const reminders = store.get('reminders') || [];
|
||||||
|
const updatedReminders = reminders.filter(r => r.id !== reminderId);
|
||||||
|
store.setReminders(updatedReminders);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close popup and refresh calendar
|
||||||
|
this.closeDayDetail();
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
console.log(`[Calendar] Reminder ${reminderId} deleted`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Calendar] Failed to delete reminder:', error);
|
||||||
|
alert('Fehler beim Löschen der Erinnerung. Bitte versuche es erneut.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// HELPERS
|
// HELPERS
|
||||||
// =====================
|
// =====================
|
||||||
|
|||||||
543
frontend/js/contacts.js
Normale Datei
543
frontend/js/contacts.js
Normale Datei
@ -0,0 +1,543 @@
|
|||||||
|
/**
|
||||||
|
* TASKMATE - Contacts Manager
|
||||||
|
* ===========================
|
||||||
|
* Kontaktverwaltung mit Kartenansicht
|
||||||
|
*/
|
||||||
|
|
||||||
|
import api from './api.js';
|
||||||
|
import { $, $$, formatDate, debounce } from './utils.js';
|
||||||
|
import store from './store.js';
|
||||||
|
|
||||||
|
class ContactsManager {
|
||||||
|
constructor() {
|
||||||
|
this.contacts = [];
|
||||||
|
this.filteredContacts = [];
|
||||||
|
this.allTags = new Set();
|
||||||
|
this.searchQuery = '';
|
||||||
|
this.filterTag = '';
|
||||||
|
this.sortBy = 'created_at';
|
||||||
|
this.sortOrder = 'desc';
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
console.log('[Contacts] init() called, initialized:', this.initialized);
|
||||||
|
|
||||||
|
if (this.initialized) {
|
||||||
|
await this.loadContacts();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
this.contactsView = $('#view-contacts');
|
||||||
|
this.contactsGrid = $('#contacts-grid');
|
||||||
|
this.contactsEmpty = $('#contacts-empty');
|
||||||
|
this.tagFilter = $('#contacts-tag-filter');
|
||||||
|
this.sortSelect = $('#contacts-sort');
|
||||||
|
this.newContactBtn = $('#btn-new-contact');
|
||||||
|
|
||||||
|
console.log('[Contacts] DOM Elements check:');
|
||||||
|
console.log(' contactsView:', this.contactsView);
|
||||||
|
console.log(' newContactBtn:', this.newContactBtn);
|
||||||
|
console.log(' contactsGrid:', this.contactsGrid);
|
||||||
|
|
||||||
|
// Modal Elements
|
||||||
|
this.contactModal = $('#contact-modal');
|
||||||
|
this.modalTitle = $('#contact-modal-title');
|
||||||
|
this.contactForm = $('#contact-form');
|
||||||
|
|
||||||
|
console.log('[Contacts] Modal Elements check:');
|
||||||
|
console.log(' contactModal:', this.contactModal);
|
||||||
|
console.log(' contactForm:', this.contactForm);
|
||||||
|
this.contactIdInput = $('#contact-id');
|
||||||
|
this.firstNameInput = $('#contact-first-name');
|
||||||
|
this.lastNameInput = $('#contact-last-name');
|
||||||
|
this.companyInput = $('#contact-company');
|
||||||
|
this.positionInput = $('#contact-position');
|
||||||
|
this.emailInput = $('#contact-email');
|
||||||
|
this.phoneInput = $('#contact-phone');
|
||||||
|
this.mobileInput = $('#contact-mobile');
|
||||||
|
this.addressInput = $('#contact-address');
|
||||||
|
this.postalCodeInput = $('#contact-postal-code');
|
||||||
|
this.cityInput = $('#contact-city');
|
||||||
|
this.countryInput = $('#contact-country');
|
||||||
|
this.websiteInput = $('#contact-website');
|
||||||
|
this.notesInput = $('#contact-notes');
|
||||||
|
this.tagsInput = $('#contact-tags');
|
||||||
|
this.deleteContactBtn = $('#btn-delete-contact');
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
this.initialized = true;
|
||||||
|
console.log('[Contacts] Initialization complete');
|
||||||
|
|
||||||
|
await this.loadContacts();
|
||||||
|
|
||||||
|
// Store subscriptions
|
||||||
|
store.subscribe('contacts', this.handleContactsUpdate.bind(this));
|
||||||
|
|
||||||
|
// Window events
|
||||||
|
window.addEventListener('app:refresh', () => this.loadContacts());
|
||||||
|
window.addEventListener('modal:close', () => this.loadContacts());
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
console.log('[Contacts] bindEvents() called');
|
||||||
|
|
||||||
|
// Tag Filter
|
||||||
|
if (this.tagFilter) {
|
||||||
|
this.tagFilter.addEventListener('change', (e) => {
|
||||||
|
this.filterTag = e.target.value;
|
||||||
|
this.filterContacts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
if (this.sortSelect) {
|
||||||
|
this.sortSelect.addEventListener('change', (e) => {
|
||||||
|
const [sortBy, sortOrder] = e.target.value.split('-');
|
||||||
|
this.sortBy = sortBy;
|
||||||
|
this.sortOrder = sortOrder;
|
||||||
|
this.sortContacts();
|
||||||
|
this.renderContacts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Contact Button
|
||||||
|
console.log('[Contacts] newContactBtn element:', this.newContactBtn);
|
||||||
|
if (this.newContactBtn) {
|
||||||
|
console.log('[Contacts] Binding click event to newContactBtn');
|
||||||
|
this.newContactBtn.addEventListener('click', () => {
|
||||||
|
console.log('[Contacts] New contact button clicked!');
|
||||||
|
this.showContactModal();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('[Contacts] newContactBtn not found!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form
|
||||||
|
if (this.contactForm) {
|
||||||
|
this.contactForm.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.saveContact();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete Button
|
||||||
|
if (this.deleteContactBtn) {
|
||||||
|
this.deleteContactBtn.addEventListener('click', () => this.deleteContact());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Close
|
||||||
|
const modalCloseBtn = this.contactModal?.querySelector('.modal-close');
|
||||||
|
if (modalCloseBtn) {
|
||||||
|
modalCloseBtn.addEventListener('click', () => this.hideContactModal());
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalCancelBtn = this.contactModal?.querySelector('.modal-cancel');
|
||||||
|
if (modalCancelBtn) {
|
||||||
|
modalCancelBtn.addEventListener('click', () => this.hideContactModal());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket Events
|
||||||
|
const socket = window.socket;
|
||||||
|
if (socket) {
|
||||||
|
socket.on('contact:created', (data) => {
|
||||||
|
console.log('[Contacts] Socket: contact created', data);
|
||||||
|
this.contacts.unshift(data.contact);
|
||||||
|
this.updateTagsList();
|
||||||
|
this.filterContacts();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('contact:updated', (data) => {
|
||||||
|
console.log('[Contacts] Socket: contact updated', data);
|
||||||
|
const index = this.contacts.findIndex(c => c.id === data.contact.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.contacts[index] = data.contact;
|
||||||
|
this.updateTagsList();
|
||||||
|
this.filterContacts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('contact:deleted', (data) => {
|
||||||
|
console.log('[Contacts] Socket: contact deleted', data);
|
||||||
|
this.contacts = this.contacts.filter(c => c.id !== data.contactId);
|
||||||
|
this.updateTagsList();
|
||||||
|
this.filterContacts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadContacts() {
|
||||||
|
try {
|
||||||
|
console.log('[Contacts] Loading contacts...');
|
||||||
|
const response = await api.getContacts();
|
||||||
|
this.contacts = response.data || response || [];
|
||||||
|
|
||||||
|
this.updateTagsList();
|
||||||
|
this.filterContacts();
|
||||||
|
|
||||||
|
console.log(`[Contacts] Loaded ${this.contacts.length} contacts`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Contacts] Error loading contacts:', error);
|
||||||
|
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||||
|
detail: { message: 'Fehler beim Laden der Kontakte', type: 'error' }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterContacts() {
|
||||||
|
this.filteredContacts = this.contacts.filter(contact => {
|
||||||
|
// Search filter
|
||||||
|
if (this.searchQuery) {
|
||||||
|
const query = this.searchQuery.toLowerCase();
|
||||||
|
const searchFields = [
|
||||||
|
contact.firstName,
|
||||||
|
contact.lastName,
|
||||||
|
contact.company,
|
||||||
|
contact.email,
|
||||||
|
contact.position,
|
||||||
|
contact.phone,
|
||||||
|
contact.mobile
|
||||||
|
].filter(Boolean).join(' ').toLowerCase();
|
||||||
|
|
||||||
|
if (!searchFields.includes(query)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag filter
|
||||||
|
if (this.filterTag && contact.tags) {
|
||||||
|
if (!contact.tags.includes(this.filterTag)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sortContacts();
|
||||||
|
this.renderContacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
sortContacts() {
|
||||||
|
this.filteredContacts.sort((a, b) => {
|
||||||
|
let aVal = a[this.sortBy] || '';
|
||||||
|
let bVal = b[this.sortBy] || '';
|
||||||
|
|
||||||
|
// Handle name sorting
|
||||||
|
if (this.sortBy === 'name') {
|
||||||
|
aVal = `${a.lastName || ''} ${a.firstName || ''}`.trim();
|
||||||
|
bVal = `${b.lastName || ''} ${b.firstName || ''}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof aVal === 'string') {
|
||||||
|
aVal = aVal.toLowerCase();
|
||||||
|
bVal = bVal.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sortOrder === 'asc') {
|
||||||
|
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
||||||
|
} else {
|
||||||
|
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContacts() {
|
||||||
|
if (!this.contactsGrid) return;
|
||||||
|
|
||||||
|
if (this.filteredContacts.length === 0) {
|
||||||
|
this.contactsGrid.classList.add('hidden');
|
||||||
|
this.contactsEmpty.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contactsGrid.classList.remove('hidden');
|
||||||
|
this.contactsEmpty.classList.add('hidden');
|
||||||
|
|
||||||
|
const html = this.filteredContacts.map(contact => this.createContactCard(contact)).join('');
|
||||||
|
this.contactsGrid.innerHTML = html;
|
||||||
|
|
||||||
|
// Bind card events
|
||||||
|
this.bindCardEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
createContactCard(contact) {
|
||||||
|
const displayName = this.getContactDisplayName(contact);
|
||||||
|
const initials = this.getContactInitials(contact);
|
||||||
|
const tags = contact.tags || [];
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="contact-card" data-contact-id="${contact.id}">
|
||||||
|
<div class="contact-card-header">
|
||||||
|
<div class="contact-avatar">
|
||||||
|
${initials}
|
||||||
|
</div>
|
||||||
|
<div class="contact-actions">
|
||||||
|
<button class="btn-icon btn-edit-contact" title="Bearbeiten">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="contact-card-body">
|
||||||
|
<h3 class="contact-name">${displayName}</h3>
|
||||||
|
${contact.company ? `<div class="contact-company">${contact.company}</div>` : ''}
|
||||||
|
${contact.position ? `<div class="contact-position">${contact.position}</div>` : ''}
|
||||||
|
${contact.email ? `<div class="contact-email"><i class="fas fa-envelope"></i> ${contact.email}</div>` : ''}
|
||||||
|
${contact.phone ? `<div class="contact-phone"><i class="fas fa-phone"></i> ${contact.phone}</div>` : ''}
|
||||||
|
${contact.mobile ? `<div class="contact-mobile"><i class="fas fa-mobile"></i> ${contact.mobile}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
${tags.length > 0 ? `
|
||||||
|
<div class="contact-tags">
|
||||||
|
${tags.map(tag => `<span class="contact-tag">${tag}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getContactDisplayName(contact) {
|
||||||
|
const parts = [];
|
||||||
|
if (contact.firstName) parts.push(contact.firstName);
|
||||||
|
if (contact.lastName) parts.push(contact.lastName);
|
||||||
|
|
||||||
|
if (parts.length > 0) {
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return contact.company || 'Unbenannt';
|
||||||
|
}
|
||||||
|
|
||||||
|
getContactInitials(contact) {
|
||||||
|
let initials = '';
|
||||||
|
|
||||||
|
if (contact.firstName) {
|
||||||
|
initials += contact.firstName.charAt(0).toUpperCase();
|
||||||
|
}
|
||||||
|
if (contact.lastName) {
|
||||||
|
initials += contact.lastName.charAt(0).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initials && contact.company) {
|
||||||
|
initials = contact.company.charAt(0).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return initials || '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
bindCardEvents() {
|
||||||
|
// Edit buttons
|
||||||
|
$$('.btn-edit-contact').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const card = btn.closest('.contact-card');
|
||||||
|
const contactId = parseInt(card.dataset.contactId);
|
||||||
|
this.editContact(contactId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Card click
|
||||||
|
$$('.contact-card').forEach(card => {
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
const contactId = parseInt(card.dataset.contactId);
|
||||||
|
this.editContact(contactId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTagsList() {
|
||||||
|
// Collect all unique tags
|
||||||
|
this.allTags.clear();
|
||||||
|
this.contacts.forEach(contact => {
|
||||||
|
if (contact.tags) {
|
||||||
|
contact.tags.forEach(tag => this.allTags.add(tag));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update tag filter dropdown
|
||||||
|
if (this.tagFilter) {
|
||||||
|
const currentValue = this.tagFilter.value;
|
||||||
|
this.tagFilter.innerHTML = '<option value="">Alle Tags</option>';
|
||||||
|
|
||||||
|
Array.from(this.allTags).sort().forEach(tag => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = tag;
|
||||||
|
option.textContent = tag;
|
||||||
|
this.tagFilter.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tagFilter.value = currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showContactModal(contact = null) {
|
||||||
|
console.log('[Contacts] showContactModal called with:', contact);
|
||||||
|
console.log('[Contacts] contactModal element:', this.contactModal);
|
||||||
|
|
||||||
|
if (!this.contactModal) {
|
||||||
|
console.error('[Contacts] Contact modal not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Contacts] Resetting form and showing modal');
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
this.contactForm.reset();
|
||||||
|
this.contactIdInput.value = '';
|
||||||
|
|
||||||
|
if (contact) {
|
||||||
|
// Edit mode
|
||||||
|
this.modalTitle.textContent = 'Kontakt bearbeiten';
|
||||||
|
this.deleteContactBtn.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Fill form
|
||||||
|
this.contactIdInput.value = contact.id;
|
||||||
|
this.firstNameInput.value = contact.firstName || '';
|
||||||
|
this.lastNameInput.value = contact.lastName || '';
|
||||||
|
this.companyInput.value = contact.company || '';
|
||||||
|
this.positionInput.value = contact.position || '';
|
||||||
|
this.emailInput.value = contact.email || '';
|
||||||
|
this.phoneInput.value = contact.phone || '';
|
||||||
|
this.mobileInput.value = contact.mobile || '';
|
||||||
|
this.addressInput.value = contact.address || '';
|
||||||
|
this.postalCodeInput.value = contact.postalCode || '';
|
||||||
|
this.cityInput.value = contact.city || '';
|
||||||
|
this.countryInput.value = contact.country || '';
|
||||||
|
this.websiteInput.value = contact.website || '';
|
||||||
|
this.notesInput.value = contact.notes || '';
|
||||||
|
this.tagsInput.value = (contact.tags || []).join(', ');
|
||||||
|
} else {
|
||||||
|
// Create mode
|
||||||
|
this.modalTitle.textContent = 'Neuer Kontakt';
|
||||||
|
this.deleteContactBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal overlay
|
||||||
|
const overlay = $('#modal-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.classList.remove('hidden');
|
||||||
|
overlay.classList.add('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contactModal.classList.remove('hidden');
|
||||||
|
this.contactModal.classList.add('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideContactModal() {
|
||||||
|
if (this.contactModal) {
|
||||||
|
this.contactModal.classList.remove('visible');
|
||||||
|
this.contactModal.classList.add('hidden');
|
||||||
|
|
||||||
|
// Hide modal overlay
|
||||||
|
const overlay = $('#modal-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.classList.remove('visible');
|
||||||
|
overlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('modal:close'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async editContact(contactId) {
|
||||||
|
const contact = this.contacts.find(c => c.id === contactId);
|
||||||
|
if (contact) {
|
||||||
|
this.showContactModal(contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveContact() {
|
||||||
|
const contactId = this.contactIdInput.value;
|
||||||
|
|
||||||
|
const contactData = {
|
||||||
|
firstName: this.firstNameInput.value.trim(),
|
||||||
|
lastName: this.lastNameInput.value.trim(),
|
||||||
|
company: this.companyInput.value.trim(),
|
||||||
|
position: this.positionInput.value.trim(),
|
||||||
|
email: this.emailInput.value.trim(),
|
||||||
|
phone: this.phoneInput.value.trim(),
|
||||||
|
mobile: this.mobileInput.value.trim(),
|
||||||
|
address: this.addressInput.value.trim(),
|
||||||
|
postalCode: this.postalCodeInput.value.trim(),
|
||||||
|
city: this.cityInput.value.trim(),
|
||||||
|
country: this.countryInput.value.trim(),
|
||||||
|
website: this.websiteInput.value.trim(),
|
||||||
|
notes: this.notesInput.value.trim(),
|
||||||
|
tags: this.tagsInput.value.split(',').map(t => t.trim()).filter(Boolean)
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (contactId) {
|
||||||
|
// Update
|
||||||
|
await api.updateContact(contactId, contactData);
|
||||||
|
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||||
|
detail: { message: 'Kontakt aktualisiert', type: 'success' }
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Create
|
||||||
|
await api.createContact(contactData);
|
||||||
|
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||||
|
detail: { message: 'Kontakt erstellt', type: 'success' }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideContactModal();
|
||||||
|
await this.loadContacts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Contacts] Error saving contact:', error);
|
||||||
|
const errorMsg = error.response?.data?.errors?.[0] || 'Fehler beim Speichern';
|
||||||
|
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||||
|
detail: { message: errorMsg, type: 'error' }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteContact() {
|
||||||
|
const contactId = this.contactIdInput.value;
|
||||||
|
if (!contactId) return;
|
||||||
|
|
||||||
|
const contact = this.contacts.find(c => c.id === parseInt(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' }
|
||||||
|
}));
|
||||||
|
this.hideContactModal();
|
||||||
|
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' }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContactsUpdate(contacts) {
|
||||||
|
this.contacts = contacts;
|
||||||
|
this.updateTagsList();
|
||||||
|
this.filterContacts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
const contactsManager = new ContactsManager();
|
||||||
|
|
||||||
|
// Export instance for external access
|
||||||
|
export { contactsManager };
|
||||||
|
|
||||||
|
// Export functions
|
||||||
|
export async function initContacts() {
|
||||||
|
await contactsManager.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshContacts() {
|
||||||
|
return contactsManager.loadContacts();
|
||||||
|
}
|
||||||
@ -167,6 +167,9 @@ class KnowledgeManager {
|
|||||||
|
|
||||||
// Drag & Drop for entries
|
// Drag & Drop for entries
|
||||||
this.bindEntryDragEvents();
|
this.bindEntryDragEvents();
|
||||||
|
|
||||||
|
// Sidebar resize functionality
|
||||||
|
this.bindResizeEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -1143,6 +1146,79 @@ class KnowledgeManager {
|
|||||||
store.closeModal(modalId);
|
store.closeModal(modalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// SIDEBAR RESIZE
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
bindResizeEvents() {
|
||||||
|
// Use native DOM methods instead of $ utility
|
||||||
|
this.resizeHandle = document.getElementById('knowledge-resize-handle');
|
||||||
|
this.knowledgeLayoutContainer = document.querySelector('.knowledge-layout');
|
||||||
|
|
||||||
|
if (!this.resizeHandle || !this.knowledgeLayoutContainer) return;
|
||||||
|
|
||||||
|
// Load saved width from localStorage
|
||||||
|
const savedWidth = localStorage.getItem('knowledge-sidebar-width');
|
||||||
|
if (savedWidth) {
|
||||||
|
this.setSidebarWidth(parseInt(savedWidth));
|
||||||
|
}
|
||||||
|
|
||||||
|
let isResizing = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startWidth = 0;
|
||||||
|
|
||||||
|
this.resizeHandle.addEventListener('mousedown', (e) => {
|
||||||
|
isResizing = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startWidth = this.getCurrentSidebarWidth();
|
||||||
|
|
||||||
|
this.resizeHandle.classList.add('dragging');
|
||||||
|
this.knowledgeLayoutContainer.classList.add('resizing');
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', this.handleResize);
|
||||||
|
document.addEventListener('mouseup', this.handleResizeEnd);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handleResize = (e) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - startX;
|
||||||
|
const newWidth = Math.max(220, Math.min(800, startWidth + deltaX));
|
||||||
|
|
||||||
|
this.setSidebarWidth(newWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleResizeEnd = () => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
isResizing = false;
|
||||||
|
this.resizeHandle.classList.remove('dragging');
|
||||||
|
this.knowledgeLayoutContainer.classList.remove('resizing');
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', this.handleResize);
|
||||||
|
document.removeEventListener('mouseup', this.handleResizeEnd);
|
||||||
|
|
||||||
|
// Save current width to localStorage
|
||||||
|
const currentWidth = this.getCurrentSidebarWidth();
|
||||||
|
localStorage.setItem('knowledge-sidebar-width', currentWidth.toString());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentSidebarWidth() {
|
||||||
|
const computedStyle = getComputedStyle(this.knowledgeLayoutContainer);
|
||||||
|
const gridColumns = computedStyle.gridTemplateColumns;
|
||||||
|
const match = gridColumns.match(/(\d+)px/);
|
||||||
|
return match ? parseInt(match[1]) : 450;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSidebarWidth(width) {
|
||||||
|
if (this.knowledgeLayoutContainer) {
|
||||||
|
this.knowledgeLayoutContainer.style.gridTemplateColumns = `${width}px 1fr`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// UTILITIES
|
// UTILITIES
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@ -318,16 +318,19 @@ class MobileManager {
|
|||||||
// Don't swipe if modal is open
|
// Don't swipe if modal is open
|
||||||
if ($('.modal-overlay:not(.hidden)')) return;
|
if ($('.modal-overlay:not(.hidden)')) return;
|
||||||
|
|
||||||
// Don't swipe on scrollable elements
|
// Don't swipe on specific interactive elements, but allow swipe in column-body
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if (target.closest('.column-body') ||
|
if (target.closest('.modal') ||
|
||||||
target.closest('.modal') ||
|
|
||||||
target.closest('.calendar-grid') ||
|
target.closest('.calendar-grid') ||
|
||||||
target.closest('.knowledge-entry-list') ||
|
target.closest('.knowledge-entry-list') ||
|
||||||
target.closest('.list-table') ||
|
target.closest('.list-table') ||
|
||||||
target.closest('input') ||
|
target.closest('input') ||
|
||||||
target.closest('textarea') ||
|
target.closest('textarea') ||
|
||||||
target.closest('select')) {
|
target.closest('select') ||
|
||||||
|
target.closest('button') ||
|
||||||
|
target.closest('a[href]') ||
|
||||||
|
target.closest('.task-card .priority-stars') ||
|
||||||
|
target.closest('.task-card .task-counts')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,7 +375,24 @@ class MobileManager {
|
|||||||
// Prevent scroll
|
// Prevent scroll
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Show indicators
|
// Show indicators based on current view
|
||||||
|
if (this.currentView === 'board') {
|
||||||
|
// In board view: show column navigation indicators
|
||||||
|
const columns = $$('.column');
|
||||||
|
const currentColumnIndex = this.getCurrentColumnIndex();
|
||||||
|
|
||||||
|
if (deltaX > this.SWIPE_THRESHOLD && currentColumnIndex > 0) {
|
||||||
|
this.swipeIndicatorLeft?.classList.add('visible');
|
||||||
|
this.swipeIndicatorRight?.classList.remove('visible');
|
||||||
|
} else if (deltaX < -this.SWIPE_THRESHOLD && currentColumnIndex < columns.length - 1) {
|
||||||
|
this.swipeIndicatorRight?.classList.add('visible');
|
||||||
|
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||||
|
} else {
|
||||||
|
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||||
|
this.swipeIndicatorRight?.classList.remove('visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In other views: show view navigation indicators
|
||||||
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
||||||
|
|
||||||
if (deltaX > this.SWIPE_THRESHOLD && currentIndex > 0) {
|
if (deltaX > this.SWIPE_THRESHOLD && currentIndex > 0) {
|
||||||
@ -386,6 +406,7 @@ class MobileManager {
|
|||||||
this.swipeIndicatorRight?.classList.remove('visible');
|
this.swipeIndicatorRight?.classList.remove('visible');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle swipe end
|
* Handle swipe end
|
||||||
@ -404,6 +425,20 @@ class MobileManager {
|
|||||||
const isValidSwipe = Math.abs(deltaX) > this.SWIPE_THRESHOLD || velocity > this.SWIPE_VELOCITY_THRESHOLD;
|
const isValidSwipe = Math.abs(deltaX) > this.SWIPE_THRESHOLD || velocity > this.SWIPE_VELOCITY_THRESHOLD;
|
||||||
|
|
||||||
if (isValidSwipe) {
|
if (isValidSwipe) {
|
||||||
|
if (this.currentView === 'board') {
|
||||||
|
// In board view: navigate between columns
|
||||||
|
const columns = $$('.column');
|
||||||
|
const currentColumnIndex = this.getCurrentColumnIndex();
|
||||||
|
|
||||||
|
if (deltaX > 0 && currentColumnIndex > 0) {
|
||||||
|
// Swipe right - previous column
|
||||||
|
this.scrollToColumn(currentColumnIndex - 1);
|
||||||
|
} else if (deltaX < 0 && currentColumnIndex < columns.length - 1) {
|
||||||
|
// Swipe left - next column
|
||||||
|
this.scrollToColumn(currentColumnIndex + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In other views: navigate between views
|
||||||
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
||||||
|
|
||||||
if (deltaX > 0 && currentIndex > 0) {
|
if (deltaX > 0 && currentIndex > 0) {
|
||||||
@ -414,6 +449,7 @@ class MobileManager {
|
|||||||
this.switchView(this.viewOrder[currentIndex + 1]);
|
this.switchView(this.viewOrder[currentIndex + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.resetSwipe();
|
this.resetSwipe();
|
||||||
}
|
}
|
||||||
@ -434,6 +470,36 @@ class MobileManager {
|
|||||||
this.swipeIndicatorRight?.classList.remove('visible');
|
this.swipeIndicatorRight?.classList.remove('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current visible column index in mobile board view
|
||||||
|
*/
|
||||||
|
getCurrentColumnIndex() {
|
||||||
|
const boardContainer = $('.board-container');
|
||||||
|
if (!boardContainer) return 0;
|
||||||
|
|
||||||
|
const containerWidth = boardContainer.offsetWidth;
|
||||||
|
const scrollLeft = boardContainer.scrollLeft;
|
||||||
|
const columnWidth = 300; // Approximate column width in mobile
|
||||||
|
|
||||||
|
return Math.round(scrollLeft / columnWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to specific column in mobile board view
|
||||||
|
*/
|
||||||
|
scrollToColumn(columnIndex) {
|
||||||
|
const boardContainer = $('.board-container');
|
||||||
|
if (!boardContainer) return;
|
||||||
|
|
||||||
|
const columnWidth = 300; // Approximate column width in mobile
|
||||||
|
const targetScrollLeft = columnIndex * columnWidth;
|
||||||
|
|
||||||
|
boardContainer.scrollTo({
|
||||||
|
left: targetScrollLeft,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// TOUCH DRAG & DROP
|
// TOUCH DRAG & DROP
|
||||||
// =====================
|
// =====================
|
||||||
|
|||||||
556
frontend/js/reminders.js
Normale Datei
556
frontend/js/reminders.js
Normale Datei
@ -0,0 +1,556 @@
|
|||||||
|
/**
|
||||||
|
* TASKMATE - Reminders Module
|
||||||
|
* ===========================
|
||||||
|
* Erinnerungsmanagement
|
||||||
|
*/
|
||||||
|
|
||||||
|
import store from './store.js';
|
||||||
|
import api from './api.js';
|
||||||
|
import {
|
||||||
|
$, $$, createElement, clearElement, formatDate
|
||||||
|
} from './utils.js';
|
||||||
|
|
||||||
|
class ReminderManager {
|
||||||
|
constructor() {
|
||||||
|
this.modal = null;
|
||||||
|
this.form = null;
|
||||||
|
this.mode = 'create';
|
||||||
|
this.reminderId = null;
|
||||||
|
this.selectedColor = '#F59E0B';
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.modal = $('#reminder-modal');
|
||||||
|
this.form = $('#reminder-form');
|
||||||
|
|
||||||
|
// Wait for DOM to be ready, then bind events
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => this.bindEvents());
|
||||||
|
} else {
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for modal events
|
||||||
|
window.addEventListener('modal:open', (e) => {
|
||||||
|
if (e.detail.modalId === 'reminder-modal') {
|
||||||
|
this.open(e.detail.mode, e.detail.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('modal:close', (e) => {
|
||||||
|
if (e.detail.modalId === 'reminder-modal') {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also bind button when calendar view is loaded
|
||||||
|
window.addEventListener('app:view-changed', (e) => {
|
||||||
|
if (e.detail.view === 'calendar') {
|
||||||
|
setTimeout(() => this.bindCalendarButton(), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Form submission
|
||||||
|
this.form?.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||||
|
|
||||||
|
// Close buttons
|
||||||
|
$$('.modal-close', this.modal)?.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => this.close());
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btn-cancel-reminder')?.addEventListener('click', () => this.close());
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
$('#btn-delete-reminder')?.addEventListener('click', () => this.handleDelete());
|
||||||
|
|
||||||
|
// Color picker trigger
|
||||||
|
const colorTrigger = $('#color-picker-trigger');
|
||||||
|
const colorDropdown = $('#color-picker-dropdown');
|
||||||
|
|
||||||
|
if (colorTrigger) {
|
||||||
|
colorTrigger.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
colorDropdown.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color picker options
|
||||||
|
$$('.color-picker-dropdown .color-option').forEach(option => {
|
||||||
|
option.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const color = e.target.dataset.color;
|
||||||
|
|
||||||
|
// Update selected state
|
||||||
|
$$('.color-picker-dropdown .color-option').forEach(opt => opt.classList.remove('selected'));
|
||||||
|
e.target.classList.add('selected');
|
||||||
|
|
||||||
|
// Update trigger color and form value
|
||||||
|
colorTrigger.style.backgroundColor = color;
|
||||||
|
this.selectedColor = color;
|
||||||
|
$('#reminder-color').value = color;
|
||||||
|
|
||||||
|
// Close dropdown
|
||||||
|
colorDropdown.classList.add('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!e.target.closest('.color-picker-wrapper')) {
|
||||||
|
colorDropdown?.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup custom select for user dropdown
|
||||||
|
this.setupCustomSelect();
|
||||||
|
|
||||||
|
// Calendar button will be bound separately
|
||||||
|
this.bindCalendarButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindCalendarButton() {
|
||||||
|
const reminderBtn = $('#btn-new-reminder');
|
||||||
|
if (reminderBtn) {
|
||||||
|
console.log('[Reminder] Button found, binding event');
|
||||||
|
|
||||||
|
// Remove existing event listeners
|
||||||
|
const newBtn = reminderBtn.cloneNode(true);
|
||||||
|
reminderBtn.parentNode.replaceChild(newBtn, reminderBtn);
|
||||||
|
|
||||||
|
newBtn.addEventListener('click', (e) => {
|
||||||
|
console.log('[Reminder] Button clicked!');
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
console.log('[Reminder] Opening modal...');
|
||||||
|
console.log('[Reminder] Modal element:', this.modal);
|
||||||
|
|
||||||
|
this.open('create', {});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('[Reminder] Button #btn-new-reminder not found!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async open(mode = 'create', data = {}) {
|
||||||
|
console.log('[Reminder] open() called with mode:', mode, 'data:', data);
|
||||||
|
console.log('[Reminder] Modal exists:', !!this.modal);
|
||||||
|
|
||||||
|
this.mode = mode;
|
||||||
|
this.reminderId = data.reminderId || null;
|
||||||
|
|
||||||
|
// Find modal if not already set
|
||||||
|
if (!this.modal) {
|
||||||
|
this.modal = $('#reminder-modal');
|
||||||
|
console.log('[Reminder] Modal found on second attempt:', !!this.modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.modal) {
|
||||||
|
console.error('[Reminder] Modal element not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal title
|
||||||
|
const titleEl = $('#reminder-modal-title');
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.textContent = mode === 'edit' ? 'Erinnerung bearbeiten' : 'Neue Erinnerung';
|
||||||
|
console.log('[Reminder] Title set to:', titleEl.textContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button text
|
||||||
|
const saveBtn = $('#btn-save-reminder .btn-text');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.textContent = mode === 'edit' ? 'Aktualisieren' : 'Speichern';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide delete button
|
||||||
|
const deleteBtn = $('#btn-delete-reminder');
|
||||||
|
if (deleteBtn) {
|
||||||
|
if (mode === 'edit' && this.reminderId) {
|
||||||
|
deleteBtn.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
deleteBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resetForm();
|
||||||
|
await this.loadUsers(); // Benutzer laden
|
||||||
|
|
||||||
|
if (mode === 'edit' && this.reminderId) {
|
||||||
|
this.loadReminder();
|
||||||
|
} else if (data.prefill) {
|
||||||
|
this.prefillForm(data.prefill);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
console.log('[Reminder] Showing modal...');
|
||||||
|
|
||||||
|
// Create overlay if it doesn't exist
|
||||||
|
let overlay = document.querySelector('.modal-overlay');
|
||||||
|
if (!overlay) {
|
||||||
|
overlay = document.createElement('div');
|
||||||
|
overlay.className = 'modal-overlay';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show overlay first
|
||||||
|
overlay.classList.add('visible');
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
this.modal.classList.remove('hidden');
|
||||||
|
this.modal.classList.add('visible');
|
||||||
|
|
||||||
|
console.log('[Reminder] Modal and overlay should now be visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
// Hide modal
|
||||||
|
this.modal.classList.remove('visible');
|
||||||
|
setTimeout(() => this.modal.classList.add('hidden'), 200);
|
||||||
|
|
||||||
|
// Hide overlay
|
||||||
|
const overlay = document.querySelector('.modal-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.classList.remove('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resetForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
if (this.form) {
|
||||||
|
this.form.reset();
|
||||||
|
$('#reminder-time').value = '09:00';
|
||||||
|
|
||||||
|
// Reset color selection
|
||||||
|
$$('.color-picker-dropdown .color-option').forEach(opt => opt.classList.remove('selected'));
|
||||||
|
$$('.color-picker-dropdown .color-option[data-color="#F59E0B"]').forEach(opt => opt.classList.add('selected'));
|
||||||
|
const colorTrigger = $('#color-picker-trigger');
|
||||||
|
if (colorTrigger) {
|
||||||
|
colorTrigger.style.backgroundColor = '#F59E0B';
|
||||||
|
}
|
||||||
|
this.selectedColor = '#F59E0B';
|
||||||
|
$('#reminder-color').value = '#F59E0B';
|
||||||
|
|
||||||
|
// Reset advance days
|
||||||
|
$$('input[name="advance-days"]').forEach(cb => {
|
||||||
|
cb.checked = cb.value === '1';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefillForm(prefill) {
|
||||||
|
if (prefill.date) {
|
||||||
|
$('#reminder-date').value = prefill.date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadUsers() {
|
||||||
|
console.log('[Reminder] Loading users...');
|
||||||
|
let users = store.get('users') || [];
|
||||||
|
console.log('[Reminder] Users from store:', users);
|
||||||
|
|
||||||
|
// If no users in store, try to load them
|
||||||
|
if (users.length === 0) {
|
||||||
|
try {
|
||||||
|
console.log('[Reminder] No users in store, loading from API...');
|
||||||
|
const response = await api.request('/auth/users');
|
||||||
|
users = response.data || response;
|
||||||
|
console.log('[Reminder] Users from API:', users);
|
||||||
|
|
||||||
|
// Update store
|
||||||
|
if (users.length > 0) {
|
||||||
|
store.setUsers(users);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Reminder] Failed to load users:', error);
|
||||||
|
users = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionsContainer = $('#reminder-assignee-options');
|
||||||
|
|
||||||
|
if (!optionsContainer) {
|
||||||
|
console.warn('[Reminder] Assignee options container not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing options except first one
|
||||||
|
optionsContainer.innerHTML = `
|
||||||
|
<div class="custom-select-option" data-value="">
|
||||||
|
<span class="option-text">Alle Benutzer</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add user options with avatars
|
||||||
|
users.forEach(user => {
|
||||||
|
console.log('[Reminder] Adding user:', user);
|
||||||
|
|
||||||
|
// Get user info
|
||||||
|
const displayName = user.displayName || user.display_name || user.username || user.email;
|
||||||
|
const initials = user.initials || this.getInitials(displayName);
|
||||||
|
const color = user.color || '#6366F1';
|
||||||
|
|
||||||
|
// Create option element
|
||||||
|
const option = document.createElement('div');
|
||||||
|
option.className = 'custom-select-option';
|
||||||
|
option.dataset.value = user.id;
|
||||||
|
option.dataset.initials = initials;
|
||||||
|
option.dataset.color = color;
|
||||||
|
option.dataset.displayName = displayName;
|
||||||
|
|
||||||
|
option.innerHTML = `
|
||||||
|
<div class="option-avatar" style="background-color: ${color}">${initials}</div>
|
||||||
|
<span class="option-text">${displayName}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
optionsContainer.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup custom select behavior
|
||||||
|
this.setupCustomSelect();
|
||||||
|
|
||||||
|
console.log('[Reminder] Users loaded, total options:', optionsContainer.children.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCustomSelect() {
|
||||||
|
const wrapper = $('#reminder-assignee-wrapper');
|
||||||
|
const trigger = $('#reminder-assignee-trigger');
|
||||||
|
const options = $('#reminder-assignee-options');
|
||||||
|
const hiddenInput = $('#reminder-assignee');
|
||||||
|
|
||||||
|
if (!wrapper || !trigger || !options || !hiddenInput) {
|
||||||
|
console.log('[Reminder] Custom select elements not found, skipping setup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueDisplay = trigger.querySelector('.custom-select-value');
|
||||||
|
|
||||||
|
// Remove existing listeners by cloning elements
|
||||||
|
const newTrigger = trigger.cloneNode(true);
|
||||||
|
trigger.parentNode.replaceChild(newTrigger, trigger);
|
||||||
|
|
||||||
|
const newOptions = options.cloneNode(true);
|
||||||
|
options.parentNode.replaceChild(newOptions, options);
|
||||||
|
|
||||||
|
// Re-get elements after cloning
|
||||||
|
const freshTrigger = $('#reminder-assignee-trigger');
|
||||||
|
const freshOptions = $('#reminder-assignee-options');
|
||||||
|
const freshValueDisplay = freshTrigger.querySelector('.custom-select-value');
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
freshTrigger.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
wrapper.classList.toggle('open');
|
||||||
|
console.log('[Reminder] Dropdown toggled, open:', wrapper.classList.contains('open'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle option selection
|
||||||
|
freshOptions.addEventListener('click', (e) => {
|
||||||
|
const option = e.target.closest('.custom-select-option');
|
||||||
|
if (!option) return;
|
||||||
|
|
||||||
|
const value = option.dataset.value;
|
||||||
|
const displayName = option.dataset.displayName;
|
||||||
|
const initials = option.dataset.initials;
|
||||||
|
const color = option.dataset.color;
|
||||||
|
|
||||||
|
// Update hidden input
|
||||||
|
hiddenInput.value = value;
|
||||||
|
|
||||||
|
// Update display
|
||||||
|
if (value === '') {
|
||||||
|
freshValueDisplay.innerHTML = 'Alle Benutzer';
|
||||||
|
} else {
|
||||||
|
freshValueDisplay.innerHTML = `
|
||||||
|
<div class="selected-user-avatar" style="background-color: ${color}">${initials}</div>
|
||||||
|
<span>${displayName}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update selected state
|
||||||
|
freshOptions.querySelectorAll('.custom-select-option').forEach(opt => {
|
||||||
|
opt.classList.remove('selected');
|
||||||
|
});
|
||||||
|
option.classList.add('selected');
|
||||||
|
|
||||||
|
// Close dropdown
|
||||||
|
wrapper.classList.remove('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!wrapper.contains(e.target)) {
|
||||||
|
wrapper.classList.remove('open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadReminder() {
|
||||||
|
try {
|
||||||
|
const response = await api.request(`/reminders/${this.reminderId}`);
|
||||||
|
const reminder = response.data;
|
||||||
|
|
||||||
|
$('#reminder-title').value = reminder.title || '';
|
||||||
|
$('#reminder-description').value = reminder.description || '';
|
||||||
|
$('#reminder-date').value = reminder.reminder_date || '';
|
||||||
|
$('#reminder-time').value = reminder.reminder_time || '09:00';
|
||||||
|
$('#reminder-assignee').value = reminder.assigned_to || '';
|
||||||
|
|
||||||
|
// Set color
|
||||||
|
this.selectedColor = reminder.color || '#F59E0B';
|
||||||
|
$('#reminder-color').value = this.selectedColor;
|
||||||
|
$$('.color-option').forEach(opt => opt.classList.remove('selected'));
|
||||||
|
$$(`[data-color="${this.selectedColor}"]`).forEach(opt => opt.classList.add('selected'));
|
||||||
|
|
||||||
|
// Set advance days
|
||||||
|
const advanceDays = reminder.advance_days || ['1'];
|
||||||
|
$$('input[name="advance-days"]').forEach(cb => {
|
||||||
|
cb.checked = advanceDays.includes(cb.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading reminder:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const saveBtn = $('#btn-save-reminder');
|
||||||
|
const btnText = saveBtn.querySelector('.btn-text');
|
||||||
|
const btnLoading = saveBtn.querySelector('.btn-loading');
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
btnText.classList.add('hidden');
|
||||||
|
btnLoading.classList.remove('hidden');
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData(this.form);
|
||||||
|
|
||||||
|
// Get advance days
|
||||||
|
const advanceDays = [];
|
||||||
|
$$('input[name="advance-days"]:checked').forEach(cb => {
|
||||||
|
advanceDays.push(cb.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (advanceDays.length === 0) {
|
||||||
|
throw new Error('Bitte wählen Sie mindestens eine Erinnerungszeit aus');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
project_id: store.get('currentProjectId'),
|
||||||
|
title: formData.get('reminder-title') || $('#reminder-title').value,
|
||||||
|
description: $('#reminder-description').value || null,
|
||||||
|
reminder_date: $('#reminder-date').value,
|
||||||
|
reminder_time: $('#reminder-time').value,
|
||||||
|
assigned_to: $('#reminder-assignee').value || null,
|
||||||
|
color: this.selectedColor,
|
||||||
|
advance_days: advanceDays
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.mode === 'edit') {
|
||||||
|
await api.request(`/reminders/${this.reminderId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await api.request('/reminders', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update store and refresh calendar
|
||||||
|
if (this.mode === 'edit') {
|
||||||
|
const updatedData = await api.request(`/reminders/${this.reminderId}`);
|
||||||
|
store.updateReminder(this.reminderId, updatedData.data);
|
||||||
|
} else {
|
||||||
|
// Load all reminders to get the new one
|
||||||
|
const projectId = store.get('currentProjectId');
|
||||||
|
const allReminders = await api.getReminders(projectId);
|
||||||
|
store.setReminders(allReminders);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('app:refresh'));
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving reminder:', error);
|
||||||
|
alert(`Fehler beim Speichern: ${error.message || 'Unbekannter Fehler'}`);
|
||||||
|
} finally {
|
||||||
|
// Hide loading
|
||||||
|
btnText.classList.remove('hidden');
|
||||||
|
btnLoading.classList.add('hidden');
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper Methods
|
||||||
|
getInitials(name) {
|
||||||
|
if (!name) return '?';
|
||||||
|
|
||||||
|
return name
|
||||||
|
.split(' ')
|
||||||
|
.map(part => part.charAt(0).toUpperCase())
|
||||||
|
.slice(0, 2)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Methods
|
||||||
|
async getRemindersByProject(projectId) {
|
||||||
|
try {
|
||||||
|
const response = await api.request(`/reminders?project_id=${projectId}`);
|
||||||
|
return response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching reminders:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDelete() {
|
||||||
|
if (!this.reminderId) return;
|
||||||
|
|
||||||
|
const reminderTitle = $('#reminder-title').value || 'diese Erinnerung';
|
||||||
|
|
||||||
|
if (!confirm(`Möchten Sie "${reminderTitle}" wirklich löschen?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.deleteReminder(this.reminderId);
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
// Reload reminders
|
||||||
|
const projectId = store.get('currentProjectId');
|
||||||
|
const allReminders = await api.getReminders(projectId);
|
||||||
|
store.setReminders(allReminders);
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('app:refresh'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting reminder:', error);
|
||||||
|
alert('Fehler beim Löschen der Erinnerung. Bitte versuche es erneut.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteReminder(id) {
|
||||||
|
try {
|
||||||
|
await api.request(`/reminders/${id}`, { method: 'DELETE' });
|
||||||
|
window.dispatchEvent(new CustomEvent('app:refresh'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting reminder:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
const reminderManager = new ReminderManager();
|
||||||
|
export default reminderManager;
|
||||||
@ -27,6 +27,7 @@ class Store {
|
|||||||
columns: [],
|
columns: [],
|
||||||
tasks: [],
|
tasks: [],
|
||||||
labels: [],
|
labels: [],
|
||||||
|
reminders: [],
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
filters: {
|
filters: {
|
||||||
@ -363,6 +364,34 @@ class Store {
|
|||||||
}, 'REMOVE_LABEL');
|
}, 'REMOVE_LABEL');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// REMINDER ACTIONS
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
setReminders(reminders) {
|
||||||
|
this.setState({ reminders }, 'SET_REMINDERS');
|
||||||
|
}
|
||||||
|
|
||||||
|
addReminder(reminder) {
|
||||||
|
this.setState({
|
||||||
|
reminders: [...this.state.reminders, reminder]
|
||||||
|
}, 'ADD_REMINDER');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateReminder(reminderId, updates) {
|
||||||
|
this.setState({
|
||||||
|
reminders: this.state.reminders.map(r =>
|
||||||
|
r.id === reminderId ? { ...r, ...updates } : r
|
||||||
|
)
|
||||||
|
}, 'UPDATE_REMINDER');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeReminder(reminderId) {
|
||||||
|
this.setState({
|
||||||
|
reminders: this.state.reminders.filter(r => r.id !== reminderId)
|
||||||
|
}, 'REMOVE_REMINDER');
|
||||||
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// FILTER ACTIONS
|
// FILTER ACTIONS
|
||||||
// =====================
|
// =====================
|
||||||
|
|||||||
@ -252,6 +252,15 @@ class TaskModalManager {
|
|||||||
if (mode === 'edit' && this.taskId) {
|
if (mode === 'edit' && this.taskId) {
|
||||||
await this.loadTaskData();
|
await this.loadTaskData();
|
||||||
} else {
|
} else {
|
||||||
|
// Create mode - clear all UI elements and set defaults
|
||||||
|
|
||||||
|
// Clear UI elements (render empty arrays)
|
||||||
|
this.renderSubtasks();
|
||||||
|
this.renderLinks();
|
||||||
|
this.renderFiles();
|
||||||
|
this.renderComments();
|
||||||
|
this.renderHistory();
|
||||||
|
|
||||||
// Set default column
|
// Set default column
|
||||||
if (this.columnId) {
|
if (this.columnId) {
|
||||||
const columnSelect = $('#task-status');
|
const columnSelect = $('#task-status');
|
||||||
@ -800,6 +809,8 @@ class TaskModalManager {
|
|||||||
optionsContainer.innerHTML = '';
|
optionsContainer.innerHTML = '';
|
||||||
|
|
||||||
users.forEach(user => {
|
users.forEach(user => {
|
||||||
|
// DEBUG: User-Objekt anschauen
|
||||||
|
console.log('[DEBUG] User in Dropdown:', user);
|
||||||
const option = createElement('div', { class: 'multi-select-option' });
|
const option = createElement('div', { class: 'multi-select-option' });
|
||||||
|
|
||||||
const checkbox = createElement('input', {
|
const checkbox = createElement('input', {
|
||||||
@ -818,9 +829,12 @@ class TaskModalManager {
|
|||||||
const avatar = createElement('div', {
|
const avatar = createElement('div', {
|
||||||
class: 'multi-select-option-avatar',
|
class: 'multi-select-option-avatar',
|
||||||
style: `background-color: ${user.color || '#6366F1'}`
|
style: `background-color: ${user.color || '#6366F1'}`
|
||||||
}, [user.initials || getInitials(user.display_name || user.username)]);
|
}, [user.initials || getInitials(user.display_name || user.email || 'XX')]);
|
||||||
|
|
||||||
const name = createElement('span', { class: 'multi-select-option-name' }, [user.display_name || user.username]);
|
// Probiere verschiedene Felder für den Namen
|
||||||
|
const displayName = user.displayName || user.display_name || user.name || user.username || 'Benutzer';
|
||||||
|
console.log('[DEBUG] Display name für user', user.id, ':', displayName);
|
||||||
|
const name = createElement('span', { class: 'multi-select-option-name' }, [displayName]);
|
||||||
|
|
||||||
option.appendChild(checkbox);
|
option.appendChild(checkbox);
|
||||||
option.appendChild(avatar);
|
option.appendChild(avatar);
|
||||||
@ -876,10 +890,9 @@ class TaskModalManager {
|
|||||||
} else {
|
} else {
|
||||||
const tags = selectedUsers.map(user => `
|
const tags = selectedUsers.map(user => `
|
||||||
<span class="multi-select-tag">
|
<span class="multi-select-tag">
|
||||||
<span class="multi-select-tag-avatar" style="background-color: ${user.color || '#6366F1'}">
|
<span class="multi-select-tag-avatar" style="background-color: ${user.color || '#6366F1'}" title="${user.display_name || user.email}">
|
||||||
${getInitials(user.display_name || user.username)}
|
${user.initials || getInitials(user.display_name || user.email || 'XX')}
|
||||||
</span>
|
</span>
|
||||||
${user.display_name || user.username}
|
|
||||||
</span>
|
</span>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Offline support and caching
|
* Offline support and caching
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_VERSION = '197';
|
const CACHE_VERSION = '265';
|
||||||
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;
|
||||||
@ -41,6 +41,8 @@ const STATIC_ASSETS = [
|
|||||||
'/js/knowledge.js',
|
'/js/knowledge.js',
|
||||||
'/js/coding.js',
|
'/js/coding.js',
|
||||||
'/js/mobile.js',
|
'/js/mobile.js',
|
||||||
|
'/js/reminders.js',
|
||||||
|
'/js/contacts.js',
|
||||||
'/css/list.css',
|
'/css/list.css',
|
||||||
'/css/mobile.css',
|
'/css/mobile.css',
|
||||||
'/css/admin.css',
|
'/css/admin.css',
|
||||||
@ -48,7 +50,9 @@ const STATIC_ASSETS = [
|
|||||||
'/css/notifications.css',
|
'/css/notifications.css',
|
||||||
'/css/gitea.css',
|
'/css/gitea.css',
|
||||||
'/css/knowledge.css',
|
'/css/knowledge.css',
|
||||||
'/css/coding.css'
|
'/css/coding.css',
|
||||||
|
'/css/reminders.css',
|
||||||
|
'/css/contacts.css'
|
||||||
];
|
];
|
||||||
|
|
||||||
// API routes to cache
|
// API routes to cache
|
||||||
|
|||||||
15908
logs/app.log
15908
logs/app.log
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
1
test_login.json
Normale Datei
1
test_login.json
Normale Datei
@ -0,0 +1 @@
|
|||||||
|
{"username":"admin","password":"Kx9#mP2$vL7@nQ4!wR"}
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren