限制考核调研发起人参与评分
This commit is contained in:
parent
8f7ba177e3
commit
62ca61d9e2
@ -2734,8 +2734,9 @@ function CreateSurveyForm({
|
||||
? 0
|
||||
: effectiveTargetEmployees.filter((employee) => employee.id !== currentEmployee?.id).length
|
||||
const evaluatorEmployees = useMemo(
|
||||
() => mergeEmployeesById(manualEvaluatorEmployees, ...Object.values(evaluatorDepartmentEmployees)),
|
||||
[manualEvaluatorEmployees, evaluatorDepartmentEmployees],
|
||||
() => mergeEmployeesById(manualEvaluatorEmployees, ...Object.values(evaluatorDepartmentEmployees))
|
||||
.filter((employee) => employee.id !== currentEmployee?.id),
|
||||
[currentEmployee?.id, manualEvaluatorEmployees, evaluatorDepartmentEmployees],
|
||||
)
|
||||
const publishValidationIssues: string[] = []
|
||||
if (!selectedTemplate) publishValidationIssues.push('选择考评模板')
|
||||
@ -3012,6 +3013,7 @@ function CreateSurveyForm({
|
||||
title={automaticWorkCooperationEvaluators ? '手动调整评分人员(可选)' : '评分人员'}
|
||||
purpose="evaluator"
|
||||
selected={evaluatorEmployees}
|
||||
excludedIds={currentEmployee?.id ? [currentEmployee.id] : []}
|
||||
onChange={setManualEvaluatorEmployees}
|
||||
onAddEmployee={(employee) => setManualEvaluatorEmployees((current) => mergeEmployeesById(current, [employee]))}
|
||||
onRemoveEmployee={removeEvaluatorEmployee}
|
||||
@ -3021,6 +3023,7 @@ function CreateSurveyForm({
|
||||
<DepartmentPicker
|
||||
departments={departments}
|
||||
selected={evaluatorDepartmentValues}
|
||||
excludedEmployeeIds={currentEmployee?.id ? [currentEmployee.id] : []}
|
||||
onChange={setEvaluatorDepartmentValues}
|
||||
onDepartmentEmployeesChange={(department, checked, employees) => {
|
||||
setEvaluatorDepartmentEmployees((current) => {
|
||||
@ -3508,11 +3511,13 @@ function EmployeePicker({
|
||||
function DepartmentPicker({
|
||||
departments,
|
||||
selected,
|
||||
excludedEmployeeIds = [],
|
||||
onChange,
|
||||
onDepartmentEmployeesChange,
|
||||
}: {
|
||||
departments: DepartmentOption[]
|
||||
selected: string[]
|
||||
excludedEmployeeIds?: string[]
|
||||
onChange: (items: string[]) => void
|
||||
onDepartmentEmployeesChange?: (value: string, checked: boolean, employees: Employee[]) => void
|
||||
}) {
|
||||
@ -3601,7 +3606,9 @@ function DepartmentPicker({
|
||||
limit: '1000',
|
||||
purpose: 'evaluator',
|
||||
})
|
||||
const employees = await apiRequest<Employee[]>(`/employees?${params.toString()}`)
|
||||
const excludedIds = new Set(excludedEmployeeIds)
|
||||
const employees = (await apiRequest<Employee[]>(`/employees?${params.toString()}`))
|
||||
.filter((employee) => !excludedIds.has(employee.id))
|
||||
employeeCacheRef.current.set(value, employees)
|
||||
return employees
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
getPerformanceSurveyMe,
|
||||
getPerformanceSurveyResults,
|
||||
listPerformanceSurveys,
|
||||
searchPerformanceSurveyEmployees,
|
||||
updatePerformanceSurvey,
|
||||
} from '@/lib/performance-survey-service'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
@ -160,6 +161,7 @@ describe('performance survey service policies', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue(baseUser)
|
||||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockReset()
|
||||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(baseEmployee)
|
||||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey())
|
||||
;(prisma.performanceSurvey.findMany as jest.Mock).mockResolvedValue([])
|
||||
@ -168,11 +170,14 @@ describe('performance survey service policies', () => {
|
||||
;(prisma.performanceSurveyResponse.findMany as jest.Mock).mockResolvedValue([])
|
||||
;(prisma.performanceSurvey.update as jest.Mock).mockResolvedValue({})
|
||||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue(null)
|
||||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockReset()
|
||||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([])
|
||||
;(prisma.performanceSurveyEmployeeProfile.count as jest.Mock).mockResolvedValue(0)
|
||||
;(prisma.performanceSurveyTarget.findMany as jest.Mock).mockResolvedValue([])
|
||||
;(prisma.user.findMany as jest.Mock).mockReset()
|
||||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([])
|
||||
;(prisma.performanceSurvey.count as jest.Mock).mockResolvedValue(0)
|
||||
;(getPerformanceSurveyTemplate as jest.Mock).mockReset()
|
||||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||||
code: 'course_share',
|
||||
name: '课程分享月度考核',
|
||||
@ -613,6 +618,31 @@ describe('performance survey service policies', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('发起人不能手动选择自己作为评分人员', async () => {
|
||||
await expect(createPerformanceSurvey(session, {
|
||||
templateCode: 'course_share',
|
||||
assessmentMonth: '2026-07',
|
||||
targetEmployeeIds: ['employee-1'],
|
||||
evaluatorEmployeeIds: ['employee-1'],
|
||||
openNow: false,
|
||||
})).rejects.toEqual(
|
||||
new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409),
|
||||
)
|
||||
|
||||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('评分人员搜索和部门批量加载都排除当前发起人', async () => {
|
||||
await searchPerformanceSurveyEmployees(session, new URLSearchParams({
|
||||
purpose: 'evaluator',
|
||||
department: '集团A/品牌中心/策划部',
|
||||
limit: '1000',
|
||||
}))
|
||||
|
||||
const query = (prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mock.calls[0][0]
|
||||
expect(query.where.AND).toContainEqual({ id: { not: 'employee-1' } })
|
||||
})
|
||||
|
||||
test('创建接口拒绝直接传入组织范围外的被考核人 ID', async () => {
|
||||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValueOnce([baseEmployee])
|
||||
|
||||
@ -769,6 +799,69 @@ describe('performance survey service policies', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
test('自动匹配的非自评人员如果是发起人则拒绝创建', async () => {
|
||||
const creator = {
|
||||
...baseEmployee,
|
||||
dingtalkUserId: baseUser.dingtalkUserId,
|
||||
departmentName: '品牌创意A部',
|
||||
departmentPath: '集团A/品牌中心/品牌创意A部',
|
||||
managerDingtalkUserId: 'ding-other-manager',
|
||||
}
|
||||
const target = {
|
||||
...creator,
|
||||
id: 'employee-target',
|
||||
displayName: '其他被考核人',
|
||||
dingtalkUserId: 'ding-target',
|
||||
managerDingtalkUserId: creator.dingtalkUserId,
|
||||
}
|
||||
const creatorManager = {
|
||||
...creator,
|
||||
id: 'employee-creator-manager',
|
||||
displayName: '发起人的主管',
|
||||
dingtalkUserId: 'ding-other-manager',
|
||||
}
|
||||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockReset()
|
||||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(creator)
|
||||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||||
code: 'work_cooperation',
|
||||
name: '工作配合度满意度调研',
|
||||
shortName: '工作配合度',
|
||||
surveyType: 'person_period',
|
||||
weight: 0.2,
|
||||
questions: [],
|
||||
scoreBands: [],
|
||||
})
|
||||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockReset()
|
||||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockImplementation(async ({ where }: any) => {
|
||||
const clauses = where?.AND || []
|
||||
const requestedIds = clauses.find((clause: any) => clause?.id?.in)?.id?.in
|
||||
if (requestedIds) return [creator, target]
|
||||
const evaluatorDingtalkUserId = clauses.find((clause: any) => clause?.dingtalkUserId)?.dingtalkUserId
|
||||
if (evaluatorDingtalkUserId === creatorManager.dingtalkUserId) return [creatorManager]
|
||||
if (evaluatorDingtalkUserId === creator.dingtalkUserId) return [creator]
|
||||
return []
|
||||
})
|
||||
;(prisma.user.findMany as jest.Mock).mockReset()
|
||||
;(prisma.user.findMany as jest.Mock).mockImplementation(async ({ where }: any) => {
|
||||
const dingtalkUserId = where?.dingtalkUserId
|
||||
if (dingtalkUserId === creatorManager.dingtalkUserId) {
|
||||
return [{ id: 'user-creator-manager', dingtalkUserId }]
|
||||
}
|
||||
return [{ id: baseUser.id, dingtalkUserId: baseUser.dingtalkUserId }]
|
||||
})
|
||||
|
||||
await expect(createPerformanceSurvey(session, {
|
||||
templateCode: 'work_cooperation',
|
||||
assessmentMonth: '2026-07',
|
||||
targetEmployeeIds: [target.id],
|
||||
openNow: false,
|
||||
})).rejects.toEqual(
|
||||
new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409),
|
||||
)
|
||||
|
||||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test.each([
|
||||
{
|
||||
label: '品牌创意A部P级人员',
|
||||
|
||||
@ -963,6 +963,9 @@ export async function searchPerformanceSurveyEmployees(session: Session | null,
|
||||
if (!ctx.isSystemAdmin && !isEvaluatorSearch) {
|
||||
and.push(getContextScopeDepartmentWhere(ctx) || { id: '__none__' })
|
||||
}
|
||||
if (isEvaluatorSearch && ctx.employee?.id) {
|
||||
and.push({ id: { not: ctx.employee.id } })
|
||||
}
|
||||
if (includeKeyword && search) {
|
||||
and.push({ OR: [
|
||||
{ displayName: { contains: search, mode: 'insensitive' } },
|
||||
@ -2083,7 +2086,11 @@ export async function createPerformanceSurvey(session: Session | null, body: any
|
||||
} catch (error) {
|
||||
throw new PerformanceSurveyApiError(error instanceof Error ? error.message : '考评模板版本无效')
|
||||
}
|
||||
const hasRequestedEvaluators = normalizeList(body?.evaluatorEmployeeIds).length > 0
|
||||
const requestedEvaluatorEmployeeIds = normalizeList(body?.evaluatorEmployeeIds)
|
||||
if (ctx.employee?.id && requestedEvaluatorEmployeeIds.includes(ctx.employee.id)) {
|
||||
throw new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409)
|
||||
}
|
||||
const hasRequestedEvaluators = requestedEvaluatorEmployeeIds.length > 0
|
||||
|| normalizeList(body?.evaluatorDepartmentNames).length > 0
|
||||
const useDefaultWorkCooperationEvaluators = usesAutomaticWorkCooperationEvaluators(
|
||||
template.code,
|
||||
@ -2158,6 +2165,19 @@ export async function createPerformanceSurvey(session: Session | null, body: any
|
||||
? await resolveWorkCooperationEvaluators(targetSource?.employee || target.targetSnapshot)
|
||||
: evaluators
|
||||
for (const evaluator of targetEvaluators) {
|
||||
const evaluatorDingtalkUserId = clean(evaluator.dingtalkUserId)
|
||||
|| clean(evaluator.employee?.dingtalkUserId)
|
||||
const isCreatorEvaluator = Boolean(
|
||||
(ctx.employee?.id && evaluator.employee?.id === ctx.employee.id)
|
||||
|| (evaluator.userId && evaluator.userId === ctx.user.id)
|
||||
|| (
|
||||
ctx.user.dingtalkUserId
|
||||
&& evaluatorDingtalkUserId === clean(ctx.user.dingtalkUserId)
|
||||
),
|
||||
)
|
||||
if (isCreatorEvaluator && !isSelfWorkflowRole(evaluator.workflowRole || null)) {
|
||||
throw new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409)
|
||||
}
|
||||
if (
|
||||
surveyKind !== 'item_course' &&
|
||||
target.employeeProfileId &&
|
||||
@ -3495,14 +3515,15 @@ async function resolveSurveyEvaluators(body: any, ctx: UserContext) {
|
||||
or.push(...getDepartmentSelectionWhere(evaluatorDepartmentNames))
|
||||
}
|
||||
|
||||
if (or.length === 0 && ctx.employee) {
|
||||
or.push({ id: ctx.employee.id })
|
||||
}
|
||||
if (or.length === 0) return []
|
||||
|
||||
const and: any[] = [
|
||||
await getPerformanceSurveyAccountProfileWhere(),
|
||||
{ OR: or },
|
||||
]
|
||||
if (ctx.employee?.id) {
|
||||
and.push({ id: { not: ctx.employee.id } })
|
||||
}
|
||||
const where: any = { AND: and }
|
||||
|
||||
const employees = or.length > 0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user