@fleetbase/fleetops-engine 0.6.32 → 0.6.34
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/addon/components/order/details/proof.js +28 -1
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/src/Console/Commands/TestEmail.php +98 -0
- package/server/src/Http/Controllers/Api/v1/ContactController.php +16 -37
- package/server/src/Http/Controllers/Api/v1/DriverController.php +11 -29
- package/server/src/Http/Controllers/Api/v1/OrderController.php +8 -8
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +3 -8
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +31 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +0 -32
- package/server/src/Http/Controllers/Internal/v1/PlaceController.php +0 -33
- package/server/src/Http/Controllers/Internal/v1/VendorController.php +0 -31
- package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +70 -6
- package/server/src/Http/Resources/v1/Driver.php +2 -2
- package/server/src/Http/Resources/v1/Index/Driver.php +2 -1
- package/server/src/Http/Resources/v1/Index/Place.php +1 -1
- package/server/src/Http/Resources/v1/Index/Vehicle.php +2 -2
- package/server/src/Http/Resources/v1/Place.php +1 -1
- package/server/src/Http/Resources/v1/Vehicle.php +2 -3
- package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +2 -3
- package/server/src/Models/Payload.php +26 -0
- package/server/src/Models/Place.php +22 -0
- package/server/src/Models/ServiceRate.php +111 -48
- package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
- package/server/src/Support/Utils.php +18 -0
|
@@ -1,3 +1,30 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { task } from 'ember-concurrency-decorators';
|
|
2
5
|
|
|
3
|
-
export default class OrderDetailsProofComponent extends Component {
|
|
6
|
+
export default class OrderDetailsProofComponent extends Component {
|
|
7
|
+
@service fetch;
|
|
8
|
+
@tracked proofs = [];
|
|
9
|
+
|
|
10
|
+
constructor(owner, { resource }) {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.loadOrderProofs.perform(resource);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@task *loadOrderProofs(order) {
|
|
16
|
+
const proofs = yield this.fetch.get(`orders/${order.id}/proofs`);
|
|
17
|
+
|
|
18
|
+
this.proofs = proofs.map((proof) => ({
|
|
19
|
+
...proof,
|
|
20
|
+
type: this.#getTypeFromRemarks(proof.remarks),
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#getTypeFromRemarks(remarks = '') {
|
|
25
|
+
if (remarks.endsWith('Photo')) return 'photo';
|
|
26
|
+
if (remarks.endsWith('Scan')) return 'scan';
|
|
27
|
+
if (remarks.endsWith('Signature')) return 'signature';
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/composer.json
CHANGED
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Console\Commands;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Mail\CustomerCredentialsMail;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Contact;
|
|
7
|
+
use Fleetbase\Models\Company;
|
|
8
|
+
use Fleetbase\Models\User;
|
|
9
|
+
use Illuminate\Console\Command;
|
|
10
|
+
use Illuminate\Support\Facades\Mail;
|
|
11
|
+
|
|
12
|
+
class TestEmail extends Command
|
|
13
|
+
{
|
|
14
|
+
/**
|
|
15
|
+
* The name and signature of the console command.
|
|
16
|
+
*
|
|
17
|
+
* @var string
|
|
18
|
+
*/
|
|
19
|
+
protected $signature = 'fleetops:test-email {email} {--type=customer_credentials : The type of email to test}';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The console command description.
|
|
23
|
+
*
|
|
24
|
+
* @var string
|
|
25
|
+
*/
|
|
26
|
+
protected $description = 'Test FleetOps email templates';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Execute the console command.
|
|
30
|
+
*
|
|
31
|
+
* @return int
|
|
32
|
+
*/
|
|
33
|
+
public function handle()
|
|
34
|
+
{
|
|
35
|
+
$email = $this->argument('email');
|
|
36
|
+
$type = $this->option('type');
|
|
37
|
+
|
|
38
|
+
$this->info('Sending test email...');
|
|
39
|
+
$this->info("Type: {$type}");
|
|
40
|
+
$this->info("To: {$email}");
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
switch ($type) {
|
|
44
|
+
case 'customer_credentials':
|
|
45
|
+
$this->sendCustomerCredentialsEmail($email);
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
default:
|
|
49
|
+
$this->error("Unknown email type: {$type}");
|
|
50
|
+
|
|
51
|
+
return Command::FAILURE;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$this->info('✓ Test email sent successfully!');
|
|
55
|
+
|
|
56
|
+
return Command::SUCCESS;
|
|
57
|
+
} catch (\Exception $e) {
|
|
58
|
+
$this->error('Failed to send test email: ' . $e->getMessage());
|
|
59
|
+
|
|
60
|
+
return Command::FAILURE;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Send a test customer credentials email.
|
|
66
|
+
*/
|
|
67
|
+
private function sendCustomerCredentialsEmail(string $email): void
|
|
68
|
+
{
|
|
69
|
+
// Create a mock user
|
|
70
|
+
$user = new User([
|
|
71
|
+
'name' => 'Test Customer',
|
|
72
|
+
'email' => $email,
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// Create a mock company
|
|
76
|
+
$company = new Company([
|
|
77
|
+
'name' => 'Test Company',
|
|
78
|
+
'public_id' => 'test_company_123',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
// Create a mock customer
|
|
82
|
+
$customer = new Contact([
|
|
83
|
+
'name' => 'Test Customer',
|
|
84
|
+
'email' => $email,
|
|
85
|
+
'phone' => '+1234567890',
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Set relations
|
|
89
|
+
$customer->setRelation('company', $company);
|
|
90
|
+
$customer->setRelation('user', $user);
|
|
91
|
+
|
|
92
|
+
// Mock password
|
|
93
|
+
$plaintextPassword = 'TestPassword123!';
|
|
94
|
+
|
|
95
|
+
// Send the email
|
|
96
|
+
Mail::to($email)->send(new CustomerCredentialsMail($plaintextPassword, $customer));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -8,7 +8,6 @@ use Fleetbase\FleetOps\Http\Resources\v1\Contact as ContactResource;
|
|
|
8
8
|
use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource;
|
|
9
9
|
use Fleetbase\FleetOps\Models\Contact;
|
|
10
10
|
use Fleetbase\Http\Controllers\Controller;
|
|
11
|
-
use Fleetbase\Models\File;
|
|
12
11
|
use Fleetbase\Models\User;
|
|
13
12
|
use Fleetbase\Support\Utils;
|
|
14
13
|
use Illuminate\Http\Request;
|
|
@@ -29,24 +28,13 @@ class ContactController extends Controller
|
|
|
29
28
|
$input['phone'] = is_string($input['phone']) ? Utils::formatPhoneNumber($input['phone']) : $input['phone'];
|
|
30
29
|
$input['type'] = empty($input['type']) ? 'contact' : $input['type'];
|
|
31
30
|
|
|
32
|
-
// Handle photo
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (Utils::isPublicId($photo)) {
|
|
37
|
-
$file = File::where('public_id', $photo)->first();
|
|
38
|
-
if ($file) {
|
|
39
|
-
$input['photo_uuid'] = $file->uuid;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
31
|
+
// Handle photo upload using FileResolverService
|
|
32
|
+
if ($request->has('photo')) {
|
|
33
|
+
$path = 'uploads/' . session('company') . '/contacts';
|
|
34
|
+
$file = app(\Fleetbase\Services\FileResolverService::class)->resolve($request->input('photo'), $path);
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
$path = implode('/', ['uploads', session('company'), 'contacts']);
|
|
46
|
-
$file = File::createFromBase64($photo, null, $path);
|
|
47
|
-
if ($file) {
|
|
48
|
-
$input['photo_uuid'] = $file->uuid;
|
|
49
|
-
}
|
|
36
|
+
if ($file) {
|
|
37
|
+
$input['photo_uuid'] = $file->uuid;
|
|
50
38
|
}
|
|
51
39
|
}
|
|
52
40
|
|
|
@@ -101,29 +89,20 @@ class ContactController extends Controller
|
|
|
101
89
|
]);
|
|
102
90
|
}
|
|
103
91
|
|
|
104
|
-
// Handle photo
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Handle photo being a file id
|
|
108
|
-
if (Utils::isPublicId($photo)) {
|
|
109
|
-
$file = File::where('public_id', $photo)->first();
|
|
110
|
-
if ($file) {
|
|
111
|
-
$input['photo_uuid'] = $file->uuid;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Handle the photo being base64 data string
|
|
116
|
-
if (Utils::isBase64String($photo)) {
|
|
117
|
-
$path = implode('/', ['uploads', session('company'), 'customers']);
|
|
118
|
-
$file = File::createFromBase64($photo, null, $path);
|
|
119
|
-
if ($file) {
|
|
120
|
-
$input['photo_uuid'] = $file->uuid;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
92
|
+
// Handle photo upload using FileResolverService
|
|
93
|
+
if ($request->has('photo')) {
|
|
94
|
+
$photo = $request->input('photo');
|
|
123
95
|
|
|
124
96
|
// Handle removal key
|
|
125
97
|
if ($photo === 'REMOVE') {
|
|
126
98
|
$input['photo_uuid'] = null;
|
|
99
|
+
} else {
|
|
100
|
+
$path = 'uploads/' . session('company') . '/contacts';
|
|
101
|
+
$file = app(\Fleetbase\Services\FileResolverService::class)->resolve($photo, $path);
|
|
102
|
+
|
|
103
|
+
if ($file) {
|
|
104
|
+
$input['photo_uuid'] = $file->uuid;
|
|
105
|
+
}
|
|
127
106
|
}
|
|
128
107
|
}
|
|
129
108
|
|
|
@@ -20,7 +20,6 @@ use Fleetbase\Http\Resources\Organization;
|
|
|
20
20
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
21
21
|
use Fleetbase\Models\Company;
|
|
22
22
|
use Fleetbase\Models\CompanyUser;
|
|
23
|
-
use Fleetbase\Models\File;
|
|
24
23
|
use Fleetbase\Models\User;
|
|
25
24
|
use Fleetbase\Models\UserDevice;
|
|
26
25
|
use Fleetbase\Models\VerificationCode;
|
|
@@ -62,6 +61,9 @@ class DriverController extends Controller
|
|
|
62
61
|
// Apply user infos
|
|
63
62
|
$userDetails = User::applyUserInfoFromRequest($request, $userDetails);
|
|
64
63
|
|
|
64
|
+
// Set company_uuid before creating user
|
|
65
|
+
$userDetails['company_uuid'] = $company->uuid;
|
|
66
|
+
|
|
65
67
|
// create user account for driver
|
|
66
68
|
$user = User::create($userDetails);
|
|
67
69
|
|
|
@@ -121,20 +123,10 @@ class DriverController extends Controller
|
|
|
121
123
|
// create the driver
|
|
122
124
|
$driver = Driver::create($input);
|
|
123
125
|
|
|
124
|
-
// Handle photo
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
$file =
|
|
128
|
-
// Handle photo being a file id
|
|
129
|
-
if (Utils::isPublicId($photo)) {
|
|
130
|
-
$file = File::where('public_id', $photo)->first();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Handle the photo being base64 data string
|
|
134
|
-
if (Utils::isBase64String($photo)) {
|
|
135
|
-
$path = implode('/', ['uploads', session('company'), 'drivers']);
|
|
136
|
-
$file = File::createFromBase64($photo, null, $path);
|
|
137
|
-
}
|
|
126
|
+
// Handle photo upload using FileResolverService
|
|
127
|
+
if ($request->has('photo')) {
|
|
128
|
+
$path = 'uploads/' . $company->uuid . '/drivers';
|
|
129
|
+
$file = app(\Fleetbase\Services\FileResolverService::class)->resolve($request->input('photo'), $path);
|
|
138
130
|
|
|
139
131
|
if ($file) {
|
|
140
132
|
$user->update(['photo_uuid' => $file->uuid]);
|
|
@@ -215,20 +207,10 @@ class DriverController extends Controller
|
|
|
215
207
|
$driver->update($input);
|
|
216
208
|
$driver->flushAttributesCache();
|
|
217
209
|
|
|
218
|
-
// Handle photo
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
$file =
|
|
222
|
-
// Handle photo being a file id
|
|
223
|
-
if (Utils::isPublicId($photo)) {
|
|
224
|
-
$file = File::where('public_id', $photo)->first();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Handle the photo being base64 data string
|
|
228
|
-
if (Utils::isBase64String($photo)) {
|
|
229
|
-
$path = implode('/', ['uploads', session('company'), 'drivers']);
|
|
230
|
-
$file = File::createFromBase64($photo, null, $path);
|
|
231
|
-
}
|
|
210
|
+
// Handle photo upload using FileResolverService
|
|
211
|
+
if ($request->has('photo')) {
|
|
212
|
+
$path = 'uploads/' . session('company') . '/drivers';
|
|
213
|
+
$file = app(\Fleetbase\Services\FileResolverService::class)->resolve($request->input('photo'), $path);
|
|
232
214
|
|
|
233
215
|
if ($file) {
|
|
234
216
|
$driver->user->update(['photo_uuid' => $file->uuid]);
|
|
@@ -352,7 +352,7 @@ class OrderController extends Controller
|
|
|
352
352
|
|
|
353
353
|
// find for the order
|
|
354
354
|
try {
|
|
355
|
-
$order = Order::findRecordOrFail($id);
|
|
355
|
+
$order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
|
|
356
356
|
} catch (ModelNotFoundException $exception) {
|
|
357
357
|
return response()->json(
|
|
358
358
|
[
|
|
@@ -518,6 +518,9 @@ class OrderController extends Controller
|
|
|
518
518
|
$order->update($input);
|
|
519
519
|
$order->flushAttributesCache();
|
|
520
520
|
|
|
521
|
+
// load required relations
|
|
522
|
+
$order->load(['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
|
|
523
|
+
|
|
521
524
|
// response the order resource
|
|
522
525
|
return new OrderResource($order);
|
|
523
526
|
}
|
|
@@ -725,7 +728,7 @@ class OrderController extends Controller
|
|
|
725
728
|
{
|
|
726
729
|
// find for the order
|
|
727
730
|
try {
|
|
728
|
-
$order = Order::findRecordOrFail($id);
|
|
731
|
+
$order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
|
|
729
732
|
} catch (ModelNotFoundException $exception) {
|
|
730
733
|
return response()->json(
|
|
731
734
|
[
|
|
@@ -805,7 +808,7 @@ class OrderController extends Controller
|
|
|
805
808
|
public function dispatchOrder(string $id)
|
|
806
809
|
{
|
|
807
810
|
try {
|
|
808
|
-
$order = Order::findRecordOrFail($id);
|
|
811
|
+
$order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
|
|
809
812
|
} catch (ModelNotFoundException $exception) {
|
|
810
813
|
return response()->json(
|
|
811
814
|
[
|
|
@@ -887,7 +890,7 @@ class OrderController extends Controller
|
|
|
887
890
|
$assignAdhocDriver = $request->input('assign');
|
|
888
891
|
|
|
889
892
|
try {
|
|
890
|
-
$order = Order::findRecordOrFail($id, ['payload.waypoints'], []);
|
|
893
|
+
$order = Order::findRecordOrFail($id, ['payload.waypoints', 'driverAssigned'], []);
|
|
891
894
|
} catch (ModelNotFoundException $exception) {
|
|
892
895
|
return response()->json(
|
|
893
896
|
[
|
|
@@ -1220,14 +1223,11 @@ class OrderController extends Controller
|
|
|
1220
1223
|
public function setDestination(string $id, string $placeId)
|
|
1221
1224
|
{
|
|
1222
1225
|
try {
|
|
1223
|
-
$order = Order::findRecordOrFail($id);
|
|
1226
|
+
$order = Order::findRecordOrFail($id, ['payload.waypoints', 'payload.pickup', 'payload.dropoff', 'driverAssigned']);
|
|
1224
1227
|
} catch (ModelNotFoundException $exception) {
|
|
1225
1228
|
return response()->apiError('Order resource not found.', 404);
|
|
1226
1229
|
}
|
|
1227
1230
|
|
|
1228
|
-
// Load required relations
|
|
1229
|
-
$order->loadMissing(['payload.waypoints', 'payload.pickup', 'payload.dropoff']);
|
|
1230
|
-
|
|
1231
1231
|
// Get the order payload
|
|
1232
1232
|
$payload = $order->payload;
|
|
1233
1233
|
|
|
@@ -27,12 +27,10 @@ class VehicleController extends Controller
|
|
|
27
27
|
{
|
|
28
28
|
// get request input
|
|
29
29
|
$input = $request->only(['status', 'make', 'model', 'year', 'trim', 'type', 'plate_number', 'vin', 'meta', 'online', 'location', 'altitude', 'heading', 'speed']);
|
|
30
|
+
|
|
30
31
|
// make sure company is set
|
|
31
32
|
$input['company_uuid'] = session('company');
|
|
32
33
|
|
|
33
|
-
// create instance of vehicle model
|
|
34
|
-
$vehicle = new Vehicle();
|
|
35
|
-
|
|
36
34
|
// set default online
|
|
37
35
|
if (!isset($input['online'])) {
|
|
38
36
|
$input['online'] = 0;
|
|
@@ -51,11 +49,8 @@ class VehicleController extends Controller
|
|
|
51
49
|
$input['location'] = Utils::getPointFromCoordinates($request->only(['latitude', 'longitude']));
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
//
|
|
55
|
-
$vehicle =
|
|
56
|
-
|
|
57
|
-
// save the vehicle
|
|
58
|
-
$vehicle->save();
|
|
52
|
+
// create the vehicle (fires 'created' event for billing resource tracking)
|
|
53
|
+
$vehicle = Vehicle::create($input);
|
|
59
54
|
|
|
60
55
|
// driver assignment
|
|
61
56
|
if ($request->has('driver')) {
|
|
@@ -136,7 +136,37 @@ class DriverController extends FleetOpsController
|
|
|
136
136
|
|
|
137
137
|
if ($input->has('user_uuid')) {
|
|
138
138
|
$user = User::where('uuid', $input->get('user_uuid'))->first();
|
|
139
|
-
|
|
139
|
+
|
|
140
|
+
// If user doesn't exist with provided UUID, create new user
|
|
141
|
+
if (!$user) {
|
|
142
|
+
$userInput = $input
|
|
143
|
+
->only(['name', 'password', 'email', 'phone', 'status', 'avatar_uuid'])
|
|
144
|
+
->filter()
|
|
145
|
+
->toArray();
|
|
146
|
+
|
|
147
|
+
// handle `photo_uuid`
|
|
148
|
+
if (isset($input['photo_uuid']) && Str::isUuid($input['photo_uuid'])) {
|
|
149
|
+
$userInput['avatar_uuid'] = $input['photo_uuid'];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Make sure password is set
|
|
153
|
+
if (empty($userInput['password'])) {
|
|
154
|
+
$userInput['password'] = Str::random(14);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Set user company
|
|
158
|
+
$userInput['company_uuid'] = session('company', $company->uuid);
|
|
159
|
+
|
|
160
|
+
// Apply user infos
|
|
161
|
+
$userInput = User::applyUserInfoFromRequest($request, $userInput);
|
|
162
|
+
|
|
163
|
+
// Create user account
|
|
164
|
+
$user = User::create($userInput);
|
|
165
|
+
|
|
166
|
+
// Set the user type to driver
|
|
167
|
+
$user->setType('driver');
|
|
168
|
+
} elseif ($input->has('photo_uuid')) {
|
|
169
|
+
// Update existing user's avatar if photo provided
|
|
140
170
|
$user->update(['avatar_uuid' => $input->get('photo_uuid')]);
|
|
141
171
|
}
|
|
142
172
|
} else {
|
|
@@ -29,14 +29,12 @@ use Fleetbase\FleetOps\Models\Waypoint;
|
|
|
29
29
|
use Fleetbase\FleetOps\Support\Utils;
|
|
30
30
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
31
31
|
use Fleetbase\Http\Requests\Internal\BulkActionRequest;
|
|
32
|
-
use Fleetbase\Http\Requests\Internal\BulkDeleteRequest;
|
|
33
32
|
use Fleetbase\Models\File;
|
|
34
33
|
use Fleetbase\Models\Type;
|
|
35
34
|
use Fleetbase\Support\TemplateString;
|
|
36
35
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
37
36
|
use Illuminate\Database\QueryException;
|
|
38
37
|
use Illuminate\Http\Request;
|
|
39
|
-
use Illuminate\Support\Collection;
|
|
40
38
|
use Illuminate\Support\Facades\Cache;
|
|
41
39
|
use Illuminate\Support\Facades\DB;
|
|
42
40
|
use Illuminate\Support\Facades\Validator;
|
|
@@ -330,36 +328,6 @@ class OrderController extends FleetOpsController
|
|
|
330
328
|
);
|
|
331
329
|
}
|
|
332
330
|
|
|
333
|
-
/**
|
|
334
|
-
* Updates a order to canceled and updates order activity.
|
|
335
|
-
*
|
|
336
|
-
* @return \Illuminate\Http\Response
|
|
337
|
-
*/
|
|
338
|
-
public function bulkDelete(BulkDeleteRequest $request)
|
|
339
|
-
{
|
|
340
|
-
$ids = $request->input('ids', []);
|
|
341
|
-
|
|
342
|
-
if (!$ids) {
|
|
343
|
-
return response()->error('Nothing to delete.');
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/** @var Order */
|
|
347
|
-
$count = Order::whereIn('uuid', $ids)->count();
|
|
348
|
-
$deleted = Order::whereIn('uuid', $ids)->delete();
|
|
349
|
-
|
|
350
|
-
if (!$deleted) {
|
|
351
|
-
return response()->error('Failed to bulk delete orders.');
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return response()->json(
|
|
355
|
-
[
|
|
356
|
-
'status' => 'OK',
|
|
357
|
-
'message' => 'Deleted ' . $count . ' orders',
|
|
358
|
-
'count' => $count,
|
|
359
|
-
]
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
331
|
/**
|
|
364
332
|
* Updates a order to canceled and updates order activity.
|
|
365
333
|
*
|
|
@@ -10,7 +10,6 @@ use Fleetbase\FleetOps\Models\Place;
|
|
|
10
10
|
use Fleetbase\FleetOps\Support\Geocoding;
|
|
11
11
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
12
12
|
use Fleetbase\Http\Requests\ImportRequest;
|
|
13
|
-
use Fleetbase\Http\Requests\Internal\BulkDeleteRequest;
|
|
14
13
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
15
14
|
use Illuminate\Http\Request;
|
|
16
15
|
use Illuminate\Support\Str;
|
|
@@ -150,38 +149,6 @@ class PlaceController extends FleetOpsController
|
|
|
150
149
|
return Excel::download(new PlaceExport($selections), $fileName);
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
/**
|
|
154
|
-
* Bulk deletes resources.
|
|
155
|
-
*
|
|
156
|
-
* @return \Illuminate\Http\Response
|
|
157
|
-
*/
|
|
158
|
-
public function bulkDelete(BulkDeleteRequest $request)
|
|
159
|
-
{
|
|
160
|
-
$ids = $request->input('ids', []);
|
|
161
|
-
|
|
162
|
-
if (!$ids) {
|
|
163
|
-
return response()->error('Nothing to delete.');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* @var \Fleetbase\Models\Place
|
|
168
|
-
*/
|
|
169
|
-
$count = Place::whereIn('uuid', $ids)->applyDirectivesForPermissions('fleet-ops list place')->count();
|
|
170
|
-
$deleted = Place::whereIn('uuid', $ids)->applyDirectivesForPermissions('fleet-ops list place')->delete();
|
|
171
|
-
|
|
172
|
-
if (!$deleted) {
|
|
173
|
-
return response()->error('Failed to bulk delete places.');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return response()->json(
|
|
177
|
-
[
|
|
178
|
-
'status' => 'OK',
|
|
179
|
-
'message' => 'Deleted ' . $count . ' places',
|
|
180
|
-
],
|
|
181
|
-
200
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
152
|
/**
|
|
186
153
|
* Get all avatar options for an vehicle.
|
|
187
154
|
*
|
|
@@ -9,7 +9,6 @@ use Fleetbase\FleetOps\Models\Driver;
|
|
|
9
9
|
use Fleetbase\FleetOps\Models\Vendor;
|
|
10
10
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
11
11
|
use Fleetbase\Http\Requests\ImportRequest;
|
|
12
|
-
use Fleetbase\Http\Requests\Internal\BulkDeleteRequest;
|
|
13
12
|
use Illuminate\Http\Request;
|
|
14
13
|
use Illuminate\Support\Facades\DB;
|
|
15
14
|
use Illuminate\Support\Str;
|
|
@@ -85,36 +84,6 @@ class VendorController extends FleetOpsController
|
|
|
85
84
|
return Excel::download(new VendorExport($selections), $fileName);
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
/**
|
|
89
|
-
* Bulk delete resources.
|
|
90
|
-
*
|
|
91
|
-
* @return \Illuminate\Http\Response
|
|
92
|
-
*/
|
|
93
|
-
public function bulkDelete(BulkDeleteRequest $request)
|
|
94
|
-
{
|
|
95
|
-
$ids = $request->input('ids', []);
|
|
96
|
-
|
|
97
|
-
if (!$ids) {
|
|
98
|
-
return response()->error('Nothing to delete.');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** @var \Fleetbase\Models\Vendor */
|
|
102
|
-
$count = Vendor::whereIn('uuid', $ids)->count();
|
|
103
|
-
$deleted = Vendor::whereIn('uuid', $ids)->delete();
|
|
104
|
-
|
|
105
|
-
if (!$deleted) {
|
|
106
|
-
return response()->error('Failed to bulk delete vendors.');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return response()->json(
|
|
110
|
-
[
|
|
111
|
-
'status' => 'OK',
|
|
112
|
-
'message' => 'Deleted ' . $count . ' vendors',
|
|
113
|
-
],
|
|
114
|
-
200
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
87
|
/**
|
|
119
88
|
* Get all status options for an vehicle.
|
|
120
89
|
*
|
|
@@ -36,8 +36,8 @@ class CreateServiceRateRequest extends FleetbaseRequest
|
|
|
36
36
|
'base_fee' => ['numeric'],
|
|
37
37
|
'per_meter_unit' => ['required_if:rate_calculation_method,per_meter', 'string', 'in:km,m'],
|
|
38
38
|
'per_meter_flat_rate_fee' => ['required_if:rate_calculation_method,per_meter', 'numeric'],
|
|
39
|
-
'meter_fees' => [Rule::requiredIf(function (
|
|
40
|
-
return in_array($input
|
|
39
|
+
'meter_fees' => [Rule::requiredIf(function () {
|
|
40
|
+
return in_array($this->input('rate_calculation_method'), ['fixed_meter', 'fixed_rate']);
|
|
41
41
|
}), 'array'],
|
|
42
42
|
'meter_fees.*.distance' => ['numeric'],
|
|
43
43
|
'meter_fees.*.fee' => ['numeric'],
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Requests\Internal;
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Http\Requests\CreateDriverRequest as CreateDriverApiRequest;
|
|
6
|
+
use Fleetbase\FleetOps\Rules\ResolvablePoint;
|
|
6
7
|
use Fleetbase\Support\Auth;
|
|
8
|
+
use Illuminate\Validation\Rule;
|
|
7
9
|
|
|
8
10
|
class CreateDriverRequest extends CreateDriverApiRequest
|
|
9
11
|
{
|
|
@@ -24,14 +26,76 @@ class CreateDriverRequest extends CreateDriverApiRequest
|
|
|
24
26
|
*/
|
|
25
27
|
public function rules()
|
|
26
28
|
{
|
|
27
|
-
$isCreating
|
|
29
|
+
$isCreating = $this->isMethod('POST');
|
|
30
|
+
$isCreatingWithUser = $this->filled('driver.user_uuid');
|
|
31
|
+
$shouldValidateUserAttributes = $isCreating && !$isCreatingWithUser;
|
|
28
32
|
|
|
29
33
|
return [
|
|
30
|
-
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// Required fields for driver creation
|
|
35
|
+
'name' => [Rule::requiredIf($shouldValidateUserAttributes), 'nullable', 'string', 'max:255'],
|
|
36
|
+
'email' => [
|
|
37
|
+
Rule::requiredIf($shouldValidateUserAttributes),
|
|
38
|
+
Rule::when($this->filled('email'), ['email']),
|
|
39
|
+
Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]),
|
|
40
|
+
],
|
|
41
|
+
'phone' => [
|
|
42
|
+
Rule::requiredIf($shouldValidateUserAttributes),
|
|
43
|
+
Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]),
|
|
44
|
+
],
|
|
45
|
+
|
|
46
|
+
// Optional fields
|
|
47
|
+
'password' => 'nullable|string|min:8',
|
|
48
|
+
'drivers_license_number' => 'nullable|string|max:255',
|
|
49
|
+
'internal_id' => 'nullable|string|max:255',
|
|
50
|
+
'country' => 'nullable|string|size:2',
|
|
51
|
+
'city' => 'nullable|string|max:255',
|
|
52
|
+
'vehicle' => 'nullable|string|starts_with:vehicle_|exists:vehicles,public_id',
|
|
53
|
+
'status' => 'nullable|string|in:active,inactive',
|
|
54
|
+
'vendor' => 'nullable|exists:vendors,public_id',
|
|
55
|
+
'job' => 'nullable|exists:orders,public_id',
|
|
56
|
+
'location' => ['nullable', new ResolvablePoint()],
|
|
57
|
+
'latitude' => ['nullable', 'required_with:longitude', 'numeric'],
|
|
58
|
+
'longitude' => ['nullable', 'required_with:latitude', 'numeric'],
|
|
59
|
+
|
|
60
|
+
// Photo/avatar
|
|
61
|
+
'photo_uuid' => 'nullable|exists:files,uuid',
|
|
62
|
+
'avatar_uuid' => 'nullable|exists:files,uuid',
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get custom attributes for validator errors.
|
|
68
|
+
*
|
|
69
|
+
* @return array
|
|
70
|
+
*/
|
|
71
|
+
public function attributes()
|
|
72
|
+
{
|
|
73
|
+
return [
|
|
74
|
+
'name' => 'driver name',
|
|
75
|
+
'email' => 'email address',
|
|
76
|
+
'phone' => 'phone number',
|
|
77
|
+
'drivers_license_number' => 'driver\'s license number',
|
|
78
|
+
'internal_id' => 'internal ID',
|
|
79
|
+
'photo_uuid' => 'photo',
|
|
80
|
+
'avatar_uuid' => 'avatar',
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get custom messages for validator errors.
|
|
86
|
+
*
|
|
87
|
+
* @return array
|
|
88
|
+
*/
|
|
89
|
+
public function messages()
|
|
90
|
+
{
|
|
91
|
+
return [
|
|
92
|
+
'name.required' => 'Driver name is required.',
|
|
93
|
+
'email.required' => 'Email address is required.',
|
|
94
|
+
'email.email' => 'Please provide a valid email address.',
|
|
95
|
+
'email.unique' => 'This email address is already registered.',
|
|
96
|
+
'phone.required' => 'Phone number is required.',
|
|
97
|
+
'phone.unique' => 'This phone number is already registered.',
|
|
98
|
+
'password.min' => 'Password must be at least 8 characters.',
|
|
35
99
|
];
|
|
36
100
|
}
|
|
37
101
|
}
|
|
@@ -50,7 +50,7 @@ class Driver extends FleetbaseResource
|
|
|
50
50
|
'jobs' => $this->whenLoaded('jobs', fn () => $this->getJobs()),
|
|
51
51
|
'vendor' => $this->whenLoaded('vendor', fn () => new Vendor($this->vendor)),
|
|
52
52
|
'fleets' => $this->whenLoaded('fleets', fn () => Fleet::collection($this->fleets()->without('drivers')->get())),
|
|
53
|
-
'location' => $this->wasRecentlyCreated ? new Point(0, 0) :
|
|
53
|
+
'location' => $this->wasRecentlyCreated ? new Point(0, 0) : Utils::castPoint($this->location),
|
|
54
54
|
'heading' => (int) data_get($this, 'heading', 0),
|
|
55
55
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
56
56
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
@@ -96,7 +96,7 @@ class Driver extends FleetbaseResource
|
|
|
96
96
|
'vehicle' => data_get($this, 'vehicle.public_id'),
|
|
97
97
|
'current_job' => data_get($this, 'currentJob.public_id'),
|
|
98
98
|
'vendor' => data_get($this, 'vendor.public_id'),
|
|
99
|
-
'location' =>
|
|
99
|
+
'location' => Utils::castPoint($this->location),
|
|
100
100
|
'heading' => (int) data_get($this, 'heading', 0),
|
|
101
101
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
102
102
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Resources\v1\Index;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
5
6
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
6
7
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
7
8
|
use Fleetbase\Support\Http;
|
|
@@ -35,7 +36,7 @@ class Driver extends FleetbaseResource
|
|
|
35
36
|
'phone' => $this->phone,
|
|
36
37
|
'photo_url' => $this->photo_url,
|
|
37
38
|
'status' => $this->status,
|
|
38
|
-
'location' => $this->wasRecentlyCreated ? new Point(0, 0) :
|
|
39
|
+
'location' => $this->wasRecentlyCreated ? new Point(0, 0) : Utils::castPoint($this->location),
|
|
39
40
|
'heading' => (int) data_get($this, 'heading', 0),
|
|
40
41
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
41
42
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
@@ -34,7 +34,7 @@ class Place extends FleetbaseResource
|
|
|
34
34
|
'city' => $this->city,
|
|
35
35
|
'country' => $this->country,
|
|
36
36
|
'avatar_url' => $this->avatar_url,
|
|
37
|
-
'location' => Utils::
|
|
37
|
+
'location' => Utils::castPoint($this->location),
|
|
38
38
|
|
|
39
39
|
// Meta flag to indicate this is an index resource
|
|
40
40
|
'meta' => [
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Resources\v1\Index;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
5
6
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
6
|
-
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
7
7
|
use Fleetbase\Support\Http;
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -35,7 +35,7 @@ class Vehicle extends FleetbaseResource
|
|
|
35
35
|
'year' => $this->year,
|
|
36
36
|
'photo_url' => $this->photo_url,
|
|
37
37
|
'status' => $this->status,
|
|
38
|
-
'location' =>
|
|
38
|
+
'location' => Utils::castPoint($this->location),
|
|
39
39
|
'heading' => (int) data_get($this, 'heading', 0),
|
|
40
40
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
41
41
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
@@ -28,7 +28,7 @@ class Place extends FleetbaseResource
|
|
|
28
28
|
'owner_uuid' => $this->when(Http::isInternalRequest(), $this->owner_uuid),
|
|
29
29
|
'owner_type' => $this->when(Http::isInternalRequest(), $this->owner_type ? Utils::toEmberResourceType($this->owner_type) : null),
|
|
30
30
|
'name' => $this->name,
|
|
31
|
-
'location' => Utils::
|
|
31
|
+
'location' => Utils::castPoint($this->location),
|
|
32
32
|
'address' => $this->address,
|
|
33
33
|
'address_html' => $this->when(Http::isInternalRequest(), $this->address_html),
|
|
34
34
|
'avatar_url' => $this->avatar_url,
|
|
@@ -4,7 +4,6 @@ namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Support\Utils;
|
|
6
6
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
7
|
-
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
8
7
|
use Fleetbase\Support\Http;
|
|
9
8
|
|
|
10
9
|
class Vehicle extends FleetbaseResource
|
|
@@ -132,7 +131,7 @@ class Vehicle extends FleetbaseResource
|
|
|
132
131
|
'updated_at' => $this->updated_at,
|
|
133
132
|
'created_at' => $this->created_at,
|
|
134
133
|
// Location & telematics
|
|
135
|
-
'location' =>
|
|
134
|
+
'location' => Utils::castPoint($this->location),
|
|
136
135
|
'heading' => (int) data_get($this, 'heading', 0),
|
|
137
136
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
138
137
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
@@ -248,7 +247,7 @@ class Vehicle extends FleetbaseResource
|
|
|
248
247
|
'updated_at' => $this->updated_at,
|
|
249
248
|
'created_at' => $this->created_at,
|
|
250
249
|
// Location & telematics
|
|
251
|
-
'location' =>
|
|
250
|
+
'location' => Utils::castPoint($this->location),
|
|
252
251
|
'heading' => (int) data_get($this, 'heading', 0),
|
|
253
252
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
254
253
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
@@ -4,7 +4,6 @@ namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Support\Utils;
|
|
6
6
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
7
|
-
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
8
7
|
use Fleetbase\Support\Http;
|
|
9
8
|
|
|
10
9
|
class VehicleWithoutDriver extends FleetbaseResource
|
|
@@ -131,7 +130,7 @@ class VehicleWithoutDriver extends FleetbaseResource
|
|
|
131
130
|
'updated_at' => $this->updated_at,
|
|
132
131
|
'created_at' => $this->created_at,
|
|
133
132
|
// Location & telematics
|
|
134
|
-
'location' =>
|
|
133
|
+
'location' => Utils::castPoint($this->location),
|
|
135
134
|
'heading' => (int) data_get($this, 'heading', 0),
|
|
136
135
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
137
136
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
@@ -245,7 +244,7 @@ class VehicleWithoutDriver extends FleetbaseResource
|
|
|
245
244
|
'updated_at' => $this->updated_at,
|
|
246
245
|
'created_at' => $this->created_at,
|
|
247
246
|
// Location & telematics
|
|
248
|
-
'location' =>
|
|
247
|
+
'location' => Utils::castPoint($this->location),
|
|
249
248
|
'heading' => (int) data_get($this, 'heading', 0),
|
|
250
249
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
251
250
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
@@ -245,6 +245,19 @@ class Payload extends Model
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// Handle entity photo upload
|
|
249
|
+
if (isset($attributes['photo'])) {
|
|
250
|
+
$path = 'uploads/' . session('company') . '/entities';
|
|
251
|
+
$file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path);
|
|
252
|
+
|
|
253
|
+
if ($file) {
|
|
254
|
+
$attributes['photo_uuid'] = $file->uuid;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Clean up raw photo data
|
|
258
|
+
unset($attributes['photo']);
|
|
259
|
+
}
|
|
260
|
+
|
|
248
261
|
$entity = new Entity($attributes);
|
|
249
262
|
$this->entities()->save($entity);
|
|
250
263
|
}
|
|
@@ -286,6 +299,19 @@ class Payload extends Model
|
|
|
286
299
|
}
|
|
287
300
|
}
|
|
288
301
|
|
|
302
|
+
// Handle entity photo upload
|
|
303
|
+
if (isset($attributes['photo'])) {
|
|
304
|
+
$path = 'uploads/' . session('company') . '/entities';
|
|
305
|
+
$file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path);
|
|
306
|
+
|
|
307
|
+
if ($file) {
|
|
308
|
+
$attributes['photo_uuid'] = $file->uuid;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Clean up raw photo data
|
|
312
|
+
unset($attributes['photo']);
|
|
313
|
+
}
|
|
314
|
+
|
|
289
315
|
Entity::insertGetUuid($attributes, $this);
|
|
290
316
|
}
|
|
291
317
|
|
|
@@ -333,6 +333,28 @@ class Place extends Model
|
|
|
333
333
|
{
|
|
334
334
|
$instance = (new static())->fillWithGoogleAddress($address);
|
|
335
335
|
|
|
336
|
+
// Before saving or returning this instance check the database for a duplicate address
|
|
337
|
+
// it cannot have any owner, and must belong to this session
|
|
338
|
+
if ($companyUuid = session('company')) {
|
|
339
|
+
$duplicate = static::where([
|
|
340
|
+
'company_uuid' => $companyUuid,
|
|
341
|
+
'street1' => $instance->street1,
|
|
342
|
+
'city' => $instance->city,
|
|
343
|
+
'country' => $instance->country,
|
|
344
|
+
])
|
|
345
|
+
->when(
|
|
346
|
+
$instance->postal_code !== null,
|
|
347
|
+
fn ($q) => $q->where('postal_code', $instance->postal_code),
|
|
348
|
+
fn ($q) => $q->whereNull('postal_code')
|
|
349
|
+
)
|
|
350
|
+
->whereNull('owner_uuid')
|
|
351
|
+
->first();
|
|
352
|
+
|
|
353
|
+
if ($duplicate) {
|
|
354
|
+
return $duplicate;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
336
358
|
if ($saveInstance) {
|
|
337
359
|
$instance->save();
|
|
338
360
|
}
|
|
@@ -12,7 +12,6 @@ use Fleetbase\Traits\HasPublicId;
|
|
|
12
12
|
use Fleetbase\Traits\HasUuid;
|
|
13
13
|
use Fleetbase\Traits\SendsWebhooks;
|
|
14
14
|
use Fleetbase\Traits\TracksApiCredential;
|
|
15
|
-
use Illuminate\Support\Facades\DB;
|
|
16
15
|
|
|
17
16
|
class ServiceRate extends Model
|
|
18
17
|
{
|
|
@@ -469,12 +468,11 @@ class ServiceRate extends Model
|
|
|
469
468
|
*/
|
|
470
469
|
public static function getServicableForPlaces($places = [], $service = null, $currency = null, ?\Closure $queryCallback = null): array
|
|
471
470
|
{
|
|
472
|
-
$reader
|
|
473
|
-
$
|
|
474
|
-
$serviceRatesQuery = static::with(['zone', 'serviceArea', 'rateFees', 'parcelFees']);
|
|
471
|
+
$reader = new GeoJSONReader();
|
|
472
|
+
$serviceRatesQuery = static::with(['zone', 'serviceArea', 'rateFees', 'parcelFees']);
|
|
475
473
|
|
|
476
474
|
if ($currency) {
|
|
477
|
-
$serviceRatesQuery->
|
|
475
|
+
$serviceRatesQuery->whereRaw('lower(currency) = ?', [strtolower($currency)]);
|
|
478
476
|
}
|
|
479
477
|
|
|
480
478
|
if ($service) {
|
|
@@ -487,44 +485,115 @@ class ServiceRate extends Model
|
|
|
487
485
|
|
|
488
486
|
$serviceRates = $serviceRatesQuery->get();
|
|
489
487
|
|
|
490
|
-
$waypoints = collect($places)
|
|
491
|
-
|
|
488
|
+
$waypoints = collect($places)
|
|
489
|
+
->map(function ($place) {
|
|
490
|
+
$place = Place::createFromMixed($place);
|
|
491
|
+
|
|
492
|
+
if (!$place instanceof Place) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
492
495
|
|
|
493
|
-
if ($place instanceof Place) {
|
|
494
496
|
$point = $place->getLocationAsPoint();
|
|
495
497
|
|
|
496
|
-
//
|
|
497
|
-
return \Brick\Geo\Point::fromText(
|
|
498
|
+
// Brick point: X=lng, Y=lat (WKT order)
|
|
499
|
+
return \Brick\Geo\Point::fromText(
|
|
500
|
+
sprintf('POINT (%F %F)', $point->getLng(), $point->getLat()),
|
|
501
|
+
4326
|
|
502
|
+
);
|
|
503
|
+
})
|
|
504
|
+
->filter()
|
|
505
|
+
->values();
|
|
506
|
+
|
|
507
|
+
if ($waypoints->isEmpty()) {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Convert a casted spatial geometry (Zone::border / ServiceArea::border)
|
|
513
|
+
* into a Brick geometry using GeoJSONReader.
|
|
514
|
+
*/
|
|
515
|
+
$toBrickGeometry = function ($spatialGeometry) use ($reader) {
|
|
516
|
+
if (!$spatialGeometry) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Most Fleetbase spatial casts/types implement toJson()
|
|
521
|
+
if (is_object($spatialGeometry) && method_exists($spatialGeometry, 'toJson')) {
|
|
522
|
+
$json = $spatialGeometry->toJson();
|
|
523
|
+
$json = is_string($json) ? trim($json) : null;
|
|
524
|
+
|
|
525
|
+
if ($json) {
|
|
526
|
+
return $reader->read($json);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Fallback if the cast ever returns array/object
|
|
533
|
+
if (is_array($spatialGeometry) || is_object($spatialGeometry)) {
|
|
534
|
+
$json = json_encode($spatialGeometry, JSON_UNESCAPED_UNICODE);
|
|
535
|
+
if ($json && $json !== 'null') {
|
|
536
|
+
return $reader->read($json);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Fallback if it’s a raw JSON string
|
|
541
|
+
if (is_string($spatialGeometry) && trim($spatialGeometry) !== '') {
|
|
542
|
+
return $reader->read($spatialGeometry);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return null;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Ensure ALL waypoints are inside the given Brick geometry.
|
|
550
|
+
*/
|
|
551
|
+
$containsAllWaypoints = function ($brickGeometry) use ($waypoints): bool {
|
|
552
|
+
if (!$brickGeometry) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
foreach ($waypoints as $waypoint) {
|
|
557
|
+
if (!$brickGeometry->contains($waypoint)) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
498
560
|
}
|
|
499
|
-
|
|
561
|
+
|
|
562
|
+
return true;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
$applicableServiceRates = [];
|
|
500
566
|
|
|
501
567
|
foreach ($serviceRates as $serviceRate) {
|
|
568
|
+
// If a service area exists, all waypoints must be inside its border
|
|
502
569
|
if ($serviceRate->hasServiceArea()) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
570
|
+
$serviceAreaBorder = $serviceRate->serviceArea?->border;
|
|
571
|
+
|
|
572
|
+
$serviceAreaGeom = null;
|
|
573
|
+
try {
|
|
574
|
+
$serviceAreaGeom = $toBrickGeometry($serviceAreaBorder);
|
|
575
|
+
} catch (\Throwable $e) {
|
|
576
|
+
continue; // invalid geojson / geometry -> reject this rate
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!$containsAllWaypoints($serviceAreaGeom)) {
|
|
580
|
+
continue;
|
|
514
581
|
}
|
|
515
582
|
}
|
|
516
583
|
|
|
584
|
+
// If a zone exists, all waypoints must be inside its border
|
|
517
585
|
if ($serviceRate->hasZone()) {
|
|
518
|
-
|
|
519
|
-
foreach ($serviceRate->zone->border as $polygon) {
|
|
520
|
-
$polygon = $reader->read($polygon->toJson());
|
|
586
|
+
$zoneBorder = $serviceRate->zone?->border;
|
|
521
587
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
588
|
+
$zoneGeom = null;
|
|
589
|
+
try {
|
|
590
|
+
$zoneGeom = $toBrickGeometry($zoneBorder);
|
|
591
|
+
} catch (\Throwable $e) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!$containsAllWaypoints($zoneGeom)) {
|
|
596
|
+
continue;
|
|
528
597
|
}
|
|
529
598
|
}
|
|
530
599
|
|
|
@@ -933,29 +1002,23 @@ class ServiceRate extends Model
|
|
|
933
1002
|
*/
|
|
934
1003
|
public function findServiceRateFeeByDistance(int $totalDistance): ?ServiceRateFee
|
|
935
1004
|
{
|
|
936
|
-
$this->
|
|
1005
|
+
$this->loadMissing('rateFees');
|
|
937
1006
|
|
|
938
|
-
|
|
939
|
-
$
|
|
1007
|
+
// Convert meters to kilometers WITHOUT rounding up
|
|
1008
|
+
$distanceInKm = $totalDistance / 1000;
|
|
940
1009
|
|
|
941
|
-
|
|
942
|
-
|
|
1010
|
+
// Ensure predictable order
|
|
1011
|
+
$rateFees = $this->rateFees->sortBy('distance');
|
|
943
1012
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
} else {
|
|
949
|
-
$distanceFee = $rateFee;
|
|
1013
|
+
// Find the first tier that covers the distance
|
|
1014
|
+
foreach ($rateFees as $rateFee) {
|
|
1015
|
+
if ($distanceInKm <= $rateFee->distance) {
|
|
1016
|
+
return $rateFee;
|
|
950
1017
|
}
|
|
951
1018
|
}
|
|
952
1019
|
|
|
953
|
-
//
|
|
954
|
-
|
|
955
|
-
$distanceFee = $this->rateFees->sortByDesc('distance')->first();
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
return $distanceFee;
|
|
1020
|
+
// If distance exceeds all tiers, use the largest tier
|
|
1021
|
+
return $rateFees->last();
|
|
959
1022
|
}
|
|
960
1023
|
|
|
961
1024
|
/**
|
|
@@ -60,6 +60,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
60
60
|
\Fleetbase\FleetOps\Console\Commands\PurgeUnpurchasedServiceQuotes::class,
|
|
61
61
|
\Fleetbase\FleetOps\Console\Commands\SendDriverNotification::class,
|
|
62
62
|
\Fleetbase\FleetOps\Console\Commands\ReplayVehicleLocations::class,
|
|
63
|
+
\Fleetbase\FleetOps\Console\Commands\TestEmail::class,
|
|
63
64
|
];
|
|
64
65
|
|
|
65
66
|
/**
|
|
@@ -334,6 +334,24 @@ class Utils extends FleetbaseUtils
|
|
|
334
334
|
return new Point((float) $latitude, (float) $longitude);
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Always return spatial point.
|
|
339
|
+
*
|
|
340
|
+
* @param [type] $mixed
|
|
341
|
+
*
|
|
342
|
+
* @return void
|
|
343
|
+
*/
|
|
344
|
+
public static function castPoint($mixed)
|
|
345
|
+
{
|
|
346
|
+
try {
|
|
347
|
+
$point = static::getPointFromMixed($mixed);
|
|
348
|
+
|
|
349
|
+
return $point;
|
|
350
|
+
} catch (\Throwable $e) {
|
|
351
|
+
return new Point(0, 0);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
337
355
|
/**
|
|
338
356
|
* Determines if the given coordinates strictly represent a Point object.
|
|
339
357
|
* These will explude resolvable coordinates from records.
|