const database = require('../config/database'); const { Op } = require('sequelize'); /** * Service-Klasse für Kalender-Daten * Lädt alle Informationen für einen Monat */ class CalendarService { constructor() { this.defaultUserId = 1; } /** * Holt Kalenderdaten für einen Monat * @param {number} userId - Benutzer-ID * @param {number} year - Jahr * @param {number} month - Monat (1-12) * @returns {Promise} Kalenderdaten */ async getCalendarMonth(userId, year, month) { const { Holiday, Sick, Vacation } = database.getModels(); const sequelize = database.sequelize; // Berechne Start- und End-Datum des Monats const firstDay = new Date(year, month - 1, 1); const lastDay = new Date(year, month, 0); // Erweitere den Bereich um die Tage aus dem vorherigen/nächsten Monat const startDate = new Date(firstDay); startDate.setDate(startDate.getDate() - ((startDate.getDay() + 6) % 7)); // Montag der ersten Woche const endDate = new Date(lastDay); const daysToAdd = (7 - ((endDate.getDay() + 6) % 7)) % 7; endDate.setDate(endDate.getDate() + daysToAdd); // Sonntag der letzten Woche const startDateStr = this._formatDate(startDate); const endDateStr = this._formatDate(endDate); // Hole alle Feiertage const holidays = await Holiday.findAll({ where: { date: { [Op.between]: [startDateStr, endDateStr] } }, raw: true }); const holidayMap = new Map(); holidays.forEach(h => { holidayMap.set(h.date, h.description); }); // Hole alle Krankheitstage const sickEntries = await Sick.findAll({ where: { user_id: userId, [Op.or]: [ { first_day: { [Op.between]: [startDateStr, endDateStr] } }, { last_day: { [Op.between]: [startDateStr, endDateStr] } }, { [Op.and]: [ { first_day: { [Op.lte]: startDateStr } }, { last_day: { [Op.gte]: endDateStr } } ] } ] }, raw: true }); // Erstelle Set mit allen Krankheitstagen const sickDays = new Set(); sickEntries.forEach(sick => { const start = new Date(Math.max(new Date(sick.first_day + 'T00:00:00'), new Date(startDateStr + 'T00:00:00'))); const end = new Date(Math.min(new Date(sick.last_day + 'T00:00:00'), new Date(endDateStr + 'T00:00:00'))); for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { sickDays.add(this._formatDate(d)); } }); // Hole alle Urlaubstage const vacationEntries = await Vacation.findAll({ where: { user_id: userId, [Op.or]: [ { first_day: { [Op.between]: [startDateStr, endDateStr] } }, { last_day: { [Op.between]: [startDateStr, endDateStr] } }, { [Op.and]: [ { first_day: { [Op.lte]: startDateStr } }, { last_day: { [Op.gte]: endDateStr } } ] } ] }, raw: true }); // Erstelle Map mit Urlaubstagen (und ob es halbe Tage sind) const vacationDays = new Map(); vacationEntries.forEach(vac => { const start = new Date(Math.max(new Date(vac.first_day + 'T00:00:00'), new Date(startDateStr + 'T00:00:00'))); const end = new Date(Math.min(new Date(vac.last_day + 'T00:00:00'), new Date(endDateStr + 'T00:00:00'))); for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { vacationDays.set(this._formatDate(d), vac.half_day === 1); } }); // Hole Arbeitszeiten über den TimeEntryService (ähnlich wie WeekOverview) // Wir verwenden eine vereinfachte Query die alle worklog-Einträge holt const workQuery = ` SELECT DATE(tstamp) as work_date, tstamp, state, relatedTo_id, id FROM worklog WHERE user_id = :userId AND DATE(tstamp) BETWEEN :startDate AND :endDate ORDER BY tstamp ASC `; const workEntries = await sequelize.query(workQuery, { replacements: { userId, startDate: startDateStr, endDate: endDateStr }, type: sequelize.QueryTypes.SELECT }); // Gruppiere nach Datum und berechne Arbeitszeit const workMap = new Map(); const entriesByDate = new Map(); workEntries.forEach(entry => { if (!entriesByDate.has(entry.work_date)) { entriesByDate.set(entry.work_date, []); } entriesByDate.get(entry.work_date).push(entry); }); // Berechne für jeden Tag die Arbeitszeit (Nettoarbeitszeit mit Pausenabzug) entriesByDate.forEach((entries, date) => { let totalWorkMinutes = 0; let totalPauseMinutes = 0; // Parse state für alle Einträge const parsedEntries = entries.map(entry => { let state = entry.state; if (typeof state === 'string' && state.startsWith('{')) { try { state = JSON.parse(state); } catch (e) { // ignore } } const action = state?.action || state; return { ...entry, action }; }); // Finde alle start work -> stop work Paare und berechne Bruttoarbeitszeit parsedEntries.forEach(entry => { if (entry.action === 'stop work') { // Finde das zugehörige start work über relatedTo_id const startEntry = parsedEntries.find(e => e.id === entry.relatedTo_id && e.action === 'start work'); if (startEntry) { const start = new Date(startEntry.tstamp); const end = new Date(entry.tstamp); const minutes = (end - start) / (1000 * 60); totalWorkMinutes += minutes; // Berechne Pausen innerhalb dieses Arbeitsblocks const pauseEntries = parsedEntries.filter(e => new Date(e.tstamp) > start && new Date(e.tstamp) < end && (e.action === 'start pause' || e.action === 'stop pause') ); // Paare start pause -> stop pause pauseEntries.forEach(pauseEntry => { if (pauseEntry.action === 'stop pause') { const pauseStart = pauseEntries.find(pe => pe.id === pauseEntry.relatedTo_id && pe.action === 'start pause' ); if (pauseStart) { const pStart = new Date(pauseStart.tstamp); const pEnd = new Date(pauseEntry.tstamp); const pauseMins = (pEnd - pStart) / (1000 * 60); totalPauseMinutes += pauseMins; } } }); } } }); const netMinutes = totalWorkMinutes - totalPauseMinutes; if (netMinutes > 0) { workMap.set(date, netMinutes / 60); // Convert to hours } }); // Erstelle Kalendertage const days = []; const currentDate = new Date(startDate); while (currentDate <= endDate) { const dateStr = this._formatDate(currentDate); const day = currentDate.getDate(); const isCurrentMonth = currentDate.getMonth() === (month - 1); const isToday = this._isToday(currentDate); const dayData = { date: dateStr, day, isCurrentMonth, isToday, holiday: holidayMap.get(dateStr) || null, sick: sickDays.has(dateStr), vacation: vacationDays.has(dateStr) ? (vacationDays.get(dateStr) ? 'half' : 'full') : null, workedHours: workMap.get(dateStr) || null }; days.push(dayData); currentDate.setDate(currentDate.getDate() + 1); } // Gruppiere in Wochen const weeks = []; for (let i = 0; i < days.length; i += 7) { weeks.push(days.slice(i, i + 7)); } return { year, month, weeks }; } /** * Formatiert ein Datum als YYYY-MM-DD */ _formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } /** * Prüft ob ein Datum heute ist */ _isToday(date) { const today = new Date(); return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear(); } } module.exports = new CalendarService();