fix: ограничить управление строками и этапами по правам доступа

This commit is contained in:
PotapovaA 2026-09-01 15:14:43 +03:00
parent d66e4fe375
commit ee3b87c69a
11 changed files with 99 additions and 92 deletions

View File

@ -8,7 +8,6 @@ import DictsNavigatorPage from '../pages/DictsNavigatorPage/DictsNavigatorPage.j
import FormsPage from '../pages/FormsPage'; 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 NavigationProjectPage from '../pages/ProjectPages/NavigationProjectPage.jsx'; import NavigationProjectPage from '../pages/ProjectPages/NavigationProjectPage.jsx';
import Svod2Page from '../pages/Svod2Page/Svod2Page.jsx'; import Svod2Page from '../pages/Svod2Page/Svod2Page.jsx';
import { SvodProvider } from '../pages/SvodPage/SvodContext.jsx'; import { SvodProvider } from '../pages/SvodPage/SvodContext.jsx';
@ -52,7 +51,6 @@ export const AppRoutes = () => {
<Route path='/admin_panel/audit-log' element={<AuditLogsPage />} /> <Route path='/admin_panel/audit-log' element={<AuditLogsPage />} />
<Route path='/dicts/dict-vsp' element={<DictVspPage />} /> <Route path='/dicts/dict-vsp' element={<DictVspPage />} />
<Route path='/dicts-navigator' element={<DictsNavigatorPage />} /> <Route path='/dicts-navigator' element={<DictsNavigatorPage />} />
<Route path='/new-table' element={<NewTablePage />} />
<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 />} />

View File

@ -26,6 +26,7 @@ import { getRowId } from './utils/rowUtils';
import { DictVspApi } from '../../api/dict-vsp'; 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 { FormsNoCanAddRow } from './constants/formConfig'; import { FormsNoCanAddRow } from './constants/formConfig';
const CONFIG_CACHE = new Map(); const CONFIG_CACHE = new Map();
@ -121,6 +122,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const [createModalType, setCreateModalType] = useState(null); const [createModalType, setCreateModalType] = useState(null);
const isFormNoCanAddRow = useMemo(() => FormsNoCanAddRow[formType]?.includes(sheetName) ?? false, [formType, sheetName]); const isFormNoCanAddRow = useMemo(() => FormsNoCanAddRow[formType]?.includes(sheetName) ?? false, [formType, sheetName]);
const canModifyRows = userRoleId === ROLES_NAME_ID.admin || columnsCurStage.length > 0;
const isVspAdditionRow = useMemo(() => { const isVspAdditionRow = useMemo(() => {
if (!formType || !sheetName) return false; if (!formType || !sheetName) return false;
@ -664,6 +666,10 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
}, [getSelectedRow]); }, [getSelectedRow]);
const handleDeleteRow = useCallback(() => { const handleDeleteRow = useCallback(() => {
if (!canModifyRows) {
toast.error('Удаление строк недоступно: нет доступных для редактирования столбцов');
return;
}
const row = getSelectedRow(); const row = getSelectedRow();
if (!row) { if (!row) {
toast.error('Выделите строку для удаления'); toast.error('Выделите строку для удаления');
@ -671,9 +677,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
} }
contextDeleteRow(getRowId(row)); contextDeleteRow(getRowId(row));
setRowSelection({}); setRowSelection({});
}, [contextDeleteRow, getSelectedRow]); }, [canModifyRows, contextDeleteRow, getSelectedRow]);
const addRow = useCallback(() => { const addRow = useCallback(() => {
if (!canModifyRows) {
toast.error('Добавление строк недоступно: нет доступных для редактирования столбцов');
return;
}
if (isVspAdditionRow) { if (isVspAdditionRow) {
return handleOpenModalSelectVsp(); return handleOpenModalSelectVsp();
} }
@ -681,13 +691,21 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
return handleOpenModalSelectExpenseItem(); return handleOpenModalSelectExpenseItem();
} }
return handleAddRow(); return handleAddRow();
}, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow, handleOpenModalSelectExpenseItem]); }, [canModifyRows, isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow, handleOpenModalSelectExpenseItem]);
const addProgram = useCallback(() => { const addProgram = useCallback(() => {
if (!canModifyRows) {
toast.error('Добавление строк недоступно: нет доступных для редактирования столбцов');
return;
}
setCreateModalType('program'); setCreateModalType('program');
}, []); }, [canModifyRows]);
const addProject = useCallback(() => { const addProject = useCallback(() => {
if (!canModifyRows) {
toast.error('Добавление строк недоступно: нет доступных для редактирования столбцов');
return;
}
const row = getSelectedRow(); const row = getSelectedRow();
if (!row) { if (!row) {
toast.error('Выделите строку для вставки'); toast.error('Выделите строку для вставки');
@ -698,7 +716,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
return; return;
} }
setCreateModalType('project'); setCreateModalType('project');
}, [getSelectedRow]); }, [canModifyRows, getSelectedRow]);
const closeSelectVspModal = useCallback(() => setIsOpenModalSelectVsp(false), []); const closeSelectVspModal = useCallback(() => setIsOpenModalSelectVsp(false), []);
const closeSelectExpenseItemModal = useCallback(() => setIsOpenModalSelectExpenseItem(false), []); const closeSelectExpenseItemModal = useCallback(() => setIsOpenModalSelectExpenseItem(false), []);
@ -777,6 +795,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
direction={direction} direction={direction}
year={year} year={year}
isFormNoCanAddRow={isFormNoCanAddRow} isFormNoCanAddRow={isFormNoCanAddRow}
canModifyRows={canModifyRows}
formType={formType} formType={formType}
validationErrors={validationErrors} validationErrors={validationErrors}
onNavigateToColumn={handleNavigateToColumn} onNavigateToColumn={handleNavigateToColumn}

View File

@ -90,6 +90,7 @@ const SettingPanel = ({
direction, direction,
year, year,
isFormNoCanAddRow, isFormNoCanAddRow,
canModifyRows,
formType, formType,
validationErrors = [], validationErrors = [],
onNavigateToColumn, onNavigateToColumn,
@ -178,7 +179,7 @@ const SettingPanel = ({
<Divider orientation='vertical' flexItem /> <Divider orientation='vertical' flexItem />
<GroupByObject title='Строки'> <GroupByObject title='Строки'>
{!isFormNoCanAddRow && ( {!isFormNoCanAddRow && canModifyRows && (
<> <>
<Tooltip title='Добавить строку'> <Tooltip title='Добавить строку'>
<IconButton onClick={onAddRow} variant='outlined' sx={controlSx}> <IconButton onClick={onAddRow} variant='outlined' sx={controlSx}>

View File

@ -1,8 +1,9 @@
import { Box, CircularProgress, Stack } from '@mui/material'; import { Box, CircularProgress, Stack } from '@mui/material';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAuth } from '../../app/context/AuthProvider';
import { useTaskStages } from '../../hooks/useTaskStages'; import { useTaskStages } from '../../hooks/useTaskStages';
import { useProjectStages } from '../../hooks/useProjectStages'; import { useProjectStages } from '../../hooks/useProjectStages';
import { SHEET_NAME } from '../../constants/constants'; import { ROLES_NAME_ID, SHEET_NAME } from '../../constants/constants';
import { PrimaryButton } from '../common/Buttons/Buttons'; import { PrimaryButton } from '../common/Buttons/Buttons';
import Modal from '../common/Modal/Modal'; import Modal from '../common/Modal/Modal';
import { EditContainer } from '../common/Modal/ModalStyled'; import { EditContainer } from '../common/Modal/ModalStyled';
@ -23,6 +24,8 @@ export const SettingModal = ({
formTypeCode = '', formTypeCode = '',
sheets, sheets,
}) => { }) => {
const { user } = useAuth();
const isAdmin = user?.role_id === ROLES_NAME_ID.admin;
const taskStages = useTaskStages(taskId); const taskStages = useTaskStages(taskId);
const projectStages = useProjectStages(projectId, year, reportType); const projectStages = useProjectStages(projectId, year, reportType);
const stages = mode === 'project' ? projectStages : taskStages; const stages = mode === 'project' ? projectStages : taskStages;
@ -137,11 +140,13 @@ export const SettingModal = ({
<> <>
<Modal open={isOpen} onClose={onClose} title={modalTitle} customSize={50}> <Modal open={isOpen} onClose={onClose} title={modalTitle} customSize={50}>
<EditContainer style={{ margin: 0 }}> <EditContainer style={{ margin: 0 }}>
{isAdmin && (
<Stack direction='row' sx={{ width: '100%', justifyContent: 'flex-end' }}> <Stack direction='row' sx={{ width: '100%', justifyContent: 'flex-end' }}>
<PrimaryButton onClick={handleAddStage} startIcon={<WhitePlus />}> <PrimaryButton onClick={handleAddStage} startIcon={<WhitePlus />}>
Добавить этап Добавить этап
</PrimaryButton> </PrimaryButton>
</Stack> </Stack>
)}
{stagesInfo.isLoading ? ( {stagesInfo.isLoading ? (
<Box <Box
@ -159,6 +164,7 @@ export const SettingModal = ({
onDelete={!isTaskMode ? handleDeleteStage : undefined} onDelete={!isTaskMode ? handleDeleteStage : undefined}
/> />
)} )}
{isAdmin && (
<AddStagesModal <AddStagesModal
isOpen={isAddEditModalOpen} isOpen={isAddEditModalOpen}
onClose={() => { onClose={() => {
@ -173,6 +179,7 @@ export const SettingModal = ({
formType={formTypeCode} formType={formTypeCode}
defaultSheet={mode === 'project' ? reportType : ''} defaultSheet={mode === 'project' ? reportType : ''}
/> />
)}
{!isTaskMode && ( {!isTaskMode && (
<DeleteStageModal <DeleteStageModal
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}

View File

@ -17,7 +17,7 @@ const TableCard = ({ table, onNavigate, action }) => {
</div> </div>
<SectionTitle> <SectionTitle>
<h2>{table.name}</h2> <h2>{table.name}</h2>
<h2>{DIRECTION_TRANSLATE[table?.direction]}</h2> <h2>{DIRECTION_TRANSLATE[table?.direction] || ''}</h2>
{/* <UsersList> {/* <UsersList>
<div className="img-user"></div> <div className="img-user"></div>
</UsersList> */} </UsersList> */}

View File

@ -167,7 +167,8 @@ export const EditUserModal = ({ isOpen, onClose, onConfirm, user, onEditSsp }) =
variant='outlined' variant='outlined'
size='small' size='small'
autoFocus autoFocus
InputProps={{ slotProps={{
input: {
endAdornment: ( endAdornment: (
<InputAdornment position='end'> <InputAdornment position='end'>
<IconButton edge='end' onClick={() => toggleEdit(field)} size='small' sx={{ padding: '4px' }}> <IconButton edge='end' onClick={() => toggleEdit(field)} size='small' sx={{ padding: '4px' }}>
@ -175,6 +176,7 @@ export const EditUserModal = ({ isOpen, onClose, onConfirm, user, onEditSsp }) =
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
), ),
},
}} }}
/> />
</Box> </Box>

View File

@ -10,7 +10,7 @@ import { DIRECTION_TRANSLATE, SHEET_NAME } from '../constants/constants';
const TableInfo = ({ formId, sheetName, direction, isProject = false }) => { const TableInfo = ({ formId, sheetName, direction, isProject = false }) => {
const path = (isProject ? '/project/' : '/task/') + formId; const path = (isProject ? '/project/' : '/task/') + formId;
const tableName = SHEET_NAME[sheetName] || sheetName; const tableName = SHEET_NAME[sheetName] || sheetName;
const directionName = DIRECTION_TRANSLATE[direction] || direction; const directionName = DIRECTION_TRANSLATE[direction] || '';
const portalContent = ( const portalContent = (
<TaskInfoContainer> <TaskInfoContainer>
<BackButton to={path} /> <BackButton to={path} />

View File

@ -1,27 +0,0 @@
import { createPortal } from 'react-dom';
import { TaskInfoContainer } from '../components/common/SwitchFormTask/SwitchFormTask.style';
import RealtimeTable from '../components/RealtimeTable';
const TableInfo = () => {
const portalContent = (
<TaskInfoContainer>
{/* <BackButton to={`/task/${table.task_id}`} />
<NameTask>{table.name}</NameTask> */}
</TaskInfoContainer>
);
const targetElement = document.getElementById('TableInfo');
if (targetElement) {
return createPortal(portalContent, targetElement);
}
};
export default function NewTablePage() {
return (
<>
<RealtimeTable />
</>
);
}

View File

@ -93,21 +93,23 @@ const TableFilters = ({
onBranchChange(newValue || ''); onBranchChange(newValue || '');
}} }}
renderInput={(params) => { renderInput={(params) => {
const { InputProps, ...rest } = params;
return ( return (
<TextField <TextField
{...rest} {...params}
placeholder='Региональные филиалы' placeholder='Региональные филиалы'
variant='outlined' variant='outlined'
size='small' size='small'
InputProps={{ slotProps={{
...InputProps, ...params.slotProps,
input: {
...params.slotProps.input,
endAdornment: ( endAdornment: (
<> <>
{isLoading && <CircularProgress color='inherit' size={20} />} {isLoading && <CircularProgress color='inherit' size={20} />}
{InputProps?.endAdornment} {params.slotProps.input.endAdornment}
</> </>
), ),
},
}} }}
/> />
); );

View File

@ -85,12 +85,12 @@ export function SvodFilters({
onChange({ selectedOrganizations: selectedAll ? [SELECT_ALL_OPTION] : value }); onChange({ selectedOrganizations: selectedAll ? [SELECT_ALL_OPTION] : value });
}} }}
sx={{ minWidth: '18.75rem', flex: '1 1 22.5rem', maxWidth: '32.5rem' }} sx={{ minWidth: '18.75rem', flex: '1 1 22.5rem', maxWidth: '32.5rem' }}
renderTags={(value, getTagProps) => renderValue={(value, getItemProps) =>
value value
.slice(0, 2) .slice(0, 2)
.map((option, index) => { .map((option, index) => {
const tagProps = getTagProps({ index }); const { key, ...itemProps } = getItemProps({ index });
return <Chip {...tagProps} key={option.id} label={option.title} size='small' />; return <Chip {...itemProps} key={option.id || key} label={option.title} size='small' />;
}) })
.concat(value.length > 2 ? [<Chip key='more' label={`+${value.length - 2}`} size='small' />] : []) .concat(value.length > 2 ? [<Chip key='more' label={`+${value.length - 2}`} size='small' />] : [])
} }
@ -99,14 +99,17 @@ export function SvodFilters({
{...params} {...params}
label='ССП/РФ' label='ССП/РФ'
placeholder={selectedOrganizations.length ? '' : emptyOrganizationsPlaceholder} placeholder={selectedOrganizations.length ? '' : emptyOrganizationsPlaceholder}
InputProps={{ slotProps={{
...(params.InputProps || {}), ...params.slotProps,
input: {
...params.slotProps.input,
endAdornment: ( endAdornment: (
<> <>
{organizationsLoading && <CircularProgress size='1.125rem' />} {organizationsLoading && <CircularProgress size='1.125rem' />}
{params.InputProps?.endAdornment} {params.slotProps.input.endAdornment}
</> </>
), ),
},
}} }}
/> />
)} )}
@ -117,12 +120,14 @@ export function SvodFilters({
onChange={(event) => onChange({ search: event.target.value })} onChange={(event) => onChange({ search: event.target.value })}
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
sx={{ minWidth: '16.25rem', flex: '1 1 17.5rem', maxWidth: '23.75rem' }} sx={{ minWidth: '16.25rem', flex: '1 1 17.5rem', maxWidth: '23.75rem' }}
InputProps={{ slotProps={{
input: {
startAdornment: ( startAdornment: (
<InputAdornment position='start'> <InputAdornment position='start'>
<SearchRoundedIcon sx={{ color: '#99a1af' }} /> <SearchRoundedIcon sx={{ color: '#99a1af' }} />
</InputAdornment> </InputAdornment>
), ),
},
}} }}
/> />
{children} {children}

View File

@ -44,7 +44,7 @@ const formatValue = (value) => {
const getShapeForSheet = (sheet) => { const getShapeForSheet = (sheet) => {
const directionShape = { q1: 0, q2: 0, q3: 0, q4: 0, year: 0 }; const directionShape = { q1: 0, q2: 0, q3: 0, q4: 0, year: 0 };
const correctedShape = sheet === 'FORM_4' ? directionShape : { q2: 0, q3: 0, q4: 0 }; const correctedShape = { q2: 0, q3: 0, q4: 0 };
return { return {
plan: { support: directionShape, development: directionShape, ...(['FORM_1', 'MAIN'].includes(sheet) ? { total_year: 0 } : {}) }, plan: { support: directionShape, development: directionShape, ...(['FORM_1', 'MAIN'].includes(sheet) ? { total_year: 0 } : {}) },
approved: { support: directionShape, development: directionShape, ...(['FORM_1', 'MAIN'].includes(sheet) ? { total_year: 0 } : {}) }, approved: { support: directionShape, development: directionShape, ...(['FORM_1', 'MAIN'].includes(sheet) ? { total_year: 0 } : {}) },