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() { - + 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 @@ 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 () => { 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 -

From class

+

In class

@@ -105,10 +105,10 @@ function resetForm() {
- +
-
+
diff --git a/app/src/apps/classroom_compliance/EntriesTable.vue b/app/src/apps/classroom_compliance/EntriesTable.vue index 89398d0..b690a12 100644 --- a/app/src/apps/classroom_compliance/EntriesTable.vue +++ b/app/src/apps/classroom_compliance/EntriesTable.vue @@ -18,8 +18,10 @@ const props = defineProps<{ const apiClient = new ClassroomComplianceAPIClient(authStore) const students: Ref = ref([]) + const lastSaveState: Ref = ref(null) const lastSaveStateTimestamp: Ref = ref(0) + const dates: Ref = ref([]) const toDate: Ref = ref(new Date()) const fromDate: Ref = ref(new Date()) @@ -27,12 +29,12 @@ const fromDate: Ref = ref(new Date()) const entriesChangedSinceLastSave = computed(() => { return lastSaveState.value === null || lastSaveState.value !== JSON.stringify(students.value) }) +const assignedDesks = computed(() => { + return students.value.length > 0 && students.value.some(s => s.deskNumber > 0) +}) onMounted(async () => { - toDate.value.setHours(0, 0, 0, 0) - fromDate.value.setHours(0, 0, 0, 0) - fromDate.value.setDate(fromDate.value.getDate() - 4) - await loadEntries() + showThisWeek() }) async function loadEntries() { @@ -57,22 +59,27 @@ function shiftDateRange(days: number) { fromDate.value.setDate(fromDate.value.getDate() + days) } -async function showPreviousDay() { - shiftDateRange(-1) +async function showPreviousWeek() { + shiftDateRange(-7) await loadEntries() } -async function showToday() { +async function showThisWeek() { + // First set the to-date to the next upcoming end-of-week (Friday). toDate.value = new Date() toDate.value.setHours(0, 0, 0, 0) + while (toDate.value.getDay() < 5) { + toDate.value.setDate(toDate.value.getDate() + 1) + } + // Then set the from-date to the Monday of that week. fromDate.value = new Date() fromDate.value.setHours(0, 0, 0, 0) fromDate.value.setDate(fromDate.value.getDate() - 4) await loadEntries() } -async function showNextDay() { - shiftDateRange(1) +async function showNextWeek() { + shiftDateRange(7) await loadEntries() } @@ -135,10 +142,10 @@ function addAllEntriesForDate(dateStr: string) { diff --git a/app/src/apps/classroom_compliance/EntryTableCell.vue b/app/src/apps/classroom_compliance/EntryTableCell.vue index 053f069..5f7d575 100644 --- a/app/src/apps/classroom_compliance/EntryTableCell.vue +++ b/app/src/apps/classroom_compliance/EntryTableCell.vue @@ -37,6 +37,17 @@ function toggleAbsence() { // Populate default additional data if student is no longer absent. model.value.phone = { compliant: true } model.value.behavior = { rating: 3 } + // If we have an initial entry known, restore data from that. + if (initialEntryJson.value) { + const initialEntry = JSON.parse(initialEntryJson.value) as Entry + if (initialEntry.absent) return + if (initialEntry.phone) { + model.value.phone = { compliant: initialEntry.phone?.compliant } + } + if (initialEntry.behavior) { + model.value.behavior = { rating: initialEntry.behavior.rating } + } + } } } } @@ -73,28 +84,32 @@ function addEntry() { diff --git a/app/src/views/LoginView.vue b/app/src/views/LoginView.vue index e39b229..dcdbf7f 100644 --- a/app/src/views/LoginView.vue +++ b/app/src/views/LoginView.vue @@ -34,7 +34,7 @@ async function doLogin() {
-
+
diff --git a/app/src/views/MyAccountView.vue b/app/src/views/MyAccountView.vue new file mode 100644 index 0000000..c031674 --- /dev/null +++ b/app/src/views/MyAccountView.vue @@ -0,0 +1,34 @@ + +