|
|
|
|
@ -45,14 +45,32 @@ window.Page_lost = (() => {
|
|
|
|
|
// ----------------------------------------------------------
|
|
|
|
|
// MODUL-STATE
|
|
|
|
|
// ----------------------------------------------------------
|
|
|
|
|
let _container = null;
|
|
|
|
|
let _appState = null;
|
|
|
|
|
let _map = null;
|
|
|
|
|
let _markers = [];
|
|
|
|
|
let _userMarker = null;
|
|
|
|
|
let _reports = [];
|
|
|
|
|
let _userPos = null;
|
|
|
|
|
let _leafletLoaded = false;
|
|
|
|
|
let _container = null;
|
|
|
|
|
let _appState = null;
|
|
|
|
|
let _map = null;
|
|
|
|
|
let _markers = [];
|
|
|
|
|
let _userMarker = null;
|
|
|
|
|
let _reports = [];
|
|
|
|
|
let _userPos = null;
|
|
|
|
|
let _leafletLoaded = false;
|
|
|
|
|
let _stylesInjected = false;
|
|
|
|
|
|
|
|
|
|
function _injectStyles() {
|
|
|
|
|
if (_stylesInjected) return;
|
|
|
|
|
_stylesInjected = true;
|
|
|
|
|
const s = document.createElement('style');
|
|
|
|
|
s.textContent = `
|
|
|
|
|
@keyframes by-lost-pulse-r {
|
|
|
|
|
0%,100% { box-shadow: 0 0 0 0 rgba(231,76,60,.55), 0 2px 6px rgba(0,0,0,.3); }
|
|
|
|
|
50% { box-shadow: 0 0 0 11px rgba(231,76,60,0), 0 2px 6px rgba(0,0,0,.3); }
|
|
|
|
|
}
|
|
|
|
|
@keyframes by-lost-pulse-p {
|
|
|
|
|
0%,100% { box-shadow: 0 0 0 0 rgba(217,119,6,.55), 0 2px 6px rgba(0,0,0,.3); }
|
|
|
|
|
50% { box-shadow: 0 0 0 11px rgba(217,119,6,0), 0 2px 6px rgba(0,0,0,.3); }
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
document.head.appendChild(s);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ----------------------------------------------------------
|
|
|
|
|
// INIT
|
|
|
|
|
@ -150,6 +168,7 @@ window.Page_lost = (() => {
|
|
|
|
|
// KARTE INITIALISIEREN
|
|
|
|
|
// ----------------------------------------------------------
|
|
|
|
|
function _initMap() {
|
|
|
|
|
_injectStyles();
|
|
|
|
|
const mapEl = document.getElementById('lost-map');
|
|
|
|
|
if (!mapEl || !window.L || _map) return;
|
|
|
|
|
|
|
|
|
|
@ -216,13 +235,23 @@ window.Page_lost = (() => {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pending = _getPending().map(p => ({
|
|
|
|
|
...p,
|
|
|
|
|
distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon),
|
|
|
|
|
}));
|
|
|
|
|
try {
|
|
|
|
|
const fetched = await API.lost.list(_userPos.lat, _userPos.lon, 25);
|
|
|
|
|
try { localStorage.setItem(_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: fetched })); } catch {}
|
|
|
|
|
|
|
|
|
|
// Remove pending items already on the server (race: sync completed during fetch)
|
|
|
|
|
const rawPending = _getPending();
|
|
|
|
|
const dedupedPending = rawPending.filter(p =>
|
|
|
|
|
!fetched.some(f => f.name === p.name &&
|
|
|
|
|
Math.abs(f.lat - p.lat) < 0.0001 &&
|
|
|
|
|
Math.abs(f.lon - p.lon) < 0.0001)
|
|
|
|
|
);
|
|
|
|
|
if (dedupedPending.length < rawPending.length) _setPending(dedupedPending);
|
|
|
|
|
|
|
|
|
|
const pending = dedupedPending.map(p => ({
|
|
|
|
|
...p,
|
|
|
|
|
distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon),
|
|
|
|
|
}));
|
|
|
|
|
_reports = [...pending, ...fetched];
|
|
|
|
|
_renderMarkers();
|
|
|
|
|
_renderHeld();
|
|
|
|
|
@ -234,10 +263,15 @@ window.Page_lost = (() => {
|
|
|
|
|
: 'Keine vermissten Hunde in deiner Nähe (25 km Radius). 🐾';
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
const offline_pending = _getPending().map(p => ({
|
|
|
|
|
...p,
|
|
|
|
|
distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon),
|
|
|
|
|
}));
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(_CACHE_KEY);
|
|
|
|
|
if (raw) {
|
|
|
|
|
_reports = [...pending, ...(JSON.parse(raw).data || [])];
|
|
|
|
|
const cached = JSON.parse(raw).data || [];
|
|
|
|
|
_reports = [...offline_pending, ...cached];
|
|
|
|
|
_renderMarkers();
|
|
|
|
|
_renderHeld();
|
|
|
|
|
_renderList();
|
|
|
|
|
@ -246,8 +280,8 @@ window.Page_lost = (() => {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
_reports = pending;
|
|
|
|
|
if (pending.length) {
|
|
|
|
|
_reports = offline_pending;
|
|
|
|
|
if (offline_pending.length) {
|
|
|
|
|
_renderMarkers();
|
|
|
|
|
_renderHeld();
|
|
|
|
|
_renderList();
|
|
|
|
|
@ -267,20 +301,21 @@ window.Page_lost = (() => {
|
|
|
|
|
_markers = [];
|
|
|
|
|
|
|
|
|
|
_reports.forEach(r => {
|
|
|
|
|
const dotColor = r._isPending ? '#d97706' : '#e74c3c';
|
|
|
|
|
const anim = r._isPending ? 'by-lost-pulse-p' : 'by-lost-pulse-r';
|
|
|
|
|
const icon = L.divIcon({
|
|
|
|
|
className : '',
|
|
|
|
|
html : `<div style="
|
|
|
|
|
background:#e74c3c;color:#fff;border-radius:50%;
|
|
|
|
|
width:34px;height:34px;
|
|
|
|
|
display:flex;align-items:center;justify-content:center;
|
|
|
|
|
font-size:17px;box-shadow:0 2px 6px rgba(0,0,0,.35);
|
|
|
|
|
border:2px solid #fff">🐕</div>`,
|
|
|
|
|
html : `<div style="background:${dotColor};color:#fff;border-radius:50%;
|
|
|
|
|
width:34px;height:34px;
|
|
|
|
|
display:flex;align-items:center;justify-content:center;
|
|
|
|
|
font-size:17px;border:2px solid #fff;
|
|
|
|
|
animation:${anim} 1.8s ease-in-out infinite">🐕</div>`,
|
|
|
|
|
iconSize : [34, 34],
|
|
|
|
|
iconAnchor : [17, 17],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const distStr = r.distanz_m !== undefined
|
|
|
|
|
? (r.distanz_m < 1000 ? `${r.distanz_m} m` : `${(r.distanz_m / 1000).toFixed(1)} km`)
|
|
|
|
|
? (r.distanz_m < 1000 ? `${Math.round(r.distanz_m)} m` : `${(r.distanz_m / 1000).toFixed(1)} km`)
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
const marker = L.marker([r.lat, r.lon], { icon })
|
|
|
|
|
@ -289,10 +324,11 @@ window.Page_lost = (() => {
|
|
|
|
|
<b>🔍 ${_escape(r.name)}</b><br>
|
|
|
|
|
${r.rasse ? _escape(r.rasse) + '<br>' : ''}
|
|
|
|
|
${distStr ? `<small>📍 ${distStr} entfernt</small><br>` : ''}
|
|
|
|
|
${r._isPending ? '<small>⏳ Sync ausstehend</small><br>' : ''}
|
|
|
|
|
<small>📅 ${_fmtDate(r.created_at)}</small>
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
marker.on('click', () => _openDetail(r));
|
|
|
|
|
if (!r._isPending) marker.on('click', () => _openDetail(r));
|
|
|
|
|
_markers.push(marker);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
@ -334,10 +370,19 @@ window.Page_lost = (() => {
|
|
|
|
|
listEl.innerHTML = _reports.map(r => _reportCard(r)).join('');
|
|
|
|
|
listEl.querySelectorAll('[data-lost-id]').forEach(card => {
|
|
|
|
|
card.addEventListener('click', () => {
|
|
|
|
|
const r = _reports.find(x => x.id === parseInt(card.dataset.lostId));
|
|
|
|
|
const id = card.dataset.lostId;
|
|
|
|
|
const r = _reports.find(x => String(x.id) === id && !x._isPending);
|
|
|
|
|
if (r) _openDetail(r);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
listEl.querySelectorAll('.lost-discard-btn').forEach(btn => {
|
|
|
|
|
btn.addEventListener('click', e => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const pid = btn.dataset.pendingId;
|
|
|
|
|
_setPending(_getPending().filter(x => x.id !== pid));
|
|
|
|
|
_loadReports();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
listEl.querySelectorAll('.lost-note-btn').forEach(btn => {
|
|
|
|
|
btn.addEventListener('click', e => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
@ -395,15 +440,24 @@ window.Page_lost = (() => {
|
|
|
|
|
Gemeldet ${_fmtDate(r.created_at)}
|
|
|
|
|
${r.melder_name ? '· ' + _escape(r.melder_name.split(' ')[0]) : ''}
|
|
|
|
|
</div>
|
|
|
|
|
${r._isPending ? `<div style="font-size:10px;color:var(--c-warning,#d97706);font-weight:600">⏳ Sync ausstehend</div>` : ''}
|
|
|
|
|
${_appState.user ? `<div style="margin-top:var(--space-2)">
|
|
|
|
|
<button class="btn btn-ghost btn-xs lost-note-btn"
|
|
|
|
|
data-lost-note-id="${r.id}"
|
|
|
|
|
data-lost-note-name="${_escape(r.name)}"
|
|
|
|
|
title="Notiz" onclick="event.stopPropagation()">
|
|
|
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz
|
|
|
|
|
</button>
|
|
|
|
|
</div>` : ''}
|
|
|
|
|
${r._isPending
|
|
|
|
|
? `<div style="margin-top:var(--space-2);display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
|
|
|
|
|
<span style="font-size:10px;color:var(--c-warning,#d97706);font-weight:600">⏳ Sync ausstehend</span>
|
|
|
|
|
<button class="btn btn-ghost btn-xs lost-discard-btn"
|
|
|
|
|
data-pending-id="${r.id}"
|
|
|
|
|
onclick="event.stopPropagation()"
|
|
|
|
|
style="color:var(--c-danger,#dc2626)">
|
|
|
|
|
🗑 Verwerfen
|
|
|
|
|
</button>
|
|
|
|
|
</div>`
|
|
|
|
|
: (_appState.user ? `<div style="margin-top:var(--space-2)">
|
|
|
|
|
<button class="btn btn-ghost btn-xs lost-note-btn"
|
|
|
|
|
data-lost-note-id="${r.id}"
|
|
|
|
|
data-lost-note-name="${_escape(r.name)}"
|
|
|
|
|
title="Notiz" onclick="event.stopPropagation()">
|
|
|
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz
|
|
|
|
|
</button>
|
|
|
|
|
</div>` : '')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -414,6 +468,7 @@ window.Page_lost = (() => {
|
|
|
|
|
// DETAIL-MODAL
|
|
|
|
|
// ----------------------------------------------------------
|
|
|
|
|
function _openDetail(r) {
|
|
|
|
|
if (r._isPending) return; // Pending-Einträge haben keine Server-ID
|
|
|
|
|
const isOwn = _appState.user && _appState.user.id === r.user_id;
|
|
|
|
|
const isAdmin = _appState.user?.rolle === 'admin';
|
|
|
|
|
const distStr = r.distanz_m !== undefined
|
|
|
|
|
@ -696,19 +751,24 @@ window.Page_lost = (() => {
|
|
|
|
|
client_time : API.clientNow(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!navigator.onLine) {
|
|
|
|
|
const pending = _addPending(payload);
|
|
|
|
|
pending.distanz_m = _userPos
|
|
|
|
|
? Math.round(_haversine(_userPos.lat, _userPos.lon, pending.lat, pending.lon))
|
|
|
|
|
: 0;
|
|
|
|
|
UI.modal.close();
|
|
|
|
|
UI.toast.success('Offline gespeichert — wird synchronisiert sobald Verbindung besteht.');
|
|
|
|
|
_loadReports();
|
|
|
|
|
return;
|
|
|
|
|
let created;
|
|
|
|
|
try {
|
|
|
|
|
created = await API.lost.report(payload);
|
|
|
|
|
} catch (netErr) {
|
|
|
|
|
// Netzwerkfehler (TypeError = fetch failed) → offline speichern
|
|
|
|
|
if (netErr instanceof TypeError || !navigator.onLine) {
|
|
|
|
|
const pending = _addPending(payload);
|
|
|
|
|
pending.distanz_m = _userPos
|
|
|
|
|
? Math.round(_haversine(_userPos.lat, _userPos.lon, pending.lat, pending.lon))
|
|
|
|
|
: 0;
|
|
|
|
|
UI.modal.close();
|
|
|
|
|
UI.toast.success('Offline gespeichert — wird synchronisiert sobald Verbindung besteht.');
|
|
|
|
|
_loadReports();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
throw netErr; // API-Fehler (z.B. 422) → weitergeben
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const created = await API.lost.report(payload);
|
|
|
|
|
|
|
|
|
|
// Foto hochladen
|
|
|
|
|
if (photoInput?.files[0]) {
|
|
|
|
|
try {
|
|
|
|
|
|