@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.
- package/RELEASE.md +25 -0
- package/addon/components/ai/route-preview-map.js +1 -5
- package/addon/components/customer/orders.js +2 -1
- package/addon/components/driver/card.hbs +49 -0
- package/addon/components/driver/card.js +30 -0
- package/addon/components/map/leaflet-live-map.js +4 -19
- package/addon/components/modals/place-details.hbs +1 -1
- package/addon/components/modals/point-map.hbs +1 -1
- package/addon/components/orchestrator-workbench.js +2 -1
- package/addon/components/order/form/route.js +3 -0
- package/addon/components/order-tracking-lookup.js +2 -1
- package/addon/components/place/details.hbs +1 -1
- package/addon/components/positions-replay.js +4 -1
- package/addon/components/service-area/details.hbs +1 -1
- package/addon/components/vehicle/details/schedules.hbs +2 -2
- package/addon/components/vehicle/details/schedules.js +7 -1
- package/addon/components/widget/live-fleet.js +7 -7
- package/addon/components/zone/details.hbs +1 -1
- package/addon/controllers/maintenance/schedules/index/edit.js +10 -1
- package/addon/controllers/management/drivers/index.js +22 -1
- package/addon/controllers/settings/map.js +16 -0
- package/addon/helpers/leaflet-tile-url.js +19 -0
- package/addon/services/map-settings.js +22 -0
- package/addon/services/vehicle-actions.js +4 -2
- package/addon/templates/management/drivers/index.hbs +67 -33
- package/addon/templates/settings/map.hbs +10 -0
- package/addon/utils/leaflet-tile-url.js +31 -0
- package/addon/utils/prepare-place-for-save.js +13 -0
- package/app/components/driver/card.js +1 -0
- package/app/helpers/leaflet-tile-url.js +1 -0
- package/app/utils/leaflet-tile-url.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/index.js +11 -0
- package/package.json +2 -1
- package/server/config/fleetops.php +44 -0
- package/server/config/geocoder.php +1 -1
- package/server/migrations/2026_08_08_000001_make_sku_and_provider_transaction_unique_indexes_soft_delete_aware.php +152 -0
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +50 -4
- package/server/src/Http/Controllers/Api/v1/DriverController.php +277 -5
- package/server/src/Http/Controllers/Api/v1/GeofenceController.php +34 -2
- package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
- package/server/src/Http/Controllers/Api/v1/NavigatorController.php +14 -13
- package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +10 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +6 -0
- package/server/src/Http/Controllers/Concerns/ResolvesReviewAccountBypass.php +62 -0
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
- package/server/src/Http/Filter/FuelReportFilter.php +29 -0
- package/server/src/Http/Filter/IssueFilter.php +29 -0
- package/server/src/Http/Filter/MaintenanceScheduleFilter.php +40 -0
- package/server/src/Http/Requests/CreateCustomerRequest.php +9 -1
- package/server/src/Http/Requests/CreateFuelReportRequest.php +14 -3
- package/server/src/Http/Requests/CreateFuelTransactionRequest.php +64 -1
- package/server/src/Http/Requests/CreatePartRequest.php +39 -1
- package/server/src/Http/Requests/CreateVehicleRequest.php +10 -5
- package/server/src/Http/Resources/v1/Manifest.php +52 -0
- package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
- package/server/src/Http/Resources/v1/TrackingNumber.php +33 -0
- package/server/src/Models/FuelReport.php +21 -0
- package/server/src/Models/Place.php +1 -1
- package/server/src/Models/Sensor.php +24 -0
- package/server/src/Support/FuelProviders/FuelProviderService.php +10 -1
- package/server/src/routes.php +22 -1
- package/server/tests/ApiCustomerControllerContractsTest.php +168 -0
- package/server/tests/ApiDriverControllerContractsTest.php +65 -0
- package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
- package/server/tests/ApiManifestControllerContractsTest.php +599 -0
- package/server/tests/ApiVehicleControllerContractsTest.php +35 -0
- package/server/tests/CompactResourceSerializationTest.php +99 -0
- package/server/tests/CustomerEndpointTest.php +4 -1
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +81 -2
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
- package/server/tests/Feature/Http/Api/GeofenceControllerContractsTest.php +99 -2
- package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +12 -6
- package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +58 -0
- package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
- package/server/tests/FuelProviderTransactionControllerContractsTest.php +17 -0
- package/server/tests/RequestContractsTest.php +102 -5
- package/server/tests/SettingControllerContractsTest.php +33 -0
- package/server/tests/SmallControllerContractsTest.php +28 -4
- package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
- package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
- package/server/tests/Unit/Http/Filter/MaintenanceScheduleFilterTest.php +69 -0
- package/server/tests/Unit/Http/Requests/CreateFuelReportRequestTest.php +21 -0
- package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
- package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
- package/server/tests/Unit/Jobs/GeofenceDwellAndBulkNotifyTest.php +7 -0
- package/server/tests/Unit/Models/SensorLastPositionDefaultTest.php +130 -0
- package/translations/en-us.yaml +4 -0
package/RELEASE.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
> v0.6.61 ~ "A grid view for drivers, a sequenceable order flow, and two silent API no-ops closed"
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
## Highlights
|
|
5
|
+
Two API defects in this release were silent: both answered `200` and neither did what the caller asked. A vehicle odometer update was discarded, and a driver-scoped list came back unscoped. Alongside them, the order config flow now publishes enough of its shape to be sequenced, and Drivers Management gains a card layout.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
## Features
|
|
9
|
+
- **Drivers Management has a card view.** A layout toggle switches the index between the table and a card grid, and the choice is remembered across visits.
|
|
10
|
+
- **The order config flow publishes its graph.** `activities`, `sequence` and `logic` now ride along with each activity, so a consumer can order the flow and offer a next step instead of rendering an unordered set. Transitions are normalised to a list of codes regardless of which of the two stored shapes a flow was authored in.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
## Bug Fixes
|
|
14
|
+
- **`PUT /v1/vehicles/{id}` silently discarded the odometer.** The field is fillable on the model but was missing from the controller's input projection, so a driver app recording mileage received a `200` and a correct-looking body while the reading was dropped. `odometer` and `odometer_unit` are now accepted and validated.
|
|
15
|
+
- **Issue and fuel-report lists scoped by `driver_uuid` came back scoped by nothing.** The base filter silently ignores a query parameter it cannot match to a method, so the filter was dropped and the response was narrowed only by company — every driver's records, with no sign the request had been narrowed at all. `driver_uuid`, `driver_assigned` and `vehicle_uuid` are now recognised on both filters.
|
|
16
|
+
- **Leaflet marker icons are served from the leaflet package** rather than resolving to a broken URL.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
## Testing
|
|
20
|
+
- Coverage held at 100% across the vehicle input projection, both driver-scoped filters, and the order config flow projection.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
## Need help?
|
|
24
|
+
- [GitHub Discussions](https://github.com/fleetbase/fleetbase/discussions)
|
|
25
|
+
- [Discord](https://discord.gg/HnTqQ6zAVn)
|
|
@@ -168,11 +168,7 @@ export default class AiRoutePreviewMapComponent extends Component {
|
|
|
168
168
|
get tileUrl() {
|
|
169
169
|
const theme = document.body?.dataset?.theme;
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
return 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
|
|
171
|
+
return this.mapSettings.getLeafletTileUrl(theme === 'dark' ? 'dark' : 'light');
|
|
176
172
|
}
|
|
177
173
|
|
|
178
174
|
get emptyText() {
|
|
@@ -9,6 +9,7 @@ import { debug } from '@ember/debug';
|
|
|
9
9
|
import { task, timeout } from 'ember-concurrency';
|
|
10
10
|
import { Control as RoutingControl } from '@fleetbase/leaflet-routing-machine';
|
|
11
11
|
import engineService from '@fleetbase/ember-core/decorators/engine-service';
|
|
12
|
+
import { DEFAULT_LEAFLET_TILE_URL } from '../../utils/leaflet-tile-url';
|
|
12
13
|
import registerComponent from '@fleetbase/ember-core/utils/register-component';
|
|
13
14
|
import OrderProgressCardComponent from '../order-progress-card';
|
|
14
15
|
import DisplayPlaceComponent from '../display-place';
|
|
@@ -38,7 +39,7 @@ export default class CustomerOrdersComponent extends Component {
|
|
|
38
39
|
@tracked longitude;
|
|
39
40
|
@tracked route;
|
|
40
41
|
@tracked query;
|
|
41
|
-
@tracked tileSourceUrl =
|
|
42
|
+
@tracked tileSourceUrl = DEFAULT_LEAFLET_TILE_URL;
|
|
42
43
|
@tracked scheduledAt;
|
|
43
44
|
@tracked deliveryInstructions = {};
|
|
44
45
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<Layout::Resource::Card ...attributes as |Card|>
|
|
2
|
+
<Card.header class="{{@headerClass}}">
|
|
3
|
+
<RegistryYield @registry="fleet-ops:component:driver:card:header:start" as |RegistryComponent|>
|
|
4
|
+
<RegistryComponent @order={{@resource}} @resource={{@resource}} />
|
|
5
|
+
</RegistryYield>
|
|
6
|
+
<div class="flex items-center w-full min-w-0">
|
|
7
|
+
<div class="font-semibold flex-1 truncate" data-test-driver-name>{{or @resource.name @resource.display_name}}</div>
|
|
8
|
+
<FaIcon @icon="circle" @size="xs" class="ml-2 flex-none {{if @resource.online 'text-green-500' 'text-gray-400'}}" data-test-driver-online-indicator />
|
|
9
|
+
</div>
|
|
10
|
+
<div class="text-gray-400 dark:text-gray-500 text-sm w-full truncate">{{n-a @resource.phone @resource.public_id}}</div>
|
|
11
|
+
{{#if (has-block "header")}}
|
|
12
|
+
{{yield to="header"}}
|
|
13
|
+
{{/if}}
|
|
14
|
+
<RegistryYield @registry="fleet-ops:component:driver:card:header:end" as |RegistryComponent|>
|
|
15
|
+
<RegistryComponent @order={{@resource}} @resource={{@resource}} />
|
|
16
|
+
</RegistryYield>
|
|
17
|
+
</Card.header>
|
|
18
|
+
<Card.body class="{{@bodyClass}} h-40 overflow-hidden">
|
|
19
|
+
<Image src={{avatar-url @resource.photo_url}} @fallbackSrc={{this.fallbackImage}} class="block w-full h-full object-cover rounded-none" data-test-driver-photo />
|
|
20
|
+
{{#if (has-block "body")}}
|
|
21
|
+
{{yield to="body"}}
|
|
22
|
+
{{/if}}
|
|
23
|
+
</Card.body>
|
|
24
|
+
<Card.footer class={{@footerClass}}>
|
|
25
|
+
<RegistryYield @registry="fleet-ops:component:driver:card:footer:start" as |RegistryComponent|>
|
|
26
|
+
<RegistryComponent @order={{@resource}} @resource={{@resource}} />
|
|
27
|
+
</RegistryYield>
|
|
28
|
+
<div class="flex flex-row items-center justify-between w-full">
|
|
29
|
+
<Badge @status={{or this.statusValue "default"}} @size="xs" data-test-driver-status-badge>{{this.statusLabel}}</Badge>
|
|
30
|
+
<div class="flex flex-row items-center space-x-1">
|
|
31
|
+
<Button @icon="eye" @size="xs" @onClick={{fn this.driverActions.transition.view @resource}} />
|
|
32
|
+
<Button @icon="pencil" @size="xs" @onClick={{fn this.driverActions.transition.edit @resource}} />
|
|
33
|
+
<Button @icon="trash" @size="xs" @type="danger" @onClick={{fn this.driverActions.delete @resource}} />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<RegistryYield @registry="fleet-ops:component:driver:card:footer:end" as |RegistryComponent|>
|
|
37
|
+
<RegistryComponent @order={{@resource}} @resource={{@resource}} />
|
|
38
|
+
</RegistryYield>
|
|
39
|
+
{{#if (has-block "footer")}}
|
|
40
|
+
{{yield to="footer"}}
|
|
41
|
+
{{/if}}
|
|
42
|
+
<div class="mt-1.5 w-full">
|
|
43
|
+
<div class="text-gray-400 dark:text-gray-500 text-xs truncate">
|
|
44
|
+
<FaIcon @icon="car" @prefix="fas" class="mr-1" />{{n-a this.assignedVehicleLabel}}
|
|
45
|
+
</div>
|
|
46
|
+
<div class="text-gray-400 dark:text-gray-500 text-xs">Last Modified: {{@resource.updatedAt}}</div>
|
|
47
|
+
</div>
|
|
48
|
+
</Card.footer>
|
|
49
|
+
</Layout::Resource::Card>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { inject as service } from '@ember/service';
|
|
3
|
+
import { get } from '@ember/object';
|
|
4
|
+
import config from 'ember-get-config';
|
|
5
|
+
|
|
6
|
+
export default class DriverCardComponent extends Component {
|
|
7
|
+
@service driverActions;
|
|
8
|
+
|
|
9
|
+
get resource() {
|
|
10
|
+
return this.args.resource;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get fallbackImage() {
|
|
14
|
+
return config?.defaultValues?.driverImage;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get statusValue() {
|
|
18
|
+
return get(this.resource, 'status') ?? '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get statusLabel() {
|
|
22
|
+
return this.statusValue || '-';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get assignedVehicleLabel() {
|
|
26
|
+
const driver = this.resource;
|
|
27
|
+
|
|
28
|
+
return get(driver, 'vehicle_assigned.display_name') ?? get(driver, 'vehicle.display_name') ?? get(driver, 'vehicle_name');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -48,7 +48,6 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
48
48
|
@tracked latitude = this.location.getLatitude();
|
|
49
49
|
@tracked longitude = this.location.getLongitude();
|
|
50
50
|
@tracked contextmenuItems = [];
|
|
51
|
-
@tracked tileUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
|
|
52
51
|
@tracked theme = 'light';
|
|
53
52
|
@tracked routes = [];
|
|
54
53
|
@tracked drivers = [];
|
|
@@ -57,6 +56,10 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
57
56
|
@tracked leafletPluginsReady = hasLeafletPluginsReady();
|
|
58
57
|
_viewportReloadLocks = new Set();
|
|
59
58
|
|
|
59
|
+
get tileUrl() {
|
|
60
|
+
return this.mapSettings.getLeafletTileUrl(this.theme);
|
|
61
|
+
}
|
|
62
|
+
|
|
60
63
|
constructor() {
|
|
61
64
|
super(...arguments);
|
|
62
65
|
|
|
@@ -888,22 +891,4 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
888
891
|
// Fallback to default Singapore longitude
|
|
889
892
|
return 103.8864;
|
|
890
893
|
}
|
|
891
|
-
|
|
892
|
-
#changeTileSource(source) {
|
|
893
|
-
switch (source) {
|
|
894
|
-
case 'dark':
|
|
895
|
-
this.theme = 'dark';
|
|
896
|
-
this.tileUrl = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
|
897
|
-
break;
|
|
898
|
-
case 'custom':
|
|
899
|
-
this.theme = 'custom';
|
|
900
|
-
this.tileUrl = source.startsWith('https://') ? source : 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
|
|
901
|
-
break;
|
|
902
|
-
case 'light':
|
|
903
|
-
default:
|
|
904
|
-
this.theme = 'light';
|
|
905
|
-
this.tileUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
|
|
906
|
-
break;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
894
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<div class="flex">
|
|
4
4
|
<div class="w-48 mb-6 mr-6">
|
|
5
5
|
<LeafletMap class="w-full h-48 mb-2 rounded-md shadow-md" @lat={{@options.place.latitude}} @lng={{@options.place.longitude}} @zoom={{12}} as |layers|>
|
|
6
|
-
<layers.tile @url=
|
|
6
|
+
<layers.tile @url={{leaflet-tile-url}} />
|
|
7
7
|
|
|
8
8
|
<layers.marker @location={{@options.place.coordinates}} />
|
|
9
9
|
</LeafletMap>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
|
|
2
2
|
<div class="fleetbase-leaflet-map-container">
|
|
3
3
|
<LeafletMap class="map-height-base" @lat={{@options.latitude}} @lng={{@options.longitude}} @zoom={{12}} as |layers|>
|
|
4
|
-
<layers.tile @url=
|
|
4
|
+
<layers.tile @url={{leaflet-tile-url}} />
|
|
5
5
|
|
|
6
6
|
<layers.marker @icon={{@options.icon}} @location={{point-to-coordinates @options.location}} @riseOnHover={{true}} as |marker|>
|
|
7
7
|
<marker.popup @maxWidth="500" @minWidth="180">
|
|
@@ -36,6 +36,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
36
36
|
@service modalsManager;
|
|
37
37
|
@service location;
|
|
38
38
|
@service mapManager;
|
|
39
|
+
@service mapSettings;
|
|
39
40
|
@service routeEngine;
|
|
40
41
|
@service('order-allocation') allocationService;
|
|
41
42
|
|
|
@@ -661,7 +662,7 @@ export default class OrchestratorWorkbenchComponent extends Component {
|
|
|
661
662
|
|
|
662
663
|
get tileSourceUrl() {
|
|
663
664
|
const isDark = document.documentElement.classList.contains('dark');
|
|
664
|
-
return isDark ? '
|
|
665
|
+
return this.mapSettings.getLeafletTileUrl(isDark ? 'dark' : 'light');
|
|
665
666
|
}
|
|
666
667
|
|
|
667
668
|
// ── Computed helpers ──────────────────────────────────────────────────────
|
|
@@ -5,6 +5,7 @@ import { action, get } from '@ember/object';
|
|
|
5
5
|
import { task } from 'ember-concurrency';
|
|
6
6
|
import { colorForId, routeColorForStatus, routeStyleForStatus } from '../../../utils/route-colors';
|
|
7
7
|
import { buildRoutePointMarkerPresentation, buildRoutePointsFromPayload, describeRoutePoint } from '../../../utils/route-visualization';
|
|
8
|
+
import preparePlaceForSave from '../../../utils/prepare-place-for-save';
|
|
8
9
|
|
|
9
10
|
const ORDER_ROUTE_PREVIEW_PADDING_BOTTOM_RIGHT = [420, 0];
|
|
10
11
|
const ORDER_ROUTE_PREVIEW_MAX_ZOOM_TWO_POINTS = 13;
|
|
@@ -153,6 +154,7 @@ export default class OrderFormRouteComponent extends Component {
|
|
|
153
154
|
@action setWaypointPlace(index, place) {
|
|
154
155
|
if (!this.args.resource.payload.waypoints[index]) return;
|
|
155
156
|
|
|
157
|
+
place = preparePlaceForSave(this.store, place);
|
|
156
158
|
this.args.resource.payload.waypoints[index].place = place;
|
|
157
159
|
this.args.resource.payload.waypoints[index]?.setProperties({
|
|
158
160
|
street1: place.street1,
|
|
@@ -186,6 +188,7 @@ export default class OrderFormRouteComponent extends Component {
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
@action setPayloadPlace(prop, place) {
|
|
191
|
+
place = preparePlaceForSave(this.store, place);
|
|
189
192
|
this.args.resource.payload[prop] = place;
|
|
190
193
|
this.previewRoute();
|
|
191
194
|
this.requestServiceQuoteRefresh(`route.${prop}.changed`);
|
|
@@ -10,6 +10,7 @@ import { task } from 'ember-concurrency';
|
|
|
10
10
|
import { OSRMv1, Control as RoutingControl } from '@fleetbase/leaflet-routing-machine';
|
|
11
11
|
import getRoutingHost from '@fleetbase/ember-core/utils/get-routing-host';
|
|
12
12
|
import engineService from '@fleetbase/ember-core/decorators/engine-service';
|
|
13
|
+
import { DEFAULT_LEAFLET_TILE_URL } from '../utils/leaflet-tile-url';
|
|
13
14
|
|
|
14
15
|
export default class OrderTrackingLookupComponent extends Component {
|
|
15
16
|
@service urlSearchParams;
|
|
@@ -28,7 +29,7 @@ export default class OrderTrackingLookupComponent extends Component {
|
|
|
28
29
|
@tracked latitude;
|
|
29
30
|
@tracked longitude;
|
|
30
31
|
@tracked route;
|
|
31
|
-
@tracked tileSourceUrl =
|
|
32
|
+
@tracked tileSourceUrl = DEFAULT_LEAFLET_TILE_URL;
|
|
32
33
|
|
|
33
34
|
constructor() {
|
|
34
35
|
super(...arguments);
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
@zoomControl={{false}}
|
|
76
76
|
as |layers|
|
|
77
77
|
>
|
|
78
|
-
<layers.tile @url=
|
|
78
|
+
<layers.tile @url={{leaflet-tile-url}} />
|
|
79
79
|
<layers.marker
|
|
80
80
|
@id={{@resource.id}}
|
|
81
81
|
@icon={{icon iconUrl=(or @resource.avatar_url "/engines-dist/images/building-marker.png") iconSize=(array 40 40)}}
|
|
@@ -15,6 +15,7 @@ export default class PositionsReplayComponent extends Component {
|
|
|
15
15
|
@service positionPlayback;
|
|
16
16
|
@service notifications;
|
|
17
17
|
@service location;
|
|
18
|
+
@service mapSettings;
|
|
18
19
|
|
|
19
20
|
/** Component ID */
|
|
20
21
|
id = guidFor(this);
|
|
@@ -29,7 +30,9 @@ export default class PositionsReplayComponent extends Component {
|
|
|
29
30
|
@tracked latitude = this.args.resource.latitude || this.location.getLatitude();
|
|
30
31
|
@tracked longitude = this.args.resource.longitude || this.location.getLongitude();
|
|
31
32
|
@tracked zoom = 14;
|
|
32
|
-
|
|
33
|
+
get tileUrl() {
|
|
34
|
+
return this.mapSettings.getLeafletTileUrl('light');
|
|
35
|
+
}
|
|
33
36
|
|
|
34
37
|
/** Computed properties - read state from service */
|
|
35
38
|
get isReplaying() {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
<div class="flex flex-col items-center justify-center py-10 text-gray-400 dark:text-gray-600">
|
|
34
34
|
<FaIcon @icon="calendar-alt" @size="2x" class="mb-3" />
|
|
35
35
|
<p class="text-sm">No maintenance schedules for this vehicle.</p>
|
|
36
|
-
<Button @text="Add Schedule" @icon="plus" @size="xs" @type="primary" class="mt-3" @onClick={{
|
|
36
|
+
<Button @text="Add Schedule" @icon="plus" @size="xs" @type="primary" class="mt-3" @onClick={{this.createSchedule}} />
|
|
37
37
|
</div>
|
|
38
38
|
{{/if}}
|
|
39
39
|
{{/if}}
|
|
40
|
-
</div>
|
|
40
|
+
</div>
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
|
+
import { action } from '@ember/object';
|
|
4
5
|
import { task } from 'ember-concurrency';
|
|
5
6
|
|
|
6
7
|
export default class VehicleDetailsSchedulesComponent extends Component {
|
|
7
8
|
@service maintenanceScheduleActions;
|
|
8
9
|
@service notifications;
|
|
9
10
|
@service store;
|
|
11
|
+
@service vehicleActions;
|
|
10
12
|
@tracked schedules = [];
|
|
11
13
|
|
|
12
14
|
get resourceId() {
|
|
@@ -22,11 +24,15 @@ export default class VehicleDetailsSchedulesComponent extends Component {
|
|
|
22
24
|
try {
|
|
23
25
|
this.schedules = yield this.store.query('maintenance-schedule', {
|
|
24
26
|
subject_uuid: this.resourceId,
|
|
25
|
-
subject_type: 'vehicle',
|
|
27
|
+
subject_type: 'fleet-ops:vehicle',
|
|
26
28
|
sort: '-created_at',
|
|
27
29
|
});
|
|
28
30
|
} catch (err) {
|
|
29
31
|
this.notifications.serverError(err);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
34
|
+
|
|
35
|
+
@action createSchedule() {
|
|
36
|
+
return this.vehicleActions.scheduleMaintenance(this.args.vehicle, {}, { refresh: false, callback: () => this.loadSchedules.perform() });
|
|
37
|
+
}
|
|
32
38
|
}
|
|
@@ -8,15 +8,11 @@ import { task } from 'ember-concurrency';
|
|
|
8
8
|
import { renderCompleted, waitForInsertedAndSized } from '@fleetbase/ember-ui/utils/dom';
|
|
9
9
|
import LeafletTrackingMarkerComponent from '../leaflet-tracking-marker';
|
|
10
10
|
import ensureLeafletDrawEditNamespace from '../../utils/leaflet-draw-namespace-guard';
|
|
11
|
+
import { DEFAULT_LEAFLET_TILE_ATTRIBUTION } from '../../utils/leaflet-tile-url';
|
|
11
12
|
|
|
12
13
|
const DEFAULT_CENTER = [1.31, 103.85];
|
|
13
14
|
const DEFAULT_ZOOM = 11;
|
|
14
|
-
|
|
15
|
-
// dashboard styling consistent with the operational live map. Falls back gracefully
|
|
16
|
-
// in dark mode via the next-leaflet-container-map theming.
|
|
17
|
-
const TILE_URL = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png';
|
|
18
|
-
const TILE_URL_DARK = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png';
|
|
19
|
-
const TILE_ATTRIBUTION = '© OpenStreetMap contributors © CARTO';
|
|
15
|
+
const TILE_ATTRIBUTION = DEFAULT_LEAFLET_TILE_ATTRIBUTION;
|
|
20
16
|
const RECONCILE_INTERVAL_MS = 5 * 60_000;
|
|
21
17
|
|
|
22
18
|
/**
|
|
@@ -30,6 +26,7 @@ export default class WidgetLiveFleetComponent extends Component {
|
|
|
30
26
|
@service fetch;
|
|
31
27
|
@service socket;
|
|
32
28
|
@service currentUser;
|
|
29
|
+
@service mapSettings;
|
|
33
30
|
|
|
34
31
|
@tracked data = null;
|
|
35
32
|
@tracked error = null;
|
|
@@ -38,7 +35,7 @@ export default class WidgetLiveFleetComponent extends Component {
|
|
|
38
35
|
|
|
39
36
|
get tileUrl() {
|
|
40
37
|
const isDark = typeof document !== 'undefined' && document.documentElement?.dataset?.theme === 'dark';
|
|
41
|
-
return isDark ?
|
|
38
|
+
return this.mapSettings.getLeafletTileUrl(isDark ? 'dark' : 'light');
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
socketChannel = null;
|
|
@@ -78,6 +75,9 @@ export default class WidgetLiveFleetComponent extends Component {
|
|
|
78
75
|
this.#ensureTrackingMarkerRegistered();
|
|
79
76
|
this.#patchLeafletDrawInitHook();
|
|
80
77
|
this.#resetMapDeferred();
|
|
78
|
+
// Widgets can render before the fleetops engine boots, so make sure the
|
|
79
|
+
// company's map settings (custom tile provider URL) are loaded.
|
|
80
|
+
this.mapSettings.load();
|
|
81
81
|
this.load.perform();
|
|
82
82
|
this.subscribe();
|
|
83
83
|
this.reconcileTimer = setInterval(() => this.load.perform(), RECONCILE_INTERVAL_MS);
|
|
@@ -21,12 +21,21 @@ export default class MaintenanceSchedulesIndexEditController extends Controller
|
|
|
21
21
|
this.overlay?.close();
|
|
22
22
|
yield this.hostRouter.refresh();
|
|
23
23
|
yield this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index.details', schedule);
|
|
24
|
-
this.
|
|
24
|
+
this.notifyUpdateSuccess(schedule);
|
|
25
25
|
} catch (err) {
|
|
26
26
|
this.notifications.serverError(err);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
notifyUpdateSuccess(schedule) {
|
|
31
|
+
this.notifications.success(
|
|
32
|
+
this.intl.t('common.resource-updated-success', {
|
|
33
|
+
resource: this.intl.t('resource.maintenance-schedule'),
|
|
34
|
+
resourceName: schedule.name,
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
@action cancel() {
|
|
31
40
|
return this.hostRouter.transitionTo('console.fleet-ops.maintenance.schedules.index');
|
|
32
41
|
}
|
|
@@ -12,6 +12,7 @@ export default class ManagementDriversIndexController extends Controller {
|
|
|
12
12
|
@service notifications;
|
|
13
13
|
@service tableContext;
|
|
14
14
|
@service intl;
|
|
15
|
+
@service appCache;
|
|
15
16
|
|
|
16
17
|
/** query params */
|
|
17
18
|
@tracked queryParams = [
|
|
@@ -45,7 +46,7 @@ export default class ManagementDriversIndexController extends Controller {
|
|
|
45
46
|
@tracked status;
|
|
46
47
|
@tracked created_at;
|
|
47
48
|
@tracked updated_at;
|
|
48
|
-
@tracked layout = 'table';
|
|
49
|
+
@tracked layout = this.appCache.get('fleetops:drivers:layout', 'table');
|
|
49
50
|
@tracked table;
|
|
50
51
|
|
|
51
52
|
hasAssignedVehicle(driver) {
|
|
@@ -71,6 +72,25 @@ export default class ManagementDriversIndexController extends Controller {
|
|
|
71
72
|
/** action buttons */
|
|
72
73
|
get actionButtons() {
|
|
73
74
|
return [
|
|
75
|
+
{
|
|
76
|
+
component: 'dropdown-button',
|
|
77
|
+
icon: 'display',
|
|
78
|
+
size: 'xs',
|
|
79
|
+
items: [
|
|
80
|
+
{
|
|
81
|
+
label: this.intl.t('common.table-view'),
|
|
82
|
+
icon: 'table-list',
|
|
83
|
+
onClick: () => this.changeLayout('table'),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
label: this.intl.t('common.grid-view'),
|
|
87
|
+
icon: 'grip',
|
|
88
|
+
onClick: () => this.changeLayout('grid'),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
renderInPlace: true,
|
|
92
|
+
helpText: 'Change the layout',
|
|
93
|
+
},
|
|
74
94
|
{
|
|
75
95
|
icon: 'refresh',
|
|
76
96
|
onClick: this.driverActions.refresh,
|
|
@@ -377,6 +397,7 @@ export default class ManagementDriversIndexController extends Controller {
|
|
|
377
397
|
|
|
378
398
|
@action changeLayout(layout) {
|
|
379
399
|
this.layout = layout;
|
|
400
|
+
this.appCache.set('fleetops:drivers:layout', layout);
|
|
380
401
|
}
|
|
381
402
|
|
|
382
403
|
@action createIssue(driver) {
|
|
@@ -33,6 +33,8 @@ export default class SettingsMapController extends Controller {
|
|
|
33
33
|
* @var {String}
|
|
34
34
|
*/
|
|
35
35
|
@tracked mapProvider = 'leaflet';
|
|
36
|
+
@tracked leafletTileUrl = '';
|
|
37
|
+
@tracked leafletDarkTileUrl = '';
|
|
36
38
|
@tracked googleMapsMapType = 'roadmap';
|
|
37
39
|
@tracked showGoogleMapsTrafficLayer = false;
|
|
38
40
|
@tracked showGoogleMapsTransitLayer = false;
|
|
@@ -62,6 +64,16 @@ export default class SettingsMapController extends Controller {
|
|
|
62
64
|
return this.mapProvider === 'google';
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* True when the user has selected Leaflet as the provider.
|
|
69
|
+
*
|
|
70
|
+
* @memberof SettingsMapController
|
|
71
|
+
* @return {Boolean}
|
|
72
|
+
*/
|
|
73
|
+
get isLeafletSelected() {
|
|
74
|
+
return this.mapProvider === 'leaflet';
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
// ─── Actions ───────────────────────────────────────────────────────────────
|
|
66
78
|
|
|
67
79
|
/**
|
|
@@ -77,6 +89,8 @@ export default class SettingsMapController extends Controller {
|
|
|
77
89
|
@task *saveSettings() {
|
|
78
90
|
const settings = {
|
|
79
91
|
mapProvider: this.mapProvider,
|
|
92
|
+
leafletTileUrl: (this.leafletTileUrl ?? '').trim(),
|
|
93
|
+
leafletDarkTileUrl: (this.leafletDarkTileUrl ?? '').trim(),
|
|
80
94
|
googleMapsMapType: this.googleMapsMapType,
|
|
81
95
|
showGoogleMapsTrafficLayer: this.showGoogleMapsTrafficLayer,
|
|
82
96
|
showGoogleMapsTransitLayer: this.showGoogleMapsTransitLayer,
|
|
@@ -105,6 +119,8 @@ export default class SettingsMapController extends Controller {
|
|
|
105
119
|
const response = yield this.mapSettings.load({ force: true });
|
|
106
120
|
|
|
107
121
|
this.mapProvider = response?.mapProvider ?? 'leaflet';
|
|
122
|
+
this.leafletTileUrl = response?.leafletTileUrl ?? '';
|
|
123
|
+
this.leafletDarkTileUrl = response?.leafletDarkTileUrl ?? '';
|
|
108
124
|
this.googleMapsMapType = response?.googleMapsMapType ?? 'roadmap';
|
|
109
125
|
this.showGoogleMapsTrafficLayer = Boolean(response?.showGoogleMapsTrafficLayer);
|
|
110
126
|
this.showGoogleMapsTransitLayer = Boolean(response?.showGoogleMapsTransitLayer);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Helper from '@ember/component/helper';
|
|
2
|
+
import { inject as service } from '@ember/service';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the Leaflet tile URL from Fleet-Ops map settings.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* <layers.tile @url={{leaflet-tile-url}} />
|
|
9
|
+
* <layers.tile @url={{leaflet-tile-url theme="dark"}} />
|
|
10
|
+
*
|
|
11
|
+
* Recomputes automatically when map settings load or change.
|
|
12
|
+
*/
|
|
13
|
+
export default class LeafletTileUrlHelper extends Helper {
|
|
14
|
+
@service mapSettings;
|
|
15
|
+
|
|
16
|
+
compute(_params, { theme = 'light' } = {}) {
|
|
17
|
+
return this.mapSettings.getLeafletTileUrl(theme);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import Service from '@ember/service';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
|
+
import { getLeafletTileUrl } from '../utils/leaflet-tile-url';
|
|
4
5
|
|
|
5
6
|
const DEFAULT_SETTINGS = {
|
|
6
7
|
mapProvider: 'leaflet',
|
|
8
|
+
leafletTileUrl: '',
|
|
9
|
+
leafletDarkTileUrl: '',
|
|
7
10
|
googleMapsApiKey: '',
|
|
8
11
|
googleMapsMapId: '',
|
|
9
12
|
googleMapsMapType: 'roadmap',
|
|
@@ -45,6 +48,25 @@ export default class MapSettingsService extends Service {
|
|
|
45
48
|
return this.mapProvider === 'google';
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
get leafletTileUrl() {
|
|
52
|
+
return this.getLeafletTileUrl('light');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get leafletDarkTileUrl() {
|
|
56
|
+
return this.getLeafletTileUrl('dark');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the Leaflet tile URL for a theme, preferring the company's
|
|
61
|
+
* configured custom tile provider and falling back to the keyless default.
|
|
62
|
+
*
|
|
63
|
+
* @param {String} theme 'light' or 'dark'
|
|
64
|
+
* @return {String}
|
|
65
|
+
*/
|
|
66
|
+
getLeafletTileUrl(theme = 'light') {
|
|
67
|
+
return getLeafletTileUrl(this.settings, theme);
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
async load({ force = false } = {}) {
|
|
49
71
|
if (!force && this.isLoaded) {
|
|
50
72
|
return this.settings;
|
|
@@ -184,8 +184,10 @@ export default class VehicleActionsService extends ResourceActionService {
|
|
|
184
184
|
},
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
-
@action scheduleMaintenance(vehicle) {
|
|
188
|
-
this.
|
|
187
|
+
@action async scheduleMaintenance(vehicle, options = {}, saveOptions = {}) {
|
|
188
|
+
vehicle = await this.resolveVehicleResource(vehicle);
|
|
189
|
+
|
|
190
|
+
return this.maintenanceScheduleActions.modal.create({ subject: vehicle }, options, saveOptions);
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
@action createWorkOrder(vehicle) {
|