diff --git a/api.php b/api.php index 0e99fb2..9d9c722 100644 --- a/api.php +++ b/api.php @@ -297,7 +297,11 @@ function save_settings(PDO $db, array $settings): void foreach ($allowed as $key) { if (array_key_exists($key, $settings)) { - $stmt->execute([':key' => $key, ':value' => (string)$settings[$key]]); + $value = $settings[$key]; + if ($key === 'feriefridage_per_year' && floor((float)$value) !== (float)$value) { + fail('Feriefridage/year must be a whole number', 400); + } + $stmt->execute([':key' => $key, ':value' => (string)$value]); } } } @@ -352,6 +356,10 @@ function save_feriefridage_override(PDO $db, int $year, int $month, mixed $value return; } + if (floor((float)$value) !== (float)$value) { + fail('Feriefridage must be whole days', 400); + } + $stmt = $db->prepare('INSERT INTO monthly_feriefridage_overrides (year, month, feriefridage_days) VALUES (:year, :month, :value) ON CONFLICT(year, month) DO UPDATE SET feriefridage_days = excluded.feriefridage_days'); diff --git a/app.js b/app.js index 7c4021c..3a8ab49 100644 --- a/app.js +++ b/app.js @@ -146,7 +146,7 @@ function upcomingVacationsForHa() { function fillSettingsForm() { for (const key of settingKeys) { const node = $(key); - if (node) node.value = state.settings[key] ?? ''; + if (node) node.value = key === 'feriefridage_per_year' ? integerValue(state.settings[key]) : (state.settings[key] ?? ''); } } @@ -156,6 +156,10 @@ async function saveSettings(event) { for (const key of settingKeys) { settings[key] = Number($(key).value); } + if (!Number.isInteger(settings.feriefridage_per_year)) { + toast('Feriefridage/year must be a whole number'); + return; + } const data = await post({ action: 'saveSettings', settings }); state.settings = normalizeSettings(data.settings); state.plan = calculatePlan(state.settings, state.entries, state.corrections, state.feriefridageOverrides, state.flexOverrides); @@ -175,6 +179,10 @@ async function saveBalanceOverride(event) { toast('Balance must be a number'); return; } + if (kind === 'feriefridage' && value !== null && !Number.isInteger(value)) { + toast('Feriefridage must be whole days'); + return; + } const actions = { vacation: 'saveCorrection', @@ -364,7 +372,8 @@ function renderDashboard() { value: current.feriefridage, hint: 'Use before 31 August', tone: current.feriefridage > 0.001 ? 'warn' : '', - step: '0.01', + step: '1', + decimals: 0, overridden: hasCorrection(state.feriefridageOverrides, year, month), }, { @@ -374,6 +383,7 @@ function renderDashboard() { hint: 'Available today', tone: current.vacation < -0.001 ? 'bad' : 'good', step: '0.01', + decimals: 2, overridden: hasCorrection(state.corrections, year, month), }, { @@ -383,6 +393,7 @@ function renderDashboard() { hint: 'Current balance', tone: current.flex < -0.001 ? 'bad' : 'good', step: '0.25', + decimals: 2, overridden: hasCorrection(state.flexOverrides, year, month), }, ]; @@ -401,7 +412,7 @@ function renderDashboardBalanceCard(card, year, month) { return `
${escapeHtml(card.label)}
- +
${escapeHtml(card.hint)}
`; @@ -449,7 +460,7 @@ function renderEntries() { ${escapeHtml(entry.title)} ${escapeHtml(source)}
${formatDate(entry.start_date)} → ${formatDate(entry.end_date)}${Number(entry.allow_unpaid) ? ' unpaid' : ''} ${days(summary.workdays)} ${days(summary.usedVacation)} - ${days(summary.usedFeriefridage)} + ${ffDays(summary.usedFeriefridage)} ${hours(summary.usedFlex)}
${days(summary.usedFlexDays)}d ${escapeHtml(entry.status)} @@ -508,7 +519,7 @@ function renderMonthRow(month) { ? month.entries.map((item) => ` ${escapeHtml(item.title)} - ${days(item.workdays)} wd · ${days(item.usedVacation)} vd · ${days(item.usedFeriefridage)} ff · ${hours(item.usedFlex)} fl + ${days(item.workdays)} wd · ${days(item.usedVacation)} vd · ${ffDays(item.usedFeriefridage)} ff · ${hours(item.usedFlex)} fl ${formatDate(item.start_date)} → ${formatDate(item.end_date)} `).join('') @@ -516,10 +527,10 @@ function renderMonthRow(month) { return ` - ${month.year} - ${monthNames[month.month - 1]}${month.expiredFeriefridage > 0 ? `
Expired FF: ${days(month.expiredFeriefridage)}` : ''} + ${month.year} - ${monthNames[month.month - 1]}${month.expiredFeriefridage > 0 ? `
Expired FF: ${ffDays(month.expiredFeriefridage)}` : ''} ${entries} - + ${days(month.usedUnpaid)} ${days(month.availableDays)} @@ -600,7 +611,7 @@ function calculatePlan(settings, entries, corrections = {}, feriefridageOverride if (month === Number(settings.feriefridage_expiry_month || 8) + 1 && feriefridageBalance > 0.001) { current.expiredFeriefridage = feriefridageBalance; if (year === today.getFullYear()) { - warnings.push({ level: 'bad', text: `${days(feriefridageBalance)} feriefridage expired on 31 August ${year}. They must be used before that date.` }); + warnings.push({ level: 'bad', text: `${ffDays(feriefridageBalance)} feriefridage expired on 31 August ${year}. They must be used before that date.` }); } feriefridageBalance = 0; } @@ -673,7 +684,7 @@ function calculatePlan(settings, entries, corrections = {}, feriefridageOverride } if (month === Number(settings.feriefridage_expiry_month || 8) && year === today.getFullYear() && feriefridageBalance > 0.001) { - warnings.push({ level: 'warn', text: `${days(feriefridageBalance)} feriefridage remain at the end of August ${year}; they must be used before 31 August.` }); + warnings.push({ level: 'warn', text: `${ffDays(feriefridageBalance)} feriefridage remain at the end of August ${year}; they must be used before 31 August.` }); } if (current.hasVacationOverride) vacationBalance = current.vacationOverride; @@ -691,7 +702,7 @@ function calculatePlan(settings, entries, corrections = {}, feriefridageOverride warnings.push({ level: 'bad', text: `Vacation balance is negative in ${monthNames[month - 1]} ${year}: ${days(vacationBalance)} days.` }); } if (feriefridageBalance < -0.001) { - warnings.push({ level: 'bad', text: `Feriefridage balance is negative in ${monthNames[month - 1]} ${year}: ${days(feriefridageBalance)} days.` }); + warnings.push({ level: 'bad', text: `Feriefridage balance is negative in ${monthNames[month - 1]} ${year}: ${ffDays(feriefridageBalance)} days.` }); } if (flexBalance < -0.001) { warnings.push({ level: 'bad', text: `FLEX balance is negative in ${monthNames[month - 1]} ${year}: ${hours(flexBalance)}.` }); @@ -808,7 +819,7 @@ function calculateCurrentSnapshot(settings, entries, corrections = {}, feriefrid } if (vacationBalance < -0.001) warnings.push({ level: 'bad', text: `Vacation balance is negative today: ${days(vacationBalance)} days.` }); - if (feriefridageBalance < -0.001) warnings.push({ level: 'bad', text: `Feriefridage balance is negative today: ${days(feriefridageBalance)} days.` }); + if (feriefridageBalance < -0.001) warnings.push({ level: 'bad', text: `Feriefridage balance is negative today: ${ffDays(feriefridageBalance)} days.` }); if (flexBalance < -0.001) warnings.push({ level: 'bad', text: `FLEX balance is negative today: ${hours(flexBalance)}.` }); if (today.getMonth() + 1 <= Number(settings.feriefridage_expiry_month || 8) && feriefridageBalance > 0.001) { const remainingAfterApprovedPlans = feriefridageRemainingAfterApprovedPlans(settings, entries, today, { @@ -817,7 +828,7 @@ function calculateCurrentSnapshot(settings, entries, corrections = {}, feriefrid flexBalance, }); if (remainingAfterApprovedPlans > 0.001) { - warnings.push({ level: 'warn', text: `${days(remainingAfterApprovedPlans)} feriefridage need approved plan before 31 August.` }); + warnings.push({ level: 'warn', text: `${ffDays(remainingAfterApprovedPlans)} feriefridage need approved plan before 31 August.` }); } } @@ -1023,6 +1034,19 @@ function days(value) { return formatNumber(value, 2); } +function ffDays(value) { + return formatNumber(value, 0); +} + +function integerValue(value) { + if (value === null || value === undefined || value === '') return ''; + return String(Math.round(Number(value || 0))); +} + +function inputNumberValue(value, decimals = 2) { + return Number(Number(value || 0).toFixed(decimals)); +} + function hours(value) { return `${formatNumber(value, 2)}h`; } diff --git a/index.html b/index.html index 40b6ba9..f8ce857 100644 --- a/index.html +++ b/index.html @@ -124,7 +124,7 @@ - +