Enhance login functionality in AuthController and AuthService; add optional action parameter to login method, execute corresponding actions post-login, and handle action warnings. Update frontend components to trigger data refresh on successful login and display warnings if actions fail. Adjust SQL query in TimeEntryService for improved grouping.

This commit is contained in:
Torsten Schulz (local)
2025-10-20 07:48:53 +02:00
parent e55f20367d
commit 648a94c4da
8 changed files with 261 additions and 14 deletions

View File

@@ -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;
```

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<Object>} 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<string|null>} 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);
}
/**

View File

@@ -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))
`;

View File

@@ -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(() => {

View File

@@ -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
})
// 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.'
}

View File

@@ -190,6 +190,9 @@
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useAuthStore } from '../stores/authStore'
import { API_BASE_URL } from '@/config/api'
const API_URL = API_BASE_URL
const authStore = useAuthStore()