import { GraduationChange, fetchGraduationChanges, submitGraduationChange, deleteGraduationChange, } from "./graduation-ENV.ts"; import { fetchStudents } from "./student-ENV.ts"; import { CourseLocalStorage, DATETIME_FMT, getName, splitmix32 } from "./utils-ENV.js"; let name; function toggle(btn, others) { if (btn.innerHTML === "X") { btn.innerHTML = "o"; } else { btn.innerHTML = "X"; for (const o of others) { o.innerHTML = "o"; } } const submitBtn = document.getElementById("submit-mass-graduation"); submitBtn.style.display = "inline"; submitBtn.disabled = false; document.getElementById("submit-mass-graduation-state").innerHTML = ""; } // https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array function shuffleArray(array) { var seed = JSON.parse(CourseLocalStorage.getItem("bycs-sol-mandatory-seed")); if (seed === null || Date(seed.endOfLife) < Date()) { seed = { seed: (Math.random() * 2 ** 32) >>> 0, // Keep the same seed for 45 minutes endOfLife: new Date(new Date().getTime() + 45 * 60000), }; CourseLocalStorage.setItem("bycs-sol-mandatory-seed", JSON.stringify(seed)); } const rng = splitmix32(seed.seed); for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } function chooseMandatory(graduations) { graduations.sort((g1, g2) => g1.mandatory - g2.mandatory); // Group the graduations according to the amount of mandatory changes const mandatoryCandidateGroups = []; let curGroup = []; let mandatory = graduations[0].mandatory; for (const g of graduations) { if (g.mandatory === mandatory) { curGroup.push(g); } else { // Start a new group mandatoryCandidateGroups.push(curGroup); curGroup = [g]; mandatory = g.mandatory; } } // push the last group mandatoryCandidateGroups.push(curGroup); // Choose from the group with the smallest amount of mandatory changes const chosen = []; for (const candidateGroup of mandatoryCandidateGroups) { if (chosen.length === 5) break; // Get a random list of possible indexes const candidatesIdxs = shuffleArray([...Array(candidateGroup.length).keys()]); chosen.push(...candidatesIdxs.slice(0, 5 - chosen.length).map((i) => candidateGroup[i])); } return chosen; } async function loadGroup() { const students = await fetchStudents(); const graduationPromises = []; document.getElementById("group-content").style.display = "block"; const mul = document.getElementById("group-mandatory-members"); const ul = document.getElementById("group-members"); mul.innerHTML = ""; ul.innerHTML = ""; const listEntries = []; for (const student of students) { const graduationPromise = student.getGraduation(); graduationPromises.push(graduationPromise); const li = document.createElement("li"); li.student = student; listEntries.push(li); const i = document.createElement("img"); i.classList.add("group-member-icon"); graduationPromise.then((graduation) => { i.src = graduation.getIcon(); }); li.appendChild(i); } const graduations = await Promise.all(graduationPromises); const chosen = chooseMandatory(graduations); for (const li of listEntries) { const student = li.student; const n = document.createElement("span"); n.classList.add("group-member-name"); n.innerHTML = `${student.name}`; li.appendChild(n); const btns = []; for (const col of ["green", "orange", "red"]) { const b = document.createElement("button"); btns.push(b); b.classList.add(`group-member-${col}`); b.innerHTML = "o"; li.appendChild(b); } for (const b of btns) { b.addEventListener("click", () => toggle( b, btns.filter((o) => o !== b), ), ); } if (chosen.includes(student.graduation)) { li.classList.add("group-mandatory-member"); mul.appendChild(li); } else { li.classList.add("group-member"); ul.appendChild(li); } } } function submitMassGraduation() { const state = document.getElementById("submit-mass-graduation-state"); state.innerHTML = ""; state.classList.add("submit-mass-graduation-inprogress"); state.style.display = "unset"; // TODO(muhq): await graduation promises const promises = []; for (const li of document.querySelectorAll("li.group-member, li.group-mandatory-member")) { const student = li.student; const mandatory = li.classList.contains("group-mandatory-member"); if (li.querySelector(".group-member-green").innerHTML === "X") { promises.push( submitGraduationChange( GraduationChange.newGreenPointChange(mandatory, "", name), student.graduation, ), ); } if (li.querySelector(".group-member-orange").innerHTML === "X") { promises.push( submitGraduationChange( GraduationChange.newOrangePointChange(mandatory, "", name), student.graduation, ), ); } if (li.querySelector(".group-member-red").innerHTML === "X") { promises.push( submitGraduationChange( GraduationChange.newRedPointChange(mandatory, "", name), student.graduation, ), ); } } Promise.all(promises) .then(() => { state.style.display = "inline"; state.innerHTML = "✓"; state.style.color = "green"; }) .catch((error) => { state.style.display = "inline"; state.innerHTML = "X"; state.style.color = "red"; // re-enable submit button document.getElementById("submit-mass-graduation").disabled = false; console.log(error); }) .finally(() => { state.classList.remove("submit-mass-graduation-inprogress"); }); } async function loadChanges() { // TODO(muhq): deduplicate code with cockpit const changesTbl = document.getElementById("graduation-changes"); const changes = await fetchGraduationChanges(); for (const change of changes) { if (change.coach !== name) continue; const changeTr = document.createElement("tr"); switch (change.change) { case "Grün": changeTr.style.background = "rgb(from green r g b / 50%)"; break; case "Rot": changeTr.style.background = "rgb(from red r g b / 50%)"; break; case "Orange": changeTr.style.background = "rgb(from orange r g b / 50%)"; break; } const tds = []; const addTd = (s) => { const changeTd = document.createElement("td"); changeTd.innerHTML = s; changeTr.appendChild(changeTd); tds.push(changeTd); }; addTd(change.change); addTd(DATETIME_FMT.format(change.date)); addTd(change.reason); let ctxFmt = ""; switch (change.change) { case "CoachZuweisung": ctxFmt = change.context.zuweisungen[student.id]; break; case "LevelÄnderung": ctxFmt = LEVEL_NAMES[change.context.level]; break; case "Grün": case "Orange": case "Rot": ctxFmt = change.context.mandatory ? "erforderlich" : ""; break; default: ctxFmt = JSON.stringify(change.context); break; } addTd(ctxFmt); const delBtn = document.createElement("button"); delBtn.innerHTML = "Löschen"; delBtn.addEventListener("click", () => { const state = document.createElement("span"); state.classList.add("graduation-delete-state"); const inprogress = document.createElement("span"); inprogress.classList.add("graduation-delete-inprogress"); state.appendChild(inprogress); delBtn.replaceWith(state); deleteGraduationChange(graduation, change) .then(() => { updateStudentInfo(); state.style.display = "inline"; state.innerHTML = "✓"; for (const td of tds) { td.style["text-decoration"] = "line-through"; } }) .catch(() => { state.style.display = "inline"; state.innerHTML = "X"; }) .finally(() => { inprogress.remove(); }); }); changeTr.appendChild(delBtn); changesTbl.appendChild(changeTr); } } async function init() { document.getElementById("submit-mass-graduation").addEventListener("click", submitMassGraduation); name = getName(); loadGroup(); loadChanges(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); }