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" }, ]; const LEVELS = [ { key: "graduate", label: "Graduate" }, { key: "engineer", label: "Engineer" }, ]; let currentLevel = LEVELS[0].key; function isMobile() { return window.matchMedia("(max-width: 600px)").matches; } function renderLevelTabs() { const board = document.getElementById("kanban-board"); if (!board || document.getElementById("level-tabs")) return; const nav = document.createElement("div"); nav.id = "level-tabs"; LEVELS.forEach(({ key, label }) => { const btn = document.createElement("button"); btn.textContent = label; btn.dataset.level = key; btn.className = "level-tab" + (key === currentLevel ? " active" : ""); btn.addEventListener("click", () => { currentLevel = key; document.querySelectorAll(".level-tab").forEach(b => b.classList.toggle("active", b.dataset.level === key) ); loadBoard(); }); nav.appendChild(btn); }); board.insertAdjacentElement("beforebegin", nav); if (!document.getElementById("level-tab-styles")) { const style = document.createElement("style"); style.id = "level-tab-styles"; style.textContent = ` #level-tabs { display: flex; gap: 0.5em; margin-bottom: 1em; } .level-tab { padding: 0.4em 1.2em; border: 1px solid #ccc; background: none; cursor: pointer; border-radius: 4px; font-size: 0.95em; } .level-tab.active { background: #333; color: #fff; border-color: #333; } `; document.head.appendChild(style); } } 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 = ""; fromSelect.innerHTML = ""; toSelect.innerHTML = ""; 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)); }); itemSelect.onchange = () => { const selected = itemSelect.selectedOptions[0]; if (selected?.dataset.state) { fromSelect.value = selected.dataset.state; } }; } const moveConfirmBtn = document.getElementById("move-confirm"); if (moveConfirmBtn) { moveConfirmBtn.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); 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 = ` ${state.label} ${counts[state.key] || 0} `; 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(); } 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?group=${currentLevel}`); const items = await res.json(); updateProgress(items); renderBoard(items); populateMobileControls(items); } renderLevelTabs(); loadBoard();