"use server" import { revalidatePath } from "next/cache" import { eq, or } from "drizzle-orm" import { createId } from "@paralleldrive/cuid2" import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import type { ActionState } from "@/shared/types/action-state" import { db } from "@/shared/db" import { classSchedule, users } from "@/shared/db/schema" import { getSchedulingRules, upsertSchedulingRules, getScheduleChanges, createScheduleChange, updateScheduleChangeStatus, getClassConflicts, getAdminClassesForScheduling, getTeachersForScheduling, getClassroomsForScheduling, getClassSubjectsForScheduling, } from "./data-access" import { autoSchedule, buildDefaultTimeSlots } from "./auto-scheduler" import { SchedulingRuleSchema, ScheduleChangeSchema, AutoScheduleParamsSchema, } from "./schema" import type { AutoScheduleParams, AutoScheduleResult, ScheduleChangeListItem, ScheduleConflict, ScheduleChangeQueryParams, } from "./types" export async function saveSchedulingRulesAction( prevState: ActionState | null, formData: FormData ): Promise> { try { await requirePermission(Permissions.SCHEDULE_ADJUST) const parsed = SchedulingRuleSchema.safeParse({ classId: formData.get("classId"), maxDailyHours: formData.get("maxDailyHours") || undefined, maxContinuousHours: formData.get("maxContinuousHours") || undefined, lunchBreakStart: formData.get("lunchBreakStart") || undefined, lunchBreakEnd: formData.get("lunchBreakEnd") || undefined, morningStart: formData.get("morningStart") || undefined, afternoonEnd: formData.get("afternoonEnd") || undefined, avoidBackToBack: formData.get("avoidBackToBack") === "true", balancedSubjects: formData.get("balancedSubjects") === "true", }) if (!parsed.success) { return { success: false, message: "Invalid form data", errors: parsed.error.flatten().fieldErrors, } } const id = await upsertSchedulingRules(parsed.data) revalidatePath("/admin/scheduling/rules") return { success: true, message: "Scheduling rules saved", data: id } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function autoScheduleAction( prevState: ActionState | null, formData: FormData ): Promise> { try { await requirePermission(Permissions.SCHEDULE_AUTO) const classId = String(formData.get("classId") ?? "").trim() if (!classId) return { success: false, message: "Class is required" } // Load rules for class (or global fallback) const rulesRows = await getSchedulingRules(classId) const rules = rulesRows.find((r) => r.classId === classId) ?? rulesRows.find((r) => r.classId === null) if (!rules) return { success: false, message: "No scheduling rules found. Please configure rules first." } // Load subjects + teacher assignments for this class const subjectRows = await getClassSubjectsForScheduling(classId) if (subjectRows.length === 0) { return { success: false, message: "No subjects assigned to this class" } } // Default weekly hours: 5 per subject (configurable in future) const subjectsInput = subjectRows.map((r) => ({ subjectId: r.subjectId, subjectName: r.subjectName, weeklyHours: 5, teacherId: r.teacherId ?? null, })) // Load teachers const teacherIds = Array.from( new Set(subjectRows.map((r) => r.teacherId).filter((v): v is string => v !== null)) ) const teacherRows = teacherIds.length > 0 ? await db .select({ id: users.id, name: users.name }) .from(users) .where(teacherIds.length === 1 ? eq(users.id, teacherIds[0]!) : or(...teacherIds.map((id) => eq(users.id, id)))) : [] const teachersInput = teacherRows.map((t) => ({ id: t.id, name: t.name ?? "Unknown" })) // Load classrooms const classroomRows = await getClassroomsForScheduling() const classroomsInput = classroomRows.map((c) => ({ id: c.id, name: c.name })) // Build default time slots: Mon-Fri, 4 morning + 4 afternoon sessions const timeSlots = buildDefaultTimeSlots(rules.morningStart, rules.afternoonEnd, rules.lunchBreakStart, rules.lunchBreakEnd) const params: AutoScheduleParams = { classId, rules, subjects: subjectsInput, teachers: teachersInput, classrooms: classroomsInput, timeSlots, } const parsed = AutoScheduleParamsSchema.safeParse(params) if (!parsed.success) { return { success: false, message: "Invalid scheduling parameters", errors: parsed.error.flatten().fieldErrors } } const result = autoSchedule(params) return { success: true, message: `Scheduled ${result.scheduledCount} sessions, ${result.conflictCount} conflicts`, data: result } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function applyAutoScheduleAction( classId: string, schedules: Array<{ weekday: number startTime: string endTime: string course: string location: string | null }> ): Promise> { try { await requirePermission(Permissions.SCHEDULE_AUTO) if (!classId) return { success: false, message: "Class is required" } if (!Array.isArray(schedules) || schedules.length === 0) { return { success: false, message: "No schedules to apply" } } // Replace existing schedule for the class await db.transaction(async (tx) => { await tx.delete(classSchedule).where(eq(classSchedule.classId, classId)) const rows = schedules.map((s) => ({ id: createId(), classId, weekday: s.weekday, startTime: s.startTime, endTime: s.endTime, course: s.course, location: s.location ?? null, })) await tx.insert(classSchedule).values(rows) }) revalidatePath("/admin/scheduling/auto") revalidatePath("/teacher/classes/schedule") return { success: true, message: `Applied ${schedules.length} schedule items`, data: schedules.length } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function requestScheduleChangeAction( prevState: ActionState | null, formData: FormData ): Promise> { try { const ctx = await requirePermission(Permissions.SCHEDULE_ADJUST) const parsed = ScheduleChangeSchema.safeParse({ classId: formData.get("classId"), originalScheduleId: formData.get("originalScheduleId") || undefined, originalTeacherId: formData.get("originalTeacherId") || undefined, substituteTeacherId: formData.get("substituteTeacherId") || undefined, originalDate: formData.get("originalDate") || undefined, newDate: formData.get("newDate") || undefined, newStartTime: formData.get("newStartTime") || undefined, newEndTime: formData.get("newEndTime") || undefined, reason: formData.get("reason"), }) if (!parsed.success) { return { success: false, message: "Invalid form data", errors: parsed.error.flatten().fieldErrors, } } const id = await createScheduleChange(parsed.data, ctx.userId) revalidatePath("/teacher/schedule-changes") revalidatePath("/admin/scheduling/changes") return { success: true, message: "Schedule change request submitted", data: id } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function approveScheduleChangeAction(changeId: string): Promise { try { const ctx = await requirePermission(Permissions.SCHEDULE_AUTO) if (!changeId) return { success: false, message: "Missing change id" } await updateScheduleChangeStatus(changeId, "approved", ctx.userId) revalidatePath("/admin/scheduling/changes") revalidatePath("/teacher/schedule-changes") return { success: true, message: "Schedule change approved" } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function rejectScheduleChangeAction( changeId: string, reason?: string ): Promise { try { const ctx = await requirePermission(Permissions.SCHEDULE_AUTO) if (!changeId) return { success: false, message: "Missing change id" } await updateScheduleChangeStatus(changeId, "rejected", ctx.userId) revalidatePath("/admin/scheduling/changes") revalidatePath("/teacher/schedule-changes") return { success: true, message: reason ? `Rejected: ${reason}` : "Schedule change rejected" } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function getScheduleChangesAction( params: ScheduleChangeQueryParams ): Promise> { try { await requirePermission(Permissions.SCHEDULE_ADJUST) const items = await getScheduleChanges(params) return { success: true, data: items } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } export async function getClassConflictsAction( classId: string ): Promise> { try { await requirePermission(Permissions.SCHEDULE_ADJUST) if (!classId) return { success: false, message: "Class is required" } const conflicts = await getClassConflicts(classId) return { success: true, data: conflicts } } catch (e) { if (e instanceof PermissionDeniedError) return { success: false, message: e.message } if (e instanceof Error) return { success: false, message: e.message } return { success: false, message: "Unexpected error" } } } // Re-export data-access for server pages export { getSchedulingRules, getScheduleChanges, getClassConflicts, getAdminClassesForScheduling, getTeachersForScheduling, getClassroomsForScheduling, }