diff --git a/api/schema/classroom_compliance.sql b/api/schema/classroom_compliance.sql index a8f1b7a..fbe4745 100644 --- a/api/schema/classroom_compliance.sql +++ b/api/schema/classroom_compliance.sql @@ -23,7 +23,8 @@ CREATE TABLE classroom_compliance_entry ( ON UPDATE CASCADE ON DELETE CASCADE, date TEXT NOT NULL, created_at INTEGER NOT NULL, - absent INTEGER NOT NULL DEFAULT 0 + absent INTEGER NOT NULL DEFAULT 0, + comment TEXT NOT NULL DEFAULT '' ); CREATE TABLE classroom_compliance_entry_phone ( diff --git a/api/source/api_modules/classroom_compliance.d b/api/source/api_modules/classroom_compliance.d index 2eae6f2..b925ed1 100644 --- a/api/source/api_modules/classroom_compliance.d +++ b/api/source/api_modules/classroom_compliance.d @@ -36,6 +36,7 @@ struct ClassroomComplianceEntry { const string date; const ulong createdAt; const bool absent; + const string comment; } struct ClassroomComplianceEntryPhone { @@ -63,6 +64,7 @@ void registerApiEndpoints(PathHandler handler) { handler.addMapping(Method.GET, STUDENT_PATH, &getStudent); handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent); handler.addMapping(Method.DELETE, STUDENT_PATH, &deleteStudent); + handler.addMapping(Method.GET, STUDENT_PATH ~ "/entries", &getStudentEntries); handler.addMapping(Method.GET, CLASS_PATH ~ "/entries", &getEntries); handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &saveEntries); @@ -344,6 +346,7 @@ void getEntries(ref HttpRequestContext ctx) { entry.date, entry.created_at, entry.absent, + entry.comment, student.id, student.name, student.desk_number, @@ -373,21 +376,22 @@ void getEntries(ref HttpRequestContext ctx) { entry.object["date"] = JSONValue(row.peek!string(1)); entry.object["createdAt"] = JSONValue(row.peek!ulong(2)); entry.object["absent"] = JSONValue(row.peek!bool(3)); + entry.object["comment"] = JSONValue(row.peek!string(4)); JSONValue phone = JSONValue(null); JSONValue behavior = JSONValue(null); if (!entry.object["absent"].boolean()) { phone = JSONValue.emptyObject; - phone.object["compliant"] = JSONValue(row.peek!bool(8)); + phone.object["compliant"] = JSONValue(row.peek!bool(9)); behavior = JSONValue.emptyObject; - behavior.object["rating"] = JSONValue(row.peek!ubyte(9)); + behavior.object["rating"] = JSONValue(row.peek!ubyte(10)); } entry.object["phone"] = phone; entry.object["behavior"] = behavior; string dateStr = entry.object["date"].str(); // Find the student object this entry belongs to, then add it to their list. - ulong studentId = row.peek!ulong(4); + ulong studentId = row.peek!ulong(5); bool studentFound = false; foreach (idx, student; students) { if (student.id == studentId) { @@ -525,11 +529,13 @@ private void insertNewEntry( ) { ulong createdAt = getUnixTimestampMillis(); bool absent = payload.object["absent"].boolean; + string comment = payload.object["comment"].str; + if (comment is null) comment = ""; db.execute( "INSERT INTO classroom_compliance_entry - (class_id, student_id, date, created_at, absent) - VALUES (?, ?, ?, ?, ?)", - classId, studentId, dateStr, createdAt, absent + (class_id, student_id, date, created_at, absent, comment) + VALUES (?, ?, ?, ?, ?, ?)", + classId, studentId, dateStr, createdAt, absent, comment ); if (!absent) { ulong entryId = db.lastInsertRowid(); @@ -564,11 +570,14 @@ private void updateEntry( JSONValue obj ) { bool absent = obj.object["absent"].boolean; + string comment = obj.object["comment"].str; + if (comment is null) comment = ""; db.execute( "UPDATE classroom_compliance_entry - SET absent = ? + SET absent = ?, comment = ? WHERE class_id = ? AND student_id = ? AND date = ? AND id = ?", - absent, classId, studentId, dateStr, entryId + absent, comment, + classId, studentId, dateStr, entryId ); if (absent) { db.execute( @@ -726,3 +735,51 @@ private Optional!double calculateScore( double score = 0.3 * phoneScore + 0.7 * behaviorScore; return Optional!double.of(score); } + +void getStudentEntries(ref HttpRequestContext ctx) { + auto db = getDb(); + User user = getUserOrThrow(ctx, db); + auto student = getStudentOrThrow(ctx, db, user); + + const query = " + SELECT + e.id, + e.date, + e.created_at, + e.absent, + e.comment, + p.compliant, + b.rating + FROM classroom_compliance_entry e + LEFT JOIN classroom_compliance_entry_phone p + ON p.entry_id = e.id + LEFT JOIN classroom_compliance_entry_behavior b + ON b.entry_id = e.id + WHERE + e.student_id = ? + ORDER BY e.date DESC + "; + JSONValue response = JSONValue.emptyArray; + foreach (row; db.execute(query, student.id)) { + JSONValue e = JSONValue.emptyObject; + bool absent = row.peek!bool(3); + e.object["id"] = JSONValue(row.peek!ulong(0)); + e.object["date"] = JSONValue(row.peek!string(1)); + e.object["createdAt"] = JSONValue(row.peek!ulong(2)); + e.object["absent"] = JSONValue(absent); + e.object["comment"] = JSONValue(row.peek!string(4)); + if (absent) { + e.object["phone"] = JSONValue(null); + e.object["behavior"] = JSONValue(null); + } else { + JSONValue phone = JSONValue.emptyObject; + phone.object["compliant"] = JSONValue(row.peek!bool(5)); + e.object["phone"] = phone; + JSONValue behavior = JSONValue.emptyObject; + behavior.object["rating"] = JSONValue(row.peek!ubyte(6)); + e.object["behavior"] = behavior; + } + response.array ~= e; + } + ctx.response.writeBodyString(response.toJSON(), "application/json"); +} diff --git a/api/source/sample_data.d b/api/source/sample_data.d index cc2927c..1b2f8cb 100644 --- a/api/source/sample_data.d +++ b/api/source/sample_data.d @@ -63,16 +63,13 @@ void insertSampleData(ref Database db) { bool absent = uniform01(rand) < 0.05; bool phoneCompliant = uniform01(rand) < 0.85; ubyte behaviorRating = 3; - string behaviorComment = null; if (uniform01(rand) < 0.25) { behaviorRating = 2; - behaviorComment = "They did not participate enough."; if (uniform01(rand) < 0.5) { behaviorRating = 3; - behaviorComment = "They are a horrible student."; } } - addEntry(db, classId, studentId, entryDate, absent, phoneCompliant, behaviorRating, behaviorComment); + addEntry(db, classId, studentId, entryDate, absent, phoneCompliant, behaviorRating); } } } @@ -107,21 +104,28 @@ void addEntry( Date date, bool absent, bool phoneCompliant, - ubyte behaviorRating, - string behaviorComment + ubyte behaviorRating ) { const entryQuery = " INSERT INTO classroom_compliance_entry - (class_id, student_id, date, created_at, absent) - VALUES (?, ?, ?, ?, ?)"; - db.execute(entryQuery, classId, studentId, date.toISOExtString(), getUnixTimestampMillis(), absent); + (class_id, student_id, date, created_at, absent, comment) + VALUES (?, ?, ?, ?, ?, ?)"; + db.execute( + entryQuery, + classId, + studentId, + date.toISOExtString(), + getUnixTimestampMillis(), + absent, + "Sample comment." + ); if (absent) return; ulong entryId = db.lastInsertRowid(); const phoneQuery = "INSERT INTO classroom_compliance_entry_phone (entry_id, compliant) VALUES (?, ?)"; db.execute(phoneQuery, entryId, phoneCompliant); const behaviorQuery = " INSERT INTO classroom_compliance_entry_behavior - (entry_id, rating, comment) - VALUES (?, ?, ?)"; - db.execute(behaviorQuery, entryId, behaviorRating, behaviorComment); + (entry_id, rating) + VALUES (?, ?)"; + db.execute(behaviorQuery, entryId, behaviorRating); } diff --git a/app/src/api/classroom_compliance.ts b/app/src/api/classroom_compliance.ts index 4b22b43..e41ab8a 100644 --- a/app/src/api/classroom_compliance.ts +++ b/app/src/api/classroom_compliance.ts @@ -2,6 +2,14 @@ import { APIClient, type APIResponse, type AuthStoreType } from './base' const BASE_URL = import.meta.env.VITE_API_URL + '/classroom-compliance' +export const EMOJI_PHONE_COMPLIANT = '📱' +export const EMOJI_PHONE_NONCOMPLIANT = '📵' +export const EMOJI_PRESENT = '✅' +export const EMOJI_ABSENT = '❌' +export const EMOJI_BEHAVIOR_GOOD = '😇' +export const EMOJI_BEHAVIOR_MEDIOCRE = '😐' +export const EMOJI_BEHAVIOR_POOR = '😡' + export interface Class { id: number number: number @@ -31,6 +39,7 @@ export interface Entry { absent: boolean phone: EntryPhone | null behavior: EntryBehavior | null + comment: string } export function getDefaultEntry(dateStr: string): Entry { @@ -41,6 +50,7 @@ export function getDefaultEntry(dateStr: string): Entry { absent: false, phone: { compliant: true }, behavior: { rating: 3 }, + comment: '', } } @@ -148,4 +158,8 @@ export class ClassroomComplianceAPIClient extends APIClient { params.append('to', toDate.toISOString().substring(0, 10)) return super.get(`/classes/${classId}/scores?${params.toString()}`) } + + getStudentEntries(classId: number, studentId: number): APIResponse { + return super.get(`/classes/${classId}/students/${studentId}/entries`) + } } diff --git a/app/src/apps/classroom_compliance/ClassView.vue b/app/src/apps/classroom_compliance/ClassView.vue index 41974f1..1cd48a7 100644 --- a/app/src/apps/classroom_compliance/ClassView.vue +++ b/app/src/apps/classroom_compliance/ClassView.vue @@ -32,8 +32,7 @@ async function deleteThisClass() {