gongxiangp2/lib/team-functions.ts

88 lines
5.7 KiB
TypeScript
Raw Permalink 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 { Prisma } from '@prisma/client'
import { prisma } from '@/lib/prisma'
import { isRosterEmployed } from '@/lib/auto-personnel-rules'
import { resolveEmployeeOrganizationPaths, personBelongsToOrganizationPath } from '@/lib/organization-material-permissions'
import { teamFunctionInputSchema, type TeamFunctionInput, type TeamFunctionPayload } from '@/lib/team-function-contract'
export class TeamFunctionError extends Error {
constructor(message: string, public status: number) { super(message) }
}
// HR 没有部门负责人字段,配置只记录人工选择的人员 ID,不按姓名、职级推断。
async function loadRoster(db: Prisma.TransactionClient) {
const units = await db.organizationUnit.findMany({
where: { isActive: true },
select: { id: true, sourceDeptId: true, name: true, path: true, parentId: true },
orderBy: [{ name: 'asc' }, { id: 'asc' }],
})
const roots = units.filter(unit => unit.name === '品牌中心')
if (roots.length !== 1) throw new TeamFunctionError('品牌中心组织节点缺失或不唯一,请先核对花名册同步。', 409)
const teams = units.filter(unit => unit.parentId === roots[0].id)
const profiles = await db.performanceSurveyEmployeeProfile.findMany({
select: { id: true, displayName: true, realName: true, jobNumber: true, employmentState: true,
departmentPath: true, sourcePayload: true,
departmentMemberships: { select: { sourceDeptId: true, departmentPath: true } } },
orderBy: [{ displayName: 'asc' }, { id: 'asc' }],
})
const pathsById = new Map(units.map(unit => [String(unit.sourceDeptId), unit.path]))
const people = profiles.filter(profile => isRosterEmployed(profile.employmentState)).map(profile => ({
id: profile.id, name: profile.displayName || profile.realName || profile.jobNumber || '未命名人员',
jobNumber: profile.jobNumber, paths: resolveEmployeeOrganizationPaths(profile, pathsById),
}))
return { teams, people }
}
export async function getTeamFunctions(canEdit: boolean): Promise<TeamFunctionPayload> {
const { teams, people } = await loadRoster(prisma)
const configs = await prisma.teamFunctionConfig.findMany({ where: { organizationUnitId: { in: teams.map(team => team.id) } } })
return { canEdit, teams: teams.map(team => {
const config = configs.find(row => row.organizationUnitId === team.id)
const candidates = people.filter(person => personBelongsToOrganizationPath(person.paths, team.path))
const lead = candidates.find(person => person.id === config?.leadEmployeeProfileId)
const content = config ? teamFunctionInputSchema.parse({
organizationUnitId: team.id, revision: config.revision,
leadEmployeeProfileId: config.leadEmployeeProfileId, description: config.description, branches: config.branches,
}) : null
return { organizationUnitId: team.id, name: team.name, path: team.path,
lead: lead ? { employeeProfileId: lead.id, name: lead.name } : null,
leadStatus: lead ? 'active' : config?.leadEmployeeProfileId ? 'invalid' : 'unset',
description: content?.description ?? '', branches: content?.branches ?? [], revision: config?.revision ?? 0,
candidates: canEdit ? candidates.map(person => ({ employeeProfileId: person.id, name: person.name, jobNumber: person.jobNumber })) : [],
}
}) }
}
export async function saveTeamFunction(input: TeamFunctionInput, actorUserId: string) {
const value = teamFunctionInputSchema.parse(input)
try {
return await prisma.$transaction(async db => {
const { teams, people } = await loadRoster(db)
const team = teams.find(row => row.id === value.organizationUnitId)
if (!team) throw new TeamFunctionError('该部门已移出品牌中心或已停用,请刷新后重试。', 409)
if (value.leadEmployeeProfileId && !people.some(person => person.id === value.leadEmployeeProfileId && personBelongsToOrganizationPath(person.paths, team.path))) {
throw new TeamFunctionError('负责人必须是该部门范围内的在职花名册人员。', 400)
}
const before = await db.teamFunctionConfig.findUnique({ where: { organizationUnitId: team.id } })
if ((before?.revision ?? 0) !== value.revision) throw new TeamFunctionError('配置已被其他人更新,请取消编辑并重新加载。', 409)
const data = { description: value.description, branches: value.branches, leadEmployeeProfileId: value.leadEmployeeProfileId, updatedByUserId: actorUserId }
let saved
if (before) {
const result = await db.teamFunctionConfig.updateMany({ where: { id: before.id, revision: value.revision }, data: { ...data, revision: { increment: 1 } } })
if (result.count !== 1) throw new TeamFunctionError('配置已被其他人更新,请取消编辑并重新加载。', 409)
saved = await db.teamFunctionConfig.findUniqueOrThrow({ where: { id: before.id } })
} else {
saved = await db.teamFunctionConfig.create({ data: { ...data, organizationUnitId: team.id } })
}
const snapshot = (config: typeof saved) => ({ organizationUnitId: config.organizationUnitId, leadEmployeeProfileId: config.leadEmployeeProfileId, description: config.description, branches: config.branches, revision: config.revision })
await db.teamFunctionConfigAudit.create({ data: { configId: saved.id, actorUserId,
before: before ? snapshot(before) : Prisma.DbNull, after: snapshot(saved) } })
return { revision: saved.revision }
}, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable })
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && ['P2002', 'P2034'].includes(error.code)) {
throw new TeamFunctionError('配置或花名册已更新,请重新加载后重试。', 409)
}
throw error
}
}