@fleetbase/fleetops-engine 0.6.39 → 0.6.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/layout/fleet-ops-sidebar.hbs +5 -4
- package/addon/components/layout/fleet-ops-sidebar.js +43 -15
- package/addon/components/orchestrator-workbench.js +5 -4
- package/addon/components/vehicle/details.hbs +22 -0
- package/addon/components/vehicle/form.hbs +12 -0
- package/addon/services/work-order-actions.js +0 -1
- package/composer.json +2 -2
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/migrations/2026_04_08_000001_add_orchestrator_columns_to_vehicles_table.php +0 -5
- package/server/migrations/2026_04_08_000004_add_orchestrator_columns_to_payloads_table.php +0 -3
- package/server/migrations/2026_04_14_000001_drop_redundant_capacity_columns_from_payloads_table.php +42 -0
- package/server/migrations/2026_04_14_000002_rename_capacity_columns_on_vehicles_table.php +48 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +15 -2
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +18 -2
- package/server/src/Models/Payload.php +2 -7
- package/server/src/Models/Vehicle.php +9 -11
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +115 -25
|
@@ -104,8 +104,9 @@
|
|
|
104
104
|
}}
|
|
105
105
|
{{else}}
|
|
106
106
|
<Layout::Sidebar::Item
|
|
107
|
-
@onClick={{menuItem.onClick}}
|
|
108
|
-
@
|
|
107
|
+
@onClick={{if menuItem._virtual (fn this.universe.transitionMenuItem (concat this.routePrefix "virtual") menuItem) menuItem.onClick}}
|
|
108
|
+
@menuItem={{menuItem}}
|
|
109
|
+
@route={{if menuItem._virtual null (concat this.routePrefix menuItem.route)}}
|
|
109
110
|
@icon={{menuItem.icon}}
|
|
110
111
|
@iconComponent={{menuItem.iconComponent}}
|
|
111
112
|
@iconComponentOptions={{menuItem.iconComponentOptions}}
|
|
@@ -135,7 +136,7 @@
|
|
|
135
136
|
>{{if menuItem.intl (t menuItem.intl) menuItem.title}}</Layout::Sidebar::Item>
|
|
136
137
|
{{/if}}
|
|
137
138
|
{{/each}}
|
|
138
|
-
{{#let (array "operations" "management" "connectivity" "maintenance" "analytics" "settings") as |sectionsToRender|}}
|
|
139
|
+
{{!-- {{#let (array "operations" "management" "connectivity" "maintenance" "analytics" "settings") as |sectionsToRender|}}
|
|
139
140
|
{{#each sectionsToRender as |section|}}
|
|
140
141
|
{{#if (eq menuPanel.routePrefix section)}}
|
|
141
142
|
{{#each (get this (concat "universe" (capitalize section) "MenuItems")) as |menuItem|}}
|
|
@@ -176,7 +177,7 @@
|
|
|
176
177
|
{{/each}}
|
|
177
178
|
{{/if}}
|
|
178
179
|
{{/each}}
|
|
179
|
-
{{/let}}
|
|
180
|
+
{{/let}} --}}
|
|
180
181
|
</Layout::Sidebar::Panel>
|
|
181
182
|
{{/each}}
|
|
182
183
|
{{#each this.universeMenuPanels as |menuPanel|}}
|
|
@@ -31,7 +31,6 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
31
31
|
|
|
32
32
|
createMenuItemsFromUniverseRegistry() {
|
|
33
33
|
const registeredMenuItems = this.menuService.getMenuItems('engine:fleet-ops');
|
|
34
|
-
console.log('Registered menu items for engine:fleet-ops:', registeredMenuItems);
|
|
35
34
|
this.universeMenuPanels = this.menuService.getMenuPanels('engine:fleet-ops');
|
|
36
35
|
this.universeMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === undefined);
|
|
37
36
|
this.universeOperationsMenuItems = registeredMenuItems.filter((menuItem) => menuItem.section === 'operations');
|
|
@@ -46,6 +45,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
46
45
|
createMenuPanels() {
|
|
47
46
|
const operationsItems = [
|
|
48
47
|
{
|
|
48
|
+
priority: 0,
|
|
49
49
|
intl: 'menu.dashboard',
|
|
50
50
|
title: this.intl.t('menu.dashboard'),
|
|
51
51
|
icon: 'home',
|
|
@@ -54,6 +54,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
54
54
|
visible: this.abilities.can('fleet-ops see order'),
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
|
+
priority: 1,
|
|
57
58
|
intl: 'menu.orchestrator',
|
|
58
59
|
title: this.intl.t('menu.orchestrator'),
|
|
59
60
|
icon: 'circle-nodes',
|
|
@@ -62,6 +63,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
62
63
|
visible: this.abilities.can('fleet-ops see order'),
|
|
63
64
|
},
|
|
64
65
|
{
|
|
66
|
+
priority: 2,
|
|
65
67
|
intl: 'menu.scheduler',
|
|
66
68
|
title: this.intl.t('menu.scheduler'),
|
|
67
69
|
icon: 'calendar-day',
|
|
@@ -70,6 +72,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
70
72
|
visible: this.abilities.can('fleet-ops see order'),
|
|
71
73
|
},
|
|
72
74
|
{
|
|
75
|
+
priority: 3,
|
|
73
76
|
intl: 'menu.order-config',
|
|
74
77
|
title: this.intl.t('menu.order-config'),
|
|
75
78
|
icon: 'diagram-project',
|
|
@@ -78,6 +81,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
78
81
|
visible: this.abilities.can('fleet-ops see order-config'),
|
|
79
82
|
},
|
|
80
83
|
{
|
|
84
|
+
priority: 4,
|
|
81
85
|
intl: 'menu.service-rates',
|
|
82
86
|
title: this.intl.t('menu.service-rates'),
|
|
83
87
|
icon: 'file-invoice-dollar',
|
|
@@ -85,10 +89,12 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
85
89
|
permission: 'fleet-ops list service-rate',
|
|
86
90
|
visible: this.abilities.can('fleet-ops see service-rate'),
|
|
87
91
|
},
|
|
88
|
-
|
|
92
|
+
...(this.universeOperationsMenuItems ?? []).map((item) => ({ ...item, _virtual: true })),
|
|
93
|
+
].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
89
94
|
|
|
90
95
|
const resourcesItems = [
|
|
91
96
|
{
|
|
97
|
+
priority: 0,
|
|
92
98
|
intl: 'menu.drivers',
|
|
93
99
|
title: this.intl.t('menu.drivers'),
|
|
94
100
|
icon: 'id-card',
|
|
@@ -99,6 +105,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
99
105
|
visible: this.abilities.can('fleet-ops see driver'),
|
|
100
106
|
},
|
|
101
107
|
{
|
|
108
|
+
priority: 1,
|
|
102
109
|
intl: 'menu.vehicles',
|
|
103
110
|
title: this.intl.t('menu.vehicles'),
|
|
104
111
|
icon: 'truck',
|
|
@@ -107,6 +114,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
107
114
|
visible: this.abilities.can('fleet-ops see vehicle'),
|
|
108
115
|
},
|
|
109
116
|
{
|
|
117
|
+
priority: 2,
|
|
110
118
|
intl: 'menu.fleets',
|
|
111
119
|
title: this.intl.t('menu.fleets'),
|
|
112
120
|
icon: 'user-group',
|
|
@@ -117,6 +125,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
117
125
|
visible: this.abilities.can('fleet-ops see fleet'),
|
|
118
126
|
},
|
|
119
127
|
{
|
|
128
|
+
priority: 3,
|
|
120
129
|
intl: 'menu.vendors',
|
|
121
130
|
title: this.intl.t('menu.vendors'),
|
|
122
131
|
icon: 'warehouse',
|
|
@@ -125,6 +134,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
125
134
|
visible: this.abilities.can('fleet-ops see vendor'),
|
|
126
135
|
},
|
|
127
136
|
{
|
|
137
|
+
priority: 4,
|
|
128
138
|
intl: 'menu.contacts',
|
|
129
139
|
title: this.intl.t('menu.contacts'),
|
|
130
140
|
icon: 'address-book',
|
|
@@ -133,6 +143,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
133
143
|
visible: this.abilities.can('fleet-ops see contact'),
|
|
134
144
|
},
|
|
135
145
|
{
|
|
146
|
+
priority: 5,
|
|
136
147
|
intl: 'menu.places',
|
|
137
148
|
title: this.intl.t('menu.places'),
|
|
138
149
|
icon: 'location-dot',
|
|
@@ -141,6 +152,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
141
152
|
visible: this.abilities.can('fleet-ops see place'),
|
|
142
153
|
},
|
|
143
154
|
{
|
|
155
|
+
priority: 6,
|
|
144
156
|
intl: 'menu.fuel-reports',
|
|
145
157
|
title: this.intl.t('menu.fuel-reports'),
|
|
146
158
|
icon: 'gas-pump',
|
|
@@ -149,6 +161,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
149
161
|
visible: this.abilities.can('fleet-ops see fuel-report'),
|
|
150
162
|
},
|
|
151
163
|
{
|
|
164
|
+
priority: 7,
|
|
152
165
|
intl: 'menu.issues',
|
|
153
166
|
title: this.intl.t('menu.issues'),
|
|
154
167
|
icon: 'triangle-exclamation',
|
|
@@ -156,10 +169,12 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
156
169
|
permission: 'fleet-ops list issue',
|
|
157
170
|
visible: this.abilities.can('fleet-ops see issue'),
|
|
158
171
|
},
|
|
159
|
-
|
|
172
|
+
...(this.universeManagementMenuItems ?? []).map((item) => ({ ...item, _virtual: true })),
|
|
173
|
+
].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
160
174
|
|
|
161
175
|
const connectivityItems = [
|
|
162
176
|
{
|
|
177
|
+
priority: 0,
|
|
163
178
|
intl: 'menu.telematics',
|
|
164
179
|
title: this.intl.t('menu.telematics'),
|
|
165
180
|
icon: 'satellite-dish',
|
|
@@ -168,6 +183,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
168
183
|
visible: this.abilities.can('fleet-ops see telematic'),
|
|
169
184
|
},
|
|
170
185
|
{
|
|
186
|
+
priority: 1,
|
|
171
187
|
intl: 'menu.devices',
|
|
172
188
|
title: this.intl.t('menu.devices'),
|
|
173
189
|
icon: 'hard-drive',
|
|
@@ -176,6 +192,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
176
192
|
visible: this.abilities.can('fleet-ops see device'),
|
|
177
193
|
},
|
|
178
194
|
{
|
|
195
|
+
priority: 2,
|
|
179
196
|
intl: 'menu.sensors',
|
|
180
197
|
title: this.intl.t('menu.sensors'),
|
|
181
198
|
icon: 'temperature-full',
|
|
@@ -184,6 +201,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
184
201
|
visible: this.abilities.can('fleet-ops see sensor'),
|
|
185
202
|
},
|
|
186
203
|
{
|
|
204
|
+
priority: 3,
|
|
187
205
|
intl: 'menu.events',
|
|
188
206
|
title: this.intl.t('menu.events'),
|
|
189
207
|
icon: 'stream',
|
|
@@ -191,18 +209,12 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
191
209
|
permission: 'fleet-ops list device-event',
|
|
192
210
|
visible: this.abilities.can('fleet-ops see device-event'),
|
|
193
211
|
},
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// title: this.intl.t('menu.tracking'),
|
|
197
|
-
// icon: 'map-marked-alt',
|
|
198
|
-
// route: 'connectivity.tracking',
|
|
199
|
-
// permission: 'fleet-ops list device',
|
|
200
|
-
// visible: this.abilities.can('fleet-ops see device'),
|
|
201
|
-
// },
|
|
202
|
-
];
|
|
212
|
+
...(this.universeConnectivityMenuItems ?? []).map((item) => ({ ...item, _virtual: true })),
|
|
213
|
+
].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
203
214
|
|
|
204
215
|
const maintenanceItems = [
|
|
205
216
|
{
|
|
217
|
+
priority: 0,
|
|
206
218
|
intl: 'menu.schedules',
|
|
207
219
|
title: this.intl.t('menu.schedules'),
|
|
208
220
|
icon: 'calendar-alt',
|
|
@@ -211,6 +223,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
211
223
|
visible: this.abilities.can('fleet-ops see maintenance-schedule'),
|
|
212
224
|
},
|
|
213
225
|
{
|
|
226
|
+
priority: 1,
|
|
214
227
|
intl: 'menu.work-orders',
|
|
215
228
|
title: this.intl.t('menu.work-orders'),
|
|
216
229
|
icon: 'clipboard-list',
|
|
@@ -219,6 +232,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
219
232
|
visible: this.abilities.can('fleet-ops see work-order'),
|
|
220
233
|
},
|
|
221
234
|
{
|
|
235
|
+
priority: 2,
|
|
222
236
|
intl: 'menu.maintenances',
|
|
223
237
|
title: this.intl.t('menu.maintenances'),
|
|
224
238
|
icon: 'history',
|
|
@@ -227,6 +241,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
227
241
|
visible: this.abilities.can('fleet-ops see maintenance'),
|
|
228
242
|
},
|
|
229
243
|
{
|
|
244
|
+
priority: 3,
|
|
230
245
|
intl: 'menu.equipment',
|
|
231
246
|
title: this.intl.t('menu.equipment'),
|
|
232
247
|
icon: 'trailer',
|
|
@@ -235,6 +250,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
235
250
|
visible: this.abilities.can('fleet-ops see equipment'),
|
|
236
251
|
},
|
|
237
252
|
{
|
|
253
|
+
priority: 4,
|
|
238
254
|
intl: 'menu.parts',
|
|
239
255
|
title: this.intl.t('menu.parts'),
|
|
240
256
|
icon: 'cog',
|
|
@@ -242,10 +258,12 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
242
258
|
permission: 'fleet-ops list part',
|
|
243
259
|
visible: this.abilities.can('fleet-ops see part'),
|
|
244
260
|
},
|
|
245
|
-
|
|
261
|
+
...(this.universeMaintenanceMenuItems ?? []).map((item) => ({ ...item, _virtual: true })),
|
|
262
|
+
].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
246
263
|
|
|
247
264
|
const analyticsItems = [
|
|
248
265
|
{
|
|
266
|
+
priority: 0,
|
|
249
267
|
intl: 'menu.reports',
|
|
250
268
|
title: this.intl.t('menu.reports'),
|
|
251
269
|
icon: 'file-import',
|
|
@@ -253,10 +271,12 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
253
271
|
permission: 'iam list report',
|
|
254
272
|
visible: this.abilities.can('fleet-ops see report'),
|
|
255
273
|
},
|
|
256
|
-
|
|
274
|
+
...(this.universeAnalyticsMenuItems ?? []).map((item) => ({ ...item, _virtual: true })),
|
|
275
|
+
].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
257
276
|
|
|
258
277
|
const settingsItems = [
|
|
259
278
|
{
|
|
279
|
+
priority: 0,
|
|
260
280
|
intl: 'menu.navigator-app',
|
|
261
281
|
title: this.intl.t('menu.navigator-app'),
|
|
262
282
|
icon: 'location-arrow',
|
|
@@ -265,6 +285,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
265
285
|
visible: this.abilities.can('fleet-ops see navigator-settings'),
|
|
266
286
|
},
|
|
267
287
|
{
|
|
288
|
+
priority: 1,
|
|
268
289
|
intl: 'menu.payments',
|
|
269
290
|
title: this.intl.t('menu.payments'),
|
|
270
291
|
icon: 'cash-register',
|
|
@@ -273,6 +294,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
273
294
|
visible: this.abilities.can('fleet-ops see payments'),
|
|
274
295
|
},
|
|
275
296
|
{
|
|
297
|
+
priority: 2,
|
|
276
298
|
intl: 'menu.notifications',
|
|
277
299
|
title: this.intl.t('menu.notifications'),
|
|
278
300
|
icon: 'bell',
|
|
@@ -281,6 +303,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
281
303
|
visible: this.abilities.can('fleet-ops see notification-settings'),
|
|
282
304
|
},
|
|
283
305
|
{
|
|
306
|
+
priority: 3,
|
|
284
307
|
intl: 'menu.routing',
|
|
285
308
|
title: this.intl.t('menu.routing'),
|
|
286
309
|
icon: 'route',
|
|
@@ -289,6 +312,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
289
312
|
visible: this.abilities.can('fleet-ops see routing-settings'),
|
|
290
313
|
},
|
|
291
314
|
{
|
|
315
|
+
priority: 4,
|
|
292
316
|
intl: 'menu.orchestrator',
|
|
293
317
|
title: this.intl.t('menu.orchestrator'),
|
|
294
318
|
icon: 'circle-nodes',
|
|
@@ -297,6 +321,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
297
321
|
visible: this.abilities.can('fleet-ops see routing-settings'),
|
|
298
322
|
},
|
|
299
323
|
{
|
|
324
|
+
priority: 5,
|
|
300
325
|
intl: 'menu.scheduling',
|
|
301
326
|
title: this.intl.t('menu.scheduling'),
|
|
302
327
|
icon: 'calendar-days',
|
|
@@ -305,6 +330,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
305
330
|
visible: this.abilities.can('fleet-ops see scheduling-settings'),
|
|
306
331
|
},
|
|
307
332
|
{
|
|
333
|
+
priority: 6,
|
|
308
334
|
intl: 'menu.custom-fields',
|
|
309
335
|
title: this.intl.t('menu.custom-fields'),
|
|
310
336
|
icon: 'rectangle-list',
|
|
@@ -313,6 +339,7 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
313
339
|
visible: this.abilities.can('fleet-ops see custom-field'),
|
|
314
340
|
},
|
|
315
341
|
{
|
|
342
|
+
priority: 7,
|
|
316
343
|
intl: 'menu.avatars',
|
|
317
344
|
title: this.intl.t('menu.avatars'),
|
|
318
345
|
icon: 'icons',
|
|
@@ -320,7 +347,8 @@ export default class LayoutFleetOpsSidebarComponent extends Component {
|
|
|
320
347
|
permission: 'fleet-ops view avatar',
|
|
321
348
|
visible: this.abilities.can('fleet-ops see avatar'),
|
|
322
349
|
},
|
|
323
|
-
|
|
350
|
+
...(this.universeSettingsMenuItems ?? []).map((item) => ({ ...item, _virtual: true })),
|
|
351
|
+
].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
324
352
|
|
|
325
353
|
const createPanel = (intl, routePrefix, items = [], options = {}) => ({
|
|
326
354
|
intl,
|
|
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
|
|
|
3
3
|
import { tracked } from '@glimmer/tracking';
|
|
4
4
|
import { action } from '@ember/object';
|
|
5
5
|
import { later } from '@ember/runloop';
|
|
6
|
+
import { debug } from '@ember/debug';
|
|
6
7
|
import { task } from 'ember-concurrency';
|
|
7
8
|
import { colorForId, waypointIconHtml } from '../utils/route-colors';
|
|
8
9
|
import polyline from '@fleetbase/ember-core/utils/polyline';
|
|
@@ -122,7 +123,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
122
123
|
const lat = this.location.getLatitude();
|
|
123
124
|
const lng = this.location.getLongitude();
|
|
124
125
|
// eslint-disable-next-line no-console
|
|
125
|
-
|
|
126
|
+
debug(`[Orchestrator] constructor: location service initial coords => ${lat}, ${lng}`);
|
|
126
127
|
if (lat != null && lng != null) {
|
|
127
128
|
this.mapCenter = { lat, lng };
|
|
128
129
|
}
|
|
@@ -132,17 +133,17 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
132
133
|
.getUserLocation()
|
|
133
134
|
.then(({ latitude, longitude }) => {
|
|
134
135
|
// eslint-disable-next-line no-console
|
|
135
|
-
|
|
136
|
+
debug(`[Orchestrator] getUserLocation resolved => ${latitude}, ${longitude} | _mapCenteredOnOrders = ${this._mapCenteredOnOrders}`);
|
|
136
137
|
if (!this._mapCenteredOnOrders) {
|
|
137
138
|
// eslint-disable-next-line no-console
|
|
138
|
-
|
|
139
|
+
debug('[Orchestrator] applying geolocation as map center (no orders centered yet)');
|
|
139
140
|
this.mapCenter = { lat: latitude, lng: longitude };
|
|
140
141
|
if (this.leafletMap?.setView) {
|
|
141
142
|
this.leafletMap.setView([latitude, longitude], this.mapZoom);
|
|
142
143
|
}
|
|
143
144
|
} else {
|
|
144
145
|
// eslint-disable-next-line no-console
|
|
145
|
-
|
|
146
|
+
debug('[Orchestrator] geolocation ignored — map already centered on orders');
|
|
146
147
|
}
|
|
147
148
|
})
|
|
148
149
|
.catch(() => {});
|
|
@@ -407,6 +407,28 @@
|
|
|
407
407
|
</div>
|
|
408
408
|
</div>
|
|
409
409
|
|
|
410
|
+
<div class="field-info-container">
|
|
411
|
+
<div class="field-name">Payload Volume</div>
|
|
412
|
+
<div class="field-value">
|
|
413
|
+
{{#if @resource.payload_capacity_volume}}
|
|
414
|
+
{{@resource.payload_capacity_volume}}
|
|
415
|
+
m³
|
|
416
|
+
{{else}}
|
|
417
|
+
{{n-a @resource.payload_capacity_volume}}
|
|
418
|
+
{{/if}}
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div class="field-info-container">
|
|
423
|
+
<div class="field-name">Pallet Capacity</div>
|
|
424
|
+
<div class="field-value">{{n-a @resource.payload_capacity_pallets}}</div>
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<div class="field-info-container">
|
|
428
|
+
<div class="field-name">Parcel Capacity</div>
|
|
429
|
+
<div class="field-value">{{n-a @resource.payload_capacity_parcels}}</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
410
432
|
<div class="field-info-container">
|
|
411
433
|
<div class="field-name">Passenger Volume</div>
|
|
412
434
|
<div class="field-value">
|
|
@@ -403,6 +403,18 @@
|
|
|
403
403
|
<UnitInput class="w-full" @value={{@resource.cargo_volume}} @unit="L" @placeholder="Cargo Volume" />
|
|
404
404
|
</InputGroup>
|
|
405
405
|
|
|
406
|
+
<InputGroup @name="Payload Volume (m³)" @helpText="The maximum cargo volume this vehicle can carry, in cubic metres.">
|
|
407
|
+
<Input @value={{@resource.payload_capacity_volume}} @type="number" class="form-input w-full" placeholder="e.g. 40" disabled={{cannot-write @resource}} />
|
|
408
|
+
</InputGroup>
|
|
409
|
+
|
|
410
|
+
<InputGroup @name="Pallet Capacity" @helpText="The maximum number of standard pallets this vehicle can carry.">
|
|
411
|
+
<Input @value={{@resource.payload_capacity_pallets}} @type="number" class="form-input w-full" placeholder="e.g. 20" disabled={{cannot-write @resource}} />
|
|
412
|
+
</InputGroup>
|
|
413
|
+
|
|
414
|
+
<InputGroup @name="Parcel Capacity" @helpText="The maximum number of parcels or packages this vehicle can carry.">
|
|
415
|
+
<Input @value={{@resource.payload_capacity_parcels}} @type="number" class="form-input w-full" placeholder="e.g. 200" disabled={{cannot-write @resource}} />
|
|
416
|
+
</InputGroup>
|
|
417
|
+
|
|
406
418
|
<InputGroup @name="Passenger Volume">
|
|
407
419
|
<UnitInput class="w-full" @value={{@resource.passenger_volume}} @unit="L" @placeholder="Passenger Volume" />
|
|
408
420
|
</InputGroup>
|
|
@@ -96,7 +96,6 @@ export default class WorkOrderActionsService extends ResourceActionService {
|
|
|
96
96
|
|
|
97
97
|
@action sendEmail(workOrder, options = {}) {
|
|
98
98
|
const assignee = workOrder.assignee;
|
|
99
|
-
console.log('Assignee:', assignee);
|
|
100
99
|
const vendorName = assignee?.name ?? workOrder.assignee_name ?? null;
|
|
101
100
|
const vendorEmail = assignee?.email ?? null;
|
|
102
101
|
const vendorPhone = assignee?.phone ?? null;
|
package/composer.json
CHANGED
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetbase/fleetops-engine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.40",
|
|
4
4
|
"description": "Fleet & Transport Management Extension for Fleetbase",
|
|
5
5
|
"fleetbase": {
|
|
6
6
|
"route": "fleet-ops"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@babel/core": "^7.23.2",
|
|
45
45
|
"@fleetbase/ember-core": "^0.3.18",
|
|
46
46
|
"@fleetbase/ember-ui": "^0.3.26",
|
|
47
|
-
"@fleetbase/fleetops-data": "^0.1.
|
|
47
|
+
"@fleetbase/fleetops-data": "^0.1.28",
|
|
48
48
|
"@fleetbase/leaflet-routing-machine": "^3.2.17",
|
|
49
49
|
"@fortawesome/ember-fontawesome": "^2.0.0",
|
|
50
50
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
<?php
|
|
2
|
-
|
|
3
2
|
use Illuminate\Database\Migrations\Migration;
|
|
4
3
|
use Illuminate\Database\Schema\Blueprint;
|
|
5
4
|
use Illuminate\Support\Facades\Schema;
|
|
6
|
-
|
|
7
5
|
/**
|
|
8
6
|
* Add Orchestrator constraint columns to the vehicles table.
|
|
9
7
|
*
|
|
@@ -24,13 +22,11 @@ return new class extends Migration {
|
|
|
24
22
|
Schema::table('vehicles', function (Blueprint $table) {
|
|
25
23
|
// Skills / capabilities
|
|
26
24
|
$table->json('skills')->nullable()->after('status');
|
|
27
|
-
|
|
28
25
|
// Multi-dimensional capacity
|
|
29
26
|
$table->unsignedDecimal('capacity_weight_kg', 10, 2)->nullable()->after('skills');
|
|
30
27
|
$table->unsignedDecimal('capacity_volume_m3', 10, 3)->nullable()->after('capacity_weight_kg');
|
|
31
28
|
$table->unsignedInteger('capacity_pallets')->nullable()->after('capacity_volume_m3');
|
|
32
29
|
$table->unsignedInteger('capacity_parcels')->nullable()->after('capacity_pallets');
|
|
33
|
-
|
|
34
30
|
// Route constraints
|
|
35
31
|
$table->unsignedInteger('max_tasks')->nullable()->after('capacity_parcels');
|
|
36
32
|
$table->time('time_window_start')->nullable()->after('max_tasks');
|
|
@@ -38,7 +34,6 @@ return new class extends Migration {
|
|
|
38
34
|
$table->boolean('return_to_depot')->default(true)->after('time_window_end');
|
|
39
35
|
});
|
|
40
36
|
}
|
|
41
|
-
|
|
42
37
|
public function down(): void
|
|
43
38
|
{
|
|
44
39
|
Schema::table('vehicles', function (Blueprint $table) {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
<?php
|
|
2
|
-
|
|
3
2
|
use Illuminate\Database\Migrations\Migration;
|
|
4
3
|
use Illuminate\Database\Schema\Blueprint;
|
|
5
4
|
use Illuminate\Support\Facades\Schema;
|
|
6
|
-
|
|
7
5
|
/**
|
|
8
6
|
* Add Orchestrator capacity columns to the payloads table.
|
|
9
7
|
*
|
|
@@ -26,7 +24,6 @@ return new class extends Migration {
|
|
|
26
24
|
$table->unsignedInteger('capacity_parcels')->nullable()->after('capacity_pallets');
|
|
27
25
|
});
|
|
28
26
|
}
|
|
29
|
-
|
|
30
27
|
public function down(): void
|
|
31
28
|
{
|
|
32
29
|
Schema::table('payloads', function (Blueprint $table) {
|
package/server/migrations/2026_04_14_000001_drop_redundant_capacity_columns_from_payloads_table.php
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
use Illuminate\Database\Migrations\Migration;
|
|
3
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
4
|
+
use Illuminate\Support\Facades\Schema;
|
|
5
|
+
/**
|
|
6
|
+
* Drop redundant denormalised capacity columns from the payloads table.
|
|
7
|
+
*
|
|
8
|
+
* The columns capacity_weight_kg, capacity_volume_m3, capacity_pallets, and
|
|
9
|
+
* capacity_parcels were added in migration 2026_04_08_000004 as a cache of
|
|
10
|
+
* aggregated entity dimensions. Storing them creates a synchronisation problem:
|
|
11
|
+
* any entity add, update, or removal requires explicit cache invalidation.
|
|
12
|
+
*
|
|
13
|
+
* The OrchestrationPayloadBuilder now computes these values dynamically from
|
|
14
|
+
* the payload->entities relationship at orchestration time, so the columns are
|
|
15
|
+
* no longer needed.
|
|
16
|
+
*
|
|
17
|
+
* Rollback restores the columns as nullable so existing data is not lost if
|
|
18
|
+
* this migration is reversed.
|
|
19
|
+
*/
|
|
20
|
+
return new class extends Migration {
|
|
21
|
+
public function up(): void
|
|
22
|
+
{
|
|
23
|
+
Schema::table('payloads', function (Blueprint $table) {
|
|
24
|
+
$table->dropColumn([
|
|
25
|
+
'capacity_weight_kg',
|
|
26
|
+
'capacity_volume_m3',
|
|
27
|
+
'capacity_pallets',
|
|
28
|
+
'capacity_parcels',
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public function down(): void
|
|
34
|
+
{
|
|
35
|
+
Schema::table('payloads', function (Blueprint $table) {
|
|
36
|
+
$table->unsignedDecimal('capacity_weight_kg', 10, 2)->nullable()->after('cod_currency');
|
|
37
|
+
$table->unsignedDecimal('capacity_volume_m3', 10, 3)->nullable()->after('capacity_weight_kg');
|
|
38
|
+
$table->unsignedInteger('capacity_pallets')->nullable()->after('capacity_volume_m3');
|
|
39
|
+
$table->unsignedInteger('capacity_parcels')->nullable()->after('capacity_pallets');
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
use Illuminate\Database\Migrations\Migration;
|
|
3
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
4
|
+
use Illuminate\Support\Facades\Schema;
|
|
5
|
+
/**
|
|
6
|
+
* Correct orchestrator capacity column names on the vehicles table.
|
|
7
|
+
*
|
|
8
|
+
* The columns added in migration 2026_04_08_000001 did not follow the Fleetbase
|
|
9
|
+
* naming convention for capacity fields. This migration:
|
|
10
|
+
*
|
|
11
|
+
* 1. Drops capacity_weight_kg — this is a duplicate of the pre-existing
|
|
12
|
+
* payload_capacity column which already represents maximum payload weight.
|
|
13
|
+
*
|
|
14
|
+
* 2. Renames the remaining capacity columns to follow the payload_capacity_*
|
|
15
|
+
* convention used by the existing payload_capacity column:
|
|
16
|
+
* capacity_volume_m3 → payload_capacity_volume
|
|
17
|
+
* capacity_pallets → payload_capacity_pallets
|
|
18
|
+
* capacity_parcels → payload_capacity_parcels
|
|
19
|
+
*
|
|
20
|
+
* Rollback reverses the renames and restores capacity_weight_kg as nullable.
|
|
21
|
+
*/
|
|
22
|
+
return new class extends Migration {
|
|
23
|
+
public function up(): void
|
|
24
|
+
{
|
|
25
|
+
Schema::table('vehicles', function (Blueprint $table) {
|
|
26
|
+
// Drop the redundant weight capacity column (payload_capacity already exists)
|
|
27
|
+
$table->dropColumn('capacity_weight_kg');
|
|
28
|
+
|
|
29
|
+
// Rename remaining columns to follow payload_capacity_* convention
|
|
30
|
+
$table->renameColumn('capacity_volume_m3', 'payload_capacity_volume');
|
|
31
|
+
$table->renameColumn('capacity_pallets', 'payload_capacity_pallets');
|
|
32
|
+
$table->renameColumn('capacity_parcels', 'payload_capacity_parcels');
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public function down(): void
|
|
37
|
+
{
|
|
38
|
+
Schema::table('vehicles', function (Blueprint $table) {
|
|
39
|
+
// Restore the dropped column
|
|
40
|
+
$table->unsignedDecimal('capacity_weight_kg', 10, 2)->nullable()->after('skills');
|
|
41
|
+
|
|
42
|
+
// Reverse the renames
|
|
43
|
+
$table->renameColumn('payload_capacity_volume', 'capacity_volume_m3');
|
|
44
|
+
$table->renameColumn('payload_capacity_pallets', 'capacity_pallets');
|
|
45
|
+
$table->renameColumn('payload_capacity_parcels', 'capacity_parcels');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -55,7 +55,14 @@ class OrderController extends Controller
|
|
|
55
55
|
set_time_limit(180);
|
|
56
56
|
|
|
57
57
|
// get request input
|
|
58
|
-
$input = $request->only([
|
|
58
|
+
$input = $request->only([
|
|
59
|
+
'internal_id', 'payload', 'service_quote', 'purchase_rate',
|
|
60
|
+
'adhoc', 'adhoc_distance', 'pod_method', 'pod_required',
|
|
61
|
+
'scheduled_at', 'status', 'meta', 'notes',
|
|
62
|
+
// Orchestrator constraints
|
|
63
|
+
'time_window_start', 'time_window_end',
|
|
64
|
+
'required_skills', 'orchestrator_priority',
|
|
65
|
+
]);
|
|
59
66
|
|
|
60
67
|
// Get order config
|
|
61
68
|
$orderConfig = OrderConfig::resolveFromIdentifier($request->only(['type', 'order_config']));
|
|
@@ -363,7 +370,13 @@ class OrderController extends Controller
|
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
// get request input
|
|
366
|
-
$input = $request->only([
|
|
373
|
+
$input = $request->only([
|
|
374
|
+
'internal_id', 'payload', 'adhoc', 'adhoc_distance',
|
|
375
|
+
'pod_method', 'pod_required', 'scheduled_at', 'meta', 'type', 'status', 'notes',
|
|
376
|
+
// Orchestrator constraints
|
|
377
|
+
'time_window_start', 'time_window_end',
|
|
378
|
+
'required_skills', 'orchestrator_priority',
|
|
379
|
+
]);
|
|
367
380
|
|
|
368
381
|
// update payload if new input or change payload by id
|
|
369
382
|
if ($request->isArray('payload')) {
|
|
@@ -26,7 +26,15 @@ class VehicleController extends Controller
|
|
|
26
26
|
public function create(CreateVehicleRequest $request)
|
|
27
27
|
{
|
|
28
28
|
// get request input
|
|
29
|
-
$input = $request->only([
|
|
29
|
+
$input = $request->only([
|
|
30
|
+
'status', 'make', 'model', 'year', 'trim', 'type', 'plate_number', 'vin',
|
|
31
|
+
'meta', 'online', 'location', 'altitude', 'heading', 'speed',
|
|
32
|
+
// Capacity
|
|
33
|
+
'payload_capacity', 'payload_capacity_volume',
|
|
34
|
+
'payload_capacity_pallets', 'payload_capacity_parcels',
|
|
35
|
+
// Orchestrator constraints
|
|
36
|
+
'skills', 'max_tasks', 'time_window_start', 'time_window_end', 'return_to_depot',
|
|
37
|
+
]);
|
|
30
38
|
|
|
31
39
|
// make sure company is set
|
|
32
40
|
$input['company_uuid'] = session('company');
|
|
@@ -97,7 +105,15 @@ class VehicleController extends Controller
|
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
// get request input
|
|
100
|
-
$input = $request->only([
|
|
108
|
+
$input = $request->only([
|
|
109
|
+
'status', 'make', 'model', 'year', 'trim', 'type', 'plate_number', 'vin',
|
|
110
|
+
'meta', 'location', 'online', 'altitude', 'heading', 'speed',
|
|
111
|
+
// Capacity
|
|
112
|
+
'payload_capacity', 'payload_capacity_volume',
|
|
113
|
+
'payload_capacity_pallets', 'payload_capacity_parcels',
|
|
114
|
+
// Orchestrator constraints
|
|
115
|
+
'skills', 'max_tasks', 'time_window_start', 'time_window_end', 'return_to_depot',
|
|
116
|
+
]);
|
|
101
117
|
|
|
102
118
|
// vendor assignment
|
|
103
119
|
if ($request->has('vendor')) {
|
|
@@ -61,7 +61,7 @@ class Payload extends Model
|
|
|
61
61
|
*
|
|
62
62
|
* @var array
|
|
63
63
|
*/
|
|
64
|
-
protected $fillable = ['_key', 'company_uuid', 'pickup_uuid', 'dropoff_uuid', 'return_uuid', 'current_waypoint_uuid', 'meta', 'payment_method', 'cod_amount', 'cod_currency', 'cod_payment_method', 'type'
|
|
64
|
+
protected $fillable = ['_key', 'company_uuid', 'pickup_uuid', 'dropoff_uuid', 'return_uuid', 'current_waypoint_uuid', 'meta', 'payment_method', 'cod_amount', 'cod_currency', 'cod_payment_method', 'type'];
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* The attributes that should be cast to native types.
|
|
@@ -69,12 +69,7 @@ class Payload extends Model
|
|
|
69
69
|
* @var array
|
|
70
70
|
*/
|
|
71
71
|
protected $casts = [
|
|
72
|
-
'meta'
|
|
73
|
-
// Orchestrator
|
|
74
|
-
'capacity_weight_kg' => 'decimal:2',
|
|
75
|
-
'capacity_volume_m3' => 'decimal:3',
|
|
76
|
-
'capacity_pallets' => 'integer',
|
|
77
|
-
'capacity_parcels' => 'integer',
|
|
72
|
+
'meta' => Json::class,
|
|
78
73
|
];
|
|
79
74
|
|
|
80
75
|
/**
|
|
@@ -207,10 +207,9 @@ class Vehicle extends Model
|
|
|
207
207
|
'lease_expires_at',
|
|
208
208
|
// Orchestrator
|
|
209
209
|
'skills',
|
|
210
|
-
'
|
|
211
|
-
'
|
|
212
|
-
'
|
|
213
|
-
'capacity_parcels',
|
|
210
|
+
'payload_capacity_volume',
|
|
211
|
+
'payload_capacity_pallets',
|
|
212
|
+
'payload_capacity_parcels',
|
|
214
213
|
'max_tasks',
|
|
215
214
|
'time_window_start',
|
|
216
215
|
'time_window_end',
|
|
@@ -307,13 +306,12 @@ class Vehicle extends Model
|
|
|
307
306
|
'gvwr' => 'decimal:2',
|
|
308
307
|
'gcwr' => 'decimal:2',
|
|
309
308
|
// Orchestrator
|
|
310
|
-
'skills'
|
|
311
|
-
'
|
|
312
|
-
'
|
|
313
|
-
'
|
|
314
|
-
'
|
|
315
|
-
'
|
|
316
|
-
'return_to_depot' => 'boolean',
|
|
309
|
+
'skills' => Json::class,
|
|
310
|
+
'payload_capacity_volume' => 'decimal:3',
|
|
311
|
+
'payload_capacity_pallets' => 'integer',
|
|
312
|
+
'payload_capacity_parcels' => 'integer',
|
|
313
|
+
'max_tasks' => 'integer',
|
|
314
|
+
'return_to_depot' => 'boolean',
|
|
317
315
|
];
|
|
318
316
|
|
|
319
317
|
public function photo(): BelongsTo
|
|
@@ -16,9 +16,12 @@ use Illuminate\Support\Collection;
|
|
|
16
16
|
* adapter (e.g. VroomOrchestrationEngine) calls the builder and then maps the
|
|
17
17
|
* normalized output to its own wire format.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Vehicle capacity is read from the existing first-class columns on the
|
|
20
|
+
* vehicles table (payload_capacity, payload_capacity_volume, etc.).
|
|
21
|
+
*
|
|
22
|
+
* Order/payload capacity demand is computed dynamically by aggregating the
|
|
23
|
+
* weight and dimensions of the payload's entities — no denormalised cache
|
|
24
|
+
* columns are needed or used on the payloads table.
|
|
22
25
|
*/
|
|
23
26
|
class OrchestrationPayloadBuilder
|
|
24
27
|
{
|
|
@@ -41,6 +44,102 @@ class OrchestrationPayloadBuilder
|
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Compute the multi-dimensional capacity demand for a payload by aggregating
|
|
49
|
+
* its entities' weight and volume values.
|
|
50
|
+
*
|
|
51
|
+
* Returns a 4-element integer array matching the vehicle capacity array:
|
|
52
|
+
* [weight_kg, volume_litres, pallets, parcels]
|
|
53
|
+
*
|
|
54
|
+
* Weight is normalised to kg from entity.weight_unit.
|
|
55
|
+
* Volume is derived from entity length × width × height (dimensions_unit normalised to metres)
|
|
56
|
+
* and expressed in litres (×1000) so it can be stored as an integer for VROOM.
|
|
57
|
+
*
|
|
58
|
+
* Falls back to order meta keys (weight_kg, volume_m3, pallets, parcels) for
|
|
59
|
+
* backwards compatibility with orders that were created before entity-level
|
|
60
|
+
* dimension data was captured.
|
|
61
|
+
*/
|
|
62
|
+
protected static function computePayloadDemand(Order $order): array
|
|
63
|
+
{
|
|
64
|
+
$payload = $order->payload;
|
|
65
|
+
|
|
66
|
+
if ($payload && $payload->entities && $payload->entities->isNotEmpty()) {
|
|
67
|
+
$totalWeightKg = 0.0;
|
|
68
|
+
$totalVolumeLit = 0.0;
|
|
69
|
+
$totalParcels = 0;
|
|
70
|
+
|
|
71
|
+
foreach ($payload->entities as $entity) {
|
|
72
|
+
// --- Weight ---
|
|
73
|
+
$rawWeight = (float) ($entity->weight ?? 0);
|
|
74
|
+
$weightUnit = strtolower($entity->weight_unit ?? 'kg');
|
|
75
|
+
$weightKg = match ($weightUnit) {
|
|
76
|
+
'g', 'gram', 'grams' => $rawWeight / 1000,
|
|
77
|
+
'lb', 'lbs', 'pound', 'pounds' => $rawWeight * 0.453592,
|
|
78
|
+
'oz', 'ounce', 'ounces' => $rawWeight * 0.0283495,
|
|
79
|
+
't', 'ton', 'tonne', 'tonnes' => $rawWeight * 1000,
|
|
80
|
+
default => $rawWeight, // kg assumed
|
|
81
|
+
};
|
|
82
|
+
$totalWeightKg += $weightKg;
|
|
83
|
+
|
|
84
|
+
// --- Volume (L × W × H → m³ → litres) ---
|
|
85
|
+
$l = (float) ($entity->length ?? 0);
|
|
86
|
+
$w = (float) ($entity->width ?? 0);
|
|
87
|
+
$h = (float) ($entity->height ?? 0);
|
|
88
|
+
$unit = strtolower($entity->dimensions_unit ?? 'm');
|
|
89
|
+
|
|
90
|
+
if ($l > 0 && $w > 0 && $h > 0) {
|
|
91
|
+
// Normalise to metres
|
|
92
|
+
$factor = match ($unit) {
|
|
93
|
+
'cm', 'centimeter', 'centimetre' => 0.01,
|
|
94
|
+
'mm', 'millimeter', 'millimetre' => 0.001,
|
|
95
|
+
'in', 'inch', 'inches' => 0.0254,
|
|
96
|
+
'ft', 'foot', 'feet' => 0.3048,
|
|
97
|
+
default => 1.0, // metres assumed
|
|
98
|
+
};
|
|
99
|
+
$volumeM3 = ($l * $factor) * ($w * $factor) * ($h * $factor);
|
|
100
|
+
$totalVolumeLit += $volumeM3 * 1000; // m³ → litres
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
$totalParcels++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
(int) round($totalWeightKg),
|
|
108
|
+
(int) round($totalVolumeLit),
|
|
109
|
+
0, // pallet count not tracked at entity level
|
|
110
|
+
$totalParcels,
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fallback: order meta keys for backwards compatibility
|
|
115
|
+
return [
|
|
116
|
+
(int) round((float) ($order->getMeta('weight_kg') ?? 0)),
|
|
117
|
+
(int) round((float) ($order->getMeta('volume_m3') ?? 0) * 1000),
|
|
118
|
+
(int) ($order->getMeta('pallets') ?? 0),
|
|
119
|
+
(int) ($order->getMeta('parcels') ?? 1),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build the vehicle capacity array from the vehicle's first-class columns.
|
|
125
|
+
*
|
|
126
|
+
* Returns a 4-element integer array:
|
|
127
|
+
* [weight_kg, volume_litres, pallets, parcels]
|
|
128
|
+
*
|
|
129
|
+
* payload_capacity — existing column, weight in kg
|
|
130
|
+
* payload_capacity_volume — new column, volume in m³ (stored as litres for VROOM)
|
|
131
|
+
* payload_capacity_pallets / payload_capacity_parcels — new columns
|
|
132
|
+
*/
|
|
133
|
+
protected static function buildVehicleCapacity(Vehicle $vehicle): array
|
|
134
|
+
{
|
|
135
|
+
return [
|
|
136
|
+
(int) round((float) ($vehicle->payload_capacity ?? static::safeMeta($vehicle, 'max_weight_kg', 0))),
|
|
137
|
+
(int) round((float) ($vehicle->payload_capacity_volume ?? static::safeMeta($vehicle, 'max_volume_m3', 0)) * 1000),
|
|
138
|
+
(int) ($vehicle->payload_capacity_pallets ?? static::safeMeta($vehicle, 'max_pallets', 0)),
|
|
139
|
+
(int) ($vehicle->payload_capacity_parcels ?? static::safeMeta($vehicle, 'max_parcels', 100)),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
44
143
|
/**
|
|
45
144
|
* Build the normalized job list from a collection of Orders.
|
|
46
145
|
*
|
|
@@ -51,7 +150,7 @@ class OrchestrationPayloadBuilder
|
|
|
51
150
|
* - service: service time in seconds (waypoint.service_time → order meta → default 300)
|
|
52
151
|
* - time_windows: [[earliest_unix, latest_unix]] from order.time_window_start/end or scheduled_at
|
|
53
152
|
* - skills: integer skill codes from order.required_skills or custom fields
|
|
54
|
-
* - amount: multi-dimensional capacity demand [weight_kg,
|
|
153
|
+
* - amount: multi-dimensional capacity demand [weight_kg, volume_litres, pallets, parcels]
|
|
55
154
|
* - priority: orchestrator_priority (0–100, higher = more important)
|
|
56
155
|
* - description: human-readable label for debugging
|
|
57
156
|
*/
|
|
@@ -87,14 +186,9 @@ class OrchestrationPayloadBuilder
|
|
|
87
186
|
?? 300
|
|
88
187
|
);
|
|
89
188
|
|
|
90
|
-
// --- Capacity demand
|
|
91
|
-
//
|
|
92
|
-
$job['amount'] =
|
|
93
|
-
(int) round((float) ($payload?->capacity_weight_kg ?? $order->getMeta('weight_kg') ?? 0)),
|
|
94
|
-
(int) round((float) ($payload?->capacity_volume_m3 ?? $order->getMeta('volume_m3') ?? 0) * 1000), // store as litres for integer
|
|
95
|
-
(int) ($payload?->capacity_pallets ?? $order->getMeta('pallets') ?? 0),
|
|
96
|
-
(int) ($payload?->capacity_parcels ?? $order->getMeta('parcels') ?? 1),
|
|
97
|
-
];
|
|
189
|
+
// --- Capacity demand ---
|
|
190
|
+
// Aggregated dynamically from payload entities; falls back to order meta.
|
|
191
|
+
$job['amount'] = static::computePayloadDemand($order);
|
|
98
192
|
|
|
99
193
|
// --- Time windows ---
|
|
100
194
|
// Prefer explicit orchestrator time_window columns, fall back to scheduled_at
|
|
@@ -148,12 +242,7 @@ class OrchestrationPayloadBuilder
|
|
|
148
242
|
if ($vehicle->return_to_depot && $vehicle->location) {
|
|
149
243
|
$entry['end'] = [$vehicle->location->getLng(), $vehicle->location->getLat()];
|
|
150
244
|
}
|
|
151
|
-
$entry['capacity'] =
|
|
152
|
-
(int) round((float) ($vehicle->capacity_weight_kg ?? static::safeMeta($vehicle, 'max_weight_kg', 0))),
|
|
153
|
-
(int) round((float) ($vehicle->capacity_volume_m3 ?? static::safeMeta($vehicle, 'max_volume_m3', 0)) * 1000),
|
|
154
|
-
(int) ($vehicle->capacity_pallets ?? static::safeMeta($vehicle, 'max_pallets', 0)),
|
|
155
|
-
(int) ($vehicle->capacity_parcels ?? static::safeMeta($vehicle, 'max_parcels', 100)),
|
|
156
|
-
];
|
|
245
|
+
$entry['capacity'] = static::buildVehicleCapacity($vehicle);
|
|
157
246
|
if ($vehicle->max_tasks !== null && $vehicle->max_tasks > 0) {
|
|
158
247
|
$entry['max_tasks'] = (int) $vehicle->max_tasks;
|
|
159
248
|
}
|
|
@@ -172,6 +261,13 @@ class OrchestrationPayloadBuilder
|
|
|
172
261
|
})->filter()->values()->toArray();
|
|
173
262
|
}
|
|
174
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Build vehicles for driver+vehicle allocation mode.
|
|
266
|
+
*
|
|
267
|
+
* Uses driver.location as the start position, falling back to vehicle.location.
|
|
268
|
+
* Capacity is read from the vehicle. Time windows prefer the driver's explicit
|
|
269
|
+
* window, then the driver's active shift, then the vehicle's window.
|
|
270
|
+
*/
|
|
175
271
|
public static function buildVehicles(Collection $vehicles): array
|
|
176
272
|
{
|
|
177
273
|
return $vehicles->map(function (Vehicle $vehicle) {
|
|
@@ -193,13 +289,7 @@ class OrchestrationPayloadBuilder
|
|
|
193
289
|
}
|
|
194
290
|
|
|
195
291
|
// --- Multi-dimensional capacity ---
|
|
196
|
-
|
|
197
|
-
$entry['capacity'] = [
|
|
198
|
-
(int) round((float) ($vehicle->capacity_weight_kg ?? static::safeMeta($vehicle, 'max_weight_kg', 0))),
|
|
199
|
-
(int) round((float) ($vehicle->capacity_volume_m3 ?? static::safeMeta($vehicle, 'max_volume_m3', 0)) * 1000),
|
|
200
|
-
(int) ($vehicle->capacity_pallets ?? static::safeMeta($vehicle, 'max_pallets', 0)),
|
|
201
|
-
(int) ($vehicle->capacity_parcels ?? static::safeMeta($vehicle, 'max_parcels', 100)),
|
|
202
|
-
];
|
|
292
|
+
$entry['capacity'] = static::buildVehicleCapacity($vehicle);
|
|
203
293
|
|
|
204
294
|
// --- Max tasks ---
|
|
205
295
|
if ($vehicle->max_tasks !== null && $vehicle->max_tasks > 0) {
|