From c65f00c83703c6fa0e2ae7e27b168254e27b622a Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 27 Oct 2026 01:07:30 +0400 Subject: [PATCH] =?UTF-8?q?AURORA-1690:=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B8=20=D1=80=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=BB=D0=B8=D0=BA=D1=83=20=D0=BD=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=D1=82=D0=B8=D0=BF=20=D0=90=D0=A0=D0=9C=20?= =?UTF-8?q?=D0=9A=D0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #184 Co-Authored-By: Claude Opus 5.5 --- .../184-user-role/specs/app-shell-ui/spec.md | 10 ++++++ web/src/widgets/Sidebar/Sidebar.tsx | 4 +-- web/src/widgets/UserMenu/UserMenu.tsx | 26 ++++++++++++++ web/src/widgets/UserMenu/userInfo.test.ts | 34 +++++++++++++++++++ web/src/widgets/UserMenu/userInfo.ts | 14 ++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 openspec/changes/184-user-role/specs/app-shell-ui/spec.md create mode 100644 web/src/widgets/UserMenu/UserMenu.tsx create mode 100644 web/src/widgets/UserMenu/userInfo.test.ts create mode 100644 web/src/widgets/UserMenu/userInfo.ts diff --git a/openspec/changes/184-user-role/specs/app-shell-ui/spec.md b/openspec/changes/184-user-role/specs/app-shell-ui/spec.md new file mode 100644 index 0000000..5404af6 --- /dev/null +++ b/openspec/changes/184-user-role/specs/app-shell-ui/spec.md @@ -0,0 +1,10 @@ +## ADDED Requirements + +### Requirement: Текущий пользователь и его роли в боковом меню +Клик по логотипу «АРМ КА» в раскрытом боковом меню SHALL раскрывать блок с данными текущего пользователя из `GET /me`: +ФИО, логин (email), роли (коды как отдаёт API, через запятую; без ролей — «не назначена») и филиал. Повторный клик или +клик вне блока SHALL закрывать его. Роли показываются как есть, по ним доступ на фронте не решается. + +#### Scenario: Пользователь с ролью +- **WHEN** аналитик с ролью ANALYST кликает по логотипу «АРМ КА» +- **THEN** раскрывается блок с его ФИО, email, ролью ANALYST и филиалом diff --git a/web/src/widgets/Sidebar/Sidebar.tsx b/web/src/widgets/Sidebar/Sidebar.tsx index 5f8a7f0..db12ff7 100644 --- a/web/src/widgets/Sidebar/Sidebar.tsx +++ b/web/src/widgets/Sidebar/Sidebar.tsx @@ -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}>
- {!isCollapsed && } + {!isCollapsed && } diff --git a/web/src/widgets/UserMenu/UserMenu.tsx b/web/src/widgets/UserMenu/UserMenu.tsx new file mode 100644 index 0000000..a7832de --- /dev/null +++ b/web/src/widgets/UserMenu/UserMenu.tsx @@ -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 = ( +
+ {userInfoRows(me).map((r) => ( +
+
{r.label}
+
{r.value}
+
+ ))} +
+ ); + return ( + + + + ); +} diff --git a/web/src/widgets/UserMenu/userInfo.test.ts b/web/src/widgets/UserMenu/userInfo.test.ts new file mode 100644 index 0000000..175d921 --- /dev/null +++ b/web/src/widgets/UserMenu/userInfo.test.ts @@ -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('не назначена'); + }); +}); diff --git a/web/src/widgets/UserMenu/userInfo.ts b/web/src/widgets/UserMenu/userInfo.ts new file mode 100644 index 0000000..8893196 --- /dev/null +++ b/web/src/widgets/UserMenu/userInfo.ts @@ -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 !== ''); +}