новые своды для проектов
This commit is contained in:
parent
b8632b445d
commit
f3539a5f36
File diff suppressed because it is too large
Load Diff
@ -4,43 +4,7 @@ import { FormsSheetApi } from '../../../api/form_sheet';
|
|||||||
import { ProjectsApi } from '../../../api/projects';
|
import { ProjectsApi } from '../../../api/projects';
|
||||||
import { useRealtimeActions, useRealtimeConnection } from '../contexts/RealtimeContext';
|
import { useRealtimeActions, useRealtimeConnection } from '../contexts/RealtimeContext';
|
||||||
import { addRootRow, deleteRow, insertCell, insertProjectRow, updateCells } from '../utils/cellUtils';
|
import { addRootRow, deleteRow, insertCell, insertProjectRow, updateCells } from '../utils/cellUtils';
|
||||||
import { getParentId, isVspNewRow } from '../utils/rowUtils';
|
import { buildRowHierarchy, getParentId, isVspNewRow } from '../utils/rowUtils';
|
||||||
|
|
||||||
const ROW_TYPE_LEVELS = {
|
|
||||||
ROOT: 0,
|
|
||||||
GROUP: 1,
|
|
||||||
ITEM: 2,
|
|
||||||
SUB_ITEM: 3,
|
|
||||||
INPUT: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildHierarchy = (items) => {
|
|
||||||
const result = [];
|
|
||||||
const stack = [];
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
const currentLevel = ROW_TYPE_LEVELS[item.row_type] ?? 999;
|
|
||||||
|
|
||||||
while (stack.length > 0 && (ROW_TYPE_LEVELS[stack.at(-1).row_type] ?? 999) >= currentLevel) {
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent = stack.at(-1);
|
|
||||||
const newNode = { ...item, subRows: [] };
|
|
||||||
|
|
||||||
if (parent) {
|
|
||||||
parent.subRows.push(newNode);
|
|
||||||
} else {
|
|
||||||
result.push(newNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.row_type !== 'INPUT') {
|
|
||||||
stack.push(newNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
@ -91,7 +55,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hierarchicalData = buildHierarchy(data);
|
const hierarchicalData = buildRowHierarchy(data);
|
||||||
setData(hierarchicalData);
|
setData(hierarchicalData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isCancelled) console.error('Failed to load initial data:', err);
|
if (!isCancelled) console.error('Failed to load initial data:', err);
|
||||||
|
|||||||
@ -1,3 +1,34 @@
|
|||||||
|
const ROW_TYPE_LEVELS = {
|
||||||
|
ROOT: 0,
|
||||||
|
GROUP: 1,
|
||||||
|
ITEM: 2,
|
||||||
|
SUB_ITEM: 3,
|
||||||
|
INPUT: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildRowHierarchy(items, mapItem = (item) => item) {
|
||||||
|
const result = [];
|
||||||
|
const stack = [];
|
||||||
|
|
||||||
|
for (const [index, item] of items.entries()) {
|
||||||
|
const currentLevel = ROW_TYPE_LEVELS[item.row_type] ?? 999;
|
||||||
|
|
||||||
|
while (stack.length > 0 && (ROW_TYPE_LEVELS[stack.at(-1).row_type] ?? 999) >= currentLevel) {
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = stack.at(-1);
|
||||||
|
const newNode = { ...mapItem(item, index), subRows: [] };
|
||||||
|
|
||||||
|
if (parent) parent.subRows.push(newNode);
|
||||||
|
else result.push(newNode);
|
||||||
|
|
||||||
|
if (item.row_type !== 'INPUT') stack.push(newNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDataRowId(data) {
|
export function getDataRowId(data) {
|
||||||
return data?.id || data?.line_id;
|
return data?.id || data?.line_id;
|
||||||
}
|
}
|
||||||
|
|||||||
21
web/src/components/RealtimeTable/utils/rowUtils.test.js
Normal file
21
web/src/components/RealtimeTable/utils/rowUtils.test.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { buildRowHierarchy } from './rowUtils.js';
|
||||||
|
|
||||||
|
test('row hierarchy uses row types when depth skips a level and keeps INPUT rows as siblings', () => {
|
||||||
|
const rows = [
|
||||||
|
{ row_type: 'ROOT', depth: 0, id: 'root' },
|
||||||
|
{ row_type: 'ITEM', depth: 2, id: 'item-1' },
|
||||||
|
{ row_type: 'INPUT', depth: 4, id: 'input-1' },
|
||||||
|
{ row_type: 'INPUT', depth: 4, id: 'input-2' },
|
||||||
|
{ row_type: 'ITEM', depth: 2, id: 'item-2' },
|
||||||
|
{ row_type: 'ROOT', depth: 0, id: 'next-root' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const tree = buildRowHierarchy(rows);
|
||||||
|
|
||||||
|
assert.deepEqual(tree.map(({ id }) => id), ['root', 'next-root']);
|
||||||
|
assert.deepEqual(tree[0].subRows.map(({ id }) => id), ['item-1', 'item-2']);
|
||||||
|
assert.deepEqual(tree[0].subRows[0].subRows.map(({ id }) => id), ['input-1', 'input-2']);
|
||||||
|
assert.deepEqual(tree[0].subRows[0].subRows[0].subRows, []);
|
||||||
|
});
|
||||||
@ -224,7 +224,7 @@ export default function ProjectSummariesPage() {
|
|||||||
<Box sx={{ px: '2rem', pt: '1rem', pb: '1.5rem', height: '100%', minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
<Box sx={{ px: '2rem', pt: '1rem', pb: '1.5rem', height: '100%', minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
<HeaderSwitch />
|
<HeaderSwitch />
|
||||||
<Paper elevation={0} sx={{ border: '0.0625rem solid #e5e7eb', borderRadius: '0.75rem', overflow: 'hidden', flexShrink: 0 }}>
|
<Paper elevation={0} sx={{ border: '0.0625rem solid #e5e7eb', borderRadius: '0.75rem', overflow: 'hidden', flexShrink: 0 }}>
|
||||||
<Tabs value={sheet} onChange={(_event, value) => setSheet(value)} sx={{ px: '0.5rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}>
|
<Tabs value={sheet} onChange={(_event, value) => setSheet(value)} sx={{ px: '1rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}>
|
||||||
{SHEETS.map(({ value, label }) => <Tab key={value} value={value} label={label} sx={{ textTransform: 'none', minHeight: '3rem' }} />)}
|
{SHEETS.map(({ value, label }) => <Tab key={value} value={value} label={label} sx={{ textTransform: 'none', minHeight: '3rem' }} />)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<SvodFilters
|
<SvodFilters
|
||||||
@ -254,7 +254,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} />
|
<SummaryTable columns={columns} data={filteredRows} loading={loading} colorBySource />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Alert, Box, Button, Paper, Tab, Tabs, Typography } from '@mui/material'
|
|||||||
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||||||
import { MRT_Localization_RU } from 'material-react-table/locales/ru';
|
import { MRT_Localization_RU } from 'material-react-table/locales/ru';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
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';
|
||||||
@ -355,6 +356,7 @@ const matchesSearch = (row, query) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Svod2Page() {
|
export default function Svod2Page() {
|
||||||
|
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 [tableStateBySheet, setTableStateBySheet] = useState(createInitialTableState);
|
const [tableStateBySheet, setTableStateBySheet] = useState(createInitialTableState);
|
||||||
@ -436,6 +438,11 @@ export default function Svod2Page() {
|
|||||||
invalidateSummary(sheet, year, selectedOrganizationIds);
|
invalidateSummary(sheet, year, selectedOrganizationIds);
|
||||||
loadSummary();
|
loadSummary();
|
||||||
}, [invalidateSummary, loadSummary, selectedOrganizationIds, sheet, year]);
|
}, [invalidateSummary, loadSummary, selectedOrganizationIds, sheet, year]);
|
||||||
|
const openFilteredTasks = useCallback(() => {
|
||||||
|
const params = new URLSearchParams({ year: String(year), form_type: 'FORM_2' });
|
||||||
|
if (selectedOrganizationIds.length) params.set('org_units', selectedOrganizationIds.join(','));
|
||||||
|
navigate(`/tasks?${params.toString()}`);
|
||||||
|
}, [navigate, selectedOrganizationIds, year]);
|
||||||
|
|
||||||
const filteredRows = useMemo(() => {
|
const filteredRows = useMemo(() => {
|
||||||
if (!hasCurrentData) return [];
|
if (!hasCurrentData) return [];
|
||||||
@ -571,9 +578,9 @@ export default function Svod2Page() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
value={sheet}
|
value={sheet}
|
||||||
onChange={(_event, value) => setSheet(value)}
|
onChange={(_event, value) => setSheet(value)}
|
||||||
sx={{ px: '0.5rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}>
|
sx={{ px: '1rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}>
|
||||||
{SHEETS.map((item) => (
|
{SHEETS.map((item) => (
|
||||||
<Tab key={item.value} value={item.value} label={item.label} sx={{ textTransform: 'none', 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
|
<SvodFilters
|
||||||
@ -589,6 +596,9 @@ export default function Svod2Page() {
|
|||||||
{`Найдено ${filteredRows.length.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`}
|
{`Найдено ${filteredRows.length.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
<PrimaryButton variant='contained' onClick={openFilteredTasks} sx={{ ml: 'auto', height: '2.5rem', flexShrink: 0 }}>
|
||||||
|
Перейти к задачам
|
||||||
|
</PrimaryButton>
|
||||||
{hasCurrentData ? (
|
{hasCurrentData ? (
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
@ -596,7 +606,7 @@ export default function Svod2Page() {
|
|||||||
startIcon={<RefreshRoundedIcon />}
|
startIcon={<RefreshRoundedIcon />}
|
||||||
disabled={loading || selectedOrganizations.length === 0}
|
disabled={loading || selectedOrganizations.length === 0}
|
||||||
onClick={refreshSummary}
|
onClick={refreshSummary}
|
||||||
sx={{ ml: 'auto', height: '2.5rem', flexShrink: 0 }}>
|
sx={{ height: '2.5rem', flexShrink: 0 }}>
|
||||||
Обновить
|
Обновить
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
@ -605,7 +615,7 @@ export default function Svod2Page() {
|
|||||||
color='default'
|
color='default'
|
||||||
disabled={loading || selectedOrganizations.length === 0}
|
disabled={loading || selectedOrganizations.length === 0}
|
||||||
onClick={refreshSummary}
|
onClick={refreshSummary}
|
||||||
sx={{ ml: 'auto', height: '2.5rem', flexShrink: 0 }}>
|
sx={{ height: '2.5rem', flexShrink: 0 }}>
|
||||||
Загрузить
|
Загрузить
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { blueColumn } 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';
|
||||||
|
|
||||||
const tableSize = (rem) => rem * 16;
|
const tableSize = (rem) => rem * 16;
|
||||||
const ROW_VIRTUALIZER_OPTIONS = {
|
const ROW_VIRTUALIZER_OPTIONS = {
|
||||||
@ -10,8 +11,14 @@ const ROW_VIRTUALIZER_OPTIONS = {
|
|||||||
measureElement: (element) => element?.offsetHeight || 37,
|
measureElement: (element) => element?.offsetHeight || 37,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSummaryRowColor = (row) => {
|
const SOURCE_COLORS = { ahr: greenColumn, oper: blueColumn, cap: orangeColumn };
|
||||||
|
|
||||||
|
const getSummaryRowColor = (row, colorBySource) => {
|
||||||
const rowType = row.original?.row_type || row.row_type;
|
const rowType = row.original?.row_type || row.row_type;
|
||||||
|
if (colorBySource) {
|
||||||
|
const source = row.original?.data?.header?.source?.toLowerCase();
|
||||||
|
if (SOURCE_COLORS[source]) return SOURCE_COLORS[source][rowType];
|
||||||
|
}
|
||||||
const sectionCodeValue = row.original?.data?.section_code ?? row.original?.data?.header?.section_code;
|
const sectionCodeValue = row.original?.data?.section_code ?? row.original?.data?.header?.section_code;
|
||||||
if (sectionCodeValue === null || sectionCodeValue === undefined || sectionCodeValue === '') return undefined;
|
if (sectionCodeValue === null || sectionCodeValue === undefined || sectionCodeValue === '') return undefined;
|
||||||
const sectionCode = String(sectionCodeValue)[0];
|
const sectionCode = String(sectionCodeValue)[0];
|
||||||
@ -26,27 +33,11 @@ const getTextColor = (backgroundColor) => {
|
|||||||
return 0.299 * red + 0.587 * green + 0.114 * blue < 128 ? '#fff' : '#000';
|
return 0.299 * red + 0.587 * green + 0.114 * blue < 128 ? '#fff' : '#000';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildSummaryTree = (sourceRows) => {
|
export const buildSummaryTree = (sourceRows) =>
|
||||||
const roots = [];
|
buildRowHierarchy(sourceRows, (row, index) => ({
|
||||||
const parents = [];
|
...row,
|
||||||
|
_rowId: `${row.sort_order ?? index}-${row.data?.section_code ?? row.data?.header?.section_code ?? index}`,
|
||||||
for (const [index, sourceRow] of sourceRows.entries()) {
|
}));
|
||||||
const row = {
|
|
||||||
...sourceRow,
|
|
||||||
_rowId: `${sourceRow.sort_order ?? index}-${sourceRow.data?.section_code ?? sourceRow.data?.header?.section_code ?? index}`,
|
|
||||||
subRows: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
while (parents.length > row.depth) parents.pop();
|
|
||||||
if (row.depth > 0 && parents[row.depth - 1]) parents[row.depth - 1].subRows.push(row);
|
|
||||||
else roots.push(row);
|
|
||||||
|
|
||||||
parents[row.depth] = row;
|
|
||||||
parents.length = row.depth + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return roots;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filterSummaryTree = (tree, query) =>
|
export const filterSummaryTree = (tree, query) =>
|
||||||
tree.flatMap((row) => {
|
tree.flatMap((row) => {
|
||||||
@ -64,7 +55,7 @@ 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 }) {
|
export const SummaryTable = memo(function SummaryTable({ columns, data, loading, colorBySource = false }) {
|
||||||
const table = useMaterialReactTable({
|
const table = useMaterialReactTable({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
@ -141,7 +132,7 @@ export const SummaryTable = memo(function SummaryTable({ columns, data, loading
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
muiTableBodyCellProps: ({ row, column }) => {
|
muiTableBodyCellProps: ({ row, column }) => {
|
||||||
const backgroundColor = getSummaryRowColor(row);
|
const backgroundColor = getSummaryRowColor(row, colorBySource);
|
||||||
return {
|
return {
|
||||||
sx: {
|
sx: {
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
|
|||||||
@ -259,9 +259,9 @@ export default function SvodPage() {
|
|||||||
onChange={(_event, value) => setSheet(value)}
|
onChange={(_event, value) => setSheet(value)}
|
||||||
variant='scrollable'
|
variant='scrollable'
|
||||||
scrollButtons='auto'
|
scrollButtons='auto'
|
||||||
sx={{ px: '0.5rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}>
|
sx={{ px: '1rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}>
|
||||||
{SHEETS.map((item) => (
|
{SHEETS.map((item) => (
|
||||||
<Tab key={item.value} value={item.value} label={item.label} sx={{ textTransform: 'none', 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}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user