refactor(school,classes): 完成 school/grade/class 审计全量改进项
P0-1/P0-2: 删除 grade-management 死模块,年级 CRUD 统一由 school 模块负责 P0-3: classes/actions.ts 从 974 行拆分为 6 个职责文件 + barrel re-export P0-5: 13 个页面 i18n 全量接入(grades/departments/academic-year/classes/insights) P1-1: 角色硬编码改为 hasAdminScope/hasTeacherScope/hasStudentScope 基于 dataScope.type P1-3: 新增 SchoolErrorBoundary + SchoolListSkeleton/SchoolCardSkeleton,4 个页面包裹 Error Boundary P1-4: classes/types.ts 跨领域类型添加归属决策注释 P1-5: schools-view.tsx 拆分为组合模式(SchoolFormDialog + SchoolDeleteDialog + SchoolListToolbar) P1-6: 新增 getSchoolsForUser/getGradesForUser 权限感知查询函数 P2-1: 抽取 useSchoolData hook,对话框状态管理与 UI 分离 同步更新架构图文档 004/005
This commit is contained in:
@@ -205,6 +205,172 @@ export const getGradesForStaff = cache(async (staffId: string): Promise<GradeLis
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 根据用户角色返回可见的学校列表(权限感知)。
|
||||
* - admin: 返回全量学校
|
||||
* - grade_head / teaching_head: 返回其负责年级所在学校
|
||||
* - teacher: 返回其任课班级所在学校
|
||||
* - 其他角色: 返回空数组
|
||||
*/
|
||||
export const getSchoolsForUser = cache(async (userId: string): Promise<SchoolListItem[]> => {
|
||||
const id = userId.trim()
|
||||
if (!id) return []
|
||||
|
||||
try {
|
||||
const roleRows = await db
|
||||
.select({ name: roles.name })
|
||||
.from(roles)
|
||||
.innerJoin(usersToRoles, eq(usersToRoles.roleId, roles.id))
|
||||
.where(eq(usersToRoles.userId, id))
|
||||
|
||||
const roleNames = new Set(roleRows.map((r) => r.name))
|
||||
|
||||
if (roleNames.has("admin")) {
|
||||
return await getSchools()
|
||||
}
|
||||
|
||||
let schoolIds: string[] = []
|
||||
|
||||
if (roleNames.has("grade_head") || roleNames.has("teaching_head")) {
|
||||
const gradeRows = await db
|
||||
.select({ schoolId: grades.schoolId })
|
||||
.from(grades)
|
||||
.where(or(eq(grades.gradeHeadId, id), eq(grades.teachingHeadId, id)))
|
||||
schoolIds = gradeRows.map((r) => r.schoolId)
|
||||
} else if (roleNames.has("teacher")) {
|
||||
const { getAccessibleClassIdsForTeacher, getGradeIdsByClassIds } = await import("@/modules/classes/data-access")
|
||||
const classIds = await getAccessibleClassIdsForTeacher(id)
|
||||
if (classIds.length === 0) return []
|
||||
|
||||
const gradeIds = await getGradeIdsByClassIds(classIds)
|
||||
if (gradeIds.length === 0) return []
|
||||
|
||||
const gradeRows = await db
|
||||
.select({ schoolId: grades.schoolId })
|
||||
.from(grades)
|
||||
.where(inArray(grades.id, gradeIds))
|
||||
schoolIds = gradeRows.map((r) => r.schoolId)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
const uniqueSchoolIds = Array.from(
|
||||
new Set(schoolIds.filter((v): v is string => typeof v === "string" && v.length > 0))
|
||||
)
|
||||
if (uniqueSchoolIds.length === 0) return []
|
||||
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(schools)
|
||||
.where(inArray(schools.id, uniqueSchoolIds))
|
||||
.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("getSchoolsForUser failed:", error)
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 根据用户角色返回可见的年级列表(权限感知)。
|
||||
* - admin: 返回全量年级
|
||||
* - grade_head / teaching_head: 返回其负责的年级
|
||||
* - teacher: 返回其任课班级所在年级
|
||||
* - 其他角色: 返回空数组
|
||||
*/
|
||||
export const getGradesForUser = cache(async (userId: string): Promise<GradeListItem[]> => {
|
||||
const id = userId.trim()
|
||||
if (!id) return []
|
||||
|
||||
try {
|
||||
const roleRows = await db
|
||||
.select({ name: roles.name })
|
||||
.from(roles)
|
||||
.innerJoin(usersToRoles, eq(usersToRoles.roleId, roles.id))
|
||||
.where(eq(usersToRoles.userId, id))
|
||||
|
||||
const roleNames = new Set(roleRows.map((r) => r.name))
|
||||
|
||||
if (roleNames.has("admin")) {
|
||||
return await getGrades()
|
||||
}
|
||||
|
||||
if (roleNames.has("grade_head") || roleNames.has("teaching_head")) {
|
||||
return await getGradesForStaff(id)
|
||||
}
|
||||
|
||||
if (roleNames.has("teacher")) {
|
||||
const { getAccessibleClassIdsForTeacher, getGradeIdsByClassIds } = await import("@/modules/classes/data-access")
|
||||
const classIds = await getAccessibleClassIdsForTeacher(id)
|
||||
if (classIds.length === 0) return []
|
||||
|
||||
const gradeIds = await getGradeIdsByClassIds(classIds)
|
||||
if (gradeIds.length === 0) return []
|
||||
|
||||
const uniqueGradeIds = Array.from(new Set(gradeIds))
|
||||
if (uniqueGradeIds.length === 0) return []
|
||||
|
||||
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(inArray(grades.id, uniqueGradeIds))
|
||||
.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<string, StaffOption>()
|
||||
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),
|
||||
}))
|
||||
}
|
||||
|
||||
return []
|
||||
} catch (error) {
|
||||
console.error("getGradesForUser failed:", error)
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutations — DB write operations (called only from actions.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user