Files
App/www/dashboard.js
HiepLM 098e1b2b69
Some checks failed
Test / test (push) Has been cancelled
Chuyển lưu trữ dữ liệu sang data base
2026-06-17 11:16:30 +07:00

844 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
const STORAGE_KEY_V3 = "phenikaax_dashboard_v3";
const STORAGE_KEY_V2 = "phenikaax_dashboard_v2";
const PAGE_SIZE = 10;
const DEFAULT_ID = "dashboard_default";
const el = (id) => document.getElementById(id);
const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
const listViewEl = el("dashboardListView");
const createViewEl = el("dashboardCreateView");
const designerViewEl = el("dashboardDesignerView");
const gridEl = el("dashboardGrid");
const designerEmptyEl = el("dashboardDesignerEmpty");
const tableBodyEl = el("dashboardTableBody");
const tableWrapEl = el("dashboardTableWrap");
const filterInputEl = el("dashboardFilterInput");
const listCountEl = el("dashboardListCount");
const pageLabelEl = el("dashboardPageLabel");
const designerTitleEl = el("dashboardDesignerTitle");
const editDialogEl = el("dashboardEditDialog");
const permissionsDialogEl = el("dashboardPermissionsDialog");
const addDialogEl = el("dashboardAddWidgetDialog");
const editWidgetDialogEl = el("dashboardEditWidgetDialog");
const addTypeEl = el("dashboardWidgetType");
const addFieldsEl = el("dashboardAddWidgetFields");
const editFieldsEl = el("dashboardEditWidgetFields");
const editWidgetIdEl = el("dashboardEditWidgetId");
const editWidgetTypeEl = el("dashboardEditWidgetType");
const store = {
dashboards: [],
activeDashboardId: null,
view: "list",
filter: "",
page: 1,
editMode: false,
pollActive: false,
queueUnsub: null,
userGroups: [],
};
function widgetTypeLabel(type) {
return t(`dashboard.widget.${type}`) || type;
}
function newId(prefix = "d") {
if (typeof crypto !== "undefined" && crypto.randomUUID) return `${prefix}_${crypto.randomUUID()}`;
return `${prefix}_${Date.now().toString(36)}`;
}
function missions() {
return window.MissionsApp || null;
}
function currentUser() {
return window.AuthApp?.getUser?.() || null;
}
function canEditDashboardsModule() {
const user = currentUser();
if (!user?.permissions) return true;
const level = user.permissions.dashboard;
return level === "write" || level === undefined;
}
function activeDashboard() {
return store.dashboards.find((d) => d.id === store.activeDashboardId) || null;
}
function dashboardCanEdit(dashboard) {
if (!dashboard) return false;
if (!canEditDashboardsModule()) return false;
const user = currentUser();
if (!user) return true;
const groups = Array.isArray(dashboard.editGroups) ? dashboard.editGroups : [];
if (!groups.length) return true;
return groups.includes(user.group_id);
}
function normalizeStr(value) {
return String(value || "").trim();
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function activeWidgets() {
return activeDashboard()?.widgets || [];
}
function setActiveWidgets(widgets) {
const db = activeDashboard();
if (db) db.widgets = widgets;
}
function bootstrapDefaultDashboard(widgets = []) {
store.dashboards = [
{
id: DEFAULT_ID,
name: "Default Dashboard",
createdBy: t("dashboard.createdBy.system"),
createdByUser: null,
isDefault: true,
editGroups: ["group_administrators", "group_distributors", "group_users"],
widgets: Array.isArray(widgets) ? widgets : [],
},
];
store.activeDashboardId = DEFAULT_ID;
persistStore();
}
function migrateFromV2() {
try {
const raw = localStorage.getItem(STORAGE_KEY_V2);
if (!raw) return false;
const data = JSON.parse(raw);
const widgets = Array.isArray(data.widgets) ? data.widgets : [];
bootstrapDefaultDashboard(widgets);
return true;
} catch {
return false;
}
}
function loadStoreLocal() {
try {
const raw = localStorage.getItem(STORAGE_KEY_V3);
if (!raw) {
if (!migrateFromV2()) bootstrapDefaultDashboard();
return;
}
const data = JSON.parse(raw);
store.dashboards = Array.isArray(data.dashboards) ? data.dashboards : [];
store.activeDashboardId = data.activeDashboardId || store.dashboards[0]?.id || null;
if (!store.dashboards.length) bootstrapDefaultDashboard();
else if (!store.activeDashboardId) store.activeDashboardId = store.dashboards[0].id;
} catch {
bootstrapDefaultDashboard();
}
}
let persistTimer = null;
async function loadStoreFromBackend() {
try {
const res = await fetch("/api/dashboards", { credentials: "include" });
if (!res.ok) {
loadStoreLocal();
return;
}
const data = await res.json();
store.dashboards = Array.isArray(data.dashboards) ? data.dashboards : [];
store.activeDashboardId = data.activeDashboardId || store.dashboards[0]?.id || null;
if (!store.dashboards.length) bootstrapDefaultDashboard();
else if (!store.activeDashboardId) store.activeDashboardId = store.dashboards[0].id;
} catch {
loadStoreLocal();
}
}
function persistStore() {
clearTimeout(persistTimer);
persistTimer = setTimeout(syncStoreToBackend, 400);
}
async function syncStoreToBackend() {
try {
await fetch("/api/dashboards", {
credentials: "include",
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dashboards: store.dashboards,
activeDashboardId: store.activeDashboardId,
}),
});
} catch {
/* keep in-memory state; retry on next persist */
}
}
async function loadUserGroups() {
try {
const res = await fetch("/api/user_groups", { credentials: "include" });
if (!res.ok) return;
const data = await res.json();
store.userGroups = Array.isArray(data.groups) ? data.groups : [];
} catch {
store.userGroups = [];
}
}
function filteredDashboards() {
const q = normalizeStr(store.filter).toLowerCase();
const list = [...store.dashboards];
list.sort((a, b) => {
if (a.isDefault && !b.isDefault) return -1;
if (!a.isDefault && b.isDefault) return 1;
return a.name.localeCompare(b.name);
});
if (!q) return list;
return list.filter((d) => d.name.toLowerCase().includes(q));
}
function paginatedDashboards() {
const all = filteredDashboards();
const totalPages = Math.max(1, Math.ceil(all.length / PAGE_SIZE));
if (store.page > totalPages) store.page = totalPages;
if (store.page < 1) store.page = 1;
const start = (store.page - 1) * PAGE_SIZE;
return { all, pageItems: all.slice(start, start + PAGE_SIZE), totalPages };
}
function renderPermissionsChecklist(container, selected = []) {
if (!container) return;
container.replaceChildren();
const selectedSet = new Set(selected);
store.userGroups.forEach((group) => {
const label = document.createElement("label");
const input = document.createElement("input");
input.type = "checkbox";
input.value = group.id;
input.checked = selectedSet.has(group.id);
label.appendChild(input);
label.appendChild(document.createTextNode(group.name || group.id));
container.appendChild(label);
});
if (!store.userGroups.length) {
const note = document.createElement("p");
note.className = "mutedNote";
note.textContent = "—";
container.appendChild(note);
}
}
function readPermissionsChecklist(container) {
const ids = [];
container?.querySelectorAll('input[type="checkbox"]:checked').forEach((node) => {
ids.push(node.value);
});
return ids;
}
function refreshNav() {
window.NavApp?.refreshFlyout?.();
}
function setView(view) {
store.view = view;
if (listViewEl) listViewEl.hidden = view !== "list";
if (createViewEl) createViewEl.hidden = view !== "create";
if (designerViewEl) designerViewEl.hidden = view !== "designer";
if (view === "list") {
stopDashboardPoll();
renderListUI();
} else if (view === "create") {
stopDashboardPoll();
} else {
renderDesignerChrome();
renderDashboard();
startDashboardPoll();
}
}
function setActiveDashboard(id) {
if (!store.dashboards.some((d) => d.id === id)) return;
store.activeDashboardId = id;
persistStore();
refreshNav();
}
function renderListUI() {
const { all, pageItems, totalPages } = paginatedDashboards();
if (listCountEl) listCountEl.textContent = t("dashboard.list.itemsFound", { count: all.length });
if (pageLabelEl) pageLabelEl.textContent = t("dashboard.list.pageOf", { page: store.page, total: totalPages });
document.querySelectorAll(".dashboardPageBtn").forEach((btn) => {
const action = btn.dataset.pageAction;
if (action === "first" || action === "prev") btn.disabled = store.page <= 1;
else if (action === "next" || action === "last") btn.disabled = store.page >= totalPages;
});
if (tableBodyEl) {
tableBodyEl.replaceChildren();
if (!pageItems.length) {
const tr = document.createElement("tr");
tr.className = "dashboardTableEmptyRow";
tr.innerHTML = `<td colspan="4">${escapeHtml(t("dashboard.list.empty"))}</td>`;
tableBodyEl.appendChild(tr);
} else {
pageItems.forEach((dashboard) => {
const tr = document.createElement("tr");
tr.className = "dashboardTableRow";
if (dashboard.isDefault) tr.classList.add("is-default");
if (dashboard.id === store.activeDashboardId) tr.classList.add("is-active");
const canEdit = dashboardCanEdit(dashboard);
const isActive = dashboard.id === store.activeDashboardId;
tr.innerHTML = `
<td class="dashboardTableStatusCol">
${isActive ? `<span class="dashboardActiveMark" aria-label="${escapeHtml(t("dashboard.list.active"))}">✓</span>` : `<span class="dashboardActiveMark dashboardActiveMark--placeholder" aria-hidden="true"></span>`}
</td>
<td class="dashboardTableNameCell">${escapeHtml(dashboard.name)}</td>
<td class="dashboardTableCreatedCell">${escapeHtml(dashboard.createdBy || "—")}</td>
<td class="dashboardTableFuncCol">
<div class="dashboardFuncBtns">
<button type="button" class="dashboardFuncBtn" data-action="design" data-id="${escapeHtml(dashboard.id)}" title="${escapeHtml(t("dashboard.list.design"))}" aria-label="${escapeHtml(t("dashboard.list.design"))}">
<span class="dashboardFuncIcon dashboardFuncIcon--design" aria-hidden="true"></span>
</button>
<button type="button" class="dashboardFuncBtn" data-action="edit" data-id="${escapeHtml(dashboard.id)}" title="${escapeHtml(t("dashboard.list.edit"))}" aria-label="${escapeHtml(t("dashboard.list.edit"))}" ${canEdit ? "" : "disabled"}>
<span class="dashboardFuncIcon dashboardFuncIcon--edit" aria-hidden="true"></span>
</button>
<button type="button" class="dashboardFuncBtn dashboardFuncBtn--delete" data-action="delete" data-id="${escapeHtml(dashboard.id)}" title="${escapeHtml(t("dashboard.list.delete"))}" aria-label="${escapeHtml(t("dashboard.list.delete"))}" ${canEdit && !dashboard.isDefault ? "" : "disabled"}>
<span class="dashboardFuncIcon dashboardFuncIcon--delete" aria-hidden="true"></span>
</button>
</div>
</td>`;
tableBodyEl.appendChild(tr);
});
}
}
if (tableWrapEl) tableWrapEl.classList.toggle("is-empty", all.length === 0);
const createBtn = el("dashboardCreateBtn");
if (createBtn) createBtn.disabled = !canEditDashboardsModule();
}
function renderDesignerChrome() {
const db = activeDashboard();
if (designerTitleEl) designerTitleEl.textContent = db?.name || "—";
}
function openCreateView() {
if (!canEditDashboardsModule()) return;
const nameInput = el("dashboardCreateName");
if (nameInput) nameInput.value = "";
renderPermissionsChecklist(el("dashboardCreatePermissions"), currentUser()?.group_id ? [currentUser().group_id] : []);
setView("create");
requestAnimationFrame(() => nameInput?.focus());
}
function openCreatePermissionsDialog() {
const container = el("dashboardCreatePermissions");
const current = readPermissionsChecklist(container);
const defaults = current.length ? current : currentUser()?.group_id ? [currentUser().group_id] : [];
renderPermissionsChecklist(container, defaults);
permissionsDialogEl?.showModal();
}
function closeCreateView() {
setView("list");
window.NavApp?.syncDashboardSection?.("dashboard-list");
}
function openEditDashboardDialog(id) {
const dashboard = store.dashboards.find((d) => d.id === id);
if (!dashboard || !dashboardCanEdit(dashboard)) {
alert(t("dashboard.list.noEditPermission"));
return;
}
el("dashboardEditId").value = dashboard.id;
el("dashboardEditName").value = dashboard.name;
renderPermissionsChecklist(el("dashboardEditPermissions"), dashboard.editGroups || []);
editDialogEl?.showModal();
}
function openDesignerFor(id) {
setActiveDashboard(id);
setView("designer");
window.NavApp?.syncDashboardSection?.(`dashboard-${id}`);
}
function deleteDashboard(id) {
const dashboard = store.dashboards.find((d) => d.id === id);
if (!dashboard) return;
if (dashboard.isDefault) {
alert(t("dashboard.list.cannotDeleteDefault"));
return;
}
if (!dashboardCanEdit(dashboard)) {
alert(t("dashboard.list.noEditPermission"));
return;
}
if (!confirm(t("dashboard.list.deleteConfirm", { name: dashboard.name }))) return;
store.dashboards = store.dashboards.filter((d) => d.id !== id);
if (store.activeDashboardId === id) {
store.activeDashboardId = store.dashboards[0]?.id || null;
}
persistStore();
refreshNav();
renderListUI();
}
function widgetTitle(widget) {
if (widget.title) return widget.title;
return widgetTypeLabel(widget.type);
}
function missionOptions(selected) {
const list = missions()?.getMissions?.() || [];
return list
.map(
(m) =>
`<option value="${escapeHtml(m.id)}" ${m.id === selected ? "selected" : ""}>${escapeHtml(m.name)} (${escapeHtml(m.group)})</option>`
)
.join("");
}
function groupOptions(selected) {
const groups = missions()?.getGroups?.() || ["Missions"];
return groups
.map((g) => `<option value="${escapeHtml(g)}" ${g === selected ? "selected" : ""}>${escapeHtml(g)}</option>`)
.join("");
}
function fillTypeFields(container, type, widget = {}) {
if (!container) return;
container.innerHTML = "";
if (type === "mission_button") {
container.innerHTML = `
<div class="row rowWide">
<label>${t("dashboard.widget.field.mission")}</label>
<select data-field="mission_id">${missionOptions(widget.mission_id || "")}</select>
</div>
<div class="row rowWide">
<label>${t("dashboard.widget.field.title")}</label>
<input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" placeholder="${escapeHtml(t("dashboard.widget.titlePlaceholder"))}" />
</div>`;
} else if (type === "mission_group") {
container.innerHTML = `
<div class="row rowWide">
<label>${t("dashboard.widget.field.group")}</label>
<select data-field="group">${groupOptions(widget.group || "Missions")}</select>
</div>
<div class="row rowWide">
<label>${t("dashboard.widget.field.title")}</label>
<input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" />
</div>`;
} else if (type === "mission_queue") {
container.innerHTML = `
<div class="row rowWide">
<label>${t("dashboard.widget.field.title")}</label>
<input data-field="title" type="text" value="${escapeHtml(widget.title || "Mission queue")}" />
</div>`;
} else if (type === "pause_continue") {
container.innerHTML = `
<div class="row rowWide">
<label>${t("dashboard.widget.field.title")}</label>
<input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" />
</div>
<p class="mutedNote">${escapeHtml(t("dashboard.widget.pauseHint"))}</p>`;
}
}
function readFields(container) {
const out = {};
container?.querySelectorAll("[data-field]").forEach((node) => {
out[node.dataset.field] = node.value;
});
return out;
}
function renderMissionButtonWidget(widget, bodyEl) {
const m = missions()?.getMissionById?.(widget.mission_id);
const label = m?.name || t("dashboard.widget.selectMission");
bodyEl.innerHTML = `
<button type="button" class="dashboardMissionBtn" data-run-mission="${escapeHtml(widget.mission_id || "")}">
<span class="dashboardMissionBtnIcon">▶</span>
<span>${escapeHtml(label)}</span>
</button>
${!m ? `<p class="mutedNote dashboardWidgetHint">${t("dashboard.widget.configHint")}</p>` : ""}`;
bodyEl.querySelector("[data-run-mission]")?.addEventListener("click", () => {
if (!widget.mission_id) return;
missions()?.queueMission?.(widget.mission_id);
});
}
function renderMissionGroupWidget(widget, bodyEl) {
const group = widget.group || "Missions";
const list = (missions()?.getMissions?.() || []).filter((m) => m.group === group);
if (!list.length) {
bodyEl.innerHTML = `<p class="mutedNote">${t("dashboard.widget.emptyGroup", { group })}</p>`;
return;
}
bodyEl.innerHTML = `<div class="dashboardMissionGroupList"></div>`;
const listEl = bodyEl.querySelector(".dashboardMissionGroupList");
list.forEach((m) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "dashboardMissionGroupBtn";
btn.innerHTML = `<span class="dashboardMissionBtnIcon">▶</span><span>${escapeHtml(m.name)}</span>`;
btn.addEventListener("click", () => missions()?.queueMission?.(m.id));
listEl.appendChild(btn);
});
}
function renderMissionQueueWidget(widget, bodyEl) {
bodyEl.innerHTML = `
<div class="dashboardQueueRunner mutedNote" data-role="runner">—</div>
<div class="dashboardQueueList" data-role="list"></div>
<p class="mutedNote dashboardQueueEmpty" data-role="empty">${t("dashboard.widget.queueEmpty")}</p>
<button type="button" class="btn subtle btnBlock dashboardQueueClear">${t("dashboard.widget.clearQueue")}</button>`;
bodyEl.querySelector(".dashboardQueueClear")?.addEventListener("click", () => missions()?.clearQueue?.());
refreshQueueWidget(bodyEl);
}
function refreshQueueWidget(bodyEl) {
const snap = missions()?.getQueueSnapshot?.();
if (!snap) return;
missions()?.renderQueueInto?.(
{
listEl: bodyEl.querySelector('[data-role="list"]'),
runnerEl: bodyEl.querySelector('[data-role="runner"]'),
emptyEl: bodyEl.querySelector('[data-role="empty"]'),
},
{ compact: true }
);
}
function renderPauseContinueWidget(widget, bodyEl) {
const snap = missions()?.getQueueSnapshot?.();
const state = snap?.runner?.state || "idle";
const paused = state === "paused" || snap?.runner?.paused;
const running = state === "running" || paused;
bodyEl.innerHTML = `
<div class="dashboardRunnerControls">
<button type="button" class="dashboardPauseBtn ${paused ? "is-paused" : ""}" data-pause-action="${paused ? "continue" : "pause"}" ${running ? "" : "disabled"}>
${paused ? t("dashboard.widget.continue") : t("dashboard.widget.pause")}
</button>
<button type="button" class="dashboardCancelBtn" data-cancel-mission ${running ? "" : "disabled"}>
${t("dashboard.widget.cancelMission")}
</button>
</div>
<p class="mutedNote dashboardWidgetHint">${running ? (paused ? t("dashboard.widget.runner.paused") : t("dashboard.widget.runner.running")) : t("dashboard.widget.runner.idle")}</p>`;
bodyEl.querySelector("[data-pause-action]")?.addEventListener("click", async (evt) => {
const action = evt.currentTarget.dataset.pauseAction;
try {
if (action === "pause") await missions()?.pauseRunner?.();
else await missions()?.continueRunner?.();
} catch (e) {
alert(e.message);
}
});
bodyEl.querySelector("[data-cancel-mission]")?.addEventListener("click", async () => {
try {
await missions()?.cancelRunner?.();
} catch (e) {
alert(e.message);
}
});
}
function renderWidget(widget) {
const card = document.createElement("article");
card.className = `dashboardWidget dashboardWidget--${widget.type}`;
card.dataset.widgetId = widget.id;
card.innerHTML = `
<div class="dashboardWidgetHeader">
<div class="dashboardWidgetTitle">${escapeHtml(widgetTitle(widget))}</div>
<div class="dashboardWidgetChrome" hidden>
<button type="button" class="iconBtn" data-widget-config title="${escapeHtml(t("common.configure"))}">⚙</button>
<button type="button" class="iconBtn danger" data-widget-delete title="${escapeHtml(t("common.delete"))}">×</button>
</div>
</div>
<div class="dashboardWidgetBody"></div>`;
const bodyEl = card.querySelector(".dashboardWidgetBody");
switch (widget.type) {
case "mission_button":
renderMissionButtonWidget(widget, bodyEl);
break;
case "mission_group":
renderMissionGroupWidget(widget, bodyEl);
break;
case "mission_queue":
renderMissionQueueWidget(widget, bodyEl);
break;
case "pause_continue":
renderPauseContinueWidget(widget, bodyEl);
break;
default:
bodyEl.innerHTML = `<p class="mutedNote">${t("dashboard.widget.unsupported")}</p>`;
}
card.querySelector("[data-widget-config]")?.addEventListener("click", () => openEditDialog(widget.id));
card.querySelector("[data-widget-delete]")?.addEventListener("click", () => deleteWidget(widget.id));
return card;
}
function renderDashboard() {
if (!gridEl) return;
const widgets = activeWidgets();
gridEl.innerHTML = "";
if (designerEmptyEl) designerEmptyEl.hidden = widgets.length > 0;
gridEl.classList.toggle("dashboardGrid--edit", store.editMode);
widgets.forEach((w) => gridEl.appendChild(renderWidget(w)));
gridEl.querySelectorAll(".dashboardWidgetChrome").forEach((n) => {
n.hidden = !store.editMode;
});
}
function refreshDynamicWidgets() {
activeWidgets().forEach((widget) => {
const card = gridEl?.querySelector(`[data-widget-id="${widget.id}"]`);
if (!card) return;
const bodyEl = card.querySelector(".dashboardWidgetBody");
if (widget.type === "mission_queue") refreshQueueWidget(bodyEl);
if (widget.type === "pause_continue") renderPauseContinueWidget(widget, bodyEl);
});
}
function openEditDialog(widgetId) {
const widget = activeWidgets().find((w) => w.id === widgetId);
if (!widget) return;
editWidgetIdEl.value = widget.id;
editWidgetTypeEl.value = widgetTypeLabel(widget.type);
fillTypeFields(editFieldsEl, widget.type, widget);
editWidgetDialogEl.showModal();
}
function deleteWidget(widgetId) {
if (!confirm(t("dashboard.widget.deleteConfirm"))) return;
const db = activeDashboard();
if (!db) return;
db.widgets = db.widgets.filter((w) => w.id !== widgetId);
persistStore();
renderDashboard();
editWidgetDialogEl.close();
}
function handleNav(section) {
if (section === "dashboard-list") {
setView("list");
return;
}
if (section.startsWith("dashboard-")) {
const id = section.slice("dashboard-".length);
if (store.dashboards.some((d) => d.id === id)) {
setActiveDashboard(id);
setView("designer");
}
}
}
function getNavItems() {
const items = [{ section: "dashboard-list", page: "dashboard", label: t("nav.dashboardsList") }];
store.dashboards.forEach((dashboard) => {
items.push({
section: `dashboard-${dashboard.id}`,
page: "dashboard",
label: dashboard.name,
});
});
return items;
}
function bindEvents() {
el("dashboardCreateBtn")?.addEventListener("click", openCreateView);
el("dashboardCreateBackBtn")?.addEventListener("click", closeCreateView);
el("dashboardCreateCancelBtn")?.addEventListener("click", closeCreateView);
el("dashboardCreatePermissionsBtn")?.addEventListener("click", openCreatePermissionsDialog);
el("dashboardPermissionsForm")?.addEventListener("submit", (evt) => {
evt.preventDefault();
permissionsDialogEl?.close();
});
el("dashboardClearFiltersBtn")?.addEventListener("click", () => {
store.filter = "";
store.page = 1;
if (filterInputEl) filterInputEl.value = "";
renderListUI();
});
filterInputEl?.addEventListener("input", () => {
store.filter = filterInputEl.value;
store.page = 1;
renderListUI();
});
document.getElementById("dashboardPagination")?.addEventListener("click", (evt) => {
const btn = evt.target.closest("[data-page-action]");
if (!btn || btn.disabled) return;
const { totalPages } = paginatedDashboards();
const action = btn.dataset.pageAction;
if (action === "first") store.page = 1;
else if (action === "prev") store.page = Math.max(1, store.page - 1);
else if (action === "next") store.page = Math.min(totalPages, store.page + 1);
else if (action === "last") store.page = totalPages;
renderListUI();
});
tableBodyEl?.addEventListener("click", (evt) => {
const btn = evt.target.closest("[data-action]");
if (!btn) return;
const id = btn.dataset.id;
const action = btn.dataset.action;
if (action === "design") openDesignerFor(id);
else if (action === "edit") openEditDashboardDialog(id);
else if (action === "delete") deleteDashboard(id);
});
el("dashboardBackToListBtn")?.addEventListener("click", () => {
setView("list");
window.NavApp?.syncDashboardSection?.("dashboard-list");
});
el("dashboardCreateForm")?.addEventListener("submit", (evt) => {
evt.preventDefault();
const name = normalizeStr(el("dashboardCreateName").value);
if (!name) return;
const user = currentUser();
const dashboard = {
id: newId("dashboard"),
name,
createdBy: user?.group_name || user?.display_name || user?.username || "—",
createdByUser: user?.username || null,
isDefault: false,
editGroups: readPermissionsChecklist(el("dashboardCreatePermissions")),
widgets: [],
};
store.dashboards.push(dashboard);
store.activeDashboardId = dashboard.id;
persistStore();
refreshNav();
openDesignerFor(dashboard.id);
});
el("dashboardEditForm")?.addEventListener("submit", (evt) => {
evt.preventDefault();
const id = el("dashboardEditId").value;
const dashboard = store.dashboards.find((d) => d.id === id);
if (!dashboard || !dashboardCanEdit(dashboard)) return;
dashboard.name = normalizeStr(el("dashboardEditName").value);
dashboard.editGroups = readPermissionsChecklist(el("dashboardEditPermissions"));
persistStore();
editDialogEl?.close();
refreshNav();
if (store.view === "list") renderListUI();
else renderDesignerChrome();
});
addTypeEl?.addEventListener("change", () => fillTypeFields(addFieldsEl, addTypeEl.value));
el("dashboardAddWidgetForm")?.addEventListener("submit", (evt) => {
evt.preventDefault();
const db = activeDashboard();
if (!db) return;
const type = addTypeEl.value;
const fields = readFields(addFieldsEl);
db.widgets.push({ id: newId("w"), type, ...fields });
persistStore();
addDialogEl.close();
renderDashboard();
});
el("dashboardEditWidgetForm")?.addEventListener("submit", (evt) => {
evt.preventDefault();
const id = editWidgetIdEl.value;
const widget = activeWidgets().find((w) => w.id === id);
if (!widget) return;
Object.assign(widget, readFields(editFieldsEl));
persistStore();
editWidgetDialogEl.close();
renderDashboard();
});
el("dashboardDeleteWidgetBtn")?.addEventListener("click", () => deleteWidget(editWidgetIdEl.value));
}
function startDashboardPoll() {
if (window.AuthApp && !window.AuthApp.isReady()) return;
if (store.view !== "designer") return;
stopDashboardPoll();
missions()?.refreshQueue?.();
store.queueUnsub = missions()?.onQueueUpdate?.(() => refreshDynamicWidgets());
missions()?.startQueuePoll?.();
store.pollActive = true;
}
function stopDashboardPoll() {
if (store.pollActive) {
missions()?.stopQueuePoll?.();
store.pollActive = false;
}
if (store.queueUnsub) {
store.queueUnsub();
store.queueUnsub = null;
}
}
async function init() {
await loadStoreFromBackend();
await loadUserGroups();
bindEvents();
setView("list");
}
window.DashboardApp = {
init,
getNavItems,
handleNav,
onPageShow() {
if (store.view === "designer") {
renderDesignerChrome();
renderDashboard();
startDashboardPoll();
} else {
renderListUI();
}
},
onPageHide() {
stopDashboardPoll();
},
refresh() {
if (store.view === "list") renderListUI();
else renderDashboard();
},
};
async function boot() {
await init();
refreshNav();
}
window.addEventListener("lm:locale-change", () => {
if (store.view === "list") renderListUI();
else {
renderDesignerChrome();
renderDashboard();
}
refreshNav();
});
if (window.AuthApp?.isReady()) boot();
else window.addEventListener("lm:auth-ready", boot, { once: true });
window.addEventListener("lm:auth-logout", stopDashboardPoll);
})();