aboutsummaryrefslogtreecommitdiff
path: root/src/graduation-common.js
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2026-08-03 14:31:25 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2026-08-03 14:31:25 +0200
commit6839831af54cd9302119bcc3bffafe10475958d4 (patch)
tree12a1c1fbd864e9579cb9c3a09d4bf4686a280b73 /src/graduation-common.js
parentfccb70cbd876785f6833dfe6efc10ec9e445556d (diff)
downloadbycs-sol-6839831af54cd9302119bcc3bffafe10475958d4.tar.gz
bycs-sol-6839831af54cd9302119bcc3bffafe10475958d4.zip
refactor project structure
* merge sites into single project root * fix name of mass-graduation site * enable typescript support
Diffstat (limited to 'src/graduation-common.js')
-rw-r--r--src/graduation-common.js396
1 files changed, 396 insertions, 0 deletions
diff --git a/src/graduation-common.js b/src/graduation-common.js
new file mode 100644
index 0000000..0c28465
--- /dev/null
+++ b/src/graduation-common.js
@@ -0,0 +1,396 @@
+import { getSubmitDb, loadFromStudentDatabase, addEntryToStudentDatabase } from "./database.js"
+
+import NeulingPng from "../assets/Neuling.png";
+import EntdeckerPng from "../assets/Entdecker.png";
+import NavigatorPng from "../assets/Navigator.png";
+import PioneerPng from "../assets/Pioneer.png";
+
+const LEVEL_NAMES = ["Neuling", "Entdecker", "Navigator", "Pionieer"];
+
+const LEVEL_IMAGES = [
+ new URL(NeulingPng),
+ new URL(EntdeckerPng),
+ new URL(NavigatorPng),
+ new URL(PioneerPng),
+];
+
+class GraduationChange {
+ constructor(date, change, reason, coach, context) {
+ this.date = date;
+ this.change = change;
+ this.reason = reason;
+ this.coach = coach;
+ this.context = context;
+ }
+
+ isPublic() {
+ return [
+ "Grün", "Orange", "Rot",
+ "ObservationStart", "ObservationEnd",
+ "LevelChange"
+ ].includes(this.change);
+ }
+
+ 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,
+ };
+ }
+
+ static fromDBEntry(e) {
+ const {coach, json} = e;
+ const change = JSON.parse(json);
+ 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) {
+ return new GraduationChange(new Date(), "Grün", reason, coach, "");
+ }
+
+ static newOrangePointChange(reason, coach) {
+ return new GraduationChange(new Date(), "Orange", reason, coach, "");
+ }
+
+ static newRedPointChange(reason, coach) {
+ return new GraduationChange(new Date(), "Rot", reason, coach, "");
+ }
+}
+
+class Graduation {
+ 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;
+ }
+
+ static from(student, changes) {
+ let green = 0;
+ 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":
+ console.assert(openObservationRequest, "No request for observation start");
+ observationPeriod = new ObservationPeriod(change.date);
+ openObservationRequest = null;
+ break;
+
+ case "ObservationEnd":
+ 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":
+ 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,
+ openObservationRequest, observationPeriod, pastObservations,
+ changes);
+ }
+
+ addChange(change) {
+ // TODO(muhq): Implement incremental addition
+ let changes = this.changes;
+ changes.push(change);
+ // This updates the student.graduation reference as well
+ Graduation.from(this.student, changes);
+ }
+
+ /**
+ * Request observation
+ */
+ async requestObservation() {
+ 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));
+ }
+
+ /**
+ * Return if the student is in observation.
+ */
+ isObserved() {
+ return Boolean(this.observation);
+ }
+
+ /**
+ * Return the URI of the icon representing the graduation level.
+ */
+ getIcon() {
+ return LEVEL_IMAGES[this.level - 1];
+ }
+
+ /**
+ * Return the name of the current graduation level.
+ * @return {String} The name of the current level.
+ */
+ getLevelName() {
+ return LEVEL_NAMES[this.level - 1];
+ }
+}
+
+class ObservationPeriod {
+ constructor(start) {
+ this.start = start;
+ this.observations = [];
+ }
+
+ finishPeriod(success) {
+ this.end = new Date();
+ this.success = success;
+ }
+
+ ended() {
+ return Boolean(this.end);
+ }
+
+ ready() {
+ 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().map(() => ({"yes":0, "no":0}));
+ for (const observation of this.observations) {
+ for (let i = 0; i < sums.length; i++) {
+ if (observation.status[i]) {
+ if (observation.status[i] === "yes")
+ sums[i].yes++;
+ else
+ sums[i].no++;
+ }
+ }
+ }
+ return sums;
+ }
+
+ addObservation(observation) {
+ this.observations.push(observation);
+ }
+}
+
+const OBSERVATION_TYPES = [
+ "Arbeitet konzentriert an Aufgaben",
+ "Arbeitet zunehmend selbstständig",
+ "Führt das Logbuch zuverlässig",
+ "Bringt Material mit",
+ "Hält Regeln ein",
+ "Geht verantwortungsvoll mit Materialien und LernRaum um",
+ "Nimmt Rückmeldungen an und setzt sie um",
+];
+
+class Observation {
+ constructor(date, 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))
+ }
+}
+
+function changesFromDbEntries(entries) {
+ let changes = [];
+ for (const e of entries) {
+ const gradChange = GraduationChange.fromDBEntry(e);
+ changes.push(gradChange);
+ }
+ return changes;
+}
+
+async function fetchGraduationChangesFromDb(student, type) {
+ const data = await loadFromStudentDatabase(student, type);
+ return changesFromDbEntries(data);
+}
+
+const submitEntriesPromise = async function() {
+ const db = await getSubmitDb();
+ const entries = await db.fetchEntries();
+ return changesFromDbEntries(entries);
+}();
+
+async function addSubmittedChanges(student, changes) {
+ let submitEntries = await submitEntriesPromise;
+ for (const e of submitEntries.filter( (c) => c.coach === student.name )) {
+ changes.push(e);
+ }
+ return changes;
+}
+
+async function fetchGraduationChanges(student) {
+ let changes = await fetchGraduationChangesFromDb(student, "shadow");
+ return addSubmittedChanges(student, changes);
+}
+
+async function fetchPublicGraduationChanges(student) {
+ let changes = await fetchGraduationChangesFromDb(student, "public");
+ return addSubmittedChanges(student, changes);
+}
+
+async function fetchGraduation(student) {
+ const changes = await fetchGraduationChanges(student);
+ return Graduation.from(student, changes);
+}
+
+async function fetchPublicGraduation(student) {
+ const changes = await fetchPublicGraduationChanges(student);
+ return Graduation.from(student, changes);
+}
+
+async function submitGraduationChange(student, change) {
+ student.graduation.addChange(change);
+
+ // Submit changes concurrently
+ let promises = [];
+
+ if (change.isPublic()) {
+ promises.push(
+ // TODO(muhq): strip non public fields from changes
+ addEntryToStudentDatabase(student, "public", JSON.stringify(change.publicDBFields()))
+ );
+ }
+
+ promises.push(
+ addEntryToStudentDatabase(student, "shadow", JSON.stringify(change.dbFields()))
+ );
+
+ await Promise.all(promises);
+}
+
+async function submitObservation(student, observation) {
+ console.assert(student.graduation.observation, "no active observation");
+ student.graduation.observation.addObservation(observation);
+ await submitGraduationChange(student, GraduationChange.newObservationChange(observation));
+}
+
+export {
+ LEVEL_NAMES,
+ LEVEL_IMAGES,
+ GraduationChange,
+ Graduation,
+ fetchGraduation,
+ fetchPublicGraduation,
+ fetchGraduationChanges,
+ fetchPublicGraduationChanges,
+ submitGraduationChange,
+ submitObservation,
+
+ OBSERVATION_TYPES,
+ Observation,
+ ObservationPeriod,
+};