Files
org_web/assets/scripts/competency-status-board.js
2026-02-22 22:09:44 +00:00

183 lines
4.7 KiB
JavaScript
Executable File

const STATES = [
{ key: "completed", label: "Completed" },
{ key: "manager_review", label: "Manager Review" },
{ key: "in_progress", label: "In Progress" },
{ key: "not_started", label: "Not Started" },
{ key: "comments", label: "Comments" },
];
function isMobile() {
return window.matchMedia("(max-width: 600px)").matches;
}
function populateMobileControls(items) {
if (!isMobile()) return;
const panel = document.getElementById("mobile-move-panel");
const itemSelect = document.getElementById("move-item");
const fromSelect = document.getElementById("move-from");
const toSelect = document.getElementById("move-to");
itemSelect.innerHTML = "<option value=''>Select competency</option>";
fromSelect.innerHTML = "<option value=''>From</option>";
toSelect.innerHTML = "<option value=''>To</option>";
items.forEach((item) => {
const opt = document.createElement("option");
opt.value = item.id;
opt.textContent = item.title;
opt.dataset.state = item.state;
itemSelect.appendChild(opt);
});
STATES.forEach((s) => {
fromSelect.appendChild(new Option(s.label, s.key));
toSelect.appendChild(new Option(s.label, s.key));
});
// Auto-fill "from" when item selected
itemSelect.onchange = () => {
const selected = itemSelect.selectedOptions[0];
if (selected?.dataset.state) {
fromSelect.value = selected.dataset.state;
}
};
}
document.getElementById("move-confirm").addEventListener("click", async () => {
const itemId = document.getElementById("move-item").value;
const from = document.getElementById("move-from").value;
const to = document.getElementById("move-to").value;
if (!itemId || !to) {
alert("Select an item and target column");
return;
}
if (from === to) {
alert("Item is already in that column");
return;
}
const res = await fetch(`/api/competencies/items/${itemId}/state`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state: to }),
});
if (res.ok) {
loadBoard();
} else {
alert("Failed to move competency");
}
});
let draggedItemId = null;
function countByState(items) {
return items.reduce((acc, item) => {
acc[item.state] = (acc[item.state] || 0) + 1;
return acc;
}, {});
}
async function updateProgress(items) {
const total = items.length;
const completed = items.filter(i => i.state === "completed").length;
const percent = total === 0 ? 0 : Math.round((completed / total) * 100);
console.log(`Progress: ${percent}% (${completed}/${total})`);
const fill = document.getElementById("progress-fill");
const label = document.getElementById("progress-label");
if (fill) fill.style.width = `${percent}%`;
if (label) label.textContent = `${percent}% completed`;
}
function createCard(item) {
const card = document.createElement("div");
card.className = "kanban-card";
card.draggable = true;
card.dataset.id = item.id;
card.textContent = item.title;
card.addEventListener("dragstart", () => {
draggedItemId = item.id;
card.classList.add("dragging");
});
card.addEventListener("dragend", () => {
draggedItemId = null;
card.classList.remove("dragging");
});
return card;
}
function createColumn(state, items, counts) {
const col = document.createElement("div");
col.className = "kanban-column";
col.dataset.state = state.key;
const header = document.createElement("h3");
header.innerHTML = `
<span class="kanban-title">${state.label}</span>
<span class="kanban-count">${counts[state.key] || 0}</span>
`;
const list = document.createElement("div");
list.className = "kanban-list";
list.addEventListener("dragover", (e) => e.preventDefault());
list.addEventListener("drop", async () => {
if (!draggedItemId) return;
const res = await fetch(`/api/competencies/items/${draggedItemId}/state`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state: state.key }),
});
if (res.ok) {
loadBoard(); // simple & safe re-render
} else {
alert("Failed to update state");
}
});
items
.filter((i) => i.state === state.key)
.forEach((i) => list.appendChild(createCard(i)));
col.appendChild(header);
col.appendChild(list);
return col;
}
function renderBoard(items) {
const board = document.getElementById("kanban-board");
if (!board) return;
board.innerHTML = "";
const counts = countByState(items);
STATES.forEach((state) => {
board.appendChild(createColumn(state, items, counts));
});
}
async function loadBoard() {
const res = await fetch("/api/competencies/items");
const items = await res.json();
updateProgress(items);
renderBoard(items);
populateMobileControls(items);
}
loadBoard();