Files
BoostAI/Frontend/src/components/dashboard/teacher/dashboard-teacher-classroom-detail.data.ts
2026-05-25 17:05:06 +01:00

153 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { apiFetchJson } from "../../../lib/api";
import type { ApiAssignment, ApiClassroom, ApiListResponse, ApiReviewQueueItem, ApiReviewSummary, ApiStudent } from "../../../lib/api-types";
import {
getAssignmentReviewHref,
getDashboardTeacherClassroomHref,
} from "../../../lib/routes";
import { teacherDashboardLabels } from "../../../content/dashboard-labels";
import { isIndividualRedoAssignment } from "./dashboard-teacher-assignments.data";
import {
buildTeacherShell,
formatRelativeTime,
formatShortDate,
initialsFor,
queueStatusLabel,
queueStatusTone,
studentNote,
} from "./dashboard-teacher-classroom-detail.helpers";
import type { TeacherClassroomDetailData, TeacherClassroomRedoAssignmentItem } from "./dashboard-teacher-classroom-detail.types";
export type { TeacherClassroomDetailData, TeacherClassroomDetailStudentItem, TeacherClassroomRedoAssignmentItem } from "./dashboard-teacher-classroom-detail.types";
export const getTeacherClassroomDetailData = async (
teacherId: number,
classroomId: number,
selectedStudentId?: number | null,
): Promise<TeacherClassroomDetailData | null> => {
const [classroomsResponse, assignmentsResponse] = await Promise.all([
apiFetchJson<ApiListResponse<ApiClassroom>>(`/api/teachers/${teacherId}/classrooms`),
apiFetchJson<ApiListResponse<ApiAssignment>>(`/api/teachers/${teacherId}/assignments`),
]);
const classrooms = classroomsResponse.data;
const classroom = classrooms.find((entry) => entry.id === classroomId);
if (!classroom) return null;
const classroomAssignments = assignmentsResponse.data.filter((assignment) => assignment.classroom_id === classroomId);
const redoAssignments = classroomAssignments.filter(isIndividualRedoAssignment);
const [studentsResponse, reviewSummaryEntries, reviewQueueEntries] = await Promise.all([
apiFetchJson<ApiListResponse<ApiStudent>>(`/api/classrooms/${classroomId}/students`),
Promise.all(classroomAssignments.map(async (assignment) => [assignment.id, await apiFetchJson<ApiReviewSummary>(`/api/assignments/${assignment.id}/review-summary`)] as const)),
Promise.all(redoAssignments.map(async (assignment) => [assignment.id, (await apiFetchJson<ApiListResponse<ApiReviewQueueItem>>(`/api/assignments/${assignment.id}/review`)).data] as const)),
]);
const students = studentsResponse.data.slice().sort((left, right) => left.full_name.localeCompare(right.full_name));
const reviewSummaryByAssignment = new Map(reviewSummaryEntries);
const reviewQueueByAssignment = new Map(reviewQueueEntries);
const selectedStudent =
(selectedStudentId != null ? students.find((student) => student.id === selectedStudentId) : null) ?? students[0] ?? null;
const studentItems = students.map((student) => {
const studentRedoRows = redoAssignments
.map((assignment) => ({ assignment, row: (reviewQueueByAssignment.get(assignment.id) ?? []).find((item) => item.student_id === student.id) ?? null }))
.filter((entry) => entry.row != null);
const liveRedoCount = studentRedoRows.filter((entry) => entry.assignment.status !== "closed").length;
const closedRedoCount = studentRedoRows.filter((entry) => entry.assignment.status === "closed").length;
const submittedCount = studentRedoRows.filter((entry) => (entry.row?.submitted_questions ?? 0) > 0).length;
return {
id: student.id,
name: student.full_name,
email: student.email,
initials: initialsFor(student.full_name),
statusLabel: submittedCount > 0 ? "Needs review" : liveRedoCount > 0 ? "Redo active" : closedRedoCount > 0 ? "Redo history" : "No redo",
redoCountLabel: `${studentRedoRows.length} redo assignment${studentRedoRows.length === 1 ? "" : "s"}`,
note: studentNote(submittedCount, liveRedoCount, closedRedoCount),
href: getDashboardTeacherClassroomHref(classroomId, student.id),
selected: student.id === selectedStudent?.id,
};
});
const selectedStudentRedoAssignments = selectedStudent
? redoAssignments
.map((assignment) => {
const queueRow = (reviewQueueByAssignment.get(assignment.id) ?? []).find((item) => item.student_id === selectedStudent.id);
if (!queueRow) return null;
const latestActivity = queueRow.latest_submitted_at ?? queueRow.latest_reviewed_at;
const summary = reviewSummaryByAssignment.get(assignment.id);
return {
id: assignment.id,
title: assignment.title,
statusLabel: assignment.status === "closed" ? "Closed" : queueStatusLabel(queueRow),
statusTone: assignment.status === "closed" ? "success" : queueStatusTone(queueRow),
dueLabel: `Due ${formatShortDate(assignment.due_at)}`,
progressLabel: `${queueRow.reviewed_questions}/${queueRow.total_questions} reviewed`,
nextStepLabel: queueRow.next_step_outcome ? `Next step: ${queueRow.next_step_outcome.replace(/_/g, " ")}` : "Next step still pending",
note:
(queueRow.submitted_questions ?? 0) > 0
? `${queueRow.submitted_questions} question${queueRow.submitted_questions === 1 ? " is" : "s are"} waiting for review.`
: summary && summary.reviewed > 0
? `Latest activity ${formatRelativeTime(latestActivity)}.`
: assignment.status === "closed"
? "Closed redo assignment kept here for this students history."
: "Assigned redo work with no submission yet.",
href: getAssignmentReviewHref("teacher", assignment.id),
};
})
.filter((item): item is TeacherClassroomRedoAssignmentItem => item != null)
.sort((left, right) => {
const leftClosed = left.statusLabel === "Closed" ? 1 : 0;
const rightClosed = right.statusLabel === "Closed" ? 1 : 0;
return leftClosed - rightClosed || left.title.localeCompare(right.title);
})
: [];
const liveRedoCount = redoAssignments.filter((assignment) => assignment.status !== "closed").length;
const classroomNeedsReviewCount = redoAssignments.reduce((total, assignment) => {
const rows = reviewQueueByAssignment.get(assignment.id) ?? [];
return total + rows.filter((row) => row.submitted_questions > 0).length;
}, 0);
return {
shell: buildTeacherShell(classroom.name, classroom.code, classrooms.length, classroomNeedsReviewCount),
classroom: {
id: classroom.id,
name: classroom.name,
description: classroom.description?.trim() || "Track each students follow-up work and individual redo assignments from here.",
codeLabel: classroom.code ? `Invite code ${classroom.code}` : "Invite code not set",
stats: [
{ label: teacherDashboardLabels.classroomDetail.students, value: `${students.length}`, note: students.length === 0 ? "No students in this classroom yet" : "Roster ready for drilldown" },
{ label: teacherDashboardLabels.classroomDetail.redoAssignments, value: `${redoAssignments.length}`, note: `${liveRedoCount} live · ${redoAssignments.length - liveRedoCount} closed` },
{ label: teacherDashboardLabels.classroomDetail.waitingReview, value: `${classroomNeedsReviewCount}`, note: classroomNeedsReviewCount > 0 ? "Student-specific redo work needs attention" : "Nothing urgent in redo follow-up" },
],
},
students: {
title: teacherDashboardLabels.classroomDetail.students,
description: "Pick a student to inspect their individual redo assignments in this classroom.",
items: studentItems,
},
selectedStudent: {
id: selectedStudent?.id ?? null,
name: selectedStudent?.full_name ?? null,
email: selectedStudent?.email ?? null,
note: selectedStudent
? selectedStudentRedoAssignments.length > 0
? `${selectedStudentRedoAssignments.length} redo assignment${selectedStudentRedoAssignments.length === 1 ? "" : "s"} tied to this student in ${classroom.name}.`
: "This student does not have any individual redo assignments in this classroom yet."
: "Select a student to view their individual redo assignments.",
},
redoAssignments: {
title: selectedStudent ? `${selectedStudent.full_name} redo assignments` : "Student redo assignments",
description: selectedStudent
? "These are the individual redo assignments created specifically for the selected student."
: "Select a student from the roster to inspect their individual redo assignments.",
items: selectedStudentRedoAssignments,
},
};
};