diff --git a/api/schema/classroom_compliance.sql b/api/schema/classroom_compliance.sql
index b1486c6..a8f1b7a 100644
--- a/api/schema/classroom_compliance.sql
+++ b/api/schema/classroom_compliance.sql
@@ -35,6 +35,5 @@ CREATE TABLE classroom_compliance_entry_phone (
CREATE TABLE classroom_compliance_entry_behavior (
entry_id INTEGER PRIMARY KEY REFERENCES classroom_compliance_entry(id)
ON UPDATE CASCADE ON DELETE CASCADE,
- rating INTEGER NOT NULL,
- comment TEXT
+ rating INTEGER NOT NULL
);
diff --git a/api/source/api_modules/auth.d b/api/source/api_modules/auth.d
index 623a70b..40dd086 100644
--- a/api/source/api_modules/auth.d
+++ b/api/source/api_modules/auth.d
@@ -25,7 +25,7 @@ private struct UserResponse {
bool isAdmin;
}
-Optional!User getUser(ref HttpRequestContext ctx) {
+Optional!User getUser(ref HttpRequestContext ctx, ref Database db) {
import std.base64;
import std.string : startsWith;
import std.digest.sha;
@@ -40,7 +40,6 @@ Optional!User getUser(ref HttpRequestContext ctx) {
size_t idx = countUntil(decoded, ':');
string username = decoded[0..idx];
auto passwordHash = toHexString(sha256Of(decoded[idx+1 .. $]));
- Database db = getDb();
Optional!User optUser = findOne!(User)(db, "SELECT * FROM user WHERE username = ?", username);
if (!optUser.isNull && optUser.value.passwordHash != passwordHash) {
return Optional!User.empty;
@@ -48,8 +47,8 @@ Optional!User getUser(ref HttpRequestContext ctx) {
return optUser;
}
-User getUserOrThrow(ref HttpRequestContext ctx) {
- Optional!User optUser = getUser(ctx);
+User getUserOrThrow(ref HttpRequestContext ctx, ref Database db) {
+ Optional!User optUser = getUser(ctx, db);
if (optUser.isNull) {
throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
}
@@ -57,7 +56,8 @@ User getUserOrThrow(ref HttpRequestContext ctx) {
}
void loginEndpoint(ref HttpRequestContext ctx) {
- Optional!User optUser = getUser(ctx);
+ Database db = getDb();
+ Optional!User optUser = getUser(ctx, db);
if (optUser.isNull) {
ctx.response.status = HttpStatus.UNAUTHORIZED;
ctx.response.writeBodyString("Invalid credentials.");
diff --git a/api/source/api_modules/classroom_compliance.d b/api/source/api_modules/classroom_compliance.d
index 32f0d0c..2eae6f2 100644
--- a/api/source/api_modules/classroom_compliance.d
+++ b/api/source/api_modules/classroom_compliance.d
@@ -6,7 +6,6 @@ import d2sqlite3;
import slf4d;
import std.typecons : Nullable;
import std.datetime;
-import std.format;
import std.json;
import std.algorithm;
import std.array;
@@ -47,7 +46,6 @@ struct ClassroomComplianceEntryPhone {
struct ClassroomComplianceEntryBehavior {
const ulong entryId;
const ubyte rating;
- const string comment;
}
void registerApiEndpoints(PathHandler handler) {
@@ -71,13 +69,13 @@ void registerApiEndpoints(PathHandler handler) {
}
void createClass(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
+ auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
struct ClassPayload {
ushort number;
string schoolYear;
}
auto payload = readJsonPayload!(ClassPayload)(ctx);
- auto db = getDb();
const bool classNumberExists = canFind(
db,
"SELECT id FROM classroom_compliance_class WHERE number = ? AND school_year = ? AND user_id = ?",
@@ -104,8 +102,8 @@ void createClass(ref HttpRequestContext ctx) {
}
void getClasses(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
auto classes = findAll!(ClassroomComplianceClass)(
db,
"SELECT * FROM classroom_compliance_class WHERE user_id = ? ORDER BY school_year DESC, number ASC",
@@ -115,21 +113,21 @@ void getClasses(ref HttpRequestContext ctx) {
}
void getClass(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
- auto cls = getClassOrThrow(ctx, user);
+ auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
+ auto cls = getClassOrThrow(ctx, db, user);
writeJsonBody(ctx, cls);
}
void deleteClass(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
- auto cls = getClassOrThrow(ctx, user);
auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
+ auto cls = getClassOrThrow(ctx, db, user);
db.execute("DELETE FROM classroom_compliance_class WHERE id = ? AND user_id = ?", cls.id, user.id);
}
-ClassroomComplianceClass getClassOrThrow(ref HttpRequestContext ctx, in User user) {
+ClassroomComplianceClass getClassOrThrow(ref HttpRequestContext ctx, ref Database db, in User user) {
ulong classId = ctx.request.getPathParamAs!ulong("classId");
- auto db = getDb();
return findOne!(ClassroomComplianceClass)(
db,
"SELECT * FROM classroom_compliance_class WHERE user_id = ? AND id = ?",
@@ -139,15 +137,15 @@ ClassroomComplianceClass getClassOrThrow(ref HttpRequestContext ctx, in User use
}
void createStudent(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
- auto cls = getClassOrThrow(ctx, user);
+ auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
+ auto cls = getClassOrThrow(ctx, db, user);
struct StudentPayload {
string name;
ushort deskNumber;
bool removed;
}
auto payload = readJsonPayload!(StudentPayload)(ctx);
- auto db = getDb();
bool studentExists = canFind(
db,
"SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
@@ -183,9 +181,9 @@ void createStudent(ref HttpRequestContext ctx) {
}
void getStudents(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
- auto cls = getClassOrThrow(ctx, user);
auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
+ auto cls = getClassOrThrow(ctx, db, user);
auto students = findAll!(ClassroomComplianceStudent)(
db,
"SELECT * FROM classroom_compliance_student WHERE class_id = ? ORDER BY name ASC",
@@ -195,14 +193,16 @@ void getStudents(ref HttpRequestContext ctx) {
}
void getStudent(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
- auto student = getStudentOrThrow(ctx, user);
+ auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
+ auto student = getStudentOrThrow(ctx, db, user);
writeJsonBody(ctx, student);
}
void updateStudent(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
- auto student = getStudentOrThrow(ctx, user);
+ auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
+ auto student = getStudentOrThrow(ctx, db, user);
struct StudentUpdatePayload {
string name;
ushort deskNumber;
@@ -216,7 +216,6 @@ void updateStudent(ref HttpRequestContext ctx) {
&& payload.removed == student.removed
) return;
// Check that the new name doesn't already exist.
- auto db = getDb();
bool newNameExists = payload.name != student.name && canFind(
db,
"SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
@@ -256,9 +255,9 @@ void updateStudent(ref HttpRequestContext ctx) {
}
void deleteStudent(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
- auto student = getStudentOrThrow(ctx, user);
auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
+ auto student = getStudentOrThrow(ctx, db, user);
db.execute(
"DELETE FROM classroom_compliance_student WHERE id = ? AND class_id = ?",
student.id,
@@ -266,10 +265,9 @@ void deleteStudent(ref HttpRequestContext ctx) {
);
}
-ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, in User user) {
+ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, ref Database db, in User user) {
ulong classId = ctx.request.getPathParamAs!ulong("classId");
ulong studentId = ctx.request.getPathParamAs!ulong("studentId");
- auto db = getDb();
string query = "
SELECT s.*
FROM classroom_compliance_student s
@@ -286,8 +284,9 @@ ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, in User
}
void getEntries(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
- auto cls = getClassOrThrow(ctx, user);
+ auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
+ auto cls = getClassOrThrow(ctx, db, user);
// Default to getting entries from the last 5 days.
SysTime now = Clock.currTime();
Date toDate = Date(now.year, now.month, now.day);
@@ -310,10 +309,19 @@ void getEntries(ref HttpRequestContext ctx) {
return;
}
}
+ if (fromDate > toDate) {
+ ctx.response.status = HttpStatus.BAD_REQUEST;
+ ctx.response.writeBodyString("Invalid date range. From-date must be less than or equal to the to-date.");
+ return;
+ }
+ if (toDate - fromDate > days(10)) {
+ ctx.response.status = HttpStatus.BAD_REQUEST;
+ ctx.response.writeBodyString("Date range is too big. Only ranges of 10 days or less are allowed.");
+ return;
+ }
infoF!"Getting entries from %s to %s"(fromDate.toISOExtString(), toDate.toISOExtString());
- auto db = getDb();
-
+ // First prepare a list of all students, including ones which don't have any entries.
ClassroomComplianceStudent[] students = findAll!(ClassroomComplianceStudent)(
db,
"SELECT * FROM classroom_compliance_student WHERE class_id = ? ORDER BY name ASC",
@@ -326,10 +334,11 @@ void getEntries(ref HttpRequestContext ctx) {
obj.object["name"] = JSONValue(s.name);
obj.object["removed"] = JSONValue(s.removed);
obj.object["entries"] = JSONValue.emptyObject;
+ obj.object["score"] = JSONValue(null);
return obj;
}).array;
- const query = "
+ const entriesQuery = "
SELECT
entry.id,
entry.date,
@@ -340,8 +349,7 @@ void getEntries(ref HttpRequestContext ctx) {
student.desk_number,
student.removed,
phone.compliant,
- behavior.rating,
- behavior.comment
+ behavior.rating
FROM classroom_compliance_entry entry
LEFT JOIN classroom_compliance_entry_phone phone
ON phone.entry_id = entry.id
@@ -357,9 +365,9 @@ void getEntries(ref HttpRequestContext ctx) {
student.id ASC,
entry.date ASC
";
- ResultRange result = db.execute(query, cls.id, fromDate.toISOExtString(), toDate.toISOExtString());
+ ResultRange entriesResult = db.execute(entriesQuery, cls.id, fromDate.toISOExtString(), toDate.toISOExtString());
// Serialize the results into a custom-formatted response object.
- foreach (row; result) {
+ foreach (row; entriesResult) {
JSONValue entry = JSONValue.emptyObject;
entry.object["id"] = JSONValue(row.peek!ulong(0));
entry.object["date"] = JSONValue(row.peek!string(1));
@@ -373,7 +381,6 @@ void getEntries(ref HttpRequestContext ctx) {
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;
@@ -394,6 +401,23 @@ void getEntries(ref HttpRequestContext ctx) {
}
}
+ // Find scores for each student for this timeframe.
+ Optional!double[ulong] scores = getScores(db, cls.id, fromDate, toDate);
+ foreach (studentId, score; scores) {
+ JSONValue scoreValue = score.isNull ? JSONValue(null) : JSONValue(score.value);
+ bool studentFound = false;
+ foreach (idx, student; students) {
+ if (studentId == student.id) {
+ studentObjects[idx].object["score"] = scoreValue;
+ studentFound = true;
+ break;
+ }
+ }
+ if (!studentFound) {
+ throw new Exception("Failed to find student.");
+ }
+ }
+
JSONValue response = JSONValue.emptyObject;
// Provide the list of dates that we're providing data for, to make it easier for the frontend.
response.object["dates"] = JSONValue.emptyArray;
@@ -417,63 +441,79 @@ void getEntries(ref HttpRequestContext ctx) {
}
void saveEntries(ref HttpRequestContext ctx) {
- User user = getUserOrThrow(ctx);
- auto cls = getClassOrThrow(ctx, user);
- JSONValue bodyContent = ctx.request.readBodyAsJson();
auto db = getDb();
+ User user = getUserOrThrow(ctx, db);
+ auto cls = getClassOrThrow(ctx, db, user);
+ JSONValue bodyContent = ctx.request.readBodyAsJson();
db.begin();
- foreach (JSONValue studentObj; bodyContent.object["students"].array) {
- ulong studentId = studentObj.object["id"].integer();
- JSONValue entries = studentObj.object["entries"];
- foreach (string dateStr, JSONValue entry; entries.object) {
- if (entry.isNull) {
- db.execute(
- "DELETE FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
- cls.id,
- studentId,
- dateStr
+ try {
+ foreach (JSONValue studentObj; bodyContent.object["students"].array) {
+ ulong studentId = studentObj.object["id"].integer();
+ JSONValue entries = studentObj.object["entries"];
+ foreach (string dateStr, JSONValue entry; entries.object) {
+ if (entry.isNull) {
+ deleteEntry(db, cls.id, studentId, dateStr);
+ continue;
+ }
+
+ Optional!ClassroomComplianceEntry existingEntry = findOne!(ClassroomComplianceEntry)(
+ db,
+ "SELECT * FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
+ cls.id, studentId, dateStr
);
- infoF!"Deleted entry for student %s on %s"(studentId, dateStr);
- continue;
- }
- Optional!ClassroomComplianceEntry existingEntry = findOne!(ClassroomComplianceEntry)(
- db,
- "SELECT * FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
- cls.id, studentId, dateStr
- );
+ ulong entryId = entry.object["id"].integer();
+ bool creatingNewEntry = entryId == 0;
- ulong entryId = entry.object["id"].integer();
- bool creatingNewEntry = entryId == 0;
+ if (creatingNewEntry) {
+ if (!existingEntry.isNull) {
+ ctx.response.status = HttpStatus.BAD_REQUEST;
+ ctx.response.writeBodyString("Cannot create a new entry when one already exists.");
+ return;
+ }
- if (creatingNewEntry) {
- if (!existingEntry.isNull) {
- ctx.response.status = HttpStatus.BAD_REQUEST;
- ctx.response.writeBodyString(
- format!"Cannot create a new entry for student %d on %s when one already exists."(
- studentId,
- dateStr
- )
- );
- return;
+ insertNewEntry(db, cls.id, studentId, dateStr, entry);
+ } else {
+ if (existingEntry.isNull) {
+ ctx.response.status = HttpStatus.BAD_REQUEST;
+ ctx.response.writeBodyString("Cannot update an entry which doesn't exist.");
+ return;
+ }
+ updateEntry(db, cls.id, studentId, dateStr, entryId, entry);
}
-
- insertNewEntry(db, cls.id, studentId, dateStr, entry);
- } else {
- if (existingEntry.isNull) {
- ctx.response.status = HttpStatus.BAD_REQUEST;
- ctx.response.writeBodyString(
- format!"Cannot update entry %d because it doesn't exist."(
- entryId
- )
- );
- return;
- }
- updateEntry(db, cls.id, studentId, dateStr, entryId, entry);
}
}
+ db.commit();
+ } catch (HttpStatusException e) {
+ db.rollback();
+ ctx.response.status = e.status;
+ ctx.response.writeBodyString(e.message);
+ } catch (JSONException e) {
+ db.rollback();
+ ctx.response.status = HttpStatus.BAD_REQUEST;
+ ctx.response.writeBodyString("Invalid JSON payload.");
+ warn(e);
+ } catch (Exception e) {
+ db.rollback();
+ ctx.response.status = HttpStatus.INTERNAL_SERVER_ERROR;
+ ctx.response.writeBodyString("An internal server error occurred: " ~ e.msg);
+ error(e);
}
- db.commit();
+}
+
+private void deleteEntry(
+ ref Database db,
+ ulong classId,
+ ulong studentId,
+ string dateStr
+) {
+ db.execute(
+ "DELETE FROM classroom_compliance_entry WHERE class_id = ? AND student_id = ? AND date = ?",
+ classId,
+ studentId,
+ dateStr
+ );
+ infoF!"Deleted entry for student %s on %s"(studentId, dateStr);
}
private void insertNewEntry(
@@ -507,9 +547,9 @@ private void insertNewEntry(
);
ubyte behaviorRating = cast(ubyte) payload.object["behavior"].object["rating"].integer;
db.execute(
- "INSERT INTO classroom_compliance_entry_behavior (entry_id, rating, comment)
- VALUES (?, ?, ?)",
- entryId, behaviorRating, ""
+ "INSERT INTO classroom_compliance_entry_behavior (entry_id, rating)
+ VALUES (?, ?)",
+ entryId, behaviorRating
);
}
infoF!"Created new entry for student %d: %s"(studentId, payload);
@@ -587,3 +627,102 @@ private void updateEntry(
}
infoF!"Updated entry %d"(entryId);
}
+
+Optional!double[ulong] getScores(
+ ref Database db,
+ ulong classId,
+ Date fromDate,
+ Date toDate
+) {
+ infoF!"Getting scores from %s to %s"(fromDate.toISOExtString(), toDate.toISOExtString());
+
+ // First populate all students with an initial "null" score.
+ Optional!double[ulong] scores;
+ ResultRange studentsResult = db.execute(
+ "SELECT id FROM classroom_compliance_student WHERE class_id = ?",
+ classId
+ );
+ foreach (row; studentsResult) {
+ scores[row.peek!ulong(0)] = Optional!double.empty;
+ }
+
+ const query = "
+ SELECT
+ e.student_id,
+ COUNT(e.id) AS entry_count,
+ SUM(e.absent) AS absence_count,
+ SUM(NOT p.compliant) AS phone_noncompliance_count,
+ SUM(b.rating = 3) AS behavior_good,
+ SUM(b.rating = 2) AS behavior_mediocre,
+ SUM(b.rating = 1) AS behavior_poor
+ 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.date >= ?
+ AND e.date <= ?
+ AND e.class_id = ?
+ GROUP BY e.student_id
+ ";
+ ResultRange result = db.execute(
+ query,
+ fromDate.toISOExtString(),
+ toDate.toISOExtString(),
+ classId
+ );
+ foreach (row; result) {
+ ulong studentId = row.peek!ulong(0);
+ uint entryCount = row.peek!uint(1);
+ uint absenceCount = row.peek!uint(2);
+ uint phoneNonComplianceCount = row.peek!uint(3);
+ uint behaviorGoodCount = row.peek!uint(4);
+ uint behaviorMediocreCount = row.peek!uint(5);
+ uint behaviorPoorCount = row.peek!uint(6);
+ scores[studentId] = calculateScore(
+ entryCount,
+ absenceCount,
+ phoneNonComplianceCount,
+ behaviorGoodCount,
+ behaviorMediocreCount,
+ behaviorPoorCount
+ );
+ }
+ return scores;
+}
+
+private Optional!double calculateScore(
+ uint entryCount,
+ uint absenceCount,
+ uint phoneNonComplianceCount,
+ uint behaviorGoodCount,
+ uint behaviorMediocreCount,
+ uint behaviorPoorCount
+) {
+ if (
+ entryCount == 0
+ || entryCount <= absenceCount
+ ) return Optional!double.empty;
+
+ const uint presentCount = entryCount - absenceCount;
+
+ // Phone subscore:
+ uint phoneCompliantCount;
+ if (presentCount < phoneNonComplianceCount) {
+ phoneCompliantCount = 0;
+ } else {
+ phoneCompliantCount = presentCount - phoneNonComplianceCount;
+ }
+ double phoneScore = phoneCompliantCount / cast(double) presentCount;
+
+ // Behavior subscore:
+ double behaviorScore = (
+ behaviorGoodCount * 1.0
+ + behaviorMediocreCount * 0.5
+ + behaviorPoorCount * 0
+ ) / cast(double) presentCount;
+
+ double score = 0.3 * phoneScore + 0.7 * behaviorScore;
+ return Optional!double.of(score);
+}
diff --git a/app/src/App.vue b/app/src/App.vue
index fb64af3..634721a 100644
--- a/app/src/App.vue
+++ b/app/src/App.vue
@@ -15,19 +15,19 @@ async function logOut() {
-
@@ -38,4 +38,17 @@ async function logOut() {
-
+
diff --git a/app/src/api/auth.ts b/app/src/api/auth.ts
index d11eca6..7679a9f 100644
--- a/app/src/api/auth.ts
+++ b/app/src/api/auth.ts
@@ -5,7 +5,7 @@ const BASE_URL = import.meta.env.VITE_API_URL + '/auth'
export interface User {
id: number
username: string
- createdAt: Date
+ createdAt: number
isLocked: boolean
isAdmin: boolean
}
diff --git a/app/src/api/base.ts b/app/src/api/base.ts
index f94dd28..7ac2455 100644
--- a/app/src/api/base.ts
+++ b/app/src/api/base.ts
@@ -102,18 +102,12 @@ export abstract class APIClient {
protected async handleAPIResponse(promise: Promise): Promise {
try {
const response = await promise
- if (response.ok) {
- return (await response.json()) as T
- }
- if (response.status === 400) {
- return new BadRequestError(await response.text())
- }
- if (response.status === 401) {
- return new AuthenticationError(await response.text())
- }
- return new InternalServerError(await response.text())
+ if (response.ok) return (await response.json()) as T
+ return this.transformErrorResponse(response)
} catch (error) {
- return new NetworkError('' + error)
+ return new NetworkError(
+ '' + error + " (We couldn't connect to the remote server; it might be down!)",
+ )
}
}
@@ -123,12 +117,17 @@ export abstract class APIClient {
try {
const response = await promise
if (response.ok) return undefined
- if (response.status === 401) {
- return new AuthenticationError(await response.text())
- }
- return new InternalServerError(await response.text())
+ return this.transformErrorResponse(response)
} catch (error) {
- return new NetworkError('' + error)
+ return new NetworkError(
+ '' + error + " (We couldn't connect to the remote server; it might be down!)",
+ )
}
}
+
+ private async transformErrorResponse(r: Response): Promise {
+ if (r.status === 401) return new AuthenticationError(await r.text())
+ if (r.status === 400) return new BadRequestError(await r.text())
+ return new InternalServerError(await r.text())
+ }
}
diff --git a/app/src/api/classroom_compliance.ts b/app/src/api/classroom_compliance.ts
index 8e214d8..4b22b43 100644
--- a/app/src/api/classroom_compliance.ts
+++ b/app/src/api/classroom_compliance.ts
@@ -22,7 +22,6 @@ export interface EntryPhone {
export interface EntryBehavior {
rating: number
- comment?: string
}
export interface Entry {
@@ -51,6 +50,7 @@ export interface EntriesResponseStudent {
deskNumber: number
removed: boolean
entries: Record
+ score: number | null
}
export interface EntriesResponse {
@@ -73,6 +73,15 @@ export interface EntriesPayload {
students: EntriesPayloadStudent[]
}
+export interface StudentScore {
+ id: number
+ score: number | null
+}
+
+export interface ScoresResponse {
+ scores: StudentScore[]
+}
+
export class ClassroomComplianceAPIClient extends APIClient {
constructor(authStore: AuthStoreType) {
super(BASE_URL, authStore)
@@ -132,4 +141,11 @@ export class ClassroomComplianceAPIClient extends APIClient {
saveEntries(classId: number, payload: EntriesPayload): APIResponse {
return super.postWithNoExpectedResponse(`/classes/${classId}/entries`, payload)
}
+
+ getScores(classId: number, fromDate: Date, toDate: Date): APIResponse {
+ const params = new URLSearchParams()
+ params.append('from', fromDate.toISOString().substring(0, 10))
+ params.append('to', toDate.toISOString().substring(0, 10))
+ return super.get(`/classes/${classId}/scores?${params.toString()}`)
+ }
}
diff --git a/app/src/apps/classroom_compliance/ClassItem.vue b/app/src/apps/classroom_compliance/ClassItem.vue
index 184accc..0f39c8a 100644
--- a/app/src/apps/classroom_compliance/ClassItem.vue
+++ b/app/src/apps/classroom_compliance/ClassItem.vue
@@ -1,23 +1,36 @@
-
+
Class
-
- View
-
diff --git a/app/src/apps/classroom_compliance/ClassView.vue b/app/src/apps/classroom_compliance/ClassView.vue
index 783d174..41974f1 100644
--- a/app/src/apps/classroom_compliance/ClassView.vue
+++ b/app/src/apps/classroom_compliance/ClassView.vue
@@ -2,7 +2,7 @@
import { useAuthStore } from '@/stores/auth'
import { onMounted, ref, useTemplateRef, type Ref } from 'vue'
import EntriesTable from '@/apps/classroom_compliance/EntriesTable.vue'
-import { RouterLink, useRouter } from 'vue-router'
+import { useRouter } from 'vue-router'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import { ClassroomComplianceAPIClient, type Class } from '@/api/classroom_compliance'
@@ -35,18 +35,15 @@ async function deleteThisClass() {
Class #
ID:
School Year:
-
-
- Actions:
- Add Student
-
-
+
+
+
+
Are you sure you want to delete this class? All data associated with it (settings, students,
diff --git a/app/src/apps/classroom_compliance/ClassesView.vue b/app/src/apps/classroom_compliance/ClassesView.vue
index 8d590b5..a805fa6 100644
--- a/app/src/apps/classroom_compliance/ClassesView.vue
+++ b/app/src/apps/classroom_compliance/ClassesView.vue
@@ -3,10 +3,12 @@ import { ClassroomComplianceAPIClient, type Class } from '@/api/classroom_compli
import { useAuthStore } from '@/stores/auth'
import { type Ref, ref, onMounted } from 'vue'
import ClassItem from '@/apps/classroom_compliance/ClassItem.vue'
+import { useRouter } from 'vue-router'
const classes: Ref = ref([])
const authStore = useAuthStore()
+const router = useRouter()
const apiClient = new ClassroomComplianceAPIClient(authStore)
onMounted(async () => {
@@ -15,9 +17,11 @@ onMounted(async () => {
-
- Add Class
+
+
+
+
+
-
diff --git a/app/src/apps/classroom_compliance/EditClassView.vue b/app/src/apps/classroom_compliance/EditClassView.vue
index 0948460..8a62c29 100644
--- a/app/src/apps/classroom_compliance/EditClassView.vue
+++ b/app/src/apps/classroom_compliance/EditClassView.vue
@@ -69,7 +69,7 @@ function resetForm() {
-
+
diff --git a/app/src/apps/classroom_compliance/EditStudentView.vue b/app/src/apps/classroom_compliance/EditStudentView.vue
index 1a321e7..b576878 100644
--- a/app/src/apps/classroom_compliance/EditStudentView.vue
+++ b/app/src/apps/classroom_compliance/EditStudentView.vue
@@ -93,7 +93,7 @@ function resetForm() {
Add New Student
-