Files
stechuhr3/backend/src/repositories/WorklogRepository.js

602 lines
17 KiB
JavaScript

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<Array>} 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<Object|null>} 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<Object>} 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<Object>} 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<boolean>} 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<Object|null>} 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>} 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<Object|null>} 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>} 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>} 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<number>} worklogIds - Array von Worklog-IDs
* @returns {Promise<Map>} 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>} 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<Array>} 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<Array>} 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<Object>} 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<Object>} 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>} 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();