Files
NextEdu/src/modules/course-plans/data-access.ts
SpecialX 4f0ef217a0 refactor(modules): update existing module implementations across attendance, audit, auth, classes, course-plans, exams, files, homework, layout, proctoring, questions, scheduling, textbooks, users
- Update attendance components and data-access for record management

- Update audit log views, filters, and data-access

- Update auth login and register forms

- Update classes actions, components, and data-access (admin, schedule, stats)

- Update course-plans actions, form, list, progress, and schema

- Update exams actions, AI pipeline, preview components, and hooks

- Update files components (icon, list, preview, upload) and data-access

- Update homework assignment form, review view, auto-save hook, and stats-service

- Update layout sidebar, header, and navigation config

- Update proctoring actions, anti-cheat monitor, and data-access

- Update questions actions, components (dialog, actions, columns, filters), and data-access

- Update scheduling actions, auto-scheduler, components, and schema

- Update textbooks constants and text-selection hook

- Update users class-registration, import-dialog, data-access, and user-service
2026-06-23 17:38:56 +08:00

327 lines
9.7 KiB
TypeScript

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<CoursePlanListItem[]> => {
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<CoursePlanWithItems | null> => {
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<string> {
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<UpdateCoursePlanInput>
): Promise<void> {
const update: Partial<typeof coursePlans.$inferSelect> = {}
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<void> {
await db.delete(coursePlans).where(eq(coursePlans.id, id))
}
export async function createCoursePlanItem(
data: CreateCoursePlanItemInput
): Promise<string> {
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<UpdateCoursePlanItemInput>
): Promise<void> {
const update: Partial<typeof coursePlanItems.$inferSelect> = {}
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<void> {
await db.delete(coursePlanItems).where(eq(coursePlanItems.id, id))
}
export async function reorderCoursePlanItems(
planId: string,
items: ReorderCoursePlanItemInput[]
): Promise<void> {
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 []
}
})