This commit is contained in:
Carsten Graf
2026-01-26 17:58:12 +01:00
parent dfbc7d8bbc
commit ca48cdf78f
9 changed files with 971 additions and 178 deletions

View File

@@ -79,7 +79,8 @@ function registerAuthRoutes(app) {
LDAPService.authenticate(username, password, (authErr, authSuccess) => {
if (authErr || !authSuccess) {
// LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
if (err || !user) {
return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
}
@@ -93,7 +94,8 @@ function registerAuthRoutes(app) {
});
} else {
// LDAP-Authentifizierung erfolgreich - hole Benutzer aus Datenbank
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
if (err || !user) {
return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' });
}
@@ -104,7 +106,8 @@ function registerAuthRoutes(app) {
});
} else {
// LDAP nicht aktiviert - verwende lokale Authentifizierung
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
if (err || !user) {
return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
}

View File

@@ -190,12 +190,12 @@ function registerTimesheetRoutes(app) {
const { week_start, week_end, version_reason } = req.body;
const userId = req.session.userId;
// Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind
db.all(`SELECT id, date, start_time, end_time, vacation_type, sick_status, updated_at FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date, updated_at DESC, id DESC`,
[userId, week_start, week_end],
(err, entries) => {
// Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind
db.all(`SELECT id, date, start_time, end_time, vacation_type, sick_status, overtime_taken_hours, updated_at FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date, updated_at DESC, id DESC`,
[userId, week_start, week_end],
(err, entries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Prüfen der Daten' });
}
@@ -221,81 +221,100 @@ function registerTimesheetRoutes(app) {
// Prüfe nur Werktage (Montag-Freitag, erste 5 Tage)
// Samstag und Sonntag sind optional
// Bei ganztägigem Urlaub (vacation_type = 'full') ist der Tag als ausgefüllt zu betrachten
// Bei 8 Überstunden (ganzer Tag) ist der Tag auch als ausgefüllt zu betrachten
// week_start ist bereits im Format YYYY-MM-DD
const startDateParts = week_start.split('-');
const startYear = parseInt(startDateParts[0]);
const startMonth = parseInt(startDateParts[1]) - 1; // Monat ist 0-basiert
const startDay = parseInt(startDateParts[2]);
let missingDays = [];
for (let i = 0; i < 5; i++) {
// Datum direkt berechnen ohne Zeitzonenprobleme
const date = new Date(startYear, startMonth, startDay + i);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
const entry = entriesByDate[dateStr];
// Wenn ganztägiger Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
const isSick = entry && (entry.sick_status === 1 || entry.sick_status === true);
if (entry && (entry.vacation_type === 'full' || isSick)) {
continue; // Tag ist ausgefüllt
// User-Daten laden für Überstunden-Berechnung
db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' });
}
// Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein
// start_time und end_time könnten null, undefined oder leer strings sein
const hasStartTime = entry && entry.start_time && entry.start_time.toString().trim() !== '';
const hasEndTime = entry && entry.end_time && entry.end_time.toString().trim() !== '';
const wochenstunden = user?.wochenstunden || 0;
const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8;
if (!entry || !hasStartTime || !hasEndTime) {
missingDays.push(dateStr);
}
}
if (missingDays.length > 0) {
return res.status(400).json({
error: `Nicht alle Werktage (Montag bis Freitag) sind ausgefüllt. Fehlende Tage: ${missingDays.join(', ')}. Bitte füllen Sie alle Werktage mit Start- und Endzeit aus. Wochenende ist optional.`
});
}
// Alle Tage ausgefüllt - Woche abschicken (immer neue Version erstellen)
// Prüfe welche Version die letzte ist
db.get(`SELECT MAX(version) as max_version FROM weekly_timesheets
WHERE user_id = ? AND week_start = ? AND week_end = ?`,
[userId, week_start, week_end],
(err, result) => {
if (err) return res.status(500).json({ error: 'Fehler beim Prüfen der Version' });
let missingDays = [];
for (let i = 0; i < 5; i++) {
// Datum direkt berechnen ohne Zeitzonenprobleme
const date = new Date(startYear, startMonth, startDay + i);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
const entry = entriesByDate[dateStr];
const maxVersion = result && result.max_version ? result.max_version : 0;
const newVersion = maxVersion + 1;
// Wenn bereits eine Version existiert, ist version_reason erforderlich
if (maxVersion > 0 && (!version_reason || version_reason.trim() === '')) {
return res.status(400).json({
error: 'Bitte geben Sie einen Grund für die neue Version an.'
});
// Wenn ganztägiger Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
const isSick = entry && (entry.sick_status === 1 || entry.sick_status === true);
if (entry && (entry.vacation_type === 'full' || isSick)) {
continue; // Tag ist ausgefüllt
}
// Neue Version erstellen (nicht überschreiben)
db.run(`INSERT INTO weekly_timesheets (user_id, week_start, week_end, version, status, version_reason)
VALUES (?, ?, ?, ?, 'eingereicht', ?)`,
[userId, week_start, week_end, newVersion, version_reason ? version_reason.trim() : null],
(err) => {
if (err) return res.status(500).json({ error: 'Fehler beim Abschicken' });
// Status der Einträge aktualisieren (optional - für Nachverfolgung)
db.run(`UPDATE timesheet_entries
SET status = 'eingereicht'
WHERE user_id = ? AND date >= ? AND date <= ?`,
[userId, week_start, week_end],
(err) => {
if (err) return res.status(500).json({ error: 'Fehler beim Aktualisieren des Status' });
res.json({ success: true, version: newVersion });
});
});
});
// Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind
const overtimeValue = entry && entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0;
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
if (isFullDayOvertime) {
continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag)
}
// Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein
// start_time und end_time könnten null, undefined oder leer strings sein
const hasStartTime = entry && entry.start_time && entry.start_time.toString().trim() !== '';
const hasEndTime = entry && entry.end_time && entry.end_time.toString().trim() !== '';
if (!entry || !hasStartTime || !hasEndTime) {
missingDays.push(dateStr);
}
}
if (missingDays.length > 0) {
return res.status(400).json({
error: `Nicht alle Werktage (Montag bis Freitag) sind ausgefüllt. Fehlende Tage: ${missingDays.join(', ')}. Bitte füllen Sie alle Werktage mit Start- und Endzeit aus. Wochenende ist optional.`
});
}
// Alle Tage ausgefüllt - Woche abschicken (immer neue Version erstellen)
// Prüfe welche Version die letzte ist
db.get(`SELECT MAX(version) as max_version FROM weekly_timesheets
WHERE user_id = ? AND week_start = ? AND week_end = ?`,
[userId, week_start, week_end],
(err, result) => {
if (err) return res.status(500).json({ error: 'Fehler beim Prüfen der Version' });
const maxVersion = result && result.max_version ? result.max_version : 0;
const newVersion = maxVersion + 1;
// Wenn bereits eine Version existiert, ist version_reason erforderlich
if (maxVersion > 0 && (!version_reason || version_reason.trim() === '')) {
return res.status(400).json({
error: 'Bitte geben Sie einen Grund für die neue Version an.'
});
}
// Neue Version erstellen (nicht überschreiben)
db.run(`INSERT INTO weekly_timesheets (user_id, week_start, week_end, version, status, version_reason)
VALUES (?, ?, ?, ?, 'eingereicht', ?)`,
[userId, week_start, week_end, newVersion, version_reason ? version_reason.trim() : null],
(err) => {
if (err) return res.status(500).json({ error: 'Fehler beim Abschicken' });
// Status der Einträge aktualisieren (optional - für Nachverfolgung)
db.run(`UPDATE timesheet_entries
SET status = 'eingereicht'
WHERE user_id = ? AND date >= ? AND date <= ?`,
[userId, week_start, week_end],
(err) => {
if (err) return res.status(500).json({ error: 'Fehler beim Aktualisieren des Status' });
res.json({ success: true, version: newVersion });
});
});
});
});
});
});

View File

@@ -51,6 +51,22 @@ function registerUserRoutes(app) {
});
});
// API: Client-IP abrufen
app.get('/api/user/client-ip', requireAuth, (req, res) => {
// Versuche verschiedene Methoden, um die Client-IP zu erhalten
const clientIp = req.ip ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : null) ||
req.headers['x-real-ip'] ||
'unknown';
// Entferne IPv6-Präfix falls vorhanden (::ffff:192.168.1.1 -> 192.168.1.1)
const cleanIp = clientIp.replace(/^::ffff:/, '');
res.json({ client_ip: cleanIp });
});
// API: Ping-IP abrufen
app.get('/api/user/ping-ip', requireAuth, (req, res) => {
const userId = req.session.userId;
@@ -120,6 +136,52 @@ function registerUserRoutes(app) {
res.json({ success: true, currentRole: role });
});
// API: Verplante Urlaubstage (alle Wochen, auch nicht-eingereichte)
app.get('/api/user/planned-vacation', requireAuth, (req, res) => {
const userId = req.session.userId;
const { getCalendarWeek } = require('../helpers/utils');
db.all(`SELECT date, vacation_type FROM timesheet_entries
WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`,
[userId],
(err, entries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' });
}
let plannedDays = 0;
const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } }
entries.forEach(entry => {
const dayValue = entry.vacation_type === 'full' ? 1 : 0.5;
plannedDays += dayValue;
// Berechne Kalenderwoche
const date = new Date(entry.date);
const year = date.getFullYear();
const week = getCalendarWeek(entry.date);
const weekKey = `${year}-KW${week}`;
if (!weeksMap[weekKey]) {
weeksMap[weekKey] = { year, week, days: 0 };
}
weeksMap[weekKey].days += dayValue;
});
// Konvertiere zu sortiertem Array
const weeks = Object.values(weeksMap).sort((a, b) => {
if (a.year !== b.year) return a.year - b.year;
return a.week - b.week;
});
res.json({
plannedVacationDays: plannedDays,
weeks: weeks
});
}
);
});
// API: Gesamtstatistiken für Mitarbeiter (Überstunden und Urlaubstage)
app.get('/api/user/stats', requireAuth, (req, res) => {
const userId = req.session.userId;
@@ -134,29 +196,66 @@ function registerUserRoutes(app) {
const urlaubstage = user.urlaubstage || 0;
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
// Alle eingereichten Wochen abrufen
db.all(`SELECT DISTINCT week_start, week_end
FROM weekly_timesheets
WHERE user_id = ? AND status = 'eingereicht'
ORDER BY week_start`,
// Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte)
const { getCalendarWeek } = require('../helpers/utils');
db.all(`SELECT date, vacation_type FROM timesheet_entries
WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`,
[userId],
(err, weeks) => {
(err, allVacationEntries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' });
}
// Wenn keine Wochen vorhanden
if (!weeks || weeks.length === 0) {
return res.json({
currentOvertime: overtimeOffsetHours,
remainingVacation: urlaubstage,
totalOvertimeHours: 0,
totalOvertimeTaken: 0,
totalVacationDays: 0,
urlaubstage: urlaubstage,
overtimeOffsetHours: overtimeOffsetHours
});
}
let plannedVacationDays = 0;
const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } }
(allVacationEntries || []).forEach(entry => {
const dayValue = entry.vacation_type === 'full' ? 1 : 0.5;
plannedVacationDays += dayValue;
// Berechne Kalenderwoche
const date = new Date(entry.date);
const year = date.getFullYear();
const week = getCalendarWeek(entry.date);
const weekKey = `${year}-KW${week}`;
if (!weeksMap[weekKey]) {
weeksMap[weekKey] = { year, week, days: 0 };
}
weeksMap[weekKey].days += dayValue;
});
// Konvertiere zu sortiertem Array
const plannedWeeks = Object.values(weeksMap).sort((a, b) => {
if (a.year !== b.year) return a.year - b.year;
return a.week - b.week;
});
// Alle eingereichten Wochen abrufen
db.all(`SELECT DISTINCT week_start, week_end
FROM weekly_timesheets
WHERE user_id = ? AND status = 'eingereicht'
ORDER BY week_start`,
[userId],
(err, weeks) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
}
// Wenn keine Wochen vorhanden
if (!weeks || weeks.length === 0) {
return res.json({
currentOvertime: overtimeOffsetHours,
remainingVacation: urlaubstage,
totalOvertimeHours: 0,
totalOvertimeTaken: 0,
totalVacationDays: 0,
plannedVacationDays: plannedVacationDays,
plannedWeeks: plannedWeeks,
urlaubstage: urlaubstage,
overtimeOffsetHours: overtimeOffsetHours
});
}
let totalOvertimeHours = 0;
let totalOvertimeTaken = 0;
@@ -246,6 +345,8 @@ function registerUserRoutes(app) {
totalOvertimeHours: totalOvertimeHours,
totalOvertimeTaken: totalOvertimeTaken,
totalVacationDays: totalVacationDays,
plannedVacationDays: plannedVacationDays,
plannedWeeks: plannedWeeks,
urlaubstage: urlaubstage,
overtimeOffsetHours: overtimeOffsetHours
});
@@ -259,11 +360,24 @@ function registerUserRoutes(app) {
let weekVacationDays = 0;
let weekVacationHours = 0;
const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8;
let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden
entries.forEach(entry => {
// Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind
const overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0;
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
if (entry.overtime_taken_hours) {
weekOvertimeTaken += entry.overtime_taken_hours;
}
// Wenn 8 Überstunden eingetragen sind, zählt der Tag als 0 Stunden
// Diese Tage werden separat gezählt, um die Sollstunden anzupassen
if (isFullDayOvertime) {
fullDayOvertimeDays++;
}
// Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden
if (entry.vacation_type === 'full') {
weekVacationDays += 1;
@@ -273,12 +387,12 @@ function registerUserRoutes(app) {
weekVacationDays += 0.5;
weekVacationHours += 4; // Halber Tag = 4 Stunden
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
if (entry.total_hours) {
if (entry.total_hours && !isFullDayOvertime) {
weekTotalHours += entry.total_hours;
}
} else {
// Kein Urlaub - zähle nur Arbeitsstunden
if (entry.total_hours) {
// Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden)
if (entry.total_hours && !isFullDayOvertime) {
weekTotalHours += entry.total_hours;
}
}
@@ -287,11 +401,22 @@ function registerUserRoutes(app) {
// Sollstunden berechnen
const sollStunden = (wochenstunden / 5) * workdays;
// Überstunden für diese Woche: Urlaub zählt als normale Arbeitszeit
// Überstunden für diese Woche berechnen (wie im Frontend: totalHoursWithVacation - sollStunden)
// Wenn 8 Überstunden genommen wurden, zählen diese Tage als 0 Stunden
// Die Berechnung: (totalHours + vacationHours) - sollStunden
// Bei 8 Überstunden: totalHours = 0, daher: 0 - sollStunden = -sollStunden für diese Woche
// Die negativen Stunden (wegen 0 statt Sollstunden) werden durch die verbrauchten Überstunden ausgeglichen
// Daher: adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours)
// So werden die Tage mit 8 Überstunden nicht zu negativen Überstunden führen
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours;
const weekOvertimeHours = weekTotalHoursWithVacation - sollStunden;
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
// weekOvertimeHours = Überstunden diese Woche (wie im Frontend berechnet)
const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden;
// Kumulativ addieren
// WICHTIG: weekOvertimeHours enthält bereits die Überstunden dieser Woche (kann negativ sein bei 8 Überstunden)
// weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden)
// Die aktuellen Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden
totalOvertimeHours += weekOvertimeHours;
totalOvertimeTaken += weekOvertimeTaken;
totalVacationDays += weekVacationDays;
@@ -300,6 +425,9 @@ function registerUserRoutes(app) {
// Wenn alle Wochen verarbeitet wurden, Antwort senden
if (processedWeeks === weeks.length && !hasError) {
// Aktuelle Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden + Offset
// 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;
@@ -309,6 +437,8 @@ function registerUserRoutes(app) {
totalOvertimeHours: totalOvertimeHours,
totalOvertimeTaken: totalOvertimeTaken,
totalVacationDays: totalVacationDays,
plannedVacationDays: plannedVacationDays,
plannedWeeks: plannedWeeks,
urlaubstage: urlaubstage,
overtimeOffsetHours: overtimeOffsetHours
});
@@ -316,6 +446,7 @@ function registerUserRoutes(app) {
});
});
});
});
});
});
}