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; changes: Array; constructor(student: Student, green: number, orange: number, red: number, level: number, observationRequested: Date | null, observation: ObservationPeriod | null, pastObservations: Array, changes: Array) { 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) { 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; 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; constructor(date: Date, status: Array) { 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, };