From b1f9cfa71027ddcb91f7e59f79309524966c912a Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Tue, 2 Sep 2025 16:01:46 -0400 Subject: [PATCH] Added labels for students, and one more checklist item. --- api/schema/classroom_compliance.sql | 8 +++ .../api_modules/classroom_compliance/api.d | 2 + .../classroom_compliance/api_student.d | 34 ++++++++++ app/src/api/classroom_compliance.ts | 11 ++++ .../classroom_compliance/EntriesTable.vue | 5 +- .../apps/classroom_compliance/StudentView.vue | 62 +++++++++++++++++++ .../entries_table/EntryTableCell.vue | 3 +- .../entries_table/StudentNameCell.vue | 39 +++++++++++- 8 files changed, 158 insertions(+), 6 deletions(-) diff --git a/api/schema/classroom_compliance.sql b/api/schema/classroom_compliance.sql index cb7c1f8..307b4a8 100644 --- a/api/schema/classroom_compliance.sql +++ b/api/schema/classroom_compliance.sql @@ -23,6 +23,14 @@ CREATE TABLE classroom_compliance_student ( removed BOOLEAN NOT NULL DEFAULT FALSE ); +CREATE TABLE classroom_compliance_student_label ( + student_id BIGINT NOT NULL + REFERENCES classroom_compliance_student(id) + ON UPDATE CASCADE ON DELETE CASCADE, + label VARCHAR(255) NOT NULL, + PRIMARY KEY (student_id, label) +); + CREATE TABLE classroom_compliance_entry ( id BIGSERIAL PRIMARY KEY, class_id BIGINT NOT NULL diff --git a/api/source/api_modules/classroom_compliance/api.d b/api/source/api_modules/classroom_compliance/api.d index 3bd8779..b1ac90d 100644 --- a/api/source/api_modules/classroom_compliance/api.d +++ b/api/source/api_modules/classroom_compliance/api.d @@ -32,6 +32,8 @@ void registerApiEndpoints(PathHandler handler) { handler.addMapping(Method.PUT, STUDENT_PATH ~ "/class", &moveStudentToOtherClass); handler.addMapping(Method.GET, STUDENT_PATH ~ "/entries", &getStudentEntries); handler.addMapping(Method.GET, STUDENT_PATH ~ "/overview", &getStudentOverview); + handler.addMapping(Method.GET, STUDENT_PATH ~ "/labels", &getStudentLabels); + handler.addMapping(Method.POST, STUDENT_PATH ~ "/labels", &updateStudentLabels); handler.addMapping(Method.GET, CLASS_PATH ~ "/entries", &getEntries); handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &saveEntries); diff --git a/api/source/api_modules/classroom_compliance/api_student.d b/api/source/api_modules/classroom_compliance/api_student.d index 6cfed85..83177f8 100644 --- a/api/source/api_modules/classroom_compliance/api_student.d +++ b/api/source/api_modules/classroom_compliance/api_student.d @@ -279,3 +279,37 @@ void moveStudentToOtherClass(ref HttpRequestContext ctx) { conn.commit(); // We just return 200 OK, no response body. } + +void getStudentLabels(ref HttpRequestContext ctx) { + Connection conn = getDb(); + scope(exit) conn.close(); + User user = getUserOrThrow(ctx, conn); + auto student = getStudentOrThrow(ctx, conn, user); + string[] labels = findAll( + conn, + "SELECT label FROM classroom_compliance_student_label WHERE student_id = ? ORDER BY label ASC", + (r) => r.getString(1), + student.id + ); + writeJsonBody(ctx, labels); +} + +void updateStudentLabels(ref HttpRequestContext ctx) { + Connection conn = getDb(); + scope(exit) conn.close(); + User user = getUserOrThrow(ctx, conn); + auto student = getStudentOrThrow(ctx, conn, user); + string[] labels = readJsonPayload!(string[])(ctx); + + conn.setAutoCommit(false); + update(conn, "DELETE FROM classroom_compliance_student_label WHERE student_id = ?", student.id); + PreparedStatement ps = conn.prepareStatement( + "INSERT INTO classroom_compliance_student_label (student_id, label) VALUES (?, ?)" + ); + foreach (label; labels) { + ps.setUlong(1, student.id); + ps.setString(2, label); + ps.executeUpdate(); + } + conn.commit(); +} diff --git a/app/src/api/classroom_compliance.ts b/app/src/api/classroom_compliance.ts index 49b33f3..50f194d 100644 --- a/app/src/api/classroom_compliance.ts +++ b/app/src/api/classroom_compliance.ts @@ -245,4 +245,15 @@ export class ClassroomComplianceAPIClient extends APIClient { ): APIResponse { return super.get(`/classes/${classId}/students/${studentId}/overview`) } + + getStudentLabels(classId: number, studentId: number): APIResponse { + return super.get(`/classes/${classId}/students/${studentId}/labels`) + } + + updateStudentLabels(classId: number, studentId: number, labels: string[]): APIResponse { + return super.postWithNoExpectedResponse( + `/classes/${classId}/students/${studentId}/labels`, + labels, + ) + } } diff --git a/app/src/apps/classroom_compliance/EntriesTable.vue b/app/src/apps/classroom_compliance/EntriesTable.vue index 393e3af..506a50a 100644 --- a/app/src/apps/classroom_compliance/EntriesTable.vue +++ b/app/src/apps/classroom_compliance/EntriesTable.vue @@ -308,8 +308,9 @@ defineExpose({ {{ idx + 1 - }}. - + }}. + = ref(null) const student: Ref = ref(null) const entries: Ref = ref([]) +const labels: Ref = ref([]) +const newLabel = ref('') const statistics: Ref = ref(null) // Filtered set of entries for "last week", used in the week overview dialog. const lastWeeksEntries = computed(() => { @@ -50,6 +52,8 @@ const lastWeeksEntries = computed(() => { const apiClient = new ClassroomComplianceAPIClient(authStore) const deleteConfirmDialog = useTemplateRef('deleteConfirmDialog') const weekOverviewDialog = useTemplateRef('weekOverviewDialog') +const labelsDialog = useTemplateRef('labelsDialog') + onMounted(async () => { const classIdNumber = parseInt(props.classId, 10) cls.value = await apiClient.getClass(classIdNumber).handleErrorsWithAlert() @@ -78,8 +82,23 @@ onMounted(async () => { console.warn('Failed to get student statistics: ', result.message) } }) + fetchLabels() }) +async function fetchLabels() { + if (!cls.value || !student.value) { + labels.value = [] + return + } + const result = await apiClient.getStudentLabels(cls.value.id, student.value.id).result + if (result instanceof APIError) { + console.error("Failed to get labels.", result.message) + labels.value = [] + } else { + labels.value = result + } +} + async function deleteThisStudent() { if (!cls.value || !student.value || cls.value.archived) return const choice = await deleteConfirmDialog.value?.show() @@ -93,6 +112,27 @@ function getFormattedDate(entry: Entry) { const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; return days[d.getDay()] + ', ' + d.toLocaleDateString() } + +async function addLabel() { + if (!cls.value || !student.value || cls.value.archived) return + const newLabels = [...labels.value, newLabel.value.trim()] + const success = await apiClient.updateStudentLabels(cls.value.id, student.value.id, newLabels) + .handleErrorsWithAlertNoBody() + if (success) { + newLabel.value = '' + await fetchLabels() + } +} + +async function deleteLabel(label: string) { + if (!cls.value || !student.value || cls.value.archived) return + const newLabels = labels.value.filter(s => s !== label) + const success = await apiClient.updateStudentLabels(cls.value.id, student.value.id, newLabels) + .handleErrorsWithAlertNoBody() + if (success) { + await fetchLabels() + } +}