Massdownload

This commit is contained in:
Carsten Graf
2026-01-23 17:29:46 +01:00
parent a0acd188a8
commit daf4f9b77c
13 changed files with 2135 additions and 104 deletions

View File

@@ -1,14 +1,17 @@
// 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,
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
@@ -39,7 +42,8 @@ function registerVerwaltungRoutes(app) {
username: ts.username,
personalnummer: ts.personalnummer,
wochenstunden: ts.wochenstunden,
urlaubstage: ts.urlaubstage
urlaubstage: ts.urlaubstage,
overtime_offset_hours: ts.overtime_offset_hours
},
weeks: {}
};
@@ -89,19 +93,40 @@ function registerVerwaltungRoutes(app) {
});
});
// 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 FROM users WHERE id = ?', [userId], (err, user) => {
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
@@ -155,6 +180,7 @@ function registerVerwaltungRoutes(app) {
const totalHoursWithVacation = totalHours + vacationHours;
const overtimeHours = totalHoursWithVacation - sollStunden;
const remainingOvertime = overtimeHours - overtimeTaken;
const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours;
// Verbleibende Urlaubstage
const remainingVacation = urlaubstage - vacationDays;
@@ -167,6 +193,8 @@ function registerVerwaltungRoutes(app) {
overtimeHours,
overtimeTaken,
remainingOvertime,
overtimeOffsetHours,
remainingOvertimeWithOffset,
vacationDays,
remainingVacation,
workdays
@@ -190,6 +218,127 @@ function registerVerwaltungRoutes(app) {
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;