const database = require('../config/database'); const { Op } = require('sequelize'); /** * Repository für Worklog-Datenbankzugriff * Verwendet Sequelize ORM */ class WorklogRepository { /** * Alle Worklog-Einträge für einen Benutzer abrufen * @param {number} userId - ID des Benutzers * @param {Object} options - Filteroptionen * @returns {Promise} Liste der Worklog-Einträge */ async findAllByUser(userId, options = {}) { const { Worklog } = database.getModels(); const { limit, offset, orderBy = 'tstamp', order = 'DESC' } = options; return await Worklog.findAll({ where: { user_id: userId }, order: [[orderBy, order]], limit: limit || null, offset: offset || 0 }); } /** * Worklog-Eintrag anhand der ID abrufen * @param {number} id - Worklog-ID * @returns {Promise} Worklog-Eintrag oder null */ async findById(id) { const { Worklog } = database.getModels(); return await Worklog.findByPk(id); } /** * Neuen Worklog-Eintrag erstellen * @param {Object} worklogData - Worklog-Daten * @returns {Promise} Erstellter Worklog-Eintrag */ async create(worklogData) { const { Worklog } = database.getModels(); const { user_id, state, tstamp, relatedTo_id = null } = worklogData; return await Worklog.create({ version: 0, user_id, state, tstamp: tstamp || new Date(), relatedTo_id }); } /** * Worklog-Eintrag aktualisieren * @param {number} id - Worklog-ID * @param {Object} updateData - Zu aktualisierende Daten * @returns {Promise} Aktualisierter Worklog-Eintrag */ async update(id, updateData) { const { Worklog } = database.getModels(); const worklog = await Worklog.findByPk(id); if (!worklog) { throw new Error(`Worklog mit ID ${id} nicht gefunden`); } // Update-Daten vorbereiten const updates = {}; if (updateData.state !== undefined) { updates.state = updateData.state; } if (updateData.tstamp !== undefined) { updates.tstamp = updateData.tstamp; } if (updateData.relatedTo_id !== undefined) { updates.relatedTo_id = updateData.relatedTo_id; } // Version inkrementieren updates.version = worklog.version + 1; await worklog.update(updates); return worklog; } /** * Worklog-Eintrag löschen * @param {number} id - Worklog-ID * @returns {Promise} true wenn erfolgreich */ async delete(id) { const { Worklog } = database.getModels(); const deleted = await Worklog.destroy({ where: { id } }); return deleted > 0; } /** * Letzten Worklog-Eintrag für Benutzer finden * @param {number} userId - Benutzer-ID * @returns {Promise} Letzter Worklog-Eintrag oder null */ async findLatestByUser(userId) { const { Worklog } = database.getModels(); return await Worklog.findOne({ where: { user_id: userId }, order: [['tstamp', 'DESC'], ['id', 'DESC']], raw: true }); } /** * Alle Worklog-Einträge für Benutzer finden * @param {number} userId - Benutzer-ID * @returns {Promise} Array von Worklog-Einträgen */ async findByUser(userId) { const { Worklog } = database.getModels(); return await Worklog.findAll({ where: { user_id: userId }, order: [['tstamp', 'ASC'], ['id', 'ASC']], raw: true }); } /** * Letzten offenen Worklog-Eintrag für Benutzer finden * @param {number} userId - Benutzer-ID * @returns {Promise} Laufender Worklog-Eintrag oder null */ async findRunningByUser(userId) { const { Worklog } = database.getModels(); return await Worklog.findOne({ where: { user_id: userId, relatedTo_id: null }, order: [['tstamp', 'DESC']], include: [{ model: Worklog, as: 'clockOut', required: false }] }).then(worklog => { // Nur zurückgeben wenn kein clockOut existiert if (worklog && !worklog.clockOut) { return worklog; } return null; }); } /** * Vacation-Einträge für einen Benutzer in einem Datumsbereich abrufen * @param {number} userId - Benutzer-ID * @param {Date} startDate - Start-Datum * @param {Date} endDate - End-Datum * @returns {Promise} Array von Vacation-Einträgen mit expandierten Tagen */ async getVacationsByUserInDateRange(userId, startDate, endDate) { const { Vacation } = database.getModels(); try { // Hole alle Vacation-Einträge, die sich mit dem Datumsbereich überschneiden const vacations = await Vacation.findAll({ where: { user_id: userId, [Op.or]: [ { first_day: { [Op.between]: [startDate, endDate] } }, { last_day: { [Op.between]: [startDate, endDate] } }, { [Op.and]: [ { first_day: { [Op.lte]: startDate } }, { last_day: { [Op.gte]: endDate } } ] } ] }, raw: true, order: [['first_day', 'ASC']] }); // Expandiere jeden Vacation-Eintrag in einzelne Tage const expandedVacations = []; vacations.forEach(vac => { const first = new Date(vac.first_day); const last = new Date(vac.last_day); // Iteriere über alle Tage im Urlaubsbereich for (let d = new Date(first); d <= last; d.setDate(d.getDate() + 1)) { // Nur Tage hinzufügen, die im gewünschten Bereich liegen if (d >= startDate && d <= endDate) { // Überspringe Wochenenden (Samstag=6, Sonntag=0) const dayOfWeek = d.getDay(); if (dayOfWeek !== 0 && dayOfWeek !== 6) { expandedVacations.push({ date: new Date(d), half_day: vac.vacation_type === 1 ? 1 : 0, vacation_type: vac.vacation_type }); } } } }); return expandedVacations; } catch (error) { console.error('Fehler beim Abrufen der Vacation-Einträge:', error); return []; } } /** * Sick-Einträge für einen Benutzer in einem Datumsbereich abrufen * @param {number} userId - Benutzer-ID * @param {Date} startDate - Start-Datum * @param {Date} endDate - End-Datum * @returns {Promise} Array von Sick-Einträgen mit expandierten Tagen */ async getSickByUserInDateRange(userId, startDate, endDate) { const { Sick, SickType } = database.getModels(); try { // Hole alle Sick-Einträge, die sich mit dem Datumsbereich überschneiden const sickEntries = await Sick.findAll({ where: { user_id: userId, [Op.or]: [ { first_day: { [Op.between]: [startDate, endDate] } }, { last_day: { [Op.between]: [startDate, endDate] } }, { [Op.and]: [ { first_day: { [Op.lte]: startDate } }, { last_day: { [Op.gte]: endDate } } ] } ] }, include: [{ model: SickType, as: 'sickType', attributes: ['description'] }], order: [['first_day', 'ASC']] }); // Expandiere jeden Sick-Eintrag in einzelne Tage const expandedSick = []; sickEntries.forEach(sick => { const first = new Date(sick.first_day); const last = new Date(sick.last_day); const sickTypeDesc = sick.sickType?.description || 'self'; // Iteriere über alle Tage im Krankheitsbereich for (let d = new Date(first); d <= last; d.setDate(d.getDate() + 1)) { // Nur Tage hinzufügen, die im gewünschten Bereich liegen if (d >= startDate && d <= endDate) { // Überspringe Wochenenden (Samstag=6, Sonntag=0) const dayOfWeek = d.getDay(); if (dayOfWeek !== 0 && dayOfWeek !== 6) { expandedSick.push({ date: new Date(d), sick_type: sickTypeDesc, sick_type_id: sick.sick_type_id }); } } } }); return expandedSick; } catch (error) { console.error('Fehler beim Abrufen der Sick-Einträge:', error); return []; } } /** * Timefix-Einträge für Worklog-IDs abrufen * @param {Array} worklogIds - Array von Worklog-IDs * @returns {Promise} Map von worklog_id zu Timefix-Einträgen */ async getTimefixesByWorklogIds(worklogIds) { if (!worklogIds || worklogIds.length === 0) { return new Map(); } const { Timefix } = database.getModels(); try { const timefixes = await Timefix.findAll({ where: { worklog_id: { [Op.in]: worklogIds } }, raw: true }); // Gruppiere nach worklog_id const timefixMap = new Map(); timefixes.forEach(fix => { if (!timefixMap.has(fix.worklog_id)) { timefixMap.set(fix.worklog_id, []); } timefixMap.get(fix.worklog_id).push(fix); }); return timefixMap; } catch (error) { console.error('Fehler beim Abrufen der Timefix-Einträge:', error); return new Map(); } } /** * Worklog-Paare für Benutzer in einem Datumsbereich finden * @param {number} userId - Benutzer-ID * @param {Date} startDate - Start-Datum * @param {Date} endDate - End-Datum * @returns {Promise} Array von Worklog-Paaren */ async findPairsByUserInDateRange(userId, startDate, endDate) { const { Worklog } = database.getModels(); try { const results = await Worklog.findAll({ attributes: ['id', 'version', 'user_id', 'state', 'tstamp', 'relatedTo_id'], where: { user_id: userId, tstamp: { [Op.between]: [startDate, endDate] } }, order: [['tstamp', 'ASC']], raw: true }); // Gruppiere Start/Stop-Paare basierend auf dem action-Feld const pairs = []; const startEntries = {}; results.forEach(entry => { // Parse state JSON let action = ''; try { const state = typeof entry.state === 'string' ? JSON.parse(entry.state) : entry.state; action = state.action || state; } catch (e) { action = entry.state; } if (action === 'start work') { startEntries[entry.id] = entry; } else if (action === 'stop work' && entry.relatedTo_id) { const startEntry = startEntries[entry.relatedTo_id]; if (startEntry) { pairs.push({ id: startEntry.id, start_time: startEntry.tstamp, end_time: entry.tstamp, start_state: startEntry.state, end_state: entry.state }); delete startEntries[entry.relatedTo_id]; } } }); // Füge laufende Einträge hinzu Object.values(startEntries).forEach(startEntry => { pairs.push({ id: startEntry.id, start_time: startEntry.tstamp, end_time: null, start_state: startEntry.state, end_state: null }); }); return pairs; } catch (error) { console.error('Fehler beim Abrufen der Worklog-Paare:', error); return []; } } /** * Worklog-Einträge nach Datumsbereich abrufen * @param {number} userId - Benutzer-ID * @param {Date} startDate - Startdatum * @param {Date} endDate - Enddatum * @returns {Promise} Gefilterte Worklog-Einträge */ async findByDateRange(userId, startDate, endDate) { const { Worklog } = database.getModels(); return await Worklog.findAll({ where: { user_id: userId, tstamp: { [Op.between]: [startDate, endDate] } }, order: [['tstamp', 'ASC']] // Aufsteigend, damit Start vor Stop kommt }); } /** * Zusammengehörige Worklog-Paare abrufen (Clock In/Out) * @param {number} userId - Benutzer-ID * @param {Object} options - Optionen * @returns {Promise} Liste von Worklog-Paaren */ async findPairsByUser(userId, options = {}) { const { Worklog } = database.getModels(); const sequelize = database.getSequelize(); const { limit, offset } = options; // Raw Query für bessere Performance bei Pairs const query = ` SELECT w1.id as start_id, w1.tstamp as start_time, w1.state as start_state, w2.id as end_id, w2.tstamp as end_time, w2.state as end_state, TIMESTAMPDIFF(SECOND, w1.tstamp, w2.tstamp) as duration FROM worklog w1 LEFT JOIN worklog w2 ON w2.relatedTo_id = w1.id WHERE w1.user_id = :userId AND w1.relatedTo_id IS NULL ORDER BY w1.tstamp DESC ${limit ? `LIMIT ${limit}` : ''} ${offset ? `OFFSET ${offset}` : ''} `; const results = await sequelize.query(query, { replacements: { userId }, type: sequelize.constructor.QueryTypes.SELECT }); return Array.isArray(results) ? results : []; } /** * Statistiken für Benutzer berechnen * @param {number} userId - Benutzer-ID * @returns {Promise} Statistik-Objekt */ async getStatistics(userId) { const sequelize = database.getSequelize(); const query = ` SELECT COUNT(DISTINCT w1.id) as total_entries, COUNT(DISTINCT w2.id) as completed_entries, COUNT(DISTINCT CASE WHEN w2.id IS NULL THEN w1.id END) as running_entries, COALESCE(SUM(TIMESTAMPDIFF(SECOND, w1.tstamp, w2.tstamp)), 0) as total_seconds FROM worklog w1 LEFT JOIN worklog w2 ON w2.relatedTo_id = w1.id WHERE w1.user_id = :userId AND w1.relatedTo_id IS NULL `; const results = await sequelize.query(query, { replacements: { userId }, type: sequelize.constructor.QueryTypes.SELECT }); return (Array.isArray(results) && results[0]) ? results[0] : { total_entries: 0, completed_entries: 0, running_entries: 0, total_seconds: 0 }; } /** * Statistiken für heute abrufen * @param {number} userId - Benutzer-ID * @returns {Promise} Heutige Statistiken */ async getTodayStatistics(userId) { const sequelize = database.getSequelize(); const query = ` SELECT COUNT(DISTINCT w1.id) as entries, COALESCE(SUM(TIMESTAMPDIFF(SECOND, w1.tstamp, w2.tstamp)), 0) as seconds FROM worklog w1 LEFT JOIN worklog w2 ON w2.relatedTo_id = w1.id WHERE w1.user_id = :userId AND w1.relatedTo_id IS NULL AND DATE(w1.tstamp) = CURDATE() `; const results = await sequelize.query(query, { replacements: { userId }, type: sequelize.constructor.QueryTypes.SELECT }); return (Array.isArray(results) && results[0]) ? results[0] : { entries: 0, seconds: 0 }; } /** * Feiertage in einem Datumsbereich abrufen * @param {Date} startDate - Start-Datum * @param {Date} endDate - End-Datum * @returns {Promise} Array von Holiday-Einträgen mit Datum und Stunden */ async getHolidaysInDateRange(startDate, endDate, userId = null) { const { Holiday, State, User } = database.getModels(); try { // Hole User-Bundesland (falls userId angegeben) let userStateId = null; if (userId) { const user = await User.findByPk(userId, { attributes: ['state_id'], raw: true }); userStateId = user?.state_id; } const holidays = await Holiday.findAll({ where: { date: { [Op.between]: [startDate, endDate] } }, include: [ { model: State, as: 'states', attributes: ['id'], through: { attributes: [] }, required: false } ], order: [['date', 'ASC']] }); // Filtere nach User-Bundesland (falls userId angegeben) if (userId && userStateId) { return holidays.filter(h => { const holidayStates = h.states || []; const isFederal = holidayStates.length === 0; const appliesToUser = isFederal || holidayStates.some(s => s.id === userStateId); return appliesToUser; }).map(h => ({ date: h.date, hours: h.hours, description: h.description })); } // Ohne userId: Alle Feiertage zurückgeben return holidays.map(h => ({ date: h.date, hours: h.hours, description: h.description })); } catch (error) { console.error('Fehler beim Abrufen der Feiertage:', error); return []; } } } module.exports = new WorklogRepository();