const database = require('../config/database'); const { Op } = require('sequelize'); /** * Service-Klasse für Zeitkorrekturen * Enthält die gesamte Business-Logik für Timefixes */ class TimefixService { constructor() { this.defaultUserId = 1; } /** * Holt alle Worklog-Einträge für ein bestimmtes Datum * @param {number} userId - Benutzer-ID * @param {string} date - Datum im Format YYYY-MM-DD * @returns {Promise} Array von Worklog-Einträgen mit korrigierten Zeiten */ async getWorklogEntriesForDate(userId, date) { const { Worklog } = database.getModels(); const sequelize = database.sequelize; // Hole alle Worklog-Einträge für das Datum const entries = await Worklog.findAll({ where: { user_id: userId, tstamp: { [Op.like]: `${date}%` } }, order: [['tstamp', 'ASC']], raw: true }); // Hole auch die Timefixes für diese Einträge const entryIds = entries.map(e => e.id); const timefixesForEntries = await sequelize.query( `SELECT worklog_id, fix_type, fix_date_time FROM timefix WHERE worklog_id IN (?)`, { replacements: [entryIds], type: sequelize.QueryTypes.SELECT } ); // Erstelle eine Map: worklog_id -> Timefix const timefixMap = new Map(); timefixesForEntries.forEach(tf => { if (!timefixMap.has(tf.worklog_id)) { timefixMap.set(tf.worklog_id, []); } timefixMap.get(tf.worklog_id).push(tf); }); // Formatiere die Einträge für das Frontend const formattedEntries = entries.map(entry => { // Prüfe ob es eine Korrektur gibt const timefixForEntry = timefixMap.get(entry.id); let hours, minutes, action; if (timefixForEntry && timefixForEntry.length > 0) { // Verwende korrigierte Werte aus Timefix const tf = timefixForEntry[0]; // WICHTIG: DB speichert UTC, Frontend erwartet lokale Zeit if (typeof tf.fix_date_time === 'string') { // Parse als UTC und konvertiere zu lokaler Zeit const [datePart, timePart] = tf.fix_date_time.split(' '); const [h, m, s] = timePart.split(':').map(Number); // Erstelle UTC-Date aus den Komponenten const [year, month, day] = datePart.split('-').map(Number); const utcDate = new Date(Date.UTC(year, month - 1, day, h, m, s)); // Extrahiere lokale Stunden/Minuten hours = utcDate.getHours(); minutes = utcDate.getMinutes(); } else if (tf.fix_date_time instanceof Date) { // Date-Objekt: Verwende lokale Zeit hours = tf.fix_date_time.getHours(); minutes = tf.fix_date_time.getMinutes(); } action = tf.fix_type; } else { // Keine Korrektur - verwende Original-Werte aus Worklog // WICHTIG: DB speichert UTC, Frontend erwartet lokale Zeit if (typeof entry.tstamp === 'string') { // Parse als UTC und konvertiere zu lokaler Zeit const [datePart, timePart] = entry.tstamp.split(' '); const [h, m, s] = timePart.split(':').map(Number); // Erstelle UTC-Date aus den Komponenten const [year, month, day] = datePart.split('-').map(Number); const utcDate = new Date(Date.UTC(year, month - 1, day, h, m, s)); // Extrahiere lokale Stunden/Minuten hours = utcDate.getHours(); minutes = utcDate.getMinutes(); } else if (entry.tstamp instanceof Date) { // Date-Objekt: Verwende lokale Zeit hours = entry.tstamp.getHours(); minutes = entry.tstamp.getMinutes(); } // Parse state action = entry.state; if (typeof action === 'string') { try { const parsed = JSON.parse(action); action = parsed.action || action; } catch (e) { // action bleibt als String } } } return { id: entry.id, time: `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`, action: action, tstamp: entry.tstamp }; }).filter(e => e !== null); return formattedEntries; } /** * Holt alle Zeitkorrekturen für den heutigen Tag * @param {number} userId - Benutzer-ID * @returns {Promise} Array von Zeitkorrekturen */ async getTodayTimefixes(userId) { const sequelize = database.sequelize; // Berechne Start und Ende des heutigen Tages (lokale Zeit) const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0); const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59); // Hole alle Timefixes für heute mit Raw SQL const todayStartStr = `${todayStart.getFullYear()}-${String(todayStart.getMonth() + 1).padStart(2, '0')}-${String(todayStart.getDate()).padStart(2, '0')} 00:00:00`; const todayEndStr = `${todayEnd.getFullYear()}-${String(todayEnd.getMonth() + 1).padStart(2, '0')}-${String(todayEnd.getDate()).padStart(2, '0')} 23:59:59`; const timefixes = await sequelize.query( `SELECT id, user_id, worklog_id, fix_type, fix_date_time FROM timefix WHERE user_id = ? AND fix_date_time BETWEEN ? AND ? ORDER BY fix_date_time DESC`, { replacements: [userId, todayStartStr, todayEndStr], type: sequelize.QueryTypes.SELECT } ); // Hole die zugehörigen Worklog-Einträge separat mit Raw SQL const worklogIds = timefixes.map(tf => tf.worklog_id); if (worklogIds.length === 0) { return []; } const worklogs = await sequelize.query( `SELECT id, tstamp, state, relatedTo_id FROM worklog WHERE id IN (?)`, { replacements: [worklogIds], type: sequelize.QueryTypes.SELECT } ); // Erstelle eine Map für schnellen Zugriff const worklogMap = new Map(); worklogs.forEach(wl => { worklogMap.set(wl.id, wl); }); // Formatiere die Daten für das Frontend const formattedTimefixes = timefixes.map(tf => { const worklog = worklogMap.get(tf.worklog_id); if (!worklog || !worklog.tstamp) { console.error('Worklog oder tstamp fehlt für Timefix:', tf.id); return null; } // KORRIGIERTE Werte aus timefix.fix_date_time let newDate, newHours, newMinutes; if (typeof tf.fix_date_time === 'string') { const [datePart, timePart] = tf.fix_date_time.split(' '); newDate = datePart; [newHours, newMinutes] = timePart.split(':').map(Number); } else if (tf.fix_date_time instanceof Date) { const d = tf.fix_date_time; newDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; newHours = d.getHours(); newMinutes = d.getMinutes(); } else { console.error('Unbekannter fix_date_time-Typ:', typeof tf.fix_date_time); return null; } // ORIGINAL Werte aus worklog let originalDate, originalHours, originalMinutes; if (typeof worklog.tstamp === 'string') { const [datePart, timePart] = worklog.tstamp.split(' '); originalDate = datePart; [originalHours, originalMinutes] = timePart.split(':').map(Number); } else if (worklog.tstamp instanceof Date) { const d = worklog.tstamp; originalDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; originalHours = d.getHours(); originalMinutes = d.getMinutes(); } else { console.error('Unbekannter tstamp-Typ:', typeof worklog.tstamp); return null; } // Parse original action aus worklog.state let originalAction = worklog.state; if (typeof originalAction === 'string') { try { const parsed = JSON.parse(originalAction); originalAction = parsed.action || originalAction; } catch (e) { // action bleibt als String } } return { id: tf.id, worklogId: worklog.id, fixType: tf.fix_type, fixDateTime: tf.fix_date_time, // Neue Werte (KORRIGIERT - aus timefix.fix_date_time) newDate: newDate, newTime: `${newHours.toString().padStart(2, '0')}:${newMinutes.toString().padStart(2, '0')}`, newAction: tf.fix_type, // Original-Werte (aus worklog) originalDate: originalDate, originalTime: `${originalHours.toString().padStart(2, '0')}:${originalMinutes.toString().padStart(2, '0')}`, originalAction: originalAction }; }).filter(tf => tf !== null); return formattedTimefixes; } /** * Erstellt eine neue Zeitkorrektur * @param {number} userId - Benutzer-ID * @param {number} worklogId - Worklog-Eintrag-ID * @param {string} newDate - Neues Datum (YYYY-MM-DD) * @param {string} newTime - Neue Uhrzeit (HH:MM) * @param {string} newAction - Neue Aktion * @returns {Promise} Erstellter Timefix */ async createTimefix(userId, worklogId, newDate, newTime, newAction) { const { Timefix } = database.getModels(); const sequelize = database.sequelize; // Hole den Original-Worklog-Eintrag mit Raw SQL const worklogResult = await sequelize.query( `SELECT id, user_id, tstamp, state FROM worklog WHERE id = ?`, { replacements: [worklogId], type: sequelize.QueryTypes.SELECT } ); if (!worklogResult || worklogResult.length === 0) { throw new Error('Worklog-Eintrag nicht gefunden'); } const worklog = worklogResult[0]; // Prüfe Berechtigung if (worklog.user_id !== userId) { throw new Error('Keine Berechtigung für diesen Eintrag'); } // Erstelle die korrigierte Zeit als Date-Objekt const [newHours, newMinutes] = newTime.split(':').map(Number); const [newYear, newMonth, newDay] = newDate.split('-').map(Number); const correctedDateTime = new Date(newYear, newMonth - 1, newDay, newHours, newMinutes, 0); // Prüfe ob bereits ein Timefix existiert const existingTimefix = await Timefix.findOne({ where: { worklog_id: worklogId, fix_type: newAction } }); if (existingTimefix) { // Update existierender Timefix await existingTimefix.update({ fix_date_time: correctedDateTime }); return { id: existingTimefix.id, worklogId: existingTimefix.worklog_id, fixType: existingTimefix.fix_type, fixDateTime: existingTimefix.fix_date_time }; } // Erstelle neuen Timefix const timefix = await Timefix.create({ user_id: userId, worklog_id: worklogId, fix_type: newAction, fix_date_time: correctedDateTime }); return { id: timefix.id, worklogId: timefix.worklog_id, fixType: timefix.fix_type, fixDateTime: timefix.fix_date_time }; } /** * Löscht eine Zeitkorrektur * @param {number} userId - Benutzer-ID * @param {number} timefixId - Timefix-ID * @returns {Promise} */ async deleteTimefix(userId, timefixId) { const { Timefix } = database.getModels(); const timefix = await Timefix.findByPk(timefixId); if (!timefix) { throw new Error('Zeitkorrektur nicht gefunden'); } // Prüfe Berechtigung if (timefix.user_id !== userId) { throw new Error('Keine Berechtigung für diese Zeitkorrektur'); } await timefix.destroy(); } } module.exports = new TimefixService();