@fleetbase/fleetops-engine 0.6.48 → 0.6.50

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 (266) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/driver/details.hbs +5 -1
  7. package/addon/components/driver/form.hbs +11 -1
  8. package/addon/components/fleet-listing-panel.hbs +2 -2
  9. package/addon/components/fleet-listing-panel.js +10 -0
  10. package/addon/components/map/google-live-map.js +18 -75
  11. package/addon/components/map/leaflet-live-map.hbs +187 -37
  12. package/addon/components/map/leaflet-live-map.js +184 -20
  13. package/addon/components/modals/order-change-destination.hbs +46 -0
  14. package/addon/components/modals/order-change-destination.js +106 -0
  15. package/addon/components/modals/stop-activity.hbs +28 -0
  16. package/addon/components/modals/stop-activity.js +52 -0
  17. package/addon/components/modals/update-order-activity.hbs +45 -3
  18. package/addon/components/modals/update-order-activity.js +143 -5
  19. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  20. package/addon/components/orchestrator/phase-builder.js +13 -0
  21. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  22. package/addon/components/orchestrator/resource-panel.js +2 -4
  23. package/addon/components/orchestrator-workbench.js +1 -0
  24. package/addon/components/order/activity-list.hbs +2 -2
  25. package/addon/components/order/activity-timeline.hbs +31 -29
  26. package/addon/components/order/details/activity.hbs +3 -3
  27. package/addon/components/order/details/activity.js +11 -0
  28. package/addon/components/order/details/documents.js +2 -0
  29. package/addon/components/order/details/purchase-rate.hbs +2 -2
  30. package/addon/components/order/details/route.hbs +11 -3
  31. package/addon/components/order/details/route.js +91 -14
  32. package/addon/components/order/details/tracking.hbs +101 -72
  33. package/addon/components/order/details/tracking.js +149 -2
  34. package/addon/components/order/form/details.hbs +3 -3
  35. package/addon/components/order/form/documents.js +11 -4
  36. package/addon/components/order/form/route.hbs +108 -104
  37. package/addon/components/order/form/route.js +37 -1
  38. package/addon/components/order/form/service-rate.hbs +2 -2
  39. package/addon/components/order/form.hbs +2 -2
  40. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  41. package/addon/components/order-config-manager/activity-flow.js +178 -12
  42. package/addon/components/place/address.js +1 -1
  43. package/addon/components/route-list.hbs +17 -17
  44. package/addon/components/route-list.js +42 -10
  45. package/addon/components/service-rate/details.hbs +41 -0
  46. package/addon/components/service-rate/form.hbs +118 -0
  47. package/addon/components/service-rate/form.js +55 -0
  48. package/addon/components/vehicle/form.js +1 -1
  49. package/addon/components/vehicle/pill.hbs +1 -1
  50. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  51. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  52. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  53. package/addon/components/widget/fuel-efficiency.js +43 -0
  54. package/addon/components/widget/geofence-violations.hbs +44 -0
  55. package/addon/components/widget/geofence-violations.js +55 -0
  56. package/addon/components/widget/issues-insights.hbs +43 -0
  57. package/addon/components/widget/issues-insights.js +62 -0
  58. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  59. package/addon/components/widget/kpi-active-orders.js +5 -0
  60. package/addon/components/widget/kpi-aov.hbs +10 -0
  61. package/addon/components/widget/kpi-aov.js +5 -0
  62. package/addon/components/widget/kpi-distance.hbs +10 -0
  63. package/addon/components/widget/kpi-distance.js +5 -0
  64. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  65. package/addon/components/widget/kpi-drivers-online.js +5 -0
  66. package/addon/components/widget/kpi-earnings.hbs +10 -0
  67. package/addon/components/widget/kpi-earnings.js +5 -0
  68. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  69. package/addon/components/widget/kpi-open-issues.js +5 -0
  70. package/addon/components/widget/kpi-tile.hbs +59 -0
  71. package/addon/components/widget/kpi-tile.js +168 -0
  72. package/addon/components/widget/live-fleet.hbs +122 -0
  73. package/addon/components/widget/live-fleet.js +365 -0
  74. package/addon/components/widget/maintenance-overview.hbs +45 -0
  75. package/addon/components/widget/maintenance-overview.js +40 -0
  76. package/addon/components/widget/on-time-delivery.hbs +27 -0
  77. package/addon/components/widget/on-time-delivery.js +73 -0
  78. package/addon/components/widget/operations-pulse.hbs +51 -0
  79. package/addon/components/widget/operations-pulse.js +173 -0
  80. package/addon/components/widget/orders-by-status.hbs +26 -0
  81. package/addon/components/widget/orders-by-status.js +54 -0
  82. package/addon/components/widget/revenue-trend.hbs +34 -0
  83. package/addon/components/widget/revenue-trend.js +67 -0
  84. package/addon/components/widget/top-drivers.hbs +44 -0
  85. package/addon/components/widget/top-drivers.js +54 -0
  86. package/addon/controllers/management/vehicles/index/new.js +1 -1
  87. package/addon/controllers/operations/orders/index/details.js +4 -4
  88. package/addon/controllers/operations/orders/index.js +14 -0
  89. package/addon/controllers/settings/routing.js +26 -0
  90. package/addon/extension.js +172 -3
  91. package/addon/helpers/place-address-html.js +2 -2
  92. package/addon/routes/operations/service-rates/index.js +1 -1
  93. package/addon/services/geofence.js +163 -19
  94. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  95. package/addon/services/map-adapter/google.js +13 -2
  96. package/addon/services/vehicle-actions.js +1 -1
  97. package/addon/styles/fleetops-engine.css +205 -88
  98. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  99. package/addon/templates/maintenance/parts/index.hbs +1 -1
  100. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  101. package/addon/templates/management/vehicles/index.hbs +1 -1
  102. package/addon/templates/settings/avatars.hbs +1 -1
  103. package/addon/templates/settings/custom-fields.hbs +1 -1
  104. package/addon/templates/settings/map.hbs +1 -1
  105. package/addon/templates/settings/navigator-app.hbs +1 -1
  106. package/addon/templates/settings/notifications.hbs +1 -1
  107. package/addon/templates/settings/orchestrator.hbs +1 -1
  108. package/addon/templates/settings/payments/index.hbs +1 -1
  109. package/addon/templates/settings/payments/onboard.hbs +1 -1
  110. package/addon/templates/settings/routing.hbs +22 -2
  111. package/addon/templates/settings/scheduling.hbs +1 -1
  112. package/addon/utils/fleet-ops-options.js +2 -1
  113. package/addon/utils/live-map-card-content.js +171 -0
  114. package/addon/utils/normalize-order-config-flow.js +41 -0
  115. package/addon/utils/place-address-html.js +3 -2
  116. package/addon/utils/route-visualization.js +54 -7
  117. package/app/components/modals/order-change-destination.js +1 -0
  118. package/app/components/modals/stop-activity.js +1 -0
  119. package/app/components/widget/fuel-efficiency.js +1 -0
  120. package/app/components/widget/geofence-violations.js +1 -0
  121. package/app/components/widget/issues-insights.js +1 -0
  122. package/app/components/widget/kpi-active-orders.js +1 -0
  123. package/app/components/widget/kpi-aov.js +1 -0
  124. package/app/components/widget/kpi-distance.js +1 -0
  125. package/app/components/widget/kpi-drivers-online.js +1 -0
  126. package/app/components/widget/kpi-earnings.js +1 -0
  127. package/app/components/widget/kpi-open-issues.js +1 -0
  128. package/app/components/widget/kpi-tile.js +1 -0
  129. package/app/components/widget/live-fleet.js +1 -0
  130. package/app/components/widget/maintenance-overview.js +1 -0
  131. package/app/components/widget/on-time-delivery.js +1 -0
  132. package/app/components/widget/operations-pulse.js +1 -0
  133. package/app/components/widget/orders-by-status.js +1 -0
  134. package/app/components/widget/revenue-trend.js +1 -0
  135. package/app/components/widget/top-drivers.js +1 -0
  136. package/app/utils/live-map-card-content.js +1 -0
  137. package/composer.json +1 -1
  138. package/extension.json +1 -1
  139. package/package.json +3 -3
  140. package/pnpm-workspace.yaml +1 -0
  141. package/server/config/telematics.php +111 -1
  142. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  143. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  144. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  145. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  146. package/server/resources/views/labels/default.php +57 -49
  147. package/server/resources/views/labels/waypoint-label.php +27 -20
  148. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  149. package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
  150. package/server/seeders/Testing/FleetSeeder.php +341 -0
  151. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  152. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  153. package/server/seeders/Testing/OrdersSeeder.php +198 -0
  154. package/server/seeders/Testing/TestingSeeder.php +40 -0
  155. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
  156. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  157. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  158. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  159. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  160. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  161. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  162. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  163. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  164. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  165. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  166. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  167. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  168. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  169. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  170. package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
  171. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  172. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  173. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  174. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  175. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  176. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  177. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  179. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  180. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  181. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  182. package/server/src/Http/Resources/v1/Customer.php +79 -0
  183. package/server/src/Http/Resources/v1/Driver.php +2 -0
  184. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  185. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  186. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  187. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  188. package/server/src/Http/Resources/v1/Payload.php +23 -20
  189. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  190. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  191. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  192. package/server/src/Models/Contact.php +92 -15
  193. package/server/src/Models/Customer.php +64 -0
  194. package/server/src/Models/Driver.php +14 -3
  195. package/server/src/Models/Entity.php +2 -2
  196. package/server/src/Models/Order.php +29 -4
  197. package/server/src/Models/OrderConfig.php +14 -11
  198. package/server/src/Models/Payload.php +17 -1
  199. package/server/src/Models/ServiceRate.php +273 -1
  200. package/server/src/Models/ServiceRateFee.php +39 -9
  201. package/server/src/Models/TrackingNumber.php +8 -2
  202. package/server/src/Models/Vehicle.php +18 -2
  203. package/server/src/Models/Waypoint.php +2 -2
  204. package/server/src/Notifications/LateDeparture.php +48 -0
  205. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  206. package/server/src/Notifications/RouteDeviation.php +48 -0
  207. package/server/src/Observers/ContactObserver.php +2 -0
  208. package/server/src/Observers/ServiceRateObserver.php +6 -6
  209. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
  210. package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
  211. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  212. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  213. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  214. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  215. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  216. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  217. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  218. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  219. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  220. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  221. package/server/src/Support/Analytics/RevenueTrend.php +78 -0
  222. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  223. package/server/src/Support/CustomerAuth.php +98 -0
  224. package/server/src/Support/LabelPdf.php +52 -0
  225. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  226. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  227. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  228. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  229. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  230. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  231. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  232. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  233. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  234. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  235. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  236. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  237. package/server/src/Support/Metrics/Registry.php +46 -0
  238. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  239. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  240. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  241. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  242. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  243. package/server/src/Support/Metrics.php +46 -249
  244. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  245. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  246. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  247. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  248. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  249. package/server/src/Tracking/TrackingStop.php +13 -11
  250. package/server/src/routes.php +50 -0
  251. package/server/tests/AnalyticsRoutesTest.php +41 -0
  252. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  253. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  254. package/server/tests/CustomerEndpointTest.php +199 -0
  255. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  256. package/server/tests/LabelPdfRenderingTest.php +23 -0
  257. package/server/tests/LiveControllerViewportTest.php +60 -0
  258. package/server/tests/MetricsRegistryTest.php +84 -0
  259. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  260. package/server/tests/OrderConfigEndpointTest.php +86 -0
  261. package/server/tests/OrderFileAttachmentTest.php +20 -0
  262. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  263. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  264. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  265. package/translations/en-us.yaml +38 -0
  266. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -22,6 +22,10 @@ use Illuminate\Http\Request;
22
22
  */
23
23
  class LiveController extends Controller
24
24
  {
25
+ protected const DEFAULT_VIEWPORT_LIMIT = 500;
26
+ protected const MAX_VIEWPORT_LIMIT = 1000;
27
+ protected const VIEWPORT_BOUNDS_PRECISION = 4;
28
+
25
29
  /**
26
30
  * Get coordinates for active orders.
27
31
  *
@@ -134,33 +138,19 @@ class LiveController extends Controller
134
138
  */
135
139
  public function drivers(Request $request)
136
140
  {
137
- $bounds = $request->input('bounds'); // Map viewport bounds: [south, west, north, east]
138
- $cacheParams = ['bounds' => $bounds];
141
+ $bounds = $this->normalizeLiveBounds($request);
142
+ $limit = $this->normalizeLiveLimit($request);
143
+ $cacheParams = ['bounds' => $bounds, 'limit' => $limit];
139
144
 
140
- return LiveCacheService::remember('drivers', $cacheParams, function () use ($bounds) {
145
+ return LiveCacheService::remember('drivers', $cacheParams, function () use ($bounds, $limit) {
141
146
  $query = Driver::where(['company_uuid' => session('company')])
142
147
  ->with(['user', 'vehicle'])
143
148
  ->applyDirectivesForPermissions('fleet-ops list driver');
144
149
 
145
- // Filter out drivers with invalid coordinates
146
- $query->whereNotNull('location')
147
- ->whereRaw('
148
- ST_Y(location) BETWEEN -90 AND 90
149
- AND ST_X(location) BETWEEN -180 AND 180
150
- AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
151
- ');
152
-
153
- // Apply spatial filtering if bounds are provided
154
- if ($bounds && is_array($bounds) && count($bounds) === 4) {
155
- [$south, $west, $north, $east] = $bounds;
156
-
157
- // Use MySQL spatial functions for POINT column
158
- // ST_Within checks if location is within the bounding box
159
- $query->whereRaw(
160
- 'ST_Within(location, ST_MakeEnvelope(POINT(?, ?), POINT(?, ?)))',
161
- [$west, $south, $east, $north]
162
- );
163
- }
150
+ $this->applyLiveLocationGuards($query);
151
+ $this->applyLiveViewportBounds($query, $bounds);
152
+
153
+ $query->orderByDesc('updated_at')->orderByDesc('id')->limit($limit);
164
154
 
165
155
  $drivers = $query->get();
166
156
 
@@ -175,34 +165,20 @@ class LiveController extends Controller
175
165
  */
176
166
  public function vehicles(Request $request)
177
167
  {
178
- $bounds = $request->input('bounds'); // Map viewport bounds: [south, west, north, east]
179
- $cacheParams = ['bounds' => $bounds];
168
+ $bounds = $this->normalizeLiveBounds($request);
169
+ $limit = $this->normalizeLiveLimit($request);
170
+ $cacheParams = ['bounds' => $bounds, 'limit' => $limit];
180
171
 
181
- return LiveCacheService::remember('vehicles', $cacheParams, function () use ($bounds) {
172
+ return LiveCacheService::remember('vehicles', $cacheParams, function () use ($bounds, $limit) {
182
173
  // Fetch vehicles that are online
183
174
  $query = Vehicle::where(['company_uuid' => session('company')])
184
175
  ->with(['devices', 'driver'])
185
176
  ->applyDirectivesForPermissions('fleet-ops list vehicle');
186
177
 
187
- // Filter out vehicles with invalid coordinates
188
- $query->whereNotNull('location')
189
- ->whereRaw('
190
- ST_Y(location) BETWEEN -90 AND 90
191
- AND ST_X(location) BETWEEN -180 AND 180
192
- AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
193
- ');
194
-
195
- // Apply spatial filtering if bounds are provided
196
- if ($bounds && is_array($bounds) && count($bounds) === 4) {
197
- [$south, $west, $north, $east] = $bounds;
198
-
199
- // Use MySQL spatial functions for POINT column
200
- // ST_Within checks if location is within the bounding box
201
- $query->whereRaw(
202
- 'ST_Within(location, ST_MakeEnvelope(POINT(?, ?), POINT(?, ?)))',
203
- [$west, $south, $east, $north]
204
- );
205
- }
178
+ $this->applyLiveLocationGuards($query);
179
+ $this->applyLiveViewportBounds($query, $bounds);
180
+
181
+ $query->orderByDesc('updated_at')->orderByDesc('id')->limit($limit);
206
182
 
207
183
  $vehicles = $query->get();
208
184
 
@@ -217,39 +193,98 @@ class LiveController extends Controller
217
193
  */
218
194
  public function places(Request $request)
219
195
  {
196
+ $bounds = $this->normalizeLiveBounds($request);
197
+ $limit = $this->normalizeLiveLimit($request);
198
+
220
199
  // Cache key includes filter parameters
221
- $cacheParams = $request->only(['query', 'type', 'country', 'limit', 'bounds']);
200
+ $cacheParams = array_merge($request->only(['query', 'type', 'country']), [
201
+ 'bounds' => $bounds,
202
+ 'limit' => $limit,
203
+ ]);
222
204
 
223
- return LiveCacheService::remember('places', $cacheParams, function () use ($request) {
205
+ return LiveCacheService::remember('places', $cacheParams, function () use ($request, $bounds, $limit) {
224
206
  // Query places based on filters
225
207
  $query = Place::where(['company_uuid' => session('company')])
226
208
  ->filter(new PlaceFilter($request))
227
209
  ->applyDirectivesForPermissions('fleet-ops list place');
228
210
 
229
- // Filter out places with invalid coordinates
230
- $query->whereNotNull('location')
231
- ->whereRaw('
232
- ST_Y(location) BETWEEN -90 AND 90
233
- AND ST_X(location) BETWEEN -180 AND 180
234
- AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
235
- ');
236
-
237
- // Apply spatial filtering if bounds are provided
238
- $bounds = $request->input('bounds');
239
- if ($bounds && is_array($bounds) && count($bounds) === 4) {
240
- [$south, $west, $north, $east] = $bounds;
241
-
242
- // Use MySQL spatial functions for POINT column
243
- // ST_Within checks if location is within the bounding box
244
- $query->whereRaw(
245
- 'ST_Within(location, ST_MakeEnvelope(POINT(?, ?), POINT(?, ?)))',
246
- [$west, $south, $east, $north]
247
- );
248
- }
211
+ $this->applyLiveLocationGuards($query);
212
+ $this->applyLiveViewportBounds($query, $bounds);
213
+
214
+ $query->orderByDesc('updated_at')->orderByDesc('id')->limit($limit);
249
215
 
250
216
  $places = $query->get();
251
217
 
252
218
  return PlaceIndexResource::collection($places);
253
219
  });
254
220
  }
221
+
222
+ protected function normalizeLiveBounds(Request $request): ?array
223
+ {
224
+ $bounds = $request->input('bounds');
225
+
226
+ if (!is_array($bounds) || count($bounds) !== 4) {
227
+ return null;
228
+ }
229
+
230
+ $bounds = array_values($bounds);
231
+
232
+ if (array_filter($bounds, fn ($value) => !is_numeric($value))) {
233
+ return null;
234
+ }
235
+
236
+ [$south, $west, $north, $east] = array_map('floatval', $bounds);
237
+
238
+ if ($south < -90 || $south > 90 || $north < -90 || $north > 90) {
239
+ return null;
240
+ }
241
+
242
+ if ($west < -180 || $west > 180 || $east < -180 || $east > 180) {
243
+ return null;
244
+ }
245
+
246
+ if ($south > $north || $west > $east) {
247
+ return null;
248
+ }
249
+
250
+ return array_map(
251
+ fn ($value) => round($value, static::VIEWPORT_BOUNDS_PRECISION),
252
+ [$south, $west, $north, $east]
253
+ );
254
+ }
255
+
256
+ protected function normalizeLiveLimit(Request $request): int
257
+ {
258
+ $limit = (int) $request->input('limit', static::DEFAULT_VIEWPORT_LIMIT);
259
+
260
+ if ($limit < 1) {
261
+ return static::DEFAULT_VIEWPORT_LIMIT;
262
+ }
263
+
264
+ return min($limit, static::MAX_VIEWPORT_LIMIT);
265
+ }
266
+
267
+ protected function applyLiveLocationGuards($query): void
268
+ {
269
+ $query->whereNotNull('location')
270
+ ->whereRaw('
271
+ ST_Y(location) BETWEEN -90 AND 90
272
+ AND ST_X(location) BETWEEN -180 AND 180
273
+ AND NOT (ST_X(location) = 0 AND ST_Y(location) = 0)
274
+ ');
275
+ }
276
+
277
+ protected function applyLiveViewportBounds($query, ?array $bounds): void
278
+ {
279
+ if (!$bounds) {
280
+ return;
281
+ }
282
+
283
+ [$south, $west, $north, $east] = $bounds;
284
+
285
+ $query->whereRaw(
286
+ 'ST_Y(location) BETWEEN ? AND ? AND ST_X(location) BETWEEN ? AND ?',
287
+ [$south, $north, $west, $east]
288
+ );
289
+ }
255
290
  }
@@ -3,15 +3,17 @@
3
3
  namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
4
 
5
5
  use Fleetbase\FleetOps\Support\Metrics;
6
+ use Fleetbase\FleetOps\Support\Metrics\Registry;
6
7
  use Fleetbase\Http\Controllers\Controller;
7
8
  use Illuminate\Http\Request;
9
+ use Illuminate\Support\Carbon;
8
10
 
9
11
  class MetricsController extends Controller
10
12
  {
11
13
  /**
12
- * Gets all key metrics for the current session.
13
- *
14
- * @return \Illuminate\Http\Response
14
+ * Legacy bulk endpoint. Returns a flat map of slug → scalar value for the
15
+ * requested period. Preserved for backward compat; the dashboard widgets
16
+ * now prefer per-slug GET /metrics/{slug}.
15
17
  */
16
18
  public function all(Request $request)
17
19
  {
@@ -27,4 +29,81 @@ class MetricsController extends Controller
27
29
 
28
30
  return response()->json($data);
29
31
  }
32
+
33
+ /**
34
+ * Per-metric endpoint backing <Widget::KpiTile>.
35
+ *
36
+ * Accepts:
37
+ * ?period=7d|30d|90d OR ?start=ISO&end=ISO
38
+ * ?sparkline=true to include a per-day mini-chart series
39
+ * ?compare=true to include period-over-period delta (default: true)
40
+ *
41
+ * Returns: { slug, value, format, currency?, delta_pct?, sparkline? }
42
+ */
43
+ public function show(Request $request, string $slug)
44
+ {
45
+ $class = Registry::resolve($slug);
46
+ if ($class === null) {
47
+ return response()->json(['error' => "Unknown metric: {$slug}"], 404);
48
+ }
49
+
50
+ [$start, $end] = $this->resolvePeriod($request);
51
+
52
+ $metric = $class::forCompany($request->user()->company)->between($start, $end);
53
+
54
+ if ($request->boolean('compare', true)) {
55
+ $duration = $end->getTimestamp() - $start->getTimestamp();
56
+ $compareEnd = (clone $start);
57
+ $compareStart = (clone $start)->modify("-{$duration} seconds");
58
+ $metric->compareTo($compareStart, $compareEnd);
59
+ }
60
+
61
+ if ($request->boolean('sparkline')) {
62
+ $buckets = (int) $request->input('sparkline_buckets', 14);
63
+ $metric->withSparkline($buckets, 'day');
64
+ }
65
+
66
+ return response()->json($metric->get());
67
+ }
68
+
69
+ /**
70
+ * Parse ?period=7d/30d/90d into a [start, end] pair, falling back to
71
+ * explicit ?start=&end= ISO dates, then to a 30-day default.
72
+ */
73
+ private function resolvePeriod(Request $request): array
74
+ {
75
+ $period = $request->string('period')->toString();
76
+
77
+ if ($period !== '') {
78
+ $days = match ($period) {
79
+ '7d' => 7,
80
+ '14d' => 14,
81
+ '30d' => 30,
82
+ '90d' => 90,
83
+ '180d' => 180,
84
+ '365d' => 365,
85
+ default => null,
86
+ };
87
+
88
+ if ($days !== null) {
89
+ $end = Carbon::now()->toDateTime();
90
+ $start = Carbon::now()->subDays($days)->toDateTime();
91
+
92
+ return [$start, $end];
93
+ }
94
+ }
95
+
96
+ $start = $request->date('start') ?? Carbon::now()->subDays(30)->toDateTime();
97
+ $end = $request->date('end') ?? Carbon::now()->toDateTime();
98
+
99
+ if (!$start instanceof \DateTime) {
100
+ $start = Carbon::parse($start)->toDateTime();
101
+ }
102
+
103
+ if (!$end instanceof \DateTime) {
104
+ $end = Carbon::parse($end)->toDateTime();
105
+ }
106
+
107
+ return [$start, $end];
108
+ }
30
109
  }