feat: 默认以当前账户发起考核调研

This commit is contained in:
chenpei 2026-07-10 15:13:36 +08:00
parent 687ec77967
commit 7af0c1b926
4 changed files with 115 additions and 28 deletions

View File

@ -2130,7 +2130,6 @@ function CreateSurveyForm({
const [saving, setSaving] = useState(false)
const [publishingLabel, setPublishingLabel] = useState('')
const [error, setError] = useState<string | null>(null)
const defaultTargetInitializedRef = useRef(false)
const selectedTemplate = templateCode || templates[0]?.code || ''
const selectedTemplateInfo = templates.find((template) => template.code === selectedTemplate)
@ -2140,33 +2139,26 @@ function CreateSurveyForm({
1,
Number(selectedTemplateInfo?.requiredWorkItemCount || DEFAULT_VISUAL_CASE_COUNT),
)
const effectiveTargetEmployees = useMemo(
() => surveyKind === 'item_course'
? []
: mergeEmployeesById(currentEmployee ? [currentEmployee] : [], targetEmployees),
[currentEmployee, surveyKind, targetEmployees],
)
const currentTargetSelected = Boolean(
currentEmployee?.id && targetEmployees.some((employee) => employee.id === currentEmployee.id),
currentEmployee?.id && effectiveTargetEmployees.some((employee) => employee.id === currentEmployee.id),
)
const createdUploadTarget = visualTemplateSelected
? (createdSurvey?.targets || []).find((target: any) => target.canUploadWorkItems)
: null
const delegatedTargetCount = surveyKind === 'item_course'
? 0
: targetEmployees.filter((employee) => employee.id !== currentEmployee?.id).length
: effectiveTargetEmployees.filter((employee) => employee.id !== currentEmployee?.id).length
const evaluatorEmployees = useMemo(
() => mergeEmployeesById(manualEvaluatorEmployees, ...Object.values(evaluatorDepartmentEmployees)),
[manualEvaluatorEmployees, evaluatorDepartmentEmployees],
)
useEffect(() => {
if (
defaultTargetInitializedRef.current
|| surveyKind === 'item_course'
|| !currentEmployee?.id
) {
return
}
defaultTargetInitializedRef.current = true
setTargetEmployees((current) => current.length > 0 ? current : [currentEmployee])
}, [currentEmployee, surveyKind])
const removeEvaluatorEmployee = (id: string) => {
setManualEvaluatorEmployees((current) => current.filter((employee) => employee.id !== id))
setEvaluatorDepartmentEmployees((current) => {
@ -2193,8 +2185,8 @@ function CreateSurveyForm({
if (surveyKind === 'item_course' && targetItemsToSubmit.length === 0) {
throw new Error(`请填写${selectedTemplateInfo?.itemName || '课程、事项或活动'}名称`)
}
if (surveyKind !== 'item_course' && targetEmployees.length === 0) {
throw new Error(`请至少选择 1 个${selectedTemplateInfo?.ownerRoleName || '被考核人'}`)
if (surveyKind !== 'item_course' && !currentEmployee?.id) {
throw new Error('当前账户尚未匹配考核人员档案,暂时无法发起调研')
}
const visualCasesToSubmit = visualCases.slice(0, visualRequiredCount)
if (visualTemplateSelected && currentTargetSelected) {
@ -2221,7 +2213,7 @@ function CreateSurveyForm({
templateCode: selectedTemplate,
dueAt: assessmentMonth ? `${assessmentMonth}-01T00:00:00` : null,
openNow,
targetEmployeeIds: surveyKind === 'item_course' ? [] : targetEmployees.map((employee) => employee.id),
targetEmployeeIds: effectiveTargetEmployees.map((employee) => employee.id),
targetItems: targetItemsToSubmit,
evaluatorEmployeeIds: evaluatorEmployees.map((employee) => employee.id),
}),
@ -2252,7 +2244,7 @@ function CreateSurveyForm({
setTitle('')
setDescription('')
setDueAt(getCurrentMonthValue())
setTargetEmployees(surveyKind === 'item_course' || !currentEmployee ? [] : [currentEmployee])
setTargetEmployees([])
setManualEvaluatorEmployees([])
setEvaluatorDepartmentValues([])
setEvaluatorDepartmentEmployees({})
@ -2310,7 +2302,7 @@ function CreateSurveyForm({
{error && <div className="border border-[#ffdad6] bg-[#fff6f4] px-4 py-3 text-sm font-semibold text-[#93000a]">{error}</div>}
<button
disabled={saving || Boolean(createdSurvey)}
disabled={saving || Boolean(createdSurvey) || (surveyKind !== 'item_course' && !currentEmployee?.id)}
onClick={submit}
className="h-12 w-full rounded bg-[#0050cb] px-5 text-[15px] font-bold text-white hover:bg-[#003fa4] disabled:opacity-50"
>
@ -2339,10 +2331,43 @@ function CreateSurveyForm({
<div className="grid gap-8 lg:grid-cols-2">
<div>
{surveyKind !== 'item_course' ? (
<div>
<EmployeePicker title={selectedTemplateInfo?.ownerRoleName || '被考核人'} purpose="target" selected={targetEmployees} onChange={setTargetEmployees} />
<div className="grid gap-4">
<div>
<div className="text-[12px] font-semibold tracking-[0.05em] text-[#5f5e5e]">
{selectedTemplateInfo?.ownerRoleName || '被考核人'}
</div>
{currentEmployee ? (
<div className="mt-2 flex items-center gap-3 border border-[#dce5f3] bg-[#f8faff] px-3 py-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#0050cb] text-[13px] font-bold text-white">
{getSurveyInitials(currentEmployee.displayName)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-[14px] font-bold text-[#1a1c1c]">{currentEmployee.displayName}</div>
<div className="mt-0.5 truncate text-[12px] text-[#5f5e5e]">
{[currentEmployee.departmentName, currentEmployee.positionLevel || currentEmployee.levelType]
.filter(Boolean)
.join(' · ') || '当前登录账户'}
</div>
</div>
<span className="shrink-0 rounded-full bg-[#e8f0ff] px-2 py-1 text-[11px] font-bold text-[#0050cb]">
当前账户
</span>
</div>
) : (
<div className="mt-2 border border-[#ffdad6] bg-[#fff6f4] px-3 py-3 text-[12px] font-semibold text-[#93000a]">
当前账户尚未匹配考核人员档案
</div>
)}
</div>
<EmployeePicker
title="添加其他被考核人(可选)"
purpose="target"
selected={targetEmployees}
excludedIds={currentEmployee?.id ? [currentEmployee.id] : []}
onChange={setTargetEmployees}
/>
{delegatedTargetCount > 0 && (
<div className="mt-2 text-[12px] font-semibold text-[#0050cb]">
<div className="text-[12px] font-semibold text-[#0050cb]">
将向 {delegatedTargetCount} 位被考核人发送钉钉确认
</div>
)}
@ -2645,6 +2670,7 @@ function EmployeePicker({
title,
purpose,
selected,
excludedIds = [],
onChange,
onAddEmployee,
onRemoveEmployee,
@ -2652,6 +2678,7 @@ function EmployeePicker({
title: string
purpose?: 'target' | 'evaluator'
selected: Employee[]
excludedIds?: string[]
onChange: (employees: Employee[]) => void
onAddEmployee?: (employee: Employee) => void
onRemoveEmployee?: (id: string) => void
@ -2717,7 +2744,7 @@ function EmployeePicker({
return () => document.removeEventListener('mousedown', closeWhenOutside)
}, [])
const selectedIds = new Set(selected.map((employee) => employee.id))
const selectedIds = new Set([...selected.map((employee) => employee.id), ...excludedIds])
const selectEmployee = (employee: Employee) => {
if (selectedIds.has(employee.id)) return
if (onAddEmployee) {

View File

@ -0,0 +1,27 @@
import { getPerformanceSurveyTargetEmployeeIds } from '@/lib/performance-survey-target-policy'
describe('performance survey target policy', () => {
test('人员考核默认使用当前账户', () => {
expect(getPerformanceSurveyTargetEmployeeIds({
requestedEmployeeIds: [],
currentEmployeeId: 'employee-current',
surveyKind: 'person_period',
})).toEqual(['employee-current'])
})
test('添加其他被考核人时保留当前账户并去重', () => {
expect(getPerformanceSurveyTargetEmployeeIds({
requestedEmployeeIds: ['employee-other', 'employee-current', 'employee-other'],
currentEmployeeId: 'employee-current',
surveyKind: 'visual_case',
})).toEqual(['employee-current', 'employee-other'])
})
test('事项型调研不自动注入当前账户', () => {
expect(getPerformanceSurveyTargetEmployeeIds({
requestedEmployeeIds: ['employee-other'],
currentEmployeeId: 'employee-current',
surveyKind: 'item_course',
})).toEqual(['employee-other'])
})
})

View File

@ -38,6 +38,7 @@ import {
withPerformanceSurveyImageProcessingSlot,
type PerformanceSurveyStoredImageGroup,
} from '@/lib/performance-survey-image-processing'
import { getPerformanceSurveyTargetEmployeeIds } from '@/lib/performance-survey-target-policy'
import { deleteFromS3, uploadToS3 } from '@/lib/s3'
export class PerformanceSurveyApiError extends Error {
@ -1472,7 +1473,11 @@ export async function createPerformanceSurvey(session: Session | null, body: any
throw new PerformanceSurveyApiError('只能创建本人部门范围内的考核调研', 403)
}
const targets = await resolveSurveyTargets(body, surveyKind)
if (surveyKind !== 'item_course' && !ctx.employee?.id) {
throw new PerformanceSurveyApiError('当前账户尚未匹配考核人员档案,暂时无法发起调研', 409)
}
const targets = await resolveSurveyTargets(body, surveyKind, ctx.employee?.id)
if (targets.length === 0) {
throw new PerformanceSurveyApiError(
surveyKind === 'item_course'
@ -2442,8 +2447,12 @@ export async function runPerformanceSurveyNotificationMaintenance(session: Sessi
return { reminderCount: reminderIds.length, recovered }
}
async function resolveSurveyTargets(body: any, surveyKind: string) {
const targetEmployeeIds = normalizeList(body?.targetEmployeeIds)
async function resolveSurveyTargets(body: any, surveyKind: string, currentEmployeeId?: string | null) {
const targetEmployeeIds = getPerformanceSurveyTargetEmployeeIds({
requestedEmployeeIds: body?.targetEmployeeIds,
currentEmployeeId,
surveyKind,
})
const itemTargets = Array.isArray(body?.targetItems)
? body.targetItems
.map((item: any) => ({

View File

@ -0,0 +1,24 @@
type PerformanceSurveyTargetPolicyInput = {
requestedEmployeeIds: unknown
currentEmployeeId?: string | null
surveyKind: string
}
function normalizeEmployeeIds(value: unknown): string[] {
if (!Array.isArray(value)) return []
return Array.from(new Set(
value
.map((item) => String(item || '').trim())
.filter(Boolean),
))
}
export function getPerformanceSurveyTargetEmployeeIds({
requestedEmployeeIds,
currentEmployeeId,
surveyKind,
}: PerformanceSurveyTargetPolicyInput): string[] {
const requested = normalizeEmployeeIds(requestedEmployeeIds)
if (surveyKind === 'item_course') return requested
return normalizeEmployeeIds([currentEmployeeId, ...requested])
}