459 lines
15 KiB
JavaScript
459 lines
15 KiB
JavaScript
(() => {
|
|
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, ">")
|
|
.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 = `
|
|
<div>
|
|
<div class="missionListItemTitle">${escapeHtml(trigger.name)}</div>
|
|
<div class="missionListItemMeta">
|
|
Coil <span class="mono">${coil}</span>
|
|
→ ${escapeHtml(missionName(trigger.mission_id))}
|
|
· ${trigger.enabled === false ? t("common.disabled") : t("common.enabled")}
|
|
· ${t("integrations.coilState", { state: on ? "ON" : "OFF" })}
|
|
</div>
|
|
</div>
|
|
<div class="missionListItemActions">
|
|
<button type="button" class="btn subtle" data-fire-coil="${coil}">${t("integrations.fireTrigger")}</button>
|
|
<button type="button" class="btn subtle danger" data-delete-trigger="${escapeHtml(trigger.id)}">${t("common.delete")}</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((trigger, coilId) => {
|
|
const on = store.coils[String(coilId)] === true;
|
|
chips.push(
|
|
`<button type="button" class="integrationCoilChip${on ? " on" : ""}" data-fire-coil="${coilId}" title="${escapeHtml(trigger.name)}">
|
|
${coilId}
|
|
</button>`
|
|
);
|
|
});
|
|
coilGridEl.innerHTML =
|
|
chips.length > 0
|
|
? chips.join("")
|
|
: `<span class="mutedNote">${t("integrations.coilsEmpty")}</span>`;
|
|
}
|
|
|
|
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 = `
|
|
<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)}">${t("integrations.schedule.runNow")}</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(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 });
|
|
})();
|