405 lines
14 KiB
JavaScript
405 lines
14 KiB
JavaScript
(() => {
|
|
const PAGE_SIZE = 10;
|
|
|
|
const ICONS = {
|
|
io: `<svg class="ioModulesMirIcon" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><rect x="3" y="6" width="14" height="8" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="7" cy="10" r="1.2" fill="currentColor"/><circle cx="13" cy="10" r="1.2" fill="currentColor"/><path d="M10 3v3M10 14v3" stroke="currentColor" stroke-width="1.3" 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>`,
|
|
};
|
|
|
|
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, ">")
|
|
.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()
|
|
? `<div class="mapsMirRowActions">
|
|
<button type="button" class="mapsMirIconBtn ioModuleEditBtn" data-edit="${escapeHtml(mod.id)}" title="${escapeHtml(t("common.edit"))}">${ICONS.edit}</button>
|
|
<button type="button" class="mapsMirIconBtn mapsMirIconBtn--danger ioModuleDeleteBtn" data-delete="${escapeHtml(mod.id)}" title="${escapeHtml(t("common.delete"))}">${ICONS.delete}</button>
|
|
</div>`
|
|
: "";
|
|
|
|
const statusClass = mod.connected ? "ioModulesMirStatus--on" : "ioModulesMirStatus--off";
|
|
|
|
tr.innerHTML = `
|
|
<td class="ioModulesMirCellIcon">${ICONS.io}</td>
|
|
<td class="ioModulesMirCellName">
|
|
<button type="button" class="mapsMirNameLink ioModulesMirNameLink" data-edit="${escapeHtml(mod.id)}">${escapeHtml(mod.name || "—")}</button>
|
|
</td>
|
|
<td>${escapeHtml(typeLabel(mod.module_type))}</td>
|
|
<td>${escapeHtml(mod.ip_address || "—")}</td>
|
|
<td><span class="ioModulesMirStatus ${statusClass}">${escapeHtml(statusLabel(mod))}</span></td>
|
|
<td class="mapsMirCellFunctions">${actions}</td>
|
|
`;
|
|
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] };
|
|
})();
|