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 @@
-
+