// dashboard.js // Full functionality: populate, start/stop status loops, load members, drag & drop between lists and groups, // drop zones accept only friendly/enemy respectively, status color updates, localStorage persistence // ---- 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) { const card = document.createElement("div"); card.classList.add("member-card"); card.setAttribute("draggable", "true"); card.dataset.id = member.id; card.dataset.kind = kind; const nameDiv = document.createElement("div"); nameDiv.className = "name"; nameDiv.textContent = member.name; if (kind === "friendly") nameDiv.style.color = "#8fd38f"; // green if (kind === "enemy") nameDiv.style.color = "#ff6b6b"; // red const statsDiv = document.createElement("div"); statsDiv.className = "stats"; statsDiv.innerHTML = `Lv: ${member.level}
Est: ${member.estimate}
Status: ${member.status || "Unknown"}`; card.appendChild(nameDiv); card.appendChild(statsDiv); member.domElement = card; card.addEventListener("dragstart", (e) => { const payload = JSON.stringify({ kind, id: member.id }); e.dataTransfer.setData("text/plain", payload); card.style.opacity = "0.5"; }); card.addEventListener("dragend", () => { card.style.opacity = "1"; }); 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 ---- 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) return console.error("Failed to fetch members:", res.status); const arr = await res.json(); const container = faction === "friendly" ? friendlyContainer : enemyContainer; const map = faction === "friendly" ? friendlyMembers : enemyMembers; 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 ---- 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) return; const statusData = await res.json(); Object.keys(statusData).forEach(k => { const id = parseInt(k); const member = map.get(id); if (!member) return; 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 ---- 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/Stop status refresh loops ---- let friendlyStatusIntervalHandle = null; let enemyStatusIntervalHandle = null; async function toggleFriendlyStatus() { const btn = document.getElementById("friendly-status-btn"); if (friendlyStatusIntervalHandle) { clearInterval(friendlyStatusIntervalHandle); friendlyStatusIntervalHandle = null; btn.textContent = "Start Refresh"; return; } 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 }) }); friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000); btn.textContent = "Stop Refresh"; refreshStatus("friendly"); } async function toggleEnemyStatus() { const btn = document.getElementById("enemy-status-btn"); if (enemyStatusIntervalHandle) { clearInterval(enemyStatusIntervalHandle); enemyStatusIntervalHandle = null; btn.textContent = "Start Refresh"; return; } 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 }) }); enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000); btn.textContent = "Stop Refresh"; 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(); 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; 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) return; const map = kind === "friendly" ? friendlyMembers : enemyMembers; const member = map.get(idn); if (!member) return; const prev = member.domElement?.parentElement; if (prev && prev !== zone) prev.removeChild(member.domElement); zone.appendChild(member.domElement); // persist assignment in localStorage saveAssignments(); }); }); } // ---- Persist card assignments to localStorage ---- function saveAssignments() { const groups = document.querySelectorAll(".group"); const data = {}; groups.forEach(g => { const gid = g.dataset.id; data[gid] = { friendly: [], enemy: [] }; g.querySelectorAll(".friendly-zone .member-card").forEach(c => data[gid].friendly.push(parseInt(c.dataset.id))); g.querySelectorAll(".enemy-zone .member-card").forEach(c => data[gid].enemy.push(parseInt(c.dataset.id))); }); localStorage.setItem("battleAssignments", JSON.stringify(data)); } // ---- Restore card assignments from localStorage ---- function loadAssignments() { const data = JSON.parse(localStorage.getItem("battleAssignments") || "{}"); Object.keys(data).forEach(gid => { const group = document.querySelector(`.group[data-id="${gid}"]`); if (!group) return; data[gid].friendly.forEach(id => { const member = friendlyMembers.get(id); if (member) group.querySelector(".friendly-zone").appendChild(member.domElement); }); data[gid].enemy.forEach(id => { const member = enemyMembers.get(id); if (member) group.querySelector(".enemy-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", toggleFriendlyStatus); document.getElementById("enemy-status-btn").addEventListener("click", toggleEnemyStatus); setupDropZones(); attachCardClickLogging(); } // ---- On load: wire and attempt to load any existing JSON lists ---- document.addEventListener("DOMContentLoaded", async () => { wireUp(); await loadMembers("friendly"); await loadMembers("enemy"); loadAssignments(); // restore positions after load });