(() => { const PAGE_SIZE = 10; const ICONS = { transition: ``, edit: ``, delete: ``, }; const el = (id) => document.getElementById(id); const t = (key, vars) => window.I18n?.t(key, vars) ?? key; const listEl = el("transitionList"); const emptyEl = el("transitionListEmpty"); const tableEl = el("transitionsTable"); const createBtnEl = el("transitionCreateBtn"); const filterInputEl = el("transitionsFilterInput"); const filterCountEl = el("transitionsFilterCount"); const pageLabelEl = el("transitionsPageLabel"); const dialogEl = el("transitionEditDialog"); const formEl = el("transitionEditForm"); const titleEl = el("transitionEditTitle"); const deleteBtnEl = el("transitionEditDeleteBtn"); const deleteConfirmDialogEl = el("transitionDeleteConfirmDialog"); const deleteConfirmTextEl = el("transitionDeleteConfirmText"); const fields = { site: el("transitionEditSite"), fromMap: el("transitionEditFromMap"), toMap: el("transitionEditToMap"), startPos: el("transitionEditStartPos"), goalPos: el("transitionEditGoalPos"), mission: el("transitionEditMission"), }; const store = { transitions: [], sites: [], maps: [], missions: [], editingId: null, 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 [sites, maps, missions, transitions] = await Promise.all([ apiJson("/api/sites"), apiJson("/api/maps"), apiJson("/api/missions"), apiJson("/api/transitions"), ]); store.sites = Array.isArray(sites.sites) ? sites.sites : []; store.maps = Array.isArray(maps.maps) ? maps.maps : []; store.missions = Array.isArray(missions.missions) ? missions.missions : []; store.transitions = Array.isArray(transitions.transitions) ? transitions.transitions : []; } function siteName(id) { const s = store.sites.find((x) => x.id === id); return s?.name || id || "—"; } function mapName(id) { const m = store.maps.find((x) => x.id === id); return m?.name || id || "—"; } function missionName(id) { const m = store.missions.find((x) => x.id === id); return m?.name || id || "—"; } function positionName(mapId, positionId) { const m = store.maps.find((x) => x.id === mapId); const zones = Array.isArray(m?.zones) ? m.zones : []; const hit = zones.find((z) => z && z.type === "position" && z.id === positionId); return hit?.name || positionId || "—"; } function positionOptionsForMap(mapId) { const m = store.maps.find((x) => x.id === mapId); const zones = Array.isArray(m?.zones) ? m.zones : []; const positions = zones.filter((z) => z && z.type === "position" && typeof z.id === "string"); return positions.map((p) => ({ value: p.id, label: p.name || p.id })); } function fillSelect(selectEl, options, value) { if (!selectEl) return; selectEl.innerHTML = ""; options.forEach((opt) => { const o = document.createElement("option"); o.value = opt.value; o.textContent = opt.label; if (opt.value === value) o.selected = true; selectEl.appendChild(o); }); } function filteredTransitions() { const q = store.filter.trim().toLowerCase(); let items = [...store.transitions].sort((a, b) => { const sa = siteName(a.site_id).localeCompare(siteName(b.site_id)); if (sa !== 0) return sa; const startA = positionName(a.from_map_id, a.start_position_id); const startB = positionName(b.from_map_id, b.start_position_id); return startA.localeCompare(startB); }); if (q) { items = items.filter((tr) => { const start = positionName(tr.from_map_id, tr.start_position_id).toLowerCase(); const goal = positionName(tr.to_map_id, tr.goal_position_id).toLowerCase(); const mission = missionName(tr.mission_id).toLowerCase(); const fromMap = mapName(tr.from_map_id).toLowerCase(); const toMap = mapName(tr.to_map_id).toLowerCase(); const site = siteName(tr.site_id).toLowerCase(); return ( start.includes(q) || goal.includes(q) || mission.includes(q) || fromMap.includes(q) || toMap.includes(q) || site.includes(q) ); }); } return items; } function pageCount(total) { return Math.max(1, Math.ceil(total / PAGE_SIZE)); } function pagedItems(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("transitions.itemsFound", { n: totalItems }); if (pageLabelEl) pageLabelEl.textContent = t("transitions.pageOf", { page: store.page, total: totalPages }); const atStart = store.page <= 1; const atEnd = store.page >= totalPages; el("transitionsPageFirst")?.toggleAttribute("disabled", atStart); el("transitionsPagePrev")?.toggleAttribute("disabled", atStart); el("transitionsPageNext")?.toggleAttribute("disabled", atEnd); el("transitionsPageLast")?.toggleAttribute("disabled", atEnd); } function renderList() { if (!listEl) return; const items = filteredTransitions(); const pageItems = pagedItems(items); updatePagerUi(items.length); listEl.innerHTML = ""; const showEmpty = items.length === 0; if (tableEl) tableEl.hidden = showEmpty; if (emptyEl) { emptyEl.hidden = !showEmpty; emptyEl.textContent = store.filter.trim() ? t("transitions.emptyFilter") : t("transitions.empty"); } let lastSiteId = null; pageItems.forEach((tr) => { const siteId = tr.site_id || ""; if (siteId !== lastSiteId) { lastSiteId = siteId; const siteTr = document.createElement("tr"); siteTr.className = "mapsMirSiteRow"; siteTr.innerHTML = `${escapeHtml(siteName(siteId))}`; listEl.appendChild(siteTr); } const startLabel = positionName(tr.from_map_id, tr.start_position_id); const goalLabel = positionName(tr.to_map_id, tr.goal_position_id); const missionLabel = missionName(tr.mission_id); const createdBy = tr.created_by || "—"; const trEl = document.createElement("tr"); trEl.className = "mapsMirRow transMirRow"; trEl.dataset.id = tr.id; const actions = canWrite() ? `
` : ""; trEl.innerHTML = ` ${ICONS.transition} ${escapeHtml(goalLabel)} ${escapeHtml(missionLabel)} ${escapeHtml(createdBy)} ${actions}`; trEl.querySelectorAll("[data-edit]").forEach((btn) => { btn.addEventListener("click", () => openDialog(btn.dataset.edit)); }); trEl.querySelector("[data-delete]")?.addEventListener("click", () => openDeleteConfirm(tr.id)); trEl.addEventListener("dblclick", () => { if (canWrite()) openDialog(tr.id); }); listEl.appendChild(trEl); }); if (createBtnEl) createBtnEl.hidden = !canWrite(); } function currentEditingTransition() { return store.editingId ? store.transitions.find((x) => x.id === store.editingId) : null; } function readPayload() { return { site_id: fields.site?.value || "", from_map_id: fields.fromMap?.value || "", to_map_id: fields.toMap?.value || "", start_position_id: fields.startPos?.value || "", goal_position_id: fields.goalPos?.value || "", mission_id: fields.mission?.value || "", }; } function updateDependentSelects() { const fromMapId = fields.fromMap?.value || ""; const toMapId = fields.toMap?.value || ""; fillSelect(fields.startPos, positionOptionsForMap(fromMapId), fields.startPos?.value); fillSelect(fields.goalPos, positionOptionsForMap(toMapId), fields.goalPos?.value); } function refreshMapSelectsForSite(existing) { const siteOpts = store.sites.map((s) => ({ value: s.id, label: s.name || s.id })); const defaultSite = existing?.site_id || store.maps[0]?.site_id || siteOpts[0]?.value || ""; fillSelect(fields.site, siteOpts, fields.site?.value || defaultSite); const mapsForSite = store.maps.filter((m) => (m.site_id || "") === (fields.site?.value || "")); const mapOpts = mapsForSite.map((m) => ({ value: m.id, label: m.name || m.id })); fillSelect(fields.fromMap, mapOpts, existing?.from_map_id || mapOpts[0]?.value); fillSelect(fields.toMap, mapOpts, existing?.to_map_id || mapOpts[0]?.value); updateDependentSelects(); } function openDialog(id = null) { store.editingId = id; const existing = id ? store.transitions.find((x) => x.id === id) : null; if (titleEl) titleEl.textContent = existing ? t("transitions.editTitle") : t("transitions.createTitle"); refreshMapSelectsForSite(existing); if (fields.startPos && existing?.start_position_id) fields.startPos.value = existing.start_position_id; if (fields.goalPos && existing?.goal_position_id) fields.goalPos.value = existing.goal_position_id; const missionOpts = store.missions.map((m) => ({ value: m.id, label: m.name || m.id })); fillSelect(fields.mission, missionOpts, existing?.mission_id || missionOpts[0]?.value); const ro = !canWrite(); Object.values(fields).forEach((node) => { if (!node) return; node.disabled = ro; }); if (deleteBtnEl) deleteBtnEl.hidden = !existing || ro; dialogEl?.showModal(); } async function saveDialog() { if (!canWrite()) return; const payload = readPayload(); if ( !payload.site_id || !payload.from_map_id || !payload.to_map_id || !payload.start_position_id || !payload.goal_position_id || !payload.mission_id ) { alert(t("transitions.error.missing")); return; } try { if (store.editingId) { await apiJson(`/api/transitions/${encodeURIComponent(store.editingId)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } else { await apiJson("/api/transitions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } await refreshAll(); renderList(); dialogEl?.close(); } catch (e) { alert(e.message); } } function openDeleteConfirm(id) { const tr = store.transitions.find((x) => x.id === id); if (!tr || !canWrite()) return; store.pendingDeleteId = id; if (deleteConfirmTextEl) { deleteConfirmTextEl.textContent = t("transitions.deleteConfirmText", { from: positionName(tr.from_map_id, tr.start_position_id), to: positionName(tr.to_map_id, tr.goal_position_id), }); } deleteConfirmDialogEl?.showModal(); } function openDeleteConfirmFromDialog() { const tr = currentEditingTransition(); if (!tr) return; openDeleteConfirm(tr.id); } async function confirmDelete() { const id = store.pendingDeleteId || store.editingId; if (!id || !canWrite()) return; try { await apiJson(`/api/transitions/${encodeURIComponent(id)}`, { method: "DELETE" }); deleteConfirmDialogEl?.close(); dialogEl?.close(); store.editingId = null; store.pendingDeleteId = null; await refreshAll(); renderList(); } catch (e) { alert(e.message); } } function clearFilters() { store.filter = ""; store.page = 1; if (filterInputEl) filterInputEl.value = ""; renderList(); } function bindEvents() { createBtnEl?.addEventListener("click", () => openDialog(null)); formEl?.addEventListener("submit", (evt) => { evt.preventDefault(); saveDialog(); }); el("transitionEditCancelBtn")?.addEventListener("click", () => dialogEl?.close()); dialogEl?.addEventListener("cancel", (evt) => { evt.preventDefault(); dialogEl?.close(); }); fields.site?.addEventListener("change", () => { const existing = currentEditingTransition(); refreshMapSelectsForSite(existing); }); fields.fromMap?.addEventListener("change", updateDependentSelects); fields.toMap?.addEventListener("change", updateDependentSelects); deleteBtnEl?.addEventListener("click", openDeleteConfirmFromDialog); el("transitionDeleteCancelBtn")?.addEventListener("click", () => { store.pendingDeleteId = null; deleteConfirmDialogEl?.close(); }); el("transitionDeleteYesBtn")?.addEventListener("click", confirmDelete); deleteConfirmDialogEl?.addEventListener("cancel", (evt) => { evt.preventDefault(); store.pendingDeleteId = null; deleteConfirmDialogEl?.close(); }); filterInputEl?.addEventListener("input", () => { store.filter = filterInputEl.value; store.page = 1; renderList(); }); el("transitionsClearFiltersBtn")?.addEventListener("click", clearFilters); el("transitionsPageFirst")?.addEventListener("click", () => { store.page = 1; renderList(); }); el("transitionsPagePrev")?.addEventListener("click", () => { store.page = Math.max(1, store.page - 1); renderList(); }); el("transitionsPageNext")?.addEventListener("click", () => { store.page += 1; renderList(); }); el("transitionsPageLast")?.addEventListener("click", () => { store.page = pageCount(filteredTransitions().length); renderList(); }); el("transitionsHelpBtn")?.addEventListener("click", () => { alert(t("transitions.helpBody")); }); window.addEventListener("lm:locale-change", () => renderList()); } async function onPageShow() { try { await refreshAll(); renderList(); } catch (e) { if (emptyEl) { emptyEl.hidden = false; emptyEl.textContent = e.message; } if (tableEl) tableEl.hidden = true; } } function onPageHide() { dialogEl?.close(); deleteConfirmDialogEl?.close(); } bindEvents(); window.TransitionsApp = { onPageShow, onPageHide }; })();