@fleetbase/fleetops-engine 0.6.46 → 0.6.48
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/admin/routing-settings.hbs +71 -0
- package/addon/components/admin/routing-settings.js +124 -0
- package/addon/components/customer/orders.js +1 -1
- package/addon/components/display-place.hbs +1 -1
- package/addon/components/map/order-list-overlay/order.hbs +18 -3
- package/addon/components/map/toolbar.hbs +2 -2
- package/addon/components/map/toolbar.js +10 -2
- package/addon/components/orchestrator/phase-builder.hbs +28 -5
- package/addon/components/orchestrator/phase-builder.js +21 -1
- package/addon/components/orchestrator/resource-panel.hbs +19 -1
- package/addon/components/orchestrator/resource-panel.js +4 -0
- package/addon/components/orchestrator-workbench.js +3 -1
- package/addon/components/order/details/purchase-rate.hbs +19 -22
- package/addon/components/order/details/route.hbs +26 -1
- package/addon/components/order/details/route.js +50 -0
- package/addon/components/order/details/tracking.hbs +171 -27
- package/addon/components/order/details/tracking.js +457 -0
- package/addon/components/order/form/details.hbs +1 -2
- package/addon/components/order/form/details.js +6 -0
- package/addon/components/order/form/route.js +9 -1
- package/addon/components/order/kanban-card.hbs +18 -3
- package/addon/components/order-list-overlay/order.hbs +18 -3
- package/addon/components/order-progress-card.hbs +21 -6
- package/addon/components/order-tracking-lookup.hbs +7 -7
- package/addon/components/order-tracking-lookup.js +1 -1
- package/addon/components/route-list.hbs +78 -8
- package/addon/components/route-list.js +48 -6
- package/addon/components/tracking-stop-progress.hbs +45 -0
- package/addon/components/tracking-stop-progress.js +79 -0
- package/addon/controllers/settings/payments/index.js +1 -1
- package/addon/controllers/settings/routing.js +98 -1
- package/addon/helpers/format-duration.js +38 -0
- package/addon/routes/application.js +6 -0
- package/addon/services/order-list-overlay.js +2 -2
- package/addon/styles/fleetops-engine.css +733 -0
- package/addon/templates/settings/routing.hbs +60 -2
- package/app/components/tracking-stop-progress.js +1 -0
- package/app/helpers/format-duration.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +7 -0
- package/server/config/fleetops.php +32 -11
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
- package/server/seeders/VroomOrchestrationSeeder.php +402 -0
- package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +69 -15
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +119 -12
- package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
- package/server/src/Models/Contact.php +122 -13
- package/server/src/Models/Order.php +1 -1
- package/server/src/Models/Payload.php +8 -8
- package/server/src/Models/Place.php +0 -3
- package/server/src/Models/ServiceRate.php +1 -1
- package/server/src/Observers/ContactObserver.php +8 -0
- package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +330 -38
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
- package/server/src/Providers/FleetOpsServiceProvider.php +27 -0
- package/server/src/Support/LiveOrderQuery.php +75 -0
- package/server/src/Support/Metrics.php +15 -0
- package/server/src/Support/OSRM.php +11 -6
- package/server/src/Support/OrderTracker.php +7 -582
- package/server/src/Support/Utils.php +1 -1
- package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
- package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
- package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
- package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
- package/server/src/Tracking/README.md +25 -0
- package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
- package/server/src/Tracking/TrackingContext.php +63 -0
- package/server/src/Tracking/TrackingContextBuilder.php +204 -0
- package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
- package/server/src/Tracking/TrackingOptions.php +49 -0
- package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
- package/server/src/Tracking/TrackingProviderManager.php +89 -0
- package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
- package/server/src/Tracking/TrackingProviderResult.php +20 -0
- package/server/src/Tracking/TrackingStop.php +59 -0
- package/server/src/routes.php +11 -0
- package/server/tests/CapacityAllocationEngineTest.php +108 -0
- package/server/tests/LiveOrderQueryTest.php +34 -0
- package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
- package/server/tests/OrchestratorConsumableApiTest.php +74 -0
- package/server/tests/PingDriverEndpointTest.php +19 -0
- package/server/tests/TrackingIntelligenceTest.php +176 -0
- package/server/tests/VroomOrchestrationEngineTest.php +159 -0
- package/translations/en-us.yaml +4 -0
|
@@ -13,6 +13,7 @@ use Fleetbase\FleetOps\Models\Place;
|
|
|
13
13
|
use Fleetbase\FleetOps\Models\Route;
|
|
14
14
|
use Fleetbase\FleetOps\Models\Vehicle;
|
|
15
15
|
use Fleetbase\FleetOps\Support\LiveCacheService;
|
|
16
|
+
use Fleetbase\FleetOps\Support\LiveOrderQuery;
|
|
16
17
|
use Fleetbase\Http\Controllers\Controller;
|
|
17
18
|
use Illuminate\Http\Request;
|
|
18
19
|
|
|
@@ -110,50 +111,13 @@ class LiveController extends Controller
|
|
|
110
111
|
];
|
|
111
112
|
|
|
112
113
|
return LiveCacheService::remember('orders', $cacheParams, function () use ($exclude, $active, $unassigned) {
|
|
113
|
-
$query =
|
|
114
|
-
|
|
115
|
-
$
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
$q->orWhereHas('pickup');
|
|
119
|
-
$q->orWhereHas('dropoff');
|
|
120
|
-
}
|
|
121
|
-
);
|
|
122
|
-
})
|
|
123
|
-
->whereNotIn('status', ['canceled', 'completed', 'expired'])
|
|
124
|
-
->whereHas('trackingNumber')
|
|
125
|
-
->whereHas('trackingStatuses')
|
|
126
|
-
->whereNotIn('public_id', $exclude)
|
|
127
|
-
->whereNull('deleted_at')
|
|
128
|
-
->applyDirectivesForPermissions('fleet-ops list order')
|
|
129
|
-
->with([
|
|
130
|
-
'payload.entities',
|
|
131
|
-
'payload.dropoff',
|
|
132
|
-
'payload.pickup',
|
|
133
|
-
'payload.return',
|
|
134
|
-
'payload.firstWaypointMarker.place',
|
|
135
|
-
'payload.lastWaypointMarker.place',
|
|
136
|
-
'trackingNumber',
|
|
137
|
-
'trackingStatuses',
|
|
138
|
-
'driverAssigned' => function ($query) {
|
|
139
|
-
$query->without(['jobs', 'currentJob']);
|
|
140
|
-
},
|
|
141
|
-
'vehicleAssigned' => function ($query) {
|
|
142
|
-
$query->without(['fleets', 'vendor']);
|
|
143
|
-
},
|
|
144
|
-
'customer',
|
|
145
|
-
'facilitator',
|
|
114
|
+
$query = LiveOrderQuery::make(session('company'), [
|
|
115
|
+
'exclude' => $exclude,
|
|
116
|
+
'active' => $active,
|
|
117
|
+
'unassigned' => $unassigned,
|
|
118
|
+
'with_relations' => true,
|
|
146
119
|
]);
|
|
147
120
|
|
|
148
|
-
if ($active) {
|
|
149
|
-
$query->whereHas('driverAssigned');
|
|
150
|
-
$query->whereNotIn('status', ['created', 'completed', 'expired', 'order_canceled', 'canceled', 'pending']);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if ($unassigned) {
|
|
154
|
-
$query->whereNull('driver_assigned_uuid');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
121
|
$query->limit(60); // max 60 latest
|
|
158
122
|
$query->latest();
|
|
159
123
|
|
|
@@ -573,7 +573,7 @@ class OrchestrationController extends Controller
|
|
|
573
573
|
$isMulti = $orderType === 'multi_waypoint';
|
|
574
574
|
|
|
575
575
|
// ── Resolve OrderConfig ───────────────────────────────────────
|
|
576
|
-
$orderConfigUuid
|
|
576
|
+
$orderConfigUuid = null;
|
|
577
577
|
$resolvedOrderConfig = null;
|
|
578
578
|
if (!empty($firstRow['type'])) {
|
|
579
579
|
$resolvedOrderConfig = OrderConfig::resolveFromIdentifier($firstRow['type']);
|
|
@@ -26,16 +26,17 @@ use Fleetbase\FleetOps\Models\Proof;
|
|
|
26
26
|
use Fleetbase\FleetOps\Models\ServiceQuote;
|
|
27
27
|
use Fleetbase\FleetOps\Models\TrackingStatus;
|
|
28
28
|
use Fleetbase\FleetOps\Models\Waypoint;
|
|
29
|
+
use Fleetbase\FleetOps\Notifications\OrderPing;
|
|
29
30
|
use Fleetbase\FleetOps\Support\Utils;
|
|
30
31
|
use Fleetbase\Http\Requests\ExportRequest;
|
|
31
32
|
use Fleetbase\Http\Requests\Internal\BulkActionRequest;
|
|
32
33
|
use Fleetbase\Models\File;
|
|
33
34
|
use Fleetbase\Models\Type;
|
|
35
|
+
use Fleetbase\Support\Auth;
|
|
34
36
|
use Fleetbase\Support\TemplateString;
|
|
35
37
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
36
38
|
use Illuminate\Database\QueryException;
|
|
37
39
|
use Illuminate\Http\Request;
|
|
38
|
-
use Illuminate\Support\Facades\Cache;
|
|
39
40
|
use Illuminate\Support\Facades\DB;
|
|
40
41
|
use Illuminate\Support\Facades\Validator;
|
|
41
42
|
use Illuminate\Support\Str;
|
|
@@ -97,6 +98,8 @@ class OrderController extends FleetOpsController
|
|
|
97
98
|
function ($request, &$input) {
|
|
98
99
|
$serviceQuote = ServiceQuote::resolveFromRequest($request);
|
|
99
100
|
|
|
101
|
+
$this->normalizeCustomerType($input);
|
|
102
|
+
|
|
100
103
|
// if service quote is applied, resolve it
|
|
101
104
|
if ($serviceQuote instanceof ServiceQuote && $serviceQuote->fromIntegratedVendor()) {
|
|
102
105
|
// create order with integrated vendor, then resume fleetbase order creation
|
|
@@ -119,7 +122,7 @@ class OrderController extends FleetOpsController
|
|
|
119
122
|
|
|
120
123
|
if ($resolvedOrderConfig) {
|
|
121
124
|
$input['order_config_uuid'] = $resolvedOrderConfig->uuid;
|
|
122
|
-
$input['type']
|
|
125
|
+
$input['type'] = $resolvedOrderConfig->key;
|
|
123
126
|
} elseif (!isset($input['type'])) {
|
|
124
127
|
$input['type'] = 'transport';
|
|
125
128
|
}
|
|
@@ -218,6 +221,33 @@ class OrderController extends FleetOpsController
|
|
|
218
221
|
}
|
|
219
222
|
}
|
|
220
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Resolve the concrete polymorphic customer type from the submitted UUID.
|
|
226
|
+
*/
|
|
227
|
+
protected function normalizeCustomerType(array &$input): void
|
|
228
|
+
{
|
|
229
|
+
$customerUuid = data_get($input, 'customer_uuid') ?? data_get($input, 'customer.uuid');
|
|
230
|
+
if (!$customerUuid) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
$customer = Utils::getUuid(
|
|
235
|
+
['contacts', 'vendors'],
|
|
236
|
+
[
|
|
237
|
+
'uuid' => $customerUuid,
|
|
238
|
+
'company_uuid' => session('company'),
|
|
239
|
+
],
|
|
240
|
+
[
|
|
241
|
+
'with_table' => true,
|
|
242
|
+
]
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (is_array($customer)) {
|
|
246
|
+
$input['customer_uuid'] = Utils::get($customer, 'uuid');
|
|
247
|
+
$input['customer_type'] = Utils::getModelClassName(Utils::get($customer, 'table'));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
221
251
|
/**
|
|
222
252
|
* Route which enables editing of an order route.
|
|
223
253
|
*
|
|
@@ -733,7 +763,7 @@ class OrderController extends FleetOpsController
|
|
|
733
763
|
return response()->error('No order found.');
|
|
734
764
|
}
|
|
735
765
|
|
|
736
|
-
$waypoint
|
|
766
|
+
$waypoint = $request->filled('waypoint') ? Waypoint::findByPlace($request->input('waypoint'), $order) : null;
|
|
737
767
|
$orderConfig = $order->ensureOrderConfig();
|
|
738
768
|
if (!$orderConfig) {
|
|
739
769
|
return response()->error('No order config found for order.');
|
|
@@ -763,33 +793,57 @@ class OrderController extends FleetOpsController
|
|
|
763
793
|
*
|
|
764
794
|
* @return \Illuminate\Http\Response
|
|
765
795
|
*/
|
|
766
|
-
public function trackerInfo(string $id)
|
|
796
|
+
public function trackerInfo(Request $request, string $id)
|
|
767
797
|
{
|
|
768
798
|
$order = Order::findById($id);
|
|
769
799
|
if (!$order) {
|
|
770
800
|
return response()->error('No order found.');
|
|
771
801
|
}
|
|
772
802
|
|
|
773
|
-
|
|
774
|
-
$cacheKey = "order:{$order->uuid}:tracker";
|
|
775
|
-
$trackerInfo = Cache::remember($cacheKey, 30, function () use ($order) {
|
|
776
|
-
return $order->tracker()->toArray();
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
return response()->json($trackerInfo);
|
|
803
|
+
return response()->json($order->tracker()->toArray($request->only(['provider', 'fallbacks', 'traffic_enabled'])));
|
|
780
804
|
}
|
|
781
805
|
|
|
782
|
-
public function waypointEtas(string $id)
|
|
806
|
+
public function waypointEtas(Request $request, string $id)
|
|
783
807
|
{
|
|
784
808
|
$order = Order::findById($id);
|
|
785
809
|
if (!$order) {
|
|
786
810
|
return response()->error('No order found.');
|
|
787
811
|
}
|
|
788
812
|
|
|
789
|
-
|
|
790
|
-
|
|
813
|
+
return response()->json($order->tracker()->eta($request->only(['provider', 'fallbacks', 'traffic_enabled'])));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Ping the assigned driver to refresh/order attention in the driver app.
|
|
818
|
+
*
|
|
819
|
+
* @return \Illuminate\Http\JsonResponse
|
|
820
|
+
*/
|
|
821
|
+
public function pingDriver(string $id)
|
|
822
|
+
{
|
|
823
|
+
if (!Auth::can('fleet-ops update order')) {
|
|
824
|
+
return response()->error('Unauthorized.', 403);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
$order = Order::findByIdOrFail($id, ['driverAssigned']);
|
|
829
|
+
} catch (ModelNotFoundException $e) {
|
|
830
|
+
return response()->error('Order resource not found.', 404);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (!$order->driverAssigned) {
|
|
834
|
+
return response()->error('Order does not have an assigned driver.', 422);
|
|
835
|
+
}
|
|
791
836
|
|
|
792
|
-
|
|
837
|
+
try {
|
|
838
|
+
$order->driverAssigned->notify(new OrderPing($order));
|
|
839
|
+
|
|
840
|
+
return response()->json([
|
|
841
|
+
'status' => 'ok',
|
|
842
|
+
'message' => 'Driver app ping sent.',
|
|
843
|
+
]);
|
|
844
|
+
} catch (\Throwable $e) {
|
|
845
|
+
return response()->error('Unable to ping driver app.', 500);
|
|
846
|
+
}
|
|
793
847
|
}
|
|
794
848
|
|
|
795
849
|
/**
|
|
@@ -226,7 +226,7 @@ class ServiceQuoteController extends FleetOpsController
|
|
|
226
226
|
$entities = collect($entities)->mapInto(Entity::class);
|
|
227
227
|
|
|
228
228
|
// should all be Place like
|
|
229
|
-
$waypoints
|
|
229
|
+
$waypoints = collect([$pickup, ...$waypoints, $dropoff])->filter();
|
|
230
230
|
$endpointCount = (int) ($pickup instanceof Place) + (int) ($dropoff instanceof Place);
|
|
231
231
|
|
|
232
232
|
// if facilitator is an integrated partner resolve service quotes from bridge
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry;
|
|
5
6
|
use Fleetbase\Http\Controllers\Controller;
|
|
6
7
|
use Fleetbase\Models\Setting;
|
|
7
8
|
use Fleetbase\Support\Auth;
|
|
@@ -211,12 +212,12 @@ class SettingController extends Controller
|
|
|
211
212
|
{
|
|
212
213
|
$routingSettings = Setting::lookupCompany('routing', ['router' => 'osrm', 'unit' => 'km']);
|
|
213
214
|
|
|
214
|
-
$displayEngine
|
|
215
|
-
$optimizationEngine
|
|
216
|
-
$routingSettings['router']
|
|
217
|
-
$routingSettings['display_engine']
|
|
218
|
-
$routingSettings['optimization_engine']
|
|
219
|
-
$routingSettings['routing_display_engine']
|
|
215
|
+
$displayEngine = data_get($routingSettings, 'display_engine', data_get($routingSettings, 'routing_display_engine', data_get($routingSettings, 'router', 'osrm')));
|
|
216
|
+
$optimizationEngine = data_get($routingSettings, 'optimization_engine', data_get($routingSettings, 'routing_optimization_engine', $displayEngine));
|
|
217
|
+
$routingSettings['router'] = $displayEngine;
|
|
218
|
+
$routingSettings['display_engine'] = $displayEngine;
|
|
219
|
+
$routingSettings['optimization_engine'] = $optimizationEngine;
|
|
220
|
+
$routingSettings['routing_display_engine'] = $displayEngine;
|
|
220
221
|
$routingSettings['routing_optimization_engine'] = $optimizationEngine;
|
|
221
222
|
|
|
222
223
|
// always default to km if no unit is set
|
|
@@ -227,6 +228,87 @@ class SettingController extends Controller
|
|
|
227
228
|
return response()->json($routingSettings);
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Save order tracking intelligence settings.
|
|
233
|
+
*
|
|
234
|
+
* @param Request $request the HTTP request object containing tracking settings
|
|
235
|
+
*
|
|
236
|
+
* @return \Illuminate\Http\JsonResponse
|
|
237
|
+
*/
|
|
238
|
+
public function saveTrackingSettings(Request $request)
|
|
239
|
+
{
|
|
240
|
+
$config = $this->trackingDefaults();
|
|
241
|
+
$fallbacks = $request->input('fallbacks', data_get($config, 'fallbacks', ['osrm', 'calculated']));
|
|
242
|
+
if (is_string($fallbacks)) {
|
|
243
|
+
$fallbacks = array_values(array_filter(array_map('trim', explode(',', $fallbacks))));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
Setting::configureCompany('tracking', [
|
|
247
|
+
'provider' => $request->input('provider', data_get($config, 'provider', 'google_routes')),
|
|
248
|
+
'fallbacks' => $fallbacks,
|
|
249
|
+
'traffic_enabled' => $request->boolean('traffic_enabled', data_get($config, 'traffic_enabled', true)),
|
|
250
|
+
'cache_ttl_seconds' => (int) $request->input('cache_ttl_seconds', data_get($config, 'cache_ttl_seconds', 60)),
|
|
251
|
+
'route_cache_ttl_seconds' => (int) $request->input('route_cache_ttl_seconds', data_get($config, 'route_cache_ttl_seconds', 600)),
|
|
252
|
+
'stale_location_threshold_seconds' => (int) $request->input('stale_location_threshold_seconds', data_get($config, 'stale_location_threshold_seconds', 300)),
|
|
253
|
+
'default_vehicle_speed_kph' => (float) $request->input('default_vehicle_speed_kph', data_get($config, 'default_vehicle_speed_kph', 35)),
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
return response()->json([
|
|
257
|
+
'status' => 'ok',
|
|
258
|
+
'message' => 'Tracking settings succesfully saved.',
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Retrieve order tracking intelligence settings.
|
|
264
|
+
*
|
|
265
|
+
* @return \Illuminate\Http\JsonResponse
|
|
266
|
+
*/
|
|
267
|
+
public function getTrackingSettings()
|
|
268
|
+
{
|
|
269
|
+
$config = $this->trackingDefaults();
|
|
270
|
+
$trackingSettings = Setting::lookupCompany('tracking', [
|
|
271
|
+
'provider' => data_get($config, 'provider', 'google_routes'),
|
|
272
|
+
'fallbacks' => data_get($config, 'fallbacks', ['osrm', 'calculated']),
|
|
273
|
+
'traffic_enabled' => data_get($config, 'traffic_enabled', true),
|
|
274
|
+
'cache_ttl_seconds' => data_get($config, 'cache_ttl_seconds', 60),
|
|
275
|
+
'route_cache_ttl_seconds' => data_get($config, 'route_cache_ttl_seconds', 600),
|
|
276
|
+
'stale_location_threshold_seconds' => data_get($config, 'stale_location_threshold_seconds', 300),
|
|
277
|
+
'default_vehicle_speed_kph' => data_get($config, 'default_vehicle_speed_kph', 35),
|
|
278
|
+
]);
|
|
279
|
+
$trackingSettings['providers'] = $this->trackingProviderOptions();
|
|
280
|
+
|
|
281
|
+
return response()->json($trackingSettings);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
public function getAdminTrackingSettings()
|
|
285
|
+
{
|
|
286
|
+
return response()->json(array_merge($this->trackingDefaults(), [
|
|
287
|
+
'providers' => $this->trackingProviderOptions(),
|
|
288
|
+
]));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public function saveAdminTrackingSettings(Request $request)
|
|
292
|
+
{
|
|
293
|
+
$config = config('fleetops.tracking', []);
|
|
294
|
+
$fallbacks = $request->input('fallbacks', data_get($config, 'fallbacks', ['osrm', 'calculated']));
|
|
295
|
+
if (is_string($fallbacks)) {
|
|
296
|
+
$fallbacks = array_values(array_filter(array_map('trim', explode(',', $fallbacks))));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Setting::configure('fleet-ops.tracking-settings', [
|
|
300
|
+
'provider' => $request->input('provider', data_get($config, 'provider', 'google_routes')),
|
|
301
|
+
'fallbacks' => $fallbacks,
|
|
302
|
+
'traffic_enabled' => $request->boolean('traffic_enabled', data_get($config, 'traffic_enabled', true)),
|
|
303
|
+
'cache_ttl_seconds' => (int) $request->input('cache_ttl_seconds', data_get($config, 'cache_ttl_seconds', 60)),
|
|
304
|
+
'route_cache_ttl_seconds' => (int) $request->input('route_cache_ttl_seconds', data_get($config, 'route_cache_ttl_seconds', 600)),
|
|
305
|
+
'stale_location_threshold_seconds' => (int) $request->input('stale_location_threshold_seconds', data_get($config, 'stale_location_threshold_seconds', 300)),
|
|
306
|
+
'default_vehicle_speed_kph' => (float) $request->input('default_vehicle_speed_kph', data_get($config, 'default_vehicle_speed_kph', 35)),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
return response()->json($this->getAdminTrackingSettings()->getData(true));
|
|
310
|
+
}
|
|
311
|
+
|
|
230
312
|
/**
|
|
231
313
|
* Retrieve and return the map provider settings for the current company.
|
|
232
314
|
*
|
|
@@ -244,15 +326,15 @@ class SettingController extends Controller
|
|
|
244
326
|
'mapProvider' => 'leaflet',
|
|
245
327
|
];
|
|
246
328
|
|
|
247
|
-
$systemMapSettings
|
|
248
|
-
$mapSettings
|
|
329
|
+
$systemMapSettings = Setting::lookup('fleet-ops.map-settings', []);
|
|
330
|
+
$mapSettings = Setting::lookupFromCompany('fleet-ops.map-settings', $defaults);
|
|
249
331
|
$mapSettings['mapProvider'] = data_get($mapSettings, 'mapProvider') ?: data_get($systemMapSettings, 'mapProvider', 'leaflet');
|
|
250
332
|
|
|
251
333
|
// Source the Google Maps API key from the system-level services config
|
|
252
334
|
// that is managed by the core-api admin settings panel. This ensures a
|
|
253
335
|
// single source of truth and avoids duplicating key management.
|
|
254
336
|
$mapSettings['googleMapsApiKey'] = config('services.google_maps.api_key', env('GOOGLE_MAPS_API_KEY', ''));
|
|
255
|
-
$mapSettings['googleMapsMapId']
|
|
337
|
+
$mapSettings['googleMapsMapId'] = data_get($systemMapSettings, 'googleMapsMapId', '');
|
|
256
338
|
|
|
257
339
|
return response()->json($mapSettings);
|
|
258
340
|
}
|
|
@@ -289,7 +371,7 @@ class SettingController extends Controller
|
|
|
289
371
|
public function getAdminMapSettings()
|
|
290
372
|
{
|
|
291
373
|
$defaults = [
|
|
292
|
-
'mapProvider'
|
|
374
|
+
'mapProvider' => 'leaflet',
|
|
293
375
|
'googleMapsMapId' => '',
|
|
294
376
|
];
|
|
295
377
|
|
|
@@ -299,13 +381,13 @@ class SettingController extends Controller
|
|
|
299
381
|
public function saveAdminMapSettings(Request $request)
|
|
300
382
|
{
|
|
301
383
|
$allowedProviders = ['leaflet', 'google'];
|
|
302
|
-
$mapProvider
|
|
384
|
+
$mapProvider = $request->input('mapProvider', 'leaflet');
|
|
303
385
|
if (!in_array($mapProvider, $allowedProviders)) {
|
|
304
386
|
$mapProvider = 'leaflet';
|
|
305
387
|
}
|
|
306
388
|
|
|
307
389
|
$settings = [
|
|
308
|
-
'mapProvider'
|
|
390
|
+
'mapProvider' => $mapProvider,
|
|
309
391
|
'googleMapsMapId' => (string) $request->input('googleMapsMapId', ''),
|
|
310
392
|
];
|
|
311
393
|
|
|
@@ -431,4 +513,29 @@ class SettingController extends Controller
|
|
|
431
513
|
'settings' => $normalized,
|
|
432
514
|
]);
|
|
433
515
|
}
|
|
516
|
+
|
|
517
|
+
protected function trackingDefaults(): array
|
|
518
|
+
{
|
|
519
|
+
$config = config('fleetops.tracking', []);
|
|
520
|
+
$systemSettings = Setting::lookup('fleet-ops.tracking-settings', []);
|
|
521
|
+
|
|
522
|
+
return array_merge($config, is_array($systemSettings) ? $systemSettings : []);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
protected function trackingProviderOptions(): array
|
|
526
|
+
{
|
|
527
|
+
$registry = app(TrackingProviderRegistry::class);
|
|
528
|
+
|
|
529
|
+
return collect($registry->all())->map(function ($provider, $key) {
|
|
530
|
+
$label = $key === 'osrm' ? 'OSRM' : str($key)->replace('_', ' ')->title()->toString();
|
|
531
|
+
|
|
532
|
+
return [
|
|
533
|
+
'key' => $key,
|
|
534
|
+
'name' => $label,
|
|
535
|
+
'value' => $key,
|
|
536
|
+
'label' => $label,
|
|
537
|
+
'capabilities' => $provider->capabilities()->toArray(),
|
|
538
|
+
];
|
|
539
|
+
})->values()->all();
|
|
540
|
+
}
|
|
434
541
|
}
|
|
@@ -19,8 +19,8 @@ class Payload extends FleetbaseResource
|
|
|
19
19
|
public function toArray($request): array
|
|
20
20
|
{
|
|
21
21
|
$isInternal = Http::isInternalRequest();
|
|
22
|
-
$pickup
|
|
23
|
-
$dropoff
|
|
22
|
+
$pickup = $this->index_pickup_place;
|
|
23
|
+
$dropoff = $this->index_dropoff_place;
|
|
24
24
|
|
|
25
25
|
return [
|
|
26
26
|
'id' => $this->when($isInternal, $this->id, $this->public_id),
|
|
@@ -5,6 +5,7 @@ namespace Fleetbase\FleetOps\Models;
|
|
|
5
5
|
use Fleetbase\Casts\Json;
|
|
6
6
|
use Fleetbase\FleetOps\Exceptions\UserAlreadyExistsException;
|
|
7
7
|
use Fleetbase\FleetOps\Support\Utils;
|
|
8
|
+
use Fleetbase\Models\CompanyUser;
|
|
8
9
|
use Fleetbase\Models\Invite;
|
|
9
10
|
use Fleetbase\Models\Model;
|
|
10
11
|
use Fleetbase\Models\User;
|
|
@@ -261,6 +262,14 @@ class Contact extends Model
|
|
|
261
262
|
return $this->type === 'customer';
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Determines if this contact is a Fleet-Ops customer.
|
|
267
|
+
*/
|
|
268
|
+
public function isCustomer(): bool
|
|
269
|
+
{
|
|
270
|
+
return $this->type === 'customer';
|
|
271
|
+
}
|
|
272
|
+
|
|
264
273
|
/**
|
|
265
274
|
* Creates a new contact from an import row.
|
|
266
275
|
*/
|
|
@@ -308,19 +317,33 @@ class Contact extends Model
|
|
|
308
317
|
public static function createUserFromContact(Contact $contact, bool $sendInvite = false, bool $update = false): User
|
|
309
318
|
{
|
|
310
319
|
// Check if user already exist with email or phone number
|
|
311
|
-
$existingUser =
|
|
312
|
-
|
|
313
|
-
$
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
320
|
+
$existingUser = null;
|
|
321
|
+
if ($contact->email || $contact->phone) {
|
|
322
|
+
$existingUser = User::where('company_uuid', $contact->company_uuid)
|
|
323
|
+
->where(function ($query) use ($contact) {
|
|
324
|
+
if ($contact->email) {
|
|
325
|
+
$query->where('email', $contact->email);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if ($contact->phone) {
|
|
329
|
+
$method = $contact->email ? 'orWhere' : 'where';
|
|
330
|
+
$query->{$method}('phone', Utils::formatPhoneNumber($contact->phone));
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
->whereNull('deleted_at')
|
|
334
|
+
->first();
|
|
335
|
+
}
|
|
336
|
+
|
|
318
337
|
if ($existingUser) {
|
|
319
338
|
// Check if existing user belongs to another contact
|
|
320
339
|
$existingUserContact = Contact::where(['user_uuid' => $existingUser->uuid, 'company_uuid' => $contact->company_uuid])->whereHas('user')->first();
|
|
321
340
|
if ($existingUserContact) {
|
|
322
341
|
throw new UserAlreadyExistsException('User already exists, try to assigning the user to this contact.', $existingUser);
|
|
323
342
|
} else {
|
|
343
|
+
if ($contact->isCustomer()) {
|
|
344
|
+
$contact->normalizeCustomerUser($existingUser);
|
|
345
|
+
}
|
|
346
|
+
|
|
324
347
|
// Assign the user to this contact instead
|
|
325
348
|
$contact->setAttribute('user_uuid', $existingUser->uuid);
|
|
326
349
|
if ($update) {
|
|
@@ -351,10 +374,10 @@ class Contact extends Model
|
|
|
351
374
|
$user->setType($contact->type);
|
|
352
375
|
|
|
353
376
|
// Assing to company
|
|
354
|
-
$user->assignCompany($contact->company, $
|
|
377
|
+
$user->assignCompany($contact->company, $contact->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
|
|
355
378
|
|
|
356
379
|
// Assign customer role
|
|
357
|
-
if ($
|
|
380
|
+
if ($contact->isCustomer()) {
|
|
358
381
|
$user->assignSingleRole('Fleet-Ops Customer');
|
|
359
382
|
}
|
|
360
383
|
|
|
@@ -386,6 +409,87 @@ class Contact extends Model
|
|
|
386
409
|
return $user;
|
|
387
410
|
}
|
|
388
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Normalize the linked user for a customer contact.
|
|
414
|
+
*/
|
|
415
|
+
public function normalizeCustomerUser(?User $user = null, bool $quiet = false): ?User
|
|
416
|
+
{
|
|
417
|
+
if (!$this->isCustomer()) {
|
|
418
|
+
return $user;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
$user ??= $this->getUser();
|
|
422
|
+
if (!$user) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if ($user->type !== 'customer') {
|
|
427
|
+
if ($quiet) {
|
|
428
|
+
$user->forceFill(['type' => 'customer'])->saveQuietly();
|
|
429
|
+
} else {
|
|
430
|
+
$user->setType('customer');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
$this->loadMissing('company');
|
|
435
|
+
if ($this->company) {
|
|
436
|
+
$companyUser = CompanyUser::firstOrCreate(
|
|
437
|
+
[
|
|
438
|
+
'company_uuid' => $this->company->uuid,
|
|
439
|
+
'user_uuid' => $user->uuid,
|
|
440
|
+
],
|
|
441
|
+
[
|
|
442
|
+
'company_uuid' => $this->company->uuid,
|
|
443
|
+
'user_uuid' => $user->uuid,
|
|
444
|
+
'status' => $user->status ?? 'active',
|
|
445
|
+
]
|
|
446
|
+
);
|
|
447
|
+
$user->forceFill(['company_uuid' => $this->company->uuid])->saveQuietly();
|
|
448
|
+
$user->setRelation('companyUser', $companyUser);
|
|
449
|
+
|
|
450
|
+
if ($companyUser) {
|
|
451
|
+
$companyUser->assignSingleRole('Fleet-Ops Customer');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
$this->setRelation('user', $user);
|
|
456
|
+
|
|
457
|
+
return $user;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Repair a historical customer contact and its linked user.
|
|
462
|
+
*/
|
|
463
|
+
public function repairCustomerTypeInvariant(bool $quiet = false): ?User
|
|
464
|
+
{
|
|
465
|
+
if (!$this->isCustomer()) {
|
|
466
|
+
$this->forceFill(['type' => 'customer'])->saveQuietly();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
$user = $this->getUser();
|
|
470
|
+
if (!$user && ($this->email || $this->phone)) {
|
|
471
|
+
if ($quiet) {
|
|
472
|
+
$this->loadMissing('company');
|
|
473
|
+
$user = User::create([
|
|
474
|
+
'company_uuid' => $this->company_uuid,
|
|
475
|
+
'name' => $this->name,
|
|
476
|
+
'email' => $this->email,
|
|
477
|
+
'phone' => $this->phone ? Utils::formatPhoneNumber($this->phone) : null,
|
|
478
|
+
'username' => Str::slug($this->name . '_' . Str::random(4), '_'),
|
|
479
|
+
'password' => Str::random(),
|
|
480
|
+
'timezone' => $this->company->timezone ?? date_default_timezone_get(),
|
|
481
|
+
'status' => 'pending',
|
|
482
|
+
]);
|
|
483
|
+
$user->forceFill(['type' => 'customer'])->saveQuietly();
|
|
484
|
+
$this->forceFill(['user_uuid' => $user->uuid])->saveQuietly();
|
|
485
|
+
} else {
|
|
486
|
+
$user = $this->createUser();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return $this->normalizeCustomerUser($user, $quiet);
|
|
491
|
+
}
|
|
492
|
+
|
|
389
493
|
/**
|
|
390
494
|
* Assigns a user to the company and optionally sends an invitation email.
|
|
391
495
|
*
|
|
@@ -406,16 +510,21 @@ class Contact extends Model
|
|
|
406
510
|
public function assignUser(User $user, bool $sendInvite = false): self
|
|
407
511
|
{
|
|
408
512
|
// Load company
|
|
409
|
-
$this->loadMissing('
|
|
513
|
+
$this->loadMissing('company');
|
|
514
|
+
|
|
515
|
+
if ($this->isCustomer() && $user->type !== 'customer') {
|
|
516
|
+
$user->setType('customer');
|
|
517
|
+
}
|
|
410
518
|
|
|
411
519
|
// Assing to company
|
|
412
|
-
$user->assignCompany($this->company, $this->
|
|
520
|
+
$user->assignCompany($this->company, $this->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
|
|
413
521
|
|
|
414
522
|
// Get the company user instance
|
|
415
523
|
$companyUser = $user->getCompanyUser($this->company);
|
|
416
524
|
|
|
417
|
-
|
|
418
|
-
|
|
525
|
+
if ($companyUser) {
|
|
526
|
+
$companyUser->assignSingleRole($this->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
|
|
527
|
+
}
|
|
419
528
|
|
|
420
529
|
// Set user to contact
|
|
421
530
|
$this->update(['user_uuid' => $user->uuid]);
|
|
@@ -1739,7 +1739,7 @@ class Order extends Model
|
|
|
1739
1739
|
}
|
|
1740
1740
|
}
|
|
1741
1741
|
|
|
1742
|
-
$company
|
|
1742
|
+
$company = $this->relationLoaded('company') ? $this->company : $this->company()->first();
|
|
1743
1743
|
$orderConfig = OrderConfig::defaultOrCreate($company);
|
|
1744
1744
|
if ($orderConfig instanceof OrderConfig) {
|
|
1745
1745
|
$orderConfig->setOrderContext($this);
|