463 lines
17 KiB
TypeScript
463 lines
17 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache"
|
|
import { and, eq, sql } from "drizzle-orm"
|
|
import { auth } from "@/auth"
|
|
|
|
import { db } from "@/shared/db"
|
|
import { grades } from "@/shared/db/schema"
|
|
import type { ActionState } from "@/shared/types/action-state"
|
|
import {
|
|
createAdminClass,
|
|
createClassScheduleItem,
|
|
createTeacherClass,
|
|
deleteAdminClass,
|
|
deleteClassScheduleItem,
|
|
deleteTeacherClass,
|
|
enrollStudentByEmail,
|
|
enrollStudentByInvitationCode,
|
|
ensureClassInvitationCode,
|
|
regenerateClassInvitationCode,
|
|
setClassSubjectTeachers,
|
|
setStudentEnrollmentStatus,
|
|
updateAdminClass,
|
|
updateClassScheduleItem,
|
|
updateTeacherClass,
|
|
} from "./data-access"
|
|
import { DEFAULT_CLASS_SUBJECTS, type ClassSubject } from "./types"
|
|
|
|
const isClassSubject = (v: string): v is ClassSubject => DEFAULT_CLASS_SUBJECTS.includes(v as ClassSubject)
|
|
|
|
export async function createTeacherClassAction(
|
|
prevState: ActionState<string> | null,
|
|
formData: FormData
|
|
): Promise<ActionState<string>> {
|
|
const schoolName = formData.get("schoolName")
|
|
const schoolId = formData.get("schoolId")
|
|
const name = formData.get("name")
|
|
const grade = formData.get("grade")
|
|
const gradeId = formData.get("gradeId")
|
|
const homeroom = formData.get("homeroom")
|
|
const room = formData.get("room")
|
|
|
|
if (typeof name !== "string" || name.trim().length === 0) {
|
|
return { success: false, message: "Class name is required" }
|
|
}
|
|
if (typeof grade !== "string" || grade.trim().length === 0) {
|
|
return { success: false, message: "Grade is required" }
|
|
}
|
|
|
|
const session = await auth()
|
|
if (!session?.user) return { success: false, message: "Unauthorized" }
|
|
|
|
const role = String(session.user.role ?? "")
|
|
if (role !== "admin") {
|
|
const userId = String(session.user.id ?? "").trim()
|
|
if (!userId) return { success: false, message: "Unauthorized" }
|
|
|
|
const normalizedGradeId = typeof gradeId === "string" ? gradeId.trim() : ""
|
|
const normalizedGradeName = grade.trim().toLowerCase()
|
|
const where = normalizedGradeId
|
|
? and(eq(grades.id, normalizedGradeId), eq(grades.gradeHeadId, userId))
|
|
: and(eq(grades.gradeHeadId, userId), sql`LOWER(${grades.name}) = ${normalizedGradeName}`)
|
|
|
|
const [ownedGrade] = await db.select({ id: grades.id }).from(grades).where(where).limit(1)
|
|
if (!ownedGrade) {
|
|
return { success: false, message: "Only admins and grade heads can create classes" }
|
|
}
|
|
}
|
|
|
|
try {
|
|
const id = await createTeacherClass({
|
|
schoolName: typeof schoolName === "string" ? schoolName : null,
|
|
schoolId: typeof schoolId === "string" ? schoolId : null,
|
|
name,
|
|
grade,
|
|
gradeId: typeof gradeId === "string" ? gradeId : null,
|
|
homeroom: typeof homeroom === "string" ? homeroom : null,
|
|
room: typeof room === "string" ? room : null,
|
|
})
|
|
revalidatePath("/teacher/classes/my")
|
|
revalidatePath("/teacher/classes/students")
|
|
revalidatePath("/teacher/classes/schedule")
|
|
return { success: true, message: "Class created successfully", data: id }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to create class" }
|
|
}
|
|
}
|
|
|
|
export async function updateTeacherClassAction(
|
|
classId: string,
|
|
prevState: ActionState | null,
|
|
formData: FormData
|
|
): Promise<ActionState> {
|
|
const schoolName = formData.get("schoolName")
|
|
const schoolId = formData.get("schoolId")
|
|
const name = formData.get("name")
|
|
const grade = formData.get("grade")
|
|
const gradeId = formData.get("gradeId")
|
|
const homeroom = formData.get("homeroom")
|
|
const room = formData.get("room")
|
|
|
|
if (typeof classId !== "string" || classId.trim().length === 0) {
|
|
return { success: false, message: "Missing class id" }
|
|
}
|
|
|
|
try {
|
|
await updateTeacherClass(classId, {
|
|
schoolName: typeof schoolName === "string" ? schoolName : undefined,
|
|
schoolId: typeof schoolId === "string" ? schoolId : undefined,
|
|
name: typeof name === "string" ? name : undefined,
|
|
grade: typeof grade === "string" ? grade : undefined,
|
|
gradeId: typeof gradeId === "string" ? gradeId : undefined,
|
|
homeroom: typeof homeroom === "string" ? homeroom : undefined,
|
|
room: typeof room === "string" ? room : undefined,
|
|
})
|
|
revalidatePath("/teacher/classes/my")
|
|
revalidatePath("/teacher/classes/students")
|
|
revalidatePath("/teacher/classes/schedule")
|
|
return { success: true, message: "Class updated successfully" }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to update class" }
|
|
}
|
|
}
|
|
|
|
export async function deleteTeacherClassAction(classId: string): Promise<ActionState> {
|
|
if (typeof classId !== "string" || classId.trim().length === 0) {
|
|
return { success: false, message: "Missing class id" }
|
|
}
|
|
|
|
try {
|
|
await deleteTeacherClass(classId)
|
|
revalidatePath("/teacher/classes/my")
|
|
revalidatePath("/teacher/classes/students")
|
|
revalidatePath("/teacher/classes/schedule")
|
|
return { success: true, message: "Class deleted successfully" }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to delete class" }
|
|
}
|
|
}
|
|
|
|
export async function enrollStudentByEmailAction(
|
|
classId: string,
|
|
prevState: ActionState | null,
|
|
formData: FormData
|
|
): Promise<ActionState> {
|
|
const email = formData.get("email")
|
|
if (typeof classId !== "string" || classId.trim().length === 0) {
|
|
return { success: false, message: "Please select a class" }
|
|
}
|
|
if (typeof email !== "string" || email.trim().length === 0) {
|
|
return { success: false, message: "Student email is required" }
|
|
}
|
|
|
|
try {
|
|
await enrollStudentByEmail(classId, email)
|
|
revalidatePath("/teacher/classes/students")
|
|
revalidatePath("/teacher/classes/my")
|
|
return { success: true, message: "Student added successfully" }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to add student" }
|
|
}
|
|
}
|
|
|
|
export async function joinClassByInvitationCodeAction(
|
|
prevState: ActionState<{ classId: string }> | null,
|
|
formData: FormData
|
|
): Promise<ActionState<{ classId: string }>> {
|
|
const code = formData.get("code")
|
|
if (typeof code !== "string" || code.trim().length === 0) {
|
|
return { success: false, message: "Invitation code is required" }
|
|
}
|
|
|
|
const session = await auth()
|
|
if (!session?.user?.id || String(session.user.role ?? "") !== "student") {
|
|
return { success: false, message: "Unauthorized" }
|
|
}
|
|
|
|
try {
|
|
const classId = await enrollStudentByInvitationCode(session.user.id, code)
|
|
revalidatePath("/student/learning/courses")
|
|
revalidatePath("/student/schedule")
|
|
revalidatePath("/profile")
|
|
return { success: true, message: "Joined class successfully", data: { classId } }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to join class" }
|
|
}
|
|
}
|
|
|
|
export async function ensureClassInvitationCodeAction(classId: string): Promise<ActionState<{ code: string }>> {
|
|
if (typeof classId !== "string" || classId.trim().length === 0) {
|
|
return { success: false, message: "Missing class id" }
|
|
}
|
|
|
|
try {
|
|
const code = await ensureClassInvitationCode(classId)
|
|
revalidatePath("/teacher/classes/my")
|
|
revalidatePath(`/teacher/classes/my/${encodeURIComponent(classId)}`)
|
|
return { success: true, message: "Invitation code ready", data: { code } }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to generate code" }
|
|
}
|
|
}
|
|
|
|
export async function regenerateClassInvitationCodeAction(classId: string): Promise<ActionState<{ code: string }>> {
|
|
if (typeof classId !== "string" || classId.trim().length === 0) {
|
|
return { success: false, message: "Missing class id" }
|
|
}
|
|
|
|
try {
|
|
const code = await regenerateClassInvitationCode(classId)
|
|
revalidatePath("/teacher/classes/my")
|
|
revalidatePath(`/teacher/classes/my/${encodeURIComponent(classId)}`)
|
|
return { success: true, message: "Invitation code updated", data: { code } }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to regenerate code" }
|
|
}
|
|
}
|
|
|
|
export async function setStudentEnrollmentStatusAction(
|
|
classId: string,
|
|
studentId: string,
|
|
status: "active" | "inactive"
|
|
): Promise<ActionState> {
|
|
if (!classId?.trim() || !studentId?.trim()) {
|
|
return { success: false, message: "Missing enrollment info" }
|
|
}
|
|
|
|
try {
|
|
await setStudentEnrollmentStatus(classId, studentId, status)
|
|
revalidatePath("/teacher/classes/students")
|
|
revalidatePath("/teacher/classes/my")
|
|
return { success: true, message: "Student updated successfully" }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to update student" }
|
|
}
|
|
}
|
|
|
|
export async function createClassScheduleItemAction(
|
|
prevState: ActionState<string> | null,
|
|
formData: FormData
|
|
): Promise<ActionState<string>> {
|
|
const classId = formData.get("classId")
|
|
const weekday = formData.get("weekday")
|
|
const startTime = formData.get("startTime")
|
|
const endTime = formData.get("endTime")
|
|
const course = formData.get("course")
|
|
const location = formData.get("location")
|
|
|
|
if (typeof classId !== "string" || classId.trim().length === 0) {
|
|
return { success: false, message: "Please select a class" }
|
|
}
|
|
if (typeof weekday !== "string" || weekday.trim().length === 0) {
|
|
return { success: false, message: "Weekday is required" }
|
|
}
|
|
const weekdayNum = Number(weekday)
|
|
if (!Number.isInteger(weekdayNum) || weekdayNum < 1 || weekdayNum > 7) {
|
|
return { success: false, message: "Invalid weekday" }
|
|
}
|
|
if (typeof course !== "string" || course.trim().length === 0) {
|
|
return { success: false, message: "Course is required" }
|
|
}
|
|
if (typeof startTime !== "string" || typeof endTime !== "string") {
|
|
return { success: false, message: "Time is required" }
|
|
}
|
|
|
|
try {
|
|
const id = await createClassScheduleItem({
|
|
classId,
|
|
weekday: weekdayNum as 1 | 2 | 3 | 4 | 5 | 6 | 7,
|
|
startTime,
|
|
endTime,
|
|
course,
|
|
location: typeof location === "string" ? location : null,
|
|
})
|
|
revalidatePath("/teacher/classes/schedule")
|
|
return { success: true, message: "Schedule item created successfully", data: id }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to create schedule item" }
|
|
}
|
|
}
|
|
|
|
export async function updateClassScheduleItemAction(
|
|
scheduleId: string,
|
|
prevState: ActionState | null,
|
|
formData: FormData
|
|
): Promise<ActionState> {
|
|
const classId = formData.get("classId")
|
|
const weekday = formData.get("weekday")
|
|
const startTime = formData.get("startTime")
|
|
const endTime = formData.get("endTime")
|
|
const course = formData.get("course")
|
|
const location = formData.get("location")
|
|
|
|
if (typeof scheduleId !== "string" || scheduleId.trim().length === 0) {
|
|
return { success: false, message: "Missing schedule id" }
|
|
}
|
|
|
|
const weekdayNum = typeof weekday === "string" && weekday.trim().length > 0 ? Number(weekday) : undefined
|
|
if (weekdayNum !== undefined && (!Number.isInteger(weekdayNum) || weekdayNum < 1 || weekdayNum > 7)) {
|
|
return { success: false, message: "Invalid weekday" }
|
|
}
|
|
|
|
try {
|
|
await updateClassScheduleItem(scheduleId, {
|
|
classId: typeof classId === "string" ? classId : undefined,
|
|
weekday: weekdayNum as 1 | 2 | 3 | 4 | 5 | 6 | 7 | undefined,
|
|
startTime: typeof startTime === "string" ? startTime : undefined,
|
|
endTime: typeof endTime === "string" ? endTime : undefined,
|
|
course: typeof course === "string" ? course : undefined,
|
|
location: typeof location === "string" ? location : undefined,
|
|
})
|
|
revalidatePath("/teacher/classes/schedule")
|
|
return { success: true, message: "Schedule item updated successfully" }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to update schedule item" }
|
|
}
|
|
}
|
|
|
|
export async function deleteClassScheduleItemAction(scheduleId: string): Promise<ActionState> {
|
|
if (typeof scheduleId !== "string" || scheduleId.trim().length === 0) {
|
|
return { success: false, message: "Missing schedule id" }
|
|
}
|
|
|
|
try {
|
|
await deleteClassScheduleItem(scheduleId)
|
|
revalidatePath("/teacher/classes/schedule")
|
|
return { success: true, message: "Schedule item deleted successfully" }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to delete schedule item" }
|
|
}
|
|
}
|
|
|
|
export async function createAdminClassAction(
|
|
prevState: ActionState<string> | undefined,
|
|
formData: FormData
|
|
): Promise<ActionState<string>> {
|
|
const session = await auth()
|
|
if (!session?.user?.id || String(session.user.role ?? "") !== "admin") {
|
|
return { success: false, message: "Unauthorized" }
|
|
}
|
|
|
|
const schoolName = formData.get("schoolName")
|
|
const schoolId = formData.get("schoolId")
|
|
const name = formData.get("name")
|
|
const grade = formData.get("grade")
|
|
const gradeId = formData.get("gradeId")
|
|
const teacherId = formData.get("teacherId")
|
|
const homeroom = formData.get("homeroom")
|
|
const room = formData.get("room")
|
|
|
|
if (typeof name !== "string" || name.trim().length === 0) {
|
|
return { success: false, message: "Class name is required" }
|
|
}
|
|
if (typeof grade !== "string" || grade.trim().length === 0) {
|
|
return { success: false, message: "Grade is required" }
|
|
}
|
|
if (typeof teacherId !== "string" || teacherId.trim().length === 0) {
|
|
return { success: false, message: "Teacher is required" }
|
|
}
|
|
|
|
try {
|
|
const id = await createAdminClass({
|
|
schoolName: typeof schoolName === "string" ? schoolName : null,
|
|
schoolId: typeof schoolId === "string" ? schoolId : null,
|
|
name,
|
|
grade,
|
|
gradeId: typeof gradeId === "string" ? gradeId : null,
|
|
teacherId,
|
|
homeroom: typeof homeroom === "string" ? homeroom : null,
|
|
room: typeof room === "string" ? room : null,
|
|
})
|
|
revalidatePath("/admin/school/classes")
|
|
revalidatePath("/teacher/classes/my")
|
|
revalidatePath("/teacher/classes/students")
|
|
revalidatePath("/teacher/classes/schedule")
|
|
return { success: true, message: "Class created successfully", data: id }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to create class" }
|
|
}
|
|
}
|
|
|
|
export async function updateAdminClassAction(
|
|
classId: string,
|
|
prevState: ActionState | undefined,
|
|
formData: FormData
|
|
): Promise<ActionState> {
|
|
const schoolName = formData.get("schoolName")
|
|
const schoolId = formData.get("schoolId")
|
|
const name = formData.get("name")
|
|
const grade = formData.get("grade")
|
|
const gradeId = formData.get("gradeId")
|
|
const teacherId = formData.get("teacherId")
|
|
const homeroom = formData.get("homeroom")
|
|
const room = formData.get("room")
|
|
const subjectTeachers = formData.get("subjectTeachers")
|
|
|
|
if (typeof classId !== "string" || classId.trim().length === 0) {
|
|
return { success: false, message: "Missing class id" }
|
|
}
|
|
|
|
try {
|
|
await updateAdminClass(classId, {
|
|
schoolName: typeof schoolName === "string" ? schoolName : undefined,
|
|
schoolId: typeof schoolId === "string" ? schoolId : undefined,
|
|
name: typeof name === "string" ? name : undefined,
|
|
grade: typeof grade === "string" ? grade : undefined,
|
|
gradeId: typeof gradeId === "string" ? gradeId : undefined,
|
|
teacherId: typeof teacherId === "string" ? teacherId : undefined,
|
|
homeroom: typeof homeroom === "string" ? homeroom : undefined,
|
|
room: typeof room === "string" ? room : undefined,
|
|
})
|
|
|
|
if (typeof subjectTeachers === "string" && subjectTeachers.trim().length > 0) {
|
|
const parsed = JSON.parse(subjectTeachers) as unknown
|
|
if (!Array.isArray(parsed)) throw new Error("Invalid subject teachers")
|
|
|
|
await setClassSubjectTeachers({
|
|
classId,
|
|
assignments: parsed.flatMap((item) => {
|
|
if (!item || typeof item !== "object") return []
|
|
const subject = (item as { subject?: unknown }).subject
|
|
const teacherId = (item as { teacherId?: unknown }).teacherId
|
|
|
|
if (typeof subject !== "string" || !isClassSubject(subject)) return []
|
|
|
|
if (teacherId === null || typeof teacherId === "undefined") {
|
|
return [{ subject, teacherId: null }]
|
|
}
|
|
|
|
if (typeof teacherId !== "string") return []
|
|
const trimmed = teacherId.trim()
|
|
return [{ subject, teacherId: trimmed.length > 0 ? trimmed : null }]
|
|
}),
|
|
})
|
|
}
|
|
|
|
revalidatePath("/admin/school/classes")
|
|
revalidatePath("/teacher/classes/my")
|
|
revalidatePath("/teacher/classes/students")
|
|
revalidatePath("/teacher/classes/schedule")
|
|
return { success: true, message: "Class updated successfully" }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to update class" }
|
|
}
|
|
}
|
|
|
|
export async function deleteAdminClassAction(classId: string): Promise<ActionState> {
|
|
if (typeof classId !== "string" || classId.trim().length === 0) {
|
|
return { success: false, message: "Missing class id" }
|
|
}
|
|
|
|
try {
|
|
await deleteAdminClass(classId)
|
|
revalidatePath("/admin/school/classes")
|
|
revalidatePath("/teacher/classes/my")
|
|
revalidatePath("/teacher/classes/students")
|
|
revalidatePath("/teacher/classes/schedule")
|
|
return { success: true, message: "Class deleted successfully" }
|
|
} catch (error) {
|
|
return { success: false, message: error instanceof Error ? error.message : "Failed to delete class" }
|
|
}
|
|
}
|