@fleetbase/fleetops-engine 0.6.47 → 0.6.49
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/driver/details.hbs +5 -1
- package/addon/components/driver/form.hbs +11 -1
- package/addon/components/fleet-listing-panel.hbs +2 -2
- package/addon/components/fleet-listing-panel.js +10 -0
- package/addon/components/map/google-live-map.js +12 -73
- package/addon/components/map/leaflet-live-map.hbs +149 -33
- 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 +49 -5
- package/addon/components/orchestrator/phase-builder.js +34 -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 +4 -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.hbs +2 -2
- 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/components/widget/fleet-ops-key-metrics.hbs +19 -17
- package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
- package/addon/components/widget/fuel-efficiency.hbs +26 -0
- package/addon/components/widget/fuel-efficiency.js +43 -0
- package/addon/components/widget/geofence-violations.hbs +44 -0
- package/addon/components/widget/geofence-violations.js +55 -0
- package/addon/components/widget/issues-insights.hbs +43 -0
- package/addon/components/widget/issues-insights.js +62 -0
- package/addon/components/widget/kpi-active-orders.hbs +10 -0
- package/addon/components/widget/kpi-active-orders.js +5 -0
- package/addon/components/widget/kpi-aov.hbs +10 -0
- package/addon/components/widget/kpi-aov.js +5 -0
- package/addon/components/widget/kpi-distance.hbs +10 -0
- package/addon/components/widget/kpi-distance.js +5 -0
- package/addon/components/widget/kpi-drivers-online.hbs +10 -0
- package/addon/components/widget/kpi-drivers-online.js +5 -0
- package/addon/components/widget/kpi-earnings.hbs +10 -0
- package/addon/components/widget/kpi-earnings.js +5 -0
- package/addon/components/widget/kpi-open-issues.hbs +11 -0
- package/addon/components/widget/kpi-open-issues.js +5 -0
- package/addon/components/widget/kpi-tile.hbs +59 -0
- package/addon/components/widget/kpi-tile.js +168 -0
- package/addon/components/widget/live-fleet.hbs +122 -0
- package/addon/components/widget/live-fleet.js +365 -0
- package/addon/components/widget/maintenance-overview.hbs +45 -0
- package/addon/components/widget/maintenance-overview.js +40 -0
- package/addon/components/widget/on-time-delivery.hbs +27 -0
- package/addon/components/widget/on-time-delivery.js +73 -0
- package/addon/components/widget/operations-pulse.hbs +51 -0
- package/addon/components/widget/operations-pulse.js +173 -0
- package/addon/components/widget/orders-by-status.hbs +26 -0
- package/addon/components/widget/orders-by-status.js +54 -0
- package/addon/components/widget/revenue-trend.hbs +34 -0
- package/addon/components/widget/revenue-trend.js +67 -0
- package/addon/components/widget/top-drivers.hbs +44 -0
- package/addon/components/widget/top-drivers.js +54 -0
- package/addon/controllers/settings/routing.js +124 -1
- package/addon/extension.js +172 -3
- package/addon/helpers/format-duration.js +38 -0
- package/addon/routes/application.js +6 -0
- package/addon/services/map-adapter/google.js +13 -2
- package/addon/services/order-list-overlay.js +2 -2
- package/addon/styles/fleetops-engine.css +806 -46
- package/addon/templates/maintenance/equipment/index.hbs +1 -1
- package/addon/templates/maintenance/parts/index.hbs +1 -1
- package/addon/templates/maintenance/schedules/index.hbs +1 -1
- package/addon/templates/management/vehicles/index.hbs +1 -1
- package/addon/templates/settings/avatars.hbs +1 -1
- package/addon/templates/settings/custom-fields.hbs +1 -1
- package/addon/templates/settings/map.hbs +1 -1
- package/addon/templates/settings/navigator-app.hbs +1 -1
- package/addon/templates/settings/notifications.hbs +1 -1
- package/addon/templates/settings/orchestrator.hbs +1 -1
- package/addon/templates/settings/payments/index.hbs +1 -1
- package/addon/templates/settings/payments/onboard.hbs +1 -1
- package/addon/templates/settings/routing.hbs +82 -4
- package/addon/templates/settings/scheduling.hbs +1 -1
- package/addon/utils/live-map-card-content.js +171 -0
- package/app/components/tracking-stop-progress.js +1 -0
- package/app/components/widget/fuel-efficiency.js +1 -0
- package/app/components/widget/geofence-violations.js +1 -0
- package/app/components/widget/issues-insights.js +1 -0
- package/app/components/widget/kpi-active-orders.js +1 -0
- package/app/components/widget/kpi-aov.js +1 -0
- package/app/components/widget/kpi-distance.js +1 -0
- package/app/components/widget/kpi-drivers-online.js +1 -0
- package/app/components/widget/kpi-earnings.js +1 -0
- package/app/components/widget/kpi-open-issues.js +1 -0
- package/app/components/widget/kpi-tile.js +1 -0
- package/app/components/widget/live-fleet.js +1 -0
- package/app/components/widget/maintenance-overview.js +1 -0
- package/app/components/widget/on-time-delivery.js +1 -0
- package/app/components/widget/operations-pulse.js +1 -0
- package/app/components/widget/orders-by-status.js +1 -0
- package/app/components/widget/revenue-trend.js +1 -0
- package/app/components/widget/top-drivers.js +1 -0
- package/app/helpers/format-duration.js +1 -0
- package/app/utils/live-map-card-content.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +1 -0
- package/server/config/fleetops.php +32 -11
- package/server/config/telematics.php +111 -1
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
- package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
- package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
- package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
- package/server/seeders/Testing/FleetSeeder.php +341 -0
- package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
- package/server/seeders/Testing/NetworkSeeder.php +250 -0
- package/server/seeders/Testing/OrdersSeeder.php +214 -0
- package/server/seeders/Testing/TestingSeeder.php +40 -0
- package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -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/AnalyticsController.php +119 -0
- 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/MetricsController.php +82 -3
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
- package/server/src/Http/Resources/v1/Driver.php +2 -0
- package/server/src/Http/Resources/v1/Fleet.php +4 -0
- package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
- package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
- package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
- package/server/src/Models/Contact.php +122 -13
- package/server/src/Models/Driver.php +2 -0
- package/server/src/Models/Order.php +1 -1
- package/server/src/Models/OrderConfig.php +14 -11
- 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/Notifications/LateDeparture.php +48 -0
- package/server/src/Notifications/ProlongedStoppage.php +48 -0
- package/server/src/Notifications/RouteDeviation.php +48 -0
- package/server/src/Observers/ContactObserver.php +8 -0
- package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
- package/server/src/Providers/FleetOpsServiceProvider.php +32 -0
- package/server/src/Providers/NotificationServiceProvider.php +3 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
- package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
- package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
- package/server/src/Support/Analytics/IssuesInsights.php +67 -0
- package/server/src/Support/Analytics/LiveFleet.php +127 -0
- package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
- package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
- package/server/src/Support/Analytics/OperationsPulse.php +86 -0
- package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
- package/server/src/Support/Analytics/RevenueTrend.php +79 -0
- package/server/src/Support/Analytics/TopDrivers.php +92 -0
- package/server/src/Support/LiveOrderQuery.php +75 -0
- package/server/src/Support/Metrics/AbstractMetric.php +170 -0
- package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
- package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
- package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
- package/server/src/Support/Metrics/EarningsMetric.php +30 -0
- package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
- package/server/src/Support/Metrics/MoneyMetric.php +30 -0
- package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
- package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
- package/server/src/Support/Metrics/Registry.php +46 -0
- package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
- package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
- package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
- package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
- package/server/src/Support/Metrics.php +46 -234
- package/server/src/Support/OSRM.php +11 -6
- package/server/src/Support/OrderTracker.php +7 -582
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
- 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 +27 -0
- package/server/tests/AnalyticsRoutesTest.php +41 -0
- package/server/tests/CapacityAllocationEngineTest.php +108 -0
- package/server/tests/LiveOrderQueryTest.php +34 -0
- package/server/tests/MetricsRegistryTest.php +88 -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 +250 -0
- package/translations/en-us.yaml +41 -0
|
@@ -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;
|
|
@@ -111,17 +112,23 @@ class OrderController extends FleetOpsController
|
|
|
111
112
|
$input['integrated_vendor_order'] = $integratedVendorOrder;
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
// Normalize order config + type
|
|
115
|
-
//
|
|
116
|
-
$
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
// Normalize order config + type. Invalid explicit configs must
|
|
116
|
+
// not silently fall back to the default transport config.
|
|
117
|
+
$hasExplicitOrderConfig = !empty($input['order_config_uuid']);
|
|
118
|
+
$hasExplicitOrderType = !empty($input['type']);
|
|
119
|
+
$resolvedOrderConfig = OrderConfig::resolveFromIdentifier([$input['order_config_uuid'] ?? null, $input['type'] ?? null]);
|
|
120
|
+
|
|
121
|
+
if (!$resolvedOrderConfig && $hasExplicitOrderConfig) {
|
|
122
|
+
throw new FleetbaseRequestValidationException(['order_config_uuid' => 'The selected order config is invalid.']);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!$resolvedOrderConfig && !$hasExplicitOrderType) {
|
|
126
|
+
$resolvedOrderConfig = OrderConfig::defaultOrCreate();
|
|
119
127
|
}
|
|
120
|
-
$resolvedOrderConfig ??= OrderConfig::defaultOrCreate();
|
|
121
128
|
|
|
122
129
|
if ($resolvedOrderConfig) {
|
|
123
130
|
$input['order_config_uuid'] = $resolvedOrderConfig->uuid;
|
|
124
|
-
$input['type']
|
|
131
|
+
$input['type'] = $resolvedOrderConfig->key;
|
|
125
132
|
} elseif (!isset($input['type'])) {
|
|
126
133
|
$input['type'] = 'transport';
|
|
127
134
|
}
|
|
@@ -762,7 +769,7 @@ class OrderController extends FleetOpsController
|
|
|
762
769
|
return response()->error('No order found.');
|
|
763
770
|
}
|
|
764
771
|
|
|
765
|
-
$waypoint
|
|
772
|
+
$waypoint = $request->filled('waypoint') ? Waypoint::findByPlace($request->input('waypoint'), $order) : null;
|
|
766
773
|
$orderConfig = $order->ensureOrderConfig();
|
|
767
774
|
if (!$orderConfig) {
|
|
768
775
|
return response()->error('No order config found for order.');
|
|
@@ -792,33 +799,57 @@ class OrderController extends FleetOpsController
|
|
|
792
799
|
*
|
|
793
800
|
* @return \Illuminate\Http\Response
|
|
794
801
|
*/
|
|
795
|
-
public function trackerInfo(string $id)
|
|
802
|
+
public function trackerInfo(Request $request, string $id)
|
|
796
803
|
{
|
|
797
804
|
$order = Order::findById($id);
|
|
798
805
|
if (!$order) {
|
|
799
806
|
return response()->error('No order found.');
|
|
800
807
|
}
|
|
801
808
|
|
|
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);
|
|
809
|
+
return response()->json($order->tracker()->toArray($request->only(['provider', 'fallbacks', 'traffic_enabled'])));
|
|
809
810
|
}
|
|
810
811
|
|
|
811
|
-
public function waypointEtas(string $id)
|
|
812
|
+
public function waypointEtas(Request $request, string $id)
|
|
812
813
|
{
|
|
813
814
|
$order = Order::findById($id);
|
|
814
815
|
if (!$order) {
|
|
815
816
|
return response()->error('No order found.');
|
|
816
817
|
}
|
|
817
818
|
|
|
818
|
-
|
|
819
|
-
|
|
819
|
+
return response()->json($order->tracker()->eta($request->only(['provider', 'fallbacks', 'traffic_enabled'])));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Ping the assigned driver to refresh/order attention in the driver app.
|
|
824
|
+
*
|
|
825
|
+
* @return \Illuminate\Http\JsonResponse
|
|
826
|
+
*/
|
|
827
|
+
public function pingDriver(string $id)
|
|
828
|
+
{
|
|
829
|
+
if (!Auth::can('fleet-ops update order')) {
|
|
830
|
+
return response()->error('Unauthorized.', 403);
|
|
831
|
+
}
|
|
820
832
|
|
|
821
|
-
|
|
833
|
+
try {
|
|
834
|
+
$order = Order::findByIdOrFail($id, ['driverAssigned']);
|
|
835
|
+
} catch (ModelNotFoundException $e) {
|
|
836
|
+
return response()->error('Order resource not found.', 404);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (!$order->driverAssigned) {
|
|
840
|
+
return response()->error('Order does not have an assigned driver.', 422);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
$order->driverAssigned->notify(new OrderPing($order));
|
|
845
|
+
|
|
846
|
+
return response()->json([
|
|
847
|
+
'status' => 'ok',
|
|
848
|
+
'message' => 'Driver app ping sent.',
|
|
849
|
+
]);
|
|
850
|
+
} catch (\Throwable $e) {
|
|
851
|
+
return response()->error('Unable to ping driver app.', 500);
|
|
852
|
+
}
|
|
822
853
|
}
|
|
823
854
|
|
|
824
855
|
/**
|
|
@@ -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,91 @@ 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
|
+
'alerts' => $this->normalizeTrackingAlertSettings((array) $request->input('alerts', data_get($config, 'alerts', []))),
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
return response()->json([
|
|
258
|
+
'status' => 'ok',
|
|
259
|
+
'message' => 'Tracking settings succesfully saved.',
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Retrieve order tracking intelligence settings.
|
|
265
|
+
*
|
|
266
|
+
* @return \Illuminate\Http\JsonResponse
|
|
267
|
+
*/
|
|
268
|
+
public function getTrackingSettings()
|
|
269
|
+
{
|
|
270
|
+
$config = $this->trackingDefaults();
|
|
271
|
+
$trackingSettings = Setting::lookupCompany('tracking', [
|
|
272
|
+
'provider' => data_get($config, 'provider', 'google_routes'),
|
|
273
|
+
'fallbacks' => data_get($config, 'fallbacks', ['osrm', 'calculated']),
|
|
274
|
+
'traffic_enabled' => data_get($config, 'traffic_enabled', true),
|
|
275
|
+
'cache_ttl_seconds' => data_get($config, 'cache_ttl_seconds', 60),
|
|
276
|
+
'route_cache_ttl_seconds' => data_get($config, 'route_cache_ttl_seconds', 600),
|
|
277
|
+
'stale_location_threshold_seconds' => data_get($config, 'stale_location_threshold_seconds', 300),
|
|
278
|
+
'default_vehicle_speed_kph' => data_get($config, 'default_vehicle_speed_kph', 35),
|
|
279
|
+
'alerts' => data_get($config, 'alerts', $this->trackingAlertDefaults()),
|
|
280
|
+
]);
|
|
281
|
+
$trackingSettings['alerts'] = $this->normalizeTrackingAlertSettings(data_get($trackingSettings, 'alerts', []));
|
|
282
|
+
$trackingSettings['providers'] = $this->trackingProviderOptions();
|
|
283
|
+
|
|
284
|
+
return response()->json($trackingSettings);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public function getAdminTrackingSettings()
|
|
288
|
+
{
|
|
289
|
+
return response()->json(array_merge($this->trackingDefaults(), [
|
|
290
|
+
'providers' => $this->trackingProviderOptions(),
|
|
291
|
+
]));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public function saveAdminTrackingSettings(Request $request)
|
|
295
|
+
{
|
|
296
|
+
$config = config('fleetops.tracking', []);
|
|
297
|
+
$fallbacks = $request->input('fallbacks', data_get($config, 'fallbacks', ['osrm', 'calculated']));
|
|
298
|
+
if (is_string($fallbacks)) {
|
|
299
|
+
$fallbacks = array_values(array_filter(array_map('trim', explode(',', $fallbacks))));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
Setting::configure('fleet-ops.tracking-settings', [
|
|
303
|
+
'provider' => $request->input('provider', data_get($config, 'provider', 'google_routes')),
|
|
304
|
+
'fallbacks' => $fallbacks,
|
|
305
|
+
'traffic_enabled' => $request->boolean('traffic_enabled', data_get($config, 'traffic_enabled', true)),
|
|
306
|
+
'cache_ttl_seconds' => (int) $request->input('cache_ttl_seconds', data_get($config, 'cache_ttl_seconds', 60)),
|
|
307
|
+
'route_cache_ttl_seconds' => (int) $request->input('route_cache_ttl_seconds', data_get($config, 'route_cache_ttl_seconds', 600)),
|
|
308
|
+
'stale_location_threshold_seconds' => (int) $request->input('stale_location_threshold_seconds', data_get($config, 'stale_location_threshold_seconds', 300)),
|
|
309
|
+
'default_vehicle_speed_kph' => (float) $request->input('default_vehicle_speed_kph', data_get($config, 'default_vehicle_speed_kph', 35)),
|
|
310
|
+
'alerts' => $this->normalizeTrackingAlertSettings((array) $request->input('alerts', data_get($config, 'alerts', []))),
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
return response()->json($this->getAdminTrackingSettings()->getData(true));
|
|
314
|
+
}
|
|
315
|
+
|
|
230
316
|
/**
|
|
231
317
|
* Retrieve and return the map provider settings for the current company.
|
|
232
318
|
*
|
|
@@ -244,15 +330,15 @@ class SettingController extends Controller
|
|
|
244
330
|
'mapProvider' => 'leaflet',
|
|
245
331
|
];
|
|
246
332
|
|
|
247
|
-
$systemMapSettings
|
|
248
|
-
$mapSettings
|
|
333
|
+
$systemMapSettings = Setting::lookup('fleet-ops.map-settings', []);
|
|
334
|
+
$mapSettings = Setting::lookupFromCompany('fleet-ops.map-settings', $defaults);
|
|
249
335
|
$mapSettings['mapProvider'] = data_get($mapSettings, 'mapProvider') ?: data_get($systemMapSettings, 'mapProvider', 'leaflet');
|
|
250
336
|
|
|
251
337
|
// Source the Google Maps API key from the system-level services config
|
|
252
338
|
// that is managed by the core-api admin settings panel. This ensures a
|
|
253
339
|
// single source of truth and avoids duplicating key management.
|
|
254
340
|
$mapSettings['googleMapsApiKey'] = config('services.google_maps.api_key', env('GOOGLE_MAPS_API_KEY', ''));
|
|
255
|
-
$mapSettings['googleMapsMapId']
|
|
341
|
+
$mapSettings['googleMapsMapId'] = data_get($systemMapSettings, 'googleMapsMapId', '');
|
|
256
342
|
|
|
257
343
|
return response()->json($mapSettings);
|
|
258
344
|
}
|
|
@@ -289,7 +375,7 @@ class SettingController extends Controller
|
|
|
289
375
|
public function getAdminMapSettings()
|
|
290
376
|
{
|
|
291
377
|
$defaults = [
|
|
292
|
-
'mapProvider'
|
|
378
|
+
'mapProvider' => 'leaflet',
|
|
293
379
|
'googleMapsMapId' => '',
|
|
294
380
|
];
|
|
295
381
|
|
|
@@ -299,13 +385,13 @@ class SettingController extends Controller
|
|
|
299
385
|
public function saveAdminMapSettings(Request $request)
|
|
300
386
|
{
|
|
301
387
|
$allowedProviders = ['leaflet', 'google'];
|
|
302
|
-
$mapProvider
|
|
388
|
+
$mapProvider = $request->input('mapProvider', 'leaflet');
|
|
303
389
|
if (!in_array($mapProvider, $allowedProviders)) {
|
|
304
390
|
$mapProvider = 'leaflet';
|
|
305
391
|
}
|
|
306
392
|
|
|
307
393
|
$settings = [
|
|
308
|
-
'mapProvider'
|
|
394
|
+
'mapProvider' => $mapProvider,
|
|
309
395
|
'googleMapsMapId' => (string) $request->input('googleMapsMapId', ''),
|
|
310
396
|
];
|
|
311
397
|
|
|
@@ -431,4 +517,70 @@ class SettingController extends Controller
|
|
|
431
517
|
'settings' => $normalized,
|
|
432
518
|
]);
|
|
433
519
|
}
|
|
520
|
+
|
|
521
|
+
protected function trackingDefaults(): array
|
|
522
|
+
{
|
|
523
|
+
$config = config('fleetops.tracking', []);
|
|
524
|
+
$systemSettings = Setting::lookup('fleet-ops.tracking-settings', []);
|
|
525
|
+
|
|
526
|
+
$defaults = array_merge($config, is_array($systemSettings) ? $systemSettings : []);
|
|
527
|
+
$defaults['alerts'] = $this->normalizeTrackingAlertSettings(data_get($defaults, 'alerts', []));
|
|
528
|
+
|
|
529
|
+
return $defaults;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
protected function trackingAlertDefaults(): array
|
|
533
|
+
{
|
|
534
|
+
return [
|
|
535
|
+
'late_departures' => [
|
|
536
|
+
'enabled' => true,
|
|
537
|
+
'grace_period_minutes' => 15,
|
|
538
|
+
],
|
|
539
|
+
'route_deviations' => [
|
|
540
|
+
'enabled' => true,
|
|
541
|
+
'distance_threshold_meters' => 500,
|
|
542
|
+
],
|
|
543
|
+
'prolonged_stoppages' => [
|
|
544
|
+
'enabled' => true,
|
|
545
|
+
'duration_threshold_minutes' => 30,
|
|
546
|
+
],
|
|
547
|
+
];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
protected function normalizeTrackingAlertSettings(array $alerts): array
|
|
551
|
+
{
|
|
552
|
+
$defaults = $this->trackingAlertDefaults();
|
|
553
|
+
|
|
554
|
+
return [
|
|
555
|
+
'late_departures' => [
|
|
556
|
+
'enabled' => (bool) data_get($alerts, 'late_departures.enabled', data_get($defaults, 'late_departures.enabled')),
|
|
557
|
+
'grace_period_minutes' => max(0, (int) data_get($alerts, 'late_departures.grace_period_minutes', data_get($defaults, 'late_departures.grace_period_minutes'))),
|
|
558
|
+
],
|
|
559
|
+
'route_deviations' => [
|
|
560
|
+
'enabled' => (bool) data_get($alerts, 'route_deviations.enabled', data_get($defaults, 'route_deviations.enabled')),
|
|
561
|
+
'distance_threshold_meters' => max(0, (int) data_get($alerts, 'route_deviations.distance_threshold_meters', data_get($defaults, 'route_deviations.distance_threshold_meters'))),
|
|
562
|
+
],
|
|
563
|
+
'prolonged_stoppages' => [
|
|
564
|
+
'enabled' => (bool) data_get($alerts, 'prolonged_stoppages.enabled', data_get($defaults, 'prolonged_stoppages.enabled')),
|
|
565
|
+
'duration_threshold_minutes' => max(0, (int) data_get($alerts, 'prolonged_stoppages.duration_threshold_minutes', data_get($defaults, 'prolonged_stoppages.duration_threshold_minutes'))),
|
|
566
|
+
],
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
protected function trackingProviderOptions(): array
|
|
571
|
+
{
|
|
572
|
+
$registry = app(TrackingProviderRegistry::class);
|
|
573
|
+
|
|
574
|
+
return collect($registry->all())->map(function ($provider, $key) {
|
|
575
|
+
$label = $key === 'osrm' ? 'OSRM' : str($key)->replace('_', ' ')->title()->toString();
|
|
576
|
+
|
|
577
|
+
return [
|
|
578
|
+
'key' => $key,
|
|
579
|
+
'name' => $label,
|
|
580
|
+
'value' => $key,
|
|
581
|
+
'label' => $label,
|
|
582
|
+
'capabilities' => $provider->capabilities()->toArray(),
|
|
583
|
+
];
|
|
584
|
+
})->values()->all();
|
|
585
|
+
}
|
|
434
586
|
}
|
|
@@ -38,6 +38,7 @@ class Driver extends FleetbaseResource
|
|
|
38
38
|
'email' => $this->email,
|
|
39
39
|
'phone' => $this->phone,
|
|
40
40
|
'drivers_license_number' => $this->drivers_license_number,
|
|
41
|
+
'license_expiry' => $this->license_expiry,
|
|
41
42
|
'photo_url' => $this->photo_url,
|
|
42
43
|
'avatar_url' => $this->avatar_url,
|
|
43
44
|
'avatar_value' => $this->when(Http::isInternalRequest(), $this->getOriginal('avatar_url')),
|
|
@@ -94,6 +95,7 @@ class Driver extends FleetbaseResource
|
|
|
94
95
|
'email' => $this->email,
|
|
95
96
|
'phone' => $this->phone,
|
|
96
97
|
'photo_url' => $this->photo_url,
|
|
98
|
+
'license_expiry' => $this->license_expiry,
|
|
97
99
|
'vehicle' => data_get($this, 'vehicle.public_id'),
|
|
98
100
|
'current_job' => data_get($this, 'currentJob.public_id'),
|
|
99
101
|
'vendor' => data_get($this, 'vendor.public_id'),
|
|
@@ -23,6 +23,10 @@ class Fleet extends FleetbaseResource
|
|
|
23
23
|
}, $request->array('with'));
|
|
24
24
|
|
|
25
25
|
$this->load($with);
|
|
26
|
+
|
|
27
|
+
if (in_array('subfleets', $with, true) && in_array('vehicles', $with, true)) {
|
|
28
|
+
$this->loadMissing('subFleets.vehicles');
|
|
29
|
+
}
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
return $this->withCustomFields([
|
|
@@ -33,6 +33,7 @@ class Driver extends FleetbaseResource
|
|
|
33
33
|
'current_job_uuid'=> $this->when($isInternal, $this->current_job_uuid),
|
|
34
34
|
'name' => $this->name,
|
|
35
35
|
'vehicle_name' => $this->when($isInternal, $this->vehicle_name),
|
|
36
|
+
'email' => $this->email,
|
|
36
37
|
'phone' => $this->phone,
|
|
37
38
|
'photo_url' => $this->photo_url,
|
|
38
39
|
'status' => $this->status,
|
|
@@ -41,6 +42,53 @@ class Driver extends FleetbaseResource
|
|
|
41
42
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
42
43
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
43
44
|
'online' => data_get($this, 'online', false),
|
|
45
|
+
'meta' => [
|
|
46
|
+
'_index_resource' => true,
|
|
47
|
+
'location_coordinates' => $this->locationCoordinates(),
|
|
48
|
+
'current_order_reference' => $this->currentOrderReference(),
|
|
49
|
+
'speed_label' => $this->speedLabel(),
|
|
50
|
+
'heading_label' => $this->headingLabel(),
|
|
51
|
+
'status_label' => $this->statusLabel(),
|
|
52
|
+
],
|
|
44
53
|
];
|
|
45
54
|
}
|
|
55
|
+
|
|
56
|
+
protected function currentOrderReference(): ?string
|
|
57
|
+
{
|
|
58
|
+
$this->loadMissing('currentOrder');
|
|
59
|
+
$order = data_get($this, 'currentOrder');
|
|
60
|
+
|
|
61
|
+
return data_get($order, 'tracking') ?? data_get($order, 'public_id');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected function locationCoordinates(): ?string
|
|
65
|
+
{
|
|
66
|
+
$location = Utils::castPoint($this->location);
|
|
67
|
+
|
|
68
|
+
return $location ? $this->formatCoordinate($location->getLat()) . ' ' . $this->formatCoordinate($location->getLng()) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected function speedLabel(): string
|
|
72
|
+
{
|
|
73
|
+
$speed = data_get($this, 'speed');
|
|
74
|
+
|
|
75
|
+
return is_numeric($speed) ? ((int) $speed) . ' km/h' : '-';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected function headingLabel(): string
|
|
79
|
+
{
|
|
80
|
+
$heading = data_get($this, 'heading');
|
|
81
|
+
|
|
82
|
+
return is_numeric($heading) ? ((int) $heading) . ' deg' : '-';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected function statusLabel(): ?string
|
|
86
|
+
{
|
|
87
|
+
return $this->status ? str($this->status)->replace(['_', '-'], ' ')->headline()->toString() : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected function formatCoordinate(float $coordinate): string
|
|
91
|
+
{
|
|
92
|
+
return (string) round($coordinate, 4);
|
|
93
|
+
}
|
|
46
94
|
}
|
|
@@ -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),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Resources\v1\Index;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
5
6
|
use Fleetbase\FleetOps\Support\Utils;
|
|
6
7
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
7
8
|
use Fleetbase\Support\Http;
|
|
@@ -28,8 +29,12 @@ class Vehicle extends FleetbaseResource
|
|
|
28
29
|
'company_uuid' => $this->when($isInternal, $this->company_uuid),
|
|
29
30
|
'vendor_uuid' => $this->when($isInternal, $this->vendor_uuid),
|
|
30
31
|
'photo_uuid' => $this->when($isInternal, $this->photo_uuid),
|
|
32
|
+
'internal_id' => $this->internal_id,
|
|
31
33
|
'display_name' => $this->display_name,
|
|
34
|
+
'driver_name' => $this->driver_name,
|
|
32
35
|
'plate_number' => $this->plate_number,
|
|
36
|
+
'serial_number' => $this->serial_number,
|
|
37
|
+
'vin' => $this->vin,
|
|
33
38
|
'make' => $this->make,
|
|
34
39
|
'model' => $this->model,
|
|
35
40
|
'year' => $this->year,
|
|
@@ -43,8 +48,76 @@ class Vehicle extends FleetbaseResource
|
|
|
43
48
|
|
|
44
49
|
// Meta flag to indicate this is an index resource
|
|
45
50
|
'meta' => [
|
|
46
|
-
'_index_resource'
|
|
51
|
+
'_index_resource' => true,
|
|
52
|
+
'current_order_reference' => $this->currentOrderReference(),
|
|
53
|
+
'location_coordinates' => $this->locationCoordinates(),
|
|
54
|
+
'speed_label' => $this->speedLabel(),
|
|
55
|
+
'heading_label' => $this->headingLabel(),
|
|
56
|
+
'status_label' => $this->statusLabel(),
|
|
47
57
|
],
|
|
48
58
|
];
|
|
49
59
|
}
|
|
60
|
+
|
|
61
|
+
protected function currentOrderReference(): ?string
|
|
62
|
+
{
|
|
63
|
+
$this->loadMissing('driver.currentOrder');
|
|
64
|
+
$order = data_get($this, 'driver.currentOrder') ?? $this->currentPositionOrder() ?? $this->currentVehicleOrder();
|
|
65
|
+
|
|
66
|
+
return data_get($order, 'tracking') ?? data_get($order, 'public_id');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected function locationCoordinates(): ?string
|
|
70
|
+
{
|
|
71
|
+
$location = Utils::castPoint($this->location);
|
|
72
|
+
|
|
73
|
+
return $location ? $this->formatCoordinate($location->getLat()) . ' ' . $this->formatCoordinate($location->getLng()) : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protected function speedLabel(): string
|
|
77
|
+
{
|
|
78
|
+
$speed = data_get($this->lastKnownPosition(), 'speed', data_get($this, 'speed'));
|
|
79
|
+
|
|
80
|
+
return is_numeric($speed) ? ((int) $speed) . ' km/h' : '-';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected function headingLabel(): string
|
|
84
|
+
{
|
|
85
|
+
$heading = data_get($this->lastKnownPosition(), 'heading', data_get($this, 'heading'));
|
|
86
|
+
|
|
87
|
+
return is_numeric($heading) ? ((int) $heading) . ' deg' : '-';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected function statusLabel(): ?string
|
|
91
|
+
{
|
|
92
|
+
return $this->status ? str($this->status)->replace(['_', '-'], ' ')->headline()->toString() : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected function currentPositionOrder(): ?Order
|
|
96
|
+
{
|
|
97
|
+
$orderUuid = data_get($this->lastKnownPosition(), 'order_uuid');
|
|
98
|
+
|
|
99
|
+
return $orderUuid ? Order::where('uuid', $orderUuid)->first() : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected function currentVehicleOrder(): ?Order
|
|
103
|
+
{
|
|
104
|
+
return Order::where('vehicle_assigned_uuid', $this->uuid)
|
|
105
|
+
->whereNotIn('status', ['completed', 'canceled', 'cancelled'])
|
|
106
|
+
->latest()
|
|
107
|
+
->first();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
protected function lastKnownPosition()
|
|
111
|
+
{
|
|
112
|
+
if (!$this->resource->relationLoaded('last_known_position')) {
|
|
113
|
+
$this->resource->setRelation('last_known_position', $this->getLastKnownPosition());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return $this->resource->getRelation('last_known_position');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
protected function formatCoordinate(float $coordinate): string
|
|
120
|
+
{
|
|
121
|
+
return (string) round($coordinate, 4);
|
|
122
|
+
}
|
|
50
123
|
}
|