@fleetbase/fleetops-engine 0.6.64 → 0.6.65
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 +7 -5
- package/addon/components/attachment-list.hbs +65 -0
- package/addon/components/attachment-list.js +8 -0
- package/addon/components/cell/attached-vehicle.hbs +1 -1
- package/addon/components/cell/attached-vehicle.js +3 -3
- package/addon/components/cell/driver-identity.js +4 -4
- package/addon/components/cell/driver-name.hbs +1 -1
- package/addon/components/cell/translated-value.hbs +9 -0
- package/addon/components/cell/translated-value.js +57 -0
- package/addon/components/contact/card.hbs +58 -0
- package/addon/components/contact/card.js +19 -0
- package/addon/components/contact/form.hbs +1 -1
- package/addon/components/contact/panel-header.hbs +1 -1
- package/addon/components/customer/form.hbs +2 -2
- package/addon/components/device/manager.hbs +26 -28
- package/addon/components/device/manager.js +28 -2
- package/addon/components/device/panel-tabs/vehicle.hbs +13 -13
- package/addon/components/device/panel-tabs/vehicle.js +8 -0
- package/addon/components/driver/card.hbs +24 -13
- package/addon/components/driver/card.js +20 -6
- package/addon/components/driver/form.hbs +3 -3
- package/addon/components/driver/panel-header.hbs +2 -2
- package/addon/components/driver/pill.hbs +2 -2
- package/addon/components/equipment/form.js +11 -4
- package/addon/components/equipment/pill.hbs +20 -0
- package/addon/components/equipment/pill.js +3 -0
- package/addon/components/filter/multi-model.hbs +19 -0
- package/addon/components/filter/multi-model.js +90 -0
- package/addon/components/fleet/driver-listing.hbs +1 -1
- package/addon/components/fleet/vehicle-listing.hbs +1 -1
- package/addon/components/fleet-panel/driver-listing.hbs +1 -1
- package/addon/components/fleet-panel/vehicle-listing.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +9 -7
- package/addon/components/maintenance/form.js +12 -4
- package/addon/components/maintenance-schedule/form.js +13 -7
- package/addon/components/map/leaflet-live-map.hbs +52 -0
- package/addon/components/map/order-list-overlay/order.hbs +4 -4
- package/addon/components/modals/attach-equipment.hbs +22 -0
- package/addon/components/modals/attach-equipment.js +3 -0
- package/addon/components/modals/attach-telematic-device.hbs +13 -8
- package/addon/components/modals/attach-telematic-device.js +11 -1
- package/addon/components/modals/attach-trailer.hbs +30 -0
- package/addon/components/modals/attach-trailer.js +3 -0
- package/addon/components/modals/contact-form.hbs +2 -2
- package/addon/components/modals/convert-customer-to-vendor.hbs +1 -1
- package/addon/components/modals/driver-form.hbs +2 -2
- package/addon/components/modals/select-trailer.hbs +35 -0
- package/addon/components/modals/select-trailer.js +3 -0
- package/addon/components/modals/vehicle-form.hbs +2 -2
- package/addon/components/orchestrator/resource-panel.hbs +2 -2
- package/addon/components/order/customer-avatar-stack.hbs +2 -2
- package/addon/components/order/details/detail.hbs +4 -4
- package/addon/components/order/details/payload.hbs +2 -2
- package/addon/components/order/kanban-card.hbs +4 -4
- package/addon/components/order-list-overlay/order.hbs +4 -4
- package/addon/components/trailer/card.hbs +68 -0
- package/addon/components/trailer/card.js +22 -0
- package/addon/components/trailer/details/connections.hbs +57 -0
- package/addon/components/trailer/details/connections.js +59 -0
- package/addon/components/trailer/details/equipment.hbs +25 -0
- package/addon/components/trailer/details/equipment.js +52 -0
- package/addon/components/trailer/details/maintenance-history.hbs +47 -0
- package/addon/components/trailer/details/maintenance-history.js +40 -0
- package/addon/components/trailer/details/schedules.hbs +45 -0
- package/addon/components/trailer/details/schedules.js +40 -0
- package/addon/components/trailer/details/work-orders.hbs +47 -0
- package/addon/components/trailer/details/work-orders.js +40 -0
- package/addon/components/trailer/details.hbs +399 -0
- package/addon/components/trailer/details.js +118 -0
- package/addon/components/trailer/form.hbs +447 -0
- package/addon/components/trailer/form.js +165 -0
- package/addon/components/trailer/panel-header.hbs +45 -0
- package/addon/components/trailer/panel-header.js +3 -0
- package/addon/components/trailer/pill.hbs +21 -0
- package/addon/components/trailer/pill.js +3 -0
- package/addon/components/vehicle/card.hbs +42 -11
- package/addon/components/vehicle/card.js +31 -0
- package/addon/components/vehicle/details/equipment.hbs +25 -0
- package/addon/components/vehicle/details/equipment.js +55 -0
- package/addon/components/vehicle/details/trailers.hbs +35 -0
- package/addon/components/vehicle/details/trailers.js +55 -0
- package/addon/components/vehicle/form.hbs +2 -2
- package/addon/components/vehicle/panel-header.hbs +2 -2
- package/addon/components/vehicle/pill.hbs +2 -2
- package/addon/components/vendor/card.hbs +58 -0
- package/addon/components/vendor/card.js +6 -0
- package/addon/components/vendor/panel-header.hbs +2 -2
- package/addon/components/vendor/personnel-manager.hbs +2 -2
- package/addon/components/work-order/form.js +12 -4
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +11 -0
- package/addon/controllers/connectivity/devices/index/details.js +3 -3
- package/addon/controllers/connectivity/devices/index.js +1 -0
- package/addon/controllers/connectivity/telematics/details/attachments.js +28 -47
- package/addon/controllers/connectivity/telematics/details/devices.js +1 -0
- package/addon/controllers/management/contacts/customers.js +4 -0
- package/addon/controllers/management/contacts/index.js +32 -0
- package/addon/controllers/management/trailers/index/details.js +119 -0
- package/addon/controllers/management/trailers/index/edit.js +67 -0
- package/addon/controllers/management/trailers/index/new.js +43 -0
- package/addon/controllers/management/trailers/index.js +599 -0
- package/addon/controllers/management/vehicles/index/details.js +16 -0
- package/addon/controllers/management/vehicles/index.js +78 -4
- package/addon/controllers/management/vendors/index.js +32 -0
- package/addon/controllers/operations/orders/index.js +3 -1
- package/addon/controllers/settings/custom-fields.js +8 -0
- package/addon/extension.js +7 -0
- package/addon/helpers/placeholder-image.js +6 -0
- package/addon/helpers/resource-image.js +10 -0
- package/addon/helpers/trailer-placeholder-image.js +6 -0
- package/addon/routes/management/trailers/index/details/connections.js +2 -0
- package/addon/routes/management/trailers/index/details/devices.js +2 -0
- package/addon/routes/management/trailers/index/details/equipment.js +2 -0
- package/addon/routes/management/trailers/index/details/index.js +2 -0
- package/addon/routes/management/trailers/index/details/maintenance-history.js +2 -0
- package/addon/routes/management/trailers/index/details/positions.js +2 -0
- package/addon/routes/management/trailers/index/details/schedules.js +2 -0
- package/addon/routes/management/trailers/index/details/work-orders.js +2 -0
- package/addon/routes/management/trailers/index/details.js +30 -0
- package/addon/routes/management/trailers/index/edit.js +23 -0
- package/addon/routes/management/trailers/index/new.js +14 -0
- package/addon/routes/management/trailers/index.js +42 -0
- package/addon/routes/management/vehicles/index/details/trailers.js +2 -0
- package/addon/routes/management/vehicles/index.js +7 -1
- package/addon/routes.js +17 -0
- package/addon/services/device-actions.js +26 -16
- package/addon/services/trailer-actions.js +394 -0
- package/addon/services/vehicle-actions.js +111 -0
- package/addon/styles/fleetops-engine.css +349 -0
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +19 -19
- package/addon/templates/connectivity/telematics/details/attachments.hbs +15 -15
- package/addon/templates/management/contacts/customers.hbs +48 -29
- package/addon/templates/management/contacts/index.hbs +52 -33
- package/addon/templates/management/trailers/index/details/connections.hbs +1 -0
- package/addon/templates/management/trailers/index/details/devices.hbs +1 -0
- package/addon/templates/management/trailers/index/details/equipment.hbs +1 -0
- package/addon/templates/management/trailers/index/details/index.hbs +1 -0
- package/addon/templates/management/trailers/index/details/maintenance-history.hbs +1 -0
- package/addon/templates/management/trailers/index/details/positions.hbs +1 -0
- package/addon/templates/management/trailers/index/details/schedules.hbs +1 -0
- package/addon/templates/management/trailers/index/details/work-orders.hbs +1 -0
- package/addon/templates/management/trailers/index/details.hbs +15 -0
- package/addon/templates/management/trailers/index/edit.hbs +12 -0
- package/addon/templates/management/trailers/index/new.hbs +11 -0
- package/addon/templates/management/trailers/index.hbs +67 -0
- package/addon/templates/management/vehicles/index/details/equipment.hbs +1 -1
- package/addon/templates/management/vehicles/index/details/trailers.hbs +1 -0
- package/addon/templates/management/vehicles/index/details.hbs +1 -1
- package/addon/templates/management/vendors/index.hbs +66 -32
- package/addon/utils/device-table-columns.js +10 -6
- package/addon/utils/live-map-card-content.js +56 -0
- package/addon/utils/placeholder-images.js +71 -0
- package/addon/utils/trailer-placeholder-image.js +11 -0
- package/app/components/attachment-list.js +1 -0
- package/app/components/cell/translated-value.js +1 -0
- package/app/components/contact/card.js +1 -0
- package/app/components/equipment/pill.js +1 -0
- package/app/components/filter/multi-model.js +1 -0
- package/app/components/modals/attach-equipment.js +1 -0
- package/app/components/modals/attach-trailer.js +1 -0
- package/app/components/modals/select-trailer.js +1 -0
- package/app/components/trailer/card.js +1 -0
- package/app/components/trailer/details/connections.js +1 -0
- package/app/components/trailer/details/equipment.js +1 -0
- package/app/components/trailer/details/maintenance-history.js +1 -0
- package/app/components/trailer/details/schedules.js +1 -0
- package/app/components/trailer/details/work-orders.js +1 -0
- package/app/components/trailer/details.js +1 -0
- package/app/components/trailer/form.js +1 -0
- package/app/components/trailer/panel-header.js +1 -0
- package/app/components/trailer/pill.js +1 -0
- package/app/components/vehicle/details/equipment.js +1 -0
- package/app/components/vehicle/details/trailers.js +1 -0
- package/app/components/vendor/card.js +1 -0
- package/app/controllers/management/trailers/index/details.js +1 -0
- package/app/controllers/management/trailers/index/edit.js +1 -0
- package/app/controllers/management/trailers/index/new.js +1 -0
- package/app/controllers/management/trailers/index.js +1 -0
- package/app/helpers/placeholder-image.js +1 -0
- package/app/helpers/resource-image.js +1 -0
- package/app/helpers/trailer-placeholder-image.js +1 -0
- package/app/routes/management/trailers/index/details/connections.js +1 -0
- package/app/routes/management/trailers/index/details/devices.js +1 -0
- package/app/routes/management/trailers/index/details/equipment.js +1 -0
- package/app/routes/management/trailers/index/details/index.js +1 -0
- package/app/routes/management/trailers/index/details/maintenance-history.js +1 -0
- package/app/routes/management/trailers/index/details/positions.js +1 -0
- package/app/routes/management/trailers/index/details/schedules.js +1 -0
- package/app/routes/management/trailers/index/details/work-orders.js +1 -0
- package/app/routes/management/trailers/index/details.js +1 -0
- package/app/routes/management/trailers/index/edit.js +1 -0
- package/app/routes/management/trailers/index/new.js +1 -0
- package/app/routes/management/trailers/index.js +1 -0
- package/app/routes/management/vehicles/index/details/trailers.js +1 -0
- package/app/services/trailer-actions.js +1 -0
- package/app/templates/management/trailers/index/details/connections.js +1 -0
- package/app/templates/management/trailers/index/details/devices.js +1 -0
- package/app/templates/management/trailers/index/details/equipment.js +1 -0
- package/app/templates/management/trailers/index/details/index.js +1 -0
- package/app/templates/management/trailers/index/details/maintenance-history.js +1 -0
- package/app/templates/management/trailers/index/details/positions.js +1 -0
- package/app/templates/management/trailers/index/details/schedules.js +1 -0
- package/app/templates/management/trailers/index/details/work-orders.js +1 -0
- package/app/templates/management/trailers/index/details.js +1 -0
- package/app/templates/management/trailers/index/edit.js +1 -0
- package/app/templates/management/trailers/index/new.js +1 -0
- package/app/templates/management/trailers/index.js +1 -0
- package/app/templates/management/vehicles/index/details/trailers.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/migrations/2026_09_03_000001_add_first_class_trailer_columns_to_assets_table.php +58 -0
- package/server/migrations/2026_09_03_000002_create_asset_connections_table.php +43 -0
- package/server/migrations/2026_09_03_000003_create_device_installations_table.php +33 -0
- package/server/src/Auth/Schemas/FleetOps.php +7 -0
- package/server/src/Events/TrailerLocationChanged.php +70 -0
- package/server/src/Exceptions/DeviceAlreadyAttachedException.php +18 -0
- package/server/src/Exports/TrailerExport.php +31 -0
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +8 -2
- package/server/src/Http/Controllers/Api/v1/DeviceController.php +22 -15
- package/server/src/Http/Controllers/Api/v1/EquipmentController.php +47 -0
- package/server/src/Http/Controllers/Api/v1/TrailerController.php +362 -0
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +3 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SearchController.php +16 -0
- package/server/src/Http/Controllers/Internal/v1/TrailerController.php +360 -0
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +74 -0
- package/server/src/Http/Filter/EquipmentFilter.php +51 -0
- package/server/src/Http/Filter/OrderFilter.php +15 -0
- package/server/src/Http/Filter/TrailerFilter.php +269 -0
- package/server/src/Http/Filter/VehicleFilter.php +70 -3
- package/server/src/Http/Requests/CreateDeviceRequest.php +1 -1
- package/server/src/Http/Requests/CreateEquipmentRequest.php +1 -1
- package/server/src/Http/Requests/CreateTrailerRequest.php +54 -0
- package/server/src/Http/Requests/UpdateTrailerRequest.php +7 -0
- package/server/src/Http/Resources/v1/AssetConnection.php +90 -0
- package/server/src/Http/Resources/v1/Device.php +3 -1
- package/server/src/Http/Resources/v1/Equipment.php +3 -1
- package/server/src/Http/Resources/v1/Index/Vehicle.php +10 -0
- package/server/src/Http/Resources/v1/Trailer.php +199 -0
- package/server/src/Http/Resources/v1/Vehicle.php +2 -0
- package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +3 -0
- package/server/src/Imports/TrailerImport.php +33 -0
- package/server/src/Models/Asset.php +39 -10
- package/server/src/Models/AssetConnection.php +43 -0
- package/server/src/Models/Device.php +39 -8
- package/server/src/Models/DeviceInstallation.php +27 -0
- package/server/src/Models/Equipment.php +2 -0
- package/server/src/Models/Trailer.php +181 -0
- package/server/src/Models/Vehicle.php +13 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +4 -2
- package/server/src/Support/Ai/FleetOpsAiQueryResources.php +22 -0
- package/server/src/Support/Reporting/FleetOpsReportSchema.php +38 -0
- package/server/src/Support/Telematics/TelematicService.php +35 -0
- package/server/src/Support/TrailerSummary.php +35 -0
- package/server/src/routes.php +29 -0
- package/server/tests/CompactResourceSerializationTest.php +36 -0
- package/server/tests/ControllerFilterContractsTest.php +29 -0
- package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +30 -13
- package/server/tests/Feature/Http/Api/TrailerControllerLifecycleTest.php +640 -0
- package/server/tests/Feature/Http/Internal/DeviceControllerContractsTest.php +20 -5
- package/server/tests/Feature/Http/Internal/LiveControllerEndpointsTest.php +19 -0
- package/server/tests/ModelAccessorContractsTest.php +10 -0
- package/server/tests/OrderFilterExecutionTest.php +5 -0
- package/server/tests/PublicApiControllerInputContractsTest.php +11 -0
- package/server/tests/PublicFleetOpsRestEndpointsTest.php +15 -3
- package/server/tests/ReportSchemaContractsTest.php +4 -0
- package/server/tests/TelematicsHardeningTest.php +4 -2
- package/server/tests/TrailerSupportCoverageTest.php +524 -0
- package/server/tests/Unit/Http/Resources/IndexVehicleResourceTest.php +35 -0
- package/server/tests/Unit/Models/EquipmentTest.php +22 -0
- package/server/tests/Unit/Models/ModelLookupFallbackSeamsTest.php +7 -3
- package/server/tests/Unit/Models/TrailerTest.php +204 -0
- package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +93 -0
- package/server/tests/VehicleControllerHelperContractsTest.php +14 -0
- package/translations/ar-ae.yml +396 -0
- package/translations/bg-bg.yaml +424 -0
- package/translations/en-us.yaml +423 -0
- package/translations/es-pa.yaml +425 -0
- package/translations/fr-fr.yaml +424 -0
- package/translations/mn-mn.yaml +424 -0
- package/translations/pt-br.yaml +424 -0
- package/translations/ru-ru.yaml +424 -0
- package/translations/vi-vn.yaml +435 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Models;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
6
|
+
use Fleetbase\Models\Model;
|
|
7
|
+
use Illuminate\Database\Eloquent\Builder;
|
|
8
|
+
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
9
|
+
use Illuminate\Database\Eloquent\Relations\HasOne;
|
|
10
|
+
|
|
11
|
+
class Trailer extends Asset
|
|
12
|
+
{
|
|
13
|
+
public const ASSET_CLASS = 'trailer';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lifecycle statuses. Attachment and connectivity are derived separately.
|
|
17
|
+
*/
|
|
18
|
+
public const STATUSES = ['available', 'in_use', 'maintenance', 'out_of_service', 'retired'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stable trailer classification values shared by validation, import and the console.
|
|
22
|
+
*/
|
|
23
|
+
public const TYPES = ['dry_van', 'reefer', 'flatbed', 'step_deck', 'lowboy', 'tanker', 'bulk', 'dump', 'chassis', 'curtain_side', 'car_carrier', 'livestock', 'logging', 'dolly', 'specialty', 'other'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ownership arrangements accepted for a trailer.
|
|
27
|
+
*/
|
|
28
|
+
public const OWNERSHIP_TYPES = ['owned', 'leased', 'financed', 'rented'];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Keep the API payload identity independent from the shared assets table.
|
|
32
|
+
*/
|
|
33
|
+
protected string $payloadKey = 'trailer';
|
|
34
|
+
|
|
35
|
+
protected $publicIdType = 'trailer';
|
|
36
|
+
|
|
37
|
+
protected $appends = ['category_name', 'vendor_name', 'warranty_name', 'current_location', 'photo_url', 'display_name', 'is_online', 'last_maintenance', 'next_maintenance_due', 'attachment_state', 'connectivity_status'];
|
|
38
|
+
|
|
39
|
+
protected $fillable = [
|
|
40
|
+
'company_uuid', 'category_uuid', 'vendor_uuid', 'warranty_uuid', 'telematic_uuid', 'current_place_uuid', 'photo_uuid',
|
|
41
|
+
'name', 'description', 'code', 'type', 'body_type', 'location', 'speed', 'heading', 'altitude', 'status', 'online', 'last_online_at',
|
|
42
|
+
'usage_type', 'vin', 'plate_number', 'make', 'model', 'year', 'color', 'serial_number', 'measurement_system', 'odometer', 'odometer_unit',
|
|
43
|
+
'ownership_type', 'engine_hours', 'length', 'width', 'height', 'tare_weight', 'gvwr', 'payload_capacity', 'cargo_volume', 'axle_count',
|
|
44
|
+
'tire_count', 'door_count', 'coupling_type', 'brake_type', 'abs_equipped', 'ebs_equipped', 'refrigerated', 'temperature_min',
|
|
45
|
+
'temperature_max', 'reefer_engine_hours', 'acquisition_cost', 'current_value', 'depreciation_rate', 'insurance_value', 'currency',
|
|
46
|
+
'financing_status', 'purchased_at', 'lease_expires_at', 'capacity', 'specs', 'attributes', 'telematics', 'notes', 'slug',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
protected $casts = [
|
|
50
|
+
'year' => 'integer', 'odometer' => 'integer', 'engine_hours' => 'integer', 'axle_count' => 'integer', 'tire_count' => 'integer',
|
|
51
|
+
'door_count' => 'integer', 'online' => 'boolean', 'abs_equipped' => 'boolean', 'ebs_equipped' => 'boolean', 'refrigerated' => 'boolean',
|
|
52
|
+
'last_online_at' => 'datetime', 'purchased_at' => 'date', 'lease_expires_at' => 'date', 'location' => \Fleetbase\FleetOps\Casts\Point::class,
|
|
53
|
+
'capacity' => \Fleetbase\Casts\Json::class, 'specs' => \Fleetbase\Casts\Json::class, 'attributes' => \Fleetbase\Casts\Json::class,
|
|
54
|
+
'telematics' => \Fleetbase\Casts\Json::class,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
protected static function booted(): void
|
|
58
|
+
{
|
|
59
|
+
static::addGlobalScope('trailer', fn (Builder $query) => $query->where($query->qualifyColumn('asset_class'), static::ASSET_CLASS));
|
|
60
|
+
static::creating(function (Trailer $trailer) {
|
|
61
|
+
$trailer->asset_class = static::ASSET_CLASS;
|
|
62
|
+
$trailer->status ??= 'available';
|
|
63
|
+
});
|
|
64
|
+
static::saving(fn (Trailer $trailer) => $trailer->asset_class = static::ASSET_CLASS);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Bulk removal never deletes a trailer that is still coupled to a vehicle: the
|
|
69
|
+
* active towing connection would be orphaned. Attached trailers are skipped, and
|
|
70
|
+
* the operation is refused outright when every selected trailer is attached.
|
|
71
|
+
*/
|
|
72
|
+
public function bulkRemove($ids = [])
|
|
73
|
+
{
|
|
74
|
+
$ids = array_values(array_filter(array_map('strval', (array) $ids), 'strlen'));
|
|
75
|
+
$selected = static::where('company_uuid', session('company'))
|
|
76
|
+
->where(fn ($query) => $query->whereIn('uuid', $ids)->orWhereIn('public_id', $ids))
|
|
77
|
+
->get(['uuid', 'public_id']);
|
|
78
|
+
$attachedUuids = AssetConnection::where('company_uuid', session('company'))
|
|
79
|
+
->whereIn('active_connected_uuid', $selected->pluck('uuid'))
|
|
80
|
+
->pluck('active_connected_uuid')
|
|
81
|
+
->all();
|
|
82
|
+
$blocked = $selected
|
|
83
|
+
->filter(fn (Trailer $trailer) => in_array($trailer->uuid, $attachedUuids, true))
|
|
84
|
+
->flatMap(fn (Trailer $trailer) => [$trailer->uuid, $trailer->public_id])
|
|
85
|
+
->all();
|
|
86
|
+
$deletable = array_values(array_filter($ids, fn ($id) => !in_array($id, $blocked, true)));
|
|
87
|
+
|
|
88
|
+
if (empty($deletable)) {
|
|
89
|
+
throw new \RuntimeException('Detach the selected trailers from their vehicles before deleting them.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return parent::bulkRemove($deletable);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public function currentConnection(): HasOne
|
|
96
|
+
{
|
|
97
|
+
return $this->hasOne(AssetConnection::class, 'connected_uuid', 'uuid')
|
|
98
|
+
->where('connected_type', static::class)->whereNotNull('active_connected_uuid')->whereNull('disconnected_at');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public function devices(): HasMany
|
|
102
|
+
{
|
|
103
|
+
return $this->hasMany(Device::class, 'attachable_uuid', 'uuid')->where('attachable_type', static::class);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public function equipments(): HasMany
|
|
107
|
+
{
|
|
108
|
+
return $this->hasMany(Equipment::class, 'equipable_uuid', 'uuid')->where('equipable_type', static::class);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public function positions(): HasMany
|
|
112
|
+
{
|
|
113
|
+
return $this->hasMany(Position::class, 'subject_uuid', 'uuid')->where('subject_type', static::class);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public function maintenances(): HasMany
|
|
117
|
+
{
|
|
118
|
+
return $this->hasMany(Maintenance::class, 'maintainable_uuid', 'uuid')->where('maintainable_type', static::class);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public function maintenanceSchedules(): HasMany
|
|
122
|
+
{
|
|
123
|
+
return $this->hasMany(MaintenanceSchedule::class, 'subject_uuid', 'uuid')->where('subject_type', static::class);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public function workOrders(): HasMany
|
|
127
|
+
{
|
|
128
|
+
return $this->hasMany(WorkOrder::class, 'target_uuid', 'uuid')->where('target_type', static::class);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public function connections(): HasMany
|
|
132
|
+
{
|
|
133
|
+
return $this->hasMany(AssetConnection::class, 'connected_uuid', 'uuid')->where('connected_type', static::class)->latest('connected_at');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public function createPosition(array $attributes = [], Model|string|null $destination = null): ?Position
|
|
137
|
+
{
|
|
138
|
+
$latitude = $attributes['latitude'] ?? null;
|
|
139
|
+
$longitude = $attributes['longitude'] ?? null;
|
|
140
|
+
if ($latitude === null || $longitude === null) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Position::create([
|
|
145
|
+
'company_uuid' => $this->company_uuid,
|
|
146
|
+
'subject_uuid' => $this->uuid,
|
|
147
|
+
'subject_type' => static::class,
|
|
148
|
+
'destination_uuid' => $destination instanceof Model ? $destination->uuid : $destination,
|
|
149
|
+
'coordinates' => new Point((float) $latitude, (float) $longitude),
|
|
150
|
+
'heading' => $attributes['heading'] ?? null,
|
|
151
|
+
'bearing' => $attributes['bearing'] ?? null,
|
|
152
|
+
'speed' => $attributes['speed'] ?? null,
|
|
153
|
+
'altitude' => $attributes['altitude'] ?? null,
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public function getCurrentVehicleAttribute(): ?Vehicle
|
|
158
|
+
{
|
|
159
|
+
return $this->currentConnection?->vehicle;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public function getAttachmentStateAttribute(): string
|
|
163
|
+
{
|
|
164
|
+
return $this->currentConnection ? 'attached' : 'detached';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public function getConnectivityStatusAttribute(): string
|
|
168
|
+
{
|
|
169
|
+
if (!$this->last_online_at) {
|
|
170
|
+
return 'never_connected';
|
|
171
|
+
}
|
|
172
|
+
if ($this->last_online_at->gte(now()->subMinutes(10))) {
|
|
173
|
+
return 'online';
|
|
174
|
+
}
|
|
175
|
+
if ($this->last_online_at->gte(now()->subDay())) {
|
|
176
|
+
return 'recently_offline';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return 'offline';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -375,6 +375,19 @@ class Vehicle extends Model
|
|
|
375
375
|
return $this->hasMany(Equipment::class, 'equipable_uuid', 'uuid')->where('equipable_type', static::class);
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
public function trailerConnections(): HasMany
|
|
379
|
+
{
|
|
380
|
+
return $this->hasMany(AssetConnection::class, 'connector_uuid', 'uuid')->where('connector_type', static::class)->latest('connected_at');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
public function currentTrailers(): HasManyThrough
|
|
384
|
+
{
|
|
385
|
+
return $this->hasManyThrough(Trailer::class, AssetConnection::class, 'connector_uuid', 'uuid', 'uuid', 'connected_uuid')
|
|
386
|
+
->where('asset_connections.connector_type', static::class)
|
|
387
|
+
->where('asset_connections.connected_type', Trailer::class)
|
|
388
|
+
->whereNull('asset_connections.disconnected_at');
|
|
389
|
+
}
|
|
390
|
+
|
|
378
391
|
public function maintenances(): HasMany
|
|
379
392
|
{
|
|
380
393
|
return $this->hasMany(Maintenance::class, 'maintainable_uuid', 'uuid')->where('maintainable_type', static::class);
|
|
@@ -214,8 +214,10 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
214
214
|
public function registerMorphMap(): void
|
|
215
215
|
{
|
|
216
216
|
Relation::morphMap([
|
|
217
|
-
'Fleetbase\\Models\\Vehicle'
|
|
218
|
-
'\\Fleetbase\\Models\\Vehicle'
|
|
217
|
+
'Fleetbase\\Models\\Vehicle' => \Fleetbase\FleetOps\Models\Vehicle::class,
|
|
218
|
+
'\\Fleetbase\\Models\\Vehicle' => \Fleetbase\FleetOps\Models\Vehicle::class,
|
|
219
|
+
'fleet-ops:vehicle' => \Fleetbase\FleetOps\Models\Vehicle::class,
|
|
220
|
+
'fleet-ops:trailer' => \Fleetbase\FleetOps\Models\Trailer::class,
|
|
219
221
|
]);
|
|
220
222
|
}
|
|
221
223
|
|
|
@@ -9,6 +9,7 @@ use Fleetbase\FleetOps\Models\Driver;
|
|
|
9
9
|
use Fleetbase\FleetOps\Models\Fleet;
|
|
10
10
|
use Fleetbase\FleetOps\Models\Order;
|
|
11
11
|
use Fleetbase\FleetOps\Models\ServiceArea;
|
|
12
|
+
use Fleetbase\FleetOps\Models\Trailer;
|
|
12
13
|
use Fleetbase\FleetOps\Models\Vehicle;
|
|
13
14
|
use Fleetbase\FleetOps\Models\Zone;
|
|
14
15
|
|
|
@@ -82,6 +83,27 @@ class FleetOpsAiQueryResources
|
|
|
82
83
|
maxLimit: 500,
|
|
83
84
|
));
|
|
84
85
|
|
|
86
|
+
$registry->register(new AiQueryableResource(
|
|
87
|
+
key: 'fleet-ops.trailers',
|
|
88
|
+
label: 'Fleet-Ops trailers',
|
|
89
|
+
module: 'fleet-ops',
|
|
90
|
+
modelClass: Trailer::class,
|
|
91
|
+
permission: 'fleet-ops see trailer',
|
|
92
|
+
aliases: ['trailers', 'trailer'],
|
|
93
|
+
fields: [
|
|
94
|
+
'online' => ['column' => 'online', 'type' => 'boolean'],
|
|
95
|
+
'status' => ['column' => 'status', 'type' => 'string'],
|
|
96
|
+
'type' => ['column' => 'type', 'type' => 'string'],
|
|
97
|
+
'plate_number' => ['column' => 'plate_number', 'type' => 'string'],
|
|
98
|
+
'last_online_at' => ['column' => 'last_online_at', 'type' => 'datetime'],
|
|
99
|
+
'updated_at' => ['column' => 'updated_at', 'type' => 'datetime'],
|
|
100
|
+
],
|
|
101
|
+
sampleFields: ['public_id', 'name', 'type', 'status', 'online', 'plate_number', 'last_online_at'],
|
|
102
|
+
locationField: 'location',
|
|
103
|
+
directivePermission: 'fleet-ops list trailer',
|
|
104
|
+
maxLimit: 500,
|
|
105
|
+
));
|
|
106
|
+
|
|
85
107
|
$registry->register(new AiQueryableResource(
|
|
86
108
|
key: 'fleet-ops.orders',
|
|
87
109
|
label: 'Fleet-Ops orders',
|
|
@@ -24,6 +24,9 @@ class FleetOpsReportSchema implements ReportSchema
|
|
|
24
24
|
// Register Vehicles table
|
|
25
25
|
$registry->registerTable($this->createVehiclesTable());
|
|
26
26
|
|
|
27
|
+
// Assets includes first-class trailers; filter asset_class = trailer for trailer reports.
|
|
28
|
+
$registry->registerTable($this->createAssetsTable());
|
|
29
|
+
|
|
27
30
|
// Register Places table
|
|
28
31
|
$registry->registerTable($this->createPlacesTable());
|
|
29
32
|
|
|
@@ -592,6 +595,41 @@ class FleetOpsReportSchema implements ReportSchema
|
|
|
592
595
|
]);
|
|
593
596
|
}
|
|
594
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Create the independently managed assets definition, including Trailers.
|
|
600
|
+
*/
|
|
601
|
+
protected function createAssetsTable(): Table
|
|
602
|
+
{
|
|
603
|
+
return Table::make('assets')
|
|
604
|
+
->label('Trailers and Assets')
|
|
605
|
+
->description('Independently managed fleet assets; Trailer rows use asset_class trailer')
|
|
606
|
+
->category('Fleet')
|
|
607
|
+
->extension('fleet-ops')
|
|
608
|
+
->excludeColumns(['uuid', 'company_uuid', 'deleted_at', 'meta', 'attributes'])
|
|
609
|
+
->maxRows(10000)
|
|
610
|
+
->columns([
|
|
611
|
+
Column::make('public_id', 'string')->label('Asset ID')->searchable()->filterable()->sortable(),
|
|
612
|
+
Column::make('asset_class', 'string')->label('Asset Class')->filterable()->aggregatable(),
|
|
613
|
+
Column::make('name', 'string')->label('Name')->searchable()->filterable()->sortable(),
|
|
614
|
+
Column::make('code', 'string')->label('Code')->searchable()->filterable()->sortable(),
|
|
615
|
+
Column::make('type', 'string')->label('Trailer Type')->filterable()->aggregatable(),
|
|
616
|
+
Column::make('status', 'string')->label('Lifecycle Status')->filterable()->aggregatable(),
|
|
617
|
+
Column::make('plate_number', 'string')->label('Plate Number')->searchable()->filterable(),
|
|
618
|
+
Column::make('make', 'string')->label('Make')->filterable()->aggregatable(),
|
|
619
|
+
Column::make('model', 'string')->label('Model')->filterable()->aggregatable(),
|
|
620
|
+
Column::make('year', 'integer')->label('Year')->filterable()->sortable(),
|
|
621
|
+
Column::make('gvwr', 'decimal')->label('GVWR')->filterable()->sortable()->aggregatable(),
|
|
622
|
+
Column::make('payload_capacity', 'decimal')->label('Payload Capacity')->filterable()->sortable()->aggregatable(),
|
|
623
|
+
Column::make('axle_count', 'integer')->label('Axle Count')->filterable()->sortable(),
|
|
624
|
+
Column::make('online', 'boolean')->label('Online')->filterable()->aggregatable(),
|
|
625
|
+
Column::make('last_online_at', 'datetime')->label('Last Online')->filterable()->sortable(),
|
|
626
|
+
Column::make('created_at', 'datetime')->label('Created')->filterable()->sortable(),
|
|
627
|
+
])
|
|
628
|
+
->computedColumns([
|
|
629
|
+
Column::count('total_assets', 'id')->label('Total Assets')->description('Count of selected trailer or asset rows'),
|
|
630
|
+
]);
|
|
631
|
+
}
|
|
632
|
+
|
|
595
633
|
/**
|
|
596
634
|
* Create the Places table definition.
|
|
597
635
|
*/
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Support\Telematics;
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Contracts\TelematicProviderInterface;
|
|
6
|
+
use Fleetbase\FleetOps\Events\TrailerLocationChanged;
|
|
6
7
|
use Fleetbase\FleetOps\Events\VehicleLocationChanged;
|
|
7
8
|
use Fleetbase\FleetOps\Jobs\SyncTelematicDevicesJob;
|
|
8
9
|
use Fleetbase\FleetOps\Jobs\TestTelematicConnectionJob;
|
|
@@ -10,6 +11,7 @@ use Fleetbase\FleetOps\Models\Device;
|
|
|
10
11
|
use Fleetbase\FleetOps\Models\DeviceEvent;
|
|
11
12
|
use Fleetbase\FleetOps\Models\Sensor;
|
|
12
13
|
use Fleetbase\FleetOps\Models\Telematic;
|
|
14
|
+
use Fleetbase\FleetOps\Models\Trailer;
|
|
13
15
|
use Fleetbase\FleetOps\Models\Vehicle;
|
|
14
16
|
use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
|
|
15
17
|
use Illuminate\Support\Carbon;
|
|
@@ -654,9 +656,42 @@ class TelematicService
|
|
|
654
656
|
$attachable = $device?->attachable;
|
|
655
657
|
if ($attachable instanceof Vehicle) {
|
|
656
658
|
$this->updateVehicleTelemetry($attachable, $location, $eventData, $event);
|
|
659
|
+
} elseif ($attachable instanceof Trailer) {
|
|
660
|
+
$this->updateTrailerTelemetry($attachable, $location, $eventData, $event);
|
|
657
661
|
}
|
|
658
662
|
}
|
|
659
663
|
|
|
664
|
+
protected function updateTrailerTelemetry(Trailer $trailer, array $location, array $eventData, DeviceEvent $event): void
|
|
665
|
+
{
|
|
666
|
+
$observedAt = $event->occurred_at ?? $event->created_at ?? now();
|
|
667
|
+
$currentAt = data_get($trailer->telematics, 'last_event_at');
|
|
668
|
+
if ($currentAt && $observedAt->lt(Carbon::parse($currentAt))) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
$trailer->location = new SpatialPoint($location['latitude'], $location['longitude']);
|
|
673
|
+
$trailer->online = array_key_exists('online', $eventData) && $eventData['online'] !== null ? (bool) $eventData['online'] : true;
|
|
674
|
+
$trailer->last_online_at = $observedAt;
|
|
675
|
+
foreach (['speed', 'heading', 'altitude', 'odometer', 'reefer_engine_hours'] as $field) {
|
|
676
|
+
if (array_key_exists($field, $eventData) && $eventData[$field] !== null) {
|
|
677
|
+
$trailer->{$field} = $eventData[$field];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
$trailer->telematics = array_merge($trailer->telematics ?? [], [
|
|
682
|
+
'last_event_uuid' => $event->uuid,
|
|
683
|
+
'last_event_id' => $event->public_id,
|
|
684
|
+
'last_event_type' => $event->event_type,
|
|
685
|
+
'last_event_at' => $observedAt->toISOString(),
|
|
686
|
+
'last_device_uuid' => $event->device_uuid,
|
|
687
|
+
'last_provider' => $event->provider,
|
|
688
|
+
'last_telemetry_data' => array_filter($eventData, fn ($value) => $value !== null),
|
|
689
|
+
]);
|
|
690
|
+
$trailer->save();
|
|
691
|
+
|
|
692
|
+
broadcast(new TrailerLocationChanged($trailer, ['source' => 'telematics', 'device_event_uuid' => $event->uuid, 'provider' => $event->provider]));
|
|
693
|
+
}
|
|
694
|
+
|
|
660
695
|
protected function updateVehicleTelemetry(Vehicle $vehicle, array $location, array $eventData, DeviceEvent $event): void
|
|
661
696
|
{
|
|
662
697
|
$vehicle->location = new SpatialPoint($location['latitude'], $location['longitude']);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compact towing summary of a trailer for parent payloads (vehicle listings and map
|
|
7
|
+
* popovers) that only need identity, classification and connectivity.
|
|
8
|
+
*/
|
|
9
|
+
class TrailerSummary
|
|
10
|
+
{
|
|
11
|
+
public static function compact($trailer): array
|
|
12
|
+
{
|
|
13
|
+
return [
|
|
14
|
+
'id' => $trailer->uuid,
|
|
15
|
+
'uuid' => $trailer->uuid,
|
|
16
|
+
'public_id' => $trailer->public_id,
|
|
17
|
+
'name' => $trailer->name,
|
|
18
|
+
'display_name' => $trailer->display_name,
|
|
19
|
+
'type' => $trailer->type,
|
|
20
|
+
'plate_number' => $trailer->plate_number,
|
|
21
|
+
'status' => $trailer->status,
|
|
22
|
+
'attachment_state' => 'attached',
|
|
23
|
+
'connectivity_status' => $trailer->connectivity_status,
|
|
24
|
+
'online' => (bool) $trailer->online,
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compact every trailer in a loaded collection.
|
|
30
|
+
*/
|
|
31
|
+
public static function collection(iterable $trailers): array
|
|
32
|
+
{
|
|
33
|
+
return collect($trailers)->map(fn ($trailer) => static::compact($trailer))->values()->all();
|
|
34
|
+
}
|
|
35
|
+
}
|
package/server/src/routes.php
CHANGED
|
@@ -133,6 +133,8 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
133
133
|
$router->get('{id}', 'EquipmentController@find');
|
|
134
134
|
$router->put('{id}', 'EquipmentController@update');
|
|
135
135
|
$router->delete('{id}', 'EquipmentController@delete');
|
|
136
|
+
$router->post('{id}/attach', 'EquipmentController@attach');
|
|
137
|
+
$router->post('{id}/detach', 'EquipmentController@detach');
|
|
136
138
|
});
|
|
137
139
|
// parts routes
|
|
138
140
|
$router->group(['prefix' => 'parts'], function () use ($router) {
|
|
@@ -297,6 +299,19 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
297
299
|
$router->put('{id}', 'VehicleController@update');
|
|
298
300
|
$router->delete('{id}', 'VehicleController@delete');
|
|
299
301
|
$router->match(['put', 'patch', 'post'], '{id}/track', 'VehicleController@track');
|
|
302
|
+
$router->get('{id}/trailers', 'TrailerController@vehicleTrailers');
|
|
303
|
+
});
|
|
304
|
+
// trailer routes
|
|
305
|
+
$router->group(['prefix' => 'trailers'], function () use ($router) {
|
|
306
|
+
$router->post('/', 'TrailerController@create');
|
|
307
|
+
$router->get('/', 'TrailerController@query');
|
|
308
|
+
$router->get('{id}', 'TrailerController@find');
|
|
309
|
+
$router->put('{id}', 'TrailerController@update');
|
|
310
|
+
$router->delete('{id}', 'TrailerController@delete');
|
|
311
|
+
$router->match(['put', 'patch', 'post'], '{id}/track', 'TrailerController@track');
|
|
312
|
+
$router->post('{id}/attach', 'TrailerController@attach');
|
|
313
|
+
$router->post('{id}/detach', 'TrailerController@detach');
|
|
314
|
+
$router->get('{id}/connections', 'TrailerController@connections');
|
|
300
315
|
});
|
|
301
316
|
// fleets routes
|
|
302
317
|
$router->group(['prefix' => 'fleets'], function () use ($router) {
|
|
@@ -563,10 +578,24 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
563
578
|
$router->post('{id}/unassign-orders', $controller('unassignOrders'));
|
|
564
579
|
$router->post('{id}/attach-device', $controller('attachDevice'));
|
|
565
580
|
$router->post('{id}/detach-device', $controller('detachDevice'));
|
|
581
|
+
$router->post('{id}/attach-equipment', $controller('attachEquipment'));
|
|
582
|
+
$router->post('{id}/detach-equipment', $controller('detachEquipment'));
|
|
566
583
|
$router->match(['get', 'post'], 'export', $controller('export'));
|
|
567
584
|
$router->post('import', $controller('import'));
|
|
568
585
|
}
|
|
569
586
|
);
|
|
587
|
+
$router->fleetbaseRoutes('trailers', function ($router, $controller) {
|
|
588
|
+
$router->get('statuses', $controller('statuses'));
|
|
589
|
+
$router->get('types', $controller('types'));
|
|
590
|
+
$router->post('{id}/attach', $controller('attach'));
|
|
591
|
+
$router->post('{id}/detach', $controller('detach'));
|
|
592
|
+
$router->post('{id}/attach-device', $controller('attachDevice'));
|
|
593
|
+
$router->post('{id}/detach-device', $controller('detachDevice'));
|
|
594
|
+
$router->post('{id}/attach-equipment', $controller('attachEquipment'));
|
|
595
|
+
$router->post('{id}/detach-equipment', $controller('detachEquipment'));
|
|
596
|
+
$router->match(['get', 'post'], 'export', $controller('export'));
|
|
597
|
+
$router->post('import', $controller('import'));
|
|
598
|
+
});
|
|
570
599
|
$router->fleetbaseRoutes('vehicle-devices');
|
|
571
600
|
$router->fleetbaseRoutes(
|
|
572
601
|
'vendors',
|
|
@@ -526,6 +526,20 @@ test('index vehicle resource returns compact fleet state metadata', function ()
|
|
|
526
526
|
'altitude' => 20,
|
|
527
527
|
'speed' => 54,
|
|
528
528
|
'online' => 1,
|
|
529
|
+
], [
|
|
530
|
+
'currentTrailers' => new Collection([
|
|
531
|
+
(object) [
|
|
532
|
+
'uuid' => 'trailer-uuid',
|
|
533
|
+
'public_id' => 'trailer_public',
|
|
534
|
+
'name' => 'Dry Van 7',
|
|
535
|
+
'display_name' => 'Dry Van 7',
|
|
536
|
+
'type' => 'dry_van',
|
|
537
|
+
'plate_number' => 'TRL-7',
|
|
538
|
+
'status' => 'in_use',
|
|
539
|
+
'connectivity_status' => 'never_connected',
|
|
540
|
+
'online' => 0,
|
|
541
|
+
],
|
|
542
|
+
]),
|
|
529
543
|
]);
|
|
530
544
|
|
|
531
545
|
$payload = (new TestFleetOpsIndexVehicleResource($vehicle))->resolve($request);
|
|
@@ -546,6 +560,21 @@ test('index vehicle resource returns compact fleet state metadata', function ()
|
|
|
546
560
|
'altitude' => 20,
|
|
547
561
|
'speed' => 54,
|
|
548
562
|
'online' => true,
|
|
563
|
+
'trailers' => [
|
|
564
|
+
[
|
|
565
|
+
'id' => 'trailer-uuid',
|
|
566
|
+
'uuid' => 'trailer-uuid',
|
|
567
|
+
'public_id' => 'trailer_public',
|
|
568
|
+
'name' => 'Dry Van 7',
|
|
569
|
+
'display_name' => 'Dry Van 7',
|
|
570
|
+
'type' => 'dry_van',
|
|
571
|
+
'plate_number' => 'TRL-7',
|
|
572
|
+
'status' => 'in_use',
|
|
573
|
+
'attachment_state' => 'attached',
|
|
574
|
+
'connectivity_status' => 'never_connected',
|
|
575
|
+
'online' => false,
|
|
576
|
+
],
|
|
577
|
+
],
|
|
549
578
|
'assigned_orders_count' => 6,
|
|
550
579
|
'meta' => [
|
|
551
580
|
'_index_resource' => true,
|
|
@@ -2563,6 +2592,10 @@ test('internal vehicle vendor and waypoint resources expose internal compact fie
|
|
|
2563
2592
|
'status_code' => 'arrived',
|
|
2564
2593
|
]);
|
|
2565
2594
|
|
|
2595
|
+
$vehicle->setRelation('currentTrailers', new Collection([
|
|
2596
|
+
(object) ['uuid' => 'trailer-uuid', 'public_id' => 'trailer_public', 'name' => 'Reefer 12', 'display_name' => 'Reefer 12', 'type' => 'reefer', 'plate_number' => 'TRL-12', 'status' => 'in_use', 'connectivity_status' => 'online', 'online' => 1],
|
|
2597
|
+
]));
|
|
2598
|
+
|
|
2566
2599
|
expect((new InternalVehicleResource($vehicle))->resolve($request))->toMatchArray([
|
|
2567
2600
|
'id' => 101,
|
|
2568
2601
|
'uuid' => 'fixture-uuid',
|
|
@@ -2591,6 +2624,9 @@ test('internal vehicle vendor and waypoint resources expose internal compact fie
|
|
|
2591
2624
|
'altitude' => 4,
|
|
2592
2625
|
'speed' => 18,
|
|
2593
2626
|
'meta' => ['yard' => 'east'],
|
|
2627
|
+
'trailers' => [
|
|
2628
|
+
['id' => 'trailer-uuid', 'uuid' => 'trailer-uuid', 'public_id' => 'trailer_public', 'name' => 'Reefer 12', 'display_name' => 'Reefer 12', 'type' => 'reefer', 'plate_number' => 'TRL-12', 'status' => 'in_use', 'attachment_state' => 'attached', 'connectivity_status' => 'online', 'online' => true],
|
|
2629
|
+
],
|
|
2594
2630
|
])
|
|
2595
2631
|
->and((new VehicleWithoutDriverResource($vehicle))->toWebhookPayload())->toMatchArray([
|
|
2596
2632
|
'id' => 'fixture_public',
|
|
@@ -559,6 +559,8 @@ test('vehicle filter records identity relationship fleet and telematic filters',
|
|
|
559
559
|
$filter->vehicleYear('2026');
|
|
560
560
|
$filter->driver('unassigned');
|
|
561
561
|
$filter->driver($publicIds['drivers']);
|
|
562
|
+
// The console's multi-select sends several drivers at once.
|
|
563
|
+
$filter->driver([$publicIds['drivers'], 'driver_missing']);
|
|
562
564
|
$filter->vendor(null);
|
|
563
565
|
$filter->vendor($publicIds['vendors']);
|
|
564
566
|
$filter->driverUuid('driver-uuid');
|
|
@@ -568,6 +570,33 @@ test('vehicle filter records identity relationship fleet and telematic filters',
|
|
|
568
570
|
$filter->createdAt(['2026-01-01', '2026-01-31']);
|
|
569
571
|
$filter->updatedAt('2026-02-01');
|
|
570
572
|
|
|
573
|
+
// Batch scans: a comma-separated list of internal ids is an exact match on each.
|
|
574
|
+
$filter->internalId('VEH-1, VEH-2');
|
|
575
|
+
|
|
576
|
+
// Towing, device and presence filters back the console's vehicle index filters.
|
|
577
|
+
$schema = Illuminate\Database\Eloquent\Model::resolveConnection()->getSchemaBuilder();
|
|
578
|
+
foreach (['assets' => 'trailer_filterone', 'devices' => 'device_filterone'] as $table => $publicId) {
|
|
579
|
+
$schema->create($table, function ($blueprint) {
|
|
580
|
+
$blueprint->increments('id');
|
|
581
|
+
foreach (['uuid', 'public_id', 'internal_id', 'company_uuid', 'asset_class', 'name'] as $column) {
|
|
582
|
+
$blueprint->string($column)->nullable();
|
|
583
|
+
}
|
|
584
|
+
$blueprint->timestamps();
|
|
585
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
586
|
+
});
|
|
587
|
+
Illuminate\Database\Eloquent\Model::resolveConnection()->table($table)->insert(['uuid' => $table . '-filter-uuid', 'public_id' => $publicId, 'company_uuid' => 'company-uuid', 'asset_class' => 'trailer', 'name' => $table]);
|
|
588
|
+
}
|
|
589
|
+
$filter->trailer('trailer_filterone');
|
|
590
|
+
$filter->device(['device_filterone']);
|
|
591
|
+
$filter->hasTrailer('true');
|
|
592
|
+
$filter->hasDriver('false');
|
|
593
|
+
$filter->hasDevice('');
|
|
594
|
+
|
|
595
|
+
expect($query->calls)->toContain(['whereIn', 'internal_id', ['VEH-1', 'VEH-2']])
|
|
596
|
+
->and($query->calls)->toContain(['whereDoesntHave', 'driver'])
|
|
597
|
+
->and(collect($query->calls)->where(0, 'whereHas')->pluck(1)->all())->toContain('currentTrailers')
|
|
598
|
+
->and(collect($query->calls)->where(0, 'whereHas')->flatMap(fn ($call) => $call[2])->values()->all())->toContain(['whereIn', 'assets.uuid', ['assets-filter-uuid']], ['whereIn', 'uuid', ['devices-filter-uuid']]);
|
|
599
|
+
|
|
571
600
|
expect($query->calls)->toContain(['where', ['company_uuid', 'company-uuid']])
|
|
572
601
|
->and($query->calls)->toContain(['search', 'van'])
|
|
573
602
|
->and($query->calls)->toContain(['searchWhere', ['year', 'make', 'model', 'plate_number'], 'sprinter'])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
|
+
use Fleetbase\FleetOps\Exceptions\DeviceAlreadyAttachedException;
|
|
3
4
|
use Fleetbase\FleetOps\Http\Controllers\Api\v1\DeviceController;
|
|
4
5
|
use Fleetbase\FleetOps\Http\Requests\CreateDeviceRequest;
|
|
5
6
|
use Fleetbase\FleetOps\Http\Requests\UpdateDeviceRequest;
|
|
@@ -82,22 +83,23 @@ class FleetOpsApiDeviceControllerEndpointProbe extends DeviceController
|
|
|
82
83
|
$this->lookupLogs[] = [$action, $deviceId, $vehicleId];
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
protected function logDeviceAttachmentFailure(string $action, Device $device, ?
|
|
86
|
+
protected function logDeviceAttachmentFailure(string $action, Device $device, ?EloquentModel $attachable, Throwable $exception): void
|
|
86
87
|
{
|
|
87
|
-
$this->failureLogs[] = [$action, $device->uuid, $
|
|
88
|
+
$this->failureLogs[] = [$action, $device->uuid, $attachable?->uuid, $exception->getMessage()];
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
class FleetOpsApiDeviceEndpointFake extends Device
|
|
92
93
|
{
|
|
93
|
-
public array $loads
|
|
94
|
-
public array $loadCounts
|
|
95
|
-
public array $updates
|
|
96
|
-
public array $attachments
|
|
97
|
-
public array $detaches
|
|
98
|
-
public bool $deleted
|
|
99
|
-
public bool $throwAttach
|
|
100
|
-
public bool $throwDetach
|
|
94
|
+
public array $loads = [];
|
|
95
|
+
public array $loadCounts = [];
|
|
96
|
+
public array $updates = [];
|
|
97
|
+
public array $attachments = [];
|
|
98
|
+
public array $detaches = [];
|
|
99
|
+
public bool $deleted = false;
|
|
100
|
+
public bool $throwAttach = false;
|
|
101
|
+
public bool $throwDetach = false;
|
|
102
|
+
public bool $throwAlreadyAttached = false;
|
|
101
103
|
|
|
102
104
|
public function load($relations)
|
|
103
105
|
{
|
|
@@ -139,6 +141,10 @@ class FleetOpsApiDeviceEndpointFake extends Device
|
|
|
139
141
|
throw new RuntimeException('attach exploded');
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
if ($this->throwAlreadyAttached) {
|
|
145
|
+
throw DeviceAlreadyAttachedException::for(null);
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
$this->attachments[] = [$attachable::class, $attachable->uuid];
|
|
143
149
|
|
|
144
150
|
return true;
|
|
@@ -316,6 +322,15 @@ test('api device controller attaches detaches and reports failures', function ()
|
|
|
316
322
|
];
|
|
317
323
|
$attachFailure = $attachFailureController->attach(new Request(['attachable' => 'vehicle_public']), 'attach_failure')->getData(true);
|
|
318
324
|
|
|
325
|
+
$attachConflictDevice = fleetopsApiDeviceEndpointDevice('attach-conflict');
|
|
326
|
+
$attachConflictDevice->throwAlreadyAttached = true;
|
|
327
|
+
$attachConflictController = fleetopsApiDeviceControllerEndpoint();
|
|
328
|
+
$attachConflictController->models = [
|
|
329
|
+
Device::class . ':attach_conflict' => $attachConflictDevice,
|
|
330
|
+
Vehicle::class . ':vehicle_public' => $vehicle,
|
|
331
|
+
];
|
|
332
|
+
$attachConflict = $attachConflictController->attach(new Request(['attachable' => 'vehicle_public']), 'attach_conflict');
|
|
333
|
+
|
|
319
334
|
$detachFailureDevice = fleetopsApiDeviceEndpointDevice('detach-failure');
|
|
320
335
|
$detachFailureDevice->throwDetach = true;
|
|
321
336
|
$detachFailureController = fleetopsApiDeviceControllerEndpoint();
|
|
@@ -339,9 +354,11 @@ test('api device controller attaches detaches and reports failures', function ()
|
|
|
339
354
|
])
|
|
340
355
|
->and($device->detaches)->toBe(['device-uuid'])
|
|
341
356
|
->and($missing)->toBe(['error' => 'Device resource not found.'])
|
|
342
|
-
->and($attachFailure)->toBe(['error' => 'Unable to attach device to
|
|
357
|
+
->and($attachFailure)->toBe(['error' => 'Unable to attach device to resource.'])
|
|
358
|
+
->and($attachConflict->getData(true))->toBe(['error' => 'Device is already attached to another asset. Detach it before attaching it elsewhere.'])
|
|
359
|
+
->and($attachConflict->getStatusCode())->toBe(409)
|
|
343
360
|
->and($attachFailureController->failureLogs)->toBe([['attach', 'attach-failure', 'vehicle-uuid', 'attach exploded']])
|
|
344
|
-
->and($detachFailure)->toBe(['error' => 'Unable to detach device from
|
|
361
|
+
->and($detachFailure)->toBe(['error' => 'Unable to detach device from resource.'])
|
|
345
362
|
->and($detachFailureController->failureLogs)->toBe([['detach', 'detach-failure', null, 'detach exploded']])
|
|
346
|
-
->and($lookupFailure)->toBe(['error' => 'Device or
|
|
363
|
+
->and($lookupFailure)->toBe(['error' => 'Device or attachable resource not found.']);
|
|
347
364
|
});
|