Files
faction_war_dispatch_bot/static/dashboard.js

276 lines
10 KiB
JavaScript

// dashboard.js
// Full functionality: populate, start status loops, load members, drag & drop between lists and groups,
// drop zones accept only friendly/enemy respectively, status color updates.
// ---- In-memory maps ----
const friendlyMembers = new Map(); // id -> member object
const enemyMembers = new Map();
// ---- DOM containers ----
const friendlyContainer = document.getElementById("friendly-container");
const enemyContainer = document.getElementById("enemy-container");
// utility to safe parse ints
function toInt(v) { const n = Number(v); return Number.isNaN(n) ? null : n; }
// ---- Create structured card ----
function createMemberCard(member, kind) {
// member: { id, name, level, estimate, status }
// kind: "friendly" or "enemy" (for ARIA / dataset)
const card = document.createElement("div");
card.classList.add("member-card");
card.classList.add(kind);
card.setAttribute("draggable", "true");
card.dataset.id = member.id;
card.dataset.kind = kind;
// structured children
const nameDiv = document.createElement("div");
nameDiv.className = "name";
nameDiv.textContent = member.name;
const statsDiv = document.createElement("div");
statsDiv.className = "stats";
statsDiv.innerHTML = `Lv: ${member.level} <br> Est: ${member.estimate} <br> Status: <span class="status-text">${member.status || "Unknown"}</span>`;
card.appendChild(nameDiv);
card.appendChild(statsDiv);
// store back-reference
member.domElement = card;
// drag events: encode type and id in dataTransfer
card.addEventListener("dragstart", (e) => {
const payload = JSON.stringify({ kind, id: member.id });
e.dataTransfer.setData("text/plain", payload);
// visual
card.style.opacity = "0.5";
});
card.addEventListener("dragend", () => {
card.style.opacity = "1";
});
// initial color
applyStatusClass(member);
return card;
}
// ---- Apply status CSS class to the .status-text inside a member card ----
function applyStatusClass(member) {
const span = member.domElement?.querySelector(".status-text");
if (!span) return;
span.classList.remove("status-ok", "status-traveling", "status-hospitalized");
const s = String(member.status || "").toLowerCase();
if (s === "okay") span.classList.add("status-ok");
else if (s === "traveling" || s === "abroad") span.classList.add("status-traveling");
else if (s === "hospitalized" || s === "hospital") span.classList.add("status-hospitalized");
}
// ---- Load static members from backend into the list containers ----
async function loadMembers(faction) {
const url = faction === "friendly" ? "/api/friendly_members" : "/api/enemy_members";
try {
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) {
console.error("Failed to fetch members:", res.status);
return;
}
const arr = await res.json();
const container = faction === "friendly" ? friendlyContainer : enemyContainer;
const map = faction === "friendly" ? friendlyMembers : enemyMembers;
// clear
container.innerHTML = "";
map.clear();
arr.forEach(m => {
if (!m.status) m.status = "Unknown";
const card = createMemberCard(m, faction);
map.set(m.id, m);
container.appendChild(card);
});
} catch (err) {
console.error("loadMembers error", err);
}
}
// ---- Refresh status map and update DOM (does not touch static info or position) ----
async function refreshStatus(faction) {
const url = faction === "friendly" ? "/api/friendly_status" : "/api/enemy_status";
const map = faction === "friendly" ? friendlyMembers : enemyMembers;
try {
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) {
console.warn("Status fetch failed:", res.status);
return;
}
const statusData = await res.json(); // { id: { status: "..."}, ... }
Object.keys(statusData).forEach(k => {
const id = parseInt(k);
const member = map.get(id);
if (!member) return; // not loaded yet or moved elsewhere
member.status = statusData[k].status;
const span = member.domElement.querySelector(".status-text");
span.textContent = member.status;
applyStatusClass(member);
});
} catch (err) {
console.error("refreshStatus error", err);
}
}
// ---- Populate endpoints: call backend to fetch static info (FFScouter) once ----
async function populateFriendly() {
const id = toInt(document.getElementById("friendly-id").value);
if (!id) return alert("Enter friendly faction ID");
await fetch("/api/populate_friendly", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ faction_id: id, interval: 0 })
});
await loadMembers("friendly");
}
async function populateEnemy() {
const id = toInt(document.getElementById("enemy-id").value);
if (!id) return alert("Enter enemy faction ID");
await fetch("/api/populate_enemy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ faction_id: id, interval: 0 })
});
await loadMembers("enemy");
}
// ---- Start status refresh loops on backend and JS-side polling ----
let friendlyStatusIntervalHandle = null;
let enemyStatusIntervalHandle = null;
async function startFriendlyStatus() {
const id = toInt(document.getElementById("friendly-id").value);
const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10);
if (!id) return alert("Enter friendly faction ID");
await fetch("/api/start_friendly_status", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ faction_id: id, interval })
});
// clear existing
if (friendlyStatusIntervalHandle) clearInterval(friendlyStatusIntervalHandle);
friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000);
// immediate
refreshStatus("friendly");
}
async function startEnemyStatus() {
const id = toInt(document.getElementById("enemy-id").value);
const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10);
if (!id) return alert("Enter enemy faction ID");
await fetch("/api/start_enemy_status", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ faction_id: id, interval })
});
if (enemyStatusIntervalHandle) clearInterval(enemyStatusIntervalHandle);
enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000);
refreshStatus("enemy");
}
// ---- Drag & drop handling for drop-zones ----
function setupDropZones() {
const dropZones = document.querySelectorAll(".drop-zone, .member-list");
dropZones.forEach(zone => {
zone.addEventListener("dragover", (e) => {
e.preventDefault();
// peek at payload to determine if valid
const raw = e.dataTransfer.types.includes("text/plain") ? e.dataTransfer.getData("text/plain") : null;
let valid = false;
if (raw) {
try {
const p = JSON.parse(raw);
const kind = p.kind;
const zoneType = zone.classList.contains("friendly-zone") || zone.id.includes("friendly") ? "friendly" :
zone.classList.contains("enemy-zone") || zone.id.includes("enemy") ? "enemy" : null;
if (zoneType && kind === zoneType) valid = true;
} catch {}
}
zone.classList.toggle("dragover-valid", valid);
zone.classList.toggle("dragover-invalid", !valid);
});
zone.addEventListener("dragleave", () => {
zone.classList.remove("dragover-valid", "dragover-invalid");
});
zone.addEventListener("drop", (e) => {
e.preventDefault();
zone.classList.remove("dragover-valid", "dragover-invalid");
const raw = e.dataTransfer.getData("text/plain");
if (!raw) return;
let payload;
try { payload = JSON.parse(raw); } catch { return; }
const { kind, id } = payload;
const idn = parseInt(id);
if (!kind || !idn) return;
// Determine target zone type (friendly or enemy) for validation
const zoneType = zone.classList.contains("friendly-zone") || zone.id.includes("friendly") ? "friendly" :
zone.classList.contains("enemy-zone") || zone.id.includes("enemy") ? "enemy" : null;
if (zoneType !== kind) {
// invalid drop
return;
}
// Get member object from maps
const map = kind === "friendly" ? friendlyMembers : enemyMembers;
const member = map.get(idn);
if (!member) return;
// Remove from original parent if exists (so it moves)
const prev = member.domElement?.parentElement;
if (prev && prev !== zone) prev.removeChild(member.domElement);
// Append to target zone
zone.appendChild(member.domElement);
});
});
}
// ---- Allow clicking a card to log for debug (optional) ----
function attachCardClickLogging() {
document.addEventListener("click", (e) => {
const el = e.target.closest(".member-card");
if (!el) return;
const id = parseInt(el.dataset.id);
const kind = el.dataset.kind;
const map = kind === "friendly" ? friendlyMembers : enemyMembers;
const member = map.get(id);
if (member) console.log("Member clicked:", member);
});
}
// ---- Initialize UI wiring ----
function wireUp() {
document.getElementById("friendly-populate-btn").addEventListener("click", populateFriendly);
document.getElementById("enemy-populate-btn").addEventListener("click", populateEnemy);
document.getElementById("friendly-status-btn").addEventListener("click", startFriendlyStatus);
document.getElementById("enemy-status-btn").addEventListener("click", startEnemyStatus);
setupDropZones();
attachCardClickLogging();
}
// ---- On load: wire and attempt to load any existing JSON lists ----
document.addEventListener("DOMContentLoaded", async () => {
wireUp();
// attempt to load existing data (if files exist)
await loadMembers("friendly");
await loadMembers("enemy");
});