@fleetbase/fleetops-engine 0.6.71 → 0.6.72
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 +15 -18
- package/addon/components/admin/tracking-page-settings.hbs +56 -0
- package/addon/components/admin/tracking-page-settings.js +67 -0
- package/addon/components/device/form.hbs +2 -2
- package/addon/components/driver/details.hbs +1 -1
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/equipment/form.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +11 -3
- package/addon/components/maintenance/form.hbs +3 -3
- package/addon/components/map/google-live-map.js +2 -2
- package/addon/components/map/leaflet-live-map.js +45 -32
- package/addon/components/orchestrator/phase-builder.js +2 -1
- package/addon/components/orchestrator-workbench.hbs +7 -1
- package/addon/components/orchestrator-workbench.js +16 -2
- package/addon/components/order/form/route.hbs +1 -1
- package/addon/components/order/form/route.js +13 -1
- package/addon/components/service-rate/details.hbs +1 -1
- package/addon/components/service-rate/form.hbs +1 -1
- package/addon/components/tracking-page/full-view.hbs +276 -0
- package/addon/components/tracking-page/full-view.js +474 -0
- package/addon/components/tracking-page/locale-selector.js +30 -0
- package/addon/components/tracking-page/map.hbs +44 -0
- package/addon/components/tracking-page/map.js +328 -0
- package/addon/components/tracking-page/order-list.hbs +35 -0
- package/addon/components/tracking-page/public-view.hbs +60 -0
- package/addon/components/tracking-page/settings-preview.hbs +21 -0
- package/addon/components/tracking-page/settings-preview.js +23 -0
- package/addon/components/tracking-page/verify-panel.hbs +109 -0
- package/addon/components/tracking-page.hbs +142 -0
- package/addon/components/tracking-page.js +564 -0
- package/addon/components/trailer/form.hbs +2 -2
- package/addon/components/vehicle/form.hbs +3 -3
- package/addon/components/warranty/form.hbs +2 -2
- package/addon/components/widget/live-fleet.js +54 -6
- package/addon/components/widget/proof-of-delivery.hbs +49 -0
- package/addon/components/widget/proof-of-delivery.js +62 -0
- package/addon/components/work-order/form.hbs +3 -3
- package/addon/controllers/operations/orders/index/details.js +19 -3
- package/addon/controllers/operations/scheduler/index.js +45 -14
- package/addon/controllers/settings/index.js +6 -0
- package/addon/controllers/settings/map.js +75 -0
- package/addon/controllers/settings/orchestrator.js +8 -6
- package/addon/controllers/settings/tracking-page.js +212 -0
- package/addon/extension.js +65 -1
- package/addon/helpers/parcel-box-image.js +16 -0
- package/addon/routes/operations/orders/index/new.js +5 -4
- package/addon/routes/operations/scheduler/index.js +1 -1
- package/addon/routes/settings/tracking-page.js +31 -0
- package/addon/routes.js +1 -0
- package/addon/services/location.js +69 -21
- package/addon/services/map-settings.js +28 -0
- package/addon/services/order-allocation.js +1 -1
- package/addon/styles/fleetops-engine.css +1391 -0
- package/addon/templates/settings/map.hbs +46 -0
- package/addon/templates/settings/tracking-page.hbs +318 -0
- package/addon/utils/identity-cell-resource.js +2 -56
- package/addon/utils/map-default-view.js +132 -0
- package/addon/utils/placeholder-images.js +4 -69
- package/addon/utils/resource-descriptors/assets.js +2 -57
- package/addon/utils/resource-descriptors/helpers.js +23 -122
- package/addon/utils/resource-descriptors/operations.js +2 -61
- package/addon/utils/resource-descriptors/people.js +3 -142
- package/addon/utils/resource-descriptors/shared.js +18 -0
- package/addon/utils/tracking-live-source.js +123 -0
- package/addon/utils/tracking-page-theme.js +123 -0
- package/app/components/admin/tracking-page-settings.js +1 -0
- package/app/components/tracking-page/full-view.js +1 -0
- package/app/components/tracking-page/locale-selector.js +1 -0
- package/app/components/tracking-page/map.js +1 -0
- package/app/components/tracking-page/order-list.js +1 -0
- package/app/components/tracking-page/public-view.js +1 -0
- package/app/components/tracking-page/settings-preview.js +1 -0
- package/app/components/tracking-page/verify-panel.js +1 -0
- package/app/components/tracking-page.js +1 -0
- package/app/components/widget/proof-of-delivery.js +1 -0
- package/app/controllers/settings/tracking-page.js +1 -0
- package/app/helpers/parcel-box-image.js +1 -0
- package/app/routes/settings/tracking-page.js +1 -0
- package/app/templates/settings/tracking-page.js +1 -0
- package/app/utils/map-default-view.js +1 -0
- package/app/utils/tracking-live-source.js +1 -0
- package/app/utils/tracking-page-theme.js +1 -0
- package/composer.json +2 -2
- package/extension.json +1 -1
- package/package.json +4 -4
- package/scripts/pest-bootstrap.php +4 -0
- package/server/config/fleetops.php +4 -0
- package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
- package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
- package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
- package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
- package/server/resources/views/labels/default.php +2 -2
- package/server/resources/views/labels/entity-label.php +2 -2
- package/server/resources/views/labels/waypoint-label.php +2 -2
- package/server/src/Auth/Schemas/FleetOps.php +5 -0
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
- package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
- package/server/src/Events/OrderChanged.php +257 -0
- package/server/src/Events/OrderDriverUnassigned.php +68 -0
- package/server/src/Exceptions/OrderEditLockedException.php +32 -0
- package/server/src/Expansions/CompanyExpansion.php +3 -2
- package/server/src/Expansions/UserExpansion.php +12 -10
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
- package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
- package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
- package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
- package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
- package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
- package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
- package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
- package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
- package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +84 -24
- package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
- package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
- package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
- package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
- package/server/src/Http/Filter/ContactFilter.php +1 -1
- package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
- package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
- package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
- package/server/src/Http/Filter/OrderFilter.php +39 -4
- package/server/src/Http/Filter/ProofFilter.php +18 -0
- package/server/src/Http/Filter/RouteFilter.php +18 -0
- package/server/src/Http/Filter/TelematicFilter.php +23 -0
- package/server/src/Http/Filter/WarrantyFilter.php +23 -0
- package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
- package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
- package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
- package/server/src/Http/Resources/v1/Index/Order.php +11 -2
- package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
- package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
- package/server/src/Http/Resources/v1/Zone.php +31 -23
- package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
- package/server/src/Jobs/ProcessAllocationJob.php +16 -6
- package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
- package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
- package/server/src/Listeners/HandleOrderCanceled.php +9 -1
- package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
- package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
- package/server/src/Mail/CustomerCredentialsMail.php +4 -2
- package/server/src/Models/AssetConnection.php +3 -0
- package/server/src/Models/Contact.php +3 -0
- package/server/src/Models/Device.php +28 -0
- package/server/src/Models/DeviceEvent.php +7 -0
- package/server/src/Models/Driver.php +20 -7
- package/server/src/Models/Entity.php +12 -4
- package/server/src/Models/Fleet.php +11 -32
- package/server/src/Models/FleetDriver.php +3 -0
- package/server/src/Models/FleetVehicle.php +3 -0
- package/server/src/Models/FuelReport.php +5 -13
- package/server/src/Models/Issue.php +4 -16
- package/server/src/Models/Order.php +200 -9
- package/server/src/Models/Payload.php +227 -35
- package/server/src/Models/Place.php +7 -1
- package/server/src/Models/ServiceArea.php +7 -0
- package/server/src/Models/ServiceRate.php +7 -17
- package/server/src/Models/TrackingNumber.php +101 -2
- package/server/src/Models/TrackingSession.php +33 -0
- package/server/src/Models/TrackingSessionGrant.php +22 -0
- package/server/src/Models/TrackingStatus.php +6 -0
- package/server/src/Models/Vehicle.php +21 -2
- package/server/src/Models/Vendor.php +7 -0
- package/server/src/Models/Waypoint.php +13 -4
- package/server/src/Notifications/OrderAssigned.php +2 -1
- package/server/src/Notifications/OrderCanceled.php +7 -1
- package/server/src/Notifications/OrderChanged.php +87 -0
- package/server/src/Notifications/OrderCompleted.php +2 -1
- package/server/src/Notifications/OrderDispatchFailed.php +2 -1
- package/server/src/Notifications/OrderDispatched.php +2 -1
- package/server/src/Notifications/OrderFailed.php +2 -1
- package/server/src/Notifications/OrderUnassigned.php +66 -0
- package/server/src/Notifications/WaypointCompleted.php +2 -1
- package/server/src/Observers/CompanyUserObserver.php +8 -7
- package/server/src/Observers/DriverObserver.php +6 -1
- package/server/src/Observers/OrderObserver.php +141 -1
- package/server/src/Observers/TrackingNumberObserver.php +18 -6
- package/server/src/Observers/UserObserver.php +2 -1
- package/server/src/Observers/VehicleObserver.php +8 -3
- package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
- package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
- package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
- package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
- package/server/src/Providers/EventServiceProvider.php +11 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
- package/server/src/Support/ApiCacheThrottle.php +76 -0
- package/server/src/Support/LiveOrderQuery.php +3 -1
- package/server/src/Support/OrderEdit.php +365 -0
- package/server/src/Support/OrderEditPolicy.php +292 -0
- package/server/src/Support/ProfileAccountManager.php +54 -1
- package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
- package/server/src/Support/SocketChannels.php +346 -0
- package/server/src/Support/Telematics/TelematicService.php +7 -2
- package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
- package/server/src/Support/TrackingChannel.php +55 -0
- package/server/src/Support/TrackingCode.php +242 -0
- package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
- package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
- package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
- package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
- package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
- package/server/src/Support/TrackingPage/TrackingView.php +543 -0
- package/server/src/Support/TrackingPublisher.php +261 -0
- package/server/src/Support/TrackingScope.php +182 -0
- package/server/src/Support/TrackingUpdate.php +402 -0
- package/server/src/Support/Utils.php +15 -3
- package/server/src/Traits/HasTrackingNumber.php +2 -0
- package/server/src/routes.php +39 -7
- package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
- package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
- package/server/tests/ApiEntityControllerContractsTest.php +84 -0
- package/server/tests/ApiOrderControllerContractsTest.php +245 -0
- package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
- package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
- package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
- package/server/tests/CompactResourceSerializationTest.php +23 -59
- package/server/tests/ControllerFilterContractsTest.php +1 -1
- package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
- package/server/tests/EventContractsTest.php +26 -60
- package/server/tests/ExpansionContractsTest.php +15 -2
- package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
- package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
- package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
- package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
- package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
- package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
- package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
- package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
- package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
- package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
- package/server/tests/IndexCustomerPhotoTest.php +151 -0
- package/server/tests/ModelAccessorContractsTest.php +4 -3
- package/server/tests/NotificationAndMailContractsTest.php +5 -8
- package/server/tests/ObserverContractsTest.php +72 -10
- package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
- package/server/tests/OrderFilterExecutionTest.php +167 -2
- package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
- package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
- package/server/tests/QueueJobContractsTest.php +2 -1
- package/server/tests/RequestContractsTest.php +1 -1
- package/server/tests/RouteRegistrationExecutionTest.php +26 -0
- package/server/tests/RouteSequencingEngineTest.php +17 -0
- package/server/tests/SettingControllerContractsTest.php +110 -0
- package/server/tests/Support/TrackingPageDatabase.php +200 -0
- package/server/tests/TenantScopingTest.php +206 -0
- package/server/tests/TrackingPageSettingsTest.php +343 -0
- package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
- package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
- package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
- package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
- package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
- package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
- package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
- package/server/tests/Unit/Models/FleetTest.php +4 -3
- package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
- package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
- package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
- package/server/tests/Unit/Models/PayloadTest.php +18 -4
- package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
- package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
- package/server/tests/Unit/Models/VehicleTest.php +16 -2
- package/server/tests/Unit/Models/WaypointTest.php +4 -3
- package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
- package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
- package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
- package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
- package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
- package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
- package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
- package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
- package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
- package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
- package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
- package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
- package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
- package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
- package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
- package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
- package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
- package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
- package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
- package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
- package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
- package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
- package/server/tests/VehicleResourceContractsTest.php +46 -0
- package/server/tests/VroomOrchestrationEngineTest.php +34 -1
- package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
- package/translations/ar-ae.yml +274 -0
- package/translations/bg-bg.yaml +274 -0
- package/translations/en-us.yaml +287 -0
- package/translations/es-pa.yaml +274 -0
- package/translations/fr-fr.yaml +274 -0
- package/translations/mn-mn.yaml +274 -0
- package/translations/pt-br.yaml +274 -0
- package/translations/ru-ru.yaml +276 -2
- package/translations/uk-ua.yaml +275 -1
- package/translations/vi-vn.yaml +274 -0
- package/addon/components/cell/contact-identity.hbs +0 -1
- package/addon/components/cell/customer-identity.hbs +0 -1
- package/addon/components/cell/driver-identity.hbs +0 -1
- package/addon/components/cell/fleet-identity.hbs +0 -1
- package/addon/components/cell/order-identity.hbs +0 -1
- package/addon/components/cell/place-identity.hbs +0 -1
- package/addon/components/cell/vehicle-identity.hbs +0 -1
- package/addon/components/cell/vendor-identity.hbs +0 -1
- package/addon/components/contact/pill.hbs +0 -24
- package/addon/components/contact/summary.hbs +0 -1
- package/addon/components/customer/pill.hbs +0 -24
- package/addon/components/customer/summary.hbs +0 -1
- package/addon/components/driver/pill.hbs +0 -24
- package/addon/components/driver/summary.hbs +0 -1
- package/addon/components/fleet/pill.hbs +0 -24
- package/addon/components/fleet/summary.hbs +0 -1
- package/addon/components/order/pill.hbs +0 -24
- package/addon/components/order/summary.hbs +0 -1
- package/addon/components/place/pill.hbs +0 -24
- package/addon/components/place/summary.hbs +0 -1
- package/addon/components/select-option/contact.hbs +0 -12
- package/addon/components/select-option/customer.hbs +0 -12
- package/addon/components/select-option/driver.hbs +0 -12
- package/addon/components/select-option/fleet.hbs +0 -12
- package/addon/components/select-option/order.hbs +0 -12
- package/addon/components/select-option/place.hbs +0 -12
- package/addon/components/select-option/vehicle.hbs +0 -12
- package/addon/components/select-option/vendor.hbs +0 -12
- package/addon/components/vehicle/pill.hbs +0 -24
- package/addon/components/vehicle/summary.hbs +0 -1
- package/addon/components/vendor/pill.hbs +0 -24
- package/addon/components/vendor/summary.hbs +0 -1
- package/app/components/cell/contact-identity.js +0 -1
- package/app/components/cell/customer-identity.js +0 -1
- package/app/components/cell/driver-identity.js +0 -1
- package/app/components/cell/fleet-identity.js +0 -1
- package/app/components/cell/order-identity.js +0 -1
- package/app/components/cell/place-identity.js +0 -1
- package/app/components/cell/vehicle-identity.js +0 -1
- package/app/components/cell/vendor-identity.js +0 -1
- package/app/components/contact/pill.js +0 -1
- package/app/components/contact/summary.js +0 -1
- package/app/components/customer/pill.js +0 -1
- package/app/components/customer/summary.js +0 -1
- package/app/components/driver/pill.js +0 -1
- package/app/components/driver/summary.js +0 -1
- package/app/components/fleet/pill.js +0 -1
- package/app/components/fleet/summary.js +0 -1
- package/app/components/order/pill.js +0 -1
- package/app/components/order/summary.js +0 -1
- package/app/components/place/pill.js +0 -1
- package/app/components/place/summary.js +0 -1
- package/app/components/select-option/contact.js +0 -1
- package/app/components/select-option/customer.js +0 -1
- package/app/components/select-option/driver.js +0 -1
- package/app/components/select-option/fleet.js +0 -1
- package/app/components/select-option/order.js +0 -1
- package/app/components/select-option/place.js +0 -1
- package/app/components/select-option/vehicle.js +0 -1
- package/app/components/select-option/vendor.js +0 -1
- package/app/components/vehicle/pill.js +0 -1
- package/app/components/vehicle/summary.js +0 -1
- package/app/components/vendor/pill.js +0 -1
- package/app/components/vendor/summary.js +0 -1
|
@@ -6,7 +6,6 @@ use Fleetbase\FleetOps\Models\TrackingNumber;
|
|
|
6
6
|
use Fleetbase\FleetOps\Models\TrackingStatus;
|
|
7
7
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
8
8
|
use Illuminate\Support\Str;
|
|
9
|
-
use Milon\Barcode\Facades\DNS2DFacade as DNS2D;
|
|
10
9
|
|
|
11
10
|
class TrackingNumberObserver
|
|
12
11
|
{
|
|
@@ -17,10 +16,9 @@ class TrackingNumberObserver
|
|
|
17
16
|
*/
|
|
18
17
|
public function creating(TrackingNumber $trackingNumber)
|
|
19
18
|
{
|
|
20
|
-
// generate
|
|
19
|
+
// generate the tracking number, then the qr code and barcode printed from it
|
|
21
20
|
$trackingNumber->tracking_number = $this->generateTrackingNumber($trackingNumber);
|
|
22
|
-
$trackingNumber->
|
|
23
|
-
$trackingNumber->barcode = $this->generateBarcode($trackingNumber->owner_uuid, 'PDF417');
|
|
21
|
+
$trackingNumber->fill($this->generateCodeImages($trackingNumber->tracking_number, $this->ownerPublicId($trackingNumber)));
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
/**
|
|
@@ -47,9 +45,23 @@ class TrackingNumberObserver
|
|
|
47
45
|
return TrackingNumber::generateNumber($trackingNumber->region);
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
/**
|
|
49
|
+
* @return array{qr_code: string, barcode: string}
|
|
50
|
+
*/
|
|
51
|
+
protected function generateCodeImages(string $trackingNumber, ?string $ownerPublicId): array
|
|
51
52
|
{
|
|
52
|
-
return
|
|
53
|
+
return TrackingNumber::codeImages($trackingNumber, $ownerPublicId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected function ownerPublicId(TrackingNumber $trackingNumber): ?string
|
|
57
|
+
{
|
|
58
|
+
if (empty($trackingNumber->owner_uuid)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
$publicId = data_get($trackingNumber, 'owner.public_id');
|
|
63
|
+
|
|
64
|
+
return is_string($publicId) && $publicId !== '' ? $publicId : null;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
protected function createTrackingStatus(array $attributes): TrackingStatus
|
|
@@ -20,6 +20,7 @@ class UserObserver
|
|
|
20
20
|
|
|
21
21
|
protected function deleteDrivers(string $userUuid): void
|
|
22
22
|
{
|
|
23
|
-
|
|
23
|
+
// Delete each driver through the model so DriverObserver cleans up after it
|
|
24
|
+
Driver::where('user_uuid', $userUuid)->get()->each->delete();
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -66,7 +66,7 @@ class VehicleObserver
|
|
|
66
66
|
public function deleted(Vehicle $vehicle)
|
|
67
67
|
{
|
|
68
68
|
// Unassign the deleted vehicle from matching driver/(s)
|
|
69
|
-
$this->
|
|
69
|
+
$this->unassignDriversFrom($vehicle);
|
|
70
70
|
|
|
71
71
|
$this->invalidateLiveCache();
|
|
72
72
|
}
|
|
@@ -81,9 +81,14 @@ class VehicleObserver
|
|
|
81
81
|
return Driver::where('uuid', $identifier)->whereNull('deleted_at')->withoutGlobalScopes()->first();
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
protected function
|
|
84
|
+
protected function unassignDriversFrom(Vehicle $vehicle): int
|
|
85
85
|
{
|
|
86
|
-
|
|
86
|
+
$unassigned = Driver::where(['vehicle_uuid' => $vehicle->uuid])->update(['vehicle_uuid' => null]);
|
|
87
|
+
if ($unassigned) {
|
|
88
|
+
Driver::invalidateApiCacheManually($vehicle->company_uuid);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return $unassigned;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
protected function invalidateLiveCache(): void
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Orchestration\Engines;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Orchestration\Support\RouteMetrics;
|
|
5
6
|
use Illuminate\Support\Collection;
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -18,6 +19,8 @@ use Illuminate\Support\Collection;
|
|
|
18
19
|
* using a nearest-neighbour TSP heuristic.
|
|
19
20
|
* 3. Returns one assignment entry per order, preserving the existing
|
|
20
21
|
* vehicle_id / driver_id and updating only the sequence number.
|
|
22
|
+
* 4. Estimates arrival times and route distance/duration along the
|
|
23
|
+
* sequenced stops (see RouteMetrics).
|
|
21
24
|
*
|
|
22
25
|
* This ensures that running "Assign Vehicles" followed by "Optimize Routes"
|
|
23
26
|
* produces one properly-sequenced route per vehicle rather than re-running
|
|
@@ -37,6 +40,9 @@ class RouteSequencingEngine
|
|
|
37
40
|
{
|
|
38
41
|
$assignments = [];
|
|
39
42
|
$unassigned = [];
|
|
43
|
+
$startTime = now()->timestamp;
|
|
44
|
+
$distance = 0;
|
|
45
|
+
$duration = 0;
|
|
40
46
|
|
|
41
47
|
// Group orders by their currently assigned vehicle UUID
|
|
42
48
|
$byVehicle = [];
|
|
@@ -73,6 +79,13 @@ class RouteSequencingEngine
|
|
|
73
79
|
// (or waypoints for multi-drop orders). We keep pickup before its own
|
|
74
80
|
// dropoff as a hard constraint.
|
|
75
81
|
$sequenced = $this->_sequenceOrdersForVehicle($vehicleOrders, $startLat, $startLng);
|
|
82
|
+
$metrics = RouteMetrics::measure(
|
|
83
|
+
$startLat !== null && $startLng !== null ? [$startLat, $startLng] : null,
|
|
84
|
+
array_map(fn ($stop) => ['order_id' => $stop['order_public_id'], 'lat' => $stop['lat'], 'lng' => $stop['lng']], $sequenced),
|
|
85
|
+
$startTime
|
|
86
|
+
);
|
|
87
|
+
$distance += $metrics['distance'];
|
|
88
|
+
$duration += $metrics['duration'];
|
|
76
89
|
|
|
77
90
|
// Build assignment entries — one per order, with the sequence number
|
|
78
91
|
// being the position of the order's FIRST stop in the sequenced list.
|
|
@@ -92,9 +105,11 @@ class RouteSequencingEngine
|
|
|
92
105
|
'driver_id' => $driverPublicId,
|
|
93
106
|
'sequence' => $orderSequences[$order->public_id] ?? 1,
|
|
94
107
|
'waypoint_sequence' => null,
|
|
95
|
-
'arrival' => null,
|
|
96
|
-
'duration' => null,
|
|
97
|
-
'distance' => null,
|
|
108
|
+
'arrival' => $metrics['orders'][$order->public_id]['arrival'] ?? null,
|
|
109
|
+
'duration' => $metrics['orders'][$order->public_id]['duration'] ?? null,
|
|
110
|
+
'distance' => $metrics['orders'][$order->public_id]['distance'] ?? null,
|
|
111
|
+
'route_distance' => $metrics['distance'],
|
|
112
|
+
'route_duration' => $metrics['duration'],
|
|
98
113
|
];
|
|
99
114
|
}
|
|
100
115
|
}
|
|
@@ -106,6 +121,9 @@ class RouteSequencingEngine
|
|
|
106
121
|
'engine' => 'route_sequencing',
|
|
107
122
|
'assigned' => count($assignments),
|
|
108
123
|
'unassigned' => count($unassigned),
|
|
124
|
+
'distance' => $distance,
|
|
125
|
+
'duration' => $duration,
|
|
126
|
+
'metrics' => 'estimated',
|
|
109
127
|
],
|
|
110
128
|
];
|
|
111
129
|
}
|
|
@@ -37,6 +37,11 @@ use Illuminate\Support\Facades\Log;
|
|
|
37
37
|
*/
|
|
38
38
|
class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
39
39
|
{
|
|
40
|
+
/**
|
|
41
|
+
* Arrivals below this (2001-09-09) are offsets from the route start, not unix timestamps.
|
|
42
|
+
*/
|
|
43
|
+
protected const ABSOLUTE_TIMESTAMP_THRESHOLD = 1000000000;
|
|
44
|
+
|
|
40
45
|
/**
|
|
41
46
|
* The company whose VROOM settings apply to the current allocation run.
|
|
42
47
|
* Resolved from the orders so queued runs work without a company session.
|
|
@@ -202,6 +207,14 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
|
202
207
|
|
|
203
208
|
$result = $this->mapVroomResponse($this->callVroom($vroomPayload), $jobIdReverse);
|
|
204
209
|
|
|
210
|
+
// Capacity-only solves run on a uniform placeholder matrix, so VROOM's
|
|
211
|
+
// timings are meaningless here; clear them so real estimates replace them.
|
|
212
|
+
$result['assignments'] = array_map(
|
|
213
|
+
fn (array $assignment) => array_merge($assignment, array_fill_keys(['arrival', 'duration', 'distance', 'route_distance', 'route_duration'], null)),
|
|
214
|
+
$result['assignments']
|
|
215
|
+
);
|
|
216
|
+
unset($result['summary']['distance'], $result['summary']['duration']);
|
|
217
|
+
|
|
205
218
|
$result['summary'] = array_merge($result['summary'] ?? [], [
|
|
206
219
|
'engine' => 'vroom',
|
|
207
220
|
'allocation_strategy' => 'capacity_only',
|
|
@@ -339,6 +352,7 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
|
339
352
|
protected function mapVroomResponse(array $vroomResult, array $jobIdReverse): array
|
|
340
353
|
{
|
|
341
354
|
$assignments = [];
|
|
355
|
+
$startTime = now()->timestamp;
|
|
342
356
|
|
|
343
357
|
foreach ($vroomResult['routes'] ?? [] as $route) {
|
|
344
358
|
$vehicleDesc = json_decode($route['description'] ?? '{}', true);
|
|
@@ -355,13 +369,15 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
|
355
369
|
continue;
|
|
356
370
|
}
|
|
357
371
|
$assignments[] = [
|
|
358
|
-
'order_id'
|
|
359
|
-
'vehicle_id'
|
|
360
|
-
'driver_id'
|
|
361
|
-
'sequence'
|
|
362
|
-
'arrival'
|
|
363
|
-
'duration'
|
|
364
|
-
'distance'
|
|
372
|
+
'order_id' => $orderId,
|
|
373
|
+
'vehicle_id' => $vehicleId,
|
|
374
|
+
'driver_id' => $driverId,
|
|
375
|
+
'sequence' => ++$sequence,
|
|
376
|
+
'arrival' => $this->absoluteArrival($step['arrival'] ?? null, $startTime),
|
|
377
|
+
'duration' => $step['duration'] ?? null,
|
|
378
|
+
'distance' => $step['distance'] ?? null,
|
|
379
|
+
'route_distance' => $route['distance'] ?? null,
|
|
380
|
+
'route_duration' => $route['duration'] ?? null,
|
|
365
381
|
];
|
|
366
382
|
}
|
|
367
383
|
}
|
|
@@ -374,10 +390,24 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
|
374
390
|
return [
|
|
375
391
|
'assignments' => $assignments,
|
|
376
392
|
'unassigned' => array_values(array_unique($unassigned)),
|
|
377
|
-
'summary' => $vroomResult['summary'] ?? [],
|
|
393
|
+
'summary' => array_merge(['engine' => 'vroom'], $vroomResult['summary'] ?? []),
|
|
378
394
|
];
|
|
379
395
|
}
|
|
380
396
|
|
|
397
|
+
/**
|
|
398
|
+
* VROOM reports arrivals on the timeline of the time windows it was given.
|
|
399
|
+
* Without absolute windows that timeline starts at 0, so small values are
|
|
400
|
+
* offsets from the start of the run rather than unix timestamps.
|
|
401
|
+
*/
|
|
402
|
+
protected function absoluteArrival($arrival, int $startTime): ?int
|
|
403
|
+
{
|
|
404
|
+
if ($arrival === null) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return $arrival < self::ABSOLUTE_TIMESTAMP_THRESHOLD ? $startTime + (int) $arrival : (int) $arrival;
|
|
409
|
+
}
|
|
410
|
+
|
|
381
411
|
protected function mapTaskToVroomJob(array $task, array &$jobIdReverse): ?array
|
|
382
412
|
{
|
|
383
413
|
$stop = $task['stops'][0] ?? null;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Orchestration;
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Orchestration\Contracts\OrchestrationEngineInterface;
|
|
6
|
+
use Illuminate\Support\Collection;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* OrchestrationEngineRegistry.
|
|
@@ -10,7 +11,7 @@ use Fleetbase\FleetOps\Orchestration\Contracts\OrchestrationEngineInterface;
|
|
|
10
11
|
* A simple service-locator for orchestration engines. Engines register
|
|
11
12
|
* themselves (typically from a service provider) and the
|
|
12
13
|
* OrchestrationController resolves the active engine at runtime from the
|
|
13
|
-
*
|
|
14
|
+
* organization's Orchestrator settings (see OrchestratorSettings).
|
|
14
15
|
*
|
|
15
16
|
* Third-party extensions can register their own engines by resolving this
|
|
16
17
|
* registry in their service provider:
|
|
@@ -21,6 +22,11 @@ use Fleetbase\FleetOps\Orchestration\Contracts\OrchestrationEngineInterface;
|
|
|
21
22
|
*/
|
|
22
23
|
class OrchestrationEngineRegistry
|
|
23
24
|
{
|
|
25
|
+
/**
|
|
26
|
+
* Built-in engine that runs when the selected engine is missing or unavailable.
|
|
27
|
+
*/
|
|
28
|
+
public const FALLBACK_ENGINE = 'greedy';
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* Registered engines keyed by their identifier.
|
|
26
32
|
*
|
|
@@ -80,4 +86,58 @@ class OrchestrationEngineRegistry
|
|
|
80
86
|
{
|
|
81
87
|
return isset($this->engines[$identifier]);
|
|
82
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Run the selected engine, falling back to the built-in greedy engine when
|
|
92
|
+
* the selected engine is not registered or reports itself unavailable
|
|
93
|
+
* (a RuntimeException, e.g. VROOM unreachable or misconfigured).
|
|
94
|
+
*
|
|
95
|
+
* The result's summary always names the engine that produced it. A
|
|
96
|
+
* fallback result also carries `requested_engine` and `fallback_reason`
|
|
97
|
+
* so callers can tell the user the plan did not come from their engine.
|
|
98
|
+
*
|
|
99
|
+
* @throws \RuntimeException when the selected engine fails and there is no fallback to run
|
|
100
|
+
*/
|
|
101
|
+
public function allocateWithFallback(string $identifier, Collection $orders, Collection $vehicles, array $options = []): array
|
|
102
|
+
{
|
|
103
|
+
try {
|
|
104
|
+
$engine = $this->resolve($identifier);
|
|
105
|
+
$result = $engine->allocate($orders, $vehicles, $options);
|
|
106
|
+
$result['summary'] = array_merge(['engine' => $engine->getIdentifier()], $result['summary'] ?? []);
|
|
107
|
+
|
|
108
|
+
return $result;
|
|
109
|
+
} catch (\RuntimeException $e) {
|
|
110
|
+
if ($identifier === static::FALLBACK_ENGINE || !$this->has(static::FALLBACK_ENGINE)) {
|
|
111
|
+
throw $e;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The selected solvers build multi-stop routes, so the fallback
|
|
115
|
+
// must too or orders would be dropped when vehicles are scarce.
|
|
116
|
+
$result = $this->resolve(static::FALLBACK_ENGINE)->allocate($orders, $vehicles, array_merge(['allow_multi_order' => true], $options));
|
|
117
|
+
|
|
118
|
+
return static::markFallback($result, static::FALLBACK_ENGINE, $identifier, $e->getMessage());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Record on a result that it came from a different engine than requested, and why.
|
|
124
|
+
*
|
|
125
|
+
* @param bool $warn whether the substitution is a failure the user should be warned about,
|
|
126
|
+
* rather than the expected behaviour for the requested engine
|
|
127
|
+
*/
|
|
128
|
+
public static function markFallback(array $result, string $engine, string $requestedEngine, string $reason, bool $warn = true): array
|
|
129
|
+
{
|
|
130
|
+
$result['summary'] = array_merge($result['summary'] ?? [], [
|
|
131
|
+
'engine' => $engine,
|
|
132
|
+
'requested_engine' => $requestedEngine,
|
|
133
|
+
'fallback_reason' => $reason,
|
|
134
|
+
]);
|
|
135
|
+
if (!$warn) {
|
|
136
|
+
return $result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
$result['warning'] = sprintf('The "%s" engine was used instead of "%s": %s', $engine, $requestedEngine, $reason);
|
|
140
|
+
|
|
141
|
+
return $result;
|
|
142
|
+
}
|
|
83
143
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Orchestration\Support;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Models\Setting;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* OrchestratorSettings.
|
|
9
|
+
*
|
|
10
|
+
* Single reader for the organization's Orchestrator settings. The settings
|
|
11
|
+
* page saves them as one company setting (`fleet-ops.allocation-settings`);
|
|
12
|
+
* every runtime consumer (workbench runs, the public API, auto-allocation)
|
|
13
|
+
* reads them through here so they all see what the organization selected.
|
|
14
|
+
*/
|
|
15
|
+
class OrchestratorSettings
|
|
16
|
+
{
|
|
17
|
+
public const KEY = 'fleet-ops.allocation-settings';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* System-level engine key read before the company setting existed.
|
|
21
|
+
* Still honoured as the default for organizations that never saved.
|
|
22
|
+
*/
|
|
23
|
+
public const LEGACY_ENGINE_KEY = 'fleetops.orchestrator_engine';
|
|
24
|
+
|
|
25
|
+
public const DEFAULT_ENGINE = 'greedy';
|
|
26
|
+
|
|
27
|
+
public static function defaults(): array
|
|
28
|
+
{
|
|
29
|
+
return [
|
|
30
|
+
'allocation_engine' => static::DEFAULT_ENGINE,
|
|
31
|
+
'auto_allocate_on_create' => false,
|
|
32
|
+
'auto_reallocate_on_complete' => false,
|
|
33
|
+
'max_travel_time_seconds' => 3600,
|
|
34
|
+
'balance_workload' => false,
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the organization's settings merged over the defaults.
|
|
40
|
+
*/
|
|
41
|
+
public static function forCompany(?string $companyUuid): array
|
|
42
|
+
{
|
|
43
|
+
$saved = Setting::lookupForCompany($companyUuid, static::KEY, []);
|
|
44
|
+
$saved = array_filter(is_array($saved) ? $saved : [], fn ($value) => $value !== null && $value !== '');
|
|
45
|
+
$defaults = static::defaults();
|
|
46
|
+
|
|
47
|
+
if (!isset($saved['allocation_engine'])) {
|
|
48
|
+
$defaults['allocation_engine'] = Setting::lookup(static::LEGACY_ENGINE_KEY) ?: static::DEFAULT_ENGINE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return array_merge($defaults, $saved);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Identifier of the orchestration engine the organization selected.
|
|
56
|
+
*/
|
|
57
|
+
public static function engineForCompany(?string $companyUuid): string
|
|
58
|
+
{
|
|
59
|
+
return (string) static::forCompany($companyUuid)['allocation_engine'];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Orchestration\Support;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
6
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
7
|
+
use Illuminate\Support\Collection;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* RouteMetrics.
|
|
11
|
+
*
|
|
12
|
+
* Estimates per-stop arrival times and route distance/duration for engines
|
|
13
|
+
* that only decide a sequence (greedy, the built-in route sequencer) or for
|
|
14
|
+
* solver routes that came back without some of those figures.
|
|
15
|
+
*
|
|
16
|
+
* Legs are measured with FleetOps' local distance-matrix calculation
|
|
17
|
+
* (Utils::calculateDrivingDistanceAndTime), which needs no routing provider,
|
|
18
|
+
* so estimates are always available and are flagged as such in the summary.
|
|
19
|
+
*
|
|
20
|
+
* Assignment fields follow the VROOM step convention so every engine
|
|
21
|
+
* produces the same shape:
|
|
22
|
+
* - arrival: unix timestamp the vehicle reaches the order's last stop
|
|
23
|
+
* - distance: cumulative route distance (m) at that stop
|
|
24
|
+
* - duration: cumulative travel time (s) at that stop
|
|
25
|
+
* - route_distance: total distance (m) of the vehicle's route
|
|
26
|
+
* - route_duration: total travel time (s) of the vehicle's route
|
|
27
|
+
*/
|
|
28
|
+
class RouteMetrics
|
|
29
|
+
{
|
|
30
|
+
/**
|
|
31
|
+
* Measure an ordered stop list.
|
|
32
|
+
*
|
|
33
|
+
* @param array|null $start [lat, lng] the vehicle starts from, or null to start at the first stop
|
|
34
|
+
* @param array $stops ordered list of ['order_id' => string, 'lat' => float, 'lng' => float]
|
|
35
|
+
* @param int $startTime unix timestamp the route starts
|
|
36
|
+
*
|
|
37
|
+
* @return array{orders: array<string, array{arrival: int, distance: int, duration: int}>, distance: int, duration: int}
|
|
38
|
+
*/
|
|
39
|
+
public static function measure(?array $start, array $stops, int $startTime): array
|
|
40
|
+
{
|
|
41
|
+
$distance = 0.0;
|
|
42
|
+
$duration = 0.0;
|
|
43
|
+
$previous = $start;
|
|
44
|
+
$orders = [];
|
|
45
|
+
|
|
46
|
+
foreach ($stops as $stop) {
|
|
47
|
+
$current = [(float) $stop['lat'], (float) $stop['lng']];
|
|
48
|
+
|
|
49
|
+
if ($previous !== null) {
|
|
50
|
+
$leg = Utils::calculateDrivingDistanceAndTime(new Point($previous[0], $previous[1]), new Point($current[0], $current[1]));
|
|
51
|
+
$distance += (float) $leg->distance;
|
|
52
|
+
$duration += (float) $leg->time;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The order's figures are those of its last stop in the route.
|
|
56
|
+
$orders[$stop['order_id']] = [
|
|
57
|
+
'arrival' => $startTime + (int) round($duration),
|
|
58
|
+
'distance' => (int) round($distance),
|
|
59
|
+
'duration' => (int) round($duration),
|
|
60
|
+
];
|
|
61
|
+
$previous = $current;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
'orders' => $orders,
|
|
66
|
+
'distance' => (int) round($distance),
|
|
67
|
+
'duration' => (int) round($duration),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fill in arrival, distance and duration figures the engine did not supply.
|
|
73
|
+
*
|
|
74
|
+
* A route without any engine timing is estimated in full; a route with
|
|
75
|
+
* engine timing keeps the engine's figures and only has gaps filled.
|
|
76
|
+
*
|
|
77
|
+
* @param array $result standard orchestration result
|
|
78
|
+
* @param Collection $orders orders that were orchestrated, keyed or not
|
|
79
|
+
* @param Collection $vehicles vehicles that were available to the run
|
|
80
|
+
*/
|
|
81
|
+
public static function annotate(array $result, Collection $orders, Collection $vehicles, ?int $startTime = null): array
|
|
82
|
+
{
|
|
83
|
+
$assignments = $result['assignments'] ?? [];
|
|
84
|
+
if (empty($assignments)) {
|
|
85
|
+
return $result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$startTime = $startTime ?? now()->timestamp;
|
|
89
|
+
$ordersById = $orders->keyBy('public_id');
|
|
90
|
+
$vehiclesById = $vehicles->keyBy('public_id');
|
|
91
|
+
$routes = [];
|
|
92
|
+
$estimated = false;
|
|
93
|
+
|
|
94
|
+
foreach ($assignments as $index => $assignment) {
|
|
95
|
+
$routes[$assignment['vehicle_id'] ?? ''][] = $index;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
foreach ($routes as $vehicleId => $indexes) {
|
|
99
|
+
usort($indexes, fn ($a, $b) => ($assignments[$a]['sequence'] ?? 0) <=> ($assignments[$b]['sequence'] ?? 0));
|
|
100
|
+
|
|
101
|
+
$hasEngineTiming = collect($indexes)->contains(fn ($i) => ($assignments[$i]['arrival'] ?? null) !== null
|
|
102
|
+
|| ($assignments[$i]['route_duration'] ?? null) !== null
|
|
103
|
+
|| ($assignments[$i]['route_distance'] ?? null) !== null);
|
|
104
|
+
|
|
105
|
+
$stops = [];
|
|
106
|
+
foreach ($indexes as $i) {
|
|
107
|
+
$order = $ordersById->get($assignments[$i]['order_id'] ?? null);
|
|
108
|
+
if (!$order) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
foreach (OrchestrationPayloadBuilder::buildRouteStops($order) as $stop) {
|
|
112
|
+
$stops[] = ['order_id' => $order->public_id, 'lng' => $stop['location'][0], 'lat' => $stop['location'][1]];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
$metrics = static::measure(static::vehicleStart($vehiclesById->get($vehicleId), $ordersById, $assignments, $indexes), $stops, $startTime);
|
|
117
|
+
|
|
118
|
+
foreach ($indexes as $i) {
|
|
119
|
+
$orderMetrics = $metrics['orders'][$assignments[$i]['order_id'] ?? ''] ?? null;
|
|
120
|
+
$estimate = [
|
|
121
|
+
'arrival' => $orderMetrics['arrival'] ?? null,
|
|
122
|
+
'distance' => $orderMetrics['distance'] ?? null,
|
|
123
|
+
'duration' => $orderMetrics['duration'] ?? null,
|
|
124
|
+
'route_distance' => $metrics['distance'],
|
|
125
|
+
'route_duration' => $metrics['duration'],
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
foreach ($estimate as $key => $value) {
|
|
129
|
+
if (!$hasEngineTiming || ($assignments[$i][$key] ?? null) === null) {
|
|
130
|
+
$assignments[$i][$key] = $value;
|
|
131
|
+
$estimated = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
$result['assignments'] = $assignments;
|
|
138
|
+
|
|
139
|
+
$summary = $result['summary'] ?? [];
|
|
140
|
+
$routeTotals = collect($routes)->map(fn ($indexes) => $assignments[$indexes[0]]);
|
|
141
|
+
$summary['distance'] ??= (int) $routeTotals->sum('route_distance');
|
|
142
|
+
$summary['duration'] ??= (int) $routeTotals->sum('route_duration');
|
|
143
|
+
$summary['metrics'] = $estimated ? 'estimated' : ($summary['metrics'] ?? 'engine');
|
|
144
|
+
$result['summary'] = $summary;
|
|
145
|
+
|
|
146
|
+
return $result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve the [lat, lng] a vehicle starts from: the run's vehicle list
|
|
151
|
+
* first, then the vehicle relation of one of its orders.
|
|
152
|
+
*/
|
|
153
|
+
protected static function vehicleStart($vehicle, Collection $ordersById, array $assignments, array $indexes): ?array
|
|
154
|
+
{
|
|
155
|
+
if (!$vehicle) {
|
|
156
|
+
$order = $ordersById->get($assignments[$indexes[0]]['order_id'] ?? null);
|
|
157
|
+
$vehicle = $order?->relationLoaded('vehicle') ? $order->vehicle : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
$driver = $vehicle?->relationLoaded('driver') ? $vehicle->driver : null;
|
|
161
|
+
$location = $driver?->location ?? $vehicle?->location;
|
|
162
|
+
|
|
163
|
+
return $location ? [(float) $location->getLat(), (float) $location->getLng()] : null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -23,6 +23,17 @@ class EventServiceProvider extends ServiceProvider
|
|
|
23
23
|
\Fleetbase\FleetOps\Events\OrderFailed::class => [\Fleetbase\Listeners\SendResourceLifecycleWebhook::class, \Fleetbase\FleetOps\Listeners\NotifyOrderEvent::class],
|
|
24
24
|
\Fleetbase\FleetOps\Events\OrderReady::class => [\Fleetbase\FleetOps\Listeners\HandleOrderReady::class],
|
|
25
25
|
|
|
26
|
+
/*
|
|
27
|
+
* Stop Events
|
|
28
|
+
*
|
|
29
|
+
* Public tracking channels follow waypoint and entity activity. Order status, assignment
|
|
30
|
+
* and ETA changes are published from the order observer; positions from the tracking endpoints.
|
|
31
|
+
*/
|
|
32
|
+
\Fleetbase\FleetOps\Events\WaypointActivityChanged::class => [\Fleetbase\FleetOps\Listeners\PublishTrackingUpdates::class],
|
|
33
|
+
\Fleetbase\FleetOps\Events\WaypointCompleted::class => [\Fleetbase\FleetOps\Listeners\PublishTrackingUpdates::class],
|
|
34
|
+
\Fleetbase\FleetOps\Events\EntityActivityChanged::class => [\Fleetbase\FleetOps\Listeners\PublishTrackingUpdates::class],
|
|
35
|
+
\Fleetbase\FleetOps\Events\EntityCompleted::class => [\Fleetbase\FleetOps\Listeners\PublishTrackingUpdates::class],
|
|
36
|
+
|
|
26
37
|
/*
|
|
27
38
|
* Geofence Events
|
|
28
39
|
*
|
|
@@ -159,6 +159,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
159
159
|
});
|
|
160
160
|
$this->registerNotifications();
|
|
161
161
|
$this->registerAiCapabilities();
|
|
162
|
+
$this->registerSocketChannels();
|
|
162
163
|
$this->registerExpansionsFrom(__DIR__ . '/../Expansions');
|
|
163
164
|
|
|
164
165
|
// Register built-in orchestration engines.
|
|
@@ -251,6 +252,8 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
251
252
|
\Fleetbase\FleetOps\Notifications\LateDeparture::class,
|
|
252
253
|
\Fleetbase\FleetOps\Notifications\RouteDeviation::class,
|
|
253
254
|
\Fleetbase\FleetOps\Notifications\ProlongedStoppage::class,
|
|
255
|
+
\Fleetbase\FleetOps\Notifications\OrderChanged::class,
|
|
256
|
+
\Fleetbase\FleetOps\Notifications\OrderUnassigned::class,
|
|
254
257
|
]);
|
|
255
258
|
|
|
256
259
|
// Register Notifiables
|
|
@@ -265,6 +268,16 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
265
268
|
]);
|
|
266
269
|
}
|
|
267
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Register who may subscribe to FleetOps realtime channels, and the driver socket principal.
|
|
273
|
+
*/
|
|
274
|
+
protected function registerSocketChannels(): void
|
|
275
|
+
{
|
|
276
|
+
$this->callAfterResolving(\Fleetbase\Support\SocketCluster\SocketChannelRegistry::class, function (\Fleetbase\Support\SocketCluster\SocketChannelRegistry $registry) {
|
|
277
|
+
\Fleetbase\FleetOps\Support\SocketChannels::register($registry);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
268
281
|
protected function registerAiCapabilities(): void
|
|
269
282
|
{
|
|
270
283
|
if (!Utils::classExists(\Fleetbase\Ai\Support\AiCapabilityRegistry::class)) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Support\ApiModelCache;
|
|
6
|
+
use Illuminate\Database\Eloquent\Model;
|
|
7
|
+
use Illuminate\Support\Facades\Cache;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Invalidates API caches at most once per window for writes too frequent to
|
|
11
|
+
* invalidate on every call.
|
|
12
|
+
*
|
|
13
|
+
* Location pings write drivers and vehicles every few seconds per device, and
|
|
14
|
+
* order lists embed both, so invalidating on each ping would keep those lists
|
|
15
|
+
* from ever being served from cache. Throttled, a ping costs one Cache::add,
|
|
16
|
+
* and cached lists show a location at most a window old.
|
|
17
|
+
*/
|
|
18
|
+
class ApiCacheThrottle
|
|
19
|
+
{
|
|
20
|
+
/**
|
|
21
|
+
* Seconds between invalidations for location writes.
|
|
22
|
+
*/
|
|
23
|
+
public const LOCATION_WINDOW = 60;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run a frequent write (a telemetry save) with its model events, but invalidate
|
|
27
|
+
* the model's cached lists at most once per window instead of on every write.
|
|
28
|
+
*
|
|
29
|
+
* Order and food truck lists embed vehicles, so invalidating them on every
|
|
30
|
+
* telemetry event would keep them from ever being served from cache for any
|
|
31
|
+
* company with streaming devices. Needs ApiModelCache::withoutInvalidating()
|
|
32
|
+
* (fleetbase/core-api#301); with an older core the write invalidates as before.
|
|
33
|
+
*
|
|
34
|
+
* @template T
|
|
35
|
+
*
|
|
36
|
+
* @param \Closure(): T $write
|
|
37
|
+
*
|
|
38
|
+
* @return T
|
|
39
|
+
*/
|
|
40
|
+
public static function telemetryWrite(Model $model, \Closure $write, int $seconds = self::LOCATION_WINDOW): mixed
|
|
41
|
+
{
|
|
42
|
+
// @codeCoverageIgnoreStart
|
|
43
|
+
// fleetbase/core-api ^1.6.69, which this package requires, always has it; the
|
|
44
|
+
// branch only serves an install that pinned an older core by hand.
|
|
45
|
+
if (!method_exists(ApiModelCache::class, 'withoutInvalidating')) {
|
|
46
|
+
return $write();
|
|
47
|
+
}
|
|
48
|
+
// @codeCoverageIgnoreEnd
|
|
49
|
+
|
|
50
|
+
$result = ApiModelCache::withoutInvalidating($write);
|
|
51
|
+
static::invalidate($model, $model->company_uuid, $seconds);
|
|
52
|
+
|
|
53
|
+
return $result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Invalidate the model's table for the company unless that was done within the window.
|
|
58
|
+
*/
|
|
59
|
+
public static function invalidate(Model $model, ?string $companyUuid, int $seconds = self::LOCATION_WINDOW): void
|
|
60
|
+
{
|
|
61
|
+
if (!ApiModelCache::isCachingEnabled()) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$key = 'fleetops:api_cache_throttle:' . $model->getTable() . ':' . ($companyUuid ?? 'all');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
if (Cache::add($key, true, $seconds)) {
|
|
69
|
+
ApiModelCache::invalidateModelCache($model, $companyUuid);
|
|
70
|
+
}
|
|
71
|
+
} catch (\Throwable $e) {
|
|
72
|
+
// A location ping must never fail on cache bookkeeping; cached lists then
|
|
73
|
+
// show the location until their TTL. Core versions before #301 can throw here.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|