@fleetbase/fleetops-engine 0.6.19 → 0.6.21

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 (130) hide show
  1. package/addon/components/custom-entity/form.hbs +14 -14
  2. package/addon/components/device/details.hbs +92 -43
  3. package/addon/components/device/form.hbs +108 -60
  4. package/addon/components/device/form.js +36 -8
  5. package/addon/components/device/panel-header.hbs +32 -0
  6. package/addon/components/device/panel-header.js +3 -0
  7. package/addon/components/driver/form.hbs +1 -1
  8. package/addon/components/driver/form.js +49 -47
  9. package/addon/components/entity/form.hbs +7 -5
  10. package/addon/components/layout/fleet-ops-sidebar.js +12 -12
  11. package/addon/components/map/drawer/device-event-listing.hbs +58 -0
  12. package/addon/components/map/drawer/device-event-listing.js +181 -0
  13. package/addon/components/map/drawer/position-listing.hbs +84 -0
  14. package/addon/components/map/drawer/position-listing.js +289 -0
  15. package/addon/components/map/drawer.js +2 -0
  16. package/addon/components/map/leaflet-live-map.hbs +7 -2
  17. package/addon/components/order/details/payload.hbs +6 -4
  18. package/addon/components/order/details/payload.js +2 -0
  19. package/addon/components/order/kanban.hbs +12 -10
  20. package/addon/components/order/kanban.js +27 -3
  21. package/addon/components/order-config-manager/custom-fields.js +1 -1
  22. package/addon/components/positions-replay.hbs +333 -0
  23. package/addon/components/positions-replay.js +372 -0
  24. package/addon/components/sensor/details.hbs +64 -38
  25. package/addon/components/sensor/form.hbs +112 -63
  26. package/addon/components/sensor/form.js +36 -24
  27. package/addon/components/sensor/panel-header.hbs +32 -0
  28. package/addon/components/sensor/panel-header.js +3 -0
  29. package/addon/components/telematic/details.hbs +40 -16
  30. package/addon/components/telematic/form.hbs +63 -64
  31. package/addon/components/telematic/form.js +73 -4
  32. package/addon/components/vehicle/card.hbs +1 -1
  33. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  34. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  35. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  36. package/addon/controllers/connectivity/devices/index.js +51 -9
  37. package/addon/controllers/connectivity/events/index.js +65 -16
  38. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  39. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  40. package/addon/controllers/connectivity/sensors/index.js +66 -6
  41. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  42. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  43. package/addon/controllers/connectivity/telematics/index.js +20 -11
  44. package/addon/controllers/management/fleets/index/details.js +26 -21
  45. package/addon/controllers/management/fleets/index/edit.js +9 -6
  46. package/addon/controllers/management/vehicles/index/details.js +21 -13
  47. package/addon/controllers/operations/orders/index/new.js +4 -2
  48. package/addon/controllers/operations/orders/index.js +50 -45
  49. package/addon/controllers/settings/custom-fields.js +6 -0
  50. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  51. package/addon/routes/connectivity/devices/index/details.js +27 -1
  52. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  53. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  54. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  55. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  56. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  57. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  58. package/addon/routes/operations/orders/index.js +0 -3
  59. package/addon/routes.js +1 -0
  60. package/addon/services/movement-tracker.js +81 -30
  61. package/addon/services/order-creation.js +4 -8
  62. package/addon/services/order-validation.js +3 -3
  63. package/addon/styles/fleetops-engine.css +192 -0
  64. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  65. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  66. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  67. package/addon/templates/connectivity/events/index.hbs +1 -1
  68. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  69. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  70. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  71. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  72. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  73. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  74. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  75. package/addon/templates/operations/orders/index.hbs +26 -2
  76. package/addon/utils/fleet-ops-options.js +95 -0
  77. package/addon/utils/setup-customer-portal.js +7 -0
  78. package/app/components/device/panel-header.js +1 -0
  79. package/app/components/map/drawer/device-event-listing.js +1 -0
  80. package/app/components/map/drawer/position-listing.js +1 -0
  81. package/app/components/positions-replay.js +1 -0
  82. package/app/components/sensor/panel-header.js +1 -0
  83. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  84. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  85. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  86. package/composer.json +1 -1
  87. package/extension.json +1 -1
  88. package/package.json +4 -4
  89. package/server/config/telematics.php +111 -0
  90. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  91. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  92. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  93. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  94. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  95. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  96. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  97. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  98. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  99. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  100. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  101. package/server/src/Http/Controllers/Internal/v1/OrderController.php +50 -68
  102. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  103. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  104. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  105. package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
  106. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  107. package/server/src/Http/Filter/PositionFilter.php +35 -0
  108. package/server/src/Http/Resources/v1/Position.php +44 -0
  109. package/server/src/Jobs/ReplayPositions.php +64 -0
  110. package/server/src/Jobs/SendPositionReplay.php +65 -0
  111. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  112. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  113. package/server/src/Models/Device.php +72 -10
  114. package/server/src/Models/DeviceEvent.php +7 -0
  115. package/server/src/Models/Driver.php +28 -1
  116. package/server/src/Models/Payload.php +11 -3
  117. package/server/src/Models/Place.php +9 -2
  118. package/server/src/Models/Position.php +17 -17
  119. package/server/src/Models/Sensor.php +78 -13
  120. package/server/src/Models/Telematic.php +116 -6
  121. package/server/src/Models/Vehicle.php +104 -1
  122. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  123. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  124. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  125. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  126. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  127. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  128. package/server/src/Support/Telematics/TelematicService.php +223 -0
  129. package/server/src/Support/Utils.php +1 -1
  130. package/server/src/routes.php +12 -1
@@ -3,6 +3,10 @@
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\FleetOps\Casts\Point;
7
+ use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
8
+ use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
9
+ use Fleetbase\Models\File;
6
10
  use Fleetbase\Models\Model;
7
11
  use Fleetbase\Models\User;
8
12
  use Fleetbase\Traits\HasApiModelBehavior;
@@ -15,6 +19,8 @@ use Fleetbase\Traits\TracksApiCredential;
15
19
  use Illuminate\Database\Eloquent\Relations\BelongsTo;
16
20
  use Illuminate\Database\Eloquent\Relations\HasMany;
17
21
  use Illuminate\Database\Eloquent\Relations\MorphTo;
22
+ use Illuminate\Support\Arr;
23
+ use Illuminate\Support\Str;
18
24
  use Spatie\Activitylog\LogOptions;
19
25
  use Spatie\Activitylog\Traits\LogsActivity;
20
26
  use Spatie\Sluggable\HasSlug;
@@ -37,6 +43,7 @@ class Device extends Model
37
43
  use HasMetaAttributes;
38
44
  use Searchable;
39
45
  use HasCustomFields;
46
+ use SpatialTrait;
40
47
 
41
48
  /**
42
49
  * The database table used by the model.
@@ -57,7 +64,7 @@ class Device extends Model
57
64
  *
58
65
  * @var array
59
66
  */
60
- protected $searchableColumns = ['name', 'model', 'serial_number', 'public_id'];
67
+ protected $searchableColumns = ['name', 'model', 'serial_number', 'manufacturer', 'public_id'];
61
68
 
62
69
  /**
63
70
  * The attributes that can be used for filtering.
@@ -75,14 +82,20 @@ class Device extends Model
75
82
  'company_uuid',
76
83
  'telematic_uuid',
77
84
  'warranty_uuid',
78
- 'device_type',
85
+ 'photo_uuid',
86
+ 'type',
79
87
  'device_id',
80
- 'device_provider',
81
- 'device_name',
82
- 'device_model',
83
- 'device_location',
88
+ 'internal_id',
89
+ 'imei',
90
+ 'imsi',
91
+ 'firmware_version',
92
+ 'provider',
93
+ 'name',
94
+ 'model',
95
+ 'location',
84
96
  'manufacturer',
85
97
  'serial_number',
98
+ 'last_position',
86
99
  'installation_date',
87
100
  'last_maintenance_date',
88
101
  'meta',
@@ -110,6 +123,7 @@ class Device extends Model
110
123
  'is_online',
111
124
  'attached_to_name',
112
125
  'connection_status',
126
+ 'photo_url',
113
127
  ];
114
128
 
115
129
  /**
@@ -117,7 +131,14 @@ class Device extends Model
117
131
  *
118
132
  * @var array
119
133
  */
120
- protected $hidden = ['warranty', 'telematic', 'attachable'];
134
+ protected $hidden = [];
135
+
136
+ /**
137
+ * The attributes that are spatial columns.
138
+ *
139
+ * @var array
140
+ */
141
+ protected $spatialFields = ['last_position'];
121
142
 
122
143
  /**
123
144
  * The attributes that should be cast to native types.
@@ -125,9 +146,10 @@ class Device extends Model
125
146
  * @var array
126
147
  */
127
148
  protected $casts = [
128
- 'last_online_at' => 'datetime',
129
- 'meta' => Json::class,
130
- 'options' => Json::class,
149
+ 'last_online_at' => 'datetime',
150
+ 'last_position' => Point::class,
151
+ 'meta' => Json::class,
152
+ 'options' => Json::class,
131
153
  ];
132
154
 
133
155
  /**
@@ -204,6 +226,21 @@ class Device extends Model
204
226
  return $this->hasMany(Sensor::class, 'device_uuid', 'uuid');
205
227
  }
206
228
 
229
+ public function photo(): BelongsTo
230
+ {
231
+ return $this->belongsTo(File::class);
232
+ }
233
+
234
+ /**
235
+ * Get photo URL attribute.
236
+ *
237
+ * @return string
238
+ */
239
+ public function getPhotoUrlAttribute()
240
+ {
241
+ return data_get($this, 'photo.url', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/image-file-icon.png');
242
+ }
243
+
207
244
  /**
208
245
  * Get the warranty name.
209
246
  */
@@ -432,4 +469,29 @@ class Device extends Model
432
469
  ->limit($limit)
433
470
  ->get();
434
471
  }
472
+
473
+ /**
474
+ * Creates a new position for the vehicle.
475
+ */
476
+ public function createPosition(array $attributes = [], Model|string|null $destination = null): ?Position
477
+ {
478
+ if (!isset($attributes['coordinates']) && isset($attributes['location'])) {
479
+ $attributes['coordinates'] = $attributes['location'];
480
+ }
481
+
482
+ if (!isset($attributes['coordinates']) && isset($attributes['latitude']) && isset($attributes['longitude'])) {
483
+ $attributes['coordinates'] = new SpatialPoint($attributes['latitude'], $attributes['longitude']);
484
+ }
485
+
486
+ // handle destination if set
487
+ $destinationUuid = Str::isUuid($destination) ? $destination : data_get($destination, 'uuid');
488
+
489
+ return Position::create([
490
+ ...Arr::only($attributes, ['coordinates', 'heading', 'bearing', 'speed', 'altitude', 'order_uuid']),
491
+ 'subject_uuid' => $this->uuid,
492
+ 'subject_type' => $this->getMorphClass(),
493
+ 'company_uuid' => $this->company_uuid,
494
+ 'destination_uuid' => $destinationUuid,
495
+ ]);
496
+ }
435
497
  }
@@ -4,6 +4,7 @@ namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
6
  use Fleetbase\Models\Alert;
7
+ use Fleetbase\Models\Company;
7
8
  use Fleetbase\Models\Model;
8
9
  use Fleetbase\Models\User;
9
10
  use Fleetbase\Traits\HasApiModelBehavior;
@@ -67,6 +68,7 @@ class DeviceEvent extends Model
67
68
  * @var array
68
69
  */
69
70
  protected $fillable = [
71
+ 'company_uuid',
70
72
  'device_uuid',
71
73
  'payload',
72
74
  'meta',
@@ -143,6 +145,11 @@ class DeviceEvent extends Model
143
145
  return LogOptions::defaults()->logAll();
144
146
  }
145
147
 
148
+ public function company(): BelongsTo
149
+ {
150
+ return $this->belongsTo(Company::class, 'company_uuid', 'uuid');
151
+ }
152
+
146
153
  public function device(): BelongsTo
147
154
  {
148
155
  return $this->belongsTo(Device::class, 'device_uuid', 'uuid');
@@ -8,6 +8,7 @@ use Fleetbase\FleetOps\Scopes\DriverScope;
8
8
  use Fleetbase\FleetOps\Support\Utils;
9
9
  use Fleetbase\FleetOps\Support\Utils as FleetOpsUtils;
10
10
  use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
11
+ use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
11
12
  use Fleetbase\Models\File;
12
13
  use Fleetbase\Models\Model;
13
14
  use Fleetbase\Models\User;
@@ -24,6 +25,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
24
25
  use Illuminate\Database\Eloquent\Relations\HasMany;
25
26
  use Illuminate\Database\Eloquent\Relations\HasManyThrough;
26
27
  use Illuminate\Notifications\Notifiable;
28
+ use Illuminate\Support\Arr;
27
29
  use Illuminate\Support\Collection;
28
30
  use Illuminate\Support\Facades\DB;
29
31
  use Illuminate\Support\Str;
@@ -645,7 +647,7 @@ class Driver extends Model
645
647
  $positionData = [
646
648
  'company_uuid' => session('company', $this->company_uuid),
647
649
  'subject_uuid' => $this->uuid,
648
- 'subject_type' => get_class($this),
650
+ 'subject_type' => $this->getMorphClass(),
649
651
  'coordinates' => $this->location,
650
652
  'altitude' => $this->altitude,
651
653
  'heading' => $this->heading,
@@ -661,6 +663,31 @@ class Driver extends Model
661
663
  return ($isFirstPosition || $isPast50Meters) ? Position::create($positionData) : null;
662
664
  }
663
665
 
666
+ /**
667
+ * Creates a new position for the vehicle.
668
+ */
669
+ public function createPosition(array $attributes = [], Model|string|null $destination = null): ?Position
670
+ {
671
+ if (!isset($attributes['coordinates']) && isset($attributes['location'])) {
672
+ $attributes['coordinates'] = $attributes['location'];
673
+ }
674
+
675
+ if (!isset($attributes['coordinates']) && isset($attributes['latitude']) && isset($attributes['longitude'])) {
676
+ $attributes['coordinates'] = new SpatialPoint($attributes['latitude'], $attributes['longitude']);
677
+ }
678
+
679
+ // handle destination if set
680
+ $destinationUuid = Str::isUuid($destination) ? $destination : data_get($destination, 'uuid');
681
+
682
+ return Position::create([
683
+ ...Arr::only($attributes, ['coordinates', 'heading', 'bearing', 'speed', 'altitude', 'order_uuid']),
684
+ 'subject_uuid' => $this->uuid,
685
+ 'subject_type' => $this->getMorphClass(),
686
+ 'company_uuid' => $this->company_uuid,
687
+ 'destination_uuid' => $destinationUuid,
688
+ ]);
689
+ }
690
+
664
691
  /**
665
692
  * Get the user relationship from the driver.
666
693
  */
@@ -368,7 +368,15 @@ class Payload extends Model
368
368
  ) {
369
369
  $placeUuid = $attributes['place_uuid'];
370
370
 
371
- // Path 2: public_id under "id" -> resolve to uuid
371
+ // Path 2: public_id under "uuid" -> resolve to uuid
372
+ } elseif (
373
+ is_array($attributes)
374
+ && isset($attributes['uuid'])
375
+ && ($resolvedUuid = Place::where('uuid', $attributes['uuid'])->value('uuid'))
376
+ ) {
377
+ $placeUuid = $resolvedUuid;
378
+
379
+ // Path 3: public_id under "id" -> resolve to uuid
372
380
  } elseif (
373
381
  is_array($attributes)
374
382
  && isset($attributes['id'])
@@ -376,7 +384,7 @@ class Payload extends Model
376
384
  ) {
377
385
  $placeUuid = $resolvedUuid;
378
386
 
379
- // Path 3: create from mixed payload
387
+ // Path 4: create from mixed payload
380
388
  } else {
381
389
  $place = Place::createFromMixed($attributes);
382
390
 
@@ -420,7 +428,7 @@ class Payload extends Model
420
428
  // -------- Upsert Waypoint --------
421
429
  // Uniqueness: payload + place + order for deterministic row per position.
422
430
  $unique = [
423
- 'payload_uuid' => $this->payload_uuid,
431
+ 'payload_uuid' => $this->uuid,
424
432
  'place_uuid' => $placeUuid,
425
433
  'order' => $index,
426
434
  ];
@@ -362,7 +362,12 @@ class Place extends Model
362
362
  $results = \Geocoder\Laravel\Facades\Geocoder::geocode($address)->get();
363
363
 
364
364
  if ($results->isEmpty() || !$results->first()) {
365
- return (new static())->newInstance(['street1' => $address]);
365
+ $place = (new static())->newInstance(['street1' => $address, 'location' => new SpatialPoint(0, 0)]);
366
+ if ($saveInstance) {
367
+ $place->save();
368
+ }
369
+
370
+ return $place;
366
371
  }
367
372
 
368
373
  return static::createFromGoogleAddress($results->first(), $saveInstance);
@@ -550,8 +555,10 @@ class Place extends Model
550
555
  }
551
556
 
552
557
  // If has $attributes['address']
558
+ $address = $place['address'];
553
559
  if (!empty($place['address'])) {
554
- return static::createFromGeocodingLookup($place['address'], $saveInstance);
560
+ // return static::createFromGeocodingLookup($place['address'], $saveInstance);
561
+ return static::create(array_merge($place, static::getValuesFromGeocodingLookup($address)));
555
562
  }
556
563
 
557
564
  // Perform google lookup to fill address
@@ -7,6 +7,8 @@ use Fleetbase\Models\Model;
7
7
  use Fleetbase\Traits\HasApiModelBehavior;
8
8
  use Fleetbase\Traits\HasUuid;
9
9
  use Fleetbase\Traits\TracksApiCredential;
10
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
11
+ use Illuminate\Database\Eloquent\Relations\MorphTo;
10
12
 
11
13
  class Position extends Model
12
14
  {
@@ -48,7 +50,7 @@ class Position extends Model
48
50
  *
49
51
  * @var array
50
52
  */
51
- protected $appends = [];
53
+ protected $appends = ['latitude', 'longitude'];
52
54
 
53
55
  /**
54
56
  * Get filter parameters for this model.
@@ -85,35 +87,33 @@ class Position extends Model
85
87
  */
86
88
  protected static $logName = 'position';
87
89
 
88
- /**
89
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
90
- */
91
- public function company()
90
+ public function company(): BelongsTo
92
91
  {
93
92
  return $this->belongsTo(\Fleetbase\Models\Company::class);
94
93
  }
95
94
 
96
- /**
97
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
98
- */
99
- public function order()
95
+ public function order(): BelongsTo
100
96
  {
101
97
  return $this->belongsTo(Order::class);
102
98
  }
103
99
 
104
- /**
105
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
106
- */
107
- public function destination()
100
+ public function destination(): BelongsTo
108
101
  {
109
102
  return $this->belongsTo(Place::class);
110
103
  }
111
104
 
112
- /**
113
- * @return \Illuminate\Database\Eloquent\Relations\MorphTo
114
- */
115
- public function subject()
105
+ public function subject(): MorphTo
116
106
  {
117
107
  return $this->morphTo(__FUNCTION__, 'subject_type', 'subject_uuid')->withoutGlobalScopes();
118
108
  }
109
+
110
+ public function getLongitudeAttribute(): float
111
+ {
112
+ return $this->coordinates?->getLng() ?? 0;
113
+ }
114
+
115
+ public function getLatitudeAttribute(): float
116
+ {
117
+ return $this->coordinates?->getLat() ?? 0;
118
+ }
119
119
  }
@@ -3,7 +3,11 @@
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\FleetOps\Casts\Point;
7
+ use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
8
+ use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
6
9
  use Fleetbase\Models\Alert;
10
+ use Fleetbase\Models\File;
7
11
  use Fleetbase\Models\Model;
8
12
  use Fleetbase\Models\User;
9
13
  use Fleetbase\Traits\HasApiModelBehavior;
@@ -16,6 +20,8 @@ use Fleetbase\Traits\TracksApiCredential;
16
20
  use Illuminate\Database\Eloquent\Relations\BelongsTo;
17
21
  use Illuminate\Database\Eloquent\Relations\HasMany;
18
22
  use Illuminate\Database\Eloquent\Relations\MorphTo;
23
+ use Illuminate\Support\Arr;
24
+ use Illuminate\Support\Str;
19
25
  use Spatie\Activitylog\LogOptions;
20
26
  use Spatie\Activitylog\Traits\LogsActivity;
21
27
  use Spatie\Sluggable\HasSlug;
@@ -39,6 +45,7 @@ class Sensor extends Model
39
45
  use HasMetaAttributes;
40
46
  use Searchable;
41
47
  use HasCustomFields;
48
+ use SpatialTrait;
42
49
 
43
50
  /**
44
51
  * The database table used by the model.
@@ -77,8 +84,16 @@ class Sensor extends Model
77
84
  'company_uuid',
78
85
  'device_uuid',
79
86
  'warranty_uuid',
87
+ 'telematic_uuid',
88
+ 'photo_uuid',
80
89
  'name',
81
- 'sensor_type',
90
+ 'type',
91
+ 'internal_id',
92
+ 'imei',
93
+ 'imsi',
94
+ 'firmware_version',
95
+ 'serial_number',
96
+ 'last_position',
82
97
  'unit',
83
98
  'min_threshold',
84
99
  'max_threshold',
@@ -100,12 +115,9 @@ class Sensor extends Model
100
115
  * @var array
101
116
  */
102
117
  protected $appends = [
103
- 'device_name',
104
- 'warranty_name',
105
- 'attached_to_name',
106
118
  'is_active',
107
119
  'threshold_status',
108
- 'last_reading_formatted',
120
+ 'photo_url',
109
121
  ];
110
122
 
111
123
  /**
@@ -113,7 +125,14 @@ class Sensor extends Model
113
125
  *
114
126
  * @var array
115
127
  */
116
- protected $hidden = ['device', 'warranty', 'sensorable'];
128
+ protected $hidden = [];
129
+
130
+ /**
131
+ * The attributes that are spatial columns.
132
+ *
133
+ * @var array
134
+ */
135
+ protected $spatialFields = ['last_position'];
117
136
 
118
137
  /**
119
138
  * The attributes that should be cast to native types.
@@ -121,13 +140,14 @@ class Sensor extends Model
121
140
  * @var array
122
141
  */
123
142
  protected $casts = [
124
- 'min_threshold' => 'float',
125
- 'max_threshold' => 'float',
126
- 'threshold_inclusive' => 'boolean',
127
- 'last_reading_at' => 'datetime',
128
- 'report_frequency_sec' => 'integer',
129
- 'calibration' => Json::class,
130
- 'meta' => Json::class,
143
+ 'min_threshold' => 'float',
144
+ 'max_threshold' => 'float',
145
+ 'threshold_inclusive' => 'boolean',
146
+ 'last_reading_at' => 'datetime',
147
+ 'report_frequency_sec' => 'integer',
148
+ 'last_position' => Point::class,
149
+ 'calibration' => Json::class,
150
+ 'meta' => Json::class,
131
151
  ];
132
152
 
133
153
  /**
@@ -169,6 +189,11 @@ class Sensor extends Model
169
189
  return LogOptions::defaults()->logAll();
170
190
  }
171
191
 
192
+ public function telematic(): BelongsTo
193
+ {
194
+ return $this->belongsTo(Telematic::class, 'telematic_uuid', 'uuid');
195
+ }
196
+
172
197
  public function device(): BelongsTo
173
198
  {
174
199
  return $this->belongsTo(Device::class, 'device_uuid', 'uuid');
@@ -194,12 +219,27 @@ class Sensor extends Model
194
219
  return $this->morphTo();
195
220
  }
196
221
 
222
+ public function photo(): BelongsTo
223
+ {
224
+ return $this->belongsTo(File::class);
225
+ }
226
+
197
227
  public function alerts(): HasMany
198
228
  {
199
229
  return $this->hasMany(Alert::class, 'subject_uuid', 'uuid')
200
230
  ->where('subject_type', static::class);
201
231
  }
202
232
 
233
+ /**
234
+ * Get photo URL attribute.
235
+ *
236
+ * @return string
237
+ */
238
+ public function getPhotoUrlAttribute()
239
+ {
240
+ return data_get($this, 'photo.url', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/image-file-icon.png');
241
+ }
242
+
203
243
  /**
204
244
  * Get the device name.
205
245
  */
@@ -507,4 +547,29 @@ class Sensor extends Model
507
547
  ],
508
548
  ];
509
549
  }
550
+
551
+ /**
552
+ * Creates a new position for the vehicle.
553
+ */
554
+ public function createPosition(array $attributes = [], Model|string|null $destination = null): ?Position
555
+ {
556
+ if (!isset($attributes['coordinates']) && isset($attributes['location'])) {
557
+ $attributes['coordinates'] = $attributes['location'];
558
+ }
559
+
560
+ if (!isset($attributes['coordinates']) && isset($attributes['latitude']) && isset($attributes['longitude'])) {
561
+ $attributes['coordinates'] = new SpatialPoint($attributes['latitude'], $attributes['longitude']);
562
+ }
563
+
564
+ // handle destination if set
565
+ $destinationUuid = Str::isUuid($destination) ? $destination : data_get($destination, 'uuid');
566
+
567
+ return Position::create([
568
+ ...Arr::only($attributes, ['coordinates', 'heading', 'bearing', 'speed', 'altitude', 'order_uuid']),
569
+ 'subject_uuid' => $this->uuid,
570
+ 'subject_type' => $this->getMorphClass(),
571
+ 'company_uuid' => $this->company_uuid,
572
+ 'destination_uuid' => $destinationUuid,
573
+ ]);
574
+ }
510
575
  }
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry;
6
7
  use Fleetbase\Models\Model;
7
8
  use Fleetbase\Models\User;
8
9
  use Fleetbase\Traits\HasApiModelBehavior;
@@ -85,8 +86,8 @@ class Telematic extends Model
85
86
  'last_seen_at',
86
87
  'last_metrics',
87
88
  'config',
89
+ 'credentials',
88
90
  'meta',
89
- 'slug',
90
91
  ];
91
92
 
92
93
  /**
@@ -94,7 +95,7 @@ class Telematic extends Model
94
95
  *
95
96
  * @var array
96
97
  */
97
- protected $appends = ['warranty_name', 'is_online', 'signal_strength', 'last_location'];
98
+ protected $appends = ['warranty_name', 'is_online', 'signal_strength', 'last_location', 'provider_descriptor'];
98
99
 
99
100
  /**
100
101
  * The attributes excluded from the model's JSON form.
@@ -109,10 +110,97 @@ class Telematic extends Model
109
110
  * @var array
110
111
  */
111
112
  protected $casts = [
112
- 'last_seen_at' => 'datetime',
113
- 'last_metrics' => Json::class,
114
- 'config' => Json::class,
115
- 'meta' => Json::class,
113
+ 'last_seen_at' => 'datetime',
114
+ 'last_metrics' => Json::class,
115
+ 'config' => Json::class,
116
+ 'credentials' => Json::class,
117
+ 'meta' => Json::class,
118
+ ];
119
+
120
+ /**
121
+ * Telematic statuses.
122
+ *
123
+ * @var array
124
+ */
125
+ public static $statuses = [
126
+ [
127
+ 'key' => 'initialized',
128
+ 'label' => 'Initialized',
129
+ 'description' => 'Provider entry has been created but not yet configured.',
130
+ ],
131
+ [
132
+ 'key' => 'configured',
133
+ 'label' => 'Configured',
134
+ 'description' => 'Provider credentials and settings are valid but connection not yet tested.',
135
+ ],
136
+ [
137
+ 'key' => 'connecting',
138
+ 'label' => 'Connecting',
139
+ 'description' => 'Attempting to establish a connection with provider API.',
140
+ ],
141
+ [
142
+ 'key' => 'connected',
143
+ 'label' => 'Connected',
144
+ 'description' => 'Successfully authenticated and connected to provider API.',
145
+ ],
146
+ [
147
+ 'key' => 'synchronizing',
148
+ 'label' => 'Synchronizing',
149
+ 'description' => 'Currently syncing data (devices, vehicles, positions, etc.) from provider.',
150
+ ],
151
+ [
152
+ 'key' => 'active',
153
+ 'label' => 'Active',
154
+ 'description' => 'Integration is healthy and data syncs are occurring normally.',
155
+ ],
156
+ [
157
+ 'key' => 'degraded',
158
+ 'label' => 'Degraded',
159
+ 'description' => 'Integration partially working; intermittent errors or missing data.',
160
+ ],
161
+ [
162
+ 'key' => 'disconnected',
163
+ 'label' => 'Disconnected',
164
+ 'description' => 'Connection lost or failed authentication.',
165
+ ],
166
+ [
167
+ 'key' => 'error',
168
+ 'label' => 'Error',
169
+ 'description' => 'Provider integration encountered a fatal issue.',
170
+ ],
171
+ [
172
+ 'key' => 'disabled',
173
+ 'label' => 'Disabled',
174
+ 'description' => 'Manually disabled by the user.',
175
+ ],
176
+ [
177
+ 'key' => 'archived',
178
+ 'label' => 'Archived',
179
+ 'description' => 'Deprecated or replaced integration, kept for record.',
180
+ ],
181
+ ];
182
+
183
+ /**
184
+ * Telematic health statuses.
185
+ *
186
+ * @var array
187
+ */
188
+ public static $healthStates = [
189
+ [
190
+ 'key' => 'healthy',
191
+ 'label' => 'Healthy',
192
+ 'description' => 'Integration tested and stable.',
193
+ ],
194
+ [
195
+ 'key' => 'warning',
196
+ 'label' => 'Warning',
197
+ 'description' => 'Minor issues detected (e.g., slow response, nearing quota).',
198
+ ],
199
+ [
200
+ 'key' => 'critical',
201
+ 'label' => 'Critical',
202
+ 'description' => 'Persistent failure or no data received in X hours.',
203
+ ],
116
204
  ];
117
205
 
118
206
  /**
@@ -169,6 +257,20 @@ class Telematic extends Model
169
257
  return $this->hasMany(Asset::class, 'telematic_uuid', 'uuid');
170
258
  }
171
259
 
260
+ /**
261
+ * Get the provider config.
262
+ */
263
+ public function getProviderDescriptorAttribute(): array
264
+ {
265
+ $registry = app(TelematicProviderRegistry::class);
266
+ $provider = $registry->findByKey($this->provider);
267
+ if ($provider) {
268
+ return $provider->toArray();
269
+ }
270
+
271
+ return [];
272
+ }
273
+
172
274
  /**
173
275
  * Get the warranty name.
174
276
  */
@@ -333,4 +435,12 @@ class Telematic extends Model
333
435
 
334
436
  return true;
335
437
  }
438
+
439
+ /**
440
+ * Sets a default status if none input.
441
+ */
442
+ public function setStatusAttribute(?string $status = null): void
443
+ {
444
+ $this->attributes['status'] = $status ?? 'initialized';
445
+ }
336
446
  }