gongxiangp2/lib/employee-manager.ts

130 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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级主管,请先校准人员档案`)
}