365 lines
12 KiB
JavaScript
365 lines
12 KiB
JavaScript
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>} 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>} 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<Object>} 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
|
|
// WICHTIG: Frontend sendet lokale Zeit, DB erwartet UTC
|
|
const [newHours, newMinutes] = newTime.split(':').map(Number);
|
|
const [newYear, newMonth, newDay] = newDate.split('-').map(Number);
|
|
|
|
// Erstelle lokales Date-Objekt
|
|
const localDateTime = new Date(newYear, newMonth - 1, newDay, newHours, newMinutes, 0);
|
|
|
|
// Konvertiere zu UTC-String für die Datenbank (Format: YYYY-MM-DD HH:MM:SS)
|
|
const utcYear = localDateTime.getUTCFullYear();
|
|
const utcMonth = String(localDateTime.getUTCMonth() + 1).padStart(2, '0');
|
|
const utcDay = String(localDateTime.getUTCDate()).padStart(2, '0');
|
|
const utcHours = String(localDateTime.getUTCHours()).padStart(2, '0');
|
|
const utcMinutes = String(localDateTime.getUTCMinutes()).padStart(2, '0');
|
|
const utcSeconds = String(localDateTime.getUTCSeconds()).padStart(2, '0');
|
|
const correctedDateTime = `${utcYear}-${utcMonth}-${utcDay} ${utcHours}:${utcMinutes}:${utcSeconds}`;
|
|
|
|
// 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<void>}
|
|
*/
|
|
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();
|
|
|