Sprint 19: Social, UX-Verbesserungen, Nerd2Noob-Hilfe

This commit is contained in:
rene 2026-04-17 23:53:50 +02:00
parent 10d30bf565
commit 89d87030a2
18 changed files with 930 additions and 74 deletions

View file

@ -321,6 +321,11 @@ window.Page_health = (() => {
} catch (err) {
// silent fail
}
try {
_data['gewicht_chart'] = await API.health.gewichtVerlauf(dogId);
} catch (err) {
_data['gewicht_chart'] = [];
}
}
// ----------------------------------------------------------
@ -347,15 +352,33 @@ window.Page_health = (() => {
_bindTabEvents(content);
}
// ----------------------------------------------------------
// EMPTY-STATE HELPER
// ----------------------------------------------------------
function _emptyState(icon, title, text, cta = '') {
return `<div class="empty-state">
<svg class="ph-icon empty-state-icon" aria-hidden="true">
<use href="/icons/phosphor.svg#${icon}"></use>
</svg>
<div class="empty-state-title">${title}</div>
${text ? `<p class="empty-state-text">${text}</p>` : ''}
${cta ? `<div class="empty-state-cta">${cta}</div>` : ''}
</div>`;
}
// ----------------------------------------------------------
// IMPFUNGEN — mit Ampel-Status
// ----------------------------------------------------------
function _renderImpfungen(entries) {
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Impfung eintragen</button>`;
const helpIcon = UI.help('Trage Impfdatum und nächsten Termin ein — wir erinnern dich rechtzeitig.');
if (!entries.length) return UI.emptyState({
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>', title: 'Noch keine Impfungen', text: 'Trage alle Impfungen ein, um nichts zu verpassen.', action: addBtn
});
if (!entries.length) return _emptyState(
'syringe',
'Noch keine Impfungen',
`Trage alle Impfungen ein, um nichts zu verpassen. ${helpIcon}`,
addBtn
);
const items = entries.map(e => {
const ampel = _impfAmpel(e.naechstes);
@ -453,7 +476,8 @@ window.Page_health = (() => {
</div>`;
})() : '';
const chart = sorted.length >= 2 ? _weightChart(sorted) : '';
const chartEntries = _data['gewicht_chart'] || [];
const chart = _renderWeightChart(chartEntries);
const items = sorted.slice().reverse().map(e => `
<div class="health-card" data-id="${e.id}" data-action="open-entry"
@ -478,7 +502,13 @@ window.Page_health = (() => {
</div>
${deltaHtml}
</div>
${chart ? `<div class="health-chart-wrap">${chart}</div>` : ''}
${chart ? `<div class="health-chart-wrap">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3) 0;display:flex;align-items:center;gap:var(--space-1)">
Gewichtsverlauf ${UI.help('Wird aus allen Einträgen mit Gewichtsangabe berechnet.')}
</div>
${chart}
</div>` : ''}
<div class="health-list" style="margin-top:var(--space-2)">${items}</div>
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>
`;
@ -556,6 +586,62 @@ window.Page_health = (() => {
`;
}
// ----------------------------------------------------------
// GEWICHTSVERLAUF-CHART (dedizierter Endpoint /health/gewicht)
// ----------------------------------------------------------
function _renderWeightChart(entries) {
// entries: [{datum, gewicht}, ...]
if (!entries || entries.length < 2) {
return '<p class="health-chart-empty">Mindestens 2 Gewichtseinträge für den Verlauf nötig.</p>';
}
const W = 300, H = 120, PAD = 24;
const weights = entries.map(e => e.gewicht);
const min = Math.min(...weights), max = Math.max(...weights);
const range = max - min || 1;
// x: gleichmäßig verteilt, y: normalisiert
const pts = entries.map((e, i) => {
const x = PAD + (i / (entries.length - 1)) * (W - 2 * PAD);
const y = H - PAD - ((e.gewicht - min) / range) * (H - 2 * PAD);
return { x, y, ...e };
});
const polyline = pts.map(p => `${p.x},${p.y}`).join(' ');
const area = `${pts[0].x},${H - PAD} ` + polyline + ` ${pts[pts.length - 1].x},${H - PAD}`;
// Datenpunkte + Tooltips als title-Elemente
const circles = pts.map(p =>
`<circle cx="${p.x}" cy="${p.y}" r="4" fill="var(--c-primary)">
<title>${p.datum}: ${p.gewicht} kg</title>
</circle>`
).join('');
const gId = `wg${Math.random().toString(36).slice(2, 7)}`;
return `
<div class="health-chart-wrap">
<div class="health-chart-title">Gewichtsverlauf</div>
<svg viewBox="0 0 ${W} ${H}" class="health-chart-svg" aria-label="Gewichtsverlauf">
<defs>
<linearGradient id="${gId}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--c-primary)" stop-opacity=".25"/>
<stop offset="100%" stop-color="var(--c-primary)" stop-opacity="0"/>
</linearGradient>
</defs>
<polygon points="${area}" fill="url(#${gId})"/>
<polyline points="${polyline}" fill="none" stroke="var(--c-primary)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
${circles}
<text x="${PAD - 2}" y="${H - PAD + 4}" font-size="9" fill="var(--c-text-secondary)" text-anchor="middle">${min}</text>
<text x="${PAD - 2}" y="${PAD + 4}" font-size="9" fill="var(--c-text-secondary)" text-anchor="middle">${max}</text>
</svg>
<div class="health-chart-labels">
<span>${entries[0].datum}</span>
<span>${entries[entries.length - 1].datum}</span>
</div>
</div>
`;
}
// ----------------------------------------------------------
// LÄUFIGKEIT — Timeline + Vorhersage
// ----------------------------------------------------------
@ -927,7 +1013,10 @@ window.Page_health = (() => {
const uploadField = t === 'dokument' ? `
<div class="form-group">
<label class="form-label">Datei (JPG, PNG, PDF)</label>
<label class="form-label">
Datei (JPG, PNG, PDF)
${UI.help('PDF oder Foto — z.B. Impfpass, Röntgenbild, Befund.')}
</label>
<input class="form-control" type="file" name="datei" accept="image/*,.pdf">
</div>
` : '';