// 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 const totalCumulativeOvertimeHours = cumulativeOvertimeHours + weekOvertimeHours; const totalCumulativeOvertimeTaken = cumulativeOvertimeTaken + overtimeTaken; // Variante B: Verbleibend = Summe Wochen-Überstunden („genommen“ nur Anzeige) const remainingOvertime = totalCumulativeOvertimeHours; 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;