(function () { const textDecoder = new TextDecoder(); function decodePayload(raw) { if (!raw) { return null; } const bytes = Uint8Array.from(atob(raw), char => char.charCodeAt(0)); return JSON.parse(textDecoder.decode(bytes)); } function formatNumber(value) { const rounded = Math.round(value * 10) / 10; return Number.isInteger(rounded) ? String(rounded) : rounded.toLocaleString("de-DE", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); } function formatDateLabel(value) { const [year, month, day] = value.split("-"); return `${day}.${month}.`; } function toLocalIso(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 getCssVar(name, fallback) { const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return value || fallback; } function moodIconPath(sentiment) { return `/assets/icons/mood-${sentiment}.svg`; } function updateRangeOutputs() { document.querySelectorAll("[data-output-for]").forEach(output => { const input = document.querySelector(`[name="${output.dataset.outputFor}"]`); if (!input) { return; } const sync = () => { output.textContent = input.value; }; sync(); input.addEventListener("input", sync); }); } function initHeaderDatePicker() { document.querySelectorAll(".topbar-date-input").forEach(input => { input.addEventListener("change", () => { const form = input.closest("form"); if (form) { form.submit(); } }); }); } function sleepDurationPoints(hours, points) { if (hours < 4) { return Number(points.lt4 || 0); } if (hours >= 10) { return Number(points.h10plus || 0); } const anchors = { 4: Number(points.h4 || 0), 5: Number(points.h5 || 0), 6: Number(points.h6 || 0), 7: Number(points.h7 || 0), 8: Number(points.h8 || 0), 9: Number(points.h9 || 0), 10: Number(points.h10plus || 0), }; const lower = Math.floor(hours); const upper = Math.ceil(hours); if (lower === upper) { return anchors[lower] || 0; } const fraction = hours - lower; const lowerPoints = anchors[lower] || 0; const upperPoints = anchors[upper] || 0; return Math.round((lowerPoints + ((upperPoints - lowerPoints) * fraction)) * 10) / 10; } function bandPoints(value, bands) { for (const band of bands || []) { if (value >= Number(band.min || 0) && value <= Number(band.max || 0)) { return Number(band.points || 0); } } const last = (bands || []).slice(-1)[0]; return last ? Number(last.points || 0) : 0; } function sortedRatings(ratings) { return [...(ratings || [])].sort((a, b) => Number(a.min || 0) - Number(b.min || 0)); } function labelForScore(score, ratings) { for (const rating of ratings) { if (score >= Number(rating.min || 0) && score <= Number(rating.max || 0)) { return rating.label; } } if (!ratings.length) { return "unbewertet"; } return score < Number(ratings[0].min || 0) ? ratings[0].label : ratings[ratings.length - 1].label; } function capLabel(current, cap, ratings) { const order = ratings.map(item => item.label); const currentIndex = order.indexOf(current); const capIndex = order.indexOf(cap); if (currentIndex === -1 || capIndex === -1) { return current; } return currentIndex > capIndex ? cap : current; } function sentimentForLabel(label, ratings) { const order = ratings.map(item => item.label); const index = order.indexOf(label); if (index === -1 || order.length <= 1) { return "steady"; } const ratio = index / Math.max(order.length - 1, 1); if (ratio <= 0.1) { return "storm"; } if (ratio <= 0.35) { return "heavy"; } if (ratio <= 0.65) { return "steady"; } if (ratio <= 0.9) { return "bright"; } return "radiant"; } function evaluateEntry(entry, settings) { const ratings = sortedRatings(settings.ratings || []); const scoring = settings.scoring || {}; const components = { mood: Number(entry.mood) * Number(scoring.mood_multiplier || 0), energy: Number(entry.energy) * Number(scoring.energy_multiplier || 0), stress: (11 - Number(entry.stress)) * Number(scoring.stress_multiplier || 0), sleep_hours: sleepDurationPoints(Number(entry.sleep_hours), scoring.sleep_duration_points || {}), sleep_feeling: Number(entry.sleep_feeling) * Number(scoring.sleep_feeling_multiplier || 0), sport_minutes: bandPoints(Number(entry.sport_minutes), scoring.sport_bands || []), walk_minutes: bandPoints(Number(entry.walk_minutes), scoring.walk_bands || []), note: String(entry.note || "").trim() === "" ? 0 : Number(scoring.journal_points || 0), }; const total = Math.round(Object.values(components).reduce((sum, value) => sum + Number(value), 0) * 10) / 10; let label = labelForScore(total, ratings); for (const guardrail of settings.guardrails || []) { const moodMatch = Number(entry.mood) <= Number(guardrail.mood_max || 10); const energyMatch = guardrail.energy_max === null || guardrail.energy_max === "" ? true : Number(entry.energy) <= Number(guardrail.energy_max); if (moodMatch && energyMatch) { label = capLabel(label, guardrail.cap_label, ratings); } } return { total, label, components, sentiment: sentimentForLabel(label, ratings) }; } function initTrackPreview() { const card = document.querySelector("#live-score-card"); const form = document.querySelector("#tracker-form"); if (!card || !form) { return; } const payload = decodePayload(card.dataset.payload); if (!payload) { return; } const totalNode = card.querySelector("[data-preview-total]"); const labelNode = card.querySelector("[data-preview-label]"); const iconNode = card.querySelector("[data-preview-icon]"); const componentsNode = card.querySelector("[data-preview-components]"); const componentLabels = { mood: "Stimmung", energy: "Energie", stress: "Stress", sleep_hours: "Schlafdauer", sleep_feeling: "Schlafgefühl", sport_minutes: "Sport", walk_minutes: "Spaziergang", note: "Notiz", }; const collect = () => ({ mood: Number(form.elements.mood.value), energy: Number(form.elements.energy.value), stress: Number(form.elements.stress.value), sleep_hours: Number(form.elements.sleep_hours.value || 0), sleep_feeling: Number(form.elements.sleep_feeling.value), sport_minutes: Number(form.elements.sport_minutes.value || 0), walk_minutes: Number(form.elements.walk_minutes.value || 0), note: form.elements.note.value || "", }); const render = () => { const result = evaluateEntry(collect(), payload.settings); totalNode.textContent = formatNumber(result.total); labelNode.textContent = result.label; iconNode.src = moodIconPath(result.sentiment); card.dataset.sentiment = result.sentiment; document.body.dataset.trackMood = result.sentiment; componentsNode.innerHTML = Object.entries(result.components).map(([key, value]) => { return `