284 lines
8.6 KiB
JavaScript
284 lines
8.6 KiB
JavaScript
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<Object>} 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();
|
|
|