Style update
This commit is contained in:
parent
981375725f
commit
c6e206ffe7
3 changed files with 59 additions and 6 deletions
56
app.js
56
app.js
|
|
@ -42,6 +42,7 @@ function bindEvents() {
|
||||||
$('settingsDialog').addEventListener('click', closeDialogOnBackdrop);
|
$('settingsDialog').addEventListener('click', closeDialogOnBackdrop);
|
||||||
$('entryForm').addEventListener('submit', saveEntry);
|
$('entryForm').addEventListener('submit', saveEntry);
|
||||||
$('settingsForm').addEventListener('submit', saveSettings);
|
$('settingsForm').addEventListener('submit', saveSettings);
|
||||||
|
$('deleteHiddenEntriesBtn').addEventListener('click', deleteEntriesOutsideDisplayedMonths);
|
||||||
['startDate', 'endDate', 'nonWorkingDays'].forEach((id) => $(id).addEventListener('input', updateWorkdayPreview));
|
['startDate', 'endDate', 'nonWorkingDays'].forEach((id) => $(id).addEventListener('input', updateWorkdayPreview));
|
||||||
$('cancelEditBtn').addEventListener('click', resetEntryForm);
|
$('cancelEditBtn').addEventListener('click', resetEntryForm);
|
||||||
}
|
}
|
||||||
|
|
@ -186,6 +187,43 @@ async function deleteEntry(id) {
|
||||||
toast('Entry deleted');
|
toast('Entry deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteEntriesOutsideDisplayedMonths() {
|
||||||
|
const hiddenEntries = state.entries.filter((entry) => !entryOverlapsDisplayedRange(entry));
|
||||||
|
if (!hiddenEntries.length) {
|
||||||
|
toast('No hidden entries to delete');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(`Delete ${hiddenEntries.length} entries outside the currently displayed months?`)) return;
|
||||||
|
|
||||||
|
let latestEntries = state.entries;
|
||||||
|
for (const entry of hiddenEntries) {
|
||||||
|
const data = await post({ action: 'deleteEntry', id: entry.id });
|
||||||
|
latestEntries = data.entries.map(normalizeEntry);
|
||||||
|
}
|
||||||
|
state.entries = latestEntries;
|
||||||
|
state.plan = calculatePlan(state.settings, state.entries, state.corrections, state.feriefridageOverrides, state.flexOverrides);
|
||||||
|
renderAll();
|
||||||
|
toast('Hidden entries deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
function entriesInDisplayedMonths() {
|
||||||
|
return state.entries.filter(entryOverlapsDisplayedRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryOverlapsDisplayedRange(entry) {
|
||||||
|
if (!state.plan?.range) return true;
|
||||||
|
return entry.start_date <= state.plan.range.end && entry.end_date >= state.plan.range.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPastEntry(entry) {
|
||||||
|
return entry.end_date < toIsoDate(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPastMonth(month) {
|
||||||
|
const today = new Date();
|
||||||
|
return month.year < today.getFullYear() || (month.year === today.getFullYear() && month.month < today.getMonth() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
async function post(payload) {
|
async function post(payload) {
|
||||||
const res = await fetch('api.php', {
|
const res = await fetch('api.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -317,18 +355,19 @@ function renderWarnings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEntries() {
|
function renderEntries() {
|
||||||
$('entriesHeaderTitle').textContent = `VACATIONS (${state.entries.length})`;
|
const visibleEntries = entriesInDisplayedMonths();
|
||||||
|
$('entriesHeaderTitle').textContent = `VACATIONS (${visibleEntries.length})`;
|
||||||
const tbody = $('entriesTable').querySelector('tbody');
|
const tbody = $('entriesTable').querySelector('tbody');
|
||||||
if (!state.entries.length) {
|
if (!visibleEntries.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="8" class="muted">No entries yet.</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="7" class="muted">No entries in displayed months.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = state.entries.map((entry) => {
|
tbody.innerHTML = visibleEntries.map((entry) => {
|
||||||
const summary = state.plan.entrySummaries[entry.id] || { workdays: 0, grossWorkdays: 0, nonWorkingDays: 0, usedVacation: 0, usedFeriefridage: 0, usedFlex: 0, usedFlexDays: 0 };
|
const summary = state.plan.entrySummaries[entry.id] || { workdays: 0, grossWorkdays: 0, nonWorkingDays: 0, usedVacation: 0, usedFeriefridage: 0, usedFlex: 0, usedFlexDays: 0 };
|
||||||
const source = sourceLabels[entry.source] || entry.source;
|
const source = sourceLabels[entry.source] || entry.source;
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr class="${isPastEntry(entry) ? 'past-entry' : ''}">
|
||||||
<td><strong>${escapeHtml(entry.title)}</strong> <span class="muted">${escapeHtml(source)}</span><br><span class="muted entry-date">${escapeHtml(entry.start_date)} → ${escapeHtml(entry.end_date)}</span>${Number(entry.allow_unpaid) ? ' <span class="pill unpaid">unpaid</span>' : ''}</td>
|
<td><strong>${escapeHtml(entry.title)}</strong> <span class="muted">${escapeHtml(source)}</span><br><span class="muted entry-date">${escapeHtml(entry.start_date)} → ${escapeHtml(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 strong-num">${days(summary.workdays)}</td>
|
||||||
<td class="num">${days(summary.usedVacation)}</td>
|
<td class="num">${days(summary.usedVacation)}</td>
|
||||||
|
|
@ -394,7 +433,7 @@ function renderMonthRow(month) {
|
||||||
: '<span class="muted">—</span>';
|
: '<span class="muted">—</span>';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<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: ${days(month.expiredFeriefridage)}</span>` : ''}</td>
|
||||||
<td class="entries">${entries}</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.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>
|
||||||
|
|
@ -580,6 +619,7 @@ function calculatePlan(settings, entries, corrections = {}, feriefridageOverride
|
||||||
const totalDays = vacationBalance + feriefridageBalance + (flexBalance / avgDayHours);
|
const totalDays = vacationBalance + feriefridageBalance + (flexBalance / avgDayHours);
|
||||||
return {
|
return {
|
||||||
months,
|
months,
|
||||||
|
range: { start: toIsoDate(startDate), end: toIsoDate(endDate) },
|
||||||
warnings: dedupeWarnings(warnings),
|
warnings: dedupeWarnings(warnings),
|
||||||
current: calculateCurrentSnapshot(settings, entries, corrections, feriefridageOverrides, flexOverrides),
|
current: calculateCurrentSnapshot(settings, entries, corrections, feriefridageOverrides, flexOverrides),
|
||||||
entrySummaries,
|
entrySummaries,
|
||||||
|
|
@ -817,6 +857,10 @@ function dateFromParts(year, month, day) {
|
||||||
return new Date(year, month - 1, day, 12, 0, 0);
|
return new Date(year, month - 1, day, 12, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toIsoDate(date) {
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function parseDate(value) {
|
function parseDate(value) {
|
||||||
const [year, month, day] = value.split('-').map(Number);
|
const [year, month, day] = value.split('-').map(Number);
|
||||||
return dateFromParts(year, month, day);
|
return dateFromParts(year, month, day);
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,9 @@
|
||||||
<label>Mon–Thu hours<input id="normal_day_hours" type="number" step="0.25"></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>
|
<label>Friday hours<input id="friday_hours" type="number" step="0.25"></label>
|
||||||
<p class="full help-text">Monthly vacation, feriefridage, and FLEX balances are edited directly in the planner table. Clear a value to return to the calculated balance.</p>
|
<p class="full help-text">Monthly vacation, feriefridage, and FLEX balances are edited directly in the planner table. Clear a value to return to the calculated balance.</p>
|
||||||
|
<div class="full danger-zone">
|
||||||
|
<button type="button" id="deleteHiddenEntriesBtn" class="danger">Delete entries outside displayed months</button>
|
||||||
|
</div>
|
||||||
<div class="actions full">
|
<div class="actions full">
|
||||||
<button type="submit">Save settings</button>
|
<button type="submit">Save settings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ h2 { margin-bottom: 0; font-size: 1.1rem; }
|
||||||
.compact-entries-table .pill { padding: .1rem .38rem; font-size: .68rem; }
|
.compact-entries-table .pill { padding: .1rem .38rem; font-size: .68rem; }
|
||||||
.compact-entries-table .entry-actions { gap: .25rem; }
|
.compact-entries-table .entry-actions { gap: .25rem; }
|
||||||
.compact-entries-table button.small { padding: .22rem .38rem; font-size: .68rem; }
|
.compact-entries-table button.small { padding: .22rem .38rem; font-size: .68rem; }
|
||||||
|
.compact-entries-table tr.past-entry { opacity: .58; }
|
||||||
|
.compact-entries-table tr.past-entry td:first-child strong { text-decoration: line-through; }
|
||||||
.header-warning {
|
.header-warning {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -248,6 +250,7 @@ button.small { padding: .36rem .55rem; font-size: .78rem; }
|
||||||
button.danger { background: #5a2320; color: var(--ink); border: 1px solid #5a2320; }
|
button.danger { background: #5a2320; color: var(--ink); border: 1px solid #5a2320; }
|
||||||
.actions { display: flex; justify-content: flex-end; gap: .6rem; }
|
.actions { display: flex; justify-content: flex-end; gap: .6rem; }
|
||||||
.help-text { color: var(--muted); margin: 0; font-size: .9rem; }
|
.help-text { color: var(--muted); margin: 0; font-size: .9rem; }
|
||||||
|
.danger-zone { display: flex; justify-content: flex-end; }
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
.quick-flex-form {
|
.quick-flex-form {
|
||||||
|
|
@ -290,6 +293,9 @@ table { width: 100%; border-collapse: collapse; font-size: .92rem; }
|
||||||
th, td { padding: .75rem .65rem; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
|
th, td { padding: .75rem .65rem; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
|
||||||
th { color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .04em; }
|
th { color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .04em; }
|
||||||
tbody tr:hover { background: rgba(255,255,255,.04); }
|
tbody tr:hover { background: rgba(255,255,255,.04); }
|
||||||
|
.planner-body-table tr.past-month { opacity: .55; }
|
||||||
|
.planner-body-table tr.past-month td:first-child { color: var(--muted); }
|
||||||
|
.planner-body-table tr.past-month .balance-override-input { opacity: .72; }
|
||||||
.num { text-align: right; font-variant-numeric: tabular-nums; }
|
.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
.strong-num { font-weight: 900; color: var(--ink); }
|
.strong-num { font-weight: 900; color: var(--ink); }
|
||||||
.muted { color: var(--muted); }
|
.muted { color: var(--muted); }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue