@fleetbase/fleetops-engine 0.6.63 → 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.
Files changed (289) hide show
  1. package/RELEASE.md +7 -6
  2. package/addon/components/attachment-list.hbs +65 -0
  3. package/addon/components/attachment-list.js +8 -0
  4. package/addon/components/cell/attached-vehicle.hbs +1 -1
  5. package/addon/components/cell/attached-vehicle.js +3 -3
  6. package/addon/components/cell/driver-identity.js +4 -4
  7. package/addon/components/cell/driver-name.hbs +1 -1
  8. package/addon/components/cell/translated-value.hbs +9 -0
  9. package/addon/components/cell/translated-value.js +57 -0
  10. package/addon/components/contact/card.hbs +58 -0
  11. package/addon/components/contact/card.js +19 -0
  12. package/addon/components/contact/form.hbs +1 -1
  13. package/addon/components/contact/panel-header.hbs +1 -1
  14. package/addon/components/customer/form.hbs +2 -2
  15. package/addon/components/device/manager.hbs +26 -28
  16. package/addon/components/device/manager.js +28 -2
  17. package/addon/components/device/panel-tabs/vehicle.hbs +13 -13
  18. package/addon/components/device/panel-tabs/vehicle.js +8 -0
  19. package/addon/components/driver/card.hbs +24 -13
  20. package/addon/components/driver/card.js +20 -6
  21. package/addon/components/driver/form.hbs +3 -3
  22. package/addon/components/driver/panel-header.hbs +2 -2
  23. package/addon/components/driver/pill.hbs +2 -2
  24. package/addon/components/equipment/form.js +11 -4
  25. package/addon/components/equipment/pill.hbs +20 -0
  26. package/addon/components/equipment/pill.js +3 -0
  27. package/addon/components/filter/multi-model.hbs +19 -0
  28. package/addon/components/filter/multi-model.js +90 -0
  29. package/addon/components/fleet/driver-listing.hbs +1 -1
  30. package/addon/components/fleet/vehicle-listing.hbs +1 -1
  31. package/addon/components/fleet-panel/driver-listing.hbs +1 -1
  32. package/addon/components/fleet-panel/vehicle-listing.hbs +1 -1
  33. package/addon/components/layout/fleet-ops-sidebar.js +9 -7
  34. package/addon/components/maintenance/form.js +12 -4
  35. package/addon/components/maintenance-schedule/form.js +13 -7
  36. package/addon/components/map/leaflet-live-map.hbs +52 -0
  37. package/addon/components/map/order-list-overlay/order.hbs +4 -4
  38. package/addon/components/modals/attach-equipment.hbs +22 -0
  39. package/addon/components/modals/attach-equipment.js +3 -0
  40. package/addon/components/modals/attach-telematic-device.hbs +13 -8
  41. package/addon/components/modals/attach-telematic-device.js +11 -1
  42. package/addon/components/modals/attach-trailer.hbs +30 -0
  43. package/addon/components/modals/attach-trailer.js +3 -0
  44. package/addon/components/modals/contact-form.hbs +2 -2
  45. package/addon/components/modals/convert-customer-to-vendor.hbs +1 -1
  46. package/addon/components/modals/driver-form.hbs +2 -2
  47. package/addon/components/modals/select-trailer.hbs +35 -0
  48. package/addon/components/modals/select-trailer.js +3 -0
  49. package/addon/components/modals/vehicle-form.hbs +2 -2
  50. package/addon/components/orchestrator/resource-panel.hbs +2 -2
  51. package/addon/components/order/customer-avatar-stack.hbs +2 -2
  52. package/addon/components/order/details/detail.hbs +4 -4
  53. package/addon/components/order/details/payload.hbs +2 -2
  54. package/addon/components/order/kanban-card.hbs +4 -4
  55. package/addon/components/order-list-overlay/order.hbs +4 -4
  56. package/addon/components/trailer/card.hbs +68 -0
  57. package/addon/components/trailer/card.js +22 -0
  58. package/addon/components/trailer/details/connections.hbs +57 -0
  59. package/addon/components/trailer/details/connections.js +59 -0
  60. package/addon/components/trailer/details/equipment.hbs +25 -0
  61. package/addon/components/trailer/details/equipment.js +52 -0
  62. package/addon/components/trailer/details/maintenance-history.hbs +47 -0
  63. package/addon/components/trailer/details/maintenance-history.js +40 -0
  64. package/addon/components/trailer/details/schedules.hbs +45 -0
  65. package/addon/components/trailer/details/schedules.js +40 -0
  66. package/addon/components/trailer/details/work-orders.hbs +47 -0
  67. package/addon/components/trailer/details/work-orders.js +40 -0
  68. package/addon/components/trailer/details.hbs +399 -0
  69. package/addon/components/trailer/details.js +118 -0
  70. package/addon/components/trailer/form.hbs +447 -0
  71. package/addon/components/trailer/form.js +165 -0
  72. package/addon/components/trailer/panel-header.hbs +45 -0
  73. package/addon/components/trailer/panel-header.js +3 -0
  74. package/addon/components/trailer/pill.hbs +21 -0
  75. package/addon/components/trailer/pill.js +3 -0
  76. package/addon/components/vehicle/card.hbs +42 -11
  77. package/addon/components/vehicle/card.js +31 -0
  78. package/addon/components/vehicle/details/equipment.hbs +25 -0
  79. package/addon/components/vehicle/details/equipment.js +55 -0
  80. package/addon/components/vehicle/details/trailers.hbs +35 -0
  81. package/addon/components/vehicle/details/trailers.js +55 -0
  82. package/addon/components/vehicle/form.hbs +2 -2
  83. package/addon/components/vehicle/panel-header.hbs +2 -2
  84. package/addon/components/vehicle/pill.hbs +2 -2
  85. package/addon/components/vendor/card.hbs +58 -0
  86. package/addon/components/vendor/card.js +6 -0
  87. package/addon/components/vendor/panel-header.hbs +2 -2
  88. package/addon/components/vendor/personnel-manager.hbs +2 -2
  89. package/addon/components/work-order/form.js +12 -4
  90. package/addon/controllers/connectivity/devices/index/details/vehicle.js +11 -0
  91. package/addon/controllers/connectivity/devices/index/details.js +3 -3
  92. package/addon/controllers/connectivity/devices/index.js +1 -0
  93. package/addon/controllers/connectivity/telematics/details/attachments.js +28 -47
  94. package/addon/controllers/connectivity/telematics/details/devices.js +1 -0
  95. package/addon/controllers/management/contacts/customers.js +4 -0
  96. package/addon/controllers/management/contacts/index.js +32 -0
  97. package/addon/controllers/management/trailers/index/details.js +119 -0
  98. package/addon/controllers/management/trailers/index/edit.js +67 -0
  99. package/addon/controllers/management/trailers/index/new.js +43 -0
  100. package/addon/controllers/management/trailers/index.js +599 -0
  101. package/addon/controllers/management/vehicles/index/details.js +16 -0
  102. package/addon/controllers/management/vehicles/index.js +78 -4
  103. package/addon/controllers/management/vendors/index.js +32 -0
  104. package/addon/controllers/operations/orders/index.js +3 -1
  105. package/addon/controllers/settings/custom-fields.js +8 -0
  106. package/addon/extension.js +7 -0
  107. package/addon/helpers/placeholder-image.js +6 -0
  108. package/addon/helpers/resource-image.js +10 -0
  109. package/addon/helpers/trailer-placeholder-image.js +6 -0
  110. package/addon/routes/management/trailers/index/details/connections.js +2 -0
  111. package/addon/routes/management/trailers/index/details/devices.js +2 -0
  112. package/addon/routes/management/trailers/index/details/equipment.js +2 -0
  113. package/addon/routes/management/trailers/index/details/index.js +2 -0
  114. package/addon/routes/management/trailers/index/details/maintenance-history.js +2 -0
  115. package/addon/routes/management/trailers/index/details/positions.js +2 -0
  116. package/addon/routes/management/trailers/index/details/schedules.js +2 -0
  117. package/addon/routes/management/trailers/index/details/work-orders.js +2 -0
  118. package/addon/routes/management/trailers/index/details.js +30 -0
  119. package/addon/routes/management/trailers/index/edit.js +23 -0
  120. package/addon/routes/management/trailers/index/new.js +14 -0
  121. package/addon/routes/management/trailers/index.js +42 -0
  122. package/addon/routes/management/vehicles/index/details/trailers.js +2 -0
  123. package/addon/routes/management/vehicles/index.js +7 -1
  124. package/addon/routes.js +17 -0
  125. package/addon/services/device-actions.js +26 -16
  126. package/addon/services/trailer-actions.js +394 -0
  127. package/addon/services/vehicle-actions.js +111 -0
  128. package/addon/styles/fleetops-engine.css +349 -0
  129. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +19 -19
  130. package/addon/templates/connectivity/telematics/details/attachments.hbs +15 -15
  131. package/addon/templates/management/contacts/customers.hbs +48 -29
  132. package/addon/templates/management/contacts/index.hbs +52 -33
  133. package/addon/templates/management/trailers/index/details/connections.hbs +1 -0
  134. package/addon/templates/management/trailers/index/details/devices.hbs +1 -0
  135. package/addon/templates/management/trailers/index/details/equipment.hbs +1 -0
  136. package/addon/templates/management/trailers/index/details/index.hbs +1 -0
  137. package/addon/templates/management/trailers/index/details/maintenance-history.hbs +1 -0
  138. package/addon/templates/management/trailers/index/details/positions.hbs +1 -0
  139. package/addon/templates/management/trailers/index/details/schedules.hbs +1 -0
  140. package/addon/templates/management/trailers/index/details/work-orders.hbs +1 -0
  141. package/addon/templates/management/trailers/index/details.hbs +15 -0
  142. package/addon/templates/management/trailers/index/edit.hbs +12 -0
  143. package/addon/templates/management/trailers/index/new.hbs +11 -0
  144. package/addon/templates/management/trailers/index.hbs +67 -0
  145. package/addon/templates/management/vehicles/index/details/equipment.hbs +1 -1
  146. package/addon/templates/management/vehicles/index/details/trailers.hbs +1 -0
  147. package/addon/templates/management/vehicles/index/details.hbs +1 -1
  148. package/addon/templates/management/vendors/index.hbs +66 -32
  149. package/addon/utils/device-table-columns.js +10 -6
  150. package/addon/utils/live-map-card-content.js +56 -0
  151. package/addon/utils/placeholder-images.js +71 -0
  152. package/addon/utils/trailer-placeholder-image.js +11 -0
  153. package/app/components/attachment-list.js +1 -0
  154. package/app/components/cell/translated-value.js +1 -0
  155. package/app/components/contact/card.js +1 -0
  156. package/app/components/equipment/pill.js +1 -0
  157. package/app/components/filter/multi-model.js +1 -0
  158. package/app/components/modals/attach-equipment.js +1 -0
  159. package/app/components/modals/attach-trailer.js +1 -0
  160. package/app/components/modals/select-trailer.js +1 -0
  161. package/app/components/trailer/card.js +1 -0
  162. package/app/components/trailer/details/connections.js +1 -0
  163. package/app/components/trailer/details/equipment.js +1 -0
  164. package/app/components/trailer/details/maintenance-history.js +1 -0
  165. package/app/components/trailer/details/schedules.js +1 -0
  166. package/app/components/trailer/details/work-orders.js +1 -0
  167. package/app/components/trailer/details.js +1 -0
  168. package/app/components/trailer/form.js +1 -0
  169. package/app/components/trailer/panel-header.js +1 -0
  170. package/app/components/trailer/pill.js +1 -0
  171. package/app/components/vehicle/details/equipment.js +1 -0
  172. package/app/components/vehicle/details/trailers.js +1 -0
  173. package/app/components/vendor/card.js +1 -0
  174. package/app/controllers/management/trailers/index/details.js +1 -0
  175. package/app/controllers/management/trailers/index/edit.js +1 -0
  176. package/app/controllers/management/trailers/index/new.js +1 -0
  177. package/app/controllers/management/trailers/index.js +1 -0
  178. package/app/helpers/placeholder-image.js +1 -0
  179. package/app/helpers/resource-image.js +1 -0
  180. package/app/helpers/trailer-placeholder-image.js +1 -0
  181. package/app/routes/management/trailers/index/details/connections.js +1 -0
  182. package/app/routes/management/trailers/index/details/devices.js +1 -0
  183. package/app/routes/management/trailers/index/details/equipment.js +1 -0
  184. package/app/routes/management/trailers/index/details/index.js +1 -0
  185. package/app/routes/management/trailers/index/details/maintenance-history.js +1 -0
  186. package/app/routes/management/trailers/index/details/positions.js +1 -0
  187. package/app/routes/management/trailers/index/details/schedules.js +1 -0
  188. package/app/routes/management/trailers/index/details/work-orders.js +1 -0
  189. package/app/routes/management/trailers/index/details.js +1 -0
  190. package/app/routes/management/trailers/index/edit.js +1 -0
  191. package/app/routes/management/trailers/index/new.js +1 -0
  192. package/app/routes/management/trailers/index.js +1 -0
  193. package/app/routes/management/vehicles/index/details/trailers.js +1 -0
  194. package/app/services/trailer-actions.js +1 -0
  195. package/app/templates/management/trailers/index/details/connections.js +1 -0
  196. package/app/templates/management/trailers/index/details/devices.js +1 -0
  197. package/app/templates/management/trailers/index/details/equipment.js +1 -0
  198. package/app/templates/management/trailers/index/details/index.js +1 -0
  199. package/app/templates/management/trailers/index/details/maintenance-history.js +1 -0
  200. package/app/templates/management/trailers/index/details/positions.js +1 -0
  201. package/app/templates/management/trailers/index/details/schedules.js +1 -0
  202. package/app/templates/management/trailers/index/details/work-orders.js +1 -0
  203. package/app/templates/management/trailers/index/details.js +1 -0
  204. package/app/templates/management/trailers/index/edit.js +1 -0
  205. package/app/templates/management/trailers/index/new.js +1 -0
  206. package/app/templates/management/trailers/index.js +1 -0
  207. package/app/templates/management/vehicles/index/details/trailers.js +1 -0
  208. package/composer.json +1 -1
  209. package/extension.json +1 -1
  210. package/package.json +1 -1
  211. package/server/migrations/2026_09_03_000001_add_first_class_trailer_columns_to_assets_table.php +58 -0
  212. package/server/migrations/2026_09_03_000002_create_asset_connections_table.php +43 -0
  213. package/server/migrations/2026_09_03_000003_create_device_installations_table.php +33 -0
  214. package/server/src/Auth/Schemas/FleetOps.php +7 -0
  215. package/server/src/Events/TrailerLocationChanged.php +70 -0
  216. package/server/src/Exceptions/DeviceAlreadyAttachedException.php +18 -0
  217. package/server/src/Exports/TrailerExport.php +31 -0
  218. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +8 -2
  219. package/server/src/Http/Controllers/Api/v1/DeviceController.php +22 -15
  220. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +47 -0
  221. package/server/src/Http/Controllers/Api/v1/TrailerController.php +362 -0
  222. package/server/src/Http/Controllers/Api/v1/VehicleController.php +1 -0
  223. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +3 -0
  224. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  225. package/server/src/Http/Controllers/Internal/v1/SearchController.php +16 -0
  226. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +360 -0
  227. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +74 -0
  228. package/server/src/Http/Filter/EquipmentFilter.php +51 -0
  229. package/server/src/Http/Filter/OrderFilter.php +15 -0
  230. package/server/src/Http/Filter/TrailerFilter.php +269 -0
  231. package/server/src/Http/Filter/VehicleFilter.php +70 -3
  232. package/server/src/Http/Requests/CreateDeviceRequest.php +1 -1
  233. package/server/src/Http/Requests/CreateEquipmentRequest.php +1 -1
  234. package/server/src/Http/Requests/CreateTrailerRequest.php +54 -0
  235. package/server/src/Http/Requests/UpdateTrailerRequest.php +7 -0
  236. package/server/src/Http/Resources/v1/AssetConnection.php +90 -0
  237. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +29 -3
  238. package/server/src/Http/Resources/v1/Device.php +3 -1
  239. package/server/src/Http/Resources/v1/Equipment.php +3 -1
  240. package/server/src/Http/Resources/v1/Fleet.php +3 -1
  241. package/server/src/Http/Resources/v1/Index/Vehicle.php +10 -0
  242. package/server/src/Http/Resources/v1/Trailer.php +199 -0
  243. package/server/src/Http/Resources/v1/Vehicle.php +2 -0
  244. package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +3 -0
  245. package/server/src/Imports/TrailerImport.php +33 -0
  246. package/server/src/Models/Asset.php +39 -10
  247. package/server/src/Models/AssetConnection.php +43 -0
  248. package/server/src/Models/Device.php +39 -8
  249. package/server/src/Models/DeviceInstallation.php +27 -0
  250. package/server/src/Models/Equipment.php +2 -0
  251. package/server/src/Models/Trailer.php +181 -0
  252. package/server/src/Models/Vehicle.php +13 -0
  253. package/server/src/Providers/FleetOpsServiceProvider.php +4 -2
  254. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +22 -0
  255. package/server/src/Support/Reporting/FleetOpsReportSchema.php +38 -0
  256. package/server/src/Support/Telematics/TelematicService.php +35 -0
  257. package/server/src/Support/TrailerSummary.php +35 -0
  258. package/server/src/routes.php +29 -0
  259. package/server/tests/CompactResourceSerializationTest.php +36 -0
  260. package/server/tests/ControllerFilterContractsTest.php +29 -0
  261. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +30 -13
  262. package/server/tests/Feature/Http/Api/TrailerControllerLifecycleTest.php +640 -0
  263. package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +23 -0
  264. package/server/tests/Feature/Http/Internal/DeviceControllerContractsTest.php +20 -5
  265. package/server/tests/Feature/Http/Internal/LiveControllerEndpointsTest.php +19 -0
  266. package/server/tests/ModelAccessorContractsTest.php +10 -0
  267. package/server/tests/OrderFilterExecutionTest.php +5 -0
  268. package/server/tests/PublicApiControllerInputContractsTest.php +11 -0
  269. package/server/tests/PublicFleetOpsRestEndpointsTest.php +15 -3
  270. package/server/tests/ReportSchemaContractsTest.php +4 -0
  271. package/server/tests/TelematicsHardeningTest.php +4 -2
  272. package/server/tests/TrailerSupportCoverageTest.php +524 -0
  273. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +17 -0
  274. package/server/tests/Unit/Http/Resources/IndexVehicleResourceTest.php +35 -0
  275. package/server/tests/Unit/Models/EquipmentTest.php +22 -0
  276. package/server/tests/Unit/Models/ModelLookupFallbackSeamsTest.php +7 -3
  277. package/server/tests/Unit/Models/TrailerTest.php +204 -0
  278. package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +93 -0
  279. package/server/tests/VehicleControllerHelperContractsTest.php +14 -0
  280. package/server/tests/VehicleResourceContractsTest.php +39 -0
  281. package/translations/ar-ae.yml +396 -0
  282. package/translations/bg-bg.yaml +424 -0
  283. package/translations/en-us.yaml +423 -0
  284. package/translations/es-pa.yaml +425 -0
  285. package/translations/fr-fr.yaml +424 -0
  286. package/translations/mn-mn.yaml +424 -0
  287. package/translations/pt-br.yaml +424 -0
  288. package/translations/ru-ru.yaml +424 -0
  289. package/translations/vi-vn.yaml +435 -0
@@ -0,0 +1,199 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Resources\v1;
4
+
5
+ use Fleetbase\FleetOps\Http\Resources\v1\Concerns\ResolvesPublicRelationFields;
6
+ use Fleetbase\FleetOps\Support\Utils;
7
+ use Fleetbase\Http\Resources\FleetbaseResource;
8
+ use Fleetbase\Support\Http;
9
+ use Fleetbase\Support\Resolve;
10
+
11
+ /**
12
+ * First-class Trailer representation.
13
+ *
14
+ * Internal (console) responses expose the record with its UUID identity and fully
15
+ * embedded relations so Ember Data can hydrate the store. Public API responses use
16
+ * the `trailer_` public identifier, additive `*_id` relation identifiers, and only
17
+ * expand relation objects that were requested through `with`.
18
+ */
19
+ class Trailer extends FleetbaseResource
20
+ {
21
+ use ResolvesPublicRelationFields;
22
+
23
+ /**
24
+ * Transform the resource into an array.
25
+ *
26
+ * @param \Illuminate\Http\Request $request
27
+ */
28
+ public function toArray($request): array
29
+ {
30
+ $internal = Http::isInternalRequest();
31
+ $with = $this->requestedRelations($request);
32
+
33
+ return $this->withCustomFields([
34
+ // Identity
35
+ 'id' => $this->when($internal, $this->id, $this->public_id),
36
+ 'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
37
+ 'public_id' => $this->when($internal, $this->public_id),
38
+ 'company_uuid' => $this->when(Http::isInternalRequest(), $this->company_uuid),
39
+ 'category_uuid' => $this->when(Http::isInternalRequest(), $this->category_uuid),
40
+ 'vendor_uuid' => $this->when(Http::isInternalRequest(), $this->vendor_uuid),
41
+ 'warranty_uuid' => $this->when(Http::isInternalRequest(), $this->warranty_uuid),
42
+ 'photo_uuid' => $this->when(Http::isInternalRequest(), $this->photo_uuid),
43
+ 'asset_class' => 'trailer',
44
+ 'name' => $this->name,
45
+ 'display_name' => $this->display_name,
46
+ 'code' => $this->code,
47
+ 'description' => $this->description,
48
+ 'slug' => $this->when($internal, $this->slug),
49
+ // Classification & lifecycle
50
+ 'type' => $this->type,
51
+ 'body_type' => $this->body_type,
52
+ 'usage_type' => $this->usage_type,
53
+ 'status' => $this->status,
54
+ 'attachment_state' => $this->attachment_state,
55
+ 'connectivity_status' => $this->connectivity_status,
56
+ 'online' => $this->online,
57
+ // Registration
58
+ 'vin' => $this->vin,
59
+ 'plate_number' => $this->plate_number,
60
+ 'serial_number' => $this->serial_number,
61
+ 'make' => $this->make,
62
+ 'model' => $this->model,
63
+ 'year' => $this->year,
64
+ 'color' => $this->color,
65
+ // Media
66
+ 'photo_url' => $this->photo_url,
67
+ // Relationships. Identifiers are additive and always present so a write
68
+ // can be read back; relation objects follow the public `with` contract.
69
+ 'category_id' => $this->publicIdForRelation('category', 'category_uuid'),
70
+ 'vendor_id' => $this->publicIdForRelation('vendor', 'vendor_uuid'),
71
+ 'warranty_id' => $this->publicIdForRelation('warranty', 'warranty_uuid'),
72
+ 'photo_id' => $this->publicIdForRelation('photo', 'photo_uuid'),
73
+ 'category_name' => $this->when($internal, $this->category_name),
74
+ 'vendor_name' => $this->when($internal, $this->vendor_name),
75
+ 'warranty_name' => $this->when($internal, $this->warranty_name),
76
+ 'category' => $this->publicRelationObject('category', $with, fn () => Resolve::httpResourceForModel($this->category)),
77
+ 'vendor' => $this->publicRelationObject('vendor', $with, fn () => new Vendor($this->vendor)),
78
+ 'warranty' => $this->publicRelationObject('warranty', $with, fn () => Resolve::httpResourceForModel($this->warranty)),
79
+ 'photo' => $this->publicRelationObject('photo', $with, fn () => Resolve::httpResourceForModel($this->photo)),
80
+ // Towing connection
81
+ 'vehicle_id' => $this->whenLoaded('currentConnection', fn () => $this->currentConnection?->vehicle?->public_id),
82
+ 'current_vehicle_name' => $this->whenLoaded('currentConnection', fn () => $this->currentConnection?->vehicle?->display_name),
83
+ 'attached_at' => $this->whenLoaded('currentConnection', fn () => $this->currentConnection?->connected_at),
84
+ 'current_vehicle' => $this->whenLoaded('currentConnection', fn () => $this->currentVehicleRepresentation($internal)),
85
+ 'current_connection' => $this->whenLoaded('currentConnection', fn () => $this->currentConnection ? new AssetConnection($this->currentConnection) : null),
86
+ // Ember Data embeds these collections, so the console always receives an array;
87
+ // the public API only includes them when they were loaded.
88
+ 'connections' => $this->embeddedCollection('connections', AssetConnection::class, $internal),
89
+ 'devices' => $this->embeddedCollection('devices', Device::class, $internal),
90
+ // Ember Data embeds the `equipments` relationship; the public contract reads `equipment`.
91
+ 'equipment' => $this->when(!$internal, fn () => $this->whenLoaded('equipments', fn () => Equipment::collection($this->equipments))),
92
+ 'equipments' => $this->when($internal, fn () => $this->embeddedCollection('equipments', Equipment::class, true)),
93
+ 'devices_count' => $this->whenCounted('devices'),
94
+ 'equipment_count' => $this->whenCounted('equipments'),
95
+ // Dimensions & capacity
96
+ 'measurement_system' => $this->measurement_system,
97
+ 'length' => $this->length,
98
+ 'width' => $this->width,
99
+ 'height' => $this->height,
100
+ 'tare_weight' => $this->tare_weight,
101
+ 'gvwr' => $this->gvwr,
102
+ 'payload_capacity' => $this->payload_capacity,
103
+ 'cargo_volume' => $this->cargo_volume,
104
+ 'capacity' => data_get($this, 'capacity', Utils::createObject()),
105
+ // Running gear
106
+ 'axle_count' => $this->axle_count,
107
+ 'tire_count' => $this->tire_count,
108
+ 'door_count' => $this->door_count,
109
+ 'coupling_type' => $this->coupling_type,
110
+ 'brake_type' => $this->brake_type,
111
+ 'abs_equipped' => $this->abs_equipped,
112
+ 'ebs_equipped' => $this->ebs_equipped,
113
+ // Refrigeration
114
+ 'refrigerated' => $this->refrigerated,
115
+ 'temperature_min' => $this->temperature_min,
116
+ 'temperature_max' => $this->temperature_max,
117
+ 'reefer_engine_hours' => $this->reefer_engine_hours,
118
+ // Usage
119
+ 'odometer' => $this->odometer,
120
+ 'odometer_unit' => $this->odometer_unit,
121
+ 'engine_hours' => $this->engine_hours,
122
+ // Ownership & finance
123
+ 'ownership_type' => $this->ownership_type,
124
+ 'financing_status' => $this->financing_status,
125
+ 'currency' => $this->currency,
126
+ 'acquisition_cost' => $this->acquisition_cost,
127
+ 'current_value' => $this->current_value,
128
+ 'insurance_value' => $this->insurance_value,
129
+ 'depreciation_rate' => $this->depreciation_rate,
130
+ 'purchased_at' => $this->purchased_at,
131
+ 'lease_expires_at' => $this->lease_expires_at,
132
+ // Location & telematics
133
+ 'location' => Utils::castPoint($this->location),
134
+ 'speed' => $this->speed,
135
+ 'heading' => $this->heading,
136
+ 'altitude' => $this->altitude,
137
+ 'last_online_at' => $this->last_online_at,
138
+ 'telematics' => $this->telematicsRepresentation($internal),
139
+ 'positions' => $this->whenLoaded('positions', fn () => Position::collection($this->positions)),
140
+ // Blobs, notes & timestamps
141
+ 'specs' => data_get($this, 'specs', Utils::createObject()),
142
+ 'attributes' => $this->resource->getAttribute('attributes') ?? Utils::createObject(),
143
+ 'notes' => $this->notes,
144
+ 'created_at' => $this->created_at,
145
+ 'updated_at' => $this->updated_at,
146
+ ]);
147
+ }
148
+
149
+ /**
150
+ * A loaded relation serializes through its resource; an unloaded one is omitted for the
151
+ * public API and an empty array for the console, which embeds the relationship.
152
+ */
153
+ protected function embeddedCollection(string $relation, string $resourceClass, bool $internal): mixed
154
+ {
155
+ if ($this->resource->relationLoaded($relation)) {
156
+ return $resourceClass::collection($this->{$relation});
157
+ }
158
+
159
+ return $internal ? [] : new \Illuminate\Http\Resources\MissingValue();
160
+ }
161
+
162
+ /**
163
+ * The console receives the full Vehicle record so Ember Data can hydrate it; the
164
+ * public API receives a compact public identity.
165
+ */
166
+ protected function currentVehicleRepresentation(bool $internal): mixed
167
+ {
168
+ $vehicle = $this->currentConnection?->vehicle;
169
+
170
+ if (!$vehicle) {
171
+ return null;
172
+ }
173
+
174
+ if ($internal) {
175
+ return Resolve::httpResourceForModel($vehicle);
176
+ }
177
+
178
+ return [
179
+ 'id' => $vehicle->public_id,
180
+ 'name' => $vehicle->display_name,
181
+ 'plate_number' => $vehicle->plate_number,
182
+ ];
183
+ }
184
+
185
+ /**
186
+ * Telemetry provenance references devices and events by UUID for the console;
187
+ * the public API must never expose internal identifiers.
188
+ */
189
+ protected function telematicsRepresentation(bool $internal): mixed
190
+ {
191
+ $telematics = $this->telematics;
192
+
193
+ if ($internal || !is_array($telematics)) {
194
+ return $telematics;
195
+ }
196
+
197
+ return array_filter($telematics, fn ($key) => !preg_match('/(^uuid$|_uuid$)/', (string) $key), ARRAY_FILTER_USE_KEY);
198
+ }
199
+ }
@@ -68,6 +68,8 @@ class Vehicle extends FleetbaseResource
68
68
  'warranty' => $this->publicRelationObject('warranty', $with, fn () => Resolve::httpResourceForModel($this->warranty)),
69
69
  'photo' => $this->publicRelationObject('photo', $with, fn () => Resolve::httpResourceForModel($this->photo)),
70
70
  'devices' => $this->whenLoaded('devices', fn () => $this->devices),
71
+ // Trailers currently coupled to this vehicle (`with=trailers` on the public API).
72
+ 'trailers' => $this->publicRelationObject('currentTrailers', $with, fn () => Trailer::collection($this->currentTrailers)),
71
73
  // Vehicle identification
72
74
  'make' => $this->make,
73
75
  'model' => $this->model,
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Resources\v1;
4
4
 
5
+ use Fleetbase\FleetOps\Support\TrailerSummary;
5
6
  use Fleetbase\FleetOps\Support\Utils;
6
7
  use Fleetbase\Http\Resources\FleetbaseResource;
7
8
  use Fleetbase\Support\Http;
@@ -41,6 +42,8 @@ class VehicleWithoutDriver extends FleetbaseResource
41
42
  'vendor_name' => $this->when(Http::isInternalRequest(), $this->vendor_name),
42
43
  // Relationships
43
44
  'devices' => $this->whenLoaded('devices', fn () => $this->devices),
45
+ // Current towing connections, compact; the console vehicle cards list them.
46
+ 'trailers' => $this->whenLoaded('currentTrailers', fn () => TrailerSummary::collection($this->currentTrailers)),
44
47
  // Vehicle identification
45
48
  'make' => $this->make,
46
49
  'model' => $this->model,
@@ -0,0 +1,33 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Imports;
4
+
5
+ use Fleetbase\FleetOps\Models\Trailer;
6
+ use Illuminate\Support\Collection;
7
+ use Illuminate\Validation\ValidationException;
8
+ use Maatwebsite\Excel\Concerns\ToCollection;
9
+ use Maatwebsite\Excel\Concerns\WithHeadingRow;
10
+
11
+ class TrailerImport implements ToCollection, WithHeadingRow
12
+ {
13
+ public int $imported = 0;
14
+
15
+ public function collection(Collection $rows)
16
+ {
17
+ foreach ($rows as $index => $row) {
18
+ $data = $row instanceof Collection ? array_filter($row->toArray(), fn ($v) => $v !== null && $v !== '') : $row;
19
+ if (!$data) {
20
+ continue;
21
+ }
22
+ if (empty($data['name'])) {
23
+ throw ValidationException::withMessages(["row.$index.name" => ['Name is required.']]);
24
+ }
25
+ if (!empty($data['type']) && !in_array($data['type'], Trailer::TYPES, true)) {
26
+ throw ValidationException::withMessages(["row.$index.type" => ['Invalid trailer type.']]);
27
+ }
28
+ $allowed = ['name', 'code', 'description', 'type', 'status', 'vin', 'plate_number', 'serial_number', 'make', 'model', 'year', 'length', 'width', 'height', 'tare_weight', 'gvwr', 'payload_capacity', 'cargo_volume', 'axle_count', 'tire_count', 'coupling_type', 'brake_type', 'refrigerated', 'measurement_system', 'odometer', 'odometer_unit', 'ownership_type', 'purchased_at', 'lease_expires_at', 'notes'];
29
+ Trailer::create(array_merge(array_intersect_key($data, array_flip($allowed)), ['company_uuid' => session('company')]));
30
+ $this->imported++;
31
+ }
32
+ }
33
+ }
@@ -5,6 +5,7 @@ namespace Fleetbase\FleetOps\Models;
5
5
  use Fleetbase\Casts\Json;
6
6
  use Fleetbase\Casts\PolymorphicType;
7
7
  use Fleetbase\FleetOps\Casts\Point;
8
+ use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
8
9
  use Fleetbase\Models\Category;
9
10
  use Fleetbase\Models\File;
10
11
  use Fleetbase\Models\Model;
@@ -33,6 +34,7 @@ use Spatie\Sluggable\SlugOptions;
33
34
  */
34
35
  class Asset extends Model
35
36
  {
37
+ use SpatialTrait;
36
38
  use HasUuid;
37
39
  use HasPublicId;
38
40
  use TracksApiCredential;
@@ -117,13 +119,37 @@ class Asset extends Model
117
119
  'odometer_unit',
118
120
  'transmission',
119
121
  'fuel_volume_unit',
120
- 'fuel_Type',
122
+ 'fuel_type',
121
123
  'ownership_type',
122
124
  'engine_hours',
123
125
  'gvw',
124
126
  'capacity',
125
127
  'specs',
126
128
  'attributes',
129
+ 'asset_class',
130
+ 'online',
131
+ 'last_online_at',
132
+ 'telematics',
133
+ 'purchased_at',
134
+ 'length',
135
+ 'width',
136
+ 'height',
137
+ 'tare_weight',
138
+ 'gvwr',
139
+ 'payload_capacity',
140
+ 'cargo_volume',
141
+ 'axle_count',
142
+ 'tire_count',
143
+ 'door_count',
144
+ 'body_type',
145
+ 'coupling_type',
146
+ 'brake_type',
147
+ 'abs_equipped',
148
+ 'ebs_equipped',
149
+ 'refrigerated',
150
+ 'temperature_min',
151
+ 'temperature_max',
152
+ 'reefer_engine_hours',
127
153
  'notes',
128
154
  'slug',
129
155
  ];
@@ -158,15 +184,18 @@ class Asset extends Model
158
184
  * @var array
159
185
  */
160
186
  protected $casts = [
161
- 'year' => 'integer',
162
- 'odometer' => 'integer',
163
- 'engine_hours' => 'integer',
164
- 'gvw' => 'decimal:2',
165
- 'capacity' => Json::class,
166
- 'specs' => Json::class,
167
- 'attributes' => Json::class,
168
- 'location' => Point::class,
169
- 'assigned_to_type' => PolymorphicType::class,
187
+ 'year' => 'integer',
188
+ 'odometer' => 'integer',
189
+ 'engine_hours' => 'integer',
190
+ 'gvw' => 'decimal:2',
191
+ 'capacity' => Json::class,
192
+ 'specs' => Json::class,
193
+ 'attributes' => Json::class,
194
+ 'location' => Point::class,
195
+ 'assigned_to_type' => PolymorphicType::class,
196
+ 'online' => 'boolean',
197
+ 'last_online_at' => 'datetime',
198
+ 'telematics' => Json::class,
170
199
  ];
171
200
 
172
201
  /**
@@ -0,0 +1,43 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Models;
4
+
5
+ use Fleetbase\Casts\Json;
6
+ use Fleetbase\Casts\PolymorphicType;
7
+ use Fleetbase\Models\Model;
8
+ use Fleetbase\Traits\HasPublicId;
9
+ use Fleetbase\Traits\HasUuid;
10
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
11
+ use Illuminate\Database\Eloquent\Relations\MorphTo;
12
+ use Illuminate\Database\Eloquent\SoftDeletes;
13
+
14
+ class AssetConnection extends Model
15
+ {
16
+ use HasUuid;
17
+ use HasPublicId;
18
+ use SoftDeletes;
19
+
20
+ protected $publicIdType = 'connection';
21
+ protected $fillable = ['company_uuid', 'connector_type', 'connector_uuid', 'connected_type', 'connected_uuid', 'active_connected_uuid', 'active_connector_position', 'relationship_type', 'position', 'connected_at', 'disconnected_at', 'source', 'confidence', 'notes', 'meta', 'created_by_uuid', 'updated_by_uuid'];
22
+ protected $casts = ['connector_type' => PolymorphicType::class, 'connected_type' => PolymorphicType::class, 'connected_at' => 'datetime', 'disconnected_at' => 'datetime', 'meta' => Json::class];
23
+
24
+ public function connector(): MorphTo
25
+ {
26
+ return $this->morphTo(__FUNCTION__, 'connector_type', 'connector_uuid');
27
+ }
28
+
29
+ public function connected(): MorphTo
30
+ {
31
+ return $this->morphTo(__FUNCTION__, 'connected_type', 'connected_uuid');
32
+ }
33
+
34
+ public function vehicle(): BelongsTo
35
+ {
36
+ return $this->belongsTo(Vehicle::class, 'connector_uuid', 'uuid');
37
+ }
38
+
39
+ public function trailer(): BelongsTo
40
+ {
41
+ return $this->belongsTo(Trailer::class, 'connected_uuid', 'uuid');
42
+ }
43
+ }
@@ -5,6 +5,7 @@ namespace Fleetbase\FleetOps\Models;
5
5
  use Fleetbase\Casts\Json;
6
6
  use Fleetbase\Casts\PolymorphicType;
7
7
  use Fleetbase\FleetOps\Casts\Point;
8
+ use Fleetbase\FleetOps\Exceptions\DeviceAlreadyAttachedException;
8
9
  use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
9
10
  use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
10
11
  use Fleetbase\Models\File;
@@ -280,6 +281,11 @@ class Device extends Model
280
281
  return $this->morphTo(__FUNCTION__, 'attachable_type', 'attachable_uuid');
281
282
  }
282
283
 
284
+ public function installations(): HasMany
285
+ {
286
+ return $this->hasMany(DeviceInstallation::class, 'device_uuid', 'uuid')->latest('installed_at');
287
+ }
288
+
283
289
  public function events(): HasMany
284
290
  {
285
291
  return $this->hasMany(DeviceEvent::class, 'device_uuid', 'uuid');
@@ -420,10 +426,31 @@ class Device extends Model
420
426
  */
421
427
  public function attachTo(Model $attachable): bool
422
428
  {
423
- $updated = $this->update([
424
- 'attachable_type' => get_class($attachable),
425
- 'attachable_uuid' => $attachable->uuid,
426
- ]);
429
+ if ($attachable->company_uuid && $this->company_uuid && $attachable->company_uuid !== $this->company_uuid) {
430
+ throw new \DomainException('Devices cannot be attached across companies.');
431
+ }
432
+
433
+ if ($this->attachable_type === get_class($attachable) && $this->attachable_uuid === $attachable->uuid) {
434
+ return true;
435
+ }
436
+
437
+ // Re-homing an installed device must be an explicit detach first, never a side effect.
438
+ if ($this->attachable_uuid) {
439
+ throw DeviceAlreadyAttachedException::for($this->attached_to_name);
440
+ }
441
+
442
+ $historyEnabled = app()->bound('db.schema') && \Illuminate\Support\Facades\Schema::hasTable('device_installations');
443
+ $updated = \Illuminate\Support\Facades\DB::transaction(function () use ($attachable, $historyEnabled) {
444
+ if ($historyEnabled) {
445
+ DeviceInstallation::where('company_uuid', $this->company_uuid)->where('device_uuid', $this->uuid)->whereNull('removed_at')->lockForUpdate()->update(['removed_at' => now(), 'active_device_uuid' => null]);
446
+ }
447
+ $updated = $this->update(['attachable_type' => get_class($attachable), 'attachable_uuid' => $attachable->uuid]);
448
+ if ($historyEnabled) {
449
+ DeviceInstallation::create(['company_uuid' => $this->company_uuid, 'device_uuid' => $this->uuid, 'attachable_type' => get_class($attachable), 'attachable_uuid' => $attachable->uuid, 'active_device_uuid' => $this->uuid, 'installed_at' => now(), 'source' => 'manual']);
450
+ }
451
+
452
+ return $updated;
453
+ });
427
454
 
428
455
  if ($updated) {
429
456
  activity('device_attached')
@@ -447,10 +474,14 @@ class Device extends Model
447
474
  $oldAttachableType = $this->attachable_type;
448
475
  $oldAttachableUuid = $this->attachable_uuid;
449
476
 
450
- $updated = $this->update([
451
- 'attachable_type' => null,
452
- 'attachable_uuid' => null,
453
- ]);
477
+ $historyEnabled = app()->bound('db.schema') && \Illuminate\Support\Facades\Schema::hasTable('device_installations');
478
+ $updated = \Illuminate\Support\Facades\DB::transaction(function () use ($historyEnabled) {
479
+ if ($historyEnabled) {
480
+ DeviceInstallation::where('company_uuid', $this->company_uuid)->where('device_uuid', $this->uuid)->whereNull('removed_at')->lockForUpdate()->update(['removed_at' => now(), 'active_device_uuid' => null]);
481
+ }
482
+
483
+ return $this->update(['attachable_type' => null, 'attachable_uuid' => null]);
484
+ });
454
485
 
455
486
  if ($updated) {
456
487
  activity('device_detached')
@@ -0,0 +1,27 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Models;
4
+
5
+ use Fleetbase\Casts\Json;
6
+ use Fleetbase\Casts\PolymorphicType;
7
+ use Fleetbase\Models\Model;
8
+ use Fleetbase\Traits\HasUuid;
9
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
10
+ use Illuminate\Database\Eloquent\Relations\MorphTo;
11
+
12
+ class DeviceInstallation extends Model
13
+ {
14
+ use HasUuid;
15
+ protected $fillable = ['company_uuid', 'device_uuid', 'attachable_type', 'attachable_uuid', 'active_device_uuid', 'installed_at', 'removed_at', 'source', 'metadata'];
16
+ protected $casts = ['attachable_type' => PolymorphicType::class, 'installed_at' => 'datetime', 'removed_at' => 'datetime', 'metadata' => Json::class];
17
+
18
+ public function device(): BelongsTo
19
+ {
20
+ return $this->belongsTo(Device::class, 'device_uuid', 'uuid');
21
+ }
22
+
23
+ public function attachable(): MorphTo
24
+ {
25
+ return $this->morphTo(__FUNCTION__, 'attachable_type', 'attachable_uuid');
26
+ }
27
+ }
@@ -168,6 +168,7 @@ class Equipment extends Model
168
168
  Relation::morphMap([
169
169
  'fleet-ops:vehicle' => Vehicle::class,
170
170
  'fleet-ops:driver' => Driver::class,
171
+ 'fleet-ops:trailer' => Trailer::class,
171
172
  ]);
172
173
  }
173
174
 
@@ -243,6 +244,7 @@ class Equipment extends Model
243
244
  $this->attributes['equipable_type'] = match ($type) {
244
245
  'fleet-ops:vehicle', 'vehicle' => Utils::getMutationType('fleet-ops:vehicle'),
245
246
  'fleet-ops:driver', 'driver' => Utils::getMutationType('fleet-ops:driver'),
247
+ 'fleet-ops:trailer', 'trailer' => Utils::getMutationType('fleet-ops:trailer'),
246
248
  default => $type,
247
249
  };
248
250
  }