(() => { const PAGE_SIZE = 10; const PERM_RESOURCES = [ { key: "dashboard", labelKey: "userGroups.perm.dashboard" }, { key: "config", labelKey: "userGroups.perm.config" }, { key: "maps", labelKey: "userGroups.perm.maps" }, { key: "missions", labelKey: "userGroups.perm.missions" }, { key: "sounds", labelKey: "userGroups.perm.sounds" }, { key: "integrations", labelKey: "userGroups.perm.integrations" }, { key: "users", labelKey: "userGroups.perm.users" }, ]; const ICONS = { group: ``, edit: ``, delete: ``, }; const el = (id) => document.getElementById(id); const t = (key, vars) => window.I18n?.t(key, vars) ?? key; const listEl = el("userGroupList"); const emptyEl = el("userGroupListEmpty"); const tableEl = el("userGroupsTable"); const filterInputEl = el("userGroupsFilterInput"); const filterCountEl = el("userGroupsFilterCount"); const pageLabelEl = el("userGroupsPageLabel"); const dialogEl = el("userGroupEditDialog"); const formEl = el("userGroupEditForm"); const titleEl = el("userGroupEditTitle"); const deleteBtnEl = el("userGroupEditDeleteBtn"); const permsBodyEl = el("userGroupEditPermsBody"); const deleteConfirmDialogEl = el("userGroupDeleteConfirmDialog"); const deleteConfirmTextEl = el("userGroupDeleteConfirmText"); const fields = { name: el("userGroupEditName"), allowPin: el("userGroupEditAllowPin"), }; const store = { groups: [], editingId: null, pendingDeleteId: null, filter: "", page: 1, permSelects: {}, }; function canWrite() { if (!window.AuthApp?.canWrite) return true; return window.AuthApp.canWrite("users"); } function isDistributor() { return window.AuthApp?.getUser?.()?.group_id === "group_distributors"; } function canManageGroup(group) { if (!group) return canWrite(); if (isDistributor()) return canWrite(); return canWrite() && group.id !== "group_distributors" && group.id !== "group_administrators"; } 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 data = await apiJson("/api/user_groups"); store.groups = Array.isArray(data.groups) ? data.groups : []; } function groupById(id) { return store.groups.find((g) => g.id === id) || null; } function filteredGroups() { const q = store.filter.trim().toLowerCase(); let items = [...store.groups].sort((a, b) => (a.name || "").localeCompare(b.name || "")); if (q) { items = items.filter((g) => (g.name || "").toLowerCase().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("userGroups.itemsFound", { n: totalItems }); if (pageLabelEl) pageLabelEl.textContent = t("userGroups.pageOf", { page: store.page, total: totalPages }); const atStart = store.page <= 1; const atEnd = store.page >= totalPages; el("userGroupsPageFirst")?.toggleAttribute("disabled", atStart); el("userGroupsPagePrev")?.toggleAttribute("disabled", atStart); el("userGroupsPageNext")?.toggleAttribute("disabled", atEnd); el("userGroupsPageLast")?.toggleAttribute("disabled", atEnd); } function permSummary(group) { const perms = group.permissions || {}; const writeCount = PERM_RESOURCES.filter((r) => perms[r.key] === "write").length; const readCount = PERM_RESOURCES.filter((r) => perms[r.key] === "read").length; if (writeCount === PERM_RESOURCES.length) return t("userGroups.permSummaryAllWrite"); if (writeCount === 0 && readCount === 0) return t("userGroups.permSummaryNone"); return t("userGroups.permSummaryMixed", { write: writeCount, read: readCount }); } function renderList() { if (!listEl) return; const items = filteredGroups(); 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("userGroups.emptyFilter") : t("userGroups.empty"); } pageItems.forEach((group) => { const tr = document.createElement("tr"); tr.className = "mapsMirRow userGroupsMirRow"; tr.dataset.id = group.id; const manageable = canManageGroup(group); const canDelete = manageable && !group.builtin && (group.user_count || 0) === 0; const actions = canWrite() ? `
` : ""; const pinLabel = group.allow_pin ? t("userGroups.pinYes") : t("userGroups.pinNo"); const usersLabel = t("userGroups.userCount", { n: group.user_count || 0 }); tr.innerHTML = ` ${ICONS.group} ${escapeHtml(usersLabel)} ${escapeHtml(pinLabel)} ${escapeHtml(permSummary(group))} ${actions} `; listEl.appendChild(tr); }); } function buildPermRows(permissions = {}) { if (!permsBodyEl) return; permsBodyEl.innerHTML = ""; store.permSelects = {}; PERM_RESOURCES.forEach((res) => { const tr = document.createElement("tr"); const select = document.createElement("select"); select.className = "userGroupsPermSelect"; select.dataset.resource = res.key; ["none", "read", "write"].forEach((level) => { const opt = document.createElement("option"); opt.value = level; opt.textContent = t(`userGroups.permLevel.${level}`); select.appendChild(opt); }); select.value = permissions[res.key] || "none"; store.permSelects[res.key] = select; tr.innerHTML = `${escapeHtml(t(res.labelKey))}`; const td = document.createElement("td"); td.appendChild(select); tr.appendChild(td); permsBodyEl.appendChild(tr); }); } function readPermissionsFromForm() { const perms = {}; PERM_RESOURCES.forEach((res) => { const select = store.permSelects[res.key]; perms[res.key] = select?.value || "none"; }); return perms; } function openCreateDialog() { store.editingId = null; if (titleEl) titleEl.textContent = t("userGroups.createTitle"); if (fields.name) { fields.name.value = ""; fields.name.disabled = false; } if (fields.allowPin) fields.allowPin.checked = false; buildPermRows(defaultPermissionsForNewGroup()); deleteBtnEl?.toggleAttribute("hidden", true); dialogEl?.showModal(); } function defaultPermissionsForNewGroup() { return { dashboard: "write", config: "none", maps: "none", missions: "read", sounds: "read", integrations: "read", users: "none", }; } function openEditDialog(id) { const group = groupById(id); if (!group || !canManageGroup(group)) return; store.editingId = id; if (titleEl) titleEl.textContent = t("userGroups.editTitle"); if (fields.name) fields.name.value = group.name || ""; if (fields.allowPin) fields.allowPin.checked = !!group.allow_pin; buildPermRows(group.permissions || {}); const canDelete = !group.builtin && (group.user_count || 0) === 0; deleteBtnEl?.toggleAttribute("hidden", !canDelete); dialogEl?.showModal(); } async function saveGroup(evt) { evt?.preventDefault(); const name = (fields.name?.value || "").trim(); if (!name) { alert(t("userGroups.error.missing")); return; } const payload = { name, allow_pin: !!fields.allowPin?.checked, permissions: readPermissionsFromForm(), }; try { if (store.editingId) { await apiJson(`/api/user_groups/${encodeURIComponent(store.editingId)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } else { await apiJson("/api/user_groups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } dialogEl?.close(); await refreshAll(); renderList(); } catch (e) { alert(e.message); } } function openDeleteConfirm(id) { const group = groupById(id); if (!group || group.builtin || (group.user_count || 0) > 0) return; store.pendingDeleteId = id; if (deleteConfirmTextEl) { deleteConfirmTextEl.textContent = t("userGroups.deleteConfirmText", { name: group.name || "" }); } deleteConfirmDialogEl?.showModal(); } function openDeleteConfirmFromDialog() { if (store.editingId) openDeleteConfirm(store.editingId); } async function confirmDelete() { const id = store.pendingDeleteId; if (!id) return; try { await apiJson(`/api/user_groups/${encodeURIComponent(id)}`, { method: "DELETE" }); store.pendingDeleteId = null; deleteConfirmDialogEl?.close(); dialogEl?.close(); await refreshAll(); renderList(); } catch (e) { alert(e.message); } } function clearFilters() { store.filter = ""; store.page = 1; if (filterInputEl) filterInputEl.value = ""; renderList(); } function bindEvents() { el("userGroupCreateBtn")?.addEventListener("click", () => { if (canWrite()) openCreateDialog(); }); formEl?.addEventListener("submit", saveGroup); el("userGroupEditCancelBtn")?.addEventListener("click", () => dialogEl?.close()); deleteBtnEl?.addEventListener("click", openDeleteConfirmFromDialog); listEl?.addEventListener("click", (evt) => { const editBtn = evt.target.closest("[data-edit]"); const deleteBtn = evt.target.closest("[data-delete]"); if (editBtn?.dataset.edit) openEditDialog(editBtn.dataset.edit); else if (deleteBtn?.dataset.delete) openDeleteConfirm(deleteBtn.dataset.delete); }); deleteConfirmDialogEl?.addEventListener("cancel", (evt) => { evt.preventDefault(); store.pendingDeleteId = null; deleteConfirmDialogEl?.close(); }); el("userGroupDeleteCancelBtn")?.addEventListener("click", () => { store.pendingDeleteId = null; deleteConfirmDialogEl?.close(); }); el("userGroupDeleteYesBtn")?.addEventListener("click", confirmDelete); filterInputEl?.addEventListener("input", () => { store.filter = filterInputEl.value; store.page = 1; renderList(); }); el("userGroupsClearFiltersBtn")?.addEventListener("click", clearFilters); el("userGroupsPageFirst")?.addEventListener("click", () => { store.page = 1; renderList(); }); el("userGroupsPagePrev")?.addEventListener("click", () => { store.page = Math.max(1, store.page - 1); renderList(); }); el("userGroupsPageNext")?.addEventListener("click", () => { store.page += 1; renderList(); }); el("userGroupsPageLast")?.addEventListener("click", () => { store.page = pageCount(filteredGroups().length); renderList(); }); el("userGroupsHelpBtn")?.addEventListener("click", () => alert(t("userGroups.helpBody"))); window.addEventListener("lm:locale-change", () => { if (store.editingId || dialogEl?.open) { const perms = readPermissionsFromForm(); buildPermRows(perms); } renderList(); }); } async function onPageShow() { if (!window.AuthApp?.canAccessPage?.("user-groups")) return; document.body.classList.toggle("auth-readonly-user-groups", !canWrite()); 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.UserGroupsApp = { onPageShow, onPageHide }; })();