Boost Azure Demo

This commit is contained in:
MangoPig
2026-05-25 17:05:06 +01:00
parent 675285e99d
commit 4f79137d89
230 changed files with 43275 additions and 2644 deletions

View File

@@ -0,0 +1,152 @@
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,
},
};
};