import "server-only" import { cache } from "react" import { and, asc, eq, inArray, or, sql } from "drizzle-orm" import { db } from "@/shared/db" import { academicYears, departments, grades, roles, schools, subjects, users, usersToRoles } from "@/shared/db/schema" import type { AcademicYearInsertData, AcademicYearListItem, AcademicYearUpdateData, DepartmentInsertData, DepartmentListItem, DepartmentUpdateData, GradeInsertData, GradeListItem, GradeUpdateData, SchoolInsertData, SchoolListItem, SchoolUpdateData, StaffOption, } from "./types" const toIso = (d: Date): string => d.toISOString() export const getDepartments = cache(async (): Promise => { try { const rows = await db.select().from(departments).orderBy(asc(departments.name)) return rows.map((r) => ({ id: r.id, name: r.name, description: r.description ?? null, createdAt: toIso(r.createdAt), updatedAt: toIso(r.updatedAt), })) } catch (error) { console.error("getDepartments failed:", error) return [] } }) export const getAcademicYears = cache(async (): Promise => { try { const rows = await db.select().from(academicYears).orderBy(asc(academicYears.startDate)) return rows.map((r) => ({ id: r.id, name: r.name, startDate: toIso(r.startDate), endDate: toIso(r.endDate), isActive: Boolean(r.isActive), createdAt: toIso(r.createdAt), updatedAt: toIso(r.updatedAt), })) } catch (error) { console.error("getAcademicYears failed:", error) return [] } }) export const getSchools = cache(async (): Promise => { try { const rows = await db.select().from(schools).orderBy(asc(schools.name)) return rows.map((r) => ({ id: r.id, name: r.name, code: r.code ?? null, createdAt: toIso(r.createdAt), updatedAt: toIso(r.updatedAt), })) } catch (error) { console.error("getSchools failed:", error) return [] } }) export const getGrades = cache(async (): Promise => { try { const rows = await db .select({ id: grades.id, name: grades.name, order: grades.order, schoolId: schools.id, schoolName: schools.name, gradeHeadId: grades.gradeHeadId, teachingHeadId: grades.teachingHeadId, createdAt: grades.createdAt, updatedAt: grades.updatedAt, }) .from(grades) .innerJoin(schools, eq(schools.id, grades.schoolId)) .orderBy(asc(schools.name), asc(grades.order), asc(grades.name)) const headIds = Array.from( new Set( rows .flatMap((r) => [r.gradeHeadId, r.teachingHeadId]) .filter((v): v is string => typeof v === "string" && v.length > 0) ) ) const heads = headIds.length ? await db .select({ id: users.id, name: users.name, email: users.email }) .from(users) .where(inArray(users.id, headIds)) : [] const headById = new Map() for (const u of heads) { headById.set(u.id, { id: u.id, name: u.name ?? "Unnamed", email: u.email }) } return rows.map((r) => ({ id: r.id, school: { id: r.schoolId, name: r.schoolName }, name: r.name, order: Number(r.order ?? 0), gradeHead: r.gradeHeadId ? headById.get(r.gradeHeadId) ?? null : null, teachingHead: r.teachingHeadId ? headById.get(r.teachingHeadId) ?? null : null, createdAt: toIso(r.createdAt), updatedAt: toIso(r.updatedAt), })) } catch (error) { console.error("getGrades failed:", error) return [] } }) export const getStaffOptions = cache(async (): Promise => { try { const rows = await db .select({ id: users.id, name: users.name, email: users.email }) .from(users) .innerJoin(usersToRoles, eq(usersToRoles.userId, users.id)) .innerJoin(roles, eq(usersToRoles.roleId, roles.id)) .where(inArray(roles.name, ["teacher", "admin"])) .groupBy(users.id, users.name, users.email) .orderBy(asc(users.name), asc(users.email)) return rows.map((r) => ({ id: r.id, name: r.name ?? "Unnamed", email: r.email, })) } catch (error) { console.error("getStaffOptions failed:", error) return [] } }) export const getGradesForStaff = cache(async (staffId: string): Promise => { const id = staffId.trim() if (!id) return [] try { const rows = await db .select({ id: grades.id, name: grades.name, order: grades.order, schoolId: schools.id, schoolName: schools.name, gradeHeadId: grades.gradeHeadId, teachingHeadId: grades.teachingHeadId, createdAt: grades.createdAt, updatedAt: grades.updatedAt, }) .from(grades) .innerJoin(schools, eq(schools.id, grades.schoolId)) .where(or(eq(grades.gradeHeadId, id), eq(grades.teachingHeadId, id))) .orderBy(asc(schools.name), asc(grades.order), asc(grades.name)) const headIds = Array.from( new Set( rows .flatMap((r) => [r.gradeHeadId, r.teachingHeadId]) .filter((v): v is string => typeof v === "string" && v.length > 0) ) ) const heads = headIds.length ? await db .select({ id: users.id, name: users.name, email: users.email }) .from(users) .where(inArray(users.id, headIds)) : [] const headById = new Map() for (const u of heads) headById.set(u.id, { id: u.id, name: u.name ?? "Unnamed", email: u.email }) return rows.map((r) => ({ id: r.id, school: { id: r.schoolId, name: r.schoolName }, name: r.name, order: Number(r.order ?? 0), gradeHead: r.gradeHeadId ? headById.get(r.gradeHeadId) ?? null : null, teachingHead: r.teachingHeadId ? headById.get(r.teachingHeadId) ?? null : null, createdAt: toIso(r.createdAt), updatedAt: toIso(r.updatedAt), })) } catch (error) { console.error("getGradesForStaff failed:", error) return [] } }) // --------------------------------------------------------------------------- // Mutations — DB write operations (called only from actions.ts) // --------------------------------------------------------------------------- export async function createDepartment(data: DepartmentInsertData): Promise { await db.insert(departments).values({ id: data.id, name: data.name, description: data.description, }) } export async function updateDepartment( id: string, data: DepartmentUpdateData ): Promise { await db .update(departments) .set({ name: data.name, description: data.description }) .where(eq(departments.id, id)) } export async function deleteDepartment(id: string): Promise { await db.delete(departments).where(eq(departments.id, id)) } export async function createSchool(data: SchoolInsertData): Promise { await db.insert(schools).values({ id: data.id, name: data.name, code: data.code, }) } export async function updateSchool( id: string, data: SchoolUpdateData ): Promise { await db .update(schools) .set({ name: data.name, code: data.code }) .where(eq(schools.id, id)) } export async function deleteSchool(id: string): Promise { await db.delete(schools).where(eq(schools.id, id)) } export async function createGrade(data: GradeInsertData): Promise { await db.insert(grades).values({ id: data.id, schoolId: data.schoolId, name: data.name, order: data.order, gradeHeadId: data.gradeHeadId, teachingHeadId: data.teachingHeadId, }) } export async function updateGrade( id: string, data: GradeUpdateData ): Promise { await db .update(grades) .set({ schoolId: data.schoolId, name: data.name, order: data.order, gradeHeadId: data.gradeHeadId, teachingHeadId: data.teachingHeadId, }) .where(eq(grades.id, id)) } export async function deleteGrade(id: string): Promise { await db.delete(grades).where(eq(grades.id, id)) } export async function createAcademicYear( data: AcademicYearInsertData ): Promise { await db.transaction(async (tx) => { if (data.isActive) { await tx.update(academicYears).set({ isActive: false }) } await tx.insert(academicYears).values({ id: data.id, name: data.name, startDate: data.startDate, endDate: data.endDate, isActive: data.isActive, }) }) } export async function updateAcademicYear( id: string, data: AcademicYearUpdateData ): Promise { await db.transaction(async (tx) => { if (data.isActive) { await tx.update(academicYears).set({ isActive: false }) } await tx .update(academicYears) .set({ name: data.name, startDate: data.startDate, endDate: data.endDate, isActive: data.isActive, }) .where(eq(academicYears.id, id)) }) } export async function deleteAcademicYear(id: string): Promise { await db.delete(academicYears).where(eq(academicYears.id, id)) } // --------------------------------------------------------------------------- // Cross-module query interfaces — read-only access for other modules // --------------------------------------------------------------------------- export type SubjectOption = { id: string name: string code: string | null order: number } export type GradeOption = { id: string name: string schoolId: string schoolName: string order: number } export const getSubjectOptions = cache(async (): Promise => { try { const rows = await db .select({ id: subjects.id, name: subjects.name, code: subjects.code, order: subjects.order, }) .from(subjects) .orderBy(asc(subjects.order), asc(subjects.name)) return rows.map((r) => ({ id: r.id, name: r.name, code: r.code ?? null, order: Number(r.order ?? 0), })) } catch (error) { console.error("getSubjectOptions failed:", error) return [] } }) export const getGradeOptions = cache(async (): Promise => { try { const rows = await db .select({ id: grades.id, name: grades.name, order: grades.order, schoolId: schools.id, schoolName: schools.name, }) .from(grades) .innerJoin(schools, eq(schools.id, grades.schoolId)) .orderBy(asc(schools.name), asc(grades.order), asc(grades.name)) return rows.map((r) => ({ id: r.id, name: r.name, schoolId: r.schoolId, schoolName: r.schoolName, order: Number(r.order ?? 0), })) } catch (error) { console.error("getGradeOptions failed:", error) return [] } }) /** * 按 ID 获取单个年级名称。 * 供跨模块调用使用,避免为单个年级拉取全量年级选项。 */ export const getGradeNameById = cache( async (gradeId: string): Promise => { const id = gradeId.trim() if (!id) return null const [row] = await db .select({ name: grades.name }) .from(grades) .where(eq(grades.id, id)) .limit(1) return row?.name ?? null }, ) /** * 按 ID 获取单个科目名称。 * 供跨模块调用使用,避免为单个科目拉取全量科目选项或直接查询 subjects 表。 */ export const getSubjectNameById = cache( async (subjectId: string): Promise => { const id = subjectId.trim() if (!id) return null const [row] = await db .select({ name: subjects.name }) .from(subjects) .where(eq(subjects.id, id)) .limit(1) return row?.name ?? null }, ) // --------------------------------------------------------------------------- // Cross-module query interfaces — grade head/teaching head verification // --------------------------------------------------------------------------- /** * 校验用户是否为指定年级的年级主任。 * 供 classes 模块跨模块调用使用,避免直接查询 grades 表。 */ export const isGradeHead = cache(async ( gradeId: string, userId: string ): Promise => { const trimmedGradeId = gradeId.trim() const trimmedUserId = userId.trim() if (!trimmedGradeId || !trimmedUserId) return false const [row] = await db .select({ id: grades.id }) .from(grades) .where(and(eq(grades.id, trimmedGradeId), eq(grades.gradeHeadId, trimmedUserId))) .limit(1) return Boolean(row) }) /** * 校验用户是否为指定年级的年级主任或教学主任。 * 供 classes 模块跨模块调用使用,避免直接查询 grades 表。 */ export const isGradeManager = cache(async ( gradeId: string, userId: string ): Promise => { const trimmedGradeId = gradeId.trim() const trimmedUserId = userId.trim() if (!trimmedGradeId || !trimmedUserId) return false const [row] = await db .select({ id: grades.id }) .from(grades) .where( and( eq(grades.id, trimmedGradeId), or(eq(grades.gradeHeadId, trimmedUserId), eq(grades.teachingHeadId, trimmedUserId)) ) ) .limit(1) return Boolean(row) }) /** * 根据年级名称(大小写不敏感)查找用户担任年级主任的年级 ID。 * 供 classes 模块跨模块调用使用,避免直接查询 grades 表。 */ export const findGradeIdByHeadAndName = cache(async ( userId: string, gradeName: string ): Promise => { const trimmedUserId = userId.trim() const normalizedGradeName = gradeName.trim().toLowerCase() if (!trimmedUserId || !normalizedGradeName) return null const [row] = await db .select({ id: grades.id }) .from(grades) .where( and( eq(grades.gradeHeadId, trimmedUserId), sql`LOWER(${grades.name}) = ${normalizedGradeName}` ) ) .limit(1) return row?.id ?? null })