fix: 展示岗位人员并修复公共看板权限
This commit is contained in:
parent
5590c5a454
commit
19174d5050
@ -37,6 +37,17 @@ const config = {
|
||||
isActive: true,
|
||||
status: 'ready',
|
||||
memberCount: 6,
|
||||
members: [{
|
||||
id: 'employee-ruolan',
|
||||
displayName: '若兰',
|
||||
departmentName: '品牌创意B部',
|
||||
positionTitle: '营销策划主管',
|
||||
}, {
|
||||
id: 'employee-muyi',
|
||||
displayName: '木一',
|
||||
departmentName: '品牌创意A部',
|
||||
positionTitle: '品牌策划',
|
||||
}],
|
||||
rules: [{
|
||||
id: 'rule-1',
|
||||
departmentPathPrefix: '集团/品牌中心',
|
||||
@ -59,6 +70,7 @@ const config = {
|
||||
isActive: true,
|
||||
status: 'pending',
|
||||
memberCount: 2,
|
||||
members: [],
|
||||
rules: [],
|
||||
effectivePlan: null,
|
||||
draftPlan: null,
|
||||
@ -397,6 +409,25 @@ describe('PerformanceSurveyAdminConfig', () => {
|
||||
})))
|
||||
})
|
||||
|
||||
test('岗位人数可展开查看具体人员并可收起', async () => {
|
||||
const request = createRequest()
|
||||
render(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
|
||||
|
||||
const expandButton = (await screen.findAllByRole('button', { name: '展开人员名单' }))[0]
|
||||
expect(expandButton).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByText('若兰')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
expect(expandButton).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByRole('list', { name: '品牌设计师人员名单' })).toBeInTheDocument()
|
||||
expect(screen.getByText('若兰')).toBeInTheDocument()
|
||||
expect(screen.getByText('品牌创意B部 · 营销策划主管')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起人员名单' }))
|
||||
expect(screen.queryByText('若兰')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('人员批量分配提交最终 REST 请求体', async () => {
|
||||
const request = createRequest()
|
||||
render(<PerformanceSurveyAdminConfig mode="people" request={request} />)
|
||||
|
||||
@ -96,6 +96,13 @@ type PositionGroupRecord = {
|
||||
isActive: boolean
|
||||
status: 'ready' | 'pending' | 'disabled'
|
||||
memberCount: number
|
||||
members?: Array<{
|
||||
id: string
|
||||
displayName: string
|
||||
departmentName?: string | null
|
||||
departmentPath?: string | null
|
||||
positionTitle?: string | null
|
||||
}>
|
||||
rules: Array<{
|
||||
id: string
|
||||
departmentPathPrefix: string
|
||||
@ -837,6 +844,7 @@ function PositionConfigWorkspace({
|
||||
const [editing, setEditing] = useState(Boolean(selected?.draftPlan) || !selected?.effectivePlan)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [newName, setNewName] = useState('')
|
||||
const [expandedMemberGroupIds, setExpandedMemberGroupIds] = useState<Set<string>>(() => new Set())
|
||||
const editorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@ -850,6 +858,14 @@ function PositionConfigWorkspace({
|
||||
}, [onDirtyChange, selected])
|
||||
|
||||
const markDirty = () => onDirtyChange(true)
|
||||
const toggleMemberList = (groupId: string) => {
|
||||
setExpandedMemberGroupIds((current) => {
|
||||
const next = new Set(current)
|
||||
if (next.has(groupId)) next.delete(groupId)
|
||||
else next.add(groupId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const availableTemplateVersions = useMemo(() => data.templates.flatMap((template) => {
|
||||
const availability = getTemplateAvailability(template, month)
|
||||
@ -1092,20 +1108,66 @@ function PositionConfigWorkspace({
|
||||
</div>
|
||||
{data.groups.map((group) => {
|
||||
const groupState = getPositionGroupState(group)
|
||||
const memberListId = `position-group-members-${group.id}`
|
||||
const groupLabelId = `position-group-label-${group.id}`
|
||||
const isMemberListExpanded = expandedMemberGroupIds.has(group.id)
|
||||
return (
|
||||
<button
|
||||
key={group.id}
|
||||
type="button"
|
||||
onClick={() => void selectGroup(group.id)}
|
||||
aria-label={`${group.name},${group.memberCount} 人,${groupState.label}`}
|
||||
className={`grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-b border-[#eeeeee] px-4 py-3 text-left ${selected.id === group.id ? 'bg-[#f1f4ff]' : 'hover:bg-[#fafafa]'}`}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-[13px] font-bold text-[#1a1c1c]">{group.name}</span>
|
||||
<span className="mt-0.5 block text-[11px] text-[#5f5e5e]">{group.memberCount} 人</span>
|
||||
</span>
|
||||
<StatusDot label={groupState.label} tone={groupState.tone} />
|
||||
</button>
|
||||
<div key={group.id} className="border-b border-[#eeeeee]">
|
||||
<div className={`grid grid-cols-[minmax(0,1fr)_auto] items-stretch ${selected.id === group.id ? 'bg-[#f1f4ff]' : 'hover:bg-[#fafafa]'}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void selectGroup(group.id)}
|
||||
aria-label={`${group.name},${group.memberCount} 人,${groupState.label}`}
|
||||
aria-pressed={selected.id === group.id}
|
||||
className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-4 py-3 text-left outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#0050cb]"
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span id={groupLabelId} className="block truncate text-[13px] font-bold text-[#1a1c1c]">{group.name}</span>
|
||||
<span className="mt-0.5 block text-[11px] text-[#5f5e5e]">{group.memberCount} 人</span>
|
||||
</span>
|
||||
<StatusDot label={groupState.label} tone={groupState.tone} />
|
||||
</button>
|
||||
<div className="flex items-center pr-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleMemberList(group.id)}
|
||||
aria-label={`${isMemberListExpanded ? '收起' : '展开'}人员名单`}
|
||||
aria-describedby={groupLabelId}
|
||||
aria-expanded={isMemberListExpanded}
|
||||
aria-controls={memberListId}
|
||||
className="grid min-h-11 min-w-11 place-items-center rounded text-[#5f5e5e] outline-none hover:bg-black/5 hover:text-[#1a1c1c] focus-visible:ring-2 focus-visible:ring-[#0050cb]"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className={`h-4 w-4 transition-transform duration-150 ${isMemberListExpanded ? 'rotate-180' : ''}`}
|
||||
>
|
||||
<path d="m6 8 4 4 4-4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isMemberListExpanded && (
|
||||
<div id={memberListId} className="border-t border-[#eeeeee] bg-[#fafafa] px-4 py-2.5">
|
||||
{(group.members?.length || 0) === 0 ? (
|
||||
<p className="text-[11px] text-[#777575]">当前未配置人员</p>
|
||||
) : (
|
||||
<ul className="max-h-64 divide-y divide-[#e9e9e9] overflow-y-auto" aria-label={`${group.name}人员名单`}>
|
||||
{group.members?.map((member) => {
|
||||
const memberMeta = [member.departmentName, member.positionTitle].filter(Boolean).join(' · ')
|
||||
return (
|
||||
<li key={member.id} className="py-2">
|
||||
<span className="block text-[12px] font-semibold text-[#1a1c1c]">{member.displayName}</span>
|
||||
{memberMeta && <span className="mt-0.5 block truncate text-[10px] text-[#777575]">{memberMeta}</span>}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</aside>
|
||||
|
||||
@ -299,6 +299,54 @@ describe('performance survey position admin', () => {
|
||||
expect(prisma.performanceSurveyEmployeeProfile.findMany).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('岗位管理配置返回每个岗位的具体人员名单', async () => {
|
||||
;(prisma.performanceSurveyPositionGroup.findMany as jest.Mock).mockResolvedValue([{
|
||||
id: 'group-creative',
|
||||
code: 'ai_planning_creative',
|
||||
name: 'AI策划-创意部',
|
||||
sortOrder: 10,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
archivedAt: null,
|
||||
archiveReason: null,
|
||||
rules: [],
|
||||
plans: [],
|
||||
_count: { surveys: 1, manualAssignments: 1 },
|
||||
}])
|
||||
;(prisma.performanceSurveyTemplate.findMany as jest.Mock).mockResolvedValue([])
|
||||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([{
|
||||
id: 'employee-ruolan',
|
||||
displayName: '若兰',
|
||||
departmentName: '品牌创意B部',
|
||||
departmentPath: '集团/品牌中心/品牌创意B部',
|
||||
positionTitle: '营销策划主管',
|
||||
departmentMemberships: [],
|
||||
}])
|
||||
;(prisma.performanceSurveyEmployeePositionAssignment.findMany as jest.Mock).mockResolvedValue([{
|
||||
id: 'assignment-ruolan',
|
||||
employeeProfileId: 'employee-ruolan',
|
||||
assignmentType: 'position',
|
||||
positionGroupId: 'group-creative',
|
||||
status: 'active',
|
||||
startMonth: '2026-07',
|
||||
endMonth: null,
|
||||
createdAt: new Date('2026-07-01T00:00:00.000Z'),
|
||||
}])
|
||||
|
||||
await expect(getPerformanceSurveyPositionAdminConfig('2026-07', 'positions')).resolves.toMatchObject({
|
||||
groups: [{
|
||||
id: 'group-creative',
|
||||
memberCount: 1,
|
||||
members: [{
|
||||
id: 'employee-ruolan',
|
||||
displayName: '若兰',
|
||||
departmentName: '品牌创意B部',
|
||||
positionTitle: '营销策划主管',
|
||||
}],
|
||||
}],
|
||||
})
|
||||
})
|
||||
|
||||
test('复制模板会创建独立模板和完整草稿版本', async () => {
|
||||
const definition = structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[0])
|
||||
;(prisma.performanceSurveyTemplateVersion.findUnique as jest.Mock).mockResolvedValue({
|
||||
|
||||
@ -623,7 +623,7 @@ describe('performance survey service policies', () => {
|
||||
expect(cell.rawMaxTotal).toBe(20)
|
||||
})
|
||||
|
||||
test('公共看板关系查询继续应用领导组织范围及部门和人员筛选', async () => {
|
||||
test('M级系统管理员查看公共看板时不再叠加领导组织范围', async () => {
|
||||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||||
...baseEmployee,
|
||||
levelType: 'M',
|
||||
@ -640,26 +640,14 @@ describe('performance survey service policies', () => {
|
||||
|
||||
const employeeWhere = (prisma.performanceSurveyTarget.findMany as jest.Mock).mock.calls[0][0]
|
||||
.where.employeeProfile.is
|
||||
expect(employeeWhere.AND).toEqual(expect.arrayContaining([
|
||||
{
|
||||
OR: [
|
||||
{ departmentPath: '集团A/品牌中心/策划部' },
|
||||
{ departmentPath: { startsWith: '集团A/品牌中心/策划部/' } },
|
||||
{
|
||||
departmentMemberships: {
|
||||
some: {
|
||||
OR: [
|
||||
{ departmentPath: '集团A/品牌中心/策划部' },
|
||||
{ departmentPath: { startsWith: '集团A/品牌中心/策划部/' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
expect(employeeWhere).toEqual({
|
||||
AND: [
|
||||
expect.objectContaining({ OR: expect.any(Array) }),
|
||||
{ id: 'employee-public' },
|
||||
]))
|
||||
],
|
||||
})
|
||||
expect(JSON.stringify(employeeWhere)).toContain('集团A/品牌中心')
|
||||
expect(JSON.stringify(employeeWhere)).not.toContain('集团A/品牌中心/策划部')
|
||||
expect(getPerformanceSurveyAccountProfileWhere).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@ -1043,9 +1043,15 @@ export async function getPerformanceSurveyPositionAdminConfig(
|
||||
),
|
||||
}))
|
||||
const memberCountByGroup = new Map<string, number>()
|
||||
const membersByGroup = new Map<string, ReturnType<typeof serializePositionEmployee>[]>()
|
||||
for (const item of memberships) {
|
||||
if (item.membership.group) {
|
||||
memberCountByGroup.set(item.membership.group.id, (memberCountByGroup.get(item.membership.group.id) || 0) + 1)
|
||||
const groupId = item.membership.group.id
|
||||
memberCountByGroup.set(groupId, (memberCountByGroup.get(groupId) || 0) + 1)
|
||||
membersByGroup.set(groupId, [
|
||||
...(membersByGroup.get(groupId) || []),
|
||||
serializePositionEmployee(item.employee),
|
||||
])
|
||||
}
|
||||
}
|
||||
const unmatched = memberships
|
||||
@ -1085,6 +1091,7 @@ export async function getPerformanceSurveyPositionAdminConfig(
|
||||
archiveReason: group.archiveReason,
|
||||
status: runtime.status,
|
||||
memberCount: memberCountByGroup.get(group.id) || 0,
|
||||
members: membersByGroup.get(group.id) || [],
|
||||
canHardDelete: !group.isSystem
|
||||
&& group._count.surveys === 0
|
||||
&& group._count.manualAssignments === 0
|
||||
|
||||
@ -1632,7 +1632,7 @@ export async function listPerformanceSurveyDepartments(session: Session | null,
|
||||
getSurveyMonthWhere(getMonthRange(dashboardMonth)!),
|
||||
],
|
||||
},
|
||||
...(ctx.isLeadership
|
||||
...(!ctx.isSystemAdmin && ctx.isLeadership
|
||||
? {
|
||||
employeeProfile: {
|
||||
is: ctx.scopeDepartmentPath
|
||||
@ -2146,7 +2146,7 @@ function getPublicDashboardEmployeeWhere(ctx: UserContext, params?: URLSearchPar
|
||||
const employeeId = clean(params?.get('employeeId'))
|
||||
const and: any[] = []
|
||||
|
||||
if (ctx.isLeadership) {
|
||||
if (!ctx.isSystemAdmin && ctx.isLeadership) {
|
||||
and.push(
|
||||
ctx.scopeDepartmentPath
|
||||
? getScopeDepartmentPathWhere(ctx.scopeDepartmentPath)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user