upd
This commit is contained in:
74
src/App.tsx
74
src/App.tsx
@@ -1,26 +1,58 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
||||
import OrdersPage from './pages/OrdersPage';
|
||||
import VehiclesPage from './pages/VehiclesPage';
|
||||
import VehicleDetailPage from './pages/VehicleDetailPage';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<Router>
|
||||
<div className="App">
|
||||
{/* Навигация */}
|
||||
<nav className="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div className="container">
|
||||
<Link className="navbar-brand" to="/">
|
||||
Логистическая компания
|
||||
</Link>
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarNav">
|
||||
<ul className="navbar-nav">
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/orders">
|
||||
Заказы
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/vehicles">
|
||||
Машины
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
export default App;
|
||||
{/* Основной контент */}
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="/" element={<OrdersPage />} />
|
||||
<Route path="/orders" element={<OrdersPage />} />
|
||||
<Route path="/vehicles" element={<VehiclesPage />} />
|
||||
<Route path="/vehicles/:id" element={<VehicleDetailPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
22
src/components/ErrorAlert.tsx
Normal file
22
src/components/ErrorAlert.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ErrorAlertProps {
|
||||
message: string;
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
const ErrorAlert: React.FC<ErrorAlertProps> = ({ message, onRetry }) => {
|
||||
return (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<h4 className="alert-heading">Ошибка!</h4>
|
||||
<p>{message}</p>
|
||||
{onRetry && (
|
||||
<button className="btn btn-danger" onClick={onRetry}>
|
||||
Попробовать снова
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorAlert;
|
||||
14
src/components/Loading.tsx
Normal file
14
src/components/Loading.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
const Loading: React.FC = () => {
|
||||
return (
|
||||
<div className="loading">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
<p>Загрузка данных...</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
97
src/components/OrderFilters.tsx
Normal file
97
src/components/OrderFilters.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
|
||||
interface OrderFiltersProps {
|
||||
filters: {
|
||||
clientName: string;
|
||||
minCost: string;
|
||||
maxCost: string;
|
||||
orderDate: string;
|
||||
status: string;
|
||||
};
|
||||
onFiltersChange: (filters: any) => void;
|
||||
}
|
||||
|
||||
const OrderFilters: React.FC<OrderFiltersProps> = ({ filters, onFiltersChange }) => {
|
||||
const handleChange = (field: string, value: string) => {
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
[field]: value
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">Фильтры заказов</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row g-3">
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="clientName" className="form-label">Клиент</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="clientName"
|
||||
value={filters.clientName}
|
||||
onChange={(e) => handleChange('clientName', e.target.value)}
|
||||
placeholder="ФИО или наименование"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-2">
|
||||
<label htmlFor="minCost" className="form-label">Мин. стоимость</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
id="minCost"
|
||||
value={filters.minCost}
|
||||
onChange={(e) => handleChange('minCost', e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-2">
|
||||
<label htmlFor="maxCost" className="form-label">Макс. стоимость</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
id="maxCost"
|
||||
value={filters.maxCost}
|
||||
onChange={(e) => handleChange('maxCost', e.target.value)}
|
||||
placeholder="100000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-2">
|
||||
<label htmlFor="orderDate" className="form-label">Дата заказа</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
id="orderDate"
|
||||
value={filters.orderDate}
|
||||
onChange={(e) => handleChange('orderDate', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="status" className="form-label">Статус</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="status"
|
||||
value={filters.status}
|
||||
onChange={(e) => handleChange('status', e.target.value)}
|
||||
>
|
||||
<option value="">Все статусы</option>
|
||||
<option value="pending">Ожидает</option>
|
||||
<option value="in_progress">В процессе</option>
|
||||
<option value="completed">Завершен</option>
|
||||
<option value="cancelled">Отменен</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderFilters;
|
||||
68
src/components/VehicleFilters.tsx
Normal file
68
src/components/VehicleFilters.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
|
||||
interface VehicleFiltersProps {
|
||||
filters: {
|
||||
driverName: string;
|
||||
vehicleType: string;
|
||||
licensePlate: string;
|
||||
};
|
||||
onFiltersChange: (filters: any) => void;
|
||||
}
|
||||
|
||||
const VehicleFilters: React.FC<VehicleFiltersProps> = ({ filters, onFiltersChange }) => {
|
||||
const handleChange = (field: string, value: string) => {
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
[field]: value
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">Фильтры машин</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row g-3">
|
||||
<div className="col-md-4">
|
||||
<label htmlFor="driverName" className="form-label">Водитель</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="driverName"
|
||||
value={filters.driverName}
|
||||
onChange={(e) => handleChange('driverName', e.target.value)}
|
||||
placeholder="ФИО водителя"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
<label htmlFor="vehicleType" className="form-label">Тип машины</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="vehicleType"
|
||||
value={filters.vehicleType}
|
||||
onChange={(e) => handleChange('vehicleType', e.target.value)}
|
||||
placeholder="Тип машины"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
<label htmlFor="licensePlate" className="form-label">Гос. номер</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="licensePlate"
|
||||
value={filters.licensePlate}
|
||||
onChange={(e) => handleChange('licensePlate', e.target.value)}
|
||||
placeholder="Гос. номер"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VehicleFilters;
|
||||
311
src/components/WaybillWidget.tsx
Normal file
311
src/components/WaybillWidget.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Order, WaybillEntry } from '../types';
|
||||
import { waybillApi, ordersApi } from '../services/api';
|
||||
import Loading from '../components/Loading';
|
||||
import ErrorAlert from '../components/ErrorAlert';
|
||||
|
||||
interface WaybillWidgetProps {
|
||||
vehicleId: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
let durationOrder;
|
||||
|
||||
const WaybillWidget: React.FC<WaybillWidgetProps> = ({ vehicleId, date }) => {
|
||||
const [entries, setEntries] = useState<WaybillEntry[]>([]);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [availableOrders, setAvailableOrders] = useState<Order[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
|
||||
const [selectedTimeSlot, setSelectedTimeSlot] = useState<string | null>(null);
|
||||
const [duration, setDuration] = useState<number>(2);
|
||||
|
||||
// Часовые интервалы с 8:00 до 20:00
|
||||
const timeSlots = Array.from({ length: 13 }, (_, i) => {
|
||||
const hour = i + 8;
|
||||
return `${hour.toString().padStart(2, '0')}:00`;
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const response = await ordersApi.getOrders();
|
||||
setOrders(response.data);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Загружаем записи путевого листа
|
||||
const entriesResponse = await waybillApi.getEntries();
|
||||
const vehicleEntries = entriesResponse.data.filter(
|
||||
entry => entry.vehicleId === vehicleId && entry.date === date
|
||||
);
|
||||
setEntries(vehicleEntries);
|
||||
|
||||
// Загружаем доступные заказы
|
||||
const ordersResponse = await ordersApi.getOrders();
|
||||
const assignedOrderIds = new Set(vehicleEntries.map(entry => entry.orderId));
|
||||
const available = ordersResponse.data.filter(
|
||||
order => !assignedOrderIds.has(order.id) && order.status !== 'completed'
|
||||
);
|
||||
setAvailableOrders(available);
|
||||
|
||||
} catch (err) {
|
||||
setError('Не удалось загрузить данные путевого листа');
|
||||
console.error('Error loading waybill data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [vehicleId, date]);
|
||||
|
||||
const getOrderForTimeSlot = (timeSlot: string): WaybillEntry | undefined => {
|
||||
return entries.find(entry => {
|
||||
const startHour = parseInt(entry.startTime.split(':')[0]);
|
||||
const endHour = parseInt(entry.endTime.split(':')[0]);
|
||||
const slotHour = parseInt(timeSlot.split(':')[0]);
|
||||
return slotHour >= startHour && slotHour < endHour;
|
||||
});
|
||||
};
|
||||
|
||||
const getOrderInfo = (orderId: number, allOrders: Order[]): Order | undefined => {
|
||||
// Ищем заказ среди всех заказов
|
||||
const foundOrder = allOrders.find(order => order.id === orderId);
|
||||
if (foundOrder) {
|
||||
return foundOrder;
|
||||
}
|
||||
|
||||
// Если заказ не найден, но есть в путевом листе - создаем базовый объект
|
||||
const waybillEntry = entries.find(entry => entry.orderId === orderId);
|
||||
if (waybillEntry) {
|
||||
const basicOrder: Order = {
|
||||
id: waybillEntry.orderId,
|
||||
clientName: `Заказ #${waybillEntry.orderId}`,
|
||||
orderCost: 0,
|
||||
orderDate: new Date().toISOString().split('T')[0],
|
||||
status: 'in_progress' as const,
|
||||
address: 'Адрес не указан',
|
||||
description: 'Заказ запланирован в путевом листе'
|
||||
};
|
||||
return basicOrder;
|
||||
}
|
||||
|
||||
// Если заказ не найден нигде
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const handleTimeSlotClick = async (timeSlot: string) => {
|
||||
if (selectedOrder) {
|
||||
try {
|
||||
const startHour = parseInt(timeSlot.split(':')[0]);
|
||||
const endHour = startHour + duration; // Используем выбранную длительность
|
||||
|
||||
const newEntry: Omit<WaybillEntry, 'id'> = {
|
||||
vehicleId,
|
||||
orderId: selectedOrder.id,
|
||||
startTime: timeSlot,
|
||||
endTime: `${endHour.toString().padStart(2, '0')}:00`,
|
||||
date
|
||||
};
|
||||
|
||||
await waybillApi.createEntry(newEntry);
|
||||
setSelectedOrder(null);
|
||||
setSelectedTimeSlot(null);
|
||||
await loadData();
|
||||
|
||||
} catch (err) {
|
||||
setError('Не удалось добавить заказ в путевой лист');
|
||||
console.error('Error creating waybill entry:', err);
|
||||
}
|
||||
} else {
|
||||
setSelectedTimeSlot(timeSlot);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOrderSelect = async (order: Order) => {
|
||||
if (selectedTimeSlot) {
|
||||
try {
|
||||
const startHour = parseInt(selectedTimeSlot.split(':')[0]);
|
||||
const endHour = startHour + duration; // Используем выбранную длительность
|
||||
|
||||
const newEntry: Omit<WaybillEntry, 'id'> = {
|
||||
vehicleId,
|
||||
orderId: order.id,
|
||||
startTime: selectedTimeSlot,
|
||||
endTime: `${endHour.toString().padStart(2, '0')}:00`,
|
||||
date
|
||||
};
|
||||
|
||||
await waybillApi.createEntry(newEntry);
|
||||
setSelectedTimeSlot(null);
|
||||
setSelectedOrder(null);
|
||||
await loadData();
|
||||
|
||||
} catch (err) {
|
||||
setError('Не удалось добавить заказ в путевой лист');
|
||||
console.error('Error creating waybill entry:', err);
|
||||
}
|
||||
} else {
|
||||
setSelectedOrder(order);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveEntry = async (entryId: number) => {
|
||||
try {
|
||||
await waybillApi.deleteEntry(entryId);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
setError('Не удалось удалить запись из путевого листа');
|
||||
console.error('Error deleting waybill entry:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <Loading />;
|
||||
if (error) return <ErrorAlert message={error} onRetry={loadData} />;
|
||||
|
||||
return (
|
||||
<div className="waybill-widget">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">Путевой лист на {new Date(date).toLocaleDateString()}</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{/* Состояние выбора */}
|
||||
<div className="alert alert-info mb-3">
|
||||
{selectedOrder && !selectedTimeSlot && (
|
||||
<div>
|
||||
<p>Выбран заказ: <strong>{selectedOrder.clientName}</strong>. Теперь выберите время.</p>
|
||||
<div className="mt-2">
|
||||
<label htmlFor="duration" className="form-label">Длительность заказа (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="duration"
|
||||
className="form-control"
|
||||
min="1"
|
||||
max="8"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(parseInt(e.target.value) || 1)}
|
||||
style={{ width: '100px', display: 'inline-block', marginLeft: '10px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedTimeSlot && !selectedOrder && (
|
||||
<div>
|
||||
<p>Выбрано время: <strong>{selectedTimeSlot}</strong>. Теперь выберите заказ.</p>
|
||||
<div className="mt-2">
|
||||
<label htmlFor="duration" className="form-label">Длительность заказа (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="duration"
|
||||
className="form-control"
|
||||
min="1"
|
||||
max="8"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(parseInt(e.target.value) || 1)}
|
||||
style={{ width: '100px', display: 'inline-block', marginLeft: '10px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!selectedOrder && !selectedTimeSlot && (
|
||||
<p>Выберите время или заказ для планирования доставки.</p>
|
||||
)}
|
||||
{selectedOrder && selectedTimeSlot && (
|
||||
<p>
|
||||
Готово к добавлению: <strong>{selectedOrder.clientName}</strong> на время <strong>{selectedTimeSlot}</strong>
|
||||
продолжительностью <strong>{duration} час(ов)</strong>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Таблица временных интервалов */}
|
||||
<div className="table-responsive mb-4">
|
||||
<table className="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Время</th>
|
||||
<th>Заказ</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{timeSlots.map(timeSlot => {
|
||||
const entry = getOrderForTimeSlot(timeSlot);
|
||||
const order = entry ? getOrderInfo(entry.orderId, orders) : undefined;
|
||||
return (
|
||||
<tr
|
||||
key={timeSlot}
|
||||
className={selectedTimeSlot === timeSlot ? 'table-primary' : ''}
|
||||
>
|
||||
<td>{timeSlot}</td>
|
||||
<td>
|
||||
{order ? (
|
||||
<div>
|
||||
<strong>{order.clientName}</strong>
|
||||
<br />
|
||||
<small className="text-muted">{order.address}</small>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted">Свободно</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{order && entry ? (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => handleRemoveEntry(entry.id)}
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => handleTimeSlotClick(timeSlot)}
|
||||
>
|
||||
Выбрать
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Список доступных заказов */}
|
||||
<div className="available-orders">
|
||||
<h6>Доступные заказы:</h6>
|
||||
<div className="row">
|
||||
{availableOrders.map(order => (
|
||||
<div key={order.id} className="col-md-6 mb-2">
|
||||
<div
|
||||
className={`card ${selectedOrder?.id === order.id ? 'border-primary' : ''}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => handleOrderSelect(order)}
|
||||
>
|
||||
<div className="card-body py-2">
|
||||
<h6 className="card-title mb-1">{order.clientName}</h6>
|
||||
<p className="card-text mb-1">
|
||||
<small>{order.address}</small>
|
||||
</p>
|
||||
<p className="card-text mb-0">
|
||||
<small className="text-muted">
|
||||
{order.orderCost.toLocaleString()} руб.
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WaybillWidget;
|
||||
185
src/pages/OrdersPage.tsx
Normal file
185
src/pages/OrdersPage.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Order } from '../types';
|
||||
import { ordersApi } from '../services/api';
|
||||
import OrderFilters from '../components/OrderFilters';
|
||||
import Loading from '../components/Loading';
|
||||
import ErrorAlert from '../components/ErrorAlert';
|
||||
|
||||
const OrdersPage: React.FC = () => {
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [filteredOrders, setFilteredOrders] = useState<Order[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
clientName: '',
|
||||
minCost: '',
|
||||
maxCost: '',
|
||||
orderDate: '',
|
||||
status: ''
|
||||
});
|
||||
|
||||
// Загрузка данных
|
||||
const loadOrders = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await ordersApi.getOrders();
|
||||
setOrders(response.data);
|
||||
setFilteredOrders(response.data);
|
||||
} catch (err) {
|
||||
setError('Не удалось загрузить список заказов');
|
||||
console.error('Error loading orders:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Функция изменения статуса заказа
|
||||
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 =>
|
||||
order.id === orderId ? updatedOrder : order
|
||||
)
|
||||
);
|
||||
|
||||
console.log(`Статус заказа ${orderId} изменен на: ${newStatus}`);
|
||||
} catch (err) {
|
||||
setError('Не удалось обновить статус заказа');
|
||||
console.error('Error updating order status:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadOrders();
|
||||
}, []);
|
||||
|
||||
// Применение фильтров
|
||||
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]);
|
||||
|
||||
if (loading) return <Loading />;
|
||||
if (error) return <ErrorAlert message={error} onRetry={loadOrders} />;
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h1 className="mb-4">Список заказов</h1>
|
||||
|
||||
<OrderFilters filters={filters} onFiltersChange={setFilters} />
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">
|
||||
Найдено заказов: {filteredOrders.length}
|
||||
</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{filteredOrders.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>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredOrders.map(order => (
|
||||
<tr key={order.id}>
|
||||
<td>{order.id}</td>
|
||||
<td>{order.clientName}</td>
|
||||
<td>{order.orderCost.toLocaleString()} руб.</td>
|
||||
<td>{new Date(order.orderDate).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<span className={`badge ${
|
||||
order.status === 'completed' ? 'bg-success' :
|
||||
order.status === 'in_progress' ? 'bg-primary' :
|
||||
order.status === 'pending' ? 'bg-warning' : 'bg-danger'
|
||||
}`}>
|
||||
{order.status === 'pending' ? 'Ожидает' :
|
||||
order.status === 'in_progress' ? 'В процессе' :
|
||||
order.status === 'completed' ? 'Завершен' : 'Отменен'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{order.address}</td>
|
||||
<td>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={order.status}
|
||||
onChange={(e) => updateOrderStatus(order.id, e.target.value as Order['status'])}
|
||||
style={{ width: '140px' }}
|
||||
>
|
||||
<option value="pending">Ожидает</option>
|
||||
<option value="in_progress">В процессе</option>
|
||||
<option value="completed">Завершен</option>
|
||||
<option value="cancelled">Отменен</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersPage;
|
||||
132
src/pages/VehicleDetailPage.tsx
Normal file
132
src/pages/VehicleDetailPage.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { Vehicle, Order, WaybillEntry } from '../types';
|
||||
import { vehiclesApi, ordersApi, waybillApi } from '../services/api';
|
||||
import WaybillWidget from '../components/WaybillWidget';
|
||||
import Loading from '../components/Loading';
|
||||
import ErrorAlert from '../components/ErrorAlert';
|
||||
|
||||
const VehicleDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [vehicle, setVehicle] = useState<Vehicle | null>(null);
|
||||
const [completedOrders, setCompletedOrders] = useState<Order[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedDate, setSelectedDate] = useState(
|
||||
new Date().toISOString().split('T')[0]
|
||||
);
|
||||
|
||||
const loadVehicleData = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Загружаем данные машины
|
||||
const vehicleResponse = await vehiclesApi.getVehicle(parseInt(id));
|
||||
setVehicle(vehicleResponse.data);
|
||||
|
||||
// Загружаем историю выполненных заказов
|
||||
const ordersResponse = await ordersApi.getOrders();
|
||||
const completed = ordersResponse.data.filter(
|
||||
order => order.status === 'completed'
|
||||
);
|
||||
setCompletedOrders(completed);
|
||||
|
||||
} catch (err) {
|
||||
setError('Не удалось загрузить данные машины');
|
||||
console.error('Error loading vehicle data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadVehicleData();
|
||||
}, [id]);
|
||||
|
||||
if (loading) return <Loading />;
|
||||
if (error) return <ErrorAlert message={error} onRetry={loadVehicleData} />;
|
||||
if (!vehicle) return <div>Машина не найдена</div>;
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol className="breadcrumb">
|
||||
<li className="breadcrumb-item">
|
||||
<Link to="/vehicles">Машины</Link>
|
||||
</li>
|
||||
<li className="breadcrumb-item active">{vehicle.licensePlate}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-4">
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">Информация о машине</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p><strong>Тип машины:</strong> {vehicle.vehicleType}</p>
|
||||
<p><strong>Водитель:</strong> {vehicle.driverName}</p>
|
||||
<p><strong>Гос. номер:</strong> {vehicle.licensePlate}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">История выполненных заказов</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{completedOrders.length === 0 ? (
|
||||
<p className="text-muted">Нет выполненных заказов</p>
|
||||
) : (
|
||||
<div className="list-group">
|
||||
{completedOrders.map(order => (
|
||||
<div key={order.id} className="list-group-item">
|
||||
<h6 className="mb-1">{order.clientName}</h6>
|
||||
<p className="mb-1 small">{order.address}</p>
|
||||
<small className="text-muted">
|
||||
{new Date(order.orderDate).toLocaleDateString()} - {' '}
|
||||
{order.orderCost.toLocaleString()} руб.
|
||||
</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-8">
|
||||
<div className="card">
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 className="mb-0">Путевой лист</h5>
|
||||
<div>
|
||||
<label htmlFor="waybillDate" className="form-label me-2">
|
||||
Дата:
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="waybillDate"
|
||||
className="form-control"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<WaybillWidget
|
||||
vehicleId={vehicle.id}
|
||||
date={selectedDate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VehicleDetailPage;
|
||||
111
src/pages/VehiclesPage.tsx
Normal file
111
src/pages/VehiclesPage.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Vehicle } from '../types';
|
||||
import { vehiclesApi } from '../services/api';
|
||||
import VehicleFilters from '../components/VehicleFilters';
|
||||
import Loading from '../components/Loading';
|
||||
import ErrorAlert from '../components/ErrorAlert';
|
||||
|
||||
const VehiclesPage: React.FC = () => {
|
||||
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
||||
const [filteredVehicles, setFilteredVehicles] = useState<Vehicle[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
driverName: '',
|
||||
vehicleType: '',
|
||||
licensePlate: ''
|
||||
});
|
||||
|
||||
const loadVehicles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await vehiclesApi.getVehicles();
|
||||
setVehicles(response.data);
|
||||
setFilteredVehicles(response.data);
|
||||
} catch (err) {
|
||||
setError('Не удалось загрузить список машин');
|
||||
console.error('Error loading vehicles:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadVehicles();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let result = vehicles;
|
||||
|
||||
if (filters.driverName) {
|
||||
result = result.filter(vehicle =>
|
||||
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 (error) return <ErrorAlert message={error} onRetry={loadVehicles} />;
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h1 className="mb-4">Список машин</h1>
|
||||
|
||||
<VehicleFilters filters={filters} onFiltersChange={setFilters} />
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">
|
||||
Найдено машин: {filteredVehicles.length}
|
||||
</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{filteredVehicles.length === 0 ? (
|
||||
<p className="text-muted">Машины не найдены</p>
|
||||
) : (
|
||||
<div className="row">
|
||||
{filteredVehicles.map(vehicle => (
|
||||
<div key={vehicle.id} className="col-md-6 mb-3">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{vehicle.vehicleType}</h5>
|
||||
<p className="card-text">
|
||||
<strong>Водитель:</strong> {vehicle.driverName}<br/>
|
||||
<strong>Гос. номер:</strong> {vehicle.licensePlate}
|
||||
</p>
|
||||
<Link
|
||||
to={`/vehicles/${vehicle.id}`}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Подробнее
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VehiclesPage;
|
||||
34
src/services/api.ts
Normal file
34
src/services/api.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import axios from 'axios';
|
||||
import { Order, Vehicle, WaybillEntry } from '../types';
|
||||
|
||||
const API_BASE = 'http://localhost:3001';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE,
|
||||
});
|
||||
|
||||
// Сервис для работы с заказами
|
||||
// Сервис для работы с заказами
|
||||
export const ordersApi = {
|
||||
getOrders: () => api.get<Order[]>('/orders'),
|
||||
getOrder: (id: number) => api.get<Order>(`/orders/${id}`),
|
||||
updateOrder: (id: number, order: Order) => api.put<Order>(`/orders/${id}`, order),
|
||||
};
|
||||
|
||||
// Сервис для работы с машинами
|
||||
export const vehiclesApi = {
|
||||
getVehicles: () => api.get<Vehicle[]>('/vehicles'),
|
||||
getVehicle: (id: number) => api.get<Vehicle>(`/vehicles/${id}`),
|
||||
};
|
||||
|
||||
// Сервис для работы с путевыми листами
|
||||
export const waybillApi = {
|
||||
getEntries: () => api.get<WaybillEntry[]>('/waybillEntries'),
|
||||
createEntry: (entry: Omit<WaybillEntry, 'id'>) =>
|
||||
api.post<WaybillEntry>('/waybillEntries', entry),
|
||||
updateEntry: (id: number, entry: Partial<WaybillEntry>) =>
|
||||
api.put<WaybillEntry>(`/waybillEntries/${id}`, entry),
|
||||
deleteEntry: (id: number) => api.delete(`/waybillEntries/${id}`),
|
||||
};
|
||||
|
||||
export default api;
|
||||
32
src/types/index.ts
Normal file
32
src/types/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Основные типы данных
|
||||
export interface Order {
|
||||
id: number;
|
||||
clientName: string;
|
||||
orderCost: number;
|
||||
orderDate: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
||||
address: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Vehicle {
|
||||
id: number;
|
||||
driverName: string;
|
||||
vehicleType: string;
|
||||
licensePlate: string;
|
||||
}
|
||||
|
||||
export interface WaybillEntry {
|
||||
id: number;
|
||||
vehicleId: number;
|
||||
orderId: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface Waybill {
|
||||
vehicleId: number;
|
||||
date: Date;
|
||||
entries: WaybillEntry[];
|
||||
}
|
||||
Reference in New Issue
Block a user