From da6eed8803ac29efe767c0141ca2a552f5b99cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20K=C3=BChn?= Date: Mon, 11 May 2026 19:04:36 +0200 Subject: [PATCH] Allow viewing past days + enforce edit cutoff server-side - Clicking any past day dot now opens a stats panel (read-only for days older than yesterday) - Entry form and delete buttons hidden for non-editable days - Backend silently restores locked offsets (< yesterday) on PATCH, preventing backdated edits - Negative buffer no longer shows green: badge and progress bar are amber/red when buf < 0 Co-Authored-By: Claude Sonnet 4.6 --- public/js/render.js | 35 +++++++++++++++++-------------- src/Controller/GoalController.php | 14 ++++++++++++- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/public/js/render.js b/public/js/render.js index 717f06c..abd1a83 100644 --- a/public/js/render.js +++ b/public/js/render.js @@ -37,7 +37,7 @@ function delGoal(id) { function selD(gid, off) { const g = state.goals.find(x => x.id === gid); - if (!g || !editable(g, off)) return; + if (!g) return; state.selDay[gid] = state.selDay[gid] === off ? null : off; render(); } @@ -70,10 +70,11 @@ function buildNameWrap(g) { } function buildPanel(g, off) { - const t = tOff(g), sets = g.sets[String(off)] || [], tot = dTot(g, off); - const lbl = off === t ? tr('heute') : tr('gestern'), k = g.id + '_' + off; + const t = tOff(g), sets = g.sets[String(off)] || [], tot = dTot(g, off), ed = editable(g, off); + const lbl = off === t ? tr('heute') : off === t - 1 ? tr('gestern') : null; + const k = g.id + '_' + off; const el = tpl('tpl-panel'); - el.querySelector('.dpanel-title').textContent = lbl + ' — ' + fd(o2d(g, off)); + el.querySelector('.dpanel-title').textContent = (lbl ? lbl + ' — ' : '') + fd(o2d(g, off)); el.querySelector('.dpanel-sub').textContent = tot + ' / ' + g.daily + ' ' + g.unit; const body = el.querySelector('.dpanel-body'); if (sets.length) { @@ -86,29 +87,31 @@ function buildPanel(g, off) { const strong = document.createElement('strong'); strong.textContent = s.amount; span.appendChild(strong); span.appendChild(document.createTextNode(' ' + g.unit)); const btn = row.querySelector('.sdel'); - btn.dataset.g = g.id; btn.dataset.o = off; btn.dataset.i = i; + if (ed) { btn.dataset.g = g.id; btn.dataset.o = off; btn.dataset.i = i; } else { btn.remove(); } body.appendChild(row); } } else { body.appendChild(tpl('tpl-nosets')); } - const addRow = tpl('tpl-add-row'); - const inp = addRow.querySelector('.num-in'); - inp.placeholder = g.daily; inp.value = state.addAmt[k] || ''; inp.dataset.k = k; inp.dataset.g = g.id; inp.dataset.o = off; - const abtn = addRow.querySelector('.btn-as'); - abtn.dataset.g = g.id; abtn.dataset.o = off; - addRow.querySelector('.ulbl').textContent = g.unit; - body.appendChild(addRow); + if (ed) { + const addRow = tpl('tpl-add-row'); + const inp = addRow.querySelector('.num-in'); + inp.placeholder = g.daily; inp.value = state.addAmt[k] || ''; inp.dataset.k = k; inp.dataset.g = g.id; inp.dataset.o = off; + const abtn = addRow.querySelector('.btn-as'); + abtn.dataset.g = g.id; abtn.dataset.o = off; + addRow.querySelector('.ulbl').textContent = g.unit; + body.appendChild(addRow); + } return el; } function buildCard(g) { const c = calc(g), t = c.tOff; - const fc = c.surplus > 0 ? 'var(--blue)' : c.st === 0 ? 'var(--green)' : c.dailyDelta <= 0 ? 'var(--green)' : c.dailyDelta <= g.daily * .2 ? 'var(--amber)' : 'var(--red)'; + const fc = c.surplus > 0 ? 'var(--blue)' : c.st === 0 && c.buf >= 0 ? 'var(--green)' : c.dailyDelta <= 0 ? 'var(--green)' : c.dailyDelta <= g.daily * .2 ? 'var(--amber)' : 'var(--red)'; let bc, bt; const bufStr = (c.buf > 0 ? '+' : '') + c.buf; if (c.ok && c.surplus > 0) { bc = 'b-buf'; bt = bufStr; } - else if (c.ok) { bc = 'b-done'; bt = bufStr; } + else if (c.ok && c.buf >= 0) { bc = 'b-done'; bt = bufStr; } else if (c.dailyDelta <= 0) { bc = 'b-ok'; bt = bufStr; } else if (c.dailyDelta <= g.daily * .2) { bc = 'b-warn'; bt = bufStr; } else { bc = 'b-danger'; bt = bufStr; } @@ -153,7 +156,7 @@ function buildCard(g) { const it = i === t, iy = i === t - 1, is = sel === i, ed = editable(g, i); const dot = tpl('tpl-dot'); dot.className = dcls(g, i) + (is ? ' rs' : it ? ' rt' : iy && t > 0 ? ' ry' : ''); - if (ed) { dot.dataset.g = g.id; dot.dataset.d = i; } + if (i <= t) { dot.dataset.g = g.id; dot.dataset.d = i; } dot.textContent = dlbl(g, i); dotsWrap.appendChild(dot); } @@ -266,7 +269,7 @@ function wire() { inp.onkeydown = function(e) { if (e.key === 'Enter') commitRen(gid); if (e.key === 'Escape') cancelRen(); }; inp.onblur = function() { commitRen(gid); }; }); - document.querySelectorAll('.de').forEach(d => { + document.querySelectorAll('.de, .dl').forEach(d => { d.onclick = function(e) { e.stopPropagation(); selD(this.dataset.g, parseInt(this.dataset.d, 10)); }; }); document.querySelectorAll('.btn-as').forEach(b => { diff --git a/src/Controller/GoalController.php b/src/Controller/GoalController.php index 2716976..70be6f2 100644 --- a/src/Controller/GoalController.php +++ b/src/Controller/GoalController.php @@ -84,7 +84,19 @@ class GoalController extends AbstractController if (isset($data['unit'])) $goal->setUnit((string)$data['unit']); if (isset($data['daily'])) $goal->setDaily((float)$data['daily']); if (isset($data['days'])) $goal->setDays((int)$data['days']); - if (isset($data['sets'])) $goal->setSets((array)$data['sets']); + if (isset($data['sets'])) { + $newSets = (array)$data['sets']; + $existing = $goal->getSets(); + $startTs = (clone $goal->getStart())->setTime(0, 0, 0)->getTimestamp(); + $todayOffset = (int)round((mktime(0, 0, 0) - $startTs) / 86400); + foreach ($existing as $offset => $entries) { + if ((int)$offset < $todayOffset - 1) $newSets[(string)$offset] = $entries; + } + foreach (array_keys($newSets) as $offset) { + if ((int)$offset < $todayOffset - 1 && !array_key_exists((string)$offset, $existing)) unset($newSets[$offset]); + } + $goal->setSets($newSets); + } $this->em->flush(); return new JsonResponse(['ok' => true]);