// 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'); // 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, 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 }, 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); }); // 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); }); return { ...employee, 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; // 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' }); } db.run('UPDATE users SET overtime_offset_hours = ? WHERE id = ?', [normalized, userId], (err) => { if (err) { console.error('Fehler beim Speichern des Überstunden-Offsets:', err); return res.status(500).json({ error: 'Fehler beim Speichern des Überstunden-Offsets' }); } res.json({ success: true, overtime_offset_hours: normalized }); }); }); // 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; // User-Daten abrufen db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => { if (err || !user) { return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); } const wochenstunden = user.wochenstunden || 0; const urlaubstage = user.urlaubstage || 0; const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; // Einträge für die Woche abrufen db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type FROM timesheet_entries WHERE user_id = ? AND date >= ? AND date <= ? ORDER BY date`, [userId, week_start, week_end], (err, entries) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); } // Berechnungen let totalHours = 0; let overtimeTaken = 0; let vacationDays = 0; let vacationHours = 0; entries.forEach(entry => { if (entry.total_hours) { totalHours += entry.total_hours; } if (entry.overtime_taken_hours) { overtimeTaken += entry.overtime_taken_hours; } if (entry.vacation_type === 'full') { vacationDays += 1; vacationHours += 8; // Ganzer Tag = 8 Stunden } else if (entry.vacation_type === 'half') { vacationDays += 0.5; vacationHours += 4; // Halber Tag = 4 Stunden } }); // Anzahl Werktage berechnen (Montag-Freitag) const startDate = new Date(week_start); const endDate = new Date(week_end); let workdays = 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++; } } // Sollstunden berechnen const sollStunden = (wochenstunden / 5) * workdays; // Überstunden berechnen: Urlaub zählt als normale Arbeitszeit // Überstunden = (Tatsächliche Stunden + Urlaubsstunden) - Sollstunden const totalHoursWithVacation = totalHours + vacationHours; const overtimeHours = totalHoursWithVacation - sollStunden; const remainingOvertime = overtimeHours - overtimeTaken; const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours; // Verbleibende Urlaubstage const remainingVacation = urlaubstage - vacationDays; res.json({ wochenstunden, urlaubstage, totalHours, sollStunden, overtimeHours, overtimeTaken, remainingOvertime, overtimeOffsetHours, remainingOvertimeWithOffset, vacationDays, remainingVacation, 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;