Andrey Epifancev d71c46e846 fix(web): список консолидаций и «Взять в работу» по спеке (AURORA-1589, PR #166)
«Только в работе» выключен по умолчанию, параметр — boolean; ошибка загрузки списка
показывается; пункты «⋯» disabled с подсказкой «Будет позже», клик не выбирает строку;
«Взять в работу» неактивна по коду CANCELLED. Модалка [35]: опечатки, плейсхолдер,
≤ 1000 символов, «К3», ошибки (403 и прочие) — текст сервера, ФИО без второй точки;
после взятия в работу перечитываются список и панель «Организации» («Ответственный»).
Боковое меню: thunk списка с объектом, отступ секций. Порядок импортов (biome).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:15:55 +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 { 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>
);
}