diff --git a/backend/MIGRATION_timewish_dates.md b/backend/MIGRATION_timewish_dates.md new file mode 100644 index 0000000..ce7189c --- /dev/null +++ b/backend/MIGRATION_timewish_dates.md @@ -0,0 +1,55 @@ +# Migration: Timewish-Tabelle erweitern + +## Problem +Die Produktions-Datenbank fehlen die Spalten `start_date` und `end_date` in der `timewish` Tabelle. +Dies führt zu Fehler 500 bei `/api/time-entries/stats/summary`. + +## Fehler-Log +``` +ER_BAD_FIELD_ERROR: Unknown column 'start_date' in 'field list' +``` + +## Lösung +Führe das Migrations-Skript auf dem Produktionsserver aus: + +```bash +# 1. Auf den Server verbinden +ssh ihr-server + +# 2. Zum Backend-Verzeichnis wechseln +cd /var/www/timeclock/backend + +# 3. Migration ausführen +mysql -u timeclock -p timeclock < add-timewish-dates.sql + +# Oder mit root-User: +sudo mysql timeclock < add-timewish-dates.sql +``` + +## Was macht das Skript? +1. Prüft ob die Spalten bereits existieren +2. Fügt `start_date` hinzu (DEFAULT '2000-01-01') +3. Fügt `end_date` hinzu (DEFAULT NULL = unbegrenzt gültig) +4. Zeigt die neue Tabellenstruktur an + +## Nach der Migration +Alle bestehenden timewish-Einträge erhalten: +- `start_date` = '2000-01-01' (gilt seit langem) +- `end_date` = NULL (gilt unbegrenzt) + +Das entspricht dem bisherigen Verhalten. + +## Testen +Nach der Migration sollte dieser API-Call erfolgreich sein: +```bash +curl http://localhost:3010/api/time-entries/stats/summary \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +## Rollback (falls nötig) +Falls Probleme auftreten: +```sql +ALTER TABLE timewish DROP COLUMN start_date; +ALTER TABLE timewish DROP COLUMN end_date; +``` + diff --git a/backend/add-timewish-dates.sql b/backend/add-timewish-dates.sql new file mode 100644 index 0000000..9452e0f --- /dev/null +++ b/backend/add-timewish-dates.sql @@ -0,0 +1,48 @@ +-- Migration: Füge start_date und end_date zu timewish Tabelle hinzu +-- Datum: 2025-10-20 +-- Beschreibung: Erweitert die timewish Tabelle um Gültigkeitsbereiche + +-- Prüfe ob Spalten bereits existieren (für Sicherheit) +SET @col_exists = ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'timewish' + AND COLUMN_NAME = 'start_date' +); + +-- Füge start_date hinzu, falls nicht vorhanden +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE `timewish` ADD COLUMN `start_date` DATE NOT NULL DEFAULT "2000-01-01" COMMENT "Ab welchem Datum gilt dieser Timewish"', + 'SELECT "Spalte start_date existiert bereits" AS Info' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Prüfe ob end_date bereits existiert +SET @col_exists = ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'timewish' + AND COLUMN_NAME = 'end_date' +); + +-- Füge end_date hinzu, falls nicht vorhanden +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE `timewish` ADD COLUMN `end_date` DATE DEFAULT NULL COMMENT "Bis welchem Datum gilt dieser Timewish (NULL = unbegrenzt)"', + 'SELECT "Spalte end_date existiert bereits" AS Info' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Zeige Ergebnis +SELECT + 'Migration abgeschlossen!' AS Status, + 'start_date und end_date zur timewish Tabelle hinzugefügt' AS Details; + +-- Zeige aktuelle Tabellenstruktur +DESCRIBE timewish; + diff --git a/backend/src/controllers/AuthController.js b/backend/src/controllers/AuthController.js index ff48451..7497f4b 100644 --- a/backend/src/controllers/AuthController.js +++ b/backend/src/controllers/AuthController.js @@ -40,7 +40,7 @@ class AuthController { */ async login(req, res) { try { - const { email, password } = req.body; + const { email, password, action } = req.body; if (!email || !password) { return res.status(400).json({ @@ -49,14 +49,21 @@ class AuthController { }); } - const result = await authService.login(email, password); + const result = await authService.login(email, password, action); - res.json({ + const response = { success: true, message: 'Login erfolgreich', token: result.token, user: result.user - }); + }; + + // Füge Warnung hinzu, falls vorhanden + if (result.actionWarning) { + response.actionWarning = result.actionWarning; + } + + res.json(response); } catch (error) { console.error('Login-Fehler:', error); diff --git a/backend/src/services/AuthService.js b/backend/src/services/AuthService.js index 1635bc0..7332d20 100644 --- a/backend/src/services/AuthService.js +++ b/backend/src/services/AuthService.js @@ -124,9 +124,10 @@ class AuthService { * Benutzer einloggen * @param {string} email - E-Mail-Adresse * @param {string} password - Passwort + * @param {string} action - Gewünschte Aktion nach Login (optional: '0', '1', '2', '3', '4') * @returns {Promise} Token und Benutzer-Info */ - async login(email, password) { + async login(email, password, action = '0') { const { User, AuthInfo, AuthToken } = database.getModels(); console.log('Login-Versuch für E-Mail:', email); @@ -211,7 +212,7 @@ class AuthService { version: 0 }); - return { + const result = { token, user: { id: authInfo.user.id, @@ -220,6 +221,112 @@ class AuthService { role: authInfo.user.role } }; + + // Führe gewünschte Aktion aus (falls angegeben und gültig) + if (action && action !== '0') { + try { + const actionWarning = await this._executeLoginAction(authInfo.user.id, action); + if (actionWarning) { + result.actionWarning = actionWarning; + } + } catch (error) { + console.error('Fehler beim Ausführen der Login-Aktion:', error); + result.actionWarning = `Aktion konnte nicht ausgeführt werden: ${error.message}`; + } + } + + return result; + } + + /** + * Führt die gewünschte Aktion nach dem Login aus + * @param {number} userId - Benutzer-ID + * @param {string} action - Gewünschte Aktion ('1', '2', '3', '4') + * @returns {Promise} Warnung, falls Aktion nicht ausgeführt werden konnte + * @private + */ + async _executeLoginAction(userId, action) { + const timeEntryService = require('./TimeEntryService'); + + // Hole den aktuellen Status + const currentState = await timeEntryService.getCurrentState(userId); + + console.log(`Login-Aktion angefordert: ${action}, aktueller Status: ${currentState || 'null'}`); + + // Mapping: action-Nummer -> Worklog-Action + const actionMap = { + '1': 'start work', // Arbeit beginnen + '2': 'start pause', // Pause beginnen + '3': 'stop pause', // Pause beenden + '4': 'stop work' // Feierabend + }; + + const worklogAction = actionMap[action]; + if (!worklogAction) { + return 'Ungültige Aktion'; + } + + // Validiere, ob die Aktion erlaubt ist + const isValid = this._isValidLoginAction(currentState, worklogAction); + + if (!isValid) { + const actionNames = { + 'start work': 'Arbeit beginnen', + 'start pause': 'Pause beginnen', + 'stop pause': 'Pause beenden', + 'stop work': 'Feierabend' + }; + + const currentStateNames = { + 'start work': 'Arbeit läuft', + 'stop work': 'Feierabend', + 'start pause': 'Pause läuft', + 'stop pause': 'Pause beendet', + 'null': 'Keine Aktion' + }; + + const actionName = actionNames[worklogAction] || worklogAction; + const currentStateName = currentStateNames[currentState || 'null'] || currentState; + + return `Aktion "${actionName}" kann nicht ausgeführt werden (aktueller Status: ${currentStateName})`; + } + + // Führe die Aktion aus + try { + await timeEntryService.clock(userId, worklogAction); + console.log(`Login-Aktion erfolgreich ausgeführt: ${worklogAction}`); + return null; // Kein Fehler + } catch (error) { + console.error('Fehler beim Ausführen der Login-Aktion:', error); + return `Aktion konnte nicht ausgeführt werden: ${error.message}`; + } + } + + /** + * Validiert, ob eine Login-Aktion erlaubt ist + * @param {string|null} currentState - Aktueller Status + * @param {string} action - Gewünschte Aktion + * @returns {boolean} True, wenn die Aktion erlaubt ist + * @private + */ + _isValidLoginAction(currentState, action) { + // Erlaubte Übergänge basierend auf den Anforderungen: + // 1. Arbeit beginnen: user hat noch gar keine Aktion, oder letzte Aktion war stop work + // 2. Pause beginnen: letzte Aktion "start work" oder "stop pause" + // 3. Pause beenden: letzte Aktion "start pause" + // 4. Feierabend: letzte Aktion "start work" oder "stop pause" + + const allowedTransitions = { + 'start work': ['null', 'stop work'], + 'start pause': ['start work', 'stop pause'], + 'stop pause': ['start pause'], + 'stop work': ['start work', 'stop pause'] + }; + + const allowed = allowedTransitions[action] || []; + const stateToCheck = currentState || 'null'; + + return allowed.includes(stateToCheck); } /** diff --git a/backend/src/services/TimeEntryService.js b/backend/src/services/TimeEntryService.js index 0b12cab..6061461 100644 --- a/backend/src/services/TimeEntryService.js +++ b/backend/src/services/TimeEntryService.js @@ -1069,7 +1069,7 @@ class TimeEntryService { AND w1.user_id = ? AND DATE(COALESCE(w1_fix.fix_date_time, w1.tstamp)) <= ? AND DAYOFWEEK(DATE(COALESCE(w1_fix.fix_date_time, w1.tstamp))) BETWEEN 2 AND 6 - GROUP BY DATE(COALESCE(w1_fix.fix_date_time, w1.tstamp)) + GROUP BY DATE(COALESCE(w1_fix.fix_date_time, w1.tstamp)), DAYOFWEEK(DATE(COALESCE(w1_fix.fix_date_time, w1.tstamp))) ORDER BY DATE(COALESCE(w1_fix.fix_date_time, w1.tstamp)) `; diff --git a/frontend/src/components/StatusBox.vue b/frontend/src/components/StatusBox.vue index 959506d..7c75408 100644 --- a/frontend/src/components/StatusBox.vue +++ b/frontend/src/components/StatusBox.vue @@ -39,7 +39,9 @@ import { onMounted, onBeforeUnmount, ref, computed } from 'vue' import { useTimeStore } from '../stores/timeStore' import { useAuthStore } from '../stores/authStore' +import { API_BASE_URL } from '@/config/api' +const API_URL = API_BASE_URL const timeStore = useTimeStore() const authStore = useAuthStore() const stats = ref({}) @@ -92,7 +94,7 @@ const fetchStats = async () => { const fetchCurrentState = async () => { try { - const response = await fetch('${API_URL}/time-entries/current-state', { + const response = await fetch(`${API_URL}/time-entries/current-state`, { headers: authStore.getAuthHeaders() }) @@ -108,7 +110,7 @@ const fetchCurrentState = async () => { // Lade die aktuellen Worklog-Daten (nur einmal pro Minute) const fetchWorklogData = async () => { try { - const response = await fetch('${API_URL}/time-entries/running', { + const response = await fetch(`${API_URL}/time-entries/running`, { headers: authStore.getAuthHeaders() }) @@ -277,7 +279,7 @@ const handleAction = async (action) => { try { loading.value = true - const response = await fetch('${API_URL}/time-entries/clock', { + const response = await fetch(`${API_URL}/time-entries/clock`, { method: 'POST', headers: { ...authStore.getAuthHeaders(), @@ -341,6 +343,14 @@ const rightButton = computed(() => { } }) +// Event-Handler für Login +const handleLoginCompleted = async () => { + console.log('DEBUG: Login completed, lade Daten neu...') + await fetchCurrentState() + await fetchWorklogData() + await fetchStats() +} + onMounted(async () => { // Initiales Laden await fetchCurrentState() @@ -359,11 +369,15 @@ onMounted(async () => { updateCurrentlyWorkedTime() updateOpenTime() }, 500) + + // Event-Listener für Login + window.addEventListener('login-completed', handleLoginCompleted) }) onBeforeUnmount(() => { if (dataFetchInterval) clearInterval(dataFetchInterval) if (displayUpdateInterval) clearInterval(displayUpdateInterval) + window.removeEventListener('login-completed', handleLoginCompleted) }) const displayRows = computed(() => { diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index cf7f215..0e12f86 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -131,13 +131,26 @@ const handleLogin = async () => { try { error.value = '' - await authStore.login({ + const result = await authStore.login({ email: loginForm.value.email, - password: loginForm.value.password + password: loginForm.value.password, + action: loginAction.value }) - // Nach erfolgreichem Login zum Dashboard - router.push('/') + // Event auslösen, damit StatusBox sich aktualisiert + window.dispatchEvent(new CustomEvent('login-completed')) + + // Zeige Warnung, falls Aktion nicht ausgeführt werden konnte + if (result.actionWarning) { + error.value = result.actionWarning + // Trotzdem zum Dashboard weiterleiten (nach kurzer Verzögerung) + setTimeout(() => { + router.push('/') + }, 2000) + } else { + // Nach erfolgreichem Login zum Dashboard + router.push('/') + } } catch (err) { error.value = err.message || 'Login fehlgeschlagen. Bitte überprüfen Sie Ihre Eingaben.' } diff --git a/frontend/src/views/WeekOverview.vue b/frontend/src/views/WeekOverview.vue index 0a95c25..88972a8 100644 --- a/frontend/src/views/WeekOverview.vue +++ b/frontend/src/views/WeekOverview.vue @@ -190,6 +190,9 @@