/* ============================================================
BAN YARO — Adoption (Tierheim-Hunde in der Nähe)
Seiten-Modul: Hunde aus deutschen Tierheimen finden.
============================================================ */
window.Page_adoption = (() => {
// ----------------------------------------------------------
// MODUL-STATE
// ----------------------------------------------------------
let _container = null;
let _appState = null;
let _lat = null;
let _lon = null;
let _radius = 50;
let _rasseFilter = '';
let _activeTab = 'hunde';
let _data = null; // { animals, shelters, has_petfinder }
let _loading = false;
let _communityData = null; // [] listings from /adoption/community
let _myListings = null; // [] eigene Inserate
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
_render();
// Standort automatisch versuchen
_tryAutoLocate();
}
// ----------------------------------------------------------
// REFRESH
// ----------------------------------------------------------
async function refresh() {
if (_lat && _lon) {
await _loadData();
}
}
// ----------------------------------------------------------
// RENDER — Grundstruktur
// ----------------------------------------------------------
function _render() {
_container.innerHTML = `
${UI.skeleton(4)}
`;
// Events
_container.querySelector('#adp-radius')
?.addEventListener('change', e => {
_radius = parseInt(e.target.value);
if (_lat && _lon) _loadData();
});
_container.querySelector('#adp-rasse')
?.addEventListener('input', e => {
_rasseFilter = e.target.value.trim().toLowerCase();
_renderContent();
});
_container.querySelector('#adp-btn-locate')
?.addEventListener('click', _locateUser);
_container.querySelector('#adp-btn-geocode')
?.addEventListener('click', _geocodePLZ);
_container.querySelector('#adp-plz')
?.addEventListener('keydown', e => {
if (e.key === 'Enter') _geocodePLZ();
});
_container.querySelectorAll('.adp-tab').forEach(btn => {
btn.addEventListener('click', () => {
_activeTab = btn.dataset.tab;
_container.querySelectorAll('.adp-tab').forEach(b => {
const isActive = b.dataset.tab === _activeTab;
b.style.color = isActive ? 'var(--c-primary)' : 'var(--c-text-secondary)';
b.style.fontWeight = isActive ? '600' : 'normal';
b.style.borderBottom = isActive ? '2px solid var(--c-primary)' : '2px solid transparent';
});
_renderContent();
});
});
}
// ----------------------------------------------------------
// STANDORT AUTOMATISCH ERMITTELN
// ----------------------------------------------------------
async function _tryAutoLocate() {
try {
const pos = await API.getLocation({ timeout: 6000, maximumAge: 300_000 });
_lat = pos.lat;
_lon = pos.lon;
await _loadData();
} catch {
// Standort nicht verfügbar → PLZ-Eingabe zeigen
document.getElementById('adp-plz-row')?.style.setProperty('display', 'flex', 'important');
document.getElementById('adp-plz-row').style.display = 'flex';
_showNoLocation();
}
}
async function _locateUser() {
const btn = _container.querySelector('#adp-btn-locate');
if (btn) btn.disabled = true;
try {
const pos = await API.getLocation({ timeout: 10000 });
_lat = pos.lat;
_lon = pos.lon;
document.getElementById('adp-plz-row').style.display = 'none';
await _loadData();
} catch {
UI.toast.error('Standort konnte nicht ermittelt werden. Bitte PLZ eingeben.');
document.getElementById('adp-plz-row').style.display = 'flex';
} finally {
if (btn) btn.disabled = false;
}
}
async function _geocodePLZ() {
const plz = (_container.querySelector('#adp-plz')?.value || '').trim();
if (!plz) return;
const btn = _container.querySelector('#adp-btn-geocode');
if (btn) btn.disabled = true;
try {
const geo = await API.get(`/adoption/geocode?plz=${encodeURIComponent(plz)}`);
if (geo.lat && geo.lon) {
_lat = geo.lat;
_lon = geo.lon;
await _loadData();
} else {
UI.toast.error(`PLZ "${plz}" nicht gefunden.`);
}
} catch {
UI.toast.error('Geocoding fehlgeschlagen. Bitte erneut versuchen.');
} finally {
if (btn) btn.disabled = false;
}
}
// ----------------------------------------------------------
// DATEN LADEN
// ----------------------------------------------------------
async function _loadData() {
if (_loading || !_lat || !_lon) return;
_loading = true;
const content = _container.querySelector('#adp-content');
if (content) content.innerHTML = UI.skeleton(4);
try {
_data = await API.get(`/adoption/nearby?lat=${_lat}&lon=${_lon}&radius=${_radius}`);
_renderContent();
} catch {
if (content) content.innerHTML = UI.emptyState({
icon: 'warning',
title: 'Daten konnten nicht geladen werden',
text: 'Bitte versuche es erneut.',
});
} finally {
_loading = false;
}
}
async function _loadCommunity() {
const content = _container.querySelector('#adp-content');
if (content) content.innerHTML = UI.skeleton(4);
try {
const url = _lat && _lon
? `/adoption/community?lat=${_lat}&lon=${_lon}`
: '/adoption/community';
_communityData = await API.get(url);
if (_appState?.user) {
try {
_myListings = await API.get('/adoption/community/my');
} catch {
_myListings = [];
}
}
_renderCommunity(content);
} catch {
if (content) content.innerHTML = UI.emptyState({
icon: 'warning',
title: 'Weitervermittlungs-Inserate konnten nicht geladen werden',
text: 'Bitte versuche es erneut.',
});
}
}
// ----------------------------------------------------------
// INHALT RENDERN (je nach Tab)
// ----------------------------------------------------------
function _renderContent() {
const content = _container.querySelector('#adp-content');
if (!content) return;
if (_activeTab === 'community') {
_loadCommunity();
return;
}
if (!_data) { _showNoLocation(); return; }
if (_activeTab === 'hunde') _renderHunde(content);
else _renderTierheime(content);
}
function _showNoLocation() {
const content = _container.querySelector('#adp-content');
if (!content) return;
content.innerHTML = `
`;
}
// ------------------------------------------------------------------
// TAB: HUNDE
// ------------------------------------------------------------------
function _renderHunde(content) {
let animals = (_data?.animals || []);
// Rasse-Filter
if (_rasseFilter) {
animals = animals.filter(a =>
(a.rasse || '').toLowerCase().includes(_rasseFilter) ||
(a.name || '').toLowerCase().includes(_rasseFilter)
);
}
const hasPetFinder = _data?.has_petfinder;
const infoText = hasPetFinder
? `${animals.length} Hunde im Umkreis von ${_radius} km (via PetFinder)`
: '';
if (!animals.length) {
content.innerHTML = `
${_rasseFilter ? `Keine Hunde gefunden für "${UI.escape(_rasseFilter)}"` : `Keine Hunde im Umkreis von ${_radius} km gefunden.`}
Tipp: Schau auch im Tab „Tierheime" nach lokalen Tierheimen direkt.
`;
return;
}
content.innerHTML = `
${infoText ? `${infoText}
` : ''}
${animals.map(a => _animalCard(a)).join('')}
`;
// Klick-Events
content.querySelectorAll('[data-adp-url]').forEach(card => {
card.addEventListener('click', () => {
window.open(card.dataset.adpUrl, '_blank', 'noopener,noreferrer');
});
});
}
function _animalCard(a) {
const foto = a.foto_url
? `
`
: '🐶
';
const distTxt = a.distanz_km != null ? `${a.distanz_km} km` : '';
const alterTxt = a.alter_jahre != null ? `${_formatAlter(a.alter_jahre)}` : '';
const rasseTxt = a.rasse || '';
const tierheim = a.tierheim || '';
return `
${foto}
${UI.escape(a.name)}
${rasseTxt ? `
${UI.escape(rasseTxt)}
` : ''}
${alterTxt ? `
${UI.escape(alterTxt)}
` : ''}
${a.geschlecht ? `
${a.geschlecht === 'männlich' ? '♂' : '♀'}
` : ''}
${distTxt ? `
${UI.escape(distTxt)}
` : ''}
${tierheim ? `
${UI.icon('house-line')} ${UI.escape(tierheim)}
` : ''}
`;
}
// ------------------------------------------------------------------
// TAB: TIERHEIME
// ------------------------------------------------------------------
function _renderTierheime(content) {
const shelters = _data?.shelters || [];
if (!shelters.length) {
content.innerHTML = `
`;
return;
}
content.innerHTML = `
${shelters.length} Tierheim${shelters.length !== 1 ? 'e' : ''} im Umkreis von ${_radius} km
${shelters.map(s => _shelterRow(s)).join('')}
`;
}
function _shelterRow(s) {
return `
🏠
${UI.escape(s.name)}
${UI.escape(s.plz)} ${UI.escape(s.stadt)}
${s.distanz_km} km
Hunde ansehen ${UI.icon('arrow-right')}
`;
}
// ------------------------------------------------------------------
// TAB: WEITERVERMITTLUNG (Community)
// ------------------------------------------------------------------
function _renderCommunity(content) {
if (!content) return;
const listings = _communityData || [];
const isLoggedIn = !!_appState?.user;
const fabHtml = isLoggedIn ? `
` : '';
if (!listings.length) {
content.innerHTML = `
🐾
Noch keine Hunde zur Weitervermittlung
Hier können Halter Hunde privat zur Weitervermittlung anbieten —
zum Beispiel bei Umzug, Krankheit oder Allergie.
${isLoggedIn ? `
` : `
Bitte anmelden, um ein Inserat zu erstellen.
`}
${fabHtml}
`;
content.querySelector('#adp-empty-create')?.addEventListener('click', _openCreateModal);
content.querySelector('#adp-fab-create')?.addEventListener('click', _openCreateModal);
return;
}
// Eigene Inserate trennen
const myIds = new Set((_myListings || []).map(l => l.id));
content.innerHTML = `
${listings.length} Inserat${listings.length !== 1 ? 'e' : ''} zur Weitervermittlung
${listings.map(l => _communityCard(l)).join('')}
${isLoggedIn && _myListings && _myListings.length ? `
Meine Inserate
${_myListings.map(l => _myListingRow(l)).join('')}
` : ''}
${fabHtml}
`;
// Interest-Button Events
content.querySelectorAll('[data-adp-interest]').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.adpInterest;
const interested = btn.dataset.adpInterested === 'true';
_handleInterest(id, interested, btn);
});
});
// FAB
content.querySelector('#adp-fab-create')?.addEventListener('click', _openCreateModal);
// Meine Inserate: Status-Dropdown + Löschen
content.querySelectorAll('[data-adp-status-change]').forEach(sel => {
sel.addEventListener('change', async () => {
const id = sel.dataset.adpStatusChange;
try {
await API.patch(`/adoption/community/${id}`, { status: sel.value });
UI.toast.success('Status aktualisiert.');
_loadCommunity();
} catch {
UI.toast.error('Status konnte nicht aktualisiert werden.');
}
});
});
content.querySelectorAll('[data-adp-delete]').forEach(btn => {
btn.addEventListener('click', async () => {
if (!window.confirm('Inserat wirklich löschen?')) return;
try {
await API.del(`/adoption/community/${btn.dataset.adpDelete}`);
UI.toast.success('Inserat gelöscht.');
_communityData = null;
_myListings = null;
_loadCommunity();
} catch {
UI.toast.error('Löschen fehlgeschlagen.');
}
});
});
}
function _communityCard(l) {
const foto = l.foto_url
? `
`
: '🐾
';
const isActive = !l.status || l.status === 'active';
const statusLabel = l.status === 'reserved' ? 'Reserviert'
: l.status === 'adopted' ? 'Vermittelt'
: '';
const alterLabel = l.alter_kategorie === 'welpe' ? 'Welpe <6Mo'
: l.alter_kategorie === 'jung' ? 'Jung 6Mo–2J'
: l.alter_kategorie === 'adult' ? 'Adult 2–8J'
: l.alter_kategorie === 'senior' ? 'Senior >8J'
: '';
const genderIcon = l.geschlecht === 'maennlich' ? '♂'
: l.geschlecht === 'weiblich' ? '♀'
: '';
const distTxt = l.distanz_km != null ? `${l.distanz_km} km` : '';
const ort = [l.plz, l.ort].filter(Boolean).join(' ');
const interestBtn = l.user_interested
? ``
: ``;
return `
${foto}
${!isActive ? `
${UI.escape(statusLabel)}
` : ''}
${UI.escape(l.name)}
${l.rasse ? `
${UI.escape(l.rasse)}
` : ''}
${alterLabel ? `
${UI.escape(alterLabel)}
` : ''}
${genderIcon ? `
${genderIcon}
` : ''}
${distTxt ? `
${UI.escape(distTxt)}
` : ''}
${ort ? `
${UI.escape(ort)}
` : ''}
${l.beschreibung ? `
${UI.escape(l.beschreibung)}
` : ''}
${l.interesse_count ? `
❤️ ${l.interesse_count} Interessent${l.interesse_count !== 1 ? 'en' : ''}
` : ''}
${interestBtn}
`;
}
function _myListingRow(l) {
const statusOptions = [
{ value: 'active', label: 'Aktiv' },
{ value: 'reserved', label: 'Reserviert' },
{ value: 'adopted', label: 'Vermittelt' },
];
return `
${UI.escape(l.name)}
${l.interesse_count || 0} Interessent${(l.interesse_count || 0) !== 1 ? 'en' : ''}
`;
}
// ------------------------------------------------------------------
// INTERESSE BEKUNDEN / ZURÜCKZIEHEN
// ------------------------------------------------------------------
async function _handleInterest(id, isInterested, btn) {
if (!_appState?.user) {
UI.toast.error('Bitte anmelden um Interesse zu bekunden.');
return;
}
if (isInterested) {
// Interesse zurückziehen
try {
btn.disabled = true;
await API.del(`/adoption/community/${id}/interest`);
UI.toast.success('Interesse zurückgezogen.');
_communityData = null;
_myListings = null;
_loadCommunity();
} catch {
UI.toast.error('Fehler beim Zurückziehen des Interesses.');
btn.disabled = false;
}
return;
}
// Interesse bekunden — Modal mit optionaler Nachricht
const body = `
`;
const footer = `
`;
UI.modal.open({ title: 'Interesse bekunden', body, footer });
document.getElementById('adp-interest-form')?.addEventListener('submit', async e => {
e.preventDefault();
const submitBtn = document.getElementById('adp-interest-submit');
const fd = new FormData(e.target);
const payload = { nachricht: fd.get('nachricht') || null };
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '…'; }
try {
await API.post(`/adoption/community/${id}/interest`, payload);
UI.modal.close();
UI.toast.success('Interesse gemeldet!');
_communityData = null;
_myListings = null;
_loadCommunity();
} catch {
UI.toast.error('Fehler beim Melden des Interesses.');
if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = `${UI.icon('heart')} Interesse bekunden`; }
}
});
}
// ------------------------------------------------------------------
// INSERAT ERSTELLEN — Modal
// ------------------------------------------------------------------
function _openCreateModal() {
if (!_appState?.user) {
UI.toast.error('Bitte anmelden um ein Inserat zu erstellen.');
return;
}
const body = `
`;
const footer = `
`;
UI.modal.open({ title: 'Hund zur Vermittlung anbieten', body, footer });
document.getElementById('adp-create-form')?.addEventListener('submit', async e => {
e.preventDefault();
const submitBtn = document.getElementById('adp-create-submit');
const fd = new FormData(e.target);
// Mindestlänge Beschreibung manuell prüfen (minlength gilt nur für text)
const beschreibung = (fd.get('beschreibung') || '').trim();
if (beschreibung.length < 80) {
UI.toast.error('Beschreibung muss mindestens 80 Zeichen lang sein.');
return;
}
// FormData für multipart aufbauen
const postData = new FormData();
postData.append('name', fd.get('name') || '');
postData.append('rasse', fd.get('rasse') || '');
postData.append('alter_kategorie', fd.get('alter_kategorie') || '');
postData.append('geschlecht', fd.get('geschlecht') || '');
postData.append('plz', fd.get('plz') || '');
postData.append('ort', fd.get('ort') || '');
postData.append('beschreibung', beschreibung);
postData.append('hintergrund', fd.get('hintergrund') || '');
if (_lat) postData.append('lat', _lat);
if (_lon) postData.append('lon', _lon);
const fotoFile = document.getElementById('adp-create-foto')?.files?.[0];
if (fotoFile) postData.append('foto', fotoFile);
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '…'; }
try {
await API.upload('/adoption/community', postData);
UI.modal.close();
UI.toast.success('Inserat erstellt!');
_communityData = null;
_myListings = null;
_loadCommunity();
} catch (err) {
UI.toast.error(err.message || 'Inserat konnte nicht erstellt werden.');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = `${UI.icon('plus')} Inserat erstellen`;
}
}
});
}
// ----------------------------------------------------------
// HILFSFUNKTIONEN
// ----------------------------------------------------------
function _formatAlter(jahre) {
if (jahre < 0.5) return 'Welpe';
if (jahre < 1) return 'Jungtier';
if (jahre < 2) return `${Math.round(jahre)} Jahr`;
if (jahre < 10) return `${Math.round(jahre)} Jahre`;
return 'Senior';
}
// ----------------------------------------------------------
// PUBLIC API
// ----------------------------------------------------------
return { init, refresh };
})();