- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes - Add admin announcements edit, audit-logs overview, curriculum-map, invitation-codes, permissions, questions, roles routes - Add admin elective detail and components, files, course-plans, users, scheduling boundaries - Add messages group-compose route - Add parent course-plans, elective, grades report-card, practice routes - Add student course-plans, elective detail, error-book dialogs, grades report-card, learning study-path, leave, schedule boundaries - Add teacher attendance report, classes boundaries, course-plans boundaries, elective, exams analytics/edit-rich/all/create/new, grades report-card, homework boundaries, leave, lesson-plans calendar - Add auth loading, onboarding loading, api cron
97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
import type { JSX } from "react"
|
||
import { ArrowLeft } from "lucide-react"
|
||
import Link from "next/link"
|
||
import { getTranslations } from "next-intl/server"
|
||
|
||
import { Button } from "@/shared/components/ui/button"
|
||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||
import { requirePermission } from "@/shared/lib/auth-guard"
|
||
import { Permissions } from "@/shared/types/permissions"
|
||
import { getParam, type SearchParams } from "@/shared/lib/search-params"
|
||
|
||
import { getReportCardData } from "@/modules/grades/lib/report-card"
|
||
import { ReportCardView } from "@/modules/grades/components/report-card-view"
|
||
import { ReportCardPrintAction } from "@/modules/grades/components/report-card-print-action"
|
||
import { getAcademicYears } from "@/modules/school/data-access"
|
||
|
||
export const dynamic = "force-dynamic"
|
||
|
||
/**
|
||
* P3-1: 学生视角的成绩报告卡页面。
|
||
*
|
||
* 路由:/student/grades/report-card
|
||
* 查询参数:
|
||
* - academicYearId?: 指定学年(不传则使用当前活跃学年)
|
||
* - semester?: "1" | "2"(不传则全部学期)
|
||
*
|
||
* 权限:GRADE_RECORD_READ(学生 scope 自动限制为本人)
|
||
*/
|
||
export default async function StudentReportCardPage({
|
||
searchParams,
|
||
}: {
|
||
searchParams: Promise<SearchParams>
|
||
}): Promise<JSX.Element> {
|
||
const sp = await searchParams
|
||
const ctx = await requirePermission(Permissions.GRADE_RECORD_READ)
|
||
const t = await getTranslations("grades")
|
||
|
||
const academicYearIdParam = getParam(sp, "academicYearId")
|
||
const semesterParam = getParam(sp, "semester")
|
||
const academicYearId =
|
||
academicYearIdParam && academicYearIdParam !== "all"
|
||
? academicYearIdParam
|
||
: undefined
|
||
const semester =
|
||
semesterParam === "1" || semesterParam === "2" ? semesterParam : undefined
|
||
|
||
// 学生视角下 studentId 即为 ctx.userId(class_members scope)
|
||
const [data, academicYears] = await Promise.all([
|
||
getReportCardData(ctx.userId, ctx.dataScope, {
|
||
academicYearId,
|
||
semester,
|
||
}),
|
||
getAcademicYears(),
|
||
])
|
||
|
||
if (!data) {
|
||
return (
|
||
<div className="h-full flex-1 flex-col space-y-6 p-8 md:flex">
|
||
<Button asChild variant="ghost" size="sm" className="w-fit">
|
||
<Link href="/student/grades">
|
||
<ArrowLeft className="mr-2 h-4 w-4" aria-hidden="true" />
|
||
{t("reportCard.backToGrades")}
|
||
</Link>
|
||
</Button>
|
||
<EmptyState
|
||
title={t("reportCard.emptyTitle")}
|
||
description={t("reportCard.emptyDescription")}
|
||
icon={ArrowLeft}
|
||
className="border-none shadow-none"
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col gap-4 p-6">
|
||
<div className="flex items-center justify-between gap-4">
|
||
<Button asChild variant="ghost" size="sm">
|
||
<Link href="/student/grades">
|
||
<ArrowLeft className="mr-2 h-4 w-4" aria-hidden="true" />
|
||
{t("reportCard.backToGrades")}
|
||
</Link>
|
||
</Button>
|
||
<ReportCardPrintAction />
|
||
</div>
|
||
|
||
<ReportCardView data={data} />
|
||
|
||
<p className="text-center text-xs text-muted-foreground">
|
||
{t("reportCard.academicYearsCount", {
|
||
count: academicYears.length,
|
||
})}
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|