Compare commits
No commits in common. "645107cef87c11b90c4b18c084a78ce8e294e4dd" and "8a3e870c4f35ac7e69b981a437394c5880aa2fef" have entirely different histories.
645107cef8
...
8a3e870c4f
@ -9,7 +9,7 @@ import FormsPage from '../pages/FormsPage';
|
|||||||
import LoginPage from '../pages/LoginPage';
|
import LoginPage from '../pages/LoginPage';
|
||||||
import NewFormTablePage from '../pages/NewFormTablePage.jsx';
|
import NewFormTablePage from '../pages/NewFormTablePage.jsx';
|
||||||
import NewTablePage from '../pages/NewTablePage.jsx';
|
import NewTablePage from '../pages/NewTablePage.jsx';
|
||||||
import NavigationProjectPage from '../pages/ProjectPages/NavigationProjectPage.jsx';
|
import SummaryPage from '../pages/ProjectPages/SummaryPage.jsx';
|
||||||
import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx';
|
import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx';
|
||||||
import SvodPage from '../pages/SvodPage/SvodPage.jsx';
|
import SvodPage from '../pages/SvodPage/SvodPage.jsx';
|
||||||
import TablePage from '../pages/TablePage';
|
import TablePage from '../pages/TablePage';
|
||||||
@ -39,7 +39,7 @@ export const AppRoutes = () => {
|
|||||||
<Route element={<PrivateRoute />}>
|
<Route element={<PrivateRoute />}>
|
||||||
<Route path='/' element={<TasksPage />} />
|
<Route path='/' element={<TasksPage />} />
|
||||||
<Route path='/tasks' element={<TasksPage />} />
|
<Route path='/tasks' element={<TasksPage />} />
|
||||||
<Route path='/projects' element={<NavigationProjectPage />} />
|
<Route path='/projects' element={<SummaryPage />} />
|
||||||
<Route path='/forms' element={<FormsPage />} />
|
<Route path='/forms' element={<FormsPage />} />
|
||||||
<Route path='/task/:taskId' element={<TaskPage />} />
|
<Route path='/task/:taskId' element={<TaskPage />} />
|
||||||
<Route path='/project/:projectId' element={<TaskPage />} />
|
<Route path='/project/:projectId' element={<TaskPage />} />
|
||||||
@ -54,7 +54,7 @@ export const AppRoutes = () => {
|
|||||||
<Route path='/tables-new' element={<TablesTest />} />
|
<Route path='/tables-new' element={<TablesTest />} />
|
||||||
<Route path='/tables/:form/:sheetName' element={<NewFormTablePage />} />
|
<Route path='/tables/:form/:sheetName' element={<NewFormTablePage />} />
|
||||||
<Route path='/table/form/:formId/form-type/:formType/:sheetName/:direction/:year' element={<NewFormTablePage />} />
|
<Route path='/table/form/:formId/form-type/:formType/:sheetName/:direction/:year' element={<NewFormTablePage />} />
|
||||||
<Route path='/project-mock-summary' element={<NavigationProjectPage />} />
|
<Route path='/project-mock-summary' element={<SummaryPage />} />
|
||||||
<Route path='/svod' element={<SvodPage />} />
|
<Route path='/svod' element={<SvodPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@ -110,7 +110,7 @@ const createHighlightedContent = (originalValue, displayValue, searchQueries) =>
|
|||||||
const cleanQueries = searchQueries.filter(Boolean);
|
const cleanQueries = searchQueries.filter(Boolean);
|
||||||
if (cleanQueries.length === 0) return displayValue;
|
if (cleanQueries.length === 0) return displayValue;
|
||||||
|
|
||||||
const searchStr = String(displayValue ?? originalValue);
|
const searchStr = String(originalValue ?? displayValue);
|
||||||
const escapedQueries = cleanQueries.map((q) => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
const escapedQueries = cleanQueries.map((q) => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
|
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal';
|
|||||||
import { SelectVspModal } from './Modals/SelectVspModal';
|
import { SelectVspModal } from './Modals/SelectVspModal';
|
||||||
import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig';
|
import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig';
|
||||||
import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePaperStyles } from './constants/tableConfig';
|
import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePaperStyles } from './constants/tableConfig';
|
||||||
import { useRealtimeActions, useVspOptions } from './contexts/RealtimeContext';
|
import { useRealtimeActions } from './contexts/RealtimeContext';
|
||||||
import { useColumnSettings } from './hooks/useColumnSettings';
|
import { useColumnSettings } from './hooks/useColumnSettings';
|
||||||
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
||||||
import { useTableScale } from './hooks/useTableScale';
|
import { useTableScale } from './hooks/useTableScale';
|
||||||
@ -39,15 +39,15 @@ const ROW_VIRTUALIZER_OPTIONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getColumnVirtualizerOptions = ({ table }) => {
|
const getColumnVirtualizerOptions = ({ table }) => {
|
||||||
const orderedVisibleColumns = table.getVisibleLeafColumns();
|
const orderedVisibleColumns = [
|
||||||
|
...table.getLeftVisibleLeafColumns(),
|
||||||
|
...table.getCenterVisibleLeafColumns(),
|
||||||
|
...table.getRightVisibleLeafColumns(),
|
||||||
|
];
|
||||||
const getColumnSize = (index) => orderedVisibleColumns[index]?.getSize() ?? 150;
|
const getColumnSize = (index) => orderedVisibleColumns[index]?.getSize() ?? 150;
|
||||||
const scrollPaddingStart = table.getLeftVisibleLeafColumns().reduce((width, column) => width + column.getSize(), 0);
|
|
||||||
const scrollPaddingEnd = table.getRightVisibleLeafColumns().reduce((width, column) => width + column.getSize(), 0);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
overscan: 10,
|
overscan: 10,
|
||||||
scrollPaddingStart,
|
|
||||||
scrollPaddingEnd,
|
|
||||||
estimateSize: getColumnSize,
|
estimateSize: getColumnSize,
|
||||||
measureElement: (element) => {
|
measureElement: (element) => {
|
||||||
if (!element) return 150;
|
if (!element) return 150;
|
||||||
@ -59,18 +59,6 @@ const getColumnVirtualizerOptions = ({ table }) => {
|
|||||||
const hasVspDropdown = (columns) =>
|
const hasVspDropdown = (columns) =>
|
||||||
columns.some((column) => column.editType === 'vsp_dropdown' || (column.columns?.length && hasVspDropdown(column.columns)));
|
columns.some((column) => column.editType === 'vsp_dropdown' || (column.columns?.length && hasVspDropdown(column.columns)));
|
||||||
|
|
||||||
const normalizeFilterValue = (value) =>
|
|
||||||
String(value ?? '')
|
|
||||||
.trim()
|
|
||||||
.toLocaleLowerCase('ru-RU');
|
|
||||||
|
|
||||||
const doesCellMatchFilter = (rawValue, columnDef, query, vspRegistrationById) => {
|
|
||||||
if (normalizeFilterValue(rawValue).includes(query)) return true;
|
|
||||||
if (columnDef?.editType !== 'vsp_dropdown') return false;
|
|
||||||
|
|
||||||
return vspRegistrationById.get(Number(rawValue))?.includes(query) ?? false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const tableContentStyle = {
|
const tableContentStyle = {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -91,7 +79,6 @@ const loadingOverlayStyle = {
|
|||||||
|
|
||||||
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const vspOptions = useVspOptions();
|
|
||||||
const userRoleId = user?.role_id;
|
const userRoleId = user?.role_id;
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@ -100,9 +87,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
editingCells: dataEditingCells,
|
editingCells: dataEditingCells,
|
||||||
} = useRealtimeData(formId, sheetName, direction, formType, year);
|
} = useRealtimeData(formId, sheetName, direction, formType, year);
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
const [searchMatchIndex, setSearchMatchIndex] = useState(-1);
|
|
||||||
const [activeSearchMatch, setActiveSearchMatch] = useState(null);
|
|
||||||
const [pendingSearchDirection, setPendingSearchDirection] = useState(0);
|
|
||||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||||||
const [selectedColumnId, setSelectedColumnId] = useState();
|
const [selectedColumnId, setSelectedColumnId] = useState();
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
@ -136,9 +120,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
|
|
||||||
const handleGlobalFilterChange = useMemo(
|
const handleGlobalFilterChange = useMemo(
|
||||||
() =>
|
() =>
|
||||||
debounce((valueOrUpdater) => {
|
debounce((value) => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setGlobalFilter((currentValue) => (typeof valueOrUpdater === 'function' ? valueOrUpdater(currentValue) : valueOrUpdater));
|
setGlobalFilter(value);
|
||||||
});
|
});
|
||||||
}, 300),
|
}, 300),
|
||||||
[startTransition],
|
[startTransition],
|
||||||
@ -316,23 +300,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
[handleUpdateCell],
|
[handleUpdateCell],
|
||||||
);
|
);
|
||||||
|
|
||||||
const vspRegistrationById = useMemo(
|
|
||||||
() => new Map(vspOptions.map((vsp) => [Number(vsp.id), normalizeFilterValue(vsp.registration_number)])),
|
|
||||||
[vspOptions],
|
|
||||||
);
|
|
||||||
|
|
||||||
const globalFilterFn = useCallback(
|
|
||||||
(row, columnId, filterValue) => {
|
|
||||||
const query = normalizeFilterValue(filterValue);
|
|
||||||
if (!query) return true;
|
|
||||||
|
|
||||||
const rawValue = row.getValue(columnId);
|
|
||||||
const column = row.getAllCells().find((cell) => cell.column.id === columnId)?.column;
|
|
||||||
return doesCellMatchFilter(rawValue, column?.columnDef, query, vspRegistrationById);
|
|
||||||
},
|
|
||||||
[vspRegistrationById],
|
|
||||||
);
|
|
||||||
|
|
||||||
const createTableConfig = ({
|
const createTableConfig = ({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
@ -350,11 +317,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
setExpanded,
|
setExpanded,
|
||||||
editingCell,
|
editingCell,
|
||||||
contextStartEditing,
|
contextStartEditing,
|
||||||
globalFilterFn,
|
|
||||||
activeSearchMatch,
|
|
||||||
}) => ({
|
}) => ({
|
||||||
...BASE_TABLE_CONFIG,
|
...BASE_TABLE_CONFIG,
|
||||||
globalFilterFn,
|
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
enableRowVirtualization: true,
|
enableRowVirtualization: true,
|
||||||
@ -391,21 +355,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
muiTableHeadCellProps: {
|
muiTableHeadCellProps: {
|
||||||
sx: { boxSizing: 'border-box' },
|
sx: { boxSizing: 'border-box' },
|
||||||
},
|
},
|
||||||
muiTableBodyCellProps: (props) => {
|
muiTableBodyCellProps: getTableBodyCellProps,
|
||||||
const cellProps = getTableBodyCellProps(props);
|
|
||||||
const isActiveMatch = activeSearchMatch?.rowId === props.row.id && activeSearchMatch?.columnId === props.column.id;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...cellProps,
|
|
||||||
sx: {
|
|
||||||
...cellProps.sx,
|
|
||||||
...(isActiveMatch && {
|
|
||||||
zIndex: 7,
|
|
||||||
boxShadow: 'inset 0 0 0 3px #1976d2',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
muiTableHeadProps: {
|
muiTableHeadProps: {
|
||||||
sx: {
|
sx: {
|
||||||
display: 'table-header-group',
|
display: 'table-header-group',
|
||||||
@ -456,8 +406,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
setExpanded,
|
setExpanded,
|
||||||
editingCell,
|
editingCell,
|
||||||
contextStartEditing,
|
contextStartEditing,
|
||||||
globalFilterFn,
|
|
||||||
activeSearchMatch,
|
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
columns,
|
columns,
|
||||||
@ -474,8 +422,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
setColumnPinning,
|
setColumnPinning,
|
||||||
editingCell,
|
editingCell,
|
||||||
expanded,
|
expanded,
|
||||||
globalFilterFn,
|
|
||||||
activeSearchMatch,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -485,108 +431,31 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
const handleNavigateToColumn = useCallback(
|
const handleNavigateToColumn = useCallback(
|
||||||
(columnId) => {
|
(columnId) => {
|
||||||
if (!columnId) return;
|
if (!columnId) return;
|
||||||
console.log(columnId);
|
|
||||||
console.log(columnId);
|
|
||||||
console.log(columnId);
|
|
||||||
console.log(columnId);
|
|
||||||
|
|
||||||
setSelectedColumnId(columnId);
|
setSelectedColumnId(columnId);
|
||||||
|
|
||||||
const pinnedIds = new Set([...(columnPinning?.left || []), ...(columnPinning?.right || [])]);
|
|
||||||
if (pinnedIds.has(columnId)) return;
|
|
||||||
|
|
||||||
const visibleColumns = table.getVisibleLeafColumns();
|
|
||||||
const columnIndex = visibleColumns.findIndex((column) => column.id === columnId);
|
|
||||||
if (columnIndex < 0) return;
|
|
||||||
|
|
||||||
const columnVirtualizer = columnVirtualizerRef.current;
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const leftPinnedSize = table.getLeftVisibleLeafColumns().reduce((width, column) => width + column.getSize(), 0);
|
const pinnedIds = new Set(columnPinning?.left || []);
|
||||||
const calculatedStart = visibleColumns.slice(0, columnIndex).reduce((offset, column) => offset + column.getSize(), 0);
|
if (pinnedIds.has(columnId)) return;
|
||||||
const measurement = columnVirtualizer?.measurementsCache?.[columnIndex];
|
|
||||||
const targetStart = measurement?.start ?? calculatedStart;
|
const centerColumns = table.getVisibleLeafColumns().filter((column) => !pinnedIds.has(column.id));
|
||||||
const targetOffset = Math.max(0, targetStart - leftPinnedSize);
|
|
||||||
|
let offset = 0;
|
||||||
|
for (const column of centerColumns) {
|
||||||
|
if (column.id === columnId) break;
|
||||||
|
offset += column.getSize();
|
||||||
|
}
|
||||||
|
|
||||||
container.scrollTo({
|
container.scrollTo({
|
||||||
left: targetOffset,
|
left: Math.max(0, offset - 40),
|
||||||
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[columnPinning, containerRef, table],
|
[columnPinning, containerRef, table],
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchMatches = useMemo(() => {
|
|
||||||
const query = normalizeFilterValue(globalFilter);
|
|
||||||
if (!query) return [];
|
|
||||||
|
|
||||||
const matches = [];
|
|
||||||
for (const [rowIndex, row] of table.getRowModel().rows.entries()) {
|
|
||||||
for (const cell of row.getVisibleCells()) {
|
|
||||||
if (doesCellMatchFilter(cell.getValue(), cell.column.columnDef, query, vspRegistrationById)) {
|
|
||||||
matches.push({ rowId: row.id, rowIndex, columnId: cell.column.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}, [data, globalFilter, table, vspRegistrationById, columnVisibility]);
|
|
||||||
|
|
||||||
const navigateToSearchMatch = useCallback(
|
|
||||||
(direction) => {
|
|
||||||
if (!searchMatches.length) return;
|
|
||||||
|
|
||||||
setSearchMatchIndex((currentIndex) => {
|
|
||||||
const startIndex = currentIndex < 0 ? (direction > 0 ? -1 : 0) : currentIndex;
|
|
||||||
const nextIndex = (startIndex + direction + searchMatches.length) % searchMatches.length;
|
|
||||||
const match = searchMatches[nextIndex];
|
|
||||||
|
|
||||||
setActiveSearchMatch(match);
|
|
||||||
setRowSelection({ [match.rowId]: true });
|
|
||||||
console.log(match.columnId);
|
|
||||||
handleNavigateToColumn(match.columnId);
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
rowVirtualizerRef.current?.scrollToIndex?.(match.rowIndex < 5 ? match.rowIndex : match.rowIndex + 5, { align: 'auto', behavior: 'smooth' });
|
|
||||||
});
|
|
||||||
|
|
||||||
return nextIndex;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[handleNavigateToColumn, searchMatches],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSearchKeyDown = useCallback(
|
|
||||||
(event) => {
|
|
||||||
if (event.key !== 'Enter') return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
const direction = event.shiftKey ? -1 : 1;
|
|
||||||
const inputValue = event.target.value;
|
|
||||||
|
|
||||||
if (normalizeFilterValue(inputValue) !== normalizeFilterValue(globalFilter)) {
|
|
||||||
handleGlobalFilterChange.clear();
|
|
||||||
setGlobalFilter(inputValue);
|
|
||||||
setPendingSearchDirection(direction);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToSearchMatch(direction);
|
|
||||||
},
|
|
||||||
[globalFilter, handleGlobalFilterChange, navigateToSearchMatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchMatchIndex(-1);
|
|
||||||
setActiveSearchMatch(null);
|
|
||||||
}, [globalFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pendingSearchDirection) return;
|
|
||||||
|
|
||||||
navigateToSearchMatch(pendingSearchDirection);
|
|
||||||
setPendingSearchDirection(0);
|
|
||||||
}, [navigateToSearchMatch, pendingSearchDirection]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dataEditingCells?.line_id) {
|
if (!dataEditingCells?.line_id) {
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
@ -754,12 +623,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
columnsCurStage={columnsCurStage}
|
columnsCurStage={columnsCurStage}
|
||||||
onChangeSizeMult={setSizeMult}
|
onChangeSizeMult={setSizeMult}
|
||||||
onGlobalFilterChange={handleGlobalFilterChange}
|
onGlobalFilterChange={handleGlobalFilterChange}
|
||||||
globalFilter={globalFilter}
|
|
||||||
onSearchKeyDown={handleSearchKeyDown}
|
|
||||||
onSearchNext={() => navigateToSearchMatch(1)}
|
|
||||||
onSearchPrevious={() => navigateToSearchMatch(-1)}
|
|
||||||
searchMatchCount={searchMatches.length}
|
|
||||||
searchMatchIndex={searchMatchIndex}
|
|
||||||
sizeMult={sizeMult}
|
sizeMult={sizeMult}
|
||||||
onChangeShowColumnFilters={setShowColumnFilters}
|
onChangeShowColumnFilters={setShowColumnFilters}
|
||||||
showColumnFilters={showColumnFilters}
|
showColumnFilters={showColumnFilters}
|
||||||
|
|||||||
@ -32,32 +32,6 @@ const controlSx = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const searchSx = { height: '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 exportButtonSx = { height: '2.5rem', minHeight: '2.5rem' };
|
||||||
|
|
||||||
const SettingPanel = ({
|
const SettingPanel = ({
|
||||||
@ -72,12 +46,6 @@ const SettingPanel = ({
|
|||||||
columnsCurStage,
|
columnsCurStage,
|
||||||
onChangeSizeMult,
|
onChangeSizeMult,
|
||||||
onGlobalFilterChange,
|
onGlobalFilterChange,
|
||||||
globalFilter,
|
|
||||||
onSearchKeyDown,
|
|
||||||
onSearchNext,
|
|
||||||
onSearchPrevious,
|
|
||||||
searchMatchCount,
|
|
||||||
searchMatchIndex,
|
|
||||||
sizeMult,
|
sizeMult,
|
||||||
onChangeShowColumnFilters,
|
onChangeShowColumnFilters,
|
||||||
showColumnFilters,
|
showColumnFilters,
|
||||||
@ -250,40 +218,7 @@ const SettingPanel = ({
|
|||||||
</GroupByObject>
|
</GroupByObject>
|
||||||
<Divider orientation='vertical' flexItem />
|
<Divider orientation='vertical' flexItem />
|
||||||
<GroupByObject title='Поиск'>
|
<GroupByObject title='Поиск'>
|
||||||
<SearchComponent
|
<SearchComponent onChange={onGlobalFilterChange} height='2.5rem' sx={searchSx} />
|
||||||
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='Поиск по колонкам'>
|
<Tooltip title='Поиск по колонкам'>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
|
|||||||
@ -44,16 +44,6 @@ export const FORM_TYPE_TRANSLATE = {
|
|||||||
};
|
};
|
||||||
export const DIRECTION_TRANSLATE = { Support: 'Поддержка', Development: 'Развитие' };
|
export const DIRECTION_TRANSLATE = { Support: 'Поддержка', Development: 'Развитие' };
|
||||||
|
|
||||||
export const PROJECT_TYPES = ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'];
|
|
||||||
|
|
||||||
export const PROJECT_STATUSES = {
|
|
||||||
created: 'Создан',
|
|
||||||
agreed: 'Согласован',
|
|
||||||
approved: 'Согласован',
|
|
||||||
archived: 'В архиве',
|
|
||||||
deleted: 'Удалён',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ORG_UNIT_TYPE_OPTIONS = [
|
export const ORG_UNIT_TYPE_OPTIONS = [
|
||||||
{ value: 'ssp', label: 'ССП' },
|
{ value: 'ssp', label: 'ССП' },
|
||||||
{ value: 'rf', label: 'Региональный филиал' },
|
{ value: 'rf', label: 'Региональный филиал' },
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { PROJECT_TYPES } from './constants';
|
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_CONFIG = {
|
export const DEFAULT_PROJECT_CONFIG = {
|
||||||
'ССП/РФ': {
|
'ССП/РФ': {
|
||||||
type: 'multiselect',
|
type: 'multiselect',
|
||||||
@ -18,7 +16,7 @@ export const DEFAULT_PROJECT_CONFIG = {
|
|||||||
},
|
},
|
||||||
'Тип проекта развития': {
|
'Тип проекта развития': {
|
||||||
type: 'multiselect',
|
type: 'multiselect',
|
||||||
options: PROJECT_TYPES,
|
options: ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'],
|
||||||
placeholder: 'Выберите тип проекта',
|
placeholder: 'Выберите тип проекта',
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
required_field: true,
|
required_field: true,
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../../ProjectPages/con
|
|||||||
import {
|
import {
|
||||||
DIRECTION_TRANSLATE,
|
DIRECTION_TRANSLATE,
|
||||||
FORM_TYPE_TRANSLATE,
|
FORM_TYPE_TRANSLATE,
|
||||||
PROJECT_STATUSES,
|
|
||||||
ROLES_ID_RUSSIAN_NAME,
|
ROLES_ID_RUSSIAN_NAME,
|
||||||
SHEET_NAME,
|
SHEET_NAME,
|
||||||
STAGE_ROLE_RUSSIAN_NAME,
|
STAGE_ROLE_RUSSIAN_NAME,
|
||||||
@ -51,6 +50,14 @@ export const VALUE_TRANSFORMERS = {
|
|||||||
'Дата открытия / переезда / закрытия': transformDateChange,
|
'Дата открытия / переезда / закрытия': transformDateChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PROJECT_STATUSES = {
|
||||||
|
created: 'Создан',
|
||||||
|
agreed: 'Согласован',
|
||||||
|
approved: 'Согласован',
|
||||||
|
archived: 'В архиве',
|
||||||
|
deleted: 'Удалён',
|
||||||
|
};
|
||||||
|
|
||||||
const PLACEMENT_TYPES = {
|
const PLACEMENT_TYPES = {
|
||||||
own: 'Собственность',
|
own: 'Собственность',
|
||||||
rent: 'Аренда',
|
rent: 'Аренда',
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import { NameTask, TaskInfoContainer } from '../components/common/SwitchFormTask
|
|||||||
import TableList from '../components/common/TableList/TableList';
|
import TableList from '../components/common/TableList/TableList';
|
||||||
import { TableIcon } from '../components/common/icons/icons';
|
import { TableIcon } from '../components/common/icons/icons';
|
||||||
import { Chip } from '../components/styles/StyledChip';
|
import { Chip } from '../components/styles/StyledChip';
|
||||||
import { PROJECT_TYPES } from '../constants/constants';
|
|
||||||
|
|
||||||
const FormInfo = ({ form }) => {
|
const FormInfo = ({ form }) => {
|
||||||
const portalContent = (
|
const portalContent = (
|
||||||
@ -124,7 +123,7 @@ export default function FormPage() {
|
|||||||
add_projects_data: {
|
add_projects_data: {
|
||||||
'Тип проекта развития': {
|
'Тип проекта развития': {
|
||||||
type: 'multiselect',
|
type: 'multiselect',
|
||||||
options: PROJECT_TYPES,
|
options: ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'],
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Box, Typography } from '@mui/material';
|
import { Box, CircularProgress, Paper, Typography } from '@mui/material';
|
||||||
|
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||||||
import { useEffect, useMemo, useState, useCallback } from 'react';
|
import { useEffect, useMemo, useState, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@ -10,15 +11,14 @@ import { WhitePlus } from '../../components/common/icons/icons';
|
|||||||
import Modal from '../../components/common/Modal/Modal';
|
import Modal from '../../components/common/Modal/Modal';
|
||||||
import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig';
|
import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig';
|
||||||
import { ModalCreateProject } from '../ProjectsPage/ModalCreateProject';
|
import { ModalCreateProject } from '../ProjectsPage/ModalCreateProject';
|
||||||
|
import { createFirstColumns, secondColumns } from './columns';
|
||||||
import EditModal from './components/EditModal/EditModal';
|
import EditModal from './components/EditModal/EditModal';
|
||||||
import ProjectsTable from './components/ProjectsTable/ProjectsTable';
|
|
||||||
import ProjectSummaryTable from './components/ProjectSummaryTable/ProjectSummaryTable';
|
|
||||||
import TableFilters from './components/TableFilters/TableFilters'; // Импортируем компонент фильтров
|
import TableFilters from './components/TableFilters/TableFilters'; // Импортируем компонент фильтров
|
||||||
import { handleNavigateClick } from './utils/tableHandlers';
|
import { handleNavigateClick } from './utils/tableHandlers';
|
||||||
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';
|
||||||
|
|
||||||
const NavigationProjectPage = () => {
|
const SummaryPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [selectedRow, setSelectedRow] = useState(null);
|
const [selectedRow, setSelectedRow] = useState(null);
|
||||||
@ -42,15 +42,6 @@ const NavigationProjectPage = () => {
|
|||||||
// Состояния для фильтров
|
// Состояния для фильтров
|
||||||
const [selectedBranch, setSelectedBranch] = useState('');
|
const [selectedBranch, setSelectedBranch] = useState('');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedStatus, setSelectedStatus] = useState('');
|
|
||||||
const [archivedData, setArchivedData] = useState([]);
|
|
||||||
const [archivedTotalCount, setArchivedTotalCount] = useState(0);
|
|
||||||
const [archivedPagination, setArchivedPagination] = useState({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isArchiveMode = selectedStatus === 'archived';
|
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@ -103,7 +94,10 @@ const NavigationProjectPage = () => {
|
|||||||
const loadProjects = async () => {
|
const loadProjects = async () => {
|
||||||
setIsProjectsLoading(true);
|
setIsProjectsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await ProjectsApi.listWithReports({ limit: 1000 });
|
const response = await ProjectsApi.listWithReports({
|
||||||
|
limit: 1000,
|
||||||
|
...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}),
|
||||||
|
});
|
||||||
if (active) setTableData(response.result || []);
|
if (active) setTableData(response.result || []);
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
console.error('Error loading projects with reports:', requestError);
|
console.error('Error loading projects with reports:', requestError);
|
||||||
@ -116,75 +110,7 @@ const NavigationProjectPage = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [user, isLoading, projectsReloadKey]);
|
}, [user, isLoading, selectedBranch?.id, projectsReloadKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let active = true;
|
|
||||||
if (!user || isLoading || !isArchiveMode) return undefined;
|
|
||||||
|
|
||||||
const loadArchivedProjects = async () => {
|
|
||||||
setIsProjectsLoading(true);
|
|
||||||
try {
|
|
||||||
const offset = archivedPagination.pageIndex * archivedPagination.pageSize;
|
|
||||||
const params = {
|
|
||||||
status: 'archived',
|
|
||||||
offset,
|
|
||||||
limit: archivedPagination.pageSize,
|
|
||||||
...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}),
|
|
||||||
...(searchQuery.trim() ? { search: searchQuery.trim() } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: после реализации серверных фильтров и пагинации достаточно
|
|
||||||
// передать params в listWithReports и использовать response.result/count.
|
|
||||||
// Пока запрос возвращает полный набор, а код ниже имитирует ответ бэка.
|
|
||||||
const response = await ProjectsApi.listWithReports(params);
|
|
||||||
const query = searchQuery.toLowerCase().trim();
|
|
||||||
const filtered = (response.result || [])
|
|
||||||
.filter((item) => item.status === 'archived')
|
|
||||||
.filter((item) => !selectedBranch || item.org_unit_id === selectedBranch.id)
|
|
||||||
.map((item) => {
|
|
||||||
if (!query) return item;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
sub_rows: item.sub_rows?.filter((subItem) =>
|
|
||||||
(subItem.project || '').toLowerCase().includes(query),
|
|
||||||
) || [],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((item) => !query
|
|
||||||
|| (item.name || '').toLowerCase().includes(query)
|
|
||||||
|| item.sub_rows.length > 0);
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
setArchivedData(filtered.slice(offset, offset + archivedPagination.pageSize));
|
|
||||||
setArchivedTotalCount(filtered.length);
|
|
||||||
}
|
|
||||||
} catch (requestError) {
|
|
||||||
console.error('Error loading archived projects:', requestError);
|
|
||||||
if (active) {
|
|
||||||
setArchivedData([]);
|
|
||||||
setArchivedTotalCount(0);
|
|
||||||
toast.error('Не удалось загрузить архивные проекты');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (active) setIsProjectsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadArchivedProjects();
|
|
||||||
return () => {
|
|
||||||
active = false;
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
user,
|
|
||||||
isLoading,
|
|
||||||
isArchiveMode,
|
|
||||||
selectedBranch,
|
|
||||||
searchQuery,
|
|
||||||
archivedPagination.pageIndex,
|
|
||||||
archivedPagination.pageSize,
|
|
||||||
projectsReloadKey,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
@ -216,22 +142,13 @@ const NavigationProjectPage = () => {
|
|||||||
};
|
};
|
||||||
}, [selectedRow?.original?.id]);
|
}, [selectedRow?.original?.id]);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const getFilteredData = () => {
|
||||||
if (isArchiveMode) {
|
if (!selectedBranch && !searchQuery) {
|
||||||
return archivedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedBranch && !searchQuery && !selectedStatus) {
|
|
||||||
return tableData;
|
return tableData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = searchQuery.toLowerCase().trim();
|
|
||||||
|
|
||||||
return tableData
|
return tableData
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
if (selectedStatus && item.status !== selectedStatus) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Фильтр по филиалу (только для корневых)
|
// Фильтр по филиалу (только для корневых)
|
||||||
if (selectedBranch) {
|
if (selectedBranch) {
|
||||||
return item.org_unit_id === selectedBranch.id;
|
return item.org_unit_id === selectedBranch.id;
|
||||||
@ -239,10 +156,15 @@ const NavigationProjectPage = () => {
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (!query) {
|
if (!searchQuery) {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = searchQuery.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Проверяем, совпадает ли родитель
|
||||||
|
const parentMatches = (item.name || '').toLowerCase().includes(query);
|
||||||
|
|
||||||
// Фильтруем дочерние элементы - оставляем только те, что совпадают с поиском
|
// Фильтруем дочерние элементы - оставляем только те, что совпадают с поиском
|
||||||
const filteredSubRows = item.sub_rows?.filter((subItem) => (subItem.project || '').toLowerCase().includes(query)) || [];
|
const filteredSubRows = item.sub_rows?.filter((subItem) => (subItem.project || '').toLowerCase().includes(query)) || [];
|
||||||
|
|
||||||
@ -253,18 +175,32 @@ const NavigationProjectPage = () => {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
if (!query) {
|
if (!searchQuery) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = searchQuery.toLowerCase().trim();
|
||||||
const parentMatches = (item.name || '').toLowerCase().includes(query);
|
const parentMatches = (item.name || '').toLowerCase().includes(query);
|
||||||
const hasMatchingChildren = item.sub_rows && item.sub_rows.length > 0;
|
const hasMatchingChildren = item.sub_rows && item.sub_rows.length > 0;
|
||||||
|
|
||||||
return parentMatches || hasMatchingChildren;
|
return parentMatches || hasMatchingChildren;
|
||||||
});
|
});
|
||||||
}, [archivedData, isArchiveMode, searchQuery, selectedBranch, selectedStatus, tableData]);
|
};
|
||||||
|
const filteredData = getFilteredData();
|
||||||
|
|
||||||
const onEdit = useCallback((row) => {
|
const handleRowClick = (row) => {
|
||||||
|
if (row.depth === 0 && row) {
|
||||||
|
setSelectedRow(row);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChildOfSelectedRow = (row) => {
|
||||||
|
if (!selectedRow) return false;
|
||||||
|
const parentRowId = row.parentId;
|
||||||
|
return parentRowId === selectedRow.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEdit = (row) => {
|
||||||
const isSmeta = row.depth > 0;
|
const isSmeta = row.depth > 0;
|
||||||
const parentProject = isSmeta ? row.getParentRow()?.original : row.original;
|
const parentProject = isSmeta ? row.getParentRow()?.original : row.original;
|
||||||
const projectId = parentProject?.id;
|
const projectId = parentProject?.id;
|
||||||
@ -275,11 +211,11 @@ const NavigationProjectPage = () => {
|
|||||||
_projectFundingByKoDecision: parentProject?.funding_by_ko_decision,
|
_projectFundingByKoDecision: parentProject?.funding_by_ko_decision,
|
||||||
});
|
});
|
||||||
setEditModalOpen(true);
|
setEditModalOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onNavigate = useCallback((row) => {
|
const onNavigate = (row) => {
|
||||||
handleNavigateClick(row, navigate);
|
handleNavigateClick(row, navigate);
|
||||||
}, [navigate]);
|
};
|
||||||
|
|
||||||
const handleDeleteProject = async () => {
|
const handleDeleteProject = async () => {
|
||||||
if (!projectToDelete?.id) return;
|
if (!projectToDelete?.id) return;
|
||||||
@ -345,24 +281,241 @@ const NavigationProjectPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const firstColumns = useMemo(
|
||||||
|
() => createFirstColumns({ onDelete: setProjectToDelete, onEdit, onNavigate, orgUnitNames }),
|
||||||
|
[orgUnitNames],
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableHeadCellStyles = {
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: '12px',
|
||||||
|
lineHeight: 1.15,
|
||||||
|
textAlign: 'left',
|
||||||
|
backgroundColor: 'rgb(248, 249, 250)',
|
||||||
|
borderRight: '1px solid #e0e0e0',
|
||||||
|
'&:last-child': {
|
||||||
|
borderRight: 'none',
|
||||||
|
},
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
boxShadow: 'none',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
'& .MuiTableSortLabel-icon': {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
'& .MuiTableSortLabel-iconDirectionDesc': {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
'& .MuiTableSortLabel-iconDirectionAsc': {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectsTableOptions = {
|
||||||
|
enableColumnActions: false,
|
||||||
|
enableColumnFilters: false,
|
||||||
|
enablePagination: false,
|
||||||
|
enableSorting: true,
|
||||||
|
enableBottomToolbar: false,
|
||||||
|
enableTopToolbar: false,
|
||||||
|
enableExpanding: true,
|
||||||
|
getSubRows: (row) => row.sub_rows,
|
||||||
|
muiTableHeadCellProps: {
|
||||||
|
sx: tableHeadCellStyles,
|
||||||
|
},
|
||||||
|
muiExpandButtonProps: {
|
||||||
|
sx: {
|
||||||
|
width: '1.5rem',
|
||||||
|
height: '1.5rem',
|
||||||
|
minWidth: '1.5rem',
|
||||||
|
'& .MuiSvgIcon-root': {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
muiTableBodyRowProps: ({ row }) => {
|
||||||
|
const isSelected = selectedRow && row.id === selectedRow.id;
|
||||||
|
const isChild = isChildOfSelectedRow(row);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sx: {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgb(248, 250, 252)',
|
||||||
|
},
|
||||||
|
'&:hover td::after': {
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
|
},
|
||||||
|
...(row.depth > 0 && {
|
||||||
|
backgroundColor: 'rgb(252, 252, 253)',
|
||||||
|
}),
|
||||||
|
...((isSelected || isChild) && {
|
||||||
|
backgroundColor: 'rgb(234, 247, 236)',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgb(225, 242, 229) !important',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onClick: () => handleRowClick(row),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
muiTableBodyCellProps: ({ row, column }) => ({
|
||||||
|
sx: {
|
||||||
|
borderRight: '1px solid #e0e0e0',
|
||||||
|
'&:last-child': {
|
||||||
|
borderRight: 'none',
|
||||||
|
},
|
||||||
|
'&:hover td::after': {
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
|
},
|
||||||
|
textAlign: 'left',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
...(column.id === 'mrt-row-expand' && {
|
||||||
|
width: '1.5rem',
|
||||||
|
maxWidth: '1.5rem',
|
||||||
|
minWidth: '1.5rem',
|
||||||
|
padding: '0.5rem 0.25rem',
|
||||||
|
}),
|
||||||
|
...(column.id === 'project' &&
|
||||||
|
row.depth > 0 && {
|
||||||
|
pl: `${2 + row.depth * 0.5}rem`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
muiTablePaperProps: {
|
||||||
|
elevation: 0,
|
||||||
|
sx: {
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '1rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '45vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
muiTableContainerProps: {
|
||||||
|
sx: {
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
'& thead': {
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const summaryProjectTableOptions = {
|
||||||
|
enableColumnActions: false,
|
||||||
|
enableColumnFilters: false,
|
||||||
|
enablePagination: false,
|
||||||
|
enableSorting: true,
|
||||||
|
enableBottomToolbar: false,
|
||||||
|
enableTopToolbar: false,
|
||||||
|
enableExpanding: false,
|
||||||
|
muiTableBodyRowProps: ({ row }) => ({
|
||||||
|
sx: {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgb(248, 250, 252)',
|
||||||
|
},
|
||||||
|
'&:hover td::after': {
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
|
},
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
muiTableHeadCellProps: {
|
||||||
|
sx: tableHeadCellStyles,
|
||||||
|
},
|
||||||
|
muiTableBodyCellProps: ({ column }) => ({
|
||||||
|
sx: {
|
||||||
|
borderRight: '1px solid #e0e0e0',
|
||||||
|
'&:last-child': {
|
||||||
|
borderRight: 'none',
|
||||||
|
},
|
||||||
|
textAlign: 'left',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
muiTablePaperProps: {
|
||||||
|
elevation: 0,
|
||||||
|
sx: {
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '0 0 1rem 1rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '100%',
|
||||||
|
minHeight: 0,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
muiTableContainerProps: {
|
||||||
|
sx: {
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
'& thead': {
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initialState: {
|
||||||
|
columnPinning: { left: ['project'] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectsTable = useMaterialReactTable({
|
||||||
|
columns: firstColumns,
|
||||||
|
data: filteredData,
|
||||||
|
...projectsTableOptions,
|
||||||
|
localization: {
|
||||||
|
expand: 'Раскрыть',
|
||||||
|
expandAll: 'Раскрыть все',
|
||||||
|
collapse: 'Свернуть',
|
||||||
|
collapseAll: 'Свернуть все',
|
||||||
|
noRecordsToDisplay: 'Нет данных для отображения',
|
||||||
|
},
|
||||||
|
initialState: {
|
||||||
|
columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] },
|
||||||
|
},
|
||||||
|
state: { isLoading: isLoading || isProjectsLoading },
|
||||||
|
});
|
||||||
|
|
||||||
|
const summaryProjectTable = useMaterialReactTable({
|
||||||
|
columns: secondColumns,
|
||||||
|
data: summaryData,
|
||||||
|
...summaryProjectTableOptions,
|
||||||
|
localization: {
|
||||||
|
noRecordsToDisplay: 'Нет данных для отображения',
|
||||||
|
},
|
||||||
|
initialState: {
|
||||||
|
columnPinning: { left: ['data.header.name'] },
|
||||||
|
},
|
||||||
|
state: { isLoading: isSummaryLoading },
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSelectedProjectTitle = () => {
|
||||||
|
if (selectedRow?.original.name) {
|
||||||
|
return selectedRow.original.name;
|
||||||
|
}
|
||||||
|
return 'Проект не выбран';
|
||||||
|
};
|
||||||
|
|
||||||
// Обработчики фильтров
|
// Обработчики фильтров
|
||||||
const handleBranchChange = (value) => {
|
const handleBranchChange = (value) => {
|
||||||
setSelectedRow(null);
|
setSelectedRow(null);
|
||||||
setSummaryData([]);
|
setSummaryData([]);
|
||||||
setSelectedBranch(value);
|
setSelectedBranch(value);
|
||||||
setArchivedPagination((current) => ({ ...current, pageIndex: 0 }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchChange = (value) => {
|
const handleSearchChange = (value) => {
|
||||||
setSearchQuery(value);
|
setSearchQuery(value);
|
||||||
setArchivedPagination((current) => ({ ...current, pageIndex: 0 }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStatusChange = (value) => {
|
|
||||||
setSelectedRow(null);
|
|
||||||
setSummaryData([]);
|
|
||||||
setSelectedStatus(value);
|
|
||||||
setArchivedPagination((current) => ({ ...current, pageIndex: 0 }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateProject = async (formData) => {
|
const handleCreateProject = async (formData) => {
|
||||||
@ -406,34 +559,67 @@ const NavigationProjectPage = () => {
|
|||||||
onBranchChange={handleBranchChange}
|
onBranchChange={handleBranchChange}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
selectedStatus={selectedStatus}
|
|
||||||
onStatusChange={handleStatusChange}
|
|
||||||
/>
|
/>
|
||||||
<PrimaryButton onClick={() => setCreateModalOpen(true)} startIcon={<WhitePlus />} sx={{ height: '2.5rem' }}>
|
<PrimaryButton onClick={() => setCreateModalOpen(true)} startIcon={<WhitePlus />} sx={{ height: '2.5rem' }}>
|
||||||
Создать проект
|
Создать проект
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<ProjectsTable
|
{/* Первая таблица */}
|
||||||
data={filteredData}
|
<Paper
|
||||||
isLoading={isLoading || isProjectsLoading}
|
sx={{
|
||||||
isArchiveMode={isArchiveMode}
|
mb: '2rem',
|
||||||
archivedTotalCount={archivedTotalCount}
|
overflow: 'hidden',
|
||||||
archivedPagination={archivedPagination}
|
flex: '0 0 45vh',
|
||||||
onPaginationChange={setArchivedPagination}
|
borderRadius: '1rem',
|
||||||
selectedRow={selectedRow}
|
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
||||||
onRowSelect={setSelectedRow}
|
}}>
|
||||||
onDelete={setProjectToDelete}
|
<MaterialReactTable table={projectsTable} />
|
||||||
onEdit={onEdit}
|
</Paper>
|
||||||
onNavigate={onNavigate}
|
|
||||||
orgUnitNames={orgUnitNames}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProjectSummaryTable
|
{/* Вторая таблица */}
|
||||||
data={summaryData}
|
<Paper
|
||||||
isLoading={isSummaryLoading}
|
sx={{
|
||||||
selectedRow={selectedRow}
|
overflow: 'hidden',
|
||||||
/>
|
flex: '0 0 35vh',
|
||||||
|
borderRadius: '1rem',
|
||||||
|
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}>
|
||||||
|
<Typography
|
||||||
|
variant='subtitle1'
|
||||||
|
sx={{
|
||||||
|
p: '0.75rem 1.5rem',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderBottom: '1px solid #e0e0e0',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
flexShrink: 0,
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
}}>
|
||||||
|
{getSelectedProjectTitle()}
|
||||||
|
</Typography>
|
||||||
|
{selectedRow ? (
|
||||||
|
<Box sx={{ height: 'calc(35vh - 3rem)' }}>
|
||||||
|
{isSummaryLoading && !summaryData.length ? (
|
||||||
|
<Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<CircularProgress size={32} />
|
||||||
|
</Box>
|
||||||
|
) : <MaterialReactTable table={summaryProjectTable} />}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: 'rgb(152, 162, 179)',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}>
|
||||||
|
Разверните или выберите проект в верхней таблице
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
|
||||||
{/* Модалка редактирования */}
|
{/* Модалка редактирования */}
|
||||||
<EditModal open={editModalOpen} onClose={() => setEditModalOpen(false)} rowData={selectedRowData} onSave={onSaveEdit} isSaving={isSaving} orgUnitNames={orgUnitNames} />
|
<EditModal open={editModalOpen} onClose={() => setEditModalOpen(false)} rowData={selectedRowData} onSave={onSaveEdit} isSaving={isSaving} orgUnitNames={orgUnitNames} />
|
||||||
@ -468,4 +654,4 @@ const NavigationProjectPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NavigationProjectPage;
|
export default SummaryPage;
|
||||||
@ -3,12 +3,9 @@ import { Box, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Men
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useAuth } from '../../../../app/context/AuthProvider';
|
import { useAuth } from '../../../../app/context/AuthProvider';
|
||||||
import { DangerOutlinedButton, PrimaryButton } from '../../../../components/common/Buttons/Buttons';
|
import { DangerOutlinedButton, PrimaryButton } from '../../../../components/common/Buttons/Buttons';
|
||||||
import { PROJECT_STATUSES, PROJECT_TYPES } from '../../../../constants/constants';
|
|
||||||
import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../constants';
|
import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../constants';
|
||||||
import { canEditFirstTableField, hasReserveValuesInChildSmetas, isFirstTableFieldDependencySatisfied } from '../../constants/fieldAccess';
|
import { canEditFirstTableField, hasReserveValuesInChildSmetas, isFirstTableFieldDependencySatisfied } from '../../constants/fieldAccess';
|
||||||
|
|
||||||
const PROJECT_STATUS_OPTIONS = ['created', 'agreed', 'archived'].map((status) => [status, PROJECT_STATUSES[status]]);
|
|
||||||
|
|
||||||
const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNames = {} }) => {
|
const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNames = {} }) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [formData, setFormData] = React.useState(rowData || {});
|
const [formData, setFormData] = React.useState(rowData || {});
|
||||||
@ -56,10 +53,10 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa
|
|||||||
|
|
||||||
const projectFields = [
|
const projectFields = [
|
||||||
{ key: 'name', label: 'Проект' },
|
{ key: 'name', label: 'Проект' },
|
||||||
{ key: 'status', label: 'Статус', options: PROJECT_STATUS_OPTIONS },
|
{ key: 'status', label: 'Статус', options: [['created', 'Создан'], ['agreed', 'Согласован'], ['archived', 'В архиве']] },
|
||||||
{ key: 'technical_number', label: 'Технический номер проекта' },
|
{ key: 'technical_number', label: 'Технический номер проекта' },
|
||||||
{ key: 'org_unit_id', label: 'ССП/РФ', valueType: 'number', options: Object.entries(orgUnitNames).map(([id, title]) => [Number(id), title]) },
|
{ key: 'org_unit_id', label: 'ССП/РФ', valueType: 'number', options: Object.entries(orgUnitNames).map(([id, title]) => [Number(id), title]) },
|
||||||
{ key: 'project_type', label: 'Тип проекта', options: PROJECT_TYPES },
|
{ key: 'project_type', label: 'Тип проекта', options: ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'] },
|
||||||
{ key: 'vsp_format', label: 'Формат ВСП', options: ['Флагманский', 'Типовой', 'Розничный', 'МСБ', 'Лёгкий', 'Мини', 'Розничный-киоск', 'МБО', 'Офис самообслуживания', 'Другое'] },
|
{ key: 'vsp_format', label: 'Формат ВСП', options: ['Флагманский', 'Типовой', 'Розничный', 'МСБ', 'Лёгкий', 'Мини', 'Розничный-киоск', 'МБО', 'Офис самообслуживания', 'Другое'] },
|
||||||
{ key: 'placement_type', label: 'Размещение', options: ['Собственность', 'Аренда', 'Субаренда'] },
|
{ key: 'placement_type', label: 'Размещение', options: ['Собственность', 'Аренда', 'Субаренда'] },
|
||||||
{ key: 'staff_count', label: 'Количество сотрудников', type: 'number', valueType: 'number', min: 0 },
|
{ key: 'staff_count', label: 'Количество сотрудников', type: 'number', valueType: 'number', min: 0 },
|
||||||
|
|||||||
@ -1,148 +0,0 @@
|
|||||||
import { Box, CircularProgress, Paper, Typography } from '@mui/material';
|
|
||||||
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
|
||||||
import { secondColumns } from '../../columns';
|
|
||||||
|
|
||||||
const tableHeadCellStyles = {
|
|
||||||
fontWeight: 700,
|
|
||||||
fontSize: '12px',
|
|
||||||
lineHeight: 1.15,
|
|
||||||
textAlign: 'left',
|
|
||||||
backgroundColor: 'rgb(248, 249, 250)',
|
|
||||||
borderRight: '1px solid #e0e0e0',
|
|
||||||
'&:last-child': {
|
|
||||||
borderRight: 'none',
|
|
||||||
},
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
position: 'sticky',
|
|
||||||
top: 0,
|
|
||||||
boxShadow: 'none',
|
|
||||||
padding: '0.5rem 1rem',
|
|
||||||
'& .MuiTableSortLabel-icon': {
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
},
|
|
||||||
'& .MuiTableSortLabel-iconDirectionDesc': {
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
},
|
|
||||||
'& .MuiTableSortLabel-iconDirectionAsc': {
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectSummaryTable = ({ data, isLoading, selectedRow }) => {
|
|
||||||
const table = useMaterialReactTable({
|
|
||||||
columns: secondColumns,
|
|
||||||
data,
|
|
||||||
enableColumnActions: false,
|
|
||||||
enableColumnFilters: false,
|
|
||||||
enablePagination: false,
|
|
||||||
enableSorting: true,
|
|
||||||
enableBottomToolbar: false,
|
|
||||||
enableTopToolbar: false,
|
|
||||||
enableExpanding: false,
|
|
||||||
muiTableBodyRowProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgb(248, 250, 252)',
|
|
||||||
},
|
|
||||||
'&:hover td::after': {
|
|
||||||
backgroundColor: 'transparent !important',
|
|
||||||
},
|
|
||||||
boxShadow: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: tableHeadCellStyles,
|
|
||||||
},
|
|
||||||
muiTableBodyCellProps: {
|
|
||||||
sx: {
|
|
||||||
borderRight: '1px solid #e0e0e0',
|
|
||||||
'&:last-child': {
|
|
||||||
borderRight: 'none',
|
|
||||||
},
|
|
||||||
textAlign: 'left',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
padding: '0.5rem 1rem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
muiTablePaperProps: {
|
|
||||||
elevation: 0,
|
|
||||||
sx: {
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
borderRadius: '0 0 1rem 1rem',
|
|
||||||
overflow: 'hidden',
|
|
||||||
height: '100%',
|
|
||||||
minHeight: 0,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
muiTableContainerProps: {
|
|
||||||
sx: {
|
|
||||||
flex: 1,
|
|
||||||
overflow: 'auto',
|
|
||||||
'& thead': {
|
|
||||||
position: 'sticky',
|
|
||||||
top: 0,
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
localization: {
|
|
||||||
noRecordsToDisplay: 'Нет данных для отображения',
|
|
||||||
},
|
|
||||||
initialState: {
|
|
||||||
columnPinning: { left: ['data.header.name'] },
|
|
||||||
},
|
|
||||||
state: { isLoading },
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
overflow: 'hidden',
|
|
||||||
flex: '0 0 35vh',
|
|
||||||
borderRadius: '1rem',
|
|
||||||
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
|
||||||
}}>
|
|
||||||
<Typography
|
|
||||||
variant='subtitle1'
|
|
||||||
sx={{
|
|
||||||
p: '0.75rem 1.5rem',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderBottom: '1px solid #e0e0e0',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
flexShrink: 0,
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
}}>
|
|
||||||
{selectedRow?.original.name || 'Проект не выбран'}
|
|
||||||
</Typography>
|
|
||||||
{selectedRow ? (
|
|
||||||
<Box sx={{ height: 'calc(35vh - 3rem)' }}>
|
|
||||||
{isLoading && !data.length ? (
|
|
||||||
<Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
<CircularProgress size={32} />
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<MaterialReactTable table={table} />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: 'rgb(152, 162, 179)',
|
|
||||||
fontSize: '1rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
}}>
|
|
||||||
Разверните или выберите проект в верхней таблице
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectSummaryTable;
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
import { Paper } from '@mui/material';
|
|
||||||
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { createFirstColumns } from '../../columns';
|
|
||||||
|
|
||||||
const tableHeadCellStyles = {
|
|
||||||
fontWeight: 700,
|
|
||||||
fontSize: '12px',
|
|
||||||
lineHeight: 1.15,
|
|
||||||
textAlign: 'left',
|
|
||||||
backgroundColor: 'rgb(248, 249, 250)',
|
|
||||||
borderRight: '1px solid #e0e0e0',
|
|
||||||
'&:last-child': {
|
|
||||||
borderRight: 'none',
|
|
||||||
},
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
position: 'sticky',
|
|
||||||
top: 0,
|
|
||||||
boxShadow: 'none',
|
|
||||||
padding: '0.5rem 1rem',
|
|
||||||
'& .MuiTableSortLabel-icon': {
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
},
|
|
||||||
'& .MuiTableSortLabel-iconDirectionDesc': {
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
},
|
|
||||||
'& .MuiTableSortLabel-iconDirectionAsc': {
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectsTable = ({
|
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
isArchiveMode,
|
|
||||||
archivedTotalCount,
|
|
||||||
archivedPagination,
|
|
||||||
onPaginationChange,
|
|
||||||
selectedRow,
|
|
||||||
onRowSelect,
|
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
onNavigate,
|
|
||||||
orgUnitNames,
|
|
||||||
}) => {
|
|
||||||
const columns = useMemo(
|
|
||||||
() => createFirstColumns({ onDelete, onEdit, onNavigate, orgUnitNames }),
|
|
||||||
[onDelete, onEdit, onNavigate, orgUnitNames],
|
|
||||||
);
|
|
||||||
|
|
||||||
const table = useMaterialReactTable({
|
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
enableColumnActions: false,
|
|
||||||
enableColumnFilters: false,
|
|
||||||
enablePagination: isArchiveMode,
|
|
||||||
manualPagination: isArchiveMode,
|
|
||||||
enableSorting: true,
|
|
||||||
enableBottomToolbar: isArchiveMode,
|
|
||||||
enableTopToolbar: false,
|
|
||||||
enableExpanding: true,
|
|
||||||
getSubRows: (row) => row.sub_rows,
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: tableHeadCellStyles,
|
|
||||||
},
|
|
||||||
muiExpandButtonProps: {
|
|
||||||
sx: {
|
|
||||||
width: '1.5rem',
|
|
||||||
height: '1.5rem',
|
|
||||||
minWidth: '1.5rem',
|
|
||||||
'& .MuiSvgIcon-root': {
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
muiTableBodyRowProps: ({ row }) => {
|
|
||||||
const isSelected = selectedRow && row.id === selectedRow.id;
|
|
||||||
const isChild = selectedRow && row.parentId === selectedRow.id;
|
|
||||||
|
|
||||||
return {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgb(248, 250, 252)',
|
|
||||||
},
|
|
||||||
'&:hover td::after': {
|
|
||||||
backgroundColor: 'transparent !important',
|
|
||||||
},
|
|
||||||
...(row.depth > 0 && {
|
|
||||||
backgroundColor: 'rgb(252, 252, 253)',
|
|
||||||
}),
|
|
||||||
...((isSelected || isChild) && {
|
|
||||||
backgroundColor: 'rgb(234, 247, 236)',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgb(225, 242, 229) !important',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
onClick: () => {
|
|
||||||
if (row.depth === 0) onRowSelect(row);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
muiTableBodyCellProps: ({ row, column }) => ({
|
|
||||||
sx: {
|
|
||||||
borderRight: '1px solid #e0e0e0',
|
|
||||||
'&:last-child': {
|
|
||||||
borderRight: 'none',
|
|
||||||
},
|
|
||||||
'&:hover td::after': {
|
|
||||||
backgroundColor: 'transparent !important',
|
|
||||||
},
|
|
||||||
textAlign: 'left',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
padding: '0.5rem 1rem',
|
|
||||||
...(column.id === 'mrt-row-expand' && {
|
|
||||||
width: '1.5rem',
|
|
||||||
maxWidth: '1.5rem',
|
|
||||||
minWidth: '1.5rem',
|
|
||||||
padding: '0.5rem 0.25rem',
|
|
||||||
}),
|
|
||||||
...(column.id === 'project' &&
|
|
||||||
row.depth > 0 && {
|
|
||||||
pl: `${2 + row.depth * 0.5}rem`,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
muiTablePaperProps: {
|
|
||||||
elevation: 0,
|
|
||||||
sx: {
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
borderRadius: '1rem',
|
|
||||||
overflow: 'hidden',
|
|
||||||
height: isArchiveMode ? 'auto' : '45vh',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
muiTableContainerProps: {
|
|
||||||
sx: {
|
|
||||||
flex: 1,
|
|
||||||
overflowX: 'auto',
|
|
||||||
overflowY: isArchiveMode ? 'visible' : 'auto',
|
|
||||||
'& thead': {
|
|
||||||
position: 'sticky',
|
|
||||||
top: 0,
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
localization: {
|
|
||||||
expand: 'Раскрыть',
|
|
||||||
expandAll: 'Раскрыть все',
|
|
||||||
collapse: 'Свернуть',
|
|
||||||
collapseAll: 'Свернуть все',
|
|
||||||
noRecordsToDisplay: 'Нет данных для отображения',
|
|
||||||
rowsPerPage: 'Строк на странице',
|
|
||||||
of: 'из',
|
|
||||||
},
|
|
||||||
initialState: {
|
|
||||||
columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] },
|
|
||||||
},
|
|
||||||
...(isArchiveMode
|
|
||||||
? {
|
|
||||||
rowCount: archivedTotalCount,
|
|
||||||
onPaginationChange,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
state: {
|
|
||||||
isLoading,
|
|
||||||
...(isArchiveMode ? { pagination: archivedPagination } : {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
mb: '2rem',
|
|
||||||
overflow: 'hidden',
|
|
||||||
flex: isArchiveMode ? '0 0 auto' : '0 0 45vh',
|
|
||||||
borderRadius: '1rem',
|
|
||||||
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
|
||||||
}}>
|
|
||||||
<MaterialReactTable table={table} />
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectsTable;
|
|
||||||
@ -1,22 +1,26 @@
|
|||||||
import { PROJECT_STATUSES } from '../../../../constants/constants';
|
|
||||||
import styles from './StatusBadge.module.css';
|
import styles from './StatusBadge.module.css';
|
||||||
|
|
||||||
const StatusBadge = ({ status, className = '', ...props }) => {
|
const StatusBadge = ({ status, className = '', ...props }) => {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
deleted: {
|
deleted: {
|
||||||
class: styles.deleted,
|
class: styles.deleted,
|
||||||
|
label: 'Удален',
|
||||||
},
|
},
|
||||||
approved: {
|
approved: {
|
||||||
class: styles.approved,
|
class: styles.approved,
|
||||||
|
label: 'Согласован',
|
||||||
},
|
},
|
||||||
agreed: {
|
agreed: {
|
||||||
class: styles.approved,
|
class: styles.approved,
|
||||||
|
label: 'Согласован',
|
||||||
},
|
},
|
||||||
archived: {
|
archived: {
|
||||||
class: styles.archived,
|
class: styles.archived,
|
||||||
|
label: 'В архиве',
|
||||||
},
|
},
|
||||||
created: {
|
created: {
|
||||||
class: styles.created,
|
class: styles.created,
|
||||||
|
label: 'Создан',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,7 +31,7 @@ const StatusBadge = ({ status, className = '', ...props }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`${styles.badge} ${currentStatus.class} ${className}`} {...props}>
|
<span className={`${styles.badge} ${currentStatus.class} ${className}`} {...props}>
|
||||||
{PROJECT_STATUSES[status]}
|
{currentStatus.label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,21 +1,12 @@
|
|||||||
import { Search } from '@mui/icons-material';
|
import { Search } from '@mui/icons-material';
|
||||||
import { Autocomplete, Box, Chip, CircularProgress, MenuItem, Paper, TextField } from '@mui/material';
|
import { Autocomplete, Box, Chip, CircularProgress, Paper, TextField } from '@mui/material';
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { SspApi } from '../../../../api/ssp';
|
import { SspApi } from '../../../../api/ssp';
|
||||||
import { useAuth } from '../../../../app/context/AuthProvider';
|
import { useAuth } from '../../../../app/context/AuthProvider';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { PROJECT_STATUSES, ROLES_NAME_ID } from '../../../../constants/constants';
|
import { ROLES_NAME_ID } from '../../../../constants/constants';
|
||||||
|
|
||||||
const FILTER_PROJECT_STATUSES = ['created', 'agreed', 'archived'];
|
const TableFilters = ({ onBranchChange, onSearchChange, selectedBranch, searchQuery }) => {
|
||||||
|
|
||||||
const TableFilters = ({
|
|
||||||
onBranchChange,
|
|
||||||
onSearchChange,
|
|
||||||
onStatusChange,
|
|
||||||
selectedBranch,
|
|
||||||
selectedStatus,
|
|
||||||
searchQuery,
|
|
||||||
}) => {
|
|
||||||
const [branches, setBranches] = useState([]);
|
const [branches, setBranches] = useState([]);
|
||||||
const [branchInputValue, setBranchInputValue] = useState('');
|
const [branchInputValue, setBranchInputValue] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@ -147,26 +138,7 @@ const TableFilters = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
{(selectedBranch || searchQuery) && (
|
||||||
select
|
|
||||||
size='small'
|
|
||||||
label='Статус'
|
|
||||||
value={selectedStatus}
|
|
||||||
onChange={(event) => onStatusChange(event.target.value)}
|
|
||||||
sx={{
|
|
||||||
minWidth: 180,
|
|
||||||
maxWidth: 220,
|
|
||||||
flex: '1 1 auto',
|
|
||||||
}}>
|
|
||||||
<MenuItem value=''>Все статусы</MenuItem>
|
|
||||||
{FILTER_PROJECT_STATUSES.map((status) => (
|
|
||||||
<MenuItem key={status} value={status}>
|
|
||||||
{PROJECT_STATUSES[status]}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
{(selectedBranch || searchQuery || selectedStatus) && (
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, ml: 'auto' }}>
|
<Box sx={{ display: 'flex', gap: 1, ml: 'auto' }}>
|
||||||
<Chip
|
<Chip
|
||||||
label='Сбросить фильтры'
|
label='Сбросить фильтры'
|
||||||
@ -175,7 +147,6 @@ const TableFilters = ({
|
|||||||
setBranchInputValue('');
|
setBranchInputValue('');
|
||||||
onBranchChange('');
|
onBranchChange('');
|
||||||
onSearchChange('');
|
onSearchChange('');
|
||||||
onStatusChange('');
|
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user