Feature: Gassirunden-Chip auf Welcome öffnet direkt ORS-Vorschlag im Routen-Tab — SW by-v479, APP_VER 456
This commit is contained in:
parent
369eae5e5a
commit
ca8bb495b0
4 changed files with 61 additions and 76 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '455'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '456'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
|
|
||||||
const App = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,17 +74,32 @@ window.Page_routes = (() => {
|
||||||
|
|
||||||
// _esc und _emptyState ersetzt durch UI.escape() / UI.emptyState()
|
// _esc und _emptyState ersetzt durch UI.escape() / UI.emptyState()
|
||||||
|
|
||||||
async function init(container, appState) {
|
async function init(container, appState, params = {}) {
|
||||||
_container = container;
|
_container = container;
|
||||||
_appState = appState;
|
_appState = appState;
|
||||||
|
|
||||||
|
// Vorberechneter Vorschlag vom Welcome-Chip → direkt in Suggest-Tab anzeigen
|
||||||
|
if (params._suggestResult) {
|
||||||
|
_suggestResult = params._suggestResult;
|
||||||
|
_suggestKm = params._suggestKm || _suggestKm;
|
||||||
|
_suggestSeed = params._suggestSeed || _suggestSeed;
|
||||||
|
_browseMode = 'suggest';
|
||||||
|
}
|
||||||
|
|
||||||
_render();
|
_render();
|
||||||
UI.loadLeaflet(); // fire & forget — bereit wenn Cards gerendert werden
|
UI.loadLeaflet(); // fire & forget — bereit wenn Cards gerendert werden
|
||||||
try { _userPos = await API.getLocation(); } catch {}
|
try { _userPos = await API.getLocation(); } catch {}
|
||||||
await _loadData();
|
await _loadData();
|
||||||
|
|
||||||
|
// Vorschlag sofort rendern (Leaflet war noch nicht bereit bei _render)
|
||||||
|
if (params._suggestResult) {
|
||||||
|
_renderSuggestTab();
|
||||||
|
_showSuggestResult(params._suggestResult);
|
||||||
|
}
|
||||||
|
|
||||||
// Deep-Link: /#routes?id=123 → direkt Route-Detail öffnen
|
// Deep-Link: /#routes?id=123 → direkt Route-Detail öffnen
|
||||||
const params = new URLSearchParams((location.hash.split('?')[1] || ''));
|
const urlParams = new URLSearchParams((location.hash.split('?')[1] || ''));
|
||||||
const deepId = params.get('id');
|
const deepId = urlParams.get('id');
|
||||||
if (deepId) {
|
if (deepId) {
|
||||||
_openDetail(parseInt(deepId, 10));
|
_openDetail(parseInt(deepId, 10));
|
||||||
}
|
}
|
||||||
|
|
@ -486,19 +501,22 @@ window.Page_routes = (() => {
|
||||||
}
|
}
|
||||||
if (calcBtn) calcBtn.disabled = false;
|
if (calcBtn) calcBtn.disabled = false;
|
||||||
|
|
||||||
// Ergebnis rendern
|
_showSuggestResult(result);
|
||||||
const distStr = result.distanz_km ? result.distanz_km.toFixed(2) + ' km' : '–';
|
}
|
||||||
const durStr = result.dauer_min
|
|
||||||
|
function _showSuggestResult(result) {
|
||||||
|
_suggestResult = result;
|
||||||
|
const res = document.getElementById('rks-result');
|
||||||
|
if (!res) return;
|
||||||
|
|
||||||
|
const distStr = result.distanz_km ? result.distanz_km.toFixed(2) + ' km' : '–';
|
||||||
|
const durStr = result.dauer_min
|
||||||
? (result.dauer_min < 60 ? result.dauer_min + ' min' : Math.floor(result.dauer_min/60) + 'h ' + (result.dauer_min%60||'') + 'min').trim()
|
? (result.dauer_min < 60 ? result.dauer_min + ' min' : Math.floor(result.dauer_min/60) + 'h ' + (result.dauer_min%60||'') + 'min').trim()
|
||||||
: '–';
|
: '–';
|
||||||
const diffLabel = { leicht: 'Leicht', mittel: 'Mittel', anspruchsvoll: 'Schwer' }[result.schwierigkeit] || '';
|
const diffLabel = { leicht: 'Leicht', mittel: 'Mittel', anspruchsvoll: 'Schwer' }[result.schwierigkeit] || '';
|
||||||
|
|
||||||
if (!res) return;
|
|
||||||
res.innerHTML = `
|
res.innerHTML = `
|
||||||
<!-- Karte -->
|
|
||||||
<div id="rks-map" style="height:250px;background:var(--c-surface);margin-bottom:var(--space-3)"></div>
|
<div id="rks-map" style="height:250px;background:var(--c-surface);margin-bottom:var(--space-3)"></div>
|
||||||
|
|
||||||
<!-- Info-Zeile -->
|
|
||||||
<div style="display:flex;gap:var(--space-3);align-items:center;flex-wrap:wrap;margin-bottom:var(--space-4)">
|
<div style="display:flex;gap:var(--space-3);align-items:center;flex-wrap:wrap;margin-bottom:var(--space-4)">
|
||||||
<span style="${_pillStyle('rgba(107,114,128,0.10)','#9ca3af','rgba(107,114,128,0.30)')}">
|
<span style="${_pillStyle('rgba(107,114,128,0.10)','#9ca3af','rgba(107,114,128,0.30)')}">
|
||||||
${UI.icon('map-trifold')} ${UI.escape(distStr)}
|
${UI.icon('map-trifold')} ${UI.escape(distStr)}
|
||||||
|
|
@ -513,8 +531,6 @@ window.Page_routes = (() => {
|
||||||
<span style="font-size:0.85rem;color:var(--c-text-secondary);flex:1;min-width:0;
|
<span style="font-size:0.85rem;color:var(--c-text-secondary);flex:1;min-width:0;
|
||||||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(result.name || '')}</span>
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(result.name || '')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Aktions-Buttons -->
|
|
||||||
<div style="display:flex;gap:var(--space-3)">
|
<div style="display:flex;gap:var(--space-3)">
|
||||||
<button id="rks-nav-btn" style="${_btnStyle(false)}flex:1">
|
<button id="rks-nav-btn" style="${_btnStyle(false)}flex:1">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#navigation-arrow"></use></svg>
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#navigation-arrow"></use></svg>
|
||||||
|
|
@ -524,63 +540,42 @@ window.Page_routes = (() => {
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#floppy-disk"></use></svg>
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#floppy-disk"></use></svg>
|
||||||
Route speichern
|
Route speichern
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>`;
|
||||||
`;
|
|
||||||
|
|
||||||
// Leaflet-Karte mit dem berechneten Track
|
|
||||||
const _initMap = () => {
|
const _initMap = () => {
|
||||||
const mapEl = document.getElementById('rks-map');
|
const mapEl = document.getElementById('rks-map');
|
||||||
if (!mapEl || !window.L) return;
|
if (!mapEl || !window.L) return;
|
||||||
if (_suggestMap) { _suggestMap.remove(); _suggestMap = null; }
|
if (_suggestMap) { _suggestMap.remove(); _suggestMap = null; }
|
||||||
|
|
||||||
const track = result.gps_track || [];
|
const track = result.gps_track || [];
|
||||||
if (track.length < 2) { mapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:var(--c-text-muted)">Kein Track vorhanden</div>'; return; }
|
if (track.length < 2) return;
|
||||||
|
const lls = track.map(p => [p.lat, p.lon]);
|
||||||
const lls = track.map(p => [p.lat, p.lon]);
|
|
||||||
_suggestMap = L.map(mapEl, { zoomControl: false, attributionControl: false,
|
_suggestMap = L.map(mapEl, { zoomControl: false, attributionControl: false,
|
||||||
dragging: true, touchZoom: true, scrollWheelZoom: false });
|
dragging: true, touchZoom: true, scrollWheelZoom: false });
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_suggestMap);
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_suggestMap);
|
||||||
const poly = L.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.9 }).addTo(_suggestMap);
|
const poly = L.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.9 }).addTo(_suggestMap);
|
||||||
L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1, weight:2 }).addTo(_suggestMap);
|
L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1, weight:2 }).addTo(_suggestMap);
|
||||||
L.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap);
|
L.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap);
|
||||||
_addRouteArrows(_suggestMap, track, '#3b82f6');
|
_addRouteArrows(_suggestMap, track, '#3b82f6');
|
||||||
_suggestMap.fitBounds(poly.getBounds(), { padding: [16, 16] });
|
_suggestMap.fitBounds(poly.getBounds(), { padding: [16, 16] });
|
||||||
setTimeout(() => _suggestMap?.invalidateSize(), 120);
|
setTimeout(() => _suggestMap?.invalidateSize(), 120);
|
||||||
};
|
};
|
||||||
|
if (window.L) { _initMap(); } else {
|
||||||
if (window.L) {
|
|
||||||
_initMap();
|
|
||||||
} else {
|
|
||||||
let tries = 0;
|
let tries = 0;
|
||||||
const poll = setInterval(() => {
|
const poll = setInterval(() => {
|
||||||
if (window.L || ++tries > 40) { clearInterval(poll); if (window.L) _initMap(); }
|
if (window.L || ++tries > 40) { clearInterval(poll); if (window.L) _initMap(); }
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation starten
|
|
||||||
document.getElementById('rks-nav-btn')?.addEventListener('click', () => {
|
document.getElementById('rks-nav-btn')?.addEventListener('click', () => {
|
||||||
if (!_suggestResult) return;
|
_openNavOverlay({ id: 'suggest-' + Date.now(), name: result.name,
|
||||||
const route = {
|
gps_track: result.gps_track, distanz_km: result.distanz_km });
|
||||||
id: 'suggest-' + Date.now(),
|
|
||||||
name: _suggestResult.name,
|
|
||||||
gps_track: _suggestResult.gps_track,
|
|
||||||
distanz_km: _suggestResult.distanz_km,
|
|
||||||
};
|
|
||||||
_openNavOverlay(route);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route speichern
|
|
||||||
document.getElementById('rks-save-btn')?.addEventListener('click', async () => {
|
document.getElementById('rks-save-btn')?.addEventListener('click', async () => {
|
||||||
const btn = document.getElementById('rks-save-btn');
|
const btn = document.getElementById('rks-save-btn');
|
||||||
if (!btn || !_suggestResult) return;
|
if (!btn) return;
|
||||||
await UI.asyncButton(btn, async () => {
|
await UI.asyncButton(btn, async () => {
|
||||||
await API.post('/routes', {
|
await API.post('/routes', { name: result.name, gps_track: result.gps_track,
|
||||||
name: _suggestResult.name,
|
distanz_km: result.distanz_km, dauer_min: result.dauer_min, schwierigkeit: result.schwierigkeit });
|
||||||
gps_track: _suggestResult.gps_track,
|
|
||||||
distanz_km: _suggestResult.distanz_km,
|
|
||||||
dauer_min: _suggestResult.dauer_min,
|
|
||||||
schwierigkeit: _suggestResult.schwierigkeit,
|
|
||||||
});
|
|
||||||
UI.toast.success('Route gespeichert!');
|
UI.toast.success('Route gespeichert!');
|
||||||
await _loadData();
|
await _loadData();
|
||||||
_setBrowseMode('mine');
|
_setBrowseMode('mine');
|
||||||
|
|
|
||||||
|
|
@ -363,55 +363,45 @@ window.Page_welcome = (() => {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Versucht async eine Bank in 2 km Umkreis zu finden und ersetzt Chip 2
|
// Berechnet async eine Tages-Gassirunde via ORS und ersetzt Chip 2
|
||||||
async function _tryBenchChip(dashData) {
|
async function _tryRouteChip(dashData) {
|
||||||
if (dashData?.next_appointment) return; // Termin hat Vorrang
|
if (dashData?.next_appointment) return; // Termin hat Vorrang
|
||||||
let loc;
|
let loc;
|
||||||
try { loc = await API.getLocation({ timeout: 5000, maximumAge: 300000 }); }
|
try { loc = await API.getLocation({ timeout: 5000, maximumAge: 300000 }); }
|
||||||
catch { return; }
|
catch { return; }
|
||||||
|
|
||||||
const d = 0.018; // ~2 km in Grad
|
// Täglich stabile, aber rotierende Distanz + Variante
|
||||||
let pois;
|
const dayIdx = Math.floor(Date.now() / 86400000);
|
||||||
|
const km = [2, 4, 6][dayIdx % 3];
|
||||||
|
const seed = dayIdx % 5;
|
||||||
|
|
||||||
|
let result;
|
||||||
try {
|
try {
|
||||||
pois = await API.osm.pois('bank', loc.lat - d, loc.lon - d, loc.lat + d, loc.lon + d);
|
result = await API.post('/routes/suggest', { lat: loc.lat, lon: loc.lon, distance_km: km, seed });
|
||||||
} catch { return; }
|
} catch { return; }
|
||||||
|
if (!result?.gps_track?.length) return;
|
||||||
if (!pois || pois.length === 0) return;
|
|
||||||
|
|
||||||
// täglich stabile Auswahl, aber täglich andere Bank
|
|
||||||
const dayIdx = Math.floor(Date.now() / 86400000);
|
|
||||||
const bench = pois[dayIdx % pois.length];
|
|
||||||
const distM = Math.round(_haversine(loc.lat, loc.lon, bench.lat, bench.lon));
|
|
||||||
const distTxt = distM < 1000 ? `${distM} m` : `${(distM / 1000).toFixed(1)} km`;
|
|
||||||
const name = bench.name || 'Bank';
|
|
||||||
|
|
||||||
const chipsRow = _container.querySelector('#wc-chips-row');
|
const chipsRow = _container.querySelector('#wc-chips-row');
|
||||||
if (!chipsRow) return;
|
if (!chipsRow) return;
|
||||||
|
|
||||||
// Chip ggf. schon da (Termin-Chip) oder neu einfügen (nach Chip 1)
|
|
||||||
let chip2 = _container.querySelector('#wc-chip-mid');
|
let chip2 = _container.querySelector('#wc-chip-mid');
|
||||||
if (!chip2) {
|
if (!chip2) {
|
||||||
chip2 = document.createElement('div');
|
chip2 = document.createElement('div');
|
||||||
chip2.className = 'wc-chip';
|
chip2.className = 'wc-chip';
|
||||||
chip2.id = 'wc-chip-mid';
|
chip2.id = 'wc-chip-mid';
|
||||||
// nach erstem Chip einfügen
|
|
||||||
const first = chipsRow.querySelector('.wc-chip');
|
const first = chipsRow.querySelector('.wc-chip');
|
||||||
first ? first.after(chip2) : chipsRow.prepend(chip2);
|
first ? first.after(chip2) : chipsRow.prepend(chip2);
|
||||||
}
|
}
|
||||||
chip2.dataset.nav = 'map';
|
const durStr = result.dauer_min < 60
|
||||||
|
? `${result.dauer_min} min`
|
||||||
|
: `${Math.floor(result.dauer_min / 60)}h ${result.dauer_min % 60}min`;
|
||||||
chip2.innerHTML = `
|
chip2.innerHTML = `
|
||||||
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#path"></use></svg>
|
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#path"></use></svg>
|
||||||
<span class="wc-chip-label">Gassirunde</span>
|
<span class="wc-chip-label">Gassirunde</span>
|
||||||
<span class="wc-chip-val">${UI.escape(name)} · ${distTxt}</span>`;
|
<span class="wc-chip-val">${result.distanz_km} km · ${durStr}</span>`;
|
||||||
chip2.addEventListener('click', () => App.navigate('map'));
|
chip2.addEventListener('click', () => {
|
||||||
}
|
App.navigate('routes', true, { _suggestResult: result, _suggestKm: km, _suggestSeed: seed });
|
||||||
|
});
|
||||||
function _haversine(lat1, lon1, lat2, lon2) {
|
|
||||||
const R = 6371000;
|
|
||||||
const f1 = lat1 * Math.PI / 180, f2 = lat2 * Math.PI / 180;
|
|
||||||
const df = (lat2 - lat1) * Math.PI / 180, dl = (lon2 - lon1) * Math.PI / 180;
|
|
||||||
const a = Math.sin(df/2)**2 + Math.cos(f1)*Math.cos(f2)*Math.sin(dl/2)**2;
|
|
||||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -473,7 +463,7 @@ window.Page_welcome = (() => {
|
||||||
API.dogs.welcomeDashboard(dog.id).then(dash => {
|
API.dogs.welcomeDashboard(dog.id).then(dash => {
|
||||||
_updateHeroFromDash(dash, dog);
|
_updateHeroFromDash(dash, dog);
|
||||||
_updateChipsFromDash(dash);
|
_updateChipsFromDash(dash);
|
||||||
_tryBenchChip(dash); // nach Chips-Update: ggf. mit naher Bank ersetzen
|
_tryRouteChip(dash); // nach Chips-Update: ggf. Gassirunden-Vorschlag einfügen
|
||||||
}).catch(() => { /* Skeleton bleibt sichtbar */ });
|
}).catch(() => { /* Skeleton bleibt sichtbar */ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v478';
|
const CACHE_VERSION = 'by-v479';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue