(() => { const STORAGE_KEY = "phenikaax_dashboard_v1"; const WIDGET_LABELS = { mission_button: "Mission button", mission_group: "Mission group", mission_queue: "Mission queue", pause_continue: "Pause / Continue", }; const el = (id) => document.getElementById(id); const gridEl = el("dashboardGrid"); const emptyEl = el("dashboardEmpty"); const addDialogEl = el("dashboardAddWidgetDialog"); const editDialogEl = el("dashboardEditWidgetDialog"); const addTypeEl = el("dashboardWidgetType"); const addFieldsEl = el("dashboardAddWidgetFields"); const editFieldsEl = el("dashboardEditWidgetFields"); const editWidgetIdEl = el("dashboardEditWidgetId"); const editWidgetTypeEl = el("dashboardEditWidgetType"); const store = { widgets: [], editMode: false, pollTimer: null, queueUnsub: null, }; function newId() { if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID(); return `w_${Date.now().toString(36)}`; } function missions() { return window.MissionsApp || null; } function loadStore() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) { bootstrapDefaults(); return; } const data = JSON.parse(raw); store.widgets = Array.isArray(data.widgets) ? data.widgets : []; if (!store.widgets.length) bootstrapDefaults(); } catch { bootstrapDefaults(); } } function persistStore() { localStorage.setItem(STORAGE_KEY, JSON.stringify({ widgets: store.widgets })); } function bootstrapDefaults() { const m = missions()?.getMissions?.() || []; const firstId = m[0]?.id || ""; store.widgets = [ { id: newId(), type: "mission_button", mission_id: firstId, title: "" }, { id: newId(), type: "mission_group", group: "Missions", title: "" }, { id: newId(), type: "mission_queue", title: "Mission queue" }, { id: newId(), type: "pause_continue", title: "" }, ]; persistStore(); } function escapeHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function widgetTitle(widget) { if (widget.title) return widget.title; return WIDGET_LABELS[widget.type] || widget.type; } function missionOptions(selected) { const list = missions()?.getMissions?.() || []; return list .map( (m) => `` ) .join(""); } function groupOptions(selected) { const groups = missions()?.getGroups?.() || ["Missions"]; return groups .map((g) => ``) .join(""); } function fillTypeFields(container, type, widget = {}) { if (!container) return; container.innerHTML = ""; if (type === "mission_button") { container.innerHTML = `
Tạm dừng / tiếp tục mission đang chạy trên robot.
`; } } 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 || "Chọn mission…"; bodyEl.innerHTML = ` ${!m ? `Cấu hình widget và chọn mission.
` : ""}`; 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 = `Không có mission trong nhóm «${escapeHtml(group)}».
`; return; } bodyEl.innerHTML = ``; const listEl = bodyEl.querySelector(".dashboardMissionGroupList"); list.forEach((m) => { const btn = document.createElement("button"); btn.type = "button"; btn.className = "dashboardMissionGroupBtn"; btn.innerHTML = `▶${escapeHtml(m.name)}`; btn.addEventListener("click", () => missions()?.queueMission?.(m.id)); listEl.appendChild(btn); }); } function renderMissionQueueWidget(widget, bodyEl) { bodyEl.innerHTML = `Queue trống
`; 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 = `${running ? (paused ? "Mission đang tạm dừng" : "Mission đang chạy") : "Không có mission đang chạy"}
`; 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); } }); } function renderWidget(widget) { const card = document.createElement("article"); card.className = `dashboardWidget dashboardWidget--${widget.type}`; card.dataset.widgetId = widget.id; card.innerHTML = `Widget không hỗ trợ.
`; } 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; gridEl.innerHTML = ""; if (emptyEl) emptyEl.hidden = store.widgets.length > 0; gridEl.classList.toggle("dashboardGrid--edit", store.editMode); store.widgets.forEach((w) => gridEl.appendChild(renderWidget(w))); gridEl.querySelectorAll(".dashboardWidgetChrome").forEach((n) => { n.hidden = !store.editMode; }); } function refreshDynamicWidgets() { store.widgets.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 openAddDialog() { fillTypeFields(addFieldsEl, addTypeEl.value); addDialogEl.showModal(); } function openEditDialog(widgetId) { const widget = store.widgets.find((w) => w.id === widgetId); if (!widget) return; editWidgetIdEl.value = widget.id; editWidgetTypeEl.value = WIDGET_LABELS[widget.type] || widget.type; fillTypeFields(editFieldsEl, widget.type, widget); editDialogEl.showModal(); } function deleteWidget(widgetId) { if (!confirm("Xóa widget này?")) return; store.widgets = store.widgets.filter((w) => w.id !== widgetId); persistStore(); renderDashboard(); editDialogEl.close(); } function bindEvents() { el("dashboardAddWidgetBtn")?.addEventListener("click", openAddDialog); el("dashboardEditBtn")?.addEventListener("click", () => { store.editMode = !store.editMode; el("dashboardEditBtn").textContent = store.editMode ? "Xong" : "Sửa layout"; renderDashboard(); }); addTypeEl?.addEventListener("change", () => fillTypeFields(addFieldsEl, addTypeEl.value)); el("dashboardAddWidgetForm")?.addEventListener("submit", (evt) => { evt.preventDefault(); const type = addTypeEl.value; const fields = readFields(addFieldsEl); store.widgets.push({ id: newId(), type, ...fields }); persistStore(); addDialogEl.close(); renderDashboard(); }); el("dashboardEditWidgetForm")?.addEventListener("submit", (evt) => { evt.preventDefault(); const id = editWidgetIdEl.value; const widget = store.widgets.find((w) => w.id === id); if (!widget) return; Object.assign(widget, readFields(editFieldsEl)); persistStore(); editDialogEl.close(); renderDashboard(); }); el("dashboardDeleteWidgetBtn")?.addEventListener("click", () => deleteWidget(editWidgetIdEl.value)); } function startDashboardPoll() { stopDashboardPoll(); missions()?.refreshQueue?.(); store.queueUnsub = missions()?.onQueueUpdate?.(() => refreshDynamicWidgets()); store.pollTimer = setInterval(() => missions()?.refreshQueue?.(), 2000); } function stopDashboardPoll() { if (store.pollTimer) { clearInterval(store.pollTimer); store.pollTimer = null; } if (store.queueUnsub) { store.queueUnsub(); store.queueUnsub = null; } } function init() { loadStore(); bindEvents(); renderDashboard(); } window.DashboardApp = { init, onPageShow() { renderDashboard(); startDashboardPoll(); }, onPageHide() { stopDashboardPoll(); }, refresh() { renderDashboard(); }, }; init(); })();