(() => { const PAGE_SIZE = 10; const ICONS = { io: ``, edit: ``, delete: ``, }; const el = (id) => document.getElementById(id); const t = (key, vars) => window.I18n?.t(key, vars) ?? key; const listEl = el("ioModuleList"); const emptyEl = el("ioModuleListEmpty"); const tableEl = el("ioModulesTable"); const filterInputEl = el("ioModulesFilterInput"); const filterCountEl = el("ioModulesFilterCount"); const pageLabelEl = el("ioModulesPageLabel"); const dialogEl = el("ioModuleEditDialog"); const formEl = el("ioModuleEditForm"); const titleEl = el("ioModuleEditTitle"); const deleteBtnEl = el("ioModuleEditDeleteBtn"); const typeHintEl = el("ioModuleEditTypeHint"); const deleteConfirmDialogEl = el("ioModuleDeleteConfirmDialog"); const deleteConfirmTextEl = el("ioModuleDeleteConfirmText"); const fields = { site: el("ioModuleEditSite"), name: el("ioModuleEditName"), type: el("ioModuleEditType"), ip: el("ioModuleEditIp"), }; const store = { modules: [], sites: [], editingId: null, pendingDeleteId: null, filter: "", page: 1, }; function canWrite() { if (!window.AuthApp?.canWrite) return true; return window.AuthApp.canWrite("integrations"); } 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; } function defaultSiteId() { return store.sites[0]?.id || "site_configuration"; } function typeLabel(type) { if (type === "bluetooth") return t("ioModules.type.bluetooth"); if (type === "wise") return t("ioModules.type.wise"); return type || "—"; } function statusLabel(mod) { return mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected"); } function updateTypeHint() { if (!typeHintEl || !fields.type) return; const type = fields.type.value; typeHintEl.textContent = type === "wise" ? t("ioModules.typeHint.wise") : t("ioModules.typeHint.bluetooth"); } async function refreshAll() { const [sitesData, modulesData] = await Promise.all([ apiJson("/api/sites"), apiJson("/api/io_modules"), ]); store.sites = Array.isArray(sitesData.sites) ? sitesData.sites : []; store.modules = Array.isArray(modulesData.io_modules) ? modulesData.io_modules : []; return store.modules; } function moduleById(id) { return store.modules.find((m) => m.id === id) || null; } function filteredModules() { const q = store.filter.trim().toLowerCase(); let items = [...store.modules].sort((a, b) => (a.name || "").localeCompare(b.name || "")); if (q) { items = items.filter((m) => { const name = (m.name || "").toLowerCase(); const ip = (m.ip_address || "").toLowerCase(); const type = (m.module_type || "").toLowerCase(); return name.includes(q) || ip.includes(q) || type.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("ioModules.itemsFound", { n: totalItems }); if (pageLabelEl) pageLabelEl.textContent = t("ioModules.pageOf", { page: store.page, total: totalPages }); const atStart = store.page <= 1; const atEnd = store.page >= totalPages; el("ioModulesPageFirst")?.toggleAttribute("disabled", atStart); el("ioModulesPagePrev")?.toggleAttribute("disabled", atStart); el("ioModulesPageNext")?.toggleAttribute("disabled", atEnd); el("ioModulesPageLast")?.toggleAttribute("disabled", atEnd); } function fillSiteSelect(selectedId) { if (!fields.site) return; fields.site.innerHTML = ""; store.sites.forEach((site) => { const opt = document.createElement("option"); opt.value = site.id; opt.textContent = site.name || site.id; if (site.id === selectedId) opt.selected = true; fields.site.appendChild(opt); }); if (!fields.site.value && store.sites[0]) fields.site.value = store.sites[0].id; } function renderList() { if (!listEl) return; const items = filteredModules(); 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("ioModules.emptyFilter") : t("ioModules.empty"); } pageItems.forEach((mod) => { const tr = document.createElement("tr"); tr.className = "mapsMirRow ioModulesMirRow"; tr.dataset.id = mod.id; const actions = canWrite() ? `
` : ""; const statusClass = mod.connected ? "ioModulesMirStatus--on" : "ioModulesMirStatus--off"; tr.innerHTML = ` ${ICONS.io} ${escapeHtml(typeLabel(mod.module_type))} ${escapeHtml(mod.ip_address || "—")} ${escapeHtml(statusLabel(mod))} ${actions} `; listEl.appendChild(tr); }); } function openCreateDialog() { store.editingId = null; if (titleEl) titleEl.textContent = t("ioModules.createTitle"); fillSiteSelect(defaultSiteId()); if (fields.name) fields.name.value = ""; if (fields.type) fields.type.value = "bluetooth"; if (fields.ip) fields.ip.value = ""; updateTypeHint(); deleteBtnEl?.toggleAttribute("hidden", true); dialogEl?.showModal(); } function openEditDialog(id) { const mod = moduleById(id); if (!mod) return; store.editingId = id; if (titleEl) titleEl.textContent = t("ioModules.editTitle"); fillSiteSelect(mod.site_id || defaultSiteId()); if (fields.name) fields.name.value = mod.name || ""; if (fields.type) fields.type.value = mod.module_type || "bluetooth"; if (fields.ip) fields.ip.value = mod.ip_address || ""; updateTypeHint(); deleteBtnEl?.toggleAttribute("hidden", false); dialogEl?.showModal(); } async function saveModule(evt) { evt?.preventDefault(); const payload = { site_id: fields.site?.value || defaultSiteId(), name: (fields.name?.value || "").trim(), module_type: fields.type?.value || "bluetooth", ip_address: (fields.ip?.value || "").trim(), }; if (!payload.name || !payload.ip_address) { alert(t("ioModules.error.missing")); return; } try { if (store.editingId) { await apiJson(`/api/io_modules/${encodeURIComponent(store.editingId)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } else { await apiJson("/api/io_modules", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } dialogEl?.close(); await refreshAll(); renderList(); window.dispatchEvent(new CustomEvent("lm:io-modules-changed")); } catch (e) { alert(e.message); } } function openDeleteConfirm(id) { const mod = moduleById(id); if (!mod) return; store.pendingDeleteId = id; if (deleteConfirmTextEl) { deleteConfirmTextEl.textContent = t("ioModules.deleteConfirmText", { name: mod.name || "" }); } deleteConfirmDialogEl?.showModal(); } async function confirmDelete() { const id = store.pendingDeleteId; if (!id) return; try { const res = await fetch(`/api/io_modules/${encodeURIComponent(id)}`, { method: "DELETE", credentials: "include" }); if (!res.ok) { let msg = res.statusText; try { const err = await res.json(); if (err.error) msg = err.error; if (err.usages) msg += "\n" + JSON.stringify(err.usages, null, 2); } catch { /* ignore */ } throw new Error(msg); } store.pendingDeleteId = null; deleteConfirmDialogEl?.close(); dialogEl?.close(); await refreshAll(); renderList(); window.dispatchEvent(new CustomEvent("lm:io-modules-changed")); } catch (e) { alert(e.message); } } function clearFilters() { store.filter = ""; store.page = 1; if (filterInputEl) filterInputEl.value = ""; renderList(); } function bindEvents() { el("ioModuleCreateBtn")?.addEventListener("click", () => { if (canWrite()) openCreateDialog(); }); formEl?.addEventListener("submit", saveModule); el("ioModuleEditCancelBtn")?.addEventListener("click", () => dialogEl?.close()); deleteBtnEl?.addEventListener("click", () => { if (store.editingId) openDeleteConfirm(store.editingId); }); fields.type?.addEventListener("change", updateTypeHint); el("ioModuleTestBtn")?.addEventListener("click", async () => { const ip = (fields.ip?.value || "").trim(); if (!ip) { alert(t("ioModules.error.missing")); return; } try { await apiJson("/api/io_modules/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ip_address: ip, port: 502 }), }); alert(t("ioModules.testOk")); } catch (e) { alert(e.message); } }); listEl?.addEventListener("click", (evt) => { const editBtn = evt.target.closest("[data-edit]"); const deleteBtn = evt.target.closest("[data-delete]"); if (editBtn?.dataset.edit && canWrite()) openEditDialog(editBtn.dataset.edit); else if (deleteBtn?.dataset.delete && canWrite()) openDeleteConfirm(deleteBtn.dataset.delete); }); deleteConfirmDialogEl?.addEventListener("cancel", (evt) => { evt.preventDefault(); store.pendingDeleteId = null; deleteConfirmDialogEl?.close(); }); el("ioModuleDeleteCancelBtn")?.addEventListener("click", () => { store.pendingDeleteId = null; deleteConfirmDialogEl?.close(); }); el("ioModuleDeleteYesBtn")?.addEventListener("click", confirmDelete); filterInputEl?.addEventListener("input", () => { store.filter = filterInputEl.value; store.page = 1; renderList(); }); el("ioModulesClearFiltersBtn")?.addEventListener("click", clearFilters); el("ioModulesPageFirst")?.addEventListener("click", () => { store.page = 1; renderList(); }); el("ioModulesPagePrev")?.addEventListener("click", () => { store.page = Math.max(1, store.page - 1); renderList(); }); el("ioModulesPageNext")?.addEventListener("click", () => { store.page += 1; renderList(); }); el("ioModulesPageLast")?.addEventListener("click", () => { store.page = pageCount(filteredModules().length); renderList(); }); el("ioModulesHelpBtn")?.addEventListener("click", () => alert(t("ioModules.helpBody"))); window.addEventListener("lm:locale-change", () => renderList()); } async function onPageShow() { if (!window.AuthApp?.canAccessPage?.("io-modules")) return; document.body.classList.toggle("auth-readonly-io-modules", !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.IoModulesCatalog = { getModules: () => [...store.modules], getNames: () => store.modules.map((m) => m.name).filter(Boolean), refresh: refreshAll, }; window.IoModulesApp = { onPageShow, onPageHide, refreshAll, getModules: () => [...store.modules] }; })();