Add holidays routes to backend and frontend; implement holiday associations and update UI components for admin holidays management

This commit is contained in:
Torsten Schulz (local)
2025-10-17 22:35:31 +02:00
parent 6a0b23e694
commit 67ddf812cd
15 changed files with 1058 additions and 2 deletions

View File

@@ -76,6 +76,7 @@ class Database {
const State = require('../models/State');
const WeeklyWorktime = require('../models/WeeklyWorktime');
const Holiday = require('../models/Holiday');
const HolidayState = require('../models/HolidayState');
const Vacation = require('../models/Vacation');
const Sick = require('../models/Sick');
const SickType = require('../models/SickType');
@@ -91,6 +92,7 @@ class Database {
State.initialize(this.sequelize);
WeeklyWorktime.initialize(this.sequelize);
Holiday.initialize(this.sequelize);
HolidayState.initialize(this.sequelize);
Vacation.initialize(this.sequelize);
Sick.initialize(this.sequelize);
SickType.initialize(this.sequelize);
@@ -107,7 +109,7 @@ class Database {
* Model-Assoziationen definieren
*/
defineAssociations() {
const { User, Worklog, AuthInfo, AuthToken, AuthIdentity, State, WeeklyWorktime, Vacation, Sick, SickType, Timefix, Timewish } = this.sequelize.models;
const { User, Worklog, AuthInfo, AuthToken, AuthIdentity, State, WeeklyWorktime, Holiday, HolidayState, Vacation, Sick, SickType, Timefix, Timewish } = this.sequelize.models;
// User Assoziationen
User.hasMany(Worklog, { foreignKey: 'user_id', as: 'worklogs' });
@@ -151,6 +153,25 @@ class Database {
// SickType Assoziationen
SickType.hasMany(Sick, { foreignKey: 'sick_type_id', as: 'sickLeaves' });
// Holiday Assoziationen (Many-to-Many mit State)
Holiday.belongsToMany(State, {
through: HolidayState,
foreignKey: 'holiday_id',
otherKey: 'state_id',
as: 'states'
});
State.belongsToMany(Holiday, {
through: HolidayState,
foreignKey: 'state_id',
otherKey: 'holiday_id',
as: 'holidays'
});
// HolidayState Assoziationen
HolidayState.belongsTo(Holiday, { foreignKey: 'holiday_id', as: 'holiday' });
HolidayState.belongsTo(State, { foreignKey: 'state_id', as: 'state' });
// Timewish Assoziationen
Timewish.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
User.hasMany(Timewish, { foreignKey: 'user_id', as: 'timewishes' });

View File

@@ -0,0 +1,86 @@
const HolidayService = require('../services/HolidayService');
/**
* Controller für Feiertage
* Verarbeitet HTTP-Requests und delegiert an HolidayService
*/
class HolidayController {
/**
* Holt alle Bundesländer
*/
async getAllStates(req, res) {
try {
const states = await HolidayService.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
});
}
}
/**
* Holt alle Feiertage
*/
async getAllHolidays(req, res) {
try {
const holidays = await HolidayService.getAllHolidays();
res.json(holidays);
} catch (error) {
console.error('Fehler beim Abrufen der Feiertage:', error);
res.status(500).json({
message: 'Fehler beim Abrufen der Feiertage',
error: error.message
});
}
}
/**
* Erstellt einen neuen Feiertag
*/
async createHoliday(req, res) {
try {
const { date, hours, description, stateIds } = req.body;
if (!date || !description) {
return res.status(400).json({
message: 'Datum und Beschreibung sind erforderlich'
});
}
const holiday = await HolidayService.createHoliday(date, hours, description, stateIds);
res.status(201).json(holiday);
} catch (error) {
console.error('Fehler beim Erstellen des Feiertags:', error);
res.status(error.message.includes('existiert bereits') ? 409 : 500).json({
message: error.message
});
}
}
/**
* Löscht einen Feiertag
*/
async deleteHoliday(req, res) {
try {
const holidayId = parseInt(req.params.id);
if (isNaN(holidayId)) {
return res.status(400).json({ message: 'Ungültige ID' });
}
await HolidayService.deleteHoliday(holidayId);
res.json({ message: 'Feiertag gelöscht' });
} catch (error) {
console.error('Fehler beim Löschen des Feiertags:', error);
res.status(error.message.includes('nicht gefunden') ? 404 : 500).json({
message: error.message
});
}
}
}
module.exports = new HolidayController();

View File

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

View File

@@ -42,6 +42,19 @@ class Holiday extends Model {
return Holiday;
}
/**
* Definiert Assoziationen mit anderen Models
*/
static associate(models) {
// Holiday hat viele States (Many-to-Many über HolidayState)
Holiday.belongsToMany(models.State, {
through: models.HolidayState,
foreignKey: 'holiday_id',
otherKey: 'state_id',
as: 'states'
});
}
}
module.exports = Holiday;

View File

@@ -0,0 +1,69 @@
const { Model, DataTypes } = require('sequelize');
/**
* HolidayState Model
* Junction Table für Many-to-Many Beziehung zwischen Holiday und State
*/
class HolidayState extends Model {
static initialize(sequelize) {
HolidayState.init(
{
holiday_id: {
type: DataTypes.BIGINT,
primaryKey: true,
allowNull: false,
references: {
model: 'holiday',
key: 'id'
}
},
state_id: {
type: DataTypes.BIGINT,
primaryKey: true,
allowNull: false,
references: {
model: 'state',
key: 'id'
}
}
},
{
sequelize,
tableName: 'holiday_state',
timestamps: false,
indexes: [
{
name: 'fk_holiday_state_holiday',
fields: ['holiday_id']
},
{
name: 'fk_holiday_state_state',
fields: ['state_id']
}
]
}
);
return HolidayState;
}
/**
* Definiert Assoziationen mit anderen Models
*/
static associate(models) {
// HolidayState gehört zu einem Holiday
HolidayState.belongsTo(models.Holiday, {
foreignKey: 'holiday_id',
as: 'holiday'
});
// HolidayState gehört zu einem State
HolidayState.belongsTo(models.State, {
foreignKey: 'state_id',
as: 'state'
});
}
}
module.exports = HolidayState;

View File

@@ -33,6 +33,19 @@ class State extends Model {
return State;
}
/**
* Definiert Assoziationen mit anderen Models
*/
static associate(models) {
// State hat viele Holidays (Many-to-Many über HolidayState)
State.belongsToMany(models.Holiday, {
through: models.HolidayState,
foreignKey: 'state_id',
otherKey: 'holiday_id',
as: 'holidays'
});
}
}
module.exports = State;

View File

@@ -99,6 +99,24 @@ class User extends Model {
getWeeklyHours() {
return this.week_hours;
}
/**
* Rolle als String zurückgeben
* 0 = 'user', 1 = 'admin'
*/
getRoleString() {
return this.role === 1 ? 'admin' : 'user';
}
/**
* JSON-Serialisierung überschreiben
*/
toJSON() {
const values = { ...this.get() };
// Füge role_string hinzu für Frontend
values.role_string = this.getRoleString();
return values;
}
}
module.exports = User;

View File

@@ -9,6 +9,7 @@ const AuthInfo = require('./AuthInfo');
const State = require('./State');
const WeeklyWorktime = require('./WeeklyWorktime');
const Holiday = require('./Holiday');
const HolidayState = require('./HolidayState');
const Vacation = require('./Vacation');
const Sick = require('./Sick');
const SickType = require('./SickType');
@@ -22,6 +23,7 @@ module.exports = {
State,
WeeklyWorktime,
Holiday,
HolidayState,
Vacation,
Sick,
SickType,

View File

@@ -0,0 +1,23 @@
const express = require('express');
const router = express.Router();
const HolidayController = require('../controllers/HolidayController');
const unhashRequestIds = require('../middleware/unhashRequest');
/**
* Routen für Feiertage (nur für Admins)
*/
// GET /api/holidays/states - Alle Bundesländer abrufen
router.get('/states', HolidayController.getAllStates.bind(HolidayController));
// GET /api/holidays - Alle Feiertage abrufen
router.get('/', HolidayController.getAllHolidays.bind(HolidayController));
// POST /api/holidays - Neuen Feiertag erstellen
router.post('/', HolidayController.createHoliday.bind(HolidayController));
// DELETE /api/holidays/:id - Feiertag löschen
router.delete('/:id', unhashRequestIds, HolidayController.deleteHoliday.bind(HolidayController));
module.exports = router;

View File

@@ -0,0 +1,180 @@
const database = require('../config/database');
const { Op } = require('sequelize');
/**
* Service-Klasse für Feiertage
* Verwaltet bundesweite und regionale Feiertage
*/
class HolidayService {
/**
* 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
}));
}
/**
* Holt alle Feiertage, aufgeteilt in zukünftige und vergangene
* Vergangene: Nur aktuelles Jahr und maximal 3 Monate zurück
* @returns {Promise<Object>} { future: [], past: [] }
*/
async getAllHolidays() {
const { Holiday, State, HolidayState } = database.getModels();
const today = new Date();
const currentYear = today.getFullYear();
const todayStr = `${currentYear}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
// Berechne Startdatum für vergangene Feiertage
const threeMonthsAgo = new Date(today);
threeMonthsAgo.setMonth(today.getMonth() - 3);
const yearStart = new Date(currentYear, 0, 1);
const pastStartDate = yearStart < threeMonthsAgo ? yearStart : threeMonthsAgo;
const pastStartDateStr = `${pastStartDate.getFullYear()}-${String(pastStartDate.getMonth() + 1).padStart(2, '0')}-${String(pastStartDate.getDate()).padStart(2, '0')}`;
// Zukünftige Feiertage (inkl. heute) mit States
const futureHolidays = await Holiday.findAll({
where: {
date: {
[Op.gte]: todayStr
}
},
include: [
{
model: State,
as: 'states',
attributes: ['id', 'state_name'],
through: { attributes: [] } // Keine Attribute der Junction Table
}
],
order: [['date', 'ASC']]
});
// Vergangene Feiertage (nur aktuelles Jahr / letzte 3 Monate) mit States
const pastHolidays = await Holiday.findAll({
where: {
date: {
[Op.gte]: pastStartDateStr,
[Op.lt]: todayStr
}
},
include: [
{
model: State,
as: 'states',
attributes: ['id', 'state_name'],
through: { attributes: [] }
}
],
order: [['date', 'DESC']]
});
return {
future: futureHolidays.map(h => this._formatHoliday(h)),
past: pastHolidays.map(h => this._formatHoliday(h))
};
}
/**
* Erstellt einen neuen Feiertag
* @param {string} date - Datum (YYYY-MM-DD)
* @param {number} hours - Freie Stunden (Standard: 8)
* @param {string} description - Beschreibung
* @param {Array<number>} stateIds - Array von State-IDs (leer = bundesweit)
* @returns {Promise<Object>} Erstellter Feiertag
*/
async createHoliday(date, hours, description, stateIds = []) {
const { Holiday, State, HolidayState } = database.getModels();
// Prüfe ob schon ein Feiertag an diesem Datum existiert
const existing = await Holiday.findOne({
where: { date }
});
if (existing) {
throw new Error('An diesem Datum existiert bereits ein Feiertag');
}
const holiday = await Holiday.create({
date,
hours: hours || 8,
description,
version: 0
});
// Verknüpfe mit States (falls angegeben)
if (stateIds && stateIds.length > 0) {
for (const stateId of stateIds) {
await HolidayState.create({
holiday_id: holiday.id,
state_id: stateId
});
}
}
// Lade Holiday mit States neu
const holidayWithStates = await Holiday.findByPk(holiday.id, {
include: [
{
model: State,
as: 'states',
attributes: ['id', 'state_name'],
through: { attributes: [] }
}
]
});
return this._formatHoliday(holidayWithStates);
}
/**
* Löscht einen Feiertag
* @param {number} id - Holiday-ID
* @returns {Promise<void>}
*/
async deleteHoliday(id) {
const { Holiday } = database.getModels();
const holiday = await Holiday.findByPk(id);
if (!holiday) {
throw new Error('Feiertag nicht gefunden');
}
await holiday.destroy();
}
/**
* Formatiert einen Feiertag für die API
*/
_formatHoliday(holiday) {
// Holiday kann ein Plain Object (raw: true) oder eine Sequelize Instance sein
const states = holiday.states || [];
const stateNames = Array.isArray(states)
? states.map(s => s.state_name || s.name).filter(Boolean)
: [];
return {
id: holiday.id,
date: holiday.date,
hours: holiday.hours,
description: holiday.description,
states: stateNames,
isFederal: stateNames.length === 0 // Kein State = Bundesfeiertag
};
}
}
module.exports = new HolidayService();