Persistent server-side group assignments
This commit is contained in:
@@ -1,52 +1,54 @@
|
||||
// dashboard.js (Corrected Full Version)
|
||||
// Prevents duplicates on populate
|
||||
// Preserves group placements
|
||||
// Updates or creates cards (never recreates all)
|
||||
// Status refresh works correctly
|
||||
// dashboard.js (Server-side assignments, polling-mode)
|
||||
// - No more localStorage
|
||||
// - Polls /api/assignments every 5s to keep all clients in sync
|
||||
// - POST /api/assign_member on drop, POST /api/remove_member_assignment when returned to list
|
||||
// - Prevents duplicates, preserves card objects and status updates
|
||||
|
||||
//-----------------------------------------------------
|
||||
// Maps of members (id -> member object)
|
||||
//-----------------------------------------------------
|
||||
const friendlyMembers = new Map();
|
||||
// ---------------------------
|
||||
// In-memory maps
|
||||
// ---------------------------
|
||||
const friendlyMembers = new Map(); // id -> { id, name, level, estimate, status, domElement }
|
||||
const enemyMembers = new Map();
|
||||
|
||||
// DOM containers
|
||||
// DOM references
|
||||
const friendlyContainer = document.getElementById("friendly-container");
|
||||
const enemyContainer = document.getElementById("enemy-container");
|
||||
|
||||
// polling interval (ms) for assignments sync
|
||||
const ASSIGNMENTS_POLL_MS = 5000;
|
||||
|
||||
// utility
|
||||
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");
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Status CSS helpers
|
||||
// ---------------------------
|
||||
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";
|
||||
const s = String(status).toLowerCase();
|
||||
if (s.includes("okay")) return "status-ok";
|
||||
if (s.includes("travel") || s.includes("abroad")) return "status-traveling";
|
||||
if (s.includes("hospital")) return "status-hospitalized";
|
||||
return "";
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
// Create a new card DOM element for a member
|
||||
//-----------------------------------------------------
|
||||
function applyStatusClass(member) {
|
||||
const span = member.domElement?.querySelector(".status-text");
|
||||
if (!span) return;
|
||||
span.className = "status-text " + statusClass(member.status);
|
||||
span.textContent = member.status || "Unknown";
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Card creation & update
|
||||
// ---------------------------
|
||||
function createMemberCard(member, kind) {
|
||||
// if domElement already exists, reuse it
|
||||
if (member.domElement) return member.domElement;
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.classList.add("member-card");
|
||||
card.setAttribute("draggable", "true");
|
||||
@@ -56,54 +58,57 @@ function createMemberCard(member, 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";
|
||||
nameDiv.style.color = kind === "friendly" ? "#8fd38f" : "#ff6b6b";
|
||||
|
||||
const statsDiv = document.createElement("div");
|
||||
statsDiv.className = "stats";
|
||||
statsDiv.innerHTML = `
|
||||
Lv: ${member.level} <br>
|
||||
Est: ${member.estimate} <br>
|
||||
Status: <span class="status-text ${statusClass(member.status)}">${member.status || "Unknown"}</span>
|
||||
Status: <span class="status-text">${member.status || "Unknown"}</span>
|
||||
`;
|
||||
|
||||
card.appendChild(nameDiv);
|
||||
card.appendChild(statsDiv);
|
||||
|
||||
// save reference
|
||||
// store reference
|
||||
member.domElement = card;
|
||||
|
||||
// dragging
|
||||
// drag handlers: payload is JSON string
|
||||
card.addEventListener("dragstart", (e) => {
|
||||
e.dataTransfer.setData("text/plain", JSON.stringify({
|
||||
kind,
|
||||
id: member.id
|
||||
}));
|
||||
e.dataTransfer.setData("text/plain", JSON.stringify({ kind, id: member.id }));
|
||||
card.style.opacity = "0.5";
|
||||
});
|
||||
card.addEventListener("dragend", () => {
|
||||
card.style.opacity = "1";
|
||||
});
|
||||
|
||||
// apply initial status class
|
||||
applyStatusClass(member);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
// Update an existing card instead of replacing it
|
||||
//-----------------------------------------------------
|
||||
function updateMemberCard(member) {
|
||||
if (!member.domElement) return;
|
||||
// name might have changed (rare)
|
||||
const nameEl = member.domElement.querySelector(".name");
|
||||
if (nameEl && nameEl.textContent !== member.name) nameEl.textContent = member.name;
|
||||
|
||||
const span = member.domElement.querySelector(".status-text");
|
||||
if (span) {
|
||||
span.textContent = member.status || "Unknown";
|
||||
// stats/status
|
||||
const statsEl = member.domElement.querySelector(".stats");
|
||||
if (statsEl) {
|
||||
const statusSpan = statsEl.querySelector(".status-text");
|
||||
if (statusSpan) {
|
||||
statusSpan.textContent = member.status || "Unknown";
|
||||
statusSpan.className = "status-text " + statusClass(member.status);
|
||||
}
|
||||
}
|
||||
applyStatusClass(member);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
// Load members WITHOUT recreating card duplicates
|
||||
//-----------------------------------------------------
|
||||
// ---------------------------
|
||||
// Load members from server
|
||||
// ---------------------------
|
||||
async function loadMembers(kind) {
|
||||
const url = kind === "friendly" ? "/api/friendly_members" : "/api/enemy_members";
|
||||
const container = kind === "friendly" ? friendlyContainer : enemyContainer;
|
||||
@@ -112,114 +117,322 @@ async function loadMembers(kind) {
|
||||
try {
|
||||
const res = await fetch(url, { cache: "no-store" });
|
||||
if (!res.ok) {
|
||||
console.error("Error loading members", res.status);
|
||||
console.error("Failed to load members:", res.status);
|
||||
return;
|
||||
}
|
||||
|
||||
const list = await res.json();
|
||||
|
||||
// Keep track of IDs received so we don't remove existing group placements
|
||||
// build set of received ids
|
||||
const receivedIds = new Set(list.map(m => m.id));
|
||||
|
||||
// Update existing members or create new ones
|
||||
// update or create
|
||||
for (const m of list) {
|
||||
let existing = map.get(m.id);
|
||||
|
||||
if (existing) {
|
||||
// update info (but keep DOM placement)
|
||||
// update fields (preserve dom & placement)
|
||||
existing.name = m.name;
|
||||
existing.level = m.level;
|
||||
existing.estimate = m.estimate;
|
||||
// keep existing.status if already set (status refreshes separately)
|
||||
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",
|
||||
status: m.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);
|
||||
}
|
||||
// create card but DO NOT automatically append here;
|
||||
// placement will be handled by loadAssignmentsFromServer()
|
||||
createMemberCard(newMember, kind);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove members that no longer exist from map (rare)
|
||||
for (const existingId of map.keys()) {
|
||||
// remove obsolete members
|
||||
for (const existingId of Array.from(map.keys())) {
|
||||
if (!receivedIds.has(existingId)) {
|
||||
const member = map.get(existingId);
|
||||
if (member?.domElement?.parentElement) {
|
||||
member.domElement.parentElement.removeChild(member.domElement);
|
||||
}
|
||||
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;
|
||||
|
||||
// ---------------------------
|
||||
// Server-side assignments API interactions
|
||||
// ---------------------------
|
||||
async function fetchAssignments() {
|
||||
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);
|
||||
const res = await fetch("/api/assignments", { cache: "no-store" });
|
||||
if (!res.ok) {
|
||||
console.warn("Failed to fetch assignments:", res.status);
|
||||
return null;
|
||||
}
|
||||
return await res.json(); // expected { "1": { friendly: [...], enemy: [...] }, ... }
|
||||
} catch (err) {
|
||||
console.error("refreshStatus error", err);
|
||||
console.error("fetchAssignments error", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
// Populate endpoints (immediate build + status refresh)
|
||||
//-----------------------------------------------------
|
||||
async function saveAssignmentToServer(groupId, kind, memberId) {
|
||||
try {
|
||||
const res = await fetch("/api/assign_member", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ group_id: groupId, kind, member_id: memberId })
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn("Failed to save assignment:", res.status);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("saveAssignmentToServer error", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAssignmentFromServer(memberId) {
|
||||
try {
|
||||
const res = await fetch("/api/remove_member_assignment", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ member_id: memberId })
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn("Failed to remove assignment:", res.status);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("removeAssignmentFromServer error", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAssignmentsOnServer() {
|
||||
try {
|
||||
const res = await fetch("/api/clear_assignments", { method: "POST" });
|
||||
if (!res.ok) console.warn("clear_assignments failed:", res.status);
|
||||
} catch (err) {
|
||||
console.error("clearAssignmentsOnServer error", err);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Apply server assignments to DOM
|
||||
// ---------------------------
|
||||
function ensureMainListContains(member, kind) {
|
||||
const container = kind === "friendly" ? friendlyContainer : enemyContainer;
|
||||
// If member is not in any group zone, ensure it's in the main list (append if not present)
|
||||
const parent = member.domElement?.parentElement;
|
||||
const isInDropZone = parent && parent.classList && parent.classList.contains("drop-zone");
|
||||
if (!isInDropZone) {
|
||||
if (member.domElement && member.domElement.parentElement !== container) {
|
||||
container.appendChild(member.domElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyAssignmentsToDOM(assignments) {
|
||||
if (!assignments) return;
|
||||
|
||||
// First, move all assigned members into their group zones
|
||||
Object.keys(assignments).forEach(groupKey => {
|
||||
const group = assignments[groupKey];
|
||||
// friendly list
|
||||
(group.friendly || []).forEach(id => {
|
||||
const member = friendlyMembers.get(id);
|
||||
if (!member) return; // not loaded yet
|
||||
const zoneId = `group-${groupKey}-friendly`;
|
||||
const zone = document.getElementById(zoneId);
|
||||
if (!zone) return;
|
||||
if (member.domElement && member.domElement.parentElement !== zone) {
|
||||
// remove from previous parent
|
||||
const prev = member.domElement.parentElement;
|
||||
if (prev) prev.removeChild(member.domElement);
|
||||
zone.appendChild(member.domElement);
|
||||
}
|
||||
});
|
||||
// enemy list
|
||||
(group.enemy || []).forEach(id => {
|
||||
const member = enemyMembers.get(id);
|
||||
if (!member) return;
|
||||
const zoneId = `group-${groupKey}-enemy`;
|
||||
const zone = document.getElementById(zoneId);
|
||||
if (!zone) return;
|
||||
if (member.domElement && member.domElement.parentElement !== zone) {
|
||||
const prev = member.domElement.parentElement;
|
||||
if (prev) prev.removeChild(member.domElement);
|
||||
zone.appendChild(member.domElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Second, ensure all unassigned members are in their main lists
|
||||
friendlyMembers.forEach(member => {
|
||||
// check if member is referenced in assignments anywhere
|
||||
const assigned = Object.values(assignments).some(g => (g.friendly || []).includes(member.id));
|
||||
if (!assigned) ensureMainListContains(member, "friendly");
|
||||
});
|
||||
enemyMembers.forEach(member => {
|
||||
const assigned = Object.values(assignments).some(g => (g.enemy || []).includes(member.id));
|
||||
if (!assigned) ensureMainListContains(member, "enemy");
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Polling assignments loop
|
||||
// ---------------------------
|
||||
let assignmentsPollHandle = null;
|
||||
let lastAssignmentsSnapshot = null;
|
||||
|
||||
async function pollAssignments() {
|
||||
const assignments = await fetchAssignments();
|
||||
if (!assignments) return;
|
||||
|
||||
// quick shallow compare to avoid full DOM updates if nothing changed
|
||||
const snapshot = JSON.stringify(assignments);
|
||||
if (snapshot !== lastAssignmentsSnapshot) {
|
||||
lastAssignmentsSnapshot = snapshot;
|
||||
applyAssignmentsToDOM(assignments);
|
||||
}
|
||||
}
|
||||
|
||||
function startAssignmentsPolling() {
|
||||
if (assignmentsPollHandle) clearInterval(assignmentsPollHandle);
|
||||
assignmentsPollHandle = setInterval(pollAssignments, ASSIGNMENTS_POLL_MS);
|
||||
// run immediately
|
||||
pollAssignments();
|
||||
}
|
||||
|
||||
function stopAssignmentsPolling() {
|
||||
if (assignmentsPollHandle) {
|
||||
clearInterval(assignmentsPollHandle);
|
||||
assignmentsPollHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Dropzone wiring (sends assignment to server)
|
||||
// ---------------------------
|
||||
function setupDropZones() {
|
||||
// attach handlers once
|
||||
const zones = document.querySelectorAll(".drop-zone, .member-list");
|
||||
zones.forEach(zone => {
|
||||
// avoid double-binding by using a marker
|
||||
if (zone.dataset._drag_listeners_attached) return;
|
||||
|
||||
zone.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
// peek at payload to check validity
|
||||
let raw = null;
|
||||
try { raw = e.dataTransfer.getData("text/plain"); } catch {}
|
||||
let valid = false;
|
||||
if (raw) {
|
||||
try {
|
||||
const p = JSON.parse(raw);
|
||||
const kind = p.kind;
|
||||
const zoneType = zone.classList.contains("friendly-zone") ? "friendly" :
|
||||
zone.classList.contains("enemy-zone") ? "enemy" :
|
||||
zone.classList.contains("member-list") || zone.id === "friendly-container" || zone.id === "enemy-container"
|
||||
? "main-list" : null;
|
||||
// main-list accepts only same-kind cards
|
||||
if (zoneType === "main-list") valid = true;
|
||||
else 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", async (e) => {
|
||||
e.preventDefault();
|
||||
zone.classList.remove("dragover-valid", "dragover-invalid");
|
||||
|
||||
let payload;
|
||||
try { payload = JSON.parse(e.dataTransfer.getData("text/plain")); } catch { return; }
|
||||
if (!payload || !payload.kind || !payload.id) return;
|
||||
|
||||
const kind = payload.kind;
|
||||
const id = payload.id;
|
||||
|
||||
const zoneIsFriendly = zone.classList.contains("friendly-zone");
|
||||
const zoneIsEnemy = zone.classList.contains("enemy-zone");
|
||||
const zoneIsMainList = zone.classList.contains("member-list") || zone.id === "friendly-container" || zone.id === "enemy-container";
|
||||
|
||||
// Validate mix of kinds
|
||||
if (zoneIsFriendly && kind !== "friendly") {
|
||||
// invalid drop
|
||||
alert("Cannot drop enemy into friendly zone.");
|
||||
return;
|
||||
}
|
||||
if (zoneIsEnemy && kind !== "enemy") {
|
||||
alert("Cannot drop friendly into enemy zone.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If dropping into a group zone -> save assignment to server
|
||||
if (zoneIsFriendly || zoneIsEnemy) {
|
||||
// zone id format expected: group-<groupKey>-friendly or group-<groupKey>-enemy
|
||||
const parts = zone.id.split("-");
|
||||
// expected ["group", "<groupKey>", "friendly"]
|
||||
if (parts.length >= 3) {
|
||||
const groupKey = parts[1];
|
||||
await saveAssignmentToServer(groupKey, kind, id);
|
||||
// Move in DOM (we still move locally so UX is instant)
|
||||
const map = kind === "friendly" ? friendlyMembers : enemyMembers;
|
||||
const member = map.get(id);
|
||||
if (member && member.domElement && member.domElement.parentElement !== zone) {
|
||||
const prev = member.domElement.parentElement;
|
||||
if (prev) prev.removeChild(member.domElement);
|
||||
zone.appendChild(member.domElement);
|
||||
}
|
||||
} else {
|
||||
console.warn("Unexpected zone id format", zone.id);
|
||||
}
|
||||
} else if (zoneIsMainList) {
|
||||
// Dropped back into main list - remove assignment on server
|
||||
await removeAssignmentFromServer(id);
|
||||
// Move card back to main list container
|
||||
const map = kind === "friendly" ? friendlyMembers : enemyMembers;
|
||||
const member = map.get(id);
|
||||
const container = kind === "friendly" ? friendlyContainer : enemyContainer;
|
||||
if (member && member.domElement && member.domElement.parentElement !== container) {
|
||||
const prev = member.domElement.parentElement;
|
||||
if (prev) prev.removeChild(member.domElement);
|
||||
container.appendChild(member.domElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
zone.dataset._drag_listeners_attached = "1";
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Populate & Status functions (unchanged behavior)
|
||||
// ---------------------------
|
||||
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 })
|
||||
body: JSON.stringify({ faction_id: id, interval: 0 })
|
||||
});
|
||||
|
||||
// Load members but do NOT recreate duplicates
|
||||
// reload members and assignments
|
||||
await loadMembers("friendly");
|
||||
|
||||
// Immediately refresh status so cards show correct status
|
||||
await loadMembers("enemy"); // in case population changed cross lists
|
||||
await pollAssignments();
|
||||
await refreshStatus("friendly");
|
||||
}
|
||||
|
||||
@@ -230,22 +443,41 @@ async function populateEnemy() {
|
||||
await fetch("/api/populate_enemy", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ faction_id: id })
|
||||
body: JSON.stringify({ faction_id: id, interval: 0 })
|
||||
});
|
||||
|
||||
await loadMembers("enemy");
|
||||
await loadMembers("friendly");
|
||||
await pollAssignments();
|
||||
await refreshStatus("enemy");
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
// Status refresh toggling
|
||||
//-----------------------------------------------------
|
||||
// status refresh (pulls from server status json)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Start/Stop status loops (buttons toggle)
|
||||
let friendlyStatusIntervalHandle = null;
|
||||
let enemyStatusIntervalHandle = null;
|
||||
|
||||
async function toggleFriendlyStatus() {
|
||||
const btn = document.getElementById("friendly-status-btn");
|
||||
|
||||
if (friendlyStatusIntervalHandle) {
|
||||
clearInterval(friendlyStatusIntervalHandle);
|
||||
friendlyStatusIntervalHandle = null;
|
||||
@@ -269,7 +501,6 @@ async function toggleFriendlyStatus() {
|
||||
|
||||
async function toggleEnemyStatus() {
|
||||
const btn = document.getElementById("enemy-status-btn");
|
||||
|
||||
if (enemyStatusIntervalHandle) {
|
||||
clearInterval(enemyStatusIntervalHandle);
|
||||
enemyStatusIntervalHandle = null;
|
||||
@@ -291,136 +522,46 @@ async function toggleEnemyStatus() {
|
||||
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";
|
||||
});
|
||||
// ---------------------------
|
||||
// Reset groups (server-side)
|
||||
// ---------------------------
|
||||
async function resetGroups() {
|
||||
if (!confirm("Reset all group assignments on the server?")) return;
|
||||
await clearAssignmentsOnServer();
|
||||
// reload assignments & UI
|
||||
await pollAssignments();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
// 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
|
||||
//-----------------------------------------------------
|
||||
// ---------------------------
|
||||
// Wire up buttons & init
|
||||
// ---------------------------
|
||||
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);
|
||||
const resetBtn = document.getElementById("reset-groups-btn");
|
||||
if (resetBtn) resetBtn.addEventListener("click", resetGroups);
|
||||
|
||||
document.getElementById("reset-groups-btn").addEventListener("click", () => {
|
||||
localStorage.removeItem("battleAssignments");
|
||||
|
||||
friendlyMembers.forEach(m => friendlyContainer.appendChild(m.domElement));
|
||||
enemyMembers.forEach(m => enemyContainer.appendChild(m.domElement));
|
||||
});
|
||||
setupDropZones();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
// On Load
|
||||
//-----------------------------------------------------
|
||||
// ---------------------------
|
||||
// Initial load
|
||||
// ---------------------------
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
wireUp();
|
||||
|
||||
// load members first
|
||||
await loadMembers("friendly");
|
||||
await loadMembers("enemy");
|
||||
|
||||
loadAssignmentsFromStorage();
|
||||
// load assignments and apply them
|
||||
await pollAssignments();
|
||||
startAssignmentsPolling();
|
||||
|
||||
// kick off status polling for initial UI (but status loops are triggered by Start Refresh buttons)
|
||||
// immediate status pull so cards show current status
|
||||
await refreshStatus("friendly");
|
||||
await refreshStatus("enemy");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user