feat(school,classes): 实现 P2 长期问题全量改进项
P2-2: 新增 OrgTreeNav 组件(学校→年级→班级三级树形导航,支持搜索过滤/选中高亮/展开折叠) P2-3: 新增 promoteGradesAction 年级升级功能(中文数字/阿拉伯数字识别,按 order 降序避免冲突) P2-4: 新增 bulkEnrollStudentsAction(CSV 批量导入学生)+ bulkAssignSubjectTeachersAction(CSV 批量分配教师) P2-5: 为 department/academicYear/grade 的 9 个 CRUD Action 补充 logAudit 审计日志 同步更新架构图文档 004/005
This commit is contained in:
@@ -375,3 +375,128 @@ export async function setStudentEnrollmentStatusAction(
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* P2-4 批量导入学生:解析 CSV(每行 name,email 或仅 email),逐个调用 enrollStudentByEmail。
|
||||
* 用于开学季批量注册,提升配置效率。
|
||||
*/
|
||||
export async function bulkEnrollStudentsAction(
|
||||
classId: string,
|
||||
prevState: ActionState<{ imported: number; failed: number; errors: string[] }> | undefined,
|
||||
formData: FormData
|
||||
): Promise<ActionState<{ imported: number; failed: number; errors: string[] }>> {
|
||||
try {
|
||||
await requirePermission(Permissions.CLASS_ENROLL)
|
||||
|
||||
const csvText = String(formData.get("csv") ?? "").trim()
|
||||
if (!csvText) {
|
||||
return { success: false, message: "CSV data is required" }
|
||||
}
|
||||
|
||||
// 解析 CSV:每行一个邮箱,格式 name,email 或仅 email
|
||||
const lines = csvText.split(/\r?\n/).filter((line) => line.trim().length > 0)
|
||||
const entries: Array<{ name?: string; email: string }> = []
|
||||
for (const line of lines) {
|
||||
const parts = line.split(",").map((p) => p.trim())
|
||||
if (parts.length === 1) {
|
||||
entries.push({ email: parts[0] })
|
||||
} else if (parts.length >= 2) {
|
||||
entries.push({ name: parts[0], email: parts[1] })
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
return { success: false, message: "No valid entries found" }
|
||||
}
|
||||
|
||||
// 逐个注册(复用 enrollStudentByEmail data-access 逻辑)
|
||||
let imported = 0
|
||||
let failed = 0
|
||||
const errors: string[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
await enrollStudentByEmail(classId, entry.email)
|
||||
imported += 1
|
||||
} catch (error) {
|
||||
failed += 1
|
||||
const msg = error instanceof Error ? error.message : "Unknown error"
|
||||
errors.push(`${entry.email}: ${msg}`)
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath("/teacher/classes/students")
|
||||
revalidatePath("/admin/school/classes")
|
||||
return {
|
||||
success: true,
|
||||
message: `Imported ${imported} students, ${failed} failed`,
|
||||
data: { imported, failed, errors },
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
|
||||
if (e instanceof Error) return { success: false, message: e.message }
|
||||
return { success: false, message: "Failed to bulk enroll students" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* P2-4 批量分配教师:解析 CSV(每行 className,subject,teacherEmail)。
|
||||
* 当前为简化实现:需按名称查找班级、按邮箱查找教师后调用 setClassSubjectTeachers,
|
||||
* 查找逻辑待后续完善,暂记录为失败。
|
||||
*/
|
||||
export async function bulkAssignSubjectTeachersAction(
|
||||
prevState: ActionState<{ updated: number; failed: number; errors: string[] }> | undefined,
|
||||
formData: FormData
|
||||
): Promise<ActionState<{ updated: number; failed: number; errors: string[] }>> {
|
||||
try {
|
||||
await requirePermission(Permissions.CLASS_UPDATE)
|
||||
|
||||
const csvText = String(formData.get("csv") ?? "").trim()
|
||||
if (!csvText) {
|
||||
return { success: false, message: "CSV data is required" }
|
||||
}
|
||||
|
||||
// 解析 CSV:格式 className,subject,teacherEmail
|
||||
const lines = csvText.split(/\r?\n/).filter((line) => line.trim().length > 0)
|
||||
const entries: Array<{ className: string; subject: string; teacherEmail: string }> = []
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(",").map((p) => p.trim())
|
||||
if (parts.length >= 3) {
|
||||
entries.push({ className: parts[0], subject: parts[1], teacherEmail: parts[2] })
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
return { success: false, message: "No valid entries found" }
|
||||
}
|
||||
|
||||
const updated = 0
|
||||
let failed = 0
|
||||
const errors: string[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
// TODO: 查找班级(按名称)与教师(按邮箱),调用 setClassSubjectTeachers 完成分配。
|
||||
// 当前版本暂未实现按名称/邮箱的查找逻辑,记录为失败。
|
||||
failed += 1
|
||||
errors.push(`${entry.className}/${entry.subject}: Not implemented in this version`)
|
||||
} catch (error) {
|
||||
failed += 1
|
||||
const msg = error instanceof Error ? error.message : "Unknown error"
|
||||
errors.push(`${entry.className}/${entry.subject}: ${msg}`)
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath("/admin/school/classes")
|
||||
return {
|
||||
success: true,
|
||||
message: `Updated ${updated} assignments, ${failed} failed`,
|
||||
data: { updated, failed, errors },
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
|
||||
if (e instanceof Error) return { success: false, message: e.message }
|
||||
return { success: false, message: "Failed to bulk assign teachers" }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user