refactor: fix all P0/P1/P2 bugs and architecture issues
Bug fixes (from bugs/ directory): - Fix cross-module DB queries in 9 modules (homework, grades, parent, diagnostic, elective, proctoring, notifications, scheduling, classes) by routing through data-access functions - Fix shared/lib <-> auth circular dependency via new session.ts module - Fix divide-by-zero guard in grades data-access - Fix audit export data truncation (paginated fetch for full datasets) - Fix missing transactions in homework grading and elective lottery - Fix missing revalidatePath in course-plans actions - Fix frontend permission checks using requirePermission instead of requireAuth - Fix dashboard role routing using session.user.roles - Fix student auth pattern (migrate getDemoStudentUser to users module) - Fix ActionState return type handling in components Code quality fixes: - Remove 60+ as type assertions (replace with type guards) - Remove non-null assertions (use optional chaining or explicit checks) - Convert dynamic imports to static imports (grades, diagnostic) - Add React.cache() wrapping for read functions - Parallelize independent queries with Promise.all - Add explicit return types to 30+ arrow functions - Replace any with unknown + type guards - Fix import type for type-only imports - Add Zod validation schemas for classes and diagnostic modules - Extract duplicate code (normalizeRoleName, normalizeBcryptHash, logger IP extraction) - Add console.error to silent catch blocks - Fix permission naming consistency (exam:proctor_read -> exam:proctor:read) Architecture doc sync: - Update 004_architecture_impact_map.md and 005_architecture_data.json - Update management-modules-audit.md for P0-7 cross-module fix Moved deleted proctoring event route to deletes/ folder.
This commit is contained in:
@@ -1,21 +1,23 @@
|
||||
import "server-only";
|
||||
|
||||
import { cache } from "react"
|
||||
import { and, asc, count, desc, eq, inArray, sql, type SQL } from "drizzle-orm"
|
||||
import { and, asc, count, eq, inArray } from "drizzle-orm"
|
||||
|
||||
import { db } from "@/shared/db"
|
||||
import {
|
||||
classes,
|
||||
classEnrollments,
|
||||
grades,
|
||||
homeworkAssignmentQuestions,
|
||||
homeworkAssignmentTargets,
|
||||
homeworkAssignments,
|
||||
homeworkSubmissions,
|
||||
schools,
|
||||
subjects,
|
||||
exams,
|
||||
} from "@/shared/db/schema"
|
||||
import {
|
||||
getAssignmentIdsForStudents,
|
||||
getAssignmentMaxScoreById,
|
||||
getAssignmentTargetCounts,
|
||||
getHomeworkAssignmentsByIds,
|
||||
getHomeworkAssignmentsWithSubject,
|
||||
getHomeworkSubmissionsForStudents,
|
||||
} from "@/modules/homework/data-access-classes"
|
||||
import type {
|
||||
ClassHomeworkInsights,
|
||||
ClassHomeworkAssignmentStats,
|
||||
@@ -23,6 +25,7 @@ import type {
|
||||
GradeHomeworkInsights,
|
||||
ScoreStats,
|
||||
} from "./types"
|
||||
import type { HomeworkSubmissionRecord } from "@/modules/homework/data-access-classes"
|
||||
import {
|
||||
getAccessibleClassIdsForTeacher,
|
||||
getSessionTeacherId,
|
||||
@@ -52,6 +55,74 @@ const toScoreStats = (scores: number[]): ScoreStats => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildLatestSubmissionByKey = (
|
||||
submissions: HomeworkSubmissionRecord[]
|
||||
): Map<string, HomeworkSubmissionRecord> => {
|
||||
const map = new Map<string, HomeworkSubmissionRecord>()
|
||||
for (const s of submissions) {
|
||||
const key = `${s.assignmentId}:${s.studentId}`
|
||||
if (!map.has(key)) map.set(key, s)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const computeAssignmentStats = (params: {
|
||||
assignments: Array<{
|
||||
id: string
|
||||
title: string
|
||||
status: string | null
|
||||
createdAt: Date
|
||||
dueAt: Date | null
|
||||
subjectName?: string | null
|
||||
}>
|
||||
studentIds: string[]
|
||||
latestByKey: Map<string, HomeworkSubmissionRecord>
|
||||
maxScoreByAssignmentId: Map<string, number>
|
||||
targetCountByAssignmentId: Map<string, number>
|
||||
}): { stats: ClassHomeworkAssignmentStats[]; allScored: number[] } => {
|
||||
const { assignments, studentIds, latestByKey, maxScoreByAssignmentId, targetCountByAssignmentId } = params
|
||||
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"
|
||||
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 ?? "draft",
|
||||
subject: a.subjectName ?? null,
|
||||
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),
|
||||
}
|
||||
})
|
||||
|
||||
return { stats, allScored }
|
||||
}
|
||||
|
||||
export const getClassHomeworkInsights = cache(
|
||||
async (params: { classId: string; teacherId?: string; limit?: number }): Promise<ClassHomeworkInsights | null> => {
|
||||
const teacherId = params.teacherId ?? (await getSessionTeacherId())
|
||||
@@ -127,12 +198,7 @@ export const getClassHomeworkInsights = cache(
|
||||
}
|
||||
}
|
||||
|
||||
const assignmentIdRows = await db
|
||||
.selectDistinct({ assignmentId: homeworkAssignmentTargets.assignmentId })
|
||||
.from(homeworkAssignmentTargets)
|
||||
.where(inArray(homeworkAssignmentTargets.studentId, studentIds))
|
||||
|
||||
const assignmentIds = assignmentIdRows.map((r) => r.assignmentId)
|
||||
const assignmentIds = await getAssignmentIdsForStudents(studentIds)
|
||||
if (assignmentIds.length === 0) {
|
||||
return {
|
||||
class: {
|
||||
@@ -151,26 +217,11 @@ export const getClassHomeworkInsights = cache(
|
||||
}
|
||||
|
||||
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 assignments = await getHomeworkAssignmentsWithSubject({
|
||||
assignmentIds,
|
||||
subjectIdFilter: subjectIdFilter.length > 0 ? subjectIdFilter : undefined,
|
||||
limit,
|
||||
})
|
||||
|
||||
const usedAssignmentIds = assignments.map((a) => a.id)
|
||||
if (usedAssignmentIds.length === 0) {
|
||||
@@ -190,86 +241,19 @@ export const getClassHomeworkInsights = cache(
|
||||
}
|
||||
}
|
||||
|
||||
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, targetCountByAssignmentId, submissions] = await Promise.all([
|
||||
getAssignmentMaxScoreById(usedAssignmentIds),
|
||||
getAssignmentTargetCounts({ assignmentIds: usedAssignmentIds, studentIds }),
|
||||
getHomeworkSubmissionsForStudents({ assignmentIds: usedAssignmentIds, studentIds }),
|
||||
])
|
||||
|
||||
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 latestByKey = buildLatestSubmissionByKey(submissions)
|
||||
const { stats, allScored } = computeAssignmentStats({
|
||||
assignments,
|
||||
studentIds,
|
||||
latestByKey,
|
||||
maxScoreByAssignmentId,
|
||||
targetCountByAssignmentId,
|
||||
})
|
||||
|
||||
const overallScores = toScoreStats(allScored)
|
||||
@@ -390,12 +374,7 @@ export const getGradeHomeworkInsights = cache(
|
||||
}
|
||||
}
|
||||
|
||||
const assignmentIdRows = await db
|
||||
.selectDistinct({ assignmentId: homeworkAssignmentTargets.assignmentId })
|
||||
.from(homeworkAssignmentTargets)
|
||||
.where(inArray(homeworkAssignmentTargets.studentId, studentIds))
|
||||
|
||||
const assignmentIds = assignmentIdRows.map((r) => r.assignmentId)
|
||||
const assignmentIds = await getAssignmentIdsForStudents(studentIds)
|
||||
if (assignmentIds.length === 0) {
|
||||
const summaries: GradeHomeworkClassSummary[] = classRows.map((c) => {
|
||||
const bucket = studentsByClassId.get(c.id) ?? { all: new Set<string>(), active: new Set<string>() }
|
||||
@@ -421,11 +400,7 @@ export const getGradeHomeworkInsights = cache(
|
||||
}
|
||||
|
||||
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 assignments = await getHomeworkAssignmentsByIds({ assignmentIds, limit })
|
||||
|
||||
const usedAssignmentIds = assignments.map((a) => a.id)
|
||||
if (usedAssignmentIds.length === 0) {
|
||||
@@ -452,85 +427,19 @@ export const getGradeHomeworkInsights = cache(
|
||||
}
|
||||
}
|
||||
|
||||
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, targetCountByAssignmentId, submissions] = await Promise.all([
|
||||
getAssignmentMaxScoreById(usedAssignmentIds),
|
||||
getAssignmentTargetCounts({ assignmentIds: usedAssignmentIds, studentIds }),
|
||||
getHomeworkSubmissionsForStudents({ assignmentIds: usedAssignmentIds, studentIds }),
|
||||
])
|
||||
|
||||
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 latestByKey = buildLatestSubmissionByKey(submissions)
|
||||
const { stats, allScored } = computeAssignmentStats({
|
||||
assignments,
|
||||
studentIds,
|
||||
latestByKey,
|
||||
maxScoreByAssignmentId,
|
||||
targetCountByAssignmentId,
|
||||
})
|
||||
|
||||
const overallScores = toScoreStats(allScored)
|
||||
|
||||
Reference in New Issue
Block a user