diff --git a/api/schema/classroom_compliance.sql b/api/schema/classroom_compliance.sql index 63820a0..8e8be60 100644 --- a/api/schema/classroom_compliance.sql +++ b/api/schema/classroom_compliance.sql @@ -9,24 +9,16 @@ CREATE TABLE classroom_compliance_class ( CREATE TABLE classroom_compliance_student ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, - class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id) - ON UPDATE CASCADE ON DELETE CASCADE -); - -CREATE TABLE classroom_compliance_desk_assignment ( - id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id) ON UPDATE CASCADE ON DELETE CASCADE, - desk_number INTEGER NOT NULL, - student_id INTEGER REFERENCES classroom_compliance_student(id) - ON UPDATE CASCADE ON DELETE SET NULL + desk_number INTEGER NOT NULL DEFAULT 0, + removed INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE classroom_compliance_entry ( id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id) ON UPDATE CASCADE ON DELETE CASCADE, - class_description TEXT NOT NULL, student_id INTEGER NOT NULL REFERENCES classroom_compliance_student(id), date TEXT NOT NULL, created_at INTEGER NOT NULL, diff --git a/api/schema/sample_data.sql b/api/schema/sample_data.sql deleted file mode 100644 index 7552cb3..0000000 --- a/api/schema/sample_data.sql +++ /dev/null @@ -1,17 +0,0 @@ --- TEST DATA - --- username: test, password: test -INSERT INTO user (username, password_hash, created_at, is_locked, is_admin) VALUES ( - 'test', - '9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08', - 1734380300, - 0, - 1 -); -INSERT INTO user (username, password_hash, created_at, is_locked, is_admin) VALUES ( - 'test2', - '9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08', - 1734394062, - 0, - 0 -); diff --git a/api/source/api_modules/classroom_compliance.d b/api/source/api_modules/classroom_compliance.d index 6d2cfd3..733d8cd 100644 --- a/api/source/api_modules/classroom_compliance.d +++ b/api/source/api_modules/classroom_compliance.d @@ -2,8 +2,10 @@ module api_modules.classroom_compliance; import handy_httpd; import handy_httpd.handlers.path_handler; -import std.typecons : Nullable; import d2sqlite3; +import slf4d; +import std.typecons : Nullable; +import std.datetime; import db; import data_utils; @@ -20,19 +22,13 @@ struct ClassroomComplianceStudent { const ulong id; const string name; const ulong classId; -} - -struct ClassroomComplianceDeskAssignment { - const ulong id; - const ulong classId; const ushort deskNumber; - const ulong studentId; + const bool removed; } struct ClassroomComplianceEntry { const ulong id; const ulong classId; - const string classDescription; const ulong studentId; const string date; const ulong createdAt; @@ -56,6 +52,7 @@ void registerApiEndpoints(PathHandler handler) { handler.addMapping(Method.POST, ROOT_PATH ~ "/classes", &createClass); handler.addMapping(Method.GET, ROOT_PATH ~ "/classes", &getClasses); const CLASS_PATH = ROOT_PATH ~ "/classes/:classId:ulong"; + handler.addMapping(Method.GET, CLASS_PATH, &getClass); handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass); handler.addMapping(Method.POST, CLASS_PATH ~ "/students", &createStudent); @@ -64,11 +61,8 @@ void registerApiEndpoints(PathHandler handler) { handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent); handler.addMapping(Method.DELETE, STUDENT_PATH, &deleteStudent); - handler.addMapping(Method.POST, CLASS_PATH ~ "/desk-assignments", &setDeskAssignments); - handler.addMapping(Method.GET, CLASS_PATH ~ "/desk-assignments", &getDeskAssignments); - handler.addMapping(Method.DELETE, CLASS_PATH ~ "/desk-assignments", &removeAllDeskAssignments); - handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &createEntry); + handler.addMapping(Method.GET, CLASS_PATH ~ "/entries", &getEntries); } void createClass(ref HttpRequestContext ctx) { @@ -115,6 +109,12 @@ void getClasses(ref HttpRequestContext ctx) { writeJsonBody(ctx, classes); } +void getClass(ref HttpRequestContext ctx) { + User user = getUserOrThrow(ctx); + auto cls = getClassOrThrow(ctx, user); + writeJsonBody(ctx, cls); +} + void deleteClass(ref HttpRequestContext ctx) { User user = getUserOrThrow(ctx); auto cls = getClassOrThrow(ctx, user); @@ -138,6 +138,7 @@ void createStudent(ref HttpRequestContext ctx) { auto cls = getClassOrThrow(ctx, user); struct StudentPayload { string name; + ushort deskNumber; } auto payload = readJsonPayload!(StudentPayload)(ctx); auto db = getDb(); @@ -152,7 +153,20 @@ void createStudent(ref HttpRequestContext ctx) { ctx.response.writeBodyString("Student with that name already exists."); return; } - db.execute("INSERT INTO classroom_compliance_student (name, class_id) VALUES (?, ?)", payload.name, cls.id); + bool deskAlreadyOccupied = payload.deskNumber != 0 && canFind( + db, + "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND desk_number = ?", + cls.id, + payload.deskNumber + ); + if (deskAlreadyOccupied) { + ctx.response.status = HttpStatus.BAD_REQUEST; + ctx.response.writeBodyString("There is already a student assigned to that desk."); + } + db.execute( + "INSERT INTO classroom_compliance_student (name, class_id, desk_number) VALUES (?, ?)", + payload.name, cls.id, payload.deskNumber + ); ulong studentId = db.lastInsertRowid(); auto student = findOne!(ClassroomComplianceStudent)( db, @@ -179,10 +193,14 @@ void updateStudent(ref HttpRequestContext ctx) { auto student = getStudentOrThrow(ctx, user); struct StudentUpdatePayload { string name; + ushort deskNumber; } auto payload = readJsonPayload!(StudentUpdatePayload)(ctx); // If there is nothing to update, quit. - if (payload.name == student.name) return; + if ( + payload.name == student.name + && payload.deskNumber == student.deskNumber + ) return; // Check that the new name doesn't already exist. auto db = getDb(); bool newNameExists = canFind( @@ -196,9 +214,22 @@ void updateStudent(ref HttpRequestContext ctx) { ctx.response.writeBodyString("Student with that name already exists."); return; } + // Check that if a new desk number is assigned, that it's not already assigned to anyone else. + bool newDeskOccupied = payload.deskNumber != 0 && canFind( + db, + "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND desk_number = ?", + student.classId, + payload.deskNumber + ); + if (newDeskOccupied) { + ctx.response.status = HttpStatus.BAD_REQUEST; + ctx.response.writeBodyString("That desk is already assigned to another student."); + return; + } db.execute( - "UPDATE classroom_compliance_student SET name = ? WHERE id = ?", + "UPDATE classroom_compliance_student SET name = ?, desk_number = ? WHERE id = ?", payload.name, + payload.deskNumber, student.id ); auto updatedStudent = findOne!(ClassroomComplianceStudent)( @@ -239,135 +270,187 @@ ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, in User ).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); } -private struct DeskAssignmentPayloadEntry { - ushort deskNumber; - Nullable!ulong studentId; -} -private struct DeskAssignmentPayload { - DeskAssignmentPayloadEntry[] entries; -} - -void setDeskAssignments(ref HttpRequestContext ctx) { +void createEntry(ref HttpRequestContext ctx) { User user = getUserOrThrow(ctx); auto cls = getClassOrThrow(ctx, user); - auto payload = readJsonPayload!(DeskAssignmentPayload)(ctx); + struct EntryPhonePayload { + bool compliant; + } + struct EntryBehaviorPayload { + int rating; + Nullable!string comment; + } + struct EntryPayload { + ulong studentId; + string date; + bool absent; + Nullable!EntryPhonePayload phoneCompliance; + Nullable!EntryBehaviorPayload behaviorCompliance; + } + auto payload = readJsonPayload!(EntryPayload)(ctx); auto db = getDb(); - auto validationError = validateDeskAssignments(db, payload, cls.id); - if (validationError) { - import slf4d; - warnF!"Desk assignment validation failed: %s"(validationError.value); + bool entryAlreadyExists = canFind( + db, + "SELECT id FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?", + cls.id, + payload.studentId, + payload.date + ); + if (entryAlreadyExists) { ctx.response.status = HttpStatus.BAD_REQUEST; - ctx.response.writeBodyString(validationError.value); + ctx.response.writeBodyString("An entry already exists for this student and date."); return; } - + // Insert the entry and its attached entities in a transaction. db.begin(); try { db.execute( - "DELETE FROM classroom_compliance_desk_assignment WHERE class_id = ?", - cls.id + "INSERT INTO classroom_compliance_entry (class_id, student_id, date, created_at, absent) + VALUES (?, ?, ?, ?, ?)", + cls.id, + payload.studentId, + payload.date, + getUnixTimestampMillis(), + payload.absent ); - auto stmt = db.prepare( - "INSERT INTO classroom_compliance_desk_assignment (class_id, desk_number, student_id) VALUES (?, ?, ?)" - ); - foreach (entry; payload.entries) { - stmt.bindAll(cls.id, entry.deskNumber, entry.studentId); - stmt.execute(); - stmt.clearBindings(); - stmt.reset(); + ulong entryId = db.lastInsertRowid(); + if (!payload.absent && !payload.phoneCompliance.isNull) { + db.execute( + "INSERT INTO classroom_compliance_entry_phone (entry_id, compliant) VALUES (?, ?)", + entryId, + payload.phoneCompliance.get().compliant + ); + } + if (!payload.absent && !payload.behaviorCompliance.isNull) { + Nullable!string comment = payload.behaviorCompliance.get().comment; + if (!comment.isNull && (comment.get() is null || comment.get().length == 0)) { + comment.nullify(); + } + db.execute( + "INSERT INTO classroom_compliance_entry_behavior (entry_id, rating, comment) VALUES (?, ?, ?)", + entryId, + payload.behaviorCompliance.get().rating, + comment + ); } db.commit(); - // Return the new list of desk assignments to the user. - auto newAssignments = findAll!(ClassroomComplianceDeskAssignment)( - db, - "SELECT * FROM classroom_compliance_desk_assignment WHERE class_id = ?", - cls.id - ); - writeJsonBody(ctx, newAssignments); } catch (Exception e) { db.rollback(); } } -Optional!string validateDeskAssignments(Database db, in DeskAssignmentPayload payload, ulong classId) { - import std.algorithm : canFind, map; - import std.array; - // Check that desks are numbered 1 .. N. - for (int n = 1; n <= payload.entries.length; n++) { - bool deskPresent = false; - foreach (entry; payload.entries) { - if (entry.deskNumber == n) { - deskPresent = true; - break; - } - } - if (!deskPresent) return Optional!string.of("Desks should be numbered from 1 to N."); - } - auto allStudents = findAll!(ClassroomComplianceStudent)( - db, - "SELECT * FROM classroom_compliance_student WHERE class_id = ?", - classId - ); - auto studentIds = allStudents.map!(s => s.id).array; - // Check that if a desk is assigned to a student, that it's one from this class. - foreach (entry; payload.entries) { - if (!entry.studentId.isNull && !canFind(studentIds, entry.studentId.get)) { - return Optional!string.of("Desks cannot be assigned to students that don't belong to this class."); - } - } - - // Check that each student in the class is assigned a desk. - ushort[] takenDesks; - foreach (student; allStudents) { - ushort[] assignedDesks; - foreach (entry; payload.entries) { - if (!entry.studentId.isNull && entry.studentId.get() == student.id) { - assignedDesks ~= entry.deskNumber; - } - } - if (assignedDesks.length != 1) { - if (assignedDesks.length > 1) { - return Optional!string.of("A student is assigned to more than one desk."); - } else { - return Optional!string.of("Not all students are assigned to a desk."); - } - } - if (canFind(takenDesks, assignedDesks[0])) { - return Optional!string.of("Cannot assign more than one student to the same desk."); - } - takenDesks ~= assignedDesks[0]; - } - return Optional!string.empty(); -} - -void getDeskAssignments(ref HttpRequestContext ctx) { +void getEntries(ref HttpRequestContext ctx) { User user = getUserOrThrow(ctx); auto cls = getClassOrThrow(ctx, user); + // Default to getting entries from the last 5 days. + SysTime now = Clock.currTime(); + Date toDate = Date(now.year, now.month, now.day); + Date fromDate = toDate - days(4); + if (ctx.request.queryParams.contains("to")) { + try { + toDate = Date.fromISOExtString(ctx.request.queryParams.getFirst("to").orElse("")); + } catch (DateTimeException e) { + ctx.response.status = HttpStatus.BAD_REQUEST; + ctx.response.writeBodyString("Invalid \"to\" date."); + return; + } + } + if (ctx.request.queryParams.contains("from")) { + try { + fromDate = Date.fromISOExtString(ctx.request.queryParams.getFirst("from").orElse("")); + } catch (DateTimeException e) { + ctx.response.status = HttpStatus.BAD_REQUEST; + ctx.response.writeBodyString("Invalid \"from\" date."); + return; + } + } + infoF!"Getting entries from %s to %s"(fromDate.toISOExtString(), toDate.toISOExtString()); + auto db = getDb(); - auto deskAssignments = findAll!(ClassroomComplianceDeskAssignment)( - db, - " - SELECT d.* FROM classroom_compliance_desk_assignment d - WHERE class_id = ? - ORDER BY desk_number ASC - ", - cls.id - ); - writeJsonBody(ctx, deskAssignments); -} + const query = " + SELECT + entry.id, + entry.date, + entry.created_at, + entry.absent, + student.id, + student.name, + student.desk_number, + student.removed, + phone.compliant, + behavior.rating, + behavior.comment + FROM classroom_compliance_entry entry + LEFT JOIN classroom_compliance_entry_phone phone + ON phone.entry_id = entry.id + LEFT JOIN classroom_compliance_entry_behavior behavior + ON behavior.entry_id = entry.id + LEFT JOIN classroom_compliance_student student + ON student.id = entry.student_id + WHERE + entry.class_id = ? + AND entry.date >= ? + AND entry.date <= ? + ORDER BY + entry.date ASC, + student.desk_number ASC, + student.name ASC + "; + ResultRange result = db.execute(query, cls.id, fromDate.toISOExtString(), toDate.toISOExtString()); + // Serialize the results into a custom-formatted response object. + import std.json; + JSONValue response = JSONValue.emptyObject; + JSONValue[ulong] studentObjects; + foreach (row; result) { + ulong studentId = row.peek!ulong(4); + if (studentId !in studentObjects) { + JSONValue student = JSONValue.emptyObject; + student.object["id"] = JSONValue(row.peek!ulong(4)); + student.object["name"] = JSONValue(row.peek!string(5)); + student.object["deskNumber"] = JSONValue(row.peek!ushort(6)); + student.object["removed"] = JSONValue(row.peek!bool(7)); + student.object["entries"] = JSONValue.emptyObject; + studentObjects[studentId] = student; + } + JSONValue studentObj = studentObjects[studentId]; -void removeAllDeskAssignments(ref HttpRequestContext ctx) { - User user = getUserOrThrow(ctx); - auto cls = getClassOrThrow(ctx, user); - auto db = getDb(); - db.execute( - "DELETE FROM classroom_compliance_desk_assignment WHERE class_id = ?", - cls.id - ); -} + JSONValue entry = JSONValue.emptyObject; + entry.object["id"] = JSONValue(row.peek!ulong(0)); + entry.object["date"] = JSONValue(row.peek!string(1)); + entry.object["createdAt"] = JSONValue(row.peek!string(2)); + entry.object["absent"] = JSONValue(row.peek!bool(3)); -void createEntry(ref HttpRequestContext ctx) { - User user = getUserOrThrow(ctx); - auto cls = getClassOrThrow(ctx, user); - + JSONValue phone = JSONValue(null); + JSONValue behavior = JSONValue(null); + if (!entry.object["absent"].boolean()) { + phone = JSONValue.emptyObject; + phone.object["compliant"] = JSONValue(row.peek!bool(8)); + behavior = JSONValue.emptyObject; + behavior.object["rating"] = JSONValue(row.peek!ubyte(9)); + behavior.object["comment"] = JSONValue(row.peek!string(10)); + } + entry.object["phone"] = phone; + entry.object["behavior"] = behavior; + + string dateStr = entry.object["date"].str(); + studentObj.object["entries"].object[dateStr] = entry; + } + // Provide the list of dates that we're providing data for, to make it easier for the frontend. + response.object["dates"] = JSONValue.emptyArray; + // Also fill in "null" for any students that don't have an entry on one of these days. + Date d = fromDate; + while (d <= toDate) { + string dateStr = d.toISOExtString(); + response.object["dates"].array ~= JSONValue(dateStr); + foreach (studentObj; studentObjects) { + if (dateStr !in studentObj.object["entries"].object) { + studentObj.object["entries"].object[dateStr] = JSONValue(null); + } + } + d += days(1); + } + response.object["students"] = JSONValue(studentObjects.values); + + string jsonStr = response.toJSON(); + ctx.response.writeBodyString(jsonStr, "application/json"); } diff --git a/api/source/db.d b/api/source/db.d index 166f36a..f2079db 100644 --- a/api/source/db.d +++ b/api/source/db.d @@ -28,8 +28,8 @@ Database getDb() { db.run(authSchema); db.run(classroomComplianceSchema); - const string sampleData = import("schema/sample_data.sql"); - db.run(sampleData); + import sample_data; + insertSampleData(db); info("Initialized database schema."); } @@ -92,8 +92,8 @@ private string[] getColumnNames(T)() { } private string getArgsStr(T)() { - import std.traits : RepresentationTypeTuple; - alias types = RepresentationTypeTuple!T; + import std.traits : Fields; + alias types = Fields!T; string argsStr = ""; static foreach (i, type; types) { argsStr ~= "row.peek!(" ~ type.stringof ~ ")(" ~ i.to!string ~ ")"; diff --git a/api/source/sample_data.d b/api/source/sample_data.d new file mode 100644 index 0000000..cc2927c --- /dev/null +++ b/api/source/sample_data.d @@ -0,0 +1,127 @@ +module sample_data; + +import db; +import data_utils; +import d2sqlite3; + +import std.random; +import std.algorithm; +import std.array; +import std.datetime; + +private const STUDENT_NAMES = [ + "Andrew", + "Richard", + "Klaus", + "John", + "Wilson", + "Grace", + "Sarah", + "Rebecca", + "Lily", + "Thomas", + "Michael", + "Jennifer", + "Robert", + "Christopher", + "Margaret", + "Mordecai", + "Rigby", + "Walter", + "Roy", + "Cindy" +]; + +void insertSampleData(ref Database db) { + db.begin(); + ulong adminUserId = addUser(db, "test", "test", false, true); + ulong normalUserId = addUser(db, "test2", "test", false, false); + Random rand = Random(0); + const SysTime now = Clock.currTime(); + const Date today = Date(now.year, now.month, now.day); + + for (ushort i = 1; i <= 6; i++) { + ulong classId = addClass(db, "2024-2025", i, adminUserId); + bool classHasAssignedDesks = uniform01(rand) < 0.5; + size_t count = uniform(10, STUDENT_NAMES.length, rand); + auto studentsToAdd = randomSample(STUDENT_NAMES, count, rand); + ushort deskNumber = 1; + foreach (name; studentsToAdd) { + bool removed = uniform01(rand) < 0.1; + ushort assignedDeskNumber = 0; + if (classHasAssignedDesks) { + assignedDeskNumber = deskNumber++; + } + ulong studentId = addStudent(db, name, classId, assignedDeskNumber, removed); + + // Add entries for the last N days + for (int n = 0; n < 30; n++) { + Date entryDate = today - days(n); + bool missingEntry = uniform01(rand) < 0.05; + if (missingEntry) continue; + + 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); + } + } + } + db.commit(); +} + +ulong addUser(ref Database db, string username, string password, bool locked, bool admin) { + const query = "INSERT INTO user (username, password_hash, created_at, is_locked, is_admin) VALUES (?, ?, ?, ?, ?)"; + import std.digest.sha; + import std.stdio; + string passwordHash = cast(string) sha256Of(password).toHexString().idup; + db.execute(query, username, passwordHash, getUnixTimestampMillis(), locked, admin); + return db.lastInsertRowid(); +} + +ulong addClass(ref Database db, string schoolYear, ushort number, ulong userId) { + const query = "INSERT INTO classroom_compliance_class (number, school_year, user_id) VALUES (?, ?, ?)"; + db.execute(query, number, schoolYear, userId); + return db.lastInsertRowid(); +} + +ulong addStudent(ref Database db, string name, ulong classId, ushort deskNumber, bool removed) { + const query = "INSERT INTO classroom_compliance_student (name, class_id, desk_number, removed) VALUES (?, ?, ?, ?)"; + db.execute(query, name, classId, deskNumber, removed); + return db.lastInsertRowid(); +} + +void addEntry( + ref Database db, + ulong classId, + ulong studentId, + Date date, + bool absent, + bool phoneCompliant, + ubyte behaviorRating, + string behaviorComment +) { + 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); + 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); +} diff --git a/app/src/api/classroom_compliance.ts b/app/src/api/classroom_compliance.ts index d63f277..75fd560 100644 --- a/app/src/api/classroom_compliance.ts +++ b/app/src/api/classroom_compliance.ts @@ -8,6 +8,49 @@ export interface Class { schoolYear: string } +export interface Student { + id: number + name: string + classId: number + deskNumber: number + removed: boolean +} + +export interface EntryResponseItemPhone { + compliant: boolean +} + +export interface EntryResponseItemBehavior { + rating: number + comment?: string +} + +export interface EntryResponseItem { + id: number + date: string + createdAt: string + absent: boolean + phone?: EntryResponseItemPhone + behavior?: EntryResponseItemBehavior +} + +export interface EntryResponseStudent { + id: number + name: string + deskNumber: number + removed: boolean + entries: Record +} + +export interface EntriesResponse { + students: EntryResponseStudent[] + dates: string[] +} + +function getClassPath(classId: number): string { + return BASE_URL + '/classes/' + classId +} + export async function createClass( auth: string, number: number, @@ -28,8 +71,15 @@ export async function getClasses(auth: string): Promise { return (await response.json()) as Class[] } +export async function getClass(auth: string, id: number): Promise { + const response = await fetch(getClassPath(id), { + headers: getAuthHeaders(auth), + }) + return (await response.json()) as Class +} + export async function deleteClass(auth: string, classId: number): Promise { - const response = await fetch(BASE_URL + '/classes/' + classId, { + const response = await fetch(getClassPath(classId), { method: 'DELETE', headers: getAuthHeaders(auth), }) @@ -37,3 +87,29 @@ export async function deleteClass(auth: string, classId: number): Promise throw new Error('Failed to delete class.') } } + +export async function getStudents(auth: string, classId: number): Promise { + const response = await fetch(getClassPath(classId) + '/students', { + headers: getAuthHeaders(auth), + }) + return (await response.json()) as Student[] +} + +export async function getEntries( + auth: string, + classId: number, + fromDate?: Date, + toDate?: Date, +): Promise { + const params = new URLSearchParams() + if (fromDate) { + params.append('from', fromDate.toISOString().substring(0, 10)) + } + if (toDate) { + params.append('to', toDate.toISOString().substring(0, 10)) + } + const response = await fetch(getClassPath(classId) + '/entries?' + params.toString(), { + headers: getAuthHeaders(auth), + }) + return (await response.json()) as EntriesResponse +} diff --git a/app/src/apps/classroom_compliance/ClassItem.vue b/app/src/apps/classroom_compliance/ClassItem.vue new file mode 100644 index 0000000..b198f45 --- /dev/null +++ b/app/src/apps/classroom_compliance/ClassItem.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/src/apps/classroom_compliance/ClassView.vue b/app/src/apps/classroom_compliance/ClassView.vue new file mode 100644 index 0000000..8e43327 --- /dev/null +++ b/app/src/apps/classroom_compliance/ClassView.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/src/apps/classroom_compliance/ClassesView.vue b/app/src/apps/classroom_compliance/ClassesView.vue new file mode 100644 index 0000000..2b1cbeb --- /dev/null +++ b/app/src/apps/classroom_compliance/ClassesView.vue @@ -0,0 +1,17 @@ + + diff --git a/app/src/apps/classroom_compliance/EntriesTable.vue b/app/src/apps/classroom_compliance/EntriesTable.vue new file mode 100644 index 0000000..662f2fc --- /dev/null +++ b/app/src/apps/classroom_compliance/EntriesTable.vue @@ -0,0 +1,90 @@ + + + diff --git a/app/src/apps/classroom_compliance/EntryTableCell.vue b/app/src/apps/classroom_compliance/EntryTableCell.vue new file mode 100644 index 0000000..e9f8b21 --- /dev/null +++ b/app/src/apps/classroom_compliance/EntryTableCell.vue @@ -0,0 +1,46 @@ + + + diff --git a/app/src/apps/classroom_compliance/MainView.vue b/app/src/apps/classroom_compliance/MainView.vue new file mode 100644 index 0000000..f58bdc7 --- /dev/null +++ b/app/src/apps/classroom_compliance/MainView.vue @@ -0,0 +1,16 @@ + + diff --git a/app/src/router/index.ts b/app/src/router/index.ts index d890ad5..8db28cd 100644 --- a/app/src/router/index.ts +++ b/app/src/router/index.ts @@ -15,7 +15,18 @@ const router = createRouter({ }, { path: '/classroom-compliance', - component: () => import('@/views/apps/ClassroomCompliance.vue'), + component: () => import('@/apps/classroom_compliance/MainView.vue'), + children: [ + { + path: '', + component: () => import('@/apps/classroom_compliance/ClassesView.vue'), + }, + { + path: 'classes/:id', + component: () => import('@/apps/classroom_compliance/ClassView.vue'), + props: true, + }, + ], }, ], }) diff --git a/app/src/views/apps/ClassroomCompliance.vue b/app/src/views/apps/ClassroomCompliance.vue deleted file mode 100644 index 9017757..0000000 --- a/app/src/views/apps/ClassroomCompliance.vue +++ /dev/null @@ -1,24 +0,0 @@ - - diff --git a/bruno-api/Classroom Compliance/Get Compliance Entries.bru b/bruno-api/Classroom Compliance/Get Compliance Entries.bru new file mode 100644 index 0000000..bcad6ff --- /dev/null +++ b/bruno-api/Classroom Compliance/Get Compliance Entries.bru @@ -0,0 +1,15 @@ +meta { + name: Get Compliance Entries + type: http + seq: 9 +} + +get { + url: {{base_url}}/classroom-compliance/classes/:classId/entries + body: none + auth: none +} + +params:path { + classId: {{class_id}} +} diff --git a/bruno-api/Classroom Compliance/Get Desk Assignments.bru b/bruno-api/Classroom Compliance/Get Desk Assignments.bru deleted file mode 100644 index 85d4a7e..0000000 --- a/bruno-api/Classroom Compliance/Get Desk Assignments.bru +++ /dev/null @@ -1,15 +0,0 @@ -meta { - name: Get Desk Assignments - type: http - seq: 8 -} - -get { - url: {{base_url}}/classroom-compliance/classes/:classId/desk-assignments - body: none - auth: inherit -} - -params:path { - classId: {{class_id}} -} diff --git a/bruno-api/Classroom Compliance/Set Desk Assignments.bru b/bruno-api/Classroom Compliance/Set Desk Assignments.bru deleted file mode 100644 index 0571d1c..0000000 --- a/bruno-api/Classroom Compliance/Set Desk Assignments.bru +++ /dev/null @@ -1,30 +0,0 @@ -meta { - name: Set Desk Assignments - type: http - seq: 9 -} - -post { - url: {{base_url}}/classroom-compliance/classes/:classId/desk-assignments - body: json - auth: inherit -} - -params:path { - classId: {{class_id}} -} - -body:json { - { - "entries": [ - { - "deskNumber": 1, - "studentId": 1 - }, - { - "deskNumber": 2, - "studentId": 2 - } - ] - } -}