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
+
+ 0 Krankheitstage
+
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);