694 lines
24 KiB
JavaScript
694 lines
24 KiB
JavaScript
(() => {
|
|
const PAGE_SIZE = 10;
|
|
|
|
const ICONS = {
|
|
guide: `<svg class="pathGuideMirIcon" width="22" height="22" viewBox="0 0 22 22" aria-hidden="true"><circle cx="4" cy="11" r="2.5" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="11" cy="11" r="2.5" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="18" cy="11" r="2.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M6.5 11h2M13.5 11h2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
|
|
edit: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M9.5 2.5l2 2L5 11H3v-2L9.5 2.5z" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></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>`,
|
|
drag: `<svg class="pathGuideDragIcon" width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M5 3h1v1H5V3zm3 0h1v1H8V3zM5 6h1v1H5V6zm3 0h1v1H8V6zm-3 3h1v1H5V9zm3 0h1v1H8V9z" fill="currentColor"/></svg>`,
|
|
remove: `<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`,
|
|
};
|
|
|
|
const el = (id) => document.getElementById(id);
|
|
const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
|
|
|
|
const listViewEl = el("pathGuidesListView");
|
|
const createViewEl = el("pathGuideCreateView");
|
|
const editViewEl = el("pathGuideEditView");
|
|
const listEl = el("pathGuideList");
|
|
const emptyEl = el("pathGuideListEmpty");
|
|
const tableEl = el("pathGuidesTable");
|
|
const createBtnEl = el("pathGuideCreateBtn");
|
|
const filterInputEl = el("pathGuidesFilterInput");
|
|
const filterCountEl = el("pathGuidesFilterCount");
|
|
const pageLabelEl = el("pathGuidesPageLabel");
|
|
const deleteConfirmDialogEl = el("pathGuideDeleteConfirmDialog");
|
|
const deleteConfirmTextEl = el("pathGuideDeleteConfirmText");
|
|
const editMetaEl = el("pathGuideEditMeta");
|
|
const deleteBtnEl = el("pathGuideEditDeleteBtn");
|
|
const saveBtnEl = el("pathGuideEditSaveBtn");
|
|
|
|
const createFields = {
|
|
name: el("pathGuideCreateName"),
|
|
site: el("pathGuideCreateSite"),
|
|
map: el("pathGuideCreateMap"),
|
|
};
|
|
|
|
const pickers = {
|
|
start: el("pathGuideAddStartSelect"),
|
|
via: el("pathGuideAddViaSelect"),
|
|
goal: el("pathGuideAddGoalSelect"),
|
|
};
|
|
|
|
const lists = {
|
|
starts: el("pathGuideStartsList"),
|
|
vias: el("pathGuideViasList"),
|
|
goals: el("pathGuideGoalsList"),
|
|
};
|
|
|
|
const store = {
|
|
guides: [],
|
|
sites: [],
|
|
maps: [],
|
|
editingId: null,
|
|
pendingDeleteId: null,
|
|
filter: "",
|
|
page: 1,
|
|
draft: { starts: [], vias: [], goals: [] },
|
|
meta: { name: "", site_id: "", map_id: "" },
|
|
dragViaIndex: null,
|
|
};
|
|
|
|
function canWrite() {
|
|
if (!window.AuthApp?.canWrite) return true;
|
|
return window.AuthApp.canWrite("maps");
|
|
}
|
|
|
|
function currentUser() {
|
|
return window.AuthApp?.getUser?.() || null;
|
|
}
|
|
|
|
function canDeleteGuide(guide) {
|
|
if (!canWrite() || !guide) return false;
|
|
const user = currentUser();
|
|
if (!user) return true;
|
|
const group = guide.created_by_group;
|
|
if (group) return group === user.group_id;
|
|
return true;
|
|
}
|
|
|
|
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 [sites, maps, guides] = await Promise.all([
|
|
apiJson("/api/sites"),
|
|
apiJson("/api/maps"),
|
|
apiJson("/api/path_guides"),
|
|
]);
|
|
store.sites = Array.isArray(sites.sites) ? sites.sites : [];
|
|
store.maps = Array.isArray(maps.maps) ? maps.maps : [];
|
|
store.guides = Array.isArray(guides.path_guides) ? guides.path_guides : [];
|
|
}
|
|
|
|
function siteName(id) {
|
|
return store.sites.find((x) => x.id === id)?.name || id || "—";
|
|
}
|
|
|
|
function mapName(id) {
|
|
return store.maps.find((x) => x.id === id)?.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, excludeIds = new Set()) {
|
|
const m = store.maps.find((x) => x.id === mapId);
|
|
const zones = Array.isArray(m?.zones) ? m.zones : [];
|
|
return zones
|
|
.filter((z) => z && z.type === "position" && typeof z.id === "string" && !excludeIds.has(z.id))
|
|
.map((p) => ({ value: p.id, label: p.name || p.id }));
|
|
}
|
|
|
|
function fillSelect(selectEl, options, value, { allowBlank = false, blankLabel = "" } = {}) {
|
|
if (!selectEl) return;
|
|
selectEl.innerHTML = "";
|
|
if (allowBlank) {
|
|
const blank = document.createElement("option");
|
|
blank.value = "";
|
|
blank.textContent = blankLabel || t("pathGuides.selectPosition");
|
|
selectEl.appendChild(blank);
|
|
}
|
|
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 parseGuidePositions(guide) {
|
|
const positions = Array.isArray(guide?.positions) ? guide.positions : [];
|
|
const starts = [];
|
|
const vias = [];
|
|
const goals = [];
|
|
positions.forEach((p) => {
|
|
if (!p || typeof p.position_id !== "string") return;
|
|
const role = String(p.role || "").toLowerCase();
|
|
if (role === "start") starts.push(p.position_id);
|
|
else if (role === "via") vias.push({ position_id: p.position_id, priority: Number(p.priority) || vias.length + 1 });
|
|
else if (role === "goal") goals.push(p.position_id);
|
|
});
|
|
vias.sort((a, b) => a.priority - b.priority);
|
|
return { starts, vias, goals };
|
|
}
|
|
|
|
function draftToPositions() {
|
|
const out = [];
|
|
store.draft.starts.forEach((id) => out.push({ position_id: id, role: "start" }));
|
|
store.draft.vias.forEach((v, idx) => out.push({ position_id: v.position_id, role: "via", priority: idx + 1 }));
|
|
store.draft.goals.forEach((id) => out.push({ position_id: id, role: "goal" }));
|
|
return out;
|
|
}
|
|
|
|
function usedPositionIds() {
|
|
const ids = new Set();
|
|
store.draft.starts.forEach((id) => ids.add(id));
|
|
store.draft.vias.forEach((v) => ids.add(v.position_id));
|
|
store.draft.goals.forEach((id) => ids.add(id));
|
|
return ids;
|
|
}
|
|
|
|
function currentMapId() {
|
|
return store.meta.map_id || "";
|
|
}
|
|
|
|
function refreshPickers() {
|
|
const mapId = currentMapId();
|
|
const used = usedPositionIds();
|
|
const opts = positionOptionsForMap(mapId, used);
|
|
fillSelect(pickers.start, opts, "", { allowBlank: true });
|
|
fillSelect(pickers.via, opts, "", { allowBlank: true });
|
|
fillSelect(pickers.goal, opts, "", { allowBlank: true });
|
|
}
|
|
|
|
function renderPosCard(mapId, label, { draggable = false, viaIndex = -1, removeKind, removeKey }) {
|
|
const li = document.createElement("li");
|
|
li.className = "pathGuidePosCard";
|
|
if (draggable) {
|
|
li.draggable = true;
|
|
li.dataset.viaIndex = String(viaIndex);
|
|
li.addEventListener("dragstart", (evt) => {
|
|
store.dragViaIndex = viaIndex;
|
|
evt.dataTransfer?.setData("text/plain", String(viaIndex));
|
|
li.classList.add("pathGuidePosCard--dragging");
|
|
});
|
|
li.addEventListener("dragend", () => {
|
|
store.dragViaIndex = null;
|
|
li.classList.remove("pathGuidePosCard--dragging");
|
|
});
|
|
li.addEventListener("dragover", (evt) => {
|
|
evt.preventDefault();
|
|
li.classList.add("pathGuidePosCard--over");
|
|
});
|
|
li.addEventListener("dragleave", () => li.classList.remove("pathGuidePosCard--over"));
|
|
li.addEventListener("drop", (evt) => {
|
|
evt.preventDefault();
|
|
li.classList.remove("pathGuidePosCard--over");
|
|
const from = store.dragViaIndex;
|
|
const to = viaIndex;
|
|
if (from == null || from === to) return;
|
|
const copy = [...store.draft.vias];
|
|
const [item] = copy.splice(from, 1);
|
|
copy.splice(to, 0, item);
|
|
store.draft.vias = copy;
|
|
renderDraftLists();
|
|
});
|
|
}
|
|
|
|
const dragHtml = draggable
|
|
? `<span class="pathGuidePosDrag" title="${escapeHtml(t("pathGuides.dragHandle"))}">${ICONS.drag}</span>`
|
|
: "";
|
|
li.innerHTML = `${dragHtml}<span class="pathGuidePosCardLabel">${escapeHtml(label)}</span>
|
|
<button type="button" class="pathGuidePosRemoveBtn" data-remove-kind="${removeKind}" data-remove-key="${escapeHtml(String(removeKey))}" title="${escapeHtml(t("common.delete"))}">${ICONS.remove}</button>`;
|
|
li.querySelector("[data-remove-kind]")?.addEventListener("click", () => {
|
|
if (removeKind === "start") store.draft.starts = store.draft.starts.filter((x) => x !== removeKey);
|
|
else if (removeKind === "via") store.draft.vias = store.draft.vias.filter((_, i) => i !== removeKey);
|
|
else if (removeKind === "goal") store.draft.goals = store.draft.goals.filter((x) => x !== removeKey);
|
|
renderDraftLists();
|
|
});
|
|
return li;
|
|
}
|
|
|
|
function renderDraftLists() {
|
|
const mapId = currentMapId();
|
|
|
|
if (lists.starts) {
|
|
lists.starts.innerHTML = "";
|
|
store.draft.starts.forEach((id) => {
|
|
lists.starts.appendChild(renderPosCard(mapId, positionName(mapId, id), { removeKind: "start", removeKey: id }));
|
|
});
|
|
}
|
|
|
|
if (lists.vias) {
|
|
lists.vias.innerHTML = "";
|
|
store.draft.vias.forEach((v, idx) => {
|
|
lists.vias.appendChild(
|
|
renderPosCard(mapId, positionName(mapId, v.position_id), {
|
|
draggable: canWrite(),
|
|
viaIndex: idx,
|
|
removeKind: "via",
|
|
removeKey: idx,
|
|
}),
|
|
);
|
|
});
|
|
}
|
|
|
|
if (lists.goals) {
|
|
lists.goals.innerHTML = "";
|
|
store.draft.goals.forEach((id) => {
|
|
lists.goals.appendChild(renderPosCard(mapId, positionName(mapId, id), { removeKind: "goal", removeKey: id }));
|
|
});
|
|
}
|
|
|
|
refreshPickers();
|
|
}
|
|
|
|
function hideAllViews() {
|
|
[listViewEl, createViewEl, editViewEl].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");
|
|
}
|
|
store.editingId = null;
|
|
renderList();
|
|
}
|
|
|
|
function refreshCreateMapSelect() {
|
|
const siteId = createFields.site?.value || "";
|
|
const mapsForSite = store.maps.filter((m) => (m.site_id || "") === siteId);
|
|
const mapOpts = mapsForSite.map((m) => ({ value: m.id, label: m.name || m.id }));
|
|
fillSelect(createFields.map, mapOpts, createFields.map?.value || mapOpts[0]?.value);
|
|
}
|
|
|
|
function showCreate() {
|
|
if (!canWrite()) return;
|
|
hideAllViews();
|
|
const siteOpts = store.sites.map((s) => ({ value: s.id, label: s.name || s.id }));
|
|
fillSelect(createFields.site, siteOpts, store.maps[0]?.site_id || siteOpts[0]?.value);
|
|
refreshCreateMapSelect();
|
|
if (createFields.name) createFields.name.value = "";
|
|
if (createViewEl) {
|
|
createViewEl.hidden = false;
|
|
createViewEl.removeAttribute("aria-hidden");
|
|
}
|
|
createFields.name?.focus();
|
|
}
|
|
|
|
function showEditView({ id = null, meta = null, draft = null } = {}) {
|
|
hideAllViews();
|
|
store.editingId = id;
|
|
const existing = id ? store.guides.find((x) => x.id === id) : null;
|
|
if (existing) {
|
|
store.meta = { name: existing.name || "", site_id: existing.site_id || "", map_id: existing.map_id || "" };
|
|
store.draft = parseGuidePositions(existing);
|
|
} else if (meta) {
|
|
store.meta = { ...meta };
|
|
store.draft = draft || { starts: [], vias: [], goals: [] };
|
|
}
|
|
|
|
if (editMetaEl) {
|
|
const label = t("pathGuides.editPage.meta", {
|
|
name: store.meta.name || "—",
|
|
map: mapName(store.meta.map_id),
|
|
});
|
|
editMetaEl.textContent = label;
|
|
editMetaEl.hidden = !store.meta.name;
|
|
}
|
|
|
|
const ro = !canWrite();
|
|
if (deleteBtnEl) {
|
|
deleteBtnEl.hidden = !existing || ro || !canDeleteGuide(existing);
|
|
}
|
|
if (saveBtnEl) saveBtnEl.hidden = ro;
|
|
[el("pathGuideAddStartBtn"), el("pathGuideAddViaBtn"), el("pathGuideAddGoalBtn")].forEach((btn) => {
|
|
btn?.toggleAttribute("disabled", ro);
|
|
});
|
|
|
|
renderDraftLists();
|
|
if (editViewEl) {
|
|
editViewEl.hidden = false;
|
|
editViewEl.removeAttribute("aria-hidden");
|
|
}
|
|
}
|
|
|
|
function filteredGuides() {
|
|
const q = store.filter.trim().toLowerCase();
|
|
let items = [...store.guides].sort((a, b) => {
|
|
const ma = mapName(a.map_id).localeCompare(mapName(b.map_id));
|
|
if (ma !== 0) return ma;
|
|
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
});
|
|
if (q) {
|
|
items = items.filter((g) => {
|
|
const name = String(g.name || "").toLowerCase();
|
|
const map = mapName(g.map_id).toLowerCase();
|
|
const site = siteName(g.site_id).toLowerCase();
|
|
return name.includes(q) || map.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("pathGuides.itemsFound", { n: totalItems });
|
|
if (pageLabelEl) pageLabelEl.textContent = t("pathGuides.pageOf", { page: store.page, total: totalPages });
|
|
const atStart = store.page <= 1;
|
|
const atEnd = store.page >= totalPages;
|
|
el("pathGuidesPageFirst")?.toggleAttribute("disabled", atStart);
|
|
el("pathGuidesPagePrev")?.toggleAttribute("disabled", atStart);
|
|
el("pathGuidesPageNext")?.toggleAttribute("disabled", atEnd);
|
|
el("pathGuidesPageLast")?.toggleAttribute("disabled", atEnd);
|
|
}
|
|
|
|
function renderList() {
|
|
if (!listEl) return;
|
|
const items = filteredGuides();
|
|
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("pathGuides.emptyFilter") : t("pathGuides.empty");
|
|
}
|
|
|
|
pageItems.forEach((guide) => {
|
|
const tr = document.createElement("tr");
|
|
tr.className = "mapsMirRow pathGuideMirRow";
|
|
const actions = canWrite()
|
|
? `<div class="mapsMirRowActions">
|
|
<button type="button" class="mapsMirIconBtn" data-edit="${escapeHtml(guide.id)}" title="${escapeHtml(t("common.edit"))}">${ICONS.edit}</button>
|
|
${canDeleteGuide(guide) ? `<button type="button" class="mapsMirIconBtn mapsMirIconBtn--danger" data-delete="${escapeHtml(guide.id)}" title="${escapeHtml(t("common.delete"))}">${ICONS.delete}</button>` : ""}
|
|
</div>`
|
|
: "";
|
|
|
|
tr.innerHTML = `
|
|
<td class="pathGuideMirCellIcon">${ICONS.guide}</td>
|
|
<td>
|
|
<button type="button" class="mapsMirNameLink" data-edit="${escapeHtml(guide.id)}">${escapeHtml(guide.name || "—")}</button>
|
|
</td>
|
|
<td>${escapeHtml(mapName(guide.map_id))}</td>
|
|
<td class="pathGuideMirCellNum">${guide.starts_count ?? 0}</td>
|
|
<td class="pathGuideMirCellNum">${guide.vias_count ?? 0}</td>
|
|
<td class="pathGuideMirCellNum">${guide.goals_count ?? 0}</td>
|
|
<td class="mapsMirCellActions">${actions}</td>`;
|
|
|
|
tr.querySelectorAll("[data-edit]").forEach((btn) => {
|
|
btn.addEventListener("click", () => showEditView({ id: btn.dataset.edit }));
|
|
});
|
|
tr.querySelector("[data-delete]")?.addEventListener("click", () => openDeleteConfirm(guide.id));
|
|
listEl.appendChild(tr);
|
|
});
|
|
|
|
if (createBtnEl) createBtnEl.hidden = !canWrite();
|
|
document.body.classList.toggle("auth-readonly-path-guides", !canWrite());
|
|
}
|
|
|
|
function readPayload() {
|
|
return {
|
|
name: store.meta.name?.trim() || "",
|
|
site_id: store.meta.site_id || "",
|
|
map_id: store.meta.map_id || "",
|
|
positions: draftToPositions(),
|
|
};
|
|
}
|
|
|
|
async function saveEdit() {
|
|
if (!canWrite()) return;
|
|
const payload = readPayload();
|
|
if (!payload.name || !payload.site_id || !payload.map_id) {
|
|
alert(t("pathGuides.error.missing"));
|
|
return;
|
|
}
|
|
if (payload.positions.filter((p) => p.role === "start").length < 1 || payload.positions.filter((p) => p.role === "goal").length < 1) {
|
|
alert(t("pathGuides.error.positions"));
|
|
return;
|
|
}
|
|
try {
|
|
if (store.editingId) {
|
|
await apiJson(`/api/path_guides/${encodeURIComponent(store.editingId)}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
} else {
|
|
await apiJson("/api/path_guides", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
await refreshAll();
|
|
showList();
|
|
} catch (e) {
|
|
alert(e.message);
|
|
}
|
|
}
|
|
|
|
function openDeleteConfirm(id) {
|
|
const guide = store.guides.find((x) => x.id === id);
|
|
if (!guide || !canDeleteGuide(guide)) return;
|
|
store.pendingDeleteId = id;
|
|
if (deleteConfirmTextEl) {
|
|
deleteConfirmTextEl.textContent = t("pathGuides.deleteConfirmText", {
|
|
name: guide.name || guide.id,
|
|
map: mapName(guide.map_id),
|
|
});
|
|
}
|
|
deleteConfirmDialogEl?.showModal();
|
|
}
|
|
|
|
async function confirmDelete() {
|
|
const id = store.pendingDeleteId || store.editingId;
|
|
if (!id || !canWrite()) return;
|
|
try {
|
|
await apiJson(`/api/path_guides/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
deleteConfirmDialogEl?.close();
|
|
store.pendingDeleteId = null;
|
|
store.editingId = null;
|
|
await refreshAll();
|
|
showList();
|
|
} catch (e) {
|
|
alert(e.message);
|
|
}
|
|
}
|
|
|
|
async function pickAndAdd(selectEl, onAdd) {
|
|
if (!canWrite() || !selectEl) return;
|
|
refreshPickers();
|
|
const options = Array.from(selectEl.options).filter((o) => o.value);
|
|
if (options.length === 0) {
|
|
alert(t("pathGuides.error.noPositions"));
|
|
return;
|
|
}
|
|
if (typeof selectEl.showPicker === "function") {
|
|
selectEl.showPicker();
|
|
await new Promise((resolve) => {
|
|
const done = () => {
|
|
selectEl.removeEventListener("change", done);
|
|
selectEl.removeEventListener("cancel", done);
|
|
resolve();
|
|
};
|
|
selectEl.addEventListener("change", done, { once: true });
|
|
selectEl.addEventListener("cancel", done, { once: true });
|
|
});
|
|
} else {
|
|
selectEl.hidden = false;
|
|
selectEl.removeAttribute("aria-hidden");
|
|
selectEl.tabIndex = 0;
|
|
selectEl.focus();
|
|
return;
|
|
}
|
|
const id = selectEl.value;
|
|
if (!id) return;
|
|
onAdd(id);
|
|
selectEl.value = "";
|
|
}
|
|
|
|
function addStart(id) {
|
|
if (!id || store.draft.starts.includes(id)) return;
|
|
store.draft.starts.push(id);
|
|
renderDraftLists();
|
|
}
|
|
|
|
function addVia(id) {
|
|
if (!id || store.draft.vias.some((v) => v.position_id === id)) return;
|
|
store.draft.vias.push({ position_id: id, priority: store.draft.vias.length + 1 });
|
|
renderDraftLists();
|
|
}
|
|
|
|
function addGoal(id) {
|
|
if (!id || store.draft.goals.includes(id)) return;
|
|
store.draft.goals.push(id);
|
|
renderDraftLists();
|
|
}
|
|
|
|
function submitCreateForm(evt) {
|
|
evt.preventDefault();
|
|
const name = createFields.name?.value?.trim() || "";
|
|
const site_id = createFields.site?.value || "";
|
|
const map_id = createFields.map?.value || "";
|
|
if (!name || !site_id || !map_id) {
|
|
alert(t("pathGuides.error.missing"));
|
|
return;
|
|
}
|
|
showEditView({
|
|
meta: { name, site_id, map_id },
|
|
draft: { starts: [], vias: [], goals: [] },
|
|
});
|
|
}
|
|
|
|
function clearFilters() {
|
|
store.filter = "";
|
|
store.page = 1;
|
|
if (filterInputEl) filterInputEl.value = "";
|
|
renderList();
|
|
}
|
|
|
|
function bindEvents() {
|
|
createBtnEl?.addEventListener("click", showCreate);
|
|
el("pathGuideCreateForm")?.addEventListener("submit", submitCreateForm);
|
|
el("pathGuideCreateGoBackBtn")?.addEventListener("click", showList);
|
|
el("pathGuideCreateCancelBtn")?.addEventListener("click", showList);
|
|
el("pathGuideCreateHelpBtn")?.addEventListener("click", () => alert(t("pathGuides.helpBody")));
|
|
|
|
el("pathGuideEditGoBackBtn")?.addEventListener("click", showList);
|
|
el("pathGuideEditHelpBtn")?.addEventListener("click", () => alert(t("pathGuides.editPage.helpBody")));
|
|
saveBtnEl?.addEventListener("click", saveEdit);
|
|
deleteBtnEl?.addEventListener("click", () => {
|
|
if (store.editingId) openDeleteConfirm(store.editingId);
|
|
});
|
|
|
|
el("pathGuideAddStartBtn")?.addEventListener("click", () => {
|
|
void pickAndAdd(pickers.start, addStart);
|
|
});
|
|
el("pathGuideAddViaBtn")?.addEventListener("click", () => {
|
|
void pickAndAdd(pickers.via, addVia);
|
|
});
|
|
el("pathGuideAddGoalBtn")?.addEventListener("click", () => {
|
|
void pickAndAdd(pickers.goal, addGoal);
|
|
});
|
|
pickers.start?.addEventListener("change", () => {
|
|
if (pickers.start.value) addStart(pickers.start.value);
|
|
pickers.start.value = "";
|
|
pickers.start.hidden = true;
|
|
pickers.start.setAttribute("aria-hidden", "true");
|
|
pickers.start.tabIndex = -1;
|
|
});
|
|
pickers.via?.addEventListener("change", () => {
|
|
if (pickers.via.value) addVia(pickers.via.value);
|
|
pickers.via.value = "";
|
|
pickers.via.hidden = true;
|
|
pickers.via.setAttribute("aria-hidden", "true");
|
|
pickers.via.tabIndex = -1;
|
|
});
|
|
pickers.goal?.addEventListener("change", () => {
|
|
if (pickers.goal.value) addGoal(pickers.goal.value);
|
|
pickers.goal.value = "";
|
|
pickers.goal.hidden = true;
|
|
pickers.goal.setAttribute("aria-hidden", "true");
|
|
pickers.goal.tabIndex = -1;
|
|
});
|
|
|
|
el("pathGuideDeleteCancelBtn")?.addEventListener("click", () => {
|
|
store.pendingDeleteId = null;
|
|
deleteConfirmDialogEl?.close();
|
|
});
|
|
el("pathGuideDeleteYesBtn")?.addEventListener("click", confirmDelete);
|
|
deleteConfirmDialogEl?.addEventListener("cancel", (evt) => {
|
|
evt.preventDefault();
|
|
store.pendingDeleteId = null;
|
|
deleteConfirmDialogEl?.close();
|
|
});
|
|
|
|
createFields.site?.addEventListener("change", refreshCreateMapSelect);
|
|
|
|
filterInputEl?.addEventListener("input", () => {
|
|
store.filter = filterInputEl.value;
|
|
store.page = 1;
|
|
renderList();
|
|
});
|
|
el("pathGuidesClearFiltersBtn")?.addEventListener("click", clearFilters);
|
|
el("pathGuidesPageFirst")?.addEventListener("click", () => {
|
|
store.page = 1;
|
|
renderList();
|
|
});
|
|
el("pathGuidesPagePrev")?.addEventListener("click", () => {
|
|
store.page = Math.max(1, store.page - 1);
|
|
renderList();
|
|
});
|
|
el("pathGuidesPageNext")?.addEventListener("click", () => {
|
|
store.page += 1;
|
|
renderList();
|
|
});
|
|
el("pathGuidesPageLast")?.addEventListener("click", () => {
|
|
store.page = pageCount(filteredGuides().length);
|
|
renderList();
|
|
});
|
|
el("pathGuidesHelpBtn")?.addEventListener("click", () => alert(t("pathGuides.helpBody")));
|
|
|
|
window.addEventListener("lm:locale-change", () => {
|
|
if (!editViewEl?.hidden) renderDraftLists();
|
|
if (!listViewEl?.hidden) renderList();
|
|
});
|
|
}
|
|
|
|
async function onPageShow() {
|
|
try {
|
|
await refreshAll();
|
|
showList();
|
|
} catch (e) {
|
|
if (emptyEl) {
|
|
emptyEl.hidden = false;
|
|
emptyEl.textContent = e.message;
|
|
}
|
|
if (tableEl) tableEl.hidden = true;
|
|
}
|
|
}
|
|
|
|
function onPageHide() {
|
|
deleteConfirmDialogEl?.close();
|
|
showList();
|
|
}
|
|
|
|
bindEvents();
|
|
window.PathGuidesApp = { onPageShow, onPageHide };
|
|
})();
|