Files
SDSStundenerfassung/routes/timesheet-routes.js

688 lines
33 KiB
JavaScript

// Timesheet API Routes
const { db } = require('../database');
const { requireAuth, requireVerwaltung } = require('../middleware/auth');
const { generatePDFToBuffer } = require('../services/pdf-service');
const { getHolidaysForDateRange } = require('../services/feiertage-service');
const { hasRole } = require('../helpers/utils');
const fs = require('fs');
const {
fileExists,
savePdfBufferAtomic,
resolvePdfLocationFromTimesheetRow,
resolveAbsolutePathFromRelative,
} = require('../services/pdf-storage-service');
/** Plausibilitätsprüfung Projektnummer: 7 Ziffern, beginnt mit 5, dann YY (Jahr), dann 4 freie Ziffern (z.B. 5260001). */
function isValidProjectNumber(value) {
if (value === null || value === undefined || String(value).trim() === '') return true;
const s = String(value).trim();
return /^5\d{6}$/.test(s);
}
function validateProjectNumbers(body) {
const numbers = [
body.activity1_project_number,
body.activity2_project_number,
body.activity3_project_number,
body.activity4_project_number,
body.activity5_project_number
];
for (let i = 0; i < numbers.length; i++) {
if (!isValidProjectNumber(numbers[i])) {
return { valid: false, activityIndex: i + 1, value: numbers[i] };
}
}
return { valid: true };
}
// Routes registrieren
function registerTimesheetRoutes(app) {
// API: Stundenerfassung speichern
app.post('/api/timesheet/save', requireAuth, (req, res) => {
const {
date, start_time, end_time, break_minutes, notes,
activity1_desc, activity1_hours, activity1_project_number,
activity2_desc, activity2_hours, activity2_project_number,
activity3_desc, activity3_hours, activity3_project_number,
activity4_desc, activity4_hours, activity4_project_number,
activity5_desc, activity5_hours, activity5_project_number,
overtime_taken_hours, vacation_type, sick_status, weekend_travel
} = req.body;
const userId = req.session.userId;
const projectNumberCheck = validateProjectNumbers(req.body);
if (!projectNumberCheck.valid) {
return res.status(400).json({
error: `Ungültige Projektnummer in Tätigkeit ${projectNumberCheck.activityIndex}: Die Projektnummer muss 7 Ziffern haben, mit 5 beginnen, gefolgt vom Jahr (YY) und 4 Ziffern (z.B. 5260001).`
});
}
const hasExplicitBreakMinutes = break_minutes !== undefined && break_minutes !== null && break_minutes !== '';
const parsedRequestedBreakMinutes = hasExplicitBreakMinutes ? parseInt(break_minutes, 10) : null;
const requestedBreakMinutes = Number.isFinite(parsedRequestedBreakMinutes) && parsedRequestedBreakMinutes >= 0
? parsedRequestedBreakMinutes
: null;
// Normalisiere end_time: Leere Strings werden zu null
const normalizedEndTime = (end_time && typeof end_time === 'string' && end_time.trim() !== '') ? end_time.trim() : (end_time || null);
const normalizedStartTime = (start_time && typeof start_time === 'string' && start_time.trim() !== '') ? start_time.trim() : (start_time || null);
// Normalisiere sick_status: Boolean oder 1/0 zu Boolean
const isSick = sick_status === true || sick_status === 1 || sick_status === 'true' || sick_status === '1';
// Normalisiere weekend_travel: Boolean oder 1/0 zu Integer
const isWeekendTravel = weekend_travel === true || weekend_travel === 1 || weekend_travel === 'true' || weekend_travel === '1';
const weekendTravelValue = isWeekendTravel ? 1 : 0;
// Wochenend-Prozentsätze laden
db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => {
if (err) {
console.error('Fehler beim Laden der Optionen:', 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 laden (für Überstunden-Berechnung)
db.get('SELECT wochenstunden, arbeitstage, default_break_minutes FROM users WHERE id = ?', [userId], (err, user) => {
if (err) {
console.error('Fehler beim Laden der User-Daten:', err);
return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' });
}
const wochenstunden = user?.wochenstunden || 0;
const arbeitstage = user?.arbeitstage || 5;
const defaultBreakMinutes = Number.isInteger(user?.default_break_minutes) && user.default_break_minutes >= 0
? user.default_break_minutes
: 30;
let effectiveBreakMinutes = requestedBreakMinutes !== null ? requestedBreakMinutes : defaultBreakMinutes;
const overtimeValue = overtime_taken_hours ? parseFloat(overtime_taken_hours) : 0;
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 0;
// Überstunden-Logik: Prüfe ob ganzer Tag oder weniger
let isFullDayOvertime = false;
if (overtimeValue > 0 && fullDayHours > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) {
isFullDayOvertime = true;
}
// Prüfe ob es ein Wochenendtag ist
const dateObj = new Date(date);
const dayOfWeek = dateObj.getDay();
const isWeekend = (dayOfWeek === 6 || dayOfWeek === 0); // 6 = Samstag, 0 = Sonntag
// Gesamtstunden berechnen (aus Start- und Endzeit, nicht aus Tätigkeiten)
// Wenn ganzer Tag Urlaub oder Krank, dann zählt dieser als 8 Stunden normale Arbeitszeit
let total_hours = 0;
let finalActivity1Desc = activity1_desc;
let finalActivity1Hours = parseFloat(activity1_hours) || 0;
let finalActivity2Desc = activity2_desc;
let finalActivity3Desc = activity3_desc;
let finalActivity4Desc = activity4_desc;
let finalActivity5Desc = activity5_desc;
let finalStartTime = normalizedStartTime;
let finalEndTime = normalizedEndTime;
let appliedWeekendPercentage = null; // Wird gesetzt wenn Wochenend-Prozentsatz angewendet wird
// Überstunden-Logik: Bei vollem Tag Überstunden
if (isFullDayOvertime) {
total_hours = 0;
finalStartTime = null;
finalEndTime = null;
// Keine Tätigkeit setzen - Überstunden werden über overtime_taken_hours in der PDF angezeigt
} else if (vacation_type === 'full') {
total_hours = fullDayHours; // Ganzer Tag Urlaub = (Wochenarbeitszeit / Arbeitstage) Stunden normale Arbeitszeit
} else if (isSick) {
total_hours = fullDayHours; // Krank = (Wochenarbeitszeit / Arbeitstage) Stunden normale Arbeitszeit
finalActivity1Desc = 'Krank';
finalActivity1Hours = fullDayHours;
} else if (normalizedStartTime && normalizedEndTime) {
const start = new Date(`2000-01-01T${normalizedStartTime}`);
const end = new Date(`2000-01-01T${normalizedEndTime}`);
const diffMs = end - start;
total_hours = (diffMs / (1000 * 60 * 60)) - (effectiveBreakMinutes / 60);
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
if (isWeekend && isWeekendTravel && total_hours > 0 && !isSick && vacation_type !== 'full') {
const weekendPercentage = getWeekendPercentage(date);
if (weekendPercentage >= 100) {
total_hours = total_hours * (weekendPercentage / 100);
appliedWeekendPercentage = weekendPercentage; // Speichere den angewendeten Prozentsatz
}
}
// Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden
// Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt
}
// Ü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, break_minutes, applied_weekend_percentage FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
[userId, date], (err, row) => {
if (row) {
if (requestedBreakMinutes === null && row.break_minutes !== null && row.break_minutes !== undefined) {
effectiveBreakMinutes = row.break_minutes;
}
if (normalizedStartTime && normalizedEndTime && !isSick && vacation_type !== 'full' && !isFullDayOvertime) {
const start = new Date(`2000-01-01T${normalizedStartTime}`);
const end = new Date(`2000-01-01T${normalizedEndTime}`);
const diffMs = end - start;
total_hours = (diffMs / (1000 * 60 * 60)) - (effectiveBreakMinutes / 60);
if (isWeekend && isWeekendTravel && total_hours > 0) {
const weekendPercentage = getWeekendPercentage(date);
if (weekendPercentage >= 100) {
total_hours = total_hours * (weekendPercentage / 100);
}
}
}
// Wenn bereits ein gespeicherter Prozentsatz existiert, diesen verwenden (historische Einträge bleiben unverändert)
let finalAppliedPercentage = appliedWeekendPercentage;
if (row.applied_weekend_percentage !== null && row.applied_weekend_percentage !== undefined) {
// Verwende den gespeicherten Prozentsatz, aber nur wenn weekend_travel aktiviert ist
if (isWeekendTravel && isWeekend) {
finalAppliedPercentage = row.applied_weekend_percentage;
// Berechne total_hours neu mit gespeichertem Prozentsatz, falls nötig
if (normalizedStartTime && normalizedEndTime && !isSick && vacation_type !== 'full' && !isFullDayOvertime) {
const start = new Date(`2000-01-01T${normalizedStartTime}`);
const end = new Date(`2000-01-01T${normalizedEndTime}`);
const diffMs = end - start;
const baseHours = (diffMs / (1000 * 60 * 60)) - (effectiveBreakMinutes / 60);
if (baseHours > 0 && finalAppliedPercentage >= 100) {
total_hours = baseHours * (finalAppliedPercentage / 100);
}
}
} else {
// Wenn weekend_travel nicht aktiviert ist, aber ein gespeicherter Prozentsatz existiert, behalte ihn
finalAppliedPercentage = row.applied_weekend_percentage;
}
} else if (isWeekendTravel && isWeekend) {
// Neuer Eintrag mit weekend_travel - speichere den aktuellen Prozentsatz
finalAppliedPercentage = appliedWeekendPercentage;
}
// Update
db.run(`UPDATE timesheet_entries
SET start_time = ?, end_time = ?, break_minutes = ?, total_hours = ?, notes = ?,
activity1_desc = ?, activity1_hours = ?, activity1_project_number = ?,
activity2_desc = ?, activity2_hours = ?, activity2_project_number = ?,
activity3_desc = ?, activity3_hours = ?, activity3_project_number = ?,
activity4_desc = ?, activity4_hours = ?, activity4_project_number = ?,
activity5_desc = ?, activity5_hours = ?, activity5_project_number = ?,
overtime_taken_hours = ?, vacation_type = ?, sick_status = ?,
weekend_travel = ?, applied_weekend_percentage = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[
finalStartTime, finalEndTime, effectiveBreakMinutes, total_hours, notes,
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
finalActivity4Desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null,
finalActivity5Desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null,
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
vacation_type || null,
isSick ? 1 : 0,
weekendTravelValue,
finalAppliedPercentage,
row.id
],
(err) => {
if (err) {
console.error('Fehler beim Update:', err);
return res.status(500).json({ error: 'Fehler beim Speichern: ' + err.message });
}
res.json({ success: true, total_hours });
});
} else {
// Insert
db.run(`INSERT INTO timesheet_entries
(user_id, date, start_time, end_time, break_minutes, total_hours, notes,
activity1_desc, activity1_hours, activity1_project_number,
activity2_desc, activity2_hours, activity2_project_number,
activity3_desc, activity3_hours, activity3_project_number,
activity4_desc, activity4_hours, activity4_project_number,
activity5_desc, activity5_hours, activity5_project_number,
overtime_taken_hours, vacation_type, sick_status, weekend_travel, applied_weekend_percentage)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
userId, date, finalStartTime, finalEndTime, effectiveBreakMinutes, total_hours, notes,
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
finalActivity4Desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null,
finalActivity5Desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null,
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
vacation_type || null,
isSick ? 1 : 0,
weekendTravelValue,
appliedWeekendPercentage
],
(err) => {
if (err) {
console.error('Fehler beim Insert:', err);
return res.status(500).json({ error: 'Fehler beim Speichern: ' + err.message });
}
res.json({ success: true, total_hours });
});
}
});
});
});
});
// API: Feiertage für einen Zeitraum (Dashboard-Anzeige)
app.get('/api/timesheet/holidays', requireAuth, (req, res) => {
const { week_start, week_end } = req.query;
if (!week_start || !week_end) {
return res.status(400).json({ error: 'week_start und week_end erforderlich' });
}
getHolidaysForDateRange(week_start, week_end)
.then((set) => res.json({ dates: [...set] }))
.catch(() => res.json({ dates: [] }));
});
// API: Stundenerfassung für Woche laden
app.get('/api/timesheet/week/:weekStart', requireAuth, (req, res) => {
const userId = req.session.userId;
const weekStart = req.params.weekStart;
// Berechne Wochenende
const startDate = new Date(weekStart);
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 6);
const weekEnd = endDate.toISOString().split('T')[0];
// Prüfe ob die Woche bereits eingereicht wurde (aber ermögliche Bearbeitung)
db.get(`SELECT id, version FROM weekly_timesheets
WHERE user_id = ? AND week_start = ? AND week_end = ?
ORDER BY version DESC LIMIT 1`,
[userId, weekStart, weekEnd],
(err, weeklySheet) => {
const hasSubmittedVersion = !!weeklySheet;
const latestVersion = weeklySheet ? weeklySheet.version : 0;
// Lade alle Einträge für die Woche
db.all(`SELECT * FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date`,
[userId, weekStart, weekEnd],
(err, entries) => {
// Füge Status-Info hinzu (Bearbeitung ist immer möglich)
const entriesWithStatus = (entries || []).map(entry => ({
...entry,
week_submitted: hasSubmittedVersion, // Woche wurde eingereicht wenn weekly_timesheet existiert
latest_version: latestVersion,
has_existing_version: latestVersion > 0
}));
res.json(entriesWithStatus);
});
});
});
// API: Woche abschicken
app.post('/api/timesheet/submit', requireAuth, (req, res) => {
const { week_start, week_end, version_reason } = req.body;
const userId = req.session.userId;
// Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind
db.all(`SELECT id, date, start_time, end_time, vacation_type, sick_status, overtime_taken_hours, updated_at FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date, updated_at DESC, id DESC`,
[userId, week_start, week_end],
(err, entries) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Prüfen der Daten' });
}
// Erstelle Set mit vorhandenen Daten
// WICHTIG: Wenn mehrere Einträge für denselben Tag existieren, nimm den neuesten
const entriesByDate = {};
entries.forEach(entry => {
const existing = entriesByDate[entry.date];
// Wenn noch kein Eintrag existiert oder dieser neuer ist, verwende ihn
if (!existing) {
entriesByDate[entry.date] = entry;
} else {
// Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer)
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;
}
}
});
// Prüfe nur so viele Tage wie Arbeitstage pro Woche festgelegt sind
// Samstag und Sonntag sind optional
// Bei ganztägigem Urlaub (vacation_type = 'full') ist der Tag als ausgefüllt zu betrachten
// Bei 8 Überstunden (ganzer Tag) ist der Tag auch als ausgefüllt zu betrachten
// week_start ist bereits im Format YYYY-MM-DD
const startDateParts = week_start.split('-');
const startYear = parseInt(startDateParts[0]);
const startMonth = parseInt(startDateParts[1]) - 1; // Monat ist 0-basiert
const startDay = parseInt(startDateParts[2]);
// User-Daten laden für Überstunden-Berechnung
db.get('SELECT wochenstunden, arbeitstage FROM users WHERE id = ?', [userId], (err, user) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' });
}
const wochenstunden = user?.wochenstunden || 0;
const arbeitstage = user?.arbeitstage || 5;
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8;
// Feiertage laden: Feiertag zählt als ausgefüllt (kein Start/Ende nötig)
getHolidaysForDateRange(week_start, week_end)
.catch(() => new Set())
.then((holidaySet) => {
let missingDays = [];
for (let i = 0; i < arbeitstage; i++) {
// Datum direkt berechnen ohne Zeitzonenprobleme
const date = new Date(startYear, startMonth, startDay + i);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
const entry = entriesByDate[dateStr];
// Feiertag zählt als ausgefüllt
if (holidaySet.has(dateStr)) {
continue; // Tag ist ausgefüllt
}
// Wenn ganztägiger Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten
const isSick = entry && (entry.sick_status === 1 || entry.sick_status === true);
if (entry && (entry.vacation_type === 'full' || isSick)) {
continue; // Tag ist ausgefüllt
}
// Prüfe ob Überstunden (ganzer Tag) eingetragen sind
const overtimeValue = entry && entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0;
const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01;
if (isFullDayOvertime) {
continue; // Tag ist ausgefüllt (Überstunden = ganzer Tag)
}
// Wenn Überstunden > fullDayHours, dann müssen Start/Ende vorhanden sein
if (overtimeValue > fullDayHours) {
const hasStartTime = entry && entry.start_time && entry.start_time.toString().trim() !== '';
const hasEndTime = entry && entry.end_time && entry.end_time.toString().trim() !== '';
if (!entry || !hasStartTime || !hasEndTime) {
missingDays.push(dateStr);
continue; // Weiter zum nächsten Tag
}
}
// Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein
// start_time und end_time könnten null, undefined oder leer strings sein
const hasStartTime = entry && entry.start_time && entry.start_time.toString().trim() !== '';
const hasEndTime = entry && entry.end_time && entry.end_time.toString().trim() !== '';
if (!entry || !hasStartTime || !hasEndTime) {
missingDays.push(dateStr);
}
}
if (missingDays.length > 0) {
const requiredDaysText = arbeitstage === 1 ? '1 Tag' : `${arbeitstage} Tage`;
return res.status(400).json({
error: `Nicht alle ${requiredDaysText} sind ausgefüllt. Fehlende Tage: ${missingDays.join(', ')}. Bitte füllen Sie alle ${requiredDaysText} mit Start- und Endzeit aus. Wochenende ist optional.`
});
}
// Alle Tage ausgefüllt - Woche abschicken (immer neue Version erstellen)
// Prüfe welche Version die letzte ist
db.get(`SELECT MAX(version) as max_version FROM weekly_timesheets
WHERE user_id = ? AND week_start = ? AND week_end = ?`,
[userId, week_start, week_end],
(err, result) => {
if (err) return res.status(500).json({ error: 'Fehler beim Prüfen der Version' });
const maxVersion = result && result.max_version ? result.max_version : 0;
const newVersion = maxVersion + 1;
// Wenn bereits eine Version existiert, ist version_reason erforderlich
if (maxVersion > 0 && (!version_reason || version_reason.trim() === '')) {
return res.status(400).json({
error: 'Bitte geben Sie einen Grund für die neue Version an.'
});
}
// Neue Version erstellen (nicht überschreiben)
db.run(`INSERT INTO weekly_timesheets (user_id, week_start, week_end, version, status, version_reason)
VALUES (?, ?, ?, ?, 'eingereicht', ?)`,
[userId, week_start, week_end, newVersion, version_reason ? version_reason.trim() : null],
function(err) {
if (err) return res.status(500).json({ error: 'Fehler beim Abschicken' });
const timesheetId = this.lastID;
if (!timesheetId) {
return res.status(500).json({ error: 'Fehler beim Ermitteln der Stundenzettel-ID' });
}
// Status der Einträge aktualisieren (optional - für Nachverfolgung)
db.run(`UPDATE timesheet_entries
SET status = 'eingereicht'
WHERE user_id = ? AND date >= ? AND date <= ?`,
[userId, week_start, week_end],
async (err) => {
if (err) return res.status(500).json({ error: 'Fehler beim Aktualisieren des Status' });
// PDF direkt beim Einreichen erzeugen und auf dem Dateisystem persistieren,
// damit der abgegebene Stand versionstreu festgehalten wird.
try {
const pdfBuffer = await generatePDFToBuffer(timesheetId, req);
const pdfLocation = resolvePdfLocationFromTimesheetRow({
id: timesheetId,
user_id: userId,
week_start,
version: newVersion,
firstname: req.session.firstname || '',
lastname: req.session.lastname || '',
});
await savePdfBufferAtomic(pdfLocation.absolutePath, pdfBuffer);
db.run(
'UPDATE weekly_timesheets SET pdf_path = ? WHERE id = ?',
[pdfLocation.relativePath, timesheetId],
(pathErr) => {
if (pathErr) {
console.error('Fehler beim Speichern des PDF-Pfads:', pathErr);
return res.status(500).json({ error: 'Fehler beim Speichern des PDF-Pfads' });
}
res.json({ success: true, version: newVersion });
}
);
} catch (pdfErr) {
console.error('Fehler beim direkten Generieren des PDFs nach Einreichung:', pdfErr);
return res.status(500).json({
error: 'Woche wurde eingereicht, aber PDF konnte nicht erzeugt werden. Bitte erneut versuchen.'
});
}
});
});
});
});
});
});
});
// API: Neueste eingereichte Version für eine Woche abrufen
app.get('/api/timesheet/latest-submitted/:weekStart', requireAuth, (req, res) => {
const userId = req.session.userId;
const weekStart = req.params.weekStart;
// Berechne Wochenende
const startDate = new Date(weekStart);
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 6);
const weekEnd = endDate.toISOString().split('T')[0];
// Hole die neueste eingereichte Version für diese Woche
db.get(`SELECT id, version, submitted_at FROM weekly_timesheets
WHERE user_id = ? AND week_start = ? AND week_end = ?
ORDER BY version DESC LIMIT 1`,
[userId, weekStart, weekEnd],
(err, result) => {
if (err) {
console.error('Fehler beim Abrufen der neuesten Version:', err);
return res.status(500).json({ error: 'Fehler beim Abrufen der Version' });
}
if (result) {
res.json({
timesheetId: result.id,
version: result.version,
submitted_at: result.submitted_at
});
} else {
res.json({ timesheetId: null, version: null, submitted_at: null });
}
});
});
// API: PDF Download-Info abrufen
app.get('/api/timesheet/download-info/:id', requireVerwaltung, (req, res) => {
const timesheetId = req.params.id;
db.get(`SELECT wt.pdf_downloaded_at,
dl.firstname as downloaded_by_firstname,
dl.lastname as downloaded_by_lastname
FROM weekly_timesheets wt
LEFT JOIN users dl ON wt.pdf_downloaded_by = dl.id
WHERE wt.id = ?`, [timesheetId], (err, result) => {
if (err) {
console.error('Fehler beim Abrufen der Download-Info:', err);
return res.status(500).json({ error: 'Fehler beim Abrufen der Informationen' });
}
if (!result) {
return res.status(404).json({ error: 'Stundenzettel nicht gefunden' });
}
res.json({
downloaded: !!result.pdf_downloaded_at,
downloaded_at: result.pdf_downloaded_at,
downloaded_by_firstname: result.downloaded_by_firstname,
downloaded_by_lastname: result.downloaded_by_lastname
});
});
});
// API: PDF generieren
app.get('/api/timesheet/pdf/:id', requireAuth, (req, res) => {
const timesheetId = req.params.id;
const userId = req.session.userId;
const isVerwaltung = hasRole(req, 'verwaltung') || hasRole(req, 'admin');
// Prüfe ob User Verwaltung/Admin ist oder ob das Timesheet dem User gehört
db.get(
`SELECT wt.id, wt.user_id, wt.week_start, wt.week_end, wt.version, wt.submitted_at, wt.pdf_path,
u.firstname, u.lastname
FROM weekly_timesheets wt
JOIN users u ON wt.user_id = u.id
WHERE wt.id = ?`,
[timesheetId],
async (err, timesheet) => {
if (err || !timesheet) {
return res.status(404).send('Stundenzettel nicht gefunden');
}
// Zugriff erlauben wenn Verwaltung/Admin ODER wenn Timesheet dem User gehört
if (!(isVerwaltung || timesheet.user_id === userId)) {
return res.status(403).send('Zugriff verweigert');
}
const inline = req.query.inline === 'true';
// Zielpfad bestimmen: DB-Pfad bevorzugen, sonst berechnen
const computedLocation = resolvePdfLocationFromTimesheetRow(timesheet);
const relativePath = timesheet.pdf_path ? String(timesheet.pdf_path) : computedLocation.relativePath;
const absolutePath = timesheet.pdf_path ? resolveAbsolutePathFromRelative(relativePath) : computedLocation.absolutePath;
const filename = computedLocation.filename;
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('X-Content-Type-Options', 'nosniff');
if (inline) {
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
} else {
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
// Marker setzen, dass PDF heruntergeladen wurde (nur bei Download, nicht bei Vorschau)
const downloadedBy = req.session.userId;
if (downloadedBy) {
db.run(
`UPDATE weekly_timesheets
SET pdf_downloaded_at = CURRENT_TIMESTAMP,
pdf_downloaded_by = ?
WHERE id = ?`,
[downloadedBy, timesheetId],
(updateErr) => {
if (updateErr) {
console.error('Fehler beim Setzen des Download-Markers:', updateErr);
}
}
);
}
}
try {
const exists = await fileExists(absolutePath);
if (exists) {
const stream = fs.createReadStream(absolutePath);
stream.on('error', (streamErr) => {
console.error('Fehler beim Lesen der PDF-Datei:', streamErr);
if (!res.headersSent) res.status(500);
res.end('Fehler beim Lesen der PDF-Datei');
});
return stream.pipe(res);
}
// Nicht vorhanden: versionstreu generieren, speichern, dann senden
const pdfBuffer = await generatePDFToBuffer(timesheetId, req);
await savePdfBufferAtomic(absolutePath, pdfBuffer);
// Relativen Pfad in DB speichern (optional, Fehler nicht fatal)
if (!timesheet.pdf_path) {
db.run('UPDATE weekly_timesheets SET pdf_path = ? WHERE id = ?', [relativePath, timesheetId], (pathErr) => {
if (pathErr) {
console.warn('Warnung beim Speichern des PDF-Pfads:', pathErr.message);
}
});
}
return res.end(pdfBuffer);
} catch (e) {
console.error('Fehler beim Generieren/Speichern der PDF:', e);
if (!res.headersSent) res.status(500);
return res.end('Fehler beim Erstellen des PDFs');
}
}
);
});
}
module.exports = registerTimesheetRoutes;