fix: 权限组部门树改走花名册组织口径
跟数据口径同一套在职花名册和一人多部门,不再只按有素材库账号的人裁树。
This commit is contained in:
parent
0fa583ee8f
commit
887d911b76
@ -6,11 +6,13 @@ import { prisma } from '@/lib/prisma'
|
||||
import { getAllMaterialTypes, isSuperAdmin } from '@/lib/department-permission'
|
||||
import { filterSelectableMaterialTypes } from '@/lib/material-type-options'
|
||||
import { BINDING_STATUS } from '@/lib/employee-directory'
|
||||
import { selectVisibleOrganizationPeople } from '@/lib/auto-personnel-rules'
|
||||
import {
|
||||
collectOccupiedOrganizationPaths,
|
||||
lastOrganizationPathSegment,
|
||||
normalizeOrganizationPath,
|
||||
pruneOrganizationTree,
|
||||
resolveEmployeeOrganizationPaths,
|
||||
} from '@/lib/organization-material-permissions'
|
||||
import {
|
||||
findConflictingDepartmentGroup,
|
||||
@ -60,7 +62,7 @@ export async function GET() {
|
||||
await ensureDefaultPermissionPacks()
|
||||
await seedDefaultGroupDepartments()
|
||||
|
||||
const [packs, units, bindings, materialTypes, departmentRows] = await Promise.all([
|
||||
const [packs, units, rosterProfiles, materialTypes, departmentRows] = await Promise.all([
|
||||
prisma.materialPermissionPack.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
select: {
|
||||
@ -101,12 +103,18 @@ export async function GET() {
|
||||
}),
|
||||
prisma.organizationUnit.findMany({
|
||||
where: { isActive: true },
|
||||
select: { id: true, name: true, path: true, level: true, parentId: true },
|
||||
select: { id: true, name: true, path: true, level: true, parentId: true, sourceDeptId: true },
|
||||
orderBy: [{ level: 'asc' }, { name: 'asc' }],
|
||||
}),
|
||||
prisma.userEmployeeBinding.findMany({
|
||||
where: { status: BINDING_STATUS.ACTIVE, user: { isActive: true } },
|
||||
select: { employeeProfile: { select: { departmentPath: true } } },
|
||||
prisma.performanceSurveyEmployeeProfile.findMany({
|
||||
select: {
|
||||
departmentPath: true,
|
||||
employmentState: true,
|
||||
sourcePayload: true,
|
||||
departmentMemberships: {
|
||||
select: { sourceDeptId: true, departmentPath: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
getAllMaterialTypes().then(filterSelectableMaterialTypes),
|
||||
prisma.materialPermissionPackDepartment
|
||||
@ -124,7 +132,14 @@ export async function GET() {
|
||||
|
||||
const nodeMap = new Map<string, OrgNode>()
|
||||
for (const unit of units) {
|
||||
nodeMap.set(unit.id, { ...unit, children: [] })
|
||||
nodeMap.set(unit.id, {
|
||||
id: unit.id,
|
||||
name: unit.name,
|
||||
path: unit.path,
|
||||
level: unit.level,
|
||||
parentId: unit.parentId,
|
||||
children: [],
|
||||
})
|
||||
}
|
||||
const roots: OrgNode[] = []
|
||||
for (const node of Array.from(nodeMap.values())) {
|
||||
@ -134,9 +149,13 @@ export async function GET() {
|
||||
roots.push(node)
|
||||
}
|
||||
}
|
||||
const occupiedPaths = collectOccupiedOrganizationPaths(
|
||||
bindings.map((binding) => binding.employeeProfile?.departmentPath),
|
||||
)
|
||||
const pathBySourceDeptId = new Map(units.map((unit) => [String(unit.sourceDeptId), unit.path]))
|
||||
const occupiedPaths = collectOccupiedOrganizationPaths([
|
||||
...selectVisibleOrganizationPeople(rosterProfiles).flatMap((profile) =>
|
||||
resolveEmployeeOrganizationPaths(profile, pathBySourceDeptId),
|
||||
),
|
||||
...departmentRows.map((row) => row.departmentPath),
|
||||
])
|
||||
const tree = pruneOrganizationTree(roots, occupiedPaths)
|
||||
const departmentsByPack = new Map<string, string[]>()
|
||||
for (const row of departmentRows) {
|
||||
|
||||
@ -23,10 +23,9 @@ import { prisma } from '@/lib/prisma'
|
||||
import { isSystemAdmin, isDepartmentAdmin, isAdmin } from '@/lib/department-permission'
|
||||
import { isRosterEmployed, selectVisibleOrganizationPeople } from '@/lib/auto-personnel-rules'
|
||||
import {
|
||||
collectEmployeeOrganizationPaths,
|
||||
countUniquePeopleOnOrganizationPaths,
|
||||
parseOrganizationDepartmentIds,
|
||||
personBelongsToOrganizationPath,
|
||||
resolveEmployeeOrganizationPaths,
|
||||
} from '@/lib/organization-material-permissions'
|
||||
|
||||
interface OrgNode {
|
||||
@ -139,28 +138,6 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
const pathBySourceDeptId = new Map(units.map((unit) => [String(unit.sourceDeptId), unit.path]))
|
||||
|
||||
const resolveEmployeePaths = (profile: {
|
||||
departmentPath: string | null
|
||||
sourcePayload: unknown
|
||||
departmentMemberships: Array<{ sourceDeptId: string; departmentPath: string | null }>
|
||||
} | null | undefined): string[] => {
|
||||
if (!profile) return []
|
||||
const synced = ((profile.sourcePayload as { syncedFields?: Record<string, unknown> } | null)?.syncedFields ||
|
||||
{}) as Record<string, unknown>
|
||||
const extraIds = [
|
||||
...parseOrganizationDepartmentIds(synced.departmentIds),
|
||||
...parseOrganizationDepartmentIds(synced.employeeDeptIdList),
|
||||
...parseOrganizationDepartmentIds(synced.registerDeptIds),
|
||||
...parseOrganizationDepartmentIds(synced.flowerBookDeptId),
|
||||
]
|
||||
return collectEmployeeOrganizationPaths([
|
||||
profile.departmentPath,
|
||||
...profile.departmentMemberships.map((membership) => membership.departmentPath),
|
||||
...profile.departmentMemberships.map((membership) => pathBySourceDeptId.get(String(membership.sourceDeptId))),
|
||||
...extraIds.map((id) => pathBySourceDeptId.get(id)),
|
||||
])
|
||||
}
|
||||
|
||||
// 3. 无钉钉 ID 的活跃账号(未绑定组)
|
||||
const unboundUsers = isSysAdmin
|
||||
? await prisma.user.findMany({
|
||||
@ -173,7 +150,7 @@ export async function GET(request: NextRequest) {
|
||||
// 组装人员行(一人多部门:departmentPaths 含全部任职路径,departmentPath 仍是主部门)
|
||||
const employees: EmployeeRow[] = profiles.map((profile) => {
|
||||
const binding = profile.userBindings[0] ?? null
|
||||
const departmentPaths = resolveEmployeePaths(profile)
|
||||
const departmentPaths = resolveEmployeeOrganizationPaths(profile, pathBySourceDeptId)
|
||||
return {
|
||||
employeeProfileId: profile.id,
|
||||
userId: binding?.userId ?? null,
|
||||
|
||||
@ -361,7 +361,7 @@ export default function PermissionGroupsDashboard() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-[-0.03em] text-[#101828]">权限组</h1>
|
||||
<p className="mt-1 max-w-3xl text-sm leading-6 text-[#66758b]">
|
||||
一组就是一份权限。一个部门只能进一组,树上会标明已在哪组。人可以进多组,能看的取并集。
|
||||
一组就是一份权限。部门树跟数据口径同一套花名册组织。一个部门只能进一组,树上会标明已在哪组。人可以进多组,能看的取并集。
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
parseOrganizationDepartmentIds,
|
||||
personBelongsToOrganizationPath,
|
||||
pruneOrganizationTree,
|
||||
resolveEmployeeOrganizationPaths,
|
||||
resolveOrganizationPermissionKey,
|
||||
unionPermissionPacks,
|
||||
} from '@/lib/organization-material-permissions'
|
||||
@ -110,6 +111,28 @@ describe('organization material permission keys', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('employee org paths follow roster primary, extra posts and department ids', () => {
|
||||
const company = '江苏简尚品牌管理有限公司'
|
||||
const brand = `${company}/品牌中心`
|
||||
const ops = `${company}/运营中心G`
|
||||
const lookup = new Map([
|
||||
['2', brand],
|
||||
['9', ops],
|
||||
])
|
||||
|
||||
expect(
|
||||
resolveEmployeeOrganizationPaths(
|
||||
{
|
||||
departmentPath: brand,
|
||||
departmentMemberships: [{ sourceDeptId: '9', departmentPath: ops }],
|
||||
sourcePayload: { syncedFields: { departmentIds: '[2,9]' } },
|
||||
},
|
||||
lookup,
|
||||
),
|
||||
).toEqual([brand, ops])
|
||||
expect(resolveEmployeeOrganizationPaths(null, lookup)).toEqual([])
|
||||
})
|
||||
|
||||
test('permission tree drops revoked empty branches but keeps occupied ancestors', () => {
|
||||
const occupied = collectOccupiedOrganizationPaths([
|
||||
'江苏简尚品牌管理有限公司/运营中心A/事业一部/品类十九部',
|
||||
|
||||
@ -211,6 +211,42 @@ export function collectEmployeeOrganizationPaths(
|
||||
return collapseNestedOrganizationPaths(unique)
|
||||
}
|
||||
|
||||
export type EmployeeOrganizationPathSource = {
|
||||
departmentPath?: string | null
|
||||
sourcePayload?: unknown
|
||||
departmentMemberships?: Array<{
|
||||
sourceDeptId?: string | number | null
|
||||
departmentPath?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
/** 和数据口径同一套任职路径:主部门 + 兼任 + 花名册部门 ID。 */
|
||||
export function resolveEmployeeOrganizationPaths(
|
||||
profile: EmployeeOrganizationPathSource | null | undefined,
|
||||
pathBySourceDeptId: Map<string, string> | Record<string, string>,
|
||||
): string[] {
|
||||
if (!profile) return []
|
||||
const lookup =
|
||||
pathBySourceDeptId instanceof Map
|
||||
? pathBySourceDeptId
|
||||
: new Map(Object.entries(pathBySourceDeptId))
|
||||
const synced = ((profile.sourcePayload as { syncedFields?: Record<string, unknown> } | null)?.syncedFields ||
|
||||
{}) as Record<string, unknown>
|
||||
const extraIds = [
|
||||
...parseOrganizationDepartmentIds(synced.departmentIds),
|
||||
...parseOrganizationDepartmentIds(synced.employeeDeptIdList),
|
||||
...parseOrganizationDepartmentIds(synced.registerDeptIds),
|
||||
...parseOrganizationDepartmentIds(synced.flowerBookDeptId),
|
||||
]
|
||||
const memberships = profile.departmentMemberships || []
|
||||
return collectEmployeeOrganizationPaths([
|
||||
profile.departmentPath,
|
||||
...memberships.map((membership) => membership.departmentPath),
|
||||
...memberships.map((membership) => lookup.get(String(membership.sourceDeptId))),
|
||||
...extraIds.map((id) => lookup.get(id)),
|
||||
])
|
||||
}
|
||||
|
||||
/** 花名册常同时给上级和本组。只留最深的任职,避免「品牌创意A部 + 品牌中心」被当成两个职位。 */
|
||||
export function collapseNestedOrganizationPaths(paths: string[]): string[] {
|
||||
return paths.filter(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user