153 lines
8.0 KiB
TypeScript
153 lines
8.0 KiB
TypeScript
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 student’s 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 student’s 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,
|
||
},
|
||
};
|
||
};
|