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, };