diff --git a/checkin-server.js b/checkin-server.js index fddc806..2a4b9d4 100644 --- a/checkin-server.js +++ b/checkin-server.js @@ -14,6 +14,7 @@ checkinApp.set('views', path.join(__dirname, 'views')); // Middleware für Check-in-Server checkinApp.use(express.json()); +checkinApp.use(express.static('public')); /** Erkennt Browser-Aufruf: Accept-Header enthält text/html (z. B. beim Aufruf per QR/Link im Browser). */ function wantsHtml(req) { diff --git a/doc/Stunderfassung todo.txt b/doc/Stunderfassung todo.txt index 64f102d..bc6d3bd 100644 --- a/doc/Stunderfassung todo.txt +++ b/doc/Stunderfassung todo.txt @@ -3,7 +3,7 @@ - Offset für die Verwaltung für Urlaubstage -> DONE - Stunden pro Tag und wie viele Tage arbeit -> DONE - Reisen für Wochenende -> DONE -- LDAP Prüfung -> DONE GEHT?! +- LDAP Prüfung -> DONE - DSGVO Sicherheit -> DONE - Feiertage müssen als ausgefüllt zählen -> DONE - Mitarbeiter sollen PDF ansehen können. -> DONE @@ -12,6 +12,6 @@ - Feiertage im PDF anzeigen -> DONE - Oben wenn woche eingereicht anzeigen als hilfestellung -> DONE -- Ausgefüllte Tage anhand der Tage pro woche gültig setzten -> DONE Testen +- Ausgefüllte Tage anhand der Tage pro woche gültig setzten -> DONE - Ü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 +- Verplante Urlaubstage müssen auf abgezogen werden, wenn die Woche die gepalnt war eingereicht wurde. -> DONE \ No newline at end of file diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..399db4f Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/css/style.css b/public/css/style.css index bb4abe3..d44ae62 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1072,4 +1072,13 @@ table input[type="text"] { flex-direction: column; align-items: flex-start; } +} + +/* App Footer */ +.app-footer { + text-align: center; + padding: 16px; + color: #888; + font-size: 13px; + margin-top: 24px; } \ No newline at end of file diff --git a/public/images/favicon.png b/public/images/icons/favicon.png similarity index 100% rename from public/images/favicon.png rename to public/images/icons/favicon.png diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 6d5e6bf..3ad3cdb 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -440,12 +440,17 @@ function renderWeek() { // Stunden zur Summe hinzufügen // Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt) - // Feiertag: 8h Basis + gearbeitete Stunden (jede gearbeitete Stunde = Überstunde) + // Feiertag Werktag: 8h Basis + gearbeitete Stunden (jede gearbeitete Stunde = Überstunde) + // Feiertag am Wochenende: keine Tagesarbeitsstunden, nur gearbeitete Stunden (z. B. Reise) // Bei halbem Tag Urlaub werden die Urlaubsstunden später in der Überstunden-Berechnung hinzugezählt // Wochenend-Prozentsätze: Nur auf tatsächlich gearbeitete Stunden anwenden (nicht auf Urlaub, Krankheit, Feiertage) let hoursToAdd = 0; if (isHoliday) { - hoursToAdd = fullDayHours + (hours || 0); // (Wochenarbeitszeit / Arbeitstage) Feiertag + gearbeitete Stunden (= Überstunden) + if (isWeekend) { + hoursToAdd = hours || 0; // Feiertag am Wochenende: keine Tagesarbeitsstunden + } else { + hoursToAdd = fullDayHours + (hours || 0); // (Wochenarbeitszeit / Arbeitstage) Feiertag + gearbeitete Stunden (= Überstunden) + } } else { hoursToAdd = hours || 0; // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) @@ -488,6 +493,9 @@ function renderWeek() { } } else if (isSick) { hoursDisplay = fullDayHours.toFixed(2) + ' h (Krank)'; + } else if (isHoliday && isWeekend) { + // Feiertag am Wochenende: keine Tagesarbeitsstunden + hoursDisplay = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)'; } else if (isHoliday && !hours) { hoursDisplay = fullDayHours.toFixed(2) + ' h (Feiertag)'; } else if (isHoliday && hours) { @@ -818,7 +826,9 @@ function updateOvertimeDisplay() { } else if (sickStatus) { totalHours += fullDayHours; // Krank = (Wochenarbeitszeit / Arbeitstage) Stunden } else if (currentHolidayDates.has(dateStr)) { - // Feiertag: (Wochenarbeitszeit / Arbeitstage) Basis + gearbeitete Stunden (jede Stunde = Überstunde) + // Feiertag: Werktag = Basis + gearbeitete Stunden; Wochenende = nur gearbeitete Stunden (keine Tagesarbeitsstunden) + const dayOfWeek = date.getDay(); + const isWeekendHoliday = (dayOfWeek === 6 || dayOfWeek === 0); const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); const startTime = startInput ? startInput.value : ''; @@ -833,7 +843,11 @@ function updateOvertimeDisplay() { } else if (currentEntries[dateStr]?.total_hours) { worked = parseFloat(currentEntries[dateStr].total_hours) || 0; } - totalHours += fullDayHours + worked; // (Wochenarbeitszeit / Arbeitstage) Feiertag + gearbeitete Stunden (= Überstunden) + if (isWeekendHoliday) { + totalHours += worked; // Feiertag am Wochenende: keine Tagesarbeitsstunden + } else { + totalHours += fullDayHours + worked; // (Wochenarbeitszeit / Arbeitstage) Feiertag + gearbeitete Stunden (= Überstunden) + } } else { // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden if (isFullDayOvertime) { @@ -922,7 +936,8 @@ function updateOvertimeDisplay() { const totalHoursWithVacation = totalHours + vacationHours; const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); // overtimeHours = Überstunden diese Woche (wie im Backend berechnet) - const overtimeHours = totalHoursWithVacation - adjustedSollStunden; + // Genommene Überstunden werden abgezogen, um die Netto-Überstunden zu erhalten + const overtimeHours = totalHoursWithVacation - adjustedSollStunden - overtimeTaken; // Überstunden-Anzeige aktualisieren const overtimeSummaryItem = document.getElementById('overtimeSummaryItem'); @@ -1206,8 +1221,12 @@ async function saveEntry(input) { currentEntries[date].total_hours = totalHours; } else { // Zurück zu normaler Anzeige basierend auf anderen Status + const d = new Date(date); + const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0); if (isSick) { hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Krank)'; + } else if (isWeekendHoliday) { + hoursElement.textContent = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)'; } else if (isHoliday && !hours) { hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)'; } else if (isHoliday && hours) { @@ -1369,11 +1388,17 @@ async function saveEntry(input) { currentEntries[date].total_hours = totalHours; } else if (isSick) { hoursText = fullDayHours.toFixed(2) + ' h (Krank)'; - } else if (isHoliday && result.total_hours <= fullDayHours) { - hoursText = fullDayHours.toFixed(2) + ' h (Feiertag)'; - } else if (isHoliday && result.total_hours > fullDayHours) { - const overtime = result.total_hours - fullDayHours; - hoursText = fullDayHours.toFixed(2) + ' + ' + overtime.toFixed(2) + ' h (Überst.)'; + } else if (isHoliday) { + const d = new Date(date); + const isWeekendHoliday = (d.getDay() === 6 || d.getDay() === 0); + if (isWeekendHoliday) { + hoursText = (result.total_hours || 0).toFixed(2) + ' h (Feiertag)'; + } else if (result.total_hours <= fullDayHours) { + hoursText = fullDayHours.toFixed(2) + ' h (Feiertag)'; + } else { + const overtime = result.total_hours - fullDayHours; + hoursText = fullDayHours.toFixed(2) + ' + ' + overtime.toFixed(2) + ' h (Überst.)'; + } } hoursElement.textContent = hoursText; @@ -1973,8 +1998,12 @@ function toggleSickStatus(dateStr) { currentEntries[dateStr].total_hours = fullDayHours; } else { // Zurück zu normaler Anzeige basierend auf anderen Status + const d = new Date(dateStr); + const isWeekendHoliday = isHoliday && (d.getDay() === 6 || d.getDay() === 0); if (isFullDayVacation) { hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Urlaub)'; + } else if (isWeekendHoliday) { + hoursElement.textContent = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)'; } else if (isHoliday && !hours) { hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)'; } else if (isHoliday && hours) { diff --git a/public/manifest.json b/public/manifest.json index 5dd6ba7..458952a 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -7,12 +7,12 @@ "theme_color": "#0a5ea8", "icons": [ { - "src": "/public/images/icons/icon-192x192.png", + "src": "/images/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/public/images/icons/icon-512x512.png", + "src": "/images/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" } diff --git a/routes/verwaltung-routes.js b/routes/verwaltung-routes.js index 0a53a7d..670fd93 100644 --- a/routes/verwaltung-routes.js +++ b/routes/verwaltung-routes.js @@ -300,17 +300,33 @@ function registerVerwaltungRoutes(app) { } function processCurrentWeek(totalVacationDays) { - // Einträge für die Woche abrufen - db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type, sick_status + // Einträge für die Woche abrufen (id/updated_at für neuesten pro Tag) + db.all(`SELECT id, date, updated_at, total_hours, overtime_taken_hours, vacation_type, sick_status FROM timesheet_entries WHERE user_id = ? AND date >= ? AND date <= ? - ORDER BY date`, + ORDER BY date, updated_at DESC, id DESC`, [userId, week_start, week_end], - (err, entries) => { + (err, allEntries) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); } + // Nur neuesten Eintrag pro Tag zählen (wie PDF/Submit), sonst Doppelzählung bei Duplikaten + const entriesByDate = {}; + (allEntries || []).forEach(entry => { + const existing = entriesByDate[entry.date]; + if (!existing) { + entriesByDate[entry.date] = entry; + } else { + 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; + } + } + }); + const entries = Object.values(entriesByDate); + // Berechnungen let totalHours = 0; let overtimeTaken = 0; @@ -320,7 +336,7 @@ function registerVerwaltungRoutes(app) { entries.forEach(entry => { if (entry.overtime_taken_hours) { - overtimeTaken += entry.overtime_taken_hours; + overtimeTaken += parseFloat(entry.overtime_taken_hours) || 0; } // Krankheitstage zählen diff --git a/services/pdf-service.js b/services/pdf-service.js index 2383538..ca12c6e 100644 --- a/services/pdf-service.js +++ b/services/pdf-service.js @@ -187,7 +187,11 @@ function generatePDF(timesheetId, req, res) { if (row.type === 'holiday') { y = doc.y; x = 50; - const rowData = [formatDate(row.date), '-', '-', '-', fullDayHours.toFixed(2) + ' h (Feiertag)']; + // Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen + const holidayDay = new Date(row.date + 'T12:00:00').getDay(); + const isWeekendHoliday = (holidayDay === 0 || holidayDay === 6); + const holidayHoursStr = isWeekendHoliday ? '0 h (Feiertag)' : fullDayHours.toFixed(2) + ' h (Feiertag)'; + const rowData = [formatDate(row.date), '-', '-', '-', holidayHoursStr]; rowData.forEach((data, i) => { doc.text(data, x, y, { width: colWidths[i], align: 'left' }); x += colWidths[i]; @@ -455,7 +459,11 @@ function generatePDFToBuffer(timesheetId, req) { if (row.type === 'holiday') { y = doc.y; x = 50; - const rowDataBuf = [formatDate(row.date), '-', '-', '-', fullDayHoursBuf.toFixed(2) + ' h (Feiertag)']; + // Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen + const holidayDay = new Date(row.date + 'T12:00:00').getDay(); + const isWeekendHoliday = (holidayDay === 0 || holidayDay === 6); + const holidayHoursStr = isWeekendHoliday ? '0 h (Feiertag)' : fullDayHoursBuf.toFixed(2) + ' h (Feiertag)'; + const rowDataBuf = [formatDate(row.date), '-', '-', '-', holidayHoursStr]; rowDataBuf.forEach((data, i) => { doc.text(data, x, y, { width: colWidths[i], align: 'left' }); x += colWidths[i]; diff --git a/views/admin.ejs b/views/admin.ejs index f85bb4f..2a1761b 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -484,5 +484,6 @@ } }); + <%- include('footer') %> diff --git a/views/checkin-result.ejs b/views/checkin-result.ejs index 1e0ae43..5ac5a82 100644 --- a/views/checkin-result.ejs +++ b/views/checkin-result.ejs @@ -53,5 +53,6 @@

<%= title %>

<%= message %>

+ <%- include('footer') %> diff --git a/views/dashboard.ejs b/views/dashboard.ejs index b3d716b..d2cb997 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -507,5 +507,6 @@ } } + <%- include('footer') %> diff --git a/views/footer.ejs b/views/footer.ejs new file mode 100644 index 0000000..ec8afa7 --- /dev/null +++ b/views/footer.ejs @@ -0,0 +1,3 @@ + diff --git a/views/header.ejs b/views/header.ejs index 28b2ece..0213b6d 100644 --- a/views/header.ejs +++ b/views/header.ejs @@ -2,14 +2,14 @@ - - - - + + + + - - + + diff --git a/views/login.ejs b/views/login.ejs index a32deaa..093e140 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -93,5 +93,6 @@ + <%- include('footer') %> diff --git a/views/overtime-breakdown.ejs b/views/overtime-breakdown.ejs index 9c6bb5f..3146fec 100644 --- a/views/overtime-breakdown.ejs +++ b/views/overtime-breakdown.ejs @@ -309,5 +309,6 @@ loadOvertimeBreakdown(); }); + <%- include('footer') %> diff --git a/views/verwaltung.ejs b/views/verwaltung.ejs index ff92263..f13811b 100644 --- a/views/verwaltung.ejs +++ b/views/verwaltung.ejs @@ -867,5 +867,6 @@ } }); + <%- include('footer') %>