88 lines
5.7 KiB
TypeScript
88 lines
5.7 KiB
TypeScript
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
|
||
}
|
||
}
|