Sprint 19: Social, UX-Verbesserungen, Nerd2Noob-Hilfe
This commit is contained in:
parent
10d30bf565
commit
89d87030a2
18 changed files with 930 additions and 74 deletions
|
|
@ -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>
|
||||
` : '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue