@fleetbase/fleetops-engine 0.6.50 → 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 (159) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/details.hbs +1 -2
  4. package/addon/components/customer/form.hbs +63 -51
  5. package/addon/components/customer/form.js +27 -1
  6. package/addon/components/customer/orders.hbs +48 -0
  7. package/addon/components/customer/orders.js +67 -5
  8. package/addon/components/driver/form.hbs +1 -1
  9. package/addon/components/home/getting-started-guidance.hbs +113 -0
  10. package/addon/components/home/getting-started-guidance.js +107 -0
  11. package/addon/components/issue/details/comments.hbs +3 -0
  12. package/addon/components/issue/details/comments.js +3 -0
  13. package/addon/components/issue/details/correspondence.hbs +9 -0
  14. package/addon/components/issue/details/correspondence.js +83 -0
  15. package/addon/components/issue/details/documents.hbs +69 -0
  16. package/addon/components/issue/details/documents.js +85 -0
  17. package/addon/components/issue/details.hbs +220 -59
  18. package/addon/components/issue/details.js +245 -0
  19. package/addon/components/issue/form.hbs +19 -1
  20. package/addon/components/issue/panel-header.hbs +43 -0
  21. package/addon/components/issue/panel-header.js +22 -0
  22. package/addon/components/issue/timeline.hbs +40 -0
  23. package/addon/components/issue/timeline.js +45 -0
  24. package/addon/components/leaflet-tracking-marker.js +3 -0
  25. package/addon/components/map/drawer/position-listing.js +3 -0
  26. package/addon/components/map/leaflet-live-map.hbs +2 -2
  27. package/addon/components/map/leaflet-live-map.js +17 -0
  28. package/addon/components/map/order-list-overlay.hbs +4 -4
  29. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  30. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  31. package/addon/components/modals/issue-assign.hbs +20 -0
  32. package/addon/components/modals/issue-assign.js +49 -0
  33. package/addon/components/modals/issue-close.hbs +30 -0
  34. package/addon/components/modals/issue-close.js +73 -0
  35. package/addon/components/modals/issue-status.hbs +20 -0
  36. package/addon/components/modals/issue-status.js +43 -0
  37. package/addon/components/modals/reset-customer-credentials.js +12 -6
  38. package/addon/components/modals/update-order-activity.hbs +51 -17
  39. package/addon/components/modals/update-order-activity.js +59 -5
  40. package/addon/components/order/details/activity.js +2 -2
  41. package/addon/components/order/details/proof.hbs +40 -38
  42. package/addon/components/order/details/proof.js +31 -4
  43. package/addon/components/order/details.hbs +5 -5
  44. package/addon/components/order/details.js +19 -1
  45. package/addon/components/order/header.hbs +1 -1
  46. package/addon/components/route-list.hbs +0 -12
  47. package/addon/components/service-rate/details.hbs +1 -1
  48. package/addon/components/service-rate/form.hbs +1 -1
  49. package/addon/components/service-rate/form.js +4 -0
  50. package/addon/components/vendor/personnel-manager.hbs +166 -0
  51. package/addon/components/vendor/personnel-manager.js +143 -0
  52. package/addon/components/widget/live-fleet.hbs +1 -0
  53. package/addon/components/widget/live-fleet.js +2 -7
  54. package/addon/components/widget/revenue-trend.hbs +1 -1
  55. package/addon/components/widget/revenue-trend.js +75 -2
  56. package/addon/controllers/management/contacts/customers/details.js +31 -13
  57. package/addon/controllers/management/contacts/customers.js +16 -5
  58. package/addon/controllers/management/contacts/index/details.js +31 -13
  59. package/addon/controllers/management/contacts/index.js +7 -0
  60. package/addon/controllers/management/issues/index/details.js +52 -6
  61. package/addon/controllers/management/vendors/index/details.js +4 -0
  62. package/addon/controllers/operations/orders/index/details.js +11 -3
  63. package/addon/controllers/operations/orders/index.js +1 -1
  64. package/addon/extension.js +19 -12
  65. package/addon/helpers/format-duration.js +1 -38
  66. package/addon/initializers/load-leaflet-assets.js +5 -6
  67. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  68. package/addon/routes/management/issues/index/details.js +1 -1
  69. package/addon/routes.js +1 -0
  70. package/addon/services/contact-actions.js +231 -1
  71. package/addon/services/getting-started.js +44 -0
  72. package/addon/services/issue-actions.js +71 -0
  73. package/addon/services/leaflet-map-manager.js +2 -0
  74. package/addon/services/map-adapter/leaflet.js +19 -0
  75. package/addon/services/service-areas.js +3 -0
  76. package/addon/styles/fleetops-engine.css +1036 -0
  77. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  78. package/addon/templates/management/contacts/index/details.hbs +1 -1
  79. package/addon/templates/management/contacts.hbs +2 -2
  80. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  81. package/addon/templates/management/issues/index/details.hbs +3 -2
  82. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  83. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  84. package/addon/templates/operations/orders/index.hbs +1 -1
  85. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  86. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  87. package/addon/templates/settings/payments/onboard.hbs +8 -8
  88. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  89. package/addon/utils/leaflet-plugin-loader.js +219 -0
  90. package/addon/utils/setup-customer-portal.js +4 -64
  91. package/app/components/home/getting-started-guidance.js +1 -0
  92. package/app/components/issue/details/comments.js +1 -0
  93. package/app/components/issue/details/correspondence.js +1 -0
  94. package/app/components/issue/details/documents.js +1 -0
  95. package/app/components/issue/panel-header.js +1 -0
  96. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  97. package/app/components/modals/issue-assign.js +1 -0
  98. package/app/components/modals/issue-close.js +1 -0
  99. package/app/components/modals/issue-status.js +1 -0
  100. package/app/components/vendor/personnel-manager.js +1 -0
  101. package/app/helpers/format-duration.js +1 -1
  102. package/app/services/getting-started.js +1 -0
  103. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  104. package/app/utils/leaflet-plugin-loader.js +1 -0
  105. package/composer.json +1 -1
  106. package/extension.json +1 -1
  107. package/package.json +5 -5
  108. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  109. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  110. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  111. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  112. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  113. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  114. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  115. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  116. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -25
  117. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  118. package/server/src/Http/Controllers/Internal/v1/ContactController.php +135 -0
  119. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  120. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  121. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  122. package/server/src/Http/Controllers/Internal/v1/OrderController.php +172 -97
  123. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  124. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  125. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  126. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  127. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  128. package/server/src/Http/Resources/v1/Issue.php +25 -0
  129. package/server/src/Http/Resources/v1/Order.php +20 -18
  130. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  131. package/server/src/Models/Contact.php +30 -51
  132. package/server/src/Models/Issue.php +47 -6
  133. package/server/src/Models/Order.php +33 -6
  134. package/server/src/Models/ServiceArea.php +1 -1
  135. package/server/src/Models/VendorPersonnel.php +10 -1
  136. package/server/src/Models/Zone.php +1 -1
  137. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  138. package/server/src/Observers/ZoneObserver.php +56 -0
  139. package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
  140. package/server/src/Support/Analytics/TopDrivers.php +9 -1
  141. package/server/src/Support/GettingStarted.php +144 -0
  142. package/server/src/Support/PlaceSearch.php +282 -0
  143. package/server/src/Support/ResolvesOrderServiceStops.php +22 -12
  144. package/server/src/routes.php +33 -1
  145. package/server/tests/AnalyticsRoutesTest.php +8 -0
  146. package/server/tests/CustomerContactIdentitySafetyTest.php +41 -0
  147. package/server/tests/DriverVehicleStatusDefaultTest.php +2 -2
  148. package/server/tests/GettingStartedStatusTest.php +21 -0
  149. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  150. package/server/tests/ServiceAreaZoneApiShapeTest.php +44 -0
  151. package/translations/ar-ae.yml +9 -0
  152. package/translations/bg-bg.yaml +9 -0
  153. package/translations/en-us.yaml +11 -0
  154. package/translations/es-pa.yaml +9 -0
  155. package/translations/fr-fr.yaml +9 -0
  156. package/translations/mn-mn.yaml +9 -0
  157. package/translations/pt-br.yaml +9 -0
  158. package/translations/ru-ru.yaml +9 -0
  159. package/translations/vi-vn.yaml +9 -0
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\FleetOps\Http\Resources\v1;
4
4
 
5
5
  use Fleetbase\FleetOps\Support\Utils;
6
+ use Fleetbase\Http\Resources\File;
6
7
  use Fleetbase\Http\Resources\FleetbaseResource;
7
8
  use Fleetbase\Http\Resources\User;
8
9
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
@@ -26,12 +27,15 @@ class Issue extends FleetbaseResource
26
27
  'driver_uuid' => $this->when(Http::isInternalRequest(), $this->driver_uuid),
27
28
  'company_uuid' => $this->when(Http::isInternalRequest(), $this->company_uuid),
28
29
  'vehicle_uuid' => $this->when(Http::isInternalRequest(), $this->vehicle_uuid),
30
+ 'order_uuid' => $this->when(Http::isInternalRequest(), $this->order_uuid ?? data_get($this, 'meta.order_uuid')),
29
31
  'assigned_to_uuid' => $this->when(Http::isInternalRequest(), $this->assigned_to_uuid),
30
32
  'reported_by_uuid' => $this->when(Http::isInternalRequest(), $this->reported_by_uuid),
31
33
  'assignee' => $this->whenLoaded('assignee', fn () => new User($this->assignee)),
32
34
  'reporter' => $this->whenLoaded('reporter', fn () => new User($this->reporter)),
33
35
  'vehicle' => $this->whenLoaded('vehicle', fn () => new Vehicle($this->vehicle)),
34
36
  'driver' => $this->whenLoaded('driver', fn () => new Driver($this->driver)),
37
+ 'order' => $this->when(Http::isInternalRequest(), fn () => $this->resolveLinkedOrder()),
38
+ 'files' => $this->whenLoaded('files', fn () => File::collection($this->files)),
35
39
  'driver_name' => $this->driver_name,
36
40
  'vehicle_name' => $this->vehicle_name,
37
41
  'vehicle_id' => $this->vehicle_id,
@@ -55,6 +59,27 @@ class Issue extends FleetbaseResource
55
59
  ]);
56
60
  }
57
61
 
62
+ protected function resolveLinkedOrder()
63
+ {
64
+ if ($this->relationLoaded('order') && $this->order) {
65
+ $this->order->loadMissing(['payload.pickup', 'payload.dropoff', 'trackingNumber', 'customer']);
66
+
67
+ return new Order($this->order);
68
+ }
69
+
70
+ $orderUuid = $this->order_uuid ?? data_get($this, 'meta.order_uuid');
71
+ if (!$orderUuid) {
72
+ return null;
73
+ }
74
+
75
+ $order = \Fleetbase\FleetOps\Models\Order::with(['payload.pickup', 'payload.dropoff', 'trackingNumber', 'customer'])
76
+ ->where('uuid', $orderUuid)
77
+ ->where('company_uuid', $this->company_uuid)
78
+ ->first();
79
+
80
+ return $order ? new Order($order) : null;
81
+ }
82
+
58
83
  /**
59
84
  * Transform the resource into an webhook payload.
60
85
  *
@@ -73,24 +73,26 @@ class Order extends FleetbaseResource
73
73
  'tracking_statuses' => $this->whenLoaded('trackingStatuses', function () {
74
74
  return TrackingStatus::collection($this->trackingStatuses);
75
75
  }),
76
- 'tracking' => $this->when($isInternal, $this->trackingNumber ? $this->trackingNumber->tracking_number : null),
77
- 'barcode' => $this->when($isInternal, $this->trackingNumber ? $this->trackingNumber->barcode : null),
78
- 'qr_code' => $this->when($isInternal, $this->trackingNumber ? $this->trackingNumber->qr_code : null),
79
- 'comments' => $this->when($isInternal, Comment::collection($this->comments)),
80
- 'files' => $this->when($isInternal, $this->files, File::collection($this->files)),
81
- 'purchase_rate' => new PurchaseRate($this->purchaseRate),
82
- 'notes' => $this->notes,
83
- 'type' => $this->type,
84
- 'status' => $this->status,
85
- 'pod_method' => $this->pod_method,
86
- 'pod_required' => (bool) data_get($this, 'pod_required', false),
87
- 'dispatched' => (bool) data_get($this, 'dispatched', false),
88
- 'started' => (bool) data_get($this, 'started', false),
89
- 'adhoc' => (bool) data_get($this, 'adhoc', false),
90
- 'adhoc_distance' => (int) $this->getAdhocDistance(),
91
- 'distance' => (int) $this->distance,
92
- 'time' => (int) $this->time,
93
- 'tracker_data' => $this->when($withTrackerData, function () use ($tracker) {
76
+ 'tracking' => $this->when($isInternal, $this->trackingNumber ? $this->trackingNumber->tracking_number : null),
77
+ 'barcode' => $this->when($isInternal, $this->trackingNumber ? $this->trackingNumber->barcode : null),
78
+ 'qr_code' => $this->when($isInternal, $this->trackingNumber ? $this->trackingNumber->qr_code : null),
79
+ 'comments' => $this->when($isInternal, Comment::collection($this->comments)),
80
+ 'files' => $this->when($isInternal, $this->files, File::collection($this->files)),
81
+ 'purchase_rate' => new PurchaseRate($this->purchaseRate),
82
+ 'notes' => $this->notes,
83
+ 'type' => $this->type,
84
+ 'status' => $this->status,
85
+ 'pod_method' => $this->pod_method,
86
+ 'pod_required' => (bool) data_get($this, 'pod_required', false),
87
+ 'dispatched' => (bool) data_get($this, 'dispatched', false),
88
+ 'started' => (bool) data_get($this, 'started', false),
89
+ 'adhoc' => (bool) data_get($this, 'adhoc', false),
90
+ 'adhoc_distance' => (int) $this->getAdhocDistance(),
91
+ 'distance' => (int) $this->distance,
92
+ 'time' => (int) $this->time,
93
+ 'transaction_amount' => $this->transaction_amount,
94
+ 'currency' => $this->transaction_currency,
95
+ 'tracker_data' => $this->when($withTrackerData, function () use ($tracker) {
94
96
  return $this->resource->tracker_data ?? ($tracker ? $tracker->toArray() : null);
95
97
  }),
96
98
  'eta' => $this->when($withEta, function () use ($tracker) {
@@ -45,7 +45,7 @@ class CustomerCredentialsMail extends Mailable
45
45
  $this->customer->loadMissing('company');
46
46
 
47
47
  return new Envelope(
48
- subject: 'Your login credentials for ' . $this->customer->company->name . ' on ' . config('app.name'),
48
+ subject: 'Your ' . $this->customer->company->name . ' customer portal access is ready',
49
49
  );
50
50
  }
51
51
 
@@ -63,19 +63,18 @@ class CustomerCredentialsMail extends Mailable
63
63
  'customer' => $this->customer,
64
64
  'plaintextPassword' => $this->plaintextPassword,
65
65
  'customerPortalUrl' => $this->getCustomerPortalAccessUrl(),
66
- 'currentHour' => now()->hour,
67
66
  ]
68
67
  );
69
68
  }
70
69
 
71
70
  /**
72
- * Get the customer portal URL if available.
71
+ * Get the customer portal URL.
73
72
  */
74
- private function getCustomerPortalAccessUrl(): ?string
73
+ private function getCustomerPortalAccessUrl(): string
75
74
  {
76
75
  $customerPortalConfig = Setting::lookupFromCompany('customer-portal-config');
77
- $accessUrlSlug = data_get($customerPortalConfig, 'accessUrlSlug');
76
+ $accessUrlSlug = data_get($customerPortalConfig, 'accessUrlSlug', 'customer-portal');
78
77
 
79
- return $accessUrlSlug ? Utils::consoleUrl($accessUrlSlug) : null;
78
+ return Utils::consoleUrl($accessUrlSlug ?: 'customer-portal');
80
79
  }
81
80
  }
@@ -7,10 +7,8 @@ use Fleetbase\FleetOps\Exceptions\CustomerUserConflictException;
7
7
  use Fleetbase\FleetOps\Exceptions\UserAlreadyExistsException;
8
8
  use Fleetbase\FleetOps\Support\Utils;
9
9
  use Fleetbase\Models\CompanyUser;
10
- use Fleetbase\Models\Invite;
11
10
  use Fleetbase\Models\Model;
12
11
  use Fleetbase\Models\User;
13
- use Fleetbase\Notifications\UserInvited;
14
12
  use Fleetbase\Traits\HasApiModelBehavior;
15
13
  use Fleetbase\Traits\HasApiModelCache;
16
14
  use Fleetbase\Traits\HasCustomFields;
@@ -301,15 +299,15 @@ class Contact extends Model
301
299
  }
302
300
 
303
301
  /**
304
- * Creates a new user from the given contact and optionally sends an invitation.
302
+ * Creates a new user from the given contact without sending an organization invitation.
305
303
  *
306
304
  * This method first checks if a user with the same email or phone number as the contact already exists.
307
305
  * If such a user exists, it throws a UserAlreadyExistsException. Otherwise, it proceeds to create a new
308
306
  * user record using the contact's details, assigns the user to the contact's company, and links the
309
- * user to the contact. Optionally, it sends an invitation to the newly created user via email.
307
+ * user to the contact.
310
308
  *
311
309
  * @param Contact $contact the contact instance from which the user should be created
312
- * @param bool $sendInvite (optional) Whether to send an invitation to the newly created user. Default is false.
310
+ * @param bool $sendInvite Deprecated and ignored. Contact and customer users do not receive organization invitations.
313
311
  *
314
312
  * @return User the newly created user instance
315
313
  *
@@ -379,8 +377,8 @@ class Contact extends Model
379
377
  // Set user type
380
378
  $user->setType($contact->type);
381
379
 
382
- // Assing to company
383
- $user->assignCompany($contact->company, $contact->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
380
+ // Assign to company without triggering core organization invitations.
381
+ static::assignUserToContactCompany($contact, $user);
384
382
 
385
383
  // Assign customer role
386
384
  if ($contact->isCustomer()) {
@@ -393,23 +391,6 @@ class Contact extends Model
393
391
  $contact->update(['user_uuid' => $user->uuid]);
394
392
  }
395
393
 
396
- // Optionally, send invite
397
- if ($sendInvite) {
398
- // send invitation to user
399
- $invitation = Invite::create([
400
- 'company_uuid' => $user->company_uuid,
401
- 'created_by_uuid' => session('user'),
402
- 'subject_uuid' => $user->company_uuid,
403
- 'subject_type' => Utils::getMutationType('company'),
404
- 'protocol' => 'email',
405
- 'recipients' => [$user->email],
406
- 'reason' => 'join_company',
407
- ]);
408
-
409
- // notify user
410
- $user->notify(new UserInvited($invitation));
411
- }
412
-
413
394
  $contact->setRelation('user', $user);
414
395
 
415
396
  return $user;
@@ -491,21 +472,20 @@ class Contact extends Model
491
472
  }
492
473
 
493
474
  /**
494
- * Assigns a user to the company and optionally sends an invitation email.
475
+ * Assigns a user to the contact's company without sending an organization invitation.
495
476
  *
496
477
  * This method performs the following actions:
497
478
  * 1. Associates the provided user with the current company, assigning them a role based on the
498
479
  * company's type (e.g., 'Fleet-Ops Customer' or 'Fleet-Ops Contact').
499
480
  * 2. If the user is of type 'customer', assigns the 'Fleet-Ops Customer' role specifically.
500
481
  * 3. Updates the current entity's `user_uuid` to reference the assigned user.
501
- * 4. Optionally sends an invitation email to the user, inviting them to join the company.
502
482
  *
503
483
  * @param User $user the user to be assigned to the company
504
- * @param bool $sendInvite Determines whether to send an invitation email to the user. Defaults to false.
484
+ * @param bool $sendInvite Deprecated and ignored. Contact and customer users do not receive organization invitations.
505
485
  *
506
486
  * @return self returns the current instance to allow method chaining
507
487
  *
508
- * @throws \Exception if an error occurs during user assignment or while sending the invitation
488
+ * @throws \Exception if an error occurs during user assignment
509
489
  */
510
490
  public function assignUser(User $user, bool $sendInvite = false): self
511
491
  {
@@ -525,8 +505,8 @@ class Contact extends Model
525
505
  }
526
506
  }
527
507
 
528
- // Assing to company
529
- $user->assignCompany($this->company, $this->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
508
+ // Assign to company without triggering core organization invitations.
509
+ static::assignUserToContactCompany($this, $user);
530
510
 
531
511
  // Get the company user instance
532
512
  $companyUser = $user->getCompanyUser($this->company);
@@ -538,23 +518,6 @@ class Contact extends Model
538
518
  // Set user to contact
539
519
  $this->update(['user_uuid' => $user->uuid]);
540
520
 
541
- // Optionally, send invite
542
- if ($sendInvite) {
543
- // send invitation to user
544
- $invitation = Invite::create([
545
- 'company_uuid' => $user->company_uuid,
546
- 'created_by_uuid' => session('user'),
547
- 'subject_uuid' => $user->company_uuid,
548
- 'subject_type' => Utils::getMutationType('company'),
549
- 'protocol' => 'email',
550
- 'recipients' => [$user->email],
551
- 'reason' => 'join_company',
552
- ]);
553
-
554
- // notify user
555
- $user->notify(new UserInvited($invitation));
556
- }
557
-
558
521
  $this->setRelation('user', $user);
559
522
 
560
523
  return $this;
@@ -657,12 +620,11 @@ class Contact extends Model
657
620
  }
658
621
 
659
622
  /**
660
- * Creates a new user from the current contact instance and optionally sends an invitation.
623
+ * Creates a new user from the current contact instance without sending an organization invitation.
661
624
  *
662
- * This method is a wrapper around the createUserFromContact method. It allows creating a user directly
663
- * from a contact instance and, optionally, sending an invitation to the newly created user.
625
+ * This method is a wrapper around the createUserFromContact method.
664
626
  *
665
- * @param bool $sendInvite (optional) Whether to send an invitation to the newly created user. Default is false.
627
+ * @param bool $sendInvite Deprecated and ignored. Contact and customer users do not receive organization invitations.
666
628
  *
667
629
  * @return User the newly created user instance
668
630
  */
@@ -709,4 +671,21 @@ class Contact extends Model
709
671
  {
710
672
  return !$this->hasUser();
711
673
  }
674
+
675
+ private static function assignUserToContactCompany(Contact $contact, User $user): ?CompanyUser
676
+ {
677
+ $contact->loadMissing('company');
678
+
679
+ if (!$contact->company) {
680
+ return null;
681
+ }
682
+
683
+ $role = $contact->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact';
684
+ $companyUser = $contact->company->addUser($user, $role);
685
+
686
+ $user->forceFill(['company_uuid' => $contact->company->uuid])->saveQuietly();
687
+ $user->setRelation('companyUser', $companyUser);
688
+
689
+ return $companyUser;
690
+ }
712
691
  }
@@ -14,7 +14,11 @@ use Fleetbase\Traits\HasCustomFields;
14
14
  use Fleetbase\Traits\HasPublicId;
15
15
  use Fleetbase\Traits\HasUuid;
16
16
  use Fleetbase\Traits\TracksApiCredential;
17
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
18
+ use Illuminate\Database\Eloquent\Relations\HasMany;
17
19
  use Illuminate\Support\Str;
20
+ use Spatie\Activitylog\LogOptions;
21
+ use Spatie\Activitylog\Traits\LogsActivity;
18
22
 
19
23
  class Issue extends Model
20
24
  {
@@ -24,6 +28,7 @@ class Issue extends Model
24
28
  use SpatialTrait;
25
29
  use HasApiModelBehavior;
26
30
  use HasApiModelCache;
31
+ use LogsActivity;
27
32
  use HasCustomFields;
28
33
 
29
34
  /**
@@ -60,6 +65,7 @@ class Issue extends Model
60
65
  'assigned_to_uuid',
61
66
  'vehicle_uuid',
62
67
  'driver_uuid',
68
+ 'order_uuid',
63
69
  'issue_id',
64
70
  'location',
65
71
  'category',
@@ -106,6 +112,31 @@ class Issue extends Model
106
112
  */
107
113
  protected $appends = ['driver_name', 'vehicle_name', 'assignee_name', 'reporter_name'];
108
114
 
115
+ /**
116
+ * Get the activity log options for the model.
117
+ */
118
+ public function getActivitylogOptions(): LogOptions
119
+ {
120
+ return LogOptions::defaults()
121
+ ->logOnly([
122
+ 'title',
123
+ 'report',
124
+ 'status',
125
+ 'priority',
126
+ 'assigned_to_uuid',
127
+ 'vehicle_uuid',
128
+ 'driver_uuid',
129
+ 'order_uuid',
130
+ 'type',
131
+ 'category',
132
+ 'tags',
133
+ 'resolved_at',
134
+ 'meta',
135
+ ])
136
+ ->logOnlyDirty()
137
+ ->dontSubmitEmptyLogs();
138
+ }
139
+
109
140
  /**
110
141
  * The attributes excluded from the model's JSON form.
111
142
  *
@@ -114,7 +145,7 @@ class Issue extends Model
114
145
  protected $hidden = [];
115
146
 
116
147
  /**
117
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
148
+ * @return BelongsTo
118
149
  */
119
150
  public function reportedBy()
120
151
  {
@@ -122,7 +153,7 @@ class Issue extends Model
122
153
  }
123
154
 
124
155
  /**
125
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
156
+ * @return BelongsTo
126
157
  */
127
158
  public function reporter()
128
159
  {
@@ -130,7 +161,7 @@ class Issue extends Model
130
161
  }
131
162
 
132
163
  /**
133
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
164
+ * @return BelongsTo
134
165
  */
135
166
  public function assignedTo()
136
167
  {
@@ -138,7 +169,7 @@ class Issue extends Model
138
169
  }
139
170
 
140
171
  /**
141
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
172
+ * @return BelongsTo
142
173
  */
143
174
  public function assignee()
144
175
  {
@@ -146,7 +177,7 @@ class Issue extends Model
146
177
  }
147
178
 
148
179
  /**
149
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
180
+ * @return BelongsTo
150
181
  */
151
182
  public function vehicle()
152
183
  {
@@ -154,13 +185,23 @@ class Issue extends Model
154
185
  }
155
186
 
156
187
  /**
157
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
188
+ * @return BelongsTo
158
189
  */
159
190
  public function driver()
160
191
  {
161
192
  return $this->belongsTo(Driver::class);
162
193
  }
163
194
 
195
+ public function order(): BelongsTo
196
+ {
197
+ return $this->belongsTo(Order::class, 'order_uuid', 'uuid')->without(['comments', 'files']);
198
+ }
199
+
200
+ public function files(): HasMany
201
+ {
202
+ return $this->hasMany(\Fleetbase\Models\File::class, 'subject_uuid', 'uuid')->latest();
203
+ }
204
+
164
205
  /**
165
206
  * Set the title attribute for the Issue model.
166
207
  *
@@ -169,6 +169,7 @@ class Order extends Model
169
169
  'tracking',
170
170
  'total_entities',
171
171
  'transaction_amount',
172
+ 'transaction_currency',
172
173
  'customer_name',
173
174
  'customer_phone',
174
175
  'facilitator_name',
@@ -508,6 +509,16 @@ class Order extends Model
508
509
  return data_get($this, 'transaction.amount');
509
510
  }
510
511
 
512
+ /**
513
+ * The transaction amount for the order.
514
+ *
515
+ * @return string
516
+ */
517
+ public function getTransactionCurrencyAttribute()
518
+ {
519
+ return data_get($this, 'transaction.currency');
520
+ }
521
+
511
522
  /**
512
523
  * The customer name for the order.
513
524
  *
@@ -1143,7 +1154,17 @@ class Order extends Model
1143
1154
  $this->setRelation('purchaseRate', $purchaseRate);
1144
1155
 
1145
1156
  if ($purchaseRate->transaction_uuid) {
1146
- $this->setRelation('transaction', Transaction::where('uuid', $purchaseRate->transaction_uuid)->first());
1157
+ $transaction = Transaction::where('uuid', $purchaseRate->transaction_uuid)->first();
1158
+ if ($transaction instanceof Transaction) {
1159
+ $transaction->update([
1160
+ 'subject_uuid' => $this->uuid,
1161
+ 'subject_type' => static::class,
1162
+ 'context_uuid' => $purchaseRate->uuid,
1163
+ 'context_type' => PurchaseRate::class,
1164
+ 'direction' => Transaction::DIRECTION_CREDIT,
1165
+ ]);
1166
+ $this->setRelation('transaction', $transaction);
1167
+ }
1147
1168
  }
1148
1169
 
1149
1170
  if (
@@ -1176,12 +1197,17 @@ class Order extends Model
1176
1197
  'company_uuid' => session('company', $this->company_uuid),
1177
1198
  'customer_uuid' => $this->customer_uuid,
1178
1199
  'customer_type' => $this->customer_type,
1200
+ 'subject_uuid' => $this->uuid,
1201
+ 'subject_type' => static::class,
1202
+ 'context_uuid' => $this->uuid,
1203
+ 'context_type' => static::class,
1179
1204
  'gateway_transaction_id' => Transaction::generateNumber(),
1180
1205
  'gateway' => 'internal',
1181
1206
  'amount' => 0,
1182
1207
  'currency' => Utils::getCompanyTransactionCurrency($this->company ?? $this->company_uuid),
1183
1208
  'description' => 'Dispatch order',
1184
1209
  'type' => 'dispatch',
1210
+ 'direction' => Transaction::DIRECTION_CREDIT,
1185
1211
  'status' => 'success',
1186
1212
  ]);
1187
1213
 
@@ -1379,8 +1405,9 @@ class Order extends Model
1379
1405
  *
1380
1406
  * The process involves the following steps:
1381
1407
  * 1. Creating a new Activity instance with the 'completed' code and relevant details.
1382
- * 2. Notifying that the order has been completed via `notifyCompleted`.
1383
- * 3. Updating the order's activity with the new 'completed' activity through `updateActivity`.
1408
+ * 2. Updating the order's activity with the new 'completed' activity through `updateActivity`,
1409
+ * which applies and persists the 'completed' status.
1410
+ * 3. Notifying that the order has been completed via `notifyCompleted`.
1384
1411
  *
1385
1412
  * @param Proof|null $proof Optional. Additional proof or details for the activity update.
1386
1413
  *
@@ -1388,15 +1415,15 @@ class Order extends Model
1388
1415
  */
1389
1416
  public function complete(?Proof $proof = null): self
1390
1417
  {
1391
- $this->notifyCompleted();
1392
-
1393
1418
  $doesntHaveCompletedActivity = TrackingStatus::where(['tracking_number_uuid' => $this->tracking_number_uuid, 'code' => 'COMPLETED'])->doesntExist();
1394
1419
  if ($doesntHaveCompletedActivity) {
1395
1420
  $activity = $this->config()->getCompletedActivity();
1396
1421
 
1397
- return $this->updateActivity($activity, $proof);
1422
+ $this->updateActivity($activity, $proof);
1398
1423
  }
1399
1424
 
1425
+ $this->notifyCompleted();
1426
+
1400
1427
  return $this;
1401
1428
  }
1402
1429
 
@@ -123,7 +123,7 @@ class ServiceArea extends Model
123
123
  */
124
124
  public function zones()
125
125
  {
126
- return $this->hasMany(Zone::class)->without(['serviceArea']);
126
+ return $this->hasMany(Zone::class, 'service_area_uuid', 'uuid')->without(['serviceArea']);
127
127
  }
128
128
 
129
129
  /**
@@ -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
  }
@@ -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
  /**
@@ -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
  }