@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
@@ -16,25 +16,7 @@
16
16
  <span>{{t "order.fields.driver-assigned"}}</span>
17
17
  </div>
18
18
  <div class="field-value">
19
- <div>
20
- <a href="javascript:;" class="flex flex-row space-x-2" {{on "click" (fn this.focusOrderAssignedDriver @resource.driver_assigned)}}>
21
- <div class="relative shrink-0">
22
- <Image
23
- src={{avatar-url @resource.driver_assigned.photo_url}}
24
- @fallbackSrc={{config "defaultValues.driverImage"}}
25
- width="28"
26
- height="28"
27
- class="w-7 h-7 rounded-full ring-2 ring-gray-800 dark:ring-gray-700 shadow transition-shadow hover:shadow-md focus:shadow-md"
28
- alt={{@resource.driver_assigned.name}}
29
- />
30
- <FaIcon @icon="circle" @size="2xs" class="absolute left-0 top-0 h-2 w-2 {{if @resource.driver_assigned.online 'text-green-500' 'text-yellow-200'}}" />
31
- </div>
32
- <div>
33
- <div class="text-sm">{{n-a @resource.driver_assigned.name}}</div>
34
- <div class="text-xs text-gray-400 dark:text-gray-500">{{n-a @resource.driver_assigned.phone "No Phone"}}</div>
35
- </div>
36
- </a>
37
- </div>
19
+ <Driver::Pill @driver={{@resource.driver_assigned}} @onClick={{this.focusOrderAssignedDriver}} />
38
20
  </div>
39
21
  </div>
40
22
  <div class="field-info-container">
@@ -42,41 +24,7 @@
42
24
  <span>{{t "order.fields.vehicle-assigned"}}</span>
43
25
  </div>
44
26
  <div class="field-value">
45
- <a href="javascript:;" class="flex flex-row space-x-2 shrink-0">
46
- <div class="relative shrink-0">
47
- <Image
48
- src={{@resource.vehicle_assigned.photo_url}}
49
- @fallbackSrc={{config "defaultValues.vehicleImage"}}
50
- width="28"
51
- height="28"
52
- class="w-7 h-7 rounded-full ring-2 ring-gray-800 dark:ring-gray-700 shadow transition-shadow hover:shadow-md focus:shadow-md"
53
- alt={{n-a @resource.vehicle_assigned.name @resource.vehicle_assigned.yearMakeModel}}
54
- />
55
- <FaIcon @icon="circle" @size="2xs" class="absolute left-0 top-0 h-2 w-2 {{if @resource.vehicle_assigned.online 'text-green-500' 'text-yellow-200'}}" />
56
- </div>
57
- <div>
58
- <div class="text-sm">{{n-a @resource.vehicle_assigned.name @resource.vehicle_assigned.yearMakeModel}}</div>
59
- <div class="text-xs text-gray-400 dark:text-gray-500">{{or
60
- @resource.vehicle_assigned.plate_number
61
- @resource.vehicle_assigned.vin
62
- @resource.vehicle_assigned.serial_number
63
- @resource.vehicle_assigned.call_sign
64
- }}</div>
65
- </div>
66
- <Attach::Tooltip @class="clean" @animation="scale" @placement="top">
67
- <InputInfo>
68
- <div class="text-xs font-semibold">{{@resource.vehicle_assigned.name @resource.vehicle_assigned.yearMakeModel}}</div>
69
- <div class="text-xs">Driver: {{n-a @resource.vehicle_assigned.driver_name}}</div>
70
- <div class="text-xs">
71
- <span>Status:</span>
72
- <span class="{{if @resource.vehicle_assigned.online 'text-green-500' 'text-red-400'}}">
73
- {{if @resource.vehicle_assigned.online "Online" "Offline"}}
74
- </span>
75
- </div>
76
- <div class="text-xs truncate">Pos: {{point-coordinates @resource.vehicle_assigned.location}}</div>
77
- </InputInfo>
78
- </Attach::Tooltip>
79
- </a>
27
+ <Vehicle::Pill @vehicle={{@resource.vehicle_assigned}} />
80
28
  </div>
81
29
  </div>
82
30
  <div class="field-info-container">
@@ -36,6 +36,7 @@ export default class OrderDetailsDetailComponent extends Component {
36
36
  }
37
37
 
38
38
  @action focusOrderAssignedDriver(driver) {
39
+ console.log('[focusOrderAssignedDriver]', ...arguments);
39
40
  this.driverActions.panel.view(driver);
40
41
  this.leafletMapManager.map?.flyTo(driver.coordinates, 18);
41
42
  }
@@ -1,5 +1,5 @@
1
1
  <ContentPanel @title={{t "order.fields.payload"}} @open={{true}} @actionButtons={{this.actionButtons}} @wrapperClass="bordered-top">
2
- {{#if @resource.isMultiDropOrder}}
2
+ {{#if @resource.isMultiDrop}}
3
3
  <div class="space-y-2">
4
4
  {{#each @resource.entitiesByDestination as |group|}}
5
5
  <div class="rounded-md border border-gray-200 dark:border-gray-900 p-3">
@@ -40,13 +40,15 @@
40
40
  <DropdownButton
41
41
  @triggerClass="mr-2"
42
42
  @iconClass="icon-text-height"
43
- @buttonSize="sm"
43
+ @size="xs"
44
+ @buttonSize="xs"
44
45
  @icon="ellipsis-h"
45
46
  @iconPrefix="fas"
46
47
  @contentClass="dropdown-menu"
48
+ @renderInPlace={{true}}
47
49
  as |dd|
48
50
  >
49
- <div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
51
+ <div class="next-dd-menu mx-0 mt-0i" aria-labelledby="user-menu">
50
52
  <div class="px-1">
51
53
  <div class="text-sm flex flex-row items-center px-3 py-1 rounded-md my-1 text-gray-300">
52
54
  {{t "order.fields.waypoint-actions"}}
@@ -72,7 +74,7 @@
72
74
  @icon="plus"
73
75
  @iconPrefix="fas"
74
76
  @text={{t "order.fields.add-item-button"}}
75
- @size="sm"
77
+ @size="xs"
76
78
  @onClick={{perform this.addEntity group.waypoint}}
77
79
  @permission="fleet-ops update order"
78
80
  />
@@ -13,6 +13,8 @@ export default class OrderDetailsPayloadComponent extends Component {
13
13
  @service intl;
14
14
 
15
15
  get actionButtons() {
16
+ if (this.args.resource.isMultiDrop) return [];
17
+
16
18
  return [
17
19
  {
18
20
  type: 'default',
@@ -0,0 +1,34 @@
1
+ {{#let (or @order @resource) as |resource|}}
2
+ <Pill
3
+ @resource={{resource}}
4
+ @onClick={{@onClick}}
5
+ @anchorClass={{@anchorClass}}
6
+ @imageClass={{@imageClass}}
7
+ @imageWrapperClass={{@imageWrapperClass}}
8
+ @contentWrapperClass={{@contentWrapperClass}}
9
+ @titleClass={{@titleClass}}
10
+ @subtitleClass={{@subtitleClass}}
11
+ ...attributes
12
+ >
13
+ <:image>
14
+ <div class="pt-1">
15
+ <div class="rounded-md bg-white p-1">
16
+ <img src={{concat "data:image/png;base64," resource.tracking_number.qr_code}} class="w-12 h-12" alt={{resource.public_id}} />
17
+ </div>
18
+ </div>
19
+ </:image>
20
+ <:default>
21
+ <div class="text-sm font-semibold">{{n-a resource.tracking}}</div>
22
+ <div class="flex flex-row space-x-1">
23
+ <Badge @status={{@resource.status}} />
24
+ {{#if @resource.dispatched_at}}
25
+ <Badge @status="dispatched">{{concat "Dispatched at " @resource.dispatchedAt}}</Badge>
26
+ {{/if}}
27
+ </div>
28
+ <div class="mt-1">
29
+ <div class="text-xs text-gray-400 dark:text-gray-500">Date Created: {{@resource.createdAt}}</div>
30
+ <div class="text-xs text-gray-400 dark:text-gray-500">Type: {{smart-humanize @resource.type}}</div>
31
+ </div>
32
+ </:default>
33
+ </Pill>
34
+ {{/let}}
@@ -0,0 +1,3 @@
1
+ import Component from '@glimmer/component';
2
+
3
+ export default class OrderPillComponent extends Component {}
@@ -142,7 +142,7 @@ export default class OrderConfigManagerCustomFieldsComponent extends Component {
142
142
  });
143
143
 
144
144
  this.modalsManager.show('modals/custom-field-group-form', {
145
- title: this.intl.t('fleet-ops.component.modals.new-custom-field-group.modal-title'),
145
+ title: this.intl.t('modals.new-custom-field-group.modal-title'),
146
146
  acceptButtonIcon: 'check',
147
147
  acceptButtonIconPrefix: 'fas',
148
148
  declineButtonIcon: 'times',
@@ -0,0 +1,339 @@
1
+ <div class="positions-replay-component" ...attributes>
2
+ <div class="fleetbase-leaflet-map-container map-wrapper relative" {{set-height (or @mapHeight 300)}}>
3
+ {{#if this.loadPositions.isRunning}}
4
+ <div class="absolute inset-0 h-full w-full">
5
+ <div class="flex items-center justify-center h-full">
6
+ <Spinner />
7
+ </div>
8
+ </div>
9
+ {{/if}}
10
+ <div class="next-map-container">
11
+ <LeafletMap @lat={{this.latitude}} @lng={{this.longitude}} @zoom={{this.zoom}} @onLoad={{this.didLoadMap}} class="next-leaflet-container-map positions-replay-map" as |layers|>
12
+ <layers.tile @url={{this.tileUrl}} />
13
+ {{#if this.resource}}
14
+ <layers.tracking-marker
15
+ @id={{this.resource.id}}
16
+ @publicId={{this.resource.public_id}}
17
+ @location={{point-to-coordinates this.resource.location}}
18
+ @rotationAngle={{or this.resource.heading 0}}
19
+ @icon={{icon iconUrl=(or this.resource.avatar_url this.resource.photo_url (config "defaultValues.vehicleAvatar")) iconSize=(array 28 28)}}
20
+ @onAdd={{fn this.onTrackingMarkerAdded this.resource}}
21
+ @draggable={{false}}
22
+ as |marker|
23
+ >
24
+ <marker.popup @permanent={{false}} @sticky={{true}}>
25
+ <div class="flex flex-row">
26
+ <div class="w-14 mr-2">
27
+ <img src={{or this.resource.photo_url this.resource.avatar_url}} alt={{this.resourceName}} class="rounded-md w-14 h-12 shadow-sm" />
28
+ </div>
29
+ <div class="flex-1">
30
+ <div class="text-xs font-semibold">{{this.resourceName}}</div>
31
+ <div class="text-xs">ID: {{n-a this.resource.public_id}}</div>
32
+ {{#if (eq this.resourceType "vehicle")}}
33
+ <div class="text-xs">Serial No: {{n-a this.resource.serial_number this.resource.vin this.resource.internal_id this.resource.id}}</div>
34
+ <div class="text-xs">Driver: {{n-a this.resource.driver_name}}</div>
35
+ {{/if}}
36
+ <div class="text-xs">Status:
37
+ <span class="{{if this.resource.online 'text-green-500' 'text-red-400'}}">{{if this.resource.online "Online" "Offline"}}</span></div>
38
+ <div class="text-xs truncate">Pos: {{point-coordinates this.resource.location}}</div>
39
+ </div>
40
+ </div>
41
+ </marker.popup>
42
+ <marker.tooltip @permanent={{false}} @sticky={{true}}>
43
+ <div class="flex items-center space-x-1">
44
+ <div class="text-xs font-semibold">{{this.resource.displayName}}</div>
45
+ <div>•</div>
46
+ <div class="text-xs {{if this.resource.online 'text-green-500' 'text-red-400'}}">{{if this.resource.online "Online" "Offline"}}</div>
47
+ </div>
48
+ <div class="text-xs">ID: {{n-a this.resource.public_id}}</div>
49
+ {{#if (eq this.resourceType "vehicle")}}
50
+ <div class="text-xs">Serial No: {{or this.resource.serial_number this.resource.vin this.resource.internal_id this.resource.id "-"}}</div>
51
+ {{/if}}
52
+ <div class="text-xs truncate"><FaIcon @icon="location-dot" @size="xs" class="mr-0.5" />{{point-coordinates this.resource.location}}</div>
53
+ </marker.tooltip>
54
+ </layers.tracking-marker>
55
+ {{/if}}
56
+ {{#each this.positions as |position index|}}
57
+ <layers.circle-marker
58
+ @location={{array position.latitude position.longitude}}
59
+ @radius={{3}}
60
+ @color="#3b82f6"
61
+ @fillColor="#3b82f6"
62
+ @fillOpacity={{0.6}}
63
+ @onClick={{fn this.onPositionClicked position}}
64
+ as |marker|
65
+ >
66
+ <marker.popup>
67
+ <div class="text-xs">
68
+ <div><strong>Position {{add index 1}}</strong></div>
69
+ <div>Time: {{position.timestamp}}</div>
70
+ <div>Speed: {{position.speedKmh}} km/h</div>
71
+ <div>Heading: {{or position.heading "N/A"}}°</div>
72
+ <div>Altitude: {{or position.altitude "N/A"}} m</div>
73
+ </div>
74
+ </marker.popup>
75
+ </layers.circle-marker>
76
+ {{/each}}
77
+ </LeafletMap>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="positions-replay-filters px-3 pt-2">
82
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
83
+ <div class="filter-group relative">
84
+ <label class="block uppercase tracking-wide text-xs font-medium text-gray-700 dark:text-gray-400 mb-0.5">
85
+ Date Range
86
+ </label>
87
+ <DatePicker
88
+ @value={{this.dateFilter}}
89
+ @range={{true}}
90
+ @onSelect={{this.onDateRangeChanged}}
91
+ @autoClose={{true}}
92
+ @placeholder="Select date range"
93
+ class="w-full form-input form-input-sm"
94
+ />
95
+ </div>
96
+
97
+ <div class="filter-group">
98
+ <label class="block uppercase tracking-wide text-xs font-medium text-gray-700 dark:text-gray-400 mb-0.5">
99
+ Order
100
+ </label>
101
+ <ModelSelect
102
+ @modelName="order"
103
+ @selectedModel={{this.selectedOrder}}
104
+ @query={{this.orderFilters}}
105
+ @placeholder="Filter by order"
106
+ @triggerClass="form-select form-input form-input-sm"
107
+ @infiniteScroll={{false}}
108
+ @renderInPlace={{true}}
109
+ @onChange={{this.onOrderSelected}}
110
+ as |order|
111
+ >
112
+ <div class="flex items-center justify-between">
113
+ <span>{{order.tracking}}</span>
114
+ <span class="text-xs text-gray-500">{{order.createdAt}}</span>
115
+ </div>
116
+ </ModelSelect>
117
+ </div>
118
+
119
+ <div class="filter-actions flex items-end space-x-2">
120
+ <Button @type="danger" @icon="times" @text={{t "common.clear"}} @disabled={{this.loadPositions.isRunning}} @onClick={{this.clearFilters}} />
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ <div class="positions-replay-map-container border-b border-gray-200 dark:border-gray-700">
126
+ <div class="replay-controls px-3 py-2">
127
+ <div class="flex items-center justify-between space-x-2">
128
+ <div class="flex items-center space-x-2">
129
+ <Button @type="danger" @text="Stop" @icon="stop" @size="xs" @disabled={{not (or this.isReplaying this.isPaused)}} @onClick={{this.stopReplay}} />
130
+
131
+ {{#if (or this.isReplaying this.isPaused)}}
132
+ {{#if this.isReplaying}}
133
+ <Button @type="warning" @text="Pause" @icon="pause" @size="xs" @onClick={{this.pauseReplay}} />
134
+ {{else}}
135
+ <Button @type="success" @text="Resume" @icon="play" @size="xs" @onClick={{this.startReplay}} />
136
+ {{/if}}
137
+ {{else}}
138
+ <Button @type="success" @text="Play" @icon="play" @size="xs" @disabled={{not this.hasPositions}} @onClick={{this.startReplay}} />
139
+ {{/if}}
140
+
141
+ <div class="step-controls flex items-center space-x-1 border-l border-gray-300 dark:border-gray-700 pl-2 ml-2">
142
+ <Button @type="default" @icon="backward-step" @size="xs" @disabled={{not this.hasPositions}} @onClick={{this.stepBackward}} title="Step backward" />
143
+ <Button @type="default" @icon="forward-step" @size="xs" @disabled={{not this.hasPositions}} @onClick={{this.stepForward}} title="Step forward" />
144
+ </div>
145
+
146
+ <div class="speed-control border-l border-gray-300 dark:border-gray-700 pl-2 ml-2">
147
+ <label class="text-xs uppercase tracking-wide font-medium text-gray-700 dark:text-gray-400 mr-1">
148
+ Speed:
149
+ </label>
150
+ <Select
151
+ @value={{this.replaySpeed}}
152
+ @options={{this.speedOptions}}
153
+ @optionValue="value"
154
+ @optionLabel="label"
155
+ @onSelect={{this.onSpeedChanged}}
156
+ class="form-select speed-select"
157
+ />
158
+ </div>
159
+ </div>
160
+
161
+ {{#if (or this.isReplaying this.isPaused)}}
162
+ <div class="replay-progress flex items-center space-x-1">
163
+ <span class="text-sm text-gray-600 dark:text-gray-400">
164
+ {{this.currentReplayIndex}}/{{this.totalPositions}}
165
+ </span>
166
+ <div class="progress-bar w-32 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
167
+ <div class="progress-fill h-full bg-blue-600 transition-all duration-300" style={{this.replayProgressWidth}}></div>
168
+ </div>
169
+ <span class="text-sm font-medium text-gray-700 dark:text-gray-300">
170
+ {{this.replayProgress}}%
171
+ </span>
172
+ </div>
173
+ {{/if}}
174
+ </div>
175
+ </div>
176
+ </div>
177
+
178
+ {{#if this.hasPositions}}
179
+ <div class="flex flex-col border-b border-gray-200 dark:border-gray-700">
180
+ <div class="px-3 mb-1">
181
+ <h3 class="text-[11px] uppercase tracking-wide text-gray-500 font-semibold">Position Metrics</h3>
182
+ </div>
183
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-2 px-2 pb-2">
184
+ <div class="metric-card">
185
+ <div class="metric-label truncate text-xs text-gray-500 dark:text-gray-400">Total Distance</div>
186
+ <div class="metric-value font-semibold text-blue-600 dark:text-blue-400">
187
+ {{this.totalDistance}}
188
+ km
189
+ </div>
190
+ </div>
191
+ <div class="metric-card">
192
+ <div class="metric-label truncate text-xs text-gray-500 dark:text-gray-400">Duration</div>
193
+ <div class="metric-value font-semibold text-green-600 dark:text-green-400">
194
+ {{this.formattedDuration}}
195
+ </div>
196
+ </div>
197
+ <div class="metric-card">
198
+ <div class="metric-label truncate text-xs text-gray-500 dark:text-gray-400">Max Speed</div>
199
+ <div class="metric-value font-semibold text-orange-600 dark:text-orange-400">
200
+ {{this.maxSpeed}}
201
+ km/h
202
+ </div>
203
+ </div>
204
+ <div class="metric-card">
205
+ <div class="metric-label truncate text-xs text-gray-500 dark:text-gray-400">Avg Speed</div>
206
+ <div class="metric-value font-semibold text-purple-600 dark:text-purple-400">
207
+ {{this.avgSpeed}}
208
+ km/h
209
+ </div>
210
+ </div>
211
+ <div class="metric-card">
212
+ <div class="metric-label truncate text-xs text-gray-500 dark:text-gray-400">Speeding Events</div>
213
+ <div class="metric-value font-semibold text-red-600 dark:text-red-400">
214
+ {{this.speedingCount}}
215
+ </div>
216
+ </div>
217
+ <div class="metric-card">
218
+ <div class="metric-label truncate text-xs text-gray-500 dark:text-gray-400">Dwell Times</div>
219
+ <div class="metric-value font-semibold text-yellow-600 dark:text-yellow-400">
220
+ {{this.dwellCount}}
221
+ </div>
222
+ </div>
223
+ <div class="metric-card">
224
+ <div class="metric-label truncate text-xs text-gray-500 dark:text-gray-400">Acceleration Events</div>
225
+ <div class="metric-value font-semibold text-indigo-600 dark:text-indigo-400">
226
+ {{this.accelerationCount}}
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ {{/if}}
232
+
233
+ {{!-- {{#if this.hasPositions}}
234
+ <div class="positions-replay-timeline">
235
+ <div class="px-3">
236
+ <h3 class="text-[11px] uppercase tracking-wide text-gray-500 font-semibold">Position Timeline</h3>
237
+ </div>
238
+ <div class="timeline-container overflow-x-auto">
239
+ <div class="timeline-track flex space-x-2 pb-4">
240
+ {{#each this.positions as |position index|}}
241
+ <div
242
+ class="timeline-item flex-shrink-0 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded transition-colors
243
+ {{if (eq index this.currentReplayIndex) 'bg-blue-100 dark:bg-blue-900'}}"
244
+ role="button"
245
+ {{on "click" (fn this.onPositionClicked position)}}
246
+ >
247
+ <div class="timeline-marker w-3 h-3 rounded-full bg-blue-600 mx-auto mb-1"></div>
248
+ <div class="timeline-content text-xs text-center whitespace-nowrap">
249
+ <div class="font-medium">{{add index 1}}</div>
250
+ <div class="text-gray-500 dark:text-gray-400">{{position.speedKmh}} km/h</div>
251
+ <div class="text-gray-400 dark:text-gray-500 text-xxs">
252
+ {{format-date position.created_at "HH:mm:ss"}}
253
+ </div>
254
+ </div>
255
+ </div>
256
+ {{/each}}
257
+ </div>
258
+ </div>
259
+ </div>
260
+ {{/if}} --}}
261
+
262
+ {{#if this.hasPositions}}
263
+ <div class="positions-replay-table">
264
+ <div class="px-3">
265
+ <h3 class="text-[11px] uppercase tracking-wide text-gray-500 font-semibold">Position Data</h3>
266
+ </div>
267
+ <div class="overflow-x-auto">
268
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
269
+ <thead class="bg-gray-50 dark:bg-gray-900">
270
+ <tr>
271
+ <th class="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
272
+ #
273
+ </th>
274
+ <th class="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
275
+ Timestamp
276
+ </th>
277
+ <th class="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
278
+ Latitude
279
+ </th>
280
+ <th class="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
281
+ Longitude
282
+ </th>
283
+ <th class="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
284
+ Speed (km/h)
285
+ </th>
286
+ <th class="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
287
+ Heading
288
+ </th>
289
+ <th class="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
290
+ Altitude (m)
291
+ </th>
292
+ </tr>
293
+ </thead>
294
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
295
+ {{#each this.positions as |position index|}}
296
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 {{if (eq index this.currentReplayIndex) 'bg-blue-50 dark:bg-blue-900'}}">
297
+ <td class="px-2 py-1 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
298
+ {{add index 1}}
299
+ </td>
300
+ <td class="px-2 py-1 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
301
+ {{position.timestamp}}
302
+ </td>
303
+ <td class="px-2 py-1 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
304
+ {{format-number position.latitude minimumFractionDigits=6 maximumFractionDigits=6}}
305
+ </td>
306
+ <td class="px-2 py-1 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
307
+ {{format-number position.longitude minimumFractionDigits=6 maximumFractionDigits=6}}
308
+ </td>
309
+ <td class="px-2 py-1 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
310
+ {{position.speedKmh}}
311
+ </td>
312
+ <td class="px-2 py-1 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
313
+ {{or position.heading "N/A"}}
314
+ </td>
315
+ <td class="px-2 py-1 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
316
+ {{or position.altitude "N/A"}}
317
+ </td>
318
+ </tr>
319
+ {{/each}}
320
+ </tbody>
321
+ </table>
322
+ </div>
323
+ </div>
324
+ {{/if}}
325
+
326
+ {{#unless this.hasPositions}}
327
+ {{#if this.loadPositions.isIdle}}
328
+ <div class="empty-state bg-white dark:bg-gray-800 rounded-lg shadow-sm p-12 text-center">
329
+ <FaIcon @icon="map-marked-alt" @size="2x" class="text-gray-400 mb-2" />
330
+ <h3 class="text-base font-semibold text-gray-700 dark:text-gray-300">
331
+ No Positions Found
332
+ </h3>
333
+ <p class="text-sm text-gray-500 dark:text-gray-400">
334
+ Try adjusting your filters or select a different date range.
335
+ </p>
336
+ </div>
337
+ {{/if}}
338
+ {{/unless}}
339
+ </div>