Massdownload
This commit is contained in:
@@ -6,7 +6,7 @@ const LDAPService = require('../ldap-service');
|
||||
const { getDefaultRole } = require('../helpers/utils');
|
||||
|
||||
// Helper-Funktion für erfolgreiche Anmeldung
|
||||
function handleSuccessfulLogin(req, res, user) {
|
||||
function handleSuccessfulLogin(req, res, user, rememberMe = false) {
|
||||
// Rollen als JSON-Array parsen
|
||||
let roles = [];
|
||||
try {
|
||||
@@ -35,6 +35,13 @@ function handleSuccessfulLogin(req, res, user) {
|
||||
req.session.firstname = user.firstname;
|
||||
req.session.lastname = user.lastname;
|
||||
|
||||
// Session-Gültigkeit setzen: 30 Tage wenn "Angemeldet bleiben" aktiviert, sonst 24 Stunden
|
||||
if (rememberMe) {
|
||||
req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 Tage
|
||||
} else {
|
||||
req.session.cookie.maxAge = 24 * 60 * 60 * 1000; // 24 Stunden
|
||||
}
|
||||
|
||||
// Redirect: Immer zu Dashboard wenn Mitarbeiter-Rolle vorhanden, sonst basierend auf Standard-Rolle
|
||||
if (roles.includes('mitarbeiter')) {
|
||||
res.redirect('/dashboard');
|
||||
@@ -56,7 +63,8 @@ function registerAuthRoutes(app) {
|
||||
|
||||
// Login-Verarbeitung
|
||||
app.post('/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const { username, password, remember_me } = req.body;
|
||||
const rememberMe = remember_me === 'on' || remember_me === true;
|
||||
|
||||
// Prüfe ob LDAP aktiviert ist
|
||||
LDAPService.getConfig((err, ldapConfig) => {
|
||||
@@ -78,7 +86,7 @@ function registerAuthRoutes(app) {
|
||||
|
||||
// Versuche lokale Authentifizierung
|
||||
if (bcrypt.compareSync(password, user.password)) {
|
||||
handleSuccessfulLogin(req, res, user);
|
||||
handleSuccessfulLogin(req, res, user, rememberMe);
|
||||
} else {
|
||||
res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
||||
}
|
||||
@@ -90,7 +98,7 @@ function registerAuthRoutes(app) {
|
||||
return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' });
|
||||
}
|
||||
|
||||
handleSuccessfulLogin(req, res, user);
|
||||
handleSuccessfulLogin(req, res, user, rememberMe);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -102,7 +110,7 @@ function registerAuthRoutes(app) {
|
||||
}
|
||||
|
||||
if (bcrypt.compareSync(password, user.password)) {
|
||||
handleSuccessfulLogin(req, res, user);
|
||||
handleSuccessfulLogin(req, res, user, rememberMe);
|
||||
} else {
|
||||
res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
||||
}
|
||||
|
||||
@@ -60,8 +60,7 @@ function registerTimesheetRoutes(app) {
|
||||
total_hours = 0;
|
||||
finalStartTime = null;
|
||||
finalEndTime = null;
|
||||
finalActivity1Desc = 'Überstunden';
|
||||
finalActivity1Hours = 0;
|
||||
// Keine Tätigkeit setzen - Überstunden werden über overtime_taken_hours in der PDF angezeigt
|
||||
} else if (vacation_type === 'full') {
|
||||
total_hours = 8; // Ganzer Tag Urlaub = 8 Stunden normale Arbeitszeit
|
||||
} else if (isSick) {
|
||||
@@ -77,47 +76,8 @@ function registerTimesheetRoutes(app) {
|
||||
// Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt
|
||||
}
|
||||
|
||||
// Überstunden-Logik: Bei weniger als vollem Tag - füge "Überstunden" als Tätigkeit hinzu
|
||||
if (overtimeValue > 0 && !isFullDayOvertime && fullDayHours > 0) {
|
||||
// Prüfe ob "Überstunden" bereits in activities vorhanden
|
||||
const activities = [
|
||||
{ desc: finalActivity1Desc, hours: finalActivity1Hours },
|
||||
{ desc: finalActivity2Desc, hours: activity2_hours },
|
||||
{ desc: finalActivity3Desc, hours: activity3_hours },
|
||||
{ desc: finalActivity4Desc, hours: activity4_hours },
|
||||
{ desc: finalActivity5Desc, hours: activity5_hours }
|
||||
];
|
||||
|
||||
let foundOvertime = false;
|
||||
for (let i = 0; i < activities.length; i++) {
|
||||
if (activities[i].desc && activities[i].desc.trim().toLowerCase() === 'überstunden') {
|
||||
foundOvertime = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn nicht gefunden, füge zur ersten freien Activity-Spalte hinzu
|
||||
if (!foundOvertime) {
|
||||
for (let i = 0; i < activities.length; i++) {
|
||||
if (!activities[i].desc || activities[i].desc.trim() === '') {
|
||||
// Setze diese Activity auf "Überstunden"
|
||||
if (i === 0) {
|
||||
finalActivity1Desc = 'Überstunden';
|
||||
// Stunden bleiben unverändert (werden vom User eingegeben)
|
||||
} else if (i === 1) {
|
||||
finalActivity2Desc = 'Überstunden';
|
||||
} else if (i === 2) {
|
||||
finalActivity3Desc = 'Überstunden';
|
||||
} else if (i === 3) {
|
||||
finalActivity4Desc = 'Überstunden';
|
||||
} else if (i === 4) {
|
||||
finalActivity5Desc = 'Überstunden';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Überstunden werden nicht mehr als Tätigkeit hinzugefügt
|
||||
// Sie werden über overtime_taken_hours in der PDF angezeigt
|
||||
|
||||
// Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren
|
||||
db.get('SELECT id FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
||||
|
||||
@@ -125,13 +125,14 @@ function registerUserRoutes(app) {
|
||||
const userId = req.session.userId;
|
||||
|
||||
// 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;
|
||||
|
||||
// Alle eingereichten Wochen abrufen
|
||||
db.all(`SELECT DISTINCT week_start, week_end
|
||||
@@ -147,12 +148,13 @@ function registerUserRoutes(app) {
|
||||
// Wenn keine Wochen vorhanden
|
||||
if (!weeks || weeks.length === 0) {
|
||||
return res.json({
|
||||
currentOvertime: 0,
|
||||
currentOvertime: overtimeOffsetHours,
|
||||
remainingVacation: urlaubstage,
|
||||
totalOvertimeHours: 0,
|
||||
totalOvertimeTaken: 0,
|
||||
totalVacationDays: 0,
|
||||
urlaubstage: urlaubstage
|
||||
urlaubstage: urlaubstage,
|
||||
overtimeOffsetHours: overtimeOffsetHours
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,7 +237,7 @@ function registerUserRoutes(app) {
|
||||
// Woche nicht vollständig - überspringe diese Woche
|
||||
processedWeeks++;
|
||||
if (processedWeeks === weeks.length && !hasError) {
|
||||
const currentOvertime = totalOvertimeHours - totalOvertimeTaken;
|
||||
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
||||
const remainingVacation = urlaubstage - totalVacationDays;
|
||||
|
||||
res.json({
|
||||
@@ -244,7 +246,8 @@ function registerUserRoutes(app) {
|
||||
totalOvertimeHours: totalOvertimeHours,
|
||||
totalOvertimeTaken: totalOvertimeTaken,
|
||||
totalVacationDays: totalVacationDays,
|
||||
urlaubstage: urlaubstage
|
||||
urlaubstage: urlaubstage,
|
||||
overtimeOffsetHours: overtimeOffsetHours
|
||||
});
|
||||
}
|
||||
return; // Überspringe diese Woche
|
||||
@@ -288,7 +291,7 @@ function registerUserRoutes(app) {
|
||||
|
||||
// Wenn alle Wochen verarbeitet wurden, Antwort senden
|
||||
if (processedWeeks === weeks.length && !hasError) {
|
||||
const currentOvertime = totalOvertimeHours - totalOvertimeTaken;
|
||||
const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours;
|
||||
const remainingVacation = urlaubstage - totalVacationDays;
|
||||
|
||||
res.json({
|
||||
@@ -297,7 +300,8 @@ function registerUserRoutes(app) {
|
||||
totalOvertimeHours: totalOvertimeHours,
|
||||
totalOvertimeTaken: totalOvertimeTaken,
|
||||
totalVacationDays: totalVacationDays,
|
||||
urlaubstage: urlaubstage
|
||||
urlaubstage: urlaubstage,
|
||||
overtimeOffsetHours: overtimeOffsetHours
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user