diff --git a/.vs/logistics-app/FileContentIndex/07dfedf3-4b38-4be1-b6d6-5119e0f01a73.vsidx b/.vs/logistics-app/FileContentIndex/07dfedf3-4b38-4be1-b6d6-5119e0f01a73.vsidx deleted file mode 100644 index 3872d21..0000000 Binary files a/.vs/logistics-app/FileContentIndex/07dfedf3-4b38-4be1-b6d6-5119e0f01a73.vsidx and /dev/null differ diff --git a/.vs/logistics-app/FileContentIndex/7e5bbd21-0bd4-47e0-8ee6-9684a8c31103.vsidx b/.vs/logistics-app/FileContentIndex/7e5bbd21-0bd4-47e0-8ee6-9684a8c31103.vsidx deleted file mode 100644 index 796f7a6..0000000 Binary files a/.vs/logistics-app/FileContentIndex/7e5bbd21-0bd4-47e0-8ee6-9684a8c31103.vsidx and /dev/null differ diff --git a/.vs/logistics-app/FileContentIndex/c5a6c98d-7f4d-4b71-b220-310ab288f298.vsidx b/.vs/logistics-app/FileContentIndex/c5a6c98d-7f4d-4b71-b220-310ab288f298.vsidx deleted file mode 100644 index 607184a..0000000 Binary files a/.vs/logistics-app/FileContentIndex/c5a6c98d-7f4d-4b71-b220-310ab288f298.vsidx and /dev/null differ diff --git a/.vs/logistics-app/FileContentIndex/f8f06d21-6130-4351-a6b3-f29d63051768.vsidx b/.vs/logistics-app/FileContentIndex/f8f06d21-6130-4351-a6b3-f29d63051768.vsidx deleted file mode 100644 index ade0caa..0000000 Binary files a/.vs/logistics-app/FileContentIndex/f8f06d21-6130-4351-a6b3-f29d63051768.vsidx and /dev/null differ diff --git a/.vs/logistics-app/FileContentIndex/feeecddc-f0ee-4bfe-9db7-a14e32700516.vsidx b/.vs/logistics-app/FileContentIndex/feeecddc-f0ee-4bfe-9db7-a14e32700516.vsidx deleted file mode 100644 index 8cb7ff0..0000000 Binary files a/.vs/logistics-app/FileContentIndex/feeecddc-f0ee-4bfe-9db7-a14e32700516.vsidx and /dev/null differ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f00e1dd --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ms-dotnettools.csharp" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 42b8b46..95ddd91 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,10 +2,11 @@ "version": "0.2.0", "configurations": [ + { "name": "Debug React App", - "type": "firefox", + "type": "msedge", "request": "launch", "url": "http://localhost:3000", "webRoot": "${workspaceFolder}/src", diff --git a/db.json b/db.json index 5cf59bf..2bf0342 100644 --- a/db.json +++ b/db.json @@ -5,7 +5,7 @@ "clientName": "Иванов Иван Иванович", "orderCost": 15000, "orderDate": "2025-01-15", - "status": "in_progress", + "status": "completed", "address": "ул. Ленина, 10", "description": "Доставка строительных материалов" }, @@ -32,7 +32,7 @@ "clientName": "Сергеев Алексей Петрович", "orderCost": 12000, "orderDate": "2025-01-18", - "status": "pending", + "status": "in_progress", "address": "пр. Космонавтов, 78", "description": "Переезд квартиры" }, @@ -77,7 +77,7 @@ "clientName": "Александрова Ольга Викторовна", "orderCost": 9500, "orderDate": "2025-01-23", - "status": "pending", + "status": "cancelled", "address": "ул. Центральная, 89", "description": "Перевозка личных вещей" }, @@ -184,14 +184,6 @@ "endTime": "12:00", "date": "2025-11-21" }, - { - "id": "e320", - "vehicleId": "1", - "orderId": "1", - "startTime": "08:00", - "endTime": "10:00", - "date": "2025-11-21" - }, { "id": "08a1", "vehicleId": "2", @@ -207,6 +199,22 @@ "startTime": "08:00", "endTime": "09:00", "date": "2025-11-21" + }, + { + "id": "6f66", + "vehicleId": "1", + "orderId": "1", + "startTime": "08:00", + "endTime": "11:00", + "date": "2025-11-21" + }, + { + "id": "f275", + "vehicleId": "2", + "orderId": "4", + "startTime": "08:00", + "endTime": "10:00", + "date": "2025-11-23" } ] } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 1faee8c..7bc6847 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,53 +1,122 @@ -import React from 'react'; -import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { BrowserRouter as Router, Routes, Route, Link, Navigate } from 'react-router-dom'; import OrdersPage from './pages/OrdersPage'; import VehiclesPage from './pages/VehiclesPage'; import VehicleDetailPage from './pages/VehicleDetailPage'; +import LoginPage from './pages/LoginPage'; +import AdminPage from './pages/AdminPage'; +import ProtectedRoute from './components/ProtectedRoute'; +import AdminRoute from './components/AdminRoute'; import 'bootstrap/dist/css/bootstrap.min.css'; import './App.css'; const App: React.FC = () => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const checkAuth = (): boolean => { + return document.cookie.includes('auth_token'); + }; + + useEffect(() => { + setIsAuthenticated(checkAuth()); + + const interval = setInterval(() => { + const currentAuth = checkAuth(); + if (currentAuth !== isAuthenticated) { + setIsAuthenticated(currentAuth); + } + }, 1000); + + return () => clearInterval(interval); + }, [isAuthenticated]); + + + const handleLogout = () => { + + document.cookie = 'auth_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + document.cookie = 'user_data=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + setIsAuthenticated(false); + window.location.href = '/login'; + }; + return (
- {/* Навигация */} - + + )} - {/* Основной контент */}
- } /> - } /> - } /> - } /> + } /> + + setIsAuthenticated(true)} />} + /> + + + + + } + /> + + + + } + /> + + + + } + /> + + + + + } + /> + + } />
diff --git a/src/components/AdminRoute.tsx b/src/components/AdminRoute.tsx new file mode 100644 index 0000000..5dfba98 --- /dev/null +++ b/src/components/AdminRoute.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; + +interface AdminRouteProps { + children: React.ReactNode; +} + +const AdminRoute: React.FC = ({ children }) => { + // Функция для получения значения cookie + const getCookie = (name: string): string | null => { + const nameEQ = name + "="; + const ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; + }; + + const getUserRole = (): string | null => { + const token = getCookie('auth_token'); + if (!token) return null; + + try { + const payload = token.split('.')[1]; + const decodedPayload = JSON.parse(atob(payload)); + return decodedPayload['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'] || + decodedPayload.role || + null; + } catch (error) { + console.error('Error decoding token:', error); + return null; + } + }; + + const token = getCookie('auth_token'); + const userRole = getUserRole(); + + console.log('AdminRoute - checking admin access...'); + console.log('Token exists:', !!token); + console.log('User role:', userRole); + + if (!token) { + console.log('No token found, redirecting to login'); + return ; + } + + if (userRole !== 'admin') { + console.log('User is not admin, redirecting to orders'); + return ; + } + + console.log('User is admin, allowing access to admin panel'); + return <>{children}; +}; + +export default AdminRoute; \ No newline at end of file diff --git a/src/components/OrderFilters.tsx b/src/components/OrderFilters.tsx index 122c1df..21e28e8 100644 --- a/src/components/OrderFilters.tsx +++ b/src/components/OrderFilters.tsx @@ -1,28 +1,54 @@ import React from 'react'; +import { OrderFiltersType as OrderFiltersType } from '../types'; interface OrderFiltersProps { - filters: { - clientName: string; - minCost: string; - maxCost: string; - orderDate: string; - status: string; - }; - onFiltersChange: (filters: any) => void; + filters: OrderFiltersType; + onFiltersChange: (filters: OrderFiltersType) => void; + onApplyFilters: () => void; } -const OrderFilters: React.FC = ({ filters, onFiltersChange }) => { - const handleChange = (field: string, value: string) => { +const OrderFilters: React.FC = ({ filters, onFiltersChange, onApplyFilters }) => { + const handleChange = (field: keyof OrderFiltersType, value: string | number | undefined) => { onFiltersChange({ ...filters, [field]: value }); }; + const handleClearFilters = () => { + onFiltersChange({ + clientName: '', + minCost: undefined, + maxCost: undefined, + orderDate: '', + status: '', + page: 1, + pageSize: 50 + }); + }; + + const handleApply = () => { + onApplyFilters(); + }; + return (
-
+
Фильтры заказов
+
+ + +
@@ -32,54 +58,54 @@ const OrderFilters: React.FC = ({ filters, onFiltersChange }) type="text" className="form-control" id="clientName" - value={filters.clientName} + value={filters.clientName || ''} onChange={(e) => handleChange('clientName', e.target.value)} placeholder="ФИО или наименование" />
- +
handleChange('minCost', e.target.value)} + value={filters.minCost || ''} + onChange={(e) => handleChange('minCost', e.target.value ? parseFloat(e.target.value) : undefined)} placeholder="0" />
- +
handleChange('maxCost', e.target.value)} + value={filters.maxCost || ''} + onChange={(e) => handleChange('maxCost', e.target.value ? parseFloat(e.target.value) : undefined)} placeholder="100000" />
- +
handleChange('orderDate', e.target.value)} />
- +
setUsername(e.target.value)} + disabled={loading} + autoFocus + onKeyPress={(e) => { + if (e.key === 'Enter' && !loading) { + handleLoginClick(); + } + }} + /> +
+ +
+ + setPassword(e.target.value)} + disabled={loading} + onKeyPress={(e) => { + if (e.key === 'Enter' && !loading) { + handleLoginClick(); + } + }} + /> +
+ + + +
+ + Тестовые данные:
+ Логин: admin Пароль: admin123 (админ)
+ Логин: user Пароль: user123 (пользователь) +
+
+
+
+
+ + + ); +}; + +export default LoginPage; \ No newline at end of file diff --git a/src/pages/OrdersPage.tsx b/src/pages/OrdersPage.tsx index 829805d..97a3d28 100644 --- a/src/pages/OrdersPage.tsx +++ b/src/pages/OrdersPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Order } from '../types'; +import { Order, OrderFiltersType } from '../types'; import { ordersApi } from '../services/api'; import OrderFilters from '../components/OrderFilters'; import Loading from '../components/Loading'; @@ -7,26 +7,47 @@ import ErrorAlert from '../components/ErrorAlert'; const OrdersPage: React.FC = () => { const [orders, setOrders] = useState([]); - const [filteredOrders, setFilteredOrders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [totalCount, setTotalCount] = useState(0); - const [filters, setFilters] = useState({ + const [filters, setFilters] = useState({ clientName: '', - minCost: '', - maxCost: '', + minCost: undefined, + maxCost: undefined, orderDate: '', - status: '' + status: '', + page: 1, + pageSize: 50 }); - // Загрузка данных - const loadOrders = async () => { + const loadOrders = async (currentFilters: OrderFiltersType) => { try { setLoading(true); setError(null); - const response = await ordersApi.getOrders(); - setOrders(response.data); - setFilteredOrders(response.data); + + const apiFilters: OrderFiltersType = { + clientName: currentFilters.clientName || undefined, + minCost: currentFilters.minCost ? parseFloat(currentFilters.minCost.toString()) : undefined, + maxCost: currentFilters.maxCost ? parseFloat(currentFilters.maxCost.toString()) : undefined, + orderDate: currentFilters.orderDate || undefined, + status: currentFilters.status || undefined, + page: currentFilters.page, + pageSize: currentFilters.pageSize + }; + + Object.keys(apiFilters).forEach(key => { + if (apiFilters[key as keyof OrderFiltersType] === undefined || apiFilters[key as keyof OrderFiltersType] === '') { + delete apiFilters[key as keyof OrderFiltersType]; + } + }); + + const response = await ordersApi.getOrders(apiFilters); + let count = (response.data as any).data.items.length; + + setOrders((response.data as any).data.items); + setTotalCount(count); + } catch (err) { setError('Не удалось загрузить список заказов'); console.error('Error loading orders:', err); @@ -35,29 +56,24 @@ const OrdersPage: React.FC = () => { } }; - // Функция изменения статуса заказа const updateOrderStatus = async (orderId: number, newStatus: Order['status']) => { try { - // Находим заказ для обновления const orderToUpdate = orders.find(order => order.id === orderId); if (!orderToUpdate) return; - // Создаем обновленный заказ const updatedOrder = { ...orderToUpdate, status: newStatus }; - // Отправляем запрос на сервер await ordersApi.updateOrder(orderId, updatedOrder); - - // Обновляем локальное состояние - setOrders(prevOrders => - prevOrders.map(order => + + setOrders(prevOrders => + prevOrders.map(order => order.id === orderId ? updatedOrder : order ) ); - + console.log(`Статус заказа ${orderId} изменен на: ${newStatus}`); } catch (err) { setError('Не удалось обновить статус заказа'); @@ -66,63 +82,33 @@ const OrdersPage: React.FC = () => { }; useEffect(() => { - loadOrders(); + loadOrders(filters); }, []); - // Применение фильтров - useEffect(() => { - let result = orders; - - if (filters.clientName) { - result = result.filter(order => - order.clientName.toLowerCase().includes(filters.clientName.toLowerCase()) - ); - } - - if (filters.minCost) { - result = result.filter(order => - order.orderCost >= parseFloat(filters.minCost) - ); - } - - if (filters.maxCost) { - result = result.filter(order => - order.orderCost <= parseFloat(filters.maxCost) - ); - } - - if (filters.orderDate) { - result = result.filter(order => - order.orderDate === filters.orderDate - ); - } - - if (filters.status) { - result = result.filter(order => - order.status === filters.status - ); - } - - setFilteredOrders(result); - }, [filters, orders]); + const handleApplyFilters = () => { + loadOrders(filters); + }; if (loading) return ; - if (error) return ; + if (error) return loadOrders(filters)} />; return (

Список заказов

- - +
- Найдено заказов: {filteredOrders.length} + Найдено заказов: {totalCount}
- {filteredOrders.length === 0 ? ( + {orders.length === 0 ? (

Заказы не найдены

) : (
@@ -139,26 +125,25 @@ const OrdersPage: React.FC = () => { - {filteredOrders.map(order => ( + {orders.map(order => ( {order.id} {order.clientName} {order.orderCost.toLocaleString()} руб. {new Date(order.orderDate).toLocaleDateString()} - + order.status === 'pending' ? 'bg-warning' : 'bg-danger' + }`}> {order.status === 'pending' ? 'Ожидает' : - order.status === 'in_progress' ? 'В процессе' : - order.status === 'completed' ? 'Завершен' : 'Отменен'} + order.status === 'in_progress' ? 'В процессе' : + order.status === 'completed' ? 'Завершен' : 'Отменен'} {order.address} -