Files
autobro-front/src/pages/admin/Vehicle.tsx
2025-07-14 00:21:15 +05:00

1415 lines
48 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
TextField,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Grid,
CircularProgress,
Alert,
Card,
CardContent,
CardMedia,
CardActions,
Chip,
Divider,
InputAdornment,
Tooltip,
Fade,
Tabs,
Tab,
MenuItem,
Select,
InputLabel,
FormControl
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
import SearchIcon from '@mui/icons-material/Search';
import ViewListIcon from '@mui/icons-material/ViewList';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
import SpeedIcon from '@mui/icons-material/Speed';
import AttachMoneyIcon from '@mui/icons-material/AttachMoney';
import { useAuth } from '../../context/AuthContext';
import {
fetchCars,
createCar,
updateCar,
deleteCar,
getImageUrl,
} from '../../utils/api';
import type { Car } from '../../utils/api';
// Определяем типы здесь, так как они не экспортируются из api.ts
interface CarsResponse {
cars: Car[];
total: number;
}
const Vehicle = () => {
const { token } = useAuth();
const [cars, setCars] = useState<Car[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [totalCars, setTotalCars] = useState(0);
const [viewMode, setViewMode] = useState<'list' | 'grid'>('grid');
const [searchQuery, setSearchQuery] = useState('');
// Состояния для модальных окон
const [openCreateDialog, setOpenCreateDialog] = useState(false);
const [openEditDialog, setOpenEditDialog] = useState(false);
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
// Состояние для выбранного автомобиля
const [selectedCar, setSelectedCar] = useState<Car | null>(null);
// Состояния для формы
const [formData, setFormData] = useState({
name: '',
year: '',
mileage: '',
price: '',
base_price: '',
country_of_origin: '',
drive_type: '',
engine_type: 'бензиновый',
engine_capacity: '',
engine_power: '',
electric_motor_power: '',
hybrid_type: 'не гибрид',
power_ratio: 'не применимо',
});
const [imageFile, setImageFile] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState('');
// Загрузка списка автомобилей
const loadCars = async () => {
setLoading(true);
try {
const data: CarsResponse = await fetchCars();
setCars(data.cars);
setTotalCars(data.total);
setError('');
} catch (err) {
setError('Не удалось загрузить список автомобилей');
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadCars();
}, []);
// Фильтрация автомобилей по поисковому запросу
const filteredCars = cars.filter(car =>
car.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
car.year.toString().includes(searchQuery) ||
car.price.toString().includes(searchQuery)
);
// Обработчики для формы
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
};
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
setImageFile(file);
setImagePreview(URL.createObjectURL(file));
}
};
// Открытие диалога создания
const handleOpenCreateDialog = () => {
setFormData({
name: '',
year: '',
mileage: '',
price: '',
base_price: '',
country_of_origin: '',
drive_type: '',
engine_type: 'бензиновый',
engine_capacity: '',
engine_power: '',
electric_motor_power: '',
hybrid_type: 'не гибрид',
power_ratio: 'не применимо',
});
setImageFile(null);
setImagePreview('');
setOpenCreateDialog(true);
};
// Открытие диалога редактирования
const handleOpenEditDialog = (car: Car) => {
setSelectedCar(car);
setFormData({
name: car.name,
year: car.year.toString(),
mileage: car.mileage.toString(),
price: car.price.toString(),
base_price: car.base_price?.toString() || '',
country_of_origin: car.country_of_origin || '',
drive_type: car.drive_type || '',
engine_type: car.engine_type || 'бензиновый',
engine_capacity: car.engine_capacity?.toString() || '',
engine_power: car.engine_power?.toString() || '',
electric_motor_power: car.electric_motor_power?.toString() || '',
hybrid_type: car.hybrid_type || 'не гибрид',
power_ratio: car.power_ratio || 'не применимо',
});
setImagePreview(getImageUrl(car.image));
setImageFile(null);
setOpenEditDialog(true);
};
// Открытие диалога удаления
const handleOpenDeleteDialog = (car: Car) => {
setSelectedCar(car);
setOpenDeleteDialog(true);
};
// Закрытие диалогов
const handleCloseDialogs = () => {
setOpenCreateDialog(false);
setOpenEditDialog(false);
setOpenDeleteDialog(false);
setSelectedCar(null);
};
// Создание автомобиля
const handleCreateCar = async () => {
try {
const carData = {
name: formData.name,
year: parseInt(formData.year),
mileage: parseInt(formData.mileage),
price: parseInt(formData.price),
base_price: parseInt(formData.base_price),
country_of_origin: formData.country_of_origin,
drive_type: formData.drive_type,
engine_type: formData.engine_type,
engine_capacity: formData.engine_capacity ? parseInt(formData.engine_capacity) : undefined,
engine_power: formData.engine_power ? parseInt(formData.engine_power) : undefined,
electric_motor_power: formData.electric_motor_power ? parseInt(formData.electric_motor_power) : undefined,
hybrid_type: formData.hybrid_type || 'не гибрид',
power_ratio: formData.power_ratio || 'не применимо',
image: ''
};
await createCar(carData, imageFile || undefined);
handleCloseDialogs();
loadCars();
} catch (err) {
setError('Не удалось создать автомобиль');
console.error(err);
}
};
// Обновление автомобиля
const handleUpdateCar = async () => {
if (!selectedCar) return;
try {
const carData = {
name: formData.name,
year: parseInt(formData.year),
mileage: parseInt(formData.mileage),
price: parseInt(formData.price),
base_price: parseInt(formData.base_price),
country_of_origin: formData.country_of_origin,
drive_type: formData.drive_type,
engine_type: formData.engine_type,
engine_capacity: formData.engine_capacity ? parseInt(formData.engine_capacity) : undefined,
engine_power: formData.engine_power ? parseInt(formData.engine_power) : undefined,
electric_motor_power: formData.electric_motor_power ? parseInt(formData.electric_motor_power) : undefined,
hybrid_type: formData.hybrid_type || 'не гибрид',
power_ratio: formData.power_ratio || 'не применимо',
};
await updateCar(selectedCar.id, carData, imageFile || undefined);
handleCloseDialogs();
loadCars();
} catch (err) {
setError('Не удалось обновить автомобиль');
console.error(err);
}
};
// Удаление автомобиля
const handleDeleteCar = async () => {
if (!selectedCar) return;
try {
await deleteCar(selectedCar.id);
handleCloseDialogs();
loadCars();
} catch (err) {
setError('Не удалось удалить автомобиль');
console.error(err);
}
};
// Валидация формы
const isFormValid = () => {
return (
formData.name.trim() !== '' &&
!isNaN(parseInt(formData.year)) &&
!isNaN(parseInt(formData.mileage)) &&
!isNaN(parseInt(formData.price)) &&
!isNaN(parseInt(formData.base_price)) &&
formData.country_of_origin.trim() !== '' &&
formData.drive_type.trim() !== '' &&
formData.engine_type.trim() !== ''
);
};
// Форматирование цены
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
maximumFractionDigits: 0
}).format(price);
};
// Рендер таблицы
const renderTable = () => (
<TableContainer
component={Paper}
elevation={3}
sx={{
borderRadius: 2,
overflow: 'hidden'
}}
>
<Table>
<TableHead sx={{ bgcolor: '#2D2D2D' }}>
<TableRow>
<TableCell sx={{ color: 'white', fontWeight: 'bold' }}>ID</TableCell>
<TableCell sx={{ color: 'white', fontWeight: 'bold' }}>Изображение</TableCell>
<TableCell sx={{ color: 'white', fontWeight: 'bold' }}>Название</TableCell>
<TableCell sx={{ color: 'white', fontWeight: 'bold' }}>Год</TableCell>
<TableCell sx={{ color: 'white', fontWeight: 'bold' }}>Пробег</TableCell>
<TableCell sx={{ color: 'white', fontWeight: 'bold' }}>Цена</TableCell>
<TableCell sx={{ color: 'white', fontWeight: 'bold' }}>Действия</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredCars.map((car) => (
<TableRow
key={car.id}
sx={{
'&:hover': {
backgroundColor: '#f5f5f5'
},
transition: 'background-color 0.3s'
}}
>
<TableCell>{car.id}</TableCell>
<TableCell>
<Box
component="img"
src={getImageUrl(car.image)}
alt={car.name}
sx={{
width: "10vw",
height: "5vw",
objectFit: 'cover',
borderRadius: "0.7vw",
boxShadow: '0 0.2vw 0.4vw rgba(0,0,0,0.1)',
userSelect: "none",
pointerEvents: "none",
}}
/>
</TableCell>
<TableCell sx={{ fontWeight: 'medium' }}>{car.name}</TableCell>
<TableCell>
<Chip
icon={<CalendarTodayIcon />}
label={car.year}
size="small"
sx={{ bgcolor: '#e8f5e9', color: '#2e7d32' }}
/>
</TableCell>
<TableCell>
<Chip
icon={<SpeedIcon />}
label={`${car.mileage} км`}
size="small"
sx={{ bgcolor: '#e3f2fd', color: '#1565c0' }}
/>
</TableCell>
<TableCell>
<Chip
icon={<AttachMoneyIcon />}
label={formatPrice(car.price)}
size="small"
sx={{ bgcolor: '#fce4ec', color: '#c2185b' }}
/>
</TableCell>
<TableCell>
<Tooltip arrow TransitionComponent={Fade} TransitionProps={{ timeout: 600 }}>
<IconButton
onClick={() => handleOpenEditDialog(car)}
color="primary"
sx={{
color: "black",
'&:hover': {
color: '#C27664',
bgcolor: "transparent"
},
transition: "all 0.2s ease",
}}
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip arrow TransitionComponent={Fade} TransitionProps={{ timeout: 600 }}>
<IconButton
onClick={() => handleOpenDeleteDialog(car)}
color="error"
sx={{
'&:hover': {
color: '#C27664',
bgcolor: 'transparent',
},
transition: "all 0.2s ease",
}}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
// Рендер сетки карточек
const renderGrid = () => (
<Grid container spacing={3}>
{filteredCars.map((car) => (
<Grid item xs={12} sm={6} md={4} key={car.id}>
<Card
elevation={3}
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
borderRadius: "1vw",
transition: 'transform 0.3s, box-shadow 0.3s',
boxShadow: '0 0.5vw 1vw rgba(0,0,0,0.15)',
'&:hover': {
transform: 'translateY(-0.2vw)',
boxShadow: '0 0.5vw 1vw rgba(0,0,0,0.1)'
}
}}
>
<CardMedia
component="img"
height="160"
image={getImageUrl(car.image)}
alt={car.name}
sx={{ objectFit: 'cover',
userSelect: "none",
pointerEvents: "none",
}}
/>
<CardContent sx={{ pt: "0.5vw" }}>
<Typography gutterBottom variant="h6" component="div" sx={{ fontWeight: 'bold', mb: "0.5vw", fontFamily: "Unbounded" }}>
{car.name}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: "0.5vw" }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: "0.5vw" }}>
<CalendarTodayIcon fontSize="small" color="success" />
<Typography variant="body2" color="text.secondary" sx={{ fontFamily: "Unbounded" }}>
Год выпуска: <b>{car.year}</b>
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: "0.5vw" }}>
<SpeedIcon fontSize="small" color="primary" />
<Typography variant="body2" color="text.secondary" sx={{ fontFamily: "Unbounded" }}>
Пробег: <b>{car.mileage} км</b>
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: "0.5vw" }}>
<AttachMoneyIcon fontSize="small" color="secondary" />
<Typography variant="body2" color="text.secondary" sx={{ fontFamily: "Unbounded" }}>
Цена: <b>{formatPrice(car.price)}</b>
</Typography>
</Box>
</Box>
</CardContent>
<Divider />
<CardActions sx={{ justifyContent: 'space-between', p: "1vw" }}>
<Button
size="small"
startIcon={<EditIcon />}
onClick={() => handleOpenEditDialog(car)}
sx={{
color: 'black',
'&:hover': { color: '#C27664', bgcolor: 'transparent' },
transition: "all 0.2s ease",
fontFamily: "Unbounded",
fontWeight: "bold",
fontSize: "0.8vw",
}}
>
Изменить
</Button>
<Button
size="small"
startIcon={<DeleteIcon />}
onClick={() => handleOpenDeleteDialog(car)}
sx={{
color: 'rgb(214, 74, 74)',
'&:hover': { color: 'rgba(150, 0, 0, 1)', bgcolor: 'transparent' },
transition: "all 0.2s ease",
fontFamily: "Unbounded",
fontWeight: "bold",
fontSize: "0.8vw",
}}
>
Удалить
</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
);
return (
<Box sx={{ p: 3 }}>
{/* Заголовок и кнопки управления */}
<Paper
elevation={3}
sx={{
p: "2vw",
mb: "2vw",
borderRadius: "1vw",
background: 'linear-gradient(to right, #2D2D2D, #3D3D3D)'
}}
>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: "1vw"
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: "1vw" }}>
<DirectionsCarIcon sx={{ fontSize: "3.5vw", color: '#C27664' }} />
<Typography variant="h5" sx={{ fontFamily: 'Unbounded', fontWeight: 'bold', color: 'white' }}>
Управление автомобилями
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: "1vw" }}>
<Button
variant="contained"
onClick={handleOpenCreateDialog}
startIcon={<AddIcon />}
sx={{
bgcolor: "#C27664",
color: "white",
textTransform: "none",
fontFamily: "Unbounded",
borderRadius: "1vw",
px: "2vw",
"&:hover": { bgcolor: "#945B4D" },
}}
>
Добавить автомобиль
</Button>
</Box>
</Box>
</Paper>
{/* Панель поиска и переключения вида */}
<Paper
elevation={2}
sx={{
p: "1.5vw",
mb: "2vw",
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderRadius: "1vw",
flexWrap: 'wrap',
gap: "1vw",
boxShadow: '0 0.5vw 1vw rgba(0,0,0,0.15)'
}}
>
<TextField
placeholder="Поиск автомобилей..."
variant="outlined"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
size="small"
sx={{
minWidth: 300,
'& .MuiOutlinedInput-root': {
borderRadius: "1vw"
}
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mr: "1vw", fontFamily: "Unbounded" }}>
Всего: {totalCars} автомобилей
</Typography>
<Tabs
value={viewMode}
onChange={(_, newValue) => setViewMode(newValue)}
sx={{
'& .MuiTab-root': { minWidth: 'auto' },
'& .Mui-selected': { color: '#C27664 !important' },
'& .MuiTabs-indicator': { backgroundColor: '#C27664' }
}}
>
<Tab
icon={<ViewListIcon />}
value="list"
sx={{ minWidth: 'auto' }}
/>
<Tab
icon={<ViewModuleIcon />}
value="grid"
sx={{ minWidth: 'auto' }}
/>
</Tabs>
</Box>
</Paper>
{/* Сообщения об ошибках */}
{error && (
<Alert severity="error" sx={{ mb: "1vw", borderRadius: "1vw" }}>
{error}
</Alert>
)}
{/* Индикатор загрузки */}
{loading ? (
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: "10vw"
}}>
<CircularProgress sx={{ color: '#C27664' }} />
</Box>
) : filteredCars.length === 0 ? (
<Paper
elevation={2}
sx={{
p: "4vw",
textAlign: 'center',
borderRadius: "1vw",
bgcolor: '#f9f9f9',
boxShadow: '0 0.5vw 1vw rgba(0,0,0,0.15)'
}}
>
<Typography variant="h6" color="text.secondary" sx={{ fontFamily: "Unbounded" }}>
Автомобили не найдены
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontFamily: "Unbounded" }}>
Попробуйте изменить параметры поиска или добавьте новый автомобиль
</Typography>
</Paper>
) : (
// Отображение списка автомобилей в зависимости от выбранного режима
viewMode === 'list' ? renderTable() : renderGrid()
)}
{/* Диалог создания автомобиля */}
<Dialog
open={openCreateDialog}
onClose={handleCloseDialogs}
maxWidth="md"
fullWidth
PaperProps={{
sx: { borderRadius: "1vw" }
}}
>
<DialogTitle sx={{
fontFamily: 'Unbounded',
bgcolor: '#2D2D2D',
color: 'white',
display: 'flex',
alignItems: 'center',
gap: "1vw"
}}>
<AddIcon /> Добавить автомобиль
</DialogTitle>
<DialogContent sx={{ mt: "1vw" }}>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
{/* <TextField
name="name"
label="Название автомобиля"
value={formData.name}
sx={{ label: { fontFamily: "Unbounded" }, input: { fontFamily: "Unbounded" } }}
onChange={handleInputChange}
fullWidth
margin="normal"
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<DirectionsCarIcon color="action" />
</InputAdornment>
),
}}
/> */}
<TextField
fullWidth
placeholder="Название автомобиля"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
variant="outlined"
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: "1vw",
bgcolor: "white",
height: "4vw",
width: "100%",
border: "0.1vw solid #C27664",
fontFamily: "Unbounded",
fontSize: "1.2vw",
color: "#C27664",
mb: "1vw",
"& fieldset": {
border: "none",
},
},
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<DirectionsCarIcon sx={{ color: "#C27664" }} />
</InputAdornment>
),
}}
/>
{/* <TextField
name="year"
label="Год выпуска"
type="number"
value={formData.year}
sx={{ label: { fontFamily: "Unbounded" }, input: { fontFamily: "Unbounded" } }}
onChange={handleInputChange}
fullWidth
margin="normal"
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<CalendarTodayIcon color="action" />
</InputAdornment>
),
}}
/> */}
<TextField
fullWidth
placeholder="Год выпуска"
value={formData.year}
onChange={(e) => setFormData({ ...formData, year: e.target.value })}
variant="outlined"
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: "1vw",
bgcolor: "white",
height: "4vw",
width: "100%",
border: "0.1vw solid #C27664",
fontFamily: "Unbounded",
fontSize: "1.2vw",
color: "#C27664",
mb: "1vw",
"& fieldset": {
border: "none",
},
},
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<CalendarTodayIcon sx={{ color: "#C27664" }} />
</InputAdornment>
),
}}
/>
{/* <TextField
name="mileage"
label="Пробег (км)"
type="number"
value={formData.mileage}
sx={{ label: { fontFamily: "Unbounded" }, input: { fontFamily: "Unbounded" } }}
onChange={handleInputChange}
fullWidth
margin="normal"
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SpeedIcon color="action" />
</InputAdornment>
),
}}
/> */}
<TextField
fullWidth
placeholder="Пробег (км)"
value={formData.mileage}
onChange={(e) => setFormData({ ...formData, mileage: e.target.value })}
variant="outlined"
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: "1vw",
bgcolor: "white",
height: "4vw",
width: "100%",
border: "0.1vw solid #C27664",
fontFamily: "Unbounded",
fontSize: "1.2vw",
color: "#C27664",
mb: "1vw",
"& fieldset": {
border: "none",
},
},
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SpeedIcon sx={{ color: "#C27664" }} />
</InputAdornment>
),
}}
/>
{/* <TextField
name="price"
label="Цена (₽)"
type="number"
value={formData.price}
sx={{ label: { fontFamily: "Unbounded" }, input: { fontFamily: "Unbounded" } }}
onChange={handleInputChange}
fullWidth
margin="normal"
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<AttachMoneyIcon color="action" />
</InputAdornment>
),
}}
/> */}
<TextField
fullWidth
placeholder="Цена в рублях"
value={formData.price}
onChange={(e) => setFormData({ ...formData, price: e.target.value })}
variant="outlined"
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: "1vw",
bgcolor: "white",
height: "4vw",
width: "100%",
border: "0.1vw solid #C27664",
fontFamily: "Unbounded",
fontSize: "1.2vw",
color: "#C27664",
mb: "1vw",
"& fieldset": {
border: "none",
},
},
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Typography
sx={{
fontFamily: "Unbounded",
fontSize: "1.5vw",
color: "#C27664",
}}
>
</Typography>
</InputAdornment>
),
}}
/>
<FormControl fullWidth>
<InputLabel sx={{ fontFamily: "Unbounded", color: "#C27664", '&.Mui-focused': { color: "#C27664" } }}>Страна происхождения</InputLabel>
<Select
value={formData.country_of_origin}
onChange={(e) => setFormData({ ...formData, country_of_origin: e.target.value as string })}
displayEmpty
label="Страна происхождения"
sx={{
borderRadius: "1vw",
bgcolor: "white",
height: "4vw",
width: "100%",
fontFamily: "Unbounded",
fontSize: "1.2vw",
color: "#C27664",
mb: "1vw",
"& .MuiOutlinedInput-notchedOutline": {
border: "0.1vw solid #C27664",
},
"& .MuiSvgIcon-root": {
fontSize: "2vw",
color: "#C27664",
},
transition: "all 0.2s ease",
}}
>
<MenuItem value="Япония">Япония</MenuItem>
<MenuItem value="Китай">Китай</MenuItem>
<MenuItem value="США">США</MenuItem>
<MenuItem value="Германия">Германия</MenuItem>
<MenuItem value="Корея">Корея</MenuItem>
<MenuItem value="Италия">Италия</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel sx={{ fontFamily: "Unbounded", color: "#C27664", '&.Mui-focused': { color: "#C27664" } }}>Тип привода</InputLabel>
<Select
value={formData.drive_type}
onChange={(e) => setFormData({ ...formData, drive_type: e.target.value as string })}
displayEmpty
label="Тип привода"
sx={{
borderRadius: "1vw",
bgcolor: "white",
height: "4vw",
width: "100%",
fontFamily: "Unbounded",
fontSize: "1.2vw",
color: "#C27664",
mb: "1vw",
"& .MuiOutlinedInput-notchedOutline": {
border: "0.1vw solid #C27664",
},
"& .MuiSvgIcon-root": {
fontSize: "2vw",
color: "#C27664",
},
transition: "all 0.2s ease",
}}
>
<MenuItem value="FWD">FWD</MenuItem>
<MenuItem value="RWD">RWD</MenuItem>
<MenuItem value="4WD">4WD</MenuItem>
<MenuItem value="AWD">AWD</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel sx={{ fontFamily: "Unbounded", color: "#C27664", '&.Mui-focused': { color: "#C27664" } }}>Тип двигателя</InputLabel>
<Select
value={formData.engine_type}
onChange={(e) => setFormData({ ...formData, engine_type: e.target.value as string })}
displayEmpty
label="Тип двигателя"
sx={{
borderRadius: "1vw",
bgcolor: "white",
height: "4vw",
width: "100%",
fontFamily: "Unbounded",
fontSize: "1.2vw",
color: "#C27664",
mb: "1vw",
"& .MuiOutlinedInput-notchedOutline": {
border: "0.1vw solid #C27664",
},
"& .MuiSvgIcon-root": {
fontSize: "2vw",
color: "#C27664",
},
transition: "all 0.2s ease",
}}
>
<MenuItem value="Бензиновый">Бензиновый</MenuItem>
<MenuItem value="Дизельный">Дизельный</MenuItem>
<MenuItem value="Электрический">Электрический</MenuItem>
<MenuItem value="Гибридный">Гибридный</MenuItem>
</Select>
</FormControl>
{/* <TextField
name="engine_capacity"
label="Объем двигателя (л)"
type="number"
value={formData.engine_capacity}
sx={{ label: { fontFamily: "Unbounded" }, input: { fontFamily: "Unbounded" } }}
onChange={handleInputChange}
fullWidth
margin="normal"
variant="outlined"
/> */}
<TextField
fullWidth
placeholder="Объём двигателя"
value={formData.engine_capacity}
onChange={(e) => setFormData({ ...formData, engine_capacity: e.target.value })}
variant="outlined"
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: "1vw",
bgcolor: "white",
height: "4vw",
width: "100%",
border: "0.1vw solid #C27664",
fontFamily: "Unbounded",
fontSize: "1.2vw",
color: "#C27664",
"& fieldset": {
border: "none",
},
},
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Typography
sx={{
fontFamily: "Unbounded",
fontSize: "1.5vw",
color: "#C27664",
}}
>
см³
</Typography>
</InputAdornment>
),
}}
/>
<Button
variant="outlined"
component="label"
sx={{
mt: "1vw",
borderRadius: "1vw",
p: "1vw",
borderColor: '#C27664',
fontFamily: "Unbounded",
color: '#C27664',
'&:hover': {
borderColor: '#945B4D',
bgcolor: 'rgba(194, 118, 100, 0.1)'
}
}}
fullWidth
>
Загрузить изображение
<input
type="file"
hidden
accept="image/*"
onChange={handleImageChange}
/>
</Button>
</Grid>
<Grid item xs={12} md={6}>
<Paper
elevation={2}
sx={{
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: "1vw",
overflow: 'hidden',
bgcolor: '#2D2D2D',
boxShadow: '0 0.5vw 1vw rgba(0,0,0,0.15)'
}}
>
{imagePreview ? (
<Box
component="img"
src={imagePreview}
alt="Предпросмотр"
sx={{
width: '100%',
height: '100%',
objectFit: 'contain',
p: "1vw",
userSelect: "none",
pointerEvents: "none",
}}
/>
) : (
<Typography color="text.secondary" sx={{ p: "3vw", textAlign: 'center', color: 'white', fontFamily: "Unbounded" }}>
Предпросмотр изображения
</Typography>
)}
</Paper>
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ p: "1vw", bgcolor: '#f5f5f5' }}>
<Button
onClick={handleCloseDialogs}
sx={{
color: 'black',
fontFamily: "Unbounded",
borderRadius: "1vw",
px: "1vw",
"&:hover": { bgcolor: "transparent", color: '#C27664' },
transition: "all 0.2s ease",
}}
>
Отмена
</Button>
<Button
onClick={handleCreateCar}
disabled={!isFormValid()}
variant="contained"
sx={{
bgcolor: "#C27664",
color: "white",
fontFamily: "Unbounded",
borderRadius: "1vw",
px: "2vw",
"&:hover": { bgcolor: "#945B4D" },
"&.Mui-disabled": {
bgcolor: "rgba(194, 118, 100, 0.5)",
}
}}
>
Создать
</Button>
</DialogActions>
</Dialog>
{/* Диалог редактирования автомобиля */}
<Dialog
open={openEditDialog}
onClose={handleCloseDialogs}
maxWidth="md"
fullWidth
PaperProps={{
sx: { borderRadius: "1vw" }
}}
>
<DialogTitle sx={{
fontFamily: 'Unbounded',
bgcolor: '#2D2D2D',
color: 'white',
display: 'flex',
alignItems: 'center',
gap: "1vw"
}}>
<EditIcon /> Редактировать автомобиль
</DialogTitle>
<DialogContent sx={{ mt: "1vw" }}>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
name="name"
label="Название автомобиля"
value={formData.name}
sx={{ label: { fontFamily: "Unbounded" }, input: { fontFamily: "Unbounded" } }}
onChange={handleInputChange}
fullWidth
margin="normal"
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<DirectionsCarIcon color="action" />
</InputAdornment>
),
}}
/>
<TextField
name="year"
label="Год выпуска"
type="number"
value={formData.year}
sx={{ label: { fontFamily: "Unbounded" }, input: { fontFamily: "Unbounded" } }}
onChange={handleInputChange}
fullWidth
margin="normal"
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<CalendarTodayIcon color="action" />
</InputAdornment>
),
}}
/>
<TextField
name="mileage"
label="Пробег (км)"
type="number"
value={formData.mileage}
sx={{ label: { fontFamily: "Unbounded" }, input: { fontFamily: "Unbounded" } }}
onChange={handleInputChange}
fullWidth
margin="normal"
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SpeedIcon color="action" />
</InputAdornment>
),
}}
/>
<TextField
name="price"
label="Цена (₽)"
type="number"
value={formData.price}
sx={{ label: { fontFamily: "Unbounded" }, input: { fontFamily: "Unbounded" } }}
onChange={handleInputChange}
fullWidth
margin="normal"
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<AttachMoneyIcon color="action" />
</InputAdornment>
),
}}
/>
<Button
variant="outlined"
component="label"
sx={{
mt: "1vw",
fontFamily: "Unbounded",
borderRadius: "1vw",
p: "1vw",
borderColor: '#C27664',
color: '#C27664',
'&:hover': {
borderColor: '#945B4D',
bgcolor: '#C27664',
color: 'white',
},
transition: "all 0.2s ease",
}}
fullWidth
>
Изменить изображение
<input
type="file"
hidden
accept="image/*"
onChange={handleImageChange}
/>
</Button>
</Grid>
<Grid item xs={12} md={6}>
<Paper
elevation={2}
sx={{
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: "1vw",
overflow: 'hidden',
bgcolor: '#2D2D2D',
boxShadow: '0 0.5vw 1vw rgba(0,0,0,0.15)'
}}
>
{imagePreview ? (
<Box
component="img"
src={imagePreview}
alt="Предпросмотр"
sx={{
width: '100%',
height: '100%',
objectFit: 'contain',
p: "1vw",
userSelect: "none",
pointerEvents: "none",
}}
/>
) : (
<Typography color="text.secondary" sx={{ p: "3vw", textAlign: 'center', color: 'white', fontFamily: "Unbounded" }}>
Предпросмотр изображения
</Typography>
)}
</Paper>
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ p: "1vw", bgcolor: '#f5f5f5' }}>
<Button
onClick={handleCloseDialogs}
sx={{
color: 'black',
fontFamily: "Unbounded",
borderRadius: "1vw",
px: "1vw",
"&:hover": { bgcolor: "transparent", color: '#C27664' },
transition: "all 0.2s ease",
}}
>
Отмена
</Button>
<Button
onClick={handleUpdateCar}
disabled={!isFormValid()}
variant="contained"
sx={{
bgcolor: "#C27664",
color: "white",
fontFamily: "Unbounded",
borderRadius: "1vw",
px: "2vw",
"&:hover": { bgcolor: "#945B4D" },
"&.Mui-disabled": {
bgcolor: "rgba(194, 118, 100, 0.5)",
}
}}
>
Сохранить
</Button>
</DialogActions>
</Dialog>
{/* Диалог удаления автомобиля */}
<Dialog
open={openDeleteDialog}
onClose={handleCloseDialogs}
PaperProps={{
sx: { borderRadius: "1vw" }
}}
>
<DialogTitle sx={{
fontFamily: 'Unbounded',
bgcolor: '#2D2D2D',
color: 'white',
display: 'flex',
alignItems: 'center',
gap: "1vw"
}}>
<DeleteIcon /> Удалить автомобиль
</DialogTitle>
<DialogContent sx={{ mt: "1vw", minWidth: "40vw" }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: "1vw" }}>
{selectedCar && (
<Box
component="img"
src={getImageUrl(selectedCar.image)}
alt={selectedCar.name}
sx={{
width: "10vw",
height: "5vw",
objectFit: 'cover',
borderRadius: "1vw",
mr: "1vw",
pointerEvents: "none",
userSelect: "none",
}}
/>
)}
<Box>
<Typography variant="h6" sx={{ fontWeight: 'bold', fontFamily: "Unbounded" }}>
{selectedCar?.name}
</Typography>
{selectedCar && (
<Typography variant="body2" color="text.secondary" sx={{ fontFamily: "Unbounded" }}>
{selectedCar.year} г., {selectedCar.mileage} км, {formatPrice(selectedCar.price)}
</Typography>
)}
</Box>
</Box>
<Alert severity="warning" sx={{ mt: "1vw", fontFamily: "Unbounded" }}>
Вы уверены, что хотите удалить этот автомобиль? Это действие нельзя отменить.
</Alert>
</DialogContent>
<DialogActions sx={{ p: "1vw", bgcolor: '#f5f5f5' }}>
<Button
onClick={handleCloseDialogs}
sx={{
color: 'black',
borderRadius: "1vw",
px: "1vw",
fontFamily: "Unbounded",
"&:hover": { bgcolor: "transparent", color: '#C27664' },
transition: "all 0.2s ease",
}}
>
Отмена
</Button>
<Button
onClick={handleDeleteCar}
variant="contained"
color="error"
sx={{
borderRadius: "1vw",
px: "2vw",
fontFamily: "Unbounded",
bgcolor: "#C27664",
color: "white",
"&:hover": { bgcolor: "#945B4D" },
transition: "all 0.2s ease",
}}
>
Удалить
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default Vehicle;