258 lines
8.8 KiB
JavaScript
258 lines
8.8 KiB
JavaScript
(() => {
|
|
const PAGE_SIZE = 10;
|
|
|
|
const ICONS = {
|
|
path: `<svg class="pathsMirIcon" width="22" height="22" viewBox="0 0 22 22" aria-hidden="true"><circle cx="5" cy="11" r="2.5" fill="currentColor"/><circle cx="17" cy="11" r="2.5" fill="currentColor"/><path d="M7.5 11h7" stroke="currentColor" stroke-width="1.5" stroke-dasharray="2 2" stroke-linecap="round"/></svg>`,
|
|
view: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M1 7s2.5-4 6-4 6 4 6 4-2.5 4-6 4-6-4-6-4z" fill="none" stroke="currentColor" stroke-width="1.2"/><circle cx="7" cy="7" r="1.8" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>`,
|
|
delete: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M4 4l6 6M10 4l-6 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`,
|
|
};
|
|
|
|
const el = (id) => document.getElementById(id);
|
|
const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
|
|
|
|
const listEl = el("pathList");
|
|
const emptyEl = el("pathListEmpty");
|
|
const tableEl = el("pathsTable");
|
|
const filterInputEl = el("pathsFilterInput");
|
|
const filterCountEl = el("pathsFilterCount");
|
|
const pageLabelEl = el("pathsPageLabel");
|
|
const deleteConfirmDialogEl = el("pathDeleteConfirmDialog");
|
|
const deleteConfirmTextEl = el("pathDeleteConfirmText");
|
|
|
|
const store = {
|
|
paths: [],
|
|
maps: [],
|
|
sites: [],
|
|
pendingDeleteId: null,
|
|
filter: "",
|
|
page: 1,
|
|
};
|
|
|
|
function canWrite() {
|
|
if (!window.AuthApp?.canWrite) return true;
|
|
return window.AuthApp.canWrite("maps");
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
async function apiJson(url, opts = {}) {
|
|
const res = await fetch(url, { credentials: "include", ...opts });
|
|
const text = await res.text();
|
|
let data = null;
|
|
try {
|
|
data = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
data = null;
|
|
}
|
|
if (!res.ok) throw new Error((data && data.error) || text || res.statusText);
|
|
return data;
|
|
}
|
|
|
|
async function refreshAll() {
|
|
const [sitesData, mapsData, pathsData] = await Promise.all([
|
|
apiJson("/api/sites"),
|
|
apiJson("/api/maps"),
|
|
apiJson("/api/paths"),
|
|
]);
|
|
store.sites = Array.isArray(sitesData.sites) ? sitesData.sites : [];
|
|
store.maps = Array.isArray(mapsData.maps) ? mapsData.maps : [];
|
|
store.paths = Array.isArray(pathsData.paths) ? pathsData.paths : [];
|
|
return store.paths;
|
|
}
|
|
|
|
function mapName(id) {
|
|
return store.maps.find((m) => m.id === id)?.name || id || "—";
|
|
}
|
|
|
|
function positionLabel(mapId, positionId) {
|
|
const map = store.maps.find((m) => m.id === mapId);
|
|
const zones = Array.isArray(map?.zones) ? map.zones : [];
|
|
const hit = zones.find((z) => z && z.type === "position" && z.id === positionId);
|
|
const pname = hit?.name || positionId || "—";
|
|
return `${mapName(mapId)} / ${pname}`;
|
|
}
|
|
|
|
function filteredPaths() {
|
|
const q = store.filter.trim().toLowerCase();
|
|
let items = [...store.paths];
|
|
if (q) {
|
|
items = items.filter((p) => {
|
|
const from = positionLabel(p.map_id, p.from_position_id).toLowerCase();
|
|
const to = positionLabel(p.map_id, p.to_position_id).toLowerCase();
|
|
const map = mapName(p.map_id).toLowerCase();
|
|
return from.includes(q) || to.includes(q) || map.includes(q);
|
|
});
|
|
}
|
|
return items.sort((a, b) => {
|
|
const ma = mapName(a.map_id).localeCompare(mapName(b.map_id));
|
|
if (ma !== 0) return ma;
|
|
return positionLabel(a.map_id, a.from_position_id).localeCompare(
|
|
positionLabel(b.map_id, b.from_position_id),
|
|
);
|
|
});
|
|
}
|
|
|
|
function pageCount(total) {
|
|
return Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
}
|
|
|
|
function renderList() {
|
|
if (!listEl) return;
|
|
const items = filteredPaths();
|
|
const total = items.length;
|
|
const pages = pageCount(total);
|
|
if (store.page > pages) store.page = pages;
|
|
const start = (store.page - 1) * PAGE_SIZE;
|
|
const pageItems = items.slice(start, start + PAGE_SIZE);
|
|
|
|
if (filterCountEl) filterCountEl.textContent = t("paths.itemsFound", { n: total });
|
|
if (pageLabelEl) pageLabelEl.textContent = t("paths.pageOf", { page: store.page, total: pages });
|
|
|
|
listEl.innerHTML = "";
|
|
if (tableEl) tableEl.hidden = total === 0;
|
|
if (emptyEl) {
|
|
emptyEl.hidden = total > 0;
|
|
emptyEl.textContent = store.filter ? t("paths.emptyFilter") : t("paths.empty");
|
|
}
|
|
|
|
pageItems.forEach((path) => {
|
|
const tr = document.createElement("tr");
|
|
tr.className = "pathsMirRow";
|
|
tr.innerHTML = `
|
|
<td class="pathsMirTdIcon">${ICONS.path}</td>
|
|
<td>${escapeHtml(positionLabel(path.map_id, path.from_position_id))}</td>
|
|
<td>${escapeHtml(positionLabel(path.map_id, path.to_position_id))}</td>
|
|
<td>${escapeHtml(mapName(path.map_id))}</td>
|
|
<td class="mapsMirTdFunctions">
|
|
<button type="button" class="mapsMirIconBtn" data-view="${escapeHtml(path.id)}" title="${escapeHtml(t("paths.view"))}">${ICONS.view}</button>
|
|
${canWrite() ? `<button type="button" class="mapsMirIconBtn mapsMirIconBtn--danger" data-delete="${escapeHtml(path.id)}" title="${escapeHtml(t("common.delete"))}">${ICONS.delete}</button>` : ""}
|
|
</td>`;
|
|
listEl.appendChild(tr);
|
|
});
|
|
|
|
document.body.classList.toggle("auth-readonly-paths", !canWrite());
|
|
}
|
|
|
|
async function viewPath(id) {
|
|
const path = store.paths.find((p) => p.id === id);
|
|
if (!path) return;
|
|
let full = path;
|
|
try {
|
|
full = await apiJson(`/api/paths/${encodeURIComponent(id)}`);
|
|
} catch {
|
|
/* use list row */
|
|
}
|
|
if (window.MapsApp?.openEditorWithPath) {
|
|
window.NavApp?.selectSection?.("maps", "maps");
|
|
window.MapsApp.openEditorWithPath(full);
|
|
} else {
|
|
alert(t("paths.viewUnavailable"));
|
|
}
|
|
}
|
|
|
|
function openDeleteConfirm(id) {
|
|
const path = store.paths.find((p) => p.id === id);
|
|
if (!path) return;
|
|
store.pendingDeleteId = id;
|
|
if (deleteConfirmTextEl) {
|
|
deleteConfirmTextEl.textContent = t("paths.deleteConfirmText", {
|
|
from: positionLabel(path.map_id, path.from_position_id),
|
|
to: positionLabel(path.map_id, path.to_position_id),
|
|
});
|
|
}
|
|
deleteConfirmDialogEl?.showModal();
|
|
}
|
|
|
|
async function confirmDelete() {
|
|
const id = store.pendingDeleteId;
|
|
if (!id) return;
|
|
try {
|
|
await apiJson(`/api/paths/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
store.pendingDeleteId = null;
|
|
deleteConfirmDialogEl?.close();
|
|
await refreshAll();
|
|
renderList();
|
|
} catch (e) {
|
|
alert(e.message);
|
|
}
|
|
}
|
|
|
|
function bindEvents() {
|
|
listEl?.addEventListener("click", (evt) => {
|
|
const viewBtn = evt.target.closest("[data-view]");
|
|
const deleteBtn = evt.target.closest("[data-delete]");
|
|
if (viewBtn?.dataset.view) void viewPath(viewBtn.dataset.view);
|
|
else if (deleteBtn?.dataset.delete && canWrite()) openDeleteConfirm(deleteBtn.dataset.delete);
|
|
});
|
|
|
|
filterInputEl?.addEventListener("input", () => {
|
|
store.filter = filterInputEl.value;
|
|
store.page = 1;
|
|
renderList();
|
|
});
|
|
el("pathsClearFiltersBtn")?.addEventListener("click", () => {
|
|
store.filter = "";
|
|
store.page = 1;
|
|
if (filterInputEl) filterInputEl.value = "";
|
|
renderList();
|
|
});
|
|
el("pathsPageFirst")?.addEventListener("click", () => {
|
|
store.page = 1;
|
|
renderList();
|
|
});
|
|
el("pathsPagePrev")?.addEventListener("click", () => {
|
|
store.page = Math.max(1, store.page - 1);
|
|
renderList();
|
|
});
|
|
el("pathsPageNext")?.addEventListener("click", () => {
|
|
store.page += 1;
|
|
renderList();
|
|
});
|
|
el("pathsPageLast")?.addEventListener("click", () => {
|
|
store.page = pageCount(filteredPaths().length);
|
|
renderList();
|
|
});
|
|
el("pathsHelpBtn")?.addEventListener("click", () => alert(t("paths.helpBody")));
|
|
|
|
deleteConfirmDialogEl?.addEventListener("cancel", (evt) => {
|
|
evt.preventDefault();
|
|
store.pendingDeleteId = null;
|
|
deleteConfirmDialogEl?.close();
|
|
});
|
|
el("pathDeleteCancelBtn")?.addEventListener("click", () => {
|
|
store.pendingDeleteId = null;
|
|
deleteConfirmDialogEl?.close();
|
|
});
|
|
el("pathDeleteYesBtn")?.addEventListener("click", () => confirmDelete().catch((e) => alert(e.message)));
|
|
|
|
window.addEventListener("lm:locale-change", () => renderList());
|
|
}
|
|
|
|
async function onPageShow() {
|
|
if (!window.AuthApp?.canAccessPage?.("paths")) return;
|
|
await refreshAll();
|
|
renderList();
|
|
}
|
|
|
|
function onPageHide() {}
|
|
|
|
function init() {
|
|
bindEvents();
|
|
}
|
|
|
|
window.PathsApp = { init, onPageShow, onPageHide, refresh: refreshAll };
|
|
|
|
function boot() {
|
|
init();
|
|
}
|
|
|
|
if (window.AuthApp?.isReady()) boot();
|
|
else window.addEventListener("lm:auth-ready", boot, { once: true });
|
|
})();
|