banyaro/backend/static/js/pages/reise.js
rene 459cd425f2 Design-System Sprint A: utilities.css + 948 Inline-Styles → Utility-Klassen, SW by-v1102
PHASE 1 — Sofort-Cleanup ohne Risiko:
- Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen:
  * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary
  * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3
  * flex-between, flex-1-min, mb-1/3, mt-1/3
  * icon-xs/sm/md/lg, label-block, caption
- index.html bindet utilities.css ein
- mb-3/mt-3 ergänzt (waren in design-system.css unvollständig)

PHASE 2 — .by-tab Modifier für Vereinheitlichung:
- .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.)
- .by-tabs.sticky (Desktop vertikale Tabs für Admin)
- .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll)
- .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border)

PHASE 3 — Inline-Style → Klassen-Migration (Python-Script):
- 948 Inline-Styles entfernt (5101 → 4153, -18%)
- 962 Migrationen über 47 Page-Dateien
- Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67),
  litters.js (62), settings.js (61), zuchthunde.js (51)
- Patterns: text-muted, text-secondary, text-danger, text-xs-muted,
  text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3,
  p-3/4, mb-2/3/4, hidden, w-full, flex-1, ...
- Bewahrt bestehende class-Attribute (mergt korrekt)

Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
2026-05-27 07:11:27 +02:00

496 lines
20 KiB
JavaScript
Raw 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 _esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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)">${_esc(item)}</span>
<button class="reise-del-btn" data-hide="${_esc(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="${_esc(key)}" ${done ? 'checked' : ''}>
<span>${_esc(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)">${_esc(item)}</span>
<button class="reise-del-custom-btn" data-cat="${_esc(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="${_esc(key)}" ${done ? 'checked' : ''}>
<span class="text-primary">${_esc(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="${_esc(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="${_esc(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#${_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)">
${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)">
${_esc(l.name)}
</div>
<div class="text-sm-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 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 };
})();