From 2466bb2b02ea76f31b5be302f0577d3e21b4efd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=9F=E6=9E=97=E9=93=96?= Date: Wed, 29 Jul 2026 18:36:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8F=91=E8=B5=B7=E8=B0=83=E7=A0=94?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=85=B3=E8=81=94=E4=BA=BA=E5=91=98=E5=B2=97?= =?UTF-8?q?=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...formanceSurveyDashboardNavigation.test.tsx | 63 ++++++++++ app/performance-survey/[[...path]]/page.tsx | 112 ++++++++++-------- .../performance-survey-service-policy.test.ts | 24 +++- lib/performance-survey-service.ts | 32 +++-- 4 files changed, 165 insertions(+), 66 deletions(-) diff --git a/app/performance-survey/PerformanceSurveyDashboardNavigation.test.tsx b/app/performance-survey/PerformanceSurveyDashboardNavigation.test.tsx index 3dca3e76..381fa3a5 100644 --- a/app/performance-survey/PerformanceSurveyDashboardNavigation.test.tsx +++ b/app/performance-survey/PerformanceSurveyDashboardNavigation.test.tsx @@ -174,4 +174,67 @@ describe('考核调研顶层导航保护', () => { { scroll: false }, )) }) + + test('发起调研按当前被考核人的人员分配自动关联岗位', async () => { + mockSearchParams = new URLSearchParams('tab=manage&adminWorkspace=launch&adminMonth=2026-07') + global.fetch = jest.fn(async (input) => { + const url = String(input) + if (url.endsWith('/me')) { + return jsonResponse({ + user: { id: 'user-admin', name: '管理员', email: 'admin@example.com' }, + employee: { + id: 'employee-admin', + displayName: '管理员', + departmentName: '品牌创意部', + positionLevel: 'M1', + }, + permissions: { + canCreate: true, + isSystemAdmin: true, + isDepartmentAdmin: true, + isLeadership: true, + }, + }) + } + if (url.includes('/position-groups?month=2026-07')) { + return jsonResponse({ + month: '2026-07', + groups: [{ + id: 'group-1', + code: 'brand_designer', + name: '品牌设计师', + status: 'ready', + templateCount: 1, + }], + membership: { + status: 'matched', + groupCode: 'brand_designer', + groupName: '品牌设计师', + conflictGroupNames: [], + }, + }) + } + if (url.includes('/templates?month=2026-07&positionGroupCode=brand_designer')) { + return jsonResponse([{ + ...templateDefinition, + templateVersionId: 'version-1', + templateOptionKey: 'visual_review:version-1', + templateVariantName: '视觉案例评分', + }]) + } + if (url.includes('/departments?purpose=evaluator')) return jsonResponse([]) + if (url.endsWith('/surveys')) return jsonResponse([]) + return jsonResponse([]) + }) as jest.Mock + + render() + + expect(await screen.findByText('品牌设计师')).toBeInTheDocument() + expect(screen.queryByRole('combobox', { name: '考核岗位' })).not.toBeInTheDocument() + expect(screen.getByText('已按被考核人的人员分配自动关联,发起时无需选择。')).toBeInTheDocument() + await waitFor(() => expect(global.fetch).toHaveBeenCalledWith( + '/api/performance-survey/templates?month=2026-07&positionGroupCode=brand_designer', + expect.anything(), + )) + }) }) diff --git a/app/performance-survey/[[...path]]/page.tsx b/app/performance-survey/[[...path]]/page.tsx index 467e7948..fd688ba5 100644 --- a/app/performance-survey/[[...path]]/page.tsx +++ b/app/performance-survey/[[...path]]/page.tsx @@ -855,11 +855,6 @@ function Dashboard({ me, permissionsLoading }: { me: any | null; permissionsLoad @@ -1219,7 +1214,6 @@ function PerformanceDashboardBoard({ params.set('tab', 'manage') params.set('prefillMonth', targetMonth) params.set('prefillTemplate', templateCode) - if (selectedPositionGroup?.code) params.set('prefillPositionGroup', selectedPositionGroup.code) router.push(`/performance-survey?${params.toString()}`) } : undefined} /> @@ -2828,13 +2822,11 @@ const PERFORMANCE_SURVEY_ADMIN_WORKSPACES: PerformanceSurveyAdminWorkspace[] = [ function ManagePanel({ currentEmployee, isSystemAdmin, - canSelectAnyPosition, configurationDirty, onConfigurationDirtyChange, }: { currentEmployee: Employee | null isSystemAdmin: boolean - canSelectAnyPosition: boolean configurationDirty: boolean onConfigurationDirtyChange: (dirty: boolean) => void }) { @@ -2984,7 +2976,6 @@ function ManagePanel({ void selectWorkspace(nextWorkspace) : undefined} @@ -3079,7 +3070,6 @@ function TemplateStatsPanel({ items }: { items: any[] }) { function CreateSurveyForm({ departments, currentEmployee, - canSelectAnyPosition, defaultMonth, onMonthChange, onAdminNavigate, @@ -3087,7 +3077,6 @@ function CreateSurveyForm({ }: { departments: DepartmentOption[] currentEmployee: Employee | null - canSelectAnyPosition: boolean defaultMonth?: string onMonthChange?: (month: string) => void onAdminNavigate?: (workspace: PerformanceSurveyAdminWorkspace) => void @@ -3097,12 +3086,10 @@ function CreateSurveyForm({ const searchParams = useSearchParams() const prefillMonth = searchParams.get('prefillMonth') || '' const prefillTemplate = searchParams.get('prefillTemplate') || '' - const prefillPositionGroup = searchParams.get('prefillPositionGroup') || '' const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [templateOptionKey, setTemplateOptionKey] = useState(prefillTemplate) const [assessmentMonth, setAssessmentMonth] = useState(prefillMonth || defaultMonth || getCurrentMonthValue()) - const [positionGroupCode, setPositionGroupCode] = useState(prefillPositionGroup) const [deadlineAt, setDeadlineAt] = useState('') const openNow = true const [targetEmployees, setTargetEmployees] = useState([]) @@ -3121,10 +3108,11 @@ function CreateSurveyForm({ const positionGroupsState = useApi(positionGroupsPath, [positionGroupsPath]) const availablePositionGroups = positionGroupsState.data?.groups || [] const membershipStatus = positionGroupsState.data?.membership?.status as 'matched' | 'unmatched' | 'conflict' | undefined - const selectedPositionGroupCode = positionGroupCode - || positionGroupsState.data?.membership?.groupCode - || '' - const templatesPath = selectedPositionGroupCode + const selectedPositionGroupCode = membershipStatus === 'matched' + ? positionGroupsState.data?.membership?.groupCode || '' + : '' + const selectedPositionGroup = availablePositionGroups.find((group: any) => group.code === selectedPositionGroupCode) + const templatesPath = selectedPositionGroup?.status === 'ready' ? `/templates?month=${encodeURIComponent(assessmentMonth)}&positionGroupCode=${encodeURIComponent(selectedPositionGroupCode)}` : null const templatesState = useApi(templatesPath, [templatesPath]) @@ -3133,13 +3121,12 @@ function CreateSurveyForm({ useEffect(() => { if (prefillMonth && /^\d{4}-\d{2}$/.test(prefillMonth)) setAssessmentMonth(prefillMonth) else if (defaultMonth && /^\d{4}-\d{2}$/.test(defaultMonth)) setAssessmentMonth(defaultMonth) - if (prefillPositionGroup) setPositionGroupCode(prefillPositionGroup) if (prefillTemplate) { const matched = templates.find((template) => template.templateOptionKey === prefillTemplate) || templates.find((template) => template.code === prefillTemplate) if (matched) setTemplateOptionKey(matched.templateOptionKey || matched.code) } - }, [defaultMonth, prefillMonth, prefillPositionGroup, prefillTemplate, templates]) + }, [defaultMonth, prefillMonth, prefillTemplate, templates]) const selectedTemplate = templateOptionKey || templates[0]?.templateOptionKey || templates[0]?.code || '' const selectedTemplateInfo = templates.find((template) => (template.templateOptionKey || template.code) === selectedTemplate) @@ -3190,13 +3177,15 @@ function CreateSurveyForm({ requiresManualEvaluators && evaluatorEmployees.length === 0 ) publishValidationIssues.push('至少选择 1 名评分人') - if (!selectedPositionGroupCode) publishValidationIssues.push('选择考核岗位') - if (!canSelectAnyPosition && membershipStatus === 'unmatched') { + if (membershipStatus === 'unmatched') { publishValidationIssues.push('联系管理员配置本人考核岗位') } - if (!canSelectAnyPosition && membershipStatus === 'conflict') { + if (membershipStatus === 'conflict') { publishValidationIssues.push('联系管理员处理本人岗位重复匹配') } + if (membershipStatus === 'matched' && selectedPositionGroup?.status !== 'ready') { + publishValidationIssues.push('联系管理员完成本人岗位考核项配置') + } if (visualTemplateSelected && currentTargetSelected) { const visibleVisualCases = visualCases.slice(0, visualRequiredCount) if (visibleVisualCases.length < visualRequiredCount) { @@ -3270,7 +3259,6 @@ function CreateSurveyForm({ title: submitTitle, description, surveyKind, - positionGroupCode: selectedPositionGroupCode, templateCode: selectedTemplateCode, templateVersionId: selectedTemplateInfo?.templateVersionId || null, assessmentMonth: selectedAssessmentMonth, @@ -3317,7 +3305,6 @@ function CreateSurveyForm({ setTitle('') setDescription('') setTemplateOptionKey('') - setPositionGroupCode('') setAssessmentMonth(getCurrentMonthValue()) setDeadlineAt('') setTargetEmployees([]) @@ -3360,46 +3347,75 @@ function CreateSurveyForm({ }} type="month" /> - + {membershipStatus === 'matched' && selectedPositionGroup?.status !== 'ready' && ( +
+ 当前岗位尚未完成考核项配置,暂时不能发起调研 +
+ )} + {onAdminNavigate && positionGroupsState.data && ( - availablePositionGroups.every((group: any) => group.status !== 'ready') + membershipStatus !== 'matched' + || selectedPositionGroup?.status !== 'ready' || (selectedPositionGroupCode && !templatesState.loading && templates.length === 0) ) && (
- {availablePositionGroups.every((group: any) => group.status !== 'ready') - ? `${assessmentMonth} 暂无可发起的岗位` - : '所选岗位还没有可用考核模板'} + {membershipStatus !== 'matched' + ? '当前人员还不能自动关联岗位' + : selectedPositionGroup?.status !== 'ready' + ? '当前岗位尚未完成配置' + : '当前岗位还没有可用考核模板'}
-

请先补齐岗位设置、人员归属和可用模板,再回来发起调研。

+

请先补齐人员归属、岗位设置和可用模板,再回来发起调研。

{([ ['positions', '检查岗位设置'], diff --git a/lib/__tests__/performance-survey-service-policy.test.ts b/lib/__tests__/performance-survey-service-policy.test.ts index 726e2451..efac1ce4 100644 --- a/lib/__tests__/performance-survey-service-policy.test.ts +++ b/lib/__tests__/performance-survey-service-policy.test.ts @@ -872,7 +872,6 @@ describe('performance survey service policies', () => { await createPerformanceSurvey(session, { templateCode: 'course_share', - positionGroupCode: 'brand_designer', assessmentMonth: '2026-07', targetEmployeeIds: ['employee-1'], evaluatorEmployeeIds: ['employee-2'], @@ -891,6 +890,29 @@ describe('performance survey service policies', () => { expect(transactionClient.performanceSurveyTarget.findMany.mock.invocationCallOrder[0]).toBeLessThan( transactionClient.performanceSurvey.create.mock.invocationCallOrder[0], ) + expect(getPerformanceSurveyPositionGroupByCode).not.toHaveBeenCalled() + expect(transactionClient.performanceSurvey.create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + positionGroupCode: 'brand_designer', + positionGroupId: 'position-group-brand', + }), + }) + }) + + test('客户端不能用旧岗位参数覆盖被考核人的自动岗位', async () => { + await expect(createPerformanceSurvey(session, { + templateCode: 'course_share', + positionGroupCode: 'ai_designer', + assessmentMonth: '2026-07', + targetEmployeeIds: ['employee-1'], + evaluatorEmployeeIds: ['employee-2'], + openNow: false, + })).rejects.toEqual( + new PerformanceSurveyApiError('考核岗位已根据被考核人的人员分配自动关联,请刷新页面后重试', 409), + ) + + expect(getPerformanceSurveyPositionGroupByCode).not.toHaveBeenCalled() + expect(prisma.$transaction).not.toHaveBeenCalled() }) test('发起人不能手动选择自己作为评分人员', async () => { diff --git a/lib/performance-survey-service.ts b/lib/performance-survey-service.ts index fbab05c8..17546dd8 100644 --- a/lib/performance-survey-service.ts +++ b/lib/performance-survey-service.ts @@ -2821,28 +2821,26 @@ export async function createPerformanceSurvey(session: Session | null, body: any const assessmentMonthInput = normalizePerformanceSurveyAssessmentMonth(body?.assessmentMonth) if (!assessmentMonthInput) throw new PerformanceSurveyApiError('请选择有效的考核月份') - const positionGroupCode = clean(body?.positionGroupCode) - if (!positionGroupCode) throw new PerformanceSurveyApiError('请选择考核岗位组') - let positionGroup + if (!ctx.employee) throw new PerformanceSurveyApiError('当前账户尚未匹配人员档案', 409) + let creatorMembership try { - positionGroup = await getPerformanceSurveyPositionGroupByCode(positionGroupCode, assessmentMonthInput) + creatorMembership = await getPerformanceSurveyPositionGroupForEmployee(ctx.employee, assessmentMonthInput) } catch (error) { return rethrowPositionConfigError(error) } + if (creatorMembership.status !== 'matched' || !creatorMembership.group) { + throw new PerformanceSurveyApiError( + creatorMembership.status === 'conflict' + ? '当前被考核人命中多个考核岗位组,请联系管理员处理' + : '当前被考核人尚未配置考核岗位组', + 409, + ) + } + const positionGroup = creatorMembership.group if (positionGroup.status !== 'ready') throw new PerformanceSurveyApiError('该岗位组尚未完成配置,不能发起调研', 409) - - if (!ctx.isSystemAdmin && !ctx.isLeadership && !ctx.isDepartmentAdmin) { - if (!ctx.employee) throw new PerformanceSurveyApiError('当前账户尚未匹配人员档案', 409) - const creatorMembership = await getPerformanceSurveyPositionGroupForEmployee(ctx.employee, assessmentMonthInput) - if (creatorMembership.status !== 'matched') { - throw new PerformanceSurveyApiError( - creatorMembership.status === 'conflict' ? '当前账户命中多个考核岗位组,请联系管理员处理' : '当前账户尚未配置考核岗位组', - 409, - ) - } - if (creatorMembership.group?.code !== positionGroup.code) { - throw new PerformanceSurveyApiError('普通员工只能发起本人岗位组的考核调研', 403) - } + const legacyRequestedPositionGroupCode = clean(body?.positionGroupCode) + if (legacyRequestedPositionGroupCode && legacyRequestedPositionGroupCode !== positionGroup.code) { + throw new PerformanceSurveyApiError('考核岗位已根据被考核人的人员分配自动关联,请刷新页面后重试', 409) } const templateVersionId = clean(body?.templateVersionId)