243 lines
6.3 KiB
JavaScript
Executable File
243 lines
6.3 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" },
|
|
];
|
|
|
|
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 = "<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));
|
|
});
|
|
|
|
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 = `
|
|
<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();
|
|
} 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);
|
|
}
|
|
|
|
if (document.getElementById("kanban-board")) {
|
|
renderLevelTabs();
|
|
loadBoard();
|
|
}
|