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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import { DIRECTION_TRANSLATE, SHEET_NAME } from '../constants/constants';
const TableInfo = ({ formId, sheetName, direction, isProject = false }) => {
const path = (isProject ? '/project/' : '/task/') + formId;
const tableName = SHEET_NAME[sheetName] || sheetName;
const directionName = DIRECTION_TRANSLATE[direction] || direction;
const directionName = DIRECTION_TRANSLATE[direction] || '';
const portalContent = (
<TaskInfoContainer>
<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 || '');
}}
renderInput={(params) => {
const { InputProps, ...rest } = params;
return (
<TextField
{...rest}
{...params}
placeholder='Региональные филиалы'
variant='outlined'
size='small'
InputProps={{
...InputProps,
endAdornment: (
<>
{isLoading && <CircularProgress color='inherit' size={20} />}
{InputProps?.endAdornment}
</>
),
slotProps={{
...params.slotProps,
input: {
...params.slotProps.input,
endAdornment: (
<>
{isLoading && <CircularProgress color='inherit' size={20} />}
{params.slotProps.input.endAdornment}
</>
),
},
}}
/>
);

View File

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

View File

@ -44,7 +44,7 @@ const formatValue = (value) => {
const getShapeForSheet = (sheet) => {
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 {
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 } : {}) },