banyaro/backend/static/js/pages/reise.js
rene c517c9281d Refactor: 1167 _esc() → UI.escape() in 36 Dateien, SW by-v1113
Bündel 1 aus dem Duplikat-Audit: existierende zentrale Helper nutzen
statt lokale Duplikate.

Pure Migration ohne neuen Code:
- 1167 _esc()-Aufrufe in 36 Page-Modulen migriert auf UI.escape()
- 24 lokale _esc/_escape-Definitionen entfernt
- lost.js hatte _escape() (Variante) — 17 Aufrufe ebenfalls migriert
- jobs.js + breeder.js: tote Alias-Wrapper entfernt

UI.escape() existierte schon — wurde nur überall lokal nochmal
implementiert. Funktional identisch (gleiche 4-replace-chain für
& < > ").

Tests 19/19 grün. Frontend-LOC um ~120 Zeilen reduziert.

Hinweis: _emptyState (7 Stellen) und _icon (8 Stellen) wurden NICHT
migriert — sie haben abweichende Signaturen von UI.emptyState({...})
bzw. UI.icon(name). Eigener Sprint nötig.
2026-05-27 10:15:33 +02:00

488 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
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>' },
];
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 (23 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';
const LS_CUSTOM_KEY = 'banyaro_reise_custom'; // {catKey: ["custom item",...]}
const LS_HIDDEN_KEY = 'banyaro_reise_hidden'; // {itemKey: true} — gelöschte Standard-Items
let _editMode = false;
function _loadCustom() { try { return JSON.parse(localStorage.getItem(LS_CUSTOM_KEY) || '{}'); } catch { return {}; } }
function _saveCustom(d) { try { localStorage.setItem(LS_CUSTOM_KEY, JSON.stringify(d)); } catch {} }
function _loadHidden() { try { return JSON.parse(localStorage.getItem(LS_HIDDEN_KEY) || '{}'); } catch { return {}; } }
function _saveHidden(d) { try { localStorage.setItem(LS_HIDDEN_KEY, JSON.stringify(d)); } catch {} }
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
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 custom = _loadCustom();
const hidden = _loadHidden();
// Alle sichtbaren Items zählen
let totalItems = 0, doneItems = 0;
CHECKLIST.forEach(cat => {
cat.items.forEach((_, idx) => {
if (!hidden[_itemKey(cat.key, idx)]) {
totalItems++;
if (checked[_itemKey(cat.key, idx)]) doneItems++;
}
});
(custom[cat.key] || []).forEach((_, i) => {
totalItems++;
if (checked[`${cat.key}__custom__${i}`]) doneItems++;
});
});
const pct = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
const cats = CHECKLIST.map(cat => {
const customItems = custom[cat.key] || [];
const stdRows = cat.items.map((item, idx) => {
if (hidden[_itemKey(cat.key, idx)]) return '';
const key = _itemKey(cat.key, idx);
const done = !!checked[key];
if (_editMode) {
return `<div class="reise-check-row" style="justify-content:space-between">
<span style="flex:1;color:var(--c-text)">${UI.escape(item)}</span>
<button class="reise-del-btn" data-hide="${UI.escape(key)}"
style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0">
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>
</div>`;
}
return `<label class="reise-check-row${done ? ' done' : ''}">
<input type="checkbox" class="reise-cb" data-key="${UI.escape(key)}" ${done ? 'checked' : ''}>
<span>${UI.escape(item)}</span>
</label>`;
}).join('');
const customRows = customItems.map((item, i) => {
const key = `${cat.key}__custom__${i}`;
const done = !!checked[key];
if (_editMode) {
return `<div class="reise-check-row" style="justify-content:space-between">
<span style="flex:1;color:var(--c-primary)">${UI.escape(item)}</span>
<button class="reise-del-custom-btn" data-cat="${UI.escape(cat.key)}" data-idx="${i}"
style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0">
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>
</div>`;
}
return `<label class="reise-check-row${done ? ' done' : ''}">
<input type="checkbox" class="reise-cb" data-key="${UI.escape(key)}" ${done ? 'checked' : ''}>
<span class="text-primary">${UI.escape(item)}</span>
</label>`;
}).join('');
const addRow = _editMode ? `
<div style="padding:var(--space-2) 0;border-top:1px dashed var(--c-border);margin-top:4px">
<div style="display:flex;gap:8px;align-items:center">
<input class="reise-add-input" data-cat="${UI.escape(cat.key)}"
style="flex:1;padding:8px 10px;border-radius:8px;border:1px solid var(--c-border);
background:var(--c-bg-card);color:var(--c-text);font-size:var(--text-sm)"
placeholder="Eigenes Item hinzufügen…">
<button class="reise-add-btn btn btn-primary" data-cat="${UI.escape(cat.key)}"
style="padding:8px 12px;flex-shrink:0;font-size:var(--text-sm)">
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#plus"></use></svg>
</button>
</div>
</div>` : '';
return `<div class="card mb-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 text-primary" aria-hidden="true">
<use href="/icons/phosphor.svg#${UI.escape(cat.icon)}"></use>
</svg>
<span style="font-weight:var(--weight-semibold)">${UI.escape(cat.label)}</span>
</div>
<div style="padding:var(--space-2) var(--space-4)">
${stdRows}${customRows}${addRow}
</div>
</div>`;
}).join('');
el.innerHTML = `
<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>
<!-- Fortschritt + Buttons -->
<div style="margin-bottom:var(--space-5)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-2)">
<span class="text-sm-secondary">${doneItems} von ${totalItems} erledigt</span>
<div style="display:flex;gap:8px;align-items:center">
<span style="font-size:var(--text-sm);font-weight:700;color:var(--c-primary)">${pct}%</span>
<button id="reise-edit-toggle" style="background:${_editMode ? 'var(--c-primary)' : 'var(--c-bg-card)'};
color:${_editMode ? '#fff' : 'var(--c-text-secondary)'};border:1.5px solid var(--c-border);
border-radius:8px;padding:5px 10px;cursor:pointer;font-size:var(--text-xs);font-weight:600;
display:flex;align-items:center;gap:4px">
<svg class="ph-icon" style="width:.9rem;height:.9rem"><use href="/icons/phosphor.svg#pencil-simple"></use></svg>
${_editMode ? 'Fertig' : 'Bearbeiten'}
</button>
</div>
</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"></div>
</div>
</div>
${cats}
${!_editMode ? `<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>
Abhaken zurücksetzen
</button>
</div>` : ''}
`;
// 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();
});
});
// Edit-Toggle
el.querySelector('#reise-edit-toggle')?.addEventListener('click', () => {
_editMode = !_editMode;
_renderTabContent();
});
// Standard-Item löschen (verstecken)
el.querySelectorAll('.reise-del-btn').forEach(btn => {
btn.addEventListener('click', () => {
const h = _loadHidden();
h[btn.dataset.hide] = true;
_saveHidden(h);
_renderTabContent();
});
});
// Custom-Item löschen
el.querySelectorAll('.reise-del-custom-btn').forEach(btn => {
btn.addEventListener('click', () => {
const c = _loadCustom();
c[btn.dataset.cat] = (c[btn.dataset.cat] || []).filter((_, i) => i !== parseInt(btn.dataset.idx));
_saveCustom(c);
_renderTabContent();
});
});
// Custom-Item hinzufügen
el.querySelectorAll('.reise-add-btn').forEach(btn => {
btn.addEventListener('click', () => {
const cat = btn.dataset.cat;
const input = el.querySelector(`.reise-add-input[data-cat="${cat}"]`);
const val = (input?.value || '').trim();
if (!val) return;
const c = _loadCustom();
if (!c[cat]) c[cat] = [];
c[cat].push(val);
_saveCustom(c);
_renderTabContent();
});
});
// Enter in Add-Input
el.querySelectorAll('.reise-add-input').forEach(input => {
input.addEventListener('keydown', e => {
if (e.key === 'Enter') el.querySelector(`.reise-add-btn[data-cat="${input.dataset.cat}"]`)?.click();
});
});
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 class="flex-1-min">
<div style="font-weight:var(--weight-semibold);margin-bottom:var(--space-1)">
${UI.escape(l.name)}
</div>
<div class="text-sm-secondary">
${UI.escape(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#${UI.escape(m.icon)}"></use>
</svg>
<span style="font-size:var(--text-sm);color:var(--c-text)">${UI.escape(m.text)}</span>
</div>`).join('');
el.innerHTML = `
<div class="card mb-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 class="p-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 mb-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 class="p-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 mb-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 };
})();