diff --git a/backend/src/controllers/WorkdaysController.js b/backend/src/controllers/WorkdaysController.js new file mode 100644 index 0000000..bc88d4d --- /dev/null +++ b/backend/src/controllers/WorkdaysController.js @@ -0,0 +1,35 @@ +const WorkdaysService = require('../services/WorkdaysService'); + +/** + * Controller für Arbeitstage-Statistiken + * Verarbeitet HTTP-Requests und delegiert an WorkdaysService + */ +class WorkdaysController { + /** + * Holt Arbeitstage-Statistiken für ein Jahr + */ + async getStatistics(req, res) { + try { + const userId = req.user?.id || 1; + const year = parseInt(req.query.year) || new Date().getFullYear(); + + if (isNaN(year) || year < 1900 || year > 2100) { + return res.status(400).json({ + message: 'Ungültiges Jahr' + }); + } + + const statistics = await WorkdaysService.getWorkdaysStatistics(userId, year); + res.json(statistics); + } catch (error) { + console.error('Fehler beim Abrufen der Arbeitstage-Statistiken:', error); + res.status(500).json({ + message: 'Fehler beim Abrufen der Arbeitstage-Statistiken', + error: error.message + }); + } + } +} + +module.exports = new WorkdaysController(); + diff --git a/backend/src/index.js b/backend/src/index.js index d8dee7c..f1a48cc 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -78,6 +78,10 @@ app.use('/api/vacation', authenticateToken, vacationRouter); const sickRouter = require('./routes/sick'); app.use('/api/sick', authenticateToken, sickRouter); +// Workdays routes (geschützt) - MIT ID-Hashing +const workdaysRouter = require('./routes/workdays'); +app.use('/api/workdays', authenticateToken, workdaysRouter); + // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); diff --git a/backend/src/routes/workdays.js b/backend/src/routes/workdays.js new file mode 100644 index 0000000..bae52cb --- /dev/null +++ b/backend/src/routes/workdays.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); +const WorkdaysController = require('../controllers/WorkdaysController'); + +/** + * Routen für Arbeitstage-Statistiken + */ + +// GET /api/workdays?year=2025 - Statistiken für ein Jahr abrufen +router.get('/', WorkdaysController.getStatistics.bind(WorkdaysController)); + +module.exports = router; + diff --git a/backend/src/services/WorkdaysService.js b/backend/src/services/WorkdaysService.js new file mode 100644 index 0000000..34b8f55 --- /dev/null +++ b/backend/src/services/WorkdaysService.js @@ -0,0 +1,182 @@ +const database = require('../config/database'); +const { Op } = require('sequelize'); + +/** + * Service-Klasse für Arbeitstage-Statistiken + * Berechnet verschiedene Tage-Typen für ein Jahr + */ +class WorkdaysService { + constructor() { + this.defaultUserId = 1; + } + + /** + * Berechnet Arbeitstage-Statistiken für ein Jahr + * @param {number} userId - Benutzer-ID + * @param {number} year - Jahr + * @returns {Promise} Statistiken + */ + async getWorkdaysStatistics(userId, year) { + const { Holiday, Sick, Vacation } = database.getModels(); + const sequelize = database.sequelize; + + // Berechne Anzahl Werktage (Mo-Fr) im Jahr + const workdays = this._countWorkdays(year); + + // Hole alle Feiertage für das Jahr + const holidays = await Holiday.findAll({ + where: { + date: { + [Op.gte]: `${year}-01-01`, + [Op.lte]: `${year}-12-31` + } + }, + raw: true + }); + + // Zähle nur Feiertage die auf Werktage fallen + 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 + }).length; + + // Hole alle Krankheitstage für das Jahr + const sickEntries = await Sick.findAll({ + where: { + user_id: userId, + [Op.or]: [ + { + first_day: { + [Op.between]: [`${year}-01-01`, `${year}-12-31`] + } + }, + { + last_day: { + [Op.between]: [`${year}-01-01`, `${year}-12-31`] + } + }, + { + [Op.and]: [ + { first_day: { [Op.lte]: `${year}-01-01` } }, + { last_day: { [Op.gte]: `${year}-12-31` } } + ] + } + ] + }, + raw: true + }); + + // Zähle Krankheitstage (nur Werktage) + let sickDays = 0; + sickEntries.forEach(sick => { + const start = new Date(Math.max(new Date(sick.first_day + 'T00:00:00'), new Date(`${year}-01-01T00:00:00`))); + const end = new Date(Math.min(new Date(sick.last_day + 'T00:00:00'), new Date(`${year}-12-31T00:00:00`))); + + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const dayOfWeek = d.getDay(); + if (dayOfWeek >= 1 && dayOfWeek <= 5) { // Mo-Fr + sickDays++; + } + } + }); + + // Hole alle Urlaubstage für das Jahr + const vacationEntries = await Vacation.findAll({ + where: { + user_id: userId, + [Op.or]: [ + { + first_day: { + [Op.between]: [`${year}-01-01`, `${year}-12-31`] + } + }, + { + last_day: { + [Op.between]: [`${year}-01-01`, `${year}-12-31`] + } + }, + { + [Op.and]: [ + { first_day: { [Op.lte]: `${year}-01-01` } }, + { last_day: { [Op.gte]: `${year}-12-31` } } + ] + } + ] + }, + raw: true + }); + + // Zähle Urlaubstage (nur Werktage, berücksichtige halbe Tage) + let vacationDays = 0; + vacationEntries.forEach(vac => { + const start = new Date(Math.max(new Date(vac.first_day + 'T00:00:00'), new Date(`${year}-01-01T00:00:00`))); + const end = new Date(Math.min(new Date(vac.last_day + 'T00:00:00'), new Date(`${year}-12-31T00:00:00`))); + + const isHalfDay = vac.half_day === 1; + + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const dayOfWeek = d.getDay(); + if (dayOfWeek >= 1 && dayOfWeek <= 5) { // Mo-Fr + vacationDays += isHalfDay ? 0.5 : 1; + } + } + }); + + // Zähle gearbeitete Tage (Tage mit worklog-Einträgen) + const workedDaysQuery = ` + SELECT COUNT(DISTINCT DATE(tstamp)) as count + FROM worklog + WHERE user_id = :userId + AND DATE(tstamp) BETWEEN :startDate AND :endDate + AND state LIKE '%start work%' + `; + + const workedDaysResult = await sequelize.query(workedDaysQuery, { + replacements: { + userId, + startDate: `${year}-01-01`, + endDate: `${year}-12-31` + }, + type: sequelize.QueryTypes.SELECT + }); + + const workedDays = workedDaysResult[0]?.count || 0; + + // Berechne Prozentsatz Krankheitstage + const sickPercentage = workdays > 0 ? Math.round((sickDays / workdays) * 100) : 0; + + return { + year, + workdays, + holidays: holidayCount, + sickDays, + sickPercentage, + vacationDays, + workedDays + }; + } + + /** + * Zählt Werktage (Mo-Fr) in einem Jahr + * @param {number} year - Jahr + * @returns {number} Anzahl Werktage + */ + _countWorkdays(year) { + let count = 0; + const start = new Date(year, 0, 1); + const end = new Date(year, 11, 31); + + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const dayOfWeek = d.getDay(); + if (dayOfWeek >= 1 && dayOfWeek <= 5) { // Mo-Fr + count++; + } + } + + return count; + } +} + +module.exports = new WorkdaysService(); + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index fbd52d7..215b581 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -70,6 +70,7 @@ const pageTitle = computed(() => { 'timefix': 'Zeitkorrekturen', 'vacation': 'Urlaub', 'sick': 'Krankheit', + 'workdays': 'Arbeitstage', 'entries': 'Einträge', 'stats': 'Statistiken' } diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 77a2e23..d17717c 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -13,6 +13,7 @@ import WeekOverview from '../views/WeekOverview.vue' import Timefix from '../views/Timefix.vue' import Vacation from '../views/Vacation.vue' import Sick from '../views/Sick.vue' +import Workdays from '../views/Workdays.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -77,6 +78,12 @@ const router = createRouter({ component: Sick, meta: { requiresAuth: true } }, + { + path: '/bookings/workdays', + name: 'workdays', + component: Workdays, + meta: { requiresAuth: true } + }, { path: '/entries', name: 'entries', diff --git a/frontend/src/views/Workdays.vue b/frontend/src/views/Workdays.vue new file mode 100644 index 0000000..9262fa5 --- /dev/null +++ b/frontend/src/views/Workdays.vue @@ -0,0 +1,181 @@ + + + + + +