update editor and add tests
This commit is contained in:
+271
-161
@@ -6,10 +6,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
const ids = Object.freeze({
|
||||
addPoi: 'editor-add-poi',
|
||||
button: 'editor-button',
|
||||
coordinateGrid: 'editor-coordinate-grid',
|
||||
close: 'editor-close',
|
||||
dialog: 'editor-dialog',
|
||||
entryTitle: 'editor-entry-title',
|
||||
newRoute: 'editor-new-route',
|
||||
poiAudio: 'editor-poi-audio',
|
||||
poiCancel: 'editor-poi-cancel',
|
||||
@@ -23,53 +21,33 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
poiList: 'editor-poi-list',
|
||||
poiLon: 'editor-poi-lon',
|
||||
poiRadius: 'editor-poi-radius',
|
||||
poiSave: 'editor-poi-save',
|
||||
poiTitle: 'editor-poi-title',
|
||||
routeActivated: 'editor-route-activated',
|
||||
routeBack: 'editor-route-back',
|
||||
routeDescription: 'editor-route-description',
|
||||
routeDetailTitle: 'editor-route-detail-title',
|
||||
routeDetailView: 'editor-route-detail-view',
|
||||
routeForm: 'editor-route-form',
|
||||
routeGpx: 'editor-route-gpx',
|
||||
routeId: 'editor-route-id',
|
||||
routeList: 'editor-route-list',
|
||||
routeListTitle: 'editor-route-list-title',
|
||||
routeListView: 'editor-route-list-view',
|
||||
routeName: 'editor-route-name',
|
||||
routeSave: 'editor-route-save',
|
||||
status: 'editor-status',
|
||||
title: 'editor-dialog-title'
|
||||
status: 'editor-status'
|
||||
});
|
||||
|
||||
|
||||
const classes = Object.freeze({
|
||||
button: 'editor-button',
|
||||
dialog: 'editor-dialog',
|
||||
dialogHeader: 'editor-dialog-header',
|
||||
dialogShell: 'editor-dialog-shell',
|
||||
entryPanel: 'editor-entry-panel',
|
||||
form: 'editor-form',
|
||||
iconButton: 'editor-icon-button',
|
||||
iconRow: 'editor-icon-row',
|
||||
muted: 'editor-muted',
|
||||
poiItem: 'editor-poi-item',
|
||||
poiList: 'editor-poi-list',
|
||||
poiToolbar: 'editor-poi-toolbar',
|
||||
primaryButton: 'editor-primary-button',
|
||||
routeItem: 'editor-route-item',
|
||||
routeList: 'editor-route-list',
|
||||
roundButton: 'editor-round-button',
|
||||
secondaryButton: 'editor-secondary-button',
|
||||
status: 'editor-status',
|
||||
toolbar: 'editor-toolbar',
|
||||
toolbarBack: 'editor-toolbar-back',
|
||||
view: 'editor-view'
|
||||
routeGroup: 'editor-route-group',
|
||||
routeItem: 'editor-route-item'
|
||||
});
|
||||
|
||||
const state = {
|
||||
currentPoi: null,
|
||||
currentRoute: null,
|
||||
currentSource: null,
|
||||
pois: [],
|
||||
routes: []
|
||||
};
|
||||
@@ -194,6 +172,22 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
});
|
||||
}
|
||||
|
||||
async function parseJson(response) {
|
||||
const raw = await response.text();
|
||||
if (!raw.trim()) return {};
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResponse(response) {
|
||||
if (response.ok) return parseJson(response);
|
||||
const message = await response.text();
|
||||
throw new Error(message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
async function readJson(path) {
|
||||
const response = await fetch(apiUrl(path), {
|
||||
credentials: 'same-origin',
|
||||
@@ -225,22 +219,6 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
async function handleResponse(response) {
|
||||
if (response.ok) return parseJson(response);
|
||||
const message = await response.text();
|
||||
throw new Error(message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
async function parseJson(response) {
|
||||
const raw = await response.text();
|
||||
if (!raw.trim()) return {};
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function routeArray(payload) {
|
||||
if (Array.isArray(payload)) return payload;
|
||||
if (Array.isArray(payload?.routes)) return payload.routes;
|
||||
@@ -273,55 +251,71 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
return button;
|
||||
}
|
||||
|
||||
function routeSource(route) {
|
||||
return route?._source || route?.source || 'server';
|
||||
}
|
||||
|
||||
function routeItem(route) {
|
||||
const item = document.createElement('li');
|
||||
item.className = classes.routeItem;
|
||||
item.dataset.routeId = routeId(route);
|
||||
item.dataset.routeSource = routeSource(route);
|
||||
|
||||
const label = document.createElement('div');
|
||||
appendText(label, 'strong', routeName(route));
|
||||
appendText(label, 'small', route?.activatedAt || route?.activated_at ? 'aktiviert' : 'Entwurf oder ohne Aktivierungsdatum');
|
||||
appendText(label, 'small', routeSource(route) === 'local' ? 'Lokaler Entwurf' : 'Veröffentlichte Strecke');
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = classes.iconRow;
|
||||
actions.appendChild(iconButton('Strecke bearbeiten', '✎', 'edit-route'));
|
||||
|
||||
item.append(label, actions);
|
||||
return item;
|
||||
}
|
||||
|
||||
function routeGroup(title) {
|
||||
const item = document.createElement('li');
|
||||
item.className = classes.routeGroup;
|
||||
item.textContent = title;
|
||||
return item;
|
||||
}
|
||||
|
||||
function poiItem(poi, index) {
|
||||
const item = document.createElement('li');
|
||||
item.className = classes.poiItem;
|
||||
item.dataset.poiId = poiId(poi);
|
||||
|
||||
const title = poiTitle(poi);
|
||||
const label = document.createElement('div');
|
||||
appendText(label, 'strong', title);
|
||||
appendText(label, 'small', `Position ${index + 1}`);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = classes.iconRow;
|
||||
actions.appendChild(iconButton(`${title} nach oben schieben`, '↑', 'move-poi-up', index === 0));
|
||||
actions.appendChild(iconButton(`${title} nach unten schieben`, '↓', 'move-poi-down', index === state.pois.length - 1));
|
||||
actions.appendChild(iconButton(`${title} bearbeiten`, '✎', 'edit-poi'));
|
||||
|
||||
item.append(label, actions);
|
||||
return item;
|
||||
}
|
||||
|
||||
function renderRouteGroup(list, title, routes, emptyText) {
|
||||
list.appendChild(routeGroup(title));
|
||||
if (!routes.length) {
|
||||
const item = document.createElement('li');
|
||||
item.className = classes.routeItem;
|
||||
appendText(item, 'span', emptyText);
|
||||
list.appendChild(item);
|
||||
return;
|
||||
}
|
||||
routes.forEach(route => list.appendChild(routeItem(route)));
|
||||
}
|
||||
|
||||
function renderRouteList() {
|
||||
const list = byId(ids.routeList);
|
||||
if (!list) return;
|
||||
list.textContent = '';
|
||||
if (!state.routes.length) {
|
||||
const item = document.createElement('li');
|
||||
item.className = classes.routeItem;
|
||||
appendText(item, 'span', 'Noch keine Strecken gefunden.');
|
||||
list.appendChild(item);
|
||||
return;
|
||||
}
|
||||
state.routes.forEach(route => list.appendChild(routeItem(route)));
|
||||
const serverRoutes = state.routes.filter(route => routeSource(route) === 'server');
|
||||
const localRoutes = state.routes.filter(route => routeSource(route) === 'local');
|
||||
renderRouteGroup(list, 'Veröffentlichte Strecken', serverRoutes, 'Keine veröffentlichte Strecke vorhanden.');
|
||||
renderRouteGroup(list, 'Lokale Strecken', localRoutes, 'Noch kein lokaler Entwurf vorhanden.');
|
||||
}
|
||||
|
||||
function renderPoiList() {
|
||||
@@ -338,12 +332,28 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
state.pois.forEach((poi, index) => list.appendChild(poiItem(poi, index)));
|
||||
}
|
||||
|
||||
function localRouteSummaries() {
|
||||
if (!ns.LocalRoutes) return [];
|
||||
return ns.LocalRoutes.list().map(routePackage => ({
|
||||
...routePackage.route,
|
||||
id: routePackage.id,
|
||||
_source: 'local'
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadRoutes() {
|
||||
setStatus('Strecken werden geladen …');
|
||||
const payload = await readJson('/routes');
|
||||
state.routes = routeArray(payload);
|
||||
const payload = await readJson('/routes?activatedOnly=true');
|
||||
const serverRoutes = routeArray(payload).map(route => ({ ...route, _source: 'server' }));
|
||||
let localRoutes = [];
|
||||
try {
|
||||
localRoutes = localRouteSummaries();
|
||||
} catch (error) {
|
||||
setStatus(error.message, true);
|
||||
}
|
||||
state.routes = [...serverRoutes, ...localRoutes];
|
||||
renderRouteList();
|
||||
setStatus('');
|
||||
if (!byId(ids.status)?.dataset.kind || byId(ids.status).dataset.kind !== 'error') setStatus('');
|
||||
}
|
||||
|
||||
async function openEditor() {
|
||||
@@ -358,6 +368,11 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
await guarded(loadRoutes);
|
||||
}
|
||||
|
||||
async function openNewLocalRoute() {
|
||||
await openEditor();
|
||||
newRoute();
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
const dialog = byId(ids.dialog);
|
||||
if (dialog?.open && typeof dialog.close === 'function') dialog.close();
|
||||
@@ -377,11 +392,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
return {
|
||||
description: byId(ids.poiDescription)?.value.trim() || '',
|
||||
lat: numberOrNull(byId(ids.poiLat)?.value),
|
||||
latitude: numberOrNull(byId(ids.poiLat)?.value),
|
||||
lon: numberOrNull(byId(ids.poiLon)?.value),
|
||||
longitude: numberOrNull(byId(ids.poiLon)?.value),
|
||||
radiusM: numberOrNull(byId(ids.poiRadius)?.value) || 25,
|
||||
sortOrder: state.currentPoi?.sortOrder ?? state.pois.length,
|
||||
triggerRadiusM: numberOrNull(byId(ids.poiRadius)?.value) || 25,
|
||||
sequence: state.currentPoi?.sequence ?? state.pois.length,
|
||||
title: byId(ids.poiTitle)?.value.trim() || ''
|
||||
};
|
||||
}
|
||||
@@ -391,9 +404,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
byId(ids.routeName).value = routeName(route) === 'Unbenannte Strecke' ? '' : routeName(route);
|
||||
byId(ids.routeDescription).value = text(route?.description);
|
||||
byId(ids.routeGpx).value = '';
|
||||
byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at
|
||||
? `Aktiviert am ${route.activatedAt || route.activated_at}. Änderung der Freigabe erfolgt weiterhin manuell in der Datenbank.`
|
||||
: 'Nicht veröffentlicht: activated_at ist leer und wird durch den Editor nicht gesetzt.';
|
||||
if (state.currentSource === 'local') {
|
||||
byId(ids.routeActivated).textContent = 'Lokaler Entwurf: GPX, Texte und Medien bleiben ausschließlich im Browser. Eine Server-Freigabe ist noch nicht implementiert.';
|
||||
} else {
|
||||
byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at
|
||||
? `Veröffentlicht am ${route.activatedAt || route.activated_at}. Der Editor verändert die Freigabe nicht.`
|
||||
: 'Server-Strecke ohne Freigabedatum. Der Editor verändert activated_at nicht.';
|
||||
}
|
||||
}
|
||||
|
||||
function fillPoiForm(poi) {
|
||||
@@ -402,7 +419,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
byId(ids.poiDescription).value = text(poi?.description || poi?.text);
|
||||
byId(ids.poiLat).value = text(poi?.lat ?? poi?.latitude);
|
||||
byId(ids.poiLon).value = text(poi?.lon ?? poi?.longitude);
|
||||
byId(ids.poiRadius).value = text(poi?.radiusM ?? poi?.radius ?? 25);
|
||||
byId(ids.poiRadius).value = text(poi?.triggerRadiusM ?? poi?.radiusM ?? poi?.radius ?? 25);
|
||||
byId(ids.poiAudio).value = '';
|
||||
byId(ids.poiImages).value = '';
|
||||
}
|
||||
@@ -411,23 +428,47 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
const id = routeId(route);
|
||||
if (!id) return;
|
||||
setStatus('Streckendetails werden geladen …');
|
||||
const payload = await readJson(`/routes/${encodeURIComponent(id)}`);
|
||||
const detail = payload.route || payload;
|
||||
state.currentRoute = detail;
|
||||
state.pois = poiArray(detail);
|
||||
fillRouteForm(detail);
|
||||
state.currentSource = routeSource(route);
|
||||
|
||||
if (state.currentSource === 'local') {
|
||||
const routePackage = ns.LocalRoutes.get(id);
|
||||
if (!routePackage) throw new Error('Der lokale Entwurf wurde nicht gefunden.');
|
||||
state.currentRoute = {
|
||||
...routePackage.route,
|
||||
id: routePackage.id,
|
||||
gpx: routePackage.gpx,
|
||||
_source: 'local'
|
||||
};
|
||||
state.pois = [...routePackage.pois].sort((left, right) => Number(left.sequence) - Number(right.sequence));
|
||||
} else {
|
||||
const payload = await readJson(`/routes/${encodeURIComponent(id)}`);
|
||||
const detail = payload.route || payload;
|
||||
state.currentRoute = { ...detail, _source: 'server' };
|
||||
state.pois = poiArray(detail);
|
||||
}
|
||||
|
||||
state.currentPoi = null;
|
||||
fillRouteForm(state.currentRoute);
|
||||
renderPoiList();
|
||||
showRouteView();
|
||||
setStatus('');
|
||||
}
|
||||
|
||||
function newRoute() {
|
||||
state.currentRoute = null;
|
||||
state.currentSource = 'local';
|
||||
state.currentRoute = {
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
gpx: ns.LocalRoutes.emptyGpx('Neue Strecke'),
|
||||
_source: 'local'
|
||||
};
|
||||
state.currentPoi = null;
|
||||
state.pois = [];
|
||||
fillRouteForm({});
|
||||
fillRouteForm(state.currentRoute);
|
||||
renderPoiList();
|
||||
showRouteView();
|
||||
setStatus('Neue Strecke wird als lokaler GPX-/JSON-Entwurf angelegt.');
|
||||
}
|
||||
|
||||
function formDataFromRoute(values) {
|
||||
@@ -439,52 +480,137 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
return data;
|
||||
}
|
||||
|
||||
async function saveLocalRoute(values) {
|
||||
const file = byId(ids.routeGpx)?.files?.[0];
|
||||
let gpx = state.currentRoute?.gpx || ns.LocalRoutes.emptyGpx(values.name);
|
||||
if (file) {
|
||||
gpx = await ns.LocalRoutes.readFileAsText(file);
|
||||
const importedPois = ns.LocalRoutes.parseWaypoints(gpx);
|
||||
if (!state.pois.length && importedPois.length) state.pois = importedPois;
|
||||
}
|
||||
|
||||
const id = routeId(state.currentRoute) || ns.LocalRoutes.createRouteId();
|
||||
const saved = ns.LocalRoutes.save({
|
||||
id,
|
||||
gpx,
|
||||
route: {
|
||||
...(state.currentRoute || {}),
|
||||
id,
|
||||
name: values.name,
|
||||
description: values.description
|
||||
},
|
||||
pois: state.pois
|
||||
});
|
||||
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
|
||||
state.currentSource = 'local';
|
||||
state.pois = saved.pois;
|
||||
setStatus('Lokaler Entwurf gespeichert. Es wurden keine Daten an den Server übertragen.');
|
||||
await loadRoutes();
|
||||
await loadRoute({ id: saved.id, _source: 'local' });
|
||||
}
|
||||
|
||||
async function saveServerRoute(values) {
|
||||
const id = routeId(state.currentRoute);
|
||||
if (!id) throw new Error('Neue Strecken werden ausschließlich lokal angelegt.');
|
||||
const file = byId(ids.routeGpx)?.files?.[0];
|
||||
const path = `/routes/${encodeURIComponent(id)}`;
|
||||
const payload = file
|
||||
? await sendForm(path, 'PUT', formDataFromRoute(values))
|
||||
: await sendJson(path, 'PUT', values);
|
||||
const saved = payload.route || payload;
|
||||
state.currentRoute = { ...saved, _source: 'server' };
|
||||
setStatus('Veröffentlichte Strecke gespeichert. activated_at wurde nicht verändert.');
|
||||
await loadRoutes();
|
||||
await loadRoute({ id, _source: 'server' });
|
||||
}
|
||||
|
||||
async function saveRoute() {
|
||||
const values = routeFromForm();
|
||||
if (!values.name) throw new Error('Der Name der Strecke ist erforderlich.');
|
||||
const id = byId(ids.routeId).value;
|
||||
const file = byId(ids.routeGpx)?.files?.[0];
|
||||
const path = id ? `/routes/${encodeURIComponent(id)}` : '/routes';
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
const payload = file ? await sendForm(path, method, formDataFromRoute(values)) : await sendJson(path, method, values);
|
||||
const saved = payload.route || payload;
|
||||
state.currentRoute = Object.keys(saved).length ? saved : { ...values, id };
|
||||
setStatus('Strecke gespeichert. Neue Strecken bleiben bis zur manuellen Aktivierung unveröffentlicht.');
|
||||
await loadRoutes();
|
||||
if (routeId(state.currentRoute)) await loadRoute(state.currentRoute);
|
||||
if (state.currentSource === 'server') await saveServerRoute(values);
|
||||
else await saveLocalRoute(values);
|
||||
}
|
||||
|
||||
function currentPosition() {
|
||||
return typeof ns.App?.currentPosition === 'function' ? ns.App.currentPosition() : null;
|
||||
}
|
||||
|
||||
function newPoi() {
|
||||
state.currentPoi = null;
|
||||
fillPoiForm({ radiusM: 25 });
|
||||
const position = currentPosition();
|
||||
fillPoiForm({
|
||||
lat: position?.lat ?? '',
|
||||
lon: position?.lon ?? '',
|
||||
triggerRadiusM: 25
|
||||
});
|
||||
byId(ids.poiFormTitle).textContent = state.currentSource === 'local' ? 'GPX-POI hinzufügen' : 'Station hinzufügen';
|
||||
showPoiView();
|
||||
}
|
||||
|
||||
function editPoi(poi) {
|
||||
state.currentPoi = poi;
|
||||
fillPoiForm(poi);
|
||||
byId(ids.poiFormTitle).textContent = 'Station bearbeiten';
|
||||
showPoiView();
|
||||
}
|
||||
|
||||
async function savePoi() {
|
||||
const route = state.currentRoute;
|
||||
const currentRouteId = routeId(route);
|
||||
if (!currentRouteId) throw new Error('Speichere zuerst die Strecke.');
|
||||
const values = poiFromForm();
|
||||
if (!values.title) throw new Error('Der Titel der Station ist erforderlich.');
|
||||
async function mediaFromLocalForm(existingPoi, poiIdentifier) {
|
||||
const audioFile = byId(ids.poiAudio)?.files?.[0];
|
||||
const imageFiles = Array.from(byId(ids.poiImages)?.files || []);
|
||||
const audio = audioFile ? await ns.LocalRoutes.fileToDataUrl(audioFile) : existingPoi?.audio || null;
|
||||
const images = [...(existingPoi?.images || [])];
|
||||
for (const imageFile of imageFiles) {
|
||||
const image = await ns.LocalRoutes.fileToDataUrl(imageFile);
|
||||
images.push({
|
||||
...image,
|
||||
id: `${poiIdentifier}-image-${Date.now()}-${images.length}`,
|
||||
caption: ''
|
||||
});
|
||||
}
|
||||
return { audio, images };
|
||||
}
|
||||
|
||||
const currentPoiId = byId(ids.poiId).value;
|
||||
const path = currentPoiId
|
||||
? `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`
|
||||
: `/routes/${encodeURIComponent(currentRouteId)}/pois`;
|
||||
const method = currentPoiId ? 'PUT' : 'POST';
|
||||
const payload = await sendJson(path, method, values);
|
||||
const saved = payload.poi || payload;
|
||||
const savedPoiId = poiId(saved) || currentPoiId;
|
||||
async function persistLocalCurrentRoute() {
|
||||
const id = routeId(state.currentRoute);
|
||||
if (!id) throw new Error('Speichere zuerst die Strecke.');
|
||||
const saved = ns.LocalRoutes.save({
|
||||
id,
|
||||
gpx: state.currentRoute.gpx,
|
||||
route: state.currentRoute,
|
||||
pois: state.pois
|
||||
});
|
||||
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
|
||||
state.pois = saved.pois;
|
||||
return saved;
|
||||
}
|
||||
|
||||
if (savedPoiId) await uploadPoiMedia(currentRouteId, savedPoiId);
|
||||
setStatus('Station gespeichert.');
|
||||
await loadRoute(route);
|
||||
async function saveLocalPoi(values) {
|
||||
if (!Number.isFinite(values.lat) || !Number.isFinite(values.lon)) {
|
||||
throw new Error('Für einen GPX-POI sind gültige Breiten- und Längengrade erforderlich.');
|
||||
}
|
||||
const currentId = Number(byId(ids.poiId).value);
|
||||
const identifier = Number.isSafeInteger(currentId) ? currentId : ns.LocalRoutes.createPoiId();
|
||||
const existing = state.pois.find(poi => Number(poi.id) === identifier) || state.currentPoi;
|
||||
const media = await mediaFromLocalForm(existing, identifier);
|
||||
const savedPoi = {
|
||||
...(existing || {}),
|
||||
...values,
|
||||
...media,
|
||||
id: identifier,
|
||||
sequence: existing?.sequence ?? state.pois.length
|
||||
};
|
||||
const index = state.pois.findIndex(poi => Number(poi.id) === identifier);
|
||||
if (index >= 0) state.pois[index] = savedPoi;
|
||||
else state.pois.push(savedPoi);
|
||||
await persistLocalCurrentRoute();
|
||||
setStatus('GPX-POI und zugehörige JSON-Mediendaten lokal gespeichert.');
|
||||
await loadRoute({ id: routeId(state.currentRoute), _source: 'local' });
|
||||
}
|
||||
|
||||
async function uploadOne(path, file) {
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
return sendForm(path, 'POST', data);
|
||||
}
|
||||
|
||||
async function uploadPoiMedia(currentRouteId, currentPoiId) {
|
||||
@@ -496,30 +622,57 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadOne(path, file) {
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
return sendForm(path, 'POST', data);
|
||||
async function saveServerPoi(values) {
|
||||
const currentRouteId = routeId(state.currentRoute);
|
||||
if (!currentRouteId) throw new Error('Speichere zuerst die Strecke.');
|
||||
const currentPoiId = byId(ids.poiId).value;
|
||||
const path = currentPoiId
|
||||
? `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`
|
||||
: `/routes/${encodeURIComponent(currentRouteId)}/pois`;
|
||||
const method = currentPoiId ? 'PUT' : 'POST';
|
||||
const payload = await sendJson(path, method, values);
|
||||
const saved = payload.poi || payload;
|
||||
const savedPoiId = poiId(saved) || currentPoiId;
|
||||
if (savedPoiId) await uploadPoiMedia(currentRouteId, savedPoiId);
|
||||
setStatus('Station gespeichert.');
|
||||
await loadRoute({ id: currentRouteId, _source: 'server' });
|
||||
}
|
||||
|
||||
async function savePoi() {
|
||||
const values = poiFromForm();
|
||||
if (!values.title) throw new Error('Der Titel der Station ist erforderlich.');
|
||||
if (state.currentSource === 'local') await saveLocalPoi(values);
|
||||
else await saveServerPoi(values);
|
||||
}
|
||||
|
||||
function reorderPois(index, offset) {
|
||||
const target = index + offset;
|
||||
if (target < 0 || target >= state.pois.length) return false;
|
||||
const reordered = [...state.pois];
|
||||
[reordered[index], reordered[target]] = [reordered[target], reordered[index]];
|
||||
state.pois = reordered.map((poi, sequence) => ({ ...poi, sequence }));
|
||||
renderPoiList();
|
||||
return true;
|
||||
}
|
||||
|
||||
function swapPois(index, offset) {
|
||||
const target = index + offset;
|
||||
if (target < 0 || target >= state.pois.length) return;
|
||||
const reordered = [...state.pois];
|
||||
[reordered[index], reordered[target]] = [reordered[target], reordered[index]];
|
||||
state.pois = reordered;
|
||||
renderPoiList();
|
||||
if (!reorderPois(index, offset)) return;
|
||||
guarded(savePoiOrder);
|
||||
}
|
||||
|
||||
async function savePoiOrder() {
|
||||
const currentRouteId = routeId(state.currentRoute);
|
||||
if (!currentRouteId) return;
|
||||
if (state.currentSource === 'local') {
|
||||
await persistLocalCurrentRoute();
|
||||
setStatus('Lokale POI-Reihenfolge gespeichert.');
|
||||
return;
|
||||
}
|
||||
const jobs = state.pois.map((poi, index) => {
|
||||
const currentPoiId = poiId(poi);
|
||||
if (!currentPoiId) return null;
|
||||
const path = `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`;
|
||||
return sendJson(path, 'PUT', { ...poi, position: index, sortOrder: index });
|
||||
return sendJson(path, 'PUT', { ...poi, sequence: index, sortOrder: index });
|
||||
}).filter(Boolean);
|
||||
await Promise.all(jobs);
|
||||
setStatus('Reihenfolge gespeichert.');
|
||||
@@ -527,7 +680,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
|
||||
function routeForItem(target) {
|
||||
const item = target.closest('[data-route-id]');
|
||||
return state.routes.find(route => routeId(route) === item?.dataset.routeId);
|
||||
const id = item?.dataset.routeId;
|
||||
const source = item?.dataset.routeSource;
|
||||
return state.routes.find(route => routeId(route) === id && routeSource(route) === source);
|
||||
}
|
||||
|
||||
function poiForItem(target) {
|
||||
@@ -580,58 +735,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
submit(byId(ids.poiForm), () => guarded(savePoi));
|
||||
}
|
||||
|
||||
|
||||
function classNames() {
|
||||
return Object.values(classes);
|
||||
}
|
||||
|
||||
function testHooks() {
|
||||
return Object.freeze({
|
||||
apiUrl,
|
||||
classNames,
|
||||
closeEditor,
|
||||
fillPoiForm,
|
||||
fillRouteForm,
|
||||
handleResponse,
|
||||
isActivation,
|
||||
loadRoute,
|
||||
loadRoutes,
|
||||
newPoi,
|
||||
newRoute,
|
||||
poiArray,
|
||||
poiFromForm,
|
||||
poiItem,
|
||||
parseJson,
|
||||
readJson,
|
||||
renderPoiList,
|
||||
renderRouteList,
|
||||
routeArray,
|
||||
routeFromForm,
|
||||
routeItem,
|
||||
savePoi,
|
||||
sendForm,
|
||||
sendJson,
|
||||
setDialogExpanded,
|
||||
setDisabled,
|
||||
setStatus,
|
||||
savePoiOrder,
|
||||
saveRoute,
|
||||
state,
|
||||
swapPois,
|
||||
uploadPoiMedia
|
||||
});
|
||||
}
|
||||
|
||||
function editorApi() {
|
||||
const api = {
|
||||
return Object.freeze({
|
||||
close: closeEditor,
|
||||
classNames,
|
||||
open: openEditor,
|
||||
openNewLocalRoute,
|
||||
refresh: loadRoutes
|
||||
};
|
||||
|
||||
if (globalThis.WegwichtelEditorTestHooks === true) api.testHooks = testHooks();
|
||||
return Object.freeze(api);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
Reference in New Issue
Block a user