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'); const holidaysRouter = require('./routes/holidays');
app.use('/api/holidays', authenticateToken, holidaysRouter); 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 // Error handling middleware
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
console.error(err.stack); console.error(err.stack);

View File

@@ -539,21 +539,58 @@ class WorklogRepository {
* @param {Date} endDate - End-Datum * @param {Date} endDate - End-Datum
* @returns {Promise<Array>} Array von Holiday-Einträgen mit Datum und Stunden * @returns {Promise<Array>} Array von Holiday-Einträgen mit Datum und Stunden
*/ */
async getHolidaysInDateRange(startDate, endDate) { async getHolidaysInDateRange(startDate, endDate, userId = null) {
const { Holiday } = database.getModels(); const { Holiday, State, User } = database.getModels();
try { 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({ const holidays = await Holiday.findAll({
where: { where: {
date: { date: {
[Op.between]: [startDate, endDate] [Op.between]: [startDate, endDate]
} }
}, },
raw: true, include: [
{
model: State,
as: 'states',
attributes: ['id'],
through: { attributes: [] },
required: false
}
],
order: [['date', 'ASC']] 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) { } catch (error) {
console.error('Fehler beim Abrufen der Feiertage:', error); console.error('Fehler beim Abrufen der Feiertage:', error);
return []; 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 * @returns {Promise<Object>} Kalenderdaten
*/ */
async getCalendarMonth(userId, year, month) { async getCalendarMonth(userId, year, month) {
const { Holiday, Sick, Vacation } = database.getModels(); const { Holiday, Sick, Vacation, User, State } = database.getModels();
const sequelize = database.sequelize; 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 // Berechne Start- und End-Datum des Monats
const firstDay = new Date(year, month - 1, 1); const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0); const lastDay = new Date(year, month, 0);
@@ -36,19 +43,35 @@ class CalendarService {
const startDateStr = this._formatDate(startDate); const startDateStr = this._formatDate(startDate);
const endDateStr = this._formatDate(endDate); 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({ const holidays = await Holiday.findAll({
where: { where: {
date: { date: {
[Op.between]: [startDateStr, endDateStr] [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(); const holidayMap = new Map();
holidays.forEach(h => { 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 // 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) { async getStatistics(userId = null) {
const uid = userId || this.defaultUserId; 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 currentlyWorked = null;
let allEntries = []; let allEntries = [];
@@ -556,7 +561,7 @@ class TimeEntryService {
return true; return true;
}); });
let daySollHours = 8; // Standard: 8h let daySollHours = userDailyHours; // Fallback: User's daily_hours
if (applicableTimewish && applicableTimewish.wishtype === 2 && applicableTimewish.hours) { if (applicableTimewish && applicableTimewish.wishtype === 2 && applicableTimewish.hours) {
daySollHours = applicableTimewish.hours; daySollHours = applicableTimewish.hours;
} }
@@ -638,7 +643,7 @@ class TimeEntryService {
return true; return true;
}); });
let daySollHours = 8; let daySollHours = userDailyHours; // Fallback: User's daily_hours
if (applicableTimewish && applicableTimewish.wishtype === 2 && applicableTimewish.hours) { if (applicableTimewish && applicableTimewish.wishtype === 2 && applicableTimewish.hours) {
daySollHours = applicableTimewish.hours; daySollHours = applicableTimewish.hours;
} }
@@ -961,9 +966,10 @@ class TimeEntryService {
console.log('DEBUG _calculateTotalOvertime: Starte optimierte DB-basierte Berechnung...'); console.log('DEBUG _calculateTotalOvertime: Starte optimierte DB-basierte Berechnung...');
// Hole den Überstunden-Offset für diesen User // Hole den Überstunden-Offset und daily_hours für diesen User
const user = await User.findByPk(userId, { attributes: ['overtime_offset_minutes'], raw: true }); const user = await User.findByPk(userId, { attributes: ['overtime_offset_minutes', 'daily_hours'], raw: true });
const overtimeOffsetMinutes = user?.overtime_offset_minutes || 0; const overtimeOffsetMinutes = user?.overtime_offset_minutes || 0;
const userDailyHours = user?.daily_hours || 8;
if (overtimeOffsetMinutes !== 0) { if (overtimeOffsetMinutes !== 0) {
const offsetHours = Math.floor(Math.abs(overtimeOffsetMinutes) / 60); const offsetHours = Math.floor(Math.abs(overtimeOffsetMinutes) / 60);
@@ -1014,7 +1020,8 @@ class TimeEntryService {
return parseFloat(applicableTimewish.hours); return parseFloat(applicableTimewish.hours);
} }
return 8.0; // Fallback // Fallback: User's daily_hours
return parseFloat(userDailyHours);
}; };
// Berechne Endedatum: gestern (wie alte MySQL-Funktion) // Berechne Endedatum: gestern (wie alte MySQL-Funktion)
@@ -1073,17 +1080,43 @@ class TimeEntryService {
console.log(`DEBUG: ${workDays.length} Arbeitstage gefunden (mit timefix-Korrekturen)`); 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({ const holidays = await Holiday.findAll({
attributes: ['date'],
where: { where: {
date: { date: {
[require('sequelize').Op.lte]: endDateStr [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) // Hole Krankheitstage (expandiert)
const sickDaysRaw = await Sick.findAll({ const sickDaysRaw = await Sick.findAll({
@@ -1307,8 +1340,8 @@ class TimeEntryService {
sickMap.set(sickKey, sick); sickMap.set(sickKey, sick);
}); });
// Hole Feiertage für diese Woche // Hole Feiertage für diese Woche (nur die für das User-Bundesland gelten)
const holidays = await worklogRepository.getHolidaysInDateRange(weekStart, weekEnd); const holidays = await worklogRepository.getHolidaysInDateRange(weekStart, weekEnd, uid);
// Erstelle Map von Datum zu Holiday // Erstelle Map von Datum zu Holiday
const holidayMap = new Map(); const holidayMap = new Map();
@@ -1800,34 +1833,31 @@ class TimeEntryService {
Object.values(dayData).forEach(day => { Object.values(dayData).forEach(day => {
const dayOfWeek = new Date(day.date).getDay(); 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); const sick = sickMap.get(day.date);
if (sick) { if (sick) {
// Krankheitstage überschreiben geloggte Zeiten
day.sick = { day.sick = {
hours: 8, hours: 8,
type: sick.sick_type 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 = { const sickLabels = {
'self': 'Krank', 'self': 'Krank',
'child': 'Kind krank', 'child': 'Kind krank',
'parents': 'Eltern krank', 'parents': 'Eltern krank',
'partner': 'Partner krank' 'partner': 'Partner krank'
}; };
events.push(sickLabels[sick.sick_type] || 'Krank');
day.status = 'sick'; // Bei Krankheit: Lösche geloggte Arbeitszeit
day.statusText = sickLabels[sick.sick_type] || 'Krank'; day.workTime = null;
day.pauses = [];
return; // Überspringe weitere Verarbeitung day.totalWorkTime = null;
day.pauseTimes = [];
day.netWorkTime = null;
} }
// Prüfe auf Feiertag // Prüfe auf Feiertag
@@ -1837,48 +1867,52 @@ class TimeEntryService {
hours: holiday.hours, hours: holiday.hours,
description: holiday.description description: holiday.description
}; };
events.push(holiday.description || 'Feiertag');
// 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`;
}
} }
// Prüfe auf Urlaub (nur wenn kein Feiertag) // Prüfe auf Urlaub
if (!holiday) { const vacation = vacationMap.get(day.date);
const vacation = vacationMap.get(day.date); if (vacation) {
if (vacation) { const isHalfDay = vacation.half_day === 1;
const isHalfDay = vacation.half_day === 1; const vacationHours = isHalfDay ? 4 : 8;
const vacationHours = isHalfDay ? 4 : 8;
day.vacation = { day.vacation = {
hours: vacationHours, hours: vacationHours,
halfDay: isHalfDay halfDay: isHalfDay
}; };
events.push(isHalfDay ? 'Urlaub (halber Tag)' : 'Urlaub');
}
// Status setzen (kann später überschrieben werden wenn auch gearbeitet wurde) // Prüfe auf Wochenende
if (!day.workTime) { const isWeekend = (dayOfWeek === 0 || dayOfWeek === 6);
day.status = isHalfDay ? 'vacation-half' : 'vacation-full'; if (isWeekend) {
day.statusText = isHalfDay ? 'Urlaub (halber Tag)' : 'Urlaub'; events.push('Wochenende');
} else { }
// Urlaub + Arbeit
day.status = 'vacation-work'; // Setze Status basierend auf gesammelten Ereignissen
day.statusText = isHalfDay ? 'Urlaub (halber Tag) + Arbeit' : 'Urlaub + Arbeit'; 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';
} }
} }
} } else if (isWeekend) {
// Nur Wochenende, keine anderen Events
// Wochenenden (nur wenn kein Urlaub, Feiertag oder Arbeit) day.status = 'weekend';
if (dayOfWeek === 0 || dayOfWeek === 6) { day.statusText = 'Wochenende';
if (!day.status) {
day.status = 'weekend';
day.statusText = 'Wochenende';
}
} }
}); });

View File

@@ -17,13 +17,20 @@ class WorkdaysService {
* @returns {Promise<Object>} Statistiken * @returns {Promise<Object>} Statistiken
*/ */
async getWorkdaysStatistics(userId, year) { async getWorkdaysStatistics(userId, year) {
const { Holiday, Sick, Vacation } = database.getModels(); const { Holiday, Sick, Vacation, User, State } = database.getModels();
const sequelize = database.sequelize; 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 // Berechne Anzahl Werktage (Mo-Fr) im Jahr
const workdays = this._countWorkdays(year); 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({ const holidays = await Holiday.findAll({
where: { where: {
date: { date: {
@@ -31,14 +38,32 @@ class WorkdaysService {
[Op.lte]: `${year}-12-31` [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 holidayCount = holidays.filter(h => {
const holidayDate = new Date(h.date + 'T00:00:00'); const holidayDate = new Date(h.date + 'T00:00:00');
const dayOfWeek = holidayDate.getDay(); 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; }).length;
// Hole alle Krankheitstage für das Jahr // Hole alle Krankheitstage für das Jahr

View File

@@ -73,6 +73,7 @@ const pageTitle = computed(() => {
'workdays': 'Arbeitstage', 'workdays': 'Arbeitstage',
'calendar': 'Kalender', 'calendar': 'Kalender',
'admin-holidays': 'Feiertage', 'admin-holidays': 'Feiertage',
'settings-profile': 'Persönliches',
'entries': 'Einträge', 'entries': 'Einträge',
'stats': 'Statistiken' 'stats': 'Statistiken'
} }

View File

@@ -16,6 +16,7 @@ import Sick from '../views/Sick.vue'
import Workdays from '../views/Workdays.vue' import Workdays from '../views/Workdays.vue'
import Calendar from '../views/Calendar.vue' import Calendar from '../views/Calendar.vue'
import Holidays from '../views/Holidays.vue' import Holidays from '../views/Holidays.vue'
import Profile from '../views/Profile.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -98,6 +99,12 @@ const router = createRouter({
component: Holidays, component: Holidays,
meta: { requiresAuth: true, requiresAdmin: true } meta: { requiresAuth: true, requiresAdmin: true }
}, },
{
path: '/settings/profile',
name: 'settings-profile',
component: Profile,
meta: { requiresAuth: true }
},
{ {
path: '/entries', path: '/entries',
name: 'entries', name: 'entries',

View File

@@ -0,0 +1,326 @@
<template>
<div class="profile-page">
<div class="card">
<form @submit.prevent="saveProfile" class="profile-form">
<div class="form-group">
<label for="fullName">Voller Name (optional)</label>
<input
type="text"
id="fullName"
v-model="form.fullName"
placeholder="z.B. Max Mustermann"
>
</div>
<div class="form-group">
<label for="email">Email-Adresse</label>
<input
type="email"
id="email"
v-model="form.email"
placeholder="email@beispiel.de"
disabled
>
<small class="help-text">Die Email-Adresse kann derzeit nicht geändert werden</small>
</div>
<div class="form-group">
<label for="state">Bundesland</label>
<select
id="state"
v-model="form.stateId"
>
<option value="">Kein Bundesland ausgewählt</option>
<option
v-for="state in availableStates"
:key="state.id"
:value="state.id"
>
{{ state.name }}
</option>
</select>
</div>
<div class="form-group">
<label for="weekWorkdays">Arbeitstage pro Woche</label>
<input
type="number"
id="weekWorkdays"
v-model.number="form.weekWorkdays"
min="1"
max="7"
required
>
<small class="help-text">Standard: 5 (Montag bis Freitag)</small>
</div>
<div class="form-group">
<label for="dailyHours">Tägliche Arbeitsstunden (Grundwert)</label>
<input
type="number"
id="dailyHours"
v-model.number="form.dailyHours"
min="1"
max="24"
required
>
<small class="help-text">
Wird als Fallback verwendet, wenn kein spezifischer Zeitwunsch definiert ist.
Für detaillierte Zeitwünsche siehe <router-link to="/settings/timewish">Zeitwünsche</router-link>
</small>
</div>
<div class="form-group">
<label for="titleType">Anzeige in Seitentitel</label>
<select
id="titleType"
v-model.number="form.preferredTitleType"
>
<option :value="0">Restzeit für Tag ohne Korrektur</option>
<option :value="1">Restzeit für Tag, korrigiert nach Wochenarbeitszeit</option>
<option :value="2">Restzeit für Tag, korrigiert nach Gesamtarbeitszeit</option>
<option :value="3">Heute gearbeitete Zeit</option>
<option :value="4">Uhrzeit für Arbeitsende ohne Korrektur</option>
<option :value="5">Uhrzeit für Arbeitsende, korrigiert nach Wochenarbeitszeit</option>
<option :value="6">Uhrzeit für Arbeitsende, korrigiert nach Gesamtarbeitszeit</option>
<option :value="7">Nur App-Name</option>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="loading">
{{ loading ? 'Wird gespeichert...' : 'Einstellungen speichern' }}
</button>
</div>
</form>
</div>
<!-- Modal-Komponente -->
<Modal
v-if="showModal"
:show="showModal"
:title="modalConfig.title"
:message="modalConfig.message"
:type="modalConfig.type"
:confirmText="modalConfig.confirmText"
:cancelText="modalConfig.cancelText"
@confirm="onConfirm"
@cancel="onCancel"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useAuthStore } from '../stores/authStore'
import { useModal } from '../composables/useModal'
import Modal from '../components/Modal.vue'
const authStore = useAuthStore()
const availableStates = ref([])
const loading = ref(false)
const { showModal, modalConfig, alert, confirm, onConfirm, onCancel } = useModal()
const form = ref({
fullName: '',
email: '',
stateId: null,
weekWorkdays: 5,
dailyHours: 8,
preferredTitleType: 2
})
// Lade Profil
async function loadProfile() {
try {
loading.value = true
const response = await fetch('http://localhost:3010/api/profile', {
headers: {
'Authorization': `Bearer ${authStore.token}`
}
})
if (!response.ok) {
throw new Error('Fehler beim Laden des Profils')
}
const profile = await response.json()
form.value = {
fullName: profile.fullName || '',
email: profile.email || '',
stateId: profile.stateId || null,
weekWorkdays: profile.weekWorkdays || 5,
dailyHours: profile.dailyHours || 8,
preferredTitleType: profile.preferredTitleType || 2
}
} catch (error) {
console.error('Fehler beim Laden des Profils:', error)
await alert(`Fehler: ${error.message}`, 'Fehler')
} finally {
loading.value = false
}
}
// Lade Bundesländer
async function loadStates() {
try {
const response = await fetch('http://localhost:3010/api/profile/states', {
headers: {
'Authorization': `Bearer ${authStore.token}`
}
})
if (!response.ok) {
throw new Error('Fehler beim Laden der Bundesländer')
}
availableStates.value = await response.json()
} catch (error) {
console.error('Fehler beim Laden der Bundesländer:', error)
}
}
// Speichere Profil
async function saveProfile() {
try {
loading.value = true
const response = await fetch('http://localhost:3010/api/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authStore.token}`
},
body: JSON.stringify({
fullName: form.value.fullName,
stateId: form.value.stateId,
weekWorkdays: form.value.weekWorkdays,
dailyHours: form.value.dailyHours,
preferredTitleType: form.value.preferredTitleType
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || 'Fehler beim Speichern des Profils')
}
await alert('Einstellungen erfolgreich gespeichert', 'Erfolg')
} catch (error) {
console.error('Fehler beim Speichern des Profils:', error)
await alert(`Fehler: ${error.message}`, 'Fehler')
} finally {
loading.value = false
}
}
// Initiales Laden
onMounted(async () => {
await Promise.all([
loadStates(),
loadProfile()
])
})
</script>
<style scoped>
.profile-page {
max-width: 700px;
margin: 0 auto;
padding: 20px;
}
.card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.profile-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group label {
font-weight: 600;
font-size: 14px;
color: #333;
}
.form-group input,
.form-group select {
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
}
.form-group input:disabled {
background: #f5f5f5;
color: #999;
cursor: not-allowed;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
}
.help-text {
font-size: 12px;
color: #666;
font-style: italic;
}
.help-text a {
color: #4CAF50;
text-decoration: none;
}
.help-text a:hover {
text-decoration: underline;
}
.form-actions {
margin-top: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: linear-gradient(135deg, #4CAF50, #45a049);
color: white;
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(76, 175, 80, 0.4);
}
</style>