diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2026-08-05 11:51:51 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2026-08-05 11:51:51 +0200 |
| commit | 582df8885a3573a728394b473b826616f8969316 (patch) | |
| tree | 9c21db03f4fbfd9956ee398e0d9194ebfa5b3234 /src/graduation-common.ts | |
| parent | 46ac53e11b49a7f9cdb92c3559b05f41ce90122f (diff) | |
| download | bycs-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.ts | 347 |
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, +}; |
