254 lines
9.2 KiB
JavaScript
254 lines
9.2 KiB
JavaScript
import CloseIcon from '@mui/icons-material/Close';
|
||
import { Box, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, MenuItem, TextField, Typography } from '@mui/material';
|
||
import React from 'react';
|
||
import { useAuth } from '../../../../app/context/AuthProvider';
|
||
import { DangerOutlinedButton, PrimaryButton } from '../../../../components/common/Buttons/Buttons';
|
||
import { PROJECT_TYPES } from '../../../../constants/constants';
|
||
import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../constants';
|
||
import { canEditFirstTableField, hasReserveValuesInChildSmetas, isFirstTableFieldDependencySatisfied } from '../../constants/fieldAccess';
|
||
|
||
const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNames = {} }) => {
|
||
const { user } = useAuth();
|
||
const [formData, setFormData] = React.useState(rowData || {});
|
||
|
||
React.useEffect(() => {
|
||
if (rowData) {
|
||
setFormData(rowData);
|
||
}
|
||
}, [rowData]);
|
||
|
||
const handleChange = (field) => (event) => {
|
||
let value = event.target.value;
|
||
if (field.valueType === 'boolean') value = value === '' ? null : value === 'true';
|
||
if (field.valueType === 'number') {
|
||
if (value === '') {
|
||
value = '';
|
||
} else {
|
||
const numberValue = Number(value);
|
||
if (Number.isNaN(numberValue) || (field.min !== undefined && numberValue < field.min)) return;
|
||
value = numberValue;
|
||
}
|
||
}
|
||
|
||
const nextFormData = {
|
||
...formData,
|
||
[field.key]: value,
|
||
};
|
||
|
||
if (!value && field.key === 'krf_decision_date') {
|
||
nextFormData.fk_decision_date = '';
|
||
nextFormData.board_decision_date = '';
|
||
}
|
||
if (!value && field.key === 'fk_decision_date') {
|
||
nextFormData.board_decision_date = '';
|
||
}
|
||
|
||
setFormData(nextFormData);
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
await onSave(formData);
|
||
};
|
||
|
||
if (!rowData) return null;
|
||
|
||
const projectFields = [
|
||
{ key: 'name', label: 'Проект' },
|
||
{ key: 'status', label: 'Статус', options: [['created', 'Создан'], ['agreed', 'Согласован'], ['archived', 'В архиве']] },
|
||
{ key: 'technical_number', label: 'Технический номер проекта' },
|
||
{ 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: 'vsp_format', label: 'Формат ВСП', options: ['Флагманский', 'Типовой', 'Розничный', 'МСБ', 'Лёгкий', 'Мини', 'Розничный-киоск', 'МБО', 'Офис самообслуживания', 'Другое'] },
|
||
{ key: 'placement_type', label: 'Размещение', options: ['Собственность', 'Аренда', 'Субаренда'] },
|
||
{ key: 'staff_count', label: 'Количество сотрудников', type: 'number', valueType: 'number', min: 0 },
|
||
{ key: 'total_area', label: 'Общая площадь', type: 'number', valueType: 'number', min: 0 },
|
||
{ key: 'funding_by_ko_decision', label: 'Финансирование по решению КО', options: FUNDING_BY_DECISION },
|
||
{ key: 'krf_decision_date', label: 'Дата решения КРФ', type: 'date' },
|
||
{ key: 'fk_decision_date', label: 'Дата решения ФК', type: 'date' },
|
||
{ key: 'board_decision_date', label: 'Дата решения Правления', type: 'date' },
|
||
{ key: 'open_relocate_close_date', label: 'Дата открытия / переезда / закрытия', type: 'date' },
|
||
{ key: 'object_address', label: 'Адрес объекта', multiline: true, rows: 2 },
|
||
];
|
||
|
||
const smetaFields = [
|
||
{ key: 'year', label: 'Год сметы', type: 'number', disabled: true },
|
||
{ key: 'is_in_plan', label: 'В базовой смете', valueType: 'boolean', options: [['true', 'Да'], ['false', 'Нет']] },
|
||
{ key: 'is_in_plan_q2', label: 'В скорректированной смете 2 кв', valueType: 'boolean', options: [['true', 'Да'], ['false', 'Нет']] },
|
||
{ key: 'is_in_plan_q3', label: 'В скорректированной смете 3 кв', valueType: 'boolean', options: [['true', 'Да'], ['false', 'Нет']] },
|
||
{ key: 'is_in_plan_q4', label: 'В скорректированной смете 4 кв', valueType: 'boolean', options: [['true', 'Да'], ['false', 'Нет']] },
|
||
{ key: 'development_block', label: 'Блок развития', options: DEVELOMENT_BLOCK },
|
||
{ key: 'reserve_to_prrs_ahr', label: 'Из резерва Банка в смету ПРРС АХР', type: 'number', valueType: 'number' },
|
||
{ key: 'reserve_to_prrs_kv', label: 'Из резерва Банка в смету ПРРС КВ', type: 'number', valueType: 'number' },
|
||
];
|
||
|
||
const fields = rowData._rowType === 'smeta' ? smetaFields : projectFields;
|
||
const middle = Math.ceil(fields.length / 2);
|
||
const leftColumnFields = fields.slice(0, middle);
|
||
const rightColumnFields = fields.slice(middle);
|
||
|
||
// Компонент для поля ввода
|
||
const renderField = (field) => {
|
||
const rawValue = formData[field.key];
|
||
const isFundingByDecisionLocked = field.key === 'funding_by_ko_decision' && hasReserveValuesInChildSmetas(formData);
|
||
const isFieldDisabled =
|
||
field.disabled ||
|
||
isFundingByDecisionLocked ||
|
||
!canEditFirstTableField(field.key, user?.role_id) ||
|
||
!isFirstTableFieldDependencySatisfied(field.key, formData);
|
||
const fieldValue = field.valueType === 'boolean' && rawValue !== null && rawValue !== undefined
|
||
? String(rawValue)
|
||
: (rawValue ?? '');
|
||
return (
|
||
<Box key={field.key} sx={{ width: '390px', flexShrink: 0 }}>
|
||
<Typography
|
||
variant='caption'
|
||
sx={{
|
||
fontWeight: 600,
|
||
color: '#666',
|
||
display: 'block',
|
||
mb: 0.5,
|
||
fontSize: '0.75rem',
|
||
}}>
|
||
{field.label}
|
||
</Typography>
|
||
<TextField
|
||
fullWidth
|
||
value={fieldValue}
|
||
onChange={handleChange(field)}
|
||
variant='outlined'
|
||
size='small'
|
||
type={field.type || 'text'}
|
||
placeholder={field.placeholder || ''}
|
||
multiline={field.multiline || false}
|
||
rows={field.rows || 1}
|
||
select={Boolean(field.options)}
|
||
disabled={isFieldDisabled || isSaving}
|
||
InputLabelProps={field.type === 'date' ? { shrink: true } : undefined}
|
||
slotProps={field.type === 'number' ? { htmlInput: { min: field.min } } : undefined}
|
||
sx={{
|
||
'& .MuiOutlinedInput-root': {
|
||
bgcolor: 'transparent',
|
||
'& fieldset': {
|
||
borderColor: '#d0d0d0',
|
||
},
|
||
'&:hover fieldset': {
|
||
borderColor: '#b0b0b0',
|
||
},
|
||
'&.Mui-focused fieldset': {
|
||
borderColor: '#1976d2',
|
||
},
|
||
...(field.multiline && {
|
||
minHeight: '56px',
|
||
}),
|
||
},
|
||
'& .MuiOutlinedInput-input': {
|
||
bgcolor: 'transparent',
|
||
},
|
||
'& .MuiInputBase-input': {
|
||
bgcolor: 'transparent',
|
||
},
|
||
width: '100%',
|
||
}}
|
||
>
|
||
{field.options && [<MenuItem key='empty' value=''>Не выбрано</MenuItem>, ...field.options.map((option) => {
|
||
const [value, label] = Array.isArray(option) ? option : [option, option];
|
||
return <MenuItem key={value} value={value}>{label}</MenuItem>;
|
||
})]}
|
||
</TextField>
|
||
{isFundingByDecisionLocked && (
|
||
<Typography sx={{ mt: 0.5, color: 'warning.main', fontSize: '0.75rem', lineHeight: 1.4 }}>
|
||
Поле нельзя изменить: в одной из дочерних смет заполнено значение «Из резерва Банка в смету ПРРС АХР» или «Из резерва Банка в смету ПРРС КВ».
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<Dialog
|
||
open={open}
|
||
onClose={onClose}
|
||
maxWidth={false}
|
||
PaperProps={{
|
||
sx: {
|
||
borderRadius: '12px',
|
||
maxHeight: '90vh',
|
||
height: 'auto',
|
||
maxWidth: '820px',
|
||
width: '100%',
|
||
},
|
||
}}>
|
||
<DialogTitle
|
||
sx={{
|
||
m: 0,
|
||
p: '16px 24px',
|
||
borderBottom: '1px solid #e0e0e0',
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
bgcolor: '#f8f9fa',
|
||
}}>
|
||
<Typography variant='h6' sx={{ fontWeight: 600, fontSize: '18px' }}>
|
||
Редактирование строки
|
||
</Typography>
|
||
<IconButton onClick={onClose} size='small'>
|
||
<CloseIcon fontSize='small' />
|
||
</IconButton>
|
||
</DialogTitle>
|
||
|
||
<DialogContent
|
||
sx={{
|
||
pt: 3,
|
||
pb: 2,
|
||
px: 3,
|
||
overflow: 'auto',
|
||
}}>
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
gap: '20px',
|
||
justifyContent: 'space-between',
|
||
}}>
|
||
{/* Левая колонка */}
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 2,
|
||
flex: '0 0 auto',
|
||
}}>
|
||
{leftColumnFields.map(renderField)}
|
||
</Box>
|
||
|
||
{/* Правая колонка */}
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 2,
|
||
flex: '0 0 auto',
|
||
}}>
|
||
{rightColumnFields.map(renderField)}
|
||
</Box>
|
||
</Box>
|
||
</DialogContent>
|
||
|
||
<DialogActions
|
||
sx={{
|
||
p: 2,
|
||
px: 3,
|
||
borderTop: '1px solid #e0e0e0',
|
||
bgcolor: '#f8f9fa',
|
||
justifyContent: 'flex-end',
|
||
gap: 1,
|
||
}}>
|
||
<DangerOutlinedButton onClick={onClose} disabled={isSaving}>Отмена</DangerOutlinedButton>
|
||
|
||
<PrimaryButton onClick={handleSave} disabled={isSaving}>{isSaving ? 'Сохранение...' : 'Сохранить'}</PrimaryButton>
|
||
</DialogActions>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
export default EditModal;
|