Files
App/www/integrations.js
2026-06-13 13:35:00 +07:00

445 lines
14 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 COIL_MIN = 1001;
const COIL_MAX = 2000;
const el = (id) => document.getElementById(id);
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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 = {}) {
const res = await fetch(url, 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: "Robot chính" }];
}
}
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 = "— Chưa có mission —";
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 = "Robot chính";
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((t) => {
const row = document.createElement("div");
row.className = "missionListItem integrationRow";
const coil = t.coil_id;
const on = store.coils[String(coil)] === true;
row.innerHTML = `
<div>
<div class="missionListItemTitle">${escapeHtml(t.name)}</div>
<div class="missionListItemMeta">
Coil <span class="mono">${coil}</span>
${escapeHtml(missionName(t.mission_id))}
· ${t.enabled === false ? "Tắt" : "Bật"}
· coil hiện tại: <span class="mono">${on ? "ON" : "OFF"}</span>
</div>
</div>
<div class="missionListItemActions">
<button type="button" class="btn subtle" data-fire-coil="${coil}">Kích hoạt</button>
<button type="button" class="btn subtle danger" data-delete-trigger="${escapeHtml(t.id)}">Xóa</button>
</div>`;
triggerListEl.appendChild(row);
});
}
function renderCoilGrid() {
if (!coilGridEl) return;
const assigned = new Map(store.triggers.map((t) => [t.coil_id, t]));
const chips = [];
assigned.forEach((t, coilId) => {
const on = store.coils[String(coilId)] === true;
chips.push(
`<button type="button" class="integrationCoilChip${on ? " on" : ""}" data-fire-coil="${coilId}" title="${escapeHtml(t.name)}">
${coilId}
</button>`
);
});
coilGridEl.innerHTML =
chips.length > 0
? chips.join("")
: `<span class="mutedNote">Chưa gán coil. Thêm trigger bên trên (10012000).</span>`;
}
function formatScheduleTime(s) {
if (!s.start_at) return s.start_mode === "scheduled" ? "—" : "Ngay (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 = `
<div>
<div class="missionListItemTitle">${escapeHtml(s.name)}</div>
<div class="missionListItemMeta">
${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"}
</div>
</div>
<div class="missionListItemActions">
<button type="button" class="btn subtle" data-run-schedule="${escapeHtml(s.id)}">Chạy ngay</button>
<button type="button" class="btn subtle danger" data-delete-schedule="${escapeHtml(s.id)}">Xóa</button>
</div>`;
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("Xóa trigger Modbus này?")) 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("Xóa lịch fleet này?")) 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,
};
init();
})();