(() => { const PAGE_SIZE = 10; const ICONS = { path: ``, view: ``, delete: ``, }; 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, """); } 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 = ` ${ICONS.path} ${escapeHtml(positionLabel(path.map_id, path.from_position_id))} ${escapeHtml(positionLabel(path.map_id, path.to_position_id))} ${escapeHtml(mapName(path.map_id))} ${canWrite() ? `` : ""} `; 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 }); })();