(() => { const COIL_MIN = 1001; const COIL_MAX = 2000; const el = (id) => document.getElementById(id); const t = (key, vars) => window.I18n?.t(key, vars) ?? key; const triggerListEl = el("integrationTriggerList"); const triggerEmptyEl = el("integrationTriggerEmpty"); const coilGridEl = el("integrationCoilGrid"); const scheduleListEl = el("integrationScheduleList"); const scheduleEmptyEl = el("integrationScheduleEmpty"); const addTriggerDialogEl = el("integrationAddTriggerDialog"); const addScheduleDialogEl = el("integrationAddScheduleDialog"); const apiBaseUrlEl = el("integrationApiBaseUrl"); const store = { triggers: [], schedules: [], robots: [], coils: {}, missions: [], pollTimer: null, }; function escapeHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function missionName(id) { const m = store.missions.find((x) => x.id === id); return m ? m.name : id; } function robotLabel(id) { const r = store.robots.find((x) => x.id === id); return r ? r.name || r.id : id || "default"; } async function apiJson(url, opts = {}) { if (window.AuthApp && !window.AuthApp.isReady()) { throw new Error("not authenticated"); } 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) { const msg = (data && data.error) || text || res.statusText; throw new Error(msg); } return data; } async function refreshMissions() { try { const data = await apiJson("/api/missions"); store.missions = Array.isArray(data.missions) ? data.missions : []; } catch { store.missions = window.MissionsApp?.getMissions?.() || []; } } async function refreshRobots() { try { const data = await apiJson("/api/fleet/robots"); store.robots = Array.isArray(data) ? data : []; } catch { store.robots = [{ id: "default", name: t("integrations.defaultRobot") }]; } } async function refreshTriggers() { store.triggers = await apiJson("/api/triggers"); if (!Array.isArray(store.triggers)) store.triggers = []; } async function refreshSchedules() { store.schedules = await apiJson("/api/fleet/schedules"); if (!Array.isArray(store.schedules)) store.schedules = []; } async function refreshCoils() { store.coils = (await apiJson("/api/modbus/coils")) || {}; } function fillMissionSelect(selectEl, selected) { if (!selectEl) return; selectEl.innerHTML = ""; if (!store.missions.length) { const opt = document.createElement("option"); opt.value = ""; opt.textContent = t("integrations.noMissions"); selectEl.appendChild(opt); return; } store.missions.forEach((m) => { const opt = document.createElement("option"); opt.value = m.id; opt.textContent = m.name; if (m.id === selected) opt.selected = true; selectEl.appendChild(opt); }); } function fillRobotSelect(selectEl, selected) { if (!selectEl) return; selectEl.innerHTML = ""; store.robots.forEach((r) => { const opt = document.createElement("option"); opt.value = r.id; opt.textContent = r.name || r.id; if (r.id === selected) opt.selected = true; selectEl.appendChild(opt); }); if (!store.robots.length) { const opt = document.createElement("option"); opt.value = "default"; opt.textContent = t("integrations.defaultRobot"); opt.selected = selected === "default"; selectEl.appendChild(opt); } } function renderTriggers() { if (!triggerListEl) return; triggerListEl.innerHTML = ""; if (triggerEmptyEl) triggerEmptyEl.hidden = store.triggers.length > 0; store.triggers.forEach((trigger) => { const row = document.createElement("div"); row.className = "missionListItem integrationRow"; const coil = trigger.coil_id; const on = store.coils[String(coil)] === true; row.innerHTML = `
${escapeHtml(trigger.name)}
Coil ${coil} → ${escapeHtml(missionName(trigger.mission_id))} · ${trigger.enabled === false ? t("common.disabled") : t("common.enabled")} · ${t("integrations.coilState", { state: on ? "ON" : "OFF" })}
`; triggerListEl.appendChild(row); }); } function renderCoilGrid() { if (!coilGridEl) return; const assigned = new Map(store.triggers.map((t) => [t.coil_id, t])); const chips = []; assigned.forEach((trigger, coilId) => { const on = store.coils[String(coilId)] === true; chips.push( `` ); }); coilGridEl.innerHTML = chips.length > 0 ? chips.join("") : `${t("integrations.coilsEmpty")}`; } function formatScheduleTime(s) { if (!s.start_at) return s.start_mode === "scheduled" ? "—" : t("integrations.dialog.schedule.asap"); try { return new Date(s.start_at).toLocaleString("vi-VN"); } catch { return String(s.start_at); } } function renderSchedules() { if (!scheduleListEl) return; scheduleListEl.innerHTML = ""; if (scheduleEmptyEl) scheduleEmptyEl.hidden = store.schedules.length > 0; store.schedules.forEach((s) => { const row = document.createElement("div"); row.className = "missionListItem integrationRow"; row.innerHTML = `
${escapeHtml(s.name)}
${escapeHtml(missionName(s.mission_id))} · robot: ${escapeHtml(robotLabel(s.robot_id))} · ưu tiên ${s.priority ?? 0} · ${s.start_mode === "scheduled" ? "Lên lịch" : "ASAP"} · ${formatScheduleTime(s)} · ${s.enabled === false ? "Tắt" : "Bật"}
`; scheduleListEl.appendChild(row); }); } function updateApiBaseUrl() { const origin = window.location.origin; if (apiBaseUrlEl) apiBaseUrlEl.textContent = `${origin}/api/v2.0.0`; document.querySelectorAll(".integrationCurlHost").forEach((node) => { node.textContent = origin; }); } async function refreshAll() { await refreshMissions(); await Promise.all([refreshRobots(), refreshTriggers(), refreshSchedules(), refreshCoils()]); renderTriggers(); renderCoilGrid(); renderSchedules(); updateApiBaseUrl(); } async function openAddTriggerDialog() { await refreshMissions(); fillMissionSelect(el("integrationTriggerMission")); const coilInput = el("integrationTriggerCoil"); if (coilInput) coilInput.value = "1001"; el("integrationTriggerName").value = ""; addTriggerDialogEl?.showModal(); } async function submitAddTrigger(evt) { evt.preventDefault(); const name = el("integrationTriggerName").value.trim(); const coil_id = Number(el("integrationTriggerCoil").value); const mission_id = el("integrationTriggerMission").value; if (!name || !mission_id) return; if (coil_id < COIL_MIN || coil_id > COIL_MAX) { alert(`Coil phải từ ${COIL_MIN} đến ${COIL_MAX}`); return; } try { await apiJson("/api/triggers", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, coil_id, mission_id, enabled: true }), }); addTriggerDialogEl?.close(); await refreshAll(); } catch (err) { alert(err.message || "Không thêm được trigger"); } } async function openAddScheduleDialog() { await Promise.all([refreshMissions(), refreshRobots()]); fillMissionSelect(el("integrationScheduleMission")); fillRobotSelect(el("integrationScheduleRobot"), "default"); el("integrationScheduleName").value = ""; el("integrationSchedulePriority").value = "0"; el("integrationScheduleMode").value = "asap"; el("integrationScheduleStartAt").value = ""; toggleScheduleStartAt(); addScheduleDialogEl?.showModal(); } function toggleScheduleStartAt() { const mode = el("integrationScheduleMode")?.value || "asap"; const row = el("integrationScheduleStartAtRow"); if (row) row.hidden = mode !== "scheduled"; } async function submitAddSchedule(evt) { evt.preventDefault(); const name = el("integrationScheduleName").value.trim(); const mission_id = el("integrationScheduleMission").value; const robot_id = el("integrationScheduleRobot").value || "default"; const priority = Number(el("integrationSchedulePriority").value) || 0; const start_mode = el("integrationScheduleMode").value || "asap"; const startRaw = el("integrationScheduleStartAt").value; if (!name || !mission_id) return; const payload = { name, mission_id, robot_id, priority, start_mode, enabled: true }; if (start_mode === "scheduled" && startRaw) { payload.start_at = new Date(startRaw).toISOString(); } try { await apiJson("/api/fleet/schedules", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); addScheduleDialogEl?.close(); await refreshAll(); } catch (err) { alert(err.message || "Không thêm được lịch"); } } async function fireCoil(coilId) { try { await apiJson(`/api/modbus/coils/${coilId}/trigger`, { method: "POST" }); await refreshCoils(); renderTriggers(); renderCoilGrid(); } catch (err) { alert(err.message || "Không kích hoạt được coil"); } } async function deleteTrigger(id) { if (!confirm(t("integrations.confirm.deleteTrigger"))) return; try { await apiJson(`/api/triggers/${id}`, { method: "DELETE" }); await refreshAll(); } catch (err) { alert(err.message || "Không xóa được"); } } async function deleteSchedule(id) { if (!confirm(t("integrations.confirm.deleteSchedule"))) return; try { await apiJson(`/api/fleet/schedules/${id}`, { method: "DELETE" }); await refreshAll(); } catch (err) { alert(err.message || "Không xóa được"); } } async function runSchedule(id) { try { await apiJson(`/api/fleet/schedules/${id}/run`, { method: "POST" }); window.MissionsApp?.refreshQueue?.(); } catch (err) { alert(err.message || "Không chạy được lịch"); } } async function testRestEnqueue() { const missionId = el("integrationRestMission")?.value; if (!missionId) { alert("Chọn mission để thử"); return; } try { const data = await apiJson("/api/v2.0.0/mission_queue", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mission_id: missionId, priority: 0 }), }); alert(`Đã thêm vào queue — id ${data.id}`); window.MissionsApp?.refreshQueue?.(); } catch (err) { alert(err.message || "POST thất bại"); } } function bindEvents() { el("integrationAddTriggerBtn")?.addEventListener("click", openAddTriggerDialog); el("integrationAddScheduleBtn")?.addEventListener("click", openAddScheduleDialog); el("integrationRefreshBtn")?.addEventListener("click", () => refreshAll()); el("integrationAddTriggerForm")?.addEventListener("submit", submitAddTrigger); el("integrationAddScheduleForm")?.addEventListener("submit", submitAddSchedule); el("integrationScheduleMode")?.addEventListener("change", toggleScheduleStartAt); el("integrationRestTestBtn")?.addEventListener("click", testRestEnqueue); triggerListEl?.addEventListener("click", (evt) => { const coilBtn = evt.target.closest("[data-fire-coil]"); if (coilBtn) { fireCoil(Number(coilBtn.getAttribute("data-fire-coil"))); return; } const delBtn = evt.target.closest("[data-delete-trigger]"); if (delBtn) deleteTrigger(delBtn.getAttribute("data-delete-trigger")); }); coilGridEl?.addEventListener("click", (evt) => { const btn = evt.target.closest("[data-fire-coil]"); if (btn) fireCoil(Number(btn.getAttribute("data-fire-coil"))); }); scheduleListEl?.addEventListener("click", (evt) => { const runBtn = evt.target.closest("[data-run-schedule]"); if (runBtn) { runSchedule(runBtn.getAttribute("data-run-schedule")); return; } const delBtn = evt.target.closest("[data-delete-schedule]"); if (delBtn) deleteSchedule(delBtn.getAttribute("data-delete-schedule")); }); document.querySelectorAll("[data-close-dialog]").forEach((btn) => { btn.addEventListener("click", () => { const id = btn.getAttribute("data-close-dialog"); el(id)?.close(); }); }); } function startPoll() { stopPoll(); store.pollTimer = setInterval(async () => { try { await refreshCoils(); renderTriggers(); renderCoilGrid(); } catch { /* ignore */ } }, 3000); } function stopPoll() { if (store.pollTimer) { clearInterval(store.pollTimer); store.pollTimer = null; } } async function onPageShow() { await refreshAll(); fillMissionSelect(el("integrationRestMission")); startPoll(); } function init() { bindEvents(); updateApiBaseUrl(); } window.IntegrationsApp = { init, onPageShow, onPageHide: stopPoll, refreshAll, }; function boot() { init(); } window.addEventListener("lm:locale-change", () => { renderTriggers(); renderCoilGrid(); renderSchedules(); }); if (window.AuthApp?.isReady()) boot(); else window.addEventListener("lm:auth-ready", boot, { once: true }); })();