This commit is contained in:
2025-11-22 00:52:07 +04:00
parent dbf7dc8bbb
commit 46cd4ef38c
26 changed files with 3328 additions and 22 deletions

View 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;