@fleetbase/fleetops-engine 0.6.52 → 0.6.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.hbs +201 -0
- package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +589 -0
- package/addon/components/layout/fleet-ops-sidebar.hbs +19 -233
- package/addon/components/layout/fleet-ops-sidebar.js +287 -394
- package/addon/components/maintenance-schedule/form.js +1 -0
- package/addon/components/order/form/payload.js +3 -1
- package/addon/controllers/connectivity/devices/index.js +4 -1
- package/addon/controllers/maintenance/index.js +100 -0
- package/addon/controllers/management/fuel-transactions/index.js +1 -1
- package/addon/controllers/management/index.js +88 -0
- package/addon/controllers/settings/index.js +142 -0
- package/addon/extension.js +21 -0
- package/addon/routes/analytics/index.js +3 -0
- package/addon/routes/connectivity/devices/index.js +3 -0
- package/addon/routes/maintenance/index.js +3 -0
- package/addon/routes/maintenance/work-orders/index.js +2 -0
- package/addon/routes/settings/index.js +3 -0
- package/addon/styles/fleetops-engine.css +1801 -2
- package/addon/templates/analytics/index.hbs +14 -0
- package/addon/templates/analytics/reports/index.hbs +4 -4
- package/addon/templates/maintenance/index.hbs +113 -0
- package/addon/templates/management/fuel-transactions/index/details.hbs +1 -1
- package/addon/templates/management/index.hbs +109 -73
- package/addon/templates/operations/orders/index.hbs +1 -1
- package/addon/templates/operations/service-rates/index.hbs +1 -1
- package/addon/templates/settings/index.hbs +83 -0
- package/addon/templates/settings/routing.hbs +2 -2
- package/app/controllers/maintenance/index.js +1 -0
- package/app/controllers/management/index.js +1 -0
- package/app/controllers/settings/index.js +1 -0
- package/app/routes/analytics/index.js +1 -0
- package/app/routes/maintenance/index.js +1 -0
- package/app/routes/settings/index.js +1 -0
- package/app/templates/analytics/index.js +1 -0
- package/app/templates/maintenance/index.js +1 -0
- package/app/templates/management/index.js +1 -0
- package/app/templates/settings/index.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php +30 -0
- package/server/src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +75 -11
- package/server/src/Http/Controllers/Internal/v1/HubController.php +300 -0
- package/server/src/Http/Controllers/Internal/v1/SearchController.php +506 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +94 -10
- package/server/src/Http/Filter/DeviceFilter.php +74 -0
- package/server/src/Http/Filter/DriverFilter.php +6 -0
- package/server/src/Http/Filter/VehicleFilter.php +6 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +82 -24
- package/server/src/Models/Device.php +1 -1
- package/server/src/Models/Driver.php +1 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +51 -13
- package/server/src/routes.php +9 -0
- package/server/tests/HubRoutesTest.php +62 -0
- package/server/tests/TelematicsHardeningTest.php +94 -3
- package/translations/ar-ae.yml +1 -1
- package/translations/vi-vn.yaml +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Filter;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
6
|
+
use Fleetbase\Http\Filter\Filter;
|
|
7
|
+
|
|
8
|
+
class DeviceFilter extends Filter
|
|
9
|
+
{
|
|
10
|
+
public function queryForInternal()
|
|
11
|
+
{
|
|
12
|
+
$this->builder->where('company_uuid', $this->session->get('company'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public function queryForPublic()
|
|
16
|
+
{
|
|
17
|
+
$this->queryForInternal();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public function query(?string $searchQuery)
|
|
21
|
+
{
|
|
22
|
+
$this->builder->search($searchQuery);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public function status(string|array $status)
|
|
26
|
+
{
|
|
27
|
+
$status = Utils::arrayFrom($status);
|
|
28
|
+
|
|
29
|
+
if ($status) {
|
|
30
|
+
$this->builder->whereIn('status', $status);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public function telematic(?string $telematic)
|
|
35
|
+
{
|
|
36
|
+
$this->builder->where('telematic_uuid', $telematic);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public function telematicUuid(?string $telematic)
|
|
40
|
+
{
|
|
41
|
+
$this->telematic($telematic);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public function provider(?string $provider)
|
|
45
|
+
{
|
|
46
|
+
$this->builder->where('provider', $provider);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public function warrantyUuid(?string $warranty)
|
|
50
|
+
{
|
|
51
|
+
$this->builder->where('warranty_uuid', $warranty);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public function attachableType(?string $attachableType)
|
|
55
|
+
{
|
|
56
|
+
$this->builder->where('attachable_type', $attachableType);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public function attachableUuid(?string $attachable)
|
|
60
|
+
{
|
|
61
|
+
$this->builder->where('attachable_uuid', $attachable);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public function attachmentState(?string $attachmentState)
|
|
65
|
+
{
|
|
66
|
+
if ($attachmentState === 'attached') {
|
|
67
|
+
$this->builder->whereNotNull('attachable_uuid');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ($attachmentState === 'unattached') {
|
|
71
|
+
$this->builder->whereNull('attachable_uuid');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -70,6 +70,12 @@ class DriverFilter extends Filter
|
|
|
70
70
|
|
|
71
71
|
public function vehicle(string $vehicle)
|
|
72
72
|
{
|
|
73
|
+
if ($vehicle === 'unassigned') {
|
|
74
|
+
$this->builder->whereNull('vehicle_uuid');
|
|
75
|
+
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
if (Str::isUuid($vehicle)) {
|
|
74
80
|
$this->builder->where('vehicle_uuid', $vehicle);
|
|
75
81
|
} else {
|
|
@@ -59,6 +59,12 @@ class VehicleFilter extends Filter
|
|
|
59
59
|
|
|
60
60
|
public function driver(?string $driverId)
|
|
61
61
|
{
|
|
62
|
+
if ($driverId === 'unassigned') {
|
|
63
|
+
$this->builder->whereDoesntHave('driver');
|
|
64
|
+
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
$this->builder->whereHas(
|
|
63
69
|
'driver',
|
|
64
70
|
function ($query) use ($driverId) {
|
|
@@ -53,56 +53,107 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
53
53
|
'provider' => $this->telematic->provider,
|
|
54
54
|
]);
|
|
55
55
|
|
|
56
|
+
$cursor = null;
|
|
57
|
+
$totalFetched = 0;
|
|
58
|
+
$totalLinked = 0;
|
|
59
|
+
$totalSkipped = 0;
|
|
60
|
+
$pageCount = 0;
|
|
61
|
+
$lastProviderAllCount = null;
|
|
62
|
+
$lastProviderFiltersCount = null;
|
|
63
|
+
|
|
56
64
|
try {
|
|
57
65
|
$provider = $registry->resolve($this->telematic->provider);
|
|
58
66
|
$provider->connect($this->telematic);
|
|
59
67
|
|
|
60
|
-
$cursor = null;
|
|
61
|
-
$totalSynced = 0;
|
|
62
|
-
|
|
63
68
|
do {
|
|
64
69
|
$response = $provider->fetchDevices([
|
|
65
|
-
'limit' => $this->options['limit'] ??
|
|
70
|
+
'limit' => $this->options['limit'] ?? null,
|
|
66
71
|
'cursor' => $cursor,
|
|
67
72
|
'filters' => $this->options['filters'] ?? [],
|
|
68
73
|
]);
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
$devices = $response['devices'] ?? [];
|
|
76
|
+
$pagination = $response['pagination'] ?? [];
|
|
77
|
+
$pageCount++;
|
|
78
|
+
$totalFetched += count($devices);
|
|
79
|
+
|
|
80
|
+
if (isset($pagination['allCount'])) {
|
|
81
|
+
$lastProviderAllCount = $pagination['allCount'];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isset($pagination['filtersCount'])) {
|
|
85
|
+
$lastProviderFiltersCount = $pagination['filtersCount'];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Log::info('Device discovery page fetched', [
|
|
89
|
+
'correlation_id' => $correlationId,
|
|
90
|
+
'telematic_uuid' => $this->telematic->uuid,
|
|
91
|
+
'provider' => $this->telematic->provider,
|
|
92
|
+
'page' => $pageCount,
|
|
93
|
+
'cursor' => $cursor,
|
|
94
|
+
'limit' => $pagination['limit'] ?? ($this->options['limit'] ?? null),
|
|
95
|
+
'result_count' => count($devices),
|
|
96
|
+
'all_count' => $lastProviderAllCount,
|
|
97
|
+
'filters_count' => $lastProviderFiltersCount,
|
|
98
|
+
'next_cursor' => $response['next_cursor'] ?? null,
|
|
99
|
+
'has_more' => $response['has_more'] ?? false,
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
foreach ($devices as $devicePayload) {
|
|
71
103
|
$normalizedDevice = $provider->normalizeDevice($devicePayload);
|
|
72
104
|
try {
|
|
73
105
|
$service->linkDevice($this->telematic, $normalizedDevice);
|
|
74
|
-
$
|
|
106
|
+
$totalLinked++;
|
|
75
107
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
108
|
+
$totalSkipped++;
|
|
76
109
|
Log::warning('Skipping telematics device without provider identity', [
|
|
77
|
-
'correlation_id'
|
|
78
|
-
'provider'
|
|
110
|
+
'correlation_id' => $correlationId,
|
|
111
|
+
'provider' => $this->telematic->provider,
|
|
112
|
+
'provider_unit_id' => $devicePayload['_id'] ?? $devicePayload['id'] ?? null,
|
|
113
|
+
'device_id' => $normalizedDevice['device_id'] ?? null,
|
|
114
|
+
'name' => $normalizedDevice['name'] ?? $devicePayload['name'] ?? null,
|
|
115
|
+
'imei' => $devicePayload['imei'] ?? null,
|
|
79
116
|
]);
|
|
80
117
|
}
|
|
81
118
|
}
|
|
82
119
|
|
|
83
|
-
$cursor = $response['next_cursor'];
|
|
120
|
+
$cursor = $response['next_cursor'] ?? null;
|
|
84
121
|
|
|
85
122
|
Log::info('Device discovery progress', [
|
|
86
123
|
'correlation_id' => $correlationId,
|
|
87
|
-
'
|
|
88
|
-
'
|
|
124
|
+
'fetched' => $totalFetched,
|
|
125
|
+
'linked' => $totalLinked,
|
|
126
|
+
'skipped' => $totalSkipped,
|
|
127
|
+
'has_more' => $response['has_more'] ?? false,
|
|
89
128
|
]);
|
|
90
129
|
|
|
91
130
|
// Broadcast progress (TODO: implement WebSocket broadcasting)
|
|
92
|
-
} while ($response['has_more'] && $cursor);
|
|
131
|
+
} while (($response['has_more'] ?? false) && $cursor);
|
|
93
132
|
|
|
94
133
|
Log::info('Device discovery completed', [
|
|
95
|
-
'correlation_id'
|
|
96
|
-
'
|
|
134
|
+
'correlation_id' => $correlationId,
|
|
135
|
+
'total_fetched' => $totalFetched,
|
|
136
|
+
'total_linked' => $totalLinked,
|
|
137
|
+
'total_skipped' => $totalSkipped,
|
|
138
|
+
'page_count' => $pageCount,
|
|
139
|
+
'provider_all_count' => $lastProviderAllCount,
|
|
140
|
+
'provider_filter_count' => $lastProviderFiltersCount,
|
|
97
141
|
]);
|
|
98
142
|
|
|
99
143
|
$this->telematic->status = 'active';
|
|
100
144
|
$this->telematic->meta = array_merge($this->telematic->meta ?? [], [
|
|
101
|
-
'last_sync_job_id'
|
|
102
|
-
'last_sync_completed_at'
|
|
103
|
-
'last_sync_result'
|
|
104
|
-
'last_sync_total'
|
|
105
|
-
'
|
|
145
|
+
'last_sync_job_id' => $this->jobId,
|
|
146
|
+
'last_sync_completed_at' => now()->toDateTimeString(),
|
|
147
|
+
'last_sync_result' => 'success',
|
|
148
|
+
'last_sync_total' => $totalLinked,
|
|
149
|
+
'last_sync_fetched_total' => $totalFetched,
|
|
150
|
+
'last_sync_linked_total' => $totalLinked,
|
|
151
|
+
'last_sync_skipped_total' => $totalSkipped,
|
|
152
|
+
'last_sync_page_count' => $pageCount,
|
|
153
|
+
'last_sync_provider_total' => $lastProviderFiltersCount ?? $lastProviderAllCount,
|
|
154
|
+
'last_sync_provider_all_count' => $lastProviderAllCount,
|
|
155
|
+
'last_sync_provider_filters_count' => $lastProviderFiltersCount,
|
|
156
|
+
'last_sync_error' => null,
|
|
106
157
|
]);
|
|
107
158
|
$this->telematic->save();
|
|
108
159
|
} catch (\Exception $e) {
|
|
@@ -114,11 +165,18 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
114
165
|
|
|
115
166
|
$this->telematic->status = 'error';
|
|
116
167
|
$this->telematic->meta = array_merge($this->telematic->meta ?? [], [
|
|
117
|
-
'last_sync_job_id'
|
|
118
|
-
'last_sync_result'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
168
|
+
'last_sync_job_id' => $this->jobId,
|
|
169
|
+
'last_sync_result' => 'failed',
|
|
170
|
+
'last_sync_fetched_total' => $totalFetched,
|
|
171
|
+
'last_sync_linked_total' => $totalLinked,
|
|
172
|
+
'last_sync_skipped_total' => $totalSkipped,
|
|
173
|
+
'last_sync_page_count' => $pageCount,
|
|
174
|
+
'last_sync_provider_total' => $lastProviderFiltersCount ?? $lastProviderAllCount,
|
|
175
|
+
'last_sync_provider_all_count' => $lastProviderAllCount,
|
|
176
|
+
'last_sync_provider_filters_count' => $lastProviderFiltersCount,
|
|
177
|
+
'last_sync_error' => 'Device sync failed. Review the provider connection and server logs, then try again.',
|
|
178
|
+
'last_sync_error_type' => class_basename($e),
|
|
179
|
+
'last_sync_failed_at' => now()->toDateTimeString(),
|
|
122
180
|
]);
|
|
123
181
|
$this->telematic->save();
|
|
124
182
|
|
|
@@ -74,7 +74,7 @@ class Device extends Model
|
|
|
74
74
|
*
|
|
75
75
|
* @var array
|
|
76
76
|
*/
|
|
77
|
-
protected $filterParams = ['status', 'warranty_uuid', 'attachable_type', 'attachable_uuid', 'telematic_uuid', 'provider'];
|
|
77
|
+
protected $filterParams = ['status', 'attachment_state', 'warranty_uuid', 'attachable_type', 'attachable_uuid', 'telematic', 'telematic_uuid', 'provider'];
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* The attributes that are mass assignable.
|
|
@@ -196,7 +196,7 @@ class Driver extends Model
|
|
|
196
196
|
*
|
|
197
197
|
* @var array
|
|
198
198
|
*/
|
|
199
|
-
protected $filterParams = ['vendor', 'facilitator', 'customer', 'fleet', 'photo_uuid', 'avatar_uuid', 'avatar_value'];
|
|
199
|
+
protected $filterParams = ['vendor', 'facilitator', 'customer', 'fleet', 'vehicle', 'photo_uuid', 'avatar_uuid', 'avatar_value'];
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
202
|
* The session-agnostic columns for the model.
|
|
@@ -7,6 +7,7 @@ use Brick\Geo\Engine\GEOSEngine;
|
|
|
7
7
|
use Fleetbase\Providers\CoreServiceProvider;
|
|
8
8
|
use Fleetbase\Support\NotificationRegistry;
|
|
9
9
|
use Fleetbase\Support\Utils;
|
|
10
|
+
use Illuminate\Database\Eloquent\Relations\Relation;
|
|
10
11
|
|
|
11
12
|
if (!Utils::classExists(CoreServiceProvider::class)) {
|
|
12
13
|
throw new \Exception('FleetOps cannot be loaded without `fleetbase/core-api` installed!');
|
|
@@ -129,6 +130,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
129
130
|
*/
|
|
130
131
|
public function boot()
|
|
131
132
|
{
|
|
133
|
+
$this->registerMorphMap();
|
|
132
134
|
$this->registerObservers();
|
|
133
135
|
$this->registerCommands();
|
|
134
136
|
$this->scheduleCommands(function ($schedule) {
|
|
@@ -194,6 +196,14 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
public function registerMorphMap(): void
|
|
200
|
+
{
|
|
201
|
+
Relation::morphMap([
|
|
202
|
+
'Fleetbase\\Models\\Vehicle' => \Fleetbase\FleetOps\Models\Vehicle::class,
|
|
203
|
+
'\\Fleetbase\\Models\\Vehicle' => \Fleetbase\FleetOps\Models\Vehicle::class,
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
|
|
197
207
|
public function registerNotifications()
|
|
198
208
|
{
|
|
199
209
|
// Register Notifications
|
|
@@ -4,6 +4,7 @@ namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
|
4
4
|
|
|
5
5
|
use Illuminate\Support\Carbon;
|
|
6
6
|
use Illuminate\Support\Facades\Http;
|
|
7
|
+
use Illuminate\Support\Facades\Log;
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* AFAQY telematics provider implementation.
|
|
@@ -40,7 +41,7 @@ class AfaqyProvider extends AbstractProvider
|
|
|
40
41
|
'projection' => ['_id', 'name', 'imei', 'last_update'],
|
|
41
42
|
'filters' => new \stdClass(),
|
|
42
43
|
],
|
|
43
|
-
]);
|
|
44
|
+
], true);
|
|
44
45
|
|
|
45
46
|
return [
|
|
46
47
|
'success' => true,
|
|
@@ -61,13 +62,20 @@ class AfaqyProvider extends AbstractProvider
|
|
|
61
62
|
|
|
62
63
|
public function fetchDevices(array $options = []): array
|
|
63
64
|
{
|
|
64
|
-
$limit
|
|
65
|
-
$offset
|
|
65
|
+
$limit = max(1, min((int) ($options['limit'] ?? 500), 500));
|
|
66
|
+
$offset = (int) ($options['cursor'] ?? 0);
|
|
67
|
+
$filters = $options['filters'] ?? new \stdClass();
|
|
68
|
+
|
|
69
|
+
if (is_array($filters) && empty($filters)) {
|
|
70
|
+
$filters = new \stdClass();
|
|
71
|
+
}
|
|
66
72
|
|
|
67
73
|
$response = $this->afaqyPost('/units/lists', [
|
|
68
74
|
'data' => [
|
|
69
75
|
'address' => $options['address'] ?? false,
|
|
70
|
-
'filters' => $
|
|
76
|
+
'filters' => $filters,
|
|
77
|
+
'limit' => $limit,
|
|
78
|
+
'offset' => $offset,
|
|
71
79
|
'projection' => $options['projection'] ?? [
|
|
72
80
|
'_id',
|
|
73
81
|
'name',
|
|
@@ -82,19 +90,42 @@ class AfaqyProvider extends AbstractProvider
|
|
|
82
90
|
'sensors_last_val',
|
|
83
91
|
],
|
|
84
92
|
],
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
], true);
|
|
94
|
+
|
|
95
|
+
$devices = $response['data'] ?? [];
|
|
96
|
+
$pagination = $response['pagination'] ?? [];
|
|
97
|
+
$pageOffset = (int) ($pagination['offset'] ?? $offset);
|
|
98
|
+
$pageLimit = (int) ($pagination['limit'] ?? $limit);
|
|
99
|
+
$resultCount = (int) ($pagination['resultCount'] ?? count($devices));
|
|
100
|
+
$total = (int) ($pagination['filtersCount'] ?? $pagination['allCount'] ?? count($devices));
|
|
101
|
+
$advanceBy = $resultCount > 0 ? $resultCount : max($pageLimit, count($devices), 1);
|
|
102
|
+
$nextCursor = ($pageOffset + $advanceBy) < $total ? $pageOffset + $advanceBy : null;
|
|
103
|
+
|
|
104
|
+
Log::info('AFAQY units page fetched', [
|
|
105
|
+
'telematic_uuid' => $this->telematic?->uuid,
|
|
106
|
+
'requested_offset' => $offset,
|
|
107
|
+
'requested_limit' => $limit,
|
|
108
|
+
'provider_offset' => $pageOffset,
|
|
109
|
+
'provider_limit' => $pageLimit,
|
|
110
|
+
'provider_result_count' => $resultCount,
|
|
111
|
+
'result_count' => count($devices),
|
|
112
|
+
'all_count' => $pagination['allCount'] ?? null,
|
|
113
|
+
'filters_count' => $pagination['filtersCount'] ?? null,
|
|
114
|
+
'next_cursor' => $nextCursor,
|
|
115
|
+
'has_more' => $nextCursor !== null,
|
|
87
116
|
]);
|
|
88
117
|
|
|
89
|
-
$devices = $response['data'] ?? [];
|
|
90
|
-
$pagination = $response['pagination'] ?? [];
|
|
91
|
-
$total = (int) ($pagination['filtersCount'] ?? $pagination['allCount'] ?? count($devices));
|
|
92
|
-
$nextCursor = ($offset + $limit) < $total ? $offset + $limit : null;
|
|
93
|
-
|
|
94
118
|
return [
|
|
95
119
|
'devices' => $devices,
|
|
96
120
|
'next_cursor' => $nextCursor,
|
|
97
121
|
'has_more' => $nextCursor !== null,
|
|
122
|
+
'pagination' => [
|
|
123
|
+
'allCount' => $pagination['allCount'] ?? null,
|
|
124
|
+
'filtersCount' => $pagination['filtersCount'] ?? null,
|
|
125
|
+
'resultCount' => $resultCount,
|
|
126
|
+
'offset' => $pageOffset,
|
|
127
|
+
'limit' => $pageLimit,
|
|
128
|
+
],
|
|
98
129
|
];
|
|
99
130
|
}
|
|
100
131
|
|
|
@@ -252,11 +283,18 @@ class AfaqyProvider extends AbstractProvider
|
|
|
252
283
|
return $token;
|
|
253
284
|
}
|
|
254
285
|
|
|
255
|
-
protected function afaqyPost(string $endpoint, array $payload = []): array
|
|
286
|
+
protected function afaqyPost(string $endpoint, array $payload = [], bool $tokenInQuery = false): array
|
|
256
287
|
{
|
|
288
|
+
$url = $this->baseUrl . $endpoint;
|
|
289
|
+
$body = $tokenInQuery ? $payload : array_merge(['token' => $this->credentials['token']], $payload);
|
|
290
|
+
|
|
291
|
+
if ($tokenInQuery) {
|
|
292
|
+
$url .= (str_contains($url, '?') ? '&' : '?') . http_build_query(['token' => $this->credentials['token']]);
|
|
293
|
+
}
|
|
294
|
+
|
|
257
295
|
$response = Http::withHeaders($this->headers)
|
|
258
296
|
->timeout(30)
|
|
259
|
-
->post($
|
|
297
|
+
->post($url, $body);
|
|
260
298
|
|
|
261
299
|
if ($response->failed()) {
|
|
262
300
|
throw new \RuntimeException('AFAQY API request failed with status ' . $response->status());
|
package/server/src/routes.php
CHANGED
|
@@ -305,6 +305,8 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
305
305
|
],
|
|
306
306
|
],
|
|
307
307
|
function ($router) {
|
|
308
|
+
$router->get('search', 'SearchController@search');
|
|
309
|
+
|
|
308
310
|
$router->fleetbaseRoutes(
|
|
309
311
|
'contacts',
|
|
310
312
|
function ($router, $controller) {
|
|
@@ -671,6 +673,13 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
671
673
|
$router->get('live-fleet', 'AnalyticsController@liveFleet');
|
|
672
674
|
}
|
|
673
675
|
);
|
|
676
|
+
$router->group(
|
|
677
|
+
['prefix' => 'hubs'],
|
|
678
|
+
function ($router) {
|
|
679
|
+
$router->get('resources', 'HubController@resources');
|
|
680
|
+
$router->get('maintenance', 'HubController@maintenance');
|
|
681
|
+
}
|
|
682
|
+
);
|
|
674
683
|
$router->group(
|
|
675
684
|
['prefix' => 'getting-started'],
|
|
676
685
|
function ($router) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
test('hub routes are registered', function () {
|
|
4
|
+
$routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
|
|
5
|
+
|
|
6
|
+
expect($routes)
|
|
7
|
+
->toContain("['prefix' => 'hubs']")
|
|
8
|
+
->toContain("\$router->get('resources', 'HubController@resources');")
|
|
9
|
+
->toContain("\$router->get('maintenance', 'HubController@maintenance');");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('hub controller exposes resources and maintenance contracts', function () {
|
|
13
|
+
$controller = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/HubController.php');
|
|
14
|
+
|
|
15
|
+
expect($controller)
|
|
16
|
+
->toContain('public function resources(Request $request)')
|
|
17
|
+
->toContain('public function maintenance(Request $request)')
|
|
18
|
+
->toContain("'kpis'")
|
|
19
|
+
->toContain("'actions'")
|
|
20
|
+
->toContain("'sections'")
|
|
21
|
+
->toContain("'docs'");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('resources hub action queue includes cross workflow resource actions', function () {
|
|
25
|
+
$controller = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/HubController.php');
|
|
26
|
+
|
|
27
|
+
expect($controller)
|
|
28
|
+
->toContain("'drivers_without_vehicles'")
|
|
29
|
+
->toContain("'vehicles_without_drivers'")
|
|
30
|
+
->toContain("'vehicles_without_devices'")
|
|
31
|
+
->toContain("'unattached_devices'")
|
|
32
|
+
->toContain("'resource_issues'")
|
|
33
|
+
->toContain("'overdue_vehicle_schedules'")
|
|
34
|
+
->toContain("'open_resource_work_orders'")
|
|
35
|
+
->toContain("'low_stock_parts'")
|
|
36
|
+
->toContain("'unmatched_fuel_transactions'")
|
|
37
|
+
->toContain("'assign_vehicles_to_drivers', 'Assign vehicles to drivers'")
|
|
38
|
+
->toContain("'assign_drivers_to_vehicles', 'Assign drivers to vehicles'")
|
|
39
|
+
->toContain("'attach_devices_to_vehicles', 'Attach devices to vehicles'")
|
|
40
|
+
->toContain("'prepare_vehicle_maintenance', 'Prepare upcoming maintenance'")
|
|
41
|
+
->toContain("'close_overdue_work_orders', 'Close overdue work orders'")
|
|
42
|
+
->toContain("'replenish_low_stock_parts', 'Replenish low-stock parts'")
|
|
43
|
+
->toContain("'review_unmatched_fuel_transactions', 'Review unmatched fuel transactions'")
|
|
44
|
+
->toContain("'add_operating_places', 'Add operating places'")
|
|
45
|
+
->toContain("'add_service_vendors', 'Add vendors for service work'")
|
|
46
|
+
->toContain("'Core resources look ready'");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('resource action target filters are supported', function () {
|
|
50
|
+
$deviceFilter = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/DeviceFilter.php');
|
|
51
|
+
$driverFilter = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/DriverFilter.php');
|
|
52
|
+
$vehicleFilter = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/VehicleFilter.php');
|
|
53
|
+
|
|
54
|
+
expect($deviceFilter)
|
|
55
|
+
->toContain('public function attachmentState')
|
|
56
|
+
->toContain("attachmentState === 'unattached'")
|
|
57
|
+
->toContain('public function status')
|
|
58
|
+
->toContain('public function telematic');
|
|
59
|
+
|
|
60
|
+
expect($driverFilter)->toContain("\$vehicle === 'unassigned'");
|
|
61
|
+
expect($vehicleFilter)->toContain("\$driverId === 'unassigned'");
|
|
62
|
+
});
|
|
@@ -129,7 +129,7 @@ test('device event mark processed action is routed and company scoped', function
|
|
|
129
129
|
->toContain("'Event was already processed.'");
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
test('afaqy sync stores compact device diagnostics and
|
|
132
|
+
test('afaqy sync stores compact device diagnostics and paginates complete units lists', function () {
|
|
133
133
|
$provider = file_get_contents(__DIR__ . '/../src/Support/Telematics/Providers/AfaqyProvider.php');
|
|
134
134
|
|
|
135
135
|
expect($provider)
|
|
@@ -139,9 +139,53 @@ test('afaqy sync stores compact device diagnostics and does not leak token URLs'
|
|
|
139
139
|
->toContain("'capabilities'")
|
|
140
140
|
->not->toContain("'raw' => \$payload")
|
|
141
141
|
->not->toContain("'sensors',")
|
|
142
|
-
->not->toContain("?token=' . urlencode")
|
|
143
142
|
->toContain("'Authorization' => 'Bearer ' . \$token")
|
|
144
|
-
->toContain(
|
|
143
|
+
->toContain('?? 500), 500')
|
|
144
|
+
->toContain('if (is_array($filters) && empty($filters))')
|
|
145
|
+
->toContain('$filters = new \stdClass();')
|
|
146
|
+
->toContain("'limit' => \$limit")
|
|
147
|
+
->toContain("'offset' => \$offset")
|
|
148
|
+
->not->toContain("\n 'limit' => \$limit,\n 'offset' => \$offset,")
|
|
149
|
+
->toContain("\$this->afaqyPost('/units/lists', [")
|
|
150
|
+
->toContain('], true);')
|
|
151
|
+
->toContain("http_build_query(['token' => \$this->credentials['token']])")
|
|
152
|
+
->toContain("\$body = \$tokenInQuery ? \$payload : array_merge(['token' => \$this->credentials['token']], \$payload)")
|
|
153
|
+
->toContain('$pageOffset = (int) ($pagination[\'offset\'] ?? $offset);')
|
|
154
|
+
->toContain('$pageLimit = (int) ($pagination[\'limit\'] ?? $limit);')
|
|
155
|
+
->toContain('$advanceBy = $resultCount > 0 ? $resultCount : max($pageLimit, count($devices), 1);')
|
|
156
|
+
->toContain('$nextCursor = ($pageOffset + $advanceBy) < $total ? $pageOffset + $advanceBy : null;')
|
|
157
|
+
->toContain("'requested_limit'")
|
|
158
|
+
->toContain("'provider_limit'")
|
|
159
|
+
->toContain("'provider_result_count'")
|
|
160
|
+
->toContain("'pagination' => [")
|
|
161
|
+
->toContain("'allCount'")
|
|
162
|
+
->toContain("'filtersCount'")
|
|
163
|
+
->toContain("'resultCount'");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('telematics device sync records provider pagination and skipped device counts', function () {
|
|
167
|
+
$job = file_get_contents(__DIR__ . '/../src/Jobs/SyncTelematicDevicesJob.php');
|
|
168
|
+
$controller = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/TelematicController.php');
|
|
169
|
+
|
|
170
|
+
expect($controller)
|
|
171
|
+
->toContain("'limit' => \$request->input('limit')")
|
|
172
|
+
->not->toContain("'limit' => \$request->input('limit', 100)");
|
|
173
|
+
|
|
174
|
+
expect($job)
|
|
175
|
+
->toContain("'limit' => \$this->options['limit'] ?? null")
|
|
176
|
+
->toContain('$totalFetched')
|
|
177
|
+
->toContain('$totalLinked')
|
|
178
|
+
->toContain('$totalSkipped')
|
|
179
|
+
->toContain('$pageCount')
|
|
180
|
+
->toContain('Device discovery page fetched')
|
|
181
|
+
->toContain("'provider_unit_id'")
|
|
182
|
+
->toContain("'last_sync_fetched_total'")
|
|
183
|
+
->toContain("'last_sync_linked_total'")
|
|
184
|
+
->toContain("'last_sync_skipped_total'")
|
|
185
|
+
->toContain("'last_sync_page_count'")
|
|
186
|
+
->toContain("'last_sync_provider_total'")
|
|
187
|
+
->toContain("'last_sync_provider_all_count'")
|
|
188
|
+
->toContain("'last_sync_provider_filters_count'");
|
|
145
189
|
});
|
|
146
190
|
|
|
147
191
|
test('native endpoint fields are advanced optional overrides with provider defaults', function () {
|
|
@@ -260,6 +304,53 @@ test('telematics overview attention items render as full width stacked alerts',
|
|
|
260
304
|
->not->toContain('lg:grid-cols-2');
|
|
261
305
|
});
|
|
262
306
|
|
|
307
|
+
test('device attachment morph types are normalized and legacy aliases are tolerated', function () {
|
|
308
|
+
$migration = file_get_contents(__DIR__ . '/../migrations/2026_06_15_000001_normalize_device_attachable_vehicle_morph_types.php');
|
|
309
|
+
$command = file_get_contents(__DIR__ . '/../src/Console/Commands/FixInvalidPolymorphicRelationTypeNamespaces.php');
|
|
310
|
+
$serviceProvider = file_get_contents(__DIR__ . '/../src/Providers/FleetOpsServiceProvider.php');
|
|
311
|
+
|
|
312
|
+
expect($migration)
|
|
313
|
+
->toContain("whereNotNull('attachable_uuid')")
|
|
314
|
+
->toContain("'Fleetbase\\\\Models\\\\Vehicle'")
|
|
315
|
+
->toContain("'\\\\Fleetbase\\\\Models\\\\Vehicle'")
|
|
316
|
+
->toContain("'attachable_type' => Vehicle::class")
|
|
317
|
+
->toContain('Intentionally do not restore invalid legacy morph class names.');
|
|
318
|
+
|
|
319
|
+
expect($command)
|
|
320
|
+
->toContain('\\Fleetbase\\FleetOps\\Models\\Device::class')
|
|
321
|
+
->toContain("'columns' => ['attachable_type']");
|
|
322
|
+
|
|
323
|
+
expect($serviceProvider)
|
|
324
|
+
->toContain('use Illuminate\Database\Eloquent\Relations\Relation;')
|
|
325
|
+
->toContain('$this->registerMorphMap();')
|
|
326
|
+
->toContain("'Fleetbase\\\\Models\\\\Vehicle' => \\Fleetbase\\FleetOps\\Models\\Vehicle::class")
|
|
327
|
+
->toContain("'\\\\Fleetbase\\\\Models\\\\Vehicle' => \\Fleetbase\\FleetOps\\Models\\Vehicle::class");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('internal device attachment endpoints return specific api errors instead of raw model misses', function () {
|
|
331
|
+
$vehicleController = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/VehicleController.php');
|
|
332
|
+
$deviceController = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/DeviceController.php');
|
|
333
|
+
|
|
334
|
+
expect($vehicleController)
|
|
335
|
+
->toContain('$vehicle = $this->resolveVehicle($id);')
|
|
336
|
+
->toContain('$device = $this->resolveDevice($deviceId);')
|
|
337
|
+
->toContain('Vehicle not found or not available for this organization.')
|
|
338
|
+
->toContain('Device not found or not available for this organization.')
|
|
339
|
+
->toContain('Unable to attach device to vehicle. Please try again or contact support.')
|
|
340
|
+
->toContain('Unable to detach device from vehicle. Please try again or contact support.')
|
|
341
|
+
->toContain('logDeviceAttachmentLookupFailure')
|
|
342
|
+
->toContain('logDeviceAttachmentFailure');
|
|
343
|
+
|
|
344
|
+
expect($deviceController)
|
|
345
|
+
->toContain('$device = $this->resolveDevice($id);')
|
|
346
|
+
->toContain('$vehicle = $this->resolveVehicle($vehicleId);')
|
|
347
|
+
->toContain('Vehicle not found or not available for this organization.')
|
|
348
|
+
->toContain('Device not found or not available for this organization.')
|
|
349
|
+
->toContain('Unable to attach device to vehicle. Please try again or contact support.')
|
|
350
|
+
->toContain('Unable to detach device from vehicle. Please try again or contact support.')
|
|
351
|
+
->not->toContain('firstOrFail();');
|
|
352
|
+
});
|
|
353
|
+
|
|
263
354
|
function telematics_activity_log_method(string $model): string
|
|
264
355
|
{
|
|
265
356
|
preg_match('/public function getActivitylogOptions\(\): LogOptions\s+\{(?P<body>.*?)\n \}/s', $model, $matches);
|
package/translations/ar-ae.yml
CHANGED
|
@@ -1051,7 +1051,7 @@ fleet-ops:
|
|
|
1051
1051
|
more-waypoints: المزيد من نقاط الطريق
|
|
1052
1052
|
settings-window:
|
|
1053
1053
|
success-message: تم حفظ الإعدادات بنجاح.
|
|
1054
|
-
title: إعدادات
|
|
1054
|
+
title: إعدادات Fleet-Ops
|
|
1055
1055
|
save: حفظ التغييرات
|
|
1056
1056
|
name: مسافة التنبيه الافتراضية للطلبات الفورية
|
|
1057
1057
|
name-text: يحدد هذا الإعداد المسافة الافتراضية بالأمتار التي يجب أن تنبه الطلبات الفورية السائقين ضمن محيط نقطة استلام الطلب.
|
package/translations/vi-vn.yaml
CHANGED
|
@@ -1054,7 +1054,7 @@ fleet-ops:
|
|
|
1054
1054
|
more-waypoints: thêm điểm dừng
|
|
1055
1055
|
settings-window:
|
|
1056
1056
|
success-message: Cài đặt đã được lưu thành công.
|
|
1057
|
-
title: Cài đặt
|
|
1057
|
+
title: Cài đặt Fleet-Ops
|
|
1058
1058
|
save: Lưu Thay đổi
|
|
1059
1059
|
name: Khoảng cách Ping Adhoc Mặc định
|
|
1060
1060
|
name-text: Cài đặt này xác định khoảng cách mặc định tính bằng mét mà các đơn hàng adhoc nên ping tài xế trong khu vực của điểm đón đơn hàng.
|