@fleetbase/fleetops-engine 0.6.55 → 0.6.57

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 (161) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/ai/create-order-preview.hbs +297 -0
  3. package/addon/components/ai/create-order-preview.js +362 -0
  4. package/addon/components/ai/route-preview-map.hbs +79 -0
  5. package/addon/components/ai/route-preview-map.js +527 -0
  6. package/addon/components/device/details.js +1 -1
  7. package/addon/components/device-event/details.hbs +192 -0
  8. package/addon/components/device-event/details.js +151 -0
  9. package/addon/components/driver/schedule.js +1 -1
  10. package/addon/components/fleet/details.hbs +21 -8
  11. package/addon/components/fleet/details.js +55 -1
  12. package/addon/components/fleet/hierarchy-tree.hbs +137 -0
  13. package/addon/components/fleet/hierarchy-tree.js +528 -0
  14. package/addon/components/layout/fleet-ops-sidebar.js +15 -9
  15. package/addon/components/map/drawer/device-event-listing.js +1 -0
  16. package/addon/components/map/drawer/driver-listing.js +1 -0
  17. package/addon/components/map/drawer/place-listing.js +1 -0
  18. package/addon/components/map/drawer/vehicle-listing.js +1 -0
  19. package/addon/components/order/details.hbs +34 -1
  20. package/addon/components/order/form/details.hbs +4 -0
  21. package/addon/components/order/form.hbs +9 -1
  22. package/addon/components/telematic/details.hbs +1 -1
  23. package/addon/components/telematic/form.hbs +2 -2
  24. package/addon/components/telematic/settings.hbs +1 -1
  25. package/addon/components/vehicle/card.hbs +14 -2
  26. package/addon/components/vehicle/form.hbs +13 -1
  27. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  28. package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
  29. package/addon/controllers/connectivity/events/details.js +68 -1
  30. package/addon/controllers/connectivity/sensors/index.js +4 -0
  31. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  32. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  33. package/addon/controllers/settings/map.js +19 -0
  34. package/addon/extension.js +14 -0
  35. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  36. package/addon/routes/connectivity/events/details.js +26 -1
  37. package/addon/routes/management/fleets/index/details.js +1 -1
  38. package/addon/routes/management/vehicles/index/details.js +3 -1
  39. package/addon/services/map-adapter/google.js +66 -1
  40. package/addon/services/map-adapter/leaflet.js +1 -1
  41. package/addon/services/map-adapter-interface.js +1 -1
  42. package/addon/services/map-manager.js +16 -1
  43. package/addon/services/map-settings.js +15 -0
  44. package/addon/styles/fleetops-engine.css +487 -1
  45. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
  46. package/addon/templates/connectivity/events/details.hbs +40 -1
  47. package/addon/templates/settings/map.hbs +38 -9
  48. package/addon/utils/leaflet-plugin-loader.js +5 -5
  49. package/addon/utils/map-drawer-dropdown-position.js +11 -13
  50. package/addon/utils/route-colors.js +1 -1
  51. package/app/components/ai/create-order-preview.js +1 -0
  52. package/app/components/ai/route-preview-map.js +1 -0
  53. package/app/components/device-event/details.hbs +192 -0
  54. package/app/components/device-event/details.js +1 -0
  55. package/app/components/fleet/hierarchy-tree.js +1 -0
  56. package/app/services/route-engine.js +1 -0
  57. package/composer.json +1 -1
  58. package/extension.json +1 -1
  59. package/package.json +6 -4
  60. package/scripts/normalize-rgba-alpha.js +90 -0
  61. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  62. package/server/src/Exports/ContactExport.php +12 -5
  63. package/server/src/Exports/DeviceExport.php +92 -0
  64. package/server/src/Exports/DriverExport.php +28 -3
  65. package/server/src/Exports/EquipmentExport.php +94 -0
  66. package/server/src/Exports/FleetExport.php +22 -7
  67. package/server/src/Exports/FuelReportExport.php +16 -1
  68. package/server/src/Exports/IssueExport.php +25 -3
  69. package/server/src/Exports/MaintenanceExport.php +106 -0
  70. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  71. package/server/src/Exports/OrderExport.php +24 -5
  72. package/server/src/Exports/PartExport.php +99 -0
  73. package/server/src/Exports/PlaceExport.php +33 -7
  74. package/server/src/Exports/SensorExport.php +94 -0
  75. package/server/src/Exports/ServiceAreaExport.php +31 -7
  76. package/server/src/Exports/ServiceRateExport.php +41 -1
  77. package/server/src/Exports/TelematicExport.php +90 -0
  78. package/server/src/Exports/VehicleExport.php +274 -8
  79. package/server/src/Exports/VendorExport.php +15 -2
  80. package/server/src/Exports/WorkOrderExport.php +92 -0
  81. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  82. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  83. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  84. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  85. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  86. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  87. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  88. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
  89. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  90. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  91. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  92. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  93. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  94. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  95. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  96. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  97. package/server/src/Http/Filter/DeviceFilter.php +33 -10
  98. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  99. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  100. package/server/src/Http/Filter/PartFilter.php +47 -0
  101. package/server/src/Http/Filter/SensorFilter.php +34 -6
  102. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  103. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  104. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  105. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  106. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  107. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  108. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  109. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  110. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  111. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  112. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  113. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  114. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  115. package/server/src/Http/Resources/v1/Device.php +72 -0
  116. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  117. package/server/src/Http/Resources/v1/Fleet.php +8 -2
  118. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  119. package/server/src/Http/Resources/v1/Part.php +64 -0
  120. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  121. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  122. package/server/src/Models/Driver.php +6 -0
  123. package/server/src/Models/Order.php +20 -7
  124. package/server/src/Models/Place.php +11 -3
  125. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  126. package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
  127. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
  128. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
  129. package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
  130. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
  131. package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
  132. package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
  133. package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
  134. package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
  135. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
  136. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
  137. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
  138. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  139. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  140. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  141. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  142. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
  143. package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
  144. package/server/src/Support/Telematics/TelematicService.php +28 -9
  145. package/server/src/Support/Utils.php +52 -0
  146. package/server/src/routes.php +67 -14
  147. package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
  148. package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
  149. package/server/tests/ExportCoverageTest.php +79 -0
  150. package/server/tests/FleetHierarchyResourceTest.php +12 -0
  151. package/server/tests/MetricsRegistryTest.php +45 -5
  152. package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
  153. package/server/tests/PointResolutionTest.php +63 -0
  154. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  155. package/server/tests/TelematicsHardeningTest.php +636 -1
  156. package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
  157. package/addon/components/table/cell/dropdown/action-item.js +0 -62
  158. package/addon/components/table/cell/dropdown.hbs +0 -28
  159. package/addon/components/table/cell/dropdown.js +0 -91
  160. package/app/components/table/cell/dropdown/action-item.js +0 -1
  161. package/app/components/table/cell/dropdown.js +0 -1
@@ -0,0 +1,95 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Ai\Capabilities;
4
+
5
+ use Fleetbase\Ai\Models\AiTask;
6
+ use Fleetbase\Ai\Support\AiRelativeDateResolver;
7
+ use Fleetbase\FleetOps\Models\Order;
8
+
9
+ class OrderInsightsCapability extends AbstractFleetOpsAICapability
10
+ {
11
+ public function key(): string
12
+ {
13
+ return 'fleet-ops.order_insights';
14
+ }
15
+
16
+ public function label(): string
17
+ {
18
+ return 'Fleet-Ops order insights';
19
+ }
20
+
21
+ public function description(): string
22
+ {
23
+ return 'Answers bounded aggregate questions about Fleet-Ops orders, statuses, and order values.';
24
+ }
25
+
26
+ public function permissions(): array
27
+ {
28
+ return ['fleet-ops see order'];
29
+ }
30
+
31
+ public function resolve(AiTask $task): array
32
+ {
33
+ if (!$this->can('fleet-ops see order')) {
34
+ return ['authorized' => false, 'message' => 'Current user cannot access Fleet-Ops orders.'];
35
+ }
36
+
37
+ $prompt = $this->prompt($task);
38
+ $window = $this->dateWindow($prompt);
39
+ $amount = $this->amountThreshold($prompt);
40
+ $query = Order::where('company_uuid', session('company'));
41
+
42
+ if ($window) {
43
+ $query->whereBetween('created_at', [$window['start'], $window['end']]);
44
+ }
45
+
46
+ if ($amount !== null) {
47
+ $query->whereHas('transaction', fn ($transaction) => $transaction->where('amount', '>', $amount));
48
+ }
49
+
50
+ $total = (clone $query)->count();
51
+
52
+ return [
53
+ 'authorized' => true,
54
+ 'metric' => 'orders',
55
+ 'date_window' => $window ? [
56
+ 'label' => $window['label'],
57
+ 'timezone' => $window['timezone'],
58
+ 'start' => $window['start']->toIso8601String(),
59
+ 'end' => $window['end']->toIso8601String(),
60
+ ] : null,
61
+ 'amount_threshold' => $amount,
62
+ 'count' => $total,
63
+ 'counts_by_status' => (clone $query)
64
+ ->selectRaw('status, count(*) as aggregate')
65
+ ->groupBy('status')
66
+ ->pluck('aggregate', 'status')
67
+ ->all(),
68
+ 'sample_order_ids' => (clone $query)->latest()->limit(10)->pluck('public_id')->filter()->values()->all(),
69
+ ];
70
+ }
71
+
72
+ protected function matchesPrompt(string $prompt): bool
73
+ {
74
+ return str_contains($prompt, 'order') && $this->containsAny($prompt, ['how many', 'count', 'report', 'value', 'over', 'status', 'today', 'yesterday', 'tomorrow', 'last week', 'this week', 'next week', 'last month', 'this month', 'last 30 days', 'completed', 'active']);
75
+ }
76
+
77
+ protected function amountThreshold(string $prompt): ?float
78
+ {
79
+ if (preg_match('/(?:over|greater than|above)\s+\$?([0-9]+(?:\.[0-9]+)?)/', $prompt, $matches)) {
80
+ return (float) $matches[1];
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ protected function dateWindow(string $prompt): ?array
87
+ {
88
+ return $this->relativeDateResolver()->resolveWindow($prompt);
89
+ }
90
+
91
+ protected function relativeDateResolver(): AiRelativeDateResolver
92
+ {
93
+ return function_exists('app') ? app(AiRelativeDateResolver::class) : new AiRelativeDateResolver(null);
94
+ }
95
+ }
@@ -0,0 +1,195 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Ai\Capabilities;
4
+
5
+ use Fleetbase\Ai\Models\AiTask;
6
+ use Fleetbase\FleetOps\Models\Device;
7
+ use Fleetbase\FleetOps\Models\Driver;
8
+ use Fleetbase\FleetOps\Models\Maintenance;
9
+ use Fleetbase\FleetOps\Models\Order;
10
+ use Fleetbase\FleetOps\Models\Sensor;
11
+ use Fleetbase\FleetOps\Models\Telematic;
12
+ use Fleetbase\FleetOps\Models\Vehicle;
13
+ use Fleetbase\FleetOps\Models\WorkOrder;
14
+
15
+ class SearchResourcesCapability extends AbstractFleetOpsAICapability
16
+ {
17
+ public function key(): string
18
+ {
19
+ return 'fleet-ops.search_resources';
20
+ }
21
+
22
+ public function label(): string
23
+ {
24
+ return 'Search Fleet-Ops resources';
25
+ }
26
+
27
+ public function description(): string
28
+ {
29
+ return 'Finds relevant Fleet-Ops orders, vehicles, drivers, work orders, maintenances, devices, sensors, and telematics.';
30
+ }
31
+
32
+ public function permissions(): array
33
+ {
34
+ return [
35
+ 'fleet-ops see order',
36
+ 'fleet-ops see vehicle',
37
+ 'fleet-ops see driver',
38
+ 'fleet-ops see work-order',
39
+ 'fleet-ops see maintenance',
40
+ 'fleet-ops see device',
41
+ 'fleet-ops see sensor',
42
+ 'fleet-ops see telematic',
43
+ ];
44
+ }
45
+
46
+ public function resolve(AiTask $task): array
47
+ {
48
+ $prompt = (string) $task->prompt;
49
+ $terms = $this->searchTerms($prompt);
50
+
51
+ return [
52
+ 'query_terms' => $terms,
53
+ 'results' => array_filter([
54
+ 'orders' => $this->orders($terms),
55
+ 'vehicles' => $this->vehicles($terms),
56
+ 'drivers' => $this->drivers($terms),
57
+ 'work_orders' => $this->workOrders($terms),
58
+ 'maintenances' => $this->maintenances($terms),
59
+ 'devices' => $this->devices($terms),
60
+ 'sensors' => $this->sensors($terms),
61
+ 'telematics' => $this->telematics($terms),
62
+ ]),
63
+ ];
64
+ }
65
+
66
+ protected function matchesPrompt(string $prompt): bool
67
+ {
68
+ return $this->containsAny($prompt, ['find', 'show', 'open', 'look up', 'tell me about', 'status of', 'order', 'vehicle', 'driver', 'work order', 'maintenance', 'device', 'sensor', 'telematic']);
69
+ }
70
+
71
+ protected function orders(array $terms): array
72
+ {
73
+ if (!$this->can('fleet-ops see order')) {
74
+ return [];
75
+ }
76
+
77
+ return Order::with(['transaction', 'trackingNumber'])
78
+ ->where('company_uuid', session('company'))
79
+ ->where(function ($query) use ($terms) {
80
+ $this->whereLikeAny($query, ['public_id', 'internal_id', 'uuid', 'status', 'type'], $terms);
81
+ $query->orWhereHas('trackingNumber', fn ($tracking) => $this->whereLikeAny($tracking, ['tracking_number', 'barcode'], $terms));
82
+ })
83
+ ->limit(5)
84
+ ->get()
85
+ ->map(fn (Order $order) => [
86
+ 'id' => $order->public_id,
87
+ 'uuid' => $order->uuid,
88
+ 'tracking' => $order->tracking,
89
+ 'status' => $order->status,
90
+ 'type' => $order->type,
91
+ 'transaction_amount' => $order->transaction_amount,
92
+ 'transaction_currency' => $order->transaction_currency,
93
+ 'route' => 'console.fleet-ops.operations.orders.index.details',
94
+ 'models' => [$order->public_id ?: $order->uuid],
95
+ ])
96
+ ->values()
97
+ ->all();
98
+ }
99
+
100
+ protected function vehicles(array $terms): array
101
+ {
102
+ if (!$this->can('fleet-ops see vehicle')) {
103
+ return [];
104
+ }
105
+
106
+ return Vehicle::where('company_uuid', session('company'))
107
+ ->where(fn ($query) => $this->whereLikeAny($query, ['name', 'make', 'model', 'plate_number', 'vin', 'public_id', 'internal_id'], $terms))
108
+ ->limit(5)
109
+ ->get()
110
+ ->map(fn (Vehicle $vehicle) => [
111
+ 'id' => $vehicle->public_id,
112
+ 'uuid' => $vehicle->uuid,
113
+ 'name' => $vehicle->display_name ?: $vehicle->name,
114
+ 'plate' => $vehicle->plate_number,
115
+ 'status' => $vehicle->status,
116
+ 'route' => 'console.fleet-ops.management.vehicles.index.details',
117
+ 'models' => [$vehicle->public_id ?: $vehicle->uuid],
118
+ ])
119
+ ->values()
120
+ ->all();
121
+ }
122
+
123
+ protected function drivers(array $terms): array
124
+ {
125
+ if (!$this->can('fleet-ops see driver')) {
126
+ return [];
127
+ }
128
+
129
+ return Driver::with('user')
130
+ ->where('company_uuid', session('company'))
131
+ ->where(function ($query) use ($terms) {
132
+ $this->whereLikeAny($query, ['public_id', 'uuid', 'drivers_license_number', 'status'], $terms);
133
+ $query->orWhereHas('user', fn ($user) => $this->whereLikeAny($user, ['name', 'email', 'phone'], $terms));
134
+ })
135
+ ->limit(5)
136
+ ->get()
137
+ ->map(fn (Driver $driver) => [
138
+ 'id' => $driver->public_id,
139
+ 'uuid' => $driver->uuid,
140
+ 'name' => data_get($driver, 'user.name'),
141
+ 'status' => $driver->status,
142
+ 'route' => 'console.fleet-ops.management.drivers.index.details',
143
+ 'models' => [$driver->public_id ?: $driver->uuid],
144
+ ])
145
+ ->values()
146
+ ->all();
147
+ }
148
+
149
+ protected function workOrders(array $terms): array
150
+ {
151
+ return $this->generic(WorkOrder::class, 'fleet-ops see work-order', ['public_id', 'uuid', 'code', 'subject', 'status', 'priority'], 'console.fleet-ops.maintenance.work-orders.index.details', $terms);
152
+ }
153
+
154
+ protected function maintenances(array $terms): array
155
+ {
156
+ return $this->generic(Maintenance::class, 'fleet-ops see maintenance', ['public_id', 'uuid', 'status', 'type', 'summary', 'notes'], 'console.fleet-ops.maintenance.maintenances.index.details', $terms);
157
+ }
158
+
159
+ protected function devices(array $terms): array
160
+ {
161
+ return $this->generic(Device::class, 'fleet-ops see device', ['public_id', 'uuid', 'name', 'device_id', 'imei', 'serial_number', 'status'], 'console.fleet-ops.connectivity.devices.index.details', $terms);
162
+ }
163
+
164
+ protected function sensors(array $terms): array
165
+ {
166
+ return $this->generic(Sensor::class, 'fleet-ops see sensor', ['public_id', 'uuid', 'name', 'internal_id', 'serial_number', 'imei', 'type', 'sensor_type', 'status'], 'console.fleet-ops.connectivity.sensors.index.details', $terms);
167
+ }
168
+
169
+ protected function telematics(array $terms): array
170
+ {
171
+ return $this->generic(Telematic::class, 'fleet-ops see telematic', ['public_id', 'uuid', 'name', 'provider', 'status'], 'console.fleet-ops.connectivity.telematics.details', $terms);
172
+ }
173
+
174
+ protected function generic(string $modelClass, string $permission, array $columns, string $route, array $terms): array
175
+ {
176
+ if (!$this->can($permission)) {
177
+ return [];
178
+ }
179
+
180
+ return $modelClass::where('company_uuid', session('company'))
181
+ ->where(fn ($query) => $this->whereLikeAny($query, $columns, $terms))
182
+ ->limit(5)
183
+ ->get()
184
+ ->map(fn ($record) => [
185
+ 'id' => $record->public_id,
186
+ 'uuid' => $record->uuid,
187
+ 'name' => $record->name ?? $record->subject ?? $record->summary ?? $record->description ?? null,
188
+ 'status' => $record->status ?? null,
189
+ 'route' => $route,
190
+ 'models' => [$record->public_id ?: $record->uuid],
191
+ ])
192
+ ->values()
193
+ ->all();
194
+ }
195
+ }
@@ -0,0 +1,150 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Ai;
4
+
5
+ use Fleetbase\Ai\Support\AiQueryableResource;
6
+ use Fleetbase\Ai\Support\AiQueryRegistry;
7
+ use Fleetbase\FleetOps\Models\Device;
8
+ use Fleetbase\FleetOps\Models\Driver;
9
+ use Fleetbase\FleetOps\Models\Fleet;
10
+ use Fleetbase\FleetOps\Models\Order;
11
+ use Fleetbase\FleetOps\Models\ServiceArea;
12
+ use Fleetbase\FleetOps\Models\Vehicle;
13
+ use Fleetbase\FleetOps\Models\Zone;
14
+
15
+ class FleetOpsAiQueryResources
16
+ {
17
+ public static function register(AiQueryRegistry $registry): void
18
+ {
19
+ $registry->register(new AiQueryableResource(
20
+ key: 'fleet-ops.drivers',
21
+ label: 'Fleet-Ops drivers',
22
+ module: 'fleet-ops',
23
+ modelClass: Driver::class,
24
+ permission: 'fleet-ops see driver',
25
+ aliases: ['drivers', 'driver'],
26
+ fields: [
27
+ 'online' => ['column' => 'online', 'type' => 'boolean'],
28
+ 'status' => ['column' => 'status', 'type' => 'string'],
29
+ 'current_status' => ['column' => 'current_status', 'type' => 'string'],
30
+ 'city' => ['column' => 'city', 'type' => 'string'],
31
+ 'country' => ['column' => 'country', 'type' => 'string'],
32
+ 'vehicle_uuid' => ['column' => 'vehicle_uuid', 'type' => 'uuid'],
33
+ 'updated_at' => ['column' => 'updated_at', 'type' => 'datetime'],
34
+ 'created_at' => ['column' => 'created_at', 'type' => 'datetime'],
35
+ ],
36
+ sampleFields: ['public_id', 'name', 'status', 'current_status', 'online', 'city', 'country', 'vehicle_uuid'],
37
+ locationField: 'location',
38
+ directivePermission: 'fleet-ops list driver',
39
+ maxLimit: 500,
40
+ ));
41
+
42
+ $registry->register(new AiQueryableResource(
43
+ key: 'fleet-ops.vehicles',
44
+ label: 'Fleet-Ops vehicles',
45
+ module: 'fleet-ops',
46
+ modelClass: Vehicle::class,
47
+ permission: 'fleet-ops see vehicle',
48
+ aliases: ['vehicles', 'vehicle'],
49
+ fields: [
50
+ 'online' => ['column' => 'online', 'type' => 'boolean'],
51
+ 'status' => ['column' => 'status', 'type' => 'string'],
52
+ 'driver_uuid' => ['column' => 'driver_uuid', 'type' => 'uuid'],
53
+ 'city' => ['column' => 'city', 'type' => 'string'],
54
+ 'country' => ['column' => 'country', 'type' => 'string'],
55
+ 'updated_at' => ['column' => 'updated_at', 'type' => 'datetime'],
56
+ 'created_at' => ['column' => 'created_at', 'type' => 'datetime'],
57
+ ],
58
+ sampleFields: ['public_id', 'display_name', 'status', 'online', 'city', 'country', 'driver_uuid'],
59
+ locationField: 'location',
60
+ directivePermission: 'fleet-ops list vehicle',
61
+ maxLimit: 500,
62
+ ));
63
+
64
+ $registry->register(new AiQueryableResource(
65
+ key: 'fleet-ops.devices',
66
+ label: 'Fleet-Ops devices',
67
+ module: 'fleet-ops',
68
+ modelClass: Device::class,
69
+ permission: 'fleet-ops see device',
70
+ aliases: ['devices', 'device'],
71
+ fields: [
72
+ 'online' => ['column' => 'online', 'type' => 'boolean'],
73
+ 'status' => ['column' => 'status', 'type' => 'string'],
74
+ 'provider' => ['column' => 'provider', 'type' => 'string'],
75
+ 'telematic_uuid' => ['column' => 'telematic_uuid', 'type' => 'uuid'],
76
+ 'attachable_uuid' => ['column' => 'attachable_uuid', 'type' => 'uuid'],
77
+ 'last_online_at' => ['column' => 'last_online_at', 'type' => 'datetime'],
78
+ 'updated_at' => ['column' => 'updated_at', 'type' => 'datetime'],
79
+ ],
80
+ sampleFields: ['public_id', 'name', 'status', 'online', 'provider', 'telematic_uuid', 'attachable_uuid'],
81
+ directivePermission: 'fleet-ops list device',
82
+ maxLimit: 500,
83
+ ));
84
+
85
+ $registry->register(new AiQueryableResource(
86
+ key: 'fleet-ops.orders',
87
+ label: 'Fleet-Ops orders',
88
+ module: 'fleet-ops',
89
+ modelClass: Order::class,
90
+ permission: 'fleet-ops see order',
91
+ aliases: ['orders', 'order'],
92
+ fields: [
93
+ 'status' => ['column' => 'status', 'type' => 'string'],
94
+ 'type' => ['column' => 'type', 'type' => 'string'],
95
+ 'driver_assigned_uuid' => ['column' => 'driver_assigned_uuid', 'type' => 'uuid'],
96
+ 'vehicle_assigned_uuid' => ['column' => 'vehicle_assigned_uuid', 'type' => 'uuid'],
97
+ 'created_at' => ['column' => 'created_at', 'type' => 'datetime'],
98
+ 'scheduled_at' => ['column' => 'scheduled_at', 'type' => 'datetime'],
99
+ ],
100
+ sampleFields: ['public_id', 'status', 'type', 'driver_assigned_uuid', 'vehicle_assigned_uuid', 'created_at'],
101
+ directivePermission: 'fleet-ops list order',
102
+ maxLimit: 500,
103
+ ));
104
+
105
+ $registry->register(new AiQueryableResource(
106
+ key: 'fleet-ops.fleets',
107
+ label: 'Fleet-Ops fleets',
108
+ module: 'fleet-ops',
109
+ modelClass: Fleet::class,
110
+ permission: 'fleet-ops see fleet',
111
+ aliases: ['fleets', 'fleet'],
112
+ fields: [
113
+ 'status' => ['column' => 'status', 'type' => 'string'],
114
+ 'task' => ['column' => 'task', 'type' => 'string'],
115
+ 'updated_at' => ['column' => 'updated_at', 'type' => 'datetime'],
116
+ ],
117
+ sampleFields: ['public_id', 'name', 'status', 'task'],
118
+ directivePermission: 'fleet-ops list fleet',
119
+ ));
120
+
121
+ $registry->register(new AiQueryableResource(
122
+ key: 'fleet-ops.service_areas',
123
+ label: 'Fleet-Ops service areas',
124
+ module: 'fleet-ops',
125
+ modelClass: ServiceArea::class,
126
+ permission: 'fleet-ops see service-area',
127
+ aliases: ['service areas', 'service area', 'service_areas'],
128
+ fields: [
129
+ 'status' => ['column' => 'status', 'type' => 'string'],
130
+ 'country' => ['column' => 'country', 'type' => 'string'],
131
+ 'type' => ['column' => 'type', 'type' => 'string'],
132
+ ],
133
+ sampleFields: ['public_id', 'name', 'status', 'country', 'type'],
134
+ ));
135
+
136
+ $registry->register(new AiQueryableResource(
137
+ key: 'fleet-ops.zones',
138
+ label: 'Fleet-Ops zones',
139
+ module: 'fleet-ops',
140
+ modelClass: Zone::class,
141
+ permission: 'fleet-ops see zone',
142
+ aliases: ['zones', 'zone'],
143
+ fields: [
144
+ 'status' => ['column' => 'status', 'type' => 'string'],
145
+ 'service_area_uuid' => ['column' => 'service_area_uuid', 'type' => 'uuid'],
146
+ ],
147
+ sampleFields: ['public_id', 'name', 'status', 'service_area_uuid'],
148
+ ));
149
+ }
150
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Analytics;
4
4
 
5
- use Fleetbase\Models\Transaction;
5
+ use Fleetbase\FleetOps\Support\Metrics\ActiveRevenueQuery;
6
6
  use Illuminate\Support\Carbon;
7
7
 
8
8
  /**
@@ -32,9 +32,7 @@ class RevenueTrend extends AbstractAnalytics
32
32
  default => '%Y-%m-%d',
33
33
  };
34
34
 
35
- $rows = Transaction::where('company_uuid', $this->company->uuid)
36
- ->where('currency', $currency)
37
- ->whereBetween('created_at', [$start, $end])
35
+ $rows = ActiveRevenueQuery::forCompany($this->company, $currency, $start, $end)
38
36
  ->selectRaw('DATE_FORMAT(created_at, ?) as bucket, SUM(amount) as total', [$format])
39
37
  ->groupBy('bucket')
40
38
  ->orderBy('bucket')
@@ -47,10 +45,7 @@ class RevenueTrend extends AbstractAnalytics
47
45
  $duration = $end->getTimestamp() - $start->getTimestamp();
48
46
  $compareStart = (clone $start);
49
47
  $compareStart = (new \DateTime())->setTimestamp($start->getTimestamp() - $duration);
50
- $previousTotal = (float) Transaction::where('company_uuid', $this->company->uuid)
51
- ->where('currency', $currency)
52
- ->whereBetween('created_at', [$compareStart, $start])
53
- ->sum('amount');
48
+ $previousTotal = (float) ActiveRevenueQuery::forCompany($this->company, $currency, $compareStart, $start)->sum('amount');
54
49
 
55
50
  $deltaPct = $previousTotal > 0
56
51
  ? round((($total - $previousTotal) / $previousTotal) * 100, 1)
@@ -0,0 +1,121 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Fleetbase\Models\Company;
7
+ use Fleetbase\Models\Transaction;
8
+ use Illuminate\Database\Eloquent\Builder;
9
+ use Illuminate\Support\Facades\Schema;
10
+
11
+ class ActiveRevenueQuery
12
+ {
13
+ public const ACTIVE_STATUSES = [Transaction::STATUS_SUCCESS];
14
+ public const INACTIVE_TRANSACTION_STATUSES = ['pending', 'failed', 'cancelled', 'canceled', 'void', 'voided', 'expired', 'reversed', 'refunded', 'ignored'];
15
+ public const INACTIVE_ORDER_STATUSES = ['canceled', 'cancelled', 'order_canceled'];
16
+ public const INACTIVE_INVOICE_STATUSES = ['void', 'voided', 'cancelled', 'canceled'];
17
+
18
+ public static function forCompany(Company $company, string $currency, ?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null): Builder
19
+ {
20
+ $query = Transaction::query()
21
+ ->where('company_uuid', $company->uuid)
22
+ ->where('currency', $currency)
23
+ ->where('direction', Transaction::DIRECTION_CREDIT)
24
+ ->whereIn('status', self::ACTIVE_STATUSES)
25
+ ->whereNotIn('status', self::INACTIVE_TRANSACTION_STATUSES)
26
+ ->whereNull('deleted_at')
27
+ ->whereNull('voided_at')
28
+ ->whereNull('reversed_at')
29
+ ->whereNull('parent_transaction_uuid');
30
+
31
+ if ($start && $end) {
32
+ $query->whereBetween('created_at', [$start, $end]);
33
+ }
34
+
35
+ static::excludeInactiveOrders($query);
36
+ static::excludeInactiveInvoices($query);
37
+
38
+ return $query;
39
+ }
40
+
41
+ protected static function excludeInactiveOrders(Builder $query): void
42
+ {
43
+ if (!Schema::hasTable('orders')) {
44
+ return;
45
+ }
46
+
47
+ foreach (['subject_uuid' => 'subject_type', 'context_uuid' => 'context_type'] as $uuidColumn => $typeColumn) {
48
+ $query->whereNotExists(function ($subQuery) use ($uuidColumn, $typeColumn) {
49
+ $subQuery->selectRaw('1')
50
+ ->from('orders')
51
+ ->whereColumn('orders.uuid', "transactions.{$uuidColumn}")
52
+ ->where("transactions.{$typeColumn}", Order::class)
53
+ ->where(function ($orderQuery) {
54
+ static::inactiveOrderConstraint($orderQuery);
55
+ });
56
+ });
57
+ }
58
+
59
+ $query->whereNotExists(function ($subQuery) {
60
+ $subQuery->selectRaw('1')
61
+ ->from('orders')
62
+ ->whereColumn('orders.transaction_uuid', 'transactions.uuid')
63
+ ->where(function ($orderQuery) {
64
+ static::inactiveOrderConstraint($orderQuery);
65
+ });
66
+ });
67
+ }
68
+
69
+ protected static function excludeInactiveInvoices(Builder $query): void
70
+ {
71
+ $invoiceClass = 'Fleetbase\\Ledger\\Models\\Invoice';
72
+
73
+ if (!class_exists($invoiceClass) || !Schema::hasTable('ledger_invoices')) {
74
+ return;
75
+ }
76
+
77
+ foreach (['subject_uuid' => 'subject_type', 'context_uuid' => 'context_type'] as $uuidColumn => $typeColumn) {
78
+ $query->whereNotExists(function ($subQuery) use ($uuidColumn, $typeColumn, $invoiceClass) {
79
+ $subQuery->selectRaw('1')
80
+ ->from('ledger_invoices')
81
+ ->whereColumn('ledger_invoices.uuid', "transactions.{$uuidColumn}")
82
+ ->where("transactions.{$typeColumn}", $invoiceClass)
83
+ ->where(function ($invoiceQuery) {
84
+ static::inactiveInvoiceConstraint($invoiceQuery);
85
+ });
86
+ });
87
+ }
88
+
89
+ $query->whereNotExists(function ($subQuery) {
90
+ $subQuery->selectRaw('1')
91
+ ->from('ledger_invoices')
92
+ ->whereColumn('ledger_invoices.transaction_uuid', 'transactions.uuid')
93
+ ->where(function ($invoiceQuery) {
94
+ static::inactiveInvoiceConstraint($invoiceQuery);
95
+ });
96
+ });
97
+ }
98
+
99
+ protected static function inactiveOrderConstraint($query): void
100
+ {
101
+ $query->whereNotNull('orders.deleted_at')
102
+ ->orWhereIn('orders.status', self::INACTIVE_ORDER_STATUSES);
103
+ }
104
+
105
+ protected static function inactiveInvoiceConstraint($query): void
106
+ {
107
+ $query->whereNotNull('ledger_invoices.deleted_at')
108
+ ->orWhereIn('ledger_invoices.status', self::INACTIVE_INVOICE_STATUSES);
109
+
110
+ if (Schema::hasTable('orders')) {
111
+ $query->orWhereExists(function ($orderQuery) {
112
+ $orderQuery->selectRaw('1')
113
+ ->from('orders')
114
+ ->whereColumn('orders.uuid', 'ledger_invoices.order_uuid')
115
+ ->where(function ($inactiveOrderQuery) {
116
+ static::inactiveOrderConstraint($inactiveOrderQuery);
117
+ });
118
+ });
119
+ }
120
+ }
121
+ }
@@ -3,7 +3,6 @@
3
3
  namespace Fleetbase\FleetOps\Support\Metrics;
4
4
 
5
5
  use Fleetbase\FleetOps\Models\Order;
6
- use Fleetbase\Models\Transaction;
7
6
 
8
7
  /**
9
8
  * Average revenue per completed order in the period. Computed as
@@ -39,12 +38,7 @@ class AvgOrderValueMetric extends MoneyMetric
39
38
  return 0.0;
40
39
  }
41
40
 
42
- $revenueQuery = Transaction::where('company_uuid', $this->company->uuid)
43
- ->where('currency', $this->currency());
44
-
45
- if ($this->currentStart && $this->currentEnd) {
46
- $revenueQuery->whereBetween('created_at', [$this->currentStart, $this->currentEnd]);
47
- }
41
+ $revenueQuery = ActiveRevenueQuery::forCompany($this->company, $this->currency(), $this->currentStart, $this->currentEnd);
48
42
 
49
43
  $totalRevenue = (float) $revenueQuery->sum('amount');
50
44
 
@@ -2,8 +2,6 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Metrics;
4
4
 
5
- use Fleetbase\Models\Transaction;
6
-
7
5
  class EarningsMetric extends MoneyMetric
8
6
  {
9
7
  public static function slug(): string
@@ -13,14 +11,7 @@ class EarningsMetric extends MoneyMetric
13
11
 
14
12
  protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
15
13
  {
16
- $query = Transaction::where('company_uuid', $this->company->uuid)
17
- ->where('currency', $this->currency());
18
-
19
- if ($start && $end) {
20
- $query->whereBetween('created_at', [$start, $end]);
21
- }
22
-
23
- return $query;
14
+ return ActiveRevenueQuery::forCompany($this->company, $this->currency(), $start, $end);
24
15
  }
25
16
 
26
17
  protected function aggregate($query): float