File size: 3,293 Bytes
f6213fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { formatClock, kindLabel, toneForKind } from './formatters.mjs';

export function buildVehicleTimelineModel(preview) {
  const axis = buildAxis(preview);
  return {
    axis,
    lanes: (preview?.vehicles || []).map((vehicle) => ({
      id: `vehicle-${vehicle.vehicleId}`,
      label: vehicle.vehicleName,
      mode: 'detailed',
      badges: [
        `${vehicle.stopCount} stops`,
        `${vehicle.totalDemand}/${vehicle.totalDemand - vehicle.capacityOverage + vehicle.capacityOverage || vehicle.totalDemand}`,
      ],
      items: vehicle.stops.map((stop) => ({
        id: `vehicle-${vehicle.vehicleId}-stop-${stop.deliveryId}`,
        startMinute: Math.floor(stop.serviceStartTime / 60),
        endMinute: Math.ceil(stop.departureTime / 60),
        label: stop.label,
        meta: `${kindLabel(stop.kind)} · ${formatClock(stop.arrivalTime)} arrival`,
        tone: toneForKind(stop.kind),
      })),
    })),
  };
}

export function buildDeliveryTimelineModel(preview) {
  const axis = buildAxis(preview);
  return {
    axis,
    lanes: (preview?.deliveries || []).map((delivery) => {
      const items = [
        {
          id: `delivery-window-${delivery.deliveryId}`,
          startMinute: Math.floor(delivery.minStartTime / 60),
          endMinute: Math.ceil(delivery.maxEndTime / 60),
          label: 'Window',
          meta: `${formatClock(delivery.minStartTime)} to ${formatClock(delivery.maxEndTime)}`,
          tone: 'slate',
        },
      ];

      if (delivery.serviceStartTime != null && delivery.departureTime != null) {
        items.push({
          id: `delivery-service-${delivery.deliveryId}`,
          startMinute: Math.floor(delivery.serviceStartTime / 60),
          endMinute: Math.ceil(delivery.departureTime / 60),
          label: delivery.assignedVehicleName || 'Assigned',
          meta: `${delivery.label} · ${kindLabel(delivery.kind)}`,
          tone: toneForKind(delivery.kind),
        });
      }

      return {
        id: `delivery-${delivery.deliveryId}`,
        label: delivery.label,
        mode: 'detailed',
        badges: [kindLabel(delivery.kind), delivery.assignedVehicleName || 'Unassigned'],
        items,
      };
    }),
  };
}

function buildAxis(preview) {
  let minMinute = 6 * 60;
  let maxMinute = 21 * 60;
  const previewData = preview || { deliveries: [], vehicles: [] };

  for (const delivery of previewData.deliveries || []) {
    minMinute = Math.min(minMinute, Math.floor((delivery.minStartTime || 0) / 60) - 30);
    maxMinute = Math.max(maxMinute, Math.ceil((delivery.maxEndTime || 0) / 60) + 30);
  }
  for (const vehicle of previewData.vehicles || []) {
    minMinute = Math.min(minMinute, Math.floor((vehicle.startTime || 0) / 60) - 15);
    maxMinute = Math.max(maxMinute, Math.ceil((vehicle.endTime || 0) / 60) + 15);
  }

  minMinute = Math.max(0, minMinute);
  maxMinute = Math.min(24 * 60, Math.max(minMinute + 120, maxMinute));

  const ticks = [];
  for (let minute = minMinute; minute <= maxMinute; minute += 60) {
    ticks.push({ minute, label: formatClock(minute * 60) });
  }

  return {
    startMinute: minMinute,
    endMinute: maxMinute,
    days: [{ dayIndex: 0, label: 'Day 1' }],
    ticks,
    initialViewport: { startMinute: minMinute, endMinute: maxMinute },
  };
}