// dashboard.js (Corrected Full Version) // Prevents duplicates on populate // Preserves group placements // Updates or creates cards (never recreates all) // Status refresh works correctly //----------------------------------------------------- // Maps of members (id -> member object) //----------------------------------------------------- const friendlyMembers = new Map(); const enemyMembers = new Map(); // DOM containers const friendlyContainer = document.getElementById("friendly-container"); const enemyContainer = document.getElementById("enemy-container"); function toInt(v) { const n = Number(v); return Number.isNaN(n) ? null : n; } //----------------------------------------------------- // Status CSS assignment //----------------------------------------------------- function applyStatusClass(member) { const span = member.domElement?.querySelector(".status-text"); if (!span) return; span.classList.remove("status-ok", "status-traveling", "status-hospitalized"); const s = (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"); } function statusClass(status) { if (!status) return ""; status = status.toLowerCase(); if (status.includes("okay")) return "status-ok"; if (status.includes("travel") || status.includes("abroad")) return "status-traveling"; if (status.includes("hospital")) return "status-hospitalized"; return ""; } //----------------------------------------------------- // Create a new card DOM element for a member //----------------------------------------------------- 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"; if (kind === "enemy") nameDiv.style.color = "#ff6b6b"; 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); // save reference member.domElement = card; // dragging card.addEventListener("dragstart", (e) => { e.dataTransfer.setData("text/plain", JSON.stringify({ kind, id: member.id })); card.style.opacity = "0.5"; }); card.addEventListener("dragend", () => { card.style.opacity = "1"; }); return card; } //----------------------------------------------------- // Update an existing card instead of replacing it //----------------------------------------------------- function updateMemberCard(member) { if (!member.domElement) return; const span = member.domElement.querySelector(".status-text"); if (span) { span.textContent = member.status || "Unknown"; } applyStatusClass(member); } //----------------------------------------------------- // Load members WITHOUT recreating card duplicates //----------------------------------------------------- async function loadMembers(kind) { const url = kind === "friendly" ? "/api/friendly_members" : "/api/enemy_members"; const container = kind === "friendly" ? friendlyContainer : enemyContainer; const map = kind === "friendly" ? friendlyMembers : enemyMembers; try { const res = await fetch(url, { cache: "no-store" }); if (!res.ok) { console.error("Error loading members", res.status); return; } const list = await res.json(); // Keep track of IDs received so we don't remove existing group placements const receivedIds = new Set(list.map(m => m.id)); // Update existing members or create new ones for (const m of list) { let existing = map.get(m.id); if (existing) { // update info (but keep DOM placement) existing.name = m.name; existing.level = m.level; existing.estimate = m.estimate; if (!existing.status) existing.status = "Unknown"; updateMemberCard(existing); } else { // NEW member — create card and add to list area const newMember = { id: m.id, name: m.name, level: m.level, estimate: m.estimate, status: "Unknown", domElement: null }; const card = createMemberCard(newMember, kind); map.set(m.id, newMember); // Only add to container if they are NOT already assigned to a group if (!isMemberAssigned(newMember.id)) { container.appendChild(card); } } } // Remove members that no longer exist from map (rare) for (const existingId of map.keys()) { if (!receivedIds.has(existingId)) { const member = map.get(existingId); if (member?.domElement?.parentElement) { member.domElement.parentElement.removeChild(member.domElement); } map.delete(existingId); } } setupDropZones(); } catch (err) { console.error("loadMembers error", err); } } //----------------------------------------------------- // Check if a member is placed in a group zone //----------------------------------------------------- function isMemberAssigned(id) { return document.querySelector(`.member-card[data-id="${id}"]`)?.parentElement?.classList.contains("drop-zone") || false; } //----------------------------------------------------- // Status refresh //----------------------------------------------------- async function refreshStatus(kind) { const url = kind === "friendly" ? "/api/friendly_status" : "/api/enemy_status"; const map = kind === "friendly" ? friendlyMembers : enemyMembers; try { const res = await fetch(url, { cache: "no-store" }); if (!res.ok) return; const statusData = await res.json(); for (const idStr of Object.keys(statusData)) { const id = parseInt(idStr); const member = map.get(id); if (!member) continue; member.status = statusData[idStr].status; updateMemberCard(member); } } catch (err) { console.error("refreshStatus error", err); } } //----------------------------------------------------- // Populate endpoints (immediate build + status refresh) //----------------------------------------------------- async function populateFriendly() { const id = toInt(document.getElementById("friendly-id").value); if (!id) return alert("Enter Friendly Faction ID"); // Send faction_id to FastAPI await fetch("/api/populate_friendly", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ faction_id: id }) }); // Load members but do NOT recreate duplicates await loadMembers("friendly"); // Immediately refresh status so cards show correct status await refreshStatus("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 }) }); await loadMembers("enemy"); await refreshStatus("enemy"); } //----------------------------------------------------- // Status refresh toggling //----------------------------------------------------- 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); 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); refreshStatus("friendly"); btn.textContent = "Stop Refresh"; } 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); 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); refreshStatus("enemy"); btn.textContent = "Stop Refresh"; } //----------------------------------------------------- // Drag & Drop for all zones //----------------------------------------------------- function setupDropZones() { const zones = document.querySelectorAll(".drop-zone, .member-list"); zones.forEach(zone => { if (zone.dataset._drag_listeners_attached) return; zone.addEventListener("dragover", (e) => { e.preventDefault(); const raw = e.dataTransfer.getData("text/plain"); let valid = false; try { const p = JSON.parse(raw); const zoneType = zone.classList.contains("friendly-zone") ? "friendly" : zone.classList.contains("enemy-zone") ? "enemy" : null; if (!zoneType || p.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"); let payload; try { payload = JSON.parse(e.dataTransfer.getData("text/plain")); } catch { return; } const zoneType = zone.classList.contains("friendly-zone") ? "friendly" : zone.classList.contains("enemy-zone") ? "enemy" : null; if (zoneType && payload.kind !== zoneType) return; const map = payload.kind === "friendly" ? friendlyMembers : enemyMembers; const member = map.get(payload.id); if (!member) return; // Move the card const prev = member.domElement.parentElement; if (prev !== zone) prev.removeChild(member.domElement); zone.appendChild(member.domElement); saveAssignments(); }); zone.dataset._drag_listeners_attached = "1"; }); } //----------------------------------------------------- // Save and load group assignments //----------------------------------------------------- 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)); } function loadAssignmentsFromStorage() { 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 m = friendlyMembers.get(id); if (m?.domElement) group.querySelector(".friendly-zone").appendChild(m.domElement); }); data[gid].enemy.forEach(id => { const m = enemyMembers.get(id); if (m?.domElement) group.querySelector(".enemy-zone").appendChild(m.domElement); }); }); } //----------------------------------------------------- // Wire up buttons //----------------------------------------------------- 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); document.getElementById("reset-groups-btn").addEventListener("click", () => { localStorage.removeItem("battleAssignments"); friendlyMembers.forEach(m => friendlyContainer.appendChild(m.domElement)); enemyMembers.forEach(m => enemyContainer.appendChild(m.domElement)); }); } //----------------------------------------------------- // On Load //----------------------------------------------------- document.addEventListener("DOMContentLoaded", async () => { wireUp(); await loadMembers("friendly"); await loadMembers("enemy"); loadAssignmentsFromStorage(); });