diff --git a/app.js b/app.js
index 1852910..738b72f 100644
--- a/app.js
+++ b/app.js
@@ -42,6 +42,7 @@ function bindEvents() {
$('settingsDialog').addEventListener('click', closeDialogOnBackdrop);
$('entryForm').addEventListener('submit', saveEntry);
$('settingsForm').addEventListener('submit', saveSettings);
+ $('deleteHiddenEntriesBtn').addEventListener('click', deleteEntriesOutsideDisplayedMonths);
['startDate', 'endDate', 'nonWorkingDays'].forEach((id) => $(id).addEventListener('input', updateWorkdayPreview));
$('cancelEditBtn').addEventListener('click', resetEntryForm);
}
@@ -186,6 +187,43 @@ async function deleteEntry(id) {
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) {
const res = await fetch('api.php', {
method: 'POST',
@@ -317,18 +355,19 @@ function renderWarnings() {
}
function renderEntries() {
- $('entriesHeaderTitle').textContent = `VACATIONS (${state.entries.length})`;
+ const visibleEntries = entriesInDisplayedMonths();
+ $('entriesHeaderTitle').textContent = `VACATIONS (${visibleEntries.length})`;
const tbody = $('entriesTable').querySelector('tbody');
- if (!state.entries.length) {
- tbody.innerHTML = '
| No entries yet. |
';
+ if (!visibleEntries.length) {
+ tbody.innerHTML = '| No entries in displayed months. |
';
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 source = sourceLabels[entry.source] || entry.source;
return `
-
+
${escapeHtml(entry.title)} ${escapeHtml(source)} ${escapeHtml(entry.start_date)} → ${escapeHtml(entry.end_date)}${Number(entry.allow_unpaid) ? ' unpaid' : ''} |
${days(summary.workdays)} |
${days(summary.usedVacation)} |
@@ -394,7 +433,7 @@ function renderMonthRow(month) {
: '—';
return `
-
+
${month.year} - ${monthNames[month.month - 1]}${month.expiredFeriefridage > 0 ? ` Expired FF: ${days(month.expiredFeriefridage)}` : ''} |
${entries} |
|
@@ -580,6 +619,7 @@ function calculatePlan(settings, entries, corrections = {}, feriefridageOverride
const totalDays = vacationBalance + feriefridageBalance + (flexBalance / avgDayHours);
return {
months,
+ range: { start: toIsoDate(startDate), end: toIsoDate(endDate) },
warnings: dedupeWarnings(warnings),
current: calculateCurrentSnapshot(settings, entries, corrections, feriefridageOverrides, flexOverrides),
entrySummaries,
@@ -817,6 +857,10 @@ function dateFromParts(year, month, day) {
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) {
const [year, month, day] = value.split('-').map(Number);
return dateFromParts(year, month, day);
diff --git a/index.html b/index.html
index 27ff8bf..c4166cb 100644
--- a/index.html
+++ b/index.html
@@ -126,6 +126,9 @@
Monthly vacation, feriefridage, and FLEX balances are edited directly in the planner table. Clear a value to return to the calculated balance.
+
+
+
diff --git a/style.css b/style.css
index 1d0d889..b5f2972 100644
--- a/style.css
+++ b/style.css
@@ -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 .entry-actions { gap: .25rem; }
.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 {
display: inline-flex;
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; }
.actions { display: flex; justify-content: flex-end; gap: .6rem; }
.help-text { color: var(--muted); margin: 0; font-size: .9rem; }
+.danger-zone { display: flex; justify-content: flex-end; }
.hidden { display: none !important; }
.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 { color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .04em; }
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; }
.strong-num { font-weight: 900; color: var(--ink); }
.muted { color: var(--muted); }