@fleetbase/fleetops-engine 0.6.59 → 0.6.61

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.
Files changed (91) hide show
  1. package/RELEASE.md +25 -0
  2. package/addon/components/ai/route-preview-map.js +1 -5
  3. package/addon/components/customer/orders.js +2 -1
  4. package/addon/components/driver/card.hbs +49 -0
  5. package/addon/components/driver/card.js +30 -0
  6. package/addon/components/map/leaflet-live-map.js +4 -19
  7. package/addon/components/modals/place-details.hbs +1 -1
  8. package/addon/components/modals/point-map.hbs +1 -1
  9. package/addon/components/orchestrator-workbench.js +2 -1
  10. package/addon/components/order/form/route.js +3 -0
  11. package/addon/components/order-tracking-lookup.js +2 -1
  12. package/addon/components/place/details.hbs +1 -1
  13. package/addon/components/positions-replay.js +4 -1
  14. package/addon/components/service-area/details.hbs +1 -1
  15. package/addon/components/vehicle/details/schedules.hbs +2 -2
  16. package/addon/components/vehicle/details/schedules.js +7 -1
  17. package/addon/components/widget/live-fleet.js +7 -7
  18. package/addon/components/zone/details.hbs +1 -1
  19. package/addon/controllers/maintenance/schedules/index/edit.js +10 -1
  20. package/addon/controllers/management/drivers/index.js +22 -1
  21. package/addon/controllers/settings/map.js +16 -0
  22. package/addon/helpers/leaflet-tile-url.js +19 -0
  23. package/addon/services/map-settings.js +22 -0
  24. package/addon/services/vehicle-actions.js +4 -2
  25. package/addon/templates/management/drivers/index.hbs +67 -33
  26. package/addon/templates/settings/map.hbs +10 -0
  27. package/addon/utils/leaflet-tile-url.js +31 -0
  28. package/addon/utils/prepare-place-for-save.js +13 -0
  29. package/app/components/driver/card.js +1 -0
  30. package/app/helpers/leaflet-tile-url.js +1 -0
  31. package/app/utils/leaflet-tile-url.js +1 -0
  32. package/composer.json +1 -1
  33. package/extension.json +1 -1
  34. package/index.js +11 -0
  35. package/package.json +2 -1
  36. package/server/config/fleetops.php +44 -0
  37. package/server/config/geocoder.php +1 -1
  38. package/server/migrations/2026_08_08_000001_make_sku_and_provider_transaction_unique_indexes_soft_delete_aware.php +152 -0
  39. package/server/src/Http/Controllers/Api/v1/CustomerController.php +50 -4
  40. package/server/src/Http/Controllers/Api/v1/DriverController.php +277 -5
  41. package/server/src/Http/Controllers/Api/v1/GeofenceController.php +34 -2
  42. package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
  43. package/server/src/Http/Controllers/Api/v1/NavigatorController.php +14 -13
  44. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +10 -1
  45. package/server/src/Http/Controllers/Api/v1/VehicleController.php +6 -0
  46. package/server/src/Http/Controllers/Concerns/ResolvesReviewAccountBypass.php +62 -0
  47. package/server/src/Http/Controllers/Internal/v1/DriverController.php +1 -1
  48. package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
  49. package/server/src/Http/Filter/FuelReportFilter.php +29 -0
  50. package/server/src/Http/Filter/IssueFilter.php +29 -0
  51. package/server/src/Http/Filter/MaintenanceScheduleFilter.php +40 -0
  52. package/server/src/Http/Requests/CreateCustomerRequest.php +9 -1
  53. package/server/src/Http/Requests/CreateFuelReportRequest.php +14 -3
  54. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +64 -1
  55. package/server/src/Http/Requests/CreatePartRequest.php +39 -1
  56. package/server/src/Http/Requests/CreateVehicleRequest.php +10 -5
  57. package/server/src/Http/Resources/v1/Manifest.php +52 -0
  58. package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
  59. package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
  60. package/server/src/Http/Resources/v1/TrackingNumber.php +33 -0
  61. package/server/src/Models/FuelReport.php +21 -0
  62. package/server/src/Models/Place.php +1 -1
  63. package/server/src/Models/Sensor.php +24 -0
  64. package/server/src/Support/FuelProviders/FuelProviderService.php +10 -1
  65. package/server/src/routes.php +22 -1
  66. package/server/tests/ApiCustomerControllerContractsTest.php +168 -0
  67. package/server/tests/ApiDriverControllerContractsTest.php +65 -0
  68. package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
  69. package/server/tests/ApiManifestControllerContractsTest.php +599 -0
  70. package/server/tests/ApiVehicleControllerContractsTest.php +35 -0
  71. package/server/tests/CompactResourceSerializationTest.php +99 -0
  72. package/server/tests/CustomerEndpointTest.php +4 -1
  73. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +81 -2
  74. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
  75. package/server/tests/Feature/Http/Api/GeofenceControllerContractsTest.php +99 -2
  76. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +12 -6
  77. package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +58 -0
  78. package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
  79. package/server/tests/FuelProviderTransactionControllerContractsTest.php +17 -0
  80. package/server/tests/RequestContractsTest.php +102 -5
  81. package/server/tests/SettingControllerContractsTest.php +33 -0
  82. package/server/tests/SmallControllerContractsTest.php +28 -4
  83. package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
  84. package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
  85. package/server/tests/Unit/Http/Filter/MaintenanceScheduleFilterTest.php +69 -0
  86. package/server/tests/Unit/Http/Requests/CreateFuelReportRequestTest.php +21 -0
  87. package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
  88. package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
  89. package/server/tests/Unit/Jobs/GeofenceDwellAndBulkNotifyTest.php +7 -0
  90. package/server/tests/Unit/Models/SensorLastPositionDefaultTest.php +130 -0
  91. package/translations/en-us.yaml +4 -0
@@ -0,0 +1,129 @@
1
+ <?php
2
+
3
+ /*
4
+ * The models resolve their connection through a namespaced `config()`, which
5
+ * does not exist without the full application. Stubbed so a resource can be
6
+ * exercised on its own — the projection is the thing under test, not Eloquent.
7
+ */
8
+ if (!function_exists('Fleetbase\Traits\config')) {
9
+ eval('namespace Fleetbase\Traits; function config($key = null, $default = null) { return $key === "api.cache.enabled" ? false : $default; }');
10
+ }
11
+
12
+ if (!function_exists('Fleetbase\Models\config')) {
13
+ eval('namespace Fleetbase\Models; function config($key = null, $default = null) { return $default; }');
14
+ }
15
+
16
+ if (!function_exists('Fleetbase\FleetOps\Models\config')) {
17
+ eval('namespace Fleetbase\FleetOps\Models; function config($key = null, $default = null) { return $default; }');
18
+ }
19
+
20
+ use Fleetbase\FleetOps\Http\Resources\v1\Manifest as ManifestResource;
21
+ use Fleetbase\FleetOps\Http\Resources\v1\ManifestStop as ManifestStopResource;
22
+ use Fleetbase\FleetOps\Models\Manifest;
23
+ use Fleetbase\FleetOps\Models\ManifestStop;
24
+ use Illuminate\Http\Request;
25
+
26
+ if (!class_exists('FleetOpsSupportRequestState')) {
27
+ class FleetOpsSupportRequestState
28
+ {
29
+ public static ?Request $request = null;
30
+ }
31
+ }
32
+
33
+ if (!function_exists('Fleetbase\Support\request')) {
34
+ eval('namespace Fleetbase\Support; function request($key = null, $default = null) { return \FleetOpsSupportRequestState::$request; }');
35
+ }
36
+
37
+ /**
38
+ * The manifest's stop counts are accessors that query the database, and the
39
+ * driver and vehicle names walk relations. None of that is what a resource test
40
+ * is for, so they are answered directly — the projection is under test, not
41
+ * Eloquent.
42
+ */
43
+ class FleetOpsManifestFake extends Manifest
44
+ {
45
+ public function getCompletedStopsAttribute(): int
46
+ {
47
+ return 3;
48
+ }
49
+
50
+ public function getPendingStopsAttribute(): int
51
+ {
52
+ return 5;
53
+ }
54
+
55
+ public function getDriverNameAttribute(): ?string
56
+ {
57
+ return 'Ron';
58
+ }
59
+
60
+ public function getVehicleNameAttribute(): ?string
61
+ {
62
+ return 'EAS-01';
63
+ }
64
+ }
65
+
66
+ test('manifest resource publishes what a driver needs to run a route', function () {
67
+ $request = Request::create('/v1/manifests/manifest_public', 'GET');
68
+ FleetOpsSupportRequestState::$request = $request;
69
+
70
+ $manifest = new FleetOpsManifestFake();
71
+ $manifest->setRawAttributes([
72
+ 'id' => 3,
73
+ 'uuid' => 'manifest-uuid',
74
+ 'public_id' => 'manifest_public',
75
+ 'status' => 'in_progress',
76
+ 'total_distance_m' => 41200,
77
+ 'total_duration_s' => 5400,
78
+ 'stop_count' => 8,
79
+ 'notes' => 'Cold chain first',
80
+ ], true);
81
+
82
+ $payload = (new ManifestResource($manifest))->resolve($request);
83
+
84
+ expect($payload['id'])->toBe('manifest_public')
85
+ ->and($payload['status'])->toBe('in_progress')
86
+ ->and($payload['total_distance_m'])->toBe(41200)
87
+ ->and($payload['stop_count'])->toBe(8)
88
+ ->and($payload['notes'])->toBe('Cold chain first')
89
+ ->and($payload['completed_stops'])->toBe(3)
90
+ ->and($payload['pending_stops'])->toBe(5)
91
+ ->and($payload['driver_name'])->toBe('Ron');
92
+ });
93
+
94
+ test('manifest resource omits stops when they were not loaded, so a list stays a list', function () {
95
+ // A driver's manifest list on a busy fleet must not drag every stop of
96
+ // every route along with it.
97
+ $request = Request::create('/v1/drivers/driver_public/manifests', 'GET');
98
+ FleetOpsSupportRequestState::$request = $request;
99
+
100
+ $manifest = new FleetOpsManifestFake();
101
+ $manifest->setRawAttributes(['id' => 4, 'uuid' => 'm-uuid', 'public_id' => 'manifest_public', 'status' => 'pending'], true);
102
+
103
+ $payload = (new ManifestResource($manifest))->resolve($request);
104
+
105
+ expect($payload)->not->toHaveKey('stops');
106
+ });
107
+
108
+ test('manifest stop resource carries the sequence a re-sequence rewrites', function () {
109
+ $request = Request::create('/v1/manifest-stops/stop_public', 'GET');
110
+ FleetOpsSupportRequestState::$request = $request;
111
+
112
+ $stop = new ManifestStop();
113
+ $stop->setRawAttributes([
114
+ 'id' => 9,
115
+ 'uuid' => 'stop-uuid',
116
+ 'public_id' => 'stop_public',
117
+ 'status' => 'pending',
118
+ 'sequence' => 4,
119
+ 'distance_from_prev_m' => 1800,
120
+ 'duration_from_prev_s' => 240,
121
+ ], true);
122
+
123
+ $payload = (new ManifestStopResource($stop))->resolve($request);
124
+
125
+ expect($payload['id'])->toBe('stop_public')
126
+ ->and($payload['sequence'])->toBe(4)
127
+ ->and($payload['status'])->toBe('pending')
128
+ ->and($payload['distance_from_prev_m'])->toBe(1800);
129
+ });
@@ -45,3 +45,94 @@ test('order config resource projects empty and invalid flow as an empty list', f
45
45
  expect($payload['id'])->toBe('order_config_public')
46
46
  ->and($payload['flow'])->toBe([]);
47
47
  });
48
+
49
+ test('order config resource publishes the flow graph so consumers can sequence it', function () {
50
+ $request = Request::create('/v1/fleet-ops/order-configs/order_config_public', 'GET');
51
+ FleetOpsOrderConfigResourceRequestState::$request = $request;
52
+ FleetOpsSupportRequestState::$request = $request;
53
+
54
+ // Declared deliberately out of workflow order, which is how flows are
55
+ // stored: `completed` before the step that leads to it, `dispatched` last.
56
+ $config = new OrderConfig();
57
+ $config->setRawAttributes([
58
+ 'id' => 8,
59
+ 'uuid' => 'order-config-uuid',
60
+ 'public_id' => 'order_config_public',
61
+ 'key' => 'transport',
62
+ 'name' => 'Transport',
63
+ 'flow' => [
64
+ ['code' => 'completed', 'status' => 'Order Completed', 'complete' => true, 'activities' => []],
65
+ ['code' => 'created', 'status' => 'Order Created', 'activities' => ['dispatched']],
66
+ ['code' => 'dispatched', 'status' => 'Order Dispatched', 'activities' => ['enroute'], 'sequence' => 2],
67
+ [
68
+ 'code' => 'enroute',
69
+ 'status' => 'Driver Enroute',
70
+ 'activities' => ['completed', 'failed'],
71
+ 'logic' => [['type' => 'and', 'conditions' => []]],
72
+ ],
73
+ ],
74
+ ], true);
75
+
76
+ $flow = (new OrderConfigResource($config))->resolve($request)['flow'];
77
+ $byCode = collect($flow)->keyBy('code');
78
+
79
+ expect($byCode['created']['activities'])->toBe(['dispatched'])
80
+ ->and($byCode['dispatched']['sequence'])->toBe(2)
81
+ ->and($byCode['enroute']['activities'])->toBe(['completed', 'failed'])
82
+ ->and($byCode['enroute']['logic'])->toBe([['type' => 'and', 'conditions' => []]])
83
+ ->and($byCode['completed']['complete'])->toBeTrue();
84
+ });
85
+
86
+ test('order config resource normalises transitions written as objects', function () {
87
+ $request = Request::create('/v1/fleet-ops/order-configs/order_config_public', 'GET');
88
+ FleetOpsOrderConfigResourceRequestState::$request = $request;
89
+ FleetOpsSupportRequestState::$request = $request;
90
+
91
+ // Flows have been authored both ways; a consumer should not have to guess.
92
+ $config = new OrderConfig();
93
+ $config->setRawAttributes([
94
+ 'id' => 9,
95
+ 'uuid' => 'order-config-uuid',
96
+ 'public_id' => 'order_config_public',
97
+ 'key' => 'transport',
98
+ 'name' => 'Transport',
99
+ 'flow' => [
100
+ ['code' => 'created', 'activities' => [['code' => 'dispatched'], 'enroute', ['label' => 'no code here']]],
101
+ // A flow hand-edited down to a single transition can leave a bare
102
+ // string where the list belongs. Publish an empty list rather than
103
+ // letting the shape vary — a consumer iterating the field should
104
+ // never have to type-check it.
105
+ ['code' => 'dispatched', 'activities' => 'enroute'],
106
+ ],
107
+ ], true);
108
+
109
+ $flow = (new OrderConfigResource($config))->resolve($request)['flow'];
110
+
111
+ expect($flow[0]['activities'])->toBe(['dispatched', 'enroute'])
112
+ ->and($flow[1]['activities'])->toBe([]);
113
+ });
114
+
115
+ test('order config resource keeps flow fields absent from a legacy activity null rather than missing', function () {
116
+ $request = Request::create('/v1/fleet-ops/order-configs/order_config_public', 'GET');
117
+ FleetOpsOrderConfigResourceRequestState::$request = $request;
118
+ FleetOpsSupportRequestState::$request = $request;
119
+
120
+ $config = new OrderConfig();
121
+ $config->setRawAttributes([
122
+ 'id' => 10,
123
+ 'uuid' => 'order-config-uuid',
124
+ 'public_id' => 'order_config_public',
125
+ 'key' => 'transport',
126
+ 'name' => 'Transport',
127
+ 'flow' => [['code' => 'created', 'status' => 'Order Created']],
128
+ ], true);
129
+
130
+ $flow = (new OrderConfigResource($config))->resolve($request)['flow'];
131
+
132
+ // Present-and-null is a contract a client can read; absent is one it has to
133
+ // feel for.
134
+ expect($flow[0])->toHaveKeys(['sequence', 'activities', 'logic'])
135
+ ->and($flow[0]['sequence'])->toBeNull()
136
+ ->and($flow[0]['activities'])->toBe([])
137
+ ->and($flow[0]['logic'])->toBeNull();
138
+ });
@@ -180,4 +180,11 @@ test('navigator driver onboard settings endpoint resolves company settings', fun
180
180
  $connection->table('companies')->insert(['uuid' => 'company-2', 'public_id' => 'company_two', 'name' => 'Beta']);
181
181
  $empty = (new NavigatorController())->getDriverOnboardSettings('company_two');
182
182
  expect($empty->getData(true)['driverOnboardSettings'])->toBe([]);
183
+
184
+ // An unresolved public id is a client error. This drives the real
185
+ // errorResponse() seam, which the probe in SmallControllerContractsTest
186
+ // replaces — only the status is asserted, because the harness `response()`
187
+ // shim envelopes errors differently from the core-api macro it stands in for.
188
+ $missing = (new NavigatorController())->getDriverOnboardSettings('company_missing');
189
+ expect($missing->getStatusCode())->toBe(404);
183
190
  });
@@ -0,0 +1,130 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Sensor;
4
+ use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
5
+ use Illuminate\Database\ConnectionResolver;
6
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
7
+ use Illuminate\Database\SQLiteConnection;
8
+ use Illuminate\Http\Request;
9
+ use Illuminate\Support\Str;
10
+
11
+ /**
12
+ * Covers Sensor::boot()'s creating hook, which defaults `last_position` to
13
+ * POINT(0,0).
14
+ *
15
+ * The same migration that made devices.last_position NOT NULL did it to sensors
16
+ * too, but only Device had a default. Creating a sensor through the API failed
17
+ * with "Field 'last_position' doesn't have a default value" — the Postman
18
+ * contract run caught it, having never been able to create a sensor at all.
19
+ *
20
+ * As with the device fixtures, the dispatcher is set up front so the model event
21
+ * actually fires; without it the hook is never registered and its body never runs.
22
+ */
23
+ if (!function_exists('Fleetbase\Observers\event')) {
24
+ eval('namespace Fleetbase\Observers; function event($event = null, $payload = []) { return []; }');
25
+ }
26
+
27
+ if (!function_exists('Fleetbase\FleetOps\Observers\event')) {
28
+ eval('namespace Fleetbase\FleetOps\Observers; function event($event = null, $payload = []) { return []; }');
29
+ }
30
+
31
+ if (!function_exists('Fleetbase\Support\session')) {
32
+ eval('namespace Fleetbase\Support; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } }; } return \session($key, $default); }');
33
+ }
34
+
35
+ if (!function_exists('Fleetbase\Models\session')) {
36
+ eval('namespace Fleetbase\Models; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } public function missing($k) { return \session($k) === null; } }; } return \session($key, $default); }');
37
+ }
38
+
39
+ if (!Str::hasMacro('humanize')) {
40
+ Str::macro('humanize', fn ($value) => ucfirst(str_replace(['_', '-'], ' ', Str::snake((string) $value))));
41
+ }
42
+
43
+ function fleetopsSensorDefaultBoot(): SQLiteConnection
44
+ {
45
+ $pdo = new PDO('sqlite::memory:');
46
+ $pdo->sqliteCreateFunction('ST_PointFromText', fn ($wkt, $srid = 0, $axisOrder = null) => $wkt);
47
+ $pdo->sqliteCreateFunction('ST_GeomFromText', fn ($wkt, $srid = 0, $axisOrder = null) => $wkt);
48
+ $connection = new SQLiteConnection($pdo);
49
+ $resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
50
+ $resolver->setDefaultConnection('mysql');
51
+ EloquentModel::setConnectionResolver($resolver);
52
+
53
+ // The creating hook is registered against whichever dispatcher is present
54
+ // when the model boots, so it has to exist before Device is ever touched.
55
+ if (!EloquentModel::getEventDispatcher()) {
56
+ EloquentModel::setEventDispatcher(new Illuminate\Events\Dispatcher());
57
+ }
58
+
59
+ app()->instance('db', new class($connection) {
60
+ public function __construct(public SQLiteConnection $c)
61
+ {
62
+ }
63
+
64
+ public function connection($name = null): SQLiteConnection
65
+ {
66
+ return $this->c;
67
+ }
68
+
69
+ public function __call($method, $arguments)
70
+ {
71
+ return $this->c->{$method}(...$arguments);
72
+ }
73
+ });
74
+ app()->instance('db.schema', $connection->getSchemaBuilder());
75
+ app()->instance('responsecache', new class {
76
+ public function __call($method, $arguments)
77
+ {
78
+ return null;
79
+ }
80
+ });
81
+ Illuminate\Support\Facades\DB::clearResolvedInstance('db');
82
+ app()->instance('request', Request::create('/v1/sensors'));
83
+
84
+ config()->set('activitylog.enabled', false);
85
+ config()->set('activitylog.default_auth_driver', 'web');
86
+ app()->bind(Illuminate\Contracts\Config\Repository::class, fn () => config());
87
+
88
+ $schema = $connection->getSchemaBuilder();
89
+ $schema->create('sensors', function ($blueprint) {
90
+ $blueprint->increments('id');
91
+ foreach (['uuid', 'public_id', 'internal_id', 'company_uuid', 'telematic_uuid', 'device_uuid', 'name', 'slug', 'type', 'status', 'online', 'unit', 'last_value', 'last_reading_at', 'min_threshold', 'max_threshold', 'location', 'last_position', 'meta', 'data', 'options', 'notes', '_key'] as $column) {
92
+ $blueprint->string($column)->nullable();
93
+ }
94
+ $blueprint->timestamps();
95
+ $blueprint->timestamp('deleted_at')->nullable();
96
+ });
97
+
98
+ session(['company' => 'company-1']);
99
+
100
+ return $connection;
101
+ }
102
+
103
+ test('creating a sensor without a position defaults it to the null island', function () {
104
+ fleetopsSensorDefaultBoot();
105
+
106
+ $sensor = Sensor::create([
107
+ 'company_uuid' => 'company-1',
108
+ 'name' => 'Defaulted Sensor',
109
+ 'type' => 'temperature',
110
+ ]);
111
+
112
+ expect($sensor->last_position)->toBeInstanceOf(SpatialPoint::class)
113
+ ->and($sensor->last_position->getLat())->toBe(0.0)
114
+ ->and($sensor->last_position->getLng())->toBe(0.0);
115
+ });
116
+
117
+ test('creating a sensor with a position leaves it untouched', function () {
118
+ fleetopsSensorDefaultBoot();
119
+
120
+ $sensor = Sensor::create([
121
+ 'company_uuid' => 'company-1',
122
+ 'name' => 'Positioned Sensor',
123
+ 'type' => 'temperature',
124
+ 'last_position' => new SpatialPoint(1.3, 103.8),
125
+ ]);
126
+
127
+ expect($sensor->last_position)->toBeInstanceOf(SpatialPoint::class)
128
+ ->and($sensor->last_position->getLat())->toBe(1.3)
129
+ ->and($sensor->last_position->getLng())->toBe(103.8);
130
+ });
@@ -2006,6 +2006,10 @@ settings:
2006
2006
  map-provider: Map Provider
2007
2007
  map-provider-help-text: Choose the map engine used to render the live operations map. Leaflet uses OpenStreetMap tiles and requires no API key. Google Maps requires a valid Google Maps JavaScript API key.
2008
2008
  select-map-provider: Select map provider
2009
+ leaflet-tile-url: Leaflet Tile Provider URL
2010
+ leaflet-tile-url-help-text: Optional custom XYZ tile provider URL template used by the Leaflet map. Leave blank to use the default OpenStreetMap tiles which require no API key. Use this to point Leaflet at your own tile provider, including keyed providers such as CARTO.
2011
+ leaflet-dark-tile-url: Leaflet Dark Mode Tile Provider URL
2012
+ leaflet-dark-tile-url-help-text: Optional tile provider URL used when the console is in dark mode. Leave blank to fall back to the tile provider URL above, or the default OpenStreetMap tiles.
2009
2013
  google-maps-map-type: Map Type
2010
2014
  google-maps-map-type-help-text: The default map style to display when Google Maps is active.
2011
2015
  select-map-type: Select map type