chore: initial import for test contour
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { apiUrl } from '@/config/api';
|
||||
|
||||
import {
|
||||
useGetStockQuery,
|
||||
useUpdateStockMutation,
|
||||
useUploadStockPictureMutation,
|
||||
useDeleteStockMutation,
|
||||
} from '/src/api/apiStock';
|
||||
/**/
|
||||
import { selectUtils } from '../store/slice/utilsSlice';
|
||||
/**/
|
||||
import { TextEditor } from '../components/Editors/TextEditor'
|
||||
import { LoadingComponent } from '../components/Placeholders/LoadingComponent';
|
||||
import { ErrorComponent } from '../components/Placeholders/ErrorComponent';
|
||||
import { NotFindElement } from '../components/Placeholders/NotFindElement';
|
||||
import { EditElementForm } from '../components/Forms/EditElementForm';
|
||||
import { ResponseModals } from '../components/Modals/ResponseModals';
|
||||
/**/
|
||||
import { Modal} from '../components/Modals/Modal';
|
||||
|
||||
const isValidImage = (file) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
|
||||
if (allowedTypes.includes(file.type)) return true;
|
||||
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
return ['jpg', 'jpeg', 'png'].includes(ext);
|
||||
};
|
||||
|
||||
export function EditStockPage() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const navigateBack = () => navigate( `/promotions` );
|
||||
|
||||
const { data: stock, isFetching, error } = useGetStockQuery( { stockId: id } );
|
||||
const [ updateStock ] = useUpdateStockMutation();
|
||||
const [ uploadPicture ] = useUploadStockPictureMutation();
|
||||
const [ deleteStock ] = useDeleteStockMutation();
|
||||
|
||||
const [ isModalSuccess, setModalSuccess ] = useState( false );
|
||||
|
||||
const [ errors, setErrors ] = useState({
|
||||
name: '',
|
||||
dateRange: ''
|
||||
});
|
||||
|
||||
const [ form, setForm ] = useState({
|
||||
name: '',
|
||||
anons: '',
|
||||
picture: '',
|
||||
});
|
||||
const [ anons, setAnons ] = useState( '' );
|
||||
const [ content, setContent ] = useState( '' );
|
||||
|
||||
const fileInputRef = useRef();
|
||||
|
||||
useEffect( () => {
|
||||
if ( !stock ) return;
|
||||
|
||||
let datetimeLocalStart = '';
|
||||
let datetimeLocalEnd = '';
|
||||
|
||||
if ( stock.startDate ) {
|
||||
const dateStart = new Date( stock.startDate );
|
||||
const yearStart = dateStart.getFullYear();
|
||||
const monthStart = String( dateStart.getMonth() + 1 ).padStart( 2, '0' );
|
||||
const dayStart = String( dateStart.getDate() ).padStart( 2, '0' );
|
||||
const hoursStart = String( dateStart.getHours() ).padStart( 2, '0' );
|
||||
const minutesStart = String( dateStart.getMinutes() ).padStart( 2, '0' );
|
||||
datetimeLocalStart = `${ yearStart }-${ monthStart }-${ dayStart }T${ hoursStart }:${ minutesStart }`;
|
||||
}
|
||||
|
||||
if ( stock.endDate ) {
|
||||
const dateEnd = new Date( stock.endDate );
|
||||
const yearEnd = dateEnd.getFullYear();
|
||||
const monthEnd = String( dateEnd.getMonth() + 1 ).padStart( 2, '0' );
|
||||
const dayEnd = String( dateEnd.getDate() ).padStart( 2, '0' );
|
||||
const hoursEnd = String( dateEnd.getHours() ).padStart( 2, '0' );
|
||||
const minutesEnd = String( dateEnd.getMinutes() ).padStart( 2, '0' );
|
||||
datetimeLocalEnd = `${ yearEnd }-${ monthEnd }-${ dayEnd }T${ hoursEnd }:${ minutesEnd }`;
|
||||
}
|
||||
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
name: stock.name ?? '',
|
||||
startDate: datetimeLocalStart,
|
||||
endDate: datetimeLocalEnd,
|
||||
picture: stock.picture ? apiUrl(`/uploads/${stock.picture}`) : '',
|
||||
}));
|
||||
|
||||
setAnons( stock.anons )
|
||||
setContent( stock.content )
|
||||
}, [ stock ]);
|
||||
|
||||
const startDateInputRef = useRef(null);
|
||||
const endDateInputRef = useRef(null);
|
||||
|
||||
|
||||
const handleChange = (key) => (e) => {
|
||||
setForm(f => ({ ...f, [key]: e.target.value }));
|
||||
setErrors(err => ({ ...err, [key === 'name' ? 'name' : 'dateRange']: '' }));
|
||||
};
|
||||
|
||||
const handleFile = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return window.alert('Файл не выбран')
|
||||
if (!isValidImage(file)) {
|
||||
return window.alert('Изображения должны быть только формата JPG, JPEG или PNG');
|
||||
}
|
||||
const url = window.URL.createObjectURL(file);
|
||||
setForm(f => ({ ...f, _file: file, picture: url }));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
|
||||
let hasError = false;
|
||||
const newErrors = { name:'', dateRange:'' };
|
||||
|
||||
if (!form.name.trim()) {
|
||||
newErrors.name = 'Название не может быть пустым';
|
||||
hasError = true;
|
||||
}
|
||||
const start = new Date(form.startDate);
|
||||
const end = new Date(form.endDate);
|
||||
if (isNaN(start) || isNaN(end) || start > end) {
|
||||
newErrors.dateRange = 'Дата начал акции должна быть не позже даты окончания';
|
||||
hasError = true;
|
||||
}
|
||||
if (hasError) {
|
||||
setErrors(newErrors);
|
||||
window.alert('Пожалуйста исправьте ошибки в форме.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateStock({
|
||||
stockId: id,
|
||||
data: {
|
||||
name: form.name,
|
||||
anons: anons,
|
||||
content: content,
|
||||
startDate: String(form.startDate),
|
||||
endDate: String(form.endDate),
|
||||
}
|
||||
}).unwrap();
|
||||
|
||||
if (form._file) {
|
||||
await uploadPicture({ id, file: form._file }).unwrap();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Ошибка при обновлении акции:', err)
|
||||
}
|
||||
setModalSuccess(true)
|
||||
window.setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 2000);
|
||||
console.log('success update promotion')
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteStock({stockId: id}).unwrap()
|
||||
console.log('success delete')
|
||||
setModalSuccess(true)
|
||||
window.setTimeout(() => {
|
||||
navigateBack()
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Ошибка при удалении специалиста:', err)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
if ( isFetching ) return <LoadingComponent />
|
||||
if ( error ) return <ErrorComponent />
|
||||
if ( !stock ) return (
|
||||
<NotFindElement
|
||||
message={ `Акция с ID=${id} не найдена.` }
|
||||
navigateBack={ navigateBack }
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<EditElementForm
|
||||
navigateBack={ navigateBack }
|
||||
header={ `Редактирование акции #${ id }` }
|
||||
handleSave={ handleSave }
|
||||
handleDelete={ handleDelete }
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>Название</label>
|
||||
<input
|
||||
className="form-control"
|
||||
value={form.name}
|
||||
onChange={handleChange('name')}
|
||||
/>
|
||||
{errors.name && <small className="text-danger">{errors.name}</small>}
|
||||
</div>
|
||||
|
||||
<div className='d-flex flex-column mb-1'>
|
||||
<div className='d-flex justify-content-between' style={{ gap: '1rem'}}>
|
||||
<div className="form-group flex-grow-1">
|
||||
<label>Начало акции:</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
ref={startDateInputRef}
|
||||
className="form-control"
|
||||
value={form.startDate}
|
||||
onClick={() => {
|
||||
if (startDateInputRef.current?.showPicker) {
|
||||
startDateInputRef.current.showPicker();
|
||||
}
|
||||
}}
|
||||
onChange={handleChange('startDate')}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group flex-grow-1">
|
||||
<label>Окончание акции:</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
ref={endDateInputRef}
|
||||
className="form-control"
|
||||
value={form.endDate}
|
||||
onClick={() => {
|
||||
if (endDateInputRef.current?.showPicker) {
|
||||
endDateInputRef.current.showPicker();
|
||||
}
|
||||
}}
|
||||
onChange={handleChange('endDate')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{errors.dateRange && (
|
||||
<div className="mb-3">
|
||||
<small className="text-danger">{errors.dateRange}</small>
|
||||
</div>
|
||||
)}
|
||||
<label className='mt-0'>*Время указывается по МСК</label>
|
||||
</div>
|
||||
|
||||
<div className='d-flex justify-content-between' style={{ gap: '1rem'}}>
|
||||
<div className="form-group flex-grow-1">
|
||||
<label>Анонс</label>
|
||||
<TextEditor
|
||||
content={anons}
|
||||
setContent={setAnons}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Изображение</label>
|
||||
<div className="d-flex align-items-center flex-column" style={{ gap: '1rem'}}>
|
||||
{form.picture && (
|
||||
<img
|
||||
src={form.picture}
|
||||
alt=""
|
||||
style={{ width: '13rem', height: '13rem', objectFit: 'cover', marginRight: 16 }}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={() => fileInputRef.current.click()}
|
||||
>
|
||||
Загрузить…
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/jpeg,image/png"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Полное описание</label>
|
||||
<TextEditor
|
||||
content={content}
|
||||
setContent={setContent}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={isModalSuccess}
|
||||
title="Изменения внесены"
|
||||
hasButtons={false}
|
||||
confirmText={'Удалить врача'}
|
||||
>
|
||||
<p className='mb-1'>Изменения успешно внесены.</p>
|
||||
</Modal>
|
||||
</EditElementForm>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user