From 0a0bbe22c90ef80aedded36dc1fb01a1c5892f49 Mon Sep 17 00:00:00 2001 From: andrewlalis <andrewlalisofficial@gmail.com> Date: Wed, 19 Feb 2025 13:11:57 -0500 Subject: [PATCH] Added guide, and cleaned up handling of students who've moved. --- .../classroom_compliance/api_entry.d | 9 +- .../classroom_compliance/api_student.d | 14 ++ app/src/api/classroom_compliance.ts | 1 + .../apps/classroom_compliance/GuideView.vue | 158 ++++++++++++++++++ .../apps/classroom_compliance/MainView.vue | 16 +- .../entries_table/StudentNameCell.vue | 21 ++- app/src/apps/classroom_compliance/router.ts | 4 + 7 files changed, 206 insertions(+), 17 deletions(-) create mode 100644 app/src/apps/classroom_compliance/GuideView.vue diff --git a/api/source/api_modules/classroom_compliance/api_entry.d b/api/source/api_modules/classroom_compliance/api_entry.d index 18c8860..4c85599 100644 --- a/api/source/api_modules/classroom_compliance/api_entry.d +++ b/api/source/api_modules/classroom_compliance/api_entry.d @@ -44,6 +44,7 @@ struct EntriesTableEntry { struct EntriesTableStudentResponse { ulong id; + ulong classId; string name; ushort deskNumber; bool removed; @@ -53,6 +54,7 @@ struct EntriesTableStudentResponse { JSONValue toJsonObj() const { JSONValue obj = JSONValue.emptyObject; obj.object["id"] = JSONValue(id); + obj.object["classId"] = JSONValue(classId); obj.object["name"] = JSONValue(name); obj.object["deskNumber"] = JSONValue(deskNumber); obj.object["removed"] = JSONValue(removed); @@ -107,6 +109,7 @@ void getEntries(ref HttpRequestContext ctx) { ); EntriesTableStudentResponse[] studentObjects = students.map!(s => EntriesTableStudentResponse( s.id, + s.classId, s.name, s.deskNumber, s.removed, @@ -126,7 +129,8 @@ void getEntries(ref HttpRequestContext ctx) { student.id, student.name, student.desk_number, - student.removed + student.removed, + student.class_id FROM classroom_compliance_entry entry LEFT JOIN classroom_compliance_student student ON student.id = entry.student_id @@ -166,7 +170,7 @@ void getEntries(ref HttpRequestContext ctx) { ClassroomComplianceStudent student = ClassroomComplianceStudent( r.getUlong(8), r.getString(9), - cls.id, + r.getUlong(12), r.getUshort(10), r.getBoolean(11) ); @@ -187,6 +191,7 @@ void getEntries(ref HttpRequestContext ctx) { // Their data should still be shown, so add the student here. studentObjects ~= EntriesTableStudentResponse( student.id, + student.classId, student.name, student.deskNumber, student.removed, diff --git a/api/source/api_modules/classroom_compliance/api_student.d b/api/source/api_modules/classroom_compliance/api_student.d index c8b3930..e30fa3b 100644 --- a/api/source/api_modules/classroom_compliance/api_student.d +++ b/api/source/api_modules/classroom_compliance/api_student.d @@ -218,6 +218,7 @@ void getStudentOverview(ref HttpRequestContext ctx) { void moveStudentToOtherClass(ref HttpRequestContext ctx) { Connection conn = getDb(); + conn.setAutoCommit(false); User user = getUserOrThrow(ctx, conn); auto student = getStudentOrThrow(ctx, conn, user); struct Payload { @@ -238,11 +239,24 @@ void moveStudentToOtherClass(ref HttpRequestContext ctx) { ctx.response.writeBodyString("Invalid class was selected."); return; } + // Check that the new class doesn't already have a student with the same name. + bool studentNameExistsInNewClass = recordExists( + conn, + "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND name = ?", + payload.classId, + student.name + ); + if (studentNameExistsInNewClass) { + ctx.response.status = HttpStatus.BAD_REQUEST; + ctx.response.writeBodyString("A student in the selected class has the same name as this one."); + return; + } // All good, so update the student's class to the desired one, and reset their desk. update( conn, "UPDATE classroom_compliance_student SET class_id = ?, desk_number = 0 WHERE id = ?", payload.classId, student.id ); + conn.commit(); // We just return 200 OK, no response body. } diff --git a/app/src/api/classroom_compliance.ts b/app/src/api/classroom_compliance.ts index 936de2e..51163b3 100644 --- a/app/src/api/classroom_compliance.ts +++ b/app/src/api/classroom_compliance.ts @@ -57,6 +57,7 @@ export function getDefaultEntry(dateStr: string): Entry { export interface EntriesResponseStudent { id: number + classId: number name: string deskNumber: number removed: boolean diff --git a/app/src/apps/classroom_compliance/GuideView.vue b/app/src/apps/classroom_compliance/GuideView.vue new file mode 100644 index 0000000..a2f0434 --- /dev/null +++ b/app/src/apps/classroom_compliance/GuideView.vue @@ -0,0 +1,158 @@ +<script setup lang="ts"> +import { EMOJI_ABSENT, EMOJI_BEHAVIOR_GOOD, EMOJI_BEHAVIOR_MEDIOCRE, EMOJI_BEHAVIOR_POOR, EMOJI_PHONE_COMPLIANT, EMOJI_PHONE_NONCOMPLIANT, EMOJI_PRESENT, type Entry } from '@/api/classroom_compliance'; +import EntryTableCell from './entries_table/EntryTableCell.vue'; +import { ref, type Ref } from 'vue'; + +const sampleEntry: Ref<Entry | null> = ref({ + id: 1, + date: '2025-01-01', + createdAt: new Date().getTime(), + absent: false, + phoneCompliant: true, + behaviorRating: 3, + comment: '' +}) +</script> + +<template> + <div class="guide-page"> + <h2>Guide</h2> + <p> + This page provides a quick guide to using the <em>Classroom Compliance</em> + application. + </p> + + <hr /> + <h3>Classes</h3> + <p> + In <em>Classroom Compliance</em>, all your students' data is linked to a + particular class. You'll start by clicking <strong>Add Class</strong> for + each class you teach. It doesn't really matter if your school uses a block + schedule or daily period schedule; each class just needs a number and + school year to identify it. + </p> + <p> + You can view all your classes with the <RouterLink to="/classroom-compliance">View My Classes</RouterLink> + link at the top of this page. Click on any of the classes you see listed, + and you'll go to that class' page. + </p> + + <h4 id="the-class-page">The Class Page</h4> + <p> + This is the main page you'll be working with in <em>Classroom Compliance</em>. + Here, you'll find actions to add / remove / edit the list of students in + the class, add notes to the class, and most importantly, record students' + attendance, behavior, and phone compliance. + </p> + <p> + The following actions are available here: + </p> + <ul> + <li><strong>Add Student</strong> - Add a student to the class.</li> + <li><strong>Import Students</strong> - Import a list of students in bulk. This is useful when you've just created + a class, and you want to copy and paste a list of student names from a spreadsheet.</li> + <li><strong>Clear Assigned Desks</strong> - Resets every student's assigned desk. Useful if you've just removed + assigned seats or are reshuffling everyone.</li> + <li><strong>Delete this Class</strong> - Pretty self-explanatory. This will permanently delete this class. This + CANNOT be undone, so be careful to only delete the class when you're sure you won't need it anymore.</li> + </ul> + + <h5>Notes</h5> + <p> + Sometimes, you might want to write a note to remind yourself of something for a class. Write a note in the text + box, then click <strong>Add Note</strong> to add the note. + </p> + + <hr /> + <h3>Entries</h3> + <p> + The main point of <em>Classroom Compliance</em> is to make it easy to keep track of each student's behavior, + attendance, and phone usage. On each class' page, you'll find a large table with a row for each student, and a + column for each day of the week. This is the <strong>Entries Table</strong>. + </p> + <p> + Simply click the "+" icon to add an entry for a student, or click on the day's "+" icon to add an entry for each + student that day. Then, just click the emoji to edit the entry. A sample entry is included below for you to play + around with. + </p> + <table> + <tbody> + <tr> + <EntryTableCell date-str="2025-01-01" :last-save-state-timestamp="0" v-model="sampleEntry" + style="min-width: 150px" /> + </tr> + </tbody> + </table> + <ul> + <li>Attendance: {{ EMOJI_PRESENT }} for present, and {{ EMOJI_ABSENT }} for absent.</li> + <li>Phone Compliance: {{ EMOJI_PHONE_COMPLIANT }} for compliant, and {{ EMOJI_PHONE_NONCOMPLIANT }} for + non-compliant.</li> + <li>Behavior: {{ EMOJI_BEHAVIOR_GOOD }} for good behavior, {{ EMOJI_BEHAVIOR_MEDIOCRE }} for mediocre, and {{ + EMOJI_BEHAVIOR_POOR }} for poor.</li> + <li>If you'd like to add a comment, click 💬 and enter your comment in the popup.</li> + <li>Click the 🗑️ to remove the entry.</li> + <li><em>Note that if a student is absent, you can't add any phone or behavior information.</em></li> + </ul> + <p> + When editing, changed entries are highlighted, and you need to click <strong>Save</strong> or <strong>Discard + Edits</strong> to save or discard your changes, respectively. + </p> + + <h4>Table Views</h4> + <p> + Above the Entries Table, there's a selection of <em>views</em> to choose from. By default, <strong>Full + View</strong> is selected. + </p> + <ul> + <li><strong>Full View</strong> - Shows a full week's entries for all students, including their scores.</li> + <li><strong>Grading View</strong> - Only show students and their scores. Useful when transferring grades to + another system.</li> + <li><strong>Whiteboard View</strong> - Only shows student names and their assigned desks. Useful for showing to + your class so they know their assigned seats.</li> + <li><strong>Today View</strong> - Same as the Full View, but only shows today, which can be useful when entering + data for a class.</li> + </ul> + + <h4>Scores</h4> + <p> + Students' scores are calculated per week. The calculation is shown below: + </p> + <p> + <code>phone_score * 0.3 + behavior_score * 0.7</code> where + </p> + <ul> + <li><code>phone_score = days_compliant / days_present</code></li> + <li><code>behavior_score = (good_days * 1.0 + mediocre_days * 0.5) / days_present</code></li> + </ul> + + <hr /> + <h3>Adding / Editing Students</h3> + <p> + As mentioned briefly in <a href="#the-class-page">The Class Page</a>, you can add students one-by-one, or in bulk. + </p> + <p> + When adding a student individually, or editing an existing student, here's what you need to know: + </p> + <ul> + <li>Each student in a class needs a <strong>unique</strong> name. Unless you have twins with the same first name, + this shouldn't be an issue.</li> + <li>If a student has an assigned desk number, you can set it. Set the desk number to 0 if there's no assigned + seating for the student.</li> + <li>To preserve a record of students who have since been removed from a class, you can check the + <strong>Removed</strong> checkbox to indicate that the student is no longer in the class. Their old data will + still show up, but you can't add any new entries for them. + </li> + <li> + The <strong>Move to another class</strong> button will, as its name implies, move the student to another one of + your classes. + </li> + </ul> + </div> +</template> +<style scoped> +.guide-page { + max-width: 50ch; + margin-left: auto; + margin-right: auto; +} +</style> diff --git a/app/src/apps/classroom_compliance/MainView.vue b/app/src/apps/classroom_compliance/MainView.vue index 084409a..dcdaf4e 100644 --- a/app/src/apps/classroom_compliance/MainView.vue +++ b/app/src/apps/classroom_compliance/MainView.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import { BASE_URL, EMOJI_ABSENT, EMOJI_BEHAVIOR_GOOD, EMOJI_BEHAVIOR_MEDIOCRE, EMOJI_BEHAVIOR_POOR, EMOJI_PHONE_COMPLIANT, EMOJI_PHONE_NONCOMPLIANT, EMOJI_PRESENT } from '@/api/classroom_compliance'; +import { BASE_URL } from '@/api/classroom_compliance'; import { useAuthStore } from '@/stores/auth'; const authStore = useAuthStore() @@ -23,24 +23,14 @@ async function downloadExport() { <template> <main> <h1>Classroom Compliance</h1> - <p> - With this application, you can track each student's compliance to various classroom policies, - like: - </p> - <ul> - <li>Attendance: {{ EMOJI_PRESENT }} - Present, {{ EMOJI_ABSENT }} - Absent</li> - <li>Phone Usage (or lack thereof): {{ EMOJI_PHONE_COMPLIANT }} - Compliant, {{ EMOJI_PHONE_NONCOMPLIANT }} - - Noncompliant</li> - <li>Behavior: {{ EMOJI_BEHAVIOR_GOOD }} - Good, {{ EMOJI_BEHAVIOR_MEDIOCRE }} - Mediocre, {{ EMOJI_BEHAVIOR_POOR - }} - Poor</li> - </ul> <div class="button-bar"> <RouterLink to="/classroom-compliance">View My Classes</RouterLink> + <RouterLink to="/classroom-compliance/guide">Guide</RouterLink> <a @click.prevent="downloadExport()" href="#">Export All Data</a> </div> <hr /> - <RouterView /> + <RouterView :key="$route.fullPath" /> </main> </template> diff --git a/app/src/apps/classroom_compliance/entries_table/StudentNameCell.vue b/app/src/apps/classroom_compliance/entries_table/StudentNameCell.vue index 97721ef..9912a9e 100644 --- a/app/src/apps/classroom_compliance/entries_table/StudentNameCell.vue +++ b/app/src/apps/classroom_compliance/entries_table/StudentNameCell.vue @@ -2,14 +2,18 @@ import type { EntriesResponseStudent } from '@/api/classroom_compliance'; defineProps<{ student: EntriesResponseStudent, - classId: number + classId: number, }>() </script> <template> <td class="student-name-cell" :class="{ 'student-removed': student.removed }"> - <RouterLink :to="'/classroom-compliance/classes/' + classId + '/students/' + student.id" class="student-link"> + <RouterLink :to="'/classroom-compliance/classes/' + student.classId + '/students/' + student.id" + class="student-link"> {{ student.name }} </RouterLink> + <div v-if="classId !== student.classId" class="other-class-text"> + <RouterLink :to="'/classroom-compliance/classes/' + student.classId">In another class</RouterLink> + </div> </td> </template> <style scoped> @@ -31,4 +35,17 @@ defineProps<{ text-decoration: line-through; color: gray; } + +.other-class-text { + font-size: x-small; +} + +.other-class-text>a { + color: gray; + text-decoration: none; +} + +.other-class-text>a:hover { + text-decoration: underline; +} </style> diff --git a/app/src/apps/classroom_compliance/router.ts b/app/src/apps/classroom_compliance/router.ts index e2ee707..e2b6bb9 100644 --- a/app/src/apps/classroom_compliance/router.ts +++ b/app/src/apps/classroom_compliance/router.ts @@ -35,6 +35,10 @@ export function createClassroomComplianceRoutes(): RouteRecordRaw { component: () => import('@/apps/classroom_compliance/ImportStudentsView.vue'), props: true, }, + { + path: 'guide', + component: () => import('@/apps/classroom_compliance/GuideView.vue'), + }, ], } }