@fleetbase/fleetops-engine 0.6.49 → 0.6.51

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 (271) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/create-order-form.hbs +2 -2
  4. package/addon/components/customer/details.hbs +1 -2
  5. package/addon/components/customer/form.hbs +62 -50
  6. package/addon/components/customer/form.js +27 -1
  7. package/addon/components/customer/order-form.hbs +2 -2
  8. package/addon/components/customer/orders.hbs +50 -2
  9. package/addon/components/customer/orders.js +67 -5
  10. package/addon/components/display-place.hbs +2 -2
  11. package/addon/components/driver/form.hbs +1 -1
  12. package/addon/components/home/getting-started-guidance.hbs +113 -0
  13. package/addon/components/home/getting-started-guidance.js +107 -0
  14. package/addon/components/issue/details/comments.hbs +3 -0
  15. package/addon/components/issue/details/comments.js +3 -0
  16. package/addon/components/issue/details/correspondence.hbs +9 -0
  17. package/addon/components/issue/details/correspondence.js +83 -0
  18. package/addon/components/issue/details/documents.hbs +69 -0
  19. package/addon/components/issue/details/documents.js +85 -0
  20. package/addon/components/issue/details.hbs +220 -59
  21. package/addon/components/issue/details.js +245 -0
  22. package/addon/components/issue/form.hbs +19 -1
  23. package/addon/components/issue/panel-header.hbs +43 -0
  24. package/addon/components/issue/panel-header.js +22 -0
  25. package/addon/components/issue/timeline.hbs +40 -0
  26. package/addon/components/issue/timeline.js +45 -0
  27. package/addon/components/leaflet-tracking-marker.js +3 -0
  28. package/addon/components/map/drawer/position-listing.js +3 -0
  29. package/addon/components/map/google-live-map.js +6 -2
  30. package/addon/components/map/leaflet-live-map.hbs +48 -14
  31. package/addon/components/map/leaflet-live-map.js +201 -20
  32. package/addon/components/map/order-list-overlay.hbs +4 -4
  33. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  34. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  35. package/addon/components/modals/issue-assign.hbs +20 -0
  36. package/addon/components/modals/issue-assign.js +49 -0
  37. package/addon/components/modals/issue-close.hbs +30 -0
  38. package/addon/components/modals/issue-close.js +73 -0
  39. package/addon/components/modals/issue-status.hbs +20 -0
  40. package/addon/components/modals/issue-status.js +43 -0
  41. package/addon/components/modals/order-change-destination.hbs +46 -0
  42. package/addon/components/modals/order-change-destination.js +106 -0
  43. package/addon/components/modals/reset-customer-credentials.js +12 -6
  44. package/addon/components/modals/stop-activity.hbs +28 -0
  45. package/addon/components/modals/stop-activity.js +52 -0
  46. package/addon/components/modals/update-order-activity.hbs +78 -2
  47. package/addon/components/modals/update-order-activity.js +197 -5
  48. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  49. package/addon/components/orchestrator/resource-panel.js +2 -4
  50. package/addon/components/order/activity-list.hbs +2 -2
  51. package/addon/components/order/activity-timeline.hbs +31 -29
  52. package/addon/components/order/details/activity.hbs +3 -3
  53. package/addon/components/order/details/activity.js +13 -2
  54. package/addon/components/order/details/documents.js +2 -0
  55. package/addon/components/order/details/proof.hbs +40 -38
  56. package/addon/components/order/details/proof.js +31 -4
  57. package/addon/components/order/details/purchase-rate.hbs +2 -2
  58. package/addon/components/order/details/route.hbs +11 -3
  59. package/addon/components/order/details/route.js +91 -14
  60. package/addon/components/order/details/tracking.hbs +101 -72
  61. package/addon/components/order/details/tracking.js +149 -2
  62. package/addon/components/order/details.hbs +5 -5
  63. package/addon/components/order/details.js +19 -1
  64. package/addon/components/order/form/details.hbs +3 -3
  65. package/addon/components/order/form/documents.js +11 -4
  66. package/addon/components/order/form/route.hbs +108 -104
  67. package/addon/components/order/form/route.js +37 -1
  68. package/addon/components/order/form/service-rate.hbs +2 -2
  69. package/addon/components/order/header.hbs +1 -1
  70. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  71. package/addon/components/order-config-manager/activity-flow.js +178 -12
  72. package/addon/components/place/address.js +1 -1
  73. package/addon/components/route-list.hbs +17 -29
  74. package/addon/components/route-list.js +42 -10
  75. package/addon/components/service-rate/details.hbs +42 -1
  76. package/addon/components/service-rate/form.hbs +119 -1
  77. package/addon/components/service-rate/form.js +59 -0
  78. package/addon/components/vehicle/form.js +1 -1
  79. package/addon/components/vehicle/pill.hbs +1 -1
  80. package/addon/components/vendor/personnel-manager.hbs +166 -0
  81. package/addon/components/vendor/personnel-manager.js +143 -0
  82. package/addon/components/widget/live-fleet.hbs +1 -0
  83. package/addon/components/widget/live-fleet.js +2 -7
  84. package/addon/components/widget/revenue-trend.hbs +1 -1
  85. package/addon/components/widget/revenue-trend.js +75 -2
  86. package/addon/controllers/management/contacts/customers/details.js +31 -13
  87. package/addon/controllers/management/contacts/customers.js +16 -5
  88. package/addon/controllers/management/contacts/index/details.js +31 -13
  89. package/addon/controllers/management/contacts/index.js +7 -0
  90. package/addon/controllers/management/issues/index/details.js +52 -6
  91. package/addon/controllers/management/vehicles/index/new.js +1 -1
  92. package/addon/controllers/management/vendors/index/details.js +4 -0
  93. package/addon/controllers/operations/orders/index/details.js +15 -7
  94. package/addon/controllers/operations/orders/index.js +15 -1
  95. package/addon/extension.js +19 -12
  96. package/addon/helpers/format-duration.js +1 -38
  97. package/addon/helpers/place-address-html.js +2 -2
  98. package/addon/initializers/load-leaflet-assets.js +5 -6
  99. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  100. package/addon/routes/management/issues/index/details.js +1 -1
  101. package/addon/routes/operations/service-rates/index.js +1 -1
  102. package/addon/routes.js +1 -0
  103. package/addon/services/contact-actions.js +231 -1
  104. package/addon/services/geofence.js +163 -19
  105. package/addon/services/getting-started.js +44 -0
  106. package/addon/services/issue-actions.js +71 -0
  107. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  108. package/addon/services/leaflet-map-manager.js +2 -0
  109. package/addon/services/map-adapter/leaflet.js +19 -0
  110. package/addon/services/service-areas.js +3 -0
  111. package/addon/services/vehicle-actions.js +1 -1
  112. package/addon/styles/fleetops-engine.css +1127 -1
  113. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  114. package/addon/templates/management/contacts/index/details.hbs +1 -1
  115. package/addon/templates/management/contacts.hbs +2 -2
  116. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  117. package/addon/templates/management/issues/index/details.hbs +3 -2
  118. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  119. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  120. package/addon/templates/operations/orders/index.hbs +1 -1
  121. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  122. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  123. package/addon/templates/settings/payments/onboard.hbs +8 -8
  124. package/addon/utils/fleet-ops-options.js +2 -1
  125. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  126. package/addon/utils/leaflet-plugin-loader.js +219 -0
  127. package/addon/utils/normalize-order-config-flow.js +41 -0
  128. package/addon/utils/place-address-html.js +3 -2
  129. package/addon/utils/route-visualization.js +54 -7
  130. package/addon/utils/setup-customer-portal.js +4 -64
  131. package/app/components/home/getting-started-guidance.js +1 -0
  132. package/app/components/issue/details/comments.js +1 -0
  133. package/app/components/issue/details/correspondence.js +1 -0
  134. package/app/components/issue/details/documents.js +1 -0
  135. package/app/components/issue/panel-header.js +1 -0
  136. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  137. package/app/components/modals/issue-assign.js +1 -0
  138. package/app/components/modals/issue-close.js +1 -0
  139. package/app/components/modals/issue-status.js +1 -0
  140. package/app/components/modals/order-change-destination.js +1 -0
  141. package/app/components/modals/stop-activity.js +1 -0
  142. package/app/components/vendor/personnel-manager.js +1 -0
  143. package/app/helpers/format-duration.js +1 -1
  144. package/app/services/getting-started.js +1 -0
  145. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  146. package/app/utils/leaflet-plugin-loader.js +1 -0
  147. package/composer.json +1 -1
  148. package/extension.json +1 -1
  149. package/package.json +5 -5
  150. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  151. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  152. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  153. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  154. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  155. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  156. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  157. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  158. package/server/resources/views/labels/default.php +57 -49
  159. package/server/resources/views/labels/waypoint-label.php +27 -20
  160. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  161. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  162. package/server/seeders/Testing/FleetSeeder.php +22 -22
  163. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  164. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  165. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  166. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  167. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  168. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  169. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  170. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  171. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  172. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  173. package/server/src/Http/Controllers/Api/v1/OrderController.php +109 -80
  174. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  175. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  176. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  177. package/server/src/Http/Controllers/Internal/v1/ContactController.php +154 -0
  178. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  179. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  180. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  181. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  182. package/server/src/Http/Controllers/Internal/v1/OrderController.php +530 -52
  183. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  184. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  185. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  186. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  187. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  188. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  189. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  190. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  191. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  192. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  193. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  194. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  195. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  196. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  197. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  198. package/server/src/Http/Resources/v1/Customer.php +79 -0
  199. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  200. package/server/src/Http/Resources/v1/Issue.php +25 -0
  201. package/server/src/Http/Resources/v1/Order.php +20 -18
  202. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  203. package/server/src/Http/Resources/v1/Payload.php +23 -20
  204. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  205. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  206. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  207. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  208. package/server/src/Models/Contact.php +122 -66
  209. package/server/src/Models/Customer.php +64 -0
  210. package/server/src/Models/Driver.php +12 -3
  211. package/server/src/Models/Entity.php +2 -2
  212. package/server/src/Models/Issue.php +47 -6
  213. package/server/src/Models/Order.php +62 -10
  214. package/server/src/Models/Payload.php +17 -1
  215. package/server/src/Models/ServiceArea.php +1 -1
  216. package/server/src/Models/ServiceRate.php +273 -1
  217. package/server/src/Models/ServiceRateFee.php +39 -9
  218. package/server/src/Models/TrackingNumber.php +8 -2
  219. package/server/src/Models/Vehicle.php +18 -2
  220. package/server/src/Models/VendorPersonnel.php +10 -1
  221. package/server/src/Models/Waypoint.php +2 -2
  222. package/server/src/Models/Zone.php +1 -1
  223. package/server/src/Notifications/LateDeparture.php +2 -2
  224. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  225. package/server/src/Notifications/RouteDeviation.php +2 -2
  226. package/server/src/Observers/ContactObserver.php +2 -0
  227. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  228. package/server/src/Observers/ServiceRateObserver.php +6 -6
  229. package/server/src/Observers/ZoneObserver.php +56 -0
  230. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  231. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  232. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  233. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  234. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  235. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  236. package/server/src/Support/Analytics/TopDrivers.php +10 -2
  237. package/server/src/Support/CustomerAuth.php +98 -0
  238. package/server/src/Support/GettingStarted.php +144 -0
  239. package/server/src/Support/LabelPdf.php +52 -0
  240. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  241. package/server/src/Support/PlaceSearch.php +282 -0
  242. package/server/src/Support/ResolvesOrderServiceStops.php +522 -0
  243. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  244. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  245. package/server/src/Tracking/TrackingStop.php +13 -11
  246. package/server/src/routes.php +76 -10
  247. package/server/tests/AnalyticsRoutesTest.php +8 -0
  248. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  249. package/server/tests/CustomerContactIdentitySafetyTest.php +95 -0
  250. package/server/tests/CustomerEndpointTest.php +199 -0
  251. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  252. package/server/tests/GettingStartedStatusTest.php +21 -0
  253. package/server/tests/LabelPdfRenderingTest.php +23 -0
  254. package/server/tests/LiveControllerViewportTest.php +60 -0
  255. package/server/tests/MetricsRegistryTest.php +4 -8
  256. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  257. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  258. package/server/tests/OrderConfigEndpointTest.php +86 -0
  259. package/server/tests/OrderFileAttachmentTest.php +20 -0
  260. package/server/tests/ServiceAreaZoneApiShapeTest.php +78 -0
  261. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  262. package/translations/ar-ae.yml +9 -0
  263. package/translations/bg-bg.yaml +9 -0
  264. package/translations/en-us.yaml +12 -0
  265. package/translations/es-pa.yaml +9 -0
  266. package/translations/fr-fr.yaml +9 -0
  267. package/translations/mn-mn.yaml +9 -0
  268. package/translations/pt-br.yaml +9 -0
  269. package/translations/ru-ru.yaml +9 -0
  270. package/translations/vi-vn.yaml +9 -0
  271. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -14,12 +14,12 @@ use Illuminate\Support\Carbon;
14
14
  abstract class AbstractMetric
15
15
  {
16
16
  protected Company $company;
17
- protected ?\DateTimeInterface $start = null;
18
- protected ?\DateTimeInterface $end = null;
17
+ protected ?\DateTimeInterface $start = null;
18
+ protected ?\DateTimeInterface $end = null;
19
19
  protected ?\DateTimeInterface $compareStart = null;
20
20
  protected ?\DateTimeInterface $compareEnd = null;
21
- protected int $sparklineBuckets = 0;
22
- protected string $sparklineUnit = 'day';
21
+ protected int $sparklineBuckets = 0;
22
+ protected string $sparklineUnit = 'day';
23
23
 
24
24
  /**
25
25
  * Period boundaries for the currently-executing aggregate call.
@@ -0,0 +1,282 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Models\Place;
6
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
7
+ use Geocoder\Laravel\Facades\Geocoder;
8
+ use Illuminate\Database\Eloquent\Builder;
9
+ use Illuminate\Support\Collection;
10
+ use Illuminate\Support\Str;
11
+
12
+ class PlaceSearch
13
+ {
14
+ public static function search(Builder $query, ?string $searchQuery = null, array $options = []): Collection
15
+ {
16
+ $searchQuery = static::normalizeSearchQuery($searchQuery);
17
+ $limit = (int) data_get($options, 'limit', 30);
18
+ $geo = filter_var(data_get($options, 'geo', false), FILTER_VALIDATE_BOOLEAN);
19
+ $latitude = data_get($options, 'latitude');
20
+ $longitude = data_get($options, 'longitude');
21
+ $noQueryOrder = data_get($options, 'no_query_order', 'name_desc');
22
+
23
+ static::applySavedPlaceSearch($query, $searchQuery, $latitude, $longitude, $noQueryOrder);
24
+
25
+ if ($limit > 0) {
26
+ $query->limit($limit);
27
+ }
28
+
29
+ $savedPlaces = $query->get();
30
+ $geoPlaces = $geo ? static::geocode($searchQuery, $latitude, $longitude) : collect();
31
+
32
+ return static::mergeResults($geoPlaces, $savedPlaces, $searchQuery);
33
+ }
34
+
35
+ public static function geocode(?string $searchQuery = null, $latitude = null, $longitude = null): Collection
36
+ {
37
+ $searchQuery = static::normalizeSearchQuery($searchQuery);
38
+
39
+ if (!$searchQuery && (!$latitude || !$longitude)) {
40
+ return collect();
41
+ }
42
+
43
+ try {
44
+ if (Geocoding::canGoogleGeocode()) {
45
+ $results = collect();
46
+
47
+ try {
48
+ if ($searchQuery) {
49
+ $results = $results->merge(Geocoding::geocode($searchQuery, $latitude, $longitude));
50
+ } elseif ($latitude && $longitude) {
51
+ $results = $results->merge(Geocoding::reverseFromCoordinates($latitude, $longitude));
52
+ }
53
+ } catch (\Throwable) {
54
+ // Fall through to any nearby results already collected.
55
+ }
56
+
57
+ if ($searchQuery && $latitude && $longitude) {
58
+ try {
59
+ $results = $results->merge(Geocoding::reverseFromQuery($searchQuery, $latitude, $longitude));
60
+ } catch (\Throwable) {
61
+ // Text geocoding results should still be usable if nearby lookup fails.
62
+ }
63
+ }
64
+
65
+ return static::rankPlacesByQuery(static::uniquePlaces($results), $searchQuery);
66
+ }
67
+
68
+ if (!$searchQuery) {
69
+ return collect();
70
+ }
71
+
72
+ $results = Geocoder::geocode($searchQuery)
73
+ ->get()
74
+ ->map(fn ($googleAddress) => Place::createFromGoogleAddress($googleAddress))
75
+ ->values();
76
+
77
+ return static::rankPlacesByQuery($results, $searchQuery);
78
+ } catch (\Throwable) {
79
+ return collect();
80
+ }
81
+ }
82
+
83
+ protected static function applySavedPlaceSearch(Builder $query, ?string $searchQuery, $latitude = null, $longitude = null, string $noQueryOrder = 'name_desc'): void
84
+ {
85
+ $query->addSelect('places.*');
86
+
87
+ if ($searchQuery) {
88
+ $query->where(function ($query) use ($searchQuery) {
89
+ $contains = '%' . $searchQuery . '%';
90
+
91
+ $query->orWhereRaw('LOWER(COALESCE(places.name, "")) LIKE ?', [$contains])
92
+ ->orWhereRaw('LOWER(COALESCE(places.street1, "")) LIKE ?', [$contains])
93
+ ->orWhereRaw('LOWER(COALESCE(places.street2, "")) LIKE ?', [$contains])
94
+ ->orWhereRaw('LOWER(COALESCE(places.city, "")) LIKE ?', [$contains])
95
+ ->orWhereRaw('LOWER(COALESCE(places.province, "")) LIKE ?', [$contains])
96
+ ->orWhereRaw('LOWER(COALESCE(places.postal_code, "")) LIKE ?', [$contains]);
97
+ });
98
+
99
+ $prefix = $searchQuery . '%';
100
+ $contains = '%' . $searchQuery . '%';
101
+
102
+ $query->selectRaw(
103
+ 'CASE
104
+ WHEN LOWER(COALESCE(places.name, "")) = ? OR LOWER(COALESCE(places.street1, "")) = ? OR LOWER(COALESCE(places.postal_code, "")) = ? THEN 0
105
+ WHEN LOWER(COALESCE(places.name, "")) LIKE ? OR LOWER(COALESCE(places.street1, "")) LIKE ? OR LOWER(COALESCE(places.postal_code, "")) LIKE ? THEN 1
106
+ WHEN LOWER(COALESCE(places.name, "")) LIKE ? OR LOWER(COALESCE(places.street1, "")) LIKE ? OR LOWER(COALESCE(places.street2, "")) LIKE ? OR LOWER(COALESCE(places.city, "")) LIKE ? OR LOWER(COALESCE(places.province, "")) LIKE ? OR LOWER(COALESCE(places.postal_code, "")) LIKE ? THEN 2
107
+ ELSE 3
108
+ END AS place_search_relevance',
109
+ [$searchQuery, $searchQuery, $searchQuery, $prefix, $prefix, $prefix, $contains, $contains, $contains, $contains, $contains, $contains]
110
+ );
111
+ $query->orderBy('place_search_relevance', 'asc');
112
+ }
113
+
114
+ if ($latitude && $longitude) {
115
+ $point = new Point($latitude, $longitude);
116
+
117
+ $query->whereNotNull('places.location')->whereRaw('
118
+ ST_Y(places.location) BETWEEN -90 AND 90
119
+ AND ST_X(places.location) BETWEEN -180 AND 180
120
+ AND NOT (ST_X(places.location) = 0 AND ST_Y(places.location) = 0)
121
+ ');
122
+ $query->orderByDistanceSphere('location', $point, 'asc');
123
+ } elseif (!$searchQuery && $noQueryOrder === 'latest') {
124
+ $query->orderBy('places.created_at', 'desc');
125
+ } else {
126
+ $query->orderBy('places.name', 'desc');
127
+ }
128
+ }
129
+
130
+ protected static function mergeResults(Collection $geoPlaces, Collection $savedPlaces, ?string $searchQuery = null): Collection
131
+ {
132
+ $merged = [];
133
+ $indexes = [];
134
+
135
+ foreach ($geoPlaces as $place) {
136
+ static::pushUniquePlace($merged, $indexes, $place);
137
+ }
138
+
139
+ foreach ($savedPlaces as $place) {
140
+ $key = static::placeKey($place);
141
+
142
+ if ($key && isset($indexes[$key])) {
143
+ if (static::isStrongSavedMatch($place, $searchQuery)) {
144
+ $merged[$indexes[$key]] = $place;
145
+ }
146
+
147
+ continue;
148
+ }
149
+
150
+ static::pushUniquePlace($merged, $indexes, $place);
151
+ }
152
+
153
+ return collect($merged)->values();
154
+ }
155
+
156
+ protected static function pushUniquePlace(array &$merged, array &$indexes, ?Place $place): void
157
+ {
158
+ if (!$place instanceof Place) {
159
+ return;
160
+ }
161
+
162
+ $key = static::placeKey($place);
163
+
164
+ if ($key && isset($indexes[$key])) {
165
+ return;
166
+ }
167
+
168
+ if ($key) {
169
+ $indexes[$key] = count($merged);
170
+ }
171
+
172
+ $merged[] = $place;
173
+ }
174
+
175
+ protected static function uniquePlaces(Collection $places): Collection
176
+ {
177
+ $merged = [];
178
+ $indexes = [];
179
+
180
+ foreach ($places as $place) {
181
+ static::pushUniquePlace($merged, $indexes, $place);
182
+ }
183
+
184
+ return collect($merged)->values();
185
+ }
186
+
187
+ protected static function rankPlacesByQuery(Collection $places, ?string $searchQuery = null): Collection
188
+ {
189
+ if (!$searchQuery) {
190
+ return $places->values();
191
+ }
192
+
193
+ return $places
194
+ ->values()
195
+ ->map(fn ($place, $index) => [
196
+ 'place' => $place,
197
+ 'rank' => static::placeQueryRank($place, $searchQuery),
198
+ 'index' => $index,
199
+ ])
200
+ ->sortBy([
201
+ ['rank', 'asc'],
202
+ ['index', 'asc'],
203
+ ])
204
+ ->pluck('place')
205
+ ->values();
206
+ }
207
+
208
+ protected static function placeQueryRank(Place $place, ?string $searchQuery = null): int
209
+ {
210
+ if (!$searchQuery) {
211
+ return 4;
212
+ }
213
+
214
+ $query = static::normalizeSearchQuery($searchQuery);
215
+ $values = collect([
216
+ $place->name,
217
+ $place->street1,
218
+ $place->postal_code,
219
+ $place->toAddressString(['name']),
220
+ $place->address,
221
+ ])
222
+ ->filter()
223
+ ->map(fn ($value) => static::normalizeSearchQuery($value));
224
+
225
+ if ($values->contains($query)) {
226
+ return 0;
227
+ }
228
+
229
+ if ($values->contains(fn ($value) => Str::startsWith($value, $query))) {
230
+ return 1;
231
+ }
232
+
233
+ if ($values->contains(fn ($value) => Str::contains($value, $query))) {
234
+ return 2;
235
+ }
236
+
237
+ return 3;
238
+ }
239
+
240
+ protected static function isStrongSavedMatch(Place $place, ?string $searchQuery = null): bool
241
+ {
242
+ if (!$searchQuery) {
243
+ return false;
244
+ }
245
+
246
+ $query = static::normalizeSearchQuery($searchQuery);
247
+
248
+ return collect([$place->name, $place->street1, $place->postal_code])
249
+ ->filter()
250
+ ->map(fn ($value) => static::normalizeSearchQuery($value))
251
+ ->contains($query);
252
+ }
253
+
254
+ protected static function placeKey(Place $place): ?string
255
+ {
256
+ $address = static::normalizeSearchQuery($place->toAddressString(['name']) ?: $place->address ?: $place->street1 ?: $place->name);
257
+ $lat = data_get($place, 'latitude');
258
+ $lng = data_get($place, 'longitude');
259
+
260
+ if (!$lat && $place->location && method_exists($place->location, 'getLat')) {
261
+ $lat = $place->location->getLat();
262
+ $lng = $place->location->getLng();
263
+ }
264
+
265
+ $coordinates = $lat && $lng ? round((float) $lat, 5) . ',' . round((float) $lng, 5) : null;
266
+
267
+ if (!$address && !$coordinates) {
268
+ return null;
269
+ }
270
+
271
+ return trim($address . '|' . $coordinates, '|');
272
+ }
273
+
274
+ protected static function normalizeSearchQuery(?string $searchQuery): ?string
275
+ {
276
+ if (!is_string($searchQuery)) {
277
+ return null;
278
+ }
279
+
280
+ return (string) Str::of($searchQuery)->lower()->squish();
281
+ }
282
+ }