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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user