307 lines
9.3 KiB
JavaScript
307 lines
9.3 KiB
JavaScript
// src/components/common/ProjectCardComponent/ModalCreateProject.jsx
|
||
import { useState, useEffect } from 'react';
|
||
import { Box, Button, Divider, TextField, Typography } from '@mui/material';
|
||
import SelectWithOptions from '../AdminPanel/components/SelectWithOptions';
|
||
import Modal from '../../components/common/Modal/Modal';
|
||
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
|
||
|
||
export const ModalCreateProject = ({
|
||
open,
|
||
onClose,
|
||
onCreate,
|
||
config = {},
|
||
initialData = null,
|
||
isEdit = false,
|
||
projectId = null,
|
||
}) => {
|
||
const [projectData, setProjectData] = useState(() => {
|
||
// Если есть initialData, используем её, иначе создаем из конфига
|
||
if (initialData) {
|
||
return { ...initialData };
|
||
}
|
||
const initial = {};
|
||
Object.keys(config).forEach((key) => {
|
||
initial[key] = config[key].defaultValue;
|
||
});
|
||
return initial;
|
||
});
|
||
|
||
const [errors, setErrors] = useState({});
|
||
|
||
// Обновляем данные при изменении initialData
|
||
useEffect(() => {
|
||
if (initialData) {
|
||
setProjectData({ ...initialData });
|
||
} else {
|
||
const initial = {};
|
||
Object.keys(config).forEach((key) => {
|
||
initial[key] = config[key].defaultValue;
|
||
});
|
||
setProjectData(initial);
|
||
}
|
||
setErrors({});
|
||
}, [initialData, config]);
|
||
|
||
const handleChange = (field, value) => {
|
||
setProjectData((prev) => ({
|
||
...prev,
|
||
[field]: value,
|
||
}));
|
||
// Очищаем ошибку для поля при изменении
|
||
if (errors[field]) {
|
||
setErrors((prev) => ({
|
||
...prev,
|
||
[field]: undefined,
|
||
}));
|
||
}
|
||
};
|
||
|
||
const validateField = (fieldName, config) => {
|
||
if (config.required_field) {
|
||
const value = projectData[fieldName];
|
||
|
||
// Проверка для разных типов полей
|
||
if (config.type === 'multiselect') {
|
||
if (!value || (Array.isArray(value) && value.length === 0) || value === '') {
|
||
return `Поле "${fieldName}" обязательно для заполнения`;
|
||
}
|
||
} else if (config.type === 'text') {
|
||
if (!value || value.trim() === '') {
|
||
return `Поле "${fieldName}" обязательно для заполнения`;
|
||
}
|
||
} else if (config.type === 'number') {
|
||
if (value === null || value === undefined || value === '') {
|
||
return `Поле "${fieldName}" обязательно для заполнения`;
|
||
}
|
||
// Проверка на минимальное значение
|
||
if (config.min !== undefined && Number(value) < config.min) {
|
||
return `Значение должно быть не меньше ${config.min}`;
|
||
}
|
||
// Проверка на максимальное значение
|
||
if (config.max !== undefined && Number(value) > config.max) {
|
||
return `Значение должно быть не больше ${config.max}`;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const validateAllFields = () => {
|
||
const newErrors = {};
|
||
let hasErrors = false;
|
||
|
||
Object.entries(config).forEach(([fieldName, fieldConfig]) => {
|
||
const error = validateField(fieldName, fieldConfig);
|
||
if (error) {
|
||
newErrors[fieldName] = error;
|
||
hasErrors = true;
|
||
}
|
||
});
|
||
|
||
setErrors(newErrors);
|
||
return !hasErrors;
|
||
};
|
||
|
||
const handleClickCreate = () => {
|
||
if (validateAllFields()) {
|
||
onCreate(projectData);
|
||
// Не закрываем модалку здесь, пусть это делает родительский компонент
|
||
}
|
||
};
|
||
|
||
const handleClose = () => {
|
||
const resetData = {};
|
||
Object.keys(config).forEach((key) => {
|
||
resetData[key] = config[key].defaultValue;
|
||
});
|
||
setProjectData(resetData);
|
||
setErrors({});
|
||
onClose();
|
||
};
|
||
|
||
const renderField = (fieldName, config) => {
|
||
const hasError = !!errors[fieldName];
|
||
const errorText = errors[fieldName];
|
||
|
||
switch (config.type) {
|
||
case 'multiselect':
|
||
return (
|
||
<Box key={fieldName}>
|
||
<Typography
|
||
className="gray-color"
|
||
sx={{
|
||
fontWeight: '400',
|
||
fontSize: '0.75rem',
|
||
paddingBottom: '.25rem',
|
||
color: hasError ? 'var(--error-color, #d32f2f)' : 'var(--grey-dark)',
|
||
}}
|
||
>
|
||
{fieldName}
|
||
{config.required_field && (
|
||
<span style={{ color: 'var(--error-color, #d32f2f)', marginLeft: '4px' }}>*</span>
|
||
)}
|
||
</Typography>
|
||
<SelectWithOptions
|
||
className="sameSize"
|
||
value={projectData[fieldName] || ''}
|
||
options={config.options}
|
||
placeholder={config.placeholder}
|
||
onChange={(value) => handleChange(fieldName, value)}
|
||
width="28.25rem"
|
||
style={{
|
||
borderColor: hasError ? 'var(--error-color, #d32f2f)' : undefined,
|
||
borderWidth: hasError ? '2px' : undefined,
|
||
}}
|
||
/>
|
||
{hasError && (
|
||
<Typography
|
||
sx={{
|
||
color: 'var(--error-color, #d32f2f)',
|
||
fontSize: '0.75rem',
|
||
marginTop: '4px',
|
||
}}
|
||
>
|
||
{errorText}
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
);
|
||
|
||
case 'text':
|
||
return (
|
||
<Box key={fieldName}>
|
||
<Typography
|
||
className="gray-color"
|
||
sx={{
|
||
fontWeight: '400',
|
||
fontSize: '0.75rem',
|
||
color: hasError ? 'var(--error-color, #d32f2f)' : 'var(--grey-dark)',
|
||
paddingBottom: '.25rem',
|
||
}}
|
||
>
|
||
{fieldName}
|
||
{config.required_field && (
|
||
<span style={{ color: 'var(--error-color, #d32f2f)', marginLeft: '4px' }}>*</span>
|
||
)}
|
||
</Typography>
|
||
<TextField
|
||
className="sameSize"
|
||
value={projectData[fieldName] || ''}
|
||
onChange={(e) => handleChange(fieldName, e.target.value)}
|
||
placeholder={config.placeholder}
|
||
size="small"
|
||
error={hasError}
|
||
helperText={hasError ? errorText : ''}
|
||
FormHelperTextProps={{
|
||
sx: {
|
||
marginLeft: 0,
|
||
fontSize: '0.75rem',
|
||
},
|
||
}}
|
||
/>
|
||
</Box>
|
||
);
|
||
|
||
case 'number':
|
||
return (
|
||
<Box key={fieldName}>
|
||
<Typography
|
||
className="gray-color"
|
||
sx={{
|
||
fontWeight: '400',
|
||
fontSize: '0.75rem',
|
||
color: hasError ? 'var(--error-color, #d32f2f)' : 'var(--grey-dark)',
|
||
paddingBottom: '.25rem',
|
||
}}
|
||
>
|
||
{fieldName}
|
||
{config.required_field && (
|
||
<span style={{ color: 'var(--error-color, #d32f2f)', marginLeft: '4px' }}>*</span>
|
||
)}
|
||
</Typography>
|
||
<TextField
|
||
className="sameSize"
|
||
type="text"
|
||
value={projectData[fieldName] || ''}
|
||
onChange={(e) => {
|
||
const inputValue = e.target.value;
|
||
//только числа
|
||
const isValidInput =
|
||
/^\d*\.?\d*$/.test(inputValue) || inputValue === '';
|
||
if (isValidInput) {
|
||
handleChange(fieldName, inputValue);
|
||
}
|
||
}}
|
||
placeholder={config.placeholder}
|
||
size="small"
|
||
error={hasError}
|
||
helperText={hasError ? errorText : ''}
|
||
slotProps={{
|
||
htmlInput: {
|
||
min: config.min,
|
||
max: config.max || null,
|
||
step: config.step || 1,
|
||
},
|
||
}}
|
||
FormHelperTextProps={{
|
||
sx: {
|
||
marginLeft: 0,
|
||
fontSize: '0.75rem',
|
||
},
|
||
}}
|
||
/>
|
||
</Box>
|
||
);
|
||
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Modal
|
||
title={isEdit ? 'Редактирование проекта' : 'Создание проекта'}
|
||
open={open}
|
||
onClose={handleClose}
|
||
customSize={'43.75'}
|
||
actions={
|
||
<>
|
||
<Button
|
||
variant="grey"
|
||
onClick={handleClose}
|
||
style={{ height: '2.5rem' }}
|
||
>
|
||
Отменить
|
||
</Button>
|
||
<PrimaryButton
|
||
variant="contained"
|
||
onClick={handleClickCreate}
|
||
style={{ height: '2.5rem' }}
|
||
>
|
||
{isEdit ? 'Сохранить' : 'Создать'}
|
||
</PrimaryButton>
|
||
</>
|
||
}
|
||
>
|
||
<Divider />
|
||
<Box
|
||
sx={{
|
||
m: '1.25rem 0',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '1.25rem',
|
||
'& .sameSize': {
|
||
height: '2.69rem',
|
||
minWidth: '39.75rem',
|
||
},
|
||
'& .gray-color': { color: '#4A5565' },
|
||
}}
|
||
>
|
||
{Object.entries(config).map(([fieldName, config]) =>
|
||
renderField(fieldName, config),
|
||
)}
|
||
</Box>
|
||
<Divider />
|
||
</Modal>
|
||
);
|
||
}; |