@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
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Models\Model;
6
+ use Fleetbase\Models\User;
6
7
  use Fleetbase\Traits\HasUuid;
7
8
  use Illuminate\Database\Eloquent\Relations\BelongsTo;
8
9
 
@@ -22,7 +23,7 @@ class VendorPersonnel extends Model
22
23
  *
23
24
  * @var array
24
25
  */
25
- protected $fillable = ['vendor_uuid', 'contact_uuid'];
26
+ protected $fillable = ['vendor_uuid', 'contact_uuid', 'role', 'status', 'invited_by_uuid'];
26
27
 
27
28
  /**
28
29
  * These attributes that can be queried.
@@ -60,4 +61,12 @@ class VendorPersonnel extends Model
60
61
  {
61
62
  return $this->belongsTo(Contact::class);
62
63
  }
64
+
65
+ /**
66
+ * The Fleetbase user who invited this personnel.
67
+ */
68
+ public function invitedBy(): BelongsTo
69
+ {
70
+ return $this->belongsTo(User::class, 'invited_by_uuid');
71
+ }
63
72
  }
@@ -2,8 +2,8 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
- use Barryvdh\DomPDF\Facade\Pdf;
6
5
  use Fleetbase\Casts\PolymorphicType;
6
+ use Fleetbase\FleetOps\Support\LabelPdf;
7
7
  use Fleetbase\FleetOps\Support\Utils;
8
8
  use Fleetbase\FleetOps\Traits\HasTrackingNumber;
9
9
  use Fleetbase\FleetOps\Traits\PayloadAccessors;
@@ -94,7 +94,7 @@ class Waypoint extends Model
94
94
  */
95
95
  public function pdfLabel()
96
96
  {
97
- return Pdf::loadHTML($this->label());
97
+ return LabelPdf::fromHtml($this->label());
98
98
  }
99
99
 
100
100
  /**
@@ -118,7 +118,7 @@ class Zone extends Model
118
118
  */
119
119
  public function serviceArea()
120
120
  {
121
- return $this->belongsTo(ServiceArea::class);
121
+ return $this->belongsTo(ServiceArea::class, 'service_area_uuid', 'uuid');
122
122
  }
123
123
 
124
124
  /**
@@ -12,9 +12,9 @@ class LateDeparture extends Notification implements ShouldQueue
12
12
  {
13
13
  use Queueable;
14
14
 
15
- public static string $name = 'Late Departure';
15
+ public static string $name = 'Late Departure';
16
16
  public static string $description = 'Notify when an order has not departed after the configured grace period.';
17
- public static string $package = 'fleet-ops';
17
+ public static string $package = 'fleet-ops';
18
18
 
19
19
  public function __construct(public Order $order, public array $context = [])
20
20
  {
@@ -12,9 +12,9 @@ class ProlongedStoppage extends Notification implements ShouldQueue
12
12
  {
13
13
  use Queueable;
14
14
 
15
- public static string $name = 'Prolonged Stoppage';
15
+ public static string $name = 'Prolonged Stoppage';
16
16
  public static string $description = 'Notify when a vehicle remains stopped during an active trip beyond the configured threshold.';
17
- public static string $package = 'fleet-ops';
17
+ public static string $package = 'fleet-ops';
18
18
 
19
19
  public function __construct(public Order $order, public array $context = [])
20
20
  {
@@ -12,9 +12,9 @@ class RouteDeviation extends Notification implements ShouldQueue
12
12
  {
13
13
  use Queueable;
14
14
 
15
- public static string $name = 'Route Deviation';
15
+ public static string $name = 'Route Deviation';
16
16
  public static string $description = 'Notify when tracked vehicle location deviates from the active route beyond the configured threshold.';
17
- public static string $package = 'fleet-ops';
17
+ public static string $package = 'fleet-ops';
18
18
 
19
19
  public function __construct(public Order $order, public array $context = [])
20
20
  {
@@ -31,6 +31,8 @@ class ContactObserver
31
31
  throw new \Exception('Customer contact type cannot be changed.');
32
32
  }
33
33
 
34
+ $contact->assertCustomerIdentityIsAvailable();
35
+
34
36
  // Get the contacts assosciated user
35
37
  if ($contact->doesntHaveUser()) {
36
38
  $contact->createUser();
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Observers;
4
4
 
5
+ use Fleetbase\FleetOps\Models\Order;
5
6
  use Fleetbase\FleetOps\Models\PurchaseRate;
6
7
  use Fleetbase\FleetOps\Support\Utils;
7
8
  use Fleetbase\Models\Company;
@@ -18,7 +19,12 @@ class PurchaseRateObserver
18
19
  */
19
20
  public function creating(PurchaseRate $purchaseRate)
20
21
  {
22
+ if (!$purchaseRate->uuid) {
23
+ $purchaseRate->uuid = PurchaseRate::generateUuid();
24
+ }
25
+
21
26
  $purchaseRate->load(['serviceQuote.items', 'serviceQuote.serviceRate']);
27
+ $order = $this->resolveOrder($purchaseRate);
22
28
 
23
29
  // get company
24
30
  $company = Company::where('uuid', session('company', $purchaseRate->company_uuid))->first();
@@ -32,12 +38,17 @@ class PurchaseRateObserver
32
38
  'company_uuid' => session('company', $purchaseRate->company_uuid),
33
39
  'customer_uuid' => $purchaseRate->customer_uuid,
34
40
  'customer_type' => $purchaseRate->customer_type,
41
+ 'subject_uuid' => $order?->uuid,
42
+ 'subject_type' => $order ? Order::class : null,
43
+ 'context_uuid' => $purchaseRate->uuid,
44
+ 'context_type' => PurchaseRate::class,
35
45
  'gateway_transaction_id' => $purchaseRate->getMeta('transaction_id', Transaction::generateNumber()),
36
46
  'gateway' => 'internal',
37
47
  'amount' => data_get($purchaseRate, 'serviceQuote.amount', 0),
38
48
  'currency' => $currency,
39
49
  'description' => 'Dispatch order',
40
50
  'type' => 'dispatch',
51
+ 'direction' => Transaction::DIRECTION_CREDIT,
41
52
  'status' => 'success',
42
53
  ]);
43
54
 
@@ -56,4 +67,13 @@ class PurchaseRateObserver
56
67
  $purchaseRate->transaction_uuid = $transaction->uuid;
57
68
  $purchaseRate->status = $purchaseRate->status ?: Transaction::STATUS_SUCCESS;
58
69
  }
70
+
71
+ protected function resolveOrder(PurchaseRate $purchaseRate): ?Order
72
+ {
73
+ if (!$purchaseRate->payload_uuid) {
74
+ return null;
75
+ }
76
+
77
+ return Order::where('payload_uuid', $purchaseRate->payload_uuid)->first();
78
+ }
59
79
  }
@@ -14,10 +14,10 @@ class ServiceRateObserver
14
14
  */
15
15
  public function created(ServiceRate $serviceRate)
16
16
  {
17
- $serviceRateFees = request()->input('serviceRate.rate_fees');
18
- $serviceRateParcelFees = request()->input('serviceRate.parcel_fees');
17
+ $serviceRateFees = request()->input('serviceRate.rate_fees', request()->input('service_rate.rate_fees'));
18
+ $serviceRateParcelFees = request()->input('serviceRate.parcel_fees', request()->input('service_rate.parcel_fees'));
19
19
 
20
- if ($serviceRate->isFixedMeter() || $serviceRate->isPerDrop()) {
20
+ if ($serviceRate->isFixedMeter() || $serviceRate->isPerDrop() || $serviceRate->isMultiZoneDistance()) {
21
21
  $serviceRate->setServiceRateFees($serviceRateFees);
22
22
  }
23
23
 
@@ -33,10 +33,10 @@ class ServiceRateObserver
33
33
  */
34
34
  public function updated(ServiceRate $serviceRate)
35
35
  {
36
- $serviceRateFees = request()->input('serviceRate.rate_fees');
37
- $serviceRateParcelFees = request()->input('serviceRate.parcel_fees');
36
+ $serviceRateFees = request()->input('serviceRate.rate_fees', request()->input('service_rate.rate_fees'));
37
+ $serviceRateParcelFees = request()->input('serviceRate.parcel_fees', request()->input('service_rate.parcel_fees'));
38
38
 
39
- if ($serviceRate->isFixedMeter() || $serviceRate->isPerDrop()) {
39
+ if ($serviceRate->isFixedMeter() || $serviceRate->isPerDrop() || $serviceRate->isMultiZoneDistance()) {
40
40
  $serviceRate->setServiceRateFees($serviceRateFees);
41
41
  }
42
42
 
@@ -0,0 +1,56 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Observers;
4
+
5
+ use Fleetbase\FleetOps\Models\ServiceArea;
6
+ use Fleetbase\FleetOps\Models\Zone;
7
+
8
+ class ZoneObserver
9
+ {
10
+ /**
11
+ * Handle the Zone "created" event.
12
+ */
13
+ public function created(Zone $zone): void
14
+ {
15
+ $this->invalidateServiceAreaCache($zone);
16
+ }
17
+
18
+ /**
19
+ * Handle the Zone "updated" event.
20
+ */
21
+ public function updated(Zone $zone): void
22
+ {
23
+ $this->invalidateServiceAreaCache($zone, $zone->getOriginal('service_area_uuid'));
24
+ $this->invalidateServiceAreaCache($zone);
25
+ }
26
+
27
+ /**
28
+ * Handle the Zone "deleted" event.
29
+ */
30
+ public function deleted(Zone $zone): void
31
+ {
32
+ $this->invalidateServiceAreaCache($zone);
33
+ }
34
+
35
+ /**
36
+ * Handle the Zone "restored" event.
37
+ */
38
+ public function restored(Zone $zone): void
39
+ {
40
+ $this->invalidateServiceAreaCache($zone);
41
+ }
42
+
43
+ /**
44
+ * Invalidate service area API cache because service area responses embed zones.
45
+ */
46
+ protected function invalidateServiceAreaCache(Zone $zone, ?string $serviceAreaUuid = null): void
47
+ {
48
+ $serviceAreaUuid ??= $zone->service_area_uuid;
49
+ if (!$serviceAreaUuid) {
50
+ return;
51
+ }
52
+
53
+ ServiceArea::invalidateApiCacheManually($zone->company_uuid);
54
+ ServiceArea::invalidateApiCacheManually();
55
+ }
56
+ }
@@ -85,7 +85,7 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
85
85
  $vroomPayload = [
86
86
  'jobs' => [],
87
87
  'shipments' => [],
88
- 'vehicles' => array_map(function (array $v) use ($profile) {
88
+ 'vehicles' => array_map(function (array $v) use ($profile) {
89
89
  $vehicle = [
90
90
  'id' => crc32($v['id']), // VROOM requires integer IDs
91
91
  'description' => json_encode(['vehicle_id' => $v['id'], 'driver_id' => $v['driver_id']]),
@@ -29,6 +29,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
29
29
  \Fleetbase\FleetOps\Models\ServiceRate::class => \Fleetbase\FleetOps\Observers\ServiceRateObserver::class,
30
30
  \Fleetbase\FleetOps\Models\PurchaseRate::class => \Fleetbase\FleetOps\Observers\PurchaseRateObserver::class,
31
31
  \Fleetbase\FleetOps\Models\ServiceArea::class => \Fleetbase\FleetOps\Observers\ServiceAreaObserver::class,
32
+ \Fleetbase\FleetOps\Models\Zone::class => \Fleetbase\FleetOps\Observers\ZoneObserver::class,
32
33
  \Fleetbase\FleetOps\Models\TrackingNumber::class => \Fleetbase\FleetOps\Observers\TrackingNumberObserver::class,
33
34
  \Fleetbase\FleetOps\Models\Driver::class => \Fleetbase\FleetOps\Observers\DriverObserver::class,
34
35
  \Fleetbase\FleetOps\Models\Vehicle::class => \Fleetbase\FleetOps\Observers\VehicleObserver::class,
@@ -56,6 +57,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
56
57
  \Fleetbase\FleetOps\Console\Commands\FixInvalidPolymorphicRelationTypeNamespaces::class,
57
58
  \Fleetbase\FleetOps\Console\Commands\AssignDriverRoles::class,
58
59
  \Fleetbase\FleetOps\Console\Commands\AssignCustomerRoles::class,
60
+ \Fleetbase\FleetOps\Console\Commands\AuditCustomerUserConflicts::class,
59
61
  \Fleetbase\FleetOps\Console\Commands\SimulateOrderRouteNavigation::class,
60
62
  \Fleetbase\FleetOps\Console\Commands\DebugOrderTracker::class,
61
63
  \Fleetbase\FleetOps\Console\Commands\PurgeUnpurchasedServiceQuotes::class,
@@ -40,12 +40,12 @@ abstract class AbstractAnalytics
40
40
  {
41
41
  if ($period !== null && $period !== '') {
42
42
  $days = match ($period) {
43
- '7d' => 7,
44
- '14d' => 14,
45
- '30d' => 30,
46
- '90d' => 90,
47
- '180d' => 180,
48
- '365d' => 365,
43
+ '7d' => 7,
44
+ '14d' => 14,
45
+ '30d' => 30,
46
+ '90d' => 90,
47
+ '180d' => 180,
48
+ '365d' => 365,
49
49
  default => null,
50
50
  };
51
51
 
@@ -22,7 +22,7 @@ class FuelEfficiency extends AbstractAnalytics
22
22
  $fuelByWeek = FuelReport::where('company_uuid', $this->company->uuid)
23
23
  ->where('currency', $currency)
24
24
  ->whereBetween('created_at', [$start, $end])
25
- ->selectRaw("YEARWEEK(created_at, 1) as wk, MIN(DATE(created_at)) as wk_start, SUM(amount) as total_cost")
25
+ ->selectRaw('YEARWEEK(created_at, 1) as wk, MIN(DATE(created_at)) as wk_start, SUM(amount) as total_cost')
26
26
  ->groupBy('wk')
27
27
  ->orderBy('wk')
28
28
  ->get();
@@ -30,7 +30,7 @@ class FuelEfficiency extends AbstractAnalytics
30
30
  $distanceByWeek = Order::where('company_uuid', $this->company->uuid)
31
31
  ->where('status', 'completed')
32
32
  ->whereBetween('updated_at', [$start, $end])
33
- ->selectRaw("YEARWEEK(updated_at, 1) as wk, SUM(distance) as total_distance")
33
+ ->selectRaw('YEARWEEK(updated_at, 1) as wk, SUM(distance) as total_distance')
34
34
  ->groupBy('wk')
35
35
  ->orderBy('wk')
36
36
  ->pluck('total_distance', 'wk');
@@ -41,7 +41,7 @@ class OrdersByStatus extends AbstractAnalytics
41
41
  while ($cursor <= $endDay) {
42
42
  $labels[] = $cursor->format('M j');
43
43
  foreach (array_keys(self::STATUS_PALETTE) as $status) {
44
- $match = $rows->first(fn ($r) => $r->bucket === $cursor->format('Y-m-d') && $r->status === $status);
44
+ $match = $rows->first(fn ($r) => $r->bucket === $cursor->format('Y-m-d') && $r->status === $status);
45
45
  $byStatus[$status][] = $match ? (int) $match->total : 0;
46
46
  }
47
47
  $cursor->addDay();
@@ -4,7 +4,6 @@ namespace Fleetbase\FleetOps\Support\Analytics;
4
4
 
5
5
  use Fleetbase\Models\Transaction;
6
6
  use Illuminate\Support\Carbon;
7
- use Illuminate\Support\Facades\DB;
8
7
 
9
8
  /**
10
9
  * Time-series of revenue (sum of transactions) per bucket, filtered to the
@@ -12,7 +12,7 @@ use Illuminate\Support\Carbon;
12
12
  */
13
13
  class TopDrivers extends AbstractAnalytics
14
14
  {
15
- protected int $limit = 10;
15
+ protected int $limit = 10;
16
16
  protected string $sortBy = 'orders_completed';
17
17
 
18
18
  public function limit(int $limit): self
@@ -38,7 +38,15 @@ class TopDrivers extends AbstractAnalytics
38
38
 
39
39
  $sortColumn = match ($this->sortBy) {
40
40
  'distance' => 'distance_m',
41
- 'on_time' => 'on_time_pct',
41
+ 'on_time' => 'CASE
42
+ WHEN SUM(CASE WHEN orders.scheduled_at IS NOT NULL THEN 1 ELSE 0 END) > 0
43
+ THEN SUM(CASE
44
+ WHEN orders.scheduled_at IS NOT NULL
45
+ AND TIMESTAMPDIFF(SECOND, orders.scheduled_at, orders.updated_at) <= 1800
46
+ THEN 1 ELSE 0 END)
47
+ / SUM(CASE WHEN orders.scheduled_at IS NOT NULL THEN 1 ELSE 0 END)
48
+ ELSE -1
49
+ END',
42
50
  default => 'orders_completed',
43
51
  };
44
52
 
@@ -0,0 +1,98 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Models\Contact;
6
+ use Illuminate\Http\Request;
7
+ use Illuminate\Support\Str;
8
+ use Laravel\Sanctum\PersonalAccessToken;
9
+
10
+ /**
11
+ * Helper for resolving the authenticated customer from a request.
12
+ *
13
+ * The customer token is issued by `CustomerController` as a Sanctum
14
+ * `PersonalAccessToken` whose `name` column carries the owning Contact's UUID.
15
+ * Mirrors {@see \Fleetbase\Storefront\Support\Storefront::getCustomerFromToken}
16
+ * but with no Storefront-specific session.
17
+ */
18
+ class CustomerAuth
19
+ {
20
+ public const HEADER = 'Customer-Token';
21
+
22
+ public const APP_BINDING = 'currentCustomer';
23
+
24
+ /**
25
+ * Resolve the Contact represented by the `Customer-Token` header.
26
+ *
27
+ * Preference order when a User has more than one customer-type Contact:
28
+ * 1. Contact whose UUID matches the token's `name` column (Storefront convention).
29
+ * 2. Contact in the same company as the resolved API credential (`session('company')`).
30
+ * 3. The first remaining customer-type Contact for the token's User.
31
+ */
32
+ public static function resolveFromHeader(?Request $request = null): ?Contact
33
+ {
34
+ $request = $request ?? request();
35
+ $token = $request->header(self::HEADER);
36
+ if (!$token) {
37
+ return null;
38
+ }
39
+
40
+ $accessToken = PersonalAccessToken::findToken($token);
41
+ if (!$accessToken) {
42
+ return null;
43
+ }
44
+
45
+ // Storefront convention: token name = Contact UUID.
46
+ if (is_string($accessToken->name) && Str::isUuid($accessToken->name)) {
47
+ $contact = Contact::where('uuid', $accessToken->name)
48
+ ->where('type', 'customer')
49
+ ->first();
50
+ if ($contact) {
51
+ return $contact;
52
+ }
53
+ }
54
+
55
+ // Fallback: pick the customer Contact bound to the token's user, preferring
56
+ // the same company as the API credential when available.
57
+ $userId = $accessToken->tokenable_id ?? null;
58
+ $userIdentifier = $accessToken->tokenable->uuid ?? $userId;
59
+ if (!$userIdentifier) {
60
+ return null;
61
+ }
62
+
63
+ $contactQuery = Contact::where('user_uuid', $userIdentifier)
64
+ ->where('type', 'customer');
65
+
66
+ $sessionCompany = session('company');
67
+ if ($sessionCompany) {
68
+ $companyPreferred = (clone $contactQuery)
69
+ ->where('company_uuid', $sessionCompany)
70
+ ->first();
71
+ if ($companyPreferred) {
72
+ return $companyPreferred;
73
+ }
74
+ }
75
+
76
+ return $contactQuery->first();
77
+ }
78
+
79
+ /**
80
+ * Return the currently-bound customer (set by AuthenticateCustomerToken middleware).
81
+ */
82
+ public static function current(): ?Contact
83
+ {
84
+ return app()->bound(self::APP_BINDING) ? app(self::APP_BINDING) : null;
85
+ }
86
+
87
+ /**
88
+ * Bind the resolved customer for the current request lifecycle.
89
+ */
90
+ public static function setCurrent(Contact $contact): void
91
+ {
92
+ app()->instance(self::APP_BINDING, $contact);
93
+ session([
94
+ 'customer_id' => $contact->uuid,
95
+ 'contact_id' => $contact->uuid,
96
+ ]);
97
+ }
98
+ }
@@ -0,0 +1,144 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\TrackingStatus;
8
+ use Fleetbase\Models\Company;
9
+
10
+ class GettingStarted
11
+ {
12
+ protected Company $company;
13
+
14
+ public static function forCompany(Company $company): static
15
+ {
16
+ return new static($company);
17
+ }
18
+
19
+ public function __construct(Company $company)
20
+ {
21
+ $this->company = $company;
22
+ }
23
+
24
+ public function get(): array
25
+ {
26
+ $steps = $this->steps();
27
+ $completed = collect($steps)->filter(fn ($step) => $step['completed'])->count();
28
+ $total = count($steps);
29
+ $nextStep = collect($steps)->first(fn ($step) => !$step['completed']);
30
+ $isCompleted = $completed === $total;
31
+
32
+ return [
33
+ 'profile_source' => 'generic',
34
+ 'profile' => null,
35
+ 'is_completed' => $isCompleted,
36
+ 'progress' => [
37
+ 'completed' => $completed,
38
+ 'total' => $total,
39
+ 'percent' => $total > 0 ? (int) round(($completed / $total) * 100) : 0,
40
+ ],
41
+ 'next_step' => $nextStep['key'] ?? null,
42
+ 'steps' => $steps,
43
+ 'recommendations' => $this->recommendations(),
44
+ ];
45
+ }
46
+
47
+ protected function steps(): array
48
+ {
49
+ $companyUuid = $this->company->uuid;
50
+
51
+ $hasDriver = Driver::where('company_uuid', $companyUuid)->exists();
52
+ $hasOrder = Order::where('company_uuid', $companyUuid)->exists();
53
+
54
+ $hasAssignedOrder = Order::where('company_uuid', $companyUuid)
55
+ ->whereNotNull('driver_assigned_uuid')
56
+ ->exists();
57
+
58
+ $hasOrderActivity = TrackingStatus::where('tracking_statuses.company_uuid', $companyUuid)
59
+ ->whereNotNull('tracking_statuses.tracking_number_uuid')
60
+ ->whereNotIn('tracking_statuses.code', ['CREATED', 'ORDER_CREATED'])
61
+ ->exists();
62
+
63
+ return [
64
+ [
65
+ 'key' => 'add_driver',
66
+ 'title' => 'Add a driver',
67
+ 'description' => 'Create your first driver profile so work can be assigned.',
68
+ 'estimate' => '2 min',
69
+ 'completed' => $hasDriver,
70
+ 'icon' => 'id-card',
71
+ 'route' => 'console.fleet-ops.management.drivers.index.new',
72
+ 'docs_url' => 'https://www.fleetbase.io/docs/fleet-ops/drivers',
73
+ ],
74
+ [
75
+ 'key' => 'create_order',
76
+ 'title' => 'Create an order',
77
+ 'description' => 'Add a delivery or service order to start dispatching.',
78
+ 'estimate' => '3 min',
79
+ 'completed' => $hasOrder,
80
+ 'icon' => 'box',
81
+ 'route' => 'console.fleet-ops.operations.orders.index.new',
82
+ 'docs_url' => 'https://www.fleetbase.io/docs/fleet-ops/orders',
83
+ ],
84
+ [
85
+ 'key' => 'assign_driver',
86
+ 'title' => 'Assign driver to order',
87
+ 'description' => 'Connect the order to a driver for execution.',
88
+ 'estimate' => '1 min',
89
+ 'completed' => $hasAssignedOrder,
90
+ 'icon' => 'user-check',
91
+ 'route' => 'console.fleet-ops.operations.orders.index',
92
+ 'docs_url' => 'https://www.fleetbase.io/docs/fleet-ops/dispatch',
93
+ ],
94
+ [
95
+ 'key' => 'update_activity',
96
+ 'title' => 'Update order activity',
97
+ 'description' => 'Move the order through its workflow and capture progress.',
98
+ 'estimate' => '2 min',
99
+ 'completed' => $hasOrderActivity,
100
+ 'icon' => 'route',
101
+ 'route' => 'console.fleet-ops.operations.orders.index',
102
+ 'docs_url' => 'https://www.fleetbase.io/docs/fleet-ops/tracking',
103
+ ],
104
+ ];
105
+ }
106
+
107
+ protected function recommendations(): array
108
+ {
109
+ return [
110
+ [
111
+ 'key' => 'route_optimization',
112
+ 'title' => 'Route Optimization',
113
+ 'description' => 'Plan efficient multi-stop routes and reduce time on the road.',
114
+ 'icon' => 'route',
115
+ 'accent' => 'blue',
116
+ 'docs_url' => 'https://www.fleetbase.io/docs/fleet-ops/orchestrator',
117
+ ],
118
+ [
119
+ 'key' => 'live_fleet',
120
+ 'title' => 'Live Fleet Map',
121
+ 'description' => 'Watch active drivers, vehicles, and orders in real time.',
122
+ 'icon' => 'map-location-dot',
123
+ 'accent' => 'green',
124
+ 'docs_url' => 'https://www.fleetbase.io/docs/fleet-ops/live-map',
125
+ ],
126
+ [
127
+ 'key' => 'service_rates',
128
+ 'title' => 'Service Rates',
129
+ 'description' => 'Create pricing rules for deliveries, zones, and service areas.',
130
+ 'icon' => 'receipt',
131
+ 'accent' => 'amber',
132
+ 'docs_url' => 'https://www.fleetbase.io/docs/fleet-ops/service-rates',
133
+ ],
134
+ [
135
+ 'key' => 'customer_portal',
136
+ 'title' => 'Customer Portal',
137
+ 'description' => 'Let customers submit orders and track deliveries from a branded portal.',
138
+ 'icon' => 'store',
139
+ 'accent' => 'purple',
140
+ 'docs_url' => 'https://www.fleetbase.io/docs/customer-portal',
141
+ ],
142
+ ];
143
+ }
144
+ }
@@ -0,0 +1,52 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Barryvdh\DomPDF\Facade\Pdf;
6
+
7
+ class LabelPdf
8
+ {
9
+ /**
10
+ * Create a PDF instance for FleetOps labels with UTF-8 and embedded-font
11
+ * options applied consistently across order, waypoint, and entity labels.
12
+ */
13
+ public static function fromHtml(string $html)
14
+ {
15
+ $pdf = Pdf::loadHTML($html, 'UTF-8');
16
+
17
+ if (method_exists($pdf, 'setOptions')) {
18
+ $pdf->setOptions(static::options());
19
+ }
20
+
21
+ if (method_exists($pdf, 'getDomPDF')) {
22
+ $dompdf = $pdf->getDomPDF();
23
+ foreach (static::dompdfOptions() as $key => $value) {
24
+ if (method_exists($dompdf, 'set_option')) {
25
+ $dompdf->set_option($key, $value);
26
+ }
27
+ }
28
+ }
29
+
30
+ return $pdf;
31
+ }
32
+
33
+ public static function options(): array
34
+ {
35
+ return [
36
+ 'defaultFont' => 'DejaVu Sans',
37
+ 'isHtml5ParserEnabled' => true,
38
+ 'isFontSubsettingEnabled' => true,
39
+ 'isRemoteEnabled' => false,
40
+ ];
41
+ }
42
+
43
+ public static function dompdfOptions(): array
44
+ {
45
+ return [
46
+ 'defaultFont' => 'DejaVu Sans',
47
+ 'isHtml5ParserEnabled' => true,
48
+ 'isFontSubsettingEnabled' => true,
49
+ 'isRemoteEnabled' => false,
50
+ ];
51
+ }
52
+ }