@fleetbase/fleetops-engine 0.6.20 → 0.6.22

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 (162) hide show
  1. package/addon/components/custom-entity/form.hbs +14 -14
  2. package/addon/components/device/card.hbs +1 -0
  3. package/addon/components/device/card.js +3 -0
  4. package/addon/components/device/details.hbs +92 -43
  5. package/addon/components/device/form.hbs +108 -60
  6. package/addon/components/device/form.js +36 -8
  7. package/addon/components/device/manager.hbs +29 -0
  8. package/addon/components/device/manager.js +95 -0
  9. package/addon/components/device/panel-header.hbs +32 -0
  10. package/addon/components/device/panel-header.js +3 -0
  11. package/addon/components/device/pill.hbs +16 -0
  12. package/addon/components/device/pill.js +3 -0
  13. package/addon/components/driver/details.hbs +4 -0
  14. package/addon/components/driver/details.js +19 -1
  15. package/addon/components/driver/form.hbs +14 -3
  16. package/addon/components/driver/form.js +49 -47
  17. package/addon/components/driver/pill.hbs +17 -0
  18. package/addon/components/driver/pill.js +3 -0
  19. package/addon/components/entity/form.hbs +7 -5
  20. package/addon/components/layout/fleet-ops-sidebar.js +12 -12
  21. package/addon/components/map/drawer/device-event-listing.hbs +64 -0
  22. package/addon/components/map/drawer/device-event-listing.js +181 -0
  23. package/addon/components/map/drawer/position-listing.hbs +100 -0
  24. package/addon/components/map/drawer/position-listing.js +455 -0
  25. package/addon/components/map/drawer.js +2 -0
  26. package/addon/components/map/leaflet-live-map.hbs +7 -2
  27. package/addon/components/modals/attach-device.hbs +18 -0
  28. package/addon/components/modals/attach-device.js +3 -0
  29. package/addon/components/order/details/detail.hbs +2 -54
  30. package/addon/components/order/details/detail.js +1 -0
  31. package/addon/components/order/details/payload.hbs +6 -4
  32. package/addon/components/order/details/payload.js +2 -0
  33. package/addon/components/order/pill.hbs +34 -0
  34. package/addon/components/order/pill.js +3 -0
  35. package/addon/components/order-config-manager/custom-fields.js +1 -1
  36. package/addon/components/positions-replay.hbs +339 -0
  37. package/addon/components/positions-replay.js +409 -0
  38. package/addon/components/sensor/details.hbs +64 -38
  39. package/addon/components/sensor/form.hbs +112 -63
  40. package/addon/components/sensor/form.js +36 -24
  41. package/addon/components/sensor/panel-header.hbs +32 -0
  42. package/addon/components/sensor/panel-header.js +3 -0
  43. package/addon/components/telematic/details.hbs +40 -16
  44. package/addon/components/telematic/form.hbs +63 -64
  45. package/addon/components/telematic/form.js +73 -4
  46. package/addon/components/vehicle/card.hbs +2 -2
  47. package/addon/components/vehicle/details.hbs +4 -0
  48. package/addon/components/vehicle/details.js +19 -1
  49. package/addon/components/vehicle/form.hbs +4 -0
  50. package/addon/components/vehicle/pill.hbs +34 -0
  51. package/addon/components/vehicle/pill.js +3 -0
  52. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  53. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  54. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  55. package/addon/controllers/connectivity/devices/index.js +51 -9
  56. package/addon/controllers/connectivity/events/index.js +65 -16
  57. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  58. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  59. package/addon/controllers/connectivity/sensors/index.js +66 -6
  60. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  61. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  62. package/addon/controllers/connectivity/telematics/index.js +20 -11
  63. package/addon/controllers/management/fleets/index/details.js +26 -21
  64. package/addon/controllers/management/fleets/index/edit.js +9 -6
  65. package/addon/controllers/management/vehicles/index/details.js +26 -13
  66. package/addon/controllers/settings/custom-fields.js +6 -0
  67. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  68. package/addon/routes/connectivity/devices/index/details.js +27 -1
  69. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  70. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  71. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  72. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  73. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  74. package/addon/routes/management/drivers/index/details/positions.js +3 -0
  75. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  76. package/addon/routes.js +4 -0
  77. package/addon/services/movement-tracker.js +81 -30
  78. package/addon/services/position-playback.js +486 -0
  79. package/addon/services/resource-metadata.js +46 -0
  80. package/addon/styles/fleetops-engine.css +157 -0
  81. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  82. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  83. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  84. package/addon/templates/connectivity/events/index.hbs +1 -1
  85. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  86. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  87. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  88. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  89. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  90. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  91. package/addon/templates/management/drivers/index/details/positions.hbs +2 -0
  92. package/addon/templates/management/vehicles/index/details/devices.hbs +1 -2
  93. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  94. package/addon/utils/fleet-ops-options.js +95 -0
  95. package/app/components/device/card.js +1 -0
  96. package/app/components/device/manager.js +1 -0
  97. package/app/components/device/panel-header.js +1 -0
  98. package/app/components/device/pill.js +1 -0
  99. package/app/components/driver/pill.js +1 -0
  100. package/app/components/map/drawer/device-event-listing.js +1 -0
  101. package/app/components/map/drawer/position-listing.js +1 -0
  102. package/app/components/modals/attach-device.js +1 -0
  103. package/app/components/order/pill.js +1 -0
  104. package/app/components/positions-replay.js +1 -0
  105. package/app/components/sensor/panel-header.js +1 -0
  106. package/app/components/vehicle/pill.js +1 -0
  107. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  108. package/app/routes/management/drivers/index/details/positions.js +1 -0
  109. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  110. package/app/services/position-playback.js +1 -0
  111. package/app/services/resource-metadata.js +1 -0
  112. package/app/templates/management/drivers/index/details/positions.js +1 -0
  113. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  114. package/composer.json +1 -1
  115. package/extension.json +1 -1
  116. package/package.json +4 -4
  117. package/server/config/telematics.php +111 -0
  118. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  119. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  120. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  121. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  122. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  123. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  124. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  125. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  126. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  127. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  128. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  129. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  130. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  131. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  132. package/server/src/Http/Controllers/TelematicWebhookController.php +169 -0
  133. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  134. package/server/src/Http/Filter/PositionFilter.php +35 -0
  135. package/server/src/Http/Resources/v1/Position.php +44 -0
  136. package/server/src/Jobs/ReplayPositions.php +64 -0
  137. package/server/src/Jobs/SendPositionReplay.php +65 -0
  138. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  139. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  140. package/server/src/Models/Asset.php +10 -8
  141. package/server/src/Models/Device.php +79 -12
  142. package/server/src/Models/DeviceEvent.php +33 -3
  143. package/server/src/Models/Driver.php +28 -1
  144. package/server/src/Models/Maintenance.php +15 -12
  145. package/server/src/Models/Part.php +2 -0
  146. package/server/src/Models/Payload.php +0 -1
  147. package/server/src/Models/Place.php +4 -1
  148. package/server/src/Models/Position.php +27 -17
  149. package/server/src/Models/Sensor.php +78 -13
  150. package/server/src/Models/Telematic.php +116 -6
  151. package/server/src/Models/TrackingNumber.php +3 -1
  152. package/server/src/Models/Vehicle.php +8 -11
  153. package/server/src/Models/WorkOrder.php +8 -5
  154. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  155. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  156. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  157. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  158. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  159. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  160. package/server/src/Support/Telematics/TelematicService.php +223 -0
  161. package/server/src/Support/Utils.php +1 -1
  162. package/server/src/routes.php +24 -1
@@ -1,75 +1,141 @@
1
1
  <div class="form-wrapper" ...attributes>
2
- <ContentPanel @title="Sensor Details" @open={{true}} @wrapperClass="bordered-top">
2
+ <ContentPanel @title="Integration" @open={{true}} @wrapperClass="bordered-top">
3
3
  <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
4
- <InputGroup @name="Name" @wrapperClass="col-span-2">
5
- <Input @value={{@resource.name}} @type="text" class="w-full form-input" placeholder="Name" disabled={{cannot-write @resource}} />
4
+ <InputGroup @name={{t "device.fields.telematic"}}>
5
+ <ModelSelect
6
+ @modelName="telematic"
7
+ @selectedModel={{@resource.telematic}}
8
+ @placeholder="Select Telematic"
9
+ @triggerClass="form-select form-input"
10
+ @infiniteScroll={{false}}
11
+ @renderInPlace={{true}}
12
+ @onChange={{this.selectTelematic}}
13
+ @disabled={{cannot-write @resource}}
14
+ as |model|
15
+ >
16
+ <div class="space-x-2 text-sm">
17
+ <div class="inline-block align-top">
18
+ <div class="hide-from-trigger h-1.5 w-full" />
19
+ <Image src={{model.provider_descriptor.icon}} class="w-5 h-5" />
20
+ </div>
21
+ <div class="inline-block">
22
+ <div class="font-semibold normalize-in-trigger">{{or model.name model.public_id}}</div>
23
+ <div class="hide-from-trigger">{{n-a model.provider_descriptor.label (titleize model.provider)}}</div>
24
+ </div>
25
+ </div>
26
+ </ModelSelect>
6
27
  </InputGroup>
7
28
 
8
- <InputGroup @name="Sensor Type">
29
+ <InputGroup @name="Device">
30
+ <ModelSelect
31
+ @modelName="device"
32
+ @selectedModel={{@resource.device}}
33
+ @placeholder="Select Device"
34
+ @triggerClass="form-select form-input"
35
+ @infiniteScroll={{false}}
36
+ @renderInPlace={{true}}
37
+ @onChange={{fn (mut @resource.device)}}
38
+ @onChangeId={{fn (mut @resource.device_uuid)}}
39
+ @disabled={{cannot-write @resource}}
40
+ as |model|
41
+ >
42
+ <div class="text-sm">
43
+ <div class="font-semibold normalize-in-trigger">{{model.name}}</div>
44
+ <div class="hide-from-trigger">{{n-a model.serial_number}}</div>
45
+ </div>
46
+ </ModelSelect>
47
+ </InputGroup>
48
+
49
+ <InputGroup @name="Report Frequency (Seconds)" @type="number">
50
+ <Input
51
+ @value={{@resource.report_frequency_sec}}
52
+ @type="number"
53
+ class="w-full form-input"
54
+ placeholder="Frequency of sensor reporting in seconds"
55
+ disabled={{cannot-write @resource}}
56
+ />
57
+ </InputGroup>
58
+ </div>
59
+ </ContentPanel>
60
+
61
+ <ContentPanel @title="Identity" @open={{true}} @wrapperClass="bordered-top">
62
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
63
+ <InputGroup @name="Name" @value={{@resource.name}} @wrapperClass="col-span-2" disabled={{cannot-write @resource}} />
64
+ <InputGroup @name="Serial Number" @value={{@resource.serial_number}} @wrapperClass="col-span-2" disabled={{cannot-write @resource}} />
65
+ <InputGroup @name="Internal ID" @value={{@resource.internal_id}} disabled={{cannot-write @resource}} />
66
+ <InputGroup @name="Unit" @value={{@resource.unit}} disabled={{cannot-write @resource}} />
67
+ <InputGroup @name="Type" @wrapperClass="col-span-2">
9
68
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
10
69
  <PowerSelect
11
- @options={{this.sensorTypeOptions}}
12
- @selected={{@resource.sensor_type}}
13
- @onChange={{fn (mut @resource.sensor_type)}}
14
- @placeholder="Select Sensor Type"
70
+ @options={{get-fleet-ops-options "sensorTypes"}}
71
+ @selected={{find-by "value" @resource.type (get-fleet-ops-options "sensorTypes")}}
72
+ @onChange={{set-model-attr @resource "type"}}
73
+ @placeholder={{t "common.select-field" field=(t "common.type")}}
15
74
  @triggerClass="form-select form-input"
16
75
  @disabled={{cannot-write @resource}}
17
- as |type|
76
+ as |option|
18
77
  >
19
- {{smart-humanize type}}
78
+ <div class="text-sm">
79
+ <div class="font-semibold normalize-in-trigger">{{option.label}}</div>
80
+ <div class="hide-from-trigger">{{option.description}}</div>
81
+ </div>
20
82
  </PowerSelect>
21
83
  </div>
22
84
  </InputGroup>
85
+ </div>
86
+ </ContentPanel>
23
87
 
24
- <InputGroup @name="Unit">
25
- <Input @value={{@resource.unit}} @type="text" class="w-full form-input" placeholder="Unit (e.g., °C, PSI, %)" disabled={{cannot-write @resource}} />
26
- </InputGroup>
27
-
28
- <InputGroup @name="Minimum Threshold" @type="number">
29
- <Input @value={{@resource.min_threshold}} @type="number" class="w-full form-input" placeholder="Minimum Threshold" disabled={{cannot-write @resource}} />
88
+ <ContentPanel @title="Threshold" @open={{true}} @wrapperClass="bordered-top">
89
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
90
+ <InputGroup @name="Min Threshold" @type="number">
91
+ <Input
92
+ @value={{@resource.min_threshold}}
93
+ @type="number"
94
+ class="w-full form-input"
95
+ placeholder={{t "sensor.fields.min-threshold-placeholder"}}
96
+ disabled={{cannot-write @resource}}
97
+ />
30
98
  </InputGroup>
31
99
 
32
- <InputGroup @name="Maximum Threshold" @type="number">
33
- <Input @value={{@resource.max_threshold}} @type="number" class="w-full form-input" placeholder="Maximum Threshold" disabled={{cannot-write @resource}} />
100
+ <InputGroup @name="Max Threshold" @type="number">
101
+ <Input
102
+ @value={{@resource.max_threshold}}
103
+ @type="number"
104
+ class="w-full form-input"
105
+ placeholder={{t "sensor.fields.max-threshold-placeholder"}}
106
+ disabled={{cannot-write @resource}}
107
+ />
34
108
  </InputGroup>
35
109
 
36
- <InputGroup @name="Report Frequency (seconds)" @type="number">
37
- <Input @value={{@resource.report_frequency_sec}} @type="number" class="w-full form-input" placeholder="Report Frequency" disabled={{cannot-write @resource}} />
38
- </InputGroup>
110
+ <div class="col-span-2">
111
+ <div class="mt-2">
112
+ <Checkbox @value={{@resource.threshold_inclusive}} @onChange={{fn (mut @resource.threshold_inclusive)}}>Threshold Inclusive</Checkbox>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </ContentPanel>
39
117
 
40
- <InputGroup @name="Status">
118
+ <ContentPanel @title="Warranty & Status" @open={{true}} @wrapperClass="bordered-top">
119
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
120
+ <InputGroup @name={{t "common.status"}}>
41
121
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
42
122
  <PowerSelect
43
- @options={{this.statusOptions}}
44
- @selected={{@resource.status}}
45
- @onChange={{fn (mut @resource.status)}}
46
- @placeholder="Select Status"
123
+ @options={{get-fleet-ops-options "sensorStatuses"}}
124
+ @selected={{find-by "value" @resource.status (get-fleet-ops-options "sensorStatuses")}}
125
+ @onChange={{set-model-attr @resource "status"}}
126
+ @placeholder={{t "common.select-field" field=(t "common.status")}}
47
127
  @triggerClass="form-select form-input"
48
128
  @disabled={{cannot-write @resource}}
49
- as |status|
129
+ as |option|
50
130
  >
51
- {{smart-humanize status}}
131
+ <div class="text-sm">
132
+ <div class="font-semibold normalize-in-trigger">{{option.label}}</div>
133
+ <div class="hide-from-trigger">{{option.description}}</div>
134
+ </div>
52
135
  </PowerSelect>
53
136
  </div>
54
137
  </InputGroup>
55
138
 
56
- <InputGroup @name="Device">
57
- <ModelSelect
58
- @modelName="device"
59
- @selectedModel={{@resource.device}}
60
- @placeholder="Select Device"
61
- @triggerClass="form-select form-input"
62
- @infiniteScroll={{false}}
63
- @renderInPlace={{true}}
64
- @onChange={{fn (mut @resource.device)}}
65
- @onChangeId={{fn (mut @resource.device_uuid)}}
66
- @disabled={{cannot-write @resource}}
67
- as |model|
68
- >
69
- {{model.device_name}}
70
- </ModelSelect>
71
- </InputGroup>
72
-
73
139
  <InputGroup @name="Warranty">
74
140
  <ModelSelect
75
141
  @modelName="warranty"
@@ -86,23 +152,6 @@
86
152
  {{model.policy_number}} - {{model.provider}}
87
153
  </ModelSelect>
88
154
  </InputGroup>
89
-
90
- <div class="col-span-2">
91
- <label class="flex items-center space-x-2">
92
- <input
93
- type="checkbox"
94
- checked={{@resource.threshold_inclusive}}
95
- {{on "change" (fn (mut @resource.threshold_inclusive) (not @resource.threshold_inclusive))}}
96
- disabled={{cannot-write @resource}}
97
- class="form-checkbox"
98
- />
99
- <span class="text-xs">Threshold Inclusive</span>
100
- </label>
101
- </div>
102
-
103
- <RegistryYield @registry="fleet-ops:component:sensor:form:details" as |RegistryComponent|>
104
- <RegistryComponent @resource={{@resource}} @controller={{@controller}} @permission={{get-write-permission @resource}} />
105
- </RegistryYield>
106
155
  </div>
107
156
  </ContentPanel>
108
157
 
@@ -111,4 +160,4 @@
111
160
  <RegistryYield @registry="fleet-ops:component:sensor:form" as |RegistryComponent|>
112
161
  <RegistryComponent @resource={{@resource}} @controller={{@controller}} @permission={{get-write-permission @resource}} />
113
162
  </RegistryYield>
114
- </div>
163
+ </div>
@@ -1,29 +1,41 @@
1
1
  import Component from '@glimmer/component';
2
+ import { action } from '@ember/object';
3
+ import { inject as service } from '@ember/service';
4
+ import { task } from 'ember-concurrency';
2
5
 
3
6
  export default class SensorFormComponent extends Component {
4
- /**
5
- * Sensor type options
6
- */
7
- sensorTypeOptions = [
8
- 'temperature',
9
- 'door_status',
10
- 'fuel_level',
11
- 'tire_pressure',
12
- 'humidity',
13
- 'speed',
14
- 'acceleration',
15
- 'gyroscope',
16
- 'gps',
17
- 'battery',
18
- 'voltage',
19
- 'current',
20
- 'pressure',
21
- 'weight',
22
- 'proximity',
23
- ];
7
+ @service fetch;
8
+ @service currentUser;
9
+ @service notifications;
24
10
 
25
- /**
26
- * Status options for sensors
27
- */
28
- statusOptions = ['active', 'inactive', 'calibrating', 'maintenance', 'error'];
11
+ @action selectTelematic(telematic) {
12
+ this.args.resource.setProperties({
13
+ telematic,
14
+ telematic_uuid: telematic.id,
15
+ provider: telematic.provider,
16
+ });
17
+ }
18
+
19
+ @task *handlePhotoUpload(file) {
20
+ try {
21
+ yield this.fetch.uploadFile.perform(
22
+ file,
23
+ {
24
+ path: `uploads/${this.currentUser.companyId}/sensors/${this.args.resource.id}`,
25
+ subject_uuid: this.args.resource.id,
26
+ subject_type: 'fleet-ops:sensor',
27
+ type: 'sensor_photo',
28
+ },
29
+ (uploadedFile) => {
30
+ this.args.resource.setProperties({
31
+ photo_uuid: uploadedFile.id,
32
+ photo_url: uploadedFile.url,
33
+ photo: uploadedFile,
34
+ });
35
+ }
36
+ );
37
+ } catch (err) {
38
+ this.notifications.error('Unable to upload photo: ' + err.message);
39
+ }
40
+ }
29
41
  }
@@ -0,0 +1,32 @@
1
+ <div class="px-4 py-2">
2
+ <div class="flex flex-1 flex-row items-start justify-between">
3
+ <div class="flex flex-row space-x-3">
4
+ <div class="flex items-start justify-start rounded-full">
5
+ <Image src={{@resource.photo_url}} @fallbackSrc={{config "defaultValues.placeholderImage"}} alt={{@resource.displayName}} height="48" width="48" class="h-14 w-14 rounded-full shadow-sm" />
6
+ <Attach::Tooltip @class="clean" @animation="scale" @placement="top">
7
+ <InputInfo @text={{@resource.displayName}} />
8
+ </Attach::Tooltip>
9
+ </div>
10
+ <div class="flex flex-col">
11
+ <h1 class="text-gray-900 dark:text-white font-semibold text-lg">{{@resource.displayName}}</h1>
12
+ <div>
13
+ <div class="text-xs text-gray-400 dark:text-gray-500">{{n-a (get-fleet-ops-option-label "sensorTypes" @resource.type)}}</div>
14
+ <div class="text-xs text-gray-400 dark:text-gray-500">{{n-a @resource.serial_number}}</div>
15
+ </div>
16
+ <Badge @status={{if @resource.online "online" "offline"}}>{{if @resource.online (t "common.online") (t "common.offline")}}</Badge>
17
+ </div>
18
+ </div>
19
+ <div class="next-view-header-right">
20
+ <Layout::Resource::Panel::HeaderActions
21
+ @resource={{@resource}}
22
+ @saveTask={{@saveTask}}
23
+ @saveOptions={{@saveOptions}}
24
+ @saveDisabled={{@saveDisabled}}
25
+ @pojoResource={{@pojoResource}}
26
+ @authSchema={{@authSchema}}
27
+ @actionButtons={{@actionButtons}}
28
+ @onPressCancel={{@onPressCancel}}
29
+ />
30
+ </div>
31
+ </div>
32
+ </div>
@@ -0,0 +1,3 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class SensorPanelHeaderComponent extends Component {}
@@ -1,16 +1,45 @@
1
1
  <div class="details-wrapper" ...attributes>
2
- <ContentPanel @title="Details" @open={{true}} @wrapperClass="bordered-top">
3
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
4
- <div class="field-info-container">
5
- <div class="field-name">Name</div>
6
- <div class="field-value">{{n-a @resource.name}}</div>
2
+ <ContentPanel @title="Telematics Provider" @open={{true}} @wrapperClass="bordered-top">
3
+ <div class="field-info-container">
4
+ <div class="field-name">Provider</div>
5
+ <div class="field-value">
6
+ <div class="flex flew-row space-x-2 text-sm">
7
+ <div class="pt-1.5">
8
+ <Image src={{@resource.provider_descriptor.icon}} class="w-5 h-5" />
9
+ </div>
10
+ <div>
11
+ <div class="font-semibold">{{@resource.provider_descriptor.label}}</div>
12
+ <div class="text-xs text-gray-400 dark:text-gray-500">{{n-a @resource.provider_descriptor.description}}</div>
13
+ </div>
14
+ </div>
7
15
  </div>
16
+ </div>
17
+ </ContentPanel>
8
18
 
19
+ <ContentPanel @title="Integration Details" @open={{true}} @wrapperClass="bordered-top">
20
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
9
21
  <div class="field-info-container">
10
- <div class="field-name">Provider</div>
11
- <div class="field-value">{{n-a @resource.provider}}</div>
22
+ <div class="field-name">Integration Name</div>
23
+ <div class="field-value">{{n-a @resource.name}}</div>
12
24
  </div>
13
25
 
26
+ {{#if @resource.provider_descriptor.supportsWebhooks}}
27
+ <InputGroup @name="Webhook URL" @helpText={{concat "Configure the URL in your " @resource.provider_descriptor.label " dashboard to receive real-time updates."}}>
28
+ <ClickToCopy @value={{@resource.provider_descriptor.webhook_url}}>
29
+ <Input @value={{@resource.provider_descriptor.webhook_url}} class="form-input" readonly />
30
+ </ClickToCopy>
31
+ <small class="form-text text-muted">
32
+ Configure this URL in your
33
+ {{@resource.provider_descriptor.label}}
34
+ dashboard to receive real-time updates.
35
+ </small>
36
+ </InputGroup>
37
+ {{/if}}
38
+ </div>
39
+ </ContentPanel>
40
+
41
+ <ContentPanel @title="Telematic Blackbox" @open={{true}} @wrapperClass="bordered-top">
42
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
14
43
  <div class="field-info-container">
15
44
  <div class="field-name">Model</div>
16
45
  <div class="field-value">{{n-a @resource.model}}</div>
@@ -53,23 +82,18 @@
53
82
  <div class="field-value">{{n-a @resource.msisdn}}</div>
54
83
  </div>
55
84
 
56
- <div class="field-info-container">
57
- <div class="field-name">Warranty</div>
58
- <div class="field-value">{{n-a @resource.warranty_name}}</div>
59
- </div>
60
-
61
85
  <div class="field-info-container">
62
86
  <div class="field-name">Last Seen</div>
63
- <div class="field-value">{{format-date @resource.last_seen_at}}</div>
87
+ <div class="field-value">{{n-a (format-date @resource.last_seen_at)}}</div>
64
88
  </div>
65
89
 
66
90
  <div class="field-info-container">
67
91
  <div class="field-name">Online Status</div>
68
92
  <div class="field-value">
69
93
  {{#if @resource.is_online}}
70
- <Badge @status="success">Online</Badge>
94
+ <Badge @status="online">Online</Badge>
71
95
  {{else}}
72
- <Badge @status="danger">Offline</Badge>
96
+ <Badge @status="offline">Offline</Badge>
73
97
  {{/if}}
74
98
  </div>
75
99
  </div>
@@ -82,4 +106,4 @@
82
106
  </ContentPanel>
83
107
 
84
108
  <CustomField::Yield @subject={{@resource}} @viewMode={{true}} @wrapperClass="bordered-top" />
85
- </div>
109
+ </div>
@@ -1,78 +1,77 @@
1
1
  <div class="form-wrapper" ...attributes>
2
- <ContentPanel @title="Telematic Details" @open={{true}} @wrapperClass="bordered-top">
2
+ <ContentPanel @title="Telematics Provider" @open={{true}} @wrapperClass="bordered-top">
3
3
  <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
4
- <InputGroup @name="Name" @wrapperClass="col-span-2">
5
- <Input @value={{@resource.name}} @type="text" class="w-full form-input" placeholder="Name" disabled={{cannot-write @resource}} />
6
- </InputGroup>
7
-
8
- <InputGroup @name="Provider">
9
- <Input @value={{@resource.provider}} @type="text" class="w-full form-input" placeholder="Provider" disabled={{cannot-write @resource}} />
10
- </InputGroup>
11
-
12
- <InputGroup @name="Model">
13
- <Input @value={{@resource.model}} @type="text" class="w-full form-input" placeholder="Model" disabled={{cannot-write @resource}} />
14
- </InputGroup>
15
-
16
- <InputGroup @name="Serial Number">
17
- <Input @value={{@resource.serial_number}} @type="text" class="w-full form-input" placeholder="Serial Number" disabled={{cannot-write @resource}} />
18
- </InputGroup>
19
-
20
- <InputGroup @name="Firmware Version">
21
- <Input @value={{@resource.firmware_version}} @type="text" class="w-full form-input" placeholder="Firmware Version" disabled={{cannot-write @resource}} />
22
- </InputGroup>
23
-
24
- <InputGroup @name="IMEI">
25
- <Input @value={{@resource.imei}} @type="text" class="w-full form-input" placeholder="IMEI" disabled={{cannot-write @resource}} />
26
- </InputGroup>
27
-
28
- <InputGroup @name="ICCID">
29
- <Input @value={{@resource.iccid}} @type="text" class="w-full form-input" placeholder="ICCID" disabled={{cannot-write @resource}} />
30
- </InputGroup>
31
-
32
- <InputGroup @name="IMSI">
33
- <Input @value={{@resource.imsi}} @type="text" class="w-full form-input" placeholder="IMSI" disabled={{cannot-write @resource}} />
34
- </InputGroup>
35
-
36
- <InputGroup @name="MSISDN">
37
- <Input @value={{@resource.msisdn}} @type="text" class="w-full form-input" placeholder="MSISDN" disabled={{cannot-write @resource}} />
38
- </InputGroup>
39
-
40
- <InputGroup @name="Status">
4
+ <InputGroup @name="Provider" @wrapperClass="col-span-2">
41
5
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
42
6
  <PowerSelect
43
- @options={{this.statusOptions}}
44
- @selected={{@resource.status}}
45
- @onChange={{fn (mut @resource.status)}}
46
- @placeholder="Select Status"
7
+ @options={{this.providers}}
8
+ @selected={{this.selectedProvider}}
9
+ @onChange={{this.selectProvider}}
10
+ @placeholder="Select Provider"
47
11
  @triggerClass="form-select form-input"
48
12
  @disabled={{cannot-write @resource}}
49
- as |status|
13
+ as |provider|
50
14
  >
51
- {{smart-humanize status}}
15
+ <div class="space-x-2 text-sm">
16
+ <div class="inline-block align-top">
17
+ <div class="hide-from-trigger h-1.5 w-full" />
18
+ <Image src={{provider.icon}} class="w-5 h-5" />
19
+ </div>
20
+ <div class="inline-block">
21
+ <div class="font-semibold normalize-in-trigger">{{provider.label}}</div>
22
+ <div class="hide-from-trigger">{{n-a provider.description}}</div>
23
+ </div>
24
+ </div>
52
25
  </PowerSelect>
53
26
  </div>
54
27
  </InputGroup>
28
+ </div>
29
+ </ContentPanel>
55
30
 
56
- <InputGroup @name="Warranty">
57
- <ModelSelect
58
- @modelName="warranty"
59
- @selectedModel={{@resource.warranty}}
60
- @placeholder="Select Warranty"
61
- @triggerClass="form-select form-input"
62
- @infiniteScroll={{false}}
63
- @renderInPlace={{true}}
64
- @onChange={{fn (mut @resource.warranty)}}
65
- @onChangeId={{fn (mut @resource.warranty_uuid)}}
66
- @disabled={{cannot-write @resource}}
67
- as |model|
68
- >
69
- {{model.policy_number}} - {{model.provider}}
70
- </ModelSelect>
71
- </InputGroup>
31
+ {{#if this.selectedProvider}}
32
+ <ContentPanel @title="Credentials" @open={{true}} @actionButtons={{this.credentialsActionButtons}} @wrapperClass="bordered-top">
33
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
34
+ {{#each this.selectedProvider.required_fields as |field|}}
35
+ <InputGroup
36
+ @name={{field.label}}
37
+ @value={{get @resource.credentials field.name}}
38
+ @required={{field.required}}
39
+ @type={{if (eq field.type "password") "text" field.type}}
40
+ @helpText={{field.help_text}}
41
+ {{on "input" (fn this.setCredential field)}}
42
+ />
43
+ {{/each}}
44
+ </div>
45
+ </ContentPanel>
46
+
47
+ <ContentPanel @title="Configure Integration" @open={{true}} @wrapperClass="bordered-top">
48
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 lg:gap-2 no-input-group-padding text-xs">
49
+ <InputGroup @value={{@resource.name}} @name="Integration Name" />
50
+
51
+ {{#if this.selectedProvider.supports_webhooks}}
52
+ <InputGroup @name="Webhook URL" @helpText={{concat "Configure the URL in your " this.selectedProvider.label " dashboard to receive real-time updates."}} @wrapperClass="col-span-2">
53
+ <ClickToCopy @value={{this.selectedProvider.webhook_url}} class="w-full">
54
+ <Input @value={{this.selectedProvider.webhook_url}} class="form-input w-full" readonly />
55
+ </ClickToCopy>
56
+ <small class="form-text text-muted mt-1">
57
+ Configure this URL in your
58
+ {{this.selectedProvider.label}}
59
+ dashboard to receive real-time updates.
60
+ </small>
61
+ </InputGroup>
62
+ {{/if}}
63
+ </div>
64
+ </ContentPanel>
65
+ {{/if}}
72
66
 
73
- <RegistryYield @registry="fleet-ops:component:telematic:form:details" as |RegistryComponent|>
74
- <RegistryComponent @resource={{@resource}} @controller={{@controller}} @permission={{get-write-permission @resource}} />
75
- </RegistryYield>
67
+ <ContentPanel @title="Telematic Blackbox" @open={{true}} @wrapperClass="bordered-top">
68
+ <div class="grid grid-cols-1 gap-2 lg:grid-cols-3 gap-2 no-input-group-padding text-xs">
69
+ <InputGroup @value={{@resource.model}} @name="Model" />
70
+ <InputGroup @value={{@resource.serial_number}} @name="Serial Number" />
71
+ <InputGroup @value={{@resource.firmware_version}} @name="Firmware Version" />
72
+ <InputGroup @value={{@resource.imei}} @name="IMEI" />
73
+ <InputGroup @value={{@resource.imsi}} @name="IMSI" />
74
+ <InputGroup @value={{@resource.iccid}} @name="ICCID" />
76
75
  </div>
77
76
  </ContentPanel>
78
77
 
@@ -81,4 +80,4 @@
81
80
  <RegistryYield @registry="fleet-ops:component:telematic:form" as |RegistryComponent|>
82
81
  <RegistryComponent @resource={{@resource}} @controller={{@controller}} @permission={{get-write-permission @resource}} />
83
82
  </RegistryYield>
84
- </div>
83
+ </div>
@@ -1,8 +1,77 @@
1
1
  import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { action } from '@ember/object';
5
+ import { task } from 'ember-concurrency';
2
6
 
3
7
  export default class TelematicFormComponent extends Component {
4
- /**
5
- * Status options for telematic devices
6
- */
7
- statusOptions = ['active', 'inactive', 'maintenance', 'retired'];
8
+ @service fetch;
9
+ @service notifications;
10
+ @tracked providers = [];
11
+ @tracked selectedProvider = this.args.resource?.provider_descriptor ?? null;
12
+ @tracked connectionTestResult;
13
+
14
+ get credentialsActionButtons() {
15
+ return [
16
+ {
17
+ size: 'xs',
18
+ icon: 'plug',
19
+ text: 'Test Connection',
20
+ onClick: () => this.testConnection.perform(),
21
+ isLoading: this.testConnection.isRunning,
22
+ },
23
+ ];
24
+ }
25
+
26
+ constructor() {
27
+ super(...arguments);
28
+ this.loadProviders.perform();
29
+ }
30
+
31
+ @action setCredential(field, { target: { value } }) {
32
+ const credentials = this.args.resource.credentials ?? {};
33
+ this.args.resource.set('credentials', {
34
+ ...credentials,
35
+ [field.name]: value,
36
+ });
37
+ }
38
+
39
+ @action selectProvider(provider) {
40
+ this.selectedProvider = provider;
41
+ this.args.resource.setProperties({
42
+ provider: provider.key,
43
+ credentials: (provider.required_fields ?? {}).reduce((acc, item) => {
44
+ acc[item.name] = null;
45
+ return acc;
46
+ }, {}),
47
+ });
48
+ }
49
+
50
+ @task *loadProviders() {
51
+ try {
52
+ const providers = yield this.fetch.get('telematics/providers');
53
+ this.providers = providers;
54
+ } catch (err) {
55
+ this.notifications.serverError(err);
56
+ }
57
+ }
58
+
59
+ @task *testConnection() {
60
+ try {
61
+ const result = yield this.fetch.post(`telematics/${this.selectedProvider.key}/test-credentials`, { credentials: this.args.resource.credentials });
62
+ this.connectionTestResult = result;
63
+
64
+ if (result.success) {
65
+ this.notifications.success('Connection successful!');
66
+ } else {
67
+ this.notifications.error(result.message);
68
+ }
69
+ } catch (error) {
70
+ this.connectionTestResult = {
71
+ success: false,
72
+ message: error.message || 'Connection test failed',
73
+ };
74
+ this.notifications.error('Connection test failed');
75
+ }
76
+ }
8
77
  }
@@ -1,7 +1,7 @@
1
1
  <Layout::Resource::Card ...attributes as |Card|>
2
- <Card.header class={{@headerClass}}>
2
+ <Card.header class="{{@headerClass}} truncate">
3
3
  <div class="font-semibold">{{or @resource.name @resource.yearMakeModel}}</div>
4
- <div class="text-gray-300 dark:text-gray-500 text-sm">{{or @resource.plate_number @resource.vin @resource.serial_number @resource.call_sign}}</div>
4
+ <div class="text-gray-300 dark:text-gray-500 text-sm">{{or @resource.plate_number @resource.vin @resource.serial_number @resource.call_sign @resource.public_id}}</div>
5
5
  {{#if (has-block "header")}}
6
6
  {{yield to="header"}}
7
7
  {{/if}}