(() => { const PAGE_SIZE = 10; const ICONS = { map: ``, edit: ``, view: ``, delete: ``, active: ``, }; const el = (id) => document.getElementById(id); const t = (key, vars) => window.I18n?.t(key, vars) ?? key; const listViewEl = el("mapsListView"); const createViewEl = el("mapsCreateView"); const editorViewEl = el("mapEditorView"); const listEl = el("mapsList"); const listEmptyEl = el("mapsListEmpty"); const tableEl = el("mapsTable"); const activeHintEl = el("mapsActiveHint"); const filterInputEl = el("mapsFilterInput"); const filterCountEl = el("mapsFilterCount"); const pageLabelEl = el("mapsPageLabel"); const sitesDialogEl = el("mapsSitesDialog"); const sitesListEl = el("mapsSitesList"); const siteFormDialogEl = el("mapsSiteFormDialog"); const deleteDialogEl = el("mapsDeleteDialog"); const createSiteSelectEl = el("mapsCreateSite"); let deleteDialogResolve = null; const SITE_ICONS = { chevron: ``, edit: ``, delete: ``, }; const store = { maps: [], sites: [], activeMapId: null, filter: "", page: 1, editingSiteId: null, sitesDialogSelectedId: null, sitesDialogSnapshotId: null, }; function canWrite() { return window.AuthApp?.canWrite?.("maps") ?? true; } function currentUser() { return window.AuthApp?.getUser?.() || null; } function canDeleteMap(map) { if (!canWrite() || !map) return false; const user = currentUser(); if (!user) return true; const mapGroup = map.created_by_group; if (mapGroup) return mapGroup === user.group_id; return true; } function escapeHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } async function api(path, opts = {}) { const res = await fetch(path, { credentials: "include", ...opts }); if (!res.ok) { let msg = res.statusText; try { const err = await res.json(); if (err.error) msg = err.error; } catch { /* ignore */ } throw new Error(msg); } if (res.status === 204) return null; return res.json(); } function findMap(id) { return store.maps.find((m) => m.id === id) || null; } function findSite(id) { return store.sites.find((s) => s.id === id) || null; } function siteName(siteId) { return findSite(siteId)?.name || siteId || "—"; } function mapImageUrl(map) { if (!map?.id || !map.image_file) return null; return `/api/maps/${encodeURIComponent(map.id)}/image?t=${encodeURIComponent(map.updated_at || "")}`; } function filteredMaps() { const q = store.filter.trim().toLowerCase(); let items = [...store.maps].sort((a, b) => { const sa = siteName(a.site_id).localeCompare(siteName(b.site_id)); if (sa !== 0) return sa; return (a.name || "").localeCompare(b.name || ""); }); if (q) { items = items.filter((m) => { const name = (m.name || "").toLowerCase(); const site = siteName(m.site_id).toLowerCase(); return name.includes(q) || site.includes(q); }); } return items; } function pageCount(total) { return Math.max(1, Math.ceil(total / PAGE_SIZE)); } function pagedMaps(items) { const totalPages = pageCount(items.length); if (store.page > totalPages) store.page = totalPages; if (store.page < 1) store.page = 1; const start = (store.page - 1) * PAGE_SIZE; return items.slice(start, start + PAGE_SIZE); } function updatePagerUi(totalItems) { const totalPages = pageCount(totalItems); if (filterCountEl) { filterCountEl.textContent = t("maps.itemsFound", { n: totalItems }); } if (pageLabelEl) { pageLabelEl.textContent = t("maps.pageOf", { page: store.page, total: totalPages }); } const atStart = store.page <= 1; const atEnd = store.page >= totalPages; el("mapsPageFirst")?.toggleAttribute("disabled", atStart); el("mapsPagePrev")?.toggleAttribute("disabled", atStart); el("mapsPageNext")?.toggleAttribute("disabled", atEnd); el("mapsPageLast")?.toggleAttribute("disabled", atEnd); } async function loadSites() { const data = await api("/api/sites"); store.sites = Array.isArray(data.sites) ? data.sites : []; } async function loadMaps() { const data = await api("/api/maps"); store.maps = Array.isArray(data.maps) ? data.maps : []; } async function loadActiveMap() { try { const status = await api("/api/robot/status"); store.activeMapId = status.active_map_id || null; } catch { store.activeMapId = null; } } function renderActiveHint() { if (!activeHintEl) return; const active = findMap(store.activeMapId); if (active) { activeHintEl.hidden = false; activeHintEl.textContent = t("maps.activeHint", { name: active.name }); } else { activeHintEl.hidden = true; activeHintEl.textContent = ""; } } function renderSiteSelect(selectedId) { if (!createSiteSelectEl) return; createSiteSelectEl.innerHTML = ""; store.sites.forEach((site) => { const opt = document.createElement("option"); opt.value = site.id; opt.textContent = site.name || site.id; createSiteSelectEl.appendChild(opt); }); if (selectedId) createSiteSelectEl.value = selectedId; else if (store.sites[0]) createSiteSelectEl.value = store.sites[0].id; } function renderList() { if (!listEl) return; const items = filteredMaps(); const pageItems = pagedMaps(items); updatePagerUi(items.length); listEl.innerHTML = ""; const showEmpty = items.length === 0; if (tableEl) tableEl.hidden = showEmpty; if (listEmptyEl) { listEmptyEl.hidden = !showEmpty; listEmptyEl.textContent = store.filter.trim() ? t("maps.emptyFilter") : t("maps.empty"); } let lastSiteId = null; pageItems.forEach((map) => { const siteId = map.site_id || ""; if (siteId !== lastSiteId) { lastSiteId = siteId; const siteTr = document.createElement("tr"); siteTr.className = "mapsMirSiteRow"; siteTr.innerHTML = `${escapeHtml(siteName(siteId))}`; listEl.appendChild(siteTr); } const isActive = map.id === store.activeMapId; const tr = document.createElement("tr"); tr.className = "mapsMirRow"; tr.dataset.mapId = map.id; const activeBadge = isActive ? `${ICONS.active}${escapeHtml(t("maps.activeBadge"))}` : ""; const actions = canWrite() ? `
${canDeleteMap(map) ? `` : ""}
` : `
`; tr.innerHTML = `
${ICONS.map} ${activeBadge}
${escapeHtml(map.created_by || "—")} ${actions}`; tr.querySelector("[data-open]")?.addEventListener("click", () => openEditor(map.id)); tr.querySelector("[data-edit]")?.addEventListener("click", () => openEditor(map.id)); tr.querySelector("[data-view]")?.addEventListener("click", () => openEditor(map.id, { readOnly: !canWrite() })); tr.querySelector("[data-delete]")?.addEventListener("click", () => { void deleteMapFromList(map.id); }); tr.addEventListener("dblclick", () => openEditor(map.id)); listEl.appendChild(tr); }); renderActiveHint(); } function hideAllViews() { [listViewEl, createViewEl, editorViewEl].forEach((view) => { if (!view) return; view.hidden = true; view.setAttribute("aria-hidden", "true"); }); } function showList() { hideAllViews(); if (listViewEl) { listViewEl.hidden = false; listViewEl.removeAttribute("aria-hidden"); } window.MapEditorApp?.close?.(); } function showCreate() { if (!canWrite()) return; hideAllViews(); renderSiteSelect(); const nameEl = el("mapsCreateName"); if (nameEl) nameEl.value = ""; if (createViewEl) { createViewEl.hidden = false; createViewEl.removeAttribute("aria-hidden"); } nameEl?.focus(); } function openEditor(mapId, opts = {}) { const map = findMap(mapId); if (!map) return; hideAllViews(); if (editorViewEl) { editorViewEl.hidden = false; editorViewEl.removeAttribute("aria-hidden"); } window.MapEditorApp?.open?.(mapId, { readOnly: opts.readOnly, onMapUpdated: (updated) => { const idx = store.maps.findIndex((m) => m.id === updated.id); if (idx >= 0) store.maps[idx] = updated; else store.maps.push(updated); }, onMapDeleted: (id) => { store.maps = store.maps.filter((m) => m.id !== id); if (store.activeMapId === id) store.activeMapId = null; showList(); renderList(); }, onActivated: (id) => { store.activeMapId = id; renderList(); }, onClose: () => { showList(); renderList(); }, getSiteName: siteName, getActiveMapId: () => store.activeMapId, canWrite: canWrite(), }); } function confirmDeleteMap(map) { return new Promise((resolve) => { deleteDialogResolve = resolve; const textEl = el("mapsDeleteDialogText"); const activeWarnEl = el("mapsDeleteDialogActiveWarn"); if (textEl) textEl.textContent = t("maps.deleteDialog.text", { name: map.name || map.id }); if (activeWarnEl) { const isActive = map.id === store.activeMapId; activeWarnEl.hidden = !isActive; if (isActive) activeWarnEl.textContent = t("maps.deleteDialog.activeWarning"); } deleteDialogEl?.showModal(); }); } async function deleteMapFromList(mapId) { const map = findMap(mapId); if (!map || !canDeleteMap(map)) return; if (!(await confirmDeleteMap(map))) return; try { await api(`/api/maps/${encodeURIComponent(map.id)}`, { method: "DELETE" }); } catch (e) { alert(e.message || t("maps.deleteForbidden")); return; } store.maps = store.maps.filter((m) => m.id !== map.id); if (store.activeMapId === map.id) store.activeMapId = null; renderList(); } async function activateMap(mapId) { const map = findMap(mapId); if (!map) return; if (!map.image_file) { alert(t("maps.error.noImage")); return; } await api("/api/robot/active_map", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ map_id: mapId }), }); store.activeMapId = mapId; renderList(); } function openCreatePage() { showCreate(); } function renderSitesDialogList() { if (!sitesListEl) return; sitesListEl.innerHTML = ""; if (store.sites.length === 0) { const empty = document.createElement("li"); empty.className = "mapsMirSitesEmpty"; empty.textContent = t("maps.sitesDialog.empty"); sitesListEl.appendChild(empty); return; } store.sites.forEach((site) => { const li = document.createElement("li"); li.className = "mapsMirSitesItem"; if (site.id === store.sitesDialogSelectedId) li.classList.add("is-selected"); li.dataset.siteId = site.id; li.innerHTML = `
`; li.querySelector("[data-select-site]")?.addEventListener("click", () => { store.sitesDialogSelectedId = site.id; renderSitesDialogList(); }); li.querySelector("[data-edit-site]")?.addEventListener("click", (evt) => { evt.stopPropagation(); openSiteFormDialog(site.id); }); li.querySelector("[data-delete-site]")?.addEventListener("click", (evt) => { evt.stopPropagation(); void deleteSite(site.id); }); sitesListEl.appendChild(li); }); } async function openSitesDialog() { await loadSites(); store.sitesDialogSnapshotId = createSiteSelectEl?.value || store.sites[0]?.id || null; store.sitesDialogSelectedId = store.sitesDialogSnapshotId; renderSitesDialogList(); sitesDialogEl?.showModal(); } function closeSitesDialog(apply) { if (apply && store.sitesDialogSelectedId) { renderSiteSelect(store.sitesDialogSelectedId); } sitesDialogEl?.close(); } function openSiteFormDialog(siteId) { store.editingSiteId = siteId || null; const site = siteId ? findSite(siteId) : null; const titleEl = el("mapsSiteFormTitle"); const nameEl = el("mapsSiteName"); if (titleEl) { titleEl.textContent = site ? t("maps.siteForm.edit") : t("maps.siteForm.create"); } if (nameEl) nameEl.value = site?.name || ""; siteFormDialogEl?.showModal(); nameEl?.focus(); } async function saveSite(evt) { evt.preventDefault(); const name = el("mapsSiteName")?.value.trim(); if (!name) return; if (store.editingSiteId) { const updated = await api(`/api/sites/${encodeURIComponent(store.editingSiteId)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); const idx = store.sites.findIndex((s) => s.id === store.editingSiteId); if (idx >= 0) store.sites[idx] = updated; } else { const created = await api("/api/sites", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); store.sites.push(created); store.sitesDialogSelectedId = created.id; } siteFormDialogEl?.close(); renderSitesDialogList(); renderList(); } async function deleteSite(siteId) { const site = findSite(siteId); if (!site) return; if (!confirm(t("maps.sitesDialog.deleteConfirm", { name: site.name }))) return; try { await api(`/api/sites/${encodeURIComponent(siteId)}`, { method: "DELETE" }); } catch (e) { alert(e.message); return; } store.sites = store.sites.filter((s) => s.id !== siteId); if (store.sitesDialogSelectedId === siteId) { store.sitesDialogSelectedId = store.sites[0]?.id || null; } renderSitesDialogList(); renderList(); } async function createMap(evt) { evt.preventDefault(); const name = el("mapsCreateName")?.value.trim(); const site_id = createSiteSelectEl?.value; if (!name || !site_id) return; const user = window.AuthApp?.getUser?.(); const created = await api("/api/maps", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, site_id, created_by: user?.display_name || user?.username || "", created_by_user: user?.id || "", created_by_group: user?.group_id || "", resolution: 0.05, origin_x: 0, origin_y: 0, origin_yaw: 0, }), }); store.maps.push(created); store.filter = ""; if (filterInputEl) filterInputEl.value = ""; store.page = 1; renderList(); openEditor(created.id); } function clearFilters() { store.filter = ""; store.page = 1; if (filterInputEl) filterInputEl.value = ""; renderList(); } function goToPage(page) { const total = pageCount(filteredMaps().length); store.page = Math.min(Math.max(1, page), total); renderList(); } function bindEvents() { el("mapsCreateOpenBtn")?.addEventListener("click", openCreatePage); el("mapsCreateGoBackBtn")?.addEventListener("click", showList); el("mapsCreateCancelBtn")?.addEventListener("click", showList); el("mapsCreateHelpBtn")?.addEventListener("click", () => alert(t("maps.createPage.helpText"))); el("mapsImportSiteBtn")?.addEventListener("click", () => alert(t("maps.importComingSoon"))); el("mapsClearFiltersBtn")?.addEventListener("click", clearFilters); el("mapsHelpBtn")?.addEventListener("click", () => alert(t("maps.helpText"))); filterInputEl?.addEventListener("input", () => { store.filter = filterInputEl.value; store.page = 1; renderList(); }); el("mapsPageFirst")?.addEventListener("click", () => goToPage(1)); el("mapsPagePrev")?.addEventListener("click", () => goToPage(store.page - 1)); el("mapsPageNext")?.addEventListener("click", () => goToPage(store.page + 1)); el("mapsPageLast")?.addEventListener("click", () => goToPage(pageCount(filteredMaps().length))); el("mapsCreateForm")?.addEventListener("submit", (evt) => { createMap(evt).catch((e) => alert(e.message)); }); el("mapsDeleteYesBtn")?.addEventListener("click", () => { deleteDialogEl?.close(); deleteDialogResolve?.(true); deleteDialogResolve = null; }); el("mapsDeleteNoBtn")?.addEventListener("click", () => { deleteDialogEl?.close(); deleteDialogResolve?.(false); deleteDialogResolve = null; }); deleteDialogEl?.addEventListener("cancel", (evt) => { evt.preventDefault(); deleteDialogEl.close(); deleteDialogResolve?.(false); deleteDialogResolve = null; }); el("mapsSiteForm")?.addEventListener("submit", (evt) => { saveSite(evt).catch((e) => alert(e.message)); }); el("mapsCreateSiteBtn")?.addEventListener("click", () => { openSitesDialog().catch((e) => alert(e.message)); }); el("mapsSitesCreateBtn")?.addEventListener("click", () => openSiteFormDialog(null)); el("mapsSitesOkBtn")?.addEventListener("click", () => closeSitesDialog(true)); el("mapsSitesCancelBtn")?.addEventListener("click", () => closeSitesDialog(false)); sitesDialogEl?.addEventListener("cancel", (evt) => { evt.preventDefault(); closeSitesDialog(false); }); document.querySelectorAll("[data-close-dialog]").forEach((btn) => { btn.addEventListener("click", () => { const id = btn.getAttribute("data-close-dialog"); el(id)?.close(); }); }); } function applyReadOnly() { document.body.classList.toggle("auth-readonly-maps-page", !canWrite()); } async function refresh() { await Promise.all([loadSites(), loadMaps(), loadActiveMap()]); renderList(); } async function init() { applyReadOnly(); showList(); bindEvents(); try { await refresh(); } catch (e) { if (listEmptyEl) { listEmptyEl.hidden = false; listEmptyEl.textContent = t("common.error", { msg: e.message }); } if (tableEl) tableEl.hidden = true; } } window.MapsApp = { init, refresh, onPageShow() { applyReadOnly(); showList(); refresh().catch(() => {}); }, getMaps: () => [...store.maps], getMapById: findMap, activateMap, }; function boot() { init(); } if (window.AuthApp?.isReady()) boot(); else window.addEventListener("lm:auth-ready", boot, { once: true }); window.addEventListener("lm:locale-change", () => { renderList(); renderActiveHint(); }); })();