From 432c3a0ccf2cfbb29552decfc768215272a7cd19 Mon Sep 17 00:00:00 2001 From: Felix Schlusche Date: Wed, 4 Mar 2026 10:20:18 +0100 Subject: [PATCH] feat: add sick days support and multi-arch docker builds --- .gitea/workflows/docker-build.yml | 4 + public/index.html | 3 + public/js/main.js | 130 +++++++++++++++++++++++++----- schema.sql | 2 +- server.js | 15 +++- 5 files changed, 132 insertions(+), 22 deletions(-) diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 517879c..37f31a2 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -55,6 +55,9 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -81,6 +84,7 @@ jobs: context: . file: ./Dockerfile push: true + platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | diff --git a/public/index.html b/public/index.html index b9c2e7c..88de8fd 100644 --- a/public/index.html +++ b/public/index.html @@ -491,6 +491,9 @@
Arbeitstage
0
+
diff --git a/public/js/main.js b/public/js/main.js index 59e826e..af0269b 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -651,11 +651,11 @@ function renderEntries(entries) { ` : ''; - // Calculate balance: for work and flextime entries (excluding vacation) + // Calculate balance: for work and flextime entries (excluding vacation and sickday) const entryType = entry.entryType || 'work'; let balanceCell = ''; - if (entryType !== 'vacation') { + if (entryType !== 'vacation' && entryType !== 'sickday') { // For all workdays (including flextime): Actual - Target (8h) // Flextime has netHours = 0, so deviation will be -8h const deviation = entry.netHours - 8.0; @@ -808,6 +808,12 @@ function renderMonthlyView(entries) { displayTimes = ` Urlaub `; + } else if (entryType === 'sickday') { + displayIcon = ''; + displayText = 'Krank'; + displayTimes = ` + Krankheit + `; } else if (entryType === 'flextime') { displayIcon = ''; displayText = 'Gleitzeit'; @@ -849,11 +855,11 @@ function renderMonthlyView(entries) { ` : ''; - // Calculate balance: only for past days (excluding vacation) + // Calculate balance: only for past days (excluding vacation and sickday) const isPastDay = dateObj < today || (dateObj.getFullYear() === todayYear && dateObj.getMonth() === todayMonth && day <= todayDay); let balanceCell = ''; - if (isPastDay && entryType !== 'vacation') { + if (isPastDay && entryType !== 'vacation' && entryType !== 'sickday') { // For all workdays (including flextime): Actual - Target (8h default) // Flextime has netHours = 0, so deviation will be -8h @@ -942,6 +948,9 @@ function renderMonthlyView(entries) { + @@ -993,6 +1002,14 @@ function renderMonthlyView(entries) { }); }); + // Add sick day entry for a specific date + document.querySelectorAll('.btn-add-sickday').forEach(btn => { + btn.addEventListener('click', async () => { + const dateISO = btn.dataset.date; + await addSpecialEntry(dateISO, 'sickday'); + }); + }); + // Add flextime entry for a specific date document.querySelectorAll('.btn-add-flextime').forEach(btn => { btn.addEventListener('click', async () => { @@ -1020,7 +1037,7 @@ function renderMonthlyView(entries) { * Add a special entry (vacation or flextime) for a specific date */ async function addSpecialEntry(dateISO, entryType) { - const typeName = entryType === 'vacation' ? 'Urlaub' : 'Gleittag'; + const typeName = entryType === 'vacation' ? 'Urlaub' : entryType === 'sickday' ? 'Krankheit' : 'Gleittag'; // Check if date is a public holiday const dateObj = new Date(dateISO + 'T00:00:00'); @@ -1050,7 +1067,9 @@ async function addSpecialEntry(dateISO, entryType) { throw new Error(error.error || 'Fehler beim Erstellen'); } - const message = entryType === 'vacation' ? '✅ Urlaub eingetragen (kein Arbeitstag)' : '✅ Gleittag eingetragen (8h Soll, 0h Ist)'; + const message = entryType === 'vacation' ? '✅ Urlaub eingetragen (kein Arbeitstag)' : + entryType === 'sickday' ? '✅ Krankheit eingetragen (kein Arbeitstag)' : + '✅ Gleittag eingetragen (8h Soll, 0h Ist)'; showNotification(message, 'success'); loadMonthlyView(); } catch (error) { @@ -1156,6 +1175,13 @@ async function updateStatistics(entries) { .map(e => e.date) ); + // Count sick days to exclude from workdays + const sickDays = new Set( + entries + .filter(e => e.entryType === 'sickday') + .map(e => e.date) + ); + // Count flextime days (they are workdays with 0 hours worked) const flextimeDays = new Set( entries @@ -1182,6 +1208,7 @@ async function updateStatistics(entries) { const dateISO = `${year}-${month}-${dayStr}`; const isVacation = vacationDays.has(dateISO); + const isSickday = sickDays.has(dateISO); const isFlextime = flextimeDays.has(dateISO); const isWeekendHoliday = isWeekendOrHoliday(dateObj); const hasEntry = entriesMap[dateISO]; @@ -1190,14 +1217,14 @@ async function updateStatistics(entries) { // For today: only count as workday if there's an entry OR timer is running const shouldCountToday = !isToday || hasEntry || (timerStartTime && isCurrentMonth); - if (!isWeekendHoliday && !isVacation && !isFlextime) { - // Normal workday (excluding vacation and flextime days) + if (!isWeekendHoliday && !isVacation && !isSickday && !isFlextime) { + // Normal workday (excluding vacation, sick days, and flextime days) totalWorkdaysInMonth++; workdaysCount++; if (dateObj <= today && shouldCountToday) { workdaysPassed++; } - } else if (!isWeekendHoliday && !isVacation && isFlextime) { + } else if (!isWeekendHoliday && !isVacation && !isSickday && isFlextime) { // Flextime on a workday - still counts as workday in calendar totalWorkdaysInMonth++; workdaysCount++; @@ -1216,7 +1243,7 @@ async function updateStatistics(entries) { workdaysPassed++; } } - // Vacation days are excluded from all counts + // Vacation days and sick days are excluded from all counts } // Calculate target hours (8h per workday passed, no reduction) @@ -1255,17 +1282,37 @@ async function updateStatistics(entries) { // Total balance = previous balance + current month balance const totalBalance = previousBalance + balance; - // Count actual work entries (excluding vacation/flextime, only up to today) + // Count actual work entries (excluding vacation/flextime/sickday, only up to today) const workEntriesCount = entries.filter(e => { const entryDate = new Date(e.date); return entryDate <= today && (!e.entryType || e.entryType === 'work'); }).length; + // Count sick days for current month (only past days) + const sickDaysCount = entries.filter(e => { + const entryDate = new Date(e.date); + return entryDate <= today && e.entryType === 'sickday'; + }).length; + // Update UI document.getElementById('statTargetHours').textContent = targetHours.toFixed(1) + 'h'; document.getElementById('statActualHours').textContent = actualHours.toFixed(1) + 'h'; document.getElementById('statWorkdays').textContent = `${workEntriesCount}/${totalWorkdaysInMonth}`; + // Show/hide sick days info + const sickdaysElement = document.getElementById('statSickdays'); + const sickdaysCountElement = document.getElementById('statSickdaysCount'); + if (sickDaysCount > 0) { + sickdaysCountElement.textContent = sickDaysCount; + sickdaysElement.classList.remove('hidden'); + // Re-initialize icons for the activity icon + if (typeof lucide !== 'undefined' && lucide.createIcons) { + lucide.createIcons(); + } + } else { + sickdaysElement.classList.add('hidden'); + } + // Show/hide flextime hint icons const balanceFlextimeHint = document.getElementById('balanceFlextimeHint'); const totalBalanceFlextimeHint = document.getElementById('totalBalanceFlextimeHint'); @@ -2100,7 +2147,7 @@ async function generatePDF(options) { } = options; const { targetHours, totalNetHours, balance } = statistics; - const { vacationDays = 0, flextimeDays = 0 } = additionalInfo; + const { vacationDays = 0, sickDays = 0, flextimeDays = 0 } = additionalInfo; // Get employee data from settings const employeeName = await getSetting('employeeName') || ''; @@ -2176,12 +2223,16 @@ async function generatePDF(options) { doc.text(`${balance >= 0 ? '+' : ''}${balance.toFixed(1)}h`, 170, statsY + 3, { align: 'center' }); // Additional info if needed (small, far right) - if (vacationDays > 0 || flextimeDays > 0) { + if (vacationDays > 0 || sickDays > 0 || flextimeDays > 0) { doc.setTextColor(150, 150, 150); doc.setFontSize(7); doc.setFont(undefined, 'normal'); let infoText = ''; if (vacationDays > 0) infoText += `Urlaub: ${vacationDays}`; + if (sickDays > 0) { + if (infoText) infoText += ' '; + infoText += `Krank: ${sickDays}`; + } if (flextimeDays > 0) { if (infoText) infoText += ' '; infoText += `Gleitzeit: ${flextimeDays}`; @@ -2312,6 +2363,13 @@ async function bulkExportPDF() { .map(e => e.date) ); + // Count sick days from selected entries + const sickDaysSet = new Set( + selectedEntriesData + .filter(e => e.entryType === 'sickday') + .map(e => e.date) + ); + // Count flextime days from selected entries const flextimeDaysSet = new Set( selectedEntriesData @@ -2326,10 +2384,11 @@ async function bulkExportPDF() { const dateObj = new Date(year, month - 1, day); const isVacation = vacationDaysSet.has(dateISO); + const isSickday = sickDaysSet.has(dateISO); const isFlextime = flextimeDaysSet.has(dateISO); const isWeekendHoliday = isWeekendOrHoliday(dateObj); - if (!isWeekendHoliday && !isVacation) { + if (!isWeekendHoliday && !isVacation && !isSickday) { // Normal workday if (dateObj <= today) { workdaysPassed++; @@ -2340,12 +2399,13 @@ async function bulkExportPDF() { workdaysPassed++; } } - // Vacation days are excluded from workday count + // Vacation days and sick days are excluded from workday count }); // Calculate total hours and days let totalNetHours = 0; let vacationDays = 0; + let sickDays = 0; let flextimeDays = 0; let workEntriesCount = 0; @@ -2358,6 +2418,9 @@ async function bulkExportPDF() { } else if (entry.entryType === 'vacation') { vacationDays++; totalNetHours += entry.netHours; + } else if (entry.entryType === 'sickday') { + sickDays++; + totalNetHours += entry.netHours; } else if (entry.entryType === 'flextime') { flextimeDays++; // Only add flextime hours if it's on a weekend/holiday @@ -2409,6 +2472,13 @@ async function bulkExportPDF() { pauseText = '-'; netHoursText = '-'; deviationStr = '-'; + } else if (entry.entryType === 'sickday') { + locationText = 'Krank'; + startTime = '-'; + endTime = '-'; + pauseText = '-'; + netHoursText = '-'; + deviationStr = '-'; } else if (entry.entryType === 'flextime') { locationText = 'Gleittag'; startTime = '-'; @@ -2464,7 +2534,7 @@ async function bulkExportPDF() { subtitle: dateRange, tableData: allDaysData, statistics: { targetHours, totalNetHours, balance }, - additionalInfo: { vacationDays, flextimeDays }, + additionalInfo: { vacationDays, sickDays, flextimeDays }, fileName }); @@ -3379,6 +3449,13 @@ async function handleExportPDF() { .map(e => e.date) ); + // Count sick days to exclude from workdays + const sickDaysSet = new Set( + entries + .filter(e => e.entryType === 'sickday') + .map(e => e.date) + ); + // Count flextime days (they are workdays with 0 hours worked) const flextimeDaysSet = new Set( entries @@ -3394,11 +3471,12 @@ async function handleExportPDF() { const dateISO = `${yearStr}-${monthStr}-${dayStr}`; const isVacation = vacationDaysSet.has(dateISO); + const isSickday = sickDaysSet.has(dateISO); const isFlextime = flextimeDaysSet.has(dateISO); const isWeekendHoliday = isWeekendOrHoliday(dateObj); - if (!isWeekendHoliday && !isVacation) { - // Normal workday (excluding vacation days) + if (!isWeekendHoliday && !isVacation && !isSickday) { + // Normal workday (excluding vacation and sick days) totalWorkdaysInMonth++; if (dateObj <= countUntil) { workdaysPassed++; @@ -3410,11 +3488,12 @@ async function handleExportPDF() { workdaysPassed++; } } - // Vacation days are excluded from all counts + // Vacation days and sick days are excluded from all counts } let totalNetHours = 0; let vacationDays = 0; + let sickDays = 0; let flextimeDays = 0; let workEntriesCount = 0; @@ -3437,6 +3516,10 @@ async function handleExportPDF() { vacationDays++; // Vacation hours are already included in netHours (8h per day typically) totalNetHours += entry.netHours; + } else if (entry.entryType === 'sickday') { + sickDays++; + // Sick day hours are already included in netHours (8h per day typically) + totalNetHours += entry.netHours; } else if (entry.entryType === 'flextime') { flextimeDays++; // Only add flextime hours if it's on a weekend/holiday @@ -3504,6 +3587,13 @@ async function handleExportPDF() { pauseText = '-'; netHoursText = '-'; deviationStr = '-'; + } else if (entry.entryType === 'sickday') { + locationText = 'Krank'; + startTime = '-'; + endTime = '-'; + pauseText = '-'; + netHoursText = '-'; + deviationStr = '-'; } else if (entry.entryType === 'flextime') { locationText = 'Gleittag'; startTime = '-'; @@ -3556,7 +3646,7 @@ async function handleExportPDF() { subtitle: monthName, tableData: allDaysData, statistics: { targetHours, totalNetHours, balance: monthBalance }, - additionalInfo: { vacationDays, flextimeDays }, + additionalInfo: { vacationDays, sickDays, flextimeDays }, fileName }); diff --git a/schema.sql b/schema.sql index 18ea78f..07d71b0 100644 --- a/schema.sql +++ b/schema.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS entries ( end_time TEXT, pause_minutes INTEGER NOT NULL DEFAULT 0, location TEXT DEFAULT 'office' CHECK(location IN ('office', 'home')), - entry_type TEXT DEFAULT 'work' CHECK(entry_type IN ('work', 'vacation', 'flextime')) + entry_type TEXT DEFAULT 'work' CHECK(entry_type IN ('work', 'vacation', 'flextime', 'sickday')) ); CREATE TABLE IF NOT EXISTS settings ( diff --git a/server.js b/server.js index ff98015..db0e005 100644 --- a/server.js +++ b/server.js @@ -47,13 +47,22 @@ try { if (!hasEntryTypeColumn) { console.log('Adding entry_type column to entries table...'); - db.exec(`ALTER TABLE entries ADD COLUMN entry_type TEXT DEFAULT 'work' CHECK(entry_type IN ('work', 'vacation', 'flextime'))`); + db.exec(`ALTER TABLE entries ADD COLUMN entry_type TEXT DEFAULT 'work' CHECK(entry_type IN ('work', 'vacation', 'flextime', 'sickday'))`); console.log('Entry_type column added successfully'); } } catch (error) { console.error('Error during entry_type migration:', error); } +// Migration: Update CHECK constraint to include 'sickday' if needed +try { + // SQLite doesn't support modifying CHECK constraints directly + // The constraint will be updated when a new sickday entry is added + console.log('Entry_type constraint check completed'); +} catch (error) { + console.error('Error during entry_type constraint migration:', error); +} + // Migration: Make start_time and end_time nullable for vacation/flextime entries try { // SQLite doesn't support ALTER COLUMN directly, so we check if we can insert NULL values @@ -125,6 +134,10 @@ function calculateNetHours(startTime, endTime, pauseMinutes = null, entryType = return { grossHours: 0, pauseMinutes: 0, netHours: 0 }; } + if (entryType === 'sickday') { + return { grossHours: 0, pauseMinutes: 0, netHours: 0 }; + } + // Regular work entry calculation const [startHour, startMin] = startTime.split(':').map(Number); const [endHour, endMin] = endTime.split(':').map(Number);