V1.1 Verschiedene Anpassungen
This commit is contained in:
@@ -4,6 +4,7 @@ const { db } = require('../database');
|
||||
const { requireAuth, requireVerwaltung } = require('../middleware/auth');
|
||||
const { generatePDF } = require('../services/pdf-service');
|
||||
const { getHolidaysForDateRange } = require('../services/feiertage-service');
|
||||
const { hasRole } = require('../helpers/utils');
|
||||
|
||||
// Routes registrieren
|
||||
function registerTimesheetRoutes(app) {
|
||||
@@ -16,7 +17,7 @@ function registerTimesheetRoutes(app) {
|
||||
activity3_desc, activity3_hours, activity3_project_number,
|
||||
activity4_desc, activity4_hours, activity4_project_number,
|
||||
activity5_desc, activity5_hours, activity5_project_number,
|
||||
overtime_taken_hours, vacation_type, sick_status
|
||||
overtime_taken_hours, vacation_type, sick_status, weekend_travel
|
||||
} = req.body;
|
||||
const userId = req.session.userId;
|
||||
|
||||
@@ -27,6 +28,10 @@ function registerTimesheetRoutes(app) {
|
||||
// Normalisiere sick_status: Boolean oder 1/0 zu Boolean
|
||||
const isSick = sick_status === true || sick_status === 1 || sick_status === 'true' || sick_status === '1';
|
||||
|
||||
// Normalisiere weekend_travel: Boolean oder 1/0 zu Integer
|
||||
const isWeekendTravel = weekend_travel === true || weekend_travel === 1 || weekend_travel === 'true' || weekend_travel === '1';
|
||||
const weekendTravelValue = isWeekendTravel ? 1 : 0;
|
||||
|
||||
// Wochenend-Prozentsätze laden
|
||||
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
|
||||
if (err) {
|
||||
@@ -66,6 +71,11 @@ function registerTimesheetRoutes(app) {
|
||||
isFullDayOvertime = true;
|
||||
}
|
||||
|
||||
// Prüfe ob es ein Wochenendtag ist
|
||||
const dateObj = new Date(date);
|
||||
const dayOfWeek = dateObj.getDay();
|
||||
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0); // 6 = Samstag, 0 = Sonntag
|
||||
|
||||
// Gesamtstunden berechnen (aus Start- und Endzeit, nicht aus Tätigkeiten)
|
||||
// Wenn ganzer Tag Urlaub oder Krank, dann zählt dieser als 8 Stunden normale Arbeitszeit
|
||||
let total_hours = 0;
|
||||
@@ -77,6 +87,7 @@ function registerTimesheetRoutes(app) {
|
||||
let finalActivity5Desc = activity5_desc;
|
||||
let finalStartTime = normalizedStartTime;
|
||||
let finalEndTime = normalizedEndTime;
|
||||
let appliedWeekendPercentage = null; // Wird gesetzt wenn Wochenend-Prozentsatz angewendet wird
|
||||
|
||||
// Überstunden-Logik: Bei vollem Tag Überstunden
|
||||
if (isFullDayOvertime) {
|
||||
@@ -95,10 +106,14 @@ function registerTimesheetRoutes(app) {
|
||||
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||
const diffMs = end - start;
|
||||
total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60);
|
||||
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Urlaub/Krankheit)
|
||||
const weekendPercentage = getWeekendPercentage(date);
|
||||
if (weekendPercentage >= 100 && total_hours > 0 && !isSick && vacation_type !== 'full') {
|
||||
total_hours = total_hours * (weekendPercentage / 100);
|
||||
|
||||
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
||||
if (isWeekend && isWeekendTravel && total_hours > 0 && !isSick && vacation_type !== 'full') {
|
||||
const weekendPercentage = getWeekendPercentage(date);
|
||||
if (weekendPercentage >= 100) {
|
||||
total_hours = total_hours * (weekendPercentage / 100);
|
||||
appliedWeekendPercentage = weekendPercentage; // Speichere den angewendeten Prozentsatz
|
||||
}
|
||||
}
|
||||
// Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden
|
||||
// Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt
|
||||
@@ -108,9 +123,34 @@ function registerTimesheetRoutes(app) {
|
||||
// Sie werden über overtime_taken_hours in der PDF angezeigt
|
||||
|
||||
// Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren
|
||||
db.get('SELECT id FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
||||
db.get('SELECT id, applied_weekend_percentage FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
||||
[userId, date], (err, row) => {
|
||||
if (row) {
|
||||
// Wenn bereits ein gespeicherter Prozentsatz existiert, diesen verwenden (historische Einträge bleiben unverändert)
|
||||
let finalAppliedPercentage = appliedWeekendPercentage;
|
||||
if (row.applied_weekend_percentage !== null && row.applied_weekend_percentage !== undefined) {
|
||||
// Verwende den gespeicherten Prozentsatz, aber nur wenn weekend_travel aktiviert ist
|
||||
if (isWeekendTravel && isWeekend) {
|
||||
finalAppliedPercentage = row.applied_weekend_percentage;
|
||||
// Berechne total_hours neu mit gespeichertem Prozentsatz, falls nötig
|
||||
if (normalizedStartTime && normalizedEndTime && !isSick && vacation_type !== 'full' && !isFullDayOvertime) {
|
||||
const start = new Date(`2000-01-01T${normalizedStartTime}`);
|
||||
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||
const diffMs = end - start;
|
||||
const baseHours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60);
|
||||
if (baseHours > 0 && finalAppliedPercentage >= 100) {
|
||||
total_hours = baseHours * (finalAppliedPercentage / 100);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Wenn weekend_travel nicht aktiviert ist, aber ein gespeicherter Prozentsatz existiert, behalte ihn
|
||||
finalAppliedPercentage = row.applied_weekend_percentage;
|
||||
}
|
||||
} else if (isWeekendTravel && isWeekend) {
|
||||
// Neuer Eintrag mit weekend_travel - speichere den aktuellen Prozentsatz
|
||||
finalAppliedPercentage = appliedWeekendPercentage;
|
||||
}
|
||||
|
||||
// Update
|
||||
db.run(`UPDATE timesheet_entries
|
||||
SET start_time = ?, end_time = ?, break_minutes = ?, total_hours = ?, notes = ?,
|
||||
@@ -120,6 +160,7 @@ function registerTimesheetRoutes(app) {
|
||||
activity4_desc = ?, activity4_hours = ?, activity4_project_number = ?,
|
||||
activity5_desc = ?, activity5_hours = ?, activity5_project_number = ?,
|
||||
overtime_taken_hours = ?, vacation_type = ?, sick_status = ?,
|
||||
weekend_travel = ?, applied_weekend_percentage = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[
|
||||
@@ -132,6 +173,8 @@ function registerTimesheetRoutes(app) {
|
||||
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
|
||||
vacation_type || null,
|
||||
isSick ? 1 : 0,
|
||||
weekendTravelValue,
|
||||
finalAppliedPercentage,
|
||||
row.id
|
||||
],
|
||||
(err) => {
|
||||
@@ -150,8 +193,8 @@ function registerTimesheetRoutes(app) {
|
||||
activity3_desc, activity3_hours, activity3_project_number,
|
||||
activity4_desc, activity4_hours, activity4_project_number,
|
||||
activity5_desc, activity5_hours, activity5_project_number,
|
||||
overtime_taken_hours, vacation_type, sick_status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
overtime_taken_hours, vacation_type, sick_status, weekend_travel, applied_weekend_percentage)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
userId, date, finalStartTime, finalEndTime, break_minutes, total_hours, notes,
|
||||
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
||||
@@ -161,7 +204,9 @@ function registerTimesheetRoutes(app) {
|
||||
finalActivity5Desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null,
|
||||
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
|
||||
vacation_type || null,
|
||||
isSick ? 1 : 0
|
||||
isSick ? 1 : 0,
|
||||
weekendTravelValue,
|
||||
appliedWeekendPercentage
|
||||
],
|
||||
(err) => {
|
||||
if (err) {
|
||||
@@ -368,6 +413,40 @@ function registerTimesheetRoutes(app) {
|
||||
});
|
||||
});
|
||||
|
||||
// API: Neueste eingereichte Version für eine Woche abrufen
|
||||
app.get('/api/timesheet/latest-submitted/:weekStart', requireAuth, (req, res) => {
|
||||
const userId = req.session.userId;
|
||||
const weekStart = req.params.weekStart;
|
||||
|
||||
// Berechne Wochenende
|
||||
const startDate = new Date(weekStart);
|
||||
const endDate = new Date(startDate);
|
||||
endDate.setDate(endDate.getDate() + 6);
|
||||
const weekEnd = endDate.toISOString().split('T')[0];
|
||||
|
||||
// Hole die neueste eingereichte Version für diese Woche
|
||||
db.get(`SELECT id, version, submitted_at FROM weekly_timesheets
|
||||
WHERE user_id = ? AND week_start = ? AND week_end = ?
|
||||
ORDER BY version DESC LIMIT 1`,
|
||||
[userId, weekStart, weekEnd],
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
console.error('Fehler beim Abrufen der neuesten Version:', err);
|
||||
return res.status(500).json({ error: 'Fehler beim Abrufen der Version' });
|
||||
}
|
||||
|
||||
if (result) {
|
||||
res.json({
|
||||
timesheetId: result.id,
|
||||
version: result.version,
|
||||
submitted_at: result.submitted_at
|
||||
});
|
||||
} else {
|
||||
res.json({ timesheetId: null, version: null, submitted_at: null });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API: PDF Download-Info abrufen
|
||||
app.get('/api/timesheet/download-info/:id', requireVerwaltung, (req, res) => {
|
||||
const timesheetId = req.params.id;
|
||||
@@ -398,9 +477,24 @@ function registerTimesheetRoutes(app) {
|
||||
});
|
||||
|
||||
// API: PDF generieren
|
||||
app.get('/api/timesheet/pdf/:id', requireVerwaltung, (req, res) => {
|
||||
app.get('/api/timesheet/pdf/:id', requireAuth, (req, res) => {
|
||||
const timesheetId = req.params.id;
|
||||
generatePDF(timesheetId, req, res);
|
||||
const userId = req.session.userId;
|
||||
const isVerwaltung = hasRole(req, 'verwaltung') || hasRole(req, 'admin');
|
||||
|
||||
// Prüfe ob User Verwaltung/Admin ist oder ob das Timesheet dem User gehört
|
||||
db.get(`SELECT user_id FROM weekly_timesheets WHERE id = ?`, [timesheetId], (err, timesheet) => {
|
||||
if (err || !timesheet) {
|
||||
return res.status(404).send('Stundenzettel nicht gefunden');
|
||||
}
|
||||
|
||||
// Zugriff erlauben wenn Verwaltung/Admin ODER wenn Timesheet dem User gehört
|
||||
if (isVerwaltung || timesheet.user_id === userId) {
|
||||
generatePDF(timesheetId, req, res);
|
||||
} else {
|
||||
res.status(403).send('Zugriff verweigert');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ function registerUserRoutes(app) {
|
||||
}
|
||||
|
||||
// User-Daten abrufen
|
||||
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => {
|
||||
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours, vacation_offset_days FROM users WHERE id = ?', [userId], (err, user) => {
|
||||
if (err || !user) {
|
||||
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
||||
}
|
||||
@@ -231,6 +231,7 @@ function registerUserRoutes(app) {
|
||||
const wochenstunden = user.wochenstunden || 0;
|
||||
const urlaubstage = user.urlaubstage || 0;
|
||||
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
||||
const vacationOffsetDays = user.vacation_offset_days ? parseFloat(user.vacation_offset_days) : 0;
|
||||
|
||||
// Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte)
|
||||
const { getCalendarWeek } = require('../helpers/utils');
|
||||
@@ -282,14 +283,15 @@ function registerUserRoutes(app) {
|
||||
if (!weeks || weeks.length === 0) {
|
||||
return res.json({
|
||||
currentOvertime: overtimeOffsetHours,
|
||||
remainingVacation: urlaubstage,
|
||||
remainingVacation: urlaubstage + vacationOffsetDays,
|
||||
totalOvertimeHours: 0,
|
||||
totalOvertimeTaken: 0,
|
||||
totalVacationDays: 0,
|
||||
plannedVacationDays: plannedVacationDays,
|
||||
plannedWeeks: plannedWeeks,
|
||||
urlaubstage: urlaubstage,
|
||||
overtimeOffsetHours: overtimeOffsetHours
|
||||
overtimeOffsetHours: overtimeOffsetHours,
|
||||
vacationOffsetDays: vacationOffsetDays
|
||||
});
|
||||
}
|
||||
|
||||
@@ -381,7 +383,7 @@ function registerUserRoutes(app) {
|
||||
processedWeeks++;
|
||||
if (processedWeeks === weeks.length && !hasError) {
|
||||
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
||||
const remainingVacation = urlaubstage - totalVacationDays;
|
||||
const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays;
|
||||
|
||||
res.json({
|
||||
currentOvertime: currentOvertime,
|
||||
@@ -392,7 +394,8 @@ function registerUserRoutes(app) {
|
||||
plannedVacationDays: plannedVacationDays,
|
||||
plannedWeeks: plannedWeeks,
|
||||
urlaubstage: urlaubstage,
|
||||
overtimeOffsetHours: overtimeOffsetHours
|
||||
overtimeOffsetHours: overtimeOffsetHours,
|
||||
vacationOffsetDays: vacationOffsetDays
|
||||
});
|
||||
}
|
||||
return; // Überspringe diese Woche
|
||||
@@ -479,7 +482,7 @@ function registerUserRoutes(app) {
|
||||
// weekOvertimeHours enthält bereits die korrekte Berechnung pro Woche (wie im Frontend)
|
||||
// weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden)
|
||||
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
||||
const remainingVacation = urlaubstage - totalVacationDays;
|
||||
const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays;
|
||||
|
||||
res.json({
|
||||
currentOvertime: currentOvertime,
|
||||
@@ -490,7 +493,8 @@ function registerUserRoutes(app) {
|
||||
plannedVacationDays: plannedVacationDays,
|
||||
plannedWeeks: plannedWeeks,
|
||||
urlaubstage: urlaubstage,
|
||||
overtimeOffsetHours: overtimeOffsetHours
|
||||
overtimeOffsetHours: overtimeOffsetHours,
|
||||
vacationOffsetDays: vacationOffsetDays
|
||||
});
|
||||
}
|
||||
}); // getHolidaysForDateRange.then
|
||||
@@ -12,7 +12,7 @@ function registerVerwaltungRoutes(app) {
|
||||
// Verwaltungs-Bereich
|
||||
app.get('/verwaltung', requireVerwaltung, (req, res) => {
|
||||
db.all(`
|
||||
SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, u.overtime_offset_hours,
|
||||
SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, u.overtime_offset_hours, u.vacation_offset_days,
|
||||
dl.firstname as downloaded_by_firstname,
|
||||
dl.lastname as downloaded_by_lastname,
|
||||
(SELECT COUNT(*) FROM weekly_timesheets wt2
|
||||
@@ -44,7 +44,8 @@ function registerVerwaltungRoutes(app) {
|
||||
personalnummer: ts.personalnummer,
|
||||
wochenstunden: ts.wochenstunden,
|
||||
urlaubstage: ts.urlaubstage,
|
||||
overtime_offset_hours: ts.overtime_offset_hours
|
||||
overtime_offset_hours: ts.overtime_offset_hours,
|
||||
vacation_offset_days: ts.vacation_offset_days
|
||||
},
|
||||
weeks: {}
|
||||
};
|
||||
@@ -64,6 +65,38 @@ function registerVerwaltungRoutes(app) {
|
||||
groupedByEmployee[userId].weeks[weekKey].versions.push(ts);
|
||||
});
|
||||
|
||||
// Prüfe für jede Woche, ob nach dem letzten Download eine neue Version eingereicht wurde
|
||||
Object.values(groupedByEmployee).forEach(employee => {
|
||||
Object.values(employee.weeks).forEach(week => {
|
||||
// Finde die neueste Version mit pdf_downloaded_at (letzter Download)
|
||||
let lastDownloadTime = null;
|
||||
week.versions.forEach(version => {
|
||||
if (version.pdf_downloaded_at) {
|
||||
const downloadTime = new Date(version.pdf_downloaded_at).getTime();
|
||||
if (!lastDownloadTime || downloadTime > lastDownloadTime) {
|
||||
lastDownloadTime = downloadTime;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Prüfe, ob es eine Version gibt, die nach dem letzten Download eingereicht wurde
|
||||
let hasNewVersionAfterDownload = false;
|
||||
if (lastDownloadTime) {
|
||||
week.versions.forEach(version => {
|
||||
if (version.submitted_at) {
|
||||
const submittedTime = new Date(version.submitted_at).getTime();
|
||||
if (submittedTime > lastDownloadTime) {
|
||||
hasNewVersionAfterDownload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setze Flag auf dem week-Objekt
|
||||
week.has_new_version_after_download = hasNewVersionAfterDownload;
|
||||
});
|
||||
});
|
||||
|
||||
// Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst)
|
||||
const sortedEmployees = Object.values(groupedByEmployee).map(employee => {
|
||||
// Wochen innerhalb jedes Mitarbeiters sortieren
|
||||
@@ -114,6 +147,26 @@ function registerVerwaltungRoutes(app) {
|
||||
});
|
||||
});
|
||||
|
||||
// API: Urlaubstage-Offset für einen User setzen (positiv/negativ)
|
||||
app.put('/api/verwaltung/user/:id/vacation-offset', requireVerwaltung, (req, res) => {
|
||||
const userId = req.params.id;
|
||||
const raw = req.body ? req.body.vacation_offset_days : undefined;
|
||||
|
||||
// Leere Eingabe => 0
|
||||
const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw);
|
||||
if (!Number.isFinite(normalized)) {
|
||||
return res.status(400).json({ error: 'Ungültiger Urlaubstage-Offset' });
|
||||
}
|
||||
|
||||
db.run('UPDATE users SET vacation_offset_days = ? WHERE id = ?', [normalized, userId], (err) => {
|
||||
if (err) {
|
||||
console.error('Fehler beim Speichern des Urlaubstage-Offsets:', err);
|
||||
return res.status(500).json({ error: 'Fehler beim Speichern des Urlaubstage-Offsets' });
|
||||
}
|
||||
res.json({ success: true, vacation_offset_days: normalized });
|
||||
});
|
||||
});
|
||||
|
||||
// API: Krankheitstage für einen User im aktuellen Jahr abrufen
|
||||
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
|
||||
const userId = req.params.id;
|
||||
@@ -164,7 +217,7 @@ function registerVerwaltungRoutes(app) {
|
||||
}
|
||||
|
||||
// User-Daten abrufen
|
||||
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => {
|
||||
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours, vacation_offset_days FROM users WHERE id = ?', [userId], (err, user) => {
|
||||
if (err || !user) {
|
||||
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
||||
}
|
||||
@@ -172,6 +225,7 @@ function registerVerwaltungRoutes(app) {
|
||||
const wochenstunden = user.wochenstunden || 0;
|
||||
const urlaubstage = user.urlaubstage || 0;
|
||||
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
||||
const vacationOffsetDays = user.vacation_offset_days ? parseFloat(user.vacation_offset_days) : 0;
|
||||
|
||||
// Einträge für die Woche abrufen
|
||||
db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type, sick_status
|
||||
@@ -251,7 +305,7 @@ function registerVerwaltungRoutes(app) {
|
||||
const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours;
|
||||
|
||||
// Verbleibende Urlaubstage
|
||||
const remainingVacation = urlaubstage - vacationDays;
|
||||
const remainingVacation = urlaubstage - vacationDays + vacationOffsetDays;
|
||||
|
||||
res.json({
|
||||
wochenstunden,
|
||||
@@ -264,6 +318,7 @@ function registerVerwaltungRoutes(app) {
|
||||
overtimeOffsetHours,
|
||||
remainingOvertimeWithOffset,
|
||||
vacationDays,
|
||||
vacationOffsetDays,
|
||||
remainingVacation,
|
||||
sickDays,
|
||||
workdays
|
||||
Reference in New Issue
Block a user