diff --git a/src/App.tsx b/src/App.tsx index b16dc74..879d315 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,28 +1,64 @@ -import { Routes, Route } from 'react-router-dom' +import { Routes, Route, Navigate } from 'react-router-dom' import MainPage from './pages/MainPage.tsx' import CarPage from './pages/CarPage.tsx' import Header from './components/Header.tsx' +import AdminHeader from './components/AdminHeader.tsx' +import LoginPage from './pages/admin/LoginPage.tsx' +import AdminMainPage from './pages/admin/AdminMainPage.tsx' +import { AuthProvider, useAuth } from './context/AuthContext.tsx' +import { useLocation } from 'react-router-dom' + +// Защищенный маршрут для админ-панели +const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { + const { isAuth } = useAuth(); + + if (!isAuth) { + return ; + } + + return <>{children}; +}; + +const AppRoutes = () => { + const { isAuth } = useAuth(); + const location = useLocation(); + const isAdminLoginPage = location.pathname === "/administrator"; -function App() { return ( <> -
+ {isAdminLoginPage ? null : isAuth ? :
} - - - } + element={} /> } /> + } + /> + + + + } + /> ) } +function App() { + return ( + + + + ) +} + export default App diff --git a/src/components/AdminHeader.tsx b/src/components/AdminHeader.tsx new file mode 100644 index 0000000..c2671ad --- /dev/null +++ b/src/components/AdminHeader.tsx @@ -0,0 +1,113 @@ +import React from "react"; +import { Box, Typography, Button, AppBar, Toolbar } from "@mui/material"; +import logo from "../assets/icon/autobro.png"; +import { useResponsive } from "../theme/useResponsive"; +import { useAuth } from "../context/AuthContext"; +import { useNavigate } from "react-router-dom"; +import { scrollToAnchor } from "../utils/scrollUtils"; + +const AdminHeader = () => { + const { isMobile } = useResponsive(); + const { logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogout = () => { + logout(); + localStorage.removeItem('token'); + navigate("/administrator", { replace: true }); + }; + + const handleScrollToAnchor = (anchor: string) => { + scrollToAnchor(anchor, () => {}, false); + }; + + return ( + <> + + + + navigate("/administrator/main")} + sx={{ + width: isMobile ? "17vw" : "9vw", + height: isMobile ? "6vw" : "2.8vw", + cursor: "pointer", + "&:hover": { scale: 1.1 }, + transition: "all 0.4s ease", + marginRight: "2vw", + userSelect: "none", + }} + /> + + {!isMobile && ( + + + + + )} + + + + + Панель администратора + + + + + + + {/* Пустой блок для компенсации фиксированного хедера */} + + + ); +}; + +export default AdminHeader; \ No newline at end of file diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..6d78bdc --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,67 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { checkAuth } from '../utils/api'; + +interface AuthContextType { + isAuth: boolean; + token: string | null; + login: (token: string) => void; + logout: () => void; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [isAuth, setIsAuth] = useState(false); + const [token, setToken] = useState(localStorage.getItem('token')); + + useEffect(() => { + const verifyToken = async () => { + if (token) { + try { + const isActive = await checkAuth(token); + setIsAuth(isActive); + if (!isActive) { + // Если токен недействителен, удаляем его + localStorage.removeItem('token'); + setToken(null); + } + } catch (error) { + console.error('Ошибка проверки токена:', error); + localStorage.removeItem('token'); + setToken(null); + setIsAuth(false); + } + } else { + setIsAuth(false); + } + }; + + verifyToken(); + }, [token]); + + const login = (token: string) => { + localStorage.setItem('token', token); + setToken(token); + setIsAuth(true); + }; + + const logout = () => { + localStorage.removeItem('token'); + setToken(null); + setIsAuth(false); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth должен использоваться внутри AuthProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 209b7a5..a361e5e 100644 --- a/src/index.css +++ b/src/index.css @@ -5,5 +5,5 @@ body { padding: 0; margin: 0; - background-color: #505050; + background-color: white; } diff --git a/src/pages/admin/AdminMainPage.tsx b/src/pages/admin/AdminMainPage.tsx new file mode 100644 index 0000000..5a7f163 --- /dev/null +++ b/src/pages/admin/AdminMainPage.tsx @@ -0,0 +1,50 @@ +import { Box, Typography, Container } from '@mui/material'; +import Vehicle from './Vehicle'; +import Personal from './Personal'; +import { Navigate } from 'react-router-dom'; +import Divider from '../../components/Divider'; + +const AdminMainPage = () => { + const isAuth = localStorage.getItem('token') !== null; + + // Перенаправление на страницу логина, если пользователь не авторизован + if (!isAuth) { + return ; + } + + return ( + + + + + + + + + + + + + + + + + + ); +}; + +export default AdminMainPage; diff --git a/src/pages/admin/LoginPage.tsx b/src/pages/admin/LoginPage.tsx new file mode 100644 index 0000000..f6550cf --- /dev/null +++ b/src/pages/admin/LoginPage.tsx @@ -0,0 +1,135 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Typography, TextField, Button, Paper, Alert, CircularProgress } from '@mui/material'; +import { useAuth } from '../../context/AuthContext'; +import { useNavigate } from 'react-router-dom'; +import { getAuthToken, getCurrentUser } from '../../utils/api'; + +const LoginPage = () => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const { login, isAuth } = useAuth(); + const navigate = useNavigate(); + + // Проверяем авторизацию при загрузке страницы + useEffect(() => { + if (isAuth) { + navigate('/administrator/main', { replace: true }); + } + }, [isAuth, navigate]); + + const handleLogin = async () => { + if (!username || !password) { + setError('Введите имя пользователя и пароль'); + return; + } + + setLoading(true); + setError(''); + + try { + // Получение токена авторизации + const tokenData = await getAuthToken(username, password); + const accessToken = tokenData.access_token; + + // Получение данных пользователя и проверка активности + const userData = await getCurrentUser(accessToken); + + // Проверка активности пользователя + if (!userData.is_active) { + throw new Error('Учетная запись не активна'); + } + + // Сохраняем токен и перенаправляем пользователя + login(accessToken); + // Перенаправление произойдет автоматически благодаря useEffect + + } catch (err) { + setError(err instanceof Error ? err.message : 'Произошла ошибка'); + } finally { + setLoading(false); + } + }; + + return ( + + + + Вход в панель администратора + + + {error && ( + + {error} + + )} + + setUsername(e.target.value)} + /> + + setPassword(e.target.value)} + sx={{ mb: 3 }} + /> + + + + + ); +}; + +export default LoginPage; \ No newline at end of file diff --git a/src/pages/admin/Personal.tsx b/src/pages/admin/Personal.tsx new file mode 100644 index 0000000..15d93b1 --- /dev/null +++ b/src/pages/admin/Personal.tsx @@ -0,0 +1,968 @@ +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, + Avatar +} 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 PersonIcon from '@mui/icons-material/Person'; +import BadgeIcon from '@mui/icons-material/Badge'; +import WorkIcon from '@mui/icons-material/Work'; +import { useAuth } from '../../context/AuthContext'; +import { + fetchTeam, + createTeamMember, + updateTeamMember, + deleteTeamMember, + getImageUrl +} from '../../utils/api'; + +// Определяем типы здесь, так как они не экспортируются из api.ts +interface TeamMember { + id: number; + name: string; + surname: string; + role: string; + photo: string; +} + +interface TeamResponse { + staff: TeamMember[]; + total: number; +} + +const Personal = () => { + const { token } = useAuth(); + const [staff, setStaff] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [totalStaff, setTotalStaff] = 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 [selectedMember, setSelectedMember] = useState(null); + + // Состояния для формы + const [formData, setFormData] = useState({ + name: '', + surname: '', + role: '', + }); + const [photoFile, setPhotoFile] = useState(null); + const [photoPreview, setPhotoPreview] = useState(''); + + // Загрузка списка сотрудников + const loadStaff = async () => { + setLoading(true); + try { + const data: TeamResponse = await fetchTeam(); + setStaff(data.staff); + setTotalStaff(data.total); + setError(''); + } catch (err) { + setError('Не удалось загрузить список сотрудников'); + console.error(err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadStaff(); + }, []); + + // Фильтрация сотрудников по поисковому запросу + const filteredStaff = staff.filter(member => + member.name.toLowerCase().includes(searchQuery.toLowerCase()) || + member.surname.toLowerCase().includes(searchQuery.toLowerCase()) || + member.role.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Обработчики для формы + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value + }); + }; + + const handlePhotoChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + setPhotoFile(file); + setPhotoPreview(URL.createObjectURL(file)); + } + }; + + // Открытие диалога создания + const handleOpenCreateDialog = () => { + setFormData({ + name: '', + surname: '', + role: '', + }); + setPhotoFile(null); + setPhotoPreview(''); + setOpenCreateDialog(true); + }; + + // Открытие диалога редактирования + const handleOpenEditDialog = (member: TeamMember) => { + setSelectedMember(member); + setFormData({ + name: member.name, + surname: member.surname, + role: member.role, + }); + setPhotoPreview(getImageUrl(member.photo)); + setPhotoFile(null); + setOpenEditDialog(true); + }; + + // Открытие диалога удаления + const handleOpenDeleteDialog = (member: TeamMember) => { + setSelectedMember(member); + setOpenDeleteDialog(true); + }; + + // Закрытие диалогов + const handleCloseDialogs = () => { + setOpenCreateDialog(false); + setOpenEditDialog(false); + setOpenDeleteDialog(false); + setSelectedMember(null); + }; + + // Создание сотрудника + const handleCreateMember = async () => { + try { + const memberData = { + name: formData.name, + surname: formData.surname, + role: formData.role, + photo: '' + }; + + await createTeamMember(memberData, photoFile || undefined); + handleCloseDialogs(); + loadStaff(); + } catch (err) { + setError('Не удалось создать сотрудника'); + console.error(err); + } + }; + + // Обновление сотрудника + const handleUpdateMember = async () => { + if (!selectedMember) return; + + try { + const memberData = { + name: formData.name, + surname: formData.surname, + role: formData.role, + }; + + await updateTeamMember(selectedMember.id, memberData, photoFile || undefined); + handleCloseDialogs(); + loadStaff(); + } catch (err) { + setError('Не удалось обновить сотрудника'); + console.error(err); + } + }; + + // Удаление сотрудника + const handleDeleteMember = async () => { + if (!selectedMember) return; + + try { + await deleteTeamMember(selectedMember.id); + handleCloseDialogs(); + loadStaff(); + } catch (err) { + setError('Не удалось удалить сотрудника'); + console.error(err); + } + }; + + // Валидация формы + const isFormValid = () => { + return ( + formData.name.trim() !== '' && + formData.surname.trim() !== '' && + formData.role.trim() !== '' + ); + }; + + // Функция для получения инициалов из имени и фамилии + const getInitials = (name: string, surname: string) => { + return `${name.charAt(0)}${surname.charAt(0)}`.toUpperCase(); + }; + + // Рендер таблицы + const renderTable = () => ( + + + + + ID + Фото + Имя + Фамилия + Должность + Действия + + + + {filteredStaff.map((member) => ( + + {member.id} + + + {getInitials(member.name, member.surname)} + + + {member.name} + {member.surname} + + } + label={member.role} + size="small" + sx={{ bgcolor: '#e3f2fd', color: '#1565c0' }} + /> + + + + handleOpenEditDialog(member)} + color="primary" + sx={{ + bgcolor: 'rgba(25, 118, 210, 0.1)', + mr: 1, + '&:hover': { + bgcolor: 'rgba(25, 118, 210, 0.2)', + } + }} + > + + + + + handleOpenDeleteDialog(member)} + color="error" + sx={{ + bgcolor: 'rgba(211, 47, 47, 0.1)', + '&:hover': { + bgcolor: 'rgba(211, 47, 47, 0.2)', + } + }} + > + + + + + + ))} + +
+
+ ); + + // Рендер сетки карточек + const renderGrid = () => ( + + {filteredStaff.map((member) => ( + + + + + + + + {member.name} {member.surname} + + + } + label={member.role} + size="small" + sx={{ + bgcolor: '#e3f2fd', + color: '#1565c0', + mt: 1 + }} + /> + + + + + + + + + + + ))} + + ); + + return ( + + {/* Заголовок и кнопки управления */} + + + + + + Управление персоналом + + + + + + + + + + {/* Панель поиска и переключения вида */} + + setSearchQuery(e.target.value)} + size="small" + sx={{ + minWidth: 300, + '& .MuiOutlinedInput-root': { + borderRadius: 2 + } + }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + + Всего: {totalStaff} сотрудников + + + setViewMode(newValue)} + sx={{ + '& .MuiTab-root': { minWidth: 'auto' }, + '& .Mui-selected': { color: '#C27664 !important' }, + '& .MuiTabs-indicator': { backgroundColor: '#C27664' } + }} + > + } + value="list" + sx={{ minWidth: 'auto' }} + /> + } + value="grid" + sx={{ minWidth: 'auto' }} + /> + + + + + {/* Сообщения об ошибках */} + {error && ( + + {error} + + )} + + {/* Индикатор загрузки */} + {loading ? ( + + + + ) : filteredStaff.length === 0 ? ( + + + Сотрудники не найдены + + + Попробуйте изменить параметры поиска или добавьте нового сотрудника + + + ) : ( + // Отображение списка сотрудников в зависимости от выбранного режима + viewMode === 'list' ? renderTable() : renderGrid() + )} + + {/* Диалог создания сотрудника */} + + + Добавить сотрудника + + + + + + + + ), + }} + /> + + + + ), + }} + /> + + + + ), + }} + /> + + + + + {photoPreview ? ( + + ) : ( + + Предпросмотр фото + + )} + + + + + + + + + + + {/* Диалог редактирования сотрудника */} + + + Редактировать сотрудника + + + + + + + + ), + }} + /> + + + + ), + }} + /> + + + + ), + }} + /> + + + + + {photoPreview ? ( + + ) : ( + + Предпросмотр фото + + )} + + + + + + + + + + + {/* Диалог удаления сотрудника */} + + + Удалить сотрудника + + + + {selectedMember && ( + + {selectedMember && getInitials(selectedMember.name, selectedMember.surname)} + + )} + + + {selectedMember?.name} {selectedMember?.surname} + + {selectedMember && ( + + {selectedMember.role} + + )} + + + + + Вы уверены, что хотите удалить этого сотрудника? Это действие нельзя отменить. + + + + + + + + + ); +}; + +export default Personal; \ No newline at end of file diff --git a/src/pages/admin/Vehicle.tsx b/src/pages/admin/Vehicle.tsx new file mode 100644 index 0000000..21074d7 --- /dev/null +++ b/src/pages/admin/Vehicle.tsx @@ -0,0 +1,1415 @@ +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([]); + 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(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(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) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value + }); + }; + + const handleImageChange = (e: React.ChangeEvent) => { + 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 = () => ( + + + + + ID + Изображение + Название + Год + Пробег + Цена + Действия + + + + {filteredCars.map((car) => ( + + {car.id} + + + + {car.name} + + } + label={car.year} + size="small" + sx={{ bgcolor: '#e8f5e9', color: '#2e7d32' }} + /> + + + } + label={`${car.mileage} км`} + size="small" + sx={{ bgcolor: '#e3f2fd', color: '#1565c0' }} + /> + + + } + label={formatPrice(car.price)} + size="small" + sx={{ bgcolor: '#fce4ec', color: '#c2185b' }} + /> + + + + handleOpenEditDialog(car)} + color="primary" + sx={{ + color: "black", + '&:hover': { + color: '#C27664', + bgcolor: "transparent" + }, + transition: "all 0.2s ease", + }} + > + + + + + handleOpenDeleteDialog(car)} + color="error" + sx={{ + '&:hover': { + color: '#C27664', + bgcolor: 'transparent', + }, + transition: "all 0.2s ease", + }} + > + + + + + + ))} + +
+
+ ); + + // Рендер сетки карточек + const renderGrid = () => ( + + {filteredCars.map((car) => ( + + + + + + {car.name} + + + + + + + Год выпуска: {car.year} + + + + + + + Пробег: {car.mileage} км + + + + + + + Цена: {formatPrice(car.price)} + + + + + + + + + + + + + + ))} + + ); + + return ( + + {/* Заголовок и кнопки управления */} + + + + + + Управление автомобилями + + + + + + + + + + {/* Панель поиска и переключения вида */} + + setSearchQuery(e.target.value)} + size="small" + sx={{ + minWidth: 300, + '& .MuiOutlinedInput-root': { + borderRadius: "1vw" + } + }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + + Всего: {totalCars} автомобилей + + + setViewMode(newValue)} + sx={{ + '& .MuiTab-root': { minWidth: 'auto' }, + '& .Mui-selected': { color: '#C27664 !important' }, + '& .MuiTabs-indicator': { backgroundColor: '#C27664' } + }} + > + } + value="list" + sx={{ minWidth: 'auto' }} + /> + } + value="grid" + sx={{ minWidth: 'auto' }} + /> + + + + + {/* Сообщения об ошибках */} + {error && ( + + {error} + + )} + + {/* Индикатор загрузки */} + {loading ? ( + + + + ) : filteredCars.length === 0 ? ( + + + Автомобили не найдены + + + Попробуйте изменить параметры поиска или добавьте новый автомобиль + + + ) : ( + // Отображение списка автомобилей в зависимости от выбранного режима + viewMode === 'list' ? renderTable() : renderGrid() + )} + + {/* Диалог создания автомобиля */} + + + Добавить автомобиль + + + + + {/* + + + ), + }} + /> */} + 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: ( + + + + ), + }} + /> + {/* + + + ), + }} + /> */} + 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: ( + + + + ), + }} + /> + {/* + + + ), + }} + /> */} + 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: ( + + + + ), + }} + /> + {/* + + + ), + }} + /> */} + 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: ( + + + ₽ + + + ), + }} + /> + + Страна происхождения + + + + Тип привода + + + + Тип двигателя + + + {/* */} + 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: ( + + + см³ + + + ), + }} + /> + + + + + {imagePreview ? ( + + ) : ( + + Предпросмотр изображения + + )} + + + + + + + + + + + {/* Диалог редактирования автомобиля */} + + + Редактировать автомобиль + + + + + + + + ), + }} + /> + + + + ), + }} + /> + + + + ), + }} + /> + + + + ), + }} + /> + + + + + {imagePreview ? ( + + ) : ( + + Предпросмотр изображения + + )} + + + + + + + + + + + {/* Диалог удаления автомобиля */} + + + Удалить автомобиль + + + + {selectedCar && ( + + )} + + + {selectedCar?.name} + + {selectedCar && ( + + {selectedCar.year} г., {selectedCar.mileage} км, {formatPrice(selectedCar.price)} + + )} + + + + + Вы уверены, что хотите удалить этот автомобиль? Это действие нельзя отменить. + + + + + + + + + ); +}; + +export default Vehicle; \ No newline at end of file diff --git a/src/utils/api.ts b/src/utils/api.ts index 6637113..4e0d087 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -7,6 +7,15 @@ export interface Car { year: number; mileage: number; price: number; + base_price: number; + country_of_origin: string; + drive_type: string; + engine_type: string; + engine_capacity?: number; + engine_power?: number; + electric_motor_power?: number; + hybrid_type?: string; + power_ratio?: string; image: string; } @@ -84,6 +93,9 @@ export const createCar = async (carData: Omit, image?: File): Promise const response = await fetch(`${API_BASE_URL}/cars`, { method: 'POST', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, body: formData, }); @@ -112,6 +124,9 @@ export const updateCar = async ( } const response = await fetch(`${API_BASE_URL}/cars/${carId}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, method: 'PUT', body: formData, }); @@ -130,6 +145,9 @@ export const updateCar = async ( export const deleteCar = async (carId: number): Promise => { try { const response = await fetch(`${API_BASE_URL}/cars/${carId}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, method: 'DELETE', }); @@ -189,6 +207,9 @@ export const createTeamMember = async ( } const response = await fetch(`${API_BASE_URL}/personal`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, method: 'POST', body: formData, }); @@ -218,6 +239,9 @@ export const updateTeamMember = async ( } const response = await fetch(`${API_BASE_URL}/personal/${memberId}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, method: 'PUT', body: formData, }); @@ -236,6 +260,9 @@ export const updateTeamMember = async ( export const deleteTeamMember = async (memberId: number): Promise => { try { const response = await fetch(`${API_BASE_URL}/personal/${memberId}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, method: 'DELETE', }); @@ -246,3 +273,77 @@ export const deleteTeamMember = async (memberId: number): Promise => { return handleApiError(error); } }; + + + + +// LOGIN + +// Типы данных для авторизации +export interface TokenResponse { + access_token: string; + token_type: string; +} + +export interface UserData { + id: number; + username: string; + is_active: boolean; +} + +// Получение токена авторизации +export const getAuthToken = async (username: string, password: string): Promise => { + try { + const response = await fetch(`${API_BASE_URL}/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + username, + password, + }), + }); + + if (!response.ok) { + throw new Error('Неверное имя пользователя или пароль'); + } + + const data = await response.json(); + localStorage.setItem('token', data.access_token); + + return data; + } catch (error) { + return handleApiError(error); + } +}; + +// Получение данных текущего пользователя +export const getCurrentUser = async (token: string): Promise => { + try { + const response = await fetch(`${API_BASE_URL}/users/me`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Не удалось получить данные пользователя'); + } + + return await response.json(); + } catch (error) { + return handleApiError(error); + } +}; + +// Проверка авторизации пользователя +export const checkAuth = async (token: string): Promise => { + try { + const userData = await getCurrentUser(token); + return userData.is_active; + } catch (error) { + console.error('Authentication check failed:', error); + return false; + } +};