Feature: Welpenwachstum — SVG-Kurve, Stats-Zeile, Veränderung-Spalte im Gewichtsverlauf — SW by-v487, APP_VER 464

This commit is contained in:
rene 2026-04-29 10:49:40 +02:00
parent dc737d0c48
commit e507f4c086
3 changed files with 82 additions and 7 deletions

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '463'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '464'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {

View file

@ -465,20 +465,95 @@ window.Page_litters = (() => {
el.innerHTML = `<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Noch keine Messungen eingetragen.</p>`;
return;
}
// Chronologisch für Chart (API liefert DESC)
const asc = [...weights].reverse();
const first = asc[0].gewicht_g;
const last = asc[asc.length - 1].gewicht_g;
const gain = last - first;
const days = asc.length > 1
? Math.max(1, (new Date(asc[asc.length-1].gemessen_am) - new Date(asc[0].gemessen_am)) / 86400000)
: 1;
const dailyGain = asc.length > 1 ? (gain / days).toFixed(1) : '—';
// SVG Sparkline
const W = 400, H = 80;
const minW = Math.min(...asc.map(w => w.gewicht_g));
const maxW = Math.max(...asc.map(w => w.gewicht_g));
const range = maxW - minW || 1;
const pts = asc.map((w, i) => {
const x = asc.length === 1 ? W/2 : (i / (asc.length - 1)) * W;
const y = H - 8 - ((w.gewicht_g - minW) / range) * (H - 16);
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
const firstDate = asc[0].gemessen_am?.slice(5) || '';
const lastDate = asc[asc.length-1].gemessen_am?.slice(5) || '';
el.innerHTML = `
<!-- Stats-Zeile -->
<div style="display:flex;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-3)">
<div style="text-align:center">
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">Aktuell</div>
<div style="font-size:var(--text-base);font-weight:700;color:var(--c-primary)">${last} g</div>
</div>
<div style="text-align:center">
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">Zunahme</div>
<div style="font-size:var(--text-base);font-weight:700;color:${gain >= 0 ? 'var(--c-success)' : 'var(--c-danger)'}">
${gain >= 0 ? '+' : ''}${gain} g
</div>
</div>
<div style="text-align:center">
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">Ø tägl.</div>
<div style="font-size:var(--text-base);font-weight:700;color:var(--c-text)">${dailyGain} g</div>
</div>
<div style="text-align:center">
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">Messungen</div>
<div style="font-size:var(--text-base);font-weight:700;color:var(--c-text)">${weights.length}</div>
</div>
</div>
<!-- Wachstumskurve -->
${asc.length > 1 ? `
<div style="background:var(--c-surface);border-radius:var(--radius-md);
padding:var(--space-2) var(--space-2) var(--space-1);margin-bottom:var(--space-3)">
<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:80px;display:block" preserveAspectRatio="none">
<polyline points="${pts}" fill="none" stroke="var(--c-primary)"
stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
<!-- Punkte -->
${asc.map((w, i) => {
const x = (i / (asc.length - 1)) * W;
const y = H - 8 - ((w.gewicht_g - minW) / range) * (H - 16);
return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3"
fill="var(--c-primary)" stroke="var(--c-bg)" stroke-width="1.5"/>`;
}).join('')}
</svg>
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--c-text-muted);margin-top:2px">
<span>${firstDate}</span><span>${lastDate}</span>
</div>
</div>` : ''}
<!-- Tabelle -->
<table style="width:100%;font-size:var(--text-sm);border-collapse:collapse">
<thead>
<tr style="color:var(--c-text-secondary);font-size:var(--text-xs)">
<th style="text-align:left;padding:var(--space-1) var(--space-2)">Datum</th>
<th style="text-align:right;padding:var(--space-1) var(--space-2)">Gewicht</th>
<th style="text-align:right;padding:var(--space-1) var(--space-2)">Veränderung</th>
</tr>
</thead>
<tbody>
${weights.map((w, i) => `
<tr style="border-top:1px solid var(--c-border)${i === 0 ? ';font-weight:var(--weight-semibold)' : ''}">
<td style="padding:var(--space-1) var(--space-2)">${_fmtDate(w.gemessen_am)}</td>
<td style="padding:var(--space-1) var(--space-2);text-align:right">${w.gewicht_g} g</td>
</tr>`).join('')}
${weights.map((w, i) => {
const prev = weights[i + 1];
const diff = prev ? w.gewicht_g - prev.gewicht_g : null;
const diffStr = diff === null ? '—'
: `<span style="color:${diff >= 0 ? 'var(--c-success)' : 'var(--c-danger)'}">${diff >= 0 ? '+' : ''}${diff} g</span>`;
return `
<tr style="border-top:1px solid var(--c-border)${i === 0 ? ';font-weight:var(--weight-semibold)' : ''}">
<td style="padding:var(--space-1) var(--space-2)">${_fmtDate(w.gemessen_am)}</td>
<td style="padding:var(--space-1) var(--space-2);text-align:right">${w.gewicht_g} g</td>
<td style="padding:var(--space-1) var(--space-2);text-align:right">${diffStr}</td>
</tr>`;
}).join('')}
</tbody>
</table>`;
} catch (err) {

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v486';
const CACHE_VERSION = 'by-v487';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten