@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
@@ -1,18 +1,72 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
- import { task } from 'ember-concurrency';
4
+ import { task, timeout } from 'ember-concurrency';
5
+ import { SERVICE_QUOTE_REFRESH_REQUESTED } from '../../../services/order-creation';
6
+
7
+ const SERVICE_QUOTE_REFRESH_DEBOUNCE_MS = 500;
5
8
 
6
9
  export default class OrderFormServiceRateComponent extends Component {
7
10
  @service serviceRateActions;
11
+ @service orderCreation;
8
12
  @tracked selectedRate;
9
13
  @tracked serviceRates = [];
10
14
  @tracked serviceQuotes = [];
15
+ @tracked isAutoRefreshingServiceQuotes = false;
16
+ serviceQuoteRefreshRequestId = 0;
17
+
18
+ constructor() {
19
+ super(...arguments);
20
+ this._serviceQuoteRefreshRequested = (event) => this.handleServiceQuoteRefreshRequest(event);
21
+ this.orderCreation.on(SERVICE_QUOTE_REFRESH_REQUESTED, this._serviceQuoteRefreshRequested);
22
+ }
23
+
24
+ willDestroy() {
25
+ this.orderCreation.off(SERVICE_QUOTE_REFRESH_REQUESTED, this._serviceQuoteRefreshRequested);
26
+ this.refreshServiceQuotes.cancelAll();
27
+ super.willDestroy(...arguments);
28
+ }
11
29
 
12
30
  get isServicable() {
13
31
  return this.args.resource?.order_config && this.args.resource?.payloadCoordinates?.length >= 2;
14
32
  }
15
33
 
34
+ get canRefreshServiceQuotes() {
35
+ return this.args.resource?.servicable && this.selectedRate && this.args.resource?.payloadCoordinates?.length >= 2;
36
+ }
37
+
38
+ get hasServiceQuotes() {
39
+ return this.serviceQuotes?.length > 0;
40
+ }
41
+
42
+ get isLoadingServiceQuotes() {
43
+ return this.getServiceQuotes.isRunning || this.isAutoRefreshingServiceQuotes;
44
+ }
45
+
46
+ get shouldShowServiceQuotesLoader() {
47
+ return this.isLoadingServiceQuotes && !this.hasServiceQuotes;
48
+ }
49
+
50
+ get serviceQuoteOverride() {
51
+ return this.orderCreation.getServiceQuoteOverride(this.args.resource);
52
+ }
53
+
54
+ get hasServiceQuoteOverride() {
55
+ return this.serviceQuoteOverride?.mode === 'locked';
56
+ }
57
+
58
+ get overrideQuote() {
59
+ return this.serviceQuoteOverride?.quote;
60
+ }
61
+
62
+ get overrideQuoteItems() {
63
+ return this.overrideQuote?.items ?? [];
64
+ }
65
+
66
+ get overrideCurrency() {
67
+ return this.overrideQuote?.currency ?? this.serviceQuoteOverride?.currency ?? 'USD';
68
+ }
69
+
16
70
  @task *queryServiceRates(toggled) {
17
71
  this.args.resource.servicable = toggled;
18
72
  if (!toggled) return;
@@ -21,6 +75,74 @@ export default class OrderFormServiceRateComponent extends Component {
21
75
 
22
76
  @task *getServiceQuotes(serviceRate) {
23
77
  this.selectedRate = serviceRate;
24
- this.serviceQuotes = yield this.serviceRateActions.getServiceQuotes.perform(serviceRate, this.args.resource);
78
+ yield this.loadServiceQuotes(serviceRate);
79
+ }
80
+
81
+ @task *refreshServiceQuotes(refreshRequestId) {
82
+ this.isAutoRefreshingServiceQuotes = true;
83
+
84
+ try {
85
+ yield timeout(SERVICE_QUOTE_REFRESH_DEBOUNCE_MS);
86
+
87
+ if (!this.canRefreshServiceQuotes) {
88
+ return;
89
+ }
90
+
91
+ yield this.loadServiceQuotes(this.selectedRate);
92
+ } finally {
93
+ if (refreshRequestId === this.serviceQuoteRefreshRequestId) {
94
+ this.isAutoRefreshingServiceQuotes = false;
95
+ }
96
+ }
97
+ }
98
+
99
+ handleServiceQuoteRefreshRequest({ order } = {}) {
100
+ if (order && order !== this.args.resource) {
101
+ return;
102
+ }
103
+
104
+ if (this.hasServiceQuoteOverride) {
105
+ return;
106
+ }
107
+
108
+ if (this.args.resource?.servicable && !this.serviceRates?.length && !this.queryServiceRates.isRunning) {
109
+ this.queryServiceRates.perform(true);
110
+ }
111
+
112
+ if (!this.canRefreshServiceQuotes) {
113
+ return;
114
+ }
115
+
116
+ this.refreshServiceQuotes.cancelAll();
117
+ this.serviceQuoteRefreshRequestId++;
118
+ this.isAutoRefreshingServiceQuotes = true;
119
+ this.refreshServiceQuotes.perform(this.serviceQuoteRefreshRequestId);
120
+ }
121
+
122
+ async loadServiceQuotes(serviceRate) {
123
+ const serviceQuotes = await this.serviceRateActions.getServiceQuotes.perform(serviceRate, this.args.resource);
124
+ this.serviceQuotes = serviceQuotes ?? [];
125
+ this.clearStaleSelectedServiceQuote();
126
+ }
127
+
128
+ clearStaleSelectedServiceQuote() {
129
+ const selectedServiceQuote = this.args.resource?.service_quote_uuid;
130
+
131
+ if (!selectedServiceQuote) {
132
+ return;
133
+ }
134
+
135
+ if (!this.serviceQuotes?.length) {
136
+ this.args.resource.service_quote_uuid = null;
137
+ return;
138
+ }
139
+
140
+ const selectedQuoteExists = this.serviceQuotes.some((serviceQuote) => {
141
+ return serviceQuote?.uuid === selectedServiceQuote || serviceQuote?.id === selectedServiceQuote;
142
+ });
143
+
144
+ if (!selectedQuoteExists) {
145
+ this.args.resource.service_quote_uuid = null;
146
+ }
25
147
  }
26
148
  }
@@ -10,6 +10,7 @@
10
10
  ServiceRate=(component "order/form/service-rate" resource=@resource)
11
11
  Notes=(component "order/form/notes" resource=@resource)
12
12
  Documents=(component "order/form/documents" resource=@resource)
13
+ OrchestratorConstraints=(component "order/form/orchestrator-constraints" resource=@resource)
13
14
  Metadata=(component "order/form/metadata" resource=@resource)
14
15
  )
15
16
  }}
@@ -23,6 +24,7 @@
23
24
  <Order::Form::ServiceRate @resource={{@resource}} />
24
25
  <Order::Form::Notes @resource={{@resource}} />
25
26
  <Order::Form::Documents @resource={{@resource}} />
27
+ <Order::Form::OrchestratorConstraints @resource={{@resource}} />
26
28
  <Order::Form::Metadata @resource={{@resource}} />
27
29
  {{/if}}
28
30
  </div>
@@ -32,7 +32,7 @@
32
32
  {{#if this.attentionItems.length}}
33
33
  <section class="flex flex-col gap-3">
34
34
  {{#each this.attentionItems as |item|}}
35
- <div class="w-full rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-700 dark:bg-gray-800">
35
+ <div class="w-full rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-700 dark:bg-yellow-900/20">
36
36
  <div class="flex items-start gap-3">
37
37
  <div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300">
38
38
  <FaIcon @icon={{item.icon}} />
@@ -88,6 +88,12 @@
88
88
  <div class="text-yellow-700 dark:text-yellow-300">Webhook URL unavailable until this integration has a public ID.</div>
89
89
  {{/if}}
90
90
  </div>
91
+ {{else}}
92
+ <div class="rounded-md border border-gray-200 p-3 dark:border-gray-700 md:col-span-2">
93
+ <div class="text-gray-400 dark:text-gray-500">Update Mode</div>
94
+ <div class="mt-1 font-semibold text-gray-900 dark:text-gray-100">Provider polling</div>
95
+ <div class="mt-1 text-gray-500 dark:text-gray-400">FleetOps polls this provider for device snapshots and telemetry updates.</div>
96
+ </div>
91
97
  {{/if}}
92
98
  </div>
93
99
  </div>
@@ -415,13 +415,17 @@ export default class TelematicFormComponent extends Component {
415
415
  this.args.resource.setProperties({
416
416
  name: this.args.resource.name ?? provider.label,
417
417
  provider: provider.key,
418
- credentials: (provider.required_fields ?? []).reduce((acc, item) => {
419
- acc[item.name] = item.advanced || item.is_endpoint ? null : (item.default_value ?? null);
420
- return acc;
421
- }, {}),
418
+ credentials: this.buildProviderCredentials(provider),
422
419
  });
423
420
  }
424
421
 
422
+ buildProviderCredentials(provider) {
423
+ return (provider.required_fields ?? []).reduce((acc, item) => {
424
+ acc[item.name] = item.default_value ?? null;
425
+ return acc;
426
+ }, {});
427
+ }
428
+
425
429
  @action goToStep(index) {
426
430
  if (!this.canReachStep(index)) {
427
431
  return;
@@ -553,10 +557,7 @@ export default class TelematicFormComponent extends Component {
553
557
  this.lastConnectionTestCompletedAt = null;
554
558
 
555
559
  try {
556
- const result = yield this.fetch.post(`telematics/${this.selectedProvider.key}/test-credentials`, {
557
- credentials: this.args.resource.credentials,
558
- telematic_id: this.args.resource?.id,
559
- });
560
+ const result = yield this.fetch.post(`telematics/${this.selectedProvider.key}/test-credentials`, this.getConnectionTestPayload());
560
561
  this.connectionTestResult = result;
561
562
  this.lastConnectionTestCompletedAt = new Date();
562
563
  this.updateResourceConnectionTestMeta(result);
@@ -577,6 +578,13 @@ export default class TelematicFormComponent extends Component {
577
578
  }
578
579
  }
579
580
 
581
+ getConnectionTestPayload() {
582
+ return {
583
+ credentials: this.args.resource.credentials,
584
+ telematic_id: this.args.resource?.id,
585
+ };
586
+ }
587
+
580
588
  updateResourceConnectionTestMeta(result) {
581
589
  const meta = this.args.resource?.meta ?? {};
582
590
  const testedAt = this.lastConnectionTestCompletedAt ?? new Date();
@@ -20,6 +20,10 @@
20
20
  <div class="field-name">Subject</div>
21
21
  <div class="field-value">{{n-a @resource.subject}}</div>
22
22
  </div>
23
+ <div class="field-info-container col-span-2">
24
+ <div class="field-name">Category</div>
25
+ <div class="field-value">{{n-a (smart-humanize @resource.category)}}</div>
26
+ </div>
23
27
 
24
28
  {{! — Status & Priority — }}
25
29
  <div class="col-span-2 text-[11px] uppercase tracking-wide text-gray-500 font-semibold mt-3">
@@ -185,4 +189,4 @@
185
189
  <CustomField::Yield @subject={{@resource}} @viewMode={{true}} @wrapperClass="bordered-top" />
186
190
 
187
191
  <Spacer @height="200px" />
188
- </div>
192
+ </div>
@@ -13,6 +13,27 @@
13
13
  <Input @value={{@resource.subject}} @type="text" class="w-full form-input" placeholder="Brief description of the work order" disabled={{cannot-write @resource}} />
14
14
  </InputGroup>
15
15
 
16
+ {{! Work Classification }}
17
+ <div class="col-span-1 lg:col-span-2 text-[11px] uppercase tracking-wide text-gray-500 font-semibold mt-3">
18
+ Work Order Classification
19
+ </div>
20
+ <InputGroup @name="Category" @wrapperClass="col-span-1 lg:col-span-2">
21
+ <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
22
+ <PowerSelect
23
+ @options={{this.categoryOptions}}
24
+ @selected={{find-by "value" @resource.category this.categoryOptions}}
25
+ @onChange={{set-model-attr @resource "category"}}
26
+ @placeholder="Select Category"
27
+ @triggerClass="form-select form-input"
28
+ @disabled={{cannot-write @resource}}
29
+ as |category|
30
+ >
31
+ <div class="font-semibold normalize-in-trigger">{{category.label}}</div>
32
+ <div class="hide-from-trigger">{{category.description}}</div>
33
+ </PowerSelect>
34
+ </div>
35
+ </InputGroup>
36
+
16
37
  {{! Status & Priority }}
17
38
  <div class="col-span-1 lg:col-span-2 text-[11px] uppercase tracking-wide text-gray-500 font-semibold mt-3">
18
39
  Status & Priority
@@ -21,14 +42,15 @@
21
42
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
22
43
  <PowerSelect
23
44
  @options={{this.statusOptions}}
24
- @selected={{@resource.status}}
25
- @onChange={{fn (mut @resource.status)}}
45
+ @selected={{find-by "value" @resource.status this.statusOptions}}
46
+ @onChange={{set-model-attr @resource "status"}}
26
47
  @placeholder="Select Status"
27
48
  @triggerClass="form-select form-input"
28
49
  @disabled={{cannot-write @resource}}
29
50
  as |status|
30
51
  >
31
- {{smart-humanize status}}
52
+ <div class="font-semibold normalize-in-trigger">{{status.label}}</div>
53
+ <div class="hide-from-trigger">{{status.description}}</div>
32
54
  </PowerSelect>
33
55
  </div>
34
56
  </InputGroup>
@@ -243,4 +265,4 @@
243
265
  <ContentPanel @title="Metadata" @open={{false}} @wrapperClass="bordered-top">
244
266
  <MetadataEditor @value={{@resource.meta}} @onChange={{fn (mut @resource.meta)}} />
245
267
  </ContentPanel>
246
- </div>
268
+ </div>
@@ -1,6 +1,7 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { action } from '@ember/object';
4
+ import { workOrderCategories, workOrderStatuses } from '../../utils/fleet-ops-options';
4
5
 
5
6
  /**
6
7
  * Maps a user-facing polymorphic type string to the Ember Data model name
@@ -42,7 +43,10 @@ const ASSIGNEE_MODEL_TO_TYPE = {
42
43
 
43
44
  export default class WorkOrderFormComponent extends Component {
44
45
  /** Status options for work orders. */
45
- statusOptions = ['open', 'in_progress', 'on_hold', 'completed', 'cancelled'];
46
+ statusOptions = workOrderStatuses;
47
+
48
+ /** Operational category options for work orders. */
49
+ categoryOptions = workOrderCategories;
46
50
 
47
51
  /** Priority options for work orders. */
48
52
  priorityOptions = ['low', 'medium', 'high', 'critical'];
@@ -1,3 +1,246 @@
1
1
  import Controller from '@ember/controller';
2
+ import { action } from '@ember/object';
3
+ import { inject as service } from '@ember/service';
4
+ import { tracked } from '@glimmer/tracking';
5
+ import { task, timeout } from 'ember-concurrency';
2
6
 
3
- export default class ConnectivityDevicesIndexDetailsEventsController extends Controller {}
7
+ const severityOptions = [
8
+ { label: 'Info', value: 'info' },
9
+ { label: 'Warning', value: 'warning' },
10
+ { label: 'Error', value: 'error' },
11
+ { label: 'Critical', value: 'critical' },
12
+ { label: 'High', value: 'high' },
13
+ ];
14
+
15
+ const warningSeverities = ['warning', 'error', 'critical', 'high'];
16
+
17
+ export default class ConnectivityDevicesIndexDetailsEventsController extends Controller {
18
+ @service deviceEventActions;
19
+ @service hostRouter;
20
+ @service intl;
21
+
22
+ @tracked queryParams = [
23
+ 'events_page',
24
+ 'events_limit',
25
+ 'events_sort',
26
+ 'events_query',
27
+ 'events_event_type',
28
+ 'events_severity',
29
+ 'events_processed',
30
+ 'events_occurred_at',
31
+ 'events_created_at',
32
+ ];
33
+ @tracked device;
34
+ @tracked events_page = 1;
35
+ @tracked events_limit;
36
+ @tracked events_sort = '-created_at';
37
+ @tracked events_query;
38
+ @tracked events_event_type;
39
+ @tracked events_severity;
40
+ @tracked events_processed;
41
+ @tracked events_occurred_at;
42
+ @tracked events_created_at;
43
+
44
+ @tracked bulkActions = [];
45
+
46
+ get events() {
47
+ return Array.from(this.model ?? []);
48
+ }
49
+
50
+ get totalEvents() {
51
+ return this.model?.meta?.total ?? this.events.length;
52
+ }
53
+
54
+ get warningEventsCount() {
55
+ return this.events.filter((event) => warningSeverities.includes(String(event.severity ?? '').toLowerCase())).length;
56
+ }
57
+
58
+ get unprocessedEventsCount() {
59
+ return this.events.filter((event) => !event.processed_at && !event.is_processed).length;
60
+ }
61
+
62
+ get processedEventsCount() {
63
+ return Math.max(this.events.length - this.unprocessedEventsCount, 0);
64
+ }
65
+
66
+ get hasHealthyEventState() {
67
+ return this.events.length > 0 && this.warningEventsCount === 0 && this.unprocessedEventsCount === 0 && !this.hasActiveFilters;
68
+ }
69
+
70
+ get hasActiveFilters() {
71
+ return Boolean(this.events_query || this.events_event_type || this.events_severity || this.events_processed || this.events_occurred_at || this.events_created_at);
72
+ }
73
+
74
+ get metrics() {
75
+ return [
76
+ { label: 'Recent events', value: this.totalEvents, icon: 'list', accentClass: 'fleetops-connectivity-kpi-accent-blue' },
77
+ {
78
+ label: 'Warnings',
79
+ value: this.warningEventsCount,
80
+ icon: 'triangle-exclamation',
81
+ accentClass: this.warningEventsCount ? 'fleetops-connectivity-kpi-accent-amber' : 'fleetops-connectivity-kpi-accent-green',
82
+ },
83
+ {
84
+ label: 'Unprocessed',
85
+ value: this.unprocessedEventsCount,
86
+ icon: 'clock',
87
+ accentClass: this.unprocessedEventsCount ? 'fleetops-connectivity-kpi-accent-rose' : 'fleetops-connectivity-kpi-accent-green',
88
+ },
89
+ { label: 'Processed', value: this.processedEventsCount, icon: 'check', accentClass: 'fleetops-connectivity-kpi-accent-green' },
90
+ ];
91
+ }
92
+
93
+ get actionButtons() {
94
+ return [
95
+ {
96
+ icon: 'refresh',
97
+ size: 'sm',
98
+ onClick: this.refresh,
99
+ helpText: this.intl.t('common.refresh'),
100
+ wrapperClass: 'fleetops-telematics-action-button',
101
+ isLoading: this.refreshTask.isRunning,
102
+ disabled: this.refreshTask.isRunning,
103
+ },
104
+ ];
105
+ }
106
+
107
+ get columns() {
108
+ return [
109
+ {
110
+ sticky: true,
111
+ label: 'Event',
112
+ valuePath: 'event_type',
113
+ cellComponent: 'table/cell/anchor',
114
+ action: this.deviceEventActions.transition.view,
115
+ permission: 'fleet-ops view device-event',
116
+ resizable: true,
117
+ sortable: true,
118
+ filterable: true,
119
+ filterParam: 'events_event_type',
120
+ filterComponent: 'filter/string',
121
+ },
122
+ {
123
+ label: 'Severity',
124
+ valuePath: 'severity',
125
+ cellComponent: 'table/cell/status',
126
+ resizable: true,
127
+ sortable: true,
128
+ filterable: true,
129
+ filterParam: 'events_severity',
130
+ filterComponent: 'filter/multi-option',
131
+ filterOptions: severityOptions,
132
+ filterOptionLabel: 'label',
133
+ filterOptionValue: 'value',
134
+ },
135
+ {
136
+ label: 'Message',
137
+ valuePath: 'message',
138
+ resizable: true,
139
+ sortable: false,
140
+ },
141
+ {
142
+ label: 'Code',
143
+ valuePath: 'code',
144
+ resizable: true,
145
+ sortable: true,
146
+ },
147
+ {
148
+ label: 'Processed',
149
+ valuePath: 'processedAt',
150
+ sortParam: 'processed_at',
151
+ resizable: true,
152
+ sortable: true,
153
+ filterable: true,
154
+ filterParam: 'events_processed',
155
+ filterComponent: 'filter/multi-option',
156
+ filterOptions: [
157
+ { label: 'Processed', value: 'processed' },
158
+ { label: 'Unprocessed', value: 'unprocessed' },
159
+ ],
160
+ filterOptionLabel: 'label',
161
+ filterOptionValue: 'value',
162
+ },
163
+ {
164
+ label: 'Occurred',
165
+ valuePath: 'occurredAt',
166
+ sortParam: 'occurred_at',
167
+ resizable: true,
168
+ sortable: true,
169
+ filterable: true,
170
+ filterParam: 'events_occurred_at',
171
+ filterComponent: 'filter/date',
172
+ },
173
+ {
174
+ label: this.intl.t('column.created-at'),
175
+ valuePath: 'createdAt',
176
+ sortParam: 'created_at',
177
+ resizable: true,
178
+ sortable: true,
179
+ filterable: true,
180
+ filterParam: 'events_created_at',
181
+ filterComponent: 'filter/date',
182
+ hidden: true,
183
+ },
184
+ {
185
+ label: '',
186
+ cellComponent: 'table/cell/dropdown',
187
+ ddButtonText: false,
188
+ ddButtonIcon: 'ellipsis-h',
189
+ ddButtonIconPrefix: 'fas',
190
+ ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.device-event') }),
191
+ cellClassNames: 'overflow-visible align-middle',
192
+ wrapperClass: 'flex items-center justify-end mx-2',
193
+ sticky: 'right',
194
+ width: 60,
195
+ actions: [
196
+ {
197
+ label: this.intl.t('common.view-resource', { resource: this.intl.t('resource.device-event') }),
198
+ fn: this.deviceEventActions.transition.view,
199
+ permission: 'fleet-ops view device-event',
200
+ },
201
+ {
202
+ label: 'Mark processed',
203
+ fn: this.markProcessed,
204
+ permission: 'fleet-ops update device-event',
205
+ },
206
+ ],
207
+ sortable: false,
208
+ filterable: false,
209
+ resizable: false,
210
+ searchable: false,
211
+ },
212
+ ];
213
+ }
214
+
215
+ @action refresh() {
216
+ if (this.refreshTask.isRunning) {
217
+ return;
218
+ }
219
+
220
+ return this.refreshTask.perform();
221
+ }
222
+
223
+ @action async markProcessed(deviceEvent) {
224
+ await this.deviceEventActions.markProcessed(deviceEvent);
225
+ await this.hostRouter.refresh();
226
+ }
227
+
228
+ @task *refreshTask() {
229
+ yield this.hostRouter.refresh();
230
+ }
231
+
232
+ @task({ restartable: true }) *searchTask(event) {
233
+ const {
234
+ target: { value },
235
+ } = event;
236
+
237
+ if (!value) {
238
+ this.events_query = null;
239
+ return;
240
+ }
241
+
242
+ yield timeout(250);
243
+ if (this.events_page > 1) this.events_page = 1;
244
+ this.events_query = value;
245
+ }
246
+ }