@fleetbase/fleetops-engine 0.6.52 → 0.6.54

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.
Files changed (151) hide show
  1. package/addon/components/device/details.hbs +106 -135
  2. package/addon/components/device/details.js +174 -1
  3. package/addon/components/device/panel-header.hbs +2 -2
  4. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.hbs +201 -0
  5. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +654 -0
  6. package/addon/components/layout/fleet-ops-sidebar.hbs +19 -233
  7. package/addon/components/layout/fleet-ops-sidebar.js +364 -395
  8. package/addon/components/maintenance-schedule/form.js +1 -0
  9. package/addon/components/order/form/details.hbs +1 -43
  10. package/addon/components/order/form/details.js +18 -53
  11. package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
  12. package/addon/components/order/form/orchestrator-constraints.js +61 -0
  13. package/addon/components/order/form/payload.js +50 -2
  14. package/addon/components/order/form/route.js +15 -0
  15. package/addon/components/order/form/service-rate.hbs +62 -11
  16. package/addon/components/order/form/service-rate.js +124 -2
  17. package/addon/components/order/form.hbs +2 -0
  18. package/addon/components/telematic/details.hbs +7 -1
  19. package/addon/components/telematic/form.js +16 -8
  20. package/addon/components/work-order/details.hbs +5 -1
  21. package/addon/components/work-order/form.hbs +26 -4
  22. package/addon/components/work-order/form.js +5 -1
  23. package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
  24. package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
  25. package/addon/controllers/connectivity/devices/index/details/vehicle.js +74 -0
  26. package/addon/controllers/connectivity/devices/index/details.js +12 -0
  27. package/addon/controllers/connectivity/devices/index.js +4 -1
  28. package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
  29. package/addon/controllers/connectivity/telematics/details/devices.js +90 -41
  30. package/addon/controllers/connectivity/telematics/details/events.js +39 -11
  31. package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
  32. package/addon/controllers/maintenance/index.js +100 -0
  33. package/addon/controllers/maintenance/work-orders/index.js +18 -1
  34. package/addon/controllers/management/fuel-transactions/index.js +1 -1
  35. package/addon/controllers/management/index.js +88 -0
  36. package/addon/controllers/settings/index.js +142 -0
  37. package/addon/extension.js +21 -0
  38. package/addon/routes/analytics/index.js +3 -0
  39. package/addon/routes/connectivity/devices/index/details/events.js +37 -1
  40. package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
  41. package/addon/routes/connectivity/devices/index/details/vehicle.js +7 -0
  42. package/addon/routes/connectivity/devices/index.js +3 -0
  43. package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
  44. package/addon/routes/connectivity/telematics/details/devices.js +4 -0
  45. package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
  46. package/addon/routes/connectivity/telematics/details.js +1 -1
  47. package/addon/routes/connectivity/telematics/edit.js +1 -1
  48. package/addon/routes/maintenance/index.js +3 -0
  49. package/addon/routes/maintenance/work-orders/index.js +3 -0
  50. package/addon/routes/settings/index.js +3 -0
  51. package/addon/routes.js +2 -0
  52. package/addon/services/order-creation.js +54 -1
  53. package/addon/services/order-validation.js +3 -2
  54. package/addon/services/service-rate-actions.js +1 -0
  55. package/addon/styles/fleetops-engine.css +1821 -6
  56. package/addon/templates/analytics/index.hbs +14 -0
  57. package/addon/templates/analytics/reports/index.hbs +4 -4
  58. package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
  59. package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
  60. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +84 -0
  61. package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
  62. package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
  63. package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
  64. package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
  65. package/addon/templates/maintenance/index.hbs +113 -0
  66. package/addon/templates/management/fuel-transactions/index/details.hbs +1 -1
  67. package/addon/templates/management/index.hbs +109 -73
  68. package/addon/templates/management/vehicles/index/edit.hbs +1 -1
  69. package/addon/templates/operations/orders/index.hbs +1 -1
  70. package/addon/templates/operations/service-rates/index.hbs +1 -1
  71. package/addon/templates/settings/index.hbs +83 -0
  72. package/addon/templates/settings/routing.hbs +2 -2
  73. package/addon/utils/fleet-ops-options.js +38 -0
  74. package/app/components/order/form/orchestrator-constraints.js +1 -0
  75. package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
  76. package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
  77. package/app/controllers/maintenance/index.js +1 -0
  78. package/app/controllers/management/index.js +1 -0
  79. package/app/controllers/settings/index.js +1 -0
  80. package/app/routes/analytics/index.js +1 -0
  81. package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
  82. package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
  83. package/app/routes/maintenance/index.js +1 -0
  84. package/app/routes/settings/index.js +1 -0
  85. package/app/templates/analytics/index.js +1 -0
  86. package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
  87. package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
  88. package/app/templates/maintenance/index.js +1 -0
  89. package/app/templates/management/index.js +1 -0
  90. package/app/templates/settings/index.js +1 -0
  91. package/composer.json +1 -1
  92. package/extension.json +1 -1
  93. package/package.json +3 -3
  94. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
  95. package/server/migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php +30 -0
  96. package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
  97. package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
  98. package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -0
  99. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
  100. package/server/src/Console/Commands/SyncTelematics.php +89 -0
  101. package/server/src/Exceptions/TelematicProviderException.php +13 -2
  102. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
  103. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +112 -11
  104. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +16 -5
  105. package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
  106. package/server/src/Http/Controllers/Internal/v1/HubController.php +300 -0
  107. package/server/src/Http/Controllers/Internal/v1/LiveController.php +177 -0
  108. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
  109. package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
  110. package/server/src/Http/Controllers/Internal/v1/SearchController.php +506 -0
  111. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
  112. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -1
  113. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +94 -10
  114. package/server/src/Http/Filter/DeviceEventFilter.php +44 -0
  115. package/server/src/Http/Filter/DeviceFilter.php +131 -0
  116. package/server/src/Http/Filter/DriverFilter.php +6 -0
  117. package/server/src/Http/Filter/SensorFilter.php +125 -0
  118. package/server/src/Http/Filter/VehicleFilter.php +19 -0
  119. package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
  120. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
  121. package/server/src/Http/Resources/v1/Driver.php +11 -2
  122. package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
  123. package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
  124. package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
  125. package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
  126. package/server/src/Jobs/SyncTelematicDevicesJob.php +181 -66
  127. package/server/src/Models/Device.php +15 -1
  128. package/server/src/Models/DeviceEvent.php +1 -1
  129. package/server/src/Models/Driver.php +17 -1
  130. package/server/src/Models/Sensor.php +15 -2
  131. package/server/src/Models/Telematic.php +1 -1
  132. package/server/src/Models/Vehicle.php +5 -1
  133. package/server/src/Models/WorkOrder.php +5 -2
  134. package/server/src/Observers/DriverObserver.php +3 -3
  135. package/server/src/Observers/FleetObserver.php +23 -0
  136. package/server/src/Observers/VehicleObserver.php +3 -3
  137. package/server/src/Providers/FleetOpsServiceProvider.php +12 -0
  138. package/server/src/Support/LiveCacheService.php +1 -1
  139. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +188 -28
  140. package/server/src/Support/Telematics/Providers/SafeeProvider.php +44 -3
  141. package/server/src/Support/Telematics/TelematicService.php +330 -30
  142. package/server/src/routes.php +10 -0
  143. package/server/tests/DeviceFilterTest.php +36 -0
  144. package/server/tests/HubRoutesTest.php +62 -0
  145. package/server/tests/OperationsMonitorEndpointTest.php +38 -0
  146. package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
  147. package/server/tests/TelematicsHardeningTest.php +455 -3
  148. package/server/tests/VehicleDriverAssignmentTest.php +27 -0
  149. package/translations/ar-ae.yml +1 -1
  150. package/translations/en-us.yaml +1 -0
  151. package/translations/vi-vn.yaml +1 -1
@@ -37,6 +37,7 @@ const MODEL_TO_TYPE = {
37
37
  const ASSIGNEE_MODEL_TO_TYPE = {
38
38
  'facilitator-vendor': 'fleet-ops:vendor',
39
39
  'facilitator-contact': 'fleet-ops:contact',
40
+ 'facilitator-driver': 'fleet-ops:driver',
40
41
  'facilitator-integrated-vendor': 'fleet-ops:vendor',
41
42
  vendor: 'fleet-ops:vendor',
42
43
  contact: 'fleet-ops:contact',
@@ -28,7 +28,7 @@
28
28
  @value={{@resource.scheduled_at}}
29
29
  @minDate={{format-date-fns (now) "yyyy-MM-dd"}}
30
30
  @minTime={{format-date-fns (now) "HH:mm"}}
31
- @onUpdate={{fn (mut @resource.scheduled_at)}}
31
+ @onUpdate={{this.setScheduledAt}}
32
32
  @disabled={{or @resource.dispatched (cannot-write @resource)}}
33
33
  />
34
34
  </InputGroup>
@@ -237,46 +237,4 @@
237
237
  </div>
238
238
  </ContentPanel>
239
239
 
240
- {{! ORCHESTRATOR CONSTRAINTS }}
241
- <ContentPanel @title="Orchestrator Constraints" @open={{true}} @wrapperClass="bordered-top">
242
- <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
243
- <div class="col-span-1 lg:col-span-2 text-[11px] uppercase tracking-wide text-gray-500 font-semibold mt-1">
244
- Delivery Window
245
- </div>
246
- <InputGroup
247
- @name="Time Window Start"
248
- @helpText="Earliest acceptable arrival time at the delivery location. The date defaults to the order's scheduled date (or creation date if not yet scheduled)."
249
- >
250
- <DateTimeInput class="form-input" @value={{@resource.time_window_start}} @onUpdate={{fn this.setTimeWindow "time_window_start"}} @disabled={{cannot-write @resource}} />
251
- </InputGroup>
252
- <InputGroup
253
- @name="Time Window End"
254
- @helpText="Latest acceptable arrival time at the delivery location. The date defaults to the order's scheduled date (or creation date if not yet scheduled)."
255
- >
256
- <DateTimeInput class="form-input" @value={{@resource.time_window_end}} @onUpdate={{fn this.setTimeWindow "time_window_end"}} @disabled={{cannot-write @resource}} />
257
- </InputGroup>
258
- <div class="col-span-1 lg:col-span-2 text-[11px] uppercase tracking-wide text-gray-500 font-semibold mt-3">
259
- Requirements & Priority
260
- </div>
261
- <div class="col-span-1 lg:col-span-2">
262
- <InputGroup @name="Required Skills" @helpText="Skills the assigned driver and vehicle must have to fulfil this order.">
263
- <MultiSelect
264
- @options={{get-fleet-ops-options "driverSkills"}}
265
- @selected={{@resource.required_skills}}
266
- @onChange={{fn (mut @resource.required_skills)}}
267
- @placeholder="Select required skills…"
268
- @triggerClass="form-select form-input"
269
- @disabled={{cannot-write @resource}}
270
- as |option|
271
- >
272
- {{option.label}}
273
- </MultiSelect>
274
- </InputGroup>
275
- </div>
276
- <InputGroup @name="Orchestrator Priority" @helpText="Priority score 0–100. Higher values are scheduled first when capacity is constrained. Default is 0.">
277
- <Input @value={{@resource.orchestrator_priority}} @type="number" class="form-input w-full" placeholder="0–100" disabled={{cannot-write @resource}} />
278
- </InputGroup>
279
- </div>
280
- </ContentPanel>
281
-
282
240
  <Order::Form::CustomFields @resource={{@resource}} @customFields={{this.customFields}} />
@@ -20,9 +20,14 @@ export default class OrderFormDetailsComponent extends Component {
20
20
  this.orderConfigActions.loadAll.perform();
21
21
  }
22
22
 
23
+ get integratedVendorServiceType() {
24
+ return this.args.resource?.type;
25
+ }
26
+
23
27
  @action selectFacilitator(model) {
24
28
  this.args.resource.set('facilitator', model);
25
29
  this.args.resource.set('driver', null);
30
+ this.requestServiceQuoteRefresh('details.facilitator.changed');
26
31
  }
27
32
 
28
33
  @action selectCustomer(model) {
@@ -39,6 +44,7 @@ export default class OrderFormDetailsComponent extends Component {
39
44
  type: orderConfig.key,
40
45
  });
41
46
  this.args.resource.payload.set('type', orderConfig.key);
47
+ this.requestServiceQuoteRefresh('details.order_config.changed');
42
48
 
43
49
  try {
44
50
  const customFieldsManager = yield this.customFieldsRegistry.loadSubjectCustomFields.perform(orderConfig);
@@ -70,61 +76,16 @@ export default class OrderFormDetailsComponent extends Component {
70
76
  this.leafletLayerVisibilityManager.showModelLayer(this.args.resource.driver_assigned);
71
77
  }
72
78
 
73
- /**
74
- * Resolves the reference date to use when pre-populating the date portion
75
- * of a time window field. Prefers scheduled_at, falls back to created_at,
76
- * and finally falls back to now so there is always a valid date.
77
- *
78
- * @returns {Date}
79
- */
80
- get _timeWindowReferenceDate() {
81
- const raw = this.args.resource.scheduled_at ?? this.args.resource.created_at ?? new Date();
82
- return raw instanceof Date ? raw : new Date(raw);
79
+ @action setScheduledAt(value) {
80
+ this.args.resource.scheduled_at = value;
81
+ this.requestServiceQuoteRefresh('details.scheduled_at.changed');
83
82
  }
84
83
 
85
- /**
86
- * Called by DateTimeInput @onUpdate for time_window_start and time_window_end.
87
- *
88
- * When the user picks a time, we preserve their chosen time but replace the
89
- * date portion with the order's scheduled_at date (or created_at if
90
- * scheduled_at is not set). This means the user only ever needs to set the
91
- * time — the date is always contextually correct.
92
- *
93
- * If the incoming value already carries a different date (e.g. the user
94
- * explicitly changed it via the date part of the picker) we respect that
95
- * and do not override it.
96
- *
97
- * @param {'time_window_start'|'time_window_end'} field
98
- * @param {Date|string|null} value Value emitted by DateTimeInput
99
- */
100
- @action setTimeWindow(field, value) {
101
- if (!value) {
102
- this.args.resource[field] = null;
103
- return;
104
- }
105
-
106
- const picked = value instanceof Date ? value : new Date(value);
107
- if (isNaN(picked.getTime())) {
108
- this.args.resource[field] = value;
109
- return;
110
- }
111
-
112
- const ref = this._timeWindowReferenceDate;
113
-
114
- // Only inject the reference date when the picked value has no meaningful
115
- // date of its own — i.e. when the date portion is the Unix epoch
116
- // (1970-01-01), which is what DateTimeInput emits when the user has only
117
- // touched the time picker and not the date picker.
118
- const isEpochDate = picked.getUTCFullYear() === 1970 && picked.getUTCMonth() === 0 && picked.getUTCDate() === 1;
119
-
120
- if (isEpochDate) {
121
- const merged = new Date(ref);
122
- merged.setHours(picked.getHours(), picked.getMinutes(), picked.getSeconds(), 0);
123
- this.args.resource[field] = merged;
124
- } else {
125
- // User explicitly set a date — honour it as-is.
126
- this.args.resource[field] = picked;
127
- }
84
+ @action selectIntegratedServiceType(serviceType) {
85
+ const type = serviceType?.key ?? serviceType?.value ?? serviceType;
86
+ this.args.resource.type = type;
87
+ this.args.resource.payload.set('type', type);
88
+ this.requestServiceQuoteRefresh('details.integrated_service_type.changed');
128
89
  }
129
90
 
130
91
  @action toggleAdhoc(toggled) {
@@ -136,4 +97,8 @@ export default class OrderFormDetailsComponent extends Component {
136
97
  this.args.resource.pod_required = toggled;
137
98
  this.args.resource.pod_method = toggled ? 'scan' : null;
138
99
  }
100
+
101
+ requestServiceQuoteRefresh(reason) {
102
+ this.orderCreation.requestServiceQuoteRefresh(reason, this.args.resource);
103
+ }
139
104
  }
@@ -0,0 +1,40 @@
1
+ <ContentPanel @title="Orchestrator Constraints" @open={{true}} @wrapperClass="bordered-top">
2
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
3
+ <div class="col-span-1 lg:col-span-2 text-[11px] uppercase tracking-wide text-gray-500 font-semibold mt-1">
4
+ Delivery Window
5
+ </div>
6
+ <InputGroup
7
+ @name="Time Window Start"
8
+ @helpText="Earliest acceptable arrival time at the delivery location. The date defaults to the order's scheduled date (or creation date if not yet scheduled)."
9
+ >
10
+ <DateTimeInput class="form-input" @value={{@resource.time_window_start}} @onUpdate={{fn this.setTimeWindow "time_window_start"}} @disabled={{cannot-write @resource}} />
11
+ </InputGroup>
12
+ <InputGroup
13
+ @name="Time Window End"
14
+ @helpText="Latest acceptable arrival time at the delivery location. The date defaults to the order's scheduled date (or creation date if not yet scheduled)."
15
+ >
16
+ <DateTimeInput class="form-input" @value={{@resource.time_window_end}} @onUpdate={{fn this.setTimeWindow "time_window_end"}} @disabled={{cannot-write @resource}} />
17
+ </InputGroup>
18
+ <div class="col-span-1 lg:col-span-2 text-[11px] uppercase tracking-wide text-gray-500 font-semibold mt-3">
19
+ Requirements & Priority
20
+ </div>
21
+ <div class="col-span-1 lg:col-span-2">
22
+ <InputGroup @name="Required Skills" @helpText="Skills the assigned driver and vehicle must have to fulfil this order.">
23
+ <MultiSelect
24
+ @options={{get-fleet-ops-options "driverSkills"}}
25
+ @selected={{@resource.required_skills}}
26
+ @onChange={{fn (mut @resource.required_skills)}}
27
+ @placeholder="Select required skills..."
28
+ @triggerClass="form-select form-input"
29
+ @disabled={{cannot-write @resource}}
30
+ as |option|
31
+ >
32
+ {{option.label}}
33
+ </MultiSelect>
34
+ </InputGroup>
35
+ </div>
36
+ <InputGroup @name="Orchestrator Priority" @helpText="Priority score 0-100. Higher values are scheduled first when capacity is constrained. Default is 0.">
37
+ <Input @value={{@resource.orchestrator_priority}} @type="number" class="form-input w-full" placeholder="0-100" disabled={{cannot-write @resource}} />
38
+ </InputGroup>
39
+ </div>
40
+ </ContentPanel>
@@ -0,0 +1,61 @@
1
+ import Component from '@glimmer/component';
2
+ import { action } from '@ember/object';
3
+
4
+ export default class OrderFormOrchestratorConstraintsComponent extends Component {
5
+ /**
6
+ * Resolves the reference date to use when pre-populating the date portion
7
+ * of a time window field. Prefers scheduled_at, falls back to created_at,
8
+ * and finally falls back to now so there is always a valid date.
9
+ *
10
+ * @returns {Date}
11
+ */
12
+ get _timeWindowReferenceDate() {
13
+ const raw = this.args.resource.scheduled_at ?? this.args.resource.created_at ?? new Date();
14
+ return raw instanceof Date ? raw : new Date(raw);
15
+ }
16
+
17
+ /**
18
+ * Called by DateTimeInput @onUpdate for time_window_start and time_window_end.
19
+ *
20
+ * When the user picks a time, we preserve their chosen time but replace the
21
+ * date portion with the order's scheduled_at date (or created_at if
22
+ * scheduled_at is not set). This means the user only ever needs to set the
23
+ * time, the date is always contextually correct.
24
+ *
25
+ * If the incoming value already carries a different date (e.g. the user
26
+ * explicitly changed it via the date part of the picker) we respect that
27
+ * and do not override it.
28
+ *
29
+ * @param {'time_window_start'|'time_window_end'} field
30
+ * @param {Date|string|null} value Value emitted by DateTimeInput
31
+ */
32
+ @action setTimeWindow(field, value) {
33
+ if (!value) {
34
+ this.args.resource[field] = null;
35
+ return;
36
+ }
37
+
38
+ const picked = value instanceof Date ? value : new Date(value);
39
+ if (isNaN(picked.getTime())) {
40
+ this.args.resource[field] = value;
41
+ return;
42
+ }
43
+
44
+ const ref = this._timeWindowReferenceDate;
45
+
46
+ // Only inject the reference date when the picked value has no meaningful
47
+ // date of its own, i.e. when the date portion is the Unix epoch
48
+ // (1970-01-01), which is what DateTimeInput emits when the user has only
49
+ // touched the time picker and not the date picker.
50
+ const isEpochDate = picked.getUTCFullYear() === 1970 && picked.getUTCMonth() === 0 && picked.getUTCDate() === 1;
51
+
52
+ if (isEpochDate) {
53
+ const merged = new Date(ref);
54
+ merged.setHours(picked.getHours(), picked.getMinutes(), picked.getSeconds(), 0);
55
+ this.args.resource[field] = merged;
56
+ } else {
57
+ // User explicitly set a date, honour it as-is.
58
+ this.args.resource[field] = picked;
59
+ }
60
+ }
61
+ }
@@ -7,6 +7,7 @@ export default class OrderFormPayloadComponent extends Component {
7
7
  @service store;
8
8
  @service fetch;
9
9
  @service entityActions;
10
+ @service orderCreation;
10
11
 
11
12
  get entitiesByImportId() {
12
13
  const groups = [];
@@ -32,6 +33,7 @@ export default class OrderFormPayloadComponent extends Component {
32
33
  const { value } = target;
33
34
 
34
35
  this.args.resource.payload.entities[index].destination_uuid = value;
36
+ this.requestServiceQuoteRefresh('entity.destination.changed');
35
37
  }
36
38
 
37
39
  @action addFromCustomEntity(customEntity) {
@@ -41,11 +43,15 @@ export default class OrderFormPayloadComponent extends Component {
41
43
  });
42
44
 
43
45
  this.args.resource.payload.entities.pushObject(entity);
46
+ this.requestServiceQuoteRefresh('entity.added');
44
47
  }
45
48
 
46
49
  @action addEntities(entities = []) {
47
50
  if (isArray(entities)) {
48
51
  this.args.resource.payload.entities.pushObjects(entities);
52
+ if (entities.length) {
53
+ this.requestServiceQuoteRefresh('entity.batch_added');
54
+ }
49
55
  }
50
56
  }
51
57
 
@@ -56,19 +62,61 @@ export default class OrderFormPayloadComponent extends Component {
56
62
  });
57
63
 
58
64
  this.args.resource.payload.entities.pushObject(entity);
65
+ this.requestServiceQuoteRefresh('entity.added');
59
66
  }
60
67
 
61
68
  @action removeEntity(entity) {
62
69
  if (this.args.resource.payload.entities.length === 1) return;
63
70
 
64
71
  if (!entity.isNew) {
65
- return entity.destroyRecord();
72
+ return entity.destroyRecord().then(() => {
73
+ this.requestServiceQuoteRefresh('entity.removed');
74
+ });
66
75
  }
67
76
 
68
77
  this.args.resource.payload.entities.removeObject(entity);
78
+ this.requestServiceQuoteRefresh('entity.removed');
69
79
  }
70
80
 
71
81
  @action editEntity(entity) {
72
- this.entityActions.modal.edit(entity);
82
+ this.entityActions.modal.edit(entity, {
83
+ confirm: (modal) => {
84
+ modal.done();
85
+ this.requestServiceQuoteRefresh('entity.edited');
86
+ },
87
+ });
88
+ }
89
+
90
+ @action setEntityQuoteField(entity, field, value) {
91
+ this.setEntityValue(entity, field, value);
92
+ this.requestServiceQuoteRefresh(`entity.${field}.changed`);
93
+ }
94
+
95
+ @action setEntityCurrency(entity, value) {
96
+ this.setEntityValue(entity, 'currency', value);
97
+ this.requestServiceQuoteRefresh('entity.currency.changed');
98
+ }
99
+
100
+ @action setEntityDimensionsUnit(entity, value) {
101
+ this.setEntityValue(entity, 'dimensions_unit', value);
102
+ this.requestServiceQuoteRefresh('entity.dimensions_unit.changed');
103
+ }
104
+
105
+ @action setEntityWeightUnit(entity, value) {
106
+ this.setEntityValue(entity, 'weight_unit', value);
107
+ this.requestServiceQuoteRefresh('entity.weight_unit.changed');
108
+ }
109
+
110
+ setEntityValue(entity, field, value) {
111
+ if (typeof entity?.set === 'function') {
112
+ entity.set(field, value);
113
+ return;
114
+ }
115
+
116
+ entity[field] = value;
117
+ }
118
+
119
+ requestServiceQuoteRefresh(reason) {
120
+ this.orderCreation.requestServiceQuoteRefresh(reason, this.args.resource);
73
121
  }
74
122
  }
@@ -20,6 +20,7 @@ export default class OrderFormRouteComponent extends Component {
20
20
  @service currentUser;
21
21
  @service notifications;
22
22
  @service placeActions;
23
+ @service orderCreation;
23
24
  @tracked multipleWaypoints = false;
24
25
  @tracked routingControl;
25
26
  @tracked route;
@@ -119,6 +120,8 @@ export default class OrderFormRouteComponent extends Component {
119
120
 
120
121
  this.clearWaypoints();
121
122
  }
123
+
124
+ this.requestServiceQuoteRefresh('route.waypoints.toggled');
122
125
  }
123
126
 
124
127
  @action sortWaypoints({ sourceList, sourceIndex, targetList, targetIndex }) {
@@ -132,6 +135,7 @@ export default class OrderFormRouteComponent extends Component {
132
135
  targetList.insertAt(targetIndex, item);
133
136
 
134
137
  this.previewRoute();
138
+ this.requestServiceQuoteRefresh('route.waypoints.reordered');
135
139
  }
136
140
 
137
141
  @action addWaypoint(properties = {}) {
@@ -143,6 +147,7 @@ export default class OrderFormRouteComponent extends Component {
143
147
  this.args.resource.payload.waypoints.pushObject(waypoint);
144
148
 
145
149
  this.previewRoute();
150
+ this.requestServiceQuoteRefresh('route.waypoint.added');
146
151
  }
147
152
 
148
153
  @action setWaypointPlace(index, place) {
@@ -159,6 +164,7 @@ export default class OrderFormRouteComponent extends Component {
159
164
  location: place.location,
160
165
  });
161
166
  this.previewRoute();
167
+ this.requestServiceQuoteRefresh('route.waypoint.place.changed');
162
168
  }
163
169
 
164
170
  @action setWaypointCustomer(waypoint, model) {
@@ -170,16 +176,19 @@ export default class OrderFormRouteComponent extends Component {
170
176
  if (this.multipleWaypoints && this.args.resource.payload.waypoints.length === 1) return;
171
177
  this.args.resource.payload.waypoints.removeObject(waypoint);
172
178
  this.previewRoute();
179
+ this.requestServiceQuoteRefresh('route.waypoint.removed');
173
180
  }
174
181
 
175
182
  @action clearWaypoints() {
176
183
  this.args.resource.payload.waypoints.clear();
177
184
  this.previewRoute();
185
+ this.requestServiceQuoteRefresh('route.waypoints.cleared');
178
186
  }
179
187
 
180
188
  @action setPayloadPlace(prop, place) {
181
189
  this.args.resource.payload[prop] = place;
182
190
  this.previewRoute();
191
+ this.requestServiceQuoteRefresh(`route.${prop}.changed`);
183
192
  }
184
193
 
185
194
  @action editPlace(place) {
@@ -291,6 +300,7 @@ export default class OrderFormRouteComponent extends Component {
291
300
  }
292
301
  this.previewRoute();
293
302
  this.args.resource.set('optimized', true);
303
+ this.requestServiceQuoteRefresh('route.optimized');
294
304
  }
295
305
 
296
306
  @action setOptimizedRoute(route, trip, waypoints, engine = 'osrm') {
@@ -314,5 +324,10 @@ export default class OrderFormRouteComponent extends Component {
314
324
  const routeModel = this.store.createRecord('route', payload);
315
325
  this.args.resource.route = routeModel;
316
326
  this.route = payload;
327
+ this.requestServiceQuoteRefresh('route.changed');
328
+ }
329
+
330
+ requestServiceQuoteRefresh(reason) {
331
+ this.orderCreation.requestServiceQuoteRefresh(reason, this.args.resource);
317
332
  }
318
333
  }
@@ -1,14 +1,55 @@
1
1
  <ContentPanel @title={{t "order.fields.service"}} @open={{true}} @wrapperClass="bordered-top">
2
2
  <div>
3
- <Toggle
4
- @label={{t "order.fields.apply-service-rate"}}
5
- @wrapperClass="justify-start-i mb-4"
6
- @isToggled={{@resource.servicable}}
7
- @onToggle={{perform this.queryServiceRates}}
8
- @disabled={{or (cannot-write @resource) (not this.isServicable)}}
9
- @helpText={{t "order.fields.service-help-text"}}
10
- />
11
- {{#if @resource.servicable}}
3
+ {{#if this.hasServiceQuoteOverride}}
4
+ <div class="rounded-md border border-blue-200 bg-blue-50 px-3 py-3 text-blue-800 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-100">
5
+ <div class="flex items-start justify-between gap-3">
6
+ <div>
7
+ <div class="text-sm font-semibold">{{this.serviceQuoteOverride.title}}</div>
8
+ {{#if this.serviceQuoteOverride.description}}
9
+ <div class="mt-1 text-xs opacity-80">{{this.serviceQuoteOverride.description}}</div>
10
+ {{/if}}
11
+ </div>
12
+ <Badge @hideStatusDot={{true}} @status="info">Locked</Badge>
13
+ </div>
14
+
15
+ {{#if this.serviceQuoteOverride.isLoading}}
16
+ <div class="flex items-center justify-center py-5">
17
+ <Spinner class="text-sm flex flex-row items-center" @iconClass="mr-2" @loadingMessage="Generating contract quote..." />
18
+ </div>
19
+ {{else if this.overrideQuote}}
20
+ <div class="mt-3 rounded-md border border-blue-300 bg-blue-600/20 text-blue-50 dark:border-blue-600 dark:bg-blue-950/30">
21
+ <div class="border-b border-blue-300 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-blue-100/80 dark:border-blue-700 dark:text-blue-200/80">
22
+ {{t "order.fields.breakdown"}}
23
+ </div>
24
+ <div class="divide-y divide-blue-300/70 dark:divide-blue-700">
25
+ {{#each this.overrideQuoteItems as |item|}}
26
+ <div class="flex flex-col gap-1 px-3 py-2 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
27
+ <span class="min-w-0 whitespace-normal break-words text-sm font-semibold text-blue-50 dark:text-blue-100 w-3/4">{{item.details}}</span>
28
+ <span class="shrink-0 text-right text-sm font-medium text-blue-50 dark:text-blue-100">
29
+ {{format-currency item.amount this.overrideCurrency}}
30
+ </span>
31
+ </div>
32
+ {{/each}}
33
+ <div class="flex flex-col gap-1 px-3 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
34
+ <span class="font-semibold text-blue-50 dark:text-blue-100">{{t "common.total"}}</span>
35
+ <span class="shrink-0 text-right font-semibold text-blue-50 dark:text-blue-100">
36
+ {{format-currency this.overrideQuote.amount this.overrideCurrency}}
37
+ </span>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ {{/if}}
42
+ </div>
43
+ {{else}}
44
+ <Toggle
45
+ @label={{t "order.fields.apply-service-rate"}}
46
+ @wrapperClass="justify-start-i mb-4"
47
+ @isToggled={{@resource.servicable}}
48
+ @onToggle={{perform this.queryServiceRates}}
49
+ @disabled={{or (cannot-write @resource) (not this.isServicable)}}
50
+ @helpText={{t "order.fields.service-help-text"}}
51
+ />
52
+ {{#if @resource.servicable}}
12
53
  <div>
13
54
  <div class="space-y-2">
14
55
  {{#unless @resource.facilitator.isIntegratedVendor}}
@@ -36,18 +77,27 @@
36
77
  <Button
37
78
  @icon="refresh"
38
79
  @text={{t "order.fields.refresh-button"}}
39
- @isLoading={{this.getServiceQuotes.isRunning}}
80
+ @isLoading={{this.isLoadingServiceQuotes}}
40
81
  @disabled={{not this.selectedRate}}
41
82
  @onClick={{perform this.getServiceQuotes this.selectedRate}}
42
83
  />
43
84
  </div>
44
85
  </div>
45
- {{#if this.getServiceQuotes.isRunning}}
86
+ {{#if this.shouldShowServiceQuotesLoader}}
46
87
  <div class="flex items-center justify-center py-5">
47
88
  <Spinner class="text-sm dark:text-gray-100 flex flex-row items-center" @iconClass="mr-2" @loadingMessage={{t "order.fields.loading-message"}} />
48
89
  </div>
49
90
  {{else}}
50
91
  <InfoBlock class="my-3" @text={{t "order.fields.service-quote-info"}} />
92
+ {{#if (and this.isAutoRefreshingServiceQuotes this.hasServiceQuotes)}}
93
+ <div class="my-3 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-blue-700 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-100">
94
+ <Spinner
95
+ class="text-sm flex flex-row items-center"
96
+ @iconClass="mr-2"
97
+ @loadingMessage={{t "order.fields.updating-service-quotes-message"}}
98
+ />
99
+ </div>
100
+ {{/if}}
51
101
  <div class="radio-group-condensed -space-y-px">
52
102
  {{#each this.serviceQuotes as |serviceQuote|}}
53
103
  <div class="radio-group-item shadow-sm flex-col pl-0i pr-0i {{if (eq @resource.service_quote_uuid serviceQuote.uuid) 'is-checked'}}">
@@ -106,6 +156,7 @@
106
156
  </div>
107
157
  {{/if}}
108
158
  </div>
159
+ {{/if}}
109
160
  {{/if}}
110
161
  </div>
111
162
  </ContentPanel>