307 lines
11 KiB
TypeScript
307 lines
11 KiB
TypeScript
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;
|
||
}
|
||
|
||
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);
|
||
|
||
|
||
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 as any).data.items);
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
const entriesResponse = (await waybillApi.getEntries() as any).data;
|
||
|
||
const vehicleEntries = entriesResponse.data.filter(
|
||
(entry: { vehicleId: number; date: string; }) => entry.vehicleId === vehicleId && entry.date === date
|
||
);
|
||
setEntries(vehicleEntries);
|
||
|
||
|
||
const ordersResponse = (await ordersApi.getOrders() as any).data.data.items;
|
||
|
||
const assignedOrderIds = new Set(vehicleEntries.map((entry: { orderId: any; }) => entry.orderId));
|
||
const available = ordersResponse.filter(
|
||
(order: 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
|
||
};
|
||
console.log('Sending waybill entry:', newEntry);
|
||
|
||
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
|
||
};
|
||
console.log('Sending waybill entry:', newEntry);
|
||
|
||
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; |