Überstunden Details
This commit is contained in:
@@ -39,6 +39,32 @@ function registerDashboardRoutes(app) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Überstunden-Auswertung für Mitarbeiter
|
||||||
|
app.get('/overtime-breakdown', requireAuth, (req, res) => {
|
||||||
|
// Prüfe ob User Mitarbeiter-Rolle hat
|
||||||
|
if (!hasRole(req, 'mitarbeiter')) {
|
||||||
|
// Wenn User keine Mitarbeiter-Rolle hat, aber andere Rollen, redirecte entsprechend
|
||||||
|
if (hasRole(req, 'admin')) {
|
||||||
|
return res.redirect('/admin');
|
||||||
|
}
|
||||||
|
if (hasRole(req, 'verwaltung')) {
|
||||||
|
return res.redirect('/verwaltung');
|
||||||
|
}
|
||||||
|
return res.status(403).send('Zugriff verweigert');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('overtime-breakdown', {
|
||||||
|
user: {
|
||||||
|
id: req.session.userId,
|
||||||
|
firstname: req.session.firstname,
|
||||||
|
lastname: req.session.lastname,
|
||||||
|
username: req.session.username,
|
||||||
|
roles: req.session.roles || [],
|
||||||
|
currentRole: req.session.currentRole || 'mitarbeiter'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = registerDashboardRoutes;
|
module.exports = registerDashboardRoutes;
|
||||||
|
|||||||
244
routes/user.js
244
routes/user.js
@@ -431,25 +431,15 @@ function registerUserRoutes(app) {
|
|||||||
weekVacationDays += 0.5;
|
weekVacationDays += 0.5;
|
||||||
weekVacationHours += 4; // Halber Tag = 4 Stunden
|
weekVacationHours += 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
|
||||||
|
// WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js)
|
||||||
if (entry.total_hours && !isFullDayOvertime) {
|
if (entry.total_hours && !isFullDayOvertime) {
|
||||||
let hours = entry.total_hours;
|
weekTotalHours += parseFloat(entry.total_hours) || 0;
|
||||||
// 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 {
|
} else {
|
||||||
// Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden)
|
// Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden)
|
||||||
|
// WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js)
|
||||||
if (entry.total_hours && !isFullDayOvertime) {
|
if (entry.total_hours && !isFullDayOvertime) {
|
||||||
let hours = entry.total_hours;
|
weekTotalHours += parseFloat(entry.total_hours) || 0;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -511,6 +501,232 @@ function registerUserRoutes(app) {
|
|||||||
}); // db.get (user)
|
}); // db.get (user)
|
||||||
}); // db.get (options)
|
}); // db.get (options)
|
||||||
}); // app.get
|
}); // app.get
|
||||||
|
|
||||||
|
// API: Wöchentliche Überstunden-Aufschlüsselung
|
||||||
|
app.get('/api/user/overtime-breakdown', requireAuth, (req, res) => {
|
||||||
|
const userId = req.session.userId;
|
||||||
|
|
||||||
|
// 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, 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 overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
||||||
|
|
||||||
|
// Alle eingereichten Wochen abrufen
|
||||||
|
db.all(`SELECT DISTINCT week_start, week_end
|
||||||
|
FROM weekly_timesheets
|
||||||
|
WHERE user_id = ? AND status = 'eingereicht'
|
||||||
|
ORDER BY week_start DESC`,
|
||||||
|
[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({ weeks: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getCalendarWeek } = require('../helpers/utils');
|
||||||
|
const weekData = [];
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Feiertage für die Woche laden
|
||||||
|
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];
|
||||||
|
|
||||||
|
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) {
|
||||||
|
processedWeeks++;
|
||||||
|
if (processedWeeks === weeks.length && !hasError) {
|
||||||
|
res.json({ weeks: weekData });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Berechnungen für diese Woche
|
||||||
|
let weekTotalHours = 0;
|
||||||
|
let weekOvertimeTaken = 0;
|
||||||
|
let weekVacationDays = 0;
|
||||||
|
let weekVacationHours = 0;
|
||||||
|
|
||||||
|
const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8;
|
||||||
|
let fullDayOvertimeDays = 0;
|
||||||
|
|
||||||
|
entries.forEach(entry => {
|
||||||
|
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 += parseFloat(entry.overtime_taken_hours) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFullDayOvertime) {
|
||||||
|
fullDayOvertimeDays++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.vacation_type === 'full') {
|
||||||
|
weekVacationDays += 1;
|
||||||
|
weekVacationHours += 8;
|
||||||
|
} else if (entry.vacation_type === 'half') {
|
||||||
|
weekVacationDays += 0.5;
|
||||||
|
weekVacationHours += 4;
|
||||||
|
// WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js)
|
||||||
|
if (entry.total_hours && !isFullDayOvertime) {
|
||||||
|
weekTotalHours += parseFloat(entry.total_hours) || 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js)
|
||||||
|
if (entry.total_hours && !isFullDayOvertime) {
|
||||||
|
weekTotalHours += parseFloat(entry.total_hours) || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Feiertagsstunden
|
||||||
|
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;
|
||||||
|
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
|
||||||
|
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
|
||||||
|
const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden;
|
||||||
|
|
||||||
|
// Kalenderwoche berechnen
|
||||||
|
const calendarWeek = getCalendarWeek(week.week_start);
|
||||||
|
const year = new Date(week.week_start).getFullYear();
|
||||||
|
|
||||||
|
// Wochen-Daten hinzufügen
|
||||||
|
weekData.push({
|
||||||
|
week_start: week.week_start,
|
||||||
|
week_end: week.week_end,
|
||||||
|
calendar_week: calendarWeek,
|
||||||
|
year: year,
|
||||||
|
overtime_hours: parseFloat(weekOvertimeHours.toFixed(2)),
|
||||||
|
overtime_taken: parseFloat(weekOvertimeTaken.toFixed(2)),
|
||||||
|
total_hours: parseFloat(weekTotalHoursWithVacation.toFixed(2)),
|
||||||
|
soll_stunden: parseFloat(adjustedSollStunden.toFixed(2)),
|
||||||
|
vacation_days: parseFloat(weekVacationDays.toFixed(1)),
|
||||||
|
workdays: workdays
|
||||||
|
});
|
||||||
|
|
||||||
|
processedWeeks++;
|
||||||
|
|
||||||
|
// Wenn alle Wochen verarbeitet wurden, Antwort senden
|
||||||
|
if (processedWeeks === weeks.length && !hasError) {
|
||||||
|
// Sortiere nach Datum (neueste zuerst)
|
||||||
|
weekData.sort((a, b) => {
|
||||||
|
if (a.year !== b.year) return b.year - a.year;
|
||||||
|
if (a.calendar_week !== b.calendar_week) return b.calendar_week - a.calendar_week;
|
||||||
|
return new Date(b.week_start) - new Date(a.week_start);
|
||||||
|
});
|
||||||
|
res.json({ weeks: weekData, overtime_offset_hours: overtimeOffsetHours });
|
||||||
|
}
|
||||||
|
}); // getHolidaysForDateRange.then
|
||||||
|
}); // db.all (allEntries)
|
||||||
|
}); // weeks.forEach
|
||||||
|
}); // db.all (weeks)
|
||||||
|
}); // db.get (user)
|
||||||
|
}); // db.get (options)
|
||||||
|
}); // app.get
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = registerUserRoutes;
|
module.exports = registerUserRoutes;
|
||||||
|
|||||||
@@ -70,6 +70,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-value" id="currentOvertime">-</div>
|
<div class="stat-value" id="currentOvertime">-</div>
|
||||||
<div class="stat-unit">Stunden</div>
|
<div class="stat-unit">Stunden</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<a href="/overtime-breakdown" class="btn btn-primary" style="width: 100%; font-size: 13px; padding: 8px 12px;">Details anzeigen</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card stat-vacation">
|
<div class="stat-card stat-vacation">
|
||||||
<div class="stat-label" style="display: flex; align-items: center; gap: 5px;">
|
<div class="stat-label" style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
|||||||
281
views/overtime-breakdown.ejs
Normal file
281
views/overtime-breakdown.ejs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de-DE">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Language" content="de-DE">
|
||||||
|
<title>Überstunden-Auswertung - Stundenerfassung</title>
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<style>
|
||||||
|
.overtime-breakdown-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.overtime-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.overtime-table thead {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.overtime-table th {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.overtime-table td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.overtime-table tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.overtime-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.overtime-positive {
|
||||||
|
color: #27ae60;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.overtime-negative {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.summary-box {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.summary-box h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
.summary-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.summary-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.summary-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.summary-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<img src="/images/header.png" alt="Logo" class="navbar-logo">
|
||||||
|
<h1>Stundenerfassung</h1>
|
||||||
|
</div>
|
||||||
|
<div class="nav-right">
|
||||||
|
<span>Willkommen, <%= user.firstname %> <%= user.lastname %></span>
|
||||||
|
<% if (user.roles && user.roles.length > 1) { %>
|
||||||
|
<select id="roleSwitcher" class="role-switcher" style="margin-right: 10px; padding: 5px 10px; border-radius: 4px; border: 1px solid #ddd;">
|
||||||
|
<% const roleLabels = { 'mitarbeiter': 'Mitarbeiter', 'verwaltung': 'Verwaltung', 'admin': 'Administrator' }; %>
|
||||||
|
<% user.roles.forEach(function(role) { %>
|
||||||
|
<option value="<%= role %>" <%= user.currentRole === role ? 'selected' : '' %>><%= roleLabels[role] || role %></option>
|
||||||
|
<% }); %>
|
||||||
|
</select>
|
||||||
|
<% } %>
|
||||||
|
<a href="/logout" class="btn btn-secondary">Abmelden</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overtime-breakdown-container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2 class="page-title">Überstunden-Auswertung</h2>
|
||||||
|
<a href="/dashboard" class="btn btn-secondary">Zurück zum Dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="summaryBox" class="summary-box" style="display: none;">
|
||||||
|
<h3>Zusammenfassung</h3>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Gesamt Überstunden:</span>
|
||||||
|
<span class="summary-value" id="totalOvertime">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Davon genommen:</span>
|
||||||
|
<span class="summary-value" id="totalOvertimeTaken">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Verbleibend:</span>
|
||||||
|
<span class="summary-value" id="remainingOvertime">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading" class="loading">Lade Daten...</div>
|
||||||
|
<div id="noData" class="no-data" style="display: none;">
|
||||||
|
<p>Keine eingereichten Wochen gefunden.</p>
|
||||||
|
<p style="margin-top: 10px; font-size: 14px; color: #999;">Überstunden werden erst angezeigt, nachdem Wochen abgeschickt wurden.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="overtimeTable" class="overtime-table" style="display: none;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Kalenderwoche</th>
|
||||||
|
<th>Zeitraum</th>
|
||||||
|
<th>Gesamtstunden</th>
|
||||||
|
<th>Sollstunden</th>
|
||||||
|
<th>Überstunden</th>
|
||||||
|
<th>Genommen</th>
|
||||||
|
<th>Urlaubstage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="overtimeTableBody">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Rollenwechsel
|
||||||
|
const roleSwitcher = document.getElementById('roleSwitcher');
|
||||||
|
if (roleSwitcher) {
|
||||||
|
roleSwitcher.addEventListener('change', async function() {
|
||||||
|
const role = this.value;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user/switch-role', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ role })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
// Redirect basierend auf Rolle
|
||||||
|
if (role === 'admin') {
|
||||||
|
window.location.href = '/admin';
|
||||||
|
} else if (role === 'verwaltung') {
|
||||||
|
window.location.href = '/verwaltung';
|
||||||
|
} else {
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Rollenwechsel:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datum formatieren (DD.MM.YYYY)
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('de-DE');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Überstunden-Daten laden
|
||||||
|
async function loadOvertimeBreakdown() {
|
||||||
|
const loadingEl = document.getElementById('loading');
|
||||||
|
const noDataEl = document.getElementById('noData');
|
||||||
|
const tableEl = document.getElementById('overtimeTable');
|
||||||
|
const tableBodyEl = document.getElementById('overtimeTableBody');
|
||||||
|
const summaryBoxEl = document.getElementById('summaryBox');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user/overtime-breakdown');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Laden der Daten');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
loadingEl.style.display = 'none';
|
||||||
|
|
||||||
|
if (!data.weeks || data.weeks.length === 0) {
|
||||||
|
noDataEl.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zusammenfassung berechnen
|
||||||
|
let totalOvertime = 0;
|
||||||
|
let totalOvertimeTaken = 0;
|
||||||
|
data.weeks.forEach(week => {
|
||||||
|
totalOvertime += week.overtime_hours;
|
||||||
|
totalOvertimeTaken += week.overtime_taken;
|
||||||
|
});
|
||||||
|
const remainingOvertime = totalOvertime - totalOvertimeTaken + (data.overtime_offset_hours || 0);
|
||||||
|
|
||||||
|
// Zusammenfassung anzeigen
|
||||||
|
document.getElementById('totalOvertime').textContent =
|
||||||
|
(totalOvertime >= 0 ? '+' : '') + totalOvertime.toFixed(2) + ' h';
|
||||||
|
document.getElementById('totalOvertimeTaken').textContent =
|
||||||
|
totalOvertimeTaken.toFixed(2) + ' h';
|
||||||
|
document.getElementById('remainingOvertime').textContent =
|
||||||
|
(remainingOvertime >= 0 ? '+' : '') + remainingOvertime.toFixed(2) + ' h';
|
||||||
|
document.getElementById('remainingOvertime').className =
|
||||||
|
'summary-value ' + (remainingOvertime >= 0 ? 'overtime-positive' : 'overtime-negative');
|
||||||
|
summaryBoxEl.style.display = 'block';
|
||||||
|
|
||||||
|
// Tabelle füllen
|
||||||
|
tableBodyEl.innerHTML = '';
|
||||||
|
data.weeks.forEach(week => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const calendarWeekStr = String(week.calendar_week).padStart(2, '0');
|
||||||
|
const dateRange = formatDate(week.week_start) + ' - ' + formatDate(week.week_end);
|
||||||
|
const overtimeClass = week.overtime_hours >= 0 ? 'overtime-positive' : 'overtime-negative';
|
||||||
|
const overtimeSign = week.overtime_hours >= 0 ? '+' : '';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><strong>${week.year} KW${calendarWeekStr}</strong></td>
|
||||||
|
<td>${dateRange}</td>
|
||||||
|
<td>${week.total_hours.toFixed(2)} h</td>
|
||||||
|
<td>${week.soll_stunden.toFixed(2)} h</td>
|
||||||
|
<td class="${overtimeClass}">${overtimeSign}${week.overtime_hours.toFixed(2)} h</td>
|
||||||
|
<td>${week.overtime_taken.toFixed(2)} h</td>
|
||||||
|
<td>${week.vacation_days > 0 ? week.vacation_days.toFixed(1) : '-'}</td>
|
||||||
|
`;
|
||||||
|
tableBodyEl.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
tableEl.style.display = 'table';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Überstunden-Auswertung:', error);
|
||||||
|
loadingEl.textContent = 'Fehler beim Laden der Daten. Bitte versuchen Sie es später erneut.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beim Laden der Seite
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadOvertimeBreakdown();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user