@fleetbase/fleetops-engine 0.6.71 → 0.6.72

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 (378) hide show
  1. package/RELEASE.md +15 -18
  2. package/addon/components/admin/tracking-page-settings.hbs +56 -0
  3. package/addon/components/admin/tracking-page-settings.js +67 -0
  4. package/addon/components/device/form.hbs +2 -2
  5. package/addon/components/driver/details.hbs +1 -1
  6. package/addon/components/driver/form.hbs +1 -1
  7. package/addon/components/equipment/form.hbs +1 -1
  8. package/addon/components/layout/fleet-ops-sidebar.js +11 -3
  9. package/addon/components/maintenance/form.hbs +3 -3
  10. package/addon/components/map/google-live-map.js +2 -2
  11. package/addon/components/map/leaflet-live-map.js +45 -32
  12. package/addon/components/orchestrator/phase-builder.js +2 -1
  13. package/addon/components/orchestrator-workbench.hbs +7 -1
  14. package/addon/components/orchestrator-workbench.js +16 -2
  15. package/addon/components/order/form/route.hbs +1 -1
  16. package/addon/components/order/form/route.js +13 -1
  17. package/addon/components/service-rate/details.hbs +1 -1
  18. package/addon/components/service-rate/form.hbs +1 -1
  19. package/addon/components/tracking-page/full-view.hbs +276 -0
  20. package/addon/components/tracking-page/full-view.js +474 -0
  21. package/addon/components/tracking-page/locale-selector.js +30 -0
  22. package/addon/components/tracking-page/map.hbs +44 -0
  23. package/addon/components/tracking-page/map.js +328 -0
  24. package/addon/components/tracking-page/order-list.hbs +35 -0
  25. package/addon/components/tracking-page/public-view.hbs +60 -0
  26. package/addon/components/tracking-page/settings-preview.hbs +21 -0
  27. package/addon/components/tracking-page/settings-preview.js +23 -0
  28. package/addon/components/tracking-page/verify-panel.hbs +109 -0
  29. package/addon/components/tracking-page.hbs +142 -0
  30. package/addon/components/tracking-page.js +564 -0
  31. package/addon/components/trailer/form.hbs +2 -2
  32. package/addon/components/vehicle/form.hbs +3 -3
  33. package/addon/components/warranty/form.hbs +2 -2
  34. package/addon/components/widget/live-fleet.js +54 -6
  35. package/addon/components/widget/proof-of-delivery.hbs +49 -0
  36. package/addon/components/widget/proof-of-delivery.js +62 -0
  37. package/addon/components/work-order/form.hbs +3 -3
  38. package/addon/controllers/operations/orders/index/details.js +19 -3
  39. package/addon/controllers/operations/scheduler/index.js +45 -14
  40. package/addon/controllers/settings/index.js +6 -0
  41. package/addon/controllers/settings/map.js +75 -0
  42. package/addon/controllers/settings/orchestrator.js +8 -6
  43. package/addon/controllers/settings/tracking-page.js +212 -0
  44. package/addon/extension.js +65 -1
  45. package/addon/helpers/parcel-box-image.js +16 -0
  46. package/addon/routes/operations/orders/index/new.js +5 -4
  47. package/addon/routes/operations/scheduler/index.js +1 -1
  48. package/addon/routes/settings/tracking-page.js +31 -0
  49. package/addon/routes.js +1 -0
  50. package/addon/services/location.js +69 -21
  51. package/addon/services/map-settings.js +28 -0
  52. package/addon/services/order-allocation.js +1 -1
  53. package/addon/styles/fleetops-engine.css +1391 -0
  54. package/addon/templates/settings/map.hbs +46 -0
  55. package/addon/templates/settings/tracking-page.hbs +318 -0
  56. package/addon/utils/identity-cell-resource.js +2 -56
  57. package/addon/utils/map-default-view.js +132 -0
  58. package/addon/utils/placeholder-images.js +4 -69
  59. package/addon/utils/resource-descriptors/assets.js +2 -57
  60. package/addon/utils/resource-descriptors/helpers.js +23 -122
  61. package/addon/utils/resource-descriptors/operations.js +2 -61
  62. package/addon/utils/resource-descriptors/people.js +3 -142
  63. package/addon/utils/resource-descriptors/shared.js +18 -0
  64. package/addon/utils/tracking-live-source.js +123 -0
  65. package/addon/utils/tracking-page-theme.js +123 -0
  66. package/app/components/admin/tracking-page-settings.js +1 -0
  67. package/app/components/tracking-page/full-view.js +1 -0
  68. package/app/components/tracking-page/locale-selector.js +1 -0
  69. package/app/components/tracking-page/map.js +1 -0
  70. package/app/components/tracking-page/order-list.js +1 -0
  71. package/app/components/tracking-page/public-view.js +1 -0
  72. package/app/components/tracking-page/settings-preview.js +1 -0
  73. package/app/components/tracking-page/verify-panel.js +1 -0
  74. package/app/components/tracking-page.js +1 -0
  75. package/app/components/widget/proof-of-delivery.js +1 -0
  76. package/app/controllers/settings/tracking-page.js +1 -0
  77. package/app/helpers/parcel-box-image.js +1 -0
  78. package/app/routes/settings/tracking-page.js +1 -0
  79. package/app/templates/settings/tracking-page.js +1 -0
  80. package/app/utils/map-default-view.js +1 -0
  81. package/app/utils/tracking-live-source.js +1 -0
  82. package/app/utils/tracking-page-theme.js +1 -0
  83. package/composer.json +2 -2
  84. package/extension.json +1 -1
  85. package/package.json +4 -4
  86. package/scripts/pest-bootstrap.php +4 -0
  87. package/server/config/fleetops.php +4 -0
  88. package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
  89. package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
  90. package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
  91. package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
  92. package/server/resources/views/labels/default.php +2 -2
  93. package/server/resources/views/labels/entity-label.php +2 -2
  94. package/server/resources/views/labels/waypoint-label.php +2 -2
  95. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  96. package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
  97. package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
  98. package/server/src/Events/OrderChanged.php +257 -0
  99. package/server/src/Events/OrderDriverUnassigned.php +68 -0
  100. package/server/src/Exceptions/OrderEditLockedException.php +32 -0
  101. package/server/src/Expansions/CompanyExpansion.php +3 -2
  102. package/server/src/Expansions/UserExpansion.php +12 -10
  103. package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
  104. package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
  105. package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
  106. package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
  107. package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
  108. package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
  109. package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
  110. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
  111. package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
  112. package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
  113. package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
  114. package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
  115. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
  116. package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
  117. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
  118. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
  119. package/server/src/Http/Controllers/Internal/v1/OrderController.php +84 -24
  120. package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
  121. package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
  122. package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
  123. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
  124. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
  125. package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
  126. package/server/src/Http/Filter/ContactFilter.php +1 -1
  127. package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
  128. package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
  129. package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
  130. package/server/src/Http/Filter/OrderFilter.php +39 -4
  131. package/server/src/Http/Filter/ProofFilter.php +18 -0
  132. package/server/src/Http/Filter/RouteFilter.php +18 -0
  133. package/server/src/Http/Filter/TelematicFilter.php +23 -0
  134. package/server/src/Http/Filter/WarrantyFilter.php +23 -0
  135. package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
  136. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
  137. package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
  138. package/server/src/Http/Resources/v1/Index/Order.php +11 -2
  139. package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
  140. package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
  141. package/server/src/Http/Resources/v1/Zone.php +31 -23
  142. package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
  143. package/server/src/Jobs/ProcessAllocationJob.php +16 -6
  144. package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
  145. package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
  146. package/server/src/Listeners/HandleOrderCanceled.php +9 -1
  147. package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
  148. package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
  149. package/server/src/Mail/CustomerCredentialsMail.php +4 -2
  150. package/server/src/Models/AssetConnection.php +3 -0
  151. package/server/src/Models/Contact.php +3 -0
  152. package/server/src/Models/Device.php +28 -0
  153. package/server/src/Models/DeviceEvent.php +7 -0
  154. package/server/src/Models/Driver.php +20 -7
  155. package/server/src/Models/Entity.php +12 -4
  156. package/server/src/Models/Fleet.php +11 -32
  157. package/server/src/Models/FleetDriver.php +3 -0
  158. package/server/src/Models/FleetVehicle.php +3 -0
  159. package/server/src/Models/FuelReport.php +5 -13
  160. package/server/src/Models/Issue.php +4 -16
  161. package/server/src/Models/Order.php +200 -9
  162. package/server/src/Models/Payload.php +227 -35
  163. package/server/src/Models/Place.php +7 -1
  164. package/server/src/Models/ServiceArea.php +7 -0
  165. package/server/src/Models/ServiceRate.php +7 -17
  166. package/server/src/Models/TrackingNumber.php +101 -2
  167. package/server/src/Models/TrackingSession.php +33 -0
  168. package/server/src/Models/TrackingSessionGrant.php +22 -0
  169. package/server/src/Models/TrackingStatus.php +6 -0
  170. package/server/src/Models/Vehicle.php +21 -2
  171. package/server/src/Models/Vendor.php +7 -0
  172. package/server/src/Models/Waypoint.php +13 -4
  173. package/server/src/Notifications/OrderAssigned.php +2 -1
  174. package/server/src/Notifications/OrderCanceled.php +7 -1
  175. package/server/src/Notifications/OrderChanged.php +87 -0
  176. package/server/src/Notifications/OrderCompleted.php +2 -1
  177. package/server/src/Notifications/OrderDispatchFailed.php +2 -1
  178. package/server/src/Notifications/OrderDispatched.php +2 -1
  179. package/server/src/Notifications/OrderFailed.php +2 -1
  180. package/server/src/Notifications/OrderUnassigned.php +66 -0
  181. package/server/src/Notifications/WaypointCompleted.php +2 -1
  182. package/server/src/Observers/CompanyUserObserver.php +8 -7
  183. package/server/src/Observers/DriverObserver.php +6 -1
  184. package/server/src/Observers/OrderObserver.php +141 -1
  185. package/server/src/Observers/TrackingNumberObserver.php +18 -6
  186. package/server/src/Observers/UserObserver.php +2 -1
  187. package/server/src/Observers/VehicleObserver.php +8 -3
  188. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
  189. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
  190. package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
  191. package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
  192. package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
  193. package/server/src/Providers/EventServiceProvider.php +11 -0
  194. package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
  195. package/server/src/Support/ApiCacheThrottle.php +76 -0
  196. package/server/src/Support/LiveOrderQuery.php +3 -1
  197. package/server/src/Support/OrderEdit.php +365 -0
  198. package/server/src/Support/OrderEditPolicy.php +292 -0
  199. package/server/src/Support/ProfileAccountManager.php +54 -1
  200. package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
  201. package/server/src/Support/SocketChannels.php +346 -0
  202. package/server/src/Support/Telematics/TelematicService.php +7 -2
  203. package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
  204. package/server/src/Support/TrackingChannel.php +55 -0
  205. package/server/src/Support/TrackingCode.php +242 -0
  206. package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
  207. package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
  208. package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
  209. package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
  210. package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
  211. package/server/src/Support/TrackingPage/TrackingView.php +543 -0
  212. package/server/src/Support/TrackingPublisher.php +261 -0
  213. package/server/src/Support/TrackingScope.php +182 -0
  214. package/server/src/Support/TrackingUpdate.php +402 -0
  215. package/server/src/Support/Utils.php +15 -3
  216. package/server/src/Traits/HasTrackingNumber.php +2 -0
  217. package/server/src/routes.php +39 -7
  218. package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
  219. package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
  220. package/server/tests/ApiEntityControllerContractsTest.php +84 -0
  221. package/server/tests/ApiOrderControllerContractsTest.php +245 -0
  222. package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
  223. package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
  224. package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
  225. package/server/tests/CompactResourceSerializationTest.php +23 -59
  226. package/server/tests/ControllerFilterContractsTest.php +1 -1
  227. package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
  228. package/server/tests/EventContractsTest.php +26 -60
  229. package/server/tests/ExpansionContractsTest.php +15 -2
  230. package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
  231. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
  232. package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
  233. package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
  234. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
  235. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
  236. package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
  237. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
  238. package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
  239. package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
  240. package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
  241. package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
  242. package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
  243. package/server/tests/IndexCustomerPhotoTest.php +151 -0
  244. package/server/tests/ModelAccessorContractsTest.php +4 -3
  245. package/server/tests/NotificationAndMailContractsTest.php +5 -8
  246. package/server/tests/ObserverContractsTest.php +72 -10
  247. package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
  248. package/server/tests/OrderFilterExecutionTest.php +167 -2
  249. package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
  250. package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
  251. package/server/tests/QueueJobContractsTest.php +2 -1
  252. package/server/tests/RequestContractsTest.php +1 -1
  253. package/server/tests/RouteRegistrationExecutionTest.php +26 -0
  254. package/server/tests/RouteSequencingEngineTest.php +17 -0
  255. package/server/tests/SettingControllerContractsTest.php +110 -0
  256. package/server/tests/Support/TrackingPageDatabase.php +200 -0
  257. package/server/tests/TenantScopingTest.php +206 -0
  258. package/server/tests/TrackingPageSettingsTest.php +343 -0
  259. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
  260. package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
  261. package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
  262. package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
  263. package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
  264. package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
  265. package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
  266. package/server/tests/Unit/Models/FleetTest.php +4 -3
  267. package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
  268. package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
  269. package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
  270. package/server/tests/Unit/Models/PayloadTest.php +18 -4
  271. package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
  272. package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
  273. package/server/tests/Unit/Models/VehicleTest.php +16 -2
  274. package/server/tests/Unit/Models/WaypointTest.php +4 -3
  275. package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
  276. package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
  277. package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
  278. package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
  279. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
  280. package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
  281. package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
  282. package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
  283. package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
  284. package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
  285. package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
  286. package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
  287. package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
  288. package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
  289. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
  290. package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
  291. package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
  292. package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
  293. package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
  294. package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
  295. package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
  296. package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
  297. package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
  298. package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
  299. package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
  300. package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
  301. package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
  302. package/server/tests/VehicleResourceContractsTest.php +46 -0
  303. package/server/tests/VroomOrchestrationEngineTest.php +34 -1
  304. package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
  305. package/translations/ar-ae.yml +274 -0
  306. package/translations/bg-bg.yaml +274 -0
  307. package/translations/en-us.yaml +287 -0
  308. package/translations/es-pa.yaml +274 -0
  309. package/translations/fr-fr.yaml +274 -0
  310. package/translations/mn-mn.yaml +274 -0
  311. package/translations/pt-br.yaml +274 -0
  312. package/translations/ru-ru.yaml +276 -2
  313. package/translations/uk-ua.yaml +275 -1
  314. package/translations/vi-vn.yaml +274 -0
  315. package/addon/components/cell/contact-identity.hbs +0 -1
  316. package/addon/components/cell/customer-identity.hbs +0 -1
  317. package/addon/components/cell/driver-identity.hbs +0 -1
  318. package/addon/components/cell/fleet-identity.hbs +0 -1
  319. package/addon/components/cell/order-identity.hbs +0 -1
  320. package/addon/components/cell/place-identity.hbs +0 -1
  321. package/addon/components/cell/vehicle-identity.hbs +0 -1
  322. package/addon/components/cell/vendor-identity.hbs +0 -1
  323. package/addon/components/contact/pill.hbs +0 -24
  324. package/addon/components/contact/summary.hbs +0 -1
  325. package/addon/components/customer/pill.hbs +0 -24
  326. package/addon/components/customer/summary.hbs +0 -1
  327. package/addon/components/driver/pill.hbs +0 -24
  328. package/addon/components/driver/summary.hbs +0 -1
  329. package/addon/components/fleet/pill.hbs +0 -24
  330. package/addon/components/fleet/summary.hbs +0 -1
  331. package/addon/components/order/pill.hbs +0 -24
  332. package/addon/components/order/summary.hbs +0 -1
  333. package/addon/components/place/pill.hbs +0 -24
  334. package/addon/components/place/summary.hbs +0 -1
  335. package/addon/components/select-option/contact.hbs +0 -12
  336. package/addon/components/select-option/customer.hbs +0 -12
  337. package/addon/components/select-option/driver.hbs +0 -12
  338. package/addon/components/select-option/fleet.hbs +0 -12
  339. package/addon/components/select-option/order.hbs +0 -12
  340. package/addon/components/select-option/place.hbs +0 -12
  341. package/addon/components/select-option/vehicle.hbs +0 -12
  342. package/addon/components/select-option/vendor.hbs +0 -12
  343. package/addon/components/vehicle/pill.hbs +0 -24
  344. package/addon/components/vehicle/summary.hbs +0 -1
  345. package/addon/components/vendor/pill.hbs +0 -24
  346. package/addon/components/vendor/summary.hbs +0 -1
  347. package/app/components/cell/contact-identity.js +0 -1
  348. package/app/components/cell/customer-identity.js +0 -1
  349. package/app/components/cell/driver-identity.js +0 -1
  350. package/app/components/cell/fleet-identity.js +0 -1
  351. package/app/components/cell/order-identity.js +0 -1
  352. package/app/components/cell/place-identity.js +0 -1
  353. package/app/components/cell/vehicle-identity.js +0 -1
  354. package/app/components/cell/vendor-identity.js +0 -1
  355. package/app/components/contact/pill.js +0 -1
  356. package/app/components/contact/summary.js +0 -1
  357. package/app/components/customer/pill.js +0 -1
  358. package/app/components/customer/summary.js +0 -1
  359. package/app/components/driver/pill.js +0 -1
  360. package/app/components/driver/summary.js +0 -1
  361. package/app/components/fleet/pill.js +0 -1
  362. package/app/components/fleet/summary.js +0 -1
  363. package/app/components/order/pill.js +0 -1
  364. package/app/components/order/summary.js +0 -1
  365. package/app/components/place/pill.js +0 -1
  366. package/app/components/place/summary.js +0 -1
  367. package/app/components/select-option/contact.js +0 -1
  368. package/app/components/select-option/customer.js +0 -1
  369. package/app/components/select-option/driver.js +0 -1
  370. package/app/components/select-option/fleet.js +0 -1
  371. package/app/components/select-option/order.js +0 -1
  372. package/app/components/select-option/place.js +0 -1
  373. package/app/components/select-option/vehicle.js +0 -1
  374. package/app/components/select-option/vendor.js +0 -1
  375. package/app/components/vehicle/pill.js +0 -1
  376. package/app/components/vehicle/summary.js +0 -1
  377. package/app/components/vendor/pill.js +0 -1
  378. package/app/components/vendor/summary.js +0 -1
@@ -0,0 +1,264 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\TrackingPage;
4
+
5
+ use Illuminate\Support\Str;
6
+
7
+ /**
8
+ * The shape, defaults and sanitizing of a company's customer tracking page settings.
9
+ *
10
+ * A company keeps its config in the company setting `tracking-page`. Instance-wide
11
+ * defaults for the generic page live in the system setting `fleet-ops.tracking-page`.
12
+ * Everything here is pure, so the controller and the public tracking API read one shape.
13
+ *
14
+ * Privacy rules are not settings: the map and driver hide after delivery, and the driver
15
+ * shows only while en route to the viewer's stops, whatever is saved here.
16
+ */
17
+ class TrackingPageConfig
18
+ {
19
+ public const SETTING_KEY = 'tracking-page';
20
+ public const ADMIN_SETTING_KEY = 'fleet-ops.tracking-page';
21
+ public const SLUG_INDEX_PREFIX = 'fleet-ops.tracking-page-slugs.';
22
+ public const DEFAULT_ACCENT = '#1F5FA8';
23
+ public const DARK_INK = '#14191A';
24
+ public const LIGHT_INK = '#FFFFFF';
25
+
26
+ /**
27
+ * The FleetOps locales a tracking page can be shown in.
28
+ */
29
+ public const LOCALES = ['en-us', 'ar-ae', 'bg-bg', 'es-pa', 'fr-fr', 'mn-mn', 'pt-br', 'ru-ru', 'uk-ua', 'vi-vn'];
30
+
31
+ /**
32
+ * Slugs a company can't take: they would read as part of the page's own URLs.
33
+ */
34
+ public const RESERVED_SLUGS = [
35
+ 'admin', 'api', 'auth', 'console', 'customer-access', 'customer-portal', 'fleetbase', 'help',
36
+ 'int', 'login', 'new', 'portal', 'public', 'settings', 'support', 'track', 'tracking', 'www',
37
+ ];
38
+
39
+ /**
40
+ * Instance-wide defaults for the generic page, as an administrator saved them.
41
+ */
42
+ public static function sanitizeAdmin(array $input): array
43
+ {
44
+ return [
45
+ 'generic_page' => [
46
+ 'enabled' => static::bool(data_get($input, 'generic_page.enabled'), true),
47
+ ],
48
+ 'branding' => [
49
+ 'display_name' => static::text(data_get($input, 'branding.display_name'), 80),
50
+ 'accent' => static::color(data_get($input, 'branding.accent')) ?? static::DEFAULT_ACCENT,
51
+ 'support_phone' => static::text(data_get($input, 'branding.support_phone'), 32),
52
+ 'support_email' => static::email(data_get($input, 'branding.support_email')),
53
+ 'website' => static::url(data_get($input, 'branding.website')),
54
+ 'powered_by' => static::bool(data_get($input, 'branding.powered_by'), true),
55
+ ],
56
+ ];
57
+ }
58
+
59
+ /**
60
+ * A company's config, with every key present and every value valid.
61
+ *
62
+ * @param array $admin the sanitized instance defaults
63
+ * @param string|null $companyName used for the default slug
64
+ */
65
+ public static function sanitize(array $input, array $admin = [], ?string $companyName = null): array
66
+ {
67
+ $admin = static::sanitizeAdmin($admin);
68
+ $locale = static::oneOf(data_get($input, 'branding.default_locale'), static::LOCALES, 'en-us');
69
+
70
+ return [
71
+ 'org_page' => [
72
+ 'enabled' => static::bool(data_get($input, 'org_page.enabled'), false),
73
+ 'slug' => static::normalizeSlug(data_get($input, 'org_page.slug')) ?: static::slugify((string) $companyName),
74
+ ],
75
+ 'generic_page' => [
76
+ 'allowed' => static::bool(data_get($input, 'generic_page.allowed'), true),
77
+ ],
78
+ 'links' => [
79
+ 'target' => static::oneOf(data_get($input, 'links.target'), ['auto', 'org', 'generic'], 'auto'),
80
+ ],
81
+ 'branding' => [
82
+ 'display_name' => static::text(data_get($input, 'branding.display_name'), 80),
83
+ 'logo_uuid' => static::text(data_get($input, 'branding.logo_uuid'), 64),
84
+ 'accent' => static::color(data_get($input, 'branding.accent')) ?? $admin['branding']['accent'],
85
+ 'accent_dark' => static::color(data_get($input, 'branding.accent_dark')),
86
+ 'support_phone' => static::text(data_get($input, 'branding.support_phone'), 32),
87
+ 'support_email' => static::email(data_get($input, 'branding.support_email')),
88
+ 'website' => static::url(data_get($input, 'branding.website')),
89
+ 'powered_by' => static::bool(data_get($input, 'branding.powered_by'), $admin['branding']['powered_by']),
90
+ 'theme' => static::oneOf(data_get($input, 'branding.theme'), ['system', 'light'], 'system'),
91
+ 'default_locale' => $locale,
92
+ 'locales' => static::locales(data_get($input, 'branding.locales'), $locale),
93
+ ],
94
+ 'access' => [
95
+ 'public_status' => static::bool(data_get($input, 'access.public_status'), true),
96
+ 'channels' => [
97
+ 'sms' => static::bool(data_get($input, 'access.channels.sms'), true),
98
+ 'email' => static::bool(data_get($input, 'access.channels.email'), true),
99
+ ],
100
+ 'session_hours' => static::clampInt(data_get($input, 'access.session_hours'), 1, 168, 24),
101
+ 'account_sign_in' => static::bool(data_get($input, 'access.account_sign_in'), true),
102
+ 'account_upsell' => static::bool(data_get($input, 'access.account_upsell'), true),
103
+ ],
104
+ 'visibility' => [
105
+ 'map' => static::bool(data_get($input, 'visibility.map'), true),
106
+ 'driver_name' => static::bool(data_get($input, 'visibility.driver_name'), true),
107
+ 'vehicle' => static::bool(data_get($input, 'visibility.vehicle'), true),
108
+ 'driver_contact' => static::oneOf(data_get($input, 'visibility.driver_contact'), ['company', 'driver'], 'company'),
109
+ 'items' => static::bool(data_get($input, 'visibility.items'), true),
110
+ 'item_prices' => static::bool(data_get($input, 'visibility.item_prices'), false),
111
+ 'pod_photo' => static::bool(data_get($input, 'visibility.pod_photo'), true),
112
+ 'pod_signature' => static::bool(data_get($input, 'visibility.pod_signature'), true),
113
+ 'instructions_edit' => static::bool(data_get($input, 'visibility.instructions_edit'), false),
114
+ 'report_problem' => static::bool(data_get($input, 'visibility.report_problem'), true),
115
+ ],
116
+ ];
117
+ }
118
+
119
+ /**
120
+ * Why a slug can't be used, or null when its shape is fine. Availability is the caller's check.
121
+ *
122
+ * @return string|null `invalid` or `reserved`
123
+ */
124
+ public static function slugError(?string $slug): ?string
125
+ {
126
+ if (!is_string($slug) || preg_match('/^[a-z0-9](?:[a-z0-9-]{1,38})[a-z0-9]$/', $slug) !== 1 || str_contains($slug, '--')) {
127
+ return 'invalid';
128
+ }
129
+
130
+ return in_array($slug, static::RESERVED_SLUGS, true) ? 'reserved' : null;
131
+ }
132
+
133
+ /**
134
+ * A slug made from a company name: lowercase, hyphenated, 3 to 40 characters.
135
+ */
136
+ public static function slugify(string $name): string
137
+ {
138
+ $slug = trim(Str::limit(Str::slug($name), 40, ''), '-');
139
+
140
+ if (strlen($slug) >= 3) {
141
+ return $slug;
142
+ }
143
+
144
+ return $slug === '' ? 'company-tracking' : $slug . '-tracking';
145
+ }
146
+
147
+ /**
148
+ * The text colour readable on an accent: white when it reaches 4.5:1, otherwise dark.
149
+ */
150
+ public static function inkFor(string $accent): string
151
+ {
152
+ return static::contrastRatio($accent, static::LIGHT_INK) >= 4.5 ? static::LIGHT_INK : static::DARK_INK;
153
+ }
154
+
155
+ /**
156
+ * The WCAG contrast ratio of two `#RRGGBB` colours.
157
+ */
158
+ public static function contrastRatio(string $a, string $b): float
159
+ {
160
+ $lighter = max(static::luminance($a), static::luminance($b));
161
+ $darker = min(static::luminance($a), static::luminance($b));
162
+
163
+ return round(($lighter + 0.05) / ($darker + 0.05), 2);
164
+ }
165
+
166
+ protected static function luminance(string $hex): float
167
+ {
168
+ $channels = array_map(function (string $pair) {
169
+ $value = hexdec($pair) / 255;
170
+
171
+ return $value <= 0.03928 ? $value / 12.92 : (($value + 0.055) / 1.055) ** 2.4;
172
+ }, str_split(ltrim($hex, '#'), 2));
173
+
174
+ return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2];
175
+ }
176
+
177
+ protected static function normalizeSlug(mixed $value): string
178
+ {
179
+ return is_string($value) ? strtolower(trim($value)) : '';
180
+ }
181
+
182
+ protected static function bool(mixed $value, bool $default): bool
183
+ {
184
+ if (is_bool($value)) {
185
+ return $value;
186
+ }
187
+
188
+ // filter_var() reads null as false, so a missing value must fall back explicitly.
189
+ if ($value === null || $value === '') {
190
+ return $default;
191
+ }
192
+
193
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $default;
194
+ }
195
+
196
+ protected static function text(mixed $value, int $max): ?string
197
+ {
198
+ if (!is_string($value) && !is_numeric($value)) {
199
+ return null;
200
+ }
201
+
202
+ $value = trim((string) $value);
203
+
204
+ return $value === '' ? null : mb_substr($value, 0, $max);
205
+ }
206
+
207
+ protected static function email(mixed $value): ?string
208
+ {
209
+ $value = static::text($value, 191);
210
+
211
+ return $value !== null && filter_var($value, FILTER_VALIDATE_EMAIL) ? $value : null;
212
+ }
213
+
214
+ protected static function url(mixed $value): ?string
215
+ {
216
+ $value = static::text($value, 255);
217
+
218
+ return $value !== null && preg_match('#^https?://#i', $value) === 1 && filter_var($value, FILTER_VALIDATE_URL) ? $value : null;
219
+ }
220
+
221
+ /**
222
+ * `#RGB` or `#RRGGBB` as uppercase `#RRGGBB`, or null.
223
+ */
224
+ protected static function color(mixed $value): ?string
225
+ {
226
+ if (!is_string($value) || preg_match('/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i', trim($value), $match) !== 1) {
227
+ return null;
228
+ }
229
+
230
+ $hex = $match[1];
231
+ if (strlen($hex) === 3) {
232
+ $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
233
+ }
234
+
235
+ return '#' . strtoupper($hex);
236
+ }
237
+
238
+ protected static function oneOf(mixed $value, array $allowed, string $default): string
239
+ {
240
+ return is_string($value) && in_array($value, $allowed, true) ? $value : $default;
241
+ }
242
+
243
+ protected static function clampInt(mixed $value, int $min, int $max, int $default): int
244
+ {
245
+ if (!is_numeric($value)) {
246
+ return $default;
247
+ }
248
+
249
+ return max($min, min($max, (int) $value));
250
+ }
251
+
252
+ /**
253
+ * The enabled locales, always including the default one; every locale when none are valid.
254
+ */
255
+ protected static function locales(mixed $value, string $default): array
256
+ {
257
+ $locales = array_values(array_intersect(static::LOCALES, is_array($value) ? $value : []));
258
+ if ($locales === []) {
259
+ return static::LOCALES;
260
+ }
261
+
262
+ return in_array($default, $locales, true) ? $locales : array_merge([$default], $locales);
263
+ }
264
+ }
@@ -0,0 +1,111 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\TrackingPage;
4
+
5
+ use Fleetbase\FleetOps\Models\Contact;
6
+ use Fleetbase\FleetOps\Models\Vendor;
7
+ use Fleetbase\FleetOps\Support\TrackingScope;
8
+ use Illuminate\Database\Eloquent\Model;
9
+
10
+ /**
11
+ * Who a one-time code goes to for a tracking scope, and how their contact details are
12
+ * shown on the page: masked, and never editable, so a code can only reach details the
13
+ * company already holds.
14
+ */
15
+ class TrackingRecipient
16
+ {
17
+ public const CHANNEL_SMS = 'sms';
18
+ public const CHANNEL_EMAIL = 'email';
19
+
20
+ public function __construct(
21
+ public readonly ?Model $customer,
22
+ public readonly ?string $email,
23
+ public readonly ?string $phone,
24
+ ) {
25
+ }
26
+
27
+ /**
28
+ * The scope's customer (a Contact or a Vendor) and the contact details a code can use.
29
+ *
30
+ * @param string|null $fallbackPhone the stop place's phone, used when the customer has none
31
+ */
32
+ public static function for(?TrackingScope $scope, ?string $fallbackPhone = null, ?Model $customer = null): self
33
+ {
34
+ $customer ??= $scope ? static::loadCustomer($scope) : null;
35
+
36
+ $email = static::validEmail(data_get($customer, 'email'));
37
+ $phone = static::validPhone(data_get($customer, 'phone')) ?? static::validPhone($fallbackPhone);
38
+
39
+ return new self($customer, $email, $phone);
40
+ }
41
+
42
+ protected static function loadCustomer(TrackingScope $scope): ?Model
43
+ {
44
+ $class = match (true) {
45
+ is_a($scope->customer_type, Contact::class, true) => Contact::class,
46
+ is_a($scope->customer_type, Vendor::class, true) => Vendor::class,
47
+ default => null,
48
+ };
49
+
50
+ return $class ? $class::withoutGlobalScopes()->where('uuid', $scope->customer_uuid)->first() : null;
51
+ }
52
+
53
+ /**
54
+ * The channels a code can go out on, given the company's settings and the instance.
55
+ *
56
+ * @return array<int, array{type: string, masked: string}>
57
+ */
58
+ public function channels(array $config, bool $smsAvailable): array
59
+ {
60
+ $channels = [];
61
+ if ($this->phone && $smsAvailable && data_get($config, 'access.channels.sms', true)) {
62
+ $channels[] = ['type' => self::CHANNEL_SMS, 'masked' => static::maskPhone($this->phone)];
63
+ }
64
+
65
+ if ($this->email && data_get($config, 'access.channels.email', true)) {
66
+ $channels[] = ['type' => self::CHANNEL_EMAIL, 'masked' => static::maskEmail($this->email)];
67
+ }
68
+
69
+ return $channels;
70
+ }
71
+
72
+ public function destination(string $channel): ?string
73
+ {
74
+ return $channel === self::CHANNEL_SMS ? $this->phone : ($channel === self::CHANNEL_EMAIL ? $this->email : null);
75
+ }
76
+
77
+ /**
78
+ * `•••• •• 4821`: only the last four digits.
79
+ */
80
+ public static function maskPhone(string $phone): string
81
+ {
82
+ $digits = preg_replace('/\D/', '', $phone);
83
+
84
+ return '•••• •• ' . substr($digits, -4);
85
+ }
86
+
87
+ /**
88
+ * `m•••@g•••.com`: the first letter of each part and the top-level domain.
89
+ */
90
+ public static function maskEmail(string $email): string
91
+ {
92
+ [$local, $domain] = explode('@', $email, 2);
93
+ $tld = str_contains($domain, '.') ? substr($domain, strrpos($domain, '.')) : '';
94
+
95
+ return mb_substr($local, 0, 1) . '•••@' . mb_substr($domain, 0, 1) . '•••' . $tld;
96
+ }
97
+
98
+ protected static function validEmail(mixed $value): ?string
99
+ {
100
+ $value = is_string($value) ? trim($value) : '';
101
+
102
+ return $value !== '' && filter_var($value, FILTER_VALIDATE_EMAIL) ? $value : null;
103
+ }
104
+
105
+ protected static function validPhone(mixed $value): ?string
106
+ {
107
+ $value = is_string($value) ? trim($value) : '';
108
+
109
+ return strlen(preg_replace('/\D/', '', $value)) >= 6 ? $value : null;
110
+ }
111
+ }
@@ -0,0 +1,201 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\TrackingPage;
4
+
5
+ use Fleetbase\FleetOps\Models\Entity;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\Payload;
8
+ use Fleetbase\FleetOps\Models\Place;
9
+ use Fleetbase\FleetOps\Models\TrackingNumber;
10
+ use Fleetbase\FleetOps\Models\Waypoint;
11
+ use Fleetbase\FleetOps\Support\TrackingScope;
12
+ use Fleetbase\Models\Setting;
13
+
14
+ /**
15
+ * Turns what a visitor typed into the order and customer it may open, or null.
16
+ *
17
+ * Null covers everything a visitor must not be able to tell apart: a malformed number, an
18
+ * unknown one, one from a company that opted out of the shared page, and, on a company's
19
+ * own page, one belonging to another company. The caller answers all of them the same way.
20
+ *
21
+ * Only the `tracking_number` column is matched, never a uuid or public id.
22
+ */
23
+ class TrackingResolver
24
+ {
25
+ public const NUMBER_PATTERN = '/^[A-Za-z0-9][A-Za-z0-9._\-]{0,99}$/';
26
+
27
+ /**
28
+ * The tracking number as typed, trimmed, or null when it can't be one.
29
+ */
30
+ public static function normalize(mixed $input): ?string
31
+ {
32
+ if (!is_string($input)) {
33
+ return null;
34
+ }
35
+
36
+ $input = trim($input);
37
+
38
+ return preg_match(static::NUMBER_PATTERN, $input) === 1 ? $input : null;
39
+ }
40
+
41
+ /**
42
+ * @param string|null $slug a company page's slug; null for the shared page
43
+ */
44
+ public function resolve(mixed $input, ?string $slug = null): ?TrackingTarget
45
+ {
46
+ $number = static::normalize($input);
47
+ if ($number === null) {
48
+ return null;
49
+ }
50
+
51
+ $companyUuid = null;
52
+ if ($slug !== null) {
53
+ $companyUuid = $this->companyForSlug($slug);
54
+ if ($companyUuid === null) {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ $trackingNumber = $this->findTrackingNumber($number, $companyUuid);
60
+ $target = $trackingNumber ? $this->targetFor($trackingNumber) : null;
61
+ if ($target === null) {
62
+ return null;
63
+ }
64
+
65
+ $config = $this->config($target->companyUuid());
66
+ if ($slug === null && (!$config['generic_page']['allowed'] || !$this->adminConfig()['generic_page']['enabled'])) {
67
+ return null;
68
+ }
69
+
70
+ if ($companyUuid !== null && $target->companyUuid() !== $companyUuid) {
71
+ return null;
72
+ }
73
+
74
+ return $target->withConfig($config);
75
+ }
76
+
77
+ /**
78
+ * The company whose enabled page lives at this slug.
79
+ */
80
+ public function companyForSlug(?string $slug): ?string
81
+ {
82
+ $slug = strtolower(trim((string) $slug));
83
+ if (TrackingPageConfig::slugError($slug) !== null) {
84
+ return null;
85
+ }
86
+
87
+ $companyUuid = $this->lookupSetting(TrackingPageConfig::SLUG_INDEX_PREFIX . $slug);
88
+ if (!is_string($companyUuid) || $companyUuid === '') {
89
+ return null;
90
+ }
91
+
92
+ $config = $this->config($companyUuid);
93
+
94
+ return $config['org_page']['enabled'] && $config['org_page']['slug'] === $slug ? $companyUuid : null;
95
+ }
96
+
97
+ /**
98
+ * The order and customer a tracking number opens, from whatever owns it.
99
+ */
100
+ public function targetFor(TrackingNumber $trackingNumber): ?TrackingTarget
101
+ {
102
+ $owner = $this->ownerOf($trackingNumber);
103
+
104
+ if ($owner instanceof Order) {
105
+ return new TrackingTarget($owner, TrackingScope::forOrder($owner), $trackingNumber);
106
+ }
107
+
108
+ if ($owner instanceof Waypoint) {
109
+ $order = $this->orderForPayload($owner->payload_uuid);
110
+
111
+ return $order ? new TrackingTarget($order, TrackingScope::forStop($order, $owner), $trackingNumber, TrackingTarget::KIND_STOP, $owner) : null;
112
+ }
113
+
114
+ if ($owner instanceof Entity) {
115
+ $order = $this->orderForPayload($owner->payload_uuid);
116
+ if (!$order) {
117
+ return null;
118
+ }
119
+
120
+ $waypoint = $this->waypointForEntity($owner);
121
+
122
+ return new TrackingTarget($order, TrackingScope::forStop($order, $waypoint), $trackingNumber, TrackingTarget::KIND_ITEM, $waypoint);
123
+ }
124
+
125
+ if ($owner instanceof Place) {
126
+ // A service stop's pickup or dropoff number: the stop belongs to the order's customer.
127
+ $order = $this->orderForPlaceTrackingNumber($trackingNumber->uuid);
128
+
129
+ return $order ? new TrackingTarget($order, TrackingScope::forOrder($order), $trackingNumber, TrackingTarget::KIND_STOP) : null;
130
+ }
131
+
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * The slug to put in a company's tracking links, or null to link the shared page.
137
+ */
138
+ public function linkSlugFor(string $companyUuid): ?string
139
+ {
140
+ $config = $this->config($companyUuid);
141
+
142
+ return $config['links']['target'] !== 'generic' && $config['org_page']['enabled'] ? $config['org_page']['slug'] : null;
143
+ }
144
+
145
+ public function config(?string $companyUuid): array
146
+ {
147
+ return TrackingPageConfig::sanitize(
148
+ (array) $this->lookupSetting('company.' . $companyUuid . '.' . TrackingPageConfig::SETTING_KEY, []),
149
+ $this->adminConfig()
150
+ );
151
+ }
152
+
153
+ public function adminConfig(): array
154
+ {
155
+ return TrackingPageConfig::sanitizeAdmin((array) $this->lookupSetting(TrackingPageConfig::ADMIN_SETTING_KEY, []));
156
+ }
157
+
158
+ protected function findTrackingNumber(string $number, ?string $companyUuid): ?TrackingNumber
159
+ {
160
+ $query = TrackingNumber::withoutGlobalScopes()->where('tracking_number', $number);
161
+ if ($companyUuid !== null) {
162
+ $query->where('company_uuid', $companyUuid);
163
+ }
164
+
165
+ return $query->first();
166
+ }
167
+
168
+ protected function ownerOf(TrackingNumber $trackingNumber): mixed
169
+ {
170
+ return $trackingNumber->owner;
171
+ }
172
+
173
+ protected function orderForPayload(?string $payloadUuid): ?Order
174
+ {
175
+ return $payloadUuid ? Order::withoutGlobalScopes()->where('payload_uuid', $payloadUuid)->first() : null;
176
+ }
177
+
178
+ protected function waypointForEntity(Entity $entity): ?Waypoint
179
+ {
180
+ if (!$entity->payload_uuid || !$entity->destination_uuid) {
181
+ return null;
182
+ }
183
+
184
+ return Waypoint::withoutGlobalScopes()->where('payload_uuid', $entity->payload_uuid)->where('place_uuid', $entity->destination_uuid)->first();
185
+ }
186
+
187
+ protected function orderForPlaceTrackingNumber(string $trackingNumberUuid): ?Order
188
+ {
189
+ $payloadUuid = Payload::withoutGlobalScopes()
190
+ ->where('pickup_tracking_number_uuid', $trackingNumberUuid)
191
+ ->orWhere('dropoff_tracking_number_uuid', $trackingNumberUuid)
192
+ ->value('uuid');
193
+
194
+ return $this->orderForPayload($payloadUuid);
195
+ }
196
+
197
+ protected function lookupSetting(string $key, mixed $default = null): mixed
198
+ {
199
+ return Setting::lookup($key, $default);
200
+ }
201
+ }
@@ -0,0 +1,40 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\TrackingPage;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Fleetbase\FleetOps\Models\TrackingNumber;
7
+ use Fleetbase\FleetOps\Models\Waypoint;
8
+ use Fleetbase\FleetOps\Support\TrackingScope;
9
+
10
+ /**
11
+ * What a tracking number opened on the public page: the order, the customer within it
12
+ * whose stops the visitor may see (null when the order names nobody), the stop the number
13
+ * belongs to when it is a stop's or an item's, and the owning company's page config.
14
+ */
15
+ final class TrackingTarget
16
+ {
17
+ public const KIND_ORDER = 'order';
18
+ public const KIND_STOP = 'stop';
19
+ public const KIND_ITEM = 'item';
20
+
21
+ public function __construct(
22
+ public readonly Order $order,
23
+ public readonly ?TrackingScope $scope,
24
+ public readonly TrackingNumber $trackingNumber,
25
+ public readonly string $kind = self::KIND_ORDER,
26
+ public readonly ?Waypoint $waypoint = null,
27
+ public readonly array $config = [],
28
+ ) {
29
+ }
30
+
31
+ public function withConfig(array $config): self
32
+ {
33
+ return new self($this->order, $this->scope, $this->trackingNumber, $this->kind, $this->waypoint, $config);
34
+ }
35
+
36
+ public function companyUuid(): ?string
37
+ {
38
+ return $this->order->company_uuid;
39
+ }
40
+ }