"use server"; import { revalidatePath } from "next/cache"; import { ClassSchema, ExamSchema, StudentSchema, SubjectSchema, TeacherSchema, LessonSchema, AssignmentSchema, ResultSchema, EventSchema, AnnouncementSchema, TermSchema, HolidaySchema, SchoolTimetableSlotSchema, TimetableTemplateSchema, TimetableEntrySchema, } from "./formValidationSchemas"; import { getSupabaseClient } from "./supabase"; import { clerkClient, auth } from "@clerk/nextjs/server"; type CurrentState = { success: boolean; error: boolean }; export const createSubject = async ( currentState: CurrentState, data: SubjectSchema ) => { try { const supabase = await getSupabaseClient(); const { data: newSubject, error: subjectError } = await supabase .from("Subject") .insert([{ name: data.name, schoolId: data.schoolId }]) .select() .single(); if (subjectError) throw subjectError; if (data.teachers && data.teachers.length > 0) { const teacherConnections = data.teachers.map((tId) => ({ subjectId: newSubject.id as number, teacherId: tId, })); const { error: relError } = await supabase .from("TeacherSubject") .insert(teacherConnections); if (relError) throw relError; } return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateSubject = async ( currentState: CurrentState, data: SubjectSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const { error: subjectError } = await supabase .from("Subject") .update({ name: data.name, schoolId: data.schoolId }) .eq("id", data.id); if (subjectError) throw subjectError; // To mimic prisma `set` behaviour for m2m, first delete existing relations then insert new await supabase.from("TeacherSubject").delete().eq("subjectId", data.id); if (data.teachers && data.teachers.length > 0) { const subjectId = data.id as number; const teacherConnections = data.teachers.map((tId) => ({ subjectId: subjectId, teacherId: tId, })); const { error: relError } = await supabase .from("TeacherSubject") .insert(teacherConnections); if (relError) throw relError; } return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteSubject = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); // Assuming cascading delete is setup in Postgres for m2m, else we need to delete from join table first. const { error } = await supabase.from("Subject").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const createClass = async ( currentState: CurrentState, data: ClassSchema ) => { try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Class").insert([{ name: data.name, capacity: data.capacity, supervisorId: data.supervisorId || null, gradeId: data.gradeId, schoolId: data.schoolId, }]); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateClass = async ( currentState: CurrentState, data: ClassSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const { error } = await supabase .from("Class") .update({ name: data.name, capacity: data.capacity, supervisorId: data.supervisorId || null, gradeId: data.gradeId, schoolId: data.schoolId, }) .eq("id", data.id); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteClass = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Class").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const createTeacher = async ( currentState: CurrentState, data: TeacherSchema ) => { try { const user = await clerkClient.users.createUser({ username: data.username, password: data.password, firstName: data.name, lastName: data.surname, publicMetadata: { role: "teacher" } }); const supabase = await getSupabaseClient(); const { error: teacherError } = await supabase.from("Teacher").insert([{ id: user.id, username: data.username, name: data.name, surname: data.surname, email: data.email || null, phone: data.phone || null, address: data.address, img: data.img || null, bloodType: data.bloodType, sex: data.sex, birthday: data.birthday.toISOString(), }]); if (teacherError) throw teacherError; if (data.subjects && data.subjects.length > 0) { const subjectConnections = data.subjects.map((sId: string) => ({ subjectId: parseInt(sId), teacherId: user.id, })); const { error: relError } = await supabase .from("TeacherSubject") .insert(subjectConnections); if (relError) throw relError; } return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const ensureTeacherOnboarding = async () => { try { const { userId, sessionClaims } = auth(); if (!userId) { return { status: "anonymous" as const }; } const metadata = (sessionClaims?.metadata || {}) as { role?: string; teacherType?: "INDEPENDENT" | "AGENCY" | string; }; if (metadata.role !== "teacher") { return { status: "not-teacher" as const }; } const teacherType = (metadata.teacherType as "INDEPENDENT" | "AGENCY" | undefined) || "INDEPENDENT"; const supabase = await getSupabaseClient(); // Ensure a Teacher row exists for this Clerk user const { data: existingTeacher, error: teacherLookupError } = await supabase .from("Teacher") .select("id") .eq("id", userId) .maybeSingle(); if (teacherLookupError) { throw teacherLookupError; } if (!existingTeacher) { const user = await clerkClient.users.getUser(userId); const primaryEmail = user.emailAddresses[0]?.emailAddress || null; await supabase.from("Teacher").insert({ id: userId, username: user.username || userId, name: user.firstName || "Teacher", surname: user.lastName || "", email: primaryEmail, phone: null, address: "", bloodType: "A+", sex: "MALE", birthday: new Date().toISOString(), }); } // Check if this teacher is already linked to a school const { data: mappings, error: mappingError } = await supabase .from("TeacherSchool") .select("id, schoolId") .eq("teacherId", userId) .limit(1); if (mappingError) { throw mappingError; } if (mappings && mappings.length > 0) { return { status: "existing", schoolId: mappings[0].schoolId as string }; } // Create a dedicated School for this independent/agency teacher const schoolType = teacherType === "AGENCY" ? "AGENCY" : "INDEPENDENT"; const schoolId = `school-${schoolType.toLowerCase()}-${userId}`; const { error: schoolError } = await supabase.from("School").insert({ id: schoolId, name: schoolType === "AGENCY" ? "My Agency" : "My Independent School", type: schoolType, adminId: userId, }); if (schoolError) { throw schoolError; } const { error: mappingInsertError } = await supabase .from("TeacherSchool") .insert({ teacherId: userId, schoolId, isManaged: false, }); if (mappingInsertError) { throw mappingInsertError; } return { status: "created", schoolId, schoolType }; } catch (err) { console.log("ensureTeacherOnboarding error:", err); return { status: "error" as const }; } }; export const setActiveSchool = async (data: FormData): Promise => { try { const { userId, sessionClaims } = auth(); if (!userId) return; const schoolId = data.get("schoolId") as string; if (!schoolId) return; const role = (sessionClaims?.metadata as { role?: string })?.role || "teacher"; const supabase = await getSupabaseClient(); if (role === "admin") { const { data: school, error } = await supabase .from("School") .select("id") .eq("id", schoolId) .eq("adminId", userId) .maybeSingle(); if (error || !school) return; } else if (role === "teacher") { const { data: mapping, error } = await supabase .from("TeacherSchool") .select("id") .eq("teacherId", userId) .eq("schoolId", schoolId) .maybeSingle(); if (error || !mapping) return; } else { return; } const user = await clerkClient.users.getUser(userId); const publicMetadata = user.publicMetadata || {}; await clerkClient.users.updateUser(userId, { publicMetadata: { ...publicMetadata, schoolId, }, }); } catch (err) { console.log("setActiveSchool error:", err); } }; export const linkTeacherToSchool = async (data: FormData): Promise => { try { const { userId, sessionClaims } = auth(); if (!userId) return; const role = (sessionClaims?.metadata as { role?: string })?.role || "teacher"; if (role !== "teacher") return; const schoolId = data.get("schoolId") as string; if (!schoolId) return; const supabase = await getSupabaseClient(); // Ensure mapping exists (idempotent) const { data: existing, error: existingError } = await supabase .from("TeacherSchool") .select("id") .eq("teacherId", userId) .eq("schoolId", schoolId) .maybeSingle(); if (existingError) { console.error("linkTeacherToSchool existing check:", existingError); return; } if (!existing) { const { error: insertError } = await supabase.from("TeacherSchool").insert({ teacherId: userId, schoolId, isManaged: false, }); if (insertError) { console.error("linkTeacherToSchool insert:", insertError); return; } } revalidatePath("/list/my-schools"); } catch (err) { console.error("linkTeacherToSchool error:", err); } }; export const updateTeacher = async ( currentState: CurrentState, data: TeacherSchema ) => { if (!data.id) { return { success: false, error: true }; } try { await clerkClient.users.updateUser(data.id, { username: data.username, ...(data.password !== "" && { password: data.password }), firstName: data.name, lastName: data.surname, }); const supabase = await getSupabaseClient(); const { error: teacherError } = await supabase.from("Teacher").update({ ...(data.password !== "" && { password: data.password }), username: data.username, name: data.name, surname: data.surname, email: data.email || null, phone: data.phone || null, address: data.address, img: data.img || null, bloodType: data.bloodType, sex: data.sex, birthday: data.birthday.toISOString(), }).eq("id", data.id); if (teacherError) throw teacherError; await supabase.from("TeacherSubject").delete().eq("teacherId", data.id); if (data.subjects && data.subjects.length > 0) { const subjectConnections = data.subjects.map((sId: string) => ({ subjectId: parseInt(sId), teacherId: data.id as string, })); const { error: relError } = await supabase .from("TeacherSubject") .insert(subjectConnections); if (relError) throw relError; } return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteTeacher = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { await clerkClient.users.deleteUser(id); const supabase = await getSupabaseClient(); const { error: teacherError } = await supabase.from("Teacher").delete().eq("id", id); if (teacherError) throw teacherError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const createStudent = async ( currentState: CurrentState, data: StudentSchema ) => { try { const supabase = await getSupabaseClient(); for (const classId of data.classIds) { const { data: classRows } = await supabase.from("Class").select("capacity").eq("id", classId).single(); const { count } = await supabase.from("StudentClass").select("studentId", { count: "exact", head: true }).eq("classId", classId); if (classRows && (count ?? 0) >= classRows.capacity) { return { success: false, error: true }; } } const user = await clerkClient.users.createUser({ username: data.username, password: data.password, firstName: data.name, lastName: data.surname, publicMetadata: { role: "student" }, }); const { error: studentError } = await supabase.from("Student").insert([{ id: user.id, username: data.username, name: data.name, surname: data.surname, email: data.email || null, phone: data.phone || null, address: data.address, img: data.img || null, bloodType: data.bloodType, sex: data.sex, birthday: data.birthday.toISOString(), gradeId: data.gradeId, parentId: data.parentId, schoolId: data.schoolId, }]); if (studentError) throw studentError; if (data.classIds.length) { const { error: linkError } = await supabase.from("StudentClass").insert( data.classIds.map((classId) => ({ studentId: user.id, classId })) ); if (linkError) throw linkError; } return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateStudent = async ( currentState: CurrentState, data: StudentSchema ) => { if (!data.id) { return { success: false, error: true }; } try { await clerkClient.users.updateUser(data.id, { username: data.username, ...(data.password !== "" && { password: data.password }), firstName: data.name, lastName: data.surname, }); const supabase = await getSupabaseClient(); for (const classId of data.classIds) { const { data: classRows } = await supabase.from("Class").select("capacity").eq("id", classId).single(); const { count } = await supabase.from("StudentClass").select("studentId", { count: "exact", head: true }).eq("classId", classId); const alreadyIn = await supabase.from("StudentClass").select("studentId").eq("studentId", data.id).eq("classId", classId).maybeSingle(); if (classRows && !alreadyIn.data && (count ?? 0) >= classRows.capacity) { return { success: false, error: true }; } } const { error: studentError } = await supabase.from("Student").update({ ...(data.password !== "" && { password: data.password }), username: data.username, name: data.name, surname: data.surname, email: data.email || null, phone: data.phone || null, address: data.address, img: data.img || null, bloodType: data.bloodType, sex: data.sex, birthday: data.birthday.toISOString(), gradeId: data.gradeId, parentId: data.parentId, schoolId: data.schoolId, }).eq("id", data.id); if (studentError) throw studentError; await supabase.from("StudentClass").delete().eq("studentId", data.id); if (data.classIds.length) { const { error: linkError } = await supabase.from("StudentClass").insert( data.classIds.map((classId) => ({ studentId: data.id!, classId })) ); if (linkError) throw linkError; } return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteStudent = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { await clerkClient.users.deleteUser(id); const supabase = await getSupabaseClient(); const { error: studentError } = await supabase.from("Student").delete().eq("id", id); if (studentError) throw studentError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const createExam = async ( currentState: CurrentState, data: ExamSchema ) => { try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Exam").insert([{ title: data.title, startTime: data.startTime.toISOString(), endTime: data.endTime.toISOString(), lessonId: data.lessonId, schoolId: data.schoolId, }]); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateExam = async ( currentState: CurrentState, data: ExamSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const { error } = await supabase.from("Exam").update({ title: data.title, startTime: data.startTime.toISOString(), endTime: data.endTime.toISOString(), lessonId: data.lessonId, schoolId: data.schoolId, }).eq("id", data.id); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteExam = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Exam").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const createLesson = async ( currentState: CurrentState, data: LessonSchema ) => { try { const supabase = await getSupabaseClient(); const { data: newLesson, error: lessonError } = await supabase .from("Lesson") .insert([ { name: data.name, startTime: data.startTime.toISOString(), endTime: data.endTime.toISOString(), subjectId: data.subjectId, classId: data.classId, teacherId: data.teacherId, schoolId: data.schoolId, }, ]) .select() .single(); if (lessonError) throw lessonError; // Create an associated LessonWhiteboard entry const { error: whiteboardError } = await supabase .from("LessonWhiteboard") .insert([ { lessonId: newLesson.id, }, ]); if (whiteboardError) throw whiteboardError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateLesson = async ( currentState: CurrentState, data: LessonSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const { error: lessonError } = await supabase .from("Lesson") .update({ name: data.name, startTime: data.startTime.toISOString(), endTime: data.endTime.toISOString(), subjectId: data.subjectId, classId: data.classId, teacherId: data.teacherId, schoolId: data.schoolId, }) .eq("id", data.id); if (lessonError) throw lessonError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteLesson = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Lesson").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const createAssignment = async ( currentState: CurrentState, data: AssignmentSchema ) => { try { const supabase = await getSupabaseClient(); const { error: assignmentError } = await supabase .from("Assignment") .insert([ { title: data.title, startDate: data.startDate.toISOString(), dueDate: data.dueDate.toISOString(), lessonId: data.lessonId, schoolId: data.schoolId, }, ]); if (assignmentError) throw assignmentError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateAssignment = async ( currentState: CurrentState, data: AssignmentSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const { error: assignmentError } = await supabase .from("Assignment") .update({ title: data.title, startDate: data.startDate.toISOString(), dueDate: data.dueDate.toISOString(), lessonId: data.lessonId, schoolId: data.schoolId, }) .eq("id", data.id); if (assignmentError) throw assignmentError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteAssignment = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Assignment").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const createResult = async ( currentState: CurrentState, data: ResultSchema ) => { try { const supabase = await getSupabaseClient(); const { error: resultError } = await supabase .from("Result") .insert([ { score: data.score, studentId: data.studentId, ...(data.examId ? { examId: data.examId } : {}), ...(data.assignmentId ? { assignmentId: data.assignmentId } : {}), schoolId: data.schoolId, }, ]); if (resultError) throw resultError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateResult = async ( currentState: CurrentState, data: ResultSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const { error: resultError } = await supabase .from("Result") .update({ score: data.score, studentId: data.studentId, ...(data.examId ? { examId: data.examId } : {}), ...(data.assignmentId ? { assignmentId: data.assignmentId } : {}), schoolId: data.schoolId, }) .eq("id", data.id); if (resultError) throw resultError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteResult = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Result").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const createEvent = async ( currentState: CurrentState, data: EventSchema ) => { try { const supabase = await getSupabaseClient(); const { error: eventError } = await supabase .from("Event") .insert([ { title: data.title, description: data.description, startTime: data.startTime.toISOString(), endTime: data.endTime.toISOString(), ...(data.classId ? { classId: data.classId } : {}), schoolId: data.schoolId, }, ]); if (eventError) throw eventError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateEvent = async ( currentState: CurrentState, data: EventSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const { error: eventError } = await supabase .from("Event") .update({ title: data.title, description: data.description, startTime: data.startTime.toISOString(), endTime: data.endTime.toISOString(), ...(data.classId ? { classId: data.classId } : {}), schoolId: data.schoolId, }) .eq("id", data.id); if (eventError) throw eventError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteEvent = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Event").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const createAnnouncement = async ( currentState: CurrentState, data: AnnouncementSchema ) => { try { const supabase = await getSupabaseClient(); const { error: announcementError } = await supabase .from("Announcement") .insert([ { title: data.title, description: data.description, date: data.date.toISOString(), ...(data.classId ? { classId: data.classId } : {}), schoolId: data.schoolId, }, ]); if (announcementError) throw announcementError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateAnnouncement = async ( currentState: CurrentState, data: AnnouncementSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const { error: announcementError } = await supabase .from("Announcement") .update({ title: data.title, description: data.description, date: data.date.toISOString(), ...(data.classId ? { classId: data.classId } : {}), schoolId: data.schoolId, }) .eq("id", data.id); if (announcementError) throw announcementError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteAnnouncement = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Announcement").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; // --- TERM ACTIONS --- export const createTerm = async ( currentState: CurrentState, data: TermSchema ) => { try { const supabase = await getSupabaseClient(); const academicYearId = data.academicYearId ?? (await supabase .from("AcademicYear") .select("id") .eq("schoolId", data.schoolId) .order("id", { ascending: true }) .limit(1) .single() .then((r) => r.data?.id)); if (!academicYearId) throw new Error("No academic year found for this school."); const { error: termError } = await supabase .from("Term") .insert([ { name: data.name, startDate: data.startDate.toISOString(), endDate: data.endDate.toISOString(), schoolId: data.schoolId, academicYearId, }, ]); if (termError) throw termError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateTerm = async ( currentState: CurrentState, data: TermSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const payload: Record = { name: data.name, startDate: data.startDate.toISOString(), endDate: data.endDate.toISOString(), schoolId: data.schoolId, }; if (data.academicYearId != null) payload.academicYearId = data.academicYearId; const { error: termError } = await supabase .from("Term") .update(payload) .eq("id", data.id); if (termError) throw termError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteTerm = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Term").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; // --- HOLIDAY ACTIONS --- export const createHoliday = async ( currentState: CurrentState, data: HolidaySchema ) => { try { const supabase = await getSupabaseClient(); const academicYearId = data.academicYearId ?? (await supabase .from("AcademicYear") .select("id") .eq("schoolId", data.schoolId) .order("id", { ascending: true }) .limit(1) .single() .then((r) => r.data?.id)); const { error: holidayError } = await supabase .from("Holiday") .insert([ { name: data.name, startDate: data.startDate.toISOString(), endDate: data.endDate.toISOString(), schoolId: data.schoolId, ...(academicYearId != null && { academicYearId }), }, ]); if (holidayError) throw holidayError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateHoliday = async ( currentState: CurrentState, data: HolidaySchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const payload: Record = { name: data.name, startDate: data.startDate.toISOString(), endDate: data.endDate.toISOString(), schoolId: data.schoolId, }; if (data.academicYearId !== undefined) payload.academicYearId = data.academicYearId; const { error: holidayError } = await supabase .from("Holiday") .update(payload) .eq("id", data.id); if (holidayError) throw holidayError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteHoliday = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("Holiday").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; // --- SCHOOL TIMETABLE SLOT ACTIONS --- export const createSchoolTimetableSlot = async ( currentState: CurrentState, data: SchoolTimetableSlotSchema ) => { try { const supabase = await getSupabaseClient(); const schoolTimetableId = data.schoolTimetableId ?? (await supabase .from("SchoolTimetable") .select("id") .eq("schoolId", data.schoolId) .order("id", { ascending: true }) .limit(1) .single() .then((r) => r.data?.id)); if (!schoolTimetableId) throw new Error("No school timetable found for this school."); const { data: maxSlot } = await supabase .from("SchoolTimetableSlot") .select("position") .eq("schoolTimetableId", schoolTimetableId) .order("position", { ascending: false }) .limit(1) .single(); const position = (maxSlot?.position ?? 0) + 1; const { error: slotError } = await supabase .from("SchoolTimetableSlot") .insert([ { name: data.name, startTime: data.startTime, endTime: data.endTime, isTeachingSlot: data.isTeachingSlot, schoolId: data.schoolId, schoolTimetableId, position, }, ]); if (slotError) throw slotError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateSchoolTimetableSlot = async ( currentState: CurrentState, data: SchoolTimetableSlotSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const payload: Record = { name: data.name, startTime: data.startTime, endTime: data.endTime, isTeachingSlot: data.isTeachingSlot, schoolId: data.schoolId, }; if (data.position != null) payload.position = data.position; if (data.schoolTimetableId != null) payload.schoolTimetableId = data.schoolTimetableId; const { error: slotError } = await supabase .from("SchoolTimetableSlot") .update(payload) .eq("id", data.id); if (slotError) throw slotError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteSchoolTimetableSlot = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase.from("SchoolTimetableSlot").delete().eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; // --- TIMETABLE TEMPLATE ACTIONS --- export const createTimetableTemplate = async ( currentState: CurrentState, data: TimetableTemplateSchema ) => { try { const supabase = await getSupabaseClient(); const schoolTimetableId = data.schoolTimetableId ?? (await supabase .from("SchoolTimetable") .select("id") .eq("schoolId", data.schoolId) .order("id", { ascending: true }) .limit(1) .single() .then((r) => r.data?.id)); if (!schoolTimetableId) throw new Error("No school timetable found for this school."); const { error: templateError } = await supabase .from("TeacherTimetableTemplate") .insert([ { name: data.name, teacherId: data.teacherId, schoolId: data.schoolId, schoolTimetableId, }, ]); if (templateError) throw templateError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateTimetableTemplate = async ( currentState: CurrentState, data: TimetableTemplateSchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const payload: Record = { name: data.name, teacherId: data.teacherId, schoolId: data.schoolId, }; if (data.schoolTimetableId != null) payload.schoolTimetableId = data.schoolTimetableId; const { error: templateError } = await supabase .from("TeacherTimetableTemplate") .update(payload) .eq("id", data.id); if (templateError) throw templateError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteTimetableTemplate = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase .from("TeacherTimetableTemplate") .delete() .eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; // --- TIMETABLE ENTRY ACTIONS --- export const createTimetableEntry = async ( currentState: CurrentState, data: TimetableEntrySchema ) => { try { const supabase = await getSupabaseClient(); const templateId = data.teacherTimetableTemplateId ?? data.timetableTemplateId; if (!templateId) throw new Error("Template is required."); const { error: entryError } = await supabase .from("TeacherTimetableEntry") .insert([ { teacherTimetableTemplateId: templateId, schoolTimetableSlotId: data.schoolTimetableSlotId, classId: data.classId, subjectId: data.subjectId, dayOfWeek: data.dayOfWeek, }, ]); if (entryError) throw entryError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const updateTimetableEntry = async ( currentState: CurrentState, data: TimetableEntrySchema ) => { try { if (!data.id) return { success: false, error: true }; const supabase = await getSupabaseClient(); const templateId = data.teacherTimetableTemplateId ?? data.timetableTemplateId; if (!templateId) throw new Error("Template is required."); const { error: entryError } = await supabase .from("TeacherTimetableEntry") .update({ teacherTimetableTemplateId: templateId, schoolTimetableSlotId: data.schoolTimetableSlotId, classId: data.classId, subjectId: data.subjectId, dayOfWeek: data.dayOfWeek, }) .eq("id", data.id); if (entryError) throw entryError; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; export const deleteTimetableEntry = async ( currentState: CurrentState, data: FormData ) => { const id = data.get("id") as string; try { const supabase = await getSupabaseClient(); const { error } = await supabase .from("TeacherTimetableEntry") .delete() .eq("id", parseInt(id)); if (error) throw error; return { success: true, error: false }; } catch (err) { console.log(err); return { success: false, error: true }; } }; // --- TIMETABLE GENERATOR ACTIONS --- export const generateTimetableLessons = async ( currentState: CurrentState, data: FormData ) => { const termId = parseInt(data.get("termId") as string); const schoolId = data.get("schoolId") as string; if (!termId || !schoolId) { return { success: false, error: true, message: "Missing term or school ID." }; } try { const supabase = await getSupabaseClient(); // 1. Fetch the term const { data: term, error: termError } = await supabase .from("Term") .select("*") .eq("id", termId) .eq("schoolId", schoolId) .single(); if (termError || !term) { return { success: false, error: true, message: "Term not found." }; } // 2. Fetch holidays for the school overlapping the term const { data: holidays, error: holidayError } = await supabase .from("Holiday") .select("*") .eq("schoolId", schoolId) .gte("endDate", term.startDate) .lte("startDate", term.endDate); if (holidayError) { return { success: false, error: true, message: "Error fetching holidays." }; } // 3. Fetch all timetable entries for the school const { data: templates } = await supabase .from("TeacherTimetableTemplate") .select("id") .eq("schoolId", schoolId); if (!templates || templates.length === 0) { return { success: true, error: false, message: "No templates found for this school, skipping entry generation." }; } const templateIds = templates.map((t) => t.id); const { data: entries, error: entryError } = await supabase .from("TeacherTimetableEntry") .select("*, slot:SchoolTimetableSlot(*)") .in("teacherTimetableTemplateId", templateIds); if (entryError || !entries) { return { success: false, error: true, message: "Error fetching timetable entries." }; } const termStart = new Date(term.startDate); const termEnd = new Date(term.endDate); const { data: loadedTemplates } = await supabase .from("TeacherTimetableTemplate") .select("*"); const getTeacherId = (templateId: number) => { const t = loadedTemplates?.find((pt) => pt.id === templateId); return t?.teacherId; }; const isHoliday = (date: Date) => { const checkTime = date.getTime(); return holidays && holidays.some((h) => { const hStart = new Date(h.startDate).getTime(); const hEnd = new Date(h.endDate).getTime(); return checkTime >= hStart && checkTime <= hEnd; }); }; const mappedLessons: any[] = []; const currentDate = new Date(termStart); while (currentDate <= termEnd) { // 0 = Sunday, 6 = Saturday in JS const jsDay = currentDate.getDay(); const dbDay = jsDay === 0 ? 7 : jsDay; // Convert to 1-7 for DB logic if (!isHoliday(currentDate)) { const dayEntries = entries .filter((e) => e.dayOfWeek === dbDay) .sort((a, b) => ((a.slot as { position?: number })?.position ?? 0) - ((b.slot as { position?: number })?.position ?? 0)); for (const entry of dayEntries) { if (!entry.slot) continue; const [startHour, startMin] = (entry.slot as any).startTime.split(":").map(Number); const [endHour, endMin] = (entry.slot as any).endTime.split(":").map(Number); const lessonStart = new Date(currentDate); lessonStart.setUTCHours(startHour, startMin, 0, 0); const lessonEnd = new Date(currentDate); lessonEnd.setUTCHours(endHour, endMin, 0, 0); const teacherId = getTeacherId(entry.teacherTimetableTemplateId); if (teacherId) { mappedLessons.push({ name: `${(entry.slot as any).name}`, startTime: lessonStart.toISOString(), endTime: lessonEnd.toISOString(), subjectId: entry.subjectId, classId: entry.classId, teacherId: teacherId, schoolId: schoolId, }); } } } currentDate.setUTCDate(currentDate.getUTCDate() + 1); } if (mappedLessons.length === 0) { return { success: true, error: false, message: "No lessons generated (no valid dates/entries found)." }; } // Avoid duplicates: delete existing lessons in this date range for the same school and teachers we're generating for const teacherIdsToReplace = Array.from(new Set(mappedLessons.map((l) => l.teacherId))); const termStartISO = termStart.toISOString(); const termEndISO = termEnd.toISOString(); for (const tid of teacherIdsToReplace) { const { error: delError } = await supabase .from("Lesson") .delete() .eq("schoolId", schoolId) .eq("teacherId", tid) .gte("startTime", termStartISO) .lte("startTime", termEndISO); if (delError) { console.error("Delete existing lessons error", delError); return { success: false, error: true, message: "Failed clearing existing lessons in range." }; } } // Ensure Lesson id sequence is past max(id) so inserts get unique ids (avoids duplicate key after seed_schedule etc.) const { error: syncErr } = await (supabase as any).rpc("sync_lesson_id_sequence"); if (syncErr) { console.error("generateTimetableLessons sync sequence error:", syncErr); } const CHUNK_SIZE = 1000; for (let i = 0; i < mappedLessons.length; i += CHUNK_SIZE) { const chunk = mappedLessons.slice(i, i + CHUNK_SIZE); const { error: insertError } = await supabase.from("Lesson").insert(chunk); if (insertError) { const errMsg = insertError.message || insertError.code || "Unknown error"; console.error("generateTimetableLessons insert error:", insertError.code, insertError.message, insertError.details); return { success: false, error: true, message: `Failed inserting lessons: ${errMsg}` }; } } return { success: true, error: false, message: `Successfully generated ${mappedLessons.length} lessons!` }; } catch (err) { const message = err instanceof Error ? err.message : "An unexpected error occurred."; console.error("generateTimetableLessons unexpected error:", err); return { success: false, error: true, message }; } }; // --- GENERATE LESSONS FROM A SINGLE TEMPLATE (teacher populates own calendar) --- export const generateLessonsFromTemplate = async ( currentState: CurrentState, data: FormData ) => { const templateId = parseInt(data.get("templateId") as string); const schoolId = data.get("schoolId") as string; const termIdParam = data.get("termId") as string | null; const startDateParam = data.get("startDate") as string | null; const endDateParam = data.get("endDate") as string | null; if (!templateId || !schoolId) { return { success: false, error: true, message: "Missing template or school ID." }; } const useTerm = termIdParam && termIdParam !== ""; const useCustomRange = startDateParam && endDateParam && startDateParam !== "" && endDateParam !== ""; if (!useTerm && !useCustomRange) { return { success: false, error: true, message: "Select a term or enter start and end dates." }; } try { const supabase = await getSupabaseClient(); const { data: template, error: templateError } = await supabase .from("TeacherTimetableTemplate") .select("id, teacherId, schoolId") .eq("id", templateId) .eq("schoolId", schoolId) .single(); if (templateError || !template) { return { success: false, error: true, message: "Template not found or access denied." }; } let rangeStart: Date; let rangeEnd: Date; if (useTerm) { const termId = parseInt(termIdParam!); const { data: term, error: termError } = await supabase .from("Term") .select("startDate, endDate") .eq("id", termId) .eq("schoolId", schoolId) .single(); if (termError || !term) { return { success: false, error: true, message: "Term not found." }; } rangeStart = new Date(term.startDate); rangeEnd = new Date(term.endDate); } else { rangeStart = new Date(startDateParam!); rangeEnd = new Date(endDateParam!); if (Number.isNaN(rangeStart.getTime()) || Number.isNaN(rangeEnd.getTime())) { return { success: false, error: true, message: "Invalid date format." }; } if (rangeStart > rangeEnd) { return { success: false, error: true, message: "Start date must be before end date." }; } } const { data: holidays } = await supabase .from("Holiday") .select("*") .eq("schoolId", schoolId) .gte("endDate", rangeStart.toISOString()) .lte("startDate", rangeEnd.toISOString()); const { data: entries, error: entryError } = await supabase .from("TeacherTimetableEntry") .select("*, slot:SchoolTimetableSlot(*)") .eq("teacherTimetableTemplateId", templateId); if (entryError || !entries || entries.length === 0) { return { success: true, error: false, message: "No timetable entries in this template. Add entries first." }; } const isHoliday = (date: Date) => { const t = date.getTime(); return holidays?.some((h) => { const hStart = new Date(h.startDate).getTime(); const hEnd = new Date(h.endDate).getTime(); return t >= hStart && t <= hEnd; }) ?? false; }; const mappedLessons: Array<{ name: string; startTime: string; endTime: string; subjectId: number; classId: number; teacherId: string; schoolId: string; }> = []; const currentDate = new Date(rangeStart); while (currentDate <= rangeEnd) { const jsDay = currentDate.getDay(); const dbDay = jsDay === 0 ? 7 : jsDay; if (!isHoliday(currentDate)) { const dayEntries = entries .filter((e) => e.dayOfWeek === dbDay) .sort((a, b) => ((a.slot as { position?: number })?.position ?? 0) - ((b.slot as { position?: number })?.position ?? 0)); for (const entry of dayEntries) { if (!entry.slot) continue; const [startHour, startMin] = (entry.slot as { startTime: string }).startTime.split(":").map(Number); const [endHour, endMin] = (entry.slot as { endTime: string }).endTime.split(":").map(Number); const lessonStart = new Date(currentDate); lessonStart.setUTCHours(startHour, startMin, 0, 0); const lessonEnd = new Date(currentDate); lessonEnd.setUTCHours(endHour, endMin, 0, 0); mappedLessons.push({ name: (entry.slot as { name: string }).name, startTime: lessonStart.toISOString(), endTime: lessonEnd.toISOString(), subjectId: entry.subjectId, classId: entry.classId, teacherId: template.teacherId, schoolId: template.schoolId, }); } } currentDate.setUTCDate(currentDate.getUTCDate() + 1); } if (mappedLessons.length === 0) { return { success: true, error: false, message: "No lessons in range (no weekdays or all holidays)." }; } const rangeStartISO = rangeStart.toISOString(); const rangeEndISO = rangeEnd.toISOString(); const { error: delError } = await supabase .from("Lesson") .delete() .eq("schoolId", template.schoolId) .eq("teacherId", template.teacherId) .gte("startTime", rangeStartISO) .lte("startTime", rangeEndISO); if (delError) { const errMsg = delError.message || delError.code || "Unknown error"; console.error("generateLessonsFromTemplate delete error:", delError.code, delError.message, delError.details); return { success: false, error: true, message: `Failed clearing existing lessons: ${errMsg}` }; } // Ensure Lesson id sequence is past max(id) so inserts get unique ids (avoids duplicate key after seed_schedule etc.) const { error: syncErr } = await (supabase as any).rpc("sync_lesson_id_sequence"); if (syncErr) { console.error("generateLessonsFromTemplate sync sequence error:", syncErr); // Continue anyway; insert might still work if sequence is already fine } const CHUNK_SIZE = 1000; for (let i = 0; i < mappedLessons.length; i += CHUNK_SIZE) { const chunk = mappedLessons.slice(i, i + CHUNK_SIZE); const { error: insertError } = await supabase.from("Lesson").insert(chunk); if (insertError) { const errMsg = insertError.message || insertError.code || "Unknown error"; console.error("generateLessonsFromTemplate insert error:", insertError.code, insertError.message, insertError.details); return { success: false, error: true, message: `Failed inserting lessons: ${errMsg}` }; } } return { success: true, error: false, message: `Generated ${mappedLessons.length} lessons. Existing lessons in this range were replaced.` }; } catch (err) { const message = err instanceof Error ? err.message : "An unexpected error occurred."; console.error("generateLessonsFromTemplate unexpected error:", err); return { success: false, error: true, message }; } };