354 lines
14 KiB
JavaScript
354 lines
14 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');
|
|
|
|
// 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.overtime_taken_hours) {
|
|
overtimeTaken += entry.overtime_taken_hours;
|
|
}
|
|
|
|
// Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden
|
|
if (entry.vacation_type === 'full') {
|
|
vacationDays += 1;
|
|
vacationHours += 8; // Ganzer Tag = 8 Stunden
|
|
// Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt
|
|
} else if (entry.vacation_type === 'half') {
|
|
vacationDays += 0.5;
|
|
vacationHours += 4; // Halber Tag = 4 Stunden
|
|
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
|
|
if (entry.total_hours) {
|
|
totalHours += entry.total_hours;
|
|
}
|
|
} else {
|
|
// Kein Urlaub - zähle nur Arbeitsstunden
|
|
if (entry.total_hours) {
|
|
totalHours += entry.total_hours;
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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;
|