diff --git a/backend/src/controllers/TimewishController.js b/backend/src/controllers/TimewishController.js new file mode 100644 index 0000000..aced206 --- /dev/null +++ b/backend/src/controllers/TimewishController.js @@ -0,0 +1,73 @@ +const TimewishService = require('../services/TimewishService'); + +/** + * Controller für Zeitwünsche + * Verarbeitet HTTP-Requests und delegiert an TimewishService + */ +class TimewishController { + /** + * Holt alle Zeitwünsche + */ + async getAllTimewishes(req, res) { + try { + const userId = req.user?.id || 1; + const timewishes = await TimewishService.getAllTimewishes(userId); + res.json(timewishes); + } catch (error) { + console.error('Fehler beim Abrufen der Zeitwünsche:', error); + res.status(500).json({ + message: 'Fehler beim Abrufen der Zeitwünsche', + error: error.message + }); + } + } + + /** + * Erstellt einen neuen Zeitwunsch + */ + async createTimewish(req, res) { + try { + const userId = req.user?.id || 1; + const { day, wishtype, hours, startDate, endDate } = req.body; + + if (day === undefined || wishtype === undefined || !startDate) { + return res.status(400).json({ + message: 'Wochentag, Typ und Startdatum sind erforderlich' + }); + } + + const timewish = await TimewishService.createTimewish(userId, day, wishtype, hours, startDate, endDate); + res.status(201).json(timewish); + } catch (error) { + console.error('Fehler beim Erstellen des Zeitwunsches:', error); + res.status(error.message.includes('Überschneidung') ? 409 : 500).json({ + message: error.message + }); + } + } + + /** + * Löscht einen Zeitwunsch + */ + async deleteTimewish(req, res) { + try { + const userId = req.user?.id || 1; + const timewishId = parseInt(req.params.id); + + if (isNaN(timewishId)) { + return res.status(400).json({ message: 'Ungültige ID' }); + } + + await TimewishService.deleteTimewish(userId, timewishId); + res.json({ message: 'Zeitwunsch gelöscht' }); + } catch (error) { + console.error('Fehler beim Löschen des Zeitwunsches:', error); + res.status(error.message.includes('nicht gefunden') ? 404 : 500).json({ + message: error.message + }); + } + } +} + +module.exports = new TimewishController(); + diff --git a/backend/src/index.js b/backend/src/index.js index 6af350f..1d9b598 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -98,6 +98,10 @@ app.use('/api/profile', authenticateToken, profileRouter); const passwordRouter = require('./routes/password'); app.use('/api/password', authenticateToken, passwordRouter); +// Timewish routes (geschützt) - MIT ID-Hashing +const timewishRouter = require('./routes/timewish'); +app.use('/api/timewish', authenticateToken, timewishRouter); + // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); diff --git a/backend/src/routes/timewish.js b/backend/src/routes/timewish.js new file mode 100644 index 0000000..d7d15be --- /dev/null +++ b/backend/src/routes/timewish.js @@ -0,0 +1,20 @@ +const express = require('express'); +const router = express.Router(); +const TimewishController = require('../controllers/TimewishController'); +const unhashRequestIds = require('../middleware/unhashRequest'); + +/** + * Routen für Zeitwünsche + */ + +// GET /api/timewish - Alle Zeitwünsche abrufen +router.get('/', TimewishController.getAllTimewishes.bind(TimewishController)); + +// POST /api/timewish - Neuen Zeitwunsch erstellen +router.post('/', unhashRequestIds, TimewishController.createTimewish.bind(TimewishController)); + +// DELETE /api/timewish/:id - Zeitwunsch löschen +router.delete('/:id', unhashRequestIds, TimewishController.deleteTimewish.bind(TimewishController)); + +module.exports = router; + diff --git a/backend/src/services/TimewishService.js b/backend/src/services/TimewishService.js new file mode 100644 index 0000000..6fc59c4 --- /dev/null +++ b/backend/src/services/TimewishService.js @@ -0,0 +1,167 @@ +const database = require('../config/database'); +const { Op } = require('sequelize'); + +/** + * Service-Klasse für Zeitwünsche (Timewish) + * Verwaltet gewünschte Arbeitszeiten pro Wochentag und Zeitraum + */ +class TimewishService { + /** + * Holt alle Zeitwünsche eines Users + * @param {number} userId - Benutzer-ID + * @returns {Promise} Array von Zeitwünschen + */ + async getAllTimewishes(userId) { + const { Timewish } = database.getModels(); + + const timewishes = await Timewish.findAll({ + where: { + user_id: userId + }, + order: [['day', 'ASC'], ['start_date', 'DESC']], + raw: true + }); + + // Konvertiere DB-Format zu Frontend-Format + const dayNames = { + 1: 'Montag', + 2: 'Dienstag', + 3: 'Mittwoch', + 4: 'Donnerstag', + 5: 'Freitag', + 6: 'Samstag', + 7: 'Sonntag' + }; + + const wishtypeNames = { + 0: 'Kein Wunsch', + 1: 'Frei', + 2: 'Arbeit' + }; + + return timewishes.map(tw => ({ + id: tw.id, + day: tw.day, + dayName: dayNames[tw.day] || `Tag ${tw.day}`, + wishtype: tw.wishtype, + wishtypeName: wishtypeNames[tw.wishtype] || 'Unbekannt', + hours: tw.hours, + startDate: tw.start_date, + endDate: tw.end_date + })); + } + + /** + * Erstellt einen neuen Zeitwunsch + * @param {number} userId - Benutzer-ID + * @param {number} day - Wochentag (1=Mo, 7=So) + * @param {number} wishtype - Typ (0=Kein Wunsch, 1=Frei, 2=Arbeit) + * @param {number} hours - Stunden (nur bei wishtype=2) + * @param {string} startDate - Startdatum (YYYY-MM-DD) + * @param {string} endDate - Enddatum (YYYY-MM-DD, optional) + * @returns {Promise} Erstellter Zeitwunsch + */ + async createTimewish(userId, day, wishtype, hours, startDate, endDate) { + const { Timewish } = database.getModels(); + + // Validierung + if (day < 1 || day > 7) { + throw new Error('Wochentag muss zwischen 1 (Montag) und 7 (Sonntag) liegen'); + } + + if (wishtype < 0 || wishtype > 2) { + throw new Error('Ungültiger Wunschtyp'); + } + + if (wishtype === 2 && (!hours || hours < 0 || hours > 24)) { + throw new Error('Arbeitsstunden müssen zwischen 0 und 24 liegen'); + } + + if (!startDate) { + throw new Error('Startdatum ist erforderlich'); + } + + // Prüfe auf Überschneidungen + const overlapping = await Timewish.findOne({ + where: { + user_id: userId, + day: day, + [Op.or]: [ + // Fall 1: Neuer Eintrag hat kein Enddatum (gilt ab start_date) + endDate ? {} : { + [Op.or]: [ + // Bestehender Eintrag hat kein Enddatum UND start_date <= neuer start_date + { + end_date: null, + start_date: { [Op.lte]: startDate } + }, + // Bestehender Eintrag hat Enddatum UND überlappt + { + end_date: { [Op.gte]: startDate } + } + ] + }, + // Fall 2: Neuer Eintrag hat Enddatum + endDate ? { + [Op.or]: [ + // Bestehender Eintrag hat kein Enddatum UND start_date <= neues end_date + { + end_date: null, + start_date: { [Op.lte]: endDate } + }, + // Bestehender Eintrag hat Enddatum UND überlappt + { + start_date: { [Op.lte]: endDate }, + end_date: { [Op.gte]: startDate } + } + ] + } : {} + ] + } + }); + + if (overlapping) { + throw new Error(`Überschneidung mit bestehendem Zeitwunsch: ${overlapping.start_date} - ${overlapping.end_date || 'unbegrenzt'}`); + } + + const timewish = await Timewish.create({ + user_id: userId, + day, + wishtype, + hours: wishtype === 2 ? hours : null, + start_date: startDate, + end_date: endDate || null, + version: 0 + }); + + // Formatiere für Response + const allTimewishes = await this.getAllTimewishes(userId); + return allTimewishes.find(tw => tw.id === timewish.id); + } + + /** + * Löscht einen Zeitwunsch + * @param {number} userId - Benutzer-ID + * @param {number} timewishId - Timewish-ID + * @returns {Promise} + */ + async deleteTimewish(userId, timewishId) { + const { Timewish } = database.getModels(); + + const timewish = await Timewish.findByPk(timewishId); + + if (!timewish) { + throw new Error('Zeitwunsch nicht gefunden'); + } + + // Prüfe Berechtigung + if (timewish.user_id !== userId) { + throw new Error('Keine Berechtigung für diesen Eintrag'); + } + + await timewish.destroy(); + } +} + +module.exports = new TimewishService(); + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 44abd7f..4cfef9d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -75,6 +75,7 @@ const pageTitle = computed(() => { 'admin-holidays': 'Feiertage', 'settings-profile': 'Persönliches', 'settings-password': 'Passwort ändern', + 'settings-timewish': 'Zeitwünsche', 'entries': 'Einträge', 'stats': 'Statistiken' } diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 88a9ece..a4dc582 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -18,6 +18,7 @@ import Calendar from '../views/Calendar.vue' import Holidays from '../views/Holidays.vue' import Profile from '../views/Profile.vue' import PasswordChange from '../views/PasswordChange.vue' +import Timewish from '../views/Timewish.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -112,6 +113,12 @@ const router = createRouter({ component: PasswordChange, meta: { requiresAuth: true } }, + { + path: '/settings/timewish', + name: 'settings-timewish', + component: Timewish, + meta: { requiresAuth: true } + }, { path: '/entries', name: 'entries', diff --git a/frontend/src/views/Timewish.vue b/frontend/src/views/Timewish.vue new file mode 100644 index 0000000..3a6ed7a --- /dev/null +++ b/frontend/src/views/Timewish.vue @@ -0,0 +1,478 @@ + + + + + +