@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.
- package/addon/components/device/details.hbs +106 -135
- package/addon/components/device/details.js +174 -1
- package/addon/components/device/panel-header.hbs +2 -2
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.hbs +201 -0
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +654 -0
- package/addon/components/layout/fleet-ops-sidebar.hbs +19 -233
- package/addon/components/layout/fleet-ops-sidebar.js +364 -395
- package/addon/components/maintenance-schedule/form.js +1 -0
- package/addon/components/order/form/details.hbs +1 -43
- package/addon/components/order/form/details.js +18 -53
- package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
- package/addon/components/order/form/orchestrator-constraints.js +61 -0
- package/addon/components/order/form/payload.js +50 -2
- package/addon/components/order/form/route.js +15 -0
- package/addon/components/order/form/service-rate.hbs +62 -11
- package/addon/components/order/form/service-rate.js +124 -2
- package/addon/components/order/form.hbs +2 -0
- package/addon/components/telematic/details.hbs +7 -1
- package/addon/components/telematic/form.js +16 -8
- package/addon/components/work-order/details.hbs +5 -1
- package/addon/components/work-order/form.hbs +26 -4
- package/addon/components/work-order/form.js +5 -1
- package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
- package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +74 -0
- package/addon/controllers/connectivity/devices/index/details.js +12 -0
- package/addon/controllers/connectivity/devices/index.js +4 -1
- package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
- package/addon/controllers/connectivity/telematics/details/devices.js +90 -41
- package/addon/controllers/connectivity/telematics/details/events.js +39 -11
- package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
- package/addon/controllers/maintenance/index.js +100 -0
- package/addon/controllers/maintenance/work-orders/index.js +18 -1
- package/addon/controllers/management/fuel-transactions/index.js +1 -1
- package/addon/controllers/management/index.js +88 -0
- package/addon/controllers/settings/index.js +142 -0
- package/addon/extension.js +21 -0
- package/addon/routes/analytics/index.js +3 -0
- package/addon/routes/connectivity/devices/index/details/events.js +37 -1
- package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
- package/addon/routes/connectivity/devices/index/details/vehicle.js +7 -0
- package/addon/routes/connectivity/devices/index.js +3 -0
- package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
- package/addon/routes/connectivity/telematics/details/devices.js +4 -0
- package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
- package/addon/routes/connectivity/telematics/details.js +1 -1
- package/addon/routes/connectivity/telematics/edit.js +1 -1
- package/addon/routes/maintenance/index.js +3 -0
- package/addon/routes/maintenance/work-orders/index.js +3 -0
- package/addon/routes/settings/index.js +3 -0
- package/addon/routes.js +2 -0
- package/addon/services/order-creation.js +54 -1
- package/addon/services/order-validation.js +3 -2
- package/addon/services/service-rate-actions.js +1 -0
- package/addon/styles/fleetops-engine.css +1821 -6
- package/addon/templates/analytics/index.hbs +14 -0
- package/addon/templates/analytics/reports/index.hbs +4 -4
- package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
- package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +84 -0
- package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
- package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
- package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
- package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
- package/addon/templates/maintenance/index.hbs +113 -0
- package/addon/templates/management/fuel-transactions/index/details.hbs +1 -1
- package/addon/templates/management/index.hbs +109 -73
- package/addon/templates/management/vehicles/index/edit.hbs +1 -1
- package/addon/templates/operations/orders/index.hbs +1 -1
- package/addon/templates/operations/service-rates/index.hbs +1 -1
- package/addon/templates/settings/index.hbs +83 -0
- package/addon/templates/settings/routing.hbs +2 -2
- package/addon/utils/fleet-ops-options.js +38 -0
- package/app/components/order/form/orchestrator-constraints.js +1 -0
- package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
- package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
- package/app/controllers/maintenance/index.js +1 -0
- package/app/controllers/management/index.js +1 -0
- package/app/controllers/settings/index.js +1 -0
- package/app/routes/analytics/index.js +1 -0
- package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
- package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
- package/app/routes/maintenance/index.js +1 -0
- package/app/routes/settings/index.js +1 -0
- package/app/templates/analytics/index.js +1 -0
- package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
- package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
- package/app/templates/maintenance/index.js +1 -0
- package/app/templates/management/index.js +1 -0
- package/app/templates/settings/index.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
- package/server/migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php +30 -0
- package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
- package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
- package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -0
- package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
- package/server/src/Console/Commands/SyncTelematics.php +89 -0
- package/server/src/Exceptions/TelematicProviderException.php +13 -2
- package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +112 -11
- package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +16 -5
- package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
- package/server/src/Http/Controllers/Internal/v1/HubController.php +300 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +177 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
- package/server/src/Http/Controllers/Internal/v1/SearchController.php +506 -0
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +94 -10
- package/server/src/Http/Filter/DeviceEventFilter.php +44 -0
- package/server/src/Http/Filter/DeviceFilter.php +131 -0
- package/server/src/Http/Filter/DriverFilter.php +6 -0
- package/server/src/Http/Filter/SensorFilter.php +125 -0
- package/server/src/Http/Filter/VehicleFilter.php +19 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
- package/server/src/Http/Resources/v1/Driver.php +11 -2
- package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
- package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
- package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
- package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +181 -66
- package/server/src/Models/Device.php +15 -1
- package/server/src/Models/DeviceEvent.php +1 -1
- package/server/src/Models/Driver.php +17 -1
- package/server/src/Models/Sensor.php +15 -2
- package/server/src/Models/Telematic.php +1 -1
- package/server/src/Models/Vehicle.php +5 -1
- package/server/src/Models/WorkOrder.php +5 -2
- package/server/src/Observers/DriverObserver.php +3 -3
- package/server/src/Observers/FleetObserver.php +23 -0
- package/server/src/Observers/VehicleObserver.php +3 -3
- package/server/src/Providers/FleetOpsServiceProvider.php +12 -0
- package/server/src/Support/LiveCacheService.php +1 -1
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +188 -28
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +44 -3
- package/server/src/Support/Telematics/TelematicService.php +330 -30
- package/server/src/routes.php +10 -0
- package/server/tests/DeviceFilterTest.php +36 -0
- package/server/tests/HubRoutesTest.php +62 -0
- package/server/tests/OperationsMonitorEndpointTest.php +38 -0
- package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
- package/server/tests/TelematicsHardeningTest.php +455 -3
- package/server/tests/VehicleDriverAssignmentTest.php +27 -0
- package/translations/ar-ae.yml +1 -1
- package/translations/en-us.yaml +1 -0
- 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={{
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
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.
|
|
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>
|