Feriefridage not decimal
This commit is contained in:
parent
e0d2ab12d3
commit
36e09e7cc6
3 changed files with 46 additions and 14 deletions
10
api.php
10
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');
|
||||
|
|
|
|||
48
app.js
48
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 `
|
||||
<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`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>Mon–Thu 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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue