(() => { const weeksEl = document.querySelector("#weeks"); if (!weeksEl) return; const appShell = document.querySelector(".app-shell"); const today = fromISO(appShell.dataset.today); const saveUrl = appShell.dataset.saveUrl; const statusUrl = appShell.dataset.statusUrl; const deleteUrl = appShell.dataset.deleteUrl; const savedChoices = JSON.parse(document.querySelector("#saved-choices").textContent); const savedStatuses = JSON.parse(document.querySelector("#saved-statuses").textContent); const monthLabel = document.querySelector("#visible-month"); const selectedLabel = document.querySelector("#selected-date"); const amountControls = document.querySelector("#amount-controls"); const customAmount = document.querySelector("#custom-amount"); const saveStatus = document.querySelector("#save-status"); const dayFormatter = new Intl.DateTimeFormat(undefined, { weekday: "long", month: "long", day: "numeric", year: "numeric" }); const monthFormatter = new Intl.DateTimeFormat(undefined, { month: "long", year: "numeric" }); let firstSunday = startOfWeek(addDays(today, -14)); let lastSunday = addDays(firstSunday, 7 * 13); let selectedButton = null; let loading = false; let previousScrollTop = 0; let lastExpansionAt = 0; function fromISO(value) { const [year, month, day] = value.split("-").map(Number); return new Date(year, month - 1, day, 12); } function addDays(date, count) { const next = new Date(date); next.setDate(next.getDate() + count); return next; } function startOfWeek(date) { return addDays(date, -date.getDay()); } function iso(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } function createWeek(sunday) { const row = document.createElement("div"); row.className = "week"; row.dataset.sunday = iso(sunday); row.setAttribute("role", "row"); for (let index = 0; index < 7; index += 1) { const date = addDays(sunday, index); const button = document.createElement("button"); button.type = "button"; button.className = `day${index === 0 || index === 6 ? " is-weekend" : ""}${iso(date) === iso(today) ? " is-today" : ""}`; button.dataset.date = iso(date); button.innerHTML = `${date.getDate()}`; button.setAttribute("role", "gridcell"); button.setAttribute("aria-label", dayFormatter.format(date)); if (iso(date) === iso(today)) button.setAttribute("aria-current", "date"); applySavedChoice(button); applySavedStatus(button); button.addEventListener("click", () => selectDate(button, date)); row.appendChild(button); } return row; } function appendWeeks(count) { const fragment = document.createDocumentFragment(); for (let i = 0; i < count; i += 1) { lastSunday = addDays(lastSunday, 7); fragment.appendChild(createWeek(lastSunday)); } weeksEl.appendChild(fragment); } function prependWeeks(count) { const oldHeight = weeksEl.scrollHeight; const fragment = document.createDocumentFragment(); for (let i = count; i >= 1; i -= 1) fragment.appendChild(createWeek(addDays(firstSunday, -7 * i))); firstSunday = addDays(firstSunday, -7 * count); weeksEl.prepend(fragment); weeksEl.scrollTop += weeksEl.scrollHeight - oldHeight; } function selectDate(button, date) { selectedButton?.classList.remove("is-selected"); selectedButton?.removeAttribute("aria-selected"); selectedButton = button; button.classList.add("is-selected"); button.setAttribute("aria-selected", "true"); selectedLabel.textContent = dayFormatter.format(date); amountControls.hidden = false; saveStatus.textContent = savedChoices[iso(date)] === undefined ? "Choose an amount." : `${formatAmount(savedChoices[iso(date)])} saved.`; customAmount.value = ""; updateStatusButtons(savedStatuses[iso(date)] || "worked"); } function formatAmount(cents) { return new Intl.NumberFormat(undefined, { style: "currency", currency: "EUR" }).format(cents / 100); } function applySavedChoice(button) { const cents = savedChoices[button.dataset.date]; button.classList.toggle("has-amount", cents !== undefined); const amountLabel = button.querySelector(".day-amount"); if (amountLabel) amountLabel.textContent = cents === undefined ? "" : formatAmount(cents); } function applySavedStatus(button) { const status = savedStatuses[button.dataset.date] || "worked"; button.classList.toggle("is-vacation", status === "vacation"); button.classList.toggle("is-sick", status === "sick"); } function updateStatusButtons(status) { document.querySelectorAll("[data-status]").forEach((button) => { const selected = button.dataset.status === status; button.classList.toggle("is-active", selected); button.setAttribute("aria-pressed", String(selected)); }); } async function saveAmount(amount) { if (!selectedButton) return; saveStatus.textContent = "Saving…"; try { const response = await fetch(saveUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ date: selectedButton.dataset.date, amount }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "Could not save the amount."); savedChoices[result.date] = result.amount_cents; applySavedChoice(selectedButton); saveStatus.textContent = `${formatAmount(result.amount_cents)} saved.`; } catch (error) { saveStatus.textContent = error.message; } } async function saveDayStatus(status) { if (!selectedButton) return; saveStatus.textContent = "Saving…"; try { const response = await fetch(statusUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ date: selectedButton.dataset.date, status }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "Could not save the day status."); savedStatuses[result.date] = result.status; applySavedStatus(selectedButton); updateStatusButtons(result.status); saveStatus.textContent = `${result.status[0].toUpperCase()}${result.status.slice(1)} saved.`; } catch (error) { saveStatus.textContent = error.message; } } async function deleteDateData() { if (!selectedButton) return; const selectedDate = selectedButton.dataset.date; if (!window.confirm(`Delete all saved data for ${dayFormatter.format(fromISO(selectedDate))}?`)) return; saveStatus.textContent = "Deleting…"; try { const response = await fetch(deleteUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ date: selectedDate }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "Could not delete the date data."); delete savedChoices[result.date]; delete savedStatuses[result.date]; applySavedChoice(selectedButton); applySavedStatus(selectedButton); updateStatusButtons("worked"); customAmount.value = ""; saveStatus.textContent = "All saved data for this date was deleted."; } catch (error) { saveStatus.textContent = error.message; } } function updateMonth() { const rows = [...weeksEl.querySelectorAll(".week")]; const marker = weeksEl.getBoundingClientRect().top + Math.min(100, weeksEl.clientHeight / 3); const visible = rows.find((row) => row.getBoundingClientRect().bottom >= marker) || rows[0]; if (visible) monthLabel.textContent = monthFormatter.format(addDays(fromISO(visible.dataset.sunday), 3)); } for (let i = 0; i < 14; i += 1) weeksEl.appendChild(createWeek(addDays(firstSunday, i * 7))); lastSunday = addDays(firstSunday, 13 * 7); updateMonth(); weeksEl.addEventListener("scroll", () => { if (loading) return; const currentScrollTop = weeksEl.scrollTop; const direction = currentScrollTop > previousScrollTop ? "down" : "up"; previousScrollTop = currentScrollTop; loading = true; requestAnimationFrame(() => { const now = Date.now(); const canExpand = now - lastExpansionAt > 250; if (canExpand && direction === "up" && weeksEl.scrollTop < 80) { prependWeeks(3); previousScrollTop = weeksEl.scrollTop; lastExpansionAt = now; } else if (canExpand && direction === "down" && weeksEl.scrollHeight - weeksEl.scrollTop - weeksEl.clientHeight < 160) { appendWeeks(3); lastExpansionAt = now; } updateMonth(); loading = false; }); }, { passive: true }); document.querySelectorAll("[data-today-button]").forEach((button) => { button.addEventListener("click", () => { const target = weeksEl.querySelector(`[data-date="${iso(today)}"]`); if (target) { target.scrollIntoView({ block: "center", behavior: "smooth" }); selectDate(target, today); } else { window.location.reload(); } }); }); document.querySelectorAll("[data-amount]").forEach((button) => { button.addEventListener("click", () => saveAmount(button.dataset.amount)); }); document.querySelectorAll("[data-status]").forEach((button) => { button.addEventListener("click", () => saveDayStatus(button.dataset.status)); }); document.querySelector("#custom-amount-form").addEventListener("submit", (event) => { event.preventDefault(); if (customAmount.reportValidity()) saveAmount(customAmount.value); }); document.querySelector("#delete-date-button").addEventListener("click", deleteDateData); })();