Andrey Epifancev 78227b46c0 AURORA-1980: переименование программы и проекта
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:32:42 +04:00

360 lines
11 KiB
JavaScript
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 { Divider, IconButton, Stack, Tooltip, Typography } from '@mui/material';
import { memo, useCallback, useMemo, useState } from 'react';
import { FORM_TYPE_OPTIONS } from '../../../constants/constants';
import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
import { stageColumnsHiddenFromPicker } from '../../Stages/constants';
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
import CollapsibleTree from '../../common/CollapsibleTree';
import SearchComponent from '../../common/SearchComponent';
import DenominationSelect from '../../common/DenominationSelect';
import {
AddLine,
AllStageColumns,
CurrentStageColumns,
EditSvg,
HiddenExpenseItems,
Minus,
Pin,
Plus,
Program,
Project,
RemoveLine,
Search,
VisibleExpenseItems,
} from '../../common/icons/icons';
import { collectLeafColumns } from '../utils/columnUtils';
import CellLegend from './CellLegend';
import { AHR_LIMIT_SHEET } from '../utils/moneyUtils';
import { GroupByObject } from './GroupByObject/GroupByObject';
import { Panel } from './SettingPanel.style';
import ValidationErrorsAlert from './ValidationErrorsAlert';
import ZoomSlider from './ZoomSlider';
const controlSx = {
minHeight: '2.5rem',
flex: '0 0 2.5rem',
};
const searchSx = { height: '2.5rem' };
const searchNavigationSx = {
display: 'inline-flex',
alignItems: 'center',
height: '2rem',
border: '1px solid rgba(0, 0, 0, 0.12)',
borderRadius: '0.4rem',
overflow: 'hidden',
backgroundColor: '#fff',
};
const searchNavigationButtonSx = {
width: '1.65rem',
height: '2rem',
minWidth: '1.65rem',
padding: 0,
borderRadius: 0,
fontSize: '0.95rem',
};
const searchMatchCounterSx = {
minWidth: '2.7rem',
padding: '0 0.3rem',
fontSize: '0.7rem',
lineHeight: 1,
textAlign: 'center',
whiteSpace: 'nowrap',
color: 'rgba(0, 0, 0, 0.65)',
};
const exportButtonSx = { height: '2.5rem', minHeight: '2.5rem' };
const SettingPanel = ({
budgetYear,
selectedColumnId,
columnPinning,
columns,
onPinColumn,
onUnpinColumn,
onToggleColumnVisibility,
onSetColumnsVisibility,
columnVisibility,
columnsCurStage,
onChangeSizeMult,
onGlobalFilterChange,
globalFilter,
onSearchKeyDown,
onSearchNext,
onSearchPrevious,
searchMatchCount,
searchMatchIndex,
sizeMult,
onChangeShowColumnFilters,
showColumnFilters,
onAddRow,
onAddProgram,
onAddProject,
onRenameNode,
canRenameNode,
onDeleteRow,
formId,
sheetName,
direction,
year,
isFormNoCanAddRow,
canModifyRows,
formType,
validationErrors = [],
onNavigateToError,
headerPaths,
expenseItemsVisibilityAction,
onToggleDepthVisibility,
denomination,
onChangeDenomination,
}) => {
const [isExporting, setIsExporting] = useState(false);
const isPinned = useMemo(
() => Boolean(selectedColumnId && columnPinning?.left?.includes(selectedColumnId)),
[columnPinning, selectedColumnId],
);
const selectedColumnIds = useMemo(() => Object.keys(columnVisibility).filter((key) => !columnVisibility[key]), [columnVisibility]);
const handleChangeShowColumnFilters = useCallback(() => {
onChangeShowColumnFilters(!showColumnFilters);
}, [onChangeShowColumnFilters, showColumnFilters]);
const handleExport = useCallback(async () => {
if (isExporting) return;
if (!formId || !sheetName || (formType === 'PROJECT' && !year)) return;
setIsExporting(true);
try {
if (formType === 'PROJECT') {
await exportSheetProject(formId, sheetName, year);
} else {
await exportSheet(formId, sheetName, direction);
}
} finally {
setIsExporting(false);
}
}, [direction, formId, formType, isExporting, sheetName, year]);
const handleTogglePin = useCallback(() => {
if (!selectedColumnId) return;
isPinned ? onUnpinColumn?.(selectedColumnId) : onPinColumn?.(selectedColumnId);
}, [isPinned, onPinColumn, onUnpinColumn, selectedColumnId]);
const leafColumns = useMemo(() => collectLeafColumns({ columns }), [columns]);
const stageColumnIds = useMemo(() => new Set([...stageColumnsHiddenFromPicker, ...(columnsCurStage || [])]), [columnsCurStage]);
const showOnlyCurrentStageColumns = useMemo(() => {
if (!columnsCurStage?.length || !leafColumns.length) return false;
return leafColumns.every((column) =>
stageColumnIds.has(column.id) ? columnVisibility[column.id] !== false : columnVisibility[column.id] === false,
);
}, [columnVisibility, columnsCurStage, leafColumns, stageColumnIds]);
const handleToggleCurrentStageColumns = useCallback(() => {
if (!columnsCurStage?.length) {
console.warn('Нет столбцов для текущего этапа');
return;
}
const visibilityByColumnId = {};
for (const column of leafColumns) {
visibilityByColumnId[column.id] = showOnlyCurrentStageColumns || stageColumnIds.has(column.id);
}
onSetColumnsVisibility(visibilityByColumnId);
}, [columnsCurStage, leafColumns, onSetColumnsVisibility, showOnlyCurrentStageColumns, stageColumnIds]);
const handleSetAllColumnsVisibility = useCallback(
(columnIds, isVisible) => {
const visibilityByColumnId = Object.fromEntries(columnIds.map((columnId) => [columnId, isVisible]));
onSetColumnsVisibility(visibilityByColumnId);
},
[onSetColumnsVisibility],
);
return (
<>
<Panel data-pin-panel>
<Stack direction='row' sx={{ gap: '1.25rem', alignItems: 'center' }}>
{validationErrors.length > 0 && (
<>
<ValidationErrorsAlert errors={validationErrors} onNavigateToError={onNavigateToError} headerPaths={headerPaths} />
<Divider orientation='vertical' flexItem />
</>
)}
<GroupByObject title='Отображение'>
{/* <ColorPickerButton onColorApply={addColorForCells} /> */}
<Tooltip title='Закрепить область'>
<IconButton
variant='outlined'
sx={controlSx}
data-active={isPinned}
disabled={!selectedColumnId}
onMouseDown={(e) => e.stopPropagation()}
onClick={handleTogglePin}>
<Pin />
</IconButton>
</Tooltip>
{sheetName !== AHR_LIMIT_SHEET && (
<>
<DenominationSelect value={denomination} onChange={onChangeDenomination} minWidth={130} />
<Typography variant='caption' color='text.secondary' sx={{ whiteSpace: 'nowrap' }}>
Ввод сумм — в тыс. руб.
</Typography>
</>
)}
</GroupByObject>
<Divider orientation='vertical' flexItem />
<GroupByObject title='Строки'>
{!isFormNoCanAddRow && canModifyRows && (
<>
<Tooltip title='Добавить строку'>
<IconButton onClick={onAddRow} variant='outlined' sx={controlSx}>
<Plus />
<AddLine />
</IconButton>
</Tooltip>
<Tooltip title='Удалить строку'>
<IconButton onClick={onDeleteRow} variant='outlined' sx={controlSx}>
<Minus />
<RemoveLine />
</IconButton>
</Tooltip>
{/* проверяем на 4 форму */}
{formType === FORM_TYPE_OPTIONS[2] && (
<>
<Tooltip title='Добавить программу'>
<IconButton onClick={onAddProgram} variant='outlined' sx={controlSx}>
<Program />
</IconButton>
</Tooltip>
<Tooltip title='Добавить проект'>
<IconButton onClick={onAddProject} variant='outlined' sx={controlSx}>
<Project />
</IconButton>
</Tooltip>
<Tooltip title='Переименовать'>
<span>
<IconButton onClick={onRenameNode} disabled={!canRenameNode} variant='outlined' sx={controlSx}>
<EditSvg />
</IconButton>
</span>
</Tooltip>
</>
)}
</>
)}
<Tooltip
title={
expenseItemsVisibilityAction === 'show'
? 'Показать следующий уровень статей расходов'
: 'Скрыть нижний уровень статей расходов'
}>
<IconButton
onClick={onToggleDepthVisibility}
variant='outlined'
data-active={expenseItemsVisibilityAction === 'show'}
sx={controlSx}>
{expenseItemsVisibilityAction === 'show' ? <VisibleExpenseItems /> : <HiddenExpenseItems />}
</IconButton>
</Tooltip>
</GroupByObject>
<Divider orientation='vertical' flexItem />
<GroupByObject title='Столбцы'>
{columns ? (
<CollapsibleTree
budgetYear={budgetYear}
columnTree={columns}
onSelectNode={onToggleColumnVisibility}
onSelectAllNodes={handleSetAllColumnsVisibility}
selectedIds={selectedColumnIds}
/>
) : (
''
)}
<Tooltip title={showOnlyCurrentStageColumns ? 'Показать все столбцы' : 'Скрыть все столбцы кроме текущих'}>
<IconButton
variant='outlined'
sx={controlSx}
color='success'
data-active={showOnlyCurrentStageColumns}
onClick={handleToggleCurrentStageColumns}
disabled={!columnsCurStage || columnsCurStage.length === 0}>
{showOnlyCurrentStageColumns ? <AllStageColumns /> : <CurrentStageColumns />}
</IconButton>
</Tooltip>
</GroupByObject>
<Divider orientation='vertical' flexItem />
<GroupByObject title='Масштаб'>
<ZoomSlider onChange={onChangeSizeMult} curScale={sizeMult} />
</GroupByObject>
<Divider orientation='vertical' flexItem />
<GroupByObject title='Поиск'>
<SearchComponent
value={globalFilter}
onChange={onGlobalFilterChange}
onKeyDown={onSearchKeyDown}
height='2.5rem'
sx={searchSx}
/>
<span style={searchNavigationSx}>
<Tooltip title='Предыдущий результат (Shift+Enter)'>
<span>
<IconButton
onClick={onSearchPrevious}
disabled={!searchMatchCount}
sx={searchNavigationButtonSx}
aria-label='Предыдущий результат поиска'>
↑
</IconButton>
</span>
</Tooltip>
<span style={searchMatchCounterSx}>
{searchMatchCount ? `${Math.max(searchMatchIndex + 1, 1)}/${searchMatchCount}` : '0/0'}
</span>
<Tooltip title='Следующий результат (Enter)'>
<span>
<IconButton
onClick={onSearchNext}
disabled={!searchMatchCount}
sx={searchNavigationButtonSx}
aria-label='Следующий результат поиска'>
↓
</IconButton>
</span>
</Tooltip>
</span>
<Tooltip title='Поиск по колонкам'>
<IconButton
variant='outlined'
color='success'
data-active={showColumnFilters}
onClick={handleChangeShowColumnFilters}
sx={controlSx}>
<Search />
</IconButton>
</Tooltip>
</GroupByObject>
<Divider orientation='vertical' flexItem />
<GroupByObject title='Экспорт'>
<ExportDefaultButton
onClick={handleExport}
disabled={!formId || !sheetName || isExporting}
text={isExporting ? 'Экспорт...' : 'Экспорт'}
sx={exportButtonSx}
/>
</GroupByObject>
</Stack>
<Stack direction='row' sx={{ alignItems: 'center', gap: 0.5 }}>
<CellLegend />
</Stack>
</Panel>
</>
);
};
export default memo(SettingPanel);