- 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
327 lines
9.7 KiB
TypeScript
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 []
|
|
}
|
|
})
|