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,13 +1,12 @@
|
||||
import "server-only"
|
||||
|
||||
import { cache } from "react"
|
||||
import { and, asc, eq } from "drizzle-orm"
|
||||
|
||||
import { db } from "@/shared/db"
|
||||
import {
|
||||
classEnrollments,
|
||||
gradeRecords,
|
||||
users,
|
||||
} from "@/shared/db/schema"
|
||||
import { gradeRecords } from "@/shared/db/schema"
|
||||
import { getStudentActiveClassId } from "@/modules/classes/data-access"
|
||||
import { getUserNamesByIds } from "@/modules/users/data-access"
|
||||
|
||||
import type {
|
||||
RankingTrendPoint,
|
||||
@@ -29,93 +28,92 @@ const normalize = (score: number, fullScore: number): number => {
|
||||
* Each point represents one assessment (grouped by title), with the
|
||||
* student's normalized score, rank, and total participants.
|
||||
*/
|
||||
export async function getRankingTrend(
|
||||
studentId: string,
|
||||
subjectId?: string,
|
||||
semester?: "1" | "2"
|
||||
): Promise<RankingTrendResult | null> {
|
||||
const [student] = await db
|
||||
.select({ id: users.id, name: users.name })
|
||||
.from(users)
|
||||
.where(eq(users.id, studentId))
|
||||
.limit(1)
|
||||
if (!student) return null
|
||||
export const getRankingTrend = cache(
|
||||
async (
|
||||
studentId: string,
|
||||
subjectId?: string,
|
||||
semester?: "1" | "2"
|
||||
): Promise<RankingTrendResult | null> => {
|
||||
const studentNameMap = await getUserNamesByIds([studentId])
|
||||
const studentInfo = studentNameMap.get(studentId)
|
||||
if (!studentInfo) return null
|
||||
const studentName = studentInfo.name ?? "Unknown"
|
||||
|
||||
const [enrollment] = await db
|
||||
.select({ classId: classEnrollments.classId })
|
||||
.from(classEnrollments)
|
||||
.where(
|
||||
and(
|
||||
eq(classEnrollments.studentId, studentId),
|
||||
eq(classEnrollments.status, "active")
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
const classId = await getStudentActiveClassId(studentId)
|
||||
|
||||
if (!classId) {
|
||||
return {
|
||||
studentId,
|
||||
studentName,
|
||||
points: [],
|
||||
}
|
||||
}
|
||||
|
||||
const conditions = [eq(gradeRecords.classId, classId)]
|
||||
if (subjectId) conditions.push(eq(gradeRecords.subjectId, subjectId))
|
||||
if (semester) conditions.push(eq(gradeRecords.semester, semester))
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
title: gradeRecords.title,
|
||||
createdAt: gradeRecords.createdAt,
|
||||
studentId: gradeRecords.studentId,
|
||||
score: gradeRecords.score,
|
||||
fullScore: gradeRecords.fullScore,
|
||||
})
|
||||
.from(gradeRecords)
|
||||
.where(and(...conditions))
|
||||
.orderBy(asc(gradeRecords.createdAt))
|
||||
|
||||
const byTitle = new Map<
|
||||
string,
|
||||
{
|
||||
date: Date
|
||||
entries: Array<{ studentId: string; normalized: number }>
|
||||
}
|
||||
>()
|
||||
|
||||
for (const r of rows) {
|
||||
const entry = byTitle.get(r.title) ?? { date: r.createdAt, entries: [] }
|
||||
entry.entries.push({
|
||||
studentId: r.studentId,
|
||||
normalized: normalize(toNumber(r.score), toNumber(r.fullScore)),
|
||||
})
|
||||
byTitle.set(r.title, entry)
|
||||
}
|
||||
|
||||
const points: RankingTrendPoint[] = []
|
||||
for (const [title, entry] of byTitle.entries()) {
|
||||
if (entry.entries.length === 0) continue
|
||||
const sorted = [...entry.entries].sort((a, b) => b.normalized - a.normalized)
|
||||
// Single traversal: find rank and student entry together
|
||||
let rank = 0
|
||||
let studentEntry: { studentId: string; normalized: number } | null = null
|
||||
for (let i = 0; i < sorted.length; i += 1) {
|
||||
const e = sorted[i]
|
||||
if (e.studentId === studentId) {
|
||||
rank = i + 1
|
||||
studentEntry = e
|
||||
break
|
||||
}
|
||||
}
|
||||
if (rank <= 0 || !studentEntry) continue
|
||||
|
||||
points.push({
|
||||
title,
|
||||
date: entry.date.toISOString(),
|
||||
score: studentEntry.normalized,
|
||||
rank,
|
||||
totalStudents: sorted.length,
|
||||
})
|
||||
}
|
||||
|
||||
points.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
|
||||
|
||||
if (!enrollment) {
|
||||
return {
|
||||
studentId,
|
||||
studentName: student.name ?? "Unknown",
|
||||
points: [],
|
||||
studentName,
|
||||
points,
|
||||
}
|
||||
}
|
||||
|
||||
const conditions = [eq(gradeRecords.classId, enrollment.classId)]
|
||||
if (subjectId) conditions.push(eq(gradeRecords.subjectId, subjectId))
|
||||
if (semester) conditions.push(eq(gradeRecords.semester, semester))
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
title: gradeRecords.title,
|
||||
createdAt: gradeRecords.createdAt,
|
||||
studentId: gradeRecords.studentId,
|
||||
score: gradeRecords.score,
|
||||
fullScore: gradeRecords.fullScore,
|
||||
})
|
||||
.from(gradeRecords)
|
||||
.where(and(...conditions))
|
||||
.orderBy(asc(gradeRecords.createdAt))
|
||||
|
||||
const byTitle = new Map<
|
||||
string,
|
||||
{
|
||||
date: Date
|
||||
entries: Array<{ studentId: string; normalized: number }>
|
||||
}
|
||||
>()
|
||||
|
||||
for (const r of rows) {
|
||||
const entry = byTitle.get(r.title) ?? { date: r.createdAt, entries: [] }
|
||||
entry.entries.push({
|
||||
studentId: r.studentId,
|
||||
normalized: normalize(toNumber(r.score), toNumber(r.fullScore)),
|
||||
})
|
||||
byTitle.set(r.title, entry)
|
||||
}
|
||||
|
||||
const points: RankingTrendPoint[] = []
|
||||
for (const [title, entry] of byTitle.entries()) {
|
||||
if (entry.entries.length === 0) continue
|
||||
const sorted = [...entry.entries].sort((a, b) => b.normalized - a.normalized)
|
||||
const rank = sorted.findIndex((e) => e.studentId === studentId) + 1
|
||||
if (rank <= 0) continue
|
||||
const studentEntry = sorted.find((e) => e.studentId === studentId)
|
||||
if (!studentEntry) continue
|
||||
|
||||
points.push({
|
||||
title,
|
||||
date: entry.date.toISOString(),
|
||||
score: studentEntry.normalized,
|
||||
rank,
|
||||
totalStudents: sorted.length,
|
||||
})
|
||||
}
|
||||
|
||||
points.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
|
||||
|
||||
return {
|
||||
studentId,
|
||||
studentName: student.name ?? "Unknown",
|
||||
points,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user