Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
227 lines
5.1 KiB
TypeScript
227 lines
5.1 KiB
TypeScript
import {
|
||
BankOutlined,
|
||
ContainerOutlined,
|
||
InboxOutlined,
|
||
LeftOutlined,
|
||
PlusOutlined,
|
||
RightOutlined,
|
||
SolutionOutlined,
|
||
TeamOutlined,
|
||
UserOutlined,
|
||
} from '@ant-design/icons';
|
||
import { Button, Divider, Layout, Tooltip, message } from 'antd';
|
||
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 { quarterLabel } from '../../utils/labels';
|
||
import { NewConsolidationModal } from '../NewConsoleModal/NewConsoleModal';
|
||
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',
|
||
icon: <BankOutlined />,
|
||
label: 'Группы компаний',
|
||
},
|
||
{
|
||
link: '/groups/monozaemchiki',
|
||
icon: <UserOutlined />,
|
||
label: 'Монозаемщики',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
title: 'Справочники',
|
||
key: 'catalogues',
|
||
children: [
|
||
{
|
||
link: '/catalogue/groups',
|
||
icon: <TeamOutlined />,
|
||
label: 'ГК и организации',
|
||
},
|
||
{
|
||
link: '/catalogue/jur_l',
|
||
icon: <SolutionOutlined />,
|
||
label: 'ЮЛ вне ГК',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
title: 'Документы',
|
||
key: 'docs',
|
||
children: [
|
||
{
|
||
link: '/docstorage',
|
||
icon: <InboxOutlined />,
|
||
label: 'Единое хранилище',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
title: 'Система',
|
||
key: 'system',
|
||
children: [
|
||
{
|
||
link: '/logging',
|
||
icon: <ContainerOutlined />,
|
||
label: 'Журнал действий',
|
||
},
|
||
{
|
||
link: '/admin',
|
||
icon: <TeamOutlined />,
|
||
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 [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}>
|
||
<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);
|
||
setModalOpen(false);
|
||
message.success(`Создана консолидация ${values.year}, ${quarterLabel(values.quarter)}`);
|
||
navigate(`/groups/${values.company_group_id}?version=${card.id}`);
|
||
}}
|
||
/>
|
||
</Layout.Sider>
|
||
);
|
||
}
|