aboutsummaryrefslogtreecommitdiff
path: root/src/graduation-common.ts
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2026-08-05 11:51:51 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2026-08-05 11:51:51 +0200
commit582df8885a3573a728394b473b826616f8969316 (patch)
tree9c21db03f4fbfd9956ee398e0d9194ebfa5b3234 /src/graduation-common.ts
parent46ac53e11b49a7f9cdb92c3559b05f41ce90122f (diff)
downloadbycs-sol-582df8885a3573a728394b473b826616f8969316.tar.gz
bycs-sol-582df8885a3573a728394b473b826616f8969316.zip
convert a lot of js to typescript and use a single graduation DB
Diffstat (limited to 'src/graduation-common.ts')
-rw-r--r--src/graduation-common.ts347
1 files changed, 347 insertions, 0 deletions
diff --git a/src/graduation-common.ts b/src/graduation-common.ts
new file mode 100644
index 0000000..61600d3
--- /dev/null
+++ b/src/graduation-common.ts
@@ -0,0 +1,347 @@
+import { getGraduationDb, DatabaseEntry } from "./database-ENV.ts"
+import type { Student } from "./student-common.ts"
+
+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 {
+ date: Date;
+ studentId: number;
+ change: string;
+ reason: string;
+ coach: string;
+ context: any;
+
+ constructor(date: Date, studentId: number, change: string, reason: string, coach: string, context: any) {
+ this.date = date;
+ this.studentId = studentId;
+ this.change = change;
+ this.reason = reason;
+ this.coach = coach;
+ this.context = context;
+ }
+
+ dbFields () {
+ return {
+ "studentId": this.studentId,
+ "change": this.change,
+ "reason": this.reason,
+ "context": this.context,
+ };
+ }
+
+ static fromDBEntry(e: DatabaseEntry) {
+ const {date, coach, json} = e;
+ const change = JSON.parse(json);
+ return new GraduationChange(new Date(date), change.change, change.student, change.reason, coach, change.context);
+ }
+
+ static newObservationRequest() {
+ return new GraduationChange(new Date(), -1, "ObservationRequest", "", "", "");
+ }
+
+ static newObservationStart() {
+ return new GraduationChange(new Date(), -1, "ObservationStart", "", "", "");
+ }
+
+ static newObservationEnd(reason: string, success: boolean) {
+ return new GraduationChange(new Date(), -1, "ObservationEnd", reason, "", JSON.stringify(success));
+ }
+
+ static newObservationChange(observation: Observation) {
+ return new GraduationChange(observation.date, -1, "Observation", "", "", JSON.stringify(observation.status));
+ }
+
+ static newGreenPointChange(reason: string, coach: string) {
+ return new GraduationChange(new Date(), -1, "Grün", reason, coach, "");
+ }
+
+ static newOrangePointChange(reason: string, coach: string) {
+ return new GraduationChange(new Date(), -1, "Orange", reason, coach, "");
+ }
+
+ static newRedPointChange(reason: string, coach: string) {
+ return new GraduationChange(new Date(), -1, "Rot", reason, coach, "");
+ }
+}
+
+class Graduation {
+ student: Student;
+ green: number;
+ orange: number;
+ red: number;
+ level: number;
+ observationRequested: Date | null;
+ observation: ObservationPeriod | null;
+ pastObservations: Array<ObservationPeriod>;
+ changes: Array<GraduationChange>;
+
+ constructor(student: Student, green: number, orange: number, red: number, level: number, observationRequested: Date | null, observation: ObservationPeriod | null, pastObservations: Array<ObservationPeriod>, changes: Array<GraduationChange>) {
+ this.student = student;
+ this.green = green;
+ this.orange = orange;
+ this.red = red;
+ this.level = level;
+ this.observationRequested = observationRequested;
+ this.observation = observation;
+ this.pastObservations = pastObservations;
+ this.changes = changes;
+
+ student.graduation = this;
+ }
+
+ static from(student: Student, changes: Array<GraduationChange>) {
+ let green = 0;
+ let orange = 0;
+ let red = 0;
+ let level = 1;
+ let openObservationRequest = null;
+ let observationPeriod = null;
+ let pastObservations = [];
+
+ // The parsing logic assumes that changes are ordered chronologically
+ changes.sort((a, b) => a.date.valueOf() - b.date.valueOf());
+
+ 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 === null,
+ "No request for observation start");
+
+ observationPeriod = new ObservationPeriod(change.date);
+ openObservationRequest = null;
+ break;
+
+ case "ObservationEnd":
+ if (observationPeriod === null) {
+ console.error("No active observation period");
+ continue;
+ }
+ observationPeriod.finishPeriod(change.context.success);
+ if (change.context.success) {
+ level++;
+ }
+ pastObservations.push(observationPeriod);
+ observationPeriod = null;
+ break;
+
+ case "Observation":
+ if (observationPeriod === null) {
+ console.error("No active observation period");
+ continue;
+ }
+ const observation = Observation.fromChange(change);
+ observationPeriod.addObservation(observation);
+ break;
+
+ case "LevelChange":
+ level = change.context.level;
+ break;
+
+ default:
+ throw new Error(`Unexpected change: ${change}`);
+ }
+ }
+ return new Graduation(student, green, orange, red, level,
+ openObservationRequest, observationPeriod, pastObservations,
+ changes);
+ }
+
+ addChange(change: GraduationChange) {
+ change.studentId = this.student.id;
+ // 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;
+ const db = await getGraduationDb();
+ await db.saveEntry(JSON.stringify(request.dbFields()));
+ }
+
+ /**
+ * Start an observation
+ */
+ async startObservation() {
+ if (this.observationRequested === null) {
+ console.error("No observation requested");
+ return;
+ }
+ this.observationRequested = null;
+ this.observation = new ObservationPeriod(new Date());
+ return submitGraduationChange(this, GraduationChange.newObservationStart());
+ }
+
+ /**
+ * End an observation
+ */
+ async endObservation(reason: string, success: boolean) {
+ if (this.observation === null) {
+ console.error("No active observation");
+ return;
+ }
+ this.observation.end = new Date();
+ this.pastObservations.push(this.observation);
+ this.observation = null;
+ return submitGraduationChange(this, 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.
+ */
+ getLevelName() {
+ return LEVEL_NAMES[this.level - 1];
+ }
+}
+
+class ObservationPeriod {
+ start: Date;
+ end: Date | null;
+ observations: Array<Observation>;
+ success = false;
+
+ constructor(start: Date) {
+ this.start = start;
+ this.end = null;
+ this.observations = [];
+ }
+
+ finishPeriod(success: boolean) {
+ 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(null).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: 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 {
+ date: Date;
+ status: Array<string>;
+
+ constructor(date: Date, status: Array<string>) {
+ console.assert(status.length == OBSERVATION_TYPES.length, "invalid observation status");
+ this.date = date;
+ this.status = status;
+ }
+
+ static fromChange(change: GraduationChange) {
+ return new Observation(change.date, JSON.parse(change.context))
+ }
+}
+
+async function submitGraduationChange(graduation: Graduation, change: GraduationChange) {
+ graduation.addChange(change);
+ const db = await getGraduationDb();
+ return db.saveEntry(JSON.stringify(change.dbFields()));
+}
+
+async function submitObservation(graduation: Graduation, observation: Observation) {
+ if(graduation.observation === null) throw new Error("no active observation");
+ graduation.observation.addObservation(observation);
+ await submitGraduationChange(graduation, GraduationChange.newObservationChange(observation));
+}
+
+export {
+ LEVEL_NAMES,
+ LEVEL_IMAGES,
+ GraduationChange,
+ Graduation,
+ submitGraduationChange,
+ submitObservation,
+
+ OBSERVATION_TYPES,
+ Observation,
+ ObservationPeriod,
+};