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'),
+      },
     ],
   }
 }