feat(homework,classes,course-plans): add scans, student data, take confirm, error boundaries, dialogs, hooks, calendar

homework:

- Add data-access-scans, data-access-student, data-access-utils, data-access-exam-cross

- Add excellent-submissions, homework-take-confirm-dialog, homework-take-sidebar components

classes:

- Add class-delete-dialog, class-error-boundary, class-form-dialog, class-form-utils

- Add class-list-table, class-list-toolbar, class-skeleton

- Add schedule-create-dialog, schedule-delete-dialog, schedule-edit-dialog, schedule-utils

- Add data-access-teacher and hooks directory

course-plans:

- Add course-plan-calendar, sortable-week-row, template-picker-dialog components

- Add lib directory
This commit is contained in:
SpecialX
2026-07-03 10:25:35 +08:00
parent 20023e13fd
commit dfffb61e94
82 changed files with 6100 additions and 3321 deletions

View File

@@ -1,6 +1,7 @@
"use server"
import { revalidatePath } from "next/cache"
import { getTranslations } from "next-intl/server"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
@@ -10,6 +11,7 @@ import {
createTeacherClass,
deleteTeacherClass,
updateTeacherClass,
verifyTeacherOwnsClass,
} from "./data-access"
import { findGradeIdByHeadAndName, isGradeHead } from "@/modules/school/data-access"
import {
@@ -25,6 +27,7 @@ export async function createTeacherClassAction(
): Promise<ActionState<string>> {
try {
const ctx = await requirePermission(Permissions.CLASS_CREATE)
const t = await getTranslations("classes")
const parsed = CreateTeacherClassSchema.safeParse({
name: formData.get("name"),
@@ -36,7 +39,7 @@ export async function createTeacherClassAction(
room: formData.get("room"),
})
if (!parsed.success) {
return { success: false, message: "Class name and grade are required" }
return { success: false, message: t("actions.classNameGradeRequired") }
}
const { name, grade, schoolName, schoolId, gradeId, homeroom, room } = parsed.data
@@ -50,7 +53,7 @@ export async function createTeacherClassAction(
? await isGradeHead(normalizedGradeId, userId)
: Boolean(await findGradeIdByHeadAndName(userId, grade))
if (!isOwner) {
return { success: false, message: "Only admins and grade heads can create classes" }
return { success: false, message: t("actions.onlyAdminsAndGradeHeads") }
}
}
@@ -67,7 +70,7 @@ export async function createTeacherClassAction(
revalidatePath("/teacher/classes/my")
revalidatePath("/teacher/classes/students")
revalidatePath("/teacher/classes/schedule")
return { success: true, message: "Class created successfully", data: id }
return { success: true, message: t("actions.classCreated"), data: id }
} catch (error) {
return handleActionError(error)
}
@@ -82,7 +85,8 @@ export async function updateTeacherClassAction(
formData: FormData
): Promise<ActionState> {
try {
await requirePermission(Permissions.CLASS_UPDATE)
const ctx = await requirePermission(Permissions.CLASS_UPDATE)
const t = await getTranslations("classes")
const parsed = UpdateTeacherClassSchema.safeParse({
classId,
@@ -95,11 +99,19 @@ export async function updateTeacherClassAction(
room: formData.get("room"),
})
if (!parsed.success) {
return { success: false, message: "Missing class id" }
return { success: false, message: t("actions.missingClassId") }
}
const { classId: validatedClassId, schoolName, schoolId, name, grade, gradeId, homeroom, room } = parsed.data
// P0-5: 越权校验——教师更新班级前校验归属admin scope 跳过)
if (!hasAdminScope(ctx)) {
const owns = await verifyTeacherOwnsClass(validatedClassId, ctx.userId)
if (!owns) {
return { success: false, message: t("actions.notOwnClass") }
}
}
try {
await updateTeacherClass(validatedClassId, {
schoolName: schoolName ?? undefined,
@@ -113,7 +125,7 @@ export async function updateTeacherClassAction(
revalidatePath("/teacher/classes/my")
revalidatePath("/teacher/classes/students")
revalidatePath("/teacher/classes/schedule")
return { success: true, message: "Class updated successfully" }
return { success: true, message: t("actions.classUpdated") }
} catch (error) {
return handleActionError(error)
}
@@ -124,11 +136,20 @@ export async function updateTeacherClassAction(
export async function deleteTeacherClassAction(classId: string): Promise<ActionState> {
try {
await requirePermission(Permissions.CLASS_DELETE)
const ctx = await requirePermission(Permissions.CLASS_DELETE)
const t = await getTranslations("classes")
const parsed = DeleteTeacherClassSchema.safeParse({ classId })
if (!parsed.success) {
return { success: false, message: "Missing class id" }
return { success: false, message: t("actions.missingClassId") }
}
// P0-5: 越权校验——教师删除班级前校验归属admin scope 跳过)
if (!hasAdminScope(ctx)) {
const owns = await verifyTeacherOwnsClass(parsed.data.classId, ctx.userId)
if (!owns) {
return { success: false, message: t("actions.notOwnClass") }
}
}
try {
@@ -136,7 +157,7 @@ export async function deleteTeacherClassAction(classId: string): Promise<ActionS
revalidatePath("/teacher/classes/my")
revalidatePath("/teacher/classes/students")
revalidatePath("/teacher/classes/schedule")
return { success: true, message: "Class deleted successfully" }
return { success: true, message: t("actions.classDeleted") }
} catch (error) {
return handleActionError(error)
}