добавление селекта для изменения разрядности в твблицах
This commit is contained in:
parent
4e002150d3
commit
6c3cd5b2e0
@ -31,18 +31,18 @@ export default function ExportJobs() {
|
|||||||
if (stopped || wasHandled(job.id) || processing.has(job.id)) return;
|
if (stopped || wasHandled(job.id) || processing.has(job.id)) return;
|
||||||
const toastId = `export:${user.id}:${job.id}`;
|
const toastId = `export:${user.id}:${job.id}`;
|
||||||
if (['queued', 'processing'].includes(job.status)) {
|
if (['queued', 'processing'].includes(job.status)) {
|
||||||
showLoading(toastId, 'Идёт формирование Excel…');
|
showLoading(toastId, 'Идёт формирование файла…');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
processing.add(job.id);
|
processing.add(job.id);
|
||||||
showLoading(toastId, 'Скачивание Excel…');
|
showLoading(toastId, 'Скачивание файла…');
|
||||||
try {
|
try {
|
||||||
if (job.status === 'failed') throw new Error(job.error || 'Ошибка экспорта');
|
if (job.status === 'failed') throw new Error(job.error || 'Ошибка экспорта');
|
||||||
const response = await downloadExport(job.id);
|
const response = await downloadExport(job.id);
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
await downloadFile(response, getExportFilename(job), 'xlsx');
|
await downloadFile(response, getExportFilename(job), 'xlsx');
|
||||||
markHandled(job.id);
|
markHandled(job.id);
|
||||||
toast.update(toastId, { render: 'Excel сформирован, скачивание началось', type: 'success', isLoading: false, autoClose: 3000 });
|
toast.update(toastId, { render: 'Файл сформирован, скачивание началось', type: 'success', isLoading: false, autoClose: 3000 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
if (job.status === 'failed' || error?.response?.status === 410) markHandled(job.id);
|
if (job.status === 'failed' || error?.response?.status === 410) markHandled(job.id);
|
||||||
|
|||||||
@ -137,7 +137,7 @@ const createHighlightedContent = (originalValue, displayValue, searchQueries) =>
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const CellComponent = ({ cell, row, column, onClick, globalFilter, columnFilter, backgroundColor, isEditable, isInvalid, vspOptions }) => {
|
const CellComponent = ({ cell, row, column, onClick, globalFilter, columnFilter, backgroundColor, isEditable, isInvalid, vspOptions, displayDivisor = 1 }) => {
|
||||||
const cellKey = `${row.id}_${column.id}`;
|
const cellKey = `${row.id}_${column.id}`;
|
||||||
const isLocked = useCellLock(cellKey);
|
const isLocked = useCellLock(cellKey);
|
||||||
|
|
||||||
@ -149,7 +149,7 @@ const CellComponent = ({ cell, row, column, onClick, globalFilter, columnFilter,
|
|||||||
const isNum = value !== null && value !== undefined && value !== '' && !Number.isNaN(Number(value)) && !TEXT_COLUMN_IDS.has(column.id);
|
const isNum = value !== null && value !== undefined && value !== '' && !Number.isNaN(Number(value)) && !TEXT_COLUMN_IDS.has(column.id);
|
||||||
let display = String(value || '');
|
let display = String(value || '');
|
||||||
if (isNum) {
|
if (isNum) {
|
||||||
display = formatNumber(display, false);
|
display = formatNumber(Number(display) / displayDivisor, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVspDropdown && value != null) {
|
if (isVspDropdown && value != null) {
|
||||||
@ -160,7 +160,7 @@ const CellComponent = ({ cell, row, column, onClick, globalFilter, columnFilter,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { rawValue: value, displayValue: display, isNumeric: isNum };
|
return { rawValue: value, displayValue: display, isNumeric: isNum };
|
||||||
}, [cell, column.id, isVspDropdown, vspOptions]);
|
}, [cell, column.id, isVspDropdown, vspOptions, displayDivisor]);
|
||||||
|
|
||||||
const highlightedContent = useMemo(() => {
|
const highlightedContent = useMemo(() => {
|
||||||
const searchQueries = [globalFilter, columnFilter];
|
const searchQueries = [globalFilter, columnFilter];
|
||||||
@ -220,7 +220,8 @@ const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
|
|||||||
prevProps.backgroundColor === nextProps.backgroundColor &&
|
prevProps.backgroundColor === nextProps.backgroundColor &&
|
||||||
prevProps.isInvalid === nextProps.isInvalid &&
|
prevProps.isInvalid === nextProps.isInvalid &&
|
||||||
prevProps.isEditable === nextProps.isEditable &&
|
prevProps.isEditable === nextProps.isEditable &&
|
||||||
prevProps.vspOptions === nextProps.vspOptions
|
prevProps.vspOptions === nextProps.vspOptions &&
|
||||||
|
prevProps.displayDivisor === nextProps.displayDivisor
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { DictVspApi } from '../../api/dict-vsp';
|
|||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
import { ROLES_NAME_ID } from '../../constants/constants';
|
import { ROLES_NAME_ID } from '../../constants/constants';
|
||||||
import { FormsNoCanAddRow } from './constants/formConfig';
|
import { FormsNoCanAddRow } from './constants/formConfig';
|
||||||
|
import { loadFromStorage, saveToStorage } from './utils/localStorageUtils';
|
||||||
|
|
||||||
const CONFIG_CACHE = new Map();
|
const CONFIG_CACHE = new Map();
|
||||||
|
|
||||||
@ -161,6 +162,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
const columnVirtualizerRef = useRef(null);
|
const columnVirtualizerRef = useRef(null);
|
||||||
|
|
||||||
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale();
|
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale();
|
||||||
|
const denominationStorageKey = `${formId}_${formType}_${sheetName}_${direction}_denomination`;
|
||||||
|
const [denominationByTable, setDenominationByTable] = useState({});
|
||||||
|
const denomination = denominationByTable[denominationStorageKey] ?? loadFromStorage(denominationStorageKey, 'rub');
|
||||||
|
const setTableDenomination = useCallback((value) => {
|
||||||
|
saveToStorage(denominationStorageKey, value);
|
||||||
|
setDenominationByTable((current) => ({ ...current, [denominationStorageKey]: value }));
|
||||||
|
}, [denominationStorageKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rowVirtualizerRef.current?.measure?.();
|
rowVirtualizerRef.current?.measure?.();
|
||||||
@ -261,12 +269,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
onCellNumberClick: handleClickRowCell,
|
onCellNumberClick: handleClickRowCell,
|
||||||
onCellUpdateError: handleCellUpdateError,
|
onCellUpdateError: handleCellUpdateError,
|
||||||
isCellInvalid,
|
isCellInvalid,
|
||||||
|
denomination,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating columns:', error);
|
console.error('Error creating columns:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}, [columnsConfig, columnsCurStage, userRoleId, handleCellUpdateError, handleClickRowCell, handleUpdateCell, isCellInvalid]);
|
}, [columnsConfig, columnsCurStage, userRoleId, handleCellUpdateError, handleClickRowCell, handleUpdateCell, isCellInvalid, denomination]);
|
||||||
|
|
||||||
// MRT запоминает индексы pinned-колонок до завершения асинхронной
|
// MRT запоминает индексы pinned-колонок до завершения асинхронной
|
||||||
// загрузки конфигурации. Новая ссылка на columnPinning заставляет
|
// загрузки конфигурации. Новая ссылка на columnPinning заставляет
|
||||||
@ -774,6 +783,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
columnVisibility={columnVisibility}
|
columnVisibility={columnVisibility}
|
||||||
columnsCurStage={columnsCurStage}
|
columnsCurStage={columnsCurStage}
|
||||||
onChangeSizeMult={setSizeMult}
|
onChangeSizeMult={setSizeMult}
|
||||||
|
denomination={denomination}
|
||||||
|
onChangeDenomination={setTableDenomination}
|
||||||
onGlobalFilterChange={handleGlobalFilterChange}
|
onGlobalFilterChange={handleGlobalFilterChange}
|
||||||
globalFilter={globalFilter}
|
globalFilter={globalFilter}
|
||||||
onSearchKeyDown={handleSearchKeyDown}
|
onSearchKeyDown={handleSearchKeyDown}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { stageColumnsHiddenFromPicker } from '../../Stages/constants';
|
|||||||
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
||||||
import CollapsibleTree from '../../common/CollapsibleTree';
|
import CollapsibleTree from '../../common/CollapsibleTree';
|
||||||
import SearchComponent from '../../common/SearchComponent';
|
import SearchComponent from '../../common/SearchComponent';
|
||||||
|
import DenominationSelect from '../../common/DenominationSelect';
|
||||||
import {
|
import {
|
||||||
AddLine,
|
AddLine,
|
||||||
AllStageColumns,
|
AllStageColumns,
|
||||||
@ -97,6 +98,8 @@ const SettingPanel = ({
|
|||||||
onNavigateToColumn,
|
onNavigateToColumn,
|
||||||
expenseItemsVisibilityAction,
|
expenseItemsVisibilityAction,
|
||||||
onToggleDepthVisibility,
|
onToggleDepthVisibility,
|
||||||
|
denomination,
|
||||||
|
onChangeDenomination,
|
||||||
}) => {
|
}) => {
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
const isPinned = useMemo(
|
const isPinned = useMemo(
|
||||||
@ -176,6 +179,7 @@ const SettingPanel = ({
|
|||||||
<Pin />
|
<Pin />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<DenominationSelect value={denomination} onChange={onChangeDenomination} minWidth={130} />
|
||||||
</GroupByObject>
|
</GroupByObject>
|
||||||
|
|
||||||
<Divider orientation='vertical' flexItem />
|
<Divider orientation='vertical' flexItem />
|
||||||
|
|||||||
@ -118,7 +118,9 @@ export const getTableColumns = ({
|
|||||||
onCellUpdateError,
|
onCellUpdateError,
|
||||||
onCellNumberClick,
|
onCellNumberClick,
|
||||||
isCellInvalid,
|
isCellInvalid,
|
||||||
|
denomination = 'rub',
|
||||||
}) => {
|
}) => {
|
||||||
|
const displayDivisor = denomination === 'billion' ? 1000000000 : denomination === 'million' ? 1000000 : denomination === 'thousand' ? 1000 : 1;
|
||||||
const columnColors = { ...columnsConfig.colors };
|
const columnColors = { ...columnsConfig.colors };
|
||||||
const columns = structuredClone(columnsConfig.columns);
|
const columns = structuredClone(columnsConfig.columns);
|
||||||
|
|
||||||
@ -153,6 +155,7 @@ export const getTableColumns = ({
|
|||||||
cached.globalFilter === currentGlobalFilter &&
|
cached.globalFilter === currentGlobalFilter &&
|
||||||
cached.columnFilter === currentColumnFilter &&
|
cached.columnFilter === currentColumnFilter &&
|
||||||
cached.value === value &&
|
cached.value === value &&
|
||||||
|
cached.displayDivisor === displayDivisor &&
|
||||||
cached.isInvalid === isInvalid &&
|
cached.isInvalid === isInvalid &&
|
||||||
cached.backgroundColor === backgroundColor
|
cached.backgroundColor === backgroundColor
|
||||||
) {
|
) {
|
||||||
@ -170,7 +173,8 @@ export const getTableColumns = ({
|
|||||||
backgroundColor,
|
backgroundColor,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
value,
|
value,
|
||||||
_hash: `${globalFilter}_${columnFilter}_${key}_${value}_${isInvalid}_${backgroundColor}`,
|
displayDivisor,
|
||||||
|
_hash: `${globalFilter}_${columnFilter}_${key}_${value}_${isInvalid}_${backgroundColor}_${displayDivisor}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
rowCache[key] = props;
|
rowCache[key] = props;
|
||||||
|
|||||||
14
web/src/components/common/DenominationSelect.jsx
Normal file
14
web/src/components/common/DenominationSelect.jsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { FormControl, MenuItem, Select } from '@mui/material';
|
||||||
|
|
||||||
|
const DenominationSelect = ({ value, onChange, minWidth = 140 }) => (
|
||||||
|
<FormControl size='small' sx={{ minWidth }}>
|
||||||
|
<Select value={value} onChange={(event) => onChange(event.target.value)} aria-label='Разрядность значений'>
|
||||||
|
<MenuItem value='rub'>в руб.</MenuItem>
|
||||||
|
<MenuItem value='thousand'>в тыс. руб.</MenuItem>
|
||||||
|
<MenuItem value='million'>в млн. руб.</MenuItem>
|
||||||
|
<MenuItem value='billion'>в млрд. руб.</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DenominationSelect;
|
||||||
@ -6,7 +6,7 @@ import { ProjectsApi } from '../../api/projects';
|
|||||||
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
||||||
import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
||||||
import { PROJECT_TYPES } from '../../constants/constants';
|
import { PROJECT_TYPES } from '../../constants/constants';
|
||||||
import { SummaryTable, buildSummaryTree, countSummaryTree, filterSummaryTree } from '../SvodPage/SummaryTable';
|
import { SummaryDenominationControl, SummaryTable, buildSummaryTree, countSummaryTree, filterSummaryTree } from '../SvodPage/SummaryTable';
|
||||||
import { SvodFilters, useSummaryOrganizations } from '../SvodPage/SvodFilters';
|
import { SvodFilters, useSummaryOrganizations } from '../SvodPage/SvodFilters';
|
||||||
|
|
||||||
const SHEETS = [
|
const SHEETS = [
|
||||||
@ -234,12 +234,13 @@ export default function ProjectSummariesPage() {
|
|||||||
onChange={updateFilters}
|
onChange={updateFilters}
|
||||||
onReset={() => setProjectType('')}
|
onReset={() => setProjectType('')}
|
||||||
searchPlaceholder='Код или статья расходов'
|
searchPlaceholder='Код или статья расходов'
|
||||||
additionalFilters={
|
additionalFilters={<>
|
||||||
<TextField select size='small' label='Тип проекта' value={projectType} onChange={(event) => setProjectType(event.target.value)} sx={{ minWidth: '13rem' }}>
|
<TextField select size='small' label='Тип проекта' value={projectType} onChange={(event) => setProjectType(event.target.value)} sx={{ minWidth: '13rem' }}>
|
||||||
<MenuItem value=''>Все типы</MenuItem>
|
<MenuItem value=''>Все типы</MenuItem>
|
||||||
{PROJECT_TYPES.map((type) => <MenuItem key={type} value={type}>{type}</MenuItem>)}
|
{PROJECT_TYPES.map((type) => <MenuItem key={type} value={type}>{type}</MenuItem>)}
|
||||||
</TextField>
|
</TextField>
|
||||||
}
|
<SummaryDenominationControl tableKey={`project_svod_${sheet}`} />
|
||||||
|
</>}
|
||||||
>
|
>
|
||||||
{!loading && filteredCount !== rows.length && (
|
{!loading && filteredCount !== rows.length && (
|
||||||
<Typography variant='caption' color='text.secondary'>
|
<Typography variant='caption' color='text.secondary'>
|
||||||
@ -254,7 +255,7 @@ export default function ProjectSummariesPage() {
|
|||||||
{error ? (
|
{error ? (
|
||||||
<Alert severity='error' action={<Button color='inherit' size='small' onClick={refreshSummary}>Повторить</Button>}>{error}</Alert>
|
<Alert severity='error' action={<Button color='inherit' size='small' onClick={refreshSummary}>Повторить</Button>}>{error}</Alert>
|
||||||
) : (
|
) : (
|
||||||
<SummaryTable columns={columns} data={filteredRows} loading={loading} colorBySource />
|
<SummaryTable columns={columns} data={filteredRows} loading={loading} colorBySource tableKey={`project_svod_${sheet}`} />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
||||||
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
|
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
|
||||||
|
import DenominationSelect from '../../components/common/DenominationSelect';
|
||||||
import { useSvod } from '../SvodPage/SvodContext';
|
import { useSvod } from '../SvodPage/SvodContext';
|
||||||
import { SvodFilters } from '../SvodPage/SvodFilters';
|
import { SvodFilters } from '../SvodPage/SvodFilters';
|
||||||
|
|
||||||
@ -32,11 +33,11 @@ const COLUMN_BAND_COLORS = {
|
|||||||
|
|
||||||
const valueAt = (row, path) => path.reduce((value, key) => value?.[key], row.data);
|
const valueAt = (row, path) => path.reduce((value, key) => value?.[key], row.data);
|
||||||
|
|
||||||
const formatValue = (value) => {
|
const formatValue = (value, divisor = 1) => {
|
||||||
if (value === null || value === undefined || value === '') return '—';
|
if (value === null || value === undefined || value === '') return '—';
|
||||||
const numericValue = typeof value === 'number' ? value : Number(value);
|
const numericValue = typeof value === 'number' ? value : Number(value);
|
||||||
if (numericValue === 0) return '-';
|
if (numericValue === 0) return '-';
|
||||||
return Number.isNaN(numericValue) ? value : numberFormatter.format(numericValue);
|
return Number.isNaN(numericValue) ? value : numberFormatter.format(numericValue / divisor);
|
||||||
};
|
};
|
||||||
|
|
||||||
const textColumn = (id, header, path, size = 10, options = {}) => ({
|
const textColumn = (id, header, path, size = 10, options = {}) => ({
|
||||||
@ -342,6 +343,13 @@ const getColumnConfig = (sheet) => {
|
|||||||
return COLUMN_CONFIG_CACHE.get(sheet);
|
return COLUMN_CONFIG_CACHE.get(sheet);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scaleNumericColumns = (columns, divisor) => columns.map((column) => {
|
||||||
|
const isText = ['name', 'org_name', 'smeta_type', 'smeta_direction', 'section_code', 'contract_number'].includes(column.id);
|
||||||
|
const children = column.columns ? scaleNumericColumns(column.columns, divisor) : undefined;
|
||||||
|
if (isText || children) return { ...column, ...(children ? { columns: children } : {}) };
|
||||||
|
return { ...column, Cell: ({ cell }) => formatValue(cell.getValue(), divisor) };
|
||||||
|
});
|
||||||
|
|
||||||
const matchesSearch = (row, query) => {
|
const matchesSearch = (row, query) => {
|
||||||
const searchableData = {
|
const searchableData = {
|
||||||
...row.data?.header,
|
...row.data?.header,
|
||||||
@ -359,6 +367,15 @@ export default function Svod2Page() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod();
|
const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod();
|
||||||
const [sheet, setSheet] = useState('GO');
|
const [sheet, setSheet] = useState('GO');
|
||||||
|
const [denominations, setDenominations] = useState({});
|
||||||
|
const denominationStorageKey = `svod2_${sheet}_denomination`;
|
||||||
|
const denomination = denominations[denominationStorageKey] ?? (() => { try { return localStorage.getItem(denominationStorageKey) || 'rub'; } catch { return 'rub'; } })();
|
||||||
|
const divisor = denomination === 'billion' ? 1000000000 : denomination === 'million' ? 1000000 : denomination === 'thousand' ? 1000 : 1;
|
||||||
|
|
||||||
|
const handleDenominationChange = (value) => {
|
||||||
|
setDenominations((current) => ({ ...current, [denominationStorageKey]: value }));
|
||||||
|
try { localStorage.setItem(denominationStorageKey, value); } catch { /* storage may be unavailable */ }
|
||||||
|
};
|
||||||
const [tableStateBySheet, setTableStateBySheet] = useState(createInitialTableState);
|
const [tableStateBySheet, setTableStateBySheet] = useState(createInitialTableState);
|
||||||
const [columnFiltersBySheet, setColumnFiltersBySheet] = useState(() => Object.fromEntries(SHEETS.map(({ value }) => [value, []])));
|
const [columnFiltersBySheet, setColumnFiltersBySheet] = useState(() => Object.fromEntries(SHEETS.map(({ value }) => [value, []])));
|
||||||
const latestSummaryRequestRef = useRef(Object.fromEntries(SHEETS.map(({ value }) => [value, 0])));
|
const latestSummaryRequestRef = useRef(Object.fromEntries(SHEETS.map(({ value }) => [value, 0])));
|
||||||
@ -449,7 +466,8 @@ export default function Svod2Page() {
|
|||||||
const normalizedSearch = search.trim().toLowerCase();
|
const normalizedSearch = search.trim().toLowerCase();
|
||||||
return normalizedSearch ? rows.filter((row) => matchesSearch(row, normalizedSearch)) : rows;
|
return normalizedSearch ? rows.filter((row) => matchesSearch(row, normalizedSearch)) : rows;
|
||||||
}, [hasCurrentData, rows, search]);
|
}, [hasCurrentData, rows, search]);
|
||||||
const { columns, columnBands, columnGroupEnds } = useMemo(() => getColumnConfig(sheet), [sheet]);
|
const { columns: rawColumns, columnBands, columnGroupEnds } = useMemo(() => getColumnConfig(sheet), [sheet]);
|
||||||
|
const columns = useMemo(() => scaleNumericColumns(rawColumns, divisor), [rawColumns, divisor]);
|
||||||
const table = useMaterialReactTable({
|
const table = useMaterialReactTable({
|
||||||
columns,
|
columns,
|
||||||
data: filteredRows,
|
data: filteredRows,
|
||||||
@ -590,7 +608,8 @@ export default function Svod2Page() {
|
|||||||
onChange={updateCurrentSheetFilters}
|
onChange={updateCurrentSheetFilters}
|
||||||
searchPlaceholder='ССП/РФ, код, статья или договор'
|
searchPlaceholder='ССП/РФ, код, статья или договор'
|
||||||
emptyOrganizationsPlaceholder='Выберите ССП/РФ'
|
emptyOrganizationsPlaceholder='Выберите ССП/РФ'
|
||||||
allowSelectAll>
|
allowSelectAll
|
||||||
|
additionalFilters={<DenominationSelect value={denomination} onChange={handleDenominationChange} minWidth={135} />}>
|
||||||
{hasCurrentData && !loading && filteredRows.length !== rows.length && (
|
{hasCurrentData && !loading && filteredRows.length !== rows.length && (
|
||||||
<Typography variant='caption' color='text.secondary' sx={{ flexShrink: 0, whiteSpace: 'nowrap' }}>
|
<Typography variant='caption' color='text.secondary' sx={{ flexShrink: 0, whiteSpace: 'nowrap' }}>
|
||||||
{`Найдено ${filteredRows.length.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`}
|
{`Найдено ${filteredRows.length.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||||||
import { memo } from 'react';
|
import { memo, useEffect, useMemo, useState } from 'react';
|
||||||
|
import DenominationSelect from '../../components/common/DenominationSelect';
|
||||||
import { blueColumn, greenColumn, orangeColumn } from '../../components/RealtimeTable/constants/columnColors';
|
import { blueColumn, greenColumn, orangeColumn } from '../../components/RealtimeTable/constants/columnColors';
|
||||||
import { sectionCodeColor } from '../../components/RealtimeTable/constants/columnConfig';
|
import { sectionCodeColor } from '../../components/RealtimeTable/constants/columnConfig';
|
||||||
import { buildRowHierarchy } from '../../components/RealtimeTable/utils/rowUtils';
|
import { buildRowHierarchy } from '../../components/RealtimeTable/utils/rowUtils';
|
||||||
@ -55,9 +56,45 @@ export const filterSummaryTree = (tree, query) =>
|
|||||||
|
|
||||||
export const countSummaryTree = (tree) => tree.reduce((count, row) => count + 1 + countSummaryTree(row.subRows), 0);
|
export const countSummaryTree = (tree) => tree.reduce((count, row) => count + 1 + countSummaryTree(row.subRows), 0);
|
||||||
|
|
||||||
export const SummaryTable = memo(function SummaryTable({ columns, data, loading, colorBySource = false }) {
|
const denominationKey = (tableKey) => `summary_${tableKey}_denomination`;
|
||||||
|
const loadDenomination = (key) => {
|
||||||
|
try { return localStorage.getItem(key) || 'rub'; } catch { return 'rub'; }
|
||||||
|
};
|
||||||
|
|
||||||
|
const scaleColumns = (columns, divisor) => columns.map((column) => {
|
||||||
|
const children = column.columns ? scaleColumns(column.columns, divisor) : undefined;
|
||||||
|
const originalCell = column.Cell;
|
||||||
|
const isAmount = !['section_code', 'name', 'mrt-row-expand'].includes(column.id) && /plan|approved|fact|corrected|detail|total|quarter|residual|actual|adj|base|correction/i.test(column.id || '');
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
...(children ? { columns: children } : {}),
|
||||||
|
...(isAmount ? { Cell: ({ cell }) => {
|
||||||
|
const value = cell.getValue();
|
||||||
|
if (value === null || value === undefined || value === '') return originalCell ? originalCell({ cell }) : '—';
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric)) return originalCell ? originalCell({ cell }) : value;
|
||||||
|
if (numeric === 0) return '-';
|
||||||
|
return new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(numeric / divisor);
|
||||||
|
} } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SummaryTable = memo(function SummaryTable({ columns, data, loading, colorBySource = false, tableKey = 'default' }) {
|
||||||
|
const [denominations, setDenominations] = useState({});
|
||||||
|
const storageKey = denominationKey(tableKey);
|
||||||
|
const denomination = denominations[storageKey] ?? loadDenomination(storageKey);
|
||||||
|
useEffect(() => {
|
||||||
|
const sync = (event) => {
|
||||||
|
if (event.detail?.key === storageKey) setDenominations((current) => ({ ...current, [storageKey]: event.detail.value }));
|
||||||
|
};
|
||||||
|
window.addEventListener('summary-denomination-change', sync);
|
||||||
|
return () => window.removeEventListener('summary-denomination-change', sync);
|
||||||
|
}, [storageKey]);
|
||||||
|
const divisor = denomination === 'billion' ? 1000000000 : denomination === 'million' ? 1000000 : denomination === 'thousand' ? 1000 : 1;
|
||||||
|
|
||||||
|
const scaledColumns = useMemo(() => scaleColumns(columns, divisor), [columns, divisor]);
|
||||||
const table = useMaterialReactTable({
|
const table = useMaterialReactTable({
|
||||||
columns,
|
columns: scaledColumns,
|
||||||
data,
|
data,
|
||||||
enableExpanding: true,
|
enableExpanding: true,
|
||||||
enableExpandAll: true,
|
enableExpandAll: true,
|
||||||
@ -152,3 +189,14 @@ export const SummaryTable = memo(function SummaryTable({ columns, data, loading,
|
|||||||
|
|
||||||
return <MaterialReactTable table={table} />;
|
return <MaterialReactTable table={table} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function SummaryDenominationControl({ tableKey = 'default' }) {
|
||||||
|
const storageKey = denominationKey(tableKey);
|
||||||
|
const [values, setValues] = useState({});
|
||||||
|
const value = values[storageKey] ?? loadDenomination(storageKey);
|
||||||
|
return <DenominationSelect value={value} onChange={(next) => {
|
||||||
|
setValues((current) => ({ ...current, [storageKey]: next }));
|
||||||
|
try { localStorage.setItem(storageKey, next); } catch { /* storage may be unavailable */ }
|
||||||
|
window.dispatchEvent(new CustomEvent('summary-denomination-change', { detail: { key: storageKey, value: next } }));
|
||||||
|
}} />;
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
||||||
import { PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
import { PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
||||||
import { SummaryTable, buildSummaryTree, countSummaryTree, filterSummaryTree } from './SummaryTable';
|
import { SummaryDenominationControl, SummaryTable, buildSummaryTree, countSummaryTree, filterSummaryTree } from './SummaryTable';
|
||||||
import { useSvod } from './SvodContext';
|
import { useSvod } from './SvodContext';
|
||||||
import { SvodFilters } from './SvodFilters';
|
import { SvodFilters } from './SvodFilters';
|
||||||
|
|
||||||
@ -264,7 +264,7 @@ export default function SvodPage() {
|
|||||||
<Tab key={item.value} value={item.value} label={item.label} sx={{ textTransform: 'none', alignItems: 'flex-start', textAlign: 'left', minHeight: '3rem' }} />
|
<Tab key={item.value} value={item.value} label={item.label} sx={{ textTransform: 'none', alignItems: 'flex-start', textAlign: 'left', minHeight: '3rem' }} />
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<SvodFilters year={year} selectedOrganizations={selectedOrganizations} search={search} onChange={updateCurrentSheetFilters}>
|
<SvodFilters year={year} selectedOrganizations={selectedOrganizations} search={search} onChange={updateCurrentSheetFilters} additionalFilters={<SummaryDenominationControl tableKey={`svod_${sheet}`} />}>
|
||||||
{!loading && filteredRowsCount !== rows.length && (
|
{!loading && filteredRowsCount !== rows.length && (
|
||||||
<Typography variant='caption' color='text.secondary' sx={{ flexShrink: 0, whiteSpace: 'nowrap' }}>
|
<Typography variant='caption' color='text.secondary' sx={{ flexShrink: 0, whiteSpace: 'nowrap' }}>
|
||||||
{`Найдено ${filteredRowsCount.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`}
|
{`Найдено ${filteredRowsCount.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`}
|
||||||
@ -301,7 +301,7 @@ export default function SvodPage() {
|
|||||||
{error}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<SummaryTable columns={columns} data={filteredRows} loading={loading} />
|
<SummaryTable columns={columns} data={filteredRows} loading={loading} tableKey={`svod_${sheet}`} />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user