Feature: Reise-Checkliste + Lebens-Timeline (SW by-v700)
- Neue Seite reise.js mit 3 Tabs: Checkliste (localStorage-persistent), EU-Länder-Info, Notfälle
- Timeline-Endpoint GET /api/dogs/{id}/timeline aggregiert Tagebuch, Gesundheit, Training, Routen
- dog-profile.js: Button "Lebens-Timeline" + _showTimelineModal mit vertikaler Timeline, Jahreszahl-Marker, kategoriespezifische Farben, Milestone-Hervorhebung
This commit is contained in:
parent
40de0f38aa
commit
d6206d378e
1 changed files with 381 additions and 0 deletions
381
backend/static/js/pages/reise.js
Normal file
381
backend/static/js/pages/reise.js
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
/* ============================================================
|
||||
BAN YARO — Reise mit Hund
|
||||
Tabs: Checkliste | EU-Länder | Notfälle
|
||||
============================================================ */
|
||||
|
||||
window.Page_reise = (() => {
|
||||
|
||||
let _container = null;
|
||||
let _appState = null;
|
||||
let _activeTab = 'checkliste';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'checkliste', label: 'Checkliste', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-square"></use></svg>' },
|
||||
{ key: 'laender', label: 'EU-Länder', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#globe"></use></svg>' },
|
||||
{ key: 'notfall', label: 'Notfälle', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
|
||||
];
|
||||
|
||||
const CHECKLIST = [
|
||||
{
|
||||
key: 'dokumente',
|
||||
label: 'Dokumente',
|
||||
icon: 'file-text',
|
||||
items: [
|
||||
'EU-Heimtierausweis (Pflicht innerhalb EU)',
|
||||
'Impfpass (Tollwut mind. 21 Tage alt)',
|
||||
'Krankenkassen-Notfallkarte Tierarzt',
|
||||
'Foto des Hundes (für Vermisst-Fall)',
|
||||
'Chip-Nummer notiert',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'gesundheit',
|
||||
label: 'Gesundheit',
|
||||
icon: 'heartbeat',
|
||||
items: [
|
||||
'Zecken-/Flohschutz aufgefrischt',
|
||||
'Reisekrankheit-Mittel (falls nötig)',
|
||||
'Medikamente ausreichend eingepackt',
|
||||
'Tierarzt-Kontakt am Zielort recherchiert',
|
||||
'Verbandszeug für Hunde',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'ausruestung',
|
||||
label: 'Ausrüstung',
|
||||
icon: 'backpack',
|
||||
items: [
|
||||
'Leine + Ersatzleine',
|
||||
'Halsband mit Adressanhänger',
|
||||
'Transportbox/Reisekorb',
|
||||
'Lieblingsdecke/Schlafplatz',
|
||||
'Spielzeug (2–3 Stück)',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'futter',
|
||||
label: 'Futter & Wasser',
|
||||
icon: 'bowl-food',
|
||||
items: [
|
||||
'Genug Futter (+ Reserve)',
|
||||
'Wassernapf + Flasche',
|
||||
'Futternapf',
|
||||
'Bekannte Leckerlis',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'auto',
|
||||
label: 'Im Auto',
|
||||
icon: 'car',
|
||||
items: [
|
||||
'Sicherheitsgurt-Adapter oder Box gesichert',
|
||||
'Sonnenschutz-Netz für Fenster',
|
||||
'Pausen alle 2h eingeplant',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const LAENDER = [
|
||||
{ flag: '🇩🇪', name: 'Deutschland', regel: 'Keine Einschränkungen bei EU-Pass + Chip' },
|
||||
{ flag: '🇦🇹', name: 'Österreich', regel: 'Gleiche Regeln wie DE, Leinenpflicht in Bergbahnen' },
|
||||
{ flag: '🇨🇭', name: 'Schweiz', regel: 'Nicht-EU → eigene Einfuhrregeln, Tollwut-Titer-Test', warn: true },
|
||||
{ flag: '🇮🇹', name: 'Italien', regel: 'Leinenpflicht öffentlich, Maulkorb in öffentlichen Verkehrsmitteln' },
|
||||
{ flag: '🇫🇷', name: 'Frankreich', regel: 'Manche Strände im Sommer hundeverboten' },
|
||||
{ flag: '🇬🇷', name: 'Griechenland', regel: 'Hunde erlaubt, kaum Einschränkungen' },
|
||||
{ flag: '🇭🇷', name: 'Kroatien', regel: 'Viele Strände hundefreundlich, EU-Pass genügt' },
|
||||
{ flag: '🇬🇧', name: 'Großbritannien', regel: 'Strenge Einreise! PETS-Zertifikat + Tollwut-Impfung + Bandwurm-Behandlung nötig', warn: true },
|
||||
];
|
||||
|
||||
const SOFORTMASSNAHMEN = [
|
||||
{ icon: 'thermometer-hot', text: 'Hitzschlag: Sofort Schatten, kühlen mit lauwarmem Wasser, Tierarzt rufen' },
|
||||
{ icon: 'skull', text: 'Vergiftung: Ruhig halten, NICHT erbrechen lassen, sofort Tiergift-Notfall anrufen' },
|
||||
{ icon: 'drop', text: 'Starke Blutung: Druckverband anlegen, Druck halten, Tierarzt aufsuchen' },
|
||||
{ icon: 'bone', text: 'Knochenbruch: Ruhigstellen, nicht bewegen, Tierarzt aufsuchen' },
|
||||
{ icon: 'heartbeat', text: 'Bewusstlosigkeit: Atemwege freihalten, stabile Seitenlage, 112 rufen' },
|
||||
];
|
||||
|
||||
const LS_KEY = 'banyaro_reise_checkliste';
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------
|
||||
function _esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function _loadChecked() {
|
||||
try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
function _saveChecked(state) {
|
||||
try { localStorage.setItem(LS_KEY, JSON.stringify(state)); }
|
||||
catch {}
|
||||
}
|
||||
|
||||
function _itemKey(catKey, idx) { return `${catKey}__${idx}`; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// LIFECYCLE
|
||||
// ------------------------------------------------------------------
|
||||
function init(container, appState, params = {}) {
|
||||
_container = container;
|
||||
_appState = appState;
|
||||
if (params?.tab && TABS.some(t => t.key === params.tab)) {
|
||||
_activeTab = params.tab;
|
||||
}
|
||||
_render();
|
||||
}
|
||||
|
||||
function refresh() { _renderTabContent(); }
|
||||
function onDogChange() {}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// RENDER
|
||||
// ------------------------------------------------------------------
|
||||
function _render() {
|
||||
_container.innerHTML = `
|
||||
<div class="by-tabs" id="reise-tabs"></div>
|
||||
<div id="reise-content"></div>
|
||||
`;
|
||||
_renderTabBar();
|
||||
_renderTabContent();
|
||||
}
|
||||
|
||||
function _renderTabBar() {
|
||||
const el = _container.querySelector('#reise-tabs');
|
||||
if (!el) return;
|
||||
el.innerHTML = TABS.map(t => `
|
||||
<button class="by-tab${t.key === _activeTab ? ' active' : ''}" data-tab="${t.key}">
|
||||
${t.icon} ${t.label}
|
||||
</button>`).join('');
|
||||
el.querySelectorAll('.by-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
_activeTab = btn.dataset.tab;
|
||||
el.querySelectorAll('.by-tab').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
_renderTabContent();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _renderTabContent() {
|
||||
const el = _container.querySelector('#reise-content');
|
||||
if (!el) return;
|
||||
if (_activeTab === 'checkliste') _renderCheckliste(el);
|
||||
else if (_activeTab === 'laender') _renderLaender(el);
|
||||
else if (_activeTab === 'notfall') _renderNotfall(el);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB 1: CHECKLISTE
|
||||
// ------------------------------------------------------------------
|
||||
function _renderCheckliste(el) {
|
||||
const checked = _loadChecked();
|
||||
|
||||
const totalItems = CHECKLIST.reduce((s, c) => s + c.items.length, 0);
|
||||
const doneItems = Object.values(checked).filter(Boolean).length;
|
||||
const pct = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
|
||||
|
||||
const progressBar = `
|
||||
<div style="margin-bottom:var(--space-5)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;
|
||||
margin-bottom:var(--space-2)">
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
${doneItems} von ${totalItems} erledigt
|
||||
</span>
|
||||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-primary)">${pct}%</span>
|
||||
</div>
|
||||
<div style="height:8px;background:var(--c-surface-2);border-radius:var(--radius-full);overflow:hidden">
|
||||
<div style="height:100%;width:${pct}%;background:var(--c-primary);
|
||||
border-radius:var(--radius-full);transition:width .3s ease"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const cats = CHECKLIST.map(cat => {
|
||||
const rows = cat.items.map((item, idx) => {
|
||||
const key = _itemKey(cat.key, idx);
|
||||
const done = !!checked[key];
|
||||
return `<label class="reise-check-row${done ? ' done' : ''}" data-key="${_esc(key)}">
|
||||
<input type="checkbox" class="reise-cb" data-key="${_esc(key)}" ${done ? 'checked' : ''}>
|
||||
<span>${_esc(item)}</span>
|
||||
</label>`;
|
||||
}).join('');
|
||||
|
||||
return `<div class="card" style="margin-bottom:var(--space-4)">
|
||||
<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" style="color:var(--c-primary)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${_esc(cat.icon)}"></use>
|
||||
</svg>
|
||||
<span style="font-weight:var(--weight-semibold)">${_esc(cat.label)}</span>
|
||||
</div>
|
||||
<div style="padding:var(--space-2) var(--space-4)">
|
||||
${rows}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
el.innerHTML = `
|
||||
${progressBar}
|
||||
${cats}
|
||||
<div style="text-align:center;margin-bottom:var(--space-6)">
|
||||
<button class="btn btn-secondary" id="reise-reset-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#arrow-counter-clockwise"></use></svg>
|
||||
Alle zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
<style>
|
||||
.reise-check-row {
|
||||
display:flex;align-items:flex-start;gap:var(--space-3);
|
||||
padding:var(--space-2) 0;cursor:pointer;
|
||||
border-bottom:1px solid var(--c-surface-2);
|
||||
font-size:var(--text-sm);color:var(--c-text);line-height:1.5;
|
||||
}
|
||||
.reise-check-row:last-child { border-bottom:none; }
|
||||
.reise-check-row.done span { text-decoration:line-through;color:var(--c-text-secondary); }
|
||||
.reise-check-row input[type=checkbox] {
|
||||
flex-shrink:0;margin-top:2px;width:18px;height:18px;
|
||||
accent-color:var(--c-primary);cursor:pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
// Checkbox events
|
||||
el.querySelectorAll('.reise-cb').forEach(cb => {
|
||||
cb.addEventListener('change', () => {
|
||||
const key = cb.dataset.key;
|
||||
const cur = _loadChecked();
|
||||
cur[key] = cb.checked;
|
||||
_saveChecked(cur);
|
||||
_renderTabContent(); // re-render to update progress
|
||||
});
|
||||
});
|
||||
|
||||
el.querySelector('#reise-reset-btn')?.addEventListener('click', () => {
|
||||
_saveChecked({});
|
||||
_renderTabContent();
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB 2: EU-LÄNDER
|
||||
// ------------------------------------------------------------------
|
||||
function _renderLaender(el) {
|
||||
const cards = LAENDER.map(l => `
|
||||
<div class="card" style="margin-bottom:var(--space-3);padding:var(--space-4)">
|
||||
<div style="display:flex;align-items:flex-start;gap:var(--space-3)">
|
||||
<span style="font-size:2rem;line-height:1">${l.flag}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:var(--weight-semibold);margin-bottom:var(--space-1)">
|
||||
${_esc(l.name)}
|
||||
</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
${_esc(l.regel)}
|
||||
</div>
|
||||
</div>
|
||||
${l.warn ? `<svg class="ph-icon" style="color:var(--c-warning,#f59e0b);flex-shrink:0;width:20px;height:20px" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#warning"></use>
|
||||
</svg>` : ''}
|
||||
</div>
|
||||
</div>`).join('');
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="margin-bottom:var(--space-4);padding:var(--space-3) var(--space-4);
|
||||
background:var(--c-surface-2);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);color:var(--c-text-secondary);
|
||||
display:flex;gap:var(--space-2);align-items:flex-start">
|
||||
<svg class="ph-icon" style="flex-shrink:0;margin-top:1px" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#info"></use>
|
||||
</svg>
|
||||
<span>EU-Heimtierausweis + Mikrochip + gültige Tollwut-Impfung (mindestens 21 Tage alt)
|
||||
sind Pflicht für alle EU-Reisen. Informationen können sich ändern — immer beim
|
||||
Zielland-Konsulat oder Tierarzt aktuell prüfen.</span>
|
||||
</div>
|
||||
${cards}
|
||||
`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB 3: NOTFÄLLE
|
||||
// ------------------------------------------------------------------
|
||||
function _renderNotfall(el) {
|
||||
const massnahmen = SOFORTMASSNAHMEN.map(m => `
|
||||
<div style="display:flex;align-items:flex-start;gap:var(--space-3);
|
||||
padding:var(--space-3) 0;border-bottom:1px solid var(--c-surface-2)">
|
||||
<svg class="ph-icon" style="color:var(--c-danger,#ef4444);flex-shrink:0;margin-top:1px" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${_esc(m.icon)}"></use>
|
||||
</svg>
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text)">${_esc(m.text)}</span>
|
||||
</div>`).join('');
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="card" style="margin-bottom:var(--space-4)">
|
||||
<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--c-border);
|
||||
font-weight:var(--weight-semibold);display:flex;align-items:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" style="color:var(--c-danger,#ef4444)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#phone"></use>
|
||||
</svg>
|
||||
Notrufnummern
|
||||
</div>
|
||||
<div style="padding:var(--space-4)">
|
||||
<a href="tel:112" class="btn btn-danger w-full"
|
||||
style="margin-bottom:var(--space-3);display:flex;align-items:center;
|
||||
justify-content:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone-call"></use></svg>
|
||||
112 — EU-Notruf
|
||||
</a>
|
||||
<a href="tel:+498919240" class="btn btn-secondary w-full"
|
||||
style="margin-bottom:var(--space-2);display:flex;align-items:center;
|
||||
justify-content:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg>
|
||||
Tiergift-Notruf München
|
||||
</a>
|
||||
<div style="text-align:center;font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
+49 89 19240 (Tierärztliche Hochschule)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:var(--space-4)">
|
||||
<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--c-border);
|
||||
font-weight:var(--weight-semibold)">Tierarzt finden</div>
|
||||
<div style="padding:var(--space-4)">
|
||||
<button class="btn btn-primary w-full" id="reise-map-btn"
|
||||
style="display:flex;align-items:center;justify-content:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>
|
||||
Tierarzt in der Nähe suchen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:var(--space-4)">
|
||||
<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--c-border);
|
||||
font-weight:var(--weight-semibold);display:flex;align-items:center;gap:var(--space-2)">
|
||||
<svg class="ph-icon" style="color:var(--c-warning,#f59e0b)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#warning"></use>
|
||||
</svg>
|
||||
Sofortmaßnahmen
|
||||
</div>
|
||||
<div style="padding:var(--space-2) var(--space-4)">
|
||||
${massnahmen}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.querySelector('#reise-map-btn')?.addEventListener('click', () => {
|
||||
App.navigate('map');
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// PUBLIC
|
||||
// ------------------------------------------------------------------
|
||||
return { init, refresh, onDogChange };
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue