/** * 组织架构 API(品牌中心人员视图) * * 数据口径: * - 以 HR 花名册(PerformanceSurveyEmployeeProfile)为人员主数据; * - 素材库账号及其活跃绑定只作为可选补充,不作为人员进入组织架构的前置条件; * - 无钉钉 ID 无法绑定的活跃账号单独归入"未绑定/待人工确认"组; * - HR 档案部门路径为"未确定"的单独归入"未确定"组; * - 默认只展示花名册在职(employmentState 非 RESIGNED)。离职档案仍留在库里做托管/作者追溯, * 不进日常组织树;?includeInactive=true 才把离职的人加回名单; * - 一人多部门:按全部任职路径显示,A 同时在 1、2 部门时两边名单都有 A;上级节点仍按人去重。 * * 返回: * - tree:真实组织树(OrganizationUnit,isActive),节点带该部门直接+间接人员数; * - employees:当前可见人员的明细(含 M/P 等级、归属、直属主管、在职状态); * - unbound:未绑定账号;unclassified:部门未确定的人。 */ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { isSystemAdmin, isDepartmentAdmin, isAdmin } from '@/lib/department-permission' import { isRosterEmployed, selectVisibleOrganizationPeople } from '@/lib/auto-personnel-rules' import { countUniquePeopleOnOrganizationPaths, personBelongsToOrganizationPath, resolveEmployeeOrganizationPaths, } from '@/lib/organization-material-permissions' interface OrgNode { id: string name: string path: string level: number parentId: string | null /** 该节点(含子孙)的人数 */ count: number children: OrgNode[] } interface EmployeeRow { employeeProfileId: string userId: string | null username: string | null realName: string | null displayName: string | null jobNumber: string | null departmentPath: string | null departmentPaths: string[] departmentName: string | null positionTitle: string | null positionLevel: string | null levelType: string | null managerName: string | null isLeadership: boolean employmentState: string | null accountIsActive: boolean | null } export async function GET(request: NextRequest) { try { const session = await getServerSession(authOptions) if (!session || !session.user || !isAdmin((session.user as any).role)) { return NextResponse.json({ error: '无权限' }, { status: 403 }) } const url = new URL(request.url) const includeInactive = url.searchParams.get('includeInactive') === 'true' // 作用域:系统管理员看全部;部门管理员(DEPT_ADMIN)只看本部门分支。 // 本部门用 session 里的 managedDepartment(旧部门名)匹配组织树节点路径。 // 匹配不到时必须拒绝,避免部门管理员看到公司全量花名册。 const sessionUser = session.user as any const isSysAdmin = isSystemAdmin(sessionUser.role) let scopePrefix: string | null = null if (!isSysAdmin && isDepartmentAdmin(sessionUser.role)) { const managedName = String(sessionUser.managedDepartment || '').trim() if (!managedName) { return NextResponse.json({ error: '未配置可访问部门范围' }, { status: 403 }) } const scopeUnit = await prisma.organizationUnit.findFirst({ where: { isActive: true, name: managedName }, select: { path: true }, }) if (!scopeUnit) { return NextResponse.json({ error: '未找到可访问部门范围' }, { status: 403 }) } scopePrefix = scopeUnit.path } // 1. 完整 HR 花名册;素材库账号绑定仅作为可选补充。 const profiles = await prisma.performanceSurveyEmployeeProfile.findMany({ select: { id: true, displayName: true, realName: true, jobNumber: true, departmentPath: true, departmentName: true, positionTitle: true, positionLevel: true, levelType: true, managerName: true, isLeadership: true, employmentState: true, sourcePayload: true, departmentMemberships: { select: { sourceDeptId: true, departmentPath: true, departmentName: true, isPrimary: true, }, }, userBindings: { where: { status: 'ACTIVE' }, take: 1, select: { userId: true, user: { select: { username: true, isActive: true, }, }, }, }, }, orderBy: [{ displayName: 'asc' }, { id: 'asc' }], }) // 2. 组织树(全部有效节点) const units = await prisma.organizationUnit.findMany({ where: { isActive: true }, select: { id: true, name: true, path: true, level: true, parentId: true, sourceDeptId: true }, orderBy: [{ level: 'asc' }, { name: 'asc' }], }) const pathBySourceDeptId = new Map(units.map((unit) => [String(unit.sourceDeptId), unit.path])) // 3. 无钉钉 ID 的活跃账号(未绑定组) const unboundUsers = isSysAdmin ? await prisma.user.findMany({ where: { isActive: true, dingtalkUserId: null }, select: { id: true, username: true, realName: true, department: true }, orderBy: { createdAt: 'asc' }, }) : [] // 组装人员行(一人多部门:departmentPaths 含全部任职路径,departmentPath 仍是主部门) const employees: EmployeeRow[] = profiles.map((profile) => { const binding = profile.userBindings[0] ?? null const departmentPaths = resolveEmployeeOrganizationPaths(profile, pathBySourceDeptId) return { employeeProfileId: profile.id, userId: binding?.userId ?? null, username: binding?.user?.username ?? null, realName: profile.realName, displayName: profile.displayName, jobNumber: profile.jobNumber, departmentPath: profile.departmentPath ?? departmentPaths[0] ?? null, departmentPaths, departmentName: profile.departmentName, positionTitle: profile.positionTitle, positionLevel: profile.positionLevel, levelType: profile.levelType, managerName: profile.managerName, isLeadership: Boolean(profile.isLeadership), employmentState: profile.employmentState, accountIsActive: binding?.user?.isActive ?? null, } }) // 统计每个部门路径前缀下的人数(本级+子孙,每人只计一次) // 若为部门管理员,只统计本部门分支 const scopeEmployees = scopePrefix ? employees.filter((e) => personBelongsToOrganizationPath(e.departmentPaths, scopePrefix)) : employees const visibleEmployees = selectVisibleOrganizationPeople(scopeEmployees, includeInactive) const pathCount = countUniquePeopleOnOrganizationPaths(visibleEmployees.map((e) => e.departmentPaths)) // 构建组织树(只保留有人的分支 + 未确定组) const unitMap = new Map() for (const u of units) { unitMap.set(u.id, { id: u.id, name: u.name, path: u.path, level: u.level, parentId: u.parentId, count: pathCount.get(u.path) ?? 0, children: [], }) } const roots: OrgNode[] = [] for (const node of Array.from(unitMap.values())) { if (node.parentId && unitMap.has(node.parentId)) { unitMap.get(node.parentId)!.children.push(node) } else { roots.push(node) } } // pathCount 已按路径前缀累计,节点人数本身含子孙,不必再滚加一层。 // 只保留有人的子树(count > 0),无人的分支剪掉 const prune = (node: OrgNode): OrgNode | null => { const children = node.children.map(prune).filter((c): c is OrgNode => c !== null) if (children.length === 0 && node.count === 0) return null return { ...node, children } } const tree = roots.map(prune).filter((n): n is OrgNode => n !== null) // 未确定组(HR 档案部门路径缺失或以"未确定"开头) const unclassified = visibleEmployees.filter( (e) => e.departmentPaths.length === 0 || e.departmentPaths.every((path) => path.startsWith('未确定')), ) // 未绑定组(无钉钉 ID 的活跃账号) const unbound = unboundUsers.map((u) => ({ userId: u.id, username: u.username, realName: u.realName, oldDepartment: u.department, })) const summary = { totalEmployees: scopeEmployees.length, activeEmployees: scopeEmployees.filter((e) => isRosterEmployed(e.employmentState)).length, inactiveEmployees: scopeEmployees.filter((e) => !isRosterEmployed(e.employmentState)).length, unboundCount: unbound.length, unclassifiedCount: unclassified.length, visibleCount: visibleEmployees.length, scope: scopePrefix ?? 'all', } return NextResponse.json({ tree, employees: visibleEmployees, unclassified, unbound, summary, }) } catch (error) { console.error('Error fetching org data:', error) return NextResponse.json({ error: '获取组织架构数据失败' }, { status: 500 }) } }