refactor: P0-1/2/4 解耦修复 - 拆分过耦合文件 + dashboard 解耦
This commit is contained in:
604
src/modules/classes/data-access-stats.ts
Normal file
604
src/modules/classes/data-access-stats.ts
Normal file
@@ -0,0 +1,604 @@
|
||||
import "server-only";
|
||||
|
||||
import { cache } from "react"
|
||||
import { and, asc, count, desc, eq, inArray, sql, type SQL } from "drizzle-orm"
|
||||
|
||||
import { db } from "@/shared/db"
|
||||
import {
|
||||
classes,
|
||||
classEnrollments,
|
||||
grades,
|
||||
homeworkAssignmentQuestions,
|
||||
homeworkAssignmentTargets,
|
||||
homeworkAssignments,
|
||||
homeworkSubmissions,
|
||||
schools,
|
||||
subjects,
|
||||
exams,
|
||||
} from "@/shared/db/schema"
|
||||
import type {
|
||||
ClassHomeworkInsights,
|
||||
ClassHomeworkAssignmentStats,
|
||||
GradeHomeworkClassSummary,
|
||||
GradeHomeworkInsights,
|
||||
ScoreStats,
|
||||
} from "./types"
|
||||
import {
|
||||
getAccessibleClassIdsForTeacher,
|
||||
getSessionTeacherId,
|
||||
getTeacherSubjectIdsForClass,
|
||||
} from "./data-access"
|
||||
|
||||
const median = (sorted: number[]): number | null => {
|
||||
if (sorted.length === 0) return null
|
||||
const mid = Math.floor(sorted.length / 2)
|
||||
if (sorted.length % 2 === 1) return sorted[mid] ?? null
|
||||
const a = sorted[mid - 1]
|
||||
const b = sorted[mid]
|
||||
if (typeof a !== "number" || typeof b !== "number") return null
|
||||
return (a + b) / 2
|
||||
}
|
||||
|
||||
const toScoreStats = (scores: number[]): ScoreStats => {
|
||||
if (scores.length === 0) return { count: 0, avg: null, median: null, min: null, max: null }
|
||||
const sorted = [...scores].sort((a, b) => a - b)
|
||||
const sum = sorted.reduce((acc, v) => acc + v, 0)
|
||||
return {
|
||||
count: sorted.length,
|
||||
avg: sum / sorted.length,
|
||||
median: median(sorted),
|
||||
min: sorted[0] ?? null,
|
||||
max: sorted[sorted.length - 1] ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export const getClassHomeworkInsights = cache(
|
||||
async (params: { classId: string; teacherId?: string; limit?: number }): Promise<ClassHomeworkInsights | null> => {
|
||||
const teacherId = params.teacherId ?? (await getSessionTeacherId())
|
||||
if (!teacherId) return null
|
||||
|
||||
const classId = params.classId.trim()
|
||||
if (!classId) return null
|
||||
const accessibleIds = await getAccessibleClassIdsForTeacher(teacherId)
|
||||
if (accessibleIds.length === 0 || !accessibleIds.includes(classId)) return null
|
||||
|
||||
const [classRow] = await db
|
||||
.select({
|
||||
id: classes.id,
|
||||
name: classes.name,
|
||||
grade: classes.grade,
|
||||
homeroom: classes.homeroom,
|
||||
room: classes.room,
|
||||
invitationCode: classes.invitationCode,
|
||||
teacherId: classes.teacherId,
|
||||
})
|
||||
.from(classes)
|
||||
.where(and(eq(classes.id, classId), inArray(classes.id, accessibleIds)))
|
||||
.limit(1)
|
||||
|
||||
if (!classRow) return null
|
||||
const isHomeroomTeacher = classRow.teacherId === teacherId
|
||||
const subjectIdFilter = isHomeroomTeacher ? [] : await getTeacherSubjectIdsForClass(teacherId, classId)
|
||||
|
||||
const enrollments = await db
|
||||
.select({
|
||||
studentId: classEnrollments.studentId,
|
||||
status: classEnrollments.status,
|
||||
})
|
||||
.from(classEnrollments)
|
||||
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
|
||||
.where(and(inArray(classes.id, accessibleIds), eq(classEnrollments.classId, classId)))
|
||||
|
||||
const activeStudentIds = enrollments.filter((e) => e.status === "active").map((e) => e.studentId)
|
||||
const inactiveStudentIds = enrollments.filter((e) => e.status !== "active").map((e) => e.studentId)
|
||||
const studentIds = enrollments.map((e) => e.studentId)
|
||||
|
||||
if (!isHomeroomTeacher && subjectIdFilter.length === 0) {
|
||||
return {
|
||||
class: {
|
||||
id: classRow.id,
|
||||
name: classRow.name,
|
||||
grade: classRow.grade,
|
||||
homeroom: classRow.homeroom,
|
||||
room: classRow.room,
|
||||
invitationCode: classRow.invitationCode ?? null,
|
||||
},
|
||||
studentCounts: { total: studentIds.length, active: activeStudentIds.length, inactive: inactiveStudentIds.length },
|
||||
assignments: [],
|
||||
latest: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
}
|
||||
}
|
||||
|
||||
if (studentIds.length === 0) {
|
||||
return {
|
||||
class: {
|
||||
id: classRow.id,
|
||||
name: classRow.name,
|
||||
grade: classRow.grade,
|
||||
homeroom: classRow.homeroom,
|
||||
room: classRow.room,
|
||||
invitationCode: classRow.invitationCode ?? null,
|
||||
},
|
||||
studentCounts: { total: 0, active: 0, inactive: 0 },
|
||||
assignments: [],
|
||||
latest: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
}
|
||||
}
|
||||
|
||||
const assignmentIdRows = await db
|
||||
.selectDistinct({ assignmentId: homeworkAssignmentTargets.assignmentId })
|
||||
.from(homeworkAssignmentTargets)
|
||||
.where(inArray(homeworkAssignmentTargets.studentId, studentIds))
|
||||
|
||||
const assignmentIds = assignmentIdRows.map((r) => r.assignmentId)
|
||||
if (assignmentIds.length === 0) {
|
||||
return {
|
||||
class: {
|
||||
id: classRow.id,
|
||||
name: classRow.name,
|
||||
grade: classRow.grade,
|
||||
homeroom: classRow.homeroom,
|
||||
room: classRow.room,
|
||||
invitationCode: classRow.invitationCode ?? null,
|
||||
},
|
||||
studentCounts: { total: studentIds.length, active: activeStudentIds.length, inactive: inactiveStudentIds.length },
|
||||
assignments: [],
|
||||
latest: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
}
|
||||
}
|
||||
|
||||
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 50
|
||||
const assignmentConditions: SQL[] = [inArray(homeworkAssignments.id, assignmentIds)]
|
||||
if (subjectIdFilter.length > 0) {
|
||||
assignmentConditions.push(inArray(exams.subjectId, subjectIdFilter))
|
||||
}
|
||||
const assignments = await db
|
||||
.select({
|
||||
id: homeworkAssignments.id,
|
||||
title: homeworkAssignments.title,
|
||||
status: homeworkAssignments.status,
|
||||
createdAt: homeworkAssignments.createdAt,
|
||||
dueAt: homeworkAssignments.dueAt,
|
||||
subjectId: exams.subjectId,
|
||||
subjectName: subjects.name
|
||||
})
|
||||
.from(homeworkAssignments)
|
||||
.innerJoin(exams, eq(homeworkAssignments.sourceExamId, exams.id))
|
||||
.leftJoin(subjects, eq(exams.subjectId, subjects.id))
|
||||
.where(and(...assignmentConditions))
|
||||
.orderBy(desc(homeworkAssignments.createdAt))
|
||||
.limit(limit)
|
||||
|
||||
const usedAssignmentIds = assignments.map((a) => a.id)
|
||||
if (usedAssignmentIds.length === 0) {
|
||||
return {
|
||||
class: {
|
||||
id: classRow.id,
|
||||
name: classRow.name,
|
||||
grade: classRow.grade,
|
||||
homeroom: classRow.homeroom,
|
||||
room: classRow.room,
|
||||
invitationCode: classRow.invitationCode ?? null,
|
||||
},
|
||||
studentCounts: { total: studentIds.length, active: activeStudentIds.length, inactive: inactiveStudentIds.length },
|
||||
assignments: [],
|
||||
latest: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
}
|
||||
}
|
||||
|
||||
const maxScoreRows = await db
|
||||
.select({
|
||||
assignmentId: homeworkAssignmentQuestions.assignmentId,
|
||||
maxScore: sql<number>`COALESCE(SUM(${homeworkAssignmentQuestions.score}), 0)`,
|
||||
})
|
||||
.from(homeworkAssignmentQuestions)
|
||||
.where(inArray(homeworkAssignmentQuestions.assignmentId, usedAssignmentIds))
|
||||
.groupBy(homeworkAssignmentQuestions.assignmentId)
|
||||
|
||||
const maxScoreByAssignmentId = new Map<string, number>()
|
||||
for (const r of maxScoreRows) maxScoreByAssignmentId.set(r.assignmentId, Number(r.maxScore ?? 0))
|
||||
|
||||
const targetCountRows = await db
|
||||
.select({
|
||||
assignmentId: homeworkAssignmentTargets.assignmentId,
|
||||
targetCount: sql<number>`COUNT(*)`,
|
||||
})
|
||||
.from(homeworkAssignmentTargets)
|
||||
.where(
|
||||
and(
|
||||
inArray(homeworkAssignmentTargets.assignmentId, usedAssignmentIds),
|
||||
inArray(homeworkAssignmentTargets.studentId, studentIds)
|
||||
)
|
||||
)
|
||||
.groupBy(homeworkAssignmentTargets.assignmentId)
|
||||
|
||||
const targetCountByAssignmentId = new Map<string, number>()
|
||||
for (const r of targetCountRows) targetCountByAssignmentId.set(r.assignmentId, Number(r.targetCount ?? 0))
|
||||
|
||||
const submissions = await db.query.homeworkSubmissions.findMany({
|
||||
where: and(
|
||||
inArray(homeworkSubmissions.assignmentId, usedAssignmentIds),
|
||||
inArray(homeworkSubmissions.studentId, studentIds)
|
||||
),
|
||||
orderBy: [desc(homeworkSubmissions.createdAt)],
|
||||
})
|
||||
|
||||
const latestByKey = new Map<string, (typeof submissions)[number]>()
|
||||
for (const s of submissions) {
|
||||
const key = `${s.assignmentId}:${s.studentId}`
|
||||
if (!latestByKey.has(key)) latestByKey.set(key, s)
|
||||
}
|
||||
|
||||
const allScored: number[] = []
|
||||
const nowMs = Date.now()
|
||||
|
||||
const stats: ClassHomeworkAssignmentStats[] = assignments.map((a) => {
|
||||
const targetCount = targetCountByAssignmentId.get(a.id) ?? 0
|
||||
let submittedCount = 0
|
||||
let gradedCount = 0
|
||||
const scores: number[] = []
|
||||
const dueMs = a.dueAt ? a.dueAt.getTime() : null
|
||||
|
||||
for (const studentId of studentIds) {
|
||||
const s = latestByKey.get(`${a.id}:${studentId}`)
|
||||
if (!s) continue
|
||||
|
||||
const status = (s.status ?? "started") as string
|
||||
if (status === "submitted" || status === "graded") submittedCount += 1
|
||||
if (status === "graded" || typeof s.score === "number") gradedCount += 1
|
||||
if (typeof s.score === "number") scores.push(s.score)
|
||||
}
|
||||
|
||||
allScored.push(...scores)
|
||||
|
||||
return {
|
||||
assignmentId: a.id,
|
||||
title: a.title,
|
||||
status: (a.status as string) ?? "draft",
|
||||
subject: a.subjectName,
|
||||
createdAt: a.createdAt.toISOString(),
|
||||
dueAt: a.dueAt ? a.dueAt.toISOString() : null,
|
||||
isActive: dueMs === null || dueMs >= nowMs,
|
||||
isOverdue: typeof dueMs === "number" && dueMs < nowMs,
|
||||
maxScore: maxScoreByAssignmentId.get(a.id) ?? 0,
|
||||
targetCount,
|
||||
submittedCount,
|
||||
gradedCount,
|
||||
scoreStats: toScoreStats(scores),
|
||||
}
|
||||
})
|
||||
|
||||
const overallScores = toScoreStats(allScored)
|
||||
const latest = stats[0] ?? null
|
||||
|
||||
return {
|
||||
class: {
|
||||
id: classRow.id,
|
||||
name: classRow.name,
|
||||
grade: classRow.grade,
|
||||
homeroom: classRow.homeroom,
|
||||
room: classRow.room,
|
||||
invitationCode: classRow.invitationCode ?? null,
|
||||
},
|
||||
studentCounts: { total: studentIds.length, active: activeStudentIds.length, inactive: inactiveStudentIds.length },
|
||||
assignments: stats,
|
||||
latest,
|
||||
overallScores,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const avg = (values: number[]): number | null => {
|
||||
if (values.length === 0) return null
|
||||
const sum = values.reduce((acc, v) => acc + v, 0)
|
||||
return sum / values.length
|
||||
}
|
||||
|
||||
export const getGradeHomeworkInsights = cache(
|
||||
async (params: { gradeId: string; limit?: number }): Promise<GradeHomeworkInsights | null> => {
|
||||
const gradeId = params.gradeId.trim()
|
||||
if (!gradeId) return null
|
||||
|
||||
const [gradeRow] = await db
|
||||
.select({
|
||||
id: grades.id,
|
||||
name: grades.name,
|
||||
schoolId: schools.id,
|
||||
schoolName: schools.name,
|
||||
})
|
||||
.from(grades)
|
||||
.innerJoin(schools, eq(schools.id, grades.schoolId))
|
||||
.where(eq(grades.id, gradeId))
|
||||
.limit(1)
|
||||
|
||||
if (!gradeRow) return null
|
||||
|
||||
const classRows = await db
|
||||
.select({
|
||||
id: classes.id,
|
||||
name: classes.name,
|
||||
grade: classes.grade,
|
||||
homeroom: classes.homeroom,
|
||||
room: classes.room,
|
||||
})
|
||||
.from(classes)
|
||||
.where(eq(classes.gradeId, gradeId))
|
||||
.orderBy(asc(classes.name), asc(classes.homeroom), asc(classes.room))
|
||||
|
||||
const classIds = classRows.map((r) => r.id)
|
||||
if (classIds.length === 0) {
|
||||
return {
|
||||
grade: { id: gradeRow.id, name: gradeRow.name, school: { id: gradeRow.schoolId, name: gradeRow.schoolName } },
|
||||
classCount: 0,
|
||||
studentCounts: { total: 0, active: 0, inactive: 0 },
|
||||
assignments: [],
|
||||
latest: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
classes: [],
|
||||
}
|
||||
}
|
||||
|
||||
const enrollmentRows = await db
|
||||
.select({
|
||||
classId: classEnrollments.classId,
|
||||
studentId: classEnrollments.studentId,
|
||||
status: classEnrollments.status,
|
||||
})
|
||||
.from(classEnrollments)
|
||||
.where(inArray(classEnrollments.classId, classIds))
|
||||
|
||||
const studentActiveById = new Map<string, boolean>()
|
||||
const studentsByClassId = new Map<string, { all: Set<string>; active: Set<string> }>()
|
||||
|
||||
for (const e of enrollmentRows) {
|
||||
const prev = studentActiveById.get(e.studentId) ?? false
|
||||
const next = prev || e.status === "active"
|
||||
studentActiveById.set(e.studentId, next)
|
||||
|
||||
const bucket = studentsByClassId.get(e.classId) ?? { all: new Set<string>(), active: new Set<string>() }
|
||||
bucket.all.add(e.studentId)
|
||||
if (e.status === "active") bucket.active.add(e.studentId)
|
||||
studentsByClassId.set(e.classId, bucket)
|
||||
}
|
||||
|
||||
const studentIds = Array.from(studentActiveById.keys())
|
||||
const activeCount = Array.from(studentActiveById.values()).filter(Boolean).length
|
||||
const inactiveCount = studentIds.length - activeCount
|
||||
|
||||
if (studentIds.length === 0) {
|
||||
const summaries: GradeHomeworkClassSummary[] = classRows.map((c) => ({
|
||||
class: { id: c.id, name: c.name, grade: c.grade, homeroom: c.homeroom, room: c.room },
|
||||
studentCounts: { total: 0, active: 0, inactive: 0 },
|
||||
latestAvg: null,
|
||||
prevAvg: null,
|
||||
deltaAvg: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
}))
|
||||
|
||||
return {
|
||||
grade: { id: gradeRow.id, name: gradeRow.name, school: { id: gradeRow.schoolId, name: gradeRow.schoolName } },
|
||||
classCount: classRows.length,
|
||||
studentCounts: { total: 0, active: 0, inactive: 0 },
|
||||
assignments: [],
|
||||
latest: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
classes: summaries,
|
||||
}
|
||||
}
|
||||
|
||||
const assignmentIdRows = await db
|
||||
.selectDistinct({ assignmentId: homeworkAssignmentTargets.assignmentId })
|
||||
.from(homeworkAssignmentTargets)
|
||||
.where(inArray(homeworkAssignmentTargets.studentId, studentIds))
|
||||
|
||||
const assignmentIds = assignmentIdRows.map((r) => r.assignmentId)
|
||||
if (assignmentIds.length === 0) {
|
||||
const summaries: GradeHomeworkClassSummary[] = classRows.map((c) => {
|
||||
const bucket = studentsByClassId.get(c.id) ?? { all: new Set<string>(), active: new Set<string>() }
|
||||
return {
|
||||
class: { id: c.id, name: c.name, grade: c.grade, homeroom: c.homeroom, room: c.room },
|
||||
studentCounts: { total: bucket.all.size, active: bucket.active.size, inactive: bucket.all.size - bucket.active.size },
|
||||
latestAvg: null,
|
||||
prevAvg: null,
|
||||
deltaAvg: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
grade: { id: gradeRow.id, name: gradeRow.name, school: { id: gradeRow.schoolId, name: gradeRow.schoolName } },
|
||||
classCount: classRows.length,
|
||||
studentCounts: { total: studentIds.length, active: activeCount, inactive: inactiveCount },
|
||||
assignments: [],
|
||||
latest: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
classes: summaries,
|
||||
}
|
||||
}
|
||||
|
||||
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 50
|
||||
const assignments = await db.query.homeworkAssignments.findMany({
|
||||
where: inArray(homeworkAssignments.id, assignmentIds),
|
||||
orderBy: [desc(homeworkAssignments.createdAt)],
|
||||
limit,
|
||||
})
|
||||
|
||||
const usedAssignmentIds = assignments.map((a) => a.id)
|
||||
if (usedAssignmentIds.length === 0) {
|
||||
const summaries: GradeHomeworkClassSummary[] = classRows.map((c) => {
|
||||
const bucket = studentsByClassId.get(c.id) ?? { all: new Set<string>(), active: new Set<string>() }
|
||||
return {
|
||||
class: { id: c.id, name: c.name, grade: c.grade, homeroom: c.homeroom, room: c.room },
|
||||
studentCounts: { total: bucket.all.size, active: bucket.active.size, inactive: bucket.all.size - bucket.active.size },
|
||||
latestAvg: null,
|
||||
prevAvg: null,
|
||||
deltaAvg: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
grade: { id: gradeRow.id, name: gradeRow.name, school: { id: gradeRow.schoolId, name: gradeRow.schoolName } },
|
||||
classCount: classRows.length,
|
||||
studentCounts: { total: studentIds.length, active: activeCount, inactive: inactiveCount },
|
||||
assignments: [],
|
||||
latest: null,
|
||||
overallScores: { count: 0, avg: null, median: null, min: null, max: null },
|
||||
classes: summaries,
|
||||
}
|
||||
}
|
||||
|
||||
const maxScoreRows = await db
|
||||
.select({
|
||||
assignmentId: homeworkAssignmentQuestions.assignmentId,
|
||||
maxScore: sql<number>`COALESCE(SUM(${homeworkAssignmentQuestions.score}), 0)`,
|
||||
})
|
||||
.from(homeworkAssignmentQuestions)
|
||||
.where(inArray(homeworkAssignmentQuestions.assignmentId, usedAssignmentIds))
|
||||
.groupBy(homeworkAssignmentQuestions.assignmentId)
|
||||
|
||||
const maxScoreByAssignmentId = new Map<string, number>()
|
||||
for (const r of maxScoreRows) maxScoreByAssignmentId.set(r.assignmentId, Number(r.maxScore ?? 0))
|
||||
|
||||
const targetCountRows = await db
|
||||
.select({
|
||||
assignmentId: homeworkAssignmentTargets.assignmentId,
|
||||
targetCount: sql<number>`COUNT(*)`,
|
||||
})
|
||||
.from(homeworkAssignmentTargets)
|
||||
.where(
|
||||
and(
|
||||
inArray(homeworkAssignmentTargets.assignmentId, usedAssignmentIds),
|
||||
inArray(homeworkAssignmentTargets.studentId, studentIds)
|
||||
)
|
||||
)
|
||||
.groupBy(homeworkAssignmentTargets.assignmentId)
|
||||
|
||||
const targetCountByAssignmentId = new Map<string, number>()
|
||||
for (const r of targetCountRows) targetCountByAssignmentId.set(r.assignmentId, Number(r.targetCount ?? 0))
|
||||
|
||||
const submissions = await db.query.homeworkSubmissions.findMany({
|
||||
where: and(
|
||||
inArray(homeworkSubmissions.assignmentId, usedAssignmentIds),
|
||||
inArray(homeworkSubmissions.studentId, studentIds)
|
||||
),
|
||||
orderBy: [desc(homeworkSubmissions.createdAt)],
|
||||
})
|
||||
|
||||
const latestByKey = new Map<string, (typeof submissions)[number]>()
|
||||
for (const s of submissions) {
|
||||
const key = `${s.assignmentId}:${s.studentId}`
|
||||
if (!latestByKey.has(key)) latestByKey.set(key, s)
|
||||
}
|
||||
|
||||
const allScored: number[] = []
|
||||
const nowMs = Date.now()
|
||||
|
||||
const stats: ClassHomeworkAssignmentStats[] = assignments.map((a) => {
|
||||
const targetCount = targetCountByAssignmentId.get(a.id) ?? 0
|
||||
let submittedCount = 0
|
||||
let gradedCount = 0
|
||||
const scores: number[] = []
|
||||
const dueMs = a.dueAt ? a.dueAt.getTime() : null
|
||||
|
||||
for (const studentId of studentIds) {
|
||||
const s = latestByKey.get(`${a.id}:${studentId}`)
|
||||
if (!s) continue
|
||||
|
||||
const status = (s.status ?? "started") as string
|
||||
if (status === "submitted" || status === "graded") submittedCount += 1
|
||||
if (status === "graded" || typeof s.score === "number") gradedCount += 1
|
||||
if (typeof s.score === "number") scores.push(s.score)
|
||||
}
|
||||
|
||||
allScored.push(...scores)
|
||||
|
||||
return {
|
||||
assignmentId: a.id,
|
||||
title: a.title,
|
||||
status: (a.status as string) ?? "draft",
|
||||
createdAt: a.createdAt.toISOString(),
|
||||
dueAt: a.dueAt ? a.dueAt.toISOString() : null,
|
||||
isActive: dueMs === null || dueMs >= nowMs,
|
||||
isOverdue: typeof dueMs === "number" && dueMs < nowMs,
|
||||
maxScore: maxScoreByAssignmentId.get(a.id) ?? 0,
|
||||
targetCount,
|
||||
submittedCount,
|
||||
gradedCount,
|
||||
scoreStats: toScoreStats(scores),
|
||||
}
|
||||
})
|
||||
|
||||
const overallScores = toScoreStats(allScored)
|
||||
const latest = stats[0] ?? null
|
||||
const latestAssignmentId = stats[0]?.assignmentId ?? null
|
||||
const prevAssignmentId = stats[1]?.assignmentId ?? null
|
||||
|
||||
const classSummaries: GradeHomeworkClassSummary[] = classRows.map((c) => {
|
||||
const bucket = studentsByClassId.get(c.id) ?? { all: new Set<string>(), active: new Set<string>() }
|
||||
const classStudentIds = Array.from(bucket.all)
|
||||
|
||||
const latestScores: number[] = []
|
||||
const prevScores: number[] = []
|
||||
const overallClassScores: number[] = []
|
||||
|
||||
if (latestAssignmentId) {
|
||||
for (const studentId of classStudentIds) {
|
||||
const s = latestByKey.get(`${latestAssignmentId}:${studentId}`)
|
||||
if (typeof s?.score === "number") latestScores.push(s.score)
|
||||
}
|
||||
}
|
||||
|
||||
if (prevAssignmentId) {
|
||||
for (const studentId of classStudentIds) {
|
||||
const s = latestByKey.get(`${prevAssignmentId}:${studentId}`)
|
||||
if (typeof s?.score === "number") prevScores.push(s.score)
|
||||
}
|
||||
}
|
||||
|
||||
for (const assignmentId of usedAssignmentIds) {
|
||||
for (const studentId of classStudentIds) {
|
||||
const s = latestByKey.get(`${assignmentId}:${studentId}`)
|
||||
if (typeof s?.score === "number") overallClassScores.push(s.score)
|
||||
}
|
||||
}
|
||||
|
||||
const latestAvg = avg(latestScores)
|
||||
const prevAvg = avg(prevScores)
|
||||
|
||||
return {
|
||||
class: { id: c.id, name: c.name, grade: c.grade, homeroom: c.homeroom, room: c.room },
|
||||
studentCounts: { total: bucket.all.size, active: bucket.active.size, inactive: bucket.all.size - bucket.active.size },
|
||||
latestAvg,
|
||||
prevAvg,
|
||||
deltaAvg: typeof latestAvg === "number" && typeof prevAvg === "number" ? latestAvg - prevAvg : null,
|
||||
overallScores: toScoreStats(overallClassScores),
|
||||
}
|
||||
})
|
||||
|
||||
classSummaries.sort((a, b) => (b.latestAvg ?? -Infinity) - (a.latestAvg ?? -Infinity))
|
||||
|
||||
return {
|
||||
grade: { id: gradeRow.id, name: gradeRow.name, school: { id: gradeRow.schoolId, name: gradeRow.schoolName } },
|
||||
classCount: classRows.length,
|
||||
studentCounts: { total: studentIds.length, active: activeCount, inactive: inactiveCount },
|
||||
assignments: stats,
|
||||
latest,
|
||||
overallScores,
|
||||
classes: classSummaries,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export type ClassesDashboardStats = {
|
||||
classCount: number
|
||||
}
|
||||
|
||||
export const getClassesDashboardStats = cache(async (): Promise<ClassesDashboardStats> => {
|
||||
const [row] = await db.select({ value: count() }).from(classes)
|
||||
return { classCount: Number(row?.value ?? 0) }
|
||||
})
|
||||
Reference in New Issue
Block a user