602 lines
17 KiB
JavaScript
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();
|