From 1a291fb37fdf0e5a1b2a54825e5af7c5e986f00f Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Sun, 12 Jul 2026 09:33:58 +0200 Subject: implment observation period handling --- cockpit/index.html | 8 ++ cockpit/js/cockpit.js | 84 ++++++++++++++++--- js/config.js | 7 ++ js/database.js | 10 ++- js/graduation-common.js | 161 +++++++++++++++++++++++++++++------- js/mock.js | 6 +- js/student-common.js | 22 ++++- mass-graducation/index.css | 21 +++++ mass-graducation/index.html | 19 ++++- mass-graducation/package-lock.json | 4 +- mass-graducation/src/main.js | 163 +++++++++++++++++++++++++++++++++---- 11 files changed, 436 insertions(+), 69 deletions(-) diff --git a/cockpit/index.html b/cockpit/index.html index dab2c89..cea2c60 100644 --- a/cockpit/index.html +++ b/cockpit/index.html @@ -123,6 +123,14 @@ + +
diff --git a/cockpit/js/cockpit.js b/cockpit/js/cockpit.js index c0b20c2..15219cd 100644 --- a/cockpit/js/cockpit.js +++ b/cockpit/js/cockpit.js @@ -4,6 +4,7 @@ // } from "../../js/mock.js"; import { + OBSERVATION_TYPES, LEVEL_NAMES, GraduationChange, fetchGraduation, @@ -11,7 +12,7 @@ import { submitGraduationChange } from "../../js/graduation-ENV.js"; -import { fetchStudent, fetchStudents } from "../../js/student-ENV.js"; +import { fetchStudent, fetchStudents, Student } from "../../js/student-ENV.js"; import { isTeacher as getIsTeacher, getName, @@ -32,11 +33,6 @@ async function loadStudent(studentToLoad) { updateStudentGraduation(); } -async function loadNewStudent(id) { - student = await fetchStudent(id); - await loadStudent(student); -} - function initStudentSelection() { initSelection( "student-selection", @@ -174,11 +170,13 @@ function renderObservationStatus(graduation, node) { node.innerHTML = ""; let status; - if (isTeacher || graduation.isObserved()) { + if (isTeacher || graduation.isObserved() || graduation.observationRequested) { status = document.createElement("span"); status.classList.add("student-observation-status"); if (graduation.isObserved()) { status.innerHTML = '👁 Ich befinde mich in der Beobachtungsphase zum Levelaufstieg.'; + } else if (graduation.observationRequested) { + status.innerHTML = 'Beobachtungsphase noch nicht begonnen.'; } else { status.innerHTML = "Nicht in Beobachtung."; } @@ -274,6 +272,69 @@ async function updateStudentGraduation() { } changesTbl.appendChild(changeTr); } + + const observtionsTbl = document.getElementById("graduation-current-observation-period"); + observtionsTbl.innerHTML = ""; + if (graduation.observation) { + const sums = graduation.observation.getObservationsSums(); + for (let i = 0; i < OBSERVATION_TYPES.length; i++) { + const tr = document.createElement("tr"); + const typeTd = document.createElement("td"); + typeTd.innerHTML = OBSERVATION_TYPES[i]; + tr.appendChild(typeTd); + + const yesTd = document.createElement("td"); + yesTd.innerHTML = sums[i].yes; + tr.appendChild(yesTd); + + const noTd = document.createElement("td"); + noTd.innerHTML = sums[i].no; + tr.appendChild(noTd); + + observtionsTbl.appendChild(tr); + } + + const endDiv = document.getElementById("graduation-current-observation-period-end"); + if (!graduation.observation.ready()) { + endDiv.style.display = "none"; + return; + } + + endDiv.style.display = "block"; + + const reason = document.getElementById("graduation-current-observation-period-end-reason").value; + const successBtn = document.getElementById("graduation-current-observation-period-end-success"); + successBtn.addEventListener("click", () => { + submitGraduationChange(graduation.student, GraduationChange.newObservationEnd(reason, true)); + }); + const failureBtn = document.getElementById("graduation-current-observation-period-end-failure"); + failureBtn.addEventListener("click", () => { + submitGraduationChange(graduation.student, GraduationChange.newObservationEnd(reason, false)); + }); + } + + const pastObservtionsTbl = document.getElementById("graduation-observation-periods"); + pastObservtionsTbl.innerHTML = ""; + if (graduation.pastObservations.length > 0) { + for (const period of graduation.pastObservations) { + const tr = document.createElement("tr"); + const periodTd = document.createElement("td"); + const startDate = new Intl.DateTimeFormat("de-DE", { month: "numeric", day: "numeric" }).format(period.start); + const endDate = new Intl.DateTimeFormat("de-DE", { month: "numeric", day: "numeric", year: "numeric" }).format(period.end); + periodTd.innerHTML = `${startDate} - ${endDate}`; + tr.appendChild(periodTd); + + const successTd = document.createElement("td"); + if (period.success) { + successTd.innerHTML = "Graduierung erfolgreich" + } else { + successTd.innerHTML = "Graduierung nicht erfolgreich" + } + tr.appendChild(successTd); + + pastObservtionsTbl.appendChild(tr); + } + } } async function initCockpit() { @@ -281,15 +342,18 @@ async function initCockpit() { initStudentSelection(); const params = new URLSearchParams(location.search); const userId = params.get("userToLoad"); - if (userId) - loadNewStudent(userId); + if (userId) { + student = await fetchStudent(userId); + await loadStudent(student); + } } else { // Remove all elements only suitable for teachers for(const elem of document.querySelectorAll("[data-availability=if-lehrer]")) { elem.remove(); } - loadNewStudent(getUserId()); + const student = await Student.fetchById(getUserId()); + await loadStudent(student); } } diff --git a/js/config.js b/js/config.js index f07c5ec..5fc456c 100644 --- a/js/config.js +++ b/js/config.js @@ -3,6 +3,12 @@ function getCourseId() { return courseId; } +// TODO(muhq): retrieve when loading activities/DBs +const cockpidId = 93017047; +function getCockpitId() { + return cockpidId; +} + function getCourseParticipantsUrl() { return `https://lernplattform.bycs.de/user/index.php?id=${courseId}`; } @@ -17,6 +23,7 @@ async function getUserIconUrl(userId) { export { getCourseId, + getCockpitId, getUserIconUrl, getCourseParticipantsUrl }; diff --git a/js/database.js b/js/database.js index 7fd3d47..73c2d17 100644 --- a/js/database.js +++ b/js/database.js @@ -29,7 +29,7 @@ async function populateStudentDbs() { const courseDoc = new DOMParser().parseFromString(await courseRes.text(), 'text/html'); let dbs = new Map(); - for (const link of courseDoc.querySelectorAll("div.activityname > a.aalink")) { + for (const link of courseDoc.querySelectorAll("div.activityname a.aalink")) { const match = link.textContent.match(/(\d+)(Shadow|Public|Submit)/); if (!match) continue; const studentId = match[1]; @@ -82,7 +82,13 @@ async function loadFromDatabaseActivity(id, params) { let entry = {}; row.querySelectorAll('.db-cell').forEach(field => { const key = field.dataset.key; - const value = field.innerText.trim(); + let value; + // Moodle includes the full date in the title attribute of the included span + if (key === "date") { + value = field.querySelector("span").title; + } else { + value = field.innerText.trim(); + } entry[key] = value; }); data.push(entry); diff --git a/js/graduation-common.js b/js/graduation-common.js index 3ab1fdd..1c68f38 100644 --- a/js/graduation-common.js +++ b/js/graduation-common.js @@ -22,11 +22,15 @@ class GraduationChange { publicDBFields () { return { "change": this.change, + "date": this.date, }; } dbFields () { return { + // We include the date in the json because Moodle reports the timeadded + // locale dependent. + "date": this.date, "change": this.change, "reason": this.reason, "context": this.context, @@ -34,9 +38,25 @@ class GraduationChange { } static fromDBEntry(e) { - const {date, coach, json} = e; + const {coach, json} = e; const change = JSON.parse(json); - return new GraduationChange(new Date(date), change.change, change.reason, coach, change.context); + return new GraduationChange(new Date(change.date), change.change, change.reason, coach, change.context); + } + + static newObservationRequest() { + return new GraduationChange(new Date(), "ObservationRequest", "", "", ""); + } + + static newObservationStart() { + return new GraduationChange(new Date(), "ObservationStart", "", "", ""); + } + + static newObservationEnd(reason, success) { + return new GraduationChange(new Date(), "ObservationEnd", reason, "", JSON.stringify(success)); + } + + static newObservationChange(observation) { + return new GraduationChange(observation.date, "Observation", "", "", JSON.stringify(observation.status)); } static newGreenPointChange(reason, coach) { @@ -53,14 +73,16 @@ class GraduationChange { } class Graduation { - constructor(student, green, orange, red, level, stickers, observation, changes) { + constructor(student, green, orange, red, level, stickers, observationRequested, observation, pastObservations, changes) { this.student = student; this.green = green; this.orange = orange; this.red = red; this.level = level; this.stickers = stickers; + this.observationRequested = observationRequested; this.observation = observation; + this.pastObservations = pastObservations; this.changes = changes; student.graduation = this; @@ -71,41 +93,66 @@ class Graduation { let orange = 0; let red = 0; let level = 1; + let openObservationRequest = null; let observationPeriod = null; let pastObservations = []; let stickers = []; + + // The parsing logic assumes that changes are ordered chronologically + changes.sort((a, b) => a.date - b.date); + for (const change of changes) { switch (change.change) { case "Grün": green++; break; + case "Orange": orange++; break; + case "Rot": red++; break; + + case "ObservationRequest": + openObservationRequest = change.date; + break; + case "ObservationStart": - observationPeriod = new ObservationPeriod(change.date, null, []); + console.assert(openObservationRequest, "No request for observation start"); + observationPeriod = new ObservationPeriod(change.date); + openObservationRequest = null; break; + case "ObservationEnd": - observationPeriod.end = change.date; + console.assert(observationPeriod, "No active observation period"); + observationPeriod.finishPeriod(change.context.success); + if (change.context.success) { + level++; + } pastObservations.push(observationPeriod); observationPeriod = null; break; + case "Observation": - const observation = Observation.fromContext(change.context); - if (observationPeriod) { - observationPeriod.addObservation(observation); - } else { - alert(`No active observation period`); - } + console.assert(observationPeriod, "No active observation period"); + const observation = Observation.fromChange(change); + observationPeriod.addObservation(observation); + break; + + case "LevelChange": + level = change.context.level; break; + default: alert(`Unexpected change: ${change}`); } } - return new Graduation(student, green, orange, red, level, stickers, observationPeriod, changes); + return new Graduation(student, green, orange, red, level, + stickers, + openObservationRequest, observationPeriod, pastObservations, + changes); } addChange(change) { @@ -120,7 +167,36 @@ class Graduation { * Request observation */ async requestObservation() { - await addEntryToStudentDatabase(this.student, "submit", {change: "ObservationRequest"}); + const request = GraduationChange.newObservationRequest(); + this.observationRequested = request.date; + await addEntryToStudentDatabase(this.student, "submit", JSON.stringify(request.publicDBFields())); + } + + /** + * Start an observation + */ + async startObservation() { + if (!this.observationRequested) { + console.assert(this.observationRequested, "No observation requested"); + return; + } + this.observationRequested = null; + this.observation = new ObservationPeriod(new Date()); + return submitGraduationChange(this.student, GraduationChange.newObservationStart()); + } + + /** + * End an observation + */ + async endObservation(reason, success) { + if (!this.observation) { + console.assert(this.observation, "No active observation"); + return; + } + this.observation.end = new Date(); + this.pastObservations.push(this.observation); + this.observation = null; + return submitGraduationChange(this.student, GraduationChange.newObservationEnd(reason, success)); } /** @@ -147,10 +223,14 @@ class Graduation { } class ObservationPeriod { - constructor(start, end, observations) { + constructor(start) { this.start = start; - this.end = end; - this.observations = observations; + this.observations = []; + } + + finishPeriod(success) { + this.end = new Date(); + this.success = success; } ended() { @@ -158,18 +238,28 @@ class ObservationPeriod { } ready() { - // TODO(muhq): add time contraint - return this.observations > 10; + if (this.observations.length < 10) + return false; + + // at least 10 observation on different days + let days = new Map(); + for (const observation of this.observations) { + const d = observation.date; + days.set(`${d.getFullYear()}${d.getMonth()}${d.getDate()}`, true); + } + return days.size >= 10; } getObservationsSums() { - let sums = Array(OBSERVATION_TYPES.length).fill({"yes":0, "no":0}); + let sums = Array(OBSERVATION_TYPES.length).fill().map(() => ({"yes":0, "no":0})); for (const observation of this.observations) { for (let i = 0; i < sums.length; i++) { - if (observation.status[i]) - sums[i].yes++; - else - sums[i].no++; + if (observation.status[i]) { + if (observation.status[i] === "yes") + sums[i].yes++; + else + sums[i].no++; + } } } return sums; @@ -192,10 +282,14 @@ const OBSERVATION_TYPES = [ class Observation { constructor(date, status) { - console.assert(status.length != OBSERVATION_TYPES.length, "invalid observation status"); + console.assert(status.length == OBSERVATION_TYPES.length, "invalid observation status"); this.date = date; this.status = status; } + + static fromChange(change) { + return new Observation(change.date, JSON.parse(change.context)) + } } async function fetchGraduationChangesFromDb(student, type) { @@ -206,15 +300,23 @@ async function fetchGraduationChangesFromDb(student, type) { const gradChange = GraduationChange.fromDBEntry(e); changes.push(gradChange); } - return changes; + return changes.flat(); } async function fetchGraduationChanges(student) { - return fetchGraduationChangesFromDb(student, "shadow"); + let changes = await Promise.all([ + fetchGraduationChangesFromDb(student, "shadow"), + fetchGraduationChangesFromDb(student, "submit"), + ]); + return changes.flat(); } async function fetchPublicGraduationChanges(student) { - return fetchGraduationChangesFromDb(student, "public"); + let changes = await Promise.all([ + fetchGraduationChangesFromDb(student, "public"), + fetchGraduationChangesFromDb(student, "submit"), + ]); + return changes.flat(); } async function fetchGraduation(student) { @@ -248,8 +350,9 @@ async function submitGraduationChange(student, change) { } async function submitObservation(student, observation) { - student.graduation.addObservation(observation); - await addEntryToStudentDatabase(student, "shadow", JSON.stringify(observation)); + console.assert(student.graduation.observation, "no active observation"); + student.graduation.observation.addObservation(observation); + await submitGraduationChange(student, GraduationChange.newObservationChange(observation)); } export { diff --git a/js/mock.js b/js/mock.js index 4bcd202..91b1525 100644 --- a/js/mock.js +++ b/js/mock.js @@ -83,13 +83,13 @@ function fetchPublicGraduationMock(s) { } function submitGraduationChangeMock(s, c) { - assert(s.graduation, "no graduation") + console.assert(s.graduation, "no graduation") s.graduation.addChange(c); } function submitObservationMock(s, o) { - assert(s.graduation, "no graduation") - assert(s.graduation.observation, "no observation period") + console.assert(s.graduation, "no graduation") + console.assert(s.graduation.observation, "no observation period") s.graduation.observation.addObservation(o); } diff --git a/js/student-common.js b/js/student-common.js index c89b818..778ff8d 100644 --- a/js/student-common.js +++ b/js/student-common.js @@ -1,4 +1,4 @@ -import { getUserIconUrl, getCourseParticipantsUrl } from "./config.js"; +import { getCourseId, getUserIconUrl, getCourseParticipantsUrl } from "./config.js"; import { fetchGraduation } from "./graduation-common.js"; /** * Class to represent Students. @@ -27,6 +27,26 @@ class Student { } return this.graduation; } + + static async fetchById(id) { + let name = ""; + let group = ""; + + const res = await fetch(`/user/view.php?id=${id}&course=${getCourseId()}`); + const doc = new DOMParser().parseFromString(await res.text(), "text/html"); + + name = doc.querySelector("div.page-header-headings > h2").textContent; + + const cards = doc.querySelectorAll("div.card-body dl"); + for (const card of cards) { + if (card.querySelector("dt").textContent !== "Gruppe") + continue; + group = card.querySelector("dd > a").textContent; + break; + } + + return new Student(id, name, group); + } } async function fetchStudent(idFilter, nameFilter, groupFilter) { diff --git a/mass-graducation/index.css b/mass-graducation/index.css index 28f4b88..a8790b8 100644 --- a/mass-graducation/index.css +++ b/mass-graducation/index.css @@ -72,6 +72,8 @@ } .group-member { + display: grid; + grid-template-columns: 30px 30px 200px repeat(3, 40px) auto 20px; } .group-member-name { @@ -88,12 +90,15 @@ .group-member-green { background: green; + margin: 1px; } .group-member-orange { background: orange; + margin: 1px; } .group-member-red { background: red; + margin: 1px; } #group-member-point-reason { @@ -115,4 +120,20 @@ .group-member-observation-container { background: lightgray; + width: 600px; } + +.group-member-observation-table { + background: lightgray; + +} +.group-member-observation-table-description { + width: 80%; +} + +.group-member-observation-table-yes, +.group-member-observation-table-no { + width: 10%; + text-align: center; +} + diff --git a/mass-graducation/index.html b/mass-graducation/index.html index 1ed7042..7a65745 100644 --- a/mass-graducation/index.html +++ b/mass-graducation/index.html @@ -16,7 +16,7 @@ class="group-selection" placeholder="Klasse" /> - + @@ -26,9 +26,20 @@ - diff --git a/mass-graducation/package-lock.json b/mass-graducation/package-lock.json index 6ba8178..18aa850 100644 --- a/mass-graducation/package-lock.json +++ b/mass-graducation/package-lock.json @@ -1,11 +1,11 @@ { - "name": "cockpit", + "name": "mass-graduation", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "cockpit", + "name": "mass-graduation", "version": "1.0.0", "devDependencies": { "html-webpack-plugin": "^5.6.7", diff --git a/mass-graducation/src/main.js b/mass-graducation/src/main.js index bae20bb..3d20f0b 100644 --- a/mass-graducation/src/main.js +++ b/mass-graducation/src/main.js @@ -13,13 +13,16 @@ import { getName } from "../../js/utils-ENV.js"; let group; let name; -function toggle(btn) { - if (btn.innerHTML === "X") +function toggle(btn, other) { + if (btn.innerHTML === "X") { btn.innerHTML = "o"; - else + } else { btn.innerHTML = "X"; + if (other) + other.innerHTML = "o"; + } - document.getElementById("submit-mass-graduation").style.display = "block"; + document.getElementById("submit-mass-graduation").style.display = "inline"; } function observationClickHandler(ob, li) { @@ -36,11 +39,17 @@ function observationClickHandler(ob, li) { li.appendChild(observationDiv); const obsTbl = document.createElement("table"); - obsTbl.id = "group-member-observation-table"; + obsTbl.classList.add("group-member-observation-table"); observationDiv.appendChild(obsTbl); const obsTh = document.createElement("thead"); - obsTh.innerHTML = "JaNein"; + obsTh.innerHTML = ` + + + Ja + Nein +`; + obsTbl.appendChild(obsTh); const obsTbdy = document.createElement("tbody"); @@ -51,19 +60,28 @@ function observationClickHandler(ob, li) { const obsDesc = document.createElement("td"); obsDesc.innerHTML = obsType; + obsDesc.classList.add("group-member-observation-table-description"); obsTr.appendChild(obsDesc); const obsYes = document.createElement("td"); obsYes.innerHTML = "o"; - obsYes.addEventListener("click", () => toggle(obsYes)); obsYes.dataset.yes = "true"; + obsYes.classList.add("group-member-observation-table-yes"); obsTr.appendChild(obsYes); const obsNo = document.createElement("td"); obsNo.innerHTML = "o"; - obsNo.addEventListener("click", () => toggle(obsNo) ); + obsNo.dataset.no = "true"; + obsNo.classList.add("group-member-observation-table-no"); obsTr.appendChild(obsNo); + obsYes.addEventListener("click", () => { + toggle(obsYes, obsNo); + }); + obsNo.addEventListener("click", () => { + toggle(obsNo, obsYes); + }); + obsTbdy.appendChild(obsTr); } // Toggle the observation container visibility @@ -79,6 +97,7 @@ function observationClickHandler(ob, li) { async function loadGroup(selected) { group = selected; const students = await group.getStudents(); + let graduationPromises = []; document.getElementById("group-content").style.display = "block"; const ul = document.getElementById("group-members"); @@ -93,18 +112,22 @@ async function loadGroup(selected) { const ob = document.createElement("button"); ob.classList.add("group-member-observation-icon") - student.getGraduation().then((graduation) => { + + const graduationPromise = student.getGraduation(); + graduationPromise.then((graduation) => { if (!graduation.isObserved()) { return; } ob.innerHTML = "👁" ob.addEventListener("click", () => observationClickHandler(ob, li) ); }); + graduationPromises.push(graduationPromise); + li.appendChild(ob); const i = document.createElement("img"); i.classList.add("group-member-icon") - student.getGraduation().then((graduation) => { + graduationPromise.then((graduation) => { i.src = graduation.getIcon(); }); li.appendChild(i); @@ -134,42 +157,146 @@ async function loadGroup(selected) { ul.appendChild(li); } + + const graduations = await Promise.all(graduationPromises); + let observations = graduations.filter( + (g) => g.observationRequested || (g.observation && !g.observation.end) + ); + + const observationReuqestsContainer = document.getElementById("observation-requests-container"); + + if (observations.length == 0) { + observationReuqestsContainer.style.display = "none"; + return; + } else { + observationReuqestsContainer.style.display = "unset"; + } + + const observationReuqestsNode = document.getElementById("observation-requests"); + observationReuqestsNode.innerHTML = ""; + observations.sort((g1, g2) => { + let g1D, g2D; + g1D = g1.observationRequested ? g1.observationRequested : g1.observation.start; + g2D = g2.observationRequested ? g2.observationRequested : g2.observation.start; + return g1D - g2D; + }); + + for (const graduation of observations) { + const row = document.createElement("tr"); + const iconCell = document.createElement("td"); + const icon = document.createElement("img"); + icon.src = graduation.getIcon(); + row.appendChild(iconCell); + + const nameCell = document.createElement("td"); + nameCell.innerHTML = `${graduation.student.name}`; + row.appendChild(nameCell); + + const dateCell = document.createElement("td"); + let date; + if (graduation.observationRequested) { + date = graduation.observationRequested; + } else { + date = graduation.observation.start; + } + dateCell.innerHTML = date.toLocaleString("de-DE", { month: "numeric", day: "numeric", year: "numeric", hour: "numeric", minute: "numeric" }); + row.appendChild(dateCell); + + const btnCell = document.createElement("td"); + if (graduation.observation && !graduation.observation.ready()) { + btnCell.innerHTML = "In Beobachtungsphase"; + } else { + const startBtn = document.createElement("button"); + if (graduation.observationRequested) { + startBtn.addEventListener("click", () => { + graduation.startObservation(); + //TODO(muhq): Add observation icon-button. + startBtn.remove(); + btnCell.innerHTML = "In Beobachtungsphase"; + }); + startBtn.innerHTML = `Beobachtungsphase beginnen`; + startBtn.classList.add("start-observation"); + } else { + startBtn.addEventListener("click", () => { + window.open(`/mod/page/view.php?id=93017047&userToLoad=${graduation.student.id}`, '_blank'); + }); + startBtn.innerHTML = `Graduierung abschließen`; + startBtn.classList.add("end-observation"); + } + btnCell.appendChild(startBtn); + } + row.appendChild(btnCell); + + observationReuqestsNode.appendChild(row); + } } function submitMassGraduation() { + let promises = []; for(const li of document.querySelectorAll("li.group-member")) { const reason = li.querySelector(".group-member-point-reason").value; const student = li.student; if (li.querySelector(".group-member-green").innerHTML === "X") { - submitGraduationChange(student, GraduationChange.newGreenPointChange(reason, name)); + promises.push(submitGraduationChange(student, GraduationChange.newGreenPointChange(reason, name))); } if (li.querySelector(".group-member-orange").innerHTML === "X") { - submitGraduationChange(student, GraduationChange.newOrangePointChange(reason, name)); + promises.push(submitGraduationChange(student, GraduationChange.newOrangePointChange(reason, name))); } if (li.querySelector(".group-member-red").innerHTML === "X") { - submitGraduationChange(student, GraduationChange.newRedPointChange(reason, name)); + promises.push(submitGraduationChange(student, GraduationChange.newRedPointChange(reason, name))); } if (li.observationDiv) { let status = []; for (const row of li.observationDiv.querySelectorAll("tbody > tr")) { - status.push(row.querySelector('td[data-yes="true"]').innerHTML === "X"); + if (row.querySelector('td[data-yes="true"]').innerHTML === "X") + status.push("yes"); + else if (row.querySelector('td[data-no="true"]').innerHTML === "X") + status.push("no"); + else + status.push(""); } - submitObservation(student, new Observation(Date.now(), status)); + promises.push(submitObservation(student, new Observation(new Date(), status))); } } + + const state = document.getElementById("submit-mass-graduation-state"); + Promise.all(promises).then(() => { + state.style.display = "inline"; + state.innerHTML = "✓"; + state.style.color = "green"; + for (const li of document.querySelectorAll("li.group-member")) { + if (!li.observationDiv) + continue; + li.observationDiv.remove(); + li.observationDiv = null; + } + }).catch((error) => { + state.style.display = "inline"; + state.innerHTML = "X"; + state.style.color = "red"; + console.log(error); + }); } function printRedPointNote(li) { print(); } -window.onload = function() { +async function init() { + document.getElementById("submit-mass-graduation").addEventListener("click", submitMassGraduation); + name = getName(); + initSelection( "group-selection", async function() { return (await fetchGroups()).values(); }, (group) => `${group.name}`, loadGroup, ); - document.getElementById("submit-mass-graduation").addEventListener("click", submitMassGraduation); - name = getName(); } + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); +} else { + init(); +} + -- cgit v1.2.3