Add profile routes to backend and frontend; implement user state handling for holidays and workdays, and update UI components for profile settings

This commit is contained in:
Torsten Schulz (local)
2025-10-17 23:03:29 +02:00
parent 67ddf812cd
commit b2cef4d306
11 changed files with 738 additions and 76 deletions

View File

@@ -0,0 +1,62 @@
const ProfileService = require('../services/ProfileService');
/**
* Controller für Benutzer-Profil
* Verarbeitet HTTP-Requests und delegiert an ProfileService
*/
class ProfileController {
/**
* Holt das Benutzerprofil
*/
async getProfile(req, res) {
try {
const userId = req.user?.id || 1;
const profile = await ProfileService.getProfile(userId);
res.json(profile);
} catch (error) {
console.error('Fehler beim Abrufen des Profils:', error);
res.status(500).json({
message: 'Fehler beim Abrufen des Profils',
error: error.message
});
}
}
/**
* Aktualisiert das Benutzerprofil
*/
async updateProfile(req, res) {
try {
const userId = req.user?.id || 1;
const updates = req.body;
const profile = await ProfileService.updateProfile(userId, updates);
res.json(profile);
} catch (error) {
console.error('Fehler beim Aktualisieren des Profils:', error);
res.status(500).json({
message: 'Fehler beim Aktualisieren des Profils',
error: error.message
});
}
}
/**
* Holt alle verfügbaren Bundesländer
*/
async getStates(req, res) {
try {
const states = await ProfileService.getAllStates();
res.json(states);
} catch (error) {
console.error('Fehler beim Abrufen der Bundesländer:', error);
res.status(500).json({
message: 'Fehler beim Abrufen der Bundesländer',
error: error.message
});
}
}
}
module.exports = new ProfileController();

View File

@@ -90,6 +90,10 @@ app.use('/api/calendar', authenticateToken, calendarRouter);
const holidaysRouter = require('./routes/holidays');
app.use('/api/holidays', authenticateToken, holidaysRouter);
// Profile routes (geschützt) - MIT ID-Hashing
const profileRouter = require('./routes/profile');
app.use('/api/profile', authenticateToken, profileRouter);
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);

View File

@@ -539,21 +539,58 @@ class WorklogRepository {
* @param {Date} endDate - End-Datum
* @returns {Promise<Array>} Array von Holiday-Einträgen mit Datum und Stunden
*/
async getHolidaysInDateRange(startDate, endDate) {
const { Holiday } = database.getModels();
async getHolidaysInDateRange(startDate, endDate, userId = null) {
const { Holiday, State, User } = database.getModels();
try {
// Hole User-Bundesland (falls userId angegeben)
let userStateId = null;
if (userId) {
const user = await User.findByPk(userId, {
attributes: ['state_id'],
raw: true
});
userStateId = user?.state_id;
}
const holidays = await Holiday.findAll({
where: {
date: {
[Op.between]: [startDate, endDate]
}
},
raw: true,
include: [
{
model: State,
as: 'states',
attributes: ['id'],
through: { attributes: [] },
required: false
}
],
order: [['date', 'ASC']]
});
return holidays;
// Filtere nach User-Bundesland (falls userId angegeben)
if (userId && userStateId) {
return holidays.filter(h => {
const holidayStates = h.states || [];
const isFederal = holidayStates.length === 0;
const appliesToUser = isFederal || holidayStates.some(s => s.id === userStateId);
return appliesToUser;
}).map(h => ({
date: h.date,
hours: h.hours,
description: h.description
}));
}
// Ohne userId: Alle Feiertage zurückgeben
return holidays.map(h => ({
date: h.date,
hours: h.hours,
description: h.description
}));
} catch (error) {
console.error('Fehler beim Abrufen der Feiertage:', error);
return [];

View File

@@ -0,0 +1,19 @@
const express = require('express');
const router = express.Router();
const ProfileController = require('../controllers/ProfileController');
/**
* Routen für Benutzer-Profil
*/
// GET /api/profile/states - Alle Bundesländer abrufen
router.get('/states', ProfileController.getStates.bind(ProfileController));
// GET /api/profile - Profil abrufen
router.get('/', ProfileController.getProfile.bind(ProfileController));
// PUT /api/profile - Profil aktualisieren
router.put('/', ProfileController.updateProfile.bind(ProfileController));
module.exports = router;

View File

@@ -18,9 +18,16 @@ class CalendarService {
* @returns {Promise<Object>} Kalenderdaten
*/
async getCalendarMonth(userId, year, month) {
const { Holiday, Sick, Vacation } = database.getModels();
const { Holiday, Sick, Vacation, User, State } = database.getModels();
const sequelize = database.sequelize;
// Hole User-Bundesland
const user = await User.findByPk(userId, {
attributes: ['state_id'],
raw: true
});
const userStateId = user?.state_id;
// Berechne Start- und End-Datum des Monats
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
@@ -36,19 +43,35 @@ class CalendarService {
const startDateStr = this._formatDate(startDate);
const endDateStr = this._formatDate(endDate);
// Hole alle Feiertage
// Hole alle Feiertage für diesen Zeitraum UND das Bundesland des Users
// Feiertag gilt wenn: Kein State (Bundesfeiertag) ODER User's State ist dabei
const holidays = await Holiday.findAll({
where: {
date: {
[Op.between]: [startDateStr, endDateStr]
}
},
raw: true
include: [
{
model: State,
as: 'states',
attributes: ['id', 'state_name'],
through: { attributes: [] },
required: false // LEFT JOIN, damit Bundesfeiertage (ohne States) auch dabei sind
}
]
});
const holidayMap = new Map();
holidays.forEach(h => {
holidayMap.set(h.date, h.description);
// Feiertag gilt wenn: Keine States (Bundesfeiertag) ODER User's State ist dabei
const holidayStates = h.states || [];
const isFederal = holidayStates.length === 0;
const appliesToUser = isFederal || holidayStates.some(s => s.id === userStateId);
if (appliesToUser) {
holidayMap.set(h.date, h.description);
}
});
// Hole alle Krankheitstage

View File

@@ -0,0 +1,124 @@
const database = require('../config/database');
/**
* Service-Klasse für Benutzer-Profil-Einstellungen
*/
class ProfileService {
/**
* Holt das Benutzerprofil
* @param {number} userId - Benutzer-ID
* @returns {Promise<Object>} Profildaten
*/
async getProfile(userId) {
const { User, State, AuthInfo } = database.getModels();
const user = await User.findByPk(userId, {
include: [
{
model: State,
as: 'state',
attributes: ['id', 'state_name']
},
{
model: AuthInfo,
as: 'authInfo',
attributes: ['email']
}
]
});
if (!user) {
throw new Error('Benutzer nicht gefunden');
}
return {
id: user.id,
fullName: user.full_name,
email: user.authInfo?.email || null,
stateId: user.state_id,
stateName: user.state?.state_name || null,
weekWorkdays: user.week_workdays,
dailyHours: user.daily_hours,
preferredTitleType: user.preferred_title_type
};
}
/**
* Aktualisiert das Benutzerprofil
* @param {number} userId - Benutzer-ID
* @param {Object} updates - Zu aktualisierende Felder
* @returns {Promise<Object>} Aktualisierte Profildaten
*/
async updateProfile(userId, updates) {
const { User, State } = database.getModels();
const user = await User.findByPk(userId);
if (!user) {
throw new Error('Benutzer nicht gefunden');
}
// Aktualisiere erlaubte Felder
if (updates.fullName !== undefined) {
user.full_name = updates.fullName;
}
if (updates.stateId !== undefined) {
// Prüfe ob State existiert
if (updates.stateId) {
const state = await State.findByPk(updates.stateId);
if (!state) {
throw new Error('Ungültiges Bundesland');
}
}
user.state_id = updates.stateId;
}
if (updates.weekWorkdays !== undefined) {
if (updates.weekWorkdays < 1 || updates.weekWorkdays > 7) {
throw new Error('Arbeitstage pro Woche müssen zwischen 1 und 7 liegen');
}
user.week_workdays = updates.weekWorkdays;
}
if (updates.dailyHours !== undefined) {
if (updates.dailyHours < 1 || updates.dailyHours > 24) {
throw new Error('Tägliche Arbeitsstunden müssen zwischen 1 und 24 liegen');
}
user.daily_hours = updates.dailyHours;
}
if (updates.preferredTitleType !== undefined) {
if (updates.preferredTitleType < 0 || updates.preferredTitleType > 7) {
throw new Error('Ungültiger Seitentitel-Typ');
}
user.preferred_title_type = updates.preferredTitleType;
}
await user.save();
// Lade aktualisiertes Profil
return this.getProfile(userId);
}
/**
* Holt alle verfügbaren Bundesländer
* @returns {Promise<Array>} Array von States
*/
async getAllStates() {
const { State } = database.getModels();
const states = await State.findAll({
order: [['state_name', 'ASC']],
raw: true
});
return states.map(s => ({
id: s.id,
name: s.state_name
}));
}
}
module.exports = new ProfileService();

View File

@@ -212,6 +212,11 @@ class TimeEntryService {
async getStatistics(userId = null) {
const uid = userId || this.defaultUserId;
// Hole User-Daten (für daily_hours Fallback)
const { User } = database.getModels();
const user = await User.findByPk(uid, { attributes: ['daily_hours'], raw: true });
const userDailyHours = user?.daily_hours || 8;
let currentlyWorked = null;
let allEntries = [];
@@ -556,7 +561,7 @@ class TimeEntryService {
return true;
});
let daySollHours = 8; // Standard: 8h
let daySollHours = userDailyHours; // Fallback: User's daily_hours
if (applicableTimewish && applicableTimewish.wishtype === 2 && applicableTimewish.hours) {
daySollHours = applicableTimewish.hours;
}
@@ -638,7 +643,7 @@ class TimeEntryService {
return true;
});
let daySollHours = 8;
let daySollHours = userDailyHours; // Fallback: User's daily_hours
if (applicableTimewish && applicableTimewish.wishtype === 2 && applicableTimewish.hours) {
daySollHours = applicableTimewish.hours;
}
@@ -961,9 +966,10 @@ class TimeEntryService {
console.log('DEBUG _calculateTotalOvertime: Starte optimierte DB-basierte Berechnung...');
// Hole den Überstunden-Offset für diesen User
const user = await User.findByPk(userId, { attributes: ['overtime_offset_minutes'], raw: true });
// Hole den Überstunden-Offset und daily_hours für diesen User
const user = await User.findByPk(userId, { attributes: ['overtime_offset_minutes', 'daily_hours'], raw: true });
const overtimeOffsetMinutes = user?.overtime_offset_minutes || 0;
const userDailyHours = user?.daily_hours || 8;
if (overtimeOffsetMinutes !== 0) {
const offsetHours = Math.floor(Math.abs(overtimeOffsetMinutes) / 60);
@@ -1014,7 +1020,8 @@ class TimeEntryService {
return parseFloat(applicableTimewish.hours);
}
return 8.0; // Fallback
// Fallback: User's daily_hours
return parseFloat(userDailyHours);
};
// Berechne Endedatum: gestern (wie alte MySQL-Funktion)
@@ -1073,17 +1080,43 @@ class TimeEntryService {
console.log(`DEBUG: ${workDays.length} Arbeitstage gefunden (mit timefix-Korrekturen)`);
// Hole Feiertage
// Hole User-Bundesland
const { State: StateModel } = database.getModels();
const userForState = await User.findByPk(userId, {
attributes: ['state_id'],
raw: true
});
const userStateId = userForState?.state_id;
// Hole Feiertage mit States
const holidays = await Holiday.findAll({
attributes: ['date'],
where: {
date: {
[require('sequelize').Op.lte]: endDateStr
}
},
raw: true
include: [
{
model: StateModel,
as: 'states',
attributes: ['id'],
through: { attributes: [] },
required: false
}
]
});
// Filtere nur Feiertage die für den User gelten
const holidaySet = new Set();
holidays.forEach(h => {
const holidayStates = h.states || [];
const isFederal = holidayStates.length === 0;
const appliesToUser = isFederal || holidayStates.some(s => s.id === userStateId);
if (appliesToUser) {
holidaySet.add(h.date);
}
});
const holidaySet = new Set(holidays.map(h => h.date));
// Hole Krankheitstage (expandiert)
const sickDaysRaw = await Sick.findAll({
@@ -1307,8 +1340,8 @@ class TimeEntryService {
sickMap.set(sickKey, sick);
});
// Hole Feiertage für diese Woche
const holidays = await worklogRepository.getHolidaysInDateRange(weekStart, weekEnd);
// Hole Feiertage für diese Woche (nur die für das User-Bundesland gelten)
const holidays = await worklogRepository.getHolidaysInDateRange(weekStart, weekEnd, uid);
// Erstelle Map von Datum zu Holiday
const holidayMap = new Map();
@@ -1800,34 +1833,31 @@ class TimeEntryService {
Object.values(dayData).forEach(day => {
const dayOfWeek = new Date(day.date).getDay();
// Prüfe auf Krankheit (hat höchste Priorität, überschreibt alles außer Feiertage)
// Sammle alle Ereignisse für diesen Tag
const events = [];
// Prüfe auf Krankheit
const sick = sickMap.get(day.date);
if (sick) {
// Krankheitstage überschreiben geloggte Zeiten
day.sick = {
hours: 8,
type: sick.sick_type
};
// Lösche geloggte Arbeitszeit
day.workTime = null;
day.pauses = [];
day.totalWorkTime = null;
day.pauseTimes = [];
day.netWorkTime = null;
// Setze Status basierend auf Krankheitstyp
const sickLabels = {
'self': 'Krank',
'child': 'Kind krank',
'parents': 'Eltern krank',
'partner': 'Partner krank'
};
events.push(sickLabels[sick.sick_type] || 'Krank');
day.status = 'sick';
day.statusText = sickLabels[sick.sick_type] || 'Krank';
return; // Überspringe weitere Verarbeitung
// Bei Krankheit: Lösche geloggte Arbeitszeit
day.workTime = null;
day.pauses = [];
day.totalWorkTime = null;
day.pauseTimes = [];
day.netWorkTime = null;
}
// Prüfe auf Feiertag
@@ -1837,48 +1867,52 @@ class TimeEntryService {
hours: holiday.hours,
description: holiday.description
};
// Status setzen
if (!day.workTime) {
day.status = 'holiday';
day.statusText = holiday.description || 'Feiertag';
} else {
// Feiertag + Arbeit
day.status = 'holiday-work';
day.statusText = `${holiday.description || 'Feiertag'} + Arbeit`;
}
events.push(holiday.description || 'Feiertag');
}
// Prüfe auf Urlaub (nur wenn kein Feiertag)
if (!holiday) {
const vacation = vacationMap.get(day.date);
if (vacation) {
const isHalfDay = vacation.half_day === 1;
const vacationHours = isHalfDay ? 4 : 8;
day.vacation = {
hours: vacationHours,
halfDay: isHalfDay
};
// Status setzen (kann später überschrieben werden wenn auch gearbeitet wurde)
if (!day.workTime) {
day.status = isHalfDay ? 'vacation-half' : 'vacation-full';
day.statusText = isHalfDay ? 'Urlaub (halber Tag)' : 'Urlaub';
} else {
// Urlaub + Arbeit
day.status = 'vacation-work';
day.statusText = isHalfDay ? 'Urlaub (halber Tag) + Arbeit' : 'Urlaub + Arbeit';
// Prüfe auf Urlaub
const vacation = vacationMap.get(day.date);
if (vacation) {
const isHalfDay = vacation.half_day === 1;
const vacationHours = isHalfDay ? 4 : 8;
day.vacation = {
hours: vacationHours,
halfDay: isHalfDay
};
events.push(isHalfDay ? 'Urlaub (halber Tag)' : 'Urlaub');
}
// Prüfe auf Wochenende
const isWeekend = (dayOfWeek === 0 || dayOfWeek === 6);
if (isWeekend) {
events.push('Wochenende');
}
// Setze Status basierend auf gesammelten Ereignissen
if (events.length > 0) {
if (day.workTime) {
// Ereignis(se) + Arbeit
day.statusText = events.join(' + ') + ' + Arbeit';
day.status = 'mixed-work';
} else {
// Nur Ereignis(se), keine Arbeit
day.statusText = events.join(' + ');
// Status basierend auf primärem Ereignis
if (sick) {
day.status = 'sick';
} else if (holiday) {
day.status = 'holiday';
} else if (vacation) {
day.status = vacation.halfDay ? 'vacation-half' : 'vacation-full';
} else if (isWeekend) {
day.status = 'weekend';
}
}
}
// Wochenenden (nur wenn kein Urlaub, Feiertag oder Arbeit)
if (dayOfWeek === 0 || dayOfWeek === 6) {
if (!day.status) {
day.status = 'weekend';
day.statusText = 'Wochenende';
}
} else if (isWeekend) {
// Nur Wochenende, keine anderen Events
day.status = 'weekend';
day.statusText = 'Wochenende';
}
});

View File

@@ -17,13 +17,20 @@ class WorkdaysService {
* @returns {Promise<Object>} Statistiken
*/
async getWorkdaysStatistics(userId, year) {
const { Holiday, Sick, Vacation } = database.getModels();
const { Holiday, Sick, Vacation, User, State } = database.getModels();
const sequelize = database.sequelize;
// Hole User-Bundesland
const user = await User.findByPk(userId, {
attributes: ['state_id'],
raw: true
});
const userStateId = user?.state_id;
// Berechne Anzahl Werktage (Mo-Fr) im Jahr
const workdays = this._countWorkdays(year);
// Hole alle Feiertage für das Jahr
// Hole alle Feiertage für das Jahr mit States
const holidays = await Holiday.findAll({
where: {
date: {
@@ -31,14 +38,32 @@ class WorkdaysService {
[Op.lte]: `${year}-12-31`
}
},
raw: true
include: [
{
model: State,
as: 'states',
attributes: ['id'],
through: { attributes: [] },
required: false
}
]
});
// Zähle nur Feiertage die auf Werktage fallen
// Zähle nur Feiertage die auf Werktage fallen UND für den User gelten
const holidayCount = holidays.filter(h => {
const holidayDate = new Date(h.date + 'T00:00:00');
const dayOfWeek = holidayDate.getDay();
return dayOfWeek >= 1 && dayOfWeek <= 5; // Mo-Fr
if (dayOfWeek < 1 || dayOfWeek > 5) {
return false; // Kein Werktag
}
// Feiertag gilt wenn: Keine States (Bundesfeiertag) ODER User's State ist dabei
const holidayStates = h.states || [];
const isFederal = holidayStates.length === 0;
const appliesToUser = isFederal || holidayStates.some(s => s.id === userStateId);
return appliesToUser;
}).length;
// Hole alle Krankheitstage für das Jahr