Add Prozent im ADmin für wochenende
This commit is contained in:
20
database.js
20
database.js
@@ -249,6 +249,22 @@ function initDatabase() {
|
|||||||
name TEXT
|
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
|
// Migration: Bestehende Rollen zu JSON-Arrays konvertieren
|
||||||
// Prüfe ob Rollen noch als einfache Strings gespeichert sind (nicht als JSON-Array)
|
// Prüfe ob Rollen noch als einfache Strings gespeichert sind (nicht als JSON-Array)
|
||||||
db.all('SELECT id, role FROM users', (err, users) => {
|
db.all('SELECT id, role FROM users', (err, users) => {
|
||||||
@@ -281,13 +297,13 @@ function initDatabase() {
|
|||||||
const adminPassword = bcrypt.hashSync('admin123', 10);
|
const adminPassword = bcrypt.hashSync('admin123', 10);
|
||||||
db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role)
|
db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role)
|
||||||
VALUES (1, 'admin', ?, 'System', 'Administrator', ?)`,
|
VALUES (1, 'admin', ?, 'System', 'Administrator', ?)`,
|
||||||
[adminPassword, JSON.stringify(['admin'])]);
|
[adminPassword, JSON.stringify(['admin'])]);
|
||||||
|
|
||||||
// Standard Verwaltungs-Benutzer erstellen
|
// Standard Verwaltungs-Benutzer erstellen
|
||||||
const verwaltungPassword = bcrypt.hashSync('verwaltung123', 10);
|
const verwaltungPassword = bcrypt.hashSync('verwaltung123', 10);
|
||||||
db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role)
|
db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role)
|
||||||
VALUES (2, 'verwaltung', ?, 'Verwaltung', 'User', ?)`,
|
VALUES (2, 'verwaltung', ?, 'Verwaltung', 'User', ?)`,
|
||||||
[verwaltungPassword, JSON.stringify(['verwaltung'])]);
|
[verwaltungPassword, JSON.stringify(['verwaltung'])]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -54,6 +54,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// LDAP-Konfiguration laden
|
// LDAP-Konfiguration laden
|
||||||
loadLDAPConfig();
|
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
|
// LDAP-Konfigurationsformular
|
||||||
const ldapConfigForm = document.getElementById('ldapConfigForm');
|
const ldapConfigForm = document.getElementById('ldapConfigForm');
|
||||||
if (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
|
// LDAP-Konfiguration laden und Formular ausfüllen
|
||||||
async function loadLDAPConfig() {
|
async function loadLDAPConfig() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,6 +4,36 @@ let currentWeekStart = getMonday(new Date());
|
|||||||
let currentEntries = {};
|
let currentEntries = {};
|
||||||
let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD)
|
let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD)
|
||||||
let userWochenstunden = 0; // Wochenstunden des Users
|
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
|
// Statistiken laden
|
||||||
async function loadUserStats() {
|
async function loadUserStats() {
|
||||||
@@ -110,6 +140,9 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
// Ping-IP laden
|
// Ping-IP laden
|
||||||
loadPingIP();
|
loadPingIP();
|
||||||
|
|
||||||
|
// Wochenend-Prozentsätze laden
|
||||||
|
loadWeekendPercentages();
|
||||||
|
|
||||||
// Statistiken laden
|
// Statistiken laden
|
||||||
loadUserStats();
|
loadUserStats();
|
||||||
|
|
||||||
@@ -374,11 +407,19 @@ function renderWeek() {
|
|||||||
// Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt)
|
// Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt)
|
||||||
// Feiertag: 8h Basis + gearbeitete Stunden (jede gearbeitete Stunde = Überstunde)
|
// Feiertag: 8h Basis + gearbeitete Stunden (jede gearbeitete Stunde = Überstunde)
|
||||||
// Bei halbem Tag Urlaub werden die Urlaubsstunden später in der Überstunden-Berechnung hinzugezählt
|
// 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) {
|
if (isHoliday) {
|
||||||
totalHours += 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden)
|
hoursToAdd = 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden)
|
||||||
} else {
|
} 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
|
// Bearbeitung ist immer möglich, auch nach Abschicken
|
||||||
// Bei ganztägigem Urlaub oder Krank werden Zeitfelder deaktiviert; Feiertag: Anzeige, Zeitfelder optional (Überstunden)
|
// 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 end = new Date(`2000-01-01T${endTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
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) {
|
} else if (currentEntries[dateStr]?.total_hours && !isFullDayOvertime) {
|
||||||
// Fallback auf gespeicherte Werte
|
// 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) {
|
} else if (sickStatus) {
|
||||||
totalHours += 8; // Krank = 8 Stunden
|
totalHours += 8; // Krank = 8 Stunden
|
||||||
@@ -706,10 +759,22 @@ function updateOvertimeDisplay() {
|
|||||||
const end = new Date(`2000-01-01T${endTime}`);
|
const end = new Date(`2000-01-01T${endTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60);
|
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) {
|
} else if (currentEntries[dateStr]?.total_hours) {
|
||||||
// Fallback auf gespeicherte Werte
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
routes/admin.js
114
routes/admin.js
@@ -10,33 +10,36 @@ function registerAdminRoutes(app) {
|
|||||||
app.get('/admin', requireAdmin, (req, res) => {
|
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',
|
db.all('SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, created_at FROM users ORDER BY created_at DESC',
|
||||||
(err, users) => {
|
(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.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) => {
|
db.all('SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT 10', (err, syncLogs) => {
|
||||||
// Parse Rollen für jeden User
|
db.get('SELECT * FROM system_options WHERE id = 1', (err, options) => {
|
||||||
const usersWithRoles = (users || []).map(u => {
|
// Parse Rollen für jeden User
|
||||||
let roles = [];
|
const usersWithRoles = (users || []).map(u => {
|
||||||
try {
|
let roles = [];
|
||||||
roles = JSON.parse(u.role);
|
try {
|
||||||
if (!Array.isArray(roles)) {
|
roles = JSON.parse(u.role);
|
||||||
roles = [u.role];
|
if (!Array.isArray(roles)) {
|
||||||
|
roles = [u.role];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
roles = [u.role || 'mitarbeiter'];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
return { ...u, roles };
|
||||||
roles = [u.role || 'mitarbeiter'];
|
});
|
||||||
}
|
|
||||||
return { ...u, roles };
|
|
||||||
});
|
|
||||||
|
|
||||||
res.render('admin', {
|
res.render('admin', {
|
||||||
users: usersWithRoles,
|
users: usersWithRoles,
|
||||||
ldapConfig: ldapConfig || null,
|
ldapConfig: ldapConfig || null,
|
||||||
syncLogs: syncLogs || [],
|
syncLogs: syncLogs || [],
|
||||||
user: {
|
options: options || { saturday_percentage: 100, sunday_percentage: 100 },
|
||||||
firstname: req.session.firstname,
|
user: {
|
||||||
lastname: req.session.lastname,
|
firstname: req.session.firstname,
|
||||||
roles: req.session.roles || [],
|
lastname: req.session.lastname,
|
||||||
currentRole: req.session.currentRole || 'admin'
|
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;
|
module.exports = registerAdminRoutes;
|
||||||
|
|||||||
@@ -27,6 +27,28 @@ function registerTimesheetRoutes(app) {
|
|||||||
// Normalisiere sick_status: Boolean oder 1/0 zu Boolean
|
// Normalisiere sick_status: Boolean oder 1/0 zu Boolean
|
||||||
const isSick = sick_status === true || sick_status === 1 || sick_status === 'true' || sick_status === '1';
|
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)
|
// User-Daten laden (für Überstunden-Berechnung)
|
||||||
db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => {
|
db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -73,6 +95,11 @@ function registerTimesheetRoutes(app) {
|
|||||||
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60);
|
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
|
// Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden
|
||||||
// Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt
|
// 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)
|
// API: Feiertage für einen Zeitraum (Dashboard-Anzeige)
|
||||||
|
|||||||
572
routes/user.js
572
routes/user.js
@@ -39,6 +39,20 @@ 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)
|
// API: User-Daten abrufen (Wochenstunden)
|
||||||
app.get('/api/user/data', requireAuth, (req, res) => {
|
app.get('/api/user/data', requireAuth, (req, res) => {
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
@@ -56,11 +70,11 @@ function registerUserRoutes(app) {
|
|||||||
app.get('/api/user/client-ip', requireAuth, (req, res) => {
|
app.get('/api/user/client-ip', requireAuth, (req, res) => {
|
||||||
// Versuche verschiedene Methoden, um die Client-IP zu erhalten
|
// Versuche verschiedene Methoden, um die Client-IP zu erhalten
|
||||||
const clientIp = req.ip ||
|
const clientIp = req.ip ||
|
||||||
req.connection.remoteAddress ||
|
req.connection.remoteAddress ||
|
||||||
req.socket.remoteAddress ||
|
req.socket.remoteAddress ||
|
||||||
(req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : null) ||
|
(req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : null) ||
|
||||||
req.headers['x-real-ip'] ||
|
req.headers['x-real-ip'] ||
|
||||||
'unknown';
|
'unknown';
|
||||||
|
|
||||||
// Entferne IPv6-Präfix falls vorhanden (::ffff:192.168.1.1 -> 192.168.1.1)
|
// Entferne IPv6-Präfix falls vorhanden (::ffff:192.168.1.1 -> 192.168.1.1)
|
||||||
const cleanIp = clientIp.replace(/^::ffff:/, '');
|
const cleanIp = clientIp.replace(/^::ffff:/, '');
|
||||||
@@ -187,282 +201,316 @@ function registerUserRoutes(app) {
|
|||||||
app.get('/api/user/stats', requireAuth, (req, res) => {
|
app.get('/api/user/stats', requireAuth, (req, res) => {
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
|
|
||||||
// User-Daten abrufen
|
// Wochenend-Prozentsätze laden
|
||||||
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => {
|
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
|
||||||
if (err || !user) {
|
if (err) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
return res.status(500).json({ error: 'Fehler beim Laden der Optionen' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const wochenstunden = user.wochenstunden || 0;
|
const saturdayPercentage = options?.saturday_percentage || 100;
|
||||||
const urlaubstage = user.urlaubstage || 0;
|
const sundayPercentage = options?.sunday_percentage || 100;
|
||||||
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
|
||||||
|
|
||||||
// Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte)
|
// Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück
|
||||||
const { getCalendarWeek } = require('../helpers/utils');
|
function getWeekendPercentage(dateStr) {
|
||||||
db.all(`SELECT date, vacation_type FROM timesheet_entries
|
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 != ''`,
|
WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`,
|
||||||
[userId],
|
[userId],
|
||||||
(err, allVacationEntries) => {
|
(err, allVacationEntries) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' });
|
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 };
|
|
||||||
}
|
}
|
||||||
weeksMap[weekKey].days += dayValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Konvertiere zu sortiertem Array
|
let plannedVacationDays = 0;
|
||||||
const plannedWeeks = Object.values(weeksMap).sort((a, b) => {
|
const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } }
|
||||||
if (a.year !== b.year) return a.year - b.year;
|
|
||||||
return a.week - b.week;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Alle eingereichten Wochen abrufen
|
(allVacationEntries || []).forEach(entry => {
|
||||||
db.all(`SELECT DISTINCT week_start, week_end
|
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
|
FROM weekly_timesheets
|
||||||
WHERE user_id = ? AND status = 'eingereicht'
|
WHERE user_id = ? AND status = 'eingereicht'
|
||||||
ORDER BY week_start`,
|
ORDER BY week_start`,
|
||||||
[userId],
|
[userId],
|
||||||
(err, weeks) => {
|
(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
|
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
hasError = true;
|
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
|
||||||
return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtere auf neuesten Eintrag pro Tag
|
// Wenn keine Wochen vorhanden
|
||||||
const entriesByDate = {};
|
if (!weeks || weeks.length === 0) {
|
||||||
(allEntries || []).forEach(entry => {
|
return res.json({
|
||||||
const existing = entriesByDate[entry.date];
|
currentOvertime: overtimeOffsetHours,
|
||||||
if (!existing) {
|
remainingVacation: urlaubstage,
|
||||||
entriesByDate[entry.date] = entry;
|
totalOvertimeHours: 0,
|
||||||
} else {
|
totalOvertimeTaken: 0,
|
||||||
// Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer)
|
totalVacationDays: 0,
|
||||||
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,
|
|
||||||
plannedVacationDays: plannedVacationDays,
|
plannedVacationDays: plannedVacationDays,
|
||||||
plannedWeeks: plannedWeeks,
|
plannedWeeks: plannedWeeks,
|
||||||
urlaubstage: urlaubstage,
|
urlaubstage: urlaubstage,
|
||||||
overtimeOffsetHours: overtimeOffsetHours
|
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;
|
module.exports = registerUserRoutes;
|
||||||
|
|||||||
@@ -142,6 +142,27 @@ function registerVerwaltungRoutes(app) {
|
|||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const { week_start, week_end } = req.query;
|
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
|
// 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 FROM users WHERE id = ?', [userId], (err, user) => {
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
@@ -190,12 +211,24 @@ function registerVerwaltungRoutes(app) {
|
|||||||
vacationHours += 4; // Halber Tag = 4 Stunden
|
vacationHours += 4; // Halber Tag = 4 Stunden
|
||||||
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
|
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
|
||||||
if (entry.total_hours) {
|
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 {
|
} else {
|
||||||
// Kein Urlaub - zähle nur Arbeitsstunden
|
// Kein Urlaub - zähle nur Arbeitsstunden
|
||||||
if (entry.total_hours) {
|
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
|
// API: Admin-Kommentar speichern
|
||||||
|
|||||||
@@ -203,6 +203,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="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;">
|
<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>
|
<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
|
// Rollenwechsel-Handler
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const roleSwitcher = document.getElementById('roleSwitcher');
|
const roleSwitcher = document.getElementById('roleSwitcher');
|
||||||
|
|||||||
Reference in New Issue
Block a user