add admin
This commit is contained in:
52
src/App.tsx
52
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 <Navigate to="/administrator" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const AppRoutes = () => {
|
||||
const { isAuth } = useAuth();
|
||||
const location = useLocation();
|
||||
const isAdminLoginPage = location.pathname === "/administrator";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
{isAdminLoginPage ? null : isAuth ? <AdminHeader /> : <Header />}
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<>
|
||||
<MainPage />
|
||||
</>
|
||||
}
|
||||
element={<MainPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/car/:id"
|
||||
element={<CarPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/administrator"
|
||||
element={<LoginPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/administrator/main"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AdminMainPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<AppRoutes />
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
113
src/components/AdminHeader.tsx
Normal file
113
src/components/AdminHeader.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<AppBar position="fixed" sx={{ bgcolor: "#2D2D2D" }}>
|
||||
<Toolbar sx={{ justifyContent: "space-between", height: "5vw", userSelect: "none" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={logo}
|
||||
alt="logo"
|
||||
onClick={() => 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 && (
|
||||
<Box sx={{ display: "flex", gap: "1vw"}}>
|
||||
<Button
|
||||
onClick={() => handleScrollToAnchor("#vehicles")}
|
||||
sx={{
|
||||
color: "white",
|
||||
fontFamily: "Unbounded",
|
||||
textTransform: "none",
|
||||
fontSize: isMobile ? "3vw" : "1.2vw",
|
||||
"&:hover": { color: "#C27664" }
|
||||
}}
|
||||
>
|
||||
Автомобили
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleScrollToAnchor("#personal")}
|
||||
sx={{
|
||||
color: "white",
|
||||
fontFamily: "Unbounded",
|
||||
textTransform: "none",
|
||||
fontSize: isMobile ? "3vw" : "1.2vw",
|
||||
"&:hover": { color: "#C27664" }
|
||||
}}
|
||||
>
|
||||
Персонал
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", gap: "2vw" }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile ? "3vw" : "1.2vw",
|
||||
color: "white",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
Панель администратора
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleLogout}
|
||||
sx={{
|
||||
bgcolor: "#C27664",
|
||||
color: "white",
|
||||
fontSize: isMobile ? "2.5vw" : "1vw",
|
||||
padding: isMobile ? "1.5vw 3vw" : "0.5vw 2vw",
|
||||
textTransform: "none",
|
||||
borderRadius: isMobile ? "3vw" : "1vw",
|
||||
fontWeight: "light",
|
||||
fontFamily: "Unbounded",
|
||||
"&:hover": { bgcolor: "#945B4D" },
|
||||
}}
|
||||
>
|
||||
Выйти
|
||||
</Button>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* Пустой блок для компенсации фиксированного хедера */}
|
||||
<Box sx={{ height: isMobile ? "15vw" : "5vw", bgcolor: "#2D2D2D" }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminHeader;
|
67
src/context/AuthContext.tsx
Normal file
67
src/context/AuthContext.tsx
Normal file
@ -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<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [isAuth, setIsAuth] = useState<boolean>(false);
|
||||
const [token, setToken] = useState<string | null>(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 (
|
||||
<AuthContext.Provider value={{ isAuth, token, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth должен использоваться внутри AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
@ -5,5 +5,5 @@
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #505050;
|
||||
background-color: white;
|
||||
}
|
||||
|
50
src/pages/admin/AdminMainPage.tsx
Normal file
50
src/pages/admin/AdminMainPage.tsx
Normal file
@ -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 <Navigate to="/administrator" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', color: 'black', minHeight: '100vh' }}>
|
||||
<Container maxWidth="lg" sx={{ pt: "1vw" }}>
|
||||
<Box id="vehicles">
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="h2"
|
||||
sx={{
|
||||
fontFamily: 'Unbounded',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
</Typography>
|
||||
<Vehicle />
|
||||
</Box>
|
||||
|
||||
<Divider marginBottomDivider='1vw' marginTopDivider='5vw' />
|
||||
|
||||
<Box id="personal">
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="h2"
|
||||
sx={{
|
||||
fontFamily: 'Unbounded',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
</Typography>
|
||||
<Personal />
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminMainPage;
|
135
src/pages/admin/LoginPage.tsx
Normal file
135
src/pages/admin/LoginPage.tsx
Normal file
@ -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 (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
bgcolor: '#f5f5f5',
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 4,
|
||||
width: '100%',
|
||||
maxWidth: '400px',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
mb: 3,
|
||||
textAlign: 'center',
|
||||
fontFamily: 'Unbounded',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
Вход в панель администратора
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Имя пользователя"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Пароль"
|
||||
type="password"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{ mb: 3 }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleLogin}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
bgcolor: "#C27664",
|
||||
color: "white",
|
||||
textTransform: "none",
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
fontWeight: "light",
|
||||
fontFamily: "Unbounded",
|
||||
"&:hover": { bgcolor: "#945B4D" },
|
||||
}}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} color="inherit" /> : 'Войти'}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
968
src/pages/admin/Personal.tsx
Normal file
968
src/pages/admin/Personal.tsx
Normal file
@ -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<TeamMember[]>([]);
|
||||
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<TeamMember | null>(null);
|
||||
|
||||
// Состояния для формы
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
surname: '',
|
||||
role: '',
|
||||
});
|
||||
const [photoFile, setPhotoFile] = useState<File | null>(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<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value
|
||||
});
|
||||
};
|
||||
|
||||
const handlePhotoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 = () => (
|
||||
<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>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredStaff.map((member) => (
|
||||
<TableRow
|
||||
key={member.id}
|
||||
sx={{
|
||||
'&:hover': {
|
||||
backgroundColor: '#f5f5f5'
|
||||
},
|
||||
transition: 'background-color 0.3s'
|
||||
}}
|
||||
>
|
||||
<TableCell>{member.id}</TableCell>
|
||||
<TableCell>
|
||||
<Avatar
|
||||
src={getImageUrl(member.photo)}
|
||||
alt={`${member.name} ${member.surname}`}
|
||||
sx={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
>
|
||||
{getInitials(member.name, member.surname)}
|
||||
</Avatar>
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'medium' }}>{member.name}</TableCell>
|
||||
<TableCell>{member.surname}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
icon={<WorkIcon />}
|
||||
label={member.role}
|
||||
size="small"
|
||||
sx={{ bgcolor: '#e3f2fd', color: '#1565c0' }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title="Редактировать" arrow TransitionComponent={Fade} TransitionProps={{ timeout: 600 }}>
|
||||
<IconButton
|
||||
onClick={() => handleOpenEditDialog(member)}
|
||||
color="primary"
|
||||
sx={{
|
||||
bgcolor: 'rgba(25, 118, 210, 0.1)',
|
||||
mr: 1,
|
||||
'&:hover': {
|
||||
bgcolor: 'rgba(25, 118, 210, 0.2)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Удалить" arrow TransitionComponent={Fade} TransitionProps={{ timeout: 600 }}>
|
||||
<IconButton
|
||||
onClick={() => handleOpenDeleteDialog(member)}
|
||||
color="error"
|
||||
sx={{
|
||||
bgcolor: 'rgba(211, 47, 47, 0.1)',
|
||||
'&:hover': {
|
||||
bgcolor: 'rgba(211, 47, 47, 0.2)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
// Рендер сетки карточек
|
||||
const renderGrid = () => (
|
||||
<Grid container spacing={3}>
|
||||
{filteredStaff.map((member) => (
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={member.id}>
|
||||
<Card
|
||||
elevation={3}
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 2,
|
||||
transition: 'transform 0.3s, box-shadow 0.3s',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-5px)',
|
||||
boxShadow: '0 10px 20px rgba(0,0,0,0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'relative', pt: '100%', overflow: 'hidden' }}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={getImageUrl(member.photo)}
|
||||
alt={`${member.name} ${member.surname}`}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<CardContent sx={{ flexGrow: 1, pt: 2 }}>
|
||||
<Typography gutterBottom variant="h6" component="div" sx={{ fontWeight: 'bold', mb: 0.5 }}>
|
||||
{member.name} {member.surname}
|
||||
</Typography>
|
||||
|
||||
<Chip
|
||||
icon={<WorkIcon />}
|
||||
label={member.role}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: '#e3f2fd',
|
||||
color: '#1565c0',
|
||||
mt: 1
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
<Divider />
|
||||
|
||||
<CardActions sx={{ justifyContent: 'space-between', p: 1.5 }}>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<EditIcon />}
|
||||
onClick={() => handleOpenEditDialog(member)}
|
||||
sx={{
|
||||
color: '#1976d2',
|
||||
'&:hover': { bgcolor: 'rgba(25, 118, 210, 0.1)' }
|
||||
}}
|
||||
>
|
||||
Изменить
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={() => handleOpenDeleteDialog(member)}
|
||||
sx={{
|
||||
color: '#d32f2f',
|
||||
'&:hover': { bgcolor: 'rgba(211, 47, 47, 0.1)' }
|
||||
}}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
{/* Заголовок и кнопки управления */}
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 3,
|
||||
mb: 3,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(to right, #2D2D2D, #3D3D3D)'
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<PersonIcon sx={{ fontSize: 40, color: '#C27664' }} />
|
||||
<Typography variant="h5" sx={{ fontFamily: 'Unbounded', fontWeight: 'bold', color: 'white' }}>
|
||||
Управление персоналом
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleOpenCreateDialog}
|
||||
startIcon={<AddIcon />}
|
||||
sx={{
|
||||
bgcolor: "#C27664",
|
||||
color: "white",
|
||||
textTransform: "none",
|
||||
fontFamily: "Unbounded",
|
||||
borderRadius: 2,
|
||||
px: 3,
|
||||
"&:hover": { bgcolor: "#945B4D" },
|
||||
}}
|
||||
>
|
||||
Добавить сотрудника
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Панель поиска и переключения вида */}
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 3,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderRadius: 2,
|
||||
flexWrap: 'wrap',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
placeholder="Поиск сотрудников..."
|
||||
variant="outlined"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: 300,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 2
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mr: 2 }}>
|
||||
Всего: {totalStaff} сотрудников
|
||||
</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: 2, borderRadius: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Индикатор загрузки */}
|
||||
{loading ? (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: 400
|
||||
}}>
|
||||
<CircularProgress sx={{ color: '#C27664' }} />
|
||||
</Box>
|
||||
) : filteredStaff.length === 0 ? (
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: 'center',
|
||||
borderRadius: 2,
|
||||
bgcolor: '#f9f9f9'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Сотрудники не найдены
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Попробуйте изменить параметры поиска или добавьте нового сотрудника
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
// Отображение списка сотрудников в зависимости от выбранного режима
|
||||
viewMode === 'list' ? renderTable() : renderGrid()
|
||||
)}
|
||||
|
||||
{/* Диалог создания сотрудника */}
|
||||
<Dialog
|
||||
open={openCreateDialog}
|
||||
onClose={handleCloseDialogs}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: { borderRadius: 2 }
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{
|
||||
fontFamily: 'Unbounded',
|
||||
bgcolor: '#2D2D2D',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}>
|
||||
<AddIcon /> Добавить сотрудника
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ mt: 2 }}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
name="name"
|
||||
label="Имя"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<PersonIcon color="action" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
name="surname"
|
||||
label="Фамилия"
|
||||
value={formData.surname}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<BadgeIcon color="action" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
name="role"
|
||||
label="Должность"
|
||||
value={formData.role}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<WorkIcon color="action" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="label"
|
||||
sx={{
|
||||
mt: 2,
|
||||
borderRadius: 2,
|
||||
p: 1.5,
|
||||
borderColor: '#C27664',
|
||||
color: '#C27664',
|
||||
'&:hover': {
|
||||
borderColor: '#945B4D',
|
||||
bgcolor: 'rgba(194, 118, 100, 0.1)'
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
Загрузить фото
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
accept="image/*"
|
||||
onChange={handlePhotoChange}
|
||||
/>
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
bgcolor: '#f5f5f5'
|
||||
}}
|
||||
>
|
||||
{photoPreview ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={photoPreview}
|
||||
alt="Предпросмотр"
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
p: 2
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography color="text.secondary" sx={{ p: 4, textAlign: 'center' }}>
|
||||
Предпросмотр фото
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 2, bgcolor: '#f5f5f5' }}>
|
||||
<Button
|
||||
onClick={handleCloseDialogs}
|
||||
sx={{
|
||||
color: '#666',
|
||||
borderRadius: 2,
|
||||
px: 3
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateMember}
|
||||
disabled={!isFormValid()}
|
||||
variant="contained"
|
||||
sx={{
|
||||
bgcolor: "#C27664",
|
||||
color: "white",
|
||||
borderRadius: 2,
|
||||
px: 3,
|
||||
"&: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: 2 }
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{
|
||||
fontFamily: 'Unbounded',
|
||||
bgcolor: '#2D2D2D',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}>
|
||||
<EditIcon /> Редактировать сотрудника
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ mt: 2 }}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
name="name"
|
||||
label="Имя"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<PersonIcon color="action" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
name="surname"
|
||||
label="Фамилия"
|
||||
value={formData.surname}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<BadgeIcon color="action" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
name="role"
|
||||
label="Должность"
|
||||
value={formData.role}
|
||||
onChange={handleInputChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<WorkIcon color="action" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="label"
|
||||
sx={{
|
||||
mt: 2,
|
||||
borderRadius: 2,
|
||||
p: 1.5,
|
||||
borderColor: '#C27664',
|
||||
color: '#C27664',
|
||||
'&:hover': {
|
||||
borderColor: '#945B4D',
|
||||
bgcolor: 'rgba(194, 118, 100, 0.1)'
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
Изменить фото
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
accept="image/*"
|
||||
onChange={handlePhotoChange}
|
||||
/>
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
bgcolor: '#f5f5f5'
|
||||
}}
|
||||
>
|
||||
{photoPreview ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={photoPreview}
|
||||
alt="Предпросмотр"
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
p: 2
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography color="text.secondary" sx={{ p: 4, textAlign: 'center' }}>
|
||||
Предпросмотр фото
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 2, bgcolor: '#f5f5f5' }}>
|
||||
<Button
|
||||
onClick={handleCloseDialogs}
|
||||
sx={{
|
||||
color: '#666',
|
||||
borderRadius: 2,
|
||||
px: 3
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdateMember}
|
||||
disabled={!isFormValid()}
|
||||
variant="contained"
|
||||
sx={{
|
||||
bgcolor: "#C27664",
|
||||
color: "white",
|
||||
borderRadius: 2,
|
||||
px: 3,
|
||||
"&:hover": { bgcolor: "#945B4D" },
|
||||
"&.Mui-disabled": {
|
||||
bgcolor: "rgba(194, 118, 100, 0.5)",
|
||||
}
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Диалог удаления сотрудника */}
|
||||
<Dialog
|
||||
open={openDeleteDialog}
|
||||
onClose={handleCloseDialogs}
|
||||
PaperProps={{
|
||||
sx: { borderRadius: 2 }
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{
|
||||
fontFamily: 'Unbounded',
|
||||
bgcolor: '#2D2D2D',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}>
|
||||
<DeleteIcon /> Удалить сотрудника
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ mt: 2, minWidth: 400 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
{selectedMember && (
|
||||
<Avatar
|
||||
src={getImageUrl(selectedMember.photo)}
|
||||
alt={`${selectedMember.name} ${selectedMember.surname}`}
|
||||
sx={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
mr: 2
|
||||
}}
|
||||
>
|
||||
{selectedMember && getInitials(selectedMember.name, selectedMember.surname)}
|
||||
</Avatar>
|
||||
)}
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
|
||||
{selectedMember?.name} {selectedMember?.surname}
|
||||
</Typography>
|
||||
{selectedMember && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{selectedMember.role}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
Вы уверены, что хотите удалить этого сотрудника? Это действие нельзя отменить.
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 2, bgcolor: '#f5f5f5' }}>
|
||||
<Button
|
||||
onClick={handleCloseDialogs}
|
||||
sx={{
|
||||
color: '#666',
|
||||
borderRadius: 2,
|
||||
px: 3
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDeleteMember}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
px: 3
|
||||
}}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Personal;
|
1415
src/pages/admin/Vehicle.tsx
Normal file
1415
src/pages/admin/Vehicle.tsx
Normal file
File diff suppressed because it is too large
Load Diff
101
src/utils/api.ts
101
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<Car, 'id'>, 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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<TokenResponse> => {
|
||||
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<UserData> => {
|
||||
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<boolean> => {
|
||||
try {
|
||||
const userData = await getCurrentUser(token);
|
||||
return userData.is_active;
|
||||
} catch (error) {
|
||||
console.error('Authentication check failed:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user