Update
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ms-dotnettools.csharp"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -2,10 +2,11 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Debug React App",
|
"name": "Debug React App",
|
||||||
"type": "firefox",
|
"type": "msedge",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"url": "http://localhost:3000",
|
"url": "http://localhost:3000",
|
||||||
"webRoot": "${workspaceFolder}/src",
|
"webRoot": "${workspaceFolder}/src",
|
||||||
|
|||||||
30
db.json
30
db.json
@@ -5,7 +5,7 @@
|
|||||||
"clientName": "Иванов Иван Иванович",
|
"clientName": "Иванов Иван Иванович",
|
||||||
"orderCost": 15000,
|
"orderCost": 15000,
|
||||||
"orderDate": "2025-01-15",
|
"orderDate": "2025-01-15",
|
||||||
"status": "in_progress",
|
"status": "completed",
|
||||||
"address": "ул. Ленина, 10",
|
"address": "ул. Ленина, 10",
|
||||||
"description": "Доставка строительных материалов"
|
"description": "Доставка строительных материалов"
|
||||||
},
|
},
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"clientName": "Сергеев Алексей Петрович",
|
"clientName": "Сергеев Алексей Петрович",
|
||||||
"orderCost": 12000,
|
"orderCost": 12000,
|
||||||
"orderDate": "2025-01-18",
|
"orderDate": "2025-01-18",
|
||||||
"status": "pending",
|
"status": "in_progress",
|
||||||
"address": "пр. Космонавтов, 78",
|
"address": "пр. Космонавтов, 78",
|
||||||
"description": "Переезд квартиры"
|
"description": "Переезд квартиры"
|
||||||
},
|
},
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"clientName": "Александрова Ольга Викторовна",
|
"clientName": "Александрова Ольга Викторовна",
|
||||||
"orderCost": 9500,
|
"orderCost": 9500,
|
||||||
"orderDate": "2025-01-23",
|
"orderDate": "2025-01-23",
|
||||||
"status": "pending",
|
"status": "cancelled",
|
||||||
"address": "ул. Центральная, 89",
|
"address": "ул. Центральная, 89",
|
||||||
"description": "Перевозка личных вещей"
|
"description": "Перевозка личных вещей"
|
||||||
},
|
},
|
||||||
@@ -184,14 +184,6 @@
|
|||||||
"endTime": "12:00",
|
"endTime": "12:00",
|
||||||
"date": "2025-11-21"
|
"date": "2025-11-21"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "e320",
|
|
||||||
"vehicleId": "1",
|
|
||||||
"orderId": "1",
|
|
||||||
"startTime": "08:00",
|
|
||||||
"endTime": "10:00",
|
|
||||||
"date": "2025-11-21"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "08a1",
|
"id": "08a1",
|
||||||
"vehicleId": "2",
|
"vehicleId": "2",
|
||||||
@@ -207,6 +199,22 @@
|
|||||||
"startTime": "08:00",
|
"startTime": "08:00",
|
||||||
"endTime": "09:00",
|
"endTime": "09:00",
|
||||||
"date": "2025-11-21"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
141
src/App.tsx
141
src/App.tsx
@@ -1,53 +1,122 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Link, Navigate } from 'react-router-dom';
|
||||||
import OrdersPage from './pages/OrdersPage';
|
import OrdersPage from './pages/OrdersPage';
|
||||||
import VehiclesPage from './pages/VehiclesPage';
|
import VehiclesPage from './pages/VehiclesPage';
|
||||||
import VehicleDetailPage from './pages/VehicleDetailPage';
|
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 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(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 (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
{/* Навигация */}
|
{isAuthenticated && (
|
||||||
<nav className="navbar navbar-expand-lg navbar-dark bg-primary">
|
<nav className="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Link className="navbar-brand" to="/">
|
<Link className="navbar-brand" to="/orders">
|
||||||
Логистическая компания
|
Логистическая компания
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<div className="collapse navbar-collapse">
|
||||||
className="navbar-toggler"
|
<ul className="navbar-nav me-auto">
|
||||||
type="button"
|
<li className="nav-item">
|
||||||
data-bs-toggle="collapse"
|
<Link className="nav-link" to="/orders">Заказы</Link>
|
||||||
data-bs-target="#navbarNav"
|
</li>
|
||||||
>
|
<li className="nav-item">
|
||||||
<span className="navbar-toggler-icon"></span>
|
<Link className="nav-link" to="/vehicles">Машины</Link>
|
||||||
</button>
|
</li>
|
||||||
<div className="collapse navbar-collapse" id="navbarNav">
|
<li className="nav-item">
|
||||||
<ul className="navbar-nav">
|
<Link className="nav-link" to="/admin">Админ-панель</Link>
|
||||||
<li className="nav-item">
|
</li>
|
||||||
<Link className="nav-link" to="/orders">
|
</ul>
|
||||||
Заказы
|
<div className="navbar-nav">
|
||||||
</Link>
|
<button
|
||||||
</li>
|
className="btn btn-outline-light btn-sm"
|
||||||
<li className="nav-item">
|
onClick={handleLogout}
|
||||||
<Link className="nav-link" to="/vehicles">
|
>
|
||||||
Машины
|
Выйти
|
||||||
</Link>
|
</button>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</nav>
|
)}
|
||||||
|
|
||||||
{/* Основной контент */}
|
|
||||||
<main>
|
<main>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<OrdersPage />} />
|
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||||
<Route path="/orders" element={<OrdersPage />} />
|
|
||||||
<Route path="/vehicles" element={<VehiclesPage />} />
|
<Route
|
||||||
<Route path="/vehicles/:id" element={<VehicleDetailPage />} />
|
path="/login"
|
||||||
|
element={<LoginPage onLogin={() => setIsAuthenticated(true)} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/orders"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<OrdersPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/vehicles"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<VehiclesPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/vehicles/:id"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<VehicleDetailPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/admin"
|
||||||
|
element={
|
||||||
|
<AdminRoute>
|
||||||
|
<AdminPage />
|
||||||
|
</AdminRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
58
src/components/AdminRoute.tsx
Normal file
58
src/components/AdminRoute.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface AdminRouteProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdminRoute: React.FC<AdminRouteProps> = ({ 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 <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userRole !== 'admin') {
|
||||||
|
console.log('User is not admin, redirecting to orders');
|
||||||
|
return <Navigate to="/orders" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('User is admin, allowing access to admin panel');
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminRoute;
|
||||||
@@ -1,28 +1,54 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { OrderFiltersType as OrderFiltersType } from '../types';
|
||||||
|
|
||||||
interface OrderFiltersProps {
|
interface OrderFiltersProps {
|
||||||
filters: {
|
filters: OrderFiltersType;
|
||||||
clientName: string;
|
onFiltersChange: (filters: OrderFiltersType) => void;
|
||||||
minCost: string;
|
onApplyFilters: () => void;
|
||||||
maxCost: string;
|
|
||||||
orderDate: string;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
onFiltersChange: (filters: any) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderFilters: React.FC<OrderFiltersProps> = ({ filters, onFiltersChange }) => {
|
const OrderFilters: React.FC<OrderFiltersProps> = ({ filters, onFiltersChange, onApplyFilters }) => {
|
||||||
const handleChange = (field: string, value: string) => {
|
const handleChange = (field: keyof OrderFiltersType, value: string | number | undefined) => {
|
||||||
onFiltersChange({
|
onFiltersChange({
|
||||||
...filters,
|
...filters,
|
||||||
[field]: value
|
[field]: value
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
onFiltersChange({
|
||||||
|
clientName: '',
|
||||||
|
minCost: undefined,
|
||||||
|
maxCost: undefined,
|
||||||
|
orderDate: '',
|
||||||
|
status: '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
onApplyFilters();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
<div className="card-header">
|
<div className="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 className="mb-0">Фильтры заказов</h5>
|
<h5 className="mb-0">Фильтры заказов</h5>
|
||||||
|
<div className="d-flex">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline-secondary me-2"
|
||||||
|
onClick={handleClearFilters}
|
||||||
|
>
|
||||||
|
Сбросить
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
onClick={handleApply}
|
||||||
|
>
|
||||||
|
Применить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="row g-3">
|
<div className="row g-3">
|
||||||
@@ -32,54 +58,54 @@ const OrderFilters: React.FC<OrderFiltersProps> = ({ filters, onFiltersChange })
|
|||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="clientName"
|
id="clientName"
|
||||||
value={filters.clientName}
|
value={filters.clientName || ''}
|
||||||
onChange={(e) => handleChange('clientName', e.target.value)}
|
onChange={(e) => handleChange('clientName', e.target.value)}
|
||||||
placeholder="ФИО или наименование"
|
placeholder="ФИО или наименование"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-2">
|
<div className="col-md-2">
|
||||||
<label htmlFor="minCost" className="form-label">Мин. стоимость</label>
|
<label htmlFor="minCost" className="form-label">Мин. стоимость</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="minCost"
|
id="minCost"
|
||||||
value={filters.minCost}
|
value={filters.minCost || ''}
|
||||||
onChange={(e) => handleChange('minCost', e.target.value)}
|
onChange={(e) => handleChange('minCost', e.target.value ? parseFloat(e.target.value) : undefined)}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-2">
|
<div className="col-md-2">
|
||||||
<label htmlFor="maxCost" className="form-label">Макс. стоимость</label>
|
<label htmlFor="maxCost" className="form-label">Макс. стоимость</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="maxCost"
|
id="maxCost"
|
||||||
value={filters.maxCost}
|
value={filters.maxCost || ''}
|
||||||
onChange={(e) => handleChange('maxCost', e.target.value)}
|
onChange={(e) => handleChange('maxCost', e.target.value ? parseFloat(e.target.value) : undefined)}
|
||||||
placeholder="100000"
|
placeholder="100000"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-2">
|
<div className="col-md-2">
|
||||||
<label htmlFor="orderDate" className="form-label">Дата заказа</label>
|
<label htmlFor="orderDate" className="form-label">Дата заказа</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="orderDate"
|
id="orderDate"
|
||||||
value={filters.orderDate}
|
value={filters.orderDate || ''}
|
||||||
onChange={(e) => handleChange('orderDate', e.target.value)}
|
onChange={(e) => handleChange('orderDate', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-3">
|
<div className="col-md-3">
|
||||||
<label htmlFor="status" className="form-label">Статус</label>
|
<label htmlFor="status" className="form-label">Статус</label>
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
id="status"
|
id="status"
|
||||||
value={filters.status}
|
value={filters.status || ''}
|
||||||
onChange={(e) => handleChange('status', e.target.value)}
|
onChange={(e) => handleChange('status', e.target.value || undefined)}
|
||||||
>
|
>
|
||||||
<option value="">Все статусы</option>
|
<option value="">Все статусы</option>
|
||||||
<option value="pending">Ожидает</option>
|
<option value="pending">Ожидает</option>
|
||||||
|
|||||||
18
src/components/ProtectedRoute.tsx
Normal file
18
src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
||||||
|
const token = document.cookie.includes('auth_token');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtectedRoute;
|
||||||
@@ -1,26 +1,52 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { VehicleFiltersType as VehicleFiltersType } from '../types';
|
||||||
|
|
||||||
interface VehicleFiltersProps {
|
interface VehicleFiltersProps {
|
||||||
filters: {
|
filters: VehicleFiltersType;
|
||||||
driverName: string;
|
onFiltersChange: (filters: VehicleFiltersType) => void;
|
||||||
vehicleType: string;
|
onApplyFilters: () => void;
|
||||||
licensePlate: string;
|
|
||||||
};
|
|
||||||
onFiltersChange: (filters: any) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const VehicleFilters: React.FC<VehicleFiltersProps> = ({ filters, onFiltersChange }) => {
|
const VehicleFilters: React.FC<VehicleFiltersProps> = ({ filters, onFiltersChange, onApplyFilters }) => {
|
||||||
const handleChange = (field: string, value: string) => {
|
const handleChange = (field: keyof VehicleFiltersType, value: string | undefined) => {
|
||||||
onFiltersChange({
|
onFiltersChange({
|
||||||
...filters,
|
...filters,
|
||||||
[field]: value
|
[field]: value
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
onFiltersChange({
|
||||||
|
driverName: '',
|
||||||
|
vehicleType: '',
|
||||||
|
licensePlate: '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
onApplyFilters();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
<div className="card-header">
|
<div className="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 className="mb-0">Фильтры машин</h5>
|
<h5 className="mb-0">Фильтры машин</h5>
|
||||||
|
<div className="d-flex">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline-secondary me-2"
|
||||||
|
onClick={handleClearFilters}
|
||||||
|
>
|
||||||
|
Сбросить
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
onClick={handleApply}
|
||||||
|
>
|
||||||
|
Применить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="row g-3">
|
<div className="row g-3">
|
||||||
@@ -30,7 +56,7 @@ const VehicleFilters: React.FC<VehicleFiltersProps> = ({ filters, onFiltersChang
|
|||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="driverName"
|
id="driverName"
|
||||||
value={filters.driverName}
|
value={filters.driverName || ''}
|
||||||
onChange={(e) => handleChange('driverName', e.target.value)}
|
onChange={(e) => handleChange('driverName', e.target.value)}
|
||||||
placeholder="ФИО водителя"
|
placeholder="ФИО водителя"
|
||||||
/>
|
/>
|
||||||
@@ -42,7 +68,7 @@ const VehicleFilters: React.FC<VehicleFiltersProps> = ({ filters, onFiltersChang
|
|||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="vehicleType"
|
id="vehicleType"
|
||||||
value={filters.vehicleType}
|
value={filters.vehicleType || ''}
|
||||||
onChange={(e) => handleChange('vehicleType', e.target.value)}
|
onChange={(e) => handleChange('vehicleType', e.target.value)}
|
||||||
placeholder="Тип машины"
|
placeholder="Тип машины"
|
||||||
/>
|
/>
|
||||||
@@ -54,7 +80,7 @@ const VehicleFilters: React.FC<VehicleFiltersProps> = ({ filters, onFiltersChang
|
|||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="licensePlate"
|
id="licensePlate"
|
||||||
value={filters.licensePlate}
|
value={filters.licensePlate || ''}
|
||||||
onChange={(e) => handleChange('licensePlate', e.target.value)}
|
onChange={(e) => handleChange('licensePlate', e.target.value)}
|
||||||
placeholder="Гос. номер"
|
placeholder="Гос. номер"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ interface WaybillWidgetProps {
|
|||||||
date: string;
|
date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let durationOrder;
|
|
||||||
|
|
||||||
const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
||||||
const [entries, setEntries] = useState<WaybillEntry[]>([]);
|
const [entries, setEntries] = useState<WaybillEntry[]>([]);
|
||||||
const [orders, setOrders] = useState<Order[]>([]);
|
const [orders, setOrders] = useState<Order[]>([]);
|
||||||
@@ -21,7 +19,7 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
const [selectedTimeSlot, setSelectedTimeSlot] = useState<string | null>(null);
|
const [selectedTimeSlot, setSelectedTimeSlot] = useState<string | null>(null);
|
||||||
const [duration, setDuration] = useState<number>(2);
|
const [duration, setDuration] = useState<number>(2);
|
||||||
|
|
||||||
// Часовые интервалы с 8:00 до 20:00
|
|
||||||
const timeSlots = Array.from({ length: 13 }, (_, i) => {
|
const timeSlots = Array.from({ length: 13 }, (_, i) => {
|
||||||
const hour = i + 8;
|
const hour = i + 8;
|
||||||
return `${hour.toString().padStart(2, '0')}:00`;
|
return `${hour.toString().padStart(2, '0')}:00`;
|
||||||
@@ -30,22 +28,23 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await ordersApi.getOrders();
|
const response = await ordersApi.getOrders();
|
||||||
setOrders(response.data);
|
setOrders((response.data as any).data.items);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Загружаем записи путевого листа
|
const entriesResponse = (await waybillApi.getEntries() as any).data;
|
||||||
const entriesResponse = await waybillApi.getEntries();
|
|
||||||
const vehicleEntries = entriesResponse.data.filter(
|
const vehicleEntries = entriesResponse.data.filter(
|
||||||
entry => entry.vehicleId === vehicleId && entry.date === date
|
(entry: { vehicleId: number; date: string; }) => entry.vehicleId === vehicleId && entry.date === date
|
||||||
);
|
);
|
||||||
setEntries(vehicleEntries);
|
setEntries(vehicleEntries);
|
||||||
|
|
||||||
// Загружаем доступные заказы
|
|
||||||
const ordersResponse = await ordersApi.getOrders();
|
const ordersResponse = (await ordersApi.getOrders() as any).data.data.items;
|
||||||
const assignedOrderIds = new Set(vehicleEntries.map(entry => entry.orderId));
|
|
||||||
const available = ordersResponse.data.filter(
|
const assignedOrderIds = new Set(vehicleEntries.map((entry: { orderId: any; }) => entry.orderId));
|
||||||
order => !assignedOrderIds.has(order.id) && order.status !== 'completed'
|
const available = ordersResponse.filter(
|
||||||
|
(order: Order) => !assignedOrderIds.has(order.id) && order.status !== 'completed'
|
||||||
);
|
);
|
||||||
setAvailableOrders(available);
|
setAvailableOrders(available);
|
||||||
|
|
||||||
@@ -71,13 +70,12 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getOrderInfo = (orderId: number, allOrders: Order[]): Order | undefined => {
|
const getOrderInfo = (orderId: number, allOrders: Order[]): Order | undefined => {
|
||||||
// Ищем заказ среди всех заказов
|
|
||||||
const foundOrder = allOrders.find(order => order.id === orderId);
|
const foundOrder = allOrders.find(order => order.id === orderId);
|
||||||
if (foundOrder) {
|
if (foundOrder) {
|
||||||
return foundOrder;
|
return foundOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если заказ не найден, но есть в путевом листе - создаем базовый объект
|
|
||||||
const waybillEntry = entries.find(entry => entry.orderId === orderId);
|
const waybillEntry = entries.find(entry => entry.orderId === orderId);
|
||||||
if (waybillEntry) {
|
if (waybillEntry) {
|
||||||
const basicOrder: Order = {
|
const basicOrder: Order = {
|
||||||
@@ -92,7 +90,6 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
return basicOrder;
|
return basicOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если заказ не найден нигде
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,7 +97,7 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
if (selectedOrder) {
|
if (selectedOrder) {
|
||||||
try {
|
try {
|
||||||
const startHour = parseInt(timeSlot.split(':')[0]);
|
const startHour = parseInt(timeSlot.split(':')[0]);
|
||||||
const endHour = startHour + duration; // Используем выбранную длительность
|
const endHour = startHour + duration;
|
||||||
|
|
||||||
const newEntry: Omit<WaybillEntry, 'id'> = {
|
const newEntry: Omit<WaybillEntry, 'id'> = {
|
||||||
vehicleId,
|
vehicleId,
|
||||||
@@ -109,6 +106,7 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
endTime: `${endHour.toString().padStart(2, '0')}:00`,
|
endTime: `${endHour.toString().padStart(2, '0')}:00`,
|
||||||
date
|
date
|
||||||
};
|
};
|
||||||
|
console.log('Sending waybill entry:', newEntry);
|
||||||
|
|
||||||
await waybillApi.createEntry(newEntry);
|
await waybillApi.createEntry(newEntry);
|
||||||
setSelectedOrder(null);
|
setSelectedOrder(null);
|
||||||
@@ -128,7 +126,7 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
if (selectedTimeSlot) {
|
if (selectedTimeSlot) {
|
||||||
try {
|
try {
|
||||||
const startHour = parseInt(selectedTimeSlot.split(':')[0]);
|
const startHour = parseInt(selectedTimeSlot.split(':')[0]);
|
||||||
const endHour = startHour + duration; // Используем выбранную длительность
|
const endHour = startHour + duration;
|
||||||
|
|
||||||
const newEntry: Omit<WaybillEntry, 'id'> = {
|
const newEntry: Omit<WaybillEntry, 'id'> = {
|
||||||
vehicleId,
|
vehicleId,
|
||||||
@@ -137,6 +135,7 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
endTime: `${endHour.toString().padStart(2, '0')}:00`,
|
endTime: `${endHour.toString().padStart(2, '0')}:00`,
|
||||||
date
|
date
|
||||||
};
|
};
|
||||||
|
console.log('Sending waybill entry:', newEntry);
|
||||||
|
|
||||||
await waybillApi.createEntry(newEntry);
|
await waybillApi.createEntry(newEntry);
|
||||||
setSelectedTimeSlot(null);
|
setSelectedTimeSlot(null);
|
||||||
@@ -172,7 +171,6 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
<h5 className="mb-0">Путевой лист на {new Date(date).toLocaleDateString()}</h5>
|
<h5 className="mb-0">Путевой лист на {new Date(date).toLocaleDateString()}</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{/* Состояние выбора */}
|
|
||||||
<div className="alert alert-info mb-3">
|
<div className="alert alert-info mb-3">
|
||||||
{selectedOrder && !selectedTimeSlot && (
|
{selectedOrder && !selectedTimeSlot && (
|
||||||
<div>
|
<div>
|
||||||
@@ -220,7 +218,6 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Таблица временных интервалов */}
|
|
||||||
<div className="table-responsive mb-4">
|
<div className="table-responsive mb-4">
|
||||||
<table className="table table-bordered">
|
<table className="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -275,7 +272,6 @@ const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Список доступных заказов */}
|
|
||||||
<div className="available-orders">
|
<div className="available-orders">
|
||||||
<h6>Доступные заказы:</h6>
|
<h6>Доступные заказы:</h6>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
|
|||||||
139
src/pages/AdminPage.tsx
Normal file
139
src/pages/AdminPage.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { WaybillEntry, Order, Vehicle } from '../types';
|
||||||
|
import { waybillApi, ordersApi, vehiclesApi } from '../services/api';
|
||||||
|
import Loading from '../components/Loading';
|
||||||
|
import ErrorAlert from '../components/ErrorAlert';
|
||||||
|
|
||||||
|
const AdminPage: React.FC = () => {
|
||||||
|
const [waybillEntries, setWaybillEntries] = useState<WaybillEntry[]>([]);
|
||||||
|
const [orders, setOrders] = useState<Order[]>([]);
|
||||||
|
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const entriesData = (await waybillApi.getEntries() as any).data.data;
|
||||||
|
const ordersData = (await ordersApi.getOrders() as any).data.data.items;
|
||||||
|
const vehiclesData = (await vehiclesApi.getVehicles() as any).data.data.items;
|
||||||
|
|
||||||
|
setWaybillEntries(Array.isArray(entriesData) ? entriesData : []);
|
||||||
|
setOrders(Array.isArray(ordersData) ? ordersData : []);
|
||||||
|
setVehicles(Array.isArray(vehiclesData) ? vehiclesData : []);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError('Не удалось загрузить данные');
|
||||||
|
console.error('Error loading admin data:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteEntry = async (id: number) => {
|
||||||
|
if (window.confirm('Вы уверены, что хотите удалить эту запись?')) {
|
||||||
|
try {
|
||||||
|
await waybillApi.deleteEntry(id);
|
||||||
|
await loadData();
|
||||||
|
} catch (err) {
|
||||||
|
setError('Не удалось удалить запись');
|
||||||
|
console.error('Error deleting entry:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <Loading />;
|
||||||
|
if (error) return <ErrorAlert message={error} onRetry={loadData} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-4">
|
||||||
|
<h1 className="mb-4">Панель администратора</h1>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-4 mb-4">
|
||||||
|
<div className="card text-white bg-primary">
|
||||||
|
<div className="card-body">
|
||||||
|
<h5 className="card-title">Заказы</h5>
|
||||||
|
<p className="card-text display-4">{orders.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-4 mb-4">
|
||||||
|
<div className="card text-white bg-success">
|
||||||
|
<div className="card-body">
|
||||||
|
<h5 className="card-title">Машины</h5>
|
||||||
|
<p className="card-text display-4">{vehicles.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-4 mb-4">
|
||||||
|
<div className="card text-white bg-info">
|
||||||
|
<div className="card-body">
|
||||||
|
<h5 className="card-title">Записи путевых листов</h5>
|
||||||
|
<p className="card-text display-4">{waybillEntries.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h5 className="mb-0">Записи путевых листов</h5>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
{waybillEntries.length === 0 ? (
|
||||||
|
<p className="text-muted">Записи не найдены</p>
|
||||||
|
) : (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Машина</th>
|
||||||
|
<th>Заказ</th>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Время</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{waybillEntries.map(entry => (
|
||||||
|
<tr key={entry.id}>
|
||||||
|
<td>{entry.id}</td>
|
||||||
|
<td>
|
||||||
|
{entry.vehicleId} -
|
||||||
|
{vehicles.find(v => v.id === entry.vehicleId)?.driverName}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{entry.orderId} -
|
||||||
|
{orders.find(o => o.id === entry.orderId)?.clientName}
|
||||||
|
</td>
|
||||||
|
<td>{entry.date}</td>
|
||||||
|
<td>{entry.startTime} - {entry.endTime}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-danger"
|
||||||
|
onClick={() => handleDeleteEntry(entry.id!)}
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminPage;
|
||||||
181
src/pages/LoginPage.tsx
Normal file
181
src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { authApi } from '../services/api';
|
||||||
|
|
||||||
|
interface LoginPageProps {
|
||||||
|
onLogin: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginPage: React.FC<LoginPageProps> = ({ onLogin }) => {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedError = localStorage.getItem('login_error');
|
||||||
|
if (savedError) {
|
||||||
|
setError(savedError);
|
||||||
|
localStorage.removeItem('login_error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.cookie.includes('auth_token')) {
|
||||||
|
onLogin();
|
||||||
|
navigate('/orders');
|
||||||
|
}
|
||||||
|
}, [navigate, onLogin]);
|
||||||
|
|
||||||
|
const handleLoginClick = async () => {
|
||||||
|
|
||||||
|
if (!username.trim() || !password.trim()) {
|
||||||
|
const errorMsg = 'Введите логин и пароль';
|
||||||
|
setError(errorMsg);
|
||||||
|
localStorage.setItem('login_error', errorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
localStorage.removeItem('login_error');
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Отправка запроса на вход...');
|
||||||
|
const response = await authApi.login({ username, password });
|
||||||
|
console.log('Ответ от сервера:', response);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data?.token) {
|
||||||
|
const token = response.data.data.token;
|
||||||
|
document.cookie = `auth_token=${token}; path=/; max-age=86400`;
|
||||||
|
|
||||||
|
localStorage.removeItem('login_error');
|
||||||
|
|
||||||
|
console.log('Вход успешен, перенаправление...');
|
||||||
|
onLogin();
|
||||||
|
navigate('/orders');
|
||||||
|
} else {
|
||||||
|
const errorMsg = 'Ошибка сервера при авторизации';
|
||||||
|
setError(errorMsg);
|
||||||
|
localStorage.setItem('login_error', errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Ошибка входа:', err);
|
||||||
|
|
||||||
|
let errorMsg = '';
|
||||||
|
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
errorMsg = 'Неверный логин или пароль';
|
||||||
|
} else if (err.response?.data?.message) {
|
||||||
|
errorMsg = err.response.data.message;
|
||||||
|
} else if (err.message && err.message.includes('Network Error')) {
|
||||||
|
errorMsg = 'Ошибка соединения с сервером. Проверьте подключение.';
|
||||||
|
} else {
|
||||||
|
errorMsg = 'Ошибка при входе в систему';
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(errorMsg);
|
||||||
|
localStorage.setItem('login_error', errorMsg);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearError = () => {
|
||||||
|
setError('');
|
||||||
|
localStorage.removeItem('login_error');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-5">
|
||||||
|
<div className="row justify-content-center">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h4 className="mb-0">Вход в систему</h4>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-danger alert-dismissible fade show">
|
||||||
|
<div className="d-flex justify-content-between align-items-center">
|
||||||
|
<span>
|
||||||
|
<strong>Ошибка:</strong> {error}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close"
|
||||||
|
onClick={handleClearError}
|
||||||
|
aria-label="Закрыть"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="username" className="form-label">Логин:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
autoFocus
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter' && !loading) {
|
||||||
|
handleLoginClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="password" className="form-label">Пароль:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
id="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter' && !loading) {
|
||||||
|
handleLoginClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary w-100"
|
||||||
|
onClick={handleLoginClick}
|
||||||
|
disabled={loading || !username || !password}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||||
|
Проверка...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Войти в систему'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="mt-3 text-center">
|
||||||
|
<small className="text-muted">
|
||||||
|
Тестовые данные:<br/>
|
||||||
|
Логин: <strong>admin</strong> Пароль: <strong>admin123</strong> (админ)<br/>
|
||||||
|
Логин: <strong>user</strong> Пароль: <strong>user123</strong> (пользователь)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Order } from '../types';
|
import { Order, OrderFiltersType } from '../types';
|
||||||
import { ordersApi } from '../services/api';
|
import { ordersApi } from '../services/api';
|
||||||
import OrderFilters from '../components/OrderFilters';
|
import OrderFilters from '../components/OrderFilters';
|
||||||
import Loading from '../components/Loading';
|
import Loading from '../components/Loading';
|
||||||
@@ -7,26 +7,47 @@ import ErrorAlert from '../components/ErrorAlert';
|
|||||||
|
|
||||||
const OrdersPage: React.FC = () => {
|
const OrdersPage: React.FC = () => {
|
||||||
const [orders, setOrders] = useState<Order[]>([]);
|
const [orders, setOrders] = useState<Order[]>([]);
|
||||||
const [filteredOrders, setFilteredOrders] = useState<Order[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState<OrderFiltersType>({
|
||||||
clientName: '',
|
clientName: '',
|
||||||
minCost: '',
|
minCost: undefined,
|
||||||
maxCost: '',
|
maxCost: undefined,
|
||||||
orderDate: '',
|
orderDate: '',
|
||||||
status: ''
|
status: '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50
|
||||||
});
|
});
|
||||||
|
|
||||||
// Загрузка данных
|
const loadOrders = async (currentFilters: OrderFiltersType) => {
|
||||||
const loadOrders = async () => {
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await ordersApi.getOrders();
|
|
||||||
setOrders(response.data);
|
const apiFilters: OrderFiltersType = {
|
||||||
setFilteredOrders(response.data);
|
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) {
|
} catch (err) {
|
||||||
setError('Не удалось загрузить список заказов');
|
setError('Не удалось загрузить список заказов');
|
||||||
console.error('Error loading orders:', err);
|
console.error('Error loading orders:', err);
|
||||||
@@ -35,29 +56,24 @@ const OrdersPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция изменения статуса заказа
|
|
||||||
const updateOrderStatus = async (orderId: number, newStatus: Order['status']) => {
|
const updateOrderStatus = async (orderId: number, newStatus: Order['status']) => {
|
||||||
try {
|
try {
|
||||||
// Находим заказ для обновления
|
|
||||||
const orderToUpdate = orders.find(order => order.id === orderId);
|
const orderToUpdate = orders.find(order => order.id === orderId);
|
||||||
if (!orderToUpdate) return;
|
if (!orderToUpdate) return;
|
||||||
|
|
||||||
// Создаем обновленный заказ
|
|
||||||
const updatedOrder = {
|
const updatedOrder = {
|
||||||
...orderToUpdate,
|
...orderToUpdate,
|
||||||
status: newStatus
|
status: newStatus
|
||||||
};
|
};
|
||||||
|
|
||||||
// Отправляем запрос на сервер
|
|
||||||
await ordersApi.updateOrder(orderId, updatedOrder);
|
await ordersApi.updateOrder(orderId, updatedOrder);
|
||||||
|
|
||||||
// Обновляем локальное состояние
|
setOrders(prevOrders =>
|
||||||
setOrders(prevOrders =>
|
prevOrders.map(order =>
|
||||||
prevOrders.map(order =>
|
|
||||||
order.id === orderId ? updatedOrder : order
|
order.id === orderId ? updatedOrder : order
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Статус заказа ${orderId} изменен на: ${newStatus}`);
|
console.log(`Статус заказа ${orderId} изменен на: ${newStatus}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Не удалось обновить статус заказа');
|
setError('Не удалось обновить статус заказа');
|
||||||
@@ -66,63 +82,33 @@ const OrdersPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadOrders();
|
loadOrders(filters);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Применение фильтров
|
const handleApplyFilters = () => {
|
||||||
useEffect(() => {
|
loadOrders(filters);
|
||||||
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]);
|
|
||||||
|
|
||||||
if (loading) return <Loading />;
|
if (loading) return <Loading />;
|
||||||
if (error) return <ErrorAlert message={error} onRetry={loadOrders} />;
|
if (error) return <ErrorAlert message={error} onRetry={() => loadOrders(filters)} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-4">
|
<div className="container mt-4">
|
||||||
<h1 className="mb-4">Список заказов</h1>
|
<h1 className="mb-4">Список заказов</h1>
|
||||||
|
|
||||||
<OrderFilters filters={filters} onFiltersChange={setFilters} />
|
<OrderFilters
|
||||||
|
filters={filters}
|
||||||
|
onFiltersChange={setFilters}
|
||||||
|
onApplyFilters={handleApplyFilters}
|
||||||
|
/>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h5 className="mb-0">
|
<h5 className="mb-0">
|
||||||
Найдено заказов: {filteredOrders.length}
|
Найдено заказов: {totalCount}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{filteredOrders.length === 0 ? (
|
{orders.length === 0 ? (
|
||||||
<p className="text-muted">Заказы не найдены</p>
|
<p className="text-muted">Заказы не найдены</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
@@ -139,26 +125,25 @@ const OrdersPage: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredOrders.map(order => (
|
{orders.map(order => (
|
||||||
<tr key={order.id}>
|
<tr key={order.id}>
|
||||||
<td>{order.id}</td>
|
<td>{order.id}</td>
|
||||||
<td>{order.clientName}</td>
|
<td>{order.clientName}</td>
|
||||||
<td>{order.orderCost.toLocaleString()} руб.</td>
|
<td>{order.orderCost.toLocaleString()} руб.</td>
|
||||||
<td>{new Date(order.orderDate).toLocaleDateString()}</td>
|
<td>{new Date(order.orderDate).toLocaleDateString()}</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`badge ${
|
<span className={`badge ${order.status === 'completed' ? 'bg-success' :
|
||||||
order.status === 'completed' ? 'bg-success' :
|
|
||||||
order.status === 'in_progress' ? 'bg-primary' :
|
order.status === 'in_progress' ? 'bg-primary' :
|
||||||
order.status === 'pending' ? 'bg-warning' : 'bg-danger'
|
order.status === 'pending' ? 'bg-warning' : 'bg-danger'
|
||||||
}`}>
|
}`}>
|
||||||
{order.status === 'pending' ? 'Ожидает' :
|
{order.status === 'pending' ? 'Ожидает' :
|
||||||
order.status === 'in_progress' ? 'В процессе' :
|
order.status === 'in_progress' ? 'В процессе' :
|
||||||
order.status === 'completed' ? 'Завершен' : 'Отменен'}
|
order.status === 'completed' ? 'Завершен' : 'Отменен'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{order.address}</td>
|
<td>{order.address}</td>
|
||||||
<td>
|
<td>
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
value={order.status}
|
value={order.status}
|
||||||
onChange={(e) => updateOrderStatus(order.id, e.target.value as Order['status'])}
|
onChange={(e) => updateOrderStatus(order.id, e.target.value as Order['status'])}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
import { Vehicle, Order, WaybillEntry } from '../types';
|
import { Vehicle, Order } from '../types';
|
||||||
import { vehiclesApi, ordersApi, waybillApi } from '../services/api';
|
import { vehiclesApi, ordersApi, waybillApi } from '../services/api';
|
||||||
import WaybillWidget from '../components/WaybillWidget';
|
import WaybillWidget from '../components/WaybillWidget';
|
||||||
import Loading from '../components/Loading';
|
import Loading from '../components/Loading';
|
||||||
@@ -16,31 +16,38 @@ const VehicleDetailPage: React.FC = () => {
|
|||||||
new Date().toISOString().split('T')[0]
|
new Date().toISOString().split('T')[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadVehicleData = async () => {
|
const loadVehicleData = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const vehicleId = parseInt(id);
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
const entriesData = (await waybillApi.getEntries() as any).data.data;
|
||||||
setError(null);
|
const ordersData = (await ordersApi.getOrders() as any).data.data.items;
|
||||||
|
|
||||||
// Загружаем данные машины
|
const result = ordersData.filter((order: any) => {
|
||||||
const vehicleResponse = await vehiclesApi.getVehicle(parseInt(id));
|
const entryExists = entriesData.some((entry: any) =>
|
||||||
setVehicle(vehicleResponse.data);
|
entry.orderId === order.id && entry.vehicleId === vehicleId
|
||||||
|
|
||||||
// Загружаем историю выполненных заказов
|
|
||||||
const ordersResponse = await ordersApi.getOrders();
|
|
||||||
const completed = ordersResponse.data.filter(
|
|
||||||
order => order.status === 'completed'
|
|
||||||
);
|
);
|
||||||
setCompletedOrders(completed);
|
return order.status === 'completed' && entryExists;
|
||||||
|
});
|
||||||
} catch (err) {
|
|
||||||
setError('Не удалось загрузить данные машины');
|
setCompletedOrders(result);
|
||||||
console.error('Error loading vehicle data:', err);
|
|
||||||
} finally {
|
const vehicleData = await vehiclesApi.getVehicle(vehicleId);
|
||||||
setLoading(false);
|
setVehicle((vehicleData.data as any).data);
|
||||||
}
|
|
||||||
};
|
} catch (err) {
|
||||||
|
setError('Не удалось загрузить данные машины');
|
||||||
|
console.error('Error loading vehicle data:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadVehicleData();
|
loadVehicleData();
|
||||||
@@ -88,7 +95,7 @@ const VehicleDetailPage: React.FC = () => {
|
|||||||
<h6 className="mb-1">{order.clientName}</h6>
|
<h6 className="mb-1">{order.clientName}</h6>
|
||||||
<p className="mb-1 small">{order.address}</p>
|
<p className="mb-1 small">{order.address}</p>
|
||||||
<small className="text-muted">
|
<small className="text-muted">
|
||||||
{new Date(order.orderDate).toLocaleDateString()} - {' '}
|
{new Date(order.orderDate).toLocaleDateString()} -
|
||||||
{order.orderCost.toLocaleString()} руб.
|
{order.orderCost.toLocaleString()} руб.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,8 +124,8 @@ const VehicleDetailPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<WaybillWidget
|
<WaybillWidget
|
||||||
vehicleId={vehicle.id}
|
vehicleId={vehicle.id}
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,30 +1,52 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Vehicle } from '../types';
|
import { Vehicle, VehicleFiltersType } from '../types';
|
||||||
import { vehiclesApi } from '../services/api';
|
import { vehiclesApi } from '../services/api';
|
||||||
import VehicleFilters from '../components/VehicleFilters';
|
import VehicleFiltersComponent from '../components/VehicleFilters';
|
||||||
import Loading from '../components/Loading';
|
import Loading from '../components/Loading';
|
||||||
import ErrorAlert from '../components/ErrorAlert';
|
import ErrorAlert from '../components/ErrorAlert';
|
||||||
|
|
||||||
const VehiclesPage: React.FC = () => {
|
const VehiclesPage: React.FC = () => {
|
||||||
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
||||||
const [filteredVehicles, setFilteredVehicles] = useState<Vehicle[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [filters, setFilters] = useState({
|
|
||||||
|
const [filters, setFilters] = useState<VehicleFiltersType>({
|
||||||
driverName: '',
|
driverName: '',
|
||||||
vehicleType: '',
|
vehicleType: '',
|
||||||
licensePlate: ''
|
licensePlate: '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadVehicles = async () => {
|
|
||||||
|
const loadVehicles = async (currentFilters: VehicleFiltersType) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await vehiclesApi.getVehicles();
|
|
||||||
setVehicles(response.data);
|
|
||||||
setFilteredVehicles(response.data);
|
const apiFilters: VehicleFiltersType = {
|
||||||
|
driverName: currentFilters.driverName || undefined,
|
||||||
|
vehicleType: currentFilters.vehicleType || undefined,
|
||||||
|
licensePlate: currentFilters.licensePlate || undefined,
|
||||||
|
page: currentFilters.page,
|
||||||
|
pageSize: currentFilters.pageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Object.keys(apiFilters).forEach(key => {
|
||||||
|
if (apiFilters[key as keyof VehicleFiltersType] === undefined || apiFilters[key as keyof VehicleFiltersType] === '') {
|
||||||
|
delete apiFilters[key as keyof VehicleFiltersType];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await vehiclesApi.getVehicles(apiFilters);
|
||||||
|
let count = (response.data as any).data.items.length;
|
||||||
|
|
||||||
|
setVehicles((response.data as any).data.items);
|
||||||
|
setTotalCount(count);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Не удалось загрузить список машин');
|
setError('Не удалось загрузить список машин');
|
||||||
console.error('Error loading vehicles:', err);
|
console.error('Error loading vehicles:', err);
|
||||||
@@ -33,55 +55,41 @@ const VehiclesPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadVehicles();
|
loadVehicles(filters);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let result = vehicles;
|
|
||||||
|
|
||||||
if (filters.driverName) {
|
const handleApplyFilters = () => {
|
||||||
result = result.filter(vehicle =>
|
loadVehicles(filters);
|
||||||
vehicle.driverName.toLowerCase().includes(filters.driverName.toLowerCase())
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.vehicleType) {
|
|
||||||
result = result.filter(vehicle =>
|
|
||||||
vehicle.vehicleType.toLowerCase().includes(filters.vehicleType.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.licensePlate) {
|
|
||||||
result = result.filter(vehicle =>
|
|
||||||
vehicle.licensePlate.toLowerCase().includes(filters.licensePlate.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilteredVehicles(result);
|
|
||||||
}, [filters, vehicles]);
|
|
||||||
|
|
||||||
if (loading) return <Loading />;
|
if (loading) return <Loading />;
|
||||||
if (error) return <ErrorAlert message={error} onRetry={loadVehicles} />;
|
if (error) return <ErrorAlert message={error} onRetry={() => loadVehicles(filters)} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-4">
|
<div className="container mt-4">
|
||||||
<h1 className="mb-4">Список машин</h1>
|
<h1 className="mb-4">Список машин</h1>
|
||||||
|
|
||||||
<VehicleFilters filters={filters} onFiltersChange={setFilters} />
|
<VehicleFiltersComponent
|
||||||
|
filters={filters}
|
||||||
|
onFiltersChange={setFilters}
|
||||||
|
onApplyFilters={handleApplyFilters}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h5 className="mb-0">
|
<h5 className="mb-0">
|
||||||
Найдено машин: {filteredVehicles.length}
|
Найдено машин: {totalCount}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{filteredVehicles.length === 0 ? (
|
{vehicles.length === 0 ? (
|
||||||
<p className="text-muted">Машины не найдены</p>
|
<p className="text-muted">Машины не найдены</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{filteredVehicles.map(vehicle => (
|
{vehicles.map(vehicle => (
|
||||||
<div key={vehicle.id} className="col-md-6 mb-3">
|
<div key={vehicle.id} className="col-md-6 mb-3">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
|||||||
@@ -1,34 +1,68 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Order, Vehicle, WaybillEntry } from '../types';
|
import { Order, Vehicle, WaybillEntry, OrderFiltersType, VehicleFiltersType } from '../types';
|
||||||
|
|
||||||
const API_BASE = 'http://localhost:3001';
|
const API_BASE = 'http://localhost:5155/api';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: API_BASE,
|
baseURL: API_BASE,
|
||||||
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Сервис для работы с заказами
|
const getCookie = (name: string): string | null => {
|
||||||
// Сервис для работы с заказами
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return parts.pop()?.split(';').shift() || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = getCookie('auth_token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const ordersApi = {
|
export const ordersApi = {
|
||||||
getOrders: () => api.get<Order[]>('/orders'),
|
getOrders: (filters?: OrderFiltersType) => api.get<Order[]>('/orders', {params: filters}),
|
||||||
getOrder: (id: number) => api.get<Order>(`/orders/${id}`),
|
getOrder: (id: number) => api.get<Order>(`/orders/${id}`),
|
||||||
updateOrder: (id: number, order: Order) => api.put<Order>(`/orders/${id}`, order),
|
updateOrder: (id: number, order: Order) => api.put<Order>(`/orders/${id}`, order),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сервис для работы с машинами
|
|
||||||
export const vehiclesApi = {
|
export const vehiclesApi = {
|
||||||
getVehicles: () => api.get<Vehicle[]>('/vehicles'),
|
getVehicles: (filters?: VehicleFiltersType) => api.get<Vehicle[]>('/vehicles', {params: filters}),
|
||||||
getVehicle: (id: number) => api.get<Vehicle>(`/vehicles/${id}`),
|
getVehicle: (id: number) => api.get<Vehicle>(`/vehicles/${id}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сервис для работы с путевыми листами
|
|
||||||
export const waybillApi = {
|
export const waybillApi = {
|
||||||
getEntries: () => api.get<WaybillEntry[]>('/waybillEntries'),
|
getEntries: () =>
|
||||||
|
api.get<WaybillEntry[]>('/waybillentries'),
|
||||||
createEntry: (entry: Omit<WaybillEntry, 'id'>) =>
|
createEntry: (entry: Omit<WaybillEntry, 'id'>) =>
|
||||||
api.post<WaybillEntry>('/waybillEntries', entry),
|
api.post<WaybillEntry>('/waybillentries', entry),
|
||||||
updateEntry: (id: number, entry: Partial<WaybillEntry>) =>
|
updateEntry: (id: number, entry: Partial<WaybillEntry>) => api.put<WaybillEntry>(`/waybillEntries/${id}`, entry),
|
||||||
api.put<WaybillEntry>(`/waybillEntries/${id}`, entry),
|
|
||||||
deleteEntry: (id: number) => api.delete(`/waybillEntries/${id}`),
|
deleteEntry: (id: number) => api.delete(`/waybillEntries/${id}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const authApi = {
|
||||||
|
login: (credentials: { username: string; password: string }) =>
|
||||||
|
api.post<{
|
||||||
|
success: string; data: { token: string; username: string; role: string; expires: string }
|
||||||
|
}>('/auth/login', credentials),
|
||||||
|
logout: () => api.post('/auth/logout'),
|
||||||
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Основные типы данных
|
|
||||||
export interface Order {
|
export interface Order {
|
||||||
id: number;
|
id: number;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
@@ -9,6 +9,24 @@ export interface Order {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OrderFiltersType {
|
||||||
|
clientName?: string;
|
||||||
|
minCost?: number;
|
||||||
|
maxCost?: number;
|
||||||
|
orderDate?: string;
|
||||||
|
status?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VehicleFiltersType {
|
||||||
|
driverName?: string;
|
||||||
|
vehicleType?: string;
|
||||||
|
licensePlate?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Vehicle {
|
export interface Vehicle {
|
||||||
id: number;
|
id: number;
|
||||||
driverName: string;
|
driverName: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user