844 lines
30 KiB
JavaScript
844 lines
30 KiB
JavaScript
(() => {
|
||
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, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
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);
|
||
})();
|