Overtime Corrections mit Historie und Grund
This commit is contained in:
25
database.js
25
database.js
@@ -215,6 +215,31 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tabelle: Protokoll für Überstunden-Korrekturen durch Verwaltung
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS overtime_corrections (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
correction_hours REAL NOT NULL,
|
||||||
|
reason TEXT,
|
||||||
|
corrected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)`, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.warn('Warnung beim Erstellen der overtime_corrections Tabelle:', err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migration: reason Spalte für overtime_corrections hinzufügen (falls Tabelle bereits existiert)
|
||||||
|
db.run(`ALTER TABLE overtime_corrections ADD COLUMN reason TEXT`, (err) => {
|
||||||
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
|
if (err && !err.message.includes('duplicate column')) {
|
||||||
|
// "duplicate column" ist SQLite CLI wording; sqlite3 node liefert typischerweise "duplicate column name"
|
||||||
|
if (!err.message.includes('duplicate column name')) {
|
||||||
|
console.warn('Warnung beim Hinzufügen der Spalte reason (overtime_corrections):', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Migration: Urlaubstage-Offset (manuelle Korrektur durch Verwaltung)
|
// Migration: Urlaubstage-Offset (manuelle Korrektur durch Verwaltung)
|
||||||
db.run(`ALTER TABLE users ADD COLUMN vacation_offset_days REAL DEFAULT 0`, (err) => {
|
db.run(`ALTER TABLE users ADD COLUMN vacation_offset_days REAL DEFAULT 0`, (err) => {
|
||||||
// Fehler ignorieren wenn Spalte bereits existiert
|
// Fehler ignorieren wenn Spalte bereits existiert
|
||||||
|
|||||||
@@ -14,4 +14,5 @@
|
|||||||
- Oben wenn woche eingereicht anzeigen als hilfestellung -> DONE
|
- Oben wenn woche eingereicht anzeigen als hilfestellung -> DONE
|
||||||
- Ausgefüllte Tage anhand der Tage pro woche gültig setzten -> DONE
|
- Ausgefüllte Tage anhand der Tage pro woche gültig setzten -> DONE
|
||||||
- Überstunden müssen anhand der Tagesstunden auch auf gültig setzten (Tag ausgefüllt wenn weniger als 8h) -> DONE sollte passen
|
- Überstunden müssen anhand der Tagesstunden auch auf gültig setzten (Tag ausgefüllt wenn weniger als 8h) -> DONE sollte passen
|
||||||
- Verplante Urlaubstage müssen auf abgezogen werden, wenn die Woche die gepalnt war eingereicht wurde. -> DONE
|
- Verplante Urlaubstage müssen auf abgezogen werden, wenn die Woche die gepalnt war eingereicht wurde. -> DONE
|
||||||
|
- Grund für Überstundenkorrektur -> DONE
|
||||||
@@ -587,194 +587,219 @@ function registerUserRoutes(app) {
|
|||||||
const arbeitstage = user.arbeitstage || 5;
|
const arbeitstage = user.arbeitstage || 5;
|
||||||
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
|
||||||
|
|
||||||
// Alle eingereichten Wochen abrufen
|
// Korrekturen durch die Verwaltung (Historie) laden
|
||||||
db.all(`SELECT DISTINCT week_start, week_end
|
db.all(
|
||||||
FROM weekly_timesheets
|
`SELECT correction_hours, corrected_at, reason
|
||||||
WHERE user_id = ? AND status = 'eingereicht'
|
FROM overtime_corrections
|
||||||
ORDER BY week_start DESC`,
|
WHERE user_id = ?
|
||||||
|
ORDER BY corrected_at DESC`,
|
||||||
[userId],
|
[userId],
|
||||||
(err, weeks) => {
|
(correctionsErr, corrections) => {
|
||||||
if (err) {
|
// Falls Tabelle noch nicht existiert (z. B. alte DB), nicht hart fehlschlagen
|
||||||
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
|
const overtimeCorrections = correctionsErr ? [] : (corrections || []);
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn keine Wochen vorhanden
|
// Alle eingereichten Wochen abrufen
|
||||||
if (!weeks || weeks.length === 0) {
|
db.all(`SELECT DISTINCT week_start, week_end
|
||||||
return res.json({ weeks: [] });
|
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' });
|
||||||
|
}
|
||||||
|
|
||||||
const { getCalendarWeek } = require('../helpers/utils');
|
// Wenn keine Wochen vorhanden
|
||||||
const weekData = [];
|
if (!weeks || weeks.length === 0) {
|
||||||
let processedWeeks = 0;
|
return res.json({
|
||||||
let hasError = false;
|
weeks: [],
|
||||||
|
overtime_offset_hours: overtimeOffsetHours,
|
||||||
// Für jede Woche die Statistiken berechnen
|
overtime_corrections: overtimeCorrections
|
||||||
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 { getCalendarWeek } = require('../helpers/utils');
|
||||||
const entries = Object.values(entriesByDate);
|
const weekData = [];
|
||||||
|
let processedWeeks = 0;
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
// Feiertage für die Woche laden
|
// Für jede Woche die Statistiken berechnen
|
||||||
getHolidaysForDateRange(week.week_start, week.week_end)
|
weeks.forEach((week) => {
|
||||||
.catch(() => new Set())
|
// Einträge für diese Woche abrufen (nur neueste pro Tag)
|
||||||
.then((holidaySet) => {
|
db.all(`SELECT id, date, total_hours, overtime_taken_hours, vacation_type, sick_status, start_time, end_time, updated_at
|
||||||
// Prüfe alle 5 Werktage (Montag-Freitag)
|
FROM timesheet_entries
|
||||||
const startDate = new Date(week.week_start);
|
WHERE user_id = ? AND date >= ? AND date <= ?
|
||||||
const endDate = new Date(week.week_end);
|
ORDER BY date, updated_at DESC, id DESC`,
|
||||||
let workdays = 0;
|
[userId, week.week_start, week.week_end],
|
||||||
let filledWorkdays = 0;
|
(err, allEntries) => {
|
||||||
|
if (hasError) return;
|
||||||
|
|
||||||
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
if (err) {
|
||||||
const day = d.getDay();
|
hasError = true;
|
||||||
if (day >= 1 && day <= 5) { // Montag bis Freitag
|
return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' });
|
||||||
workdays++;
|
}
|
||||||
const dateStr = d.toISOString().split('T')[0];
|
|
||||||
if (holidaySet.has(dateStr)) {
|
// Filtere auf neuesten Eintrag pro Tag
|
||||||
filledWorkdays++;
|
const entriesByDate = {};
|
||||||
continue;
|
(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;
|
||||||
}
|
}
|
||||||
const entry = entriesByDate[dateStr];
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (entry) {
|
// Konvertiere zurück zu Array
|
||||||
const isFullDayVacation = entry.vacation_type === 'full';
|
const entries = Object.values(entriesByDate);
|
||||||
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) {
|
// Feiertage für die Woche laden
|
||||||
filledWorkdays++;
|
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
|
// Nur berechnen wenn alle Werktage ausgefüllt sind
|
||||||
if (filledWorkdays < workdays) {
|
if (filledWorkdays < workdays) {
|
||||||
processedWeeks++;
|
processedWeeks++;
|
||||||
if (processedWeeks === weeks.length && !hasError) {
|
if (processedWeeks === weeks.length && !hasError) {
|
||||||
res.json({ weeks: weekData });
|
res.json({
|
||||||
}
|
weeks: weekData,
|
||||||
return;
|
overtime_offset_hours: overtimeOffsetHours,
|
||||||
}
|
overtime_corrections: overtimeCorrections
|
||||||
|
});
|
||||||
// Berechnungen für diese Woche
|
}
|
||||||
let weekTotalHours = 0;
|
return;
|
||||||
let weekOvertimeTaken = 0;
|
|
||||||
let weekVacationDays = 0;
|
|
||||||
let weekVacationHours = 0;
|
|
||||||
|
|
||||||
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 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 += fullDayHours;
|
|
||||||
} else if (entry.vacation_type === 'half') {
|
|
||||||
weekVacationDays += 0.5;
|
|
||||||
weekVacationHours += fullDayHours / 2;
|
|
||||||
// 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)
|
// Berechnungen für diese Woche
|
||||||
if (entry.total_hours && !isFullDayOvertime) {
|
let weekTotalHours = 0;
|
||||||
weekTotalHours += parseFloat(entry.total_hours) || 0;
|
let weekOvertimeTaken = 0;
|
||||||
|
let weekVacationDays = 0;
|
||||||
|
let weekVacationHours = 0;
|
||||||
|
|
||||||
|
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 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 += fullDayHours;
|
||||||
|
} else if (entry.vacation_type === 'half') {
|
||||||
|
weekVacationDays += 0.5;
|
||||||
|
weekVacationHours += fullDayHours / 2;
|
||||||
|
// 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: (Wochenarbeitszeit / Arbeitstage) 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 += fullDayHours;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Feiertagsstunden: (Wochenarbeitszeit / Arbeitstage) pro Werktag der ein Feiertag ist
|
// Sollstunden berechnen
|
||||||
let holidayHours = 0;
|
const sollStunden = (wochenstunden / arbeitstage) * workdays;
|
||||||
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
|
||||||
const day = d.getDay();
|
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
|
||||||
if (day >= 1 && day <= 5) {
|
const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden;
|
||||||
const dateStr = d.toISOString().split('T')[0];
|
|
||||||
if (holidaySet.has(dateStr)) holidayHours += fullDayHours;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sollstunden berechnen
|
// Kalenderwoche berechnen
|
||||||
const sollStunden = (wochenstunden / arbeitstage) * workdays;
|
const calendarWeek = getCalendarWeek(week.week_start);
|
||||||
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
|
const year = new Date(week.week_start).getFullYear();
|
||||||
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
|
|
||||||
const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden;
|
|
||||||
|
|
||||||
// Kalenderwoche berechnen
|
// Wochen-Daten hinzufügen
|
||||||
const calendarWeek = getCalendarWeek(week.week_start);
|
weekData.push({
|
||||||
const year = new Date(week.week_start).getFullYear();
|
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
|
||||||
|
});
|
||||||
|
|
||||||
// Wochen-Daten hinzufügen
|
processedWeeks++;
|
||||||
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) {
|
||||||
// Wenn alle Wochen verarbeitet wurden, Antwort senden
|
// Sortiere nach Datum (neueste zuerst)
|
||||||
if (processedWeeks === weeks.length && !hasError) {
|
weekData.sort((a, b) => {
|
||||||
// Sortiere nach Datum (neueste zuerst)
|
if (a.year !== b.year) return b.year - a.year;
|
||||||
weekData.sort((a, b) => {
|
if (a.calendar_week !== b.calendar_week) return b.calendar_week - a.calendar_week;
|
||||||
if (a.year !== b.year) return b.year - a.year;
|
return new Date(b.week_start) - new Date(a.week_start);
|
||||||
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,
|
||||||
res.json({ weeks: weekData, overtime_offset_hours: overtimeOffsetHours });
|
overtime_offset_hours: overtimeOffsetHours,
|
||||||
}
|
overtime_corrections: overtimeCorrections
|
||||||
}); // getHolidaysForDateRange.then
|
});
|
||||||
}); // db.all (allEntries)
|
}
|
||||||
}); // weeks.forEach
|
}); // getHolidaysForDateRange.then
|
||||||
}); // db.all (weeks)
|
}); // db.all (allEntries)
|
||||||
|
}); // weeks.forEach
|
||||||
|
}); // db.all (weeks)
|
||||||
|
}
|
||||||
|
);
|
||||||
}); // db.get (user)
|
}); // db.get (user)
|
||||||
}); // db.get (options)
|
}); // db.get (options)
|
||||||
}); // app.get
|
}); // app.get
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ function registerVerwaltungRoutes(app) {
|
|||||||
app.put('/api/verwaltung/user/:id/overtime-offset', requireVerwaltung, (req, res) => {
|
app.put('/api/verwaltung/user/:id/overtime-offset', requireVerwaltung, (req, res) => {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const raw = req.body ? req.body.overtime_offset_hours : undefined;
|
const raw = req.body ? req.body.overtime_offset_hours : undefined;
|
||||||
|
const reasonRaw = req.body ? req.body.reason : undefined;
|
||||||
|
const reason = (reasonRaw === null || reasonRaw === undefined) ? '' : String(reasonRaw).trim();
|
||||||
|
|
||||||
// Leere Eingabe => 0
|
// Leere Eingabe => 0
|
||||||
const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw);
|
const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw);
|
||||||
@@ -139,12 +141,59 @@ function registerVerwaltungRoutes(app) {
|
|||||||
return res.status(400).json({ error: 'Ungültiger Überstunden-Offset' });
|
return res.status(400).json({ error: 'Ungültiger Überstunden-Offset' });
|
||||||
}
|
}
|
||||||
|
|
||||||
db.run('UPDATE users SET overtime_offset_hours = ? WHERE id = ?', [normalized, userId], (err) => {
|
// Neue Logik: Korrektur protokollieren + kumulativ addieren
|
||||||
if (err) {
|
// Feld in der Verwaltung soll nach dem Speichern immer auf 0 zurückgesetzt werden.
|
||||||
console.error('Fehler beim Speichern des Überstunden-Offsets:', err);
|
if (normalized === 0) {
|
||||||
return res.status(500).json({ error: 'Fehler beim Speichern des Überstunden-Offsets' });
|
return res.json({ success: true, overtime_offset_hours: 0 });
|
||||||
}
|
}
|
||||||
res.json({ success: true, overtime_offset_hours: normalized });
|
|
||||||
|
if (!reason) {
|
||||||
|
return res.status(400).json({ error: 'Bitte geben Sie einen Grund für die Korrektur an.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO overtime_corrections (user_id, correction_hours, reason, corrected_at)
|
||||||
|
VALUES (?, ?, ?, datetime('now'))`,
|
||||||
|
[userId, normalized, reason],
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Fehler beim Speichern der Überstunden-Korrektur:', err);
|
||||||
|
db.run('ROLLBACK', () => {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Speichern der Überstunden-Korrektur' });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
'UPDATE users SET overtime_offset_hours = COALESCE(overtime_offset_hours, 0) + ? WHERE id = ?',
|
||||||
|
[normalized, userId],
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Fehler beim Aktualisieren des Überstunden-Offsets:', err);
|
||||||
|
db.run('ROLLBACK', () => {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Speichern des Überstunden-Offsets' });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run('COMMIT', (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Fehler beim Commit der Überstunden-Korrektur:', err);
|
||||||
|
db.run('ROLLBACK', () => {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Speichern der Überstunden-Korrektur' });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, overtime_offset_hours: 0 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,6 +217,26 @@ function registerVerwaltungRoutes(app) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: Überstunden-Korrektur-Historie für einen User abrufen
|
||||||
|
app.get('/api/verwaltung/user/:id/overtime-corrections', requireVerwaltung, (req, res) => {
|
||||||
|
const userId = req.params.id;
|
||||||
|
|
||||||
|
db.all(
|
||||||
|
`SELECT correction_hours, corrected_at, reason
|
||||||
|
FROM overtime_corrections
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY corrected_at DESC`,
|
||||||
|
[userId],
|
||||||
|
(err, rows) => {
|
||||||
|
// Falls Tabelle noch nicht existiert (z. B. alte DB), nicht hart fehlschlagen
|
||||||
|
if (err) {
|
||||||
|
return res.json({ corrections: [] });
|
||||||
|
}
|
||||||
|
res.json({ corrections: rows || [] });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// API: Krankheitstage für einen User im aktuellen Jahr abrufen
|
// API: Krankheitstage für einen User im aktuellen Jahr abrufen
|
||||||
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
|
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
|
|||||||
@@ -153,6 +153,15 @@
|
|||||||
<span class="summary-label">Manuelle Korrektur (Verwaltung):</span>
|
<span class="summary-label">Manuelle Korrektur (Verwaltung):</span>
|
||||||
<span class="summary-value" id="overtimeOffset">-</span>
|
<span class="summary-value" id="overtimeOffset">-</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="correctionsSection" style="margin-top: 15px; display: none;">
|
||||||
|
<div id="correctionsHeader" class="collapsible-header" style="cursor: pointer; padding: 12px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span style="font-weight: 600; color: #2c3e50;">Korrekturen durch die Verwaltung</span>
|
||||||
|
<span id="correctionsToggleIcon" style="font-size: 16px; transition: transform 0.3s;">▼</span>
|
||||||
|
</div>
|
||||||
|
<div id="correctionsContent" style="display: none; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff; padding: 10px 12px;">
|
||||||
|
<ul id="correctionsList" style="margin: 0; padding-left: 18px;"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading" class="loading">Lade Daten...</div>
|
<div id="loading" class="loading">Lade Daten...</div>
|
||||||
@@ -213,6 +222,35 @@
|
|||||||
return date.toLocaleDateString('de-DE');
|
return date.toLocaleDateString('de-DE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SQLite-Datetime (YYYY-MM-DD HH:MM:SS) robust parsen
|
||||||
|
function parseSqliteDatetime(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
const s = String(value);
|
||||||
|
if (s.includes('T')) return new Date(s);
|
||||||
|
// SQLite datetime('now') liefert UTC ohne "T" / "Z"
|
||||||
|
return new Date(s.replace(' ', 'T') + 'Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHours(value) {
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n)) return '';
|
||||||
|
const sign = n > 0 ? '+' : '';
|
||||||
|
let s = sign + n.toFixed(2);
|
||||||
|
s = s.replace(/\.00$/, '');
|
||||||
|
s = s.replace(/(\.\d)0$/, '$1');
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
let correctionsExpanded = false;
|
||||||
|
function toggleCorrectionsSection() {
|
||||||
|
const content = document.getElementById('correctionsContent');
|
||||||
|
const icon = document.getElementById('correctionsToggleIcon');
|
||||||
|
if (!content || !icon) return;
|
||||||
|
correctionsExpanded = !correctionsExpanded;
|
||||||
|
content.style.display = correctionsExpanded ? 'block' : 'none';
|
||||||
|
icon.style.transform = correctionsExpanded ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
|
||||||
// Überstunden-Daten laden
|
// Überstunden-Daten laden
|
||||||
async function loadOvertimeBreakdown() {
|
async function loadOvertimeBreakdown() {
|
||||||
const loadingEl = document.getElementById('loading');
|
const loadingEl = document.getElementById('loading');
|
||||||
@@ -273,6 +311,41 @@
|
|||||||
} else {
|
} else {
|
||||||
offsetItem.style.display = 'none';
|
offsetItem.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Korrekturen durch die Verwaltung anzeigen (Collapsible, nur wenn vorhanden)
|
||||||
|
const correctionsSectionEl = document.getElementById('correctionsSection');
|
||||||
|
const correctionsListEl = document.getElementById('correctionsList');
|
||||||
|
const correctionsHeaderEl = document.getElementById('correctionsHeader');
|
||||||
|
const correctionsContentEl = document.getElementById('correctionsContent');
|
||||||
|
const correctionsIconEl = document.getElementById('correctionsToggleIcon');
|
||||||
|
const corrections = Array.isArray(data.overtime_corrections) ? data.overtime_corrections : [];
|
||||||
|
|
||||||
|
if (correctionsSectionEl && correctionsListEl && correctionsHeaderEl && correctionsContentEl && correctionsIconEl && corrections.length > 0) {
|
||||||
|
correctionsSectionEl.style.display = 'block';
|
||||||
|
correctionsListEl.innerHTML = '';
|
||||||
|
|
||||||
|
corrections.forEach(c => {
|
||||||
|
const dt = parseSqliteDatetime(c.corrected_at);
|
||||||
|
const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
|
||||||
|
const hoursText = formatHours(c.correction_hours);
|
||||||
|
const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = reason
|
||||||
|
? `Korrektur am ${dateText} ${hoursText} h – ${reason}`
|
||||||
|
: `Korrektur am ${dateText} ${hoursText} h`;
|
||||||
|
correctionsListEl.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Standard: zugeklappt
|
||||||
|
correctionsExpanded = false;
|
||||||
|
correctionsContentEl.style.display = 'none';
|
||||||
|
correctionsIconEl.style.transform = 'rotate(0deg)';
|
||||||
|
|
||||||
|
// Click-Handler setzen (idempotent)
|
||||||
|
correctionsHeaderEl.onclick = toggleCorrectionsSection;
|
||||||
|
} else if (correctionsSectionEl) {
|
||||||
|
correctionsSectionEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
summaryBoxEl.style.display = 'block';
|
summaryBoxEl.style.display = 'block';
|
||||||
|
|
||||||
|
|||||||
@@ -94,23 +94,36 @@
|
|||||||
<strong>Verbleibender Urlaub:</strong> <span class="remaining-vacation-value" data-user-id="<%= employee.user.id %>">-</span> Tage
|
<strong>Verbleibender Urlaub:</strong> <span class="remaining-vacation-value" data-user-id="<%= employee.user.id %>">-</span> Tage
|
||||||
</div>
|
</div>
|
||||||
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
||||||
<strong>Überstunden-Offset:</strong>
|
<strong>Überstunden-Korrektur:</strong>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.25"
|
step="0.25"
|
||||||
class="overtime-offset-input"
|
class="overtime-offset-input"
|
||||||
data-user-id="<%= employee.user.id %>"
|
data-user-id="<%= employee.user.id %>"
|
||||||
value="<%= (employee.user.overtime_offset_hours !== undefined && employee.user.overtime_offset_hours !== null) ? employee.user.overtime_offset_hours : 0 %>"
|
value="0"
|
||||||
style="width: 90px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px;"
|
style="width: 90px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px;"
|
||||||
title="Manuelle Korrektur (positiv oder negativ) in Stunden" />
|
title="Korrektur eingeben (z. B. +10 oder -20). Nach dem Speichern wird das Feld auf 0 gesetzt." />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-success btn-sm save-overtime-offset-btn"
|
class="btn btn-success btn-sm save-overtime-offset-btn"
|
||||||
data-user-id="<%= employee.user.id %>"
|
data-user-id="<%= employee.user.id %>"
|
||||||
style="padding: 6px 10px; white-space: nowrap;"
|
style="padding: 6px 10px; white-space: nowrap;"
|
||||||
title="Überstunden-Offset speichern">
|
title="Überstunden-Korrektur speichern (addiert/abzieht und protokolliert)">
|
||||||
Speichern
|
Speichern
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm toggle-overtime-corrections-btn"
|
||||||
|
data-user-id="<%= employee.user.id %>"
|
||||||
|
style="padding: 6px 10px; white-space: nowrap;"
|
||||||
|
title="Korrektur-Historie anzeigen/ausblenden">
|
||||||
|
Historie
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="overtime-corrections-container" data-user-id="<%= employee.user.id %>" style="display: none; margin-top: 10px; padding: 10px 12px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px;">
|
||||||
|
<div class="overtime-corrections-loading" style="color: #666; font-size: 13px;">Lade Korrekturen...</div>
|
||||||
|
<div class="overtime-corrections-empty" style="display: none; color: #999; font-size: 13px;">Keine Korrekturen vorhanden.</div>
|
||||||
|
<ul class="overtime-corrections-list" style="margin: 8px 0 0 0; padding-left: 18px; font-size: 13px;"></ul>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
||||||
<strong>Urlaubstage-Offset:</strong>
|
<strong>Urlaubstage-Offset:</strong>
|
||||||
@@ -489,6 +502,192 @@
|
|||||||
loadCurrentOvertime();
|
loadCurrentOvertime();
|
||||||
loadRemainingVacation();
|
loadRemainingVacation();
|
||||||
|
|
||||||
|
// Überstunden-Korrektur-Historie laden/anzeigen
|
||||||
|
function parseSqliteDatetime(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
const s = String(value);
|
||||||
|
if (s.includes('T')) return new Date(s);
|
||||||
|
return new Date(s.replace(' ', 'T') + 'Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHours(value) {
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n)) return '';
|
||||||
|
const sign = n > 0 ? '+' : '';
|
||||||
|
let s = sign + n.toFixed(2);
|
||||||
|
s = s.replace(/\.00$/, '');
|
||||||
|
s = s.replace(/(\.\d)0$/, '$1');
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOvertimeCorrectionReasonModal(opts) {
|
||||||
|
const title = opts && opts.title ? String(opts.title) : 'Grund für die Korrektur';
|
||||||
|
const promptText = opts && opts.prompt ? String(opts.prompt) : 'Bitte geben Sie einen Grund an, warum die Korrektur vorgenommen wird.';
|
||||||
|
const onSubmit = opts && typeof opts.onSubmit === 'function' ? opts.onSubmit : null;
|
||||||
|
const onCancel = opts && typeof opts.onCancel === 'function' ? opts.onCancel : null;
|
||||||
|
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10000;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const box = document.createElement('div');
|
||||||
|
box.style.cssText = `
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 520px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||||
|
padding: 18px 18px 14px 18px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
box.innerHTML = `
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom: 8px;">
|
||||||
|
<h3 style="margin: 0; font-size: 16px; color: #2c3e50;">${title}</h3>
|
||||||
|
<button type="button" data-action="close" class="btn btn-secondary btn-sm" style="padding: 6px 10px;">✕</button>
|
||||||
|
</div>
|
||||||
|
<div style="color:#666; font-size: 13px; margin-bottom: 10px;">${promptText}</div>
|
||||||
|
<textarea data-role="reason" rows="4" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-family: inherit; font-size: 13px; resize: vertical;" placeholder="Grund..."></textarea>
|
||||||
|
<div data-role="error" style="display:none; margin-top: 8px; color:#dc3545; font-size: 13px;"></div>
|
||||||
|
<div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 12px;">
|
||||||
|
<button type="button" data-action="cancel" class="btn btn-secondary">Abbrechen</button>
|
||||||
|
<button type="button" data-action="submit" class="btn btn-success">Speichern</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.appendChild(box);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
const textarea = box.querySelector('textarea[data-role="reason"]');
|
||||||
|
const errorEl = box.querySelector('[data-role="error"]');
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
try {
|
||||||
|
if (onCancel) onCancel();
|
||||||
|
} finally {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setError(msg) {
|
||||||
|
if (!errorEl) return;
|
||||||
|
errorEl.textContent = msg;
|
||||||
|
errorEl.style.display = msg ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
const reason = textarea ? textarea.value.trim() : '';
|
||||||
|
if (!reason) {
|
||||||
|
setError('Bitte Grund angeben.');
|
||||||
|
if (textarea) textarea.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError('');
|
||||||
|
if (onSubmit) {
|
||||||
|
await onSubmit(reason);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) cancel();
|
||||||
|
});
|
||||||
|
box.querySelectorAll('button[data-action="close"], button[data-action="cancel"]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', cancel);
|
||||||
|
});
|
||||||
|
const submitBtn = box.querySelector('button[data-action="submit"]');
|
||||||
|
if (submitBtn) submitBtn.addEventListener('click', submit);
|
||||||
|
|
||||||
|
if (textarea) {
|
||||||
|
textarea.focus();
|
||||||
|
textarea.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') cancel();
|
||||||
|
if ((e.key === 'Enter' && (e.ctrlKey || e.metaKey))) {
|
||||||
|
e.preventDefault();
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOvertimeCorrectionsForUser(userId) {
|
||||||
|
const container = document.querySelector(`.overtime-corrections-container[data-user-id="${userId}"]`);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const loadingEl = container.querySelector('.overtime-corrections-loading');
|
||||||
|
const emptyEl = container.querySelector('.overtime-corrections-empty');
|
||||||
|
const listEl = container.querySelector('.overtime-corrections-list');
|
||||||
|
|
||||||
|
if (loadingEl) loadingEl.style.display = 'block';
|
||||||
|
if (emptyEl) emptyEl.style.display = 'none';
|
||||||
|
if (listEl) listEl.innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-corrections`);
|
||||||
|
const data = await resp.json().catch(() => ({}));
|
||||||
|
const corrections = Array.isArray(data.corrections) ? data.corrections : [];
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(data.error || 'Fehler beim Laden der Korrekturen');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (corrections.length === 0) {
|
||||||
|
if (emptyEl) emptyEl.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
corrections.forEach(c => {
|
||||||
|
const dt = parseSqliteDatetime(c.corrected_at);
|
||||||
|
const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
|
||||||
|
const hoursText = formatHours(c.correction_hours);
|
||||||
|
const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = reason
|
||||||
|
? `Korrektur am ${dateText} ${hoursText} h – ${reason}`
|
||||||
|
: `Korrektur am ${dateText} ${hoursText} h`;
|
||||||
|
if (listEl) listEl.appendChild(li);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Laden der Überstunden-Korrekturen:', e);
|
||||||
|
if (emptyEl) {
|
||||||
|
emptyEl.textContent = 'Fehler beim Laden der Korrekturen.';
|
||||||
|
emptyEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (loadingEl) loadingEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.toggle-overtime-corrections-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function() {
|
||||||
|
const userId = this.dataset.userId;
|
||||||
|
const container = document.querySelector(`.overtime-corrections-container[data-user-id="${userId}"]`);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const isOpen = container.style.display !== 'none' && container.style.display !== '';
|
||||||
|
if (isOpen) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.style.display = 'block';
|
||||||
|
|
||||||
|
// Beim Öffnen immer neu laden (damit neue Korrekturen sofort sichtbar sind)
|
||||||
|
await loadOvertimeCorrectionsForUser(userId);
|
||||||
|
container.dataset.loaded = 'true';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Überstunden-Offset speichern
|
// Überstunden-Offset speichern
|
||||||
document.querySelectorAll('.save-overtime-offset-btn').forEach(btn => {
|
document.querySelectorAll('.save-overtime-offset-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async function() {
|
btn.addEventListener('click', async function() {
|
||||||
@@ -496,59 +695,90 @@
|
|||||||
const input = document.querySelector(`.overtime-offset-input[data-user-id="${userId}"]`);
|
const input = document.querySelector(`.overtime-offset-input[data-user-id="${userId}"]`);
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
|
if (this.dataset.modalOpen === 'true') return;
|
||||||
|
|
||||||
const originalText = this.textContent;
|
const originalText = this.textContent;
|
||||||
this.disabled = true;
|
|
||||||
this.textContent = '...';
|
|
||||||
|
|
||||||
// leere Eingabe => 0 (Backend macht das auch, aber UI soll sauber sein)
|
// leere Eingabe => 0 (Backend macht das auch, aber UI soll sauber sein)
|
||||||
const raw = (input.value || '').trim();
|
const raw = (input.value || '').trim();
|
||||||
const value = raw === '' ? '' : Number(raw);
|
const value = raw === '' ? '' : Number(raw);
|
||||||
|
|
||||||
try {
|
// Wenn keine Korrektur (0), nichts tun außer UI auf 0 zu normalisieren
|
||||||
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-offset`, {
|
if (value === 0) {
|
||||||
method: 'PUT',
|
input.value = 0;
|
||||||
headers: { 'Content-Type': 'application/json' },
|
this.textContent = originalText;
|
||||||
body: JSON.stringify({ overtime_offset_hours: value })
|
this.disabled = false;
|
||||||
});
|
return;
|
||||||
const data = await resp.json();
|
|
||||||
if (!resp.ok) {
|
|
||||||
alert(data.error || 'Fehler beim Speichern des Offsets');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalisiere Input auf Zahl (Backend gibt number zurück)
|
|
||||||
input.value = (data.overtime_offset_hours !== undefined && data.overtime_offset_hours !== null)
|
|
||||||
? Number(data.overtime_offset_hours)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Stats für diesen User neu laden
|
|
||||||
const statDivs = document.querySelectorAll(`.group-stats[data-user-id="${userId}"]`);
|
|
||||||
statDivs.forEach(div => {
|
|
||||||
// loading indicator optional wieder anzeigen
|
|
||||||
const loading = div.querySelector('.stats-loading');
|
|
||||||
if (loading) {
|
|
||||||
loading.style.display = 'inline-block';
|
|
||||||
loading.style.color = '#666';
|
|
||||||
loading.textContent = 'Lade Statistiken...';
|
|
||||||
}
|
|
||||||
loadStatsForDiv(div);
|
|
||||||
});
|
|
||||||
loadCurrentOvertime();
|
|
||||||
|
|
||||||
this.textContent = '✓';
|
|
||||||
setTimeout(() => {
|
|
||||||
this.textContent = originalText;
|
|
||||||
this.disabled = false;
|
|
||||||
}, 900);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Speichern des Offsets:', e);
|
|
||||||
alert('Fehler beim Speichern des Offsets');
|
|
||||||
} finally {
|
|
||||||
if (this.textContent === '...') {
|
|
||||||
this.textContent = originalText;
|
|
||||||
this.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dataset.modalOpen = 'true';
|
||||||
|
this.disabled = true;
|
||||||
|
|
||||||
|
// Modal: Grund ist Pflicht
|
||||||
|
showOvertimeCorrectionReasonModal({
|
||||||
|
title: 'Grund für die Überstunden-Korrektur',
|
||||||
|
prompt: `Korrektur: ${value > 0 ? '+' : ''}${value} h`,
|
||||||
|
onCancel: () => {
|
||||||
|
delete this.dataset.modalOpen;
|
||||||
|
this.disabled = false;
|
||||||
|
},
|
||||||
|
onSubmit: async (reason) => {
|
||||||
|
this.textContent = '...';
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-offset`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ overtime_offset_hours: value, reason })
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!resp.ok) {
|
||||||
|
alert(data.error || 'Fehler beim Speichern der Korrektur');
|
||||||
|
this.textContent = originalText;
|
||||||
|
this.disabled = false;
|
||||||
|
delete this.dataset.modalOpen;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalisiere Input auf Zahl (Backend gibt number zurück)
|
||||||
|
input.value = (data.overtime_offset_hours !== undefined && data.overtime_offset_hours !== null)
|
||||||
|
? Number(data.overtime_offset_hours)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Stats für diesen User neu laden
|
||||||
|
const statDivs = document.querySelectorAll(`.group-stats[data-user-id="${userId}"]`);
|
||||||
|
statDivs.forEach(div => {
|
||||||
|
// loading indicator optional wieder anzeigen
|
||||||
|
const loading = div.querySelector('.stats-loading');
|
||||||
|
if (loading) {
|
||||||
|
loading.style.display = 'inline-block';
|
||||||
|
loading.style.color = '#666';
|
||||||
|
loading.textContent = 'Lade Statistiken...';
|
||||||
|
}
|
||||||
|
loadStatsForDiv(div);
|
||||||
|
});
|
||||||
|
loadCurrentOvertime();
|
||||||
|
|
||||||
|
// Historie (falls geöffnet) aktualisieren
|
||||||
|
const correctionsContainer = document.querySelector(`.overtime-corrections-container[data-user-id="${userId}"]`);
|
||||||
|
if (correctionsContainer && correctionsContainer.style.display !== 'none') {
|
||||||
|
await loadOvertimeCorrectionsForUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.textContent = '✓';
|
||||||
|
setTimeout(() => {
|
||||||
|
this.textContent = originalText;
|
||||||
|
this.disabled = false;
|
||||||
|
delete this.dataset.modalOpen;
|
||||||
|
}, 900);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Speichern der Korrektur:', e);
|
||||||
|
alert('Fehler beim Speichern der Korrektur');
|
||||||
|
this.textContent = originalText;
|
||||||
|
this.disabled = false;
|
||||||
|
delete this.dataset.modalOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user