Before Fine Tune
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { apiFetchJson } from "../../../lib/api";
|
||||
import type { ApiAssignment, ApiClassroom, ApiListResponse, ApiReviewQueueItem, ApiReviewSummary, ApiStudent } from "../../../lib/api-types";
|
||||
import type { ApiAssignment, ApiAssignmentStudentQuestionDetail, ApiClassroom, ApiListResponse, ApiReviewQueueItem, ApiReviewSummary, ApiStudent } from "../../../lib/api-types";
|
||||
import {
|
||||
getAssignmentReviewHref,
|
||||
getDashboardTeacherClassroomHref,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
buildTeacherShell,
|
||||
formatRelativeTime,
|
||||
formatShortDate,
|
||||
formatCombinedScoreLabel,
|
||||
initialsFor,
|
||||
queueStatusLabel,
|
||||
queueStatusTone,
|
||||
@@ -45,6 +46,43 @@ export const getTeacherClassroomDetailData = async (
|
||||
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 scoredAssignments = classroomAssignments.filter((assignment) => assignment.status !== "draft");
|
||||
|
||||
const studentScoreEntries = await Promise.all(
|
||||
students.map(async (student) => {
|
||||
const scoreRows = await Promise.all(
|
||||
scoredAssignments.map(async (assignment) => {
|
||||
const questions = (
|
||||
await apiFetchJson<ApiListResponse<ApiAssignmentStudentQuestionDetail>>(`/api/assignments/${assignment.id}/students/${student.id}/questions`)
|
||||
).data;
|
||||
const overallScore = questions.find((question) => typeof question.overall_score === "number")?.overall_score;
|
||||
return typeof overallScore === "number" ? overallScore : null;
|
||||
}),
|
||||
);
|
||||
|
||||
const numericScores = scoreRows.filter((score): score is number => typeof score === "number");
|
||||
const combinedScore = numericScores.length > 0 ? numericScores.reduce((sum, value) => sum + value, 0) : null;
|
||||
|
||||
return [student.id, { combinedScore, scoredAssignments: numericScores.length }] as const;
|
||||
}),
|
||||
);
|
||||
|
||||
const scoreByStudentId = new Map(studentScoreEntries);
|
||||
const endangeredRankByStudentId = new Map(
|
||||
studentScoreEntries
|
||||
.filter(([, entry]) => entry.combinedScore != null)
|
||||
.sort((left, right) => {
|
||||
const scoreDelta = (left[1].combinedScore ?? Number.POSITIVE_INFINITY) - (right[1].combinedScore ?? Number.POSITIVE_INFINITY);
|
||||
if (scoreDelta !== 0) return scoreDelta;
|
||||
const scoredAssignmentDelta = left[1].scoredAssignments - right[1].scoredAssignments;
|
||||
if (scoredAssignmentDelta !== 0) return scoredAssignmentDelta;
|
||||
const leftStudent = students.find((student) => student.id === left[0]);
|
||||
const rightStudent = students.find((student) => student.id === right[0]);
|
||||
return (leftStudent?.full_name ?? "").localeCompare(rightStudent?.full_name ?? "");
|
||||
})
|
||||
.slice(0, 3)
|
||||
.map(([studentId], index) => [studentId, (index + 1) as 1 | 2 | 3]),
|
||||
);
|
||||
|
||||
const selectedStudent =
|
||||
(selectedStudentId != null ? students.find((student) => student.id === selectedStudentId) : null) ?? students[0] ?? null;
|
||||
@@ -57,6 +95,7 @@ export const getTeacherClassroomDetailData = async (
|
||||
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;
|
||||
const scoring = scoreByStudentId.get(student.id) ?? { combinedScore: null, scoredAssignments: 0 };
|
||||
|
||||
return {
|
||||
id: student.id,
|
||||
@@ -65,9 +104,11 @@ export const getTeacherClassroomDetailData = async (
|
||||
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"}`,
|
||||
combinedScoreLabel: formatCombinedScoreLabel(scoring.combinedScore, scoring.scoredAssignments),
|
||||
note: studentNote(submittedCount, liveRedoCount, closedRedoCount),
|
||||
href: getDashboardTeacherClassroomHref(classroomId, student.id),
|
||||
selected: student.id === selectedStudent?.id,
|
||||
endangeredRank: endangeredRankByStudentId.get(student.id) ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -105,3 +105,11 @@ export const studentNote = (submittedCount: number, activeRedoCount: number, clo
|
||||
if (closedRedoCount > 0) return `${closedRedoCount} closed redo assignment${closedRedoCount === 1 ? "" : "s"} available for reference.`;
|
||||
return "No individual redo assignments for this student yet.";
|
||||
};
|
||||
|
||||
export const formatCombinedScoreLabel = (combinedScore: number | null, scoredAssignments: number) => {
|
||||
if (combinedScore == null || scoredAssignments <= 0) {
|
||||
return "No scored assignments yet";
|
||||
}
|
||||
|
||||
return `Combined score ${combinedScore.toFixed(1)} across ${scoredAssignments} assignment${scoredAssignments === 1 ? "" : "s"}`;
|
||||
};
|
||||
|
||||
@@ -52,7 +52,16 @@ const DashboardTeacherClassroomDetail: Component<Props> = (props) => {
|
||||
<div class={styles.studentList}>
|
||||
<For each={props.data.students.items}>
|
||||
{(student) => (
|
||||
<A href={student.href} class={`${styles.studentCard} ${student.selected ? styles.studentCardSelected : ""}`.trim()}>
|
||||
<A
|
||||
href={student.href}
|
||||
class={[
|
||||
styles.studentCard,
|
||||
student.selected ? styles.studentCardSelected : "",
|
||||
student.endangeredRank != null ? styles[`studentCardEndangered${student.endangeredRank}`] : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<div class={styles.studentCardHeader}>
|
||||
<div class={styles.studentAvatar}>{student.initials}</div>
|
||||
<div>
|
||||
@@ -63,7 +72,11 @@ const DashboardTeacherClassroomDetail: Component<Props> = (props) => {
|
||||
<div class={styles.metaRow}>
|
||||
<span>{student.statusLabel}</span>
|
||||
<span>{student.redoCountLabel}</span>
|
||||
<Show when={student.endangeredRank != null}>
|
||||
<span>{student.endangeredRank === 1 ? "Most endangered" : student.endangeredRank === 2 ? "Second most endangered" : "Third most endangered"}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<p class={styles.studentScoreLabel}>{student.combinedScoreLabel}</p>
|
||||
<p class={styles.studentNote}>{student.note}</p>
|
||||
</A>
|
||||
)}
|
||||
|
||||
@@ -7,9 +7,11 @@ export type TeacherClassroomDetailStudentItem = {
|
||||
initials: string;
|
||||
statusLabel: string;
|
||||
redoCountLabel: string;
|
||||
combinedScoreLabel: string;
|
||||
note: string;
|
||||
href: string;
|
||||
selected: boolean;
|
||||
endangeredRank: 1 | 2 | 3 | null;
|
||||
};
|
||||
|
||||
export type TeacherClassroomRedoAssignmentItem = {
|
||||
|
||||
@@ -251,6 +251,21 @@
|
||||
background: color-mix(in srgb, var(--surface-info) 12%, var(--surface-panel-strong) 88%);
|
||||
}
|
||||
|
||||
.studentCardEndangered1 {
|
||||
border-color: color-mix(in srgb, var(--danger) 36%, var(--border-soft) 64%);
|
||||
background: color-mix(in srgb, var(--surface-danger) 22%, var(--surface-panel-strong) 78%);
|
||||
}
|
||||
|
||||
.studentCardEndangered2 {
|
||||
border-color: color-mix(in srgb, var(--warning) 30%, var(--border-soft) 70%);
|
||||
background: color-mix(in srgb, var(--surface-warning) 18%, var(--surface-panel-strong) 82%);
|
||||
}
|
||||
|
||||
.studentCardEndangered3 {
|
||||
border-color: color-mix(in srgb, var(--info) 22%, var(--border-soft) 78%);
|
||||
background: color-mix(in srgb, var(--surface-info) 12%, var(--surface-panel-strong) 88%);
|
||||
}
|
||||
|
||||
.studentCardHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -285,6 +300,12 @@
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.studentScoreLabel {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
|
||||
.selectedStudentCard {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
|
||||
Reference in New Issue
Block a user