@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,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Requests;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Http\Requests\Concerns\ScopesPublicRelationRules;
|
|
5
6
|
use Fleetbase\Http\Requests\FleetbaseRequest;
|
|
6
7
|
use Illuminate\Validation\Rule;
|
|
7
8
|
|
|
8
9
|
class CreateFleetRequest extends FleetbaseRequest
|
|
9
10
|
{
|
|
11
|
+
use ScopesPublicRelationRules;
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* Determine if the user is authorized to make this request.
|
|
12
15
|
*/
|
|
@@ -21,8 +24,33 @@ class CreateFleetRequest extends FleetbaseRequest
|
|
|
21
24
|
public function rules(): array
|
|
22
25
|
{
|
|
23
26
|
return [
|
|
24
|
-
'name'
|
|
25
|
-
'
|
|
27
|
+
'name' => [Rule::requiredIf($this->isMethod('POST')), 'nullable', 'string', 'max:191'],
|
|
28
|
+
'color' => 'nullable|string|max:64',
|
|
29
|
+
'task' => 'nullable|string|max:191',
|
|
30
|
+
/*
|
|
31
|
+
* Fleet status is not a closed set on this model: the console, the
|
|
32
|
+
* spreadsheet importer and existing integrations all write free-form
|
|
33
|
+
* values, and `active`/`disabled`/`decommissioned` are the console's
|
|
34
|
+
* options rather than a schema constraint. Restricting the public API
|
|
35
|
+
* to a subset would leave it unable to express a state the console can.
|
|
36
|
+
*/
|
|
37
|
+
'status' => 'nullable|string|max:64',
|
|
38
|
+
'service_area' => $this->publicRelationRules('service_areas'),
|
|
39
|
+
'zone' => $this->publicRelationRules('zones'),
|
|
40
|
+
'vendor' => $this->publicRelationRules('vendors'),
|
|
41
|
+
'parent_fleet' => $this->publicRelationRules('fleets'),
|
|
42
|
+
'photo' => 'nullable|string',
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get custom attributes for validator errors.
|
|
48
|
+
*/
|
|
49
|
+
public function attributes(): array
|
|
50
|
+
{
|
|
51
|
+
return [
|
|
52
|
+
'service_area' => 'service area',
|
|
53
|
+
'parent_fleet' => 'parent fleet',
|
|
26
54
|
];
|
|
27
55
|
}
|
|
28
56
|
}
|
|
@@ -25,8 +25,13 @@ class CreateServiceRateRequest extends FleetbaseRequest
|
|
|
25
25
|
return [
|
|
26
26
|
'service_name' => [Rule::requiredIf($this->isMethod('POST')), 'string'],
|
|
27
27
|
'service_type' => [Rule::requiredIf($this->isMethod('POST')), 'string'],
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// `nullable` so an empty relationship is treated as absent rather than
|
|
29
|
+
// as an invalid one. Both are optional, but without it `exists` runs
|
|
30
|
+
// against the null that ConvertEmptyStringsToNull produced and answers
|
|
31
|
+
// "The selected service area is invalid" for a field the caller simply
|
|
32
|
+
// did not fill in.
|
|
33
|
+
'service_area' => ['nullable', Rule::exists('service_areas', 'public_id')->whereNull('deleted_at')],
|
|
34
|
+
'zone' => ['nullable', Rule::exists('zones', 'public_id')->whereNull('deleted_at')],
|
|
30
35
|
'rate_calculation_method' => [Rule::requiredIf($this->isMethod('POST')), 'string', 'in:fixed_meter,fixed_rate,per_meter,per_drop,algo,parcel'],
|
|
31
36
|
'currency' => ['required', 'size:3'],
|
|
32
37
|
'base_fee' => ['numeric'],
|
|
@@ -2,12 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Requests;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Http\Requests\Concerns\ScopesPublicRelationRules;
|
|
5
6
|
use Fleetbase\FleetOps\Rules\ResolvablePoint;
|
|
6
7
|
use Fleetbase\Http\Requests\FleetbaseRequest;
|
|
7
8
|
use Illuminate\Validation\Rule;
|
|
8
9
|
|
|
9
10
|
class CreateVehicleRequest extends FleetbaseRequest
|
|
10
11
|
{
|
|
12
|
+
use ScopesPublicRelationRules;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The statuses the public API accepts for a vehicle.
|
|
16
|
+
*
|
|
17
|
+
* `active` is retained even though the model rewrites it to `available`, so
|
|
18
|
+
* clients written against the original contract keep working.
|
|
19
|
+
*
|
|
20
|
+
* @var array<int, string>
|
|
21
|
+
*/
|
|
22
|
+
public const STATUSES = [
|
|
23
|
+
'active',
|
|
24
|
+
'available',
|
|
25
|
+
'in_use',
|
|
26
|
+
'maintenance',
|
|
27
|
+
'out_of_service',
|
|
28
|
+
'reserved',
|
|
29
|
+
'retired',
|
|
30
|
+
'staging',
|
|
31
|
+
'on_route',
|
|
32
|
+
'idle',
|
|
33
|
+
'cleaning',
|
|
34
|
+
'awaiting_parts',
|
|
35
|
+
'inspection_due',
|
|
36
|
+
'inspection_failed',
|
|
37
|
+
'accident',
|
|
38
|
+
'compliance_hold',
|
|
39
|
+
'stolen',
|
|
40
|
+
'operational',
|
|
41
|
+
'decommissioned',
|
|
42
|
+
];
|
|
43
|
+
|
|
11
44
|
/**
|
|
12
45
|
* Determine if the user is authorized to make this request.
|
|
13
46
|
*/
|
|
@@ -22,35 +55,126 @@ class CreateVehicleRequest extends FleetbaseRequest
|
|
|
22
55
|
public function rules(): array
|
|
23
56
|
{
|
|
24
57
|
return [
|
|
25
|
-
'status'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'
|
|
58
|
+
'status' => ['nullable', Rule::in(static::STATUSES)],
|
|
59
|
+
|
|
60
|
+
// Identity and description
|
|
61
|
+
'internal_id' => 'nullable|string|max:191',
|
|
62
|
+
'name' => 'nullable|string|max:191',
|
|
63
|
+
'description' => 'nullable|string',
|
|
64
|
+
'make' => 'nullable|string|max:191',
|
|
65
|
+
'model' => 'nullable|string|max:191',
|
|
66
|
+
'model_type' => 'nullable|string|max:191',
|
|
67
|
+
'year' => 'nullable|integer|min:1900|max:2100',
|
|
68
|
+
'trim' => 'nullable|string|max:191',
|
|
69
|
+
'color' => 'nullable|string|max:64',
|
|
70
|
+
'type' => 'nullable|string|max:191',
|
|
71
|
+
'class' => 'nullable|string|max:191',
|
|
72
|
+
'plate_number' => 'nullable|string|max:191',
|
|
73
|
+
'vin' => 'nullable|string|max:64',
|
|
74
|
+
'serial_number' => 'nullable|string|max:191',
|
|
75
|
+
'call_sign' => 'nullable|string|max:191',
|
|
76
|
+
'fuel_card_number' => 'nullable|string|max:191',
|
|
77
|
+
|
|
78
|
+
// Measurement and operation.
|
|
79
|
+
//
|
|
80
|
+
// Validated rather than merely accepted: the model casts odometer
|
|
81
|
+
// to an integer, so an unchecked string would be stored as 0 — a
|
|
82
|
+
// vehicle reporting zero miles rather than an error.
|
|
83
|
+
'odometer' => 'nullable|numeric|min:0',
|
|
84
|
+
'odometer_unit' => 'nullable|string|max:12',
|
|
85
|
+
'odometer_at_purchase' => 'nullable|numeric|min:0',
|
|
86
|
+
'measurement_system' => 'nullable|string|max:32',
|
|
87
|
+
'fuel_type' => 'nullable|string|max:64',
|
|
88
|
+
'fuel_volume_unit' => 'nullable|string|max:12',
|
|
89
|
+
'online' => 'nullable|boolean',
|
|
90
|
+
|
|
91
|
+
// Body, capacity and dimensions
|
|
92
|
+
'transmission' => 'nullable|string|max:64',
|
|
93
|
+
'body_type' => 'nullable|string|max:191',
|
|
94
|
+
'body_sub_type' => 'nullable|string|max:191',
|
|
95
|
+
'usage_type' => 'nullable|string|max:64',
|
|
96
|
+
'ownership_type' => 'nullable|string|max:64',
|
|
97
|
+
'cargo_volume' => 'nullable|numeric|min:0',
|
|
98
|
+
'passenger_volume' => 'nullable|numeric|min:0',
|
|
99
|
+
'interior_volume' => 'nullable|numeric|min:0',
|
|
100
|
+
'weight' => 'nullable|numeric|min:0',
|
|
101
|
+
'width' => 'nullable|numeric|min:0',
|
|
102
|
+
'length' => 'nullable|numeric|min:0',
|
|
103
|
+
'height' => 'nullable|numeric|min:0',
|
|
104
|
+
'towing_capacity' => 'nullable|numeric|min:0',
|
|
105
|
+
'payload_capacity' => 'nullable|numeric|min:0',
|
|
106
|
+
'seating_capacity' => 'nullable|integer|min:0',
|
|
107
|
+
'ground_clearance' => 'nullable|numeric|min:0',
|
|
108
|
+
'bed_length' => 'nullable|numeric|min:0',
|
|
109
|
+
'fuel_capacity' => 'nullable|numeric|min:0',
|
|
110
|
+
|
|
111
|
+
// Lifecycle and financing
|
|
112
|
+
'financing_status' => 'nullable|string|max:64',
|
|
113
|
+
'loan_number_of_payments' => 'nullable|integer|min:0',
|
|
114
|
+
'loan_first_payment' => 'nullable|date',
|
|
115
|
+
'loan_amount' => 'nullable|numeric|min:0',
|
|
116
|
+
'estimated_service_life_distance_unit' => 'nullable|string|max:12',
|
|
117
|
+
'estimated_service_life_distance' => 'nullable|integer|min:0',
|
|
118
|
+
'estimated_service_life_months' => 'nullable|integer|min:0',
|
|
119
|
+
'insurance_value' => 'nullable|numeric|min:0',
|
|
120
|
+
'depreciation_rate' => 'nullable|numeric',
|
|
121
|
+
'current_value' => 'nullable|numeric|min:0',
|
|
122
|
+
'acquisition_cost' => 'nullable|numeric|min:0',
|
|
123
|
+
'currency' => 'nullable|string|size:3',
|
|
124
|
+
'purchased_at' => 'nullable|date',
|
|
125
|
+
'lease_expires_at' => 'nullable|date',
|
|
126
|
+
|
|
127
|
+
// Regulatory and engine specifications
|
|
128
|
+
'emission_standard' => 'nullable|string|max:64',
|
|
129
|
+
'dpf_equipped' => 'nullable|boolean',
|
|
130
|
+
'scr_equipped' => 'nullable|boolean',
|
|
131
|
+
'gvwr' => 'nullable|numeric|min:0',
|
|
132
|
+
'gcwr' => 'nullable|numeric|min:0',
|
|
133
|
+
'engine_number' => 'nullable|string|max:191',
|
|
134
|
+
'engine_model' => 'nullable|string|max:191',
|
|
135
|
+
'engine_make' => 'nullable|string|max:191',
|
|
136
|
+
'engine_family' => 'nullable|string|max:191',
|
|
137
|
+
'engine_configuration' => 'nullable|string|max:191',
|
|
138
|
+
'engine_displacement' => 'nullable|numeric|min:0',
|
|
139
|
+
'engine_size' => 'nullable|numeric|min:0',
|
|
140
|
+
'horsepower' => 'nullable|numeric|min:0',
|
|
141
|
+
'horsepower_rpm' => 'nullable|integer|min:0',
|
|
142
|
+
'torque' => 'nullable|numeric|min:0',
|
|
143
|
+
'torque_rpm' => 'nullable|integer|min:0',
|
|
144
|
+
'number_of_cylinders' => 'nullable|integer|min:0',
|
|
145
|
+
'cylinder_arrangement' => 'nullable|string|max:64',
|
|
146
|
+
|
|
147
|
+
// Structured and descriptive fields
|
|
148
|
+
'specs' => 'nullable|array',
|
|
149
|
+
'details' => 'nullable|array',
|
|
150
|
+
'notes' => 'nullable|string',
|
|
151
|
+
'meta' => 'nullable|array',
|
|
152
|
+
|
|
153
|
+
// Orchestrator
|
|
154
|
+
'skills' => 'nullable|array',
|
|
155
|
+
'skills.*' => 'string',
|
|
156
|
+
'payload_capacity_volume' => 'nullable|numeric|min:0',
|
|
157
|
+
'payload_capacity_pallets' => 'nullable|integer|min:0',
|
|
158
|
+
'payload_capacity_parcels' => 'nullable|integer|min:0',
|
|
159
|
+
'max_tasks' => 'nullable|integer|min:0',
|
|
160
|
+
'time_window_start' => 'nullable|date_format:H:i,H:i:s',
|
|
161
|
+
'time_window_end' => 'nullable|date_format:H:i,H:i:s',
|
|
162
|
+
'return_to_depot' => 'nullable|boolean',
|
|
163
|
+
|
|
164
|
+
// Relationships, resolved from public ids inside the caller's company
|
|
165
|
+
'vendor' => $this->publicRelationRules('vendors'),
|
|
166
|
+
'driver' => $this->publicRelationRules('drivers'),
|
|
167
|
+
'category' => $this->publicRelationRules('categories'),
|
|
168
|
+
'warranty' => $this->publicRelationRules('warranties'),
|
|
169
|
+
'photo' => 'nullable|string',
|
|
170
|
+
|
|
171
|
+
// Location
|
|
51
172
|
'location' => ['nullable', new ResolvablePoint()],
|
|
52
173
|
'latitude' => ['nullable', 'required_with:longitude'],
|
|
53
174
|
'longitude' => ['nullable', 'required_with:latitude'],
|
|
175
|
+
'altitude' => 'nullable|numeric',
|
|
176
|
+
'heading' => 'nullable|numeric',
|
|
177
|
+
'speed' => 'nullable|numeric',
|
|
54
178
|
];
|
|
55
179
|
}
|
|
56
180
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Resources\v1\Concerns;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Support\Http;
|
|
6
|
+
use Illuminate\Http\Resources\MissingValue;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders a relationship twice: as an additive `<name>_id` public identifier, and
|
|
10
|
+
* as the nested object under the name the endpoint has always used.
|
|
11
|
+
*
|
|
12
|
+
* The two are deliberately separate keys. The SDK stores whatever the API returns
|
|
13
|
+
* verbatim — `Resource::$attributes = $attributes`, no normalisation — so a
|
|
14
|
+
* property that is an object on one call and a string on another silently breaks
|
|
15
|
+
* every consumer that dereferences it. Navigator interpolates `driver.user`
|
|
16
|
+
* straight into a socket channel name; an object there would subscribe it to
|
|
17
|
+
* `user.[object Object]` and simply stop delivering messages. So an existing
|
|
18
|
+
* object key stays an object key, always, and the identifier arrives beside it.
|
|
19
|
+
*/
|
|
20
|
+
trait ResolvesPublicRelationFields
|
|
21
|
+
{
|
|
22
|
+
/**
|
|
23
|
+
* The relations the caller asked for, as real Eloquent relation names.
|
|
24
|
+
*
|
|
25
|
+
* The controller has already normalised, mapped and allowlisted `with` /
|
|
26
|
+
* `expand` by the time a resource runs, so this is a plain read.
|
|
27
|
+
*
|
|
28
|
+
* @return array<int, string>
|
|
29
|
+
*/
|
|
30
|
+
protected function requestedRelations($request): array
|
|
31
|
+
{
|
|
32
|
+
$with = $request->input('with');
|
|
33
|
+
|
|
34
|
+
if (is_string($with)) {
|
|
35
|
+
$with = explode(',', $with);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!is_array($with)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return array_values(array_filter(array_map('strval', $with), 'strlen'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The nested object for a relationship.
|
|
47
|
+
*
|
|
48
|
+
* Internal console requests keep the exact `whenLoaded` behaviour they have
|
|
49
|
+
* always had. Public requests get the object only when they asked for it,
|
|
50
|
+
* which is also what they got before: these relations were never eager
|
|
51
|
+
* loaded on the public endpoints, so the key was absent unless `with` named
|
|
52
|
+
* it. Absent stays absent; it never becomes a string.
|
|
53
|
+
*
|
|
54
|
+
* @param array<int, string> $with camelCased relations the caller asked for
|
|
55
|
+
*/
|
|
56
|
+
protected function publicRelationObject(string $relation, array $with, \Closure $resource): mixed
|
|
57
|
+
{
|
|
58
|
+
if (Http::isInternalRequest()) {
|
|
59
|
+
return $this->whenLoaded($relation, $resource);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!in_array($relation, $with, true)) {
|
|
63
|
+
return new MissingValue();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
$this->loadRelationIfPossible($relation);
|
|
67
|
+
|
|
68
|
+
return $this->{$relation} ? $resource() : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The public id behind a relationship, or null when nothing is assigned.
|
|
73
|
+
*
|
|
74
|
+
* Additive and unconditional: it is present whether or not the object is,
|
|
75
|
+
* and it does not change when the object is expanded.
|
|
76
|
+
*
|
|
77
|
+
* @param string|null $foreignKey the column holding the relation for a belongsTo;
|
|
78
|
+
* null for the inverse side, which has no local column
|
|
79
|
+
*/
|
|
80
|
+
protected function publicIdForRelation(string $relation, ?string $foreignKey): ?string
|
|
81
|
+
{
|
|
82
|
+
if ($foreignKey !== null && empty($this->{$foreignKey})) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
$resource = $this->resource;
|
|
87
|
+
|
|
88
|
+
if (is_object($resource) && method_exists($resource, 'relationLoaded') && $resource->relationLoaded($relation)) {
|
|
89
|
+
return data_get($this, $relation . '.public_id');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Not every resource wraps an Eloquent model — webhook payload fixtures and
|
|
93
|
+
// compact serializers pass plain objects — so read the already-materialised
|
|
94
|
+
// value rather than calling a relation method that is not there.
|
|
95
|
+
if (!is_object($resource) || !method_exists($resource, $relation)) {
|
|
96
|
+
return data_get($this, $relation . '.public_id');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return $this->{$relation}()->value('public_id');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private function loadRelationIfPossible(string $relation): void
|
|
103
|
+
{
|
|
104
|
+
$resource = $this->resource;
|
|
105
|
+
|
|
106
|
+
if (is_object($resource) && method_exists($resource, 'loadMissing') && method_exists($resource, $relation)) {
|
|
107
|
+
$this->loadMissing($relation);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -37,6 +37,7 @@ class Driver extends FleetbaseResource
|
|
|
37
37
|
'name' => $this->name,
|
|
38
38
|
'email' => $this->email,
|
|
39
39
|
'phone' => $this->phone,
|
|
40
|
+
'timezone' => data_get($this, 'user.timezone'),
|
|
40
41
|
'drivers_license_number' => $this->drivers_license_number,
|
|
41
42
|
'license_expiry' => $this->formatDateOnly($this->license_expiry),
|
|
42
43
|
'photo_url' => $this->photo_url,
|
|
@@ -45,25 +46,38 @@ class Driver extends FleetbaseResource
|
|
|
45
46
|
'vehicle_name' => $this->when(Http::isInternalRequest(), $this->vehicle_name),
|
|
46
47
|
'vehicle_avatar' => $this->vehicle_avatar,
|
|
47
48
|
'vendor_name' => $this->when(Http::isInternalRequest(), $this->vendor_name),
|
|
48
|
-
'assigned_orders_count' => $this->when(Http::isInternalRequest(), $this->assignedOrdersCount()),
|
|
49
|
-
'current_order_reference' => $this->when(Http::isInternalRequest(), $this->currentOrderReference()),
|
|
49
|
+
'assigned_orders_count' => $this->when(Http::isInternalRequest(), fn () => $this->assignedOrdersCount()),
|
|
50
|
+
'current_order_reference' => $this->when(Http::isInternalRequest(), fn () => $this->currentOrderReference()),
|
|
50
51
|
'vehicle' => $this->whenLoaded('vehicle', fn () => new VehicleWithoutDriver($this->vehicle)),
|
|
51
52
|
'current_job' => $this->whenLoaded('currentJob', fn () => new Order($this->currentJob)),
|
|
52
53
|
'current_job_id' => $this->when(Http::isInternalRequest(), data_get($this, 'currentJob.tracking')),
|
|
53
54
|
'jobs' => $this->whenLoaded('jobs', fn () => $this->getJobs()),
|
|
54
55
|
'vendor' => $this->whenLoaded('vendor', fn () => new Vendor($this->vendor)),
|
|
56
|
+
// Public callers receive each assignment as a public id so a write
|
|
57
|
+
// can be read back; the nested objects above are unchanged.
|
|
58
|
+
'vehicle_id' => $this->when(Http::isPublicRequest(), fn () => $this->vehicle_id),
|
|
59
|
+
'vendor_id' => $this->when(Http::isPublicRequest(), fn () => $this->vendor_id),
|
|
60
|
+
'job_id' => $this->when(Http::isPublicRequest(), fn () => data_get($this, 'currentJob.public_id')),
|
|
55
61
|
'fleets' => $this->whenLoaded('fleets', fn () => Fleet::collection($this->fleets()->without('drivers')->get())),
|
|
56
62
|
'current_shift' => $this->whenLoaded('currentShift', fn () => $this->currentShift),
|
|
57
63
|
'location' => $this->wasRecentlyCreated ? new Point(0, 0) : Utils::castPoint($this->location),
|
|
58
64
|
'heading' => (int) data_get($this, 'heading', 0),
|
|
65
|
+
'bearing' => data_get($this, 'bearing'),
|
|
59
66
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
60
67
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
61
68
|
'country' => data_get($this, 'country'),
|
|
62
69
|
'currency' => data_get($this, 'currency', Utils::getCurrenyFromCountryCode($this->country)),
|
|
63
70
|
'city' => data_get($this, 'city', Utils::getCapitalCityFromCountryCode($this->country)),
|
|
64
71
|
'online' => data_get($this, 'online', false),
|
|
72
|
+
'current_status' => data_get($this, 'current_status'),
|
|
65
73
|
'status' => $this->status,
|
|
66
74
|
'token' => $this->token,
|
|
75
|
+
// Orchestrator constraints
|
|
76
|
+
'skills' => data_get($this, 'skills'),
|
|
77
|
+
'max_travel_time' => $this->max_travel_time,
|
|
78
|
+
'max_distance' => $this->max_distance,
|
|
79
|
+
'time_window_start' => $this->time_window_start,
|
|
80
|
+
'time_window_end' => $this->time_window_end,
|
|
67
81
|
'meta' => data_get($this, 'meta', Utils::createObject()),
|
|
68
82
|
'updated_at' => $this->updated_at,
|
|
69
83
|
'created_at' => $this->created_at,
|
|
@@ -131,7 +145,13 @@ class Driver extends FleetbaseResource
|
|
|
131
145
|
'currency' => data_get($this, 'currency', Utils::getCurrenyFromCountryCode($this->country)),
|
|
132
146
|
'city' => data_get($this, 'city', Utils::getCapitalCityFromCountryCode($this->country)),
|
|
133
147
|
'online' => data_get($this, 'online', false),
|
|
148
|
+
'current_status' => data_get($this, 'current_status'),
|
|
134
149
|
'status' => $this->status,
|
|
150
|
+
'skills' => data_get($this, 'skills'),
|
|
151
|
+
'max_travel_time' => $this->max_travel_time,
|
|
152
|
+
'max_distance' => $this->max_distance,
|
|
153
|
+
'time_window_start' => $this->time_window_start,
|
|
154
|
+
'time_window_end' => $this->time_window_end,
|
|
135
155
|
'meta' => data_get($this, 'meta', Utils::createObject()),
|
|
136
156
|
'updated_at' => $this->updated_at,
|
|
137
157
|
'created_at' => $this->created_at,
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Concerns\ResolvesPublicRelationFields;
|
|
5
6
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
6
7
|
use Fleetbase\Support\Http;
|
|
7
|
-
use
|
|
8
|
+
use Fleetbase\Support\Resolve;
|
|
8
9
|
|
|
9
10
|
class Fleet extends FleetbaseResource
|
|
10
11
|
{
|
|
12
|
+
use ResolvesPublicRelationFields;
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Transform the resource into an array.
|
|
13
16
|
*
|
|
@@ -17,22 +20,15 @@ class Fleet extends FleetbaseResource
|
|
|
17
20
|
*/
|
|
18
21
|
public function toArray($request)
|
|
19
22
|
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (in_array('subfleets', $with, true)) {
|
|
28
|
-
if (in_array('drivers', $with, true)) {
|
|
29
|
-
$this->loadMissing('subFleets.drivers');
|
|
30
|
-
}
|
|
23
|
+
// The controller has already mapped these onto real relation names and
|
|
24
|
+
// dropped anything outside the public allowlist, so they are safe to
|
|
25
|
+
// hand to the loader. `subfleets` used to arrive here spelled exactly
|
|
26
|
+
// that and reach `load('subfleets')`, which is not the relation —
|
|
27
|
+
// the documented expansion raised instead of resolving.
|
|
28
|
+
$with = $this->requestedRelations($request);
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
}
|
|
30
|
+
if ($with !== []) {
|
|
31
|
+
$this->loadMissing($with);
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
return $this->withCustomFields([
|
|
@@ -40,16 +36,31 @@ class Fleet extends FleetbaseResource
|
|
|
40
36
|
'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
|
|
41
37
|
'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
|
|
42
38
|
'name' => $this->name,
|
|
39
|
+
'color' => $this->color ?? null,
|
|
43
40
|
'task' => $this->task ?? null,
|
|
44
41
|
'status' => $this->status ?? null,
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
42
|
+
'photo_url' => $this->photo_url,
|
|
43
|
+
// Closures, not values: `when()` evaluates a plain second argument
|
|
44
|
+
// eagerly, so each of these counts ran a query on every public
|
|
45
|
+
// request that then discarded the result.
|
|
46
|
+
'drivers_count' => $this->when(Http::isInternalRequest(), fn () => $this->drivers_count),
|
|
47
|
+
'drivers_online_count' => $this->when(Http::isInternalRequest(), fn () => $this->drivers_online_count),
|
|
48
|
+
'vehicles_count' => $this->when(Http::isInternalRequest(), fn () => $this->vehicles_count),
|
|
49
|
+
'vehicles_online_count' => $this->when(Http::isInternalRequest(), fn () => $this->vehicles_online_count),
|
|
50
|
+
// Additive public identifiers. Always present, unchanged when the
|
|
51
|
+
// object beside them is expanded, and never a substitute for it.
|
|
52
|
+
'service_area_id' => $this->publicIdForRelation('serviceArea', 'service_area_uuid'),
|
|
53
|
+
'zone_id' => $this->publicIdForRelation('zone', 'zone_uuid'),
|
|
54
|
+
'vendor_id' => $this->publicIdForRelation('vendor', 'vendor_uuid'),
|
|
55
|
+
'parent_fleet_id' => $this->publicIdForRelation('parentFleet', 'parent_fleet_uuid'),
|
|
56
|
+
'photo_id' => $this->publicIdForRelation('photo', 'image_uuid'),
|
|
57
|
+
// The objects keep the shape they have always had: absent unless the
|
|
58
|
+
// relation was loaded, an object when it was, never a string.
|
|
59
|
+
'service_area' => $this->publicRelationObject('serviceArea', $with, fn () => new ServiceArea($this->serviceArea)),
|
|
60
|
+
'zone' => $this->publicRelationObject('zone', $with, fn () => new Zone($this->zone)),
|
|
61
|
+
'vendor' => $this->publicRelationObject('vendor', $with, fn () => new Vendor($this->vendor)),
|
|
62
|
+
'parent_fleet' => $this->publicRelationObject('parentFleet', $with, fn () => new ParentFleet($this->parentFleet)),
|
|
63
|
+
'photo' => $this->publicRelationObject('photo', $with, fn () => Resolve::httpResourceForModel($this->photo)),
|
|
53
64
|
'subfleets' => $this->whenLoaded('subFleets', fn () => SubFleet::collection($this->subFleets)),
|
|
54
65
|
'drivers' => $this->whenLoaded('drivers', fn () => Driver::collection($this->drivers()->with(Http::isInternalRequest() || $request->has('with.jobs') ? ['jobs'] : [])->get())),
|
|
55
66
|
'vehicles' => $this->whenLoaded('vehicles', fn () => Vehicle::collection($this->vehicles)),
|
|
@@ -68,11 +79,13 @@ class Fleet extends FleetbaseResource
|
|
|
68
79
|
return [
|
|
69
80
|
'id' => $this->public_id,
|
|
70
81
|
'name' => $this->name,
|
|
82
|
+
'color' => $this->color ?? null,
|
|
71
83
|
'task' => $this->task ?? null,
|
|
72
84
|
'status' => $this->status ?? null,
|
|
73
|
-
'parent_fleet' => $this->when($this->
|
|
85
|
+
'parent_fleet' => $this->when($this->parentFleet, data_get($this, 'parentFleet.public_id')),
|
|
74
86
|
'service_area' => $this->when($this->serviceArea, data_get($this, 'serviceArea.public_id')),
|
|
75
87
|
'zone' => $this->when($this->zone, data_get($this, 'zone.public_id')),
|
|
88
|
+
'vendor' => $this->when($this->vendor, data_get($this, 'vendor.public_id')),
|
|
76
89
|
'updated_at' => $this->updated_at,
|
|
77
90
|
'created_at' => $this->created_at,
|
|
78
91
|
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
6
|
+
use Fleetbase\Support\Http;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Public projection of a Manifest — a driver's route for a day.
|
|
10
|
+
*
|
|
11
|
+
* A manifest is order-agnostic: it is a sequence of stops, which may belong to
|
|
12
|
+
* several orders or to none the driver has ever seen as an order. That is why
|
|
13
|
+
* this exposes the stops directly rather than making a consumer assemble them
|
|
14
|
+
* from orders.
|
|
15
|
+
*
|
|
16
|
+
* Totals ride along because a driver wants to know what the day looks like
|
|
17
|
+
* before starting it, and computing them client-side from the stop list gives a
|
|
18
|
+
* different answer than the server's own.
|
|
19
|
+
*/
|
|
20
|
+
class Manifest extends FleetbaseResource
|
|
21
|
+
{
|
|
22
|
+
/**
|
|
23
|
+
* Transform the resource into an array.
|
|
24
|
+
*
|
|
25
|
+
* @param \Illuminate\Http\Request $request
|
|
26
|
+
*/
|
|
27
|
+
public function toArray($request): array
|
|
28
|
+
{
|
|
29
|
+
return [
|
|
30
|
+
'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id),
|
|
31
|
+
'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
|
|
32
|
+
'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
|
|
33
|
+
'status' => $this->status,
|
|
34
|
+
'scheduled_date' => $this->scheduled_date,
|
|
35
|
+
'started_at' => $this->started_at,
|
|
36
|
+
'completed_at' => $this->completed_at,
|
|
37
|
+
'total_distance_m' => $this->total_distance_m,
|
|
38
|
+
'total_duration_s' => $this->total_duration_s,
|
|
39
|
+
'stop_count' => $this->stop_count,
|
|
40
|
+
'completed_stops' => $this->completed_stops,
|
|
41
|
+
'pending_stops' => $this->pending_stops,
|
|
42
|
+
'driver_name' => $this->driver_name,
|
|
43
|
+
'vehicle_name' => $this->vehicle_name,
|
|
44
|
+
'notes' => $this->notes,
|
|
45
|
+
// Present only when the manifest was loaded with them, so a list
|
|
46
|
+
// endpoint stays a list.
|
|
47
|
+
'stops' => ManifestStop::collection($this->whenLoaded('stops')),
|
|
48
|
+
'updated_at' => $this->updated_at,
|
|
49
|
+
'created_at' => $this->created_at,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
6
|
+
use Fleetbase\Support\Http;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Public projection of a ManifestStop.
|
|
10
|
+
*
|
|
11
|
+
* Carries the place inline rather than as an id. A driver app rendering a route
|
|
12
|
+
* needs a name and a coordinate for every stop, and making it resolve each one
|
|
13
|
+
* separately turns a route of twenty stops into twenty-one requests.
|
|
14
|
+
*
|
|
15
|
+
* `sequence` is the order to drive them in, and is the field a re-sequence
|
|
16
|
+
* rewrites.
|
|
17
|
+
*/
|
|
18
|
+
class ManifestStop extends FleetbaseResource
|
|
19
|
+
{
|
|
20
|
+
/**
|
|
21
|
+
* Transform the resource into an array.
|
|
22
|
+
*
|
|
23
|
+
* @param \Illuminate\Http\Request $request
|
|
24
|
+
*/
|
|
25
|
+
public function toArray($request): array
|
|
26
|
+
{
|
|
27
|
+
return [
|
|
28
|
+
'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id),
|
|
29
|
+
'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
|
|
30
|
+
'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
|
|
31
|
+
'status' => $this->status,
|
|
32
|
+
'sequence' => $this->sequence,
|
|
33
|
+
'estimated_arrival' => $this->estimated_arrival,
|
|
34
|
+
'actual_arrival' => $this->actual_arrival,
|
|
35
|
+
'distance_from_prev_m' => $this->distance_from_prev_m,
|
|
36
|
+
'duration_from_prev_s' => $this->duration_from_prev_s,
|
|
37
|
+
'place' => new Place($this->whenLoaded('place')),
|
|
38
|
+
'order' => $this->when($this->relationLoaded('order') && $this->order, fn () => [
|
|
39
|
+
'id' => data_get($this->order, 'public_id'),
|
|
40
|
+
'tracking_number' => data_get($this->order, 'trackingNumber.tracking_number'),
|
|
41
|
+
'status' => data_get($this->order, 'status'),
|
|
42
|
+
]),
|
|
43
|
+
'updated_at' => $this->updated_at,
|
|
44
|
+
'created_at' => $this->created_at,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
}
|