import "server-only" import { cache } from "react" import { createId } from "@paralleldrive/cuid2" import { and, asc, desc, eq, inArray, type SQL } from "drizzle-orm" import { db } from "@/shared/db" import { classes, coursePlanItems, coursePlans, subjects, users, } from "@/shared/db/schema" import { safeParseDate } from "@/shared/lib/action-utils" import type { CoursePlan, CoursePlanItem, CoursePlanListItem, CoursePlanStatus, CoursePlanWithItems, GetCoursePlansParams, ReorderCoursePlanItemInput, } from "./types" import type { CreateCoursePlanInput, CreateCoursePlanItemInput, UpdateCoursePlanInput, UpdateCoursePlanItemInput, } from "./schema" const toIso = (d: Date | null | undefined): string | null => d ? d.toISOString() : null const toIsoRequired = (d: Date): string => d.toISOString() const mapPlanRow = ( row: { id: string classId: string subjectId: string teacherId: string academicYearId: string | null semester: "1" | "2" totalHours: number completedHours: number weeklyHours: number startDate: Date | null endDate: Date | null syllabus: string | null objectives: string | null status: CoursePlanStatus createdBy: string createdAt: Date updatedAt: Date className: string | null subjectName: string | null teacherName: string | null } ): CoursePlanListItem => ({ id: row.id, classId: row.classId, subjectId: row.subjectId, teacherId: row.teacherId, academicYearId: row.academicYearId, semester: row.semester, totalHours: Number(row.totalHours), completedHours: Number(row.completedHours), weeklyHours: Number(row.weeklyHours), startDate: toIso(row.startDate), endDate: toIso(row.endDate), syllabus: row.syllabus, objectives: row.objectives, status: row.status, createdBy: row.createdBy, createdAt: toIsoRequired(row.createdAt), updatedAt: toIsoRequired(row.updatedAt), className: row.className, subjectName: row.subjectName, teacherName: row.teacherName, }) const mapItemRow = ( row: { id: string planId: string week: number topic: string content: string | null hours: number textbookChapter: string | null notes: string | null isCompleted: boolean completedAt: Date | null createdAt: Date updatedAt: Date } ): CoursePlanItem => ({ id: row.id, planId: row.planId, week: Number(row.week), topic: row.topic, content: row.content, hours: Number(row.hours), textbookChapter: row.textbookChapter, notes: row.notes, isCompleted: Boolean(row.isCompleted), completedAt: toIso(row.completedAt), createdAt: toIsoRequired(row.createdAt), updatedAt: toIsoRequired(row.updatedAt), }) const buildPlanSelect = () => db .select({ id: coursePlans.id, classId: coursePlans.classId, subjectId: coursePlans.subjectId, teacherId: coursePlans.teacherId, academicYearId: coursePlans.academicYearId, semester: coursePlans.semester, totalHours: coursePlans.totalHours, completedHours: coursePlans.completedHours, weeklyHours: coursePlans.weeklyHours, startDate: coursePlans.startDate, endDate: coursePlans.endDate, syllabus: coursePlans.syllabus, objectives: coursePlans.objectives, status: coursePlans.status, createdBy: coursePlans.createdBy, createdAt: coursePlans.createdAt, updatedAt: coursePlans.updatedAt, className: classes.name, subjectName: subjects.name, teacherName: users.name, }) .from(coursePlans) .leftJoin(classes, eq(classes.id, coursePlans.classId)) .leftJoin(subjects, eq(subjects.id, coursePlans.subjectId)) .leftJoin(users, eq(users.id, coursePlans.teacherId)) export const getCoursePlans = cache( async (params?: GetCoursePlansParams): Promise => { try { const conditions: SQL[] = [] if (params?.classId) conditions.push(eq(coursePlans.classId, params.classId)) if (params?.teacherId) conditions.push(eq(coursePlans.teacherId, params.teacherId)) if (params?.subjectId) conditions.push(eq(coursePlans.subjectId, params.subjectId)) if (params?.status) conditions.push(eq(coursePlans.status, params.status)) const query = buildPlanSelect() const rows = await (conditions.length > 0 ? query.where(and(...conditions)) : query ).orderBy(desc(coursePlans.createdAt)) return rows.map(mapPlanRow) } catch (error) { console.error("getCoursePlans failed:", error) return [] } } ) export const getCoursePlanById = cache( async (id: string): Promise => { try { const [planRow] = await buildPlanSelect() .where(eq(coursePlans.id, id)) .limit(1) if (!planRow) return null const itemRows = await db .select() .from(coursePlanItems) .where(eq(coursePlanItems.planId, id)) .orderBy(asc(coursePlanItems.week), asc(coursePlanItems.createdAt)) return { ...mapPlanRow(planRow), items: itemRows.map(mapItemRow), } } catch (error) { console.error("getCoursePlanById failed:", error) return null } } ) export async function createCoursePlan( data: CreateCoursePlanInput, createdBy: string ): Promise { const id = createId() await db.insert(coursePlans).values({ id, classId: data.classId, subjectId: data.subjectId, teacherId: data.teacherId, academicYearId: data.academicYearId, semester: data.semester, totalHours: data.totalHours, completedHours: 0, weeklyHours: data.weeklyHours, startDate: data.startDate ? safeParseDate(data.startDate, "开始日期") : null, endDate: data.endDate ? safeParseDate(data.endDate, "结束日期") : null, syllabus: data.syllabus, objectives: data.objectives, status: data.status, createdBy, }) return id } export async function updateCoursePlan( id: string, data: Partial ): Promise { const update: Partial = {} if (data.classId !== undefined) update.classId = data.classId if (data.subjectId !== undefined) update.subjectId = data.subjectId if (data.teacherId !== undefined) update.teacherId = data.teacherId if (data.academicYearId !== undefined) update.academicYearId = data.academicYearId if (data.semester !== undefined) update.semester = data.semester if (data.totalHours !== undefined) update.totalHours = data.totalHours if (data.completedHours !== undefined) update.completedHours = data.completedHours if (data.weeklyHours !== undefined) update.weeklyHours = data.weeklyHours if (data.startDate !== undefined) update.startDate = data.startDate ? safeParseDate(data.startDate, "开始日期") : null if (data.endDate !== undefined) update.endDate = data.endDate ? safeParseDate(data.endDate, "结束日期") : null if (data.syllabus !== undefined) update.syllabus = data.syllabus if (data.objectives !== undefined) update.objectives = data.objectives if (data.status !== undefined) update.status = data.status if (Object.keys(update).length === 0) return await db.update(coursePlans).set(update).where(eq(coursePlans.id, id)) } export async function deleteCoursePlan(id: string): Promise { await db.delete(coursePlans).where(eq(coursePlans.id, id)) } export async function createCoursePlanItem( data: CreateCoursePlanItemInput ): Promise { const id = createId() await db.insert(coursePlanItems).values({ id, planId: data.planId, week: data.week, topic: data.topic, content: data.content, hours: data.hours, textbookChapter: data.textbookChapter, notes: data.notes, }) return id } export async function updateCoursePlanItem( id: string, data: Partial ): Promise { const update: Partial = {} if (data.week !== undefined) update.week = data.week if (data.topic !== undefined) update.topic = data.topic if (data.content !== undefined) update.content = data.content if (data.hours !== undefined) update.hours = data.hours if (data.textbookChapter !== undefined) update.textbookChapter = data.textbookChapter if (data.notes !== undefined) update.notes = data.notes if (data.isCompleted !== undefined) update.isCompleted = data.isCompleted if (data.completedAt !== undefined) update.completedAt = data.completedAt ? safeParseDate(data.completedAt, "完成日期") : null if (Object.keys(update).length === 0) return await db.update(coursePlanItems).set(update).where(eq(coursePlanItems.id, id)) } export async function deleteCoursePlanItem(id: string): Promise { await db.delete(coursePlanItems).where(eq(coursePlanItems.id, id)) } export async function reorderCoursePlanItems( planId: string, items: ReorderCoursePlanItemInput[] ): Promise { if (items.length === 0) return const itemIds = items.map((i) => i.id) const [existing] = await db .select({ id: coursePlanItems.id }) .from(coursePlanItems) .where(and(eq(coursePlanItems.planId, planId), inArray(coursePlanItems.id, itemIds))) .limit(1) if (!existing) return await Promise.all( items.map((item) => db .update(coursePlanItems) .set({ week: item.week }) .where(eq(coursePlanItems.id, item.id)) ) ) } export type { CoursePlan, CoursePlanItem, CoursePlanWithItems } export const getSubjectOptions = cache(async (): Promise<{ id: string; name: string }[]> => { try { const rows = await db .select({ id: subjects.id, name: subjects.name }) .from(subjects) .orderBy(asc(subjects.order), asc(subjects.name)) return rows.map((r) => ({ id: r.id, name: r.name })) } catch (error) { console.error("getSubjectOptions failed:", error) return [] } })