From 75529cbdab589425c30d23552a708420c7fb4405 Mon Sep 17 00:00:00 2001 From: rene Date: Mon, 13 Apr 2026 20:22:23 +0200 Subject: [PATCH] =?UTF-8?q?UX:=20Gewicht-Tab=20mit=20gro=C3=9Fem=20Chart?= =?UTF-8?q?=20und=20ohne=20Bezeichnungsfeld?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Aktuelles Gewicht groß prominent oben angezeigt - Delta zur letzten Messung mit ▲/▼ und Farbe (grün/orange) - SVG-Diagramm: glatte Bezier-Kurve, Gradient-Füllung, Rastlinien - Formular: kein "Bezeichnung"-Feld mehr bei Gewicht-Einträgen - Detail-Modal-Titel zeigt Gewichtswert statt leerer Bezeichnung - SW-Cache → by-v9 --- backend/static/js/pages/health.js | 149 +++++++++++++++++++++--------- backend/static/sw.js | 2 +- 2 files changed, 107 insertions(+), 44 deletions(-) diff --git a/backend/static/js/pages/health.js b/backend/static/js/pages/health.js index a20b9ac..2655c15 100644 --- a/backend/static/js/pages/health.js +++ b/backend/static/js/pages/health.js @@ -268,69 +268,128 @@ window.Page_health = (() => { } // ---------------------------------------------------------- - // GEWICHT — mit SVG-Diagramm + // GEWICHT — Großanzeige + SVG-Diagramm // ---------------------------------------------------------- function _renderGewicht(entries) { const addBtn = ``; - const sorted = [...entries].sort((a, b) => a.datum.localeCompare(b.datum)); - - const chart = sorted.length >= 2 ? _weightChart(sorted) : ''; - if (!entries.length) return UI.emptyState({ icon: '⚖️', title: 'Noch keine Gewichtseinträge', action: addBtn }); + const sorted = [...entries].sort((a, b) => a.datum.localeCompare(b.datum)); + const latest = sorted[sorted.length - 1]; + const prev = sorted.length >= 2 ? sorted[sorted.length - 2] : null; + const delta = prev ? (parseFloat(latest.wert) - parseFloat(prev.wert)) : null; + + const deltaHtml = delta !== null ? (() => { + const sign = delta > 0 ? '+' : ''; + const color = delta > 0 ? 'var(--c-warning)' : delta < 0 ? 'var(--c-success)' : 'var(--c-text-muted)'; + const arrow = delta > 0 ? '▲' : delta < 0 ? '▼' : '→'; + return `
+ ${arrow} ${sign}${delta.toFixed(1)} kg seit letzter Messung +
`; + })() : ''; + + const chart = sorted.length >= 2 ? _weightChart(sorted) : ''; + const items = sorted.slice().reverse().map(e => ` -
-
-
- ${e.wert} ${e.einheit || 'kg'} -
-
${UI.time.format(e.datum + 'T00:00:00')}
- ${e.notiz ? `
${_esc(e.notiz)}
` : ''} +
+
+ + ${UI.time.format(e.datum + 'T00:00:00')} + + + ${e.wert} ${e.einheit || 'kg'} +
+ ${e.notiz ? `
${_esc(e.notiz)}
` : ''}
`).join(''); return ` +
+
+ ${latest.wert} + kg +
+ ${deltaHtml} +
${chart ? `
${chart}
` : ''} -
${items}
+
${items}
${addBtn}
`; } function _weightChart(entries) { - const W = 320, H = 120, PAD = 24; - const vals = entries.map(e => parseFloat(e.wert)); - const min = Math.min(...vals); - const max = Math.max(...vals); - const range = max - min || 1; + const W = 340, H = 160, PX = 36, PY = 16, PB = 28; + const vals = entries.map(e => parseFloat(e.wert)); + const minV = Math.min(...vals); + const maxV = Math.max(...vals); + const range = maxV - minV || 1; + const innerW = W - PX * 2; + const innerH = H - PY - PB; - const pts = entries.map((e, i) => { - const x = PAD + (i / (entries.length - 1)) * (W - PAD * 2); - const y = PAD + (1 - (parseFloat(e.wert) - min) / range) * (H - PAD * 2); - return `${x},${y}`; - }); + const toX = i => PX + (i / (entries.length - 1)) * innerW; + const toY = v => PY + (1 - (v - minV) / range) * innerH; + const pts = entries.map((e, i) => [toX(i), toY(parseFloat(e.wert))]); - const dots = entries.map((e, i) => { - const [x, y] = pts[i].split(','); - return ``; + // Smooth bezier path + const linePath = pts.reduce((acc, [x, y], i) => { + if (i === 0) return `M ${x},${y}`; + const [px, py] = pts[i - 1]; + const cpx = (px + x) / 2; + return `${acc} C ${cpx},${py} ${cpx},${y} ${x},${y}`; + }, ''); + + const fillPath = `${linePath} L ${pts[pts.length - 1][0]},${H - PB} L ${pts[0][0]},${H - PB} Z`; + + // Grid lines + const gridLines = [0, 0.5, 1].map(frac => { + const v = maxV - frac * range; + const y = toY(v); + return ` + + + ${v.toFixed(1)} + `; }).join(''); - const labels = [ - `${entries[0].datum.slice(5)}`, - `${entries[entries.length - 1].datum.slice(5)}`, - `${max.toFixed(1)}`, - `${min.toFixed(1)}`, - ].join(''); + // X-axis labels + const xIdxs = entries.length <= 3 + ? entries.map((_, i) => i) + : [0, Math.round((entries.length - 1) / 2), entries.length - 1]; + const xLabels = [...new Set(xIdxs)].map(i => { + const [m, d] = entries[i].datum.slice(5).split('-'); + return `${d}.${m}.`; + }).join(''); + // Dots + const dots = pts.map(([x, y], i) => { + const isLast = i === pts.length - 1; + return ``; + }).join(''); + + const gId = `wg${Math.random().toString(36).slice(2, 7)}`; return ` - - + + + + + + + + ${gridLines} + + ${dots} - ${labels} + ${xLabels} `; } @@ -481,7 +540,10 @@ window.Page_health = (() => {
`; - UI.modal.open({ title: `${tabInfo.icon} ${_esc(entry.bezeichnung)}`, body }); + const modalTitle = entry.typ === 'gewicht' + ? `${tabInfo.icon} ${entry.wert} ${entry.einheit || 'kg'}` + : `${tabInfo.icon} ${_esc(entry.bezeichnung)}`; + UI.modal.open({ title: modalTitle, body }); document.getElementById('health-detail-edit')?.addEventListener('click', () => { UI.modal.close(); @@ -538,12 +600,13 @@ window.Page_health = (() => { const t = typ || _activeTab; const commonFields = ` -
- - -
+ ${t !== 'gewicht' ? ` +
+ + +
` : ''}