@fleetbase/fleetops-engine 0.6.53 → 0.6.55

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 (212) hide show
  1. package/addon/components/admin/routing-settings.hbs +2 -1
  2. package/addon/components/cell/attached-vehicle.hbs +10 -0
  3. package/addon/components/cell/attached-vehicle.js +58 -0
  4. package/addon/components/cell/device-identity.hbs +24 -0
  5. package/addon/components/cell/device-identity.js +129 -0
  6. package/addon/components/cell/driver-identity.hbs +33 -0
  7. package/addon/components/cell/driver-identity.js +143 -0
  8. package/addon/components/cell/equipment-identity.hbs +5 -0
  9. package/addon/components/cell/equipment-identity.js +48 -0
  10. package/addon/components/cell/part-identity.hbs +5 -0
  11. package/addon/components/cell/part-identity.js +63 -0
  12. package/addon/components/cell/telematic-device.hbs +38 -0
  13. package/addon/components/cell/telematic-device.js +44 -0
  14. package/addon/components/cell/telematic-provider.hbs +33 -16
  15. package/addon/components/cell/telematic-provider.js +50 -8
  16. package/addon/components/cell/vehicle-identity.hbs +33 -0
  17. package/addon/components/cell/vehicle-identity.js +138 -0
  18. package/addon/components/device/details.hbs +106 -135
  19. package/addon/components/device/details.js +174 -1
  20. package/addon/components/device/form.hbs +2 -2
  21. package/addon/components/device/form.js +7 -0
  22. package/addon/components/device/panel-header.hbs +46 -21
  23. package/addon/components/device/panel-header.js +49 -1
  24. package/addon/components/device/panel-tabs/events.hbs +30 -0
  25. package/addon/components/device/panel-tabs/events.js +128 -0
  26. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  27. package/addon/components/device/panel-tabs/sensors.js +93 -0
  28. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  29. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  30. package/addon/components/equipment/form.js +8 -1
  31. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +76 -11
  32. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  33. package/addon/components/layout/fleet-ops-sidebar.js +101 -17
  34. package/addon/components/map/drawer/device-event-listing.js +2 -0
  35. package/addon/components/map/drawer/driver-listing.js +2 -0
  36. package/addon/components/map/drawer/place-listing.js +2 -0
  37. package/addon/components/map/drawer/position-listing.hbs +11 -6
  38. package/addon/components/map/drawer/position-listing.js +17 -4
  39. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  40. package/addon/components/order/form/details.hbs +1 -43
  41. package/addon/components/order/form/details.js +18 -53
  42. package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
  43. package/addon/components/order/form/orchestrator-constraints.js +61 -0
  44. package/addon/components/order/form/payload.js +48 -2
  45. package/addon/components/order/form/route.js +15 -0
  46. package/addon/components/order/form/service-rate.hbs +62 -11
  47. package/addon/components/order/form/service-rate.js +124 -2
  48. package/addon/components/order/form.hbs +2 -0
  49. package/addon/components/sensor/panel-header.hbs +47 -15
  50. package/addon/components/sensor/panel-header.js +59 -1
  51. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  52. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  53. package/addon/components/table/cell/dropdown.hbs +28 -0
  54. package/addon/components/table/cell/dropdown.js +91 -0
  55. package/addon/components/telematic/details.hbs +7 -1
  56. package/addon/components/telematic/form.js +16 -8
  57. package/addon/components/vendor/panel-header.hbs +82 -0
  58. package/addon/components/vendor/panel-header.js +48 -0
  59. package/addon/components/work-order/details.hbs +5 -1
  60. package/addon/components/work-order/form.hbs +26 -4
  61. package/addon/components/work-order/form.js +5 -1
  62. package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
  63. package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
  64. package/addon/controllers/connectivity/devices/index/details/vehicle.js +122 -0
  65. package/addon/controllers/connectivity/devices/index/details.js +12 -0
  66. package/addon/controllers/connectivity/devices/index.js +144 -129
  67. package/addon/controllers/connectivity/events/details.js +3 -0
  68. package/addon/controllers/connectivity/events/index.js +187 -11
  69. package/addon/controllers/connectivity/sensors/index.js +156 -14
  70. package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
  71. package/addon/controllers/connectivity/telematics/details/devices.js +170 -129
  72. package/addon/controllers/connectivity/telematics/details/events.js +39 -11
  73. package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
  74. package/addon/controllers/maintenance/equipment/index.js +1 -2
  75. package/addon/controllers/maintenance/parts/index.js +1 -2
  76. package/addon/controllers/maintenance/work-orders/index.js +18 -1
  77. package/addon/controllers/management/drivers/index.js +82 -42
  78. package/addon/controllers/management/fuel-reports/index.js +63 -4
  79. package/addon/controllers/management/issues/index.js +63 -4
  80. package/addon/controllers/management/vehicles/index.js +33 -6
  81. package/addon/routes/connectivity/devices/index/details/events.js +37 -1
  82. package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
  83. package/addon/routes/connectivity/devices/index/details/vehicle.js +29 -0
  84. package/addon/routes/connectivity/devices/index.js +7 -0
  85. package/addon/routes/connectivity/events/details.js +3 -0
  86. package/addon/routes/connectivity/events/index.js +6 -2
  87. package/addon/routes/connectivity/sensors/index.js +7 -2
  88. package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
  89. package/addon/routes/connectivity/telematics/details/devices.js +6 -0
  90. package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
  91. package/addon/routes/connectivity/telematics/details.js +1 -1
  92. package/addon/routes/connectivity/telematics/edit.js +1 -1
  93. package/addon/routes/maintenance/work-orders/index.js +1 -0
  94. package/addon/routes.js +2 -0
  95. package/addon/services/device-actions.js +30 -0
  96. package/addon/services/device-event-actions.js +1 -1
  97. package/addon/services/equipment-actions.js +1 -0
  98. package/addon/services/order-creation.js +54 -1
  99. package/addon/services/order-validation.js +3 -2
  100. package/addon/services/sensor-actions.js +3 -0
  101. package/addon/services/service-rate-actions.js +40 -1
  102. package/addon/services/vehicle-actions.js +24 -12
  103. package/addon/services/vendor-actions.js +3 -0
  104. package/addon/styles/fleetops-engine.css +28 -4
  105. package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
  106. package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
  107. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +147 -0
  108. package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
  109. package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
  110. package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
  111. package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
  112. package/addon/templates/management/vehicles/index/edit.hbs +1 -1
  113. package/addon/templates/management/vendors/index/details.hbs +2 -2
  114. package/addon/templates/settings/notifications.hbs +2 -1
  115. package/addon/templates/settings/routing.hbs +1 -0
  116. package/addon/utils/device-table-columns.js +288 -0
  117. package/addon/utils/fleet-ops-options.js +38 -0
  118. package/addon/utils/identity-cell-resource.js +20 -0
  119. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  120. package/addon/utils/trackable-option.js +93 -0
  121. package/app/components/cell/attached-vehicle.js +1 -0
  122. package/app/components/cell/device-identity.js +1 -0
  123. package/app/components/cell/driver-identity.js +1 -0
  124. package/app/components/cell/equipment-identity.js +1 -0
  125. package/app/components/cell/part-identity.js +1 -0
  126. package/app/components/cell/telematic-device.js +1 -0
  127. package/app/components/cell/vehicle-identity.js +1 -0
  128. package/app/components/device/panel-tabs/events.hbs +30 -0
  129. package/app/components/device/panel-tabs/events.js +128 -0
  130. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  131. package/app/components/device/panel-tabs/sensors.js +93 -0
  132. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  133. package/app/components/device/panel-tabs/vehicle.js +90 -0
  134. package/app/components/order/form/orchestrator-constraints.js +1 -0
  135. package/app/components/table/cell/dropdown/action-item.js +1 -0
  136. package/app/components/table/cell/dropdown.js +1 -0
  137. package/app/components/vendor/panel-header.js +1 -0
  138. package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
  139. package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
  140. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  141. package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
  142. package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
  143. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  144. package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
  145. package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
  146. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  147. package/app/utils/map-drawer-dropdown-position.js +1 -0
  148. package/app/utils/trackable-option.js +1 -0
  149. package/composer.json +1 -1
  150. package/extension.json +1 -1
  151. package/package.json +4 -4
  152. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
  153. package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
  154. package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
  155. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
  156. package/server/src/Console/Commands/SyncTelematics.php +89 -0
  157. package/server/src/Exceptions/TelematicProviderException.php +13 -2
  158. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
  159. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +55 -1
  160. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +17 -6
  161. package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
  162. package/server/src/Http/Controllers/Internal/v1/LiveController.php +178 -1
  163. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
  164. package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
  165. package/server/src/Http/Controllers/Internal/v1/SearchController.php +2 -2
  166. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
  167. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  168. package/server/src/Http/Filter/DeviceEventFilter.php +65 -0
  169. package/server/src/Http/Filter/DeviceFilter.php +80 -0
  170. package/server/src/Http/Filter/SensorFilter.php +125 -0
  171. package/server/src/Http/Filter/VehicleFilter.php +13 -0
  172. package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
  173. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
  174. package/server/src/Http/Resources/v1/Driver.php +11 -2
  175. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  176. package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
  177. package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
  178. package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
  179. package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
  180. package/server/src/Jobs/SyncTelematicDevicesJob.php +180 -123
  181. package/server/src/Models/Device.php +17 -1
  182. package/server/src/Models/DeviceEvent.php +82 -1
  183. package/server/src/Models/Driver.php +16 -0
  184. package/server/src/Models/Equipment.php +55 -0
  185. package/server/src/Models/Sensor.php +15 -2
  186. package/server/src/Models/Telematic.php +1 -1
  187. package/server/src/Models/Vehicle.php +5 -1
  188. package/server/src/Models/WorkOrder.php +5 -2
  189. package/server/src/Observers/DriverObserver.php +3 -3
  190. package/server/src/Observers/FleetObserver.php +23 -0
  191. package/server/src/Observers/VehicleObserver.php +3 -3
  192. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  193. package/server/src/Support/LiveCacheService.php +1 -1
  194. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +143 -20
  195. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  196. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  197. package/server/src/Support/Telematics/Providers/SafeeProvider.php +70 -5
  198. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  199. package/server/src/Support/Telematics/TelematicService.php +330 -30
  200. package/server/src/routes.php +1 -1
  201. package/server/tests/DeviceFilterTest.php +45 -0
  202. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  203. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  204. package/server/tests/OperationsMonitorEndpointTest.php +38 -0
  205. package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
  206. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  207. package/server/tests/TelematicsHardeningTest.php +598 -6
  208. package/server/tests/VehicleDriverAssignmentTest.php +27 -0
  209. package/translations/en-us.yaml +1 -0
  210. package/addon/controllers/connectivity/events/index/details.js +0 -3
  211. package/addon/routes/connectivity/events/index/details.js +0 -3
  212. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -0,0 +1,89 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Console\Commands;
4
+
5
+ use Fleetbase\FleetOps\Jobs\SyncTelematicDevicesJob;
6
+ use Fleetbase\FleetOps\Models\Telematic;
7
+ use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry;
8
+ use Illuminate\Console\Command;
9
+ use Illuminate\Support\Facades\Cache;
10
+
11
+ class SyncTelematics extends Command
12
+ {
13
+ protected $signature = 'fleetops:sync-telematics
14
+ {--provider=* : Limit polling to one or more provider keys}
15
+ {--limit=500 : Maximum provider units to fetch per page}
16
+ {--exclude-webhook-providers : Skip providers that support webhooks}
17
+ {--no-lock : Skip process locking}';
18
+
19
+ protected $description = 'Poll active telematics providers for device snapshots and positional telemetry.';
20
+
21
+ public function handle(TelematicProviderRegistry $registry): int
22
+ {
23
+ $useLock = !$this->option('no-lock');
24
+ $lock = null;
25
+
26
+ if ($useLock) {
27
+ $lock = Cache::lock('fleetops:sync-telematics', 600);
28
+ if (!$lock->get()) {
29
+ $this->warn('Another telematics sync run appears to be in progress.');
30
+
31
+ return self::SUCCESS;
32
+ }
33
+ }
34
+
35
+ try {
36
+ $providerKeys = $this->pollableProviderKeys($registry);
37
+ if (empty($providerKeys)) {
38
+ $this->info('No pollable telematics providers found.');
39
+
40
+ return self::SUCCESS;
41
+ }
42
+
43
+ $query = Telematic::withoutGlobalScopes()
44
+ ->whereIn('provider', $providerKeys)
45
+ ->whereIn('status', ['active', 'connected'])
46
+ ->whereNotNull('company_uuid');
47
+
48
+ $queued = 0;
49
+ $query->orderBy('id')->chunkById(100, function ($telematics) use (&$queued) {
50
+ foreach ($telematics as $telematic) {
51
+ SyncTelematicDevicesJob::dispatch($telematic, [
52
+ 'limit' => (int) $this->option('limit'),
53
+ ]);
54
+ $queued++;
55
+ }
56
+ });
57
+
58
+ $this->info("Queued {$queued} telematics sync job(s).");
59
+
60
+ return self::SUCCESS;
61
+ } finally {
62
+ if ($lock) {
63
+ $lock->release();
64
+ }
65
+ }
66
+ }
67
+
68
+ protected function pollableProviderKeys(TelematicProviderRegistry $registry): array
69
+ {
70
+ $requestedProviders = array_filter((array) $this->option('provider'));
71
+ $excludeWebhookProviders = (bool) $this->option('exclude-webhook-providers');
72
+
73
+ return $registry->all()
74
+ ->filter(function ($descriptor) use ($requestedProviders, $excludeWebhookProviders) {
75
+ if (!empty($requestedProviders) && !in_array($descriptor->key, $requestedProviders, true)) {
76
+ return false;
77
+ }
78
+
79
+ if (!$descriptor->supportsDiscovery) {
80
+ return false;
81
+ }
82
+
83
+ return !$excludeWebhookProviders || !$descriptor->supportsWebhooks;
84
+ })
85
+ ->keys()
86
+ ->values()
87
+ ->all();
88
+ }
89
+ }
@@ -2,8 +2,6 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Exceptions;
4
4
 
5
- use Exception;
6
-
7
5
  /**
8
6
  * Class TelematicProviderException.
9
7
  *
@@ -11,4 +9,17 @@ use Exception;
11
9
  */
12
10
  class TelematicProviderException extends \Exception
13
11
  {
12
+ protected array $context;
13
+
14
+ public function __construct(string $message = '', array $context = [], int $code = 0, ?\Throwable $previous = null)
15
+ {
16
+ parent::__construct($message, $code, $previous);
17
+
18
+ $this->context = $context;
19
+ }
20
+
21
+ public function context(): array
22
+ {
23
+ return $this->context;
24
+ }
14
25
  }
@@ -3,7 +3,6 @@
3
3
  namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
4
4
 
5
5
  use Fleetbase\FleetOps\Events\OrderDispatchFailed;
6
- use Fleetbase\FleetOps\Events\OrderReady;
7
6
  use Fleetbase\FleetOps\Events\OrderStarted;
8
7
  use Fleetbase\FleetOps\Exceptions\CustomerUserConflictException;
9
8
  use Fleetbase\FleetOps\Exceptions\UserAlreadyExistsException;
@@ -14,6 +13,7 @@ use Fleetbase\FleetOps\Http\Requests\UpdateOrderRequest;
14
13
  use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource;
15
14
  use Fleetbase\FleetOps\Http\Resources\v1\Order as OrderResource;
16
15
  use Fleetbase\FleetOps\Http\Resources\v1\Proof as ProofResource;
16
+ use Fleetbase\FleetOps\Jobs\FinalizeApiOrderCreation;
17
17
  use Fleetbase\FleetOps\Models\Contact;
18
18
  use Fleetbase\FleetOps\Models\Driver;
19
19
  use Fleetbase\FleetOps\Models\Entity;
@@ -335,26 +335,11 @@ class OrderController extends Controller
335
335
  // Determine if order should be dispatched on creation
336
336
  $shouldDispatch = $request->boolean('dispatch') && $integratedVendorOrder === null;
337
337
 
338
- // Run background processes on queue
339
- dispatch(function () use ($order, $serviceQuote, $shouldDispatch): void {
340
- // notify driver if assigned
341
- $order->notifyDriverAssigned();
342
-
343
- // set driving distance and time
344
- $order->setPreliminaryDistanceAndTime();
345
-
346
- // if service quote attached purchase
347
- $order->purchaseServiceQuote($serviceQuote);
348
-
349
- // dispatch if flagged true
350
- if ($shouldDispatch) {
351
- $order->dispatchWithActivity();
352
- }
353
-
354
- // Trigger order created event
355
- event(new OrderReady($order));
356
- })
357
- ->afterCommit();
338
+ FinalizeApiOrderCreation::dispatch(
339
+ $order->uuid,
340
+ $serviceQuote instanceof ServiceQuote ? $serviceQuote->uuid : null,
341
+ $shouldDispatch
342
+ )->afterCommit();
358
343
 
359
344
  // response the driver resource
360
345
  return new OrderResource($order);
@@ -5,6 +5,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
5
5
  use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
6
6
  use Fleetbase\FleetOps\Models\Device;
7
7
  use Fleetbase\FleetOps\Models\Vehicle;
8
+ use Fleetbase\FleetOps\Support\Utils;
8
9
  use Illuminate\Http\JsonResponse;
9
10
  use Illuminate\Http\Request;
10
11
  use Illuminate\Support\Facades\Log;
@@ -27,6 +28,7 @@ class DeviceController extends FleetOpsController
27
28
  public static function onQueryRecord($query, $request): void
28
29
  {
29
30
  $query->with(['telematic', 'warranty', 'attachable']);
31
+ $query->withCount('sensors');
30
32
 
31
33
  if ($request->filled('attachment_state')) {
32
34
  match ($request->input('attachment_state')) {
@@ -37,12 +39,41 @@ class DeviceController extends FleetOpsController
37
39
  }
38
40
 
39
41
  if ($request->filled('vehicle')) {
40
- $query->where('attachable_uuid', $request->input('vehicle'));
42
+ static::applyVehicleFilter($query, $request->input('vehicle'));
41
43
  }
42
44
 
43
45
  if ($request->filled('device_id')) {
44
46
  $query->where('device_id', 'like', '%' . $request->input('device_id') . '%');
45
47
  }
48
+
49
+ if ($request->filled('serial_number')) {
50
+ $query->where('serial_number', 'like', '%' . $request->input('serial_number') . '%');
51
+ }
52
+
53
+ if ($request->filled('connection_status')) {
54
+ $statuses = Utils::arrayFrom($request->input('connection_status'));
55
+
56
+ $query->where(function ($statusQuery) use ($statuses) {
57
+ foreach ($statuses as $status) {
58
+ match ($status) {
59
+ 'online' => $statusQuery->orWhere('last_online_at', '>=', now()->subMinutes(10)),
60
+ 'recently_offline' => $statusQuery->orWhereBetween('last_online_at', [now()->subMinutes(60), now()->subMinutes(10)]),
61
+ 'offline' => $statusQuery->orWhereBetween('last_online_at', [now()->subDay(), now()->subMinutes(60)]),
62
+ 'long_offline' => $statusQuery->orWhere('last_online_at', '<', now()->subDay()),
63
+ 'never_connected' => $statusQuery->orWhereNull('last_online_at'),
64
+ default => null,
65
+ };
66
+ }
67
+ });
68
+ }
69
+
70
+ if ($request->filled('last_online_at')) {
71
+ static::applyDateFilter($query, 'last_online_at', $request->input('last_online_at'));
72
+ }
73
+
74
+ if ($request->filled('updated_at')) {
75
+ static::applyDateFilter($query, 'updated_at', $request->input('updated_at'));
76
+ }
46
77
  }
47
78
 
48
79
  /**
@@ -54,6 +85,7 @@ class DeviceController extends FleetOpsController
54
85
  public static function onFindRecord($query, $request): void
55
86
  {
56
87
  $query->with(['telematic', 'warranty', 'attachable']);
88
+ $query->withCount('sensors');
57
89
  }
58
90
 
59
91
  /**
@@ -146,6 +178,28 @@ class DeviceController extends FleetOpsController
146
178
  ->first();
147
179
  }
148
180
 
181
+ protected static function applyDateFilter($query, string $column, string|array $value): void
182
+ {
183
+ $dates = Utils::dateRange($value);
184
+
185
+ if (is_array($dates)) {
186
+ $query->whereBetween($column, $dates);
187
+ } else {
188
+ $query->whereDate($column, $dates);
189
+ }
190
+ }
191
+
192
+ protected static function applyVehicleFilter($query, string $vehicle): void
193
+ {
194
+ $query->where(function ($vehicleQuery) use ($vehicle) {
195
+ $vehicleQuery->where('attachable_uuid', $vehicle)
196
+ ->orWhereIn('attachable_uuid', Vehicle::query()
197
+ ->where('company_uuid', session('company'))
198
+ ->where('public_id', $vehicle)
199
+ ->pluck('uuid'));
200
+ });
201
+ }
202
+
149
203
  protected function logDeviceAttachmentLookupFailure(string $action, string $missingResource, string $deviceId, ?string $vehicleId): void
150
204
  {
151
205
  Log::warning('Device attachment lookup failed', [
@@ -4,6 +4,7 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
4
 
5
5
  use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
6
6
  use Fleetbase\FleetOps\Models\DeviceEvent;
7
+ use Fleetbase\FleetOps\Support\Utils;
7
8
  use Illuminate\Http\JsonResponse;
8
9
 
9
10
  class DeviceEventController extends FleetOpsController
@@ -23,7 +24,7 @@ class DeviceEventController extends FleetOpsController
23
24
  */
24
25
  public static function onQueryRecord($query, $request): void
25
26
  {
26
- $query->with(['device']);
27
+ $query->with(['device.telematic']);
27
28
 
28
29
  if ($request->filled('telematic')) {
29
30
  $query->whereHas('device', function ($deviceQuery) use ($request) {
@@ -32,11 +33,21 @@ class DeviceEventController extends FleetOpsController
32
33
  }
33
34
 
34
35
  if ($request->filled('processed')) {
35
- match ($request->input('processed')) {
36
- 'processed' => $query->whereNotNull('processed_at'),
37
- 'unprocessed' => $query->whereNull('processed_at'),
38
- default => null,
39
- };
36
+ $states = Utils::arrayFrom($request->input('processed'));
37
+
38
+ if (!$states) {
39
+ return;
40
+ }
41
+
42
+ $query->where(function ($query) use ($states) {
43
+ foreach ($states as $state) {
44
+ match ($state) {
45
+ 'processed' => $query->orWhereNotNull('processed_at'),
46
+ 'unprocessed' => $query->orWhereNull('processed_at'),
47
+ default => null,
48
+ };
49
+ }
50
+ });
40
51
  }
41
52
  }
42
53
 
@@ -11,6 +11,7 @@ use Fleetbase\FleetOps\Models\Fleet;
11
11
  use Fleetbase\FleetOps\Models\FleetDriver;
12
12
  use Fleetbase\FleetOps\Models\FleetVehicle;
13
13
  use Fleetbase\FleetOps\Models\Vehicle;
14
+ use Fleetbase\FleetOps\Support\LiveCacheService;
14
15
  use Fleetbase\Http\Requests\ExportRequest;
15
16
  use Fleetbase\Http\Requests\ImportRequest;
16
17
  use Illuminate\Http\Request;
@@ -121,6 +122,8 @@ class FleetController extends FleetOpsController
121
122
  'driver_uuid' => $driver->uuid,
122
123
  ])->delete();
123
124
 
125
+ LiveCacheService::invalidate('operations-monitor');
126
+
124
127
  return response()->json([
125
128
  'status' => 'ok',
126
129
  'deleted' => $deleted,
@@ -153,6 +156,8 @@ class FleetController extends FleetOpsController
153
156
  ]);
154
157
  }
155
158
 
159
+ LiveCacheService::invalidate('operations-monitor');
160
+
156
161
  return response()->json([
157
162
  'status' => 'ok',
158
163
  'exists' => $exists,
@@ -178,6 +183,8 @@ class FleetController extends FleetOpsController
178
183
  'vehicle_uuid' => $vehicle->uuid,
179
184
  ])->delete();
180
185
 
186
+ LiveCacheService::invalidate('operations-monitor');
187
+
181
188
  return response()->json([
182
189
  'status' => 'ok',
183
190
  'deleted' => $deleted,
@@ -210,6 +217,8 @@ class FleetController extends FleetOpsController
210
217
  ]);
211
218
  }
212
219
 
220
+ LiveCacheService::invalidate('operations-monitor');
221
+
213
222
  return response()->json([
214
223
  'status' => 'ok',
215
224
  'exists' => $exists,
@@ -8,12 +8,16 @@ use Fleetbase\FleetOps\Http\Resources\v1\Index\Order as OrderIndexResource;
8
8
  use Fleetbase\FleetOps\Http\Resources\v1\Index\Place as PlaceIndexResource;
9
9
  use Fleetbase\FleetOps\Http\Resources\v1\Index\Vehicle as VehicleIndexResource;
10
10
  use Fleetbase\FleetOps\Models\Driver;
11
+ use Fleetbase\FleetOps\Models\Fleet;
12
+ use Fleetbase\FleetOps\Models\FleetDriver;
13
+ use Fleetbase\FleetOps\Models\FleetVehicle;
11
14
  use Fleetbase\FleetOps\Models\Order;
12
15
  use Fleetbase\FleetOps\Models\Place;
13
16
  use Fleetbase\FleetOps\Models\Route;
14
17
  use Fleetbase\FleetOps\Models\Vehicle;
15
18
  use Fleetbase\FleetOps\Support\LiveCacheService;
16
19
  use Fleetbase\FleetOps\Support\LiveOrderQuery;
20
+ use Fleetbase\FleetOps\Support\Utils;
17
21
  use Fleetbase\Http\Controllers\Controller;
18
22
  use Illuminate\Http\Request;
19
23
 
@@ -144,7 +148,7 @@ class LiveController extends Controller
144
148
 
145
149
  return LiveCacheService::remember('drivers', $cacheParams, function () use ($bounds, $limit) {
146
150
  $query = Driver::where(['company_uuid' => session('company')])
147
- ->with(['user', 'vehicle'])
151
+ ->with(['user', 'vehicle.devices'])
148
152
  ->applyDirectivesForPermissions('fleet-ops list driver');
149
153
 
150
154
  $this->applyLiveLocationGuards($query);
@@ -186,6 +190,179 @@ class LiveController extends Controller
186
190
  });
187
191
  }
188
192
 
193
+ /**
194
+ * Get the complete resource snapshot used by the operations sidebar monitor.
195
+ *
196
+ * @return array
197
+ */
198
+ public function operationsMonitor()
199
+ {
200
+ return LiveCacheService::remember('operations-monitor', [], function () {
201
+ $drivers = Driver::where(['company_uuid' => session('company')])
202
+ ->with(['user', 'vehicle'])
203
+ ->applyDirectivesForPermissions('fleet-ops list driver')
204
+ ->orderBy('id')
205
+ ->get();
206
+
207
+ $vehicles = Vehicle::where(['company_uuid' => session('company')])
208
+ ->with(['driver'])
209
+ ->applyDirectivesForPermissions('fleet-ops list vehicle')
210
+ ->orderBy('id')
211
+ ->get();
212
+
213
+ $fleets = Fleet::where(['company_uuid' => session('company')])
214
+ ->applyDirectivesForPermissions('fleet-ops list fleet')
215
+ ->orderBy('name')
216
+ ->orderBy('id')
217
+ ->get();
218
+
219
+ $driverIdsByUuid = $drivers->pluck('uuid', 'uuid');
220
+ $vehicleIdsByUuid = $vehicles->pluck('uuid', 'uuid');
221
+ $onlineDriverUuids = $drivers->where('online', true)->pluck('uuid')->flip();
222
+ $onlineVehicleUuids = $vehicles->where('online', true)->pluck('uuid')->flip();
223
+ $fleetUuids = $fleets->pluck('uuid');
224
+
225
+ $driverMemberships = FleetDriver::whereIn('fleet_uuid', $fleetUuids)
226
+ ->whereIn('driver_uuid', $driverIdsByUuid->keys())
227
+ ->get()
228
+ ->groupBy('fleet_uuid');
229
+
230
+ $vehicleMemberships = FleetVehicle::whereIn('fleet_uuid', $fleetUuids)
231
+ ->whereIn('vehicle_uuid', $vehicleIdsByUuid->keys())
232
+ ->get()
233
+ ->groupBy('fleet_uuid');
234
+
235
+ $fleetNodes = $fleets->mapWithKeys(function (Fleet $fleet) use ($driverMemberships, $vehicleMemberships, $driverIdsByUuid, $vehicleIdsByUuid, $onlineDriverUuids, $onlineVehicleUuids) {
236
+ $fleetDriverUuids = $driverMemberships->get($fleet->uuid, collect())->pluck('driver_uuid')->unique()->values();
237
+ $fleetVehicleUuids = $vehicleMemberships->get($fleet->uuid, collect())->pluck('vehicle_uuid')->unique()->values();
238
+ $driverIds = $fleetDriverUuids->map(fn ($uuid) => $driverIdsByUuid->get($uuid))->filter()->values();
239
+ $vehicleIds = $fleetVehicleUuids->map(fn ($uuid) => $vehicleIdsByUuid->get($uuid))->filter()->values();
240
+
241
+ return [
242
+ $fleet->uuid => [
243
+ 'id' => $fleet->uuid,
244
+ 'uuid' => $fleet->uuid,
245
+ 'public_id' => $fleet->public_id,
246
+ 'name' => $fleet->name,
247
+ 'task' => $fleet->task,
248
+ 'status' => $fleet->status,
249
+ 'slug' => $fleet->slug,
250
+ 'parent_fleet_uuid' => $fleet->parent_fleet_uuid,
251
+ 'drivers_count' => $driverIds->count(),
252
+ 'drivers_online_count' => $fleetDriverUuids->filter(fn ($uuid) => $onlineDriverUuids->has($uuid))->count(),
253
+ 'vehicles_count' => $vehicleIds->count(),
254
+ 'vehicles_online_count' => $fleetVehicleUuids->filter(fn ($uuid) => $onlineVehicleUuids->has($uuid))->count(),
255
+ 'driver_ids' => $driverIds->all(),
256
+ 'vehicle_ids' => $vehicleIds->all(),
257
+ 'subfleets' => [],
258
+ 'updated_at' => $fleet->updated_at,
259
+ 'created_at' => $fleet->created_at,
260
+ ],
261
+ ];
262
+ });
263
+
264
+ return [
265
+ 'drivers' => $drivers->map(fn (Driver $driver) => $this->serializeMonitorDriver($driver))->values()->all(),
266
+ 'vehicles' => $vehicles->map(fn (Vehicle $vehicle) => $this->serializeMonitorVehicle($vehicle))->values()->all(),
267
+ 'fleets' => $this->buildOperationsMonitorFleetTree($fleetNodes),
268
+ 'meta' => [
269
+ 'generated_at' => now()->toISOString(),
270
+ 'ttl' => LiveCacheService::DEFAULT_TTL,
271
+ 'drivers_count' => $drivers->count(),
272
+ 'vehicles_count' => $vehicles->count(),
273
+ 'fleets_count' => $fleets->count(),
274
+ ],
275
+ ];
276
+ });
277
+ }
278
+
279
+ protected function serializeMonitorDriver(Driver $driver): array
280
+ {
281
+ return [
282
+ 'id' => $driver->uuid,
283
+ 'uuid' => $driver->uuid,
284
+ 'public_id' => $driver->public_id,
285
+ 'company_uuid' => $driver->company_uuid,
286
+ 'user_uuid' => $driver->user_uuid,
287
+ 'vehicle_uuid' => $driver->vehicle_uuid,
288
+ 'vendor_uuid' => $driver->vendor_uuid,
289
+ 'current_job_uuid' => $driver->current_job_uuid,
290
+ 'name' => $driver->name,
291
+ 'email' => $driver->email,
292
+ 'phone' => $driver->phone,
293
+ 'photo_url' => $driver->photo_url,
294
+ 'avatar_url' => $driver->photo_url,
295
+ 'vehicle_name' => $driver->vehicle_name,
296
+ 'status' => $driver->status,
297
+ 'location' => Utils::castPoint($driver->location),
298
+ 'heading' => (int) data_get($driver, 'heading', 0),
299
+ 'altitude' => (int) data_get($driver, 'altitude', 0),
300
+ 'speed' => (int) data_get($driver, 'speed', 0),
301
+ 'online' => (bool) data_get($driver, 'online', false),
302
+ 'assigned_orders_count' => null,
303
+ 'meta' => [
304
+ '_index_resource' => true,
305
+ ],
306
+ 'updated_at' => $driver->updated_at,
307
+ 'created_at' => $driver->created_at,
308
+ ];
309
+ }
310
+
311
+ protected function serializeMonitorVehicle(Vehicle $vehicle): array
312
+ {
313
+ return [
314
+ 'id' => $vehicle->uuid,
315
+ 'uuid' => $vehicle->uuid,
316
+ 'public_id' => $vehicle->public_id,
317
+ 'company_uuid' => $vehicle->company_uuid,
318
+ 'vendor_uuid' => $vehicle->vendor_uuid,
319
+ 'photo_uuid' => $vehicle->photo_uuid,
320
+ 'internal_id' => $vehicle->internal_id,
321
+ 'name' => $vehicle->name,
322
+ 'display_name' => $vehicle->display_name,
323
+ 'driver_name' => $vehicle->driver_name,
324
+ 'plate_number' => $vehicle->plate_number,
325
+ 'serial_number' => $vehicle->serial_number,
326
+ 'fuel_card_number' => $vehicle->fuel_card_number,
327
+ 'vin' => $vehicle->vin,
328
+ 'make' => $vehicle->make,
329
+ 'model' => $vehicle->model,
330
+ 'year' => $vehicle->year,
331
+ 'photo_url' => $vehicle->photo_url,
332
+ 'avatar_url' => $vehicle->avatar_url,
333
+ 'status' => $vehicle->status,
334
+ 'location' => Utils::castPoint($vehicle->location),
335
+ 'heading' => (int) data_get($vehicle, 'heading', 0),
336
+ 'altitude' => (int) data_get($vehicle, 'altitude', 0),
337
+ 'speed' => (int) data_get($vehicle, 'speed', 0),
338
+ 'online' => (bool) data_get($vehicle, 'online', false),
339
+ 'assigned_orders_count' => null,
340
+ 'meta' => [
341
+ '_index_resource' => true,
342
+ ],
343
+ 'updated_at' => $vehicle->updated_at,
344
+ 'created_at' => $vehicle->created_at,
345
+ ];
346
+ }
347
+
348
+ protected function buildOperationsMonitorFleetTree($fleetNodes): array
349
+ {
350
+ $nodes = $fleetNodes->map(fn ($node) => $node)->all();
351
+
352
+ foreach ($nodes as $uuid => $node) {
353
+ $parentUuid = data_get($node, 'parent_fleet_uuid');
354
+
355
+ if ($parentUuid && isset($nodes[$parentUuid])) {
356
+ $nodes[$parentUuid]['subfleets'][] = &$nodes[$uuid];
357
+ }
358
+ }
359
+
360
+ return collect($nodes)
361
+ ->filter(fn ($node) => empty(data_get($node, 'parent_fleet_uuid')) || !isset($nodes[data_get($node, 'parent_fleet_uuid')]))
362
+ ->values()
363
+ ->all();
364
+ }
365
+
189
366
  /**
190
367
  * Get places based on filters for the current company.
191
368
  *
@@ -102,6 +102,7 @@ class MaintenanceScheduleController extends FleetOpsController
102
102
  'company_uuid' => $schedule->company_uuid,
103
103
  'schedule_uuid' => $schedule->uuid,
104
104
  'subject' => $schedule->name,
105
+ 'category' => 'preventive_maintenance',
105
106
  'status' => 'open',
106
107
  'priority' => $schedule->default_priority ?? 'normal',
107
108
  'target_type' => $schedule->subject_type,
@@ -6,7 +6,6 @@ use Fleetbase\Exceptions\FleetbaseRequestValidationException;
6
6
  use Fleetbase\FleetOps\Events\EntityActivityChanged;
7
7
  use Fleetbase\FleetOps\Events\EntityCompleted;
8
8
  use Fleetbase\FleetOps\Events\OrderDispatchFailed;
9
- use Fleetbase\FleetOps\Events\OrderReady;
10
9
  use Fleetbase\FleetOps\Events\OrderStarted;
11
10
  use Fleetbase\FleetOps\Events\WaypointActivityChanged;
12
11
  use Fleetbase\FleetOps\Events\WaypointCompleted;
@@ -20,6 +19,8 @@ use Fleetbase\FleetOps\Http\Resources\v1\Index\Order as OrderIndexResource;
20
19
  use Fleetbase\FleetOps\Http\Resources\v1\Order as OrderResource;
21
20
  use Fleetbase\FleetOps\Http\Resources\v1\Proof as ProofResource;
22
21
  use Fleetbase\FleetOps\Imports\OrdersImport;
22
+ use Fleetbase\FleetOps\Jobs\FinalizeInternalOrderCreation;
23
+ use Fleetbase\FleetOps\Jobs\NotifyBulkAssignedDriver;
23
24
  use Fleetbase\FleetOps\Models\Driver;
24
25
  use Fleetbase\FleetOps\Models\Entity;
25
26
  use Fleetbase\FleetOps\Models\Order;
@@ -210,14 +211,7 @@ class OrderController extends FleetOpsController
210
211
  // if service quote attached purchase
211
212
  $order->purchaseServiceQuote($serviceQuote);
212
213
 
213
- // Run background processes on queue
214
- dispatch(function () use ($order): void {
215
- // notify driver if assigned
216
- $order->notifyDriverAssigned();
217
-
218
- // Trigger order created event
219
- event(new OrderReady($order));
220
- })->afterCommit();
214
+ FinalizeInternalOrderCreation::dispatch($order->uuid)->afterCommit();
221
215
  }
222
216
  );
223
217
 
@@ -527,29 +521,7 @@ class OrderController extends FleetOpsController
527
521
 
528
522
  // Queue Per‑Order Notifications
529
523
  if (!$request->boolean('silent')) {
530
- dispatch(function () use ($orderUuids, $driver): void {
531
- // Re‑hydrate Driver To Avoid Serializing The Full Model
532
- $driver = Driver::whereUuid($driver->uuid)->first();
533
-
534
- // Stream Orders To Keep Memory Footprint Low
535
- Order::whereIn('uuid', $orderUuids)
536
- ->cursor()
537
- ->each(function (Order $order) use ($driver): void {
538
- // Synchronize In‑Memory Model
539
- $order->setRelation('driverAssigned', $driver);
540
- $order->driver_assigned_uuid = $driver->uuid;
541
-
542
- try {
543
- $order->notifyDriverAssigned();
544
- } catch (\Throwable $e) {
545
- logger()->warning(
546
- 'Failed notifying driver on order ' . $order->uuid,
547
- ['error' => $e->getMessage()]
548
- );
549
- }
550
- });
551
- })
552
- ->afterCommit();
524
+ NotifyBulkAssignedDriver::dispatch($orderUuids->all(), $driver->uuid)->afterCommit();
553
525
  }
554
526
 
555
527
  return response()->json([
@@ -332,9 +332,9 @@ class SearchController extends Controller
332
332
 
333
333
  private function searchWorkOrders(string $query, int $limit): Collection
334
334
  {
335
- return $this->searchGeneric(WorkOrder::class, ['code', 'subject', 'instructions', 'status', 'priority', 'public_id', 'uuid'], $query, $limit, fn (WorkOrder $workOrder) => [
335
+ return $this->searchGeneric(WorkOrder::class, ['code', 'subject', 'category', 'instructions', 'status', 'priority', 'public_id', 'uuid'], $query, $limit, fn (WorkOrder $workOrder) => [
336
336
  'label' => $workOrder->code ?: $workOrder->subject,
337
- 'description' => $this->description($workOrder->status, $workOrder->priority, $workOrder->subject),
337
+ 'description' => $this->description($workOrder->status, $workOrder->priority, $workOrder->category ?: $workOrder->subject),
338
338
  'icon' => 'clipboard-list',
339
339
  'type' => 'Work Order',
340
340
  'route' => 'console.fleet-ops.maintenance.work-orders.index.details',