feat: add sick days support and multi-arch docker builds
Some checks failed
Build and Push Docker Image / build (push) Failing after 15m15s
Some checks failed
Build and Push Docker Image / build (push) Failing after 15m15s
This commit is contained in:
@@ -55,6 +55,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -81,6 +84,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: |
|
build-args: |
|
||||||
|
|||||||
@@ -491,6 +491,9 @@
|
|||||||
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
<div class="stat-card glass-card rounded-xl p-5 border border-gray-600">
|
||||||
<div class="text-xs text-gray-400 mb-2 uppercase tracking-wide">Arbeitstage</div>
|
<div class="text-xs text-gray-400 mb-2 uppercase tracking-wide">Arbeitstage</div>
|
||||||
<div id="statWorkdays" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-400">0</div>
|
<div id="statWorkdays" class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-400">0</div>
|
||||||
|
<div id="statSickdays" class="text-xs text-red-400 mt-2 hidden">
|
||||||
|
<i data-lucide="activity" class="w-3 h-3 inline"></i> <span id="statSickdaysCount">0</span> Krankheitstage
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -651,11 +651,11 @@ function renderEntries(entries) {
|
|||||||
</td>
|
</td>
|
||||||
` : '<td class="hidden"></td>';
|
` : '<td class="hidden"></td>';
|
||||||
|
|
||||||
// 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';
|
const entryType = entry.entryType || 'work';
|
||||||
let balanceCell = '';
|
let balanceCell = '';
|
||||||
|
|
||||||
if (entryType !== 'vacation') {
|
if (entryType !== 'vacation' && entryType !== 'sickday') {
|
||||||
// For all workdays (including flextime): Actual - Target (8h)
|
// For all workdays (including flextime): Actual - Target (8h)
|
||||||
// Flextime has netHours = 0, so deviation will be -8h
|
// Flextime has netHours = 0, so deviation will be -8h
|
||||||
const deviation = entry.netHours - 8.0;
|
const deviation = entry.netHours - 8.0;
|
||||||
@@ -808,6 +808,12 @@ function renderMonthlyView(entries) {
|
|||||||
displayTimes = `<td class="px-4 py-3 whitespace-nowrap text-sm text-center text-gray-400" colspan="3">
|
displayTimes = `<td class="px-4 py-3 whitespace-nowrap text-sm text-center text-gray-400" colspan="3">
|
||||||
<span class="italic">Urlaub</span>
|
<span class="italic">Urlaub</span>
|
||||||
</td>`;
|
</td>`;
|
||||||
|
} else if (entryType === 'sickday') {
|
||||||
|
displayIcon = '<i data-lucide="activity" class="w-4 h-4 inline"></i>';
|
||||||
|
displayText = 'Krank';
|
||||||
|
displayTimes = `<td class="px-4 py-3 whitespace-nowrap text-sm text-center text-gray-400" colspan="3">
|
||||||
|
<span class="italic">Krankheit</span>
|
||||||
|
</td>`;
|
||||||
} else if (entryType === 'flextime') {
|
} else if (entryType === 'flextime') {
|
||||||
displayIcon = '<i data-lucide="clock" class="w-4 h-4 inline"></i>';
|
displayIcon = '<i data-lucide="clock" class="w-4 h-4 inline"></i>';
|
||||||
displayText = 'Gleitzeit';
|
displayText = 'Gleitzeit';
|
||||||
@@ -849,11 +855,11 @@ function renderMonthlyView(entries) {
|
|||||||
</td>
|
</td>
|
||||||
` : '<td class="hidden"></td>';
|
` : '<td class="hidden"></td>';
|
||||||
|
|
||||||
// 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);
|
const isPastDay = dateObj < today || (dateObj.getFullYear() === todayYear && dateObj.getMonth() === todayMonth && day <= todayDay);
|
||||||
let balanceCell = '';
|
let balanceCell = '';
|
||||||
|
|
||||||
if (isPastDay && entryType !== 'vacation') {
|
if (isPastDay && entryType !== 'vacation' && entryType !== 'sickday') {
|
||||||
// For all workdays (including flextime): Actual - Target (8h default)
|
// For all workdays (including flextime): Actual - Target (8h default)
|
||||||
// Flextime has netHours = 0, so deviation will be -8h
|
// Flextime has netHours = 0, so deviation will be -8h
|
||||||
|
|
||||||
@@ -942,6 +948,9 @@ function renderMonthlyView(entries) {
|
|||||||
<button class="btn-add-vacation inline-flex items-center justify-center w-7 h-7 text-amber-400/80 hover:text-amber-400 hover:bg-amber-500/10 rounded transition-all" data-date="${dateISO}" title="Urlaub eintragen">
|
<button class="btn-add-vacation inline-flex items-center justify-center w-7 h-7 text-amber-400/80 hover:text-amber-400 hover:bg-amber-500/10 rounded transition-all" data-date="${dateISO}" title="Urlaub eintragen">
|
||||||
<i data-lucide="plane" class="w-4 h-4"></i>
|
<i data-lucide="plane" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-add-sickday inline-flex items-center justify-center w-7 h-7 text-red-400/80 hover:text-red-400 hover:bg-red-500/10 rounded transition-all" data-date="${dateISO}" title="Krankheit eintragen">
|
||||||
|
<i data-lucide="activity" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
<button class="btn-add-flextime inline-flex items-center justify-center w-7 h-7 text-cyan-400/80 hover:text-cyan-400 hover:bg-cyan-500/10 rounded transition-all" data-date="${dateISO}" title="Gleitzeit eintragen">
|
<button class="btn-add-flextime inline-flex items-center justify-center w-7 h-7 text-cyan-400/80 hover:text-cyan-400 hover:bg-cyan-500/10 rounded transition-all" data-date="${dateISO}" title="Gleitzeit eintragen">
|
||||||
<i data-lucide="clock" class="w-4 h-4"></i>
|
<i data-lucide="clock" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -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
|
// Add flextime entry for a specific date
|
||||||
document.querySelectorAll('.btn-add-flextime').forEach(btn => {
|
document.querySelectorAll('.btn-add-flextime').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
@@ -1020,7 +1037,7 @@ function renderMonthlyView(entries) {
|
|||||||
* Add a special entry (vacation or flextime) for a specific date
|
* Add a special entry (vacation or flextime) for a specific date
|
||||||
*/
|
*/
|
||||||
async function addSpecialEntry(dateISO, entryType) {
|
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
|
// Check if date is a public holiday
|
||||||
const dateObj = new Date(dateISO + 'T00:00:00');
|
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');
|
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');
|
showNotification(message, 'success');
|
||||||
loadMonthlyView();
|
loadMonthlyView();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1156,6 +1175,13 @@ async function updateStatistics(entries) {
|
|||||||
.map(e => e.date)
|
.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)
|
// Count flextime days (they are workdays with 0 hours worked)
|
||||||
const flextimeDays = new Set(
|
const flextimeDays = new Set(
|
||||||
entries
|
entries
|
||||||
@@ -1182,6 +1208,7 @@ async function updateStatistics(entries) {
|
|||||||
const dateISO = `${year}-${month}-${dayStr}`;
|
const dateISO = `${year}-${month}-${dayStr}`;
|
||||||
|
|
||||||
const isVacation = vacationDays.has(dateISO);
|
const isVacation = vacationDays.has(dateISO);
|
||||||
|
const isSickday = sickDays.has(dateISO);
|
||||||
const isFlextime = flextimeDays.has(dateISO);
|
const isFlextime = flextimeDays.has(dateISO);
|
||||||
const isWeekendHoliday = isWeekendOrHoliday(dateObj);
|
const isWeekendHoliday = isWeekendOrHoliday(dateObj);
|
||||||
const hasEntry = entriesMap[dateISO];
|
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
|
// For today: only count as workday if there's an entry OR timer is running
|
||||||
const shouldCountToday = !isToday || hasEntry || (timerStartTime && isCurrentMonth);
|
const shouldCountToday = !isToday || hasEntry || (timerStartTime && isCurrentMonth);
|
||||||
|
|
||||||
if (!isWeekendHoliday && !isVacation && !isFlextime) {
|
if (!isWeekendHoliday && !isVacation && !isSickday && !isFlextime) {
|
||||||
// Normal workday (excluding vacation and flextime days)
|
// Normal workday (excluding vacation, sick days, and flextime days)
|
||||||
totalWorkdaysInMonth++;
|
totalWorkdaysInMonth++;
|
||||||
workdaysCount++;
|
workdaysCount++;
|
||||||
if (dateObj <= today && shouldCountToday) {
|
if (dateObj <= today && shouldCountToday) {
|
||||||
workdaysPassed++;
|
workdaysPassed++;
|
||||||
}
|
}
|
||||||
} else if (!isWeekendHoliday && !isVacation && isFlextime) {
|
} else if (!isWeekendHoliday && !isVacation && !isSickday && isFlextime) {
|
||||||
// Flextime on a workday - still counts as workday in calendar
|
// Flextime on a workday - still counts as workday in calendar
|
||||||
totalWorkdaysInMonth++;
|
totalWorkdaysInMonth++;
|
||||||
workdaysCount++;
|
workdaysCount++;
|
||||||
@@ -1216,7 +1243,7 @@ async function updateStatistics(entries) {
|
|||||||
workdaysPassed++;
|
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)
|
// Calculate target hours (8h per workday passed, no reduction)
|
||||||
@@ -1255,17 +1282,37 @@ async function updateStatistics(entries) {
|
|||||||
// Total balance = previous balance + current month balance
|
// Total balance = previous balance + current month balance
|
||||||
const totalBalance = previousBalance + 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 workEntriesCount = entries.filter(e => {
|
||||||
const entryDate = new Date(e.date);
|
const entryDate = new Date(e.date);
|
||||||
return entryDate <= today && (!e.entryType || e.entryType === 'work');
|
return entryDate <= today && (!e.entryType || e.entryType === 'work');
|
||||||
}).length;
|
}).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
|
// Update UI
|
||||||
document.getElementById('statTargetHours').textContent = targetHours.toFixed(1) + 'h';
|
document.getElementById('statTargetHours').textContent = targetHours.toFixed(1) + 'h';
|
||||||
document.getElementById('statActualHours').textContent = actualHours.toFixed(1) + 'h';
|
document.getElementById('statActualHours').textContent = actualHours.toFixed(1) + 'h';
|
||||||
document.getElementById('statWorkdays').textContent = `${workEntriesCount}/${totalWorkdaysInMonth}`;
|
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
|
// Show/hide flextime hint icons
|
||||||
const balanceFlextimeHint = document.getElementById('balanceFlextimeHint');
|
const balanceFlextimeHint = document.getElementById('balanceFlextimeHint');
|
||||||
const totalBalanceFlextimeHint = document.getElementById('totalBalanceFlextimeHint');
|
const totalBalanceFlextimeHint = document.getElementById('totalBalanceFlextimeHint');
|
||||||
@@ -2100,7 +2147,7 @@ async function generatePDF(options) {
|
|||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const { targetHours, totalNetHours, balance } = statistics;
|
const { targetHours, totalNetHours, balance } = statistics;
|
||||||
const { vacationDays = 0, flextimeDays = 0 } = additionalInfo;
|
const { vacationDays = 0, sickDays = 0, flextimeDays = 0 } = additionalInfo;
|
||||||
|
|
||||||
// Get employee data from settings
|
// Get employee data from settings
|
||||||
const employeeName = await getSetting('employeeName') || '';
|
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' });
|
doc.text(`${balance >= 0 ? '+' : ''}${balance.toFixed(1)}h`, 170, statsY + 3, { align: 'center' });
|
||||||
|
|
||||||
// Additional info if needed (small, far right)
|
// 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.setTextColor(150, 150, 150);
|
||||||
doc.setFontSize(7);
|
doc.setFontSize(7);
|
||||||
doc.setFont(undefined, 'normal');
|
doc.setFont(undefined, 'normal');
|
||||||
let infoText = '';
|
let infoText = '';
|
||||||
if (vacationDays > 0) infoText += `Urlaub: ${vacationDays}`;
|
if (vacationDays > 0) infoText += `Urlaub: ${vacationDays}`;
|
||||||
|
if (sickDays > 0) {
|
||||||
|
if (infoText) infoText += ' ';
|
||||||
|
infoText += `Krank: ${sickDays}`;
|
||||||
|
}
|
||||||
if (flextimeDays > 0) {
|
if (flextimeDays > 0) {
|
||||||
if (infoText) infoText += ' ';
|
if (infoText) infoText += ' ';
|
||||||
infoText += `Gleitzeit: ${flextimeDays}`;
|
infoText += `Gleitzeit: ${flextimeDays}`;
|
||||||
@@ -2312,6 +2363,13 @@ async function bulkExportPDF() {
|
|||||||
.map(e => e.date)
|
.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
|
// Count flextime days from selected entries
|
||||||
const flextimeDaysSet = new Set(
|
const flextimeDaysSet = new Set(
|
||||||
selectedEntriesData
|
selectedEntriesData
|
||||||
@@ -2326,10 +2384,11 @@ async function bulkExportPDF() {
|
|||||||
const dateObj = new Date(year, month - 1, day);
|
const dateObj = new Date(year, month - 1, day);
|
||||||
|
|
||||||
const isVacation = vacationDaysSet.has(dateISO);
|
const isVacation = vacationDaysSet.has(dateISO);
|
||||||
|
const isSickday = sickDaysSet.has(dateISO);
|
||||||
const isFlextime = flextimeDaysSet.has(dateISO);
|
const isFlextime = flextimeDaysSet.has(dateISO);
|
||||||
const isWeekendHoliday = isWeekendOrHoliday(dateObj);
|
const isWeekendHoliday = isWeekendOrHoliday(dateObj);
|
||||||
|
|
||||||
if (!isWeekendHoliday && !isVacation) {
|
if (!isWeekendHoliday && !isVacation && !isSickday) {
|
||||||
// Normal workday
|
// Normal workday
|
||||||
if (dateObj <= today) {
|
if (dateObj <= today) {
|
||||||
workdaysPassed++;
|
workdaysPassed++;
|
||||||
@@ -2340,12 +2399,13 @@ async function bulkExportPDF() {
|
|||||||
workdaysPassed++;
|
workdaysPassed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Vacation days are excluded from workday count
|
// Vacation days and sick days are excluded from workday count
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate total hours and days
|
// Calculate total hours and days
|
||||||
let totalNetHours = 0;
|
let totalNetHours = 0;
|
||||||
let vacationDays = 0;
|
let vacationDays = 0;
|
||||||
|
let sickDays = 0;
|
||||||
let flextimeDays = 0;
|
let flextimeDays = 0;
|
||||||
let workEntriesCount = 0;
|
let workEntriesCount = 0;
|
||||||
|
|
||||||
@@ -2358,6 +2418,9 @@ async function bulkExportPDF() {
|
|||||||
} else if (entry.entryType === 'vacation') {
|
} else if (entry.entryType === 'vacation') {
|
||||||
vacationDays++;
|
vacationDays++;
|
||||||
totalNetHours += entry.netHours;
|
totalNetHours += entry.netHours;
|
||||||
|
} else if (entry.entryType === 'sickday') {
|
||||||
|
sickDays++;
|
||||||
|
totalNetHours += entry.netHours;
|
||||||
} else if (entry.entryType === 'flextime') {
|
} else if (entry.entryType === 'flextime') {
|
||||||
flextimeDays++;
|
flextimeDays++;
|
||||||
// Only add flextime hours if it's on a weekend/holiday
|
// Only add flextime hours if it's on a weekend/holiday
|
||||||
@@ -2409,6 +2472,13 @@ async function bulkExportPDF() {
|
|||||||
pauseText = '-';
|
pauseText = '-';
|
||||||
netHoursText = '-';
|
netHoursText = '-';
|
||||||
deviationStr = '-';
|
deviationStr = '-';
|
||||||
|
} else if (entry.entryType === 'sickday') {
|
||||||
|
locationText = 'Krank';
|
||||||
|
startTime = '-';
|
||||||
|
endTime = '-';
|
||||||
|
pauseText = '-';
|
||||||
|
netHoursText = '-';
|
||||||
|
deviationStr = '-';
|
||||||
} else if (entry.entryType === 'flextime') {
|
} else if (entry.entryType === 'flextime') {
|
||||||
locationText = 'Gleittag';
|
locationText = 'Gleittag';
|
||||||
startTime = '-';
|
startTime = '-';
|
||||||
@@ -2464,7 +2534,7 @@ async function bulkExportPDF() {
|
|||||||
subtitle: dateRange,
|
subtitle: dateRange,
|
||||||
tableData: allDaysData,
|
tableData: allDaysData,
|
||||||
statistics: { targetHours, totalNetHours, balance },
|
statistics: { targetHours, totalNetHours, balance },
|
||||||
additionalInfo: { vacationDays, flextimeDays },
|
additionalInfo: { vacationDays, sickDays, flextimeDays },
|
||||||
fileName
|
fileName
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3379,6 +3449,13 @@ async function handleExportPDF() {
|
|||||||
.map(e => e.date)
|
.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)
|
// Count flextime days (they are workdays with 0 hours worked)
|
||||||
const flextimeDaysSet = new Set(
|
const flextimeDaysSet = new Set(
|
||||||
entries
|
entries
|
||||||
@@ -3394,11 +3471,12 @@ async function handleExportPDF() {
|
|||||||
const dateISO = `${yearStr}-${monthStr}-${dayStr}`;
|
const dateISO = `${yearStr}-${monthStr}-${dayStr}`;
|
||||||
|
|
||||||
const isVacation = vacationDaysSet.has(dateISO);
|
const isVacation = vacationDaysSet.has(dateISO);
|
||||||
|
const isSickday = sickDaysSet.has(dateISO);
|
||||||
const isFlextime = flextimeDaysSet.has(dateISO);
|
const isFlextime = flextimeDaysSet.has(dateISO);
|
||||||
const isWeekendHoliday = isWeekendOrHoliday(dateObj);
|
const isWeekendHoliday = isWeekendOrHoliday(dateObj);
|
||||||
|
|
||||||
if (!isWeekendHoliday && !isVacation) {
|
if (!isWeekendHoliday && !isVacation && !isSickday) {
|
||||||
// Normal workday (excluding vacation days)
|
// Normal workday (excluding vacation and sick days)
|
||||||
totalWorkdaysInMonth++;
|
totalWorkdaysInMonth++;
|
||||||
if (dateObj <= countUntil) {
|
if (dateObj <= countUntil) {
|
||||||
workdaysPassed++;
|
workdaysPassed++;
|
||||||
@@ -3410,11 +3488,12 @@ async function handleExportPDF() {
|
|||||||
workdaysPassed++;
|
workdaysPassed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Vacation days are excluded from all counts
|
// Vacation days and sick days are excluded from all counts
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalNetHours = 0;
|
let totalNetHours = 0;
|
||||||
let vacationDays = 0;
|
let vacationDays = 0;
|
||||||
|
let sickDays = 0;
|
||||||
let flextimeDays = 0;
|
let flextimeDays = 0;
|
||||||
let workEntriesCount = 0;
|
let workEntriesCount = 0;
|
||||||
|
|
||||||
@@ -3437,6 +3516,10 @@ async function handleExportPDF() {
|
|||||||
vacationDays++;
|
vacationDays++;
|
||||||
// Vacation hours are already included in netHours (8h per day typically)
|
// Vacation hours are already included in netHours (8h per day typically)
|
||||||
totalNetHours += entry.netHours;
|
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') {
|
} else if (entry.entryType === 'flextime') {
|
||||||
flextimeDays++;
|
flextimeDays++;
|
||||||
// Only add flextime hours if it's on a weekend/holiday
|
// Only add flextime hours if it's on a weekend/holiday
|
||||||
@@ -3504,6 +3587,13 @@ async function handleExportPDF() {
|
|||||||
pauseText = '-';
|
pauseText = '-';
|
||||||
netHoursText = '-';
|
netHoursText = '-';
|
||||||
deviationStr = '-';
|
deviationStr = '-';
|
||||||
|
} else if (entry.entryType === 'sickday') {
|
||||||
|
locationText = 'Krank';
|
||||||
|
startTime = '-';
|
||||||
|
endTime = '-';
|
||||||
|
pauseText = '-';
|
||||||
|
netHoursText = '-';
|
||||||
|
deviationStr = '-';
|
||||||
} else if (entry.entryType === 'flextime') {
|
} else if (entry.entryType === 'flextime') {
|
||||||
locationText = 'Gleittag';
|
locationText = 'Gleittag';
|
||||||
startTime = '-';
|
startTime = '-';
|
||||||
@@ -3556,7 +3646,7 @@ async function handleExportPDF() {
|
|||||||
subtitle: monthName,
|
subtitle: monthName,
|
||||||
tableData: allDaysData,
|
tableData: allDaysData,
|
||||||
statistics: { targetHours, totalNetHours, balance: monthBalance },
|
statistics: { targetHours, totalNetHours, balance: monthBalance },
|
||||||
additionalInfo: { vacationDays, flextimeDays },
|
additionalInfo: { vacationDays, sickDays, flextimeDays },
|
||||||
fileName
|
fileName
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS entries (
|
|||||||
end_time TEXT,
|
end_time TEXT,
|
||||||
pause_minutes INTEGER NOT NULL DEFAULT 0,
|
pause_minutes INTEGER NOT NULL DEFAULT 0,
|
||||||
location TEXT DEFAULT 'office' CHECK(location IN ('office', 'home')),
|
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 (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
|||||||
15
server.js
15
server.js
@@ -47,13 +47,22 @@ try {
|
|||||||
|
|
||||||
if (!hasEntryTypeColumn) {
|
if (!hasEntryTypeColumn) {
|
||||||
console.log('Adding entry_type column to entries table...');
|
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');
|
console.log('Entry_type column added successfully');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during entry_type migration:', 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
|
// Migration: Make start_time and end_time nullable for vacation/flextime entries
|
||||||
try {
|
try {
|
||||||
// SQLite doesn't support ALTER COLUMN directly, so we check if we can insert NULL values
|
// 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 };
|
return { grossHours: 0, pauseMinutes: 0, netHours: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entryType === 'sickday') {
|
||||||
|
return { grossHours: 0, pauseMinutes: 0, netHours: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
// Regular work entry calculation
|
// Regular work entry calculation
|
||||||
const [startHour, startMin] = startTime.split(':').map(Number);
|
const [startHour, startMin] = startTime.split(':').map(Number);
|
||||||
const [endHour, endMin] = endTime.split(':').map(Number);
|
const [endHour, endMin] = endTime.split(':').map(Number);
|
||||||
|
|||||||
Reference in New Issue
Block a user