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
+ }}
+ />
+
+
+
+
+
+ }
+ onClick={() => handleOpenEditDialog(member)}
+ sx={{
+ color: '#1976d2',
+ '&:hover': { bgcolor: 'rgba(25, 118, 210, 0.1)' }
+ }}
+ >
+ Изменить
+
+ }
+ onClick={() => handleOpenDeleteDialog(member)}
+ sx={{
+ color: '#d32f2f',
+ '&:hover': { bgcolor: 'rgba(211, 47, 47, 0.1)' }
+ }}
+ >
+ Удалить
+
+
+
+
+ ))}
+
+ );
+
+ return (
+
+ {/* Заголовок и кнопки управления */}
+
+
+
+
+
+ Управление персоналом
+
+
+
+
+ }
+ sx={{
+ bgcolor: "#C27664",
+ color: "white",
+ textTransform: "none",
+ fontFamily: "Unbounded",
+ borderRadius: 2,
+ px: 3,
+ "&:hover": { bgcolor: "#945B4D" },
+ }}
+ >
+ Добавить сотрудника
+
+
+
+
+
+ {/* Панель поиска и переключения вида */}
+
+ 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()
+ )}
+
+ {/* Диалог создания сотрудника */}
+
+
+ {/* Диалог редактирования сотрудника */}
+
+
+ {/* Диалог удаления сотрудника */}
+
+
+ );
+};
+
+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)}
+
+
+
+
+
+
+
+
+ }
+ onClick={() => handleOpenEditDialog(car)}
+ sx={{
+ color: 'black',
+ '&:hover': { color: '#C27664', bgcolor: 'transparent' },
+ transition: "all 0.2s ease",
+ fontFamily: "Unbounded",
+ fontWeight: "bold",
+ fontSize: "0.8vw",
+ }}
+ >
+ Изменить
+
+ }
+ 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",
+ }}
+ >
+ Удалить
+
+
+
+
+ ))}
+
+ );
+
+ return (
+
+ {/* Заголовок и кнопки управления */}
+
+
+
+
+
+ Управление автомобилями
+
+
+
+
+ }
+ sx={{
+ bgcolor: "#C27664",
+ color: "white",
+ textTransform: "none",
+ fontFamily: "Unbounded",
+ borderRadius: "1vw",
+ px: "2vw",
+ "&:hover": { bgcolor: "#945B4D" },
+ }}
+ >
+ Добавить автомобиль
+
+
+
+
+
+ {/* Панель поиска и переключения вида */}
+
+ 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()
+ )}
+
+ {/* Диалог создания автомобиля */}
+
+
+ {/* Диалог редактирования автомобиля */}
+
+
+ {/* Диалог удаления автомобиля */}
+
+
+ );
+};
+
+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;
+ }
+};