fix: 发起调研自动关联人员岗位
This commit is contained in:
parent
64f3fb211e
commit
2466bb2b02
@ -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(<PerformanceSurveyPage />)
|
||||
|
||||
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(),
|
||||
))
|
||||
})
|
||||
})
|
||||
|
||||
@ -855,11 +855,6 @@ function Dashboard({ me, permissionsLoading }: { me: any | null; permissionsLoad
|
||||
<ManagePanel
|
||||
currentEmployee={me?.employee || null}
|
||||
isSystemAdmin={Boolean(me?.permissions?.isSystemAdmin)}
|
||||
canSelectAnyPosition={Boolean(
|
||||
me?.permissions?.isSystemAdmin
|
||||
|| me?.permissions?.isDepartmentAdmin
|
||||
|| me?.permissions?.isLeadership,
|
||||
)}
|
||||
configurationDirty={manageConfigurationDirty}
|
||||
onConfigurationDirtyChange={setManageConfigurationDirty}
|
||||
/>
|
||||
@ -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({
|
||||
<CreateSurveyForm
|
||||
departments={departments.data || []}
|
||||
currentEmployee={currentEmployee}
|
||||
canSelectAnyPosition={canSelectAnyPosition}
|
||||
defaultMonth={adminMonth}
|
||||
onMonthChange={selectAdminMonth}
|
||||
onAdminNavigate={isSystemAdmin ? (nextWorkspace) => 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<Employee[]>([])
|
||||
@ -3121,10 +3108,11 @@ function CreateSurveyForm({
|
||||
const positionGroupsState = useApi<any>(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<Template[]>(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"
|
||||
/>
|
||||
<label className="grid gap-2 text-sm">
|
||||
<div className="grid gap-2 text-sm">
|
||||
<span className="text-[12px] font-semibold tracking-[0.05em] text-[#5f5e5e]">考核岗位</span>
|
||||
<select
|
||||
value={selectedPositionGroupCode}
|
||||
onChange={(event) => {
|
||||
setPositionGroupCode(event.target.value)
|
||||
setTemplateOptionKey('')
|
||||
setTargetEmployees([])
|
||||
}}
|
||||
className="h-11 rounded border border-[#eeeeee] bg-white px-3 text-[14px] font-semibold text-[#1a1c1c] outline-none transition-colors focus:border-[#0050cb]"
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="flex min-h-11 min-w-0 items-center justify-between gap-3 rounded border border-[#eeeeee] bg-[#f9f9f9] px-3 py-2"
|
||||
>
|
||||
<option value="">选择考核岗位</option>
|
||||
{availablePositionGroups.map((group: any) => (
|
||||
<option key={group.code} value={group.code} disabled={group.status !== 'ready'}>
|
||||
{group.name}{group.status === 'ready' ? ` · ${group.templateCount}项` : ' · 待配置'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{!canSelectAnyPosition && positionGroupsState.data?.membership?.status === 'conflict' && (
|
||||
<span className="min-w-0 break-words text-[14px] font-semibold text-[#1a1c1c]">
|
||||
{positionGroupsState.loading
|
||||
? '正在关联人员岗位...'
|
||||
: positionGroupsState.error
|
||||
? '岗位关联失败'
|
||||
: membershipStatus === 'matched'
|
||||
? positionGroupsState.data?.membership?.groupName || '已关联岗位'
|
||||
: membershipStatus === 'conflict'
|
||||
? '岗位配置冲突'
|
||||
: '尚未配置岗位'}
|
||||
</span>
|
||||
<span className="shrink-0 text-[11px] font-semibold text-[#5f5e5e]">自动关联</span>
|
||||
</div>
|
||||
{positionGroupsState.error && (
|
||||
<div
|
||||
role="alert"
|
||||
className="flex flex-wrap items-center justify-between gap-2 border border-[#ffdad6] bg-[#fff6f4] px-3 py-2 text-[12px] font-semibold text-[#93000a]"
|
||||
>
|
||||
<span className="min-w-0 break-words">岗位关联失败:{positionGroupsState.error}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void positionGroupsState.refresh()}
|
||||
className="min-h-9 shrink-0 rounded border border-[#93000a] px-3 hover:bg-[#93000a] hover:text-white"
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{membershipStatus === 'matched' && (
|
||||
<div className="text-[12px] leading-5 text-[#5f5e5e]">
|
||||
已按被考核人的人员分配自动关联,发起时无需选择。
|
||||
</div>
|
||||
)}
|
||||
{positionGroupsState.data?.membership?.status === 'conflict' && (
|
||||
<div className="text-[12px] font-semibold text-[#b42318]">
|
||||
当前人员同时命中多个岗位:{(positionGroupsState.data.membership.conflictGroupNames || []).join('、')}
|
||||
</div>
|
||||
)}
|
||||
{!canSelectAnyPosition && positionGroupsState.data?.membership?.status === 'unmatched' && (
|
||||
{positionGroupsState.data?.membership?.status === 'unmatched' && (
|
||||
<div className="text-[12px] font-semibold text-[#b42318]">
|
||||
当前人员尚未匹配考核岗位,请联系系统管理员完成岗位配置
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
{membershipStatus === 'matched' && selectedPositionGroup?.status !== 'ready' && (
|
||||
<div className="text-[12px] font-semibold text-[#b42318]">
|
||||
当前岗位尚未完成考核项配置,暂时不能发起调研
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{onAdminNavigate && positionGroupsState.data && (
|
||||
availablePositionGroups.every((group: any) => group.status !== 'ready')
|
||||
membershipStatus !== 'matched'
|
||||
|| selectedPositionGroup?.status !== 'ready'
|
||||
|| (selectedPositionGroupCode && !templatesState.loading && templates.length === 0)
|
||||
) && (
|
||||
<div className="border border-[#e4b86a] bg-[#fffaf0] px-3 py-3 text-[12px] leading-5 text-[#6f4700]">
|
||||
<div className="font-bold">
|
||||
{availablePositionGroups.every((group: any) => group.status !== 'ready')
|
||||
? `${assessmentMonth} 暂无可发起的岗位`
|
||||
: '所选岗位还没有可用考核模板'}
|
||||
{membershipStatus !== 'matched'
|
||||
? '当前人员还不能自动关联岗位'
|
||||
: selectedPositionGroup?.status !== 'ready'
|
||||
? '当前岗位尚未完成配置'
|
||||
: '当前岗位还没有可用考核模板'}
|
||||
</div>
|
||||
<p className="mt-1">请先补齐岗位设置、人员归属和可用模板,再回来发起调研。</p>
|
||||
<p className="mt-1">请先补齐人员归属、岗位设置和可用模板,再回来发起调研。</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{([
|
||||
['positions', '检查岗位设置'],
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user