Feature: Hunde-Buch — druckbare HTML-Tagebuchansicht als PDF (SW by-v700)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
20a4936397
commit
c5030024b0
4 changed files with 761 additions and 2 deletions
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '699'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '700'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt
|
||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||
|
||||
|
|
@ -78,6 +78,7 @@ const App = (() => {
|
|||
wetter: { title: 'Wetter', module: null },
|
||||
ernaehrung: { title: 'Ernährung', module: null, requiresAuth: true },
|
||||
personality: { title: 'Persönlichkeitstest', module: null },
|
||||
reise: { title: 'Reise mit Hund', module: null },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -207,6 +207,15 @@ window.Page_dog_profile = (() => {
|
|||
border-color:transparent;font-weight:700">
|
||||
✨ Jahresrückblick ${new Date().getFullYear()}
|
||||
</button>` : ''}
|
||||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-buch-btn"
|
||||
style="background:linear-gradient(135deg,#5c3a10,#7a4f1a);color:#f5e4c0;
|
||||
border-color:transparent;font-weight:700">
|
||||
📖 Hunde-Buch erstellen
|
||||
</button>` : ''}
|
||||
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-timeline-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#timeline"></use></svg>
|
||||
Lebens-Timeline 🐾
|
||||
</button>` : ''}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -281,6 +290,14 @@ window.Page_dog_profile = (() => {
|
|||
_showWrappedModal(dog);
|
||||
});
|
||||
|
||||
document.getElementById('dp-buch-btn')?.addEventListener('click', () => {
|
||||
_showBuchModal(dog);
|
||||
});
|
||||
|
||||
document.getElementById('dp-timeline-btn')?.addEventListener('click', () => {
|
||||
_showTimelineModal(dog);
|
||||
});
|
||||
|
||||
// Edit- und Add-Klicks laufen über Event-Delegation in init() — keine direkten Listener nötig.
|
||||
}
|
||||
|
||||
|
|
@ -2110,6 +2127,265 @@ window.Page_dog_profile = (() => {
|
|||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// HUNDE-BUCH
|
||||
// ----------------------------------------------------------
|
||||
function _showBuchModal(dog) {
|
||||
const currentYear = new Date().getFullYear();
|
||||
let selectedJahr = String(currentYear);
|
||||
let nurFotos = false;
|
||||
let nurMeilensteine = false;
|
||||
|
||||
const modalEl = document.createElement('div');
|
||||
modalEl.style.cssText = `
|
||||
position:fixed;inset:0;z-index:9999;
|
||||
background:rgba(0,0,0,0.55);
|
||||
display:flex;align-items:center;justify-content:center;padding:16px;
|
||||
`;
|
||||
|
||||
const renderModal = () => {
|
||||
const years = [String(currentYear - 1), String(currentYear), 'alle'];
|
||||
const yearBtns = years.map(y => {
|
||||
const active = selectedJahr === y
|
||||
? 'background:#7a4f1a;color:#f5e4c0;border-color:#7a4f1a;'
|
||||
: 'background:#f5f0e8;color:#444;border-color:#e0d4b8;';
|
||||
const label = y === 'alle' ? 'Alle' : y;
|
||||
return `<button onclick="window._buchSetJahr('${y}')" style="
|
||||
border:1px solid;border-radius:8px;padding:8px 16px;
|
||||
font-size:0.9rem;cursor:pointer;font-family:inherit;
|
||||
${active}
|
||||
">${label}</button>`;
|
||||
}).join('');
|
||||
|
||||
const togStyle = (active) =>
|
||||
active
|
||||
? 'background:#7a4f1a;color:#f5e4c0;border-color:#7a4f1a;'
|
||||
: 'background:#f5f0e8;color:#444;border-color:#e0d4b8;';
|
||||
|
||||
modalEl.innerHTML = `
|
||||
<div style="
|
||||
background:#fff;border-radius:16px;padding:32px 24px;
|
||||
max-width:420px;width:100%;box-shadow:0 8px 40px rgba(0,0,0,0.2);
|
||||
font-family:system-ui,sans-serif;
|
||||
">
|
||||
<div style="font-size:1.4rem;font-weight:700;margin-bottom:4px">📖 Hunde-Buch erstellen</div>
|
||||
<div style="font-size:0.9rem;color:#888;margin-bottom:24px">
|
||||
Eine druckbare Ansicht der schönsten Einträge.<br>Im Browser als PDF speichern.
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-weight:600;margin-bottom:10px;color:#555;font-size:0.85rem;text-transform:uppercase;letter-spacing:.05em">Jahrgang</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">${yearBtns}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:20px;display:flex;flex-direction:column;gap:10px">
|
||||
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
|
||||
<button onclick="window._buchToggleFotos()" style="
|
||||
width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;
|
||||
${togStyle(nurFotos)}
|
||||
">${nurFotos ? '✓' : ''}</button>
|
||||
<span style="font-size:0.95rem">Nur Einträge mit Fotos</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
|
||||
<button onclick="window._buchToggleMeilensteine()" style="
|
||||
width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;
|
||||
${togStyle(nurMeilensteine)}
|
||||
">${nurMeilensteine ? '✓' : ''}</button>
|
||||
<span style="font-size:0.95rem">Nur Meilensteine</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:10px">
|
||||
<button onclick="window._buchOpen()" style="
|
||||
flex:1;background:#7a4f1a;color:#f5e4c0;border:none;border-radius:10px;
|
||||
padding:14px;font-size:1rem;font-weight:700;cursor:pointer;font-family:inherit;
|
||||
">📖 Buch öffnen</button>
|
||||
<button onclick="window._buchClose()" style="
|
||||
background:#f0f0f0;color:#555;border:none;border-radius:10px;
|
||||
padding:14px 18px;font-size:1rem;cursor:pointer;font-family:inherit;
|
||||
">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
window._buchSetJahr = (y) => { selectedJahr = y; renderModal(); };
|
||||
window._buchToggleFotos = () => { nurFotos = !nurFotos; renderModal(); };
|
||||
window._buchToggleMeilensteine = () => { nurMeilensteine = !nurMeilensteine; renderModal(); };
|
||||
window._buchClose = () => {
|
||||
modalEl.remove();
|
||||
delete window._buchSetJahr;
|
||||
delete window._buchToggleFotos;
|
||||
delete window._buchToggleMeilensteine;
|
||||
delete window._buchOpen;
|
||||
delete window._buchClose;
|
||||
};
|
||||
window._buchOpen = () => {
|
||||
const params = new URLSearchParams();
|
||||
if (selectedJahr !== 'alle') params.set('jahr', selectedJahr);
|
||||
if (nurFotos) params.set('nur_fotos', 'true');
|
||||
if (nurMeilensteine) params.set('nur_meilensteine', 'true');
|
||||
const url = `/api/dogs/${dog.id}/buch?${params.toString()}`;
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
renderModal();
|
||||
document.body.appendChild(modalEl);
|
||||
modalEl.addEventListener('click', e => { if (e.target === modalEl) window._buchClose(); });
|
||||
|
||||
const onKey = e => {
|
||||
if (e.key === 'Escape') { window._buchClose(); document.removeEventListener('keydown', onKey); }
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// LEBENS-TIMELINE
|
||||
// ----------------------------------------------------------
|
||||
async function _showTimelineModal(dog) {
|
||||
UI.modal.open({
|
||||
title: `Lebens-Timeline — ${_esc(dog.name)}`,
|
||||
body: `<div id="dp-timeline-body" style="min-height:200px;text-align:center;padding:var(--space-6)">
|
||||
<svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#spinner-gap"></use>
|
||||
</svg>
|
||||
</div>`,
|
||||
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
|
||||
size: 'large',
|
||||
});
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await API.get(`/dogs/${dog.id}/timeline`);
|
||||
} catch (e) {
|
||||
const b = document.getElementById('dp-timeline-body');
|
||||
if (b) b.innerHTML = `<p style="color:var(--c-danger)">Fehler: ${_esc(e.message)}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const wrap = document.getElementById('dp-timeline-body');
|
||||
if (!wrap) return;
|
||||
|
||||
const events = data.events || [];
|
||||
if (!events.length) {
|
||||
wrap.innerHTML = `<p style="color:var(--c-text-secondary);padding:var(--space-6)">
|
||||
Noch keine Einträge vorhanden. Beginne dein Tagebuch oder trage Gesundheitsdaten ein.
|
||||
</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const _KAT = {
|
||||
meilenstein: { color: '#8b5cf6', icon: 'star', label: 'Meilenstein' },
|
||||
tagebuch: { color: 'var(--c-primary)', icon: 'book-open', label: 'Tagebuch' },
|
||||
gesundheit: { color: '#ef4444', icon: 'heartbeat', label: 'Gesundheit' },
|
||||
training: { color: '#22c55e', icon: 'target', label: 'Training' },
|
||||
route: { color: '#3b82f6', icon: 'path', label: 'Route' },
|
||||
};
|
||||
|
||||
const _fmtDate = d => {
|
||||
if (!d) return '';
|
||||
try {
|
||||
const p = d.substring(0, 10).split('-');
|
||||
return `${p[2]}.${p[1]}.${p[0]}`;
|
||||
} catch { return d; }
|
||||
};
|
||||
|
||||
let lastYear = null;
|
||||
let html = '<div class="tl-wrap">';
|
||||
|
||||
for (const ev of events) {
|
||||
const year = ev.datum ? ev.datum.substring(0, 4) : null;
|
||||
if (year && year !== lastYear) {
|
||||
html += `<div class="tl-year">${_esc(year)}</div>`;
|
||||
lastYear = year;
|
||||
}
|
||||
|
||||
const kat = _KAT[ev.kategorie] || _KAT.tagebuch;
|
||||
const big = ev.is_milestone;
|
||||
|
||||
let label = _esc(ev.titel);
|
||||
if (ev.is_first && ev.kategorie === 'tagebuch') label = `🎉 Erster Tagebucheintrag — ${label}`;
|
||||
if (ev.is_first && ev.kategorie === 'route') label = `🎉 Erste Route — ${label}`;
|
||||
if (ev.is_first && ev.kategorie === 'training') label = `🎉 Erstes Training — ${label}`;
|
||||
if (ev.typ === 'geburtstag') label = `🎂 ${label}`;
|
||||
|
||||
const dotSize = big ? '18px' : '12px';
|
||||
const dotBorder = big ? `3px solid ${kat.color}` : `2px solid ${kat.color}`;
|
||||
const dotML = big ? '6px' : '9px';
|
||||
|
||||
html += `
|
||||
<div class="tl-item${big ? ' tl-item--big' : ''}">
|
||||
<div class="tl-dot" style="width:${dotSize};height:${dotSize};
|
||||
background:${kat.color};border:${dotBorder};
|
||||
margin-left:${dotML};
|
||||
box-shadow:${big ? `0 0 0 4px ${kat.color}22` : 'none'}"></div>
|
||||
<div class="tl-card">
|
||||
${big && ev.foto_url ? `
|
||||
<div class="tl-foto" style="background-image:url(${_esc(ev.foto_url)})"></div>` : ''}
|
||||
<div class="tl-meta">
|
||||
<span class="tl-badge" style="background:${kat.color}22;color:${kat.color}">
|
||||
<svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${kat.icon}"></use>
|
||||
</svg>
|
||||
${_esc(kat.label)}
|
||||
</span>
|
||||
<span class="tl-date">${_fmtDate(ev.datum)}</span>
|
||||
</div>
|
||||
<div class="tl-title${big ? ' tl-title--big' : ''}">${label}</div>
|
||||
${ev.distanz_km ? `<div class="tl-sub">${ev.distanz_km} km</div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += `
|
||||
<style>
|
||||
.tl-wrap { padding:var(--space-2) 0;position:relative; }
|
||||
.tl-wrap::before {
|
||||
content:'';position:absolute;left:15px;top:0;bottom:0;width:2px;
|
||||
background:var(--c-border);border-radius:1px;
|
||||
}
|
||||
.tl-year {
|
||||
padding:var(--space-2) 0 var(--space-2) 48px;
|
||||
font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text-secondary);text-transform:uppercase;letter-spacing:.08em;
|
||||
}
|
||||
.tl-item {
|
||||
display:flex;align-items:flex-start;gap:var(--space-3);
|
||||
margin-bottom:var(--space-3);position:relative;
|
||||
}
|
||||
.tl-dot {
|
||||
flex-shrink:0;border-radius:50%;margin-top:4px;z-index:1;position:relative;
|
||||
}
|
||||
.tl-card {
|
||||
flex:1;min-width:0;background:var(--c-surface-2);
|
||||
border-radius:var(--radius-md);padding:var(--space-3);overflow:hidden;
|
||||
}
|
||||
.tl-item--big .tl-card { border-left:3px solid var(--c-primary); }
|
||||
.tl-foto {
|
||||
width:100%;height:120px;background-size:cover;background-position:center;
|
||||
border-radius:var(--radius-sm);margin-bottom:var(--space-2);
|
||||
}
|
||||
.tl-meta {
|
||||
display:flex;align-items:center;gap:var(--space-2);
|
||||
margin-bottom:var(--space-1);flex-wrap:wrap;
|
||||
}
|
||||
.tl-badge {
|
||||
display:inline-flex;align-items:center;gap:3px;
|
||||
padding:2px 8px;border-radius:var(--radius-full);
|
||||
font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||||
}
|
||||
.tl-date { font-size:var(--text-xs);color:var(--c-text-secondary);margin-left:auto; }
|
||||
.tl-title { font-size:var(--text-sm);color:var(--c-text);font-weight:var(--weight-medium); }
|
||||
.tl-title--big { font-weight:var(--weight-semibold);font-size:var(--text-base); }
|
||||
.tl-sub { font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px; }
|
||||
</style>`;
|
||||
|
||||
wrap.innerHTML = html;
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUBLIC
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v699';
|
||||
const CACHE_VERSION = 'by-v700';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue