From 063fb68b12aac76e2ae64fea334d84396ea916dd Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Wed, 4 Feb 2026 19:03:42 +0100 Subject: [PATCH] To-DO abgearbeitet --- Stunderfassung todo.txt | 10 +-- helpers/utils.js | 18 +++- public/js/dashboard.js | 35 +++++--- routes/timesheet-routes.js | 9 +- routes/user-routes.js | 177 +++++++++++++++++++++++-------------- 5 files changed, 158 insertions(+), 91 deletions(-) diff --git a/Stunderfassung todo.txt b/Stunderfassung todo.txt index dd3e983..77d24c5 100644 --- a/Stunderfassung todo.txt +++ b/Stunderfassung todo.txt @@ -10,8 +10,8 @@ - Wenn bereits heruntergeladen wurde und neue version da ist Meldung an Verwaltung. -> DONE Muss getestet werden - Wenn ganzer Tag Urlaub gesetzt wird steht erst 8h (Urlaub) und dann nur noch 8h -- Feiertage im PDF anzeigen -> DONE Testen noch nicht depoyed -- Oben wenn woche eingereicht anzeigen als hilfestellung -- Ausgefüllte Tage anhand der Tage pro woche gültig setzten -- Überstunden müssen anhand der Tagesstunden auch auf gültig setzten (Tag ausgefüllt wenn weniger als 8h) -- Verplante Urlaubstage müssen auf abgezogen werden, wenn die Woche die gepalnt war eingereicht wurde. \ No newline at end of file +- Feiertage im PDF anzeigen -> DONE Testen noch nicht depoyed +- Oben wenn woche eingereicht anzeigen als hilfestellung -> DONE +- Ausgefüllte Tage anhand der Tage pro woche gültig setzten -> DONE Testen +- Überstunden müssen anhand der Tagesstunden auch auf gültig setzten (Tag ausgefüllt wenn weniger als 8h) -> DONE sollte passen +- Verplante Urlaubstage müssen auf abgezogen werden, wenn die Woche die gepalnt war eingereicht wurde. -> DONE Testen \ No newline at end of file diff --git a/helpers/utils.js b/helpers/utils.js index f3bfb61..62ea7f0 100644 --- a/helpers/utils.js +++ b/helpers/utils.js @@ -117,6 +117,21 @@ function getWeekDatesFromCalendarWeek(year, weekNumber) { }; } +// Helper: Berechnet week_start (Montag) aus einem Datum +function getWeekStart(dateStr) { + const date = new Date(dateStr); + const day = date.getDay(); // 0 = Sonntag, 1 = Montag, ..., 6 = Samstag + const diff = day === 0 ? -6 : 1 - day; // Montag = 1, Sonntag = 0 + const monday = new Date(date); + monday.setDate(date.getDate() + diff); + + // Format: YYYY-MM-DD + const year = monday.getFullYear(); + const month = String(monday.getMonth() + 1).padStart(2, '0'); + const dayOfMonth = String(monday.getDate()).padStart(2, '0'); + return `${year}-${month}-${dayOfMonth}`; +} + module.exports = { hasRole, getDefaultRole, @@ -127,5 +142,6 @@ module.exports = { formatDate, formatDateTime, getCalendarWeek, - getWeekDatesFromCalendarWeek + getWeekDatesFromCalendarWeek, + getWeekStart }; diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 06d6f2e..ef4697f 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -374,8 +374,11 @@ function renderWeek() { endDate.setDate(endDate.getDate() + 6); const calendarWeek = getCalendarWeek(currentWeekStart); + const submittedText = currentEntries._weekSubmitted === true + ? `
(bereits Abgegeben)` + : ''; document.getElementById('weekTitle').innerHTML = - `Kalenderwoche ${calendarWeek}
${formatDateDE(currentWeekStart)} - ${formatDateDE(formatDate(endDate))}`; + `Kalenderwoche ${calendarWeek}
${formatDateDE(currentWeekStart)} - ${formatDateDE(formatDate(endDate))}${submittedText}`; let html = `
@@ -1410,9 +1413,10 @@ function checkWeekComplete() { let allWeekdaysFilled = true; const missingFields = []; - // Prüfe nur Werktage (Montag-Freitag, i < 5) - // Samstag und Sonntag (i >= 5) sind optional - for (let i = 0; i < 5; i++) { + // Prüfe nur so viele Tage wie Arbeitstage pro Woche festgelegt sind + // Samstag und Sonntag sind optional + const requiredDays = userArbeitstage || 5; // Fallback auf 5 wenn nicht gesetzt + for (let i = 0; i < requiredDays; i++) { const date = new Date(startDate); date.setDate(date.getDate() + i); const dateStr = formatDate(date); @@ -1482,16 +1486,19 @@ function checkWeekComplete() { } } - // Prüfe ob die Woche bereits eingereicht wurde (nicht der Status einzelner Einträge!) + // Prüfe ob die Woche bereits eingereicht wurde (für Anzeige, aber Button bleibt aktiv für neue Versionen) const weekIsSubmitted = currentEntries._weekSubmitted === true; const submitButton = document.getElementById('submitWeek'); if (submitButton) { - submitButton.disabled = weekIsSubmitted || !allWeekdaysFilled; + // Button nur deaktivieren wenn nicht alle Felder ausgefüllt sind + // Resubmission ist erlaubt, da Versionierung unterstützt wird + submitButton.disabled = !allWeekdaysFilled; - if (weekIsSubmitted) { - submitButton.title = 'Diese Woche wurde bereits eingereicht und kann nicht mehr geändert werden.'; - } else if (!allWeekdaysFilled) { - submitButton.title = `Bitte füllen Sie alle Werktage (Montag bis Freitag) aus (Start- und Endzeit). Wochenende ist optional. Fehlend: ${missingFields.join(', ')}`; + if (!allWeekdaysFilled) { + const requiredDaysText = requiredDays === 1 ? '1 Tag' : `${requiredDays} Tage`; + submitButton.title = `Bitte füllen Sie alle ${requiredDaysText} (Start- und Endzeit) aus. Wochenende ist optional. Fehlend: ${missingFields.join(', ')}`; + } else if (weekIsSubmitted) { + submitButton.title = 'Diese Woche wurde bereits eingereicht. Beim erneuten Abschicken wird eine neue Version erstellt.'; } else { submitButton.title = ''; } @@ -1545,7 +1552,7 @@ async function submitWeek() { console.log('Prüfe Validierung für Woche:', currentWeekStart); - // Frontend-Validierung: Prüfen ob alle Werktage (Montag-Freitag) ausgefüllt sind + // Frontend-Validierung: Prüfen ob so viele Tage ausgefüllt sind wie Arbeitstage pro Woche festgelegt sind let missingFields = []; let firstMissingInput = null; @@ -1556,7 +1563,8 @@ async function submitWeek() { el.style.backgroundColor = ''; }); - for (let i = 0; i < 5; i++) { + const requiredDays = userArbeitstage || 5; // Fallback auf 5 wenn nicht gesetzt + for (let i = 0; i < requiredDays; i++) { const date = new Date(startDate); date.setDate(date.getDate() + i); const dateStr = formatDate(date); @@ -1644,7 +1652,8 @@ async function submitWeek() { } // Detaillierte Fehlermeldung - const message = `❌ Bitte füllen Sie alle Werktage (Montag bis Freitag) vollständig aus!\n\n` + + const requiredDaysText = requiredDays === 1 ? '1 Tag' : `${requiredDays} Tage`; + const message = `❌ Bitte füllen Sie alle ${requiredDaysText} vollständig aus!\n\n` + `Fehlende Eingaben:\n${missingFields.map((field, index) => `\n${index + 1}. ${field}`).join('')}\n\n` + `Die fehlenden Felder wurden rot markiert.\n` + `Hinweis: Samstag und Sonntag sind optional.`; diff --git a/routes/timesheet-routes.js b/routes/timesheet-routes.js index 490bc06..5b7125d 100644 --- a/routes/timesheet-routes.js +++ b/routes/timesheet-routes.js @@ -262,7 +262,7 @@ function registerTimesheetRoutes(app) { // Füge Status-Info hinzu (Bearbeitung ist immer möglich) const entriesWithStatus = (entries || []).map(entry => ({ ...entry, - week_submitted: false, // Immer false, damit Bearbeitung möglich ist + week_submitted: hasSubmittedVersion, // Woche wurde eingereicht wenn weekly_timesheet existiert latest_version: latestVersion, has_existing_version: latestVersion > 0 })); @@ -304,7 +304,7 @@ function registerTimesheetRoutes(app) { } }); - // Prüfe nur Werktage (Montag-Freitag, erste 5 Tage) + // 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 @@ -330,7 +330,7 @@ function registerTimesheetRoutes(app) { .then((holidaySet) => { let missingDays = []; - for (let i = 0; i < 5; i++) { + for (let i = 0; i < arbeitstage; i++) { // Datum direkt berechnen ohne Zeitzonenprobleme const date = new Date(startYear, startMonth, startDay + i); const year = date.getFullYear(); @@ -380,8 +380,9 @@ function registerTimesheetRoutes(app) { } if (missingDays.length > 0) { + const requiredDaysText = arbeitstage === 1 ? '1 Tag' : `${arbeitstage} Tage`; return res.status(400).json({ - error: `Nicht alle Werktage (Montag bis Freitag) sind ausgefüllt. Fehlende Tage: ${missingDays.join(', ')}. Bitte füllen Sie alle Werktage mit Start- und Endzeit aus. Wochenende ist optional.` + 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.` }); } diff --git a/routes/user-routes.js b/routes/user-routes.js index 7635fbd..a955f26 100644 --- a/routes/user-routes.js +++ b/routes/user-routes.js @@ -154,48 +154,73 @@ function registerUserRoutes(app) { res.json({ success: true, currentRole: role }); }); - // API: Verplante Urlaubstage (alle Wochen, auch nicht-eingereichte) + // API: Verplante Urlaubstage (nur nicht-eingereichte Wochen) app.get('/api/user/planned-vacation', requireAuth, (req, res) => { const userId = req.session.userId; - const { getCalendarWeek } = require('../helpers/utils'); + const { getCalendarWeek, getWeekStart } = require('../helpers/utils'); - db.all(`SELECT date, vacation_type FROM timesheet_entries - WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, + // Zuerst alle eingereichten Wochen abrufen + db.all(`SELECT DISTINCT week_start FROM weekly_timesheets + WHERE user_id = ? AND status = 'eingereicht'`, [userId], - (err, entries) => { + (err, submittedWeeks) => { if (err) { - return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); + return res.status(500).json({ error: 'Fehler beim Abrufen der eingereichten Wochen' }); } - let plannedDays = 0; - const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } + // Set für schnelle Suche nach eingereichten Wochen + const submittedWeekStarts = new Set( + (submittedWeeks || []).map(w => w.week_start) + ); - entries.forEach(entry => { - const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; - plannedDays += dayValue; + // Alle Urlaubseinträge abrufen + db.all(`SELECT date, vacation_type FROM timesheet_entries + WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, + [userId], + (err, entries) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); + } - // Berechne Kalenderwoche - const date = new Date(entry.date); - const year = date.getFullYear(); - const week = getCalendarWeek(entry.date); - const weekKey = `${year}-KW${week}`; + let plannedDays = 0; + const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } - if (!weeksMap[weekKey]) { - weeksMap[weekKey] = { year, week, days: 0 }; + entries.forEach(entry => { + // Berechne week_start für diesen Eintrag + const weekStart = getWeekStart(entry.date); + + // Überspringe Einträge aus eingereichten Wochen + if (submittedWeekStarts.has(weekStart)) { + return; + } + + const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; + plannedDays += dayValue; + + // Berechne Kalenderwoche + const date = new Date(entry.date); + const year = date.getFullYear(); + const week = getCalendarWeek(entry.date); + const weekKey = `${year}-KW${week}`; + + if (!weeksMap[weekKey]) { + weeksMap[weekKey] = { year, week, days: 0 }; + } + weeksMap[weekKey].days += dayValue; + }); + + // Konvertiere zu sortiertem Array + const weeks = Object.values(weeksMap).sort((a, b) => { + if (a.year !== b.year) return a.year - b.year; + return a.week - b.week; + }); + + res.json({ + plannedVacationDays: plannedDays, + weeks: weeks + }); } - weeksMap[weekKey].days += dayValue; - }); - - // Konvertiere zu sortiertem Array - const weeks = Object.values(weeksMap).sort((a, b) => { - if (a.year !== b.year) return a.year - b.year; - return a.week - b.week; - }); - - res.json({ - plannedVacationDays: plannedDays, - weeks: weeks - }); + ); } ); }); @@ -237,52 +262,68 @@ function registerUserRoutes(app) { const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; const vacationOffsetDays = user.vacation_offset_days ? parseFloat(user.vacation_offset_days) : 0; - // Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte) - const { getCalendarWeek } = require('../helpers/utils'); - db.all(`SELECT date, vacation_type FROM timesheet_entries - WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, + // Verplante Urlaubstage berechnen (nur nicht-eingereichte Wochen) + const { getCalendarWeek, getWeekStart } = require('../helpers/utils'); + + // Zuerst alle eingereichten Wochen abrufen + db.all(`SELECT DISTINCT week_start, week_end + FROM weekly_timesheets + WHERE user_id = ? AND status = 'eingereicht' + ORDER BY week_start`, [userId], - (err, allVacationEntries) => { + (err, weeks) => { if (err) { - return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); + return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' }); } - let plannedVacationDays = 0; - const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } + // Set für schnelle Suche nach eingereichten Wochen + const submittedWeekStarts = new Set( + (weeks || []).map(w => w.week_start) + ); - (allVacationEntries || []).forEach(entry => { - const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; - plannedVacationDays += dayValue; - - // Berechne Kalenderwoche - const date = new Date(entry.date); - const year = date.getFullYear(); - const week = getCalendarWeek(entry.date); - const weekKey = `${year}-KW${week}`; - - if (!weeksMap[weekKey]) { - weeksMap[weekKey] = { year, week, days: 0 }; - } - weeksMap[weekKey].days += dayValue; - }); - - // Konvertiere zu sortiertem Array - const plannedWeeks = Object.values(weeksMap).sort((a, b) => { - if (a.year !== b.year) return a.year - b.year; - return a.week - b.week; - }); - - // Alle eingereichten Wochen abrufen - db.all(`SELECT DISTINCT week_start, week_end - FROM weekly_timesheets - WHERE user_id = ? AND status = 'eingereicht' - ORDER BY week_start`, + // Alle Urlaubseinträge abrufen + db.all(`SELECT date, vacation_type FROM timesheet_entries + WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, [userId], - (err, weeks) => { + (err, allVacationEntries) => { if (err) { - return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' }); + return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); } + let plannedVacationDays = 0; + const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } + + (allVacationEntries || []).forEach(entry => { + // Berechne week_start für diesen Eintrag + const weekStart = getWeekStart(entry.date); + + // Überspringe Einträge aus eingereichten Wochen + if (submittedWeekStarts.has(weekStart)) { + return; + } + + const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; + plannedVacationDays += dayValue; + + // Berechne Kalenderwoche + const date = new Date(entry.date); + const year = date.getFullYear(); + const week = getCalendarWeek(entry.date); + const weekKey = `${year}-KW${week}`; + + if (!weeksMap[weekKey]) { + weeksMap[weekKey] = { year, week, days: 0 }; + } + weeksMap[weekKey].days += dayValue; + }); + + // Konvertiere zu sortiertem Array + const plannedWeeks = Object.values(weeksMap).sort((a, b) => { + if (a.year !== b.year) return a.year - b.year; + return a.week - b.week; + }); + + // Weiter mit der Verarbeitung der eingereichten Wochen (weeks ist bereits verfügbar) // Wenn keine Wochen vorhanden if (!weeks || weeks.length === 0) { return res.json({