«Только в работе» выключен по умолчанию, параметр — boolean; ошибка загрузки списка показывается; пункты «⋯» disabled с подсказкой «Будет позже», клик не выбирает строку; «Взять в работу» неактивна по коду CANCELLED. Модалка [35]: опечатки, плейсхолдер, ≤ 1000 символов, «К3», ошибки (403 и прочие) — текст сервера, ФИО без второй точки; после взятия в работу перечитываются список и панель «Организации» («Ответственный»). Боковое меню: thunk списка с объектом, отступ секций. Порядок импортов (biome). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
218 lines
5.8 KiB
TypeScript
218 lines
5.8 KiB
TypeScript
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 { 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 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 && <Logo />}
|
||
<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>
|
||
);
|
||
}
|