Verwaltung: Backend auf Lazyloading der Kalenderwochen umgestellt

This commit is contained in:
2026-03-12 18:24:37 +01:00
parent 8595b099db
commit 91603f1617
3 changed files with 544 additions and 380 deletions

View File

@@ -44,7 +44,7 @@ function registerVerwaltungRoutes(app) {
}
// Gruppiere nach Mitarbeiter, dann nach Kalenderwoche
// Struktur: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } }
// Struktur intern: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } }
const groupedByEmployee = {};
(timesheets || []).forEach(ts => {
@@ -86,7 +86,6 @@ function registerVerwaltungRoutes(app) {
// Prüfe für jede Woche, ob nach dem letzten Download eine neue Version eingereicht wurde
Object.values(groupedByEmployee).forEach(employee => {
Object.values(employee.weeks).forEach(week => {
// Finde die neueste Version mit pdf_downloaded_at (letzter Download)
let lastDownloadTime = null;
week.versions.forEach(version => {
if (version.pdf_downloaded_at) {
@@ -96,8 +95,7 @@ function registerVerwaltungRoutes(app) {
}
}
});
// Prüfe, ob es eine Version gibt, die nach dem letzten Download eingereicht wurde
let hasNewVersionAfterDownload = false;
if (lastDownloadTime) {
week.versions.forEach(version => {
@@ -109,26 +107,29 @@ function registerVerwaltungRoutes(app) {
}
});
}
// Setze Flag auf dem week-Objekt
week.has_new_version_after_download = hasNewVersionAfterDownload;
});
});
// Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst)
const sortedEmployees = Object.values(groupedByEmployee).map(employee => {
// Wochen innerhalb jedes Mitarbeiters sortieren
// Für das Initial-Rendering der Verwaltung werden nur Mitarbeiter-Header-Daten
// benötigt. Wochen-/Versionslisten werden per AJAX nachgeladen.
const employeesForView = Object.values(groupedByEmployee).map(employee => {
const sortedWeeks = Object.values(employee.weeks).sort((a, b) => {
return new Date(b.week_start) - new Date(a.week_start);
});
// Flag: Gibt es in irgendeiner Woche eine neue Version nach Download?
const hasNewVersionAfterDownload = sortedWeeks.some(w => w.has_new_version_after_download);
const weekCount = sortedWeeks.length;
const latestWeek = weekCount > 0 ? sortedWeeks[0] : null;
return {
...employee,
user: employee.user,
has_new_version_after_download: hasNewVersionAfterDownload,
weeks: sortedWeeks
weekCount,
latest_week_start: latestWeek ? latestWeek.week_start : null,
latest_week_end: latestWeek ? latestWeek.week_end : null
};
}).sort((a, b) => {
// Mitarbeiter nach Nachname, dann Vorname sortieren
@@ -138,7 +139,7 @@ function registerVerwaltungRoutes(app) {
});
res.render('verwaltung', {
groupedByEmployee: sortedEmployees,
groupedByEmployee: employeesForView,
user: {
firstname: req.session.firstname,
lastname: req.session.lastname,
@@ -149,6 +150,122 @@ function registerVerwaltungRoutes(app) {
});
});
// API: Wochen + Versionen für einen Mitarbeiter (für Lazy-Loading in der Verwaltung)
app.get('/api/verwaltung/employee/:id/weeks', requireVerwaltung, (req, res) => {
const userId = parseInt(req.params.id, 10);
if (!Number.isFinite(userId)) {
return res.status(400).send('Ungültige User-ID');
}
db.all(`
SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, u.overtime_offset_hours, u.vacation_offset_days, u.arbeitstage,
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'
AND wt.user_id = ?
ORDER BY wt.week_start DESC, wt.user_id, wt.version DESC
`, [userId], (err, timesheets) => {
if (err) {
console.error('Fehler beim Laden der Stundenzettel (Verwaltung-API /employee/:id/weeks):', err);
return res.status(500).send('Fehler beim Laden der Verwaltungsdaten.');
}
if (!timesheets || timesheets.length === 0) {
// Kein Inhalt, aber 200, damit das Frontend eine leere Anzeige darstellen kann
return res.send('');
}
// Gruppierung wie in /verwaltung, aber nur für einen Mitarbeiter
const groupedByEmployee = {};
timesheets.forEach(ts => {
const uid = ts.user_id;
const weekKey = `${ts.week_start}_${ts.week_end}`;
if (!groupedByEmployee[uid]) {
groupedByEmployee[uid] = {
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,
vacation_offset_days: ts.vacation_offset_days
},
weeks: {}
};
}
if (!groupedByEmployee[uid].weeks[weekKey]) {
groupedByEmployee[uid].weeks[weekKey] = {
week_start: ts.week_start,
week_end: ts.week_end,
total_versions: ts.total_versions,
versions: []
};
}
groupedByEmployee[uid].weeks[weekKey].versions.push(ts);
});
const employee = Object.values(groupedByEmployee)[0];
if (!employee) {
return res.send('');
}
// Prüfe für jede Woche, ob nach dem letzten Download eine neue Version eingereicht wurde
Object.values(employee.weeks).forEach(week => {
let lastDownloadTime = null;
week.versions.forEach(version => {
if (version.pdf_downloaded_at) {
const downloadTime = new Date(version.pdf_downloaded_at).getTime();
if (!lastDownloadTime || downloadTime > lastDownloadTime) {
lastDownloadTime = downloadTime;
}
}
});
let hasNewVersionAfterDownload = false;
if (lastDownloadTime) {
week.versions.forEach(version => {
if (version.submitted_at) {
const submittedTime = new Date(version.submitted_at).getTime();
if (submittedTime > lastDownloadTime) {
hasNewVersionAfterDownload = true;
}
}
});
}
week.has_new_version_after_download = hasNewVersionAfterDownload;
});
const weeks = Object.values(employee.weeks).sort((a, b) => {
return new Date(b.week_start) - new Date(a.week_start);
});
const viewUser = {
firstname: req.session.firstname,
lastname: req.session.lastname
};
res.render('verwaltung-weeks-partial', {
employee: { user: employee.user, weeks },
user: viewUser
});
});
});
// Plausibilitätsprüfung Projektnummer: 7 Ziffern, beginnt mit 5, dann YY, dann 4 Ziffern
function isValidProjectNumber(value) {
if (!value || String(value).trim() === '') return false;