@fleetbase/fleetops-engine 0.6.47 → 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/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/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 +1 -1
- 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 +40 -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;
|
|
@@ -121,7 +122,7 @@ class OrderController extends FleetOpsController
|
|
|
121
122
|
|
|
122
123
|
if ($resolvedOrderConfig) {
|
|
123
124
|
$input['order_config_uuid'] = $resolvedOrderConfig->uuid;
|
|
124
|
-
$input['type']
|
|
125
|
+
$input['type'] = $resolvedOrderConfig->key;
|
|
125
126
|
} elseif (!isset($input['type'])) {
|
|
126
127
|
$input['type'] = 'transport';
|
|
127
128
|
}
|
|
@@ -762,7 +763,7 @@ class OrderController extends FleetOpsController
|
|
|
762
763
|
return response()->error('No order found.');
|
|
763
764
|
}
|
|
764
765
|
|
|
765
|
-
$waypoint
|
|
766
|
+
$waypoint = $request->filled('waypoint') ? Waypoint::findByPlace($request->input('waypoint'), $order) : null;
|
|
766
767
|
$orderConfig = $order->ensureOrderConfig();
|
|
767
768
|
if (!$orderConfig) {
|
|
768
769
|
return response()->error('No order config found for order.');
|
|
@@ -792,33 +793,57 @@ class OrderController extends FleetOpsController
|
|
|
792
793
|
*
|
|
793
794
|
* @return \Illuminate\Http\Response
|
|
794
795
|
*/
|
|
795
|
-
public function trackerInfo(string $id)
|
|
796
|
+
public function trackerInfo(Request $request, string $id)
|
|
796
797
|
{
|
|
797
798
|
$order = Order::findById($id);
|
|
798
799
|
if (!$order) {
|
|
799
800
|
return response()->error('No order found.');
|
|
800
801
|
}
|
|
801
802
|
|
|
802
|
-
|
|
803
|
-
$cacheKey = "order:{$order->uuid}:tracker";
|
|
804
|
-
$trackerInfo = Cache::remember($cacheKey, 30, function () use ($order) {
|
|
805
|
-
return $order->tracker()->toArray();
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
return response()->json($trackerInfo);
|
|
803
|
+
return response()->json($order->tracker()->toArray($request->only(['provider', 'fallbacks', 'traffic_enabled'])));
|
|
809
804
|
}
|
|
810
805
|
|
|
811
|
-
public function waypointEtas(string $id)
|
|
806
|
+
public function waypointEtas(Request $request, string $id)
|
|
812
807
|
{
|
|
813
808
|
$order = Order::findById($id);
|
|
814
809
|
if (!$order) {
|
|
815
810
|
return response()->error('No order found.');
|
|
816
811
|
}
|
|
817
812
|
|
|
818
|
-
|
|
819
|
-
|
|
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
|
+
}
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
$order->driverAssigned->notify(new OrderPing($order));
|
|
820
839
|
|
|
821
|
-
|
|
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
|
+
}
|
|
822
847
|
}
|
|
823
848
|
|
|
824
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);
|
|
@@ -555,15 +555,15 @@ class Payload extends Model
|
|
|
555
555
|
}
|
|
556
556
|
|
|
557
557
|
$this->loadMissing('waypointMarkers');
|
|
558
|
-
$keptWaypointIds
|
|
558
|
+
$keptWaypointIds = [];
|
|
559
559
|
$availableWaypointMarkers = $this->waypointMarkers()->get();
|
|
560
560
|
|
|
561
561
|
foreach ($waypoints as $index => $attributes) {
|
|
562
|
-
$raw
|
|
563
|
-
$type
|
|
564
|
-
$customerUuidIn
|
|
562
|
+
$raw = $attributes;
|
|
563
|
+
$type = data_get($raw, 'type', 'dropoff');
|
|
564
|
+
$customerUuidIn = data_get($raw, 'customer_uuid');
|
|
565
565
|
$customerPubIdIn = data_get($raw, 'customer_id');
|
|
566
|
-
$customerType
|
|
566
|
+
$customerType = data_get($raw, 'customer_type', 'fleetops:contact');
|
|
567
567
|
|
|
568
568
|
if (Utils::isset($attributes, 'place') && is_array(Utils::get($attributes, 'place'))) {
|
|
569
569
|
$attributes = Utils::get($attributes, 'place');
|
|
@@ -599,12 +599,12 @@ class Payload extends Model
|
|
|
599
599
|
$placeUuid = $place->uuid;
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
$customerUuid
|
|
602
|
+
$customerUuid = null;
|
|
603
603
|
$customerTypeNamespace = null;
|
|
604
604
|
|
|
605
605
|
if ($customerType) {
|
|
606
606
|
$customerTypeNamespace = Utils::getMutationType($customerType);
|
|
607
|
-
$customerModel
|
|
607
|
+
$customerModel = app($customerTypeNamespace);
|
|
608
608
|
|
|
609
609
|
if ($customerUuidIn && $customerModel->where('uuid', $customerUuidIn)->exists()) {
|
|
610
610
|
$customerUuid = $customerUuidIn;
|
|
@@ -642,7 +642,7 @@ class Payload extends Model
|
|
|
642
642
|
continue;
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
-
$waypointRecord
|
|
645
|
+
$waypointRecord = Waypoint::create($values);
|
|
646
646
|
$keptWaypointIds[] = $waypointRecord->uuid;
|
|
647
647
|
}
|
|
648
648
|
|
|
@@ -21,7 +21,6 @@ use Fleetbase\Traits\Searchable;
|
|
|
21
21
|
use Fleetbase\Traits\SendsWebhooks;
|
|
22
22
|
use Fleetbase\Traits\TracksApiCredential;
|
|
23
23
|
use Illuminate\Support\Carbon;
|
|
24
|
-
use Illuminate\Support\Facades\DB;
|
|
25
24
|
use Illuminate\Support\Str;
|
|
26
25
|
|
|
27
26
|
class Place extends Model
|
|
@@ -383,8 +382,6 @@ class Place extends Model
|
|
|
383
382
|
|
|
384
383
|
/**
|
|
385
384
|
* Create a new Place instance from a geocoding lookup.
|
|
386
|
-
*
|
|
387
|
-
* @return array
|
|
388
385
|
*/
|
|
389
386
|
public static function getValuesFromGeocodingLookup(string $address): array
|
|
390
387
|
{
|
|
@@ -740,7 +740,7 @@ class ServiceRate extends Model
|
|
|
740
740
|
|
|
741
741
|
if ($this->isAlgorithm()) {
|
|
742
742
|
$resolvedEndpointCount = $endpointCount ?? $this->inferEndpointCountFromStops($waypoints);
|
|
743
|
-
$rateFee
|
|
743
|
+
$rateFee = $this->normalizeCalculatedMoney(Algo::exec(
|
|
744
744
|
$this->algorithm,
|
|
745
745
|
$this->buildAlgorithmVariables(
|
|
746
746
|
$entities,
|