Icons, Footer, Stundenformel

This commit is contained in:
2026-02-05 13:27:39 +01:00
parent 7d6951334f
commit 3c282a0f3c
17 changed files with 100 additions and 28 deletions

View File

@@ -14,6 +14,7 @@ checkinApp.set('views', path.join(__dirname, 'views'));
// Middleware für Check-in-Server // Middleware für Check-in-Server
checkinApp.use(express.json()); 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). */ /** Erkennt Browser-Aufruf: Accept-Header enthält text/html (z. B. beim Aufruf per QR/Link im Browser). */
function wantsHtml(req) { function wantsHtml(req) {

View File

@@ -3,7 +3,7 @@
- Offset für die Verwaltung für Urlaubstage -> DONE - Offset für die Verwaltung für Urlaubstage -> DONE
- Stunden pro Tag und wie viele Tage arbeit -> DONE - Stunden pro Tag und wie viele Tage arbeit -> DONE
- Reisen für Wochenende -> DONE - Reisen für Wochenende -> DONE
- LDAP Prüfung -> DONE GEHT?! - LDAP Prüfung -> DONE
- DSGVO Sicherheit -> DONE - DSGVO Sicherheit -> DONE
- Feiertage müssen als ausgefüllt zählen -> DONE - Feiertage müssen als ausgefüllt zählen -> DONE
- Mitarbeiter sollen PDF ansehen können. -> DONE - Mitarbeiter sollen PDF ansehen können. -> DONE
@@ -12,6 +12,6 @@
- Feiertage im PDF anzeigen -> DONE - Feiertage im PDF anzeigen -> DONE
- Oben wenn woche eingereicht anzeigen als hilfestellung -> 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 - Ü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 - Verplante Urlaubstage müssen auf abgezogen werden, wenn die Woche die gepalnt war eingereicht wurde. -> DONE

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1073,3 +1073,12 @@ table input[type="text"] {
align-items: flex-start; align-items: flex-start;
} }
} }
/* App Footer */
.app-footer {
text-align: center;
padding: 16px;
color: #888;
font-size: 13px;
margin-top: 24px;
}

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -440,12 +440,17 @@ function renderWeek() {
// Stunden zur Summe hinzufügen // Stunden zur Summe hinzufügen
// Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt) // 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 // 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) // Wochenend-Prozentsätze: Nur auf tatsächlich gearbeitete Stunden anwenden (nicht auf Urlaub, Krankheit, Feiertage)
let hoursToAdd = 0; let hoursToAdd = 0;
if (isHoliday) { 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 { } else {
hoursToAdd = hours || 0; hoursToAdd = hours || 0;
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
@@ -488,6 +493,9 @@ function renderWeek() {
} }
} else if (isSick) { } else if (isSick) {
hoursDisplay = fullDayHours.toFixed(2) + ' h (Krank)'; 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) { } else if (isHoliday && !hours) {
hoursDisplay = fullDayHours.toFixed(2) + ' h (Feiertag)'; hoursDisplay = fullDayHours.toFixed(2) + ' h (Feiertag)';
} else if (isHoliday && hours) { } else if (isHoliday && hours) {
@@ -818,7 +826,9 @@ function updateOvertimeDisplay() {
} else if (sickStatus) { } else if (sickStatus) {
totalHours += fullDayHours; // Krank = (Wochenarbeitszeit / Arbeitstage) Stunden totalHours += fullDayHours; // Krank = (Wochenarbeitszeit / Arbeitstage) Stunden
} else if (currentHolidayDates.has(dateStr)) { } 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 startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`);
const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`);
const startTime = startInput ? startInput.value : ''; const startTime = startInput ? startInput.value : '';
@@ -833,7 +843,11 @@ function updateOvertimeDisplay() {
} else if (currentEntries[dateStr]?.total_hours) { } else if (currentEntries[dateStr]?.total_hours) {
worked = parseFloat(currentEntries[dateStr].total_hours) || 0; 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 { } else {
// Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden
if (isFullDayOvertime) { if (isFullDayOvertime) {
@@ -922,7 +936,8 @@ function updateOvertimeDisplay() {
const totalHoursWithVacation = totalHours + vacationHours; const totalHoursWithVacation = totalHours + vacationHours;
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
// overtimeHours = Überstunden diese Woche (wie im Backend berechnet) // 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 // Überstunden-Anzeige aktualisieren
const overtimeSummaryItem = document.getElementById('overtimeSummaryItem'); const overtimeSummaryItem = document.getElementById('overtimeSummaryItem');
@@ -1206,8 +1221,12 @@ async function saveEntry(input) {
currentEntries[date].total_hours = totalHours; currentEntries[date].total_hours = totalHours;
} else { } else {
// Zurück zu normaler Anzeige basierend auf anderen Status // 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) { if (isSick) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Krank)'; hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Krank)';
} else if (isWeekendHoliday) {
hoursElement.textContent = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)';
} else if (isHoliday && !hours) { } else if (isHoliday && !hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)'; hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)';
} else if (isHoliday && hours) { } else if (isHoliday && hours) {
@@ -1369,11 +1388,17 @@ async function saveEntry(input) {
currentEntries[date].total_hours = totalHours; currentEntries[date].total_hours = totalHours;
} else if (isSick) { } else if (isSick) {
hoursText = fullDayHours.toFixed(2) + ' h (Krank)'; hoursText = fullDayHours.toFixed(2) + ' h (Krank)';
} else if (isHoliday && result.total_hours <= fullDayHours) { } else if (isHoliday) {
hoursText = fullDayHours.toFixed(2) + ' h (Feiertag)'; const d = new Date(date);
} else if (isHoliday && result.total_hours > fullDayHours) { const isWeekendHoliday = (d.getDay() === 6 || d.getDay() === 0);
const overtime = result.total_hours - fullDayHours; if (isWeekendHoliday) {
hoursText = fullDayHours.toFixed(2) + ' + ' + overtime.toFixed(2) + ' h (Überst.)'; 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; hoursElement.textContent = hoursText;
@@ -1973,8 +1998,12 @@ function toggleSickStatus(dateStr) {
currentEntries[dateStr].total_hours = fullDayHours; currentEntries[dateStr].total_hours = fullDayHours;
} else { } else {
// Zurück zu normaler Anzeige basierend auf anderen Status // 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) { if (isFullDayVacation) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Urlaub)'; hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Urlaub)';
} else if (isWeekendHoliday) {
hoursElement.textContent = (hours ? hours.toFixed(2) : '0') + ' h (Feiertag)';
} else if (isHoliday && !hours) { } else if (isHoliday && !hours) {
hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)'; hoursElement.textContent = fullDayHours.toFixed(2) + ' h (Feiertag)';
} else if (isHoliday && hours) { } else if (isHoliday && hours) {

View File

@@ -7,12 +7,12 @@
"theme_color": "#0a5ea8", "theme_color": "#0a5ea8",
"icons": [ "icons": [
{ {
"src": "/public/images/icons/icon-192x192.png", "src": "/images/icons/icon-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/public/images/icons/icon-512x512.png", "src": "/images/icons/icon-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }

View File

@@ -300,17 +300,33 @@ function registerVerwaltungRoutes(app) {
} }
function processCurrentWeek(totalVacationDays) { function processCurrentWeek(totalVacationDays) {
// Einträge für die Woche abrufen // Einträge für die Woche abrufen (id/updated_at für neuesten pro Tag)
db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type, sick_status db.all(`SELECT id, date, updated_at, total_hours, overtime_taken_hours, vacation_type, sick_status
FROM timesheet_entries FROM timesheet_entries
WHERE user_id = ? AND date >= ? AND date <= ? WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date`, ORDER BY date, updated_at DESC, id DESC`,
[userId, week_start, week_end], [userId, week_start, week_end],
(err, entries) => { (err, allEntries) => {
if (err) { if (err) {
return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); 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 // Berechnungen
let totalHours = 0; let totalHours = 0;
let overtimeTaken = 0; let overtimeTaken = 0;
@@ -320,7 +336,7 @@ function registerVerwaltungRoutes(app) {
entries.forEach(entry => { entries.forEach(entry => {
if (entry.overtime_taken_hours) { if (entry.overtime_taken_hours) {
overtimeTaken += entry.overtime_taken_hours; overtimeTaken += parseFloat(entry.overtime_taken_hours) || 0;
} }
// Krankheitstage zählen // Krankheitstage zählen

View File

@@ -187,7 +187,11 @@ function generatePDF(timesheetId, req, res) {
if (row.type === 'holiday') { if (row.type === 'holiday') {
y = doc.y; y = doc.y;
x = 50; 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) => { rowData.forEach((data, i) => {
doc.text(data, x, y, { width: colWidths[i], align: 'left' }); doc.text(data, x, y, { width: colWidths[i], align: 'left' });
x += colWidths[i]; x += colWidths[i];
@@ -455,7 +459,11 @@ function generatePDFToBuffer(timesheetId, req) {
if (row.type === 'holiday') { if (row.type === 'holiday') {
y = doc.y; y = doc.y;
x = 50; 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) => { rowDataBuf.forEach((data, i) => {
doc.text(data, x, y, { width: colWidths[i], align: 'left' }); doc.text(data, x, y, { width: colWidths[i], align: 'left' });
x += colWidths[i]; x += colWidths[i];

View File

@@ -484,5 +484,6 @@
} }
}); });
</script> </script>
<%- include('footer') %>
</body> </body>
</html> </html>

View File

@@ -53,5 +53,6 @@
<h1><%= title %></h1> <h1><%= title %></h1>
<p class="message"><%= message %></p> <p class="message"><%= message %></p>
</div> </div>
<%- include('footer') %>
</body> </body>
</html> </html>

View File

@@ -507,5 +507,6 @@
} }
} }
</script> </script>
<%- include('footer') %>
</body> </body>
</html> </html>

3
views/footer.ejs Normal file
View File

@@ -0,0 +1,3 @@
<footer class="app-footer">
Made with &#10084;&#65039; by Carsten Graf
</footer>

View File

@@ -2,14 +2,14 @@
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<!-- Apple Icons --> <!-- Apple Icons -->
<link rel="apple-touch-icon" sizes="120x120" href="/icons/icon-120x120.png"> <link rel="apple-touch-icon" sizes="120x120" href="/images/icons//icon-120x120.png">
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png"> <link rel="apple-touch-icon" sizes="152x152" href="/images/icons/icon-152x152.png">
<link rel="apple-touch-icon" sizes="167x167" href="/icons/icon-167x167.png"> <link rel="apple-touch-icon" sizes="167x167" href="/images/icons/icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180x180.png"> <link rel="apple-touch-icon" sizes="180x180" href="/images/icons/icon-180x180.png">
<!-- Android / PWA --> <!-- Android / PWA -->
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192x192.png"> <link rel="icon" type="image/png" sizes="192x192" href="/images/icons/favicon.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512x512.png"> <link rel="icon" type="image/png" sizes="512x512" href="/images/icons/favicon.png">
<!-- iOS App Mode --> <!-- iOS App Mode -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">

View File

@@ -93,5 +93,6 @@
</script> </script>
</div> </div>
</div> </div>
<%- include('footer') %>
</body> </body>
</html> </html>

View File

@@ -309,5 +309,6 @@
loadOvertimeBreakdown(); loadOvertimeBreakdown();
}); });
</script> </script>
<%- include('footer') %>
</body> </body>
</html> </html>

View File

@@ -867,5 +867,6 @@
} }
}); });
</script> </script>
<%- include('footer') %>
</body> </body>
</html> </html>