updating access for projects #107

Merged
PotapovaA merged 1 commits from fix-sticky-columns into test 2026-08-20 13:06:23 +03:00
7 changed files with 68 additions and 18 deletions

View File

@ -12,14 +12,14 @@ export const DEFAULT_PROJECT_CONFIG = {
defaultValue: null, defaultValue: null,
min: 0, min: 0,
step: 1, step: 1,
required_field: false, required_field: true,
}, },
'Тип проекта развития': { 'Тип проекта развития': {
type: 'multiselect', type: 'multiselect',
options: ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'], options: ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'],
placeholder: 'Выберите тип проекта', placeholder: 'Выберите тип проекта',
defaultValue: null, defaultValue: null,
required_field: false, required_field: true,
}, },
'Название проекта': { 'Название проекта': {
type: 'text', type: 'text',
@ -29,16 +29,17 @@ export const DEFAULT_PROJECT_CONFIG = {
}, },
'Формат ВСП': { 'Формат ВСП': {
type: 'multiselect', type: 'multiselect',
options: ['Фланговый', 'Типовой', 'Розничный', 'МСБ', 'Лёгкий', 'Мини', 'Розничный-киоск', 'МБО', 'Офис самообслуживания', 'Другое'], options: ['Флагманский', 'Типовой', 'Розничный', 'МСБ', 'Лёгкий', 'Мини', 'Розничный-киоск', 'МБО', 'Офис самообслуживания', 'Другое'],
placeholder: 'Выберите формат ВСП', placeholder: 'Выберите формат ВСП',
defaultValue: null, defaultValue: null,
required_field: false, required_field: true,
}, },
'Адрес объекта': { 'Адрес объекта': {
type: 'text', type: 'text',
placeholder: 'Введите адрес объекта', placeholder: 'Введите адрес объекта',
defaultValue: '', defaultValue: '',
required_field: false, required_field: true,
}, },
'Целевая численность, чел.': { 'Целевая численность, чел.': {
type: 'number', type: 'number',
@ -53,7 +54,8 @@ export const DEFAULT_PROJECT_CONFIG = {
options: ['Собственность', 'Аренда', 'Субаренда'], options: ['Собственность', 'Аренда', 'Субаренда'],
placeholder: 'Выберите тип размещения', placeholder: 'Выберите тип размещения',
defaultValue: null, defaultValue: null,
required_field: false, required_field: true,
}, },
'Площадь размещения, м2': { 'Площадь размещения, м2': {
type: 'number', type: 'number',

View File

@ -135,7 +135,7 @@ export default function FormPage() {
'Формат ВСП': { 'Формат ВСП': {
type: 'multiselect', type: 'multiselect',
options: [ options: [
'Фланговый', 'Флагманский',
'Типовой', 'Типовой',
'Розничный', 'Розничный',
'МСБ', 'МСБ',

View File

@ -202,11 +202,13 @@ const SummaryPage = () => {
const onEdit = (row) => { const onEdit = (row) => {
const isSmeta = row.depth > 0; const isSmeta = row.depth > 0;
const projectId = isSmeta ? row.getParentRow()?.original?.id : row.original.id; const parentProject = isSmeta ? row.getParentRow()?.original : row.original;
const projectId = parentProject?.id;
setSelectedRowData({ setSelectedRowData({
...row.original, ...row.original,
_rowType: isSmeta ? 'smeta' : 'project', _rowType: isSmeta ? 'smeta' : 'project',
_projectId: projectId, _projectId: projectId,
_projectFundingByKoDecision: parentProject?.funding_by_ko_decision,
}); });
setEditModalOpen(true); setEditModalOpen(true);
}; };

View File

@ -1,4 +1,5 @@
import { Box, IconButton, Tooltip } from '@mui/material'; import { Box, IconButton, Tooltip } from '@mui/material';
import { differenceInMonths, isValid, parseISO } from 'date-fns';
import { Back, EditPenSvg, TrashSvg } from '../../components/common/icons/icons'; import { Back, EditPenSvg, TrashSvg } from '../../components/common/icons/icons';
import StatusBadge from './components/StatusBadge/StatusBadge'; import StatusBadge from './components/StatusBadge/StatusBadge';
import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from './constants'; import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from './constants';
@ -63,6 +64,19 @@ export const createFirstColumns = (handlers) => {
header: 'Дата открытия / переезда / закрытия', header: 'Дата открытия / переезда / закрытия',
size: 160, size: 160,
}, },
{
id: 'project_implementation_months',
accessorFn: (row) => {
if (!Array.isArray(row.sub_rows) || !row.board_decision_date) return null;
const boardDecisionDate = parseISO(row.board_decision_date);
if (!isValid(boardDecisionDate)) return null;
return differenceInMonths(new Date(), boardDecisionDate);
},
header: 'Время реализации проекта в месяцах',
size: 180,
},
{ {
accessorKey: 'project_type', accessorKey: 'project_type',
header: 'Тип проекта', header: 'Тип проекта',

View File

@ -4,7 +4,7 @@ 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 { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../constants'; import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../constants';
import { canEditFirstTableField, isFirstTableFieldDependencySatisfied } from '../../constants/fieldAccess'; import { canEditFirstTableField, hasReserveValuesInChildSmetas, isFirstTableFieldDependencySatisfied } from '../../constants/fieldAccess';
const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNames = {} }) => { const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNames = {} }) => {
const { user } = useAuth(); const { user } = useAuth();
@ -80,8 +80,10 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa
// Компонент для поля ввода // Компонент для поля ввода
const renderField = (field) => { const renderField = (field) => {
const rawValue = formData[field.key]; const rawValue = formData[field.key];
const isFundingByDecisionLocked = field.key === 'funding_by_ko_decision' && hasReserveValuesInChildSmetas(formData);
const isFieldDisabled = const isFieldDisabled =
field.disabled || field.disabled ||
isFundingByDecisionLocked ||
!canEditFirstTableField(field.key, user?.role_id) || !canEditFirstTableField(field.key, user?.role_id) ||
!isFirstTableFieldDependencySatisfied(field.key, formData); !isFirstTableFieldDependencySatisfied(field.key, formData);
const fieldValue = field.valueType === 'boolean' && rawValue !== null && rawValue !== undefined const fieldValue = field.valueType === 'boolean' && rawValue !== null && rawValue !== undefined
@ -143,6 +145,11 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa
return <MenuItem key={value} value={value}>{label}</MenuItem>; return <MenuItem key={value} value={value}>{label}</MenuItem>;
})]} })]}
</TextField> </TextField>
{isFundingByDecisionLocked && (
<Typography sx={{ mt: 0.5, color: 'warning.main', fontSize: '0.75rem', lineHeight: 1.4 }}>
Поле нельзя изменить: в одной из дочерних смет заполнено значение «Из резерва Банка в смету ПРРС АХР» или «Из резерва Банка в смету ПРРС КВ».
</Typography>
)}
</Box> </Box>
); );
}; };

View File

@ -11,14 +11,14 @@ const ALL_ROLES = [
// Матрица доступа к полям первой таблицы. // Матрица доступа к полям первой таблицы.
export const FIRST_TABLE_FIELD_ACCESS = { export const FIRST_TABLE_FIELD_ACCESS = {
name: ADMIN_ROLES, name: ADMIN_ROLES,
org_unit_id: ADMIN_ROLES, org_unit_id: ALL_ROLES,
status: DFIP_ROLES, status: DFIP_ROLES,
technical_number: [], technical_number: [],
object_address: ALL_ROLES, object_address: ALL_ROLES,
vsp_format: ALL_ROLES, vsp_format: ALL_ROLES,
placement_type: ADMIN_ROLES, placement_type: ALL_ROLES,
staff_count: ADMIN_ROLES, staff_count: ALL_ROLES,
total_area: ADMIN_ROLES, total_area: ALL_ROLES,
project_type: ALL_ROLES, project_type: ALL_ROLES,
krf_decision_date: ALL_ROLES, krf_decision_date: ALL_ROLES,
fk_decision_date: ALL_ROLES, fk_decision_date: ALL_ROLES,
@ -41,10 +41,33 @@ export const FIRST_TABLE_FIELD_DEPENDENCIES = {
board_decision_date: "fk_decision_date", board_decision_date: "fk_decision_date",
}; };
export const FIRST_TABLE_FIELD_VALUE_REQUIREMENTS = {
reserve_to_prrs_ahr: {
fieldKey: "_projectFundingByKoDecision",
value: "bank_reserve",
},
reserve_to_prrs_kv: {
fieldKey: "_projectFundingByKoDecision",
value: "bank_reserve",
},
};
export const canEditFirstTableField = (fieldKey, roleId) => export const canEditFirstTableField = (fieldKey, roleId) =>
FIRST_TABLE_FIELD_ACCESS[fieldKey]?.includes(roleId) ?? false; FIRST_TABLE_FIELD_ACCESS[fieldKey]?.includes(roleId) ?? false;
const isFilled = (value) => value !== null && value !== undefined && value !== "";
export const hasReserveValuesInChildSmetas = (project) =>
(project?.sub_rows || []).some(
(smeta) => isFilled(smeta.reserve_to_prrs_ahr) || isFilled(smeta.reserve_to_prrs_kv),
);
export const isFirstTableFieldDependencySatisfied = (fieldKey, rowData) => { export const isFirstTableFieldDependencySatisfied = (fieldKey, rowData) => {
const dependencyKey = FIRST_TABLE_FIELD_DEPENDENCIES[fieldKey]; const dependencyKey = FIRST_TABLE_FIELD_DEPENDENCIES[fieldKey];
return !dependencyKey || Boolean(rowData?.[dependencyKey]); if (dependencyKey && !rowData?.[dependencyKey]) return false;
const valueRequirement = FIRST_TABLE_FIELD_VALUE_REQUIREMENTS[fieldKey];
if (valueRequirement && rowData?.[valueRequirement.fieldKey] !== valueRequirement.value) return false;
return true;
}; };

View File

@ -120,7 +120,7 @@ export const ModalCreateProject = ({
switch (config.type) { switch (config.type) {
case 'multiselect': case 'multiselect':
return ( return (
<Box key={fieldName}> <Box key={fieldName} sx={{ mb: hasError ? '0.5rem' : 0 }}>
<Typography <Typography
className='gray-color' className='gray-color'
sx={{ sx={{
@ -159,7 +159,7 @@ export const ModalCreateProject = ({
case 'text': case 'text':
return ( return (
<Box key={fieldName}> <Box key={fieldName} sx={{ mb: hasError ? '0.5rem' : 0 }}>
<Typography <Typography
className='gray-color' className='gray-color'
sx={{ sx={{
@ -191,7 +191,7 @@ export const ModalCreateProject = ({
case 'number': case 'number':
return ( return (
<Box key={fieldName}> <Box key={fieldName} sx={{ mb: hasError ? '0.5rem' : 0 }}>
<Typography <Typography
className='gray-color' className='gray-color'
sx={{ sx={{
@ -264,9 +264,11 @@ export const ModalCreateProject = ({
flexDirection: 'column', flexDirection: 'column',
gap: '1.25rem', gap: '1.25rem',
'& .sameSize': { '& .sameSize': {
height: '2.69rem',
minWidth: '39.75rem', minWidth: '39.75rem',
}, },
'& .sameSize.MuiInputBase-root, & .sameSize .MuiInputBase-root': {
height: '2.69rem',
},
'& .gray-color': { color: '#4A5565' }, '& .gray-color': { color: '#4A5565' },
}}> }}>
{Object.entries(config).map(([fieldName, config]) => renderField(fieldName, config))} {Object.entries(config).map(([fieldName, config]) => renderField(fieldName, config))}