2026-03-07 17:32:08 +00:00

1842 lines
53 KiB
TypeScript

"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<void> => {
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<void> => {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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 };
}
};