- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes - Add dashboard-error-fallback and dashboard-loading-skeleton components - Add student/learning page, parent/leave routes, teacher textbook components - Update existing app routes across auth, dashboard, and API endpoints - Update proxy middleware and next-auth type declarations
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import type { JSX } from "react"
|
||
import { getTeacherClasses } from "@/modules/classes/data-access"
|
||
import { getClassStudentsForEntry } from "@/modules/grades/data-access"
|
||
import { getSubjectOptions } from "@/modules/school/data-access"
|
||
import { BatchGradeEntry } from "@/modules/grades/components/batch-grade-entry"
|
||
import { requirePermission } from "@/shared/lib/auth-guard"
|
||
import { Permissions } from "@/shared/types/permissions"
|
||
import { getParam, type SearchParams } from "@/shared/lib/search-params"
|
||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||
import { ClipboardList } from "lucide-react"
|
||
|
||
export const dynamic = "force-dynamic"
|
||
|
||
export default async function BatchEntryPage({
|
||
searchParams,
|
||
}: {
|
||
searchParams: Promise<SearchParams>
|
||
}): Promise<JSX.Element> {
|
||
const ctx = await requirePermission(Permissions.GRADE_RECORD_MANAGE)
|
||
const sp = await searchParams
|
||
|
||
const defaultClassId = getParam(sp, "classId")
|
||
const defaultSubjectId = getParam(sp, "subjectId")
|
||
|
||
// P3 修复:添加 scope 校验,对 class_taught scope 限制可录入的班级
|
||
const [classes, allSubjects, students] = await Promise.all([
|
||
getTeacherClasses(),
|
||
getSubjectOptions(),
|
||
defaultClassId
|
||
? getClassStudentsForEntry(defaultClassId, ctx.dataScope)
|
||
: Promise.resolve([] as Awaited<ReturnType<typeof getClassStudentsForEntry>>),
|
||
])
|
||
|
||
// 对 class_taught scope,过滤掉不在 scope 中的班级
|
||
const allowedClassIds =
|
||
ctx.dataScope.type === "class_taught" ? ctx.dataScope.classIds : null
|
||
const scopedClasses = allowedClassIds
|
||
? classes.filter((c) => allowedClassIds.includes(c.id))
|
||
: classes
|
||
|
||
const classOptions = scopedClasses.map((c) => ({ id: c.id, name: c.name }))
|
||
const subjectOptions = allSubjects.map((s) => ({ id: s.id, name: s.name }))
|
||
|
||
// 如果指定了 classId 但 scope 不允许,显示提示
|
||
if (defaultClassId && students.length === 0 && scopedClasses.length > 0) {
|
||
const classExists = scopedClasses.some((c) => c.id === defaultClassId)
|
||
if (!classExists) {
|
||
return (
|
||
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
|
||
<div>
|
||
<h1 className="text-2xl font-bold tracking-tight">Batch Grade Entry</h1>
|
||
<p className="text-muted-foreground">Enter grades for all students in a class at once.</p>
|
||
</div>
|
||
<EmptyState
|
||
title="无权访问该班级"
|
||
description="您没有权限为该班级录入成绩。"
|
||
icon={ClipboardList}
|
||
className="border-none shadow-none"
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
|
||
<div>
|
||
<h1 className="text-2xl font-bold tracking-tight">Batch Grade Entry</h1>
|
||
<p className="text-muted-foreground">Enter grades for all students in a class at once.</p>
|
||
</div>
|
||
|
||
<BatchGradeEntry
|
||
classes={classOptions}
|
||
subjects={subjectOptions}
|
||
students={students}
|
||
defaultClassId={defaultClassId}
|
||
defaultSubjectId={defaultSubjectId}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|