@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.
- package/README.md +185 -67
- package/addon/components/contact/panel-header.hbs +11 -48
- package/addon/components/customer/details.hbs +1 -2
- package/addon/components/customer/form.hbs +63 -51
- package/addon/components/customer/form.js +27 -1
- package/addon/components/customer/orders.hbs +48 -0
- package/addon/components/customer/orders.js +67 -5
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/home/getting-started-guidance.hbs +113 -0
- package/addon/components/home/getting-started-guidance.js +107 -0
- package/addon/components/issue/details/comments.hbs +3 -0
- package/addon/components/issue/details/comments.js +3 -0
- package/addon/components/issue/details/correspondence.hbs +9 -0
- package/addon/components/issue/details/correspondence.js +83 -0
- package/addon/components/issue/details/documents.hbs +69 -0
- package/addon/components/issue/details/documents.js +85 -0
- package/addon/components/issue/details.hbs +220 -59
- package/addon/components/issue/details.js +245 -0
- package/addon/components/issue/form.hbs +19 -1
- package/addon/components/issue/panel-header.hbs +43 -0
- package/addon/components/issue/panel-header.js +22 -0
- package/addon/components/issue/timeline.hbs +40 -0
- package/addon/components/issue/timeline.js +45 -0
- package/addon/components/leaflet-tracking-marker.js +3 -0
- package/addon/components/map/drawer/position-listing.js +3 -0
- package/addon/components/map/leaflet-live-map.hbs +2 -2
- package/addon/components/map/leaflet-live-map.js +17 -0
- package/addon/components/map/order-list-overlay.hbs +4 -4
- package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
- package/addon/components/modals/convert-customer-to-vendor.js +55 -0
- package/addon/components/modals/issue-assign.hbs +20 -0
- package/addon/components/modals/issue-assign.js +49 -0
- package/addon/components/modals/issue-close.hbs +30 -0
- package/addon/components/modals/issue-close.js +73 -0
- package/addon/components/modals/issue-status.hbs +20 -0
- package/addon/components/modals/issue-status.js +43 -0
- package/addon/components/modals/reset-customer-credentials.js +12 -6
- package/addon/components/modals/update-order-activity.hbs +51 -17
- package/addon/components/modals/update-order-activity.js +59 -5
- package/addon/components/order/details/activity.js +2 -2
- package/addon/components/order/details/proof.hbs +40 -38
- package/addon/components/order/details/proof.js +31 -4
- package/addon/components/order/details.hbs +5 -5
- package/addon/components/order/details.js +19 -1
- package/addon/components/order/header.hbs +1 -1
- package/addon/components/route-list.hbs +0 -12
- package/addon/components/service-rate/details.hbs +1 -1
- package/addon/components/service-rate/form.hbs +1 -1
- package/addon/components/service-rate/form.js +4 -0
- package/addon/components/vendor/personnel-manager.hbs +166 -0
- package/addon/components/vendor/personnel-manager.js +143 -0
- package/addon/components/widget/live-fleet.hbs +1 -0
- package/addon/components/widget/live-fleet.js +2 -7
- package/addon/components/widget/revenue-trend.hbs +1 -1
- package/addon/components/widget/revenue-trend.js +75 -2
- package/addon/controllers/management/contacts/customers/details.js +31 -13
- package/addon/controllers/management/contacts/customers.js +16 -5
- package/addon/controllers/management/contacts/index/details.js +31 -13
- package/addon/controllers/management/contacts/index.js +7 -0
- package/addon/controllers/management/issues/index/details.js +52 -6
- package/addon/controllers/management/vendors/index/details.js +4 -0
- package/addon/controllers/operations/orders/index/details.js +11 -3
- package/addon/controllers/operations/orders/index.js +1 -1
- package/addon/extension.js +19 -12
- package/addon/helpers/format-duration.js +1 -38
- package/addon/initializers/load-leaflet-assets.js +5 -6
- package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
- package/addon/routes/management/issues/index/details.js +1 -1
- package/addon/routes.js +1 -0
- package/addon/services/contact-actions.js +231 -1
- package/addon/services/getting-started.js +44 -0
- package/addon/services/issue-actions.js +71 -0
- package/addon/services/leaflet-map-manager.js +2 -0
- package/addon/services/map-adapter/leaflet.js +19 -0
- package/addon/services/service-areas.js +3 -0
- package/addon/styles/fleetops-engine.css +1036 -0
- package/addon/templates/management/contacts/customers/details.hbs +1 -1
- package/addon/templates/management/contacts/index/details.hbs +1 -1
- package/addon/templates/management/contacts.hbs +2 -2
- package/addon/templates/management/issues/index/details/index.hbs +3 -1
- package/addon/templates/management/issues/index/details.hbs +3 -2
- package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
- package/addon/templates/operations/orders/index/details/index.hbs +2 -2
- package/addon/templates/operations/orders/index.hbs +1 -1
- package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
- package/addon/templates/operations/service-rates/index/new.hbs +1 -0
- package/addon/templates/settings/payments/onboard.hbs +8 -8
- package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
- package/addon/utils/leaflet-plugin-loader.js +219 -0
- package/addon/utils/setup-customer-portal.js +4 -64
- package/app/components/home/getting-started-guidance.js +1 -0
- package/app/components/issue/details/comments.js +1 -0
- package/app/components/issue/details/correspondence.js +1 -0
- package/app/components/issue/details/documents.js +1 -0
- package/app/components/issue/panel-header.js +1 -0
- package/app/components/modals/convert-customer-to-vendor.js +1 -0
- package/app/components/modals/issue-assign.js +1 -0
- package/app/components/modals/issue-close.js +1 -0
- package/app/components/modals/issue-status.js +1 -0
- package/app/components/vendor/personnel-manager.js +1 -0
- package/app/helpers/format-duration.js +1 -1
- package/app/services/getting-started.js +1 -0
- package/app/utils/leaflet-draw-namespace-guard.js +1 -0
- package/app/utils/leaflet-plugin-loader.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +5 -5
- package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
- package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
- package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
- package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
- package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
- package/server/resources/views/mail/customer-credentials.blade.php +16 -13
- package/server/src/Expansions/UserFilterExpansion.php +28 -0
- package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -25
- package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +135 -0
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
- package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +172 -97
- package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
- package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
- package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
- package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
- package/server/src/Http/Resources/v1/Index/Order.php +12 -9
- package/server/src/Http/Resources/v1/Issue.php +25 -0
- package/server/src/Http/Resources/v1/Order.php +20 -18
- package/server/src/Mail/CustomerCredentialsMail.php +5 -6
- package/server/src/Models/Contact.php +30 -51
- package/server/src/Models/Issue.php +47 -6
- package/server/src/Models/Order.php +33 -6
- package/server/src/Models/ServiceArea.php +1 -1
- package/server/src/Models/VendorPersonnel.php +10 -1
- package/server/src/Models/Zone.php +1 -1
- package/server/src/Observers/PurchaseRateObserver.php +20 -0
- package/server/src/Observers/ZoneObserver.php +56 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
- package/server/src/Support/Analytics/TopDrivers.php +9 -1
- package/server/src/Support/GettingStarted.php +144 -0
- package/server/src/Support/PlaceSearch.php +282 -0
- package/server/src/Support/ResolvesOrderServiceStops.php +22 -12
- package/server/src/routes.php +33 -1
- package/server/tests/AnalyticsRoutesTest.php +8 -0
- package/server/tests/CustomerContactIdentitySafetyTest.php +41 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +2 -2
- package/server/tests/GettingStartedStatusTest.php +21 -0
- package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +44 -0
- package/translations/ar-ae.yml +9 -0
- package/translations/bg-bg.yaml +9 -0
- package/translations/en-us.yaml +11 -0
- package/translations/es-pa.yaml +9 -0
- package/translations/fr-fr.yaml +9 -0
- package/translations/mn-mn.yaml +9 -0
- package/translations/pt-br.yaml +9 -0
- package/translations/ru-ru.yaml +9 -0
- package/translations/vi-vn.yaml +9 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration {
|
|
8
|
+
public function up(): void
|
|
9
|
+
{
|
|
10
|
+
Schema::table('issues', function (Blueprint $table) {
|
|
11
|
+
if (!Schema::hasColumn('issues', 'order_uuid')) {
|
|
12
|
+
$table->uuid('order_uuid')->nullable()->index()->after('vehicle_uuid');
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public function down(): void
|
|
18
|
+
{
|
|
19
|
+
Schema::table('issues', function (Blueprint $table) {
|
|
20
|
+
if (Schema::hasColumn('issues', 'order_uuid')) {
|
|
21
|
+
$table->dropColumn('order_uuid');
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
4
|
+
use Fleetbase\FleetOps\Models\PurchaseRate;
|
|
5
|
+
use Illuminate\Database\Migrations\Migration;
|
|
6
|
+
use Illuminate\Support\Facades\DB;
|
|
7
|
+
|
|
8
|
+
return new class extends Migration {
|
|
9
|
+
public function up(): void
|
|
10
|
+
{
|
|
11
|
+
DB::table('orders')
|
|
12
|
+
->whereNotNull('transaction_uuid')
|
|
13
|
+
->orderBy('id')
|
|
14
|
+
->select(['id', 'uuid', 'transaction_uuid', 'purchase_rate_uuid'])
|
|
15
|
+
->chunkById(500, function ($orders) {
|
|
16
|
+
foreach ($orders as $order) {
|
|
17
|
+
$updates = [
|
|
18
|
+
'subject_uuid' => $order->uuid,
|
|
19
|
+
'subject_type' => Order::class,
|
|
20
|
+
'updated_at' => now(),
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
if ($order->purchase_rate_uuid) {
|
|
24
|
+
$updates['context_uuid'] = $order->purchase_rate_uuid;
|
|
25
|
+
$updates['context_type'] = PurchaseRate::class;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
DB::table('transactions')
|
|
29
|
+
->where('uuid', $order->transaction_uuid)
|
|
30
|
+
->update($updates);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public function down(): void
|
|
36
|
+
{
|
|
37
|
+
// Intentionally no-op: this migration corrects broken order transaction linkage.
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Support\Facades\DB;
|
|
5
|
+
|
|
6
|
+
return new class extends Migration {
|
|
7
|
+
/**
|
|
8
|
+
* Backfill null directions for transactions linked to FleetOps orders.
|
|
9
|
+
*/
|
|
10
|
+
public function up(): void
|
|
11
|
+
{
|
|
12
|
+
DB::table('transactions')
|
|
13
|
+
->join('orders', 'orders.transaction_uuid', '=', 'transactions.uuid')
|
|
14
|
+
->whereNull('transactions.direction')
|
|
15
|
+
->update(['transactions.direction' => 'credit']);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Direction backfills are not reversible without losing real data.
|
|
20
|
+
*/
|
|
21
|
+
public function down(): void
|
|
22
|
+
{
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
<x-mail-layout>
|
|
2
2
|
<h2 style="font-size: 18px; font-weight: 600;">
|
|
3
|
-
|
|
4
|
-
Good Morning, {{ $customer->name }}!
|
|
5
|
-
@elseif($currentHour < 18)
|
|
6
|
-
Good Afternoon, {{ $customer->name }}!
|
|
7
|
-
@else
|
|
8
|
-
Good Evening, {{ $customer->name }}!
|
|
9
|
-
@endif
|
|
3
|
+
Your customer portal access is ready
|
|
10
4
|
</h2>
|
|
11
5
|
|
|
12
|
-
|
|
6
|
+
Hi {{ $customer->name }},
|
|
13
7
|
<br />
|
|
14
8
|
<br />
|
|
15
|
-
|
|
9
|
+
{{ $customer->company->name }} has created customer portal access for you. You can use the portal to view orders, invoices, support requests, and account details when available.
|
|
16
10
|
<br />
|
|
17
|
-
Your Password: {{ $plaintextPassword }}
|
|
18
|
-
@if($customerPortalUrl)
|
|
19
11
|
<br />
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
<a href="{{ $customerPortalUrl }}" style="display:inline-block; background:#2563eb; color:#ffffff; text-decoration:none; border-radius:6px; padding:12px 18px; font-size:14px; line-height:20px; font-weight:700;">Sign in to customer portal</a>
|
|
13
|
+
<br />
|
|
14
|
+
<br />
|
|
15
|
+
<strong>Your sign-in details</strong>
|
|
16
|
+
<br />
|
|
17
|
+
Email: {{ $customer->user->email }}
|
|
18
|
+
<br />
|
|
19
|
+
Temporary password: {{ $plaintextPassword }}
|
|
20
|
+
<br />
|
|
21
|
+
Portal URL: {{ $customerPortalUrl }}
|
|
22
|
+
<br />
|
|
23
|
+
<br />
|
|
24
|
+
You can change your password after signing in.
|
|
22
25
|
|
|
23
26
|
</x-mail-layout>
|
|
@@ -47,6 +47,21 @@ class UserFilterExpansion implements Expansion
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Filter where a user is not a customer.
|
|
52
|
+
*
|
|
53
|
+
* @return void
|
|
54
|
+
*/
|
|
55
|
+
public static function isNotCustomer()
|
|
56
|
+
{
|
|
57
|
+
return function () {
|
|
58
|
+
/** @var \Fleetbase\Http\Filter\UserFilter|\Fleetbase\Http\Filter\Filter $this */
|
|
59
|
+
$this->builder->where(function ($query) {
|
|
60
|
+
$query->whereNull('type')->orWhere('type', '!=', 'customer');
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
/**
|
|
51
66
|
* Filter where doesnt have driver within the CURRENT organizations.
|
|
52
67
|
*
|
|
@@ -62,6 +77,19 @@ class UserFilterExpansion implements Expansion
|
|
|
62
77
|
};
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Filter where users can be driver.
|
|
82
|
+
*
|
|
83
|
+
* @return void
|
|
84
|
+
*/
|
|
85
|
+
public static function canBeDriver()
|
|
86
|
+
{
|
|
87
|
+
return function () {
|
|
88
|
+
/** @var \Fleetbase\Http\Filter\UserFilter|\Fleetbase\Http\Filter\Filter $this */
|
|
89
|
+
$this->builder->whereIn('type', ['user', 'admin']);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
65
93
|
/**
|
|
66
94
|
* @return void
|
|
67
95
|
*/
|
|
@@ -30,6 +30,7 @@ class IssueController extends Controller
|
|
|
30
30
|
'type',
|
|
31
31
|
'report',
|
|
32
32
|
'priority',
|
|
33
|
+
'tags',
|
|
33
34
|
'status',
|
|
34
35
|
]);
|
|
35
36
|
|
|
@@ -85,6 +86,7 @@ class IssueController extends Controller
|
|
|
85
86
|
'type',
|
|
86
87
|
'report',
|
|
87
88
|
'priority',
|
|
89
|
+
'tags',
|
|
88
90
|
'status',
|
|
89
91
|
]);
|
|
90
92
|
|
|
@@ -1013,11 +1013,10 @@ class OrderController extends Controller
|
|
|
1013
1013
|
$driver->current_job_uuid = $order->uuid;
|
|
1014
1014
|
$driver->save();
|
|
1015
1015
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
$payload->setFirstWaypoint($activity, $location);
|
|
1016
|
+
// set first service stop destination for multi-stop routes
|
|
1017
|
+
if ($this->payloadUsesServiceStopActivity($payload)) {
|
|
1018
|
+
$this->ensurePayloadCurrentServiceStop($payload);
|
|
1019
|
+
}
|
|
1021
1020
|
$order->setRelation('payload', $payload);
|
|
1022
1021
|
|
|
1023
1022
|
// update order activity
|
|
@@ -1108,37 +1107,35 @@ class OrderController extends Controller
|
|
|
1108
1107
|
/** @var \Fleetbase\LaravelMysqlSpatial\Types\Point */
|
|
1109
1108
|
$location = $order->getLastLocation();
|
|
1110
1109
|
|
|
1111
|
-
|
|
1112
|
-
$
|
|
1110
|
+
$usesServiceStopActivity = $this->payloadUsesServiceStopActivity($order->payload);
|
|
1111
|
+
$isLifecycleActivity = Utils::isActivity($activity) && in_array($activity->code, ['created', 'dispatched', 'started'], true);
|
|
1113
1112
|
|
|
1114
|
-
//
|
|
1115
|
-
|
|
1116
|
-
if (!$isMultipleWaypointOrder) {
|
|
1113
|
+
// Classic pickup/dropoff orders and static lifecycle statuses update the order.
|
|
1114
|
+
if (!$usesServiceStopActivity || $isLifecycleActivity) {
|
|
1117
1115
|
// Update order activity
|
|
1118
1116
|
$order->updateActivity($activity, $proof);
|
|
1119
1117
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1118
|
+
if (!$usesServiceStopActivity) {
|
|
1119
|
+
foreach ($order->payload->entities as $entity) {
|
|
1120
|
+
$entity->insertActivity($activity, $location, $proof);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if ($usesServiceStopActivity && $activity->is('started')) {
|
|
1125
|
+
$this->ensurePayloadCurrentServiceStop($order->payload);
|
|
1123
1126
|
}
|
|
1127
|
+
|
|
1124
1128
|
// Handle order completion
|
|
1125
1129
|
if (Utils::isActivity($activity) && $activity->completesOrder()) {
|
|
1126
1130
|
$order->driverAssigned?->unassignCurrentJob();
|
|
1127
1131
|
$order->complete($this->resolveProof($proof));
|
|
1128
1132
|
}
|
|
1129
1133
|
|
|
1130
|
-
return new OrderResource($order);
|
|
1134
|
+
return new OrderResource($order->refresh());
|
|
1131
1135
|
}
|
|
1132
1136
|
|
|
1133
|
-
if ($
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
$this->ensurePayloadCurrentServiceStop($order->payload);
|
|
1137
|
-
if (!$this->payloadHasCurrentServiceStopActivity($order->payload, $activity)) {
|
|
1138
|
-
$this->updateCurrentServiceStopActivity($order, $activity, $location, $proof);
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
return new OrderResource($order->refresh());
|
|
1137
|
+
if (!$order->started && in_array($order->status, ['created', 'dispatched'], true)) {
|
|
1138
|
+
return response()->apiError('Order must be started before waypoint activity can be updated.', 422);
|
|
1142
1139
|
}
|
|
1143
1140
|
|
|
1144
1141
|
$currentStop = $this->ensurePayloadCurrentServiceStop($order->payload);
|
|
@@ -1192,9 +1189,13 @@ class OrderController extends Controller
|
|
|
1192
1189
|
);
|
|
1193
1190
|
}
|
|
1194
1191
|
|
|
1192
|
+
$canUpdateServiceStopActivity = $this->payloadUsesServiceStopActivity($order->payload)
|
|
1193
|
+
&& ($order->started || $order->started_at || !in_array($order->status, ['created', 'dispatched'], true));
|
|
1195
1194
|
$stop = null;
|
|
1196
|
-
if ($
|
|
1197
|
-
$stop = $
|
|
1195
|
+
if ($canUpdateServiceStopActivity) {
|
|
1196
|
+
$stop = $waypointId
|
|
1197
|
+
? $this->resolveServiceStopFromKey($order->payload, $waypointId)
|
|
1198
|
+
: $this->payloadCurrentServiceStop($order->payload);
|
|
1198
1199
|
}
|
|
1199
1200
|
|
|
1200
1201
|
$orderConfig = $order->ensureOrderConfig();
|
|
@@ -7,15 +7,11 @@ use Fleetbase\FleetOps\Http\Requests\UpdatePlaceRequest;
|
|
|
7
7
|
use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource;
|
|
8
8
|
use Fleetbase\FleetOps\Http\Resources\v1\Place as PlaceResource;
|
|
9
9
|
use Fleetbase\FleetOps\Models\Place;
|
|
10
|
+
use Fleetbase\FleetOps\Support\PlaceSearch;
|
|
10
11
|
use Fleetbase\FleetOps\Support\Utils;
|
|
11
12
|
use Fleetbase\Http\Controllers\Controller;
|
|
12
13
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
13
|
-
use Geocoder\Laravel\Facades\Geocoder;
|
|
14
|
-
use Geocoder\Provider\GoogleMapsPlaces\GoogleMapsPlaces;
|
|
15
|
-
use Geocoder\Query\GeocodeQuery;
|
|
16
|
-
use Http\Adapter\Guzzle7\Client;
|
|
17
14
|
use Illuminate\Http\Request;
|
|
18
|
-
use Illuminate\Support\Facades\DB;
|
|
19
15
|
use Illuminate\Support\Str;
|
|
20
16
|
|
|
21
17
|
class PlaceController extends Controller
|
|
@@ -283,63 +279,19 @@ class PlaceController extends Controller
|
|
|
283
279
|
{
|
|
284
280
|
$searchQuery = strtolower($request->input('query'));
|
|
285
281
|
$limit = $request->input('limit', 10);
|
|
286
|
-
$geo = $request->
|
|
282
|
+
$geo = $request->boolean('geo');
|
|
287
283
|
$latitude = $request->input('latitude', false);
|
|
288
284
|
$longitude = $request->input('longitude', false);
|
|
289
285
|
|
|
290
|
-
$
|
|
291
|
-
->where('company_uuid', session('company'))
|
|
292
|
-
->whereNull('deleted_at')
|
|
293
|
-
->where(function ($q) use ($searchQuery) {
|
|
294
|
-
if (Utils::notEmpty($searchQuery)) {
|
|
295
|
-
$q->orWhere(DB::raw('lower(name)'), 'like', '%' . $searchQuery . '%');
|
|
296
|
-
$q->orWhere(DB::raw('lower(street1)'), 'like', '%' . $searchQuery . '%');
|
|
297
|
-
$q->orWhere(DB::raw('lower(postal_code)'), 'like', '%' . $searchQuery . '%');
|
|
298
|
-
}
|
|
299
|
-
})
|
|
300
|
-
->limit($limit)
|
|
301
|
-
->orderBy('name', 'desc')
|
|
302
|
-
->get()
|
|
303
|
-
->map(function ($place) {
|
|
304
|
-
$place = (array) $place;
|
|
305
|
-
$place['location'] = Utils::mysqlPointAsGeometry($place['location']);
|
|
306
|
-
$place = new Place($place);
|
|
307
|
-
$place->address = $place->toAddressString();
|
|
308
|
-
|
|
309
|
-
return $place;
|
|
310
|
-
})
|
|
311
|
-
->values();
|
|
312
|
-
|
|
313
|
-
if ($geo && Utils::notEmpty($searchQuery)) {
|
|
314
|
-
$httpClient = new Client();
|
|
315
|
-
$provider = new \Geocoder\Provider\GoogleMaps\GoogleMaps($httpClient, null, env('GOOGLE_MAPS_API_KEY'));
|
|
316
|
-
$geocoder = new \Geocoder\StatefulGeocoder($provider, 'en');
|
|
317
|
-
|
|
318
|
-
if ($latitude && $longitude) {
|
|
319
|
-
$geoResults = $geocoder->geocodeQuery(
|
|
320
|
-
GeocodeQuery::create($searchQuery)
|
|
321
|
-
->withData('mode', GoogleMapsPlaces::GEOCODE_MODE_SEARCH)
|
|
322
|
-
->withData('location', "$latitude, $longitude")
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
$geoResults = collect($geoResults->all());
|
|
326
|
-
} else {
|
|
327
|
-
$geoResults = Geocoder::geocode($searchQuery)->get();
|
|
328
|
-
}
|
|
286
|
+
$query = Place::where('company_uuid', session('company'))->whereNull('deleted_at');
|
|
329
287
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
$results = $results
|
|
340
|
-
->sortBy('name')
|
|
341
|
-
->values()
|
|
342
|
-
->toArray();
|
|
288
|
+
$results = PlaceSearch::search($query, $searchQuery, [
|
|
289
|
+
'limit' => $limit,
|
|
290
|
+
'geo' => $geo,
|
|
291
|
+
'latitude' => $latitude,
|
|
292
|
+
'longitude' => $longitude,
|
|
293
|
+
'no_query_order' => 'name_desc',
|
|
294
|
+
]);
|
|
343
295
|
|
|
344
296
|
return PlaceResource::collection($results);
|
|
345
297
|
}
|
|
@@ -4,12 +4,23 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Exports\ContactExport;
|
|
6
6
|
use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
|
|
7
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Vendor as VendorResource;
|
|
7
8
|
use Fleetbase\FleetOps\Imports\ContactImport;
|
|
9
|
+
use Fleetbase\FleetOps\Mail\CustomerCredentialsMail;
|
|
8
10
|
use Fleetbase\FleetOps\Models\Contact;
|
|
11
|
+
use Fleetbase\FleetOps\Models\Entity;
|
|
12
|
+
use Fleetbase\FleetOps\Models\Issue;
|
|
13
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
14
|
+
use Fleetbase\FleetOps\Models\PurchaseRate;
|
|
15
|
+
use Fleetbase\FleetOps\Models\Vendor;
|
|
16
|
+
use Fleetbase\FleetOps\Models\VendorPersonnel;
|
|
17
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
9
18
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
10
19
|
use Fleetbase\Http\Requests\ImportRequest;
|
|
11
20
|
use Fleetbase\Models\User;
|
|
12
21
|
use Illuminate\Http\Request;
|
|
22
|
+
use Illuminate\Support\Facades\DB;
|
|
23
|
+
use Illuminate\Support\Facades\Mail;
|
|
13
24
|
use Illuminate\Support\Str;
|
|
14
25
|
use Maatwebsite\Excel\Facades\Excel;
|
|
15
26
|
|
|
@@ -29,6 +40,7 @@ class ContactController extends FleetOpsController
|
|
|
29
40
|
{
|
|
30
41
|
$this->resolveUserInput($request, $input);
|
|
31
42
|
$this->assertCustomerIdentityIsAvailable($input);
|
|
43
|
+
$this->assertCustomerPortalCanSendWelcomeEmail($input);
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
/**
|
|
@@ -51,6 +63,7 @@ class ContactController extends FleetOpsController
|
|
|
51
63
|
{
|
|
52
64
|
if ($contact->type === 'customer') {
|
|
53
65
|
$contact->normalizeCustomerUser();
|
|
66
|
+
$this->sendCustomerPortalWelcomeEmail($contact);
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
$customFieldValues = $request->array('contact.custom_field_values');
|
|
@@ -95,6 +108,57 @@ class ContactController extends FleetOpsController
|
|
|
95
108
|
]);
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
public function convertToVendor(Request $request, string $id)
|
|
112
|
+
{
|
|
113
|
+
$contact = Contact::where('company_uuid', session('company'))
|
|
114
|
+
->where(function ($query) use ($id) {
|
|
115
|
+
$query->where('uuid', $id)->orWhere('public_id', $id)->orWhere('id', $id);
|
|
116
|
+
})
|
|
117
|
+
->firstOrFail();
|
|
118
|
+
|
|
119
|
+
$vendor = DB::transaction(function () use ($contact, $request) {
|
|
120
|
+
$originalType = $contact->type;
|
|
121
|
+
$vendor = Vendor::create([
|
|
122
|
+
'company_uuid' => $contact->company_uuid,
|
|
123
|
+
'place_uuid' => $contact->place_uuid,
|
|
124
|
+
'name' => $request->input('name', $contact->name),
|
|
125
|
+
'email' => $request->input('email', $contact->email),
|
|
126
|
+
'phone' => $request->input('phone', $contact->phone),
|
|
127
|
+
'status' => 'active',
|
|
128
|
+
'type' => 'customer',
|
|
129
|
+
'meta' => [
|
|
130
|
+
'converted_from_contact_uuid' => $contact->uuid,
|
|
131
|
+
'converted_from_contact_type' => $originalType,
|
|
132
|
+
'converted_by_uuid' => session('user'),
|
|
133
|
+
'converted_at' => now()->toISOString(),
|
|
134
|
+
],
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
VendorPersonnel::updateOrCreate(
|
|
138
|
+
['vendor_uuid' => $vendor->uuid, 'contact_uuid' => $contact->uuid],
|
|
139
|
+
['role' => 'admin', 'status' => 'active', 'invited_by_uuid' => session('user')]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
$this->migrateContactCustomerContextToVendor($contact, $vendor);
|
|
143
|
+
|
|
144
|
+
$contact->update([
|
|
145
|
+
'type' => 'customer',
|
|
146
|
+
'meta' => array_merge((array) $contact->meta, [
|
|
147
|
+
'converted_from_type' => $originalType,
|
|
148
|
+
'converted_to_vendor_uuid' => $vendor->uuid,
|
|
149
|
+
'converted_to_vendor_public_id' => $vendor->public_id,
|
|
150
|
+
'converted_to_vendor_at' => now()->toISOString(),
|
|
151
|
+
]),
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
return $vendor;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return response()->json([
|
|
158
|
+
'vendor' => (new VendorResource($vendor->load('personnels')))->resolve(),
|
|
159
|
+
]);
|
|
160
|
+
}
|
|
161
|
+
|
|
98
162
|
/**
|
|
99
163
|
* Export the contacts to excel or csv.
|
|
100
164
|
*
|
|
@@ -133,6 +197,29 @@ class ContactController extends FleetOpsController
|
|
|
133
197
|
return response()->json(['status' => 'ok', 'message' => 'Import completed', 'imported' => $importedCount]);
|
|
134
198
|
}
|
|
135
199
|
|
|
200
|
+
private function migrateContactCustomerContextToVendor(Contact $contact, Vendor $vendor): void
|
|
201
|
+
{
|
|
202
|
+
$contactType = Utils::getMutationType($contact);
|
|
203
|
+
$vendorType = Utils::getMutationType($vendor);
|
|
204
|
+
$filter = ['customer_uuid' => $contact->uuid, 'customer_type' => $contactType];
|
|
205
|
+
$replacement = ['customer_uuid' => $vendor->uuid, 'customer_type' => $vendorType];
|
|
206
|
+
|
|
207
|
+
Order::where($filter)->update($replacement);
|
|
208
|
+
PurchaseRate::where($filter)->update($replacement);
|
|
209
|
+
Entity::where($filter)->update($replacement);
|
|
210
|
+
|
|
211
|
+
Issue::where('company_uuid', $contact->company_uuid)
|
|
212
|
+
->where('meta->customer_portal->customer_uuid', $contact->uuid)
|
|
213
|
+
->where('meta->customer_portal->customer_type', 'contact')
|
|
214
|
+
->get()
|
|
215
|
+
->each(function (Issue $issue) use ($vendor) {
|
|
216
|
+
$meta = (array) $issue->meta;
|
|
217
|
+
data_set($meta, 'customer_portal.customer_uuid', $vendor->uuid);
|
|
218
|
+
data_set($meta, 'customer_portal.customer_type', 'vendor');
|
|
219
|
+
$issue->update(['meta' => $meta]);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
136
223
|
private function resolveUserInput(Request $request, array &$input): void
|
|
137
224
|
{
|
|
138
225
|
$user = data_get($input, 'user_uuid') ?? data_get($input, 'user') ?? $request->input('contact.user_uuid') ?? $request->input('contact.user');
|
|
@@ -149,6 +236,54 @@ class ContactController extends FleetOpsController
|
|
|
149
236
|
unset($input['user']);
|
|
150
237
|
}
|
|
151
238
|
|
|
239
|
+
private function assertCustomerPortalCanSendWelcomeEmail(array $input): void
|
|
240
|
+
{
|
|
241
|
+
$type = data_get($input, 'type', 'contact');
|
|
242
|
+
if ($type !== 'customer' || !data_get($input, 'meta.customer_portal.send_welcome_email')) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!$this->isCustomerPortalInstalled()) {
|
|
247
|
+
throw new \Exception('Customer portal must be installed before sending a customer welcome email.');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private function sendCustomerPortalWelcomeEmail(Contact $contact): void
|
|
252
|
+
{
|
|
253
|
+
if (!data_get($contact->meta, 'customer_portal.send_welcome_email')) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!$this->isCustomerPortalInstalled()) {
|
|
258
|
+
throw new \Exception('Customer portal must be installed before sending a customer welcome email.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
$user = $contact->getUser() ?? Contact::createUserFromContact($contact, false, true);
|
|
262
|
+
if (!$user) {
|
|
263
|
+
throw new \Exception('Unable to create customer portal login.');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
$password = Str::random(16);
|
|
267
|
+
$user->changePassword($password);
|
|
268
|
+
|
|
269
|
+
if ($user->status !== 'active') {
|
|
270
|
+
$user->activate();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
Mail::to($user)->send(new CustomerCredentialsMail($password, $contact));
|
|
274
|
+
|
|
275
|
+
$meta = (array) $contact->meta;
|
|
276
|
+
data_forget($meta, 'customer_portal.send_welcome_email');
|
|
277
|
+
$contact->forceFill(['meta' => $meta])->saveQuietly();
|
|
278
|
+
$contact->setAttribute('meta', $meta);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private function isCustomerPortalInstalled(): bool
|
|
282
|
+
{
|
|
283
|
+
return collect(Utils::getInstalledFleetbaseExtensions())
|
|
284
|
+
->contains(fn ($package) => data_get($package, 'name') === 'fleetbase/customer-portal-api');
|
|
285
|
+
}
|
|
286
|
+
|
|
152
287
|
private function assertCustomerIdentityIsAvailable(array $input, ?Contact $contact = null): void
|
|
153
288
|
{
|
|
154
289
|
$type = data_get($input, 'type', $contact?->type);
|