Overtime Corrections mit Historie und Grund

This commit is contained in:
2026-02-12 11:24:45 +01:00
parent e020aa4e46
commit 3edc0fe60c
6 changed files with 648 additions and 225 deletions

View File

@@ -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

View File

@@ -15,3 +15,4 @@
- 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

View File

@@ -587,6 +587,17 @@ 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;
// Korrekturen durch die Verwaltung (Historie) laden
db.all(
`SELECT correction_hours, corrected_at, reason
FROM overtime_corrections
WHERE user_id = ?
ORDER BY corrected_at DESC`,
[userId],
(correctionsErr, corrections) => {
// Falls Tabelle noch nicht existiert (z. B. alte DB), nicht hart fehlschlagen
const overtimeCorrections = correctionsErr ? [] : (corrections || []);
// Alle eingereichten Wochen abrufen // Alle eingereichten Wochen abrufen
db.all(`SELECT DISTINCT week_start, week_end db.all(`SELECT DISTINCT week_start, week_end
FROM weekly_timesheets FROM weekly_timesheets
@@ -600,7 +611,11 @@ function registerUserRoutes(app) {
// Wenn keine Wochen vorhanden // Wenn keine Wochen vorhanden
if (!weeks || weeks.length === 0) { if (!weeks || weeks.length === 0) {
return res.json({ weeks: [] }); return res.json({
weeks: [],
overtime_offset_hours: overtimeOffsetHours,
overtime_corrections: overtimeCorrections
});
} }
const { getCalendarWeek } = require('../helpers/utils'); const { getCalendarWeek } = require('../helpers/utils');
@@ -681,7 +696,11 @@ function registerUserRoutes(app) {
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,
overtime_offset_hours: overtimeOffsetHours,
overtime_corrections: overtimeCorrections
});
} }
return; return;
} }
@@ -769,12 +788,18 @@ function registerUserRoutes(app) {
if (a.calendar_week !== b.calendar_week) return b.calendar_week - a.calendar_week; 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); return new Date(b.week_start) - new Date(a.week_start);
}); });
res.json({ weeks: weekData, overtime_offset_hours: overtimeOffsetHours }); res.json({
weeks: weekData,
overtime_offset_hours: overtimeOffsetHours,
overtime_corrections: overtimeCorrections
});
} }
}); // getHolidaysForDateRange.then }); // getHolidaysForDateRange.then
}); // db.all (allEntries) }); // db.all (allEntries)
}); // weeks.forEach }); // weeks.forEach
}); // db.all (weeks) }); // db.all (weeks)
}
);
}); // db.get (user) }); // db.get (user)
}); // db.get (options) }); // db.get (options)
}); // app.get }); // app.get

View File

@@ -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;

View File

@@ -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');
@@ -274,6 +312,41 @@
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';
// Tabelle füllen // Tabelle füllen

View File

@@ -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,23 +695,47 @@
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);
// Wenn keine Korrektur (0), nichts tun außer UI auf 0 zu normalisieren
if (value === 0) {
input.value = 0;
this.textContent = originalText;
this.disabled = false;
return;
}
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 { try {
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-offset`, { const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-offset`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ overtime_offset_hours: value }) body: JSON.stringify({ overtime_offset_hours: value, reason })
}); });
const data = await resp.json(); const data = await resp.json();
if (!resp.ok) { if (!resp.ok) {
alert(data.error || 'Fehler beim Speichern des Offsets'); alert(data.error || 'Fehler beim Speichern der Korrektur');
this.textContent = originalText;
this.disabled = false;
delete this.dataset.modalOpen;
return; return;
} }
@@ -535,22 +758,29 @@
}); });
loadCurrentOvertime(); 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 = '✓'; this.textContent = '✓';
setTimeout(() => { setTimeout(() => {
this.textContent = originalText; this.textContent = originalText;
this.disabled = false; this.disabled = false;
delete this.dataset.modalOpen;
}, 900); }, 900);
} catch (e) { } catch (e) {
console.error('Fehler beim Speichern des Offsets:', e); console.error('Fehler beim Speichern der Korrektur:', e);
alert('Fehler beim Speichern des Offsets'); alert('Fehler beim Speichern der Korrektur');
} finally {
if (this.textContent === '...') {
this.textContent = originalText; this.textContent = originalText;
this.disabled = false; this.disabled = false;
delete this.dataset.modalOpen;
} }
} }
}); });
}); });
});
// Urlaubstage-Offset speichern // Urlaubstage-Offset speichern
document.querySelectorAll('.save-vacation-offset-btn').forEach(btn => { document.querySelectorAll('.save-vacation-offset-btn').forEach(btn => {