1842 lines
53 KiB
TypeScript
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 };
|
|
}
|
|
};
|