283 lines
7.4 KiB
JavaScript
283 lines
7.4 KiB
JavaScript
import { Box, Button, Divider, TextField, Typography } from '@mui/material';
|
|
import { useEffect, useState } from 'react';
|
|
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
|
|
import Modal from '../../components/common/Modal/Modal';
|
|
import SelectWithOptions from '../AdminPanel/components/SelectWithOptions';
|
|
|
|
export const ModalCreateProject = ({
|
|
open,
|
|
onClose,
|
|
onCreate,
|
|
config = {},
|
|
initialData = null,
|
|
title = 'Создание проекта',
|
|
buttonText = 'Создать',
|
|
}) => {
|
|
const [projectData, setProjectData] = useState(() => {
|
|
if (initialData) {
|
|
return { ...initialData };
|
|
}
|
|
// Иначе создаем из конфига
|
|
const data = {};
|
|
Object.keys(config).forEach((key) => {
|
|
data[key] = config[key].defaultValue;
|
|
});
|
|
return data;
|
|
});
|
|
|
|
const [errors, setErrors] = useState({});
|
|
|
|
useEffect(() => {
|
|
if (initialData) {
|
|
setProjectData({ ...initialData });
|
|
} else {
|
|
const data = {};
|
|
Object.keys(config).forEach((key) => {
|
|
data[key] = config[key].defaultValue;
|
|
});
|
|
setProjectData(data);
|
|
}
|
|
setErrors({});
|
|
}, [initialData, config, open]);
|
|
|
|
const handleChange = (field, value) => {
|
|
setProjectData((prev) => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
if (errors[field]) {
|
|
setErrors((prev) => ({
|
|
...prev,
|
|
[field]: undefined,
|
|
}));
|
|
}
|
|
};
|
|
|
|
const validateField = (fieldName, config) => {
|
|
const value = projectData[fieldName];
|
|
|
|
if (config.required_field) {
|
|
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.type === 'number' && value !== null && value !== undefined && value !== '') {
|
|
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);
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
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} sx={{ mb: hasError ? '0.5rem' : 0 }}>
|
|
<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} sx={{ mb: hasError ? '0.5rem' : 0 }}>
|
|
<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} sx={{ mb: hasError ? '0.5rem' : 0 }}>
|
|
<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={title}
|
|
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' }}>
|
|
{buttonText}
|
|
</PrimaryButton>
|
|
</>
|
|
}>
|
|
<Divider />
|
|
<Box
|
|
sx={{
|
|
m: '1.25rem 0',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '1.25rem',
|
|
'& .sameSize': {
|
|
minWidth: '39.75rem',
|
|
},
|
|
'& .sameSize.MuiInputBase-root, & .sameSize .MuiInputBase-root': {
|
|
height: '2.69rem',
|
|
},
|
|
'& .gray-color': { color: '#4A5565' },
|
|
}}>
|
|
{Object.entries(config).map(([fieldName, config]) => renderField(fieldName, config))}
|
|
</Box>
|
|
<Divider />
|
|
</Modal>
|
|
);
|
|
};
|