@fleetbase/fleetops-engine 0.6.60 → 0.6.62
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/RELEASE.md +21 -17
- package/addon/components/ai/route-preview-map.js +1 -5
- package/addon/components/customer/orders.js +2 -1
- package/addon/components/driver/card.hbs +49 -0
- package/addon/components/driver/card.js +30 -0
- package/addon/components/map/leaflet-live-map.js +4 -19
- package/addon/components/modals/place-details.hbs +1 -1
- package/addon/components/modals/point-map.hbs +1 -1
- package/addon/components/orchestrator-workbench.js +2 -1
- package/addon/components/order/form/route.js +3 -0
- package/addon/components/order-tracking-lookup.js +2 -1
- package/addon/components/place/details.hbs +1 -1
- package/addon/components/positions-replay.js +4 -1
- package/addon/components/service-area/details.hbs +1 -1
- package/addon/components/widget/live-fleet.js +7 -7
- package/addon/components/zone/details.hbs +1 -1
- package/addon/controllers/management/drivers/index.js +22 -1
- package/addon/controllers/settings/map.js +16 -0
- package/addon/helpers/leaflet-tile-url.js +19 -0
- package/addon/services/map-settings.js +26 -1
- package/addon/templates/management/drivers/index.hbs +67 -33
- package/addon/templates/settings/map.hbs +10 -0
- package/addon/utils/leaflet-tile-url.js +31 -0
- package/addon/utils/prepare-place-for-save.js +13 -0
- package/app/components/driver/card.js +1 -0
- package/app/helpers/leaflet-tile-url.js +1 -0
- package/app/utils/leaflet-tile-url.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/index.js +11 -0
- package/package.json +2 -1
- package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
- package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +312 -59
- package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
- package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +139 -34
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
- package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
- package/server/src/Http/Filter/DriverFilter.php +71 -15
- package/server/src/Http/Filter/FleetFilter.php +32 -33
- package/server/src/Http/Filter/FuelReportFilter.php +29 -0
- package/server/src/Http/Filter/IssueFilter.php +29 -0
- package/server/src/Http/Filter/VehicleFilter.php +57 -19
- package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
- package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
- package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
- package/server/src/Http/Requests/CreateVehicleRequest.php +150 -26
- package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
- package/server/src/Http/Resources/v1/Driver.php +22 -2
- package/server/src/Http/Resources/v1/Fleet.php +38 -25
- package/server/src/Http/Resources/v1/Manifest.php +52 -0
- package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
- package/server/src/Http/Resources/v1/Vehicle.php +46 -4
- package/server/src/Models/Driver.php +2 -2
- package/server/src/Models/Place.php +1 -1
- package/server/src/routes.php +29 -0
- package/server/tests/ApiDriverControllerContractsTest.php +317 -13
- package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
- package/server/tests/ApiEntityControllerContractsTest.php +1 -1
- package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
- package/server/tests/ApiFleetControllerContractsTest.php +354 -21
- package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
- package/server/tests/ApiIssueControllerContractsTest.php +1 -1
- package/server/tests/ApiManifestControllerContractsTest.php +599 -0
- package/server/tests/ApiOrderControllerContractsTest.php +1 -1
- package/server/tests/ApiPartControllerContractsTest.php +2 -2
- package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
- package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
- package/server/tests/ApiVehicleControllerContractsTest.php +210 -8
- package/server/tests/CompactResourceSerializationTest.php +13 -7
- package/server/tests/ControllerFilterContractsTest.php +132 -13
- package/server/tests/ControllerHelperContractsTest.php +6 -3
- package/server/tests/DriverFilterExecutionTest.php +130 -6
- package/server/tests/ExceptionContractsTest.php +15 -0
- package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
- package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
- package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
- package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
- package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
- package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
- package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
- package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
- package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
- package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
- package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
- package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
- package/server/tests/FleetHierarchyResourceTest.php +56 -8
- package/server/tests/RequestContractsTest.php +105 -7
- package/server/tests/SettingControllerContractsTest.php +33 -0
- package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
- package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
- package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
- package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
- package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
- package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
- package/translations/en-us.yaml +4 -0
|
@@ -2,14 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Filter;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Fleet;
|
|
5
7
|
use Fleetbase\FleetOps\Models\Place;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Vendor;
|
|
6
10
|
use Fleetbase\FleetOps\Support\Utils;
|
|
7
11
|
use Fleetbase\Http\Filter\Filter;
|
|
8
12
|
use Fleetbase\Models\Company;
|
|
13
|
+
use Fleetbase\Support\Http;
|
|
9
14
|
use Illuminate\Support\Str;
|
|
10
15
|
|
|
11
16
|
class DriverFilter extends Filter
|
|
12
17
|
{
|
|
18
|
+
use ResolvesPublicRelationUuids;
|
|
19
|
+
|
|
13
20
|
public function queryForInternal()
|
|
14
21
|
{
|
|
15
22
|
$this->builder->where(
|
|
@@ -43,9 +50,28 @@ class DriverFilter extends Filter
|
|
|
43
50
|
});
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Match a driver by the identifier the operator's own system uses.
|
|
55
|
+
*
|
|
56
|
+
* Two callers, two meanings. The Fleet-Ops console types into a search box
|
|
57
|
+
* and expects `VEH-10` to find `VEH-100`; an importer asks whether `VEH-10`
|
|
58
|
+
* exists and must not be told yes because `VEH-100` does. A partial match
|
|
59
|
+
* there produces a duplicate on every rerun.
|
|
60
|
+
*
|
|
61
|
+
* Core's ordinary fillable-column filtering is already equality — this
|
|
62
|
+
* method exists only because the console needs the looser behaviour, so it
|
|
63
|
+
* is the console that gets the exception. An unknown request context is
|
|
64
|
+
* treated as public: exact is the safe default of the two.
|
|
65
|
+
*/
|
|
46
66
|
public function internalId(?string $internalId)
|
|
47
67
|
{
|
|
48
|
-
$this->
|
|
68
|
+
if (Http::isInternalRequest($this->request)) {
|
|
69
|
+
$this->builder->searchWhere('internal_id', $internalId);
|
|
70
|
+
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
$this->builder->where('internal_id', '=', $internalId);
|
|
49
75
|
}
|
|
50
76
|
|
|
51
77
|
public function name(?string $name)
|
|
@@ -58,14 +84,25 @@ class DriverFilter extends Filter
|
|
|
58
84
|
);
|
|
59
85
|
}
|
|
60
86
|
|
|
87
|
+
/**
|
|
88
|
+
* A public id identifies exactly one record, so a public lookup matches it
|
|
89
|
+
* exactly. The console keeps the partial search its id column filter box
|
|
90
|
+
* has always had.
|
|
91
|
+
*/
|
|
61
92
|
public function publicId(?string $publicId)
|
|
62
93
|
{
|
|
63
|
-
$this->
|
|
94
|
+
if (Http::isInternalRequest($this->request)) {
|
|
95
|
+
$this->builder->searchWhere('public_id', $publicId);
|
|
96
|
+
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$this->builder->where('public_id', '=', $publicId);
|
|
64
101
|
}
|
|
65
102
|
|
|
66
103
|
public function facilitator(string $facilitator)
|
|
67
104
|
{
|
|
68
|
-
$this->builder->
|
|
105
|
+
$this->builder->whereIn('vendor_uuid', $this->resolvePublicRelationUuids(Vendor::class, $facilitator));
|
|
69
106
|
}
|
|
70
107
|
|
|
71
108
|
public function vehicle(string $vehicle)
|
|
@@ -76,16 +113,28 @@ class DriverFilter extends Filter
|
|
|
76
113
|
return;
|
|
77
114
|
}
|
|
78
115
|
|
|
79
|
-
|
|
116
|
+
// The console passes a uuid; the public API passes a public or internal id.
|
|
117
|
+
if (Str::isUuid($vehicle) && Http::isInternalRequest($this->request)) {
|
|
80
118
|
$this->builder->where('vehicle_uuid', $vehicle);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
119
|
+
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
$vehicleUuids = $this->resolvePublicRelationUuids(Vehicle::class, $vehicle);
|
|
124
|
+
|
|
125
|
+
if ($vehicleUuids !== []) {
|
|
126
|
+
$this->builder->whereIn('vehicle_uuid', $vehicleUuids);
|
|
127
|
+
|
|
128
|
+
return;
|
|
88
129
|
}
|
|
130
|
+
|
|
131
|
+
// Fall back to a search so a partial plate or model still narrows a list.
|
|
132
|
+
$this->builder->whereHas(
|
|
133
|
+
'vehicle',
|
|
134
|
+
function ($query) use ($vehicle) {
|
|
135
|
+
$query->search($vehicle);
|
|
136
|
+
}
|
|
137
|
+
);
|
|
89
138
|
}
|
|
90
139
|
|
|
91
140
|
public function driversLicenseNumber(?string $driversLicenseNumber)
|
|
@@ -93,12 +142,17 @@ class DriverFilter extends Filter
|
|
|
93
142
|
$this->builder->searchWhere('drivers_license_number', $driversLicenseNumber);
|
|
94
143
|
}
|
|
95
144
|
|
|
145
|
+
/**
|
|
146
|
+
* `phone` is an accessor sourced from the linked user, not a relation — the
|
|
147
|
+
* previous `whereHas('phone')` asked Eloquent for a relation that does not
|
|
148
|
+
* exist and raised a 500 for every caller of `?phone=`.
|
|
149
|
+
*/
|
|
96
150
|
public function phone(string $phone)
|
|
97
151
|
{
|
|
98
152
|
$this->builder->whereHas(
|
|
99
|
-
'
|
|
153
|
+
'user',
|
|
100
154
|
function ($query) use ($phone) {
|
|
101
|
-
$query->
|
|
155
|
+
$query->searchWhere('phone', $phone);
|
|
102
156
|
}
|
|
103
157
|
);
|
|
104
158
|
}
|
|
@@ -127,10 +181,12 @@ class DriverFilter extends Filter
|
|
|
127
181
|
|
|
128
182
|
public function fleet(string $fleet)
|
|
129
183
|
{
|
|
184
|
+
$fleetUuids = $this->resolvePublicRelationUuids(Fleet::class, $fleet);
|
|
185
|
+
|
|
130
186
|
$this->builder->whereHas(
|
|
131
187
|
'fleets',
|
|
132
|
-
function ($q) use ($
|
|
133
|
-
$q->
|
|
188
|
+
function ($q) use ($fleetUuids) {
|
|
189
|
+
$q->whereIn('fleet_uuid', $fleetUuids);
|
|
134
190
|
}
|
|
135
191
|
);
|
|
136
192
|
}
|
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Filter;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Fleet;
|
|
7
|
+
use Fleetbase\FleetOps\Models\ServiceArea;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Vendor;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Zone;
|
|
5
10
|
use Fleetbase\FleetOps\Support\Utils;
|
|
6
11
|
use Fleetbase\Http\Filter\Filter;
|
|
12
|
+
use Fleetbase\Support\Http;
|
|
7
13
|
|
|
8
14
|
class FleetFilter extends Filter
|
|
9
15
|
{
|
|
16
|
+
use ResolvesPublicRelationUuids;
|
|
17
|
+
|
|
10
18
|
public function queryForInternal()
|
|
11
19
|
{
|
|
12
20
|
$this->builder->where('company_uuid', $this->session->get('company'))->with(['serviceArea', 'zone']);
|
|
@@ -17,15 +25,17 @@ class FleetFilter extends Filter
|
|
|
17
25
|
$this->builder->where('company_uuid', $this->session->get('company'));
|
|
18
26
|
}
|
|
19
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Free-text search across a fleet's own columns.
|
|
30
|
+
*
|
|
31
|
+
* Previously matched against a `user` relation, which Fleet does not have —
|
|
32
|
+
* every `?query=` on the fleets endpoint raised a relation-not-found error
|
|
33
|
+
* rather than returning results.
|
|
34
|
+
*/
|
|
20
35
|
public function query(?string $searchQuery)
|
|
21
36
|
{
|
|
22
37
|
$this->builder->where(function ($query) use ($searchQuery) {
|
|
23
|
-
$query->
|
|
24
|
-
'user',
|
|
25
|
-
function ($query) use ($searchQuery) {
|
|
26
|
-
$query->searchWhere(['name', 'email', 'phone'], $searchQuery);
|
|
27
|
-
}
|
|
28
|
-
);
|
|
38
|
+
$query->searchWhere(['name', 'task', 'public_id'], $searchQuery);
|
|
29
39
|
});
|
|
30
40
|
}
|
|
31
41
|
|
|
@@ -38,49 +48,38 @@ class FleetFilter extends Filter
|
|
|
38
48
|
|
|
39
49
|
public function serviceArea(?string $serviceArea)
|
|
40
50
|
{
|
|
41
|
-
$this->builder->
|
|
42
|
-
'serviceArea',
|
|
43
|
-
function ($query) use ($serviceArea) {
|
|
44
|
-
$query->where('uuid', $serviceArea);
|
|
45
|
-
}
|
|
46
|
-
);
|
|
51
|
+
$this->builder->whereIn('service_area_uuid', $this->resolvePublicRelationUuids(ServiceArea::class, $serviceArea));
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
public function zone(?string $zone)
|
|
50
55
|
{
|
|
51
|
-
$this->builder->
|
|
52
|
-
'zone',
|
|
53
|
-
function ($query) use ($zone) {
|
|
54
|
-
$query->where('zone_uuid', $zone);
|
|
55
|
-
}
|
|
56
|
-
);
|
|
56
|
+
$this->builder->whereIn('zone_uuid', $this->resolvePublicRelationUuids(Zone::class, $zone));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
public function parentFleet(?string $fleet)
|
|
60
60
|
{
|
|
61
|
-
$this->builder->
|
|
62
|
-
'parent_fleet',
|
|
63
|
-
function ($query) use ($fleet) {
|
|
64
|
-
$query->where('uuid', $fleet);
|
|
65
|
-
}
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
$this->builder->searchWhere('parent_fleet_uuid', $fleet);
|
|
61
|
+
$this->builder->whereIn('parent_fleet_uuid', $this->resolvePublicRelationUuids(Fleet::class, $fleet));
|
|
69
62
|
}
|
|
70
63
|
|
|
71
64
|
public function vendor(?string $vendor)
|
|
72
65
|
{
|
|
73
|
-
$this->builder->
|
|
74
|
-
'vendor',
|
|
75
|
-
function ($query) use ($vendor) {
|
|
76
|
-
$query->where('uuid', $vendor);
|
|
77
|
-
}
|
|
78
|
-
);
|
|
66
|
+
$this->builder->whereIn('vendor_uuid', $this->resolvePublicRelationUuids(Vendor::class, $vendor));
|
|
79
67
|
}
|
|
80
68
|
|
|
69
|
+
/**
|
|
70
|
+
* A public id identifies exactly one record, so a public lookup matches it
|
|
71
|
+
* exactly. The console keeps the partial search its id column filter box
|
|
72
|
+
* has always had.
|
|
73
|
+
*/
|
|
81
74
|
public function publicId(?string $publicId)
|
|
82
75
|
{
|
|
83
|
-
$this->
|
|
76
|
+
if (Http::isInternalRequest($this->request)) {
|
|
77
|
+
$this->builder->searchWhere('public_id', $publicId);
|
|
78
|
+
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$this->builder->where('public_id', '=', $publicId);
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
public function task(?string $task)
|
|
@@ -86,6 +86,35 @@ class FuelReportFilter extends Filter
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Aliases for {@see driver()}.
|
|
91
|
+
*
|
|
92
|
+
* The base filter resolves a query parameter to a method of the same name
|
|
93
|
+
* and **silently ignores anything it cannot match**. So a client scoping a
|
|
94
|
+
* list with `driver_uuid` — the column's own name, and the name every other
|
|
95
|
+
* part of the payload uses — matched nothing, the filter was dropped, and
|
|
96
|
+
* the response came back scoped only by company: every driver's records,
|
|
97
|
+
* with a 200 and no indication that the request had been narrowed at all.
|
|
98
|
+
*
|
|
99
|
+
* For a driver app that is a disclosure, not a nuisance: a driver asking
|
|
100
|
+
* for their own fuel reports received the whole company's.
|
|
101
|
+
*/
|
|
102
|
+
public function driverUuid(?string $driver)
|
|
103
|
+
{
|
|
104
|
+
$this->driver($driver);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public function driverAssigned(?string $driver)
|
|
108
|
+
{
|
|
109
|
+
$this->driver($driver);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Alias for {@see vehicle()}, for the same reason. */
|
|
113
|
+
public function vehicleUuid(?string $vehicle)
|
|
114
|
+
{
|
|
115
|
+
$this->vehicle($vehicle);
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
public function vehicle(?string $vehicle)
|
|
90
119
|
{
|
|
91
120
|
$this->builder->whereHas('vehicle', function ($q) use ($vehicle) {
|
|
@@ -88,6 +88,35 @@ class IssueFilter extends Filter
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Aliases for {@see driver()}.
|
|
93
|
+
*
|
|
94
|
+
* The base filter resolves a query parameter to a method of the same name
|
|
95
|
+
* and **silently ignores anything it cannot match**. So a client scoping a
|
|
96
|
+
* list with `driver_uuid` — the column's own name, and the name every other
|
|
97
|
+
* part of the payload uses — matched nothing, the filter was dropped, and
|
|
98
|
+
* the response came back scoped only by company: every driver's records,
|
|
99
|
+
* with a 200 and no indication that the request had been narrowed at all.
|
|
100
|
+
*
|
|
101
|
+
* For a driver app that is a disclosure, not a nuisance: a driver asking
|
|
102
|
+
* for their own fuel reports received the whole company's.
|
|
103
|
+
*/
|
|
104
|
+
public function driverUuid(?string $driver)
|
|
105
|
+
{
|
|
106
|
+
$this->driver($driver);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public function driverAssigned(?string $driver)
|
|
110
|
+
{
|
|
111
|
+
$this->driver($driver);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Alias for {@see vehicle()}, for the same reason. */
|
|
115
|
+
public function vehicleUuid(?string $vehicle)
|
|
116
|
+
{
|
|
117
|
+
$this->vehicle($vehicle);
|
|
118
|
+
}
|
|
119
|
+
|
|
91
120
|
public function vehicle(?string $vehicle)
|
|
92
121
|
{
|
|
93
122
|
$this->builder->whereHas('vehicle', function ($q) use ($vehicle) {
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Filter;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Fleet;
|
|
5
8
|
use Fleetbase\FleetOps\Models\Vehicle;
|
|
6
9
|
use Fleetbase\FleetOps\Models\Vendor;
|
|
7
10
|
use Fleetbase\FleetOps\Support\Utils;
|
|
@@ -10,6 +13,8 @@ use Fleetbase\Support\Http;
|
|
|
10
13
|
|
|
11
14
|
class VehicleFilter extends Filter
|
|
12
15
|
{
|
|
16
|
+
use ResolvesPublicRelationUuids;
|
|
17
|
+
|
|
13
18
|
public function queryForInternal()
|
|
14
19
|
{
|
|
15
20
|
$this->builder->where('company_uuid', $this->session->get('company'));
|
|
@@ -30,14 +35,56 @@ class VehicleFilter extends Filter
|
|
|
30
35
|
$this->builder->searchWhere(['year', 'make', 'model', 'plate_number'], $display_name);
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Match a vehicle by the identifier the operator's own system uses.
|
|
40
|
+
*
|
|
41
|
+
* `internal_id` is the column an importer keys on to decide whether a
|
|
42
|
+
* vehicle already exists, and it was the one identifier the filter did not
|
|
43
|
+
* support — so every lookup fell through to a create.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Match a vehicle by the identifier the operator's own system uses.
|
|
47
|
+
*
|
|
48
|
+
* Two callers, two meanings. The Fleet-Ops console types into a search box
|
|
49
|
+
* and expects `VEH-10` to find `VEH-100`; an importer asks whether `VEH-10`
|
|
50
|
+
* exists and must not be told yes because `VEH-100` does. A partial match
|
|
51
|
+
* there produces a duplicate on every rerun.
|
|
52
|
+
*
|
|
53
|
+
* Core's ordinary fillable-column filtering is already equality — this
|
|
54
|
+
* method exists only because the console needs the looser behaviour, so it
|
|
55
|
+
* is the console that gets the exception. An unknown request context is
|
|
56
|
+
* treated as public: exact is the safe default of the two.
|
|
57
|
+
*/
|
|
58
|
+
public function internalId(?string $internalId)
|
|
59
|
+
{
|
|
60
|
+
if (Http::isInternalRequest($this->request)) {
|
|
61
|
+
$this->builder->searchWhere('internal_id', $internalId);
|
|
62
|
+
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
$this->builder->where('internal_id', '=', $internalId);
|
|
67
|
+
}
|
|
68
|
+
|
|
33
69
|
public function vin(?string $vin)
|
|
34
70
|
{
|
|
35
71
|
$this->builder->searchWhere('vin', $vin);
|
|
36
72
|
}
|
|
37
73
|
|
|
74
|
+
/**
|
|
75
|
+
* A public id identifies exactly one record, so a public lookup matches it
|
|
76
|
+
* exactly. The console keeps the partial search its id column filter box
|
|
77
|
+
* has always had.
|
|
78
|
+
*/
|
|
38
79
|
public function publicId(?string $publicIc)
|
|
39
80
|
{
|
|
40
|
-
$this->
|
|
81
|
+
if (Http::isInternalRequest($this->request)) {
|
|
82
|
+
$this->builder->searchWhere('public_id', $publicIc);
|
|
83
|
+
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$this->builder->where('public_id', '=', $publicIc);
|
|
41
88
|
}
|
|
42
89
|
|
|
43
90
|
public function plateNumber(?string $plateNumber)
|
|
@@ -68,10 +115,12 @@ class VehicleFilter extends Filter
|
|
|
68
115
|
return;
|
|
69
116
|
}
|
|
70
117
|
|
|
118
|
+
$driverUuids = $this->resolvePublicRelationUuids(Driver::class, $driverId);
|
|
119
|
+
|
|
71
120
|
$this->builder->whereHas(
|
|
72
121
|
'driver',
|
|
73
|
-
function ($query) use ($
|
|
74
|
-
$query->
|
|
122
|
+
function ($query) use ($driverUuids) {
|
|
123
|
+
$query->whereIn('uuid', $driverUuids);
|
|
75
124
|
}
|
|
76
125
|
);
|
|
77
126
|
}
|
|
@@ -82,20 +131,7 @@ class VehicleFilter extends Filter
|
|
|
82
131
|
return;
|
|
83
132
|
}
|
|
84
133
|
|
|
85
|
-
$this->builder->whereIn('vendor_uuid', Vendor::
|
|
86
|
-
->where('company_uuid', $this->session->get('company'))
|
|
87
|
-
->where(function ($query) use ($vendor) {
|
|
88
|
-
$query->where('public_id', $vendor);
|
|
89
|
-
|
|
90
|
-
if (in_array('internal_id', (new Vendor())->getFillable())) {
|
|
91
|
-
$query->orWhere('internal_id', $vendor);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (Http::isInternalRequest($this->request)) {
|
|
95
|
-
$query->orWhere('uuid', $vendor);
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
->pluck('uuid'));
|
|
134
|
+
$this->builder->whereIn('vendor_uuid', $this->resolvePublicRelationUuids(Vendor::class, $vendor));
|
|
99
135
|
}
|
|
100
136
|
|
|
101
137
|
public function driverUuid(?string $driverId)
|
|
@@ -132,10 +168,12 @@ class VehicleFilter extends Filter
|
|
|
132
168
|
|
|
133
169
|
public function fleet(string $fleet)
|
|
134
170
|
{
|
|
171
|
+
$fleetUuids = $this->resolvePublicRelationUuids(Fleet::class, $fleet);
|
|
172
|
+
|
|
135
173
|
$this->builder->whereHas(
|
|
136
174
|
'fleets',
|
|
137
|
-
function ($q) use ($
|
|
138
|
-
$q->
|
|
175
|
+
function ($q) use ($fleetUuids) {
|
|
176
|
+
$q->whereIn('fleet_uuid', $fleetUuids);
|
|
139
177
|
}
|
|
140
178
|
);
|
|
141
179
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Requests\Concerns;
|
|
4
|
+
|
|
5
|
+
use Illuminate\Validation\Rule;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds `exists` rules for public relationship inputs that are scoped to the
|
|
9
|
+
* authenticated company.
|
|
10
|
+
*
|
|
11
|
+
* An unscoped `exists:vendors,public_id` accepts another organization's public
|
|
12
|
+
* id, and the controller then resolves it to nothing — so the request looks
|
|
13
|
+
* accepted while the relationship is silently dropped. Scoping the rule turns
|
|
14
|
+
* that into an explicit validation failure, and keeps a public id from
|
|
15
|
+
* confirming or denying the existence of a resource in someone else's company.
|
|
16
|
+
*/
|
|
17
|
+
trait ScopesPublicRelationRules
|
|
18
|
+
{
|
|
19
|
+
/**
|
|
20
|
+
* An `exists` rule limited to the session company and to rows that are not
|
|
21
|
+
* soft deleted.
|
|
22
|
+
*
|
|
23
|
+
* No return type: the pest harness substitutes its own `Illuminate\Validation\Rule`
|
|
24
|
+
* stand-in, which is not an instance of the real `Rules\Exists`.
|
|
25
|
+
*/
|
|
26
|
+
protected function existsInCompany(string $table, string $column = 'public_id')
|
|
27
|
+
{
|
|
28
|
+
$companyUuid = session('company');
|
|
29
|
+
|
|
30
|
+
return Rule::exists($table, $column)->where(function ($query) use ($companyUuid) {
|
|
31
|
+
$query->where('company_uuid', $companyUuid);
|
|
32
|
+
$query->whereNull('deleted_at');
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The full rule set for an optional public relationship input.
|
|
38
|
+
*
|
|
39
|
+
* `nullable` is deliberate: sending `null` is how a caller clears an
|
|
40
|
+
* existing assignment, and `exists` is skipped for a null value.
|
|
41
|
+
*
|
|
42
|
+
* @return array<int, mixed>
|
|
43
|
+
*/
|
|
44
|
+
protected function publicRelationRules(string $table, string $column = 'public_id'): array
|
|
45
|
+
{
|
|
46
|
+
return ['nullable', 'string', $this->existsInCompany($table, $column)];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Requests;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Http\Requests\Concerns\ScopesPublicRelationRules;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
5
7
|
use Fleetbase\FleetOps\Rules\ResolvablePoint;
|
|
6
8
|
use Fleetbase\Http\Requests\FleetbaseRequest;
|
|
7
9
|
use Illuminate\Validation\Rule;
|
|
8
10
|
|
|
9
11
|
class CreateDriverRequest extends FleetbaseRequest
|
|
10
12
|
{
|
|
13
|
+
use ScopesPublicRelationRules;
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* Determine if the user is authorized to make this request.
|
|
13
17
|
*/
|
|
@@ -24,23 +28,110 @@ class CreateDriverRequest extends FleetbaseRequest
|
|
|
24
28
|
$isCreating = $this->isMethod('POST');
|
|
25
29
|
|
|
26
30
|
return [
|
|
27
|
-
'name'
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
'name' => [Rule::requiredIf($isCreating), 'nullable', 'string', 'max:191'],
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* Email and phone are optional.
|
|
35
|
+
*
|
|
36
|
+
* An operational fleet record often has neither: a subcontracted or
|
|
37
|
+
* yard-only driver may have no company mailbox and no handset. The
|
|
38
|
+
* previous contract forced both, and the only way to satisfy it was
|
|
39
|
+
* to invent an address or a number — which then sits in the tenant's
|
|
40
|
+
* user table looking real, can be mailed to, and blocks the genuine
|
|
41
|
+
* value later. Both are still validated and still unique when they
|
|
42
|
+
* are supplied; a driver created without them simply cannot sign in
|
|
43
|
+
* to Navigator until credentials are added.
|
|
44
|
+
*/
|
|
45
|
+
'email' => ['nullable', Rule::when($this->filled('email'), ['email']), $this->uniqueAmongUsers()],
|
|
46
|
+
'phone' => ['nullable', 'string', $this->uniqueAmongUsers()],
|
|
47
|
+
'password' => 'nullable|string',
|
|
48
|
+
'timezone' => 'nullable|string|max:64',
|
|
49
|
+
|
|
50
|
+
// Identity
|
|
51
|
+
'internal_id' => 'nullable|string|max:191',
|
|
52
|
+
'drivers_license_number' => 'nullable|string|max:191',
|
|
53
|
+
'license_expiry' => 'nullable|date',
|
|
54
|
+
'photo' => 'nullable|string',
|
|
55
|
+
|
|
56
|
+
// Operational
|
|
31
57
|
'country' => 'nullable|size:2',
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
58
|
+
'currency' => 'nullable|string|size:3',
|
|
59
|
+
'city' => 'nullable|string|max:191',
|
|
60
|
+
'online' => 'nullable|boolean',
|
|
61
|
+
'current_status' => 'nullable|string|max:64',
|
|
35
62
|
'status' => 'nullable|string|in:active,available,inactive',
|
|
36
|
-
'
|
|
37
|
-
'
|
|
63
|
+
'heading' => 'nullable|numeric',
|
|
64
|
+
'bearing' => 'nullable|numeric',
|
|
65
|
+
'altitude' => 'nullable|numeric',
|
|
66
|
+
'speed' => 'nullable|numeric',
|
|
38
67
|
'location' => ['nullable', new ResolvablePoint()],
|
|
39
68
|
'latitude' => ['nullable', 'required_with:longitude'],
|
|
40
69
|
'longitude' => ['nullable', 'required_with:latitude'],
|
|
70
|
+
|
|
71
|
+
// Structured / orchestrator
|
|
72
|
+
'meta' => 'nullable|array',
|
|
73
|
+
'skills' => 'nullable|array',
|
|
74
|
+
'skills.*' => 'string',
|
|
75
|
+
'max_travel_time' => 'nullable|integer|min:0',
|
|
76
|
+
'max_distance' => 'nullable|integer|min:0',
|
|
77
|
+
'time_window_start' => 'nullable|date_format:H:i,H:i:s',
|
|
78
|
+
'time_window_end' => 'nullable|date_format:H:i,H:i:s',
|
|
79
|
+
|
|
80
|
+
// Relationships, resolved from public ids inside the caller's company
|
|
81
|
+
'vehicle' => ['nullable', 'string', 'starts_with:vehicle_', $this->existsInCompany('vehicles')],
|
|
82
|
+
'vendor' => $this->publicRelationRules('vendors'),
|
|
83
|
+
'job' => $this->publicRelationRules('orders'),
|
|
41
84
|
];
|
|
42
85
|
}
|
|
43
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Uniqueness against every other live user account.
|
|
89
|
+
*
|
|
90
|
+
* Update used to skip this check entirely, so one driver could be given an
|
|
91
|
+
* address another driver already signs in with — and the two would then be
|
|
92
|
+
* indistinguishable to every identity lookup, including the password reset
|
|
93
|
+
* that matches on it. The driver's own linked user is ignored by uuid, so
|
|
94
|
+
* resending an unchanged address or number still succeeds.
|
|
95
|
+
*
|
|
96
|
+
* No return type: the pest harness substitutes its own
|
|
97
|
+
* `Illuminate\Validation\Rule`, which is not an instance of `Rules\Unique`.
|
|
98
|
+
*/
|
|
99
|
+
protected function uniqueAmongUsers()
|
|
100
|
+
{
|
|
101
|
+
$rule = Rule::unique('users')->whereNull('deleted_at');
|
|
102
|
+
|
|
103
|
+
$ownUserUuid = $this->linkedUserUuid();
|
|
104
|
+
|
|
105
|
+
return $ownUserUuid ? $rule->ignore($ownUserUuid, 'uuid') : $rule;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The uuid of the user behind the driver this request is updating.
|
|
110
|
+
*
|
|
111
|
+
* Null on create, and null when there is no resolvable route parameter —
|
|
112
|
+
* which is also what keeps this from querying during rule construction in
|
|
113
|
+
* contexts that have no route bound.
|
|
114
|
+
*/
|
|
115
|
+
protected function linkedUserUuid(): ?string
|
|
116
|
+
{
|
|
117
|
+
if ($this->isMethod('POST')) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
$id = $this->route('id');
|
|
122
|
+
|
|
123
|
+
if (!is_string($id) || $id === '') {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Driver::withoutGlobalScopes()
|
|
128
|
+
->where('company_uuid', session('company'))
|
|
129
|
+
->where(function ($query) use ($id) {
|
|
130
|
+
$query->where('public_id', $id)->orWhere('internal_id', $id);
|
|
131
|
+
})
|
|
132
|
+
->value('user_uuid');
|
|
133
|
+
}
|
|
134
|
+
|
|
44
135
|
/**
|
|
45
136
|
* Get custom attributes for validator errors.
|
|
46
137
|
*/
|