Added serial number to entries table, and button to remove desk assignments.
Build and Test App / Build-and-test-App (push) Successful in 34s Details
Build and Test API / Build-and-test-API (push) Successful in 55s Details

This commit is contained in:
Andrew Lalis 2025-02-19 11:36:16 -05:00
parent ae33f57a36
commit b5808d68f2
6 changed files with 57 additions and 6 deletions

View File

@ -19,6 +19,7 @@ void registerApiEndpoints(PathHandler handler) {
handler.addMapping(Method.GET, CLASS_PATH ~ "/notes", &getClassNotes); handler.addMapping(Method.GET, CLASS_PATH ~ "/notes", &getClassNotes);
handler.addMapping(Method.POST, CLASS_PATH ~ "/notes", &createClassNote); handler.addMapping(Method.POST, CLASS_PATH ~ "/notes", &createClassNote);
handler.addMapping(Method.DELETE, CLASS_PATH ~ "/notes/:noteId:ulong", &deleteClassNote); handler.addMapping(Method.DELETE, CLASS_PATH ~ "/notes/:noteId:ulong", &deleteClassNote);
handler.addMapping(Method.PUT, CLASS_PATH ~ "/reset-desk-numbers", &resetStudentDesks);
handler.addMapping(Method.POST, CLASS_PATH ~ "/students", &createStudent); handler.addMapping(Method.POST, CLASS_PATH ~ "/students", &createStudent);
handler.addMapping(Method.GET, CLASS_PATH ~ "/students", &getStudents); handler.addMapping(Method.GET, CLASS_PATH ~ "/students", &getStudents);

View File

@ -141,3 +141,12 @@ void deleteClassNote(ref HttpRequestContext ctx) {
const query = "DELETE FROM classroom_compliance_class_note WHERE class_id = ? AND id = ?"; const query = "DELETE FROM classroom_compliance_class_note WHERE class_id = ? AND id = ?";
update(conn, query, cls.id, noteId); update(conn, query, cls.id, noteId);
} }
void resetStudentDesks(ref HttpRequestContext ctx) {
Connection conn = getDb();
scope(exit) conn.close();
User user = getUserOrThrow(ctx, conn);
auto cls = getClassOrThrow(ctx, conn, user);
const query = "UPDATE classroom_compliance_student SET desk_number = 0 WHERE class_id = ?";
update(conn, query, cls.id);
}

View File

@ -1,4 +1,4 @@
import { APIClient, type APIResponse, type AuthStoreType } from './base' import { APIClient, APIResponse, type AuthStoreType } from './base'
export const BASE_URL = import.meta.env.VITE_API_URL + '/classroom-compliance' export const BASE_URL = import.meta.env.VITE_API_URL + '/classroom-compliance'
@ -140,6 +140,15 @@ export class ClassroomComplianceAPIClient extends APIClient {
return super.delete(`/classes/${classId}/notes/${noteId}`) return super.delete(`/classes/${classId}/notes/${noteId}`)
} }
resetStudentDesks(classId: number): APIResponse<void> {
const url = `/classes/${classId}/reset-desk-numbers`
const promise = fetch(this.baseUrl + url, {
headers: this.getAuthHeaders(),
method: 'PUT',
})
return new APIResponse(this.handleAPIResponseWithNoBody(promise))
}
getStudents(classId: number): APIResponse<Student[]> { getStudents(classId: number): APIResponse<Student[]> {
return super.get(`/classes/${classId}/students`) return super.get(`/classes/${classId}/students`)
} }

View File

@ -15,10 +15,17 @@ const router = useRouter()
const cls: Ref<Class | null> = ref(null) const cls: Ref<Class | null> = ref(null)
const notes: Ref<ClassNote[]> = ref([]) const notes: Ref<ClassNote[]> = ref([])
const noteContent: Ref<string> = ref('') const noteContent: Ref<string> = ref('')
const entriesTable = useTemplateRef('entries-table')
const apiClient = new ClassroomComplianceAPIClient(authStore) const apiClient = new ClassroomComplianceAPIClient(authStore)
const deleteClassDialog = useTemplateRef('deleteClassDialog') const deleteClassDialog = useTemplateRef('deleteClassDialog')
onMounted(() => { onMounted(() => {
loadClass()
})
function loadClass() {
const idNumber = parseInt(props.id, 10) const idNumber = parseInt(props.id, 10)
apiClient.getClass(idNumber).handleErrorsWithAlert().then(result => { apiClient.getClass(idNumber).handleErrorsWithAlert().then(result => {
if (result) { if (result) {
@ -28,7 +35,7 @@ onMounted(() => {
router.back(); router.back();
} }
}) })
}) }
async function deleteThisClass() { async function deleteThisClass() {
if (!cls.value || !(await deleteClassDialog.value?.show())) return if (!cls.value || !(await deleteClassDialog.value?.show())) return
@ -61,6 +68,13 @@ function refreshNotes() {
} }
}) })
} }
async function resetStudentDesks() {
if (!cls.value) return
await apiClient.resetStudentDesks(cls.value.id).handleErrorsWithAlert()
// Reload the table!
await entriesTable.value?.loadEntries()
}
</script> </script>
<template> <template>
<div v-if="cls"> <div v-if="cls">
@ -71,6 +85,7 @@ function refreshNotes() {
Student</button> Student</button>
<button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/import-students`)">Import <button type="button" @click="router.push(`/classroom-compliance/classes/${cls.id}/import-students`)">Import
Students</button> Students</button>
<button type="button" @click="resetStudentDesks">Clear Assigned Desks</button>
<button type="button" @click="deleteThisClass">Delete this Class</button> <button type="button" @click="deleteThisClass">Delete this Class</button>
</div> </div>
@ -82,7 +97,7 @@ function refreshNotes() {
</form> </form>
<ClassNoteItem v-for="note in notes" :key="note.id" :note="note" @noteDeleted="refreshNotes()" /> <ClassNoteItem v-for="note in notes" :key="note.id" :note="note" @noteDeleted="refreshNotes()" />
<EntriesTable :classId="cls.id" /> <EntriesTable :classId="cls.id" ref="entries-table" />
<!-- Confirmation dialog used for attempts at deleting this class. --> <!-- Confirmation dialog used for attempts at deleting this class. -->
<ConfirmDialog ref="deleteClassDialog"> <ConfirmDialog ref="deleteClassDialog">

View File

@ -237,6 +237,10 @@ function addAllEntriesForDate(dateStr: string) {
} }
} }
} }
defineExpose({
loadEntries
})
</script> </script>
<template> <template>
<div> <div>
@ -272,6 +276,7 @@ function addAllEntriesForDate(dateStr: string) {
<table class="entries-table" :class="{ 'entries-table-reduced-view': selectedView !== TableView.FULL }"> <table class="entries-table" :class="{ 'entries-table-reduced-view': selectedView !== TableView.FULL }">
<thead> <thead>
<tr> <tr>
<th v-if="selectedView !== TableView.WHITEBOARD">#</th>
<th>Student</th> <th>Student</th>
<th v-if="assignedDesks">Desk</th> <th v-if="assignedDesks">Desk</th>
<DateHeaderCell v-for="date in getVisibleDates()" :key="date" :date-str="date" <DateHeaderCell v-for="date in getVisibleDates()" :key="date" :date-str="date"
@ -280,7 +285,9 @@ function addAllEntriesForDate(dateStr: string) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="student in students" :key="student.id"> <tr v-for="(student, idx) in students" :key="student.id" style="height: 2em;">
<td v-if="selectedView !== TableView.WHITEBOARD" style="text-align: right; padding-right: 0.5em;">{{ idx + 1
}}.</td>
<StudentNameCell :student="student" :class-id="classId" /> <StudentNameCell :student="student" :class-id="classId" />
<!-- Desk Number: --> <!-- Desk Number: -->
<td v-if="assignedDesks" v-text="student.deskNumber"></td> <td v-if="assignedDesks" v-text="student.deskNumber"></td>

View File

@ -6,19 +6,29 @@ defineProps<{
}>() }>()
</script> </script>
<template> <template>
<td :class="{ 'student-removed': student.removed }"> <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/' + classId + '/students/' + student.id" class="student-link">
<span v-text="student.name"></span> {{ student.name }}
</RouterLink> </RouterLink>
</td> </td>
</template> </template>
<style scoped> <style scoped>
.student-name-cell {
padding-left: 0.5em;
text-align: left;
}
.student-link { .student-link {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
.student-link:hover {
text-decoration: underline;
}
.student-removed { .student-removed {
text-decoration: line-through; text-decoration: line-through;
color: gray;
} }
</style> </style>