@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.
@@ -104,8 +104,9 @@
104
104
  }}
105
105
  {{else}}
106
106
  <Layout::Sidebar::Item
107
- @onClick={{menuItem.onClick}}
108
- @route={{concat this.routePrefix menuItem.route}}
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
- // intl: 'menu.tracking',
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
- console.log('[Orchestrator] constructor: location service initial coords =>', { lat, lng });
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
- console.log('[Orchestrator] getUserLocation resolved =>', { latitude, longitude }, '| _mapCenteredOnOrders =', this._mapCenteredOnOrders);
136
+ debug(`[Orchestrator] getUserLocation resolved => ${latitude}, ${longitude} | _mapCenteredOnOrders = ${this._mapCenteredOnOrders}`);
136
137
  if (!this._mapCenteredOnOrders) {
137
138
  // eslint-disable-next-line no-console
138
- console.log('[Orchestrator] applying geolocation as map center (no orders centered yet)');
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
- console.log('[Orchestrator] geolocation ignored — map already centered on orders');
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
+
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.39",
3
+ "version": "0.6.40",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
@@ -96,4 +96,4 @@
96
96
  "@test:unit"
97
97
  ]
98
98
  }
99
- }
99
+ }
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Fleet-Ops",
3
- "version": "0.6.39",
3
+ "version": "0.6.40",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/fleetops",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/fleetops-engine",
3
- "version": "0.6.39",
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.27",
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) {
@@ -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(['internal_id', 'payload', 'service_quote', 'purchase_rate', 'adhoc', 'adhoc_distance', 'pod_method', 'pod_required', 'scheduled_at', 'status', 'meta', 'notes']);
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(['internal_id', 'payload', 'adhoc', 'adhoc_distance', 'pod_method', 'pod_required', 'scheduled_at', 'meta', 'type', 'status', 'notes']);
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(['status', 'make', 'model', 'year', 'trim', 'type', 'plate_number', 'vin', 'meta', 'online', 'location', 'altitude', 'heading', 'speed']);
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(['status', 'make', 'model', 'year', 'trim', 'type', 'plate_number', 'vin', 'meta', 'location', 'online', 'altitude', 'heading', 'speed']);
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', 'capacity_weight_kg', 'capacity_volume_m3', 'capacity_pallets', 'capacity_parcels'];
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' => Json::class,
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
- 'capacity_weight_kg',
211
- 'capacity_volume_m3',
212
- 'capacity_pallets',
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' => Json::class,
311
- 'capacity_weight_kg' => 'decimal:2',
312
- 'capacity_volume_m3' => 'decimal:3',
313
- 'capacity_pallets' => 'integer',
314
- 'capacity_parcels' => 'integer',
315
- 'max_tasks' => 'integer',
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
- * The builder now reads first-class orchestrator columns (skills, capacity_*,
20
- * time_window_*, service_time, orchestrator_priority) added by the 2026-04-08
21
- * migrations, falling back to custom fields and meta for backwards compatibility.
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, volume_m3, pallets, parcels]
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 (multi-dimensional) ---
91
- // [weight_kg, volume_m3, pallets, parcels] must match vehicle capacity array
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
- // [weight_kg, volume_l (×1000 from m3), pallets, parcels]
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) {