Add Prozent im ADmin für wochenende

This commit is contained in:
2026-01-30 19:43:21 +01:00
parent 32f40124a8
commit f16593a345
9 changed files with 716 additions and 369 deletions

View File

@@ -20,7 +20,7 @@ function initDatabase() {
last_week_start TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Migration: last_week_start Spalte hinzufügen falls sie nicht existiert
db.run(`ALTER TABLE users ADD COLUMN last_week_start TEXT`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
@@ -51,7 +51,7 @@ function initDatabase() {
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)`);
// Migration: Tätigkeitsfelder hinzufügen falls sie nicht existieren
const activityColumns = [
'activity1_desc', 'activity1_hours',
@@ -60,7 +60,7 @@ function initDatabase() {
'activity4_desc', 'activity4_hours',
'activity5_desc', 'activity5_hours'
];
activityColumns.forEach(col => {
const colType = col.includes('_hours') ? 'REAL' : 'TEXT';
db.run(`ALTER TABLE timesheet_entries ADD COLUMN ${col} ${colType}`, (err) => {
@@ -85,18 +85,18 @@ function initDatabase() {
FOREIGN KEY (reviewed_by) REFERENCES users(id),
FOREIGN KEY (pdf_downloaded_by) REFERENCES users(id)
)`);
// Migration: version Spalte hinzufügen falls sie nicht existiert
db.run(`ALTER TABLE weekly_timesheets ADD COLUMN version INTEGER DEFAULT 1`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
// Wenn Spalte neu erstellt wurde, bestehende Einträge haben automatisch version = 1
});
// Migration: pdf_downloaded_at Spalte hinzufügen falls sie nicht existiert
db.run(`ALTER TABLE weekly_timesheets ADD COLUMN pdf_downloaded_at DATETIME`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
});
// Migration: pdf_downloaded_by Spalte hinzufügen falls sie nicht existiert
db.run(`ALTER TABLE weekly_timesheets ADD COLUMN pdf_downloaded_by INTEGER`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
@@ -124,7 +124,7 @@ function initDatabase() {
'activity3_project_number', 'activity4_project_number',
'activity5_project_number'
];
projectNumberColumns.forEach(col => {
db.run(`ALTER TABLE timesheet_entries ADD COLUMN ${col} TEXT`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
@@ -141,7 +141,7 @@ function initDatabase() {
console.warn('Warnung beim Hinzufügen der Spalte overtime_taken_hours:', err.message);
}
});
db.run(`ALTER TABLE timesheet_entries ADD COLUMN vacation_type TEXT`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
if (err && !err.message.includes('duplicate column')) {
@@ -164,7 +164,7 @@ function initDatabase() {
console.warn('Warnung beim Hinzufügen der Spalte pause_start_time:', err.message);
}
});
db.run(`ALTER TABLE timesheet_entries ADD COLUMN pause_end_time TEXT`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
if (err && !err.message.includes('duplicate column')) {
@@ -176,15 +176,15 @@ function initDatabase() {
db.run(`ALTER TABLE users ADD COLUMN personalnummer TEXT`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
});
db.run(`ALTER TABLE users ADD COLUMN wochenstunden REAL`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
});
db.run(`ALTER TABLE users ADD COLUMN urlaubstage REAL`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
});
// Migration: Überstunden-Offset (manuelle Korrektur durch Verwaltung)
db.run(`ALTER TABLE users ADD COLUMN overtime_offset_hours REAL DEFAULT 0`, (err) => {
// Fehler ignorieren wenn Spalte bereits existiert
@@ -249,6 +249,22 @@ function initDatabase() {
name TEXT
)`);
// System-Optionen-Tabelle für Wochenend-Prozentsätze
db.run(`CREATE TABLE IF NOT EXISTS system_options (
id INTEGER PRIMARY KEY DEFAULT 1,
saturday_percentage REAL DEFAULT 100,
sunday_percentage REAL DEFAULT 100,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
CHECK (id = 1)
)`);
// Standard-Eintrag für system_options erstellen falls nicht vorhanden
db.run(`INSERT OR IGNORE INTO system_options (id, saturday_percentage, sunday_percentage) VALUES (1, 100, 100)`, (err) => {
if (err && !err.message.includes('UNIQUE constraint')) {
console.warn('Warnung beim Erstellen des Standard-Eintrags für system_options:', err.message);
}
});
// Migration: Bestehende Rollen zu JSON-Arrays konvertieren
// Prüfe ob Rollen noch als einfache Strings gespeichert sind (nicht als JSON-Array)
db.all('SELECT id, role FROM users', (err, users) => {
@@ -265,7 +281,7 @@ function initDatabase() {
} catch (e) {
// Nicht JSON, konvertiere zu JSON-Array
}
// Konvertiere zu JSON-Array
const roleArray = JSON.stringify([roleValue]);
db.run('UPDATE users SET role = ? WHERE id = ?', [roleArray, user.id], (err) => {
@@ -280,14 +296,14 @@ function initDatabase() {
// Standard Admin-Benutzer erstellen
const adminPassword = bcrypt.hashSync('admin123', 10);
db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role)
VALUES (1, 'admin', ?, 'System', 'Administrator', ?)`,
[adminPassword, JSON.stringify(['admin'])]);
VALUES (1, 'admin', ?, 'System', 'Administrator', ?)`,
[adminPassword, JSON.stringify(['admin'])]);
// Standard Verwaltungs-Benutzer erstellen
const verwaltungPassword = bcrypt.hashSync('verwaltung123', 10);
db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role)
VALUES (2, 'verwaltung', ?, 'Verwaltung', 'User', ?)`,
[verwaltungPassword, JSON.stringify(['verwaltung'])]);
VALUES (2, 'verwaltung', ?, 'Verwaltung', 'User', ?)`,
[verwaltungPassword, JSON.stringify(['verwaltung'])]);
});
}

View File

@@ -1,21 +0,0 @@
Test - Stundenerfassung
Hallo zusammen,
Mara ist auf mich mit einer Bitte herangetreten, ob ich die Stundenerfassung digitalisieren kann.
Das habe ich die letzten 2 Wochen am abend und am WE gemacht.
Ich glaube, dass das System jetzt fest fertig ist und ihr es testen könnt
Der test soll Fehler finden und mir noch die möglichkeit geben diese dann zu beheben.
Am Montag würde ich gerne eine kurze Einführung für die Leute im Büro geben.
Um ca. 11:00 Uhr für so 10-15 Minuten.
Achtet bitte am Anfangauf die Überstundenerechnung, da könnte noch der ein oder andere Fehler drin sein.
Die Seite ist im Browser zu finden unter http://stunden.sds-systemtechnik.de:3333 oder http://192.168.120.64:3333
Viele Grüße
Carsten Graf

View File

@@ -54,6 +54,43 @@ document.addEventListener('DOMContentLoaded', function() {
// LDAP-Konfiguration laden
loadLDAPConfig();
// Optionen laden
loadOptions();
// Optionen-Formular
const optionsForm = document.getElementById('optionsForm');
if (optionsForm) {
optionsForm.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = {
saturday_percentage: document.getElementById('saturdayPercentage').value,
sunday_percentage: document.getElementById('sundayPercentage').value
};
try {
const response = await fetch('/admin/options', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
alert('Optionen wurden erfolgreich gespeichert!');
} else {
alert('Fehler: ' + (result.error || 'Optionen konnten nicht gespeichert werden'));
}
} catch (error) {
console.error('Fehler:', error);
alert('Fehler beim Speichern der Optionen');
}
});
}
// LDAP-Konfigurationsformular
const ldapConfigForm = document.getElementById('ldapConfigForm');
if (ldapConfigForm) {
@@ -161,6 +198,27 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Optionen laden und Formular ausfüllen
async function loadOptions() {
try {
const response = await fetch('/admin/options');
const result = await response.json();
if (result.config) {
const config = result.config;
if (document.getElementById('saturdayPercentage')) {
document.getElementById('saturdayPercentage').value = config.saturday_percentage || 0;
}
if (document.getElementById('sundayPercentage')) {
document.getElementById('sundayPercentage').value = config.sunday_percentage || 0;
}
}
} catch (error) {
console.error('Fehler beim Laden der Optionen:', error);
}
}
// LDAP-Konfiguration laden und Formular ausfüllen
async function loadLDAPConfig() {
try {

View File

@@ -4,6 +4,36 @@ let currentWeekStart = getMonday(new Date());
let currentEntries = {};
let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD)
let userWochenstunden = 0; // Wochenstunden des Users
let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal)
// Wochenend-Prozentsätze laden
async function loadWeekendPercentages() {
try {
const response = await fetch('/api/user/weekend-percentages');
if (!response.ok) {
throw new Error('Fehler beim Laden der Wochenend-Prozentsätze');
}
const data = await response.json();
weekendPercentages.saturday = data.saturday_percentage || 100;
weekendPercentages.sunday = data.sunday_percentage || 100;
} catch (error) {
console.error('Fehler beim Laden der Wochenend-Prozentsätze:', error);
// Standardwerte verwenden
weekendPercentages.saturday = 100;
weekendPercentages.sunday = 100;
}
}
// Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück
function getWeekendPercentage(date) {
const day = date.getDay();
if (day === 6) { // Samstag
return weekendPercentages.saturday;
} else if (day === 0) { // Sonntag
return weekendPercentages.sunday;
}
return 100; // Kein Wochenende = 100% (normal)
}
// Statistiken laden
async function loadUserStats() {
@@ -110,6 +140,9 @@ document.addEventListener('DOMContentLoaded', async function() {
// Ping-IP laden
loadPingIP();
// Wochenend-Prozentsätze laden
loadWeekendPercentages();
// Statistiken laden
loadUserStats();
@@ -374,11 +407,19 @@ function renderWeek() {
// Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt)
// Feiertag: 8h Basis + gearbeitete Stunden (jede gearbeitete Stunde = Überstunde)
// Bei halbem Tag Urlaub werden die Urlaubsstunden später in der Überstunden-Berechnung hinzugezählt
// Wochenend-Prozentsätze: Nur auf tatsächlich gearbeitete Stunden anwenden (nicht auf Urlaub, Krankheit, Feiertage)
let hoursToAdd = 0;
if (isHoliday) {
totalHours += 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden)
hoursToAdd = 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden)
} else {
totalHours += hours;
hoursToAdd = hours || 0;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Urlaub/Krankheit)
const weekendPercentage = getWeekendPercentage(date);
if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
hoursToAdd = hours * (weekendPercentage / 100);
}
}
totalHours += hoursToAdd;
// Bearbeitung ist immer möglich, auch nach Abschicken
// Bei ganztägigem Urlaub oder Krank werden Zeitfelder deaktiviert; Feiertag: Anzeige, Zeitfelder optional (Überstunden)
@@ -662,10 +703,22 @@ function updateOvertimeDisplay() {
const end = new Date(`2000-01-01T${endTime}`);
const diffMs = end - start;
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
totalHours += hours;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(date);
let adjustedHours = hours;
if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) {
adjustedHours = hours * (weekendPercentage / 100);
}
totalHours += adjustedHours;
} else if (currentEntries[dateStr]?.total_hours && !isFullDayOvertime) {
// Fallback auf gespeicherte Werte
totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0;
let hours = parseFloat(currentEntries[dateStr].total_hours) || 0;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(date);
if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus) {
hours = hours * (weekendPercentage / 100);
}
totalHours += hours;
}
} else if (sickStatus) {
totalHours += 8; // Krank = 8 Stunden
@@ -706,10 +759,22 @@ function updateOvertimeDisplay() {
const end = new Date(`2000-01-01T${endTime}`);
const diffMs = end - start;
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
totalHours += hours;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(date);
let adjustedHours = hours;
if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) {
adjustedHours = hours * (weekendPercentage / 100);
}
totalHours += adjustedHours;
} else if (currentEntries[dateStr]?.total_hours) {
// Fallback auf gespeicherte Werte
totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0;
let hours = parseFloat(currentEntries[dateStr].total_hours) || 0;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(date);
if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) {
hours = hours * (weekendPercentage / 100);
}
totalHours += hours;
}
}
}

View File

@@ -10,33 +10,36 @@ function registerAdminRoutes(app) {
app.get('/admin', requireAdmin, (req, res) => {
db.all('SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, created_at FROM users ORDER BY created_at DESC',
(err, users) => {
// LDAP-Konfiguration und Sync-Log abrufen
// LDAP-Konfiguration, Sync-Log und Optionen abrufen
db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => {
db.all('SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT 10', (err, syncLogs) => {
// Parse Rollen für jeden User
const usersWithRoles = (users || []).map(u => {
let roles = [];
try {
roles = JSON.parse(u.role);
if (!Array.isArray(roles)) {
roles = [u.role];
db.get('SELECT * FROM system_options WHERE id = 1', (err, options) => {
// Parse Rollen für jeden User
const usersWithRoles = (users || []).map(u => {
let roles = [];
try {
roles = JSON.parse(u.role);
if (!Array.isArray(roles)) {
roles = [u.role];
}
} catch (e) {
roles = [u.role || 'mitarbeiter'];
}
} catch (e) {
roles = [u.role || 'mitarbeiter'];
}
return { ...u, roles };
});
res.render('admin', {
users: usersWithRoles,
ldapConfig: ldapConfig || null,
syncLogs: syncLogs || [],
user: {
firstname: req.session.firstname,
lastname: req.session.lastname,
roles: req.session.roles || [],
currentRole: req.session.currentRole || 'admin'
}
return { ...u, roles };
});
res.render('admin', {
users: usersWithRoles,
ldapConfig: ldapConfig || null,
syncLogs: syncLogs || [],
options: options || { saturday_percentage: 100, sunday_percentage: 100 },
user: {
firstname: req.session.firstname,
lastname: req.session.lastname,
roles: req.session.roles || [],
currentRole: req.session.currentRole || 'admin'
}
});
});
});
});
@@ -149,6 +152,71 @@ function registerAdminRoutes(app) {
});
}
});
// Optionen laden
app.get('/admin/options', requireAdmin, (req, res) => {
db.get('SELECT * FROM system_options WHERE id = 1', (err, options) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Laden der Optionen' });
}
// Wenn keine Optionen vorhanden, Standardwerte zurückgeben
if (!options) {
return res.json({
config: {
saturday_percentage: 100,
sunday_percentage: 100
}
});
}
res.json({ config: options });
});
});
// Optionen speichern
app.post('/admin/options', requireAdmin, (req, res) => {
const { saturday_percentage, sunday_percentage } = req.body;
// Validierung
const satPercent = parseFloat(saturday_percentage);
const sunPercent = parseFloat(sunday_percentage);
if (isNaN(satPercent) || isNaN(sunPercent)) {
return res.status(400).json({ error: 'Ungültige Prozentsätze' });
}
if (satPercent < 100 || satPercent > 200 || sunPercent < 100 || sunPercent > 200) {
return res.status(400).json({ error: 'Prozentsätze müssen zwischen 100 und 200 liegen' });
}
// Prüfe ob Eintrag existiert
db.get('SELECT id FROM system_options WHERE id = 1', (err, existing) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Prüfen der Optionen' });
}
if (existing) {
// Update
db.run('UPDATE system_options SET saturday_percentage = ?, sunday_percentage = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
[satPercent, sunPercent],
(err) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' });
}
res.json({ success: true });
});
} else {
// Insert
db.run('INSERT INTO system_options (id, saturday_percentage, sunday_percentage) VALUES (1, ?, ?)',
[satPercent, sunPercent],
(err) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' });
}
res.json({ success: true });
});
}
});
});
}
module.exports = registerAdminRoutes;

View File

@@ -27,6 +27,28 @@ 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';
// Wochenend-Prozentsätze laden
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
if (err) {
console.error('Fehler beim Laden der Optionen:', err);
return res.status(500).json({ error: 'Fehler beim Laden der Optionen' });
}
const saturdayPercentage = options?.saturday_percentage || 100;
const sundayPercentage = options?.sunday_percentage || 100;
// Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück
function getWeekendPercentage(dateStr) {
const date = new Date(dateStr);
const day = date.getDay();
if (day === 6) { // Samstag
return saturdayPercentage;
} else if (day === 0) { // Sonntag
return sundayPercentage;
}
return 100; // Kein Wochenende = 100% (normal)
}
// User-Daten laden (für Überstunden-Berechnung)
db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => {
if (err) {
@@ -73,6 +95,11 @@ 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);
}
// Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden
// Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt
}
@@ -146,6 +173,7 @@ function registerTimesheetRoutes(app) {
}
});
});
});
});
// API: Feiertage für einen Zeitraum (Dashboard-Anzeige)

View File

@@ -10,12 +10,12 @@ function registerUserRoutes(app) {
// API: Letzte bearbeitete Woche abrufen
app.get('/api/user/last-week', requireAuth, (req, res) => {
const userId = req.session.userId;
db.get('SELECT last_week_start FROM users WHERE id = ?', [userId], (err, user) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der letzten Woche' });
}
res.json({ last_week_start: user?.last_week_start || null });
});
});
@@ -24,13 +24,13 @@ function registerUserRoutes(app) {
app.post('/api/user/last-week', requireAuth, (req, res) => {
const userId = req.session.userId;
const { week_start } = req.body;
if (!week_start) {
return res.status(400).json({ error: 'week_start ist erforderlich' });
}
db.run('UPDATE users SET last_week_start = ? WHERE id = ?',
[week_start, userId],
db.run('UPDATE users SET last_week_start = ? WHERE id = ?',
[week_start, userId],
(err) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Speichern der letzten Woche' });
@@ -39,15 +39,29 @@ function registerUserRoutes(app) {
});
});
// API: Wochenend-Prozentsätze abrufen
app.get('/api/user/weekend-percentages', requireAuth, (req, res) => {
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochenend-Prozentsätze' });
}
// Wenn keine Optionen vorhanden, Standardwerte zurückgeben
res.json({
saturday_percentage: options?.saturday_percentage || 100,
sunday_percentage: options?.sunday_percentage || 100
});
});
});
// API: User-Daten abrufen (Wochenstunden)
app.get('/api/user/data', requireAuth, (req, res) => {
const userId = req.session.userId;
db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
}
res.json({ wochenstunden: user?.wochenstunden || 0 });
});
});
@@ -55,28 +69,28 @@ 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';
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;
db.get('SELECT ping_ip FROM users WHERE id = ?', [userId], (err, user) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der IP-Adresse' });
}
res.json({ ping_ip: user?.ping_ip || null });
});
});
@@ -85,21 +99,21 @@ function registerUserRoutes(app) {
app.post('/api/user/ping-ip', requireAuth, (req, res) => {
const userId = req.session.userId;
const { ping_ip } = req.body;
// Validierung: IPv4 Format (einfache Prüfung)
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ping_ip && ping_ip.trim() !== '' && !ipv4Regex.test(ping_ip.trim())) {
return res.status(400).json({ error: 'Ungültige IP-Adresse. Bitte geben Sie eine gültige IPv4-Adresse ein.' });
}
// Normalisiere: Leere Strings werden zu null
const normalizedPingIp = (ping_ip && ping_ip.trim() !== '') ? ping_ip.trim() : null;
db.run('UPDATE users SET ping_ip = ? WHERE id = ?', [normalizedPingIp, userId], (err) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Speichern der IP-Adresse' });
}
// Wenn IP entfernt wurde, lösche auch den Ping-Status für heute
if (!normalizedPingIp) {
const currentDate = getCurrentDate();
@@ -107,7 +121,7 @@ function registerUserRoutes(app) {
// Fehler ignorieren
});
}
res.json({ success: true, ping_ip: normalizedPingIp });
});
});
@@ -115,25 +129,25 @@ function registerUserRoutes(app) {
// API: Rollenwechsel
app.post('/api/user/switch-role', requireAuth, (req, res) => {
const { role } = req.body;
if (!role) {
return res.status(400).json({ error: 'Rolle ist erforderlich' });
}
// Prüfe ob User diese Rolle hat
if (!hasRole(req, role)) {
return res.status(403).json({ error: 'Sie haben keine Berechtigung für diese Rolle' });
}
// Validiere dass die Rolle eine gültige Rolle ist
const validRoles = ['mitarbeiter', 'verwaltung', 'admin'];
if (!validRoles.includes(role)) {
return res.status(400).json({ error: 'Ungültige Rolle' });
}
// Setze aktuelle Rolle
req.session.currentRole = role;
res.json({ success: true, currentRole: role });
});
@@ -141,7 +155,7 @@ function registerUserRoutes(app) {
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],
@@ -149,33 +163,33 @@ function registerUserRoutes(app) {
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({
res.json({
plannedVacationDays: plannedDays,
weeks: weeks
});
@@ -186,283 +200,317 @@ function registerUserRoutes(app) {
// API: Gesamtstatistiken für Mitarbeiter (Überstunden und Urlaubstage)
app.get('/api/user/stats', requireAuth, (req, res) => {
const userId = req.session.userId;
// User-Daten abrufen
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => {
if (err || !user) {
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
// Wochenend-Prozentsätze laden
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Laden der Optionen' });
}
const wochenstunden = user.wochenstunden || 0;
const urlaubstage = user.urlaubstage || 0;
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
// Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte)
const { getCalendarWeek } = require('../helpers/utils');
db.all(`SELECT date, vacation_type FROM timesheet_entries
const saturdayPercentage = options?.saturday_percentage || 100;
const sundayPercentage = options?.sunday_percentage || 100;
// Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück
function getWeekendPercentage(dateStr) {
const date = new Date(dateStr);
const day = date.getDay();
if (day === 6) { // Samstag
return saturdayPercentage;
} else if (day === 0) { // Sonntag
return sundayPercentage;
}
return 100; // Kein Wochenende = 100% (normal)
}
// User-Daten abrufen
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => {
if (err || !user) {
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
}
const wochenstunden = user.wochenstunden || 0;
const urlaubstage = user.urlaubstage || 0;
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
// 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, allVacationEntries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' });
}
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 };
[userId],
(err, allVacationEntries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' });
}
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
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;
let totalVacationDays = 0;
let processedWeeks = 0;
let hasError = false;
// Für jede Woche die Statistiken berechnen
weeks.forEach((week) => {
// Einträge für diese Woche abrufen (nur neueste pro Tag)
db.all(`SELECT id, date, total_hours, overtime_taken_hours, vacation_type, sick_status, start_time, end_time, updated_at
FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date, updated_at DESC, id DESC`,
[userId, week.week_start, week.week_end],
(err, allEntries) => {
if (hasError) return; // Wenn bereits ein Fehler aufgetreten ist, ignoriere weitere Ergebnisse
[userId],
(err, weeks) => {
if (err) {
hasError = true;
return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' });
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
}
// Filtere auf neuesten Eintrag pro Tag
const entriesByDate = {};
(allEntries || []).forEach(entry => {
const existing = entriesByDate[entry.date];
if (!existing) {
entriesByDate[entry.date] = entry;
} else {
// Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer)
const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0;
const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0;
if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) {
entriesByDate[entry.date] = entry;
}
}
});
// Konvertiere zurück zu Array
const entries = Object.values(entriesByDate);
// Prüfe ob Woche vollständig ausgefüllt ist (alle 5 Werktage)
// Feiertage für die Woche laden (Feiertag zählt als ausgefüllt)
getHolidaysForDateRange(week.week_start, week.week_end)
.catch(() => new Set())
.then((holidaySet) => {
// Prüfe alle 5 Werktage (Montag-Freitag)
const startDate = new Date(week.week_start);
const endDate = new Date(week.week_end);
let workdays = 0;
let filledWorkdays = 0;
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const day = d.getDay();
if (day >= 1 && day <= 5) { // Montag bis Freitag
workdays++;
const dateStr = d.toISOString().split('T')[0];
if (holidaySet.has(dateStr)) {
filledWorkdays++;
continue;
}
const entry = entriesByDate[dateStr];
// Tag gilt als ausgefüllt wenn:
// - Ganzer Tag Urlaub (vacation_type = 'full')
// - Krank (sick_status = 1)
// - ODER Start- und End-Zeit vorhanden sind
if (entry) {
const isFullDayVacation = entry.vacation_type === 'full';
const isSick = entry.sick_status === 1 || entry.sick_status === true;
const hasStartAndEnd = entry.start_time && entry.end_time &&
entry.start_time.toString().trim() !== '' &&
entry.end_time.toString().trim() !== '';
if (isFullDayVacation || isSick || hasStartAndEnd) {
filledWorkdays++;
}
}
}
}
// Nur berechnen wenn alle Werktage ausgefüllt sind
if (filledWorkdays < workdays) {
// Woche nicht vollständig - überspringe diese Woche
processedWeeks++;
if (processedWeeks === weeks.length && !hasError) {
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
const remainingVacation = urlaubstage - totalVacationDays;
res.json({
currentOvertime: currentOvertime,
remainingVacation: remainingVacation,
totalOvertimeHours: totalOvertimeHours,
totalOvertimeTaken: totalOvertimeTaken,
totalVacationDays: totalVacationDays,
plannedVacationDays: plannedVacationDays,
plannedWeeks: plannedWeeks,
urlaubstage: urlaubstage,
overtimeOffsetHours: overtimeOffsetHours
});
}
return; // Überspringe diese Woche
}
// Berechnungen für diese Woche (nur wenn vollständig ausgefüllt)
let weekTotalHours = 0;
let weekOvertimeTaken = 0;
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;
weekVacationHours += 8; // Ganzer Tag = 8 Stunden
// Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt
} else if (entry.vacation_type === 'half') {
weekVacationDays += 0.5;
weekVacationHours += 4; // Halber Tag = 4 Stunden
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
if (entry.total_hours && !isFullDayOvertime) {
weekTotalHours += entry.total_hours;
}
} else {
// Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden)
if (entry.total_hours && !isFullDayOvertime) {
weekTotalHours += entry.total_hours;
}
}
});
// Feiertagsstunden: 8h pro Werktag der ein Feiertag ist
let holidayHours = 0;
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const day = d.getDay();
if (day >= 1 && day <= 5) {
const dateStr = d.toISOString().split('T')[0];
if (holidaySet.has(dateStr)) holidayHours += 8;
}
}
// Sollstunden berechnen
const sollStunden = (wochenstunden / 5) * workdays;
// Überstunden für diese Woche: (totalHours + vacationHours + holidayHours) - adjustedSollStunden
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
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;
processedWeeks++;
// 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;
res.json({
currentOvertime: currentOvertime,
remainingVacation: remainingVacation,
totalOvertimeHours: totalOvertimeHours,
totalOvertimeTaken: totalOvertimeTaken,
totalVacationDays: totalVacationDays,
// 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;
let totalVacationDays = 0;
let processedWeeks = 0;
let hasError = false;
// Für jede Woche die Statistiken berechnen
weeks.forEach((week) => {
// Einträge für diese Woche abrufen (nur neueste pro Tag)
db.all(`SELECT id, date, total_hours, overtime_taken_hours, vacation_type, sick_status, start_time, end_time, updated_at
FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date, updated_at DESC, id DESC`,
[userId, week.week_start, week.week_end],
(err, allEntries) => {
if (hasError) return; // Wenn bereits ein Fehler aufgetreten ist, ignoriere weitere Ergebnisse
if (err) {
hasError = true;
return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' });
}
// Filtere auf neuesten Eintrag pro Tag
const entriesByDate = {};
(allEntries || []).forEach(entry => {
const existing = entriesByDate[entry.date];
if (!existing) {
entriesByDate[entry.date] = entry;
} else {
// Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer)
const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0;
const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0;
if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) {
entriesByDate[entry.date] = entry;
}
}
});
// Konvertiere zurück zu Array
const entries = Object.values(entriesByDate);
// Prüfe ob Woche vollständig ausgefüllt ist (alle 5 Werktage)
// Feiertage für die Woche laden (Feiertag zählt als ausgefüllt)
getHolidaysForDateRange(week.week_start, week.week_end)
.catch(() => new Set())
.then((holidaySet) => {
// Prüfe alle 5 Werktage (Montag-Freitag)
const startDate = new Date(week.week_start);
const endDate = new Date(week.week_end);
let workdays = 0;
let filledWorkdays = 0;
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const day = d.getDay();
if (day >= 1 && day <= 5) { // Montag bis Freitag
workdays++;
const dateStr = d.toISOString().split('T')[0];
if (holidaySet.has(dateStr)) {
filledWorkdays++;
continue;
}
const entry = entriesByDate[dateStr];
// Tag gilt als ausgefüllt wenn:
// - Ganzer Tag Urlaub (vacation_type = 'full')
// - Krank (sick_status = 1)
// - ODER Start- und End-Zeit vorhanden sind
if (entry) {
const isFullDayVacation = entry.vacation_type === 'full';
const isSick = entry.sick_status === 1 || entry.sick_status === true;
const hasStartAndEnd = entry.start_time && entry.end_time &&
entry.start_time.toString().trim() !== '' &&
entry.end_time.toString().trim() !== '';
if (isFullDayVacation || isSick || hasStartAndEnd) {
filledWorkdays++;
}
}
}
}
// Nur berechnen wenn alle Werktage ausgefüllt sind
if (filledWorkdays < workdays) {
// Woche nicht vollständig - überspringe diese Woche
processedWeeks++;
if (processedWeeks === weeks.length && !hasError) {
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
const remainingVacation = urlaubstage - totalVacationDays;
res.json({
currentOvertime: currentOvertime,
remainingVacation: remainingVacation,
totalOvertimeHours: totalOvertimeHours,
totalOvertimeTaken: totalOvertimeTaken,
totalVacationDays: totalVacationDays,
plannedVacationDays: plannedVacationDays,
plannedWeeks: plannedWeeks,
urlaubstage: urlaubstage,
overtimeOffsetHours: overtimeOffsetHours
});
}
return; // Überspringe diese Woche
}
// Berechnungen für diese Woche (nur wenn vollständig ausgefüllt)
let weekTotalHours = 0;
let weekOvertimeTaken = 0;
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;
weekVacationHours += 8; // Ganzer Tag = 8 Stunden
// Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt
} else if (entry.vacation_type === 'half') {
weekVacationDays += 0.5;
weekVacationHours += 4; // Halber Tag = 4 Stunden
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
if (entry.total_hours && !isFullDayOvertime) {
let hours = entry.total_hours;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(entry.date);
if (weekendPercentage >= 100 && hours > 0 && !entry.sick_status) {
hours = hours * (weekendPercentage / 100);
}
weekTotalHours += hours;
}
} else {
// Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden)
if (entry.total_hours && !isFullDayOvertime) {
let hours = entry.total_hours;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Krankheit)
const weekendPercentage = getWeekendPercentage(entry.date);
if (weekendPercentage > 0 && hours > 0 && !entry.sick_status) {
hours = hours * (1 + weekendPercentage / 100);
}
weekTotalHours += hours;
}
}
});
// Feiertagsstunden: 8h pro Werktag der ein Feiertag ist
let holidayHours = 0;
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const day = d.getDay();
if (day >= 1 && day <= 5) {
const dateStr = d.toISOString().split('T')[0];
if (holidaySet.has(dateStr)) holidayHours += 8;
}
}
// Sollstunden berechnen
const sollStunden = (wochenstunden / 5) * workdays;
// Überstunden für diese Woche: (totalHours + vacationHours + holidayHours) - adjustedSollStunden
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
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;
processedWeeks++;
// 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;
res.json({
currentOvertime: currentOvertime,
remainingVacation: remainingVacation,
totalOvertimeHours: totalOvertimeHours,
totalOvertimeTaken: totalOvertimeTaken,
totalVacationDays: totalVacationDays,
plannedVacationDays: plannedVacationDays,
plannedWeeks: plannedWeeks,
urlaubstage: urlaubstage,
overtimeOffsetHours: overtimeOffsetHours
});
}
}); // getHolidaysForDateRange.then
}); // db.all (allEntries)
}); // weeks.forEach
}); // db.all (weeks)
}); // db.all (allVacationEntries)
}); // db.get (user)
}); // db.get (options)
}); // app.get
}
module.exports = registerUserRoutes;

View File

@@ -142,6 +142,27 @@ function registerVerwaltungRoutes(app) {
const userId = req.params.id;
const { week_start, week_end } = req.query;
// Wochenend-Prozentsätze laden
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Laden der Optionen' });
}
const saturdayPercentage = options?.saturday_percentage || 100;
const sundayPercentage = options?.sunday_percentage || 100;
// Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück
function getWeekendPercentage(dateStr) {
const date = new Date(dateStr);
const day = date.getDay();
if (day === 6) { // Samstag
return saturdayPercentage;
} else if (day === 0) { // Sonntag
return sundayPercentage;
}
return 100; // Kein Wochenende = 100% (normal)
}
// User-Daten abrufen
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => {
if (err || !user) {
@@ -190,12 +211,24 @@ function registerVerwaltungRoutes(app) {
vacationHours += 4; // Halber Tag = 4 Stunden
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
if (entry.total_hours) {
totalHours += entry.total_hours;
let hours = entry.total_hours;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden)
const weekendPercentage = getWeekendPercentage(entry.date);
if (weekendPercentage >= 100 && hours > 0 && !entry.sick_status) {
hours = hours * (weekendPercentage / 100);
}
totalHours += hours;
}
} else {
// Kein Urlaub - zähle nur Arbeitsstunden
if (entry.total_hours) {
totalHours += entry.total_hours;
let hours = entry.total_hours;
// Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Krankheit)
const weekendPercentage = getWeekendPercentage(entry.date);
if (weekendPercentage > 0 && hours > 0 && !entry.sick_status) {
hours = hours * (1 + weekendPercentage / 100);
}
totalHours += hours;
}
}
});
@@ -248,6 +281,7 @@ function registerVerwaltungRoutes(app) {
});
});
});
});
});
// API: Admin-Kommentar speichern

View File

@@ -203,6 +203,43 @@
</div>
</div>
<div class="options-section" style="margin-top: 40px;">
<div class="collapsible-header" onclick="toggleOptionsSection()" style="cursor: pointer; padding: 15px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
<h2 style="margin: 0;">Optionen</h2>
<span id="optionsToggleIcon" style="font-size: 18px; transition: transform 0.3s;">▼</span>
</div>
<div id="optionsContent" style="display: none; padding: 20px; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff;">
<div class="options-form">
<h3>Wochenend-Prozentsätze</h3>
<p style="margin-bottom: 20px; color: #666;">Konfigurieren Sie die Prozentsätze für die Wochenendstunden. 100% entspricht normal, 150% entspricht 1,5 mal, 200% entspricht doppelt.</p>
<form id="optionsForm">
<div class="form-row">
<div class="form-group">
<label for="saturdayPercentage">Samstag-Prozentsatz</label>
<select id="saturdayPercentage" name="saturday_percentage" class="form-control" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<% for (let i = 100; i <= 200; i += 5) { %>
<option value="<%= i %>" <%= (typeof options !== 'undefined' && options && options.saturday_percentage == i) ? 'selected' : '' %>><%= i %>%</option>
<% } %>
</select>
</div>
<div class="form-group">
<label for="sundayPercentage">Sonntag-Prozentsatz</label>
<select id="sundayPercentage" name="sunday_percentage" class="form-control" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<% for (let i = 100; i <= 200; i += 5) { %>
<option value="<%= i %>" <%= (typeof options !== 'undefined' && options && options.sunday_percentage == i) ? 'selected' : '' %>><%= i %>%</option>
<% } %>
</select>
</div>
</div>
<button type="submit" class="btn btn-primary">Optionen speichern</button>
</form>
</div>
</div>
</div>
<div class="ldap-sync-section" style="margin-top: 40px;">
<div class="collapsible-header" onclick="toggleLDAPSection()" style="cursor: pointer; padding: 15px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
<h2 style="margin: 0;">LDAP-Synchronisation</h2>
@@ -375,6 +412,20 @@
}
}
// Optionen-Sektion ein-/ausklappen
function toggleOptionsSection() {
const content = document.getElementById('optionsContent');
const icon = document.getElementById('optionsToggleIcon');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
} else {
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
}
}
// Rollenwechsel-Handler
document.addEventListener('DOMContentLoaded', function() {
const roleSwitcher = document.getElementById('roleSwitcher');