Feriefridage not decimal

This commit is contained in:
marijo 2026-06-25 11:53:44 +00:00
parent e0d2ab12d3
commit 36e09e7cc6
3 changed files with 46 additions and 14 deletions

10
api.php
View file

@ -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');

48
app.js
View file

@ -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 `
<article class="card ${card.tone}">
<div class="label">${escapeHtml(card.label)}</div>
<input class="value balance-override-input dashboard-balance-input ${card.overridden ? 'is-overridden' : ''}" type="number" step="${card.step}" value="${Number(Number(card.value || 0).toFixed(2))}" data-kind="${card.kind}" data-year="${year}" data-month="${month}" aria-label="${escapeHtml(card.label)} for current month">
<input class="value balance-override-input dashboard-balance-input ${card.overridden ? 'is-overridden' : ''}" type="number" step="${card.step}" value="${inputNumberValue(card.value, card.decimals)}" data-kind="${card.kind}" data-year="${year}" data-month="${month}" aria-label="${escapeHtml(card.label)} for current month">
<div class="hint">${escapeHtml(card.hint)}</div>
</article>
`;
@ -449,7 +460,7 @@ function renderEntries() {
<td><strong>${escapeHtml(entry.title)}</strong> <span class="muted">${escapeHtml(source)}</span><br><span class="muted entry-date">${formatDate(entry.start_date)} ${formatDate(entry.end_date)}</span>${Number(entry.allow_unpaid) ? ' <span class="pill unpaid">unpaid</span>' : ''}</td>
<td class="num strong-num">${days(summary.workdays)}</td>
<td class="num">${days(summary.usedVacation)}</td>
<td class="num">${days(summary.usedFeriefridage)}</td>
<td class="num">${ffDays(summary.usedFeriefridage)}</td>
<td class="num"><span title="${days(summary.usedFlexDays)} FLEX days">${hours(summary.usedFlex)}</span><br><span class="muted">${days(summary.usedFlexDays)}d</span></td>
<td><span class="pill ${entry.status === 'approved' ? 'approved' : ''}">${escapeHtml(entry.status)}</span></td>
<td>
@ -508,7 +519,7 @@ function renderMonthRow(month) {
? month.entries.map((item) => `
<span class="month-entry ${isPastEntry(item) ? 'past-vacation-entry' : ''}">
<strong>${escapeHtml(item.title)}</strong>
<small class="planner-entry-meta"><strong>${days(item.workdays)} wd</strong> · ${days(item.usedVacation)} vd · ${days(item.usedFeriefridage)} ff · ${hours(item.usedFlex)} fl</small>
<small class="planner-entry-meta"><strong>${days(item.workdays)} wd</strong> · ${days(item.usedVacation)} vd · ${ffDays(item.usedFeriefridage)} ff · ${hours(item.usedFlex)} fl</small>
<small class="planner-entry-dates">${formatDate(item.start_date)} ${formatDate(item.end_date)}</small>
</span>
`).join('')
@ -516,10 +527,10 @@ function renderMonthRow(month) {
return `
<tr class="${isPastMonth(month) ? 'past-month' : ''}">
<td>${month.year} - ${monthNames[month.month - 1]}${month.expiredFeriefridage > 0 ? `<br><span class="warn-text">Expired FF: ${days(month.expiredFeriefridage)}</span>` : ''}</td>
<td>${month.year} - ${monthNames[month.month - 1]}${month.expiredFeriefridage > 0 ? `<br><span class="warn-text">Expired FF: ${ffDays(month.expiredFeriefridage)}</span>` : ''}</td>
<td class="entries">${entries}</td>
<td class="num ${month.vacationBalance < -0.001 ? 'bad-text' : ''}"><input class="balance-override-input ${month.hasVacationOverride ? 'is-overridden' : ''}" type="number" step="0.01" value="${month.hasVacationOverride ? Number(month.vacationOverride) : Number(month.vacationBalance.toFixed(2))}" data-kind="vacation" data-year="${month.year}" data-month="${month.month}" aria-label="Vacation days for ${monthNames[month.month - 1]} ${month.year}"></td>
<td class="num ${month.feriefridageBalance < -0.001 ? 'bad-text' : ''}"><input class="balance-override-input ${month.hasFeriefridageOverride ? 'is-overridden' : ''}" type="number" step="0.01" value="${month.hasFeriefridageOverride ? Number(month.feriefridageOverride) : Number(month.feriefridageBalance.toFixed(2))}" data-kind="feriefridage" data-year="${month.year}" data-month="${month.month}" aria-label="Feriefridage for ${monthNames[month.month - 1]} ${month.year}"></td>
<td class="num ${month.feriefridageBalance < -0.001 ? 'bad-text' : ''}"><input class="balance-override-input ${month.hasFeriefridageOverride ? 'is-overridden' : ''}" type="number" step="1" value="${inputNumberValue(month.hasFeriefridageOverride ? month.feriefridageOverride : month.feriefridageBalance, 0)}" data-kind="feriefridage" data-year="${month.year}" data-month="${month.month}" aria-label="Feriefridage for ${monthNames[month.month - 1]} ${month.year}"></td>
<td class="num ${month.flexBalance < -0.001 ? 'bad-text' : ''}"><input class="balance-override-input ${month.hasFlexOverride ? 'is-overridden' : ''}" type="number" step="0.25" value="${month.hasFlexOverride ? Number(month.flexOverride) : Number(month.flexBalance.toFixed(2))}" data-kind="flex" data-year="${month.year}" data-month="${month.month}" aria-label="FLEX hours for ${monthNames[month.month - 1]} ${month.year}"></td>
<td class="num unpaid-col ${month.usedUnpaid > 0.001 ? 'warn-text' : ''}">${days(month.usedUnpaid)}</td>
<td class="num important-col ${balanceClass}">${days(month.availableDays)}</td>
@ -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`;
}

View file

@ -124,7 +124,7 @@
<label>Previous months shown<input id="previous_months" type="number" step="1" min="0" max="120"></label>
<label>Future months shown<input id="future_months" type="number" step="1" min="0" max="120"></label>
<label>Vacation/month<input id="vacation_days_per_month" type="number" step="0.01"></label>
<label>Feriefridage/year<input id="feriefridage_per_year" type="number" step="0.01"></label>
<label>Feriefridage/year<input id="feriefridage_per_year" type="number" step="1"></label>
<label>FF deadline month<input id="feriefridage_expiry_month" type="number" step="1" min="1" max="12"></label>
<label>MonThu hours<input id="normal_day_hours" type="number" step="0.25"></label>
<label>Friday hours<input id="friday_hours" type="number" step="0.25"></label>