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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 01:07:30 +04:00

218 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Divider, Layout, Tooltip, message } from 'antd';
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 { 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 = {
icon: ReactNode;
onClick?: () => void;
title: string;
to: string | null;
cn?: string;
};
function CollapsedIcon({ icon, onClick, title, to, cn = '' }: CollapsedIconProps) {
if (to === null) {
return (
<Tooltip placement='right' title={title}>
<button type='button' onClick={onClick} className={cn || `${styles.iconBtn} ${styles.iconBtnNav}`}>
{icon}
</button>
</Tooltip>
);
}
return (
<Tooltip placement='right' title={title}>
<NavLink to={to} className={({ isActive }) => cn || `${styles.iconBtn} ${isActive ? styles.iconBtnActive : styles.iconBtnNav}`}>
{icon}
</NavLink>
</Tooltip>
);
}
type NavItem = {
link: string;
label: string;
icon?: ReactNode;
/** Право из `/me`, без которого пункт не показывается; нет — пункт виден всем. */
permission?: string;
};
type Group = {
title: string;
key: string;
children: NavItem[];
};
const config: Group[] = [
{
title: 'Отчётность',
key: 'reporting',
children: [
{
link: '/groups',
label: 'Группы компаний',
},
{
link: '/groups/monozaemchiki',
label: 'Монозаемщики',
},
],
},
{
title: 'Справочники',
key: 'catalogues',
children: [
{
link: '/catalogue/groups',
icon: <Building2 size={14} />,
label: 'ГК и организации',
},
{
link: '/catalogue/jur_l',
icon: <Users2 size={14} />,
label: 'ЮЛ вне ГК',
},
],
},
{
title: 'Документы',
key: 'docs',
children: [
{
link: '/docstorage',
icon: <Archive size={14} />,
label: 'Единое хранилище',
},
],
},
{
title: 'Система',
key: 'system',
children: [
{
link: '/logging',
icon: <ScrollText size={14} />,
label: 'Журнал действий',
},
{
link: '/admin',
icon: <UserCog2 size={14} />,
label: 'Пользователи и роли',
permission: USERS_ADMIN,
},
],
},
];
function NavLinkStyled({ to, children }: { to: string; children: ReactNode }) {
return (
<NavLink
className={({ isActive }) => {
return `${styles.linkBase} ${isActive ? styles.linkActive : ''}`;
}}
to={to}
end>
{children}
</NavLink>
);
}
const Section = memo(function Section({ isCollapsed, group }: { isCollapsed: boolean; group: Group }) {
return !isCollapsed ? (
<div className={styles.section}>
<div className={styles.sectionTitle}>{group.title}</div>
{group.children.map((i) => (
<NavLinkStyled key={i.link} to={i.link}>
{i.icon}
{i.label}
</NavLinkStyled>
))}
<Divider size='small' />
</div>
) : (
<>
{group.children.map((i) => (
<CollapsedIcon key={i.link} icon={i.icon} to={i.link} title={i.label} />
))}
</>
);
});
export function Sidebar() {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [modalOpen, setModalOpen] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<Layout.Sider
collapsed={isCollapsed}
style={{ height: 'auto', margin: '10px', borderRadius: 10 }}
width={210}
collapsedWidth={64}
collapsible
trigger={null}>
<div className={`${styles.wrapper} ${isCollapsed ? styles.wrapperCollapsed : ''}`}>
<div className={styles.topbar}>
{!isCollapsed && <UserMenu />}
<button className={styles.collapseBtn} onClick={() => setIsCollapsed(!isCollapsed)}>
{isCollapsed ? <RightOutlined /> : <LeftOutlined />}
</button>
</div>
{isCollapsed ? (
<div>
<CollapsedIcon
icon={<PlusOutlined />}
cn={`${styles.iconBtn} ${styles.iconBtnAction}`}
to={null}
title='Новая консолидация'
onClick={() => {
setModalOpen(true);
}}
/>
</div>
) : (
<>
<Button
type='default'
onClick={() => {
setModalOpen(true);
}}>
<PlusOutlined />
Новая консолидация
</Button>
</>
)}
{config.map((g) => (
<Section key={g.key} group={g} isCollapsed={isCollapsed} />
))}
</div>
<NewConsolidationModal
open={modalOpen}
onCancel={() => {
setModalOpen(false);
}}
onOk={async (values) => {
const card = await createConsolidation(values);
// перечитываем список: на странице группы он сам не обновится, и новая консолидация не станет текущей
// in_work_only: false — переход ниже открывает список без ?in_work_only (флажок выключен)
await dispatch(getConsolidationsListThunk({ company_group_id: values.company_group_id, in_work_only: false }));
setModalOpen(false);
message.success(`Создана консолидация ${values.year}, ${quarterLabel(values.quarter)}`);
navigate(`/groups/${values.company_group_id}?version=${card.id}`);
}}
/>
</Layout.Sider>
);
}