diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2026-06-24 09:18:26 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2026-06-24 09:18:26 +0200 |
| commit | af8585faa1aa7c39564c583963349c4a2922df9c (patch) | |
| tree | 5a806a738a76e5f928e68e4def47e0b3a857d490 | |
| parent | 13f9914757b2a72e77369859bbaec35339f31446 (diff) | |
| download | bycs-sol-af8585faa1aa7c39564c583963349c4a2922df9c.tar.gz bycs-sol-af8585faa1aa7c39564c583963349c4a2922df9c.zip | |
cockpit: finish prototype UI
* remove process and calendar
* add observation period
* improve mocking
| -rw-r--r-- | cockpit/index.css | 14 | ||||
| -rw-r--r-- | cockpit/index.html | 83 | ||||
| -rw-r--r-- | cockpit/js/cockpit.js | 210 | ||||
| -rw-r--r-- | js/graduation-common.js | 52 | ||||
| -rw-r--r-- | js/graduation-mock.js | 4 | ||||
| -rw-r--r-- | js/graduation-prod.js | 4 | ||||
| -rw-r--r-- | js/mock.js | 3 | ||||
| -rw-r--r-- | js/student-common.js | 11 | ||||
| -rw-r--r-- | js/student-mock.js | 10 | ||||
| -rw-r--r-- | js/student-prod.js | 2 | ||||
| -rw-r--r-- | js/utils-mock.js | 30 | ||||
| -rw-r--r-- | js/utils-prod.js | 18 |
12 files changed, 337 insertions, 104 deletions
diff --git a/cockpit/index.css b/cockpit/index.css index 810442e..a206067 100644 --- a/cockpit/index.css +++ b/cockpit/index.css @@ -100,6 +100,11 @@ flex-direction: column; } +#student-observation { + margin-left: 2rem; + margin-bottom: 1rem; +} + .student-info-mid-element { padding: 5pt; border: black solid 0.2pt; @@ -178,3 +183,12 @@ #student-info-bonus-amount { font-size: 2rem; } + +.graduation-dropdown > summary { + background: lightgray; + border: black solid 0.2pt; + border-radius: 10px; + padding: 0.25rem; + margin-top: 1rem; + margin-bottom: 1rem; +} diff --git a/cockpit/index.html b/cockpit/index.html index d0bb0c7..ddf8005 100644 --- a/cockpit/index.html +++ b/cockpit/index.html @@ -48,6 +48,9 @@ </div> </div> + <div id="student-observation" class="student-info-banner"> + </div> + <div id="student-info-mid" class="student-info-banner"> <div id="student-info-green" class="student-info-mid-element"> ⚫Grüne Punkte <span class="student-info-points" id="student-info-points-green">0</span> @@ -70,32 +73,70 @@ <button id="student-info-point-reason-submit">↵</button> </div> - <div id="student-info-bottom" class="student-info-banner"> - <div id="student-info-stickers-container" class="student-info-bottom-element"> - Monstersticker - <div id="student-info-stickers"> - </div> - </div> - <div id="student-info-bonus-container" class="student-info-bottom-element"> - Bonuskarten - <div id="student-info-bonus-row"> - <img></img> - <span id="student-info-bonus-amount">0</span> - </div> - </div> - </div> + <!-- <div id="student-info-bottom" class="student-info-banner"> --> + <!-- <div id="student-info-stickers-container" class="student-info-bottom-element"> --> + <!-- Monstersticker --> + <!-- <div id="student-info-stickers"> --> + <!-- </div> --> + <!-- </div> --> + <!-- <div id="student-info-bonus-container" class="student-info-bottom-element"> --> + <!-- Bonuskarten --> + <!-- <div id="student-info-bonus-row"> --> + <!-- <img></img> --> + <!-- <span id="student-info-bonus-amount">0</span> --> + <!-- </div> --> + <!-- </div> --> + <!-- </div> --> </div> - <details id="progress"> - <summary>Lernfortschritt</summary> - </details> + <!-- <details id="progress"> --> + <!-- <summary>Lernfortschritt</summary> --> + <!-- </details> --> - <details id="calendar"> - <summary>Kalender</summary> - </details> + <!-- <details id="calendar"> --> + <!-- <summary>Kalender</summary> --> + <!-- </details> --> - <details id="graduation"> + <details id="graduation" data-availability="if-lehrer"> <summary>Graduierung</summary> + + <details id="graduation-changes-container" class="graduation-dropdown"> + <summary>Übersicht über Grüne, Orangene und Rote Punkte</summary> + <table class="table table-striped"> + <thead> + <tr> + <th>Datum</th> + <th>Grund</th> + <th>Coach</th> + </tr> + </thead> + <tbody id="graduation-changes"> + </tbody> + </table> + </details> + + <details id="graduation-current-observation-period-container" class="graduation-dropdown"> + <summary>Aktueller Beobachtungszeitraum</summary> + <table class="table table-striped"> + <thead> + <tr> + <th></th> + <th>Ja</th> + <th>Nein</th> + </tr> + </thead> + <tbody id="graduation-current-observation-period"> + </tbody> + </table> + </details> + + <details id="graduation-observation-periods-container" class="graduation-dropdown"> + <summary>Vergangene Beobachtungszeiträume</summary> + <table class="table table-striped"> + <tbody id="graduation-observation-periods"> + </tbody> + </table> + </details> </details> </div> </body> diff --git a/cockpit/js/cockpit.js b/cockpit/js/cockpit.js index b773f5c..f8ee8e9 100644 --- a/cockpit/js/cockpit.js +++ b/cockpit/js/cockpit.js @@ -1,16 +1,21 @@ -import { - fetchSubjectsMock as fetchSubjects, - fetchProgressMock as fetchProgress, -} from "../../js/mock.js"; +// import { + // fetchSubjectsMock as fetchSubjects, + // fetchProgressMock as fetchProgress, +// } from "../../js/mock.js"; import { + LEVEL_NAMES, GraduationChange, fetchGraduation, submitGraduationChange } from "../../js/graduation-ENV.js"; -import { Student, fetchStudent, fetchStudents } from "../../js/student-ENV.js"; -import { isTeacher as getIsTeacher } from "../../js/utils-ENV.js"; +import { fetchStudent, fetchStudents } from "../../js/student-ENV.js"; +import { + isTeacher as getIsTeacher, + getCurrentName, + getUserId, +} from "../../js/utils-ENV.js"; import { initSelection } from "../../js/selection.js"; let isTeacher; @@ -18,30 +23,20 @@ let firstname; let lastname; let student; -function initCurrentName() { - const autoNameDiv = document.getElementById("auto-name"); - firstname = autoNameDiv.children[0].innerHTML; - lastname = autoNameDiv.children[1].innerHTML; - autoNameDiv.remove(); -} - -async function loadNewStudent(id, name) { - if (isTeacher) { - student = await fetchStudent(id, name); - } else { - student = new Student(id, name); - } - await loadStudent(student); -} - async function loadStudent(studentToLoad) { student = studentToLoad; fetchAndUpdateStudentInfo(); - updateStudentCalendar(); - updateStudentProgress(); + // updateStudentCalendar(); + // updateStudentProgress(); + // TODO(muhq): fetch graduation only once updateStudentGraduation(); } +async function loadNewStudent(id) { + student = await fetchStudent(id); + await loadStudent(student); +} + function initStudentSelection() { initSelection( "student-selection", @@ -96,6 +91,7 @@ function pointClickHandler(color) { submitGraduationChange(student, change); textareaDiv.style.display = "none"; updateStudentInfo(); + updateStudentGraduation(); } // Use onEVENT to override any previous handler textarea.onkeydown = function(e) { @@ -132,7 +128,7 @@ function updateStudentInfo() { if (student.graduation) { const tag = document.getElementById("student-info-tag"); - tag.innerHTML = student.graduation.level; + tag.innerHTML = student.graduation.getLevelName(); tag.style.display = "block"; document.getElementById("student-info-points-green").innerHTML = student.graduation.green; @@ -150,59 +146,131 @@ function updateStudentInfo() { renderGraduationLevel(student.graduation, document.getElementById("student-info-level")); - renderGraduationStickers(student.graduation, document.getElementById("student-info-stickers")); - renderGraduationBonus(student.graduation, document.getElementById("student-info-stickers")); + // renderGraduationStickers(student.graduation, document.getElementById("student-info-stickers")); + // renderGraduationBonus(student.graduation, document.getElementById("student-info-stickers")); + + renderObservationStatus(student.graduation, document.getElementById("student-observation")); } document.getElementById("student-content").style.display = "block"; } function renderGraduationLevel(graduation, node) { - node.innerHTML = graduation.level; + node.innerHTML = graduation.getLevelName(); } -function renderGraduationStickers(graduation, node) { -} - -function renderGraduationBonus(graduation, node) { -} - -function newProgressBar(subject) { - let bar = document.createElement("div"); - fetchProgress(subject).then((progress) => { - bar.progress = progress; - }); - - let s1 = document.createElement("div"); - s1.onhover - - bar.appendChild(s1); - let s2 = document.createElement("div"); - bar.appendChild(s2); - let s3 = document.createElement("div"); - bar.appendChild(s3); - return bar; -} - -let selectedSubjects; -async function updateStudentProgress() { - selectedSubjects = await fetchSubjects(student); - - let prog = document.getElementById("progress"); - for(const subj of selectedSubjects) { - let subjDiv = document.createElement("div"); - subjDiv.innerHTML = subj; - subjDiv.appendChild(newProgressBar(subj)) - prog.appendChild(subjDiv); +// function renderGraduationStickers(graduation, node) { + // node.innerHTML = ""; +// } + +// function renderGraduationBonus(graduation, node) { + // node.innerHTML = ""; +// } + +function renderObservationStatus(graduation, node) { + node.innerHTML = ""; + + let status; + if (isTeacher || graduation.isObserved()) { + status = document.createElement("span"); + status.classList.add("student-observation-status"); + if (graduation.isObserved()) { + status.innerHTML = '<span style="font-size: larger;">👁</span> Ich befinde mich in der Beobachtungsphase zum Levelaufstieg.'; + } else { + status.innerHTML = "Nicht in Beobachtung."; + } + } else { + status = document.createElement("button"); + status.classList.add("student-request-observation-button"); + // Level is 1-based + status.innerHTML = `Ich will ${LEVEL_NAMES[graduation.level]} werden`; + status.addEventListener("click", async function() { + await graduation.requestObservation(); + // Rerender the observation status + renderObservationStatus(graduation, node); + }); } + node.appendChild(status); } -async function updateStudentCalendar() { - let cal = document.getElementById("calendar"); -} +// function newProgressBar(subject) { + // let bar = document.createElement("div"); + // fetchProgress(subject).then((progress) => { + // bar.progress = progress; + // }); + + // let s1 = document.createElement("div"); + // s1.onhover + + // bar.appendChild(s1); + // let s2 = document.createElement("div"); + // bar.appendChild(s2); + // let s3 = document.createElement("div"); + // bar.appendChild(s3); + // return bar; +// } + +// let selectedSubjects; +// async function updateStudentProgress() { + // selectedSubjects = await fetchSubjects(student); + + // let prog = document.getElementById("progress"); + // for(const subj of selectedSubjects) { + // let subjDiv = document.createElement("div"); + // subjDiv.innerHTML = subj; + // subjDiv.appendChild(newProgressBar(subj)) + // prog.appendChild(subjDiv); + // } +// } + +// async function updateStudentCalendar() { + // let cal = document.getElementById("calendar"); +// } async function updateStudentGraduation() { - let graduation = document.getElementById("graduation"); + if (!isTeacher) return; + const graduation = await student.getGraduation(); + + const changesTbl = document.getElementById("graduation-changes"); + changesTbl.innerHTML = ""; + for (const change of graduation.changes.filter( + (ch) => ["Grün", "Rot", "Orange"].includes(ch.change) )) + { + const changeTr = document.createElement("tr"); + for (const key in change) { + if (key == "json" || key == "id" || key == "name") continue; + + if (key == "change") { + switch(change.change) { + case "Grün": + changeTr.style.background = "green"; + break; + case "Rot": + changeTr.style.background = "red"; + break; + case "Orange": + changeTr.style.background = "orange"; + break; + default: + alert(`unexpected change: ${change.change}`); + } + continue; + } + + const value = change[key]; + let fmt = ""; + if (key == "date") + fmt = `${value.getDate()}.${value.getMonth()}.${value.getFullYear()} ${value.getHours()}:${value.getMinutes()}`; + else + fmt = value; + + const changeTd = document.createElement("td"); + changeTd.innerHTML = fmt; + changeTd.dataset.key = key; + changeTr.appendChild(changeTd); + } + changesTbl.appendChild(changeTr); + } } async function initCockpit() { @@ -213,14 +281,18 @@ async function initCockpit() { if (userId) loadNewStudent(userId); } else { - loadNewStudent(null, firstname + " " + lastname); + // Remove all elements only suitable for teachers + for(const elem of document.querySelectorAll("[data-availability=if-lehrer]")) { + elem.remove(); + } + + loadNewStudent(getUserId()); } - return; } window.onload = function() { isTeacher = getIsTeacher(); DEFAULT_STUDENT_ICON = document.getElementById("student-info-icon").src; - initCurrentName(); - initCockpit(isTeacher); + [firstname, lastname] = getCurrentName(); + initCockpit(); } diff --git a/js/graduation-common.js b/js/graduation-common.js index 4716e6d..4479bec 100644 --- a/js/graduation-common.js +++ b/js/graduation-common.js @@ -1,6 +1,8 @@ import { GRADUATION_DB_ID, GRADUATION_ACTIVITY_ID, GRADUATION_DB_PARAMS_MAP } from "./config.js"; import { loadFromDatabase, addEntryToDatabaseActivity } from "./database.js" +const LEVEL_NAMES = ["Beginner", "Entdecker"]; + class GraduationChange { constructor(date, id, name, group, change, reason, coach, json) { this.date = date; @@ -34,20 +36,21 @@ class GraduationChange { static newRedPointChange(student, reason, coach) { return new GraduationChange( new Date(), - student.id, student.name, student.group, + student.id, student.name, "Rot", reason, coach, "" ); } } class Graduation { - constructor(student, green, orange, red, level, stickers, changes) { + constructor(student, green, orange, red, level, stickers, observed, changes) { this.student = student; this.green = green; this.orange = orange; this.red = red; this.level = level; this.stickers = stickers; + this.observed = observed; this.changes = changes; student.graduation = this; @@ -57,7 +60,8 @@ class Graduation { let green = 0; let orange = 0; let red = 0; - let level = "Beginner"; + let level = 1; + let observed = false; let stickers = []; for (const change of changes) { switch (change.change) { @@ -74,13 +78,20 @@ class Graduation { orange = 0; break; case "Änderung": - // TODO: + let ctx = JSON.parse(change.json) + switch (ctx.type) { + case "ObservationStart": + observed = true; + break; + default: + alert(`Unexpected ctx type: ${ctx.type}`); + } break; default: alert(`Unexpected change: ${change}`); } } - return new Graduation(student, green, orange, red, level, stickers, changes); + return new Graduation(student, green, orange, red, level, stickers, observed, changes); } addChange(change) { @@ -90,6 +101,36 @@ class Graduation { // This updates the student.graduation reference as well Graduation.from(this.student, changes); } + + /** + * Request observation + */ + async requestObservation() { + // TODO(muhq): Implement observation mark + this.observed = true; + } + + /** + * Return if the student is in observation. + */ + isObserved() { + return this.observed; + } + + /** + * Return the URI of the icon representing the graduation level. + */ + getIcon() { + return ""; + } + + /** + * Return the name of the current graduation level. + * @return {String} The name of the current level. + */ + getLevelName() { + return LEVEL_NAMES[this.level - 1]; + } } async function fetchGraduationChanges(student) { @@ -125,6 +166,7 @@ async function submitGraduationChange(student, change) { } export { + LEVEL_NAMES, GraduationChange, Graduation, fetchGraduation, diff --git a/js/graduation-mock.js b/js/graduation-mock.js index 9f67185..d35901e 100644 --- a/js/graduation-mock.js +++ b/js/graduation-mock.js @@ -1,6 +1,6 @@ -import { Graduation, GraduationChange, } from "./graduation-common.js"; +import { LEVEL_NAMES, Graduation, GraduationChange, } from "./graduation-common.js"; import { fetchGraduationMock as fetchGraduation, submitGraduationChangeMock as submitGraduationChange } from "./mock.js"; -export { GraduationChange, Graduation, fetchGraduation, submitGraduationChange};
\ No newline at end of file +export { LEVEL_NAMES, GraduationChange, Graduation, fetchGraduation, submitGraduationChange};
\ No newline at end of file diff --git a/js/graduation-prod.js b/js/graduation-prod.js index de7cbf4..cbc8d48 100644 --- a/js/graduation-prod.js +++ b/js/graduation-prod.js @@ -1,2 +1,2 @@ -import { GraduationChange, Graduation, fetchGraduation, submitGraduationChange } from "./graduation-common.js"; -export { GraduationChange, Graduation, fetchGraduation, submitGraduationChange };
\ No newline at end of file +import { LEVEL_NAMES, GraduationChange, Graduation, fetchGraduation, submitGraduationChange } from "./graduation-common.js"; +export { LEVEL_NAMES, GraduationChange, Graduation, fetchGraduation, submitGraduationChange };
\ No newline at end of file @@ -48,6 +48,7 @@ for (const s of students) { graduationChanges[s.name] = [ GraduationChange.newGreenPointChange(s, "", "Florian Fischer"), GraduationChange.newGreenPointChange(s, "", "Florian Fischer"), + new GraduationChange(new Date(), s.id, s.name, "Änderung", "", "", '{"type": "ObservationStart"}'), ]; break; case "b b b": @@ -55,6 +56,8 @@ for (const s of students) { GraduationChange.newRedPointChange(s, "", "Florian Fischer"), ]; } + // Build the student's graduation + Graduation.from(s, graduationChanges[s.name]); } function fetchGraduationMock(s) { diff --git a/js/student-common.js b/js/student-common.js index 48f7d60..f0d8ed3 100644 --- a/js/student-common.js +++ b/js/student-common.js @@ -1,8 +1,11 @@ import { getUserIconUrl, getCourseParticipantsUrl } from "./config.js"; +import { fetchGraduation } from "./graduation-common.js"; /** * Class to represent Students. */ class Student { + graduation = null; + constructor(id, name, groups) { this.id = id; this.name = name; @@ -23,6 +26,14 @@ class Student { } return this.groups.filter((g) => g !== this.getGroupName()); } + + async getGraduation() { + if(!this.graduation) { + // fetchGraduation(this) sets this.graduation + await fetchGraduation(this); + } + return this.graduation; + } } async function fetchStudent(idFilter, nameFilter, groupFilter) { diff --git a/js/student-mock.js b/js/student-mock.js index 0ea12ea..7c79e12 100644 --- a/js/student-mock.js +++ b/js/student-mock.js @@ -1,3 +1,9 @@ -import { Student, fetchStudent } from "./student-common.js"; +import { Student } from "./student-common.js"; import { fetchStudentsMock as fetchStudents } from "./mock.js"; -export { Student, fetchStudents, fetchStudent };
\ No newline at end of file + +async function fetchStudent(idFilter, nameFilter, groupFilter) { + const students = await fetchStudents(idFilter, nameFilter, groupFilter); + return students[0]; +} + +export { Student, fetchStudents, fetchStudent }; diff --git a/js/student-prod.js b/js/student-prod.js index f9c430c..9c8d7cb 100644 --- a/js/student-prod.js +++ b/js/student-prod.js @@ -1,2 +1,2 @@ import { Student, fetchStudent, fetchStudents } from "./student-common.js"; -export { Student, fetchStudents, fetchStudent };
\ No newline at end of file +export { Student, fetchStudents, fetchStudent }; diff --git a/js/utils-mock.js b/js/utils-mock.js index 4f5348d..7859a1e 100644 --- a/js/utils-mock.js +++ b/js/utils-mock.js @@ -1,3 +1,31 @@ +import { fetchStudent } from "./student-ENV.js"; + export function isTeacher() { - return true; + return !(new URLSearchParams(document.location.search)).has("student"); +} + +export function getCurrentName() { + let firstname = ""; + let lastname = ""; + + const autoNameDiv = document.getElementById("auto-name"); + if (autoNameDiv) { + firstname = autoNameDiv.children[0].innerHTML; + lastname = autoNameDiv.children[1].innerHTML; + autoNameDiv.remove(); + } + + const params = new URLSearchParams(document.location.search); + if (params.has("firstname")) + firstname = params.get("firstname") + if (params.has("lastname")) + lastname = params.get("lastname") + + return [firstname, lastname]; +} + +export function getUserId() { + const [firstname, lastname] = getCurrentName(); + + return fetchStudent(null, firstname + " " + lastname).id; } diff --git a/js/utils-prod.js b/js/utils-prod.js index 1dd7e8e..119870c 100644 --- a/js/utils-prod.js +++ b/js/utils-prod.js @@ -1,5 +1,21 @@ export function isTeacher() { return Boolean(document.querySelector('.editmode-switch-form')) - || Boolean(document.querySelector('a[href*="templates.php"]')) || document.body.classList.contains('editing'); } + +export function getCurrentName() { + let firstname = ""; + let lastname = ""; + + const autoNameDiv = document.getElementById("auto-name"); + firstname = autoNameDiv.children[0].innerHTML; + lastname = autoNameDiv.children[1].innerHTML; + autoNameDiv.remove(); + + return [firstname, lastname]; +} + +export function getUserId() { + // M is exported by Moodle + return M.cfg.userid; +} |
