130 lines
5.7 KiB
TypeScript
130 lines
5.7 KiB
TypeScript
import type { Prisma } from '@prisma/client'
|
||
import { prisma } from '@/lib/prisma'
|
||
|
||
export class EmployeeManagerResolutionError extends Error {}
|
||
|
||
export interface EmployeeManagerSubject {
|
||
id?: string | null
|
||
employeeProfileId?: string | null
|
||
displayName?: string | null
|
||
dingtalkUserId?: string | null
|
||
jobNumber?: string | null
|
||
managerDingtalkUserId?: string | null
|
||
managerJobNumber?: string | null
|
||
}
|
||
|
||
const clean = (value: unknown) => String(value ?? '').trim()
|
||
|
||
/** 共享主管候选解析:唯一标识优先;完全缺失时沿主组织向上查找最近层级的唯一在册 M 级人员。 */
|
||
export async function resolveEmployeeDirectManagerProfile(
|
||
subject: EmployeeManagerSubject,
|
||
options: { activeWhere?: Prisma.PerformanceSurveyEmployeeProfileWhereInput } = {},
|
||
) {
|
||
const name = clean(subject.displayName) || '该人员'
|
||
const employeeId = clean(subject.employeeProfileId || subject.id)
|
||
const employeeDingtalkUserId = clean(subject.dingtalkUserId)
|
||
const employeeJobNumber = clean(subject.jobNumber)
|
||
const managerDingtalkUserId = clean(subject.managerDingtalkUserId)
|
||
const managerJobNumber = clean(subject.managerJobNumber)
|
||
const activeWhere = options.activeWhere || { employmentState: 'ACTIVE' }
|
||
|
||
if (managerDingtalkUserId || managerJobNumber) {
|
||
if (
|
||
(managerDingtalkUserId && managerDingtalkUserId === employeeDingtalkUserId)
|
||
|| (managerJobNumber && managerJobNumber === employeeJobNumber)
|
||
) {
|
||
throw new EmployeeManagerResolutionError(`${name}的直属主管不能是本人,请先校准人员档案`)
|
||
}
|
||
const managers = await prisma.performanceSurveyEmployeeProfile.findMany({
|
||
where: { AND: [
|
||
activeWhere,
|
||
...(employeeId ? [{ id: { not: employeeId } }] : []),
|
||
{
|
||
...(managerDingtalkUserId ? { dingtalkUserId: managerDingtalkUserId } : {}),
|
||
...(managerJobNumber ? { jobNumber: managerJobNumber } : {}),
|
||
},
|
||
] },
|
||
take: 2,
|
||
})
|
||
if (managers.length !== 1) {
|
||
throw new EmployeeManagerResolutionError(`无法唯一匹配${name}的直属主管`)
|
||
}
|
||
return managers[0]
|
||
}
|
||
|
||
if (!employeeId) {
|
||
throw new EmployeeManagerResolutionError(`无法确定${name}所属的主组织,请先校准人员档案`)
|
||
}
|
||
const primaryMemberships = await prisma.performanceSurveyEmployeeDepartmentMembership.findMany({
|
||
where: { employeeProfileId: employeeId, isPrimary: true },
|
||
select: { sourceDeptId: true, departmentPath: true },
|
||
take: 2,
|
||
})
|
||
const departmentIds = Array.from(new Set(primaryMemberships.map(row => clean(row.sourceDeptId)).filter(Boolean)))
|
||
if (departmentIds.length !== 1) {
|
||
throw new EmployeeManagerResolutionError(`无法确定${name}所属的主组织,请先校准人员档案`)
|
||
}
|
||
const primaryMembership = primaryMemberships.find(row => clean(row.sourceDeptId) === departmentIds[0])
|
||
const numericSourceDeptId = Number(departmentIds[0])
|
||
const primaryOrganizationUnit = Number.isInteger(numericSourceDeptId)
|
||
? await prisma.organizationUnit.findUnique({
|
||
where: { sourceDeptId: numericSourceDeptId },
|
||
select: { path: true },
|
||
})
|
||
: null
|
||
const departmentPath = clean(primaryOrganizationUnit?.path) || clean(primaryMembership?.departmentPath)
|
||
const pathPrefixes = departmentPath
|
||
? departmentPath.split('/').map(part => part.trim()).filter(Boolean)
|
||
.map((_part, index, parts) => parts.slice(0, index + 1).join('/'))
|
||
: []
|
||
// 不跨到公司根节点猜主管;保留当前组织及根节点以下的直属上级链路。
|
||
const scopedPathPrefixes = pathPrefixes.length > 1 ? pathPrefixes.slice(1) : pathPrefixes
|
||
const organizationUnits = scopedPathPrefixes.length > 0
|
||
? await prisma.organizationUnit.findMany({
|
||
where: { isActive: true, path: { in: scopedPathPrefixes } },
|
||
select: { sourceDeptId: true, path: true },
|
||
})
|
||
: []
|
||
const sourceDeptIdByPath = new Map(
|
||
organizationUnits.map(unit => [clean(unit.path), clean(unit.sourceDeptId)]),
|
||
)
|
||
const lineageDepartmentIds = Array.from(new Set([
|
||
departmentIds[0],
|
||
...scopedPathPrefixes.slice().reverse().map(path => sourceDeptIdByPath.get(path)).filter(Boolean),
|
||
])) as string[]
|
||
|
||
const managers = await prisma.performanceSurveyEmployeeProfile.findMany({
|
||
where: { AND: [
|
||
activeWhere,
|
||
{ id: { not: employeeId } },
|
||
{ OR: [
|
||
{ levelType: { equals: 'M', mode: 'insensitive' } },
|
||
{ positionLevel: { startsWith: 'M', mode: 'insensitive' } },
|
||
] },
|
||
{ departmentMemberships: { some: { sourceDeptId: { in: lineageDepartmentIds } } } },
|
||
] },
|
||
include: {
|
||
departmentMemberships: {
|
||
where: { sourceDeptId: { in: lineageDepartmentIds } },
|
||
select: { sourceDeptId: true, isPrimary: true },
|
||
},
|
||
},
|
||
})
|
||
for (const sourceDeptId of lineageDepartmentIds) {
|
||
const candidates = managers.filter(manager => manager.departmentMemberships?.some(
|
||
membership => clean(membership.sourceDeptId) === sourceDeptId,
|
||
) === true)
|
||
if (candidates.length === 0) continue
|
||
const primaryCandidates = candidates.filter(manager => manager.departmentMemberships.some(
|
||
membership => clean(membership.sourceDeptId) === sourceDeptId && membership.isPrimary,
|
||
))
|
||
if (primaryCandidates.length === 1) return primaryCandidates[0]
|
||
if (primaryCandidates.length > 1 || candidates.length > 1) {
|
||
throw new EmployeeManagerResolutionError(`${name}所属组织内存在多名M级人员,请明确配置直属主管`)
|
||
}
|
||
// 当前层级没有主部门 M,仅有一个兼任 M 时才采用;多个兼任仍拒绝猜测。
|
||
return candidates[0]
|
||
}
|
||
throw new EmployeeManagerResolutionError(`${name}所属组织内没有可用的M级主管,请先校准人员档案`)
|
||
}
|