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 { 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 } }