AURORA-1690: показ пользователя и ролей по клику на логотип АРМ КА

Closes #184

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-27 01:07:30 +04:00
parent 143a484a72
commit c65f00c837
5 changed files with 86 additions and 2 deletions

View File

@ -0,0 +1,10 @@
## ADDED Requirements
### Requirement: Текущий пользователь и его роли в боковом меню
Клик по логотипу «АРМ КА» в раскрытом боковом меню SHALL раскрывать блок с данными текущего пользователя из `GET /me`:
ФИО, логин (email), роли (коды как отдаёт API, через запятую; без ролей — «не назначена») и филиал. Повторный клик или
клик вне блока SHALL закрывать его. Роли показываются как есть, по ним доступ на фронте не решается.
#### Scenario: Пользователь с ролью
- **WHEN** аналитик с ролью ANALYST кликает по логотипу «АРМ КА»
- **THEN** раскрывается блок с его ФИО, email, ролью ANALYST и филиалом

View File

@ -4,12 +4,12 @@ import { Archive, Building2, ScrollText, UserCog2, Users2 } from 'lucide-react';
import { type ReactNode, memo, useState } from 'react';
import { NavLink, useNavigate } from 'react-router-dom';
import { USERS_ADMIN } from '../../auth/permissions';
import { Logo } from '../../components/Logo';
import { createConsolidation } from '../../services/api/consolidations';
import { getConsolidationsListThunk } from '../../store/slices/consolidations/thunks';
import { useAppDispatch } from '../../store/store';
import { quarterLabel } from '../../utils/labels';
import { NewConsolidationModal } from '../NewConsoleModal/NewConsoleModal';
import { UserMenu } from '../UserMenu/UserMenu';
import styles from './styles.module.css';
type CollapsedIconProps = {
@ -164,7 +164,7 @@ export function Sidebar() {
trigger={null}>
<div className={`${styles.wrapper} ${isCollapsed ? styles.wrapperCollapsed : ''}`}>
<div className={styles.topbar}>
{!isCollapsed && <Logo />}
{!isCollapsed && <UserMenu />}
<button className={styles.collapseBtn} onClick={() => setIsCollapsed(!isCollapsed)}>
{isCollapsed ? <RightOutlined /> : <LeftOutlined />}
</button>

View File

@ -0,0 +1,26 @@
import { Popover } from 'antd';
import { useCurrentUser } from '../../auth/MeContext';
import { Logo } from '../../components/Logo';
import { userInfoRows } from './userInfo';
/** Логотип «АРМ КА»; по клику раскрывается блок с текущим пользователем и его ролями (`GET /me`). */
export function UserMenu({ compact = false }: { compact?: boolean }) {
const me = useCurrentUser();
const content = (
<dl style={{ margin: 0, minWidth: 220 }} data-testid='user-info'>
{userInfoRows(me).map((r) => (
<div key={r.label} style={{ display: 'flex', gap: 8, padding: '2px 0' }}>
<dt style={{ color: 'rgba(55, 65, 81, 0.6)', width: 90, flexShrink: 0 }}>{r.label}</dt>
<dd style={{ margin: 0, fontWeight: 500, wordBreak: 'break-word' }}>{r.value}</dd>
</div>
))}
</dl>
);
return (
<Popover trigger='click' placement='bottomLeft' content={content}>
<button type='button' title='Текущий пользователь' style={{ background: 'none', border: 0, padding: 0, cursor: 'pointer' }}>
<Logo compact={compact} />
</button>
</Popover>
);
}

View File

@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import type { Me } from '../../api/types';
import { userInfoRows } from './userInfo';
const base: Me = {
id: '1',
email: 'analyst@rshb.ru',
full_name: 'Иванов Иван',
regional_office: { id: 'o1', name: 'Центральный офис', is_head_office: true },
business_segment: null,
roles: ['ANALYST'],
permissions: [],
};
describe('userInfoRows', () => {
it('показывает ФИО, логин, роль и филиал', () => {
expect(userInfoRows(base)).toEqual([
{ label: 'Пользователь', value: 'Иванов Иван' },
{ label: 'Логин', value: 'analyst@rshb.ru' },
{ label: 'Роль', value: 'ANALYST' },
{ label: 'Филиал', value: 'Центральный офис' },
]);
});
it('несколько ролей — через запятую, подпись во множественном числе', () => {
const row = userInfoRows({ ...base, roles: ['ANALYST', 'VERIFIER'] }).find((r) => r.value.includes('VERIFIER'));
expect(row).toEqual({ label: 'Роли', value: 'ANALYST, VERIFIER' });
});
it('без ролей пишет «не назначена»', () => {
const row = userInfoRows({ ...base, roles: [] }).find((r) => r.label === 'Роль');
expect(row?.value).toBe('не назначена');
});
});

View File

@ -0,0 +1,14 @@
import type { Me } from '../../api/types';
export type UserInfoRow = { label: string; value: string };
/** Строки блока «Текущий пользователь»: ФИО, email, роли (коды как отдаёт API), филиал. Пустые значения не показываем. */
export function userInfoRows(me: Me): UserInfoRow[] {
const rows: UserInfoRow[] = [
{ label: 'Пользователь', value: me.full_name },
{ label: 'Логин', value: me.email },
{ label: me.roles.length > 1 ? 'Роли' : 'Роль', value: me.roles.length > 0 ? me.roles.join(', ') : 'не назначена' },
{ label: 'Филиал', value: me.regional_office?.name ?? '' },
];
return rows.filter((r) => r.value !== '');
}