Files
SDSStundenerfassung/routes/verwaltung-routes.js

776 lines
34 KiB
JavaScript

// Verwaltung Routes
const archiver = require('archiver');
const { db } = require('../database');
const { requireVerwaltung } = require('../middleware/auth');
const { getWeekDatesFromCalendarWeek } = require('../helpers/utils');
const { generatePDFToBuffer } = require('../services/pdf-service');
const { getHolidaysForDateRange } = require('../services/feiertage-service');
const { getCurrentOvertimeForUser } = require('../services/overtime-service');
// Routes registrieren
function registerVerwaltungRoutes(app) {
// Verwaltungs-Bereich
app.get('/verwaltung', requireVerwaltung, (req, res) => {
db.all(`
SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, u.overtime_offset_hours, u.vacation_offset_days, u.arbeitstage,
dl.firstname as downloaded_by_firstname,
dl.lastname as downloaded_by_lastname,
(SELECT COUNT(*) FROM weekly_timesheets wt2
WHERE wt2.user_id = wt.user_id
AND wt2.week_start = wt.week_start
AND wt2.week_end = wt.week_end) as total_versions
FROM weekly_timesheets wt
JOIN users u ON wt.user_id = u.id
LEFT JOIN users dl ON wt.pdf_downloaded_by = dl.id
WHERE wt.status = 'eingereicht'
ORDER BY wt.week_start DESC, wt.user_id, wt.version DESC
`, (err, timesheets) => {
// Gruppiere nach Mitarbeiter, dann nach Kalenderwoche
// Struktur: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } }
const groupedByEmployee = {};
(timesheets || []).forEach(ts => {
const userId = ts.user_id;
const weekKey = `${ts.week_start}_${ts.week_end}`;
// Level 1: Mitarbeiter
if (!groupedByEmployee[userId]) {
groupedByEmployee[userId] = {
user: {
id: ts.user_id,
firstname: ts.firstname,
lastname: ts.lastname,
username: ts.username,
personalnummer: ts.personalnummer,
wochenstunden: ts.wochenstunden,
urlaubstage: ts.urlaubstage,
overtime_offset_hours: ts.overtime_offset_hours,
vacation_offset_days: ts.vacation_offset_days
},
weeks: {}
};
}
// Level 2: Kalenderwoche
if (!groupedByEmployee[userId].weeks[weekKey]) {
groupedByEmployee[userId].weeks[weekKey] = {
week_start: ts.week_start,
week_end: ts.week_end,
total_versions: ts.total_versions,
versions: []
};
}
// Level 3: Versionen
groupedByEmployee[userId].weeks[weekKey].versions.push(ts);
});
// Prüfe für jede Woche, ob nach dem letzten Download eine neue Version eingereicht wurde
Object.values(groupedByEmployee).forEach(employee => {
Object.values(employee.weeks).forEach(week => {
// Finde die neueste Version mit pdf_downloaded_at (letzter Download)
let lastDownloadTime = null;
week.versions.forEach(version => {
if (version.pdf_downloaded_at) {
const downloadTime = new Date(version.pdf_downloaded_at).getTime();
if (!lastDownloadTime || downloadTime > lastDownloadTime) {
lastDownloadTime = downloadTime;
}
}
});
// Prüfe, ob es eine Version gibt, die nach dem letzten Download eingereicht wurde
let hasNewVersionAfterDownload = false;
if (lastDownloadTime) {
week.versions.forEach(version => {
if (version.submitted_at) {
const submittedTime = new Date(version.submitted_at).getTime();
if (submittedTime > lastDownloadTime) {
hasNewVersionAfterDownload = true;
}
}
});
}
// Setze Flag auf dem week-Objekt
week.has_new_version_after_download = hasNewVersionAfterDownload;
});
});
// Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst)
const sortedEmployees = Object.values(groupedByEmployee).map(employee => {
// Wochen innerhalb jedes Mitarbeiters sortieren
const sortedWeeks = Object.values(employee.weeks).sort((a, b) => {
return new Date(b.week_start) - new Date(a.week_start);
});
// Flag: Gibt es in irgendeiner Woche eine neue Version nach Download?
const hasNewVersionAfterDownload = sortedWeeks.some(w => w.has_new_version_after_download);
return {
...employee,
has_new_version_after_download: hasNewVersionAfterDownload,
weeks: sortedWeeks
};
}).sort((a, b) => {
// Mitarbeiter nach Nachname, dann Vorname sortieren
const nameA = `${a.user.lastname} ${a.user.firstname}`.toLowerCase();
const nameB = `${b.user.lastname} ${b.user.firstname}`.toLowerCase();
return nameA.localeCompare(nameB);
});
res.render('verwaltung', {
groupedByEmployee: sortedEmployees,
user: {
firstname: req.session.firstname,
lastname: req.session.lastname,
roles: req.session.roles || [],
currentRole: req.session.currentRole || 'verwaltung'
}
});
});
});
// API: Überstunden-Offset für einen User setzen (positiv/negativ)
app.put('/api/verwaltung/user/:id/overtime-offset', requireVerwaltung, (req, res) => {
const userId = req.params.id;
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
const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw);
if (!Number.isFinite(normalized)) {
return res.status(400).json({ error: 'Ungültiger Überstunden-Offset' });
}
// Neue Logik: Korrektur protokollieren + kumulativ addieren
// Feld in der Verwaltung soll nach dem Speichern immer auf 0 zurückgesetzt werden.
if (normalized === 0) {
return res.json({ success: true, overtime_offset_hours: 0 });
}
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 });
});
}
);
}
);
});
});
// API: Urlaubstage-Offset für einen User setzen (positiv/negativ)
app.put('/api/verwaltung/user/:id/vacation-offset', requireVerwaltung, (req, res) => {
const userId = req.params.id;
const raw = req.body ? req.body.vacation_offset_days : undefined;
// Leere Eingabe => 0
const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw);
if (!Number.isFinite(normalized)) {
return res.status(400).json({ error: 'Ungültiger Urlaubstage-Offset' });
}
db.run('UPDATE users SET vacation_offset_days = ? WHERE id = ?', [normalized, userId], (err) => {
if (err) {
console.error('Fehler beim Speichern des Urlaubstage-Offsets:', err);
return res.status(500).json({ error: 'Fehler beim Speichern des Urlaubstage-Offsets' });
}
res.json({ success: true, vacation_offset_days: normalized });
});
});
// 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
app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => {
const userId = req.params.id;
const currentYear = new Date().getFullYear();
const yearStart = `${currentYear}-01-01`;
const yearEnd = `${currentYear}-12-31`;
db.all(`SELECT DISTINCT date
FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ? AND sick_status = 1
ORDER BY date`,
[userId, yearStart, yearEnd],
(err, entries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Krankheitstage' });
}
const sickDays = entries ? entries.length : 0;
res.json({ sickDays: sickDays, year: currentYear });
}
);
});
// API: Aktuelle Überstunden für mehrere Mitarbeiter (Batch für Verwaltungs-Übersicht)
app.get('/api/verwaltung/employees/current-overtime', requireVerwaltung, (req, res) => {
const userIdsParam = req.query.userIds;
if (!userIdsParam || typeof userIdsParam !== 'string') {
return res.status(400).json({ error: 'userIds (kommagetrennt) erforderlich' });
}
const userIds = userIdsParam.split(',').map((id) => parseInt(id.trim(), 10)).filter((id) => !isNaN(id));
if (userIds.length === 0) {
return res.json({});
}
const result = {};
let pending = userIds.length;
userIds.forEach((userId) => {
getCurrentOvertimeForUser(userId, db, (err, currentOvertime) => {
if (err) {
result[String(userId)] = null;
} else {
result[String(userId)] = currentOvertime != null ? currentOvertime : 0;
}
pending--;
if (pending === 0) {
res.json(result);
}
});
});
});
// API: Überstunden- und Urlaubsstatistiken für einen User abrufen
app.get('/api/verwaltung/user/:id/stats', requireVerwaltung, (req, res) => {
const userId = req.params.id;
const { week_start, week_end } = req.query;
// Wochenend-Prozentsätze laden
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Laden der Optionen' });
}
const saturdayPercentage = options?.saturday_percentage || 100;
const sundayPercentage = options?.sunday_percentage || 100;
// Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück
function getWeekendPercentage(dateStr) {
const date = new Date(dateStr);
const day = date.getDay();
if (day === 6) { // Samstag
return saturdayPercentage;
} else if (day === 0) { // Sonntag
return sundayPercentage;
}
return 100; // Kein Wochenende = 100% (normal)
}
// User-Daten abrufen
db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours, vacation_offset_days, arbeitstage 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 arbeitstage = user.arbeitstage || 5;
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8;
const urlaubstage = user.urlaubstage || 0;
const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0;
const vacationOffsetDays = user.vacation_offset_days ? parseFloat(user.vacation_offset_days) : 0;
// Alle bereits genommenen Urlaubstage aus eingereichten Wochen berechnen
// Zuerst: Alle eingereichten Wochen abrufen
db.all(`SELECT DISTINCT week_start, week_end
FROM weekly_timesheets
WHERE user_id = ? AND status = 'eingereicht'
ORDER BY week_start`,
[userId],
(err, weeks) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
}
// Nur Wochen bis Ende der angezeigten Kalenderwoche (Stand Urlaub = Ende dieser KW)
const weeksUpToDisplayed = (weeks || []).filter((w) => w.week_end <= week_end);
// Wochen VOR der aktuellen Woche für kumulative Überstunden-Berechnung
const weeksBeforeCurrent = (weeks || []).filter((w) => w.week_end < week_end);
let processedWeeks = 0;
let totalVacationDays = 0;
const vacationByDate = {};
// Kumulative Überstunden über alle Wochen VOR der aktuellen Woche
let cumulativeOvertimeHours = 0;
let cumulativeOvertimeTaken = 0;
// Urlaubstage für alle Wochen bis zur aktuellen Woche (inklusive)
if (weeksUpToDisplayed.length === 0) {
processCurrentWeek(0, 0, 0);
} else {
weeksUpToDisplayed.forEach((week) => {
db.all(`SELECT date, vacation_type, updated_at, id
FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ?
AND vacation_type IS NOT NULL
AND vacation_type != ''
ORDER BY date, updated_at DESC, id DESC`,
[userId, week.week_start, week.week_end],
(err, weekEntries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' });
}
(weekEntries || []).forEach(entry => {
const existing = vacationByDate[entry.date];
if (!existing) {
vacationByDate[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)) {
vacationByDate[entry.date] = entry;
}
}
});
processedWeeks++;
if (processedWeeks === weeksUpToDisplayed.length) {
Object.values(vacationByDate).forEach(entry => {
if (entry.vacation_type === 'full') {
totalVacationDays += 1;
} else if (entry.vacation_type === 'half') {
totalVacationDays += 0.5;
}
});
// Berechne Überstunden für alle Wochen VOR der aktuellen Woche
if (weeksBeforeCurrent.length === 0) {
processCurrentWeek(totalVacationDays, 0, 0);
} else {
let processedOvertimeWeeks = 0;
weeksBeforeCurrent.forEach((week) => {
calculateWeekOvertime(week.week_start, week.week_end, (weekOvertime, weekOvertimeTaken) => {
cumulativeOvertimeHours += weekOvertime;
cumulativeOvertimeTaken += weekOvertimeTaken;
processedOvertimeWeeks++;
if (processedOvertimeWeeks === weeksBeforeCurrent.length) {
processCurrentWeek(totalVacationDays, cumulativeOvertimeHours, cumulativeOvertimeTaken);
}
});
});
}
}
});
});
}
function calculateWeekOvertime(weekStart, weekEnd, callback) {
db.all(`SELECT id, date, updated_at, total_hours, overtime_taken_hours, vacation_type, sick_status
FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date, updated_at DESC, id DESC`,
[userId, weekStart, weekEnd],
(err, allEntries) => {
if (err) {
return callback(0, 0);
}
// Nur neuesten Eintrag pro Tag zählen
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;
}
}
});
const entries = Object.values(entriesByDate);
let weekTotalHours = 0;
let weekOvertimeTaken = 0;
let weekVacationHours = 0;
entries.forEach(entry => {
if (entry.overtime_taken_hours) {
weekOvertimeTaken += parseFloat(entry.overtime_taken_hours) || 0;
}
if (entry.vacation_type === 'full') {
weekVacationHours += fullDayHours;
} else if (entry.vacation_type === 'half') {
weekVacationHours += fullDayHours / 2;
if (entry.total_hours) {
weekTotalHours += parseFloat(entry.total_hours) || 0;
}
} else {
if (entry.total_hours) {
weekTotalHours += parseFloat(entry.total_hours) || 0;
}
}
});
getHolidaysForDateRange(weekStart, weekEnd)
.catch(() => new Set())
.then((holidaySet) => {
const startDate = new Date(weekStart);
const endDate = new Date(weekEnd);
let workdays = 0;
let holidayDays = 0;
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) {
workdays++;
const dateStr = d.toISOString().split('T')[0];
if (holidaySet.has(dateStr)) {
holidayDays++;
// Feiertagsstunden für alle Feiertage hinzufügen (wie im PDF)
holidayHours += fullDayHours;
}
}
}
// Sollstunden = Wochenstunden (Feiertage reduzieren Soll nicht, da bezahlt)
const sollStunden = wochenstunden;
const totalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours;
const weekOvertimeHours = totalHoursWithVacation - sollStunden;
callback(weekOvertimeHours, weekOvertimeTaken);
});
});
}
function processCurrentWeek(totalVacationDays, cumulativeOvertimeHours, cumulativeOvertimeTaken) {
// Einträge für die Woche abrufen (id/updated_at für neuesten pro Tag)
db.all(`SELECT id, date, updated_at, total_hours, overtime_taken_hours, vacation_type, sick_status
FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date, updated_at DESC, id DESC`,
[userId, week_start, week_end],
(err, allEntries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' });
}
// Nur neuesten Eintrag pro Tag zählen (wie PDF/Submit), sonst Doppelzählung bei Duplikaten
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;
}
}
});
const entries = Object.values(entriesByDate);
// Berechnungen
let totalHours = 0;
let overtimeTaken = 0;
let vacationDays = 0;
let vacationHours = 0;
let sickDays = 0;
entries.forEach(entry => {
if (entry.overtime_taken_hours) {
overtimeTaken += parseFloat(entry.overtime_taken_hours) || 0;
}
// Krankheitstage zählen
if (entry.sick_status === 1 || entry.sick_status === true) {
sickDays += 1;
}
// Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden
if (entry.vacation_type === 'full') {
vacationDays += 1;
vacationHours += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden
// Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt
} else if (entry.vacation_type === 'half') {
vacationDays += 0.5;
vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden
// 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) {
totalHours += parseFloat(entry.total_hours) || 0;
}
} else {
// Kein Urlaub - zähle nur Arbeitsstunden
// WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js)
if (entry.total_hours) {
totalHours += parseFloat(entry.total_hours) || 0;
}
}
});
// Feiertage für die Woche laden ((Wochenarbeitszeit / Arbeitstage) pro Feiertag; Arbeit an Feiertag = Überstunden)
getHolidaysForDateRange(week_start, week_end)
.catch(() => new Set())
.then((holidaySet) => {
// Anzahl Werktage berechnen (Montag-Freitag)
const startDate = new Date(week_start);
const endDate = new Date(week_end);
let workdays = 0;
let holidayDays = 0;
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) { // Montag bis Freitag
workdays++;
const dateStr = d.toISOString().split('T')[0];
if (holidaySet.has(dateStr)) {
holidayDays++;
// Feiertagsstunden für alle Feiertage hinzufügen (wie im PDF)
holidayHours += fullDayHours;
}
}
}
// Sollstunden = Wochenstunden (Feiertage reduzieren Soll nicht, da bezahlt)
const sollStunden = wochenstunden;
// Überstunden für die aktuelle Woche: (Tatsächliche Stunden + Urlaubsstunden + Feiertagsstunden) - Sollstunden
const totalHoursWithVacation = totalHours + vacationHours + holidayHours;
const weekOvertimeHours = totalHoursWithVacation - sollStunden;
// Kumulative Überstunden: Summe aller Wochen bis zur aktuellen Woche
// cumulativeOvertimeHours enthält bereits alle vorherigen Wochen
const totalCumulativeOvertimeHours = cumulativeOvertimeHours + weekOvertimeHours;
const totalCumulativeOvertimeTaken = cumulativeOvertimeTaken + overtimeTaken;
// Verbleibende Überstunden = kumulative Überstunden - kumulative genommene Überstunden
const remainingOvertime = totalCumulativeOvertimeHours - totalCumulativeOvertimeTaken;
const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours;
// Verbleibende Urlaubstage (berücksichtigt alle eingereichten Wochen, nicht nur die aktuelle)
const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays;
res.json({
wochenstunden,
urlaubstage,
totalHours,
sollStunden,
weekOvertimeHours, // Überstunden nur für diese Woche
overtimeHours: totalCumulativeOvertimeHours, // Kumulative Überstunden
overtimeTaken: totalCumulativeOvertimeTaken, // Kumulative genommene Überstunden
remainingOvertime,
overtimeOffsetHours,
remainingOvertimeWithOffset,
vacationDays,
vacationOffsetDays,
remainingVacation,
totalVacationDays,
sickDays,
workdays
});
});
});
}
});
});
});
});
// API: Admin-Kommentar speichern
app.put('/api/verwaltung/timesheet/:id/comment', requireVerwaltung, (req, res) => {
const timesheetId = req.params.id;
const { comment } = req.body;
db.run('UPDATE weekly_timesheets SET admin_comment = ? WHERE id = ?',
[comment ? comment.trim() : null, timesheetId],
(err) => {
if (err) {
console.error('Fehler beim Speichern des Kommentars:', err);
return res.status(500).json({ error: 'Fehler beim Speichern des Kommentars' });
}
res.json({ success: true });
});
});
// API: Massendownload aller PDFs für eine Kalenderwoche
app.get('/api/verwaltung/bulk-download/:year/:week', requireVerwaltung, async (req, res) => {
const year = parseInt(req.params.year);
const week = parseInt(req.params.week);
const downloadedBy = req.session.userId;
// Validierung
if (!year || year < 2000 || year > 2100) {
return res.status(400).json({ error: 'Ungültiges Jahr' });
}
if (!week || week < 1 || week > 53) {
return res.status(400).json({ error: 'Ungültige Kalenderwoche (1-53)' });
}
try {
// Berechne week_start und week_end aus Jahr und KW
const { week_start, week_end } = getWeekDatesFromCalendarWeek(year, week);
// Hole alle eingereichten Stundenzettel für diese KW
db.all(`SELECT wt.id, wt.user_id, wt.version, u.firstname, u.lastname
FROM weekly_timesheets wt
JOIN users u ON wt.user_id = u.id
WHERE wt.status = 'eingereicht'
AND wt.week_start = ?
AND wt.week_end = ?
ORDER BY wt.user_id, wt.version DESC`,
[week_start, week_end],
async (err, allTimesheets) => {
if (err) {
console.error('Fehler beim Abrufen der Stundenzettel:', err);
return res.status(500).json({ error: 'Fehler beim Abrufen der Stundenzettel' });
}
if (!allTimesheets || allTimesheets.length === 0) {
return res.status(404).json({ error: `Keine eingereichten Stundenzettel für KW ${week}/${year} gefunden` });
}
// Gruppiere nach user_id und wähle neueste Version pro User
const latestByUser = {};
allTimesheets.forEach(ts => {
if (!latestByUser[ts.user_id] || ts.version > latestByUser[ts.user_id].version) {
latestByUser[ts.user_id] = ts;
}
});
const timesheetsToDownload = Object.values(latestByUser);
const timesheetIds = timesheetsToDownload.map(ts => ts.id);
// Erstelle ZIP
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', `attachment; filename="Stundenzettel_KW${String(week).padStart(2, '0')}_${year}.zip"`);
const archive = archiver('zip', { zlib: { level: 9 } });
archive.on('error', (err) => {
console.error('Fehler beim Erstellen des ZIP:', err);
if (!res.headersSent) {
res.status(500).json({ error: 'Fehler beim Erstellen des ZIP-Archivs' });
}
});
archive.pipe(res);
// Generiere PDFs sequenziell und füge sie zum ZIP hinzu
const errors = [];
for (const ts of timesheetsToDownload) {
try {
// Erstelle Mock-Request-Objekt für generatePDFToBuffer
const mockReq = {
session: { userId: downloadedBy },
query: {}
};
const pdfBuffer = await generatePDFToBuffer(ts.id, mockReq);
// Dateiname: Stundenzettel_KW{week}_{Nachname}{Vorname}_Version{version}.pdf
const employeeName = `${ts.lastname}${ts.firstname}`.replace(/\s+/g, '');
const filename = `Stundenzettel_KW${String(week).padStart(2, '0')}_${employeeName}_Version${ts.version}.pdf`;
archive.append(pdfBuffer, { name: filename });
} catch (pdfError) {
console.error(`Fehler beim Generieren des PDFs für Timesheet ${ts.id}:`, pdfError);
errors.push(`Fehler bei ${ts.firstname} ${ts.lastname}: ${pdfError.message}`);
}
}
// Warte auf ZIP-Finalisierung und markiere dann PDFs als heruntergeladen
archive.on('end', () => {
if (timesheetIds.length > 0 && downloadedBy) {
// Update alle betroffenen timesheets
const placeholders = timesheetIds.map(() => '?').join(',');
db.run(`UPDATE weekly_timesheets
SET pdf_downloaded_at = CURRENT_TIMESTAMP,
pdf_downloaded_by = ?
WHERE id IN (${placeholders})`,
[downloadedBy, ...timesheetIds],
(err) => {
if (err) {
console.error('Fehler beim Markieren der PDFs als heruntergeladen:', err);
} else {
console.log(`Massendownload: ${timesheetIds.length} PDFs als heruntergeladen markiert`);
}
});
}
});
// Finalisiere ZIP (startet den Stream)
archive.finalize();
// Wenn Fehler aufgetreten sind, aber ZIP trotzdem erstellt wurde, logge sie
if (errors.length > 0) {
console.warn('Einige PDFs konnten nicht generiert werden:', errors);
}
});
} catch (error) {
console.error('Fehler beim Massendownload:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Fehler beim Massendownload: ' + error.message });
}
}
});
}
module.exports = registerVerwaltungRoutes;