aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2026-06-24 09:18:26 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2026-06-24 09:18:26 +0200
commitaf8585faa1aa7c39564c583963349c4a2922df9c (patch)
tree5a806a738a76e5f928e68e4def47e0b3a857d490
parent13f9914757b2a72e77369859bbaec35339f31446 (diff)
downloadbycs-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.css14
-rw-r--r--cockpit/index.html83
-rw-r--r--cockpit/js/cockpit.js210
-rw-r--r--js/graduation-common.js52
-rw-r--r--js/graduation-mock.js4
-rw-r--r--js/graduation-prod.js4
-rw-r--r--js/mock.js3
-rw-r--r--js/student-common.js11
-rw-r--r--js/student-mock.js10
-rw-r--r--js/student-prod.js2
-rw-r--r--js/utils-mock.js30
-rw-r--r--js/utils-prod.js18
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">&crarr;</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;">&#128065</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
diff --git a/js/mock.js b/js/mock.js
index fe60ef3..e4aca53 100644
--- a/js/mock.js
+++ b/js/mock.js
@@ -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;
+}