Added statistics overview for each student.
This commit is contained in:
parent
ad111c3cca
commit
5610430232
|
@ -65,6 +65,7 @@ void registerApiEndpoints(PathHandler handler) {
|
||||||
handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent);
|
handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent);
|
||||||
handler.addMapping(Method.DELETE, STUDENT_PATH, &deleteStudent);
|
handler.addMapping(Method.DELETE, STUDENT_PATH, &deleteStudent);
|
||||||
handler.addMapping(Method.GET, STUDENT_PATH ~ "/entries", &getStudentEntries);
|
handler.addMapping(Method.GET, STUDENT_PATH ~ "/entries", &getStudentEntries);
|
||||||
|
handler.addMapping(Method.GET, STUDENT_PATH ~ "/overview", &getStudentOverview);
|
||||||
|
|
||||||
handler.addMapping(Method.GET, CLASS_PATH ~ "/entries", &getEntries);
|
handler.addMapping(Method.GET, CLASS_PATH ~ "/entries", &getEntries);
|
||||||
handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &saveEntries);
|
handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &saveEntries);
|
||||||
|
@ -783,3 +784,63 @@ void getStudentEntries(ref HttpRequestContext ctx) {
|
||||||
}
|
}
|
||||||
ctx.response.writeBodyString(response.toJSON(), "application/json");
|
ctx.response.writeBodyString(response.toJSON(), "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void getStudentOverview(ref HttpRequestContext ctx) {
|
||||||
|
auto db = getDb();
|
||||||
|
User user = getUserOrThrow(ctx, db);
|
||||||
|
auto student = getStudentOrThrow(ctx, db, user);
|
||||||
|
|
||||||
|
const ulong entryCount = findOne!ulong(
|
||||||
|
db,
|
||||||
|
"SELECT COUNT(*) FROM classroom_compliance_entry WHERE student_id = ?",
|
||||||
|
student.id
|
||||||
|
).orElse(0);
|
||||||
|
if (entryCount == 0) {
|
||||||
|
ctx.response.status = HttpStatus.NOT_FOUND;
|
||||||
|
ctx.response.writeBodyString("No entries for this student.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ulong absenceCount = findOne!ulong(
|
||||||
|
db,
|
||||||
|
"SELECT COUNT(*) FROM classroom_compliance_entry WHERE student_id = ? AND absent = true",
|
||||||
|
student.id
|
||||||
|
).orElse(0);
|
||||||
|
const ulong phoneNoncomplianceCount = findOne!ulong(
|
||||||
|
db,
|
||||||
|
"
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM classroom_compliance_entry_phone p
|
||||||
|
LEFT JOIN classroom_compliance_entry e
|
||||||
|
ON e.id = p.entry_id
|
||||||
|
WHERE p.compliant = false AND e.student_id = ?
|
||||||
|
",
|
||||||
|
student.id
|
||||||
|
).orElse(0);
|
||||||
|
const behaviorCountQuery = "
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM classroom_compliance_entry_behavior b
|
||||||
|
LEFT JOIN classroom_compliance_entry e
|
||||||
|
ON e.id = b.entry_id
|
||||||
|
WHERE e.student_id = ? AND b.rating = ?
|
||||||
|
";
|
||||||
|
|
||||||
|
const ulong behaviorGoodCount = findOne!ulong(db, behaviorCountQuery, student.id, 3).orElse(0);
|
||||||
|
const ulong behaviorMediocreCount = findOne!ulong(db, behaviorCountQuery, student.id, 2).orElse(0);
|
||||||
|
const ulong behaviorPoorCount = findOne!ulong(db, behaviorCountQuery, student.id, 1).orElse(0);
|
||||||
|
|
||||||
|
// Calculate derived statistics.
|
||||||
|
const ulong attendanceCount = entryCount - absenceCount;
|
||||||
|
double attendanceRate = attendanceCount / cast(double) entryCount;
|
||||||
|
double phoneComplianceRate = (attendanceCount - phoneNoncomplianceCount) / cast(double) attendanceCount;
|
||||||
|
double behaviorScore = (
|
||||||
|
behaviorGoodCount * 1.0 +
|
||||||
|
behaviorMediocreCount * 0.5
|
||||||
|
) / attendanceCount;
|
||||||
|
|
||||||
|
JSONValue response = JSONValue.emptyObject;
|
||||||
|
response.object["attendanceRate"] = JSONValue(attendanceRate);
|
||||||
|
response.object["phoneComplianceRate"] = JSONValue(phoneComplianceRate);
|
||||||
|
response.object["behaviorScore"] = JSONValue(behaviorScore);
|
||||||
|
response.object["entryCount"] = JSONValue(entryCount);
|
||||||
|
ctx.response.writeBodyString(response.toJSON(), "application/json");
|
||||||
|
}
|
||||||
|
|
|
@ -92,6 +92,13 @@ export interface ScoresResponse {
|
||||||
scores: StudentScore[]
|
scores: StudentScore[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StudentStatisticsOverview {
|
||||||
|
attendanceRate: number
|
||||||
|
phoneComplianceRate: number
|
||||||
|
behaviorScore: number
|
||||||
|
entryCount: number
|
||||||
|
}
|
||||||
|
|
||||||
export class ClassroomComplianceAPIClient extends APIClient {
|
export class ClassroomComplianceAPIClient extends APIClient {
|
||||||
constructor(authStore: AuthStoreType) {
|
constructor(authStore: AuthStoreType) {
|
||||||
super(BASE_URL, authStore)
|
super(BASE_URL, authStore)
|
||||||
|
@ -162,4 +169,11 @@ export class ClassroomComplianceAPIClient extends APIClient {
|
||||||
getStudentEntries(classId: number, studentId: number): APIResponse<Entry[]> {
|
getStudentEntries(classId: number, studentId: number): APIResponse<Entry[]> {
|
||||||
return super.get(`/classes/${classId}/students/${studentId}/entries`)
|
return super.get(`/classes/${classId}/students/${studentId}/entries`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStudentStatisticsOverview(
|
||||||
|
classId: number,
|
||||||
|
studentId: number,
|
||||||
|
): APIResponse<StudentStatisticsOverview> {
|
||||||
|
return super.get(`/classes/${classId}/students/${studentId}/overview`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ClassroomComplianceAPIClient, type Class, type Entry, type Student } from '@/api/classroom_compliance'
|
import { ClassroomComplianceAPIClient, type Class, type Entry, type Student, type StudentStatisticsOverview } from '@/api/classroom_compliance'
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { onMounted, ref, useTemplateRef, type Ref } from 'vue'
|
import { onMounted, ref, useTemplateRef, type Ref } from 'vue'
|
||||||
|
@ -16,6 +16,7 @@ const router = useRouter()
|
||||||
const cls: Ref<Class | null> = ref(null)
|
const cls: Ref<Class | null> = ref(null)
|
||||||
const student: Ref<Student | null> = ref(null)
|
const student: Ref<Student | null> = ref(null)
|
||||||
const entries: Ref<Entry[]> = ref([])
|
const entries: Ref<Entry[]> = ref([])
|
||||||
|
const statistics: Ref<StudentStatisticsOverview | null> = ref(null)
|
||||||
|
|
||||||
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
const apiClient = new ClassroomComplianceAPIClient(authStore)
|
||||||
const deleteConfirmDialog = useTemplateRef('deleteConfirmDialog')
|
const deleteConfirmDialog = useTemplateRef('deleteConfirmDialog')
|
||||||
|
@ -39,6 +40,8 @@ onMounted(async () => {
|
||||||
entries.value = values
|
entries.value = values
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
apiClient.getStudentStatisticsOverview(cls.value.id, student.value.id).handleErrorsWithAlert()
|
||||||
|
.then(stats => statistics.value = stats)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function deleteThisStudent() {
|
async function deleteThisStudent() {
|
||||||
|
@ -70,6 +73,16 @@ async function deleteThisStudent() {
|
||||||
<button type="button" @click="deleteThisStudent">Delete</button>
|
<button type="button" @click="deleteThisStudent">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="statistics">
|
||||||
|
<h3>Statistics</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Attendance Rate: {{ (statistics.attendanceRate * 100).toFixed(1) }}%</li>
|
||||||
|
<li>Phone Compliance Rate: {{ (statistics.phoneComplianceRate * 100).toFixed(1) }}%</li>
|
||||||
|
<li>Behavior Score: {{ (statistics.behaviorScore * 100).toFixed(1) }}%</li>
|
||||||
|
<li>From a total of {{ statistics.entryCount }} entries</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Entries</h3>
|
<h3>Entries</h3>
|
||||||
<p>
|
<p>
|
||||||
Below is a record of all entries saved for <span>{{ student.name }}</span>.
|
Below is a record of all entries saved for <span>{{ student.name }}</span>.
|
||||||
|
|
Loading…
Reference in New Issue