@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.
Files changed (284) hide show
  1. package/RELEASE.md +7 -5
  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/Device.php +3 -1
  238. package/server/src/Http/Resources/v1/Equipment.php +3 -1
  239. package/server/src/Http/Resources/v1/Index/Vehicle.php +10 -0
  240. package/server/src/Http/Resources/v1/Trailer.php +199 -0
  241. package/server/src/Http/Resources/v1/Vehicle.php +2 -0
  242. package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +3 -0
  243. package/server/src/Imports/TrailerImport.php +33 -0
  244. package/server/src/Models/Asset.php +39 -10
  245. package/server/src/Models/AssetConnection.php +43 -0
  246. package/server/src/Models/Device.php +39 -8
  247. package/server/src/Models/DeviceInstallation.php +27 -0
  248. package/server/src/Models/Equipment.php +2 -0
  249. package/server/src/Models/Trailer.php +181 -0
  250. package/server/src/Models/Vehicle.php +13 -0
  251. package/server/src/Providers/FleetOpsServiceProvider.php +4 -2
  252. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +22 -0
  253. package/server/src/Support/Reporting/FleetOpsReportSchema.php +38 -0
  254. package/server/src/Support/Telematics/TelematicService.php +35 -0
  255. package/server/src/Support/TrailerSummary.php +35 -0
  256. package/server/src/routes.php +29 -0
  257. package/server/tests/CompactResourceSerializationTest.php +36 -0
  258. package/server/tests/ControllerFilterContractsTest.php +29 -0
  259. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +30 -13
  260. package/server/tests/Feature/Http/Api/TrailerControllerLifecycleTest.php +640 -0
  261. package/server/tests/Feature/Http/Internal/DeviceControllerContractsTest.php +20 -5
  262. package/server/tests/Feature/Http/Internal/LiveControllerEndpointsTest.php +19 -0
  263. package/server/tests/ModelAccessorContractsTest.php +10 -0
  264. package/server/tests/OrderFilterExecutionTest.php +5 -0
  265. package/server/tests/PublicApiControllerInputContractsTest.php +11 -0
  266. package/server/tests/PublicFleetOpsRestEndpointsTest.php +15 -3
  267. package/server/tests/ReportSchemaContractsTest.php +4 -0
  268. package/server/tests/TelematicsHardeningTest.php +4 -2
  269. package/server/tests/TrailerSupportCoverageTest.php +524 -0
  270. package/server/tests/Unit/Http/Resources/IndexVehicleResourceTest.php +35 -0
  271. package/server/tests/Unit/Models/EquipmentTest.php +22 -0
  272. package/server/tests/Unit/Models/ModelLookupFallbackSeamsTest.php +7 -3
  273. package/server/tests/Unit/Models/TrailerTest.php +204 -0
  274. package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +93 -0
  275. package/server/tests/VehicleControllerHelperContractsTest.php +14 -0
  276. package/translations/ar-ae.yml +396 -0
  277. package/translations/bg-bg.yaml +424 -0
  278. package/translations/en-us.yaml +423 -0
  279. package/translations/es-pa.yaml +425 -0
  280. package/translations/fr-fr.yaml +424 -0
  281. package/translations/mn-mn.yaml +424 -0
  282. package/translations/pt-br.yaml +424 -0
  283. package/translations/ru-ru.yaml +424 -0
  284. package/translations/vi-vn.yaml +435 -0
@@ -0,0 +1,269 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Filter;
4
+
5
+ use Fleetbase\FleetOps\Models\Device;
6
+ use Fleetbase\FleetOps\Models\Vehicle;
7
+ use Fleetbase\FleetOps\Models\Vendor;
8
+ use Fleetbase\FleetOps\Support\Utils;
9
+ use Fleetbase\Http\Filter\Filter;
10
+ use Fleetbase\Models\Category;
11
+ use Fleetbase\Support\Http;
12
+
13
+ class TrailerFilter extends Filter
14
+ {
15
+ public function queryForInternal()
16
+ {
17
+ $this->builder->where('company_uuid', $this->session->get('company'));
18
+ }
19
+
20
+ public function queryForPublic()
21
+ {
22
+ $this->queryForInternal();
23
+ }
24
+
25
+ public function query(?string $value)
26
+ {
27
+ $this->builder->search($value);
28
+ }
29
+
30
+ public function publicId(?string $value)
31
+ {
32
+ $this->builder->searchWhere('public_id', $value);
33
+ }
34
+
35
+ public function name(?string $value)
36
+ {
37
+ $this->builder->searchWhere('name', $value);
38
+ }
39
+
40
+ public function code(?string $value)
41
+ {
42
+ $this->builder->searchWhere('code', $value);
43
+ }
44
+
45
+ public function trailerType($value)
46
+ {
47
+ $this->whereOneOf('type', $value);
48
+ }
49
+
50
+ public function status($value)
51
+ {
52
+ $this->whereOneOf('status', $value);
53
+ }
54
+
55
+ public function trailerMake(?string $value)
56
+ {
57
+ $this->builder->searchWhere('make', $value);
58
+ }
59
+
60
+ public function trailerModel(?string $value)
61
+ {
62
+ $this->builder->searchWhere('model', $value);
63
+ }
64
+
65
+ public function trailerYear($value)
66
+ {
67
+ $this->whereOneOf('year', $value);
68
+ }
69
+
70
+ public function plateNumber(?string $value)
71
+ {
72
+ $this->builder->searchWhere('plate_number', $value);
73
+ }
74
+
75
+ public function vin(?string $value)
76
+ {
77
+ $this->builder->searchWhere('vin', $value);
78
+ }
79
+
80
+ public function serialNumber(?string $value)
81
+ {
82
+ $this->builder->searchWhere('serial_number', $value);
83
+ }
84
+
85
+ public function bodyType(?string $value)
86
+ {
87
+ $this->builder->searchWhere('body_type', $value);
88
+ }
89
+
90
+ public function length($value)
91
+ {
92
+ $this->whereOneOf('length', $value);
93
+ }
94
+
95
+ public function axleCount($value)
96
+ {
97
+ $this->whereOneOf('axle_count', $value);
98
+ }
99
+
100
+ public function gvwr($value)
101
+ {
102
+ $this->whereOneOf('gvwr', $value);
103
+ }
104
+
105
+ public function payloadCapacity($value)
106
+ {
107
+ $this->whereOneOf('payload_capacity', $value);
108
+ }
109
+
110
+ public function ownershipType($value)
111
+ {
112
+ $this->whereOneOf('ownership_type', $value);
113
+ }
114
+
115
+ public function refrigerated($value)
116
+ {
117
+ $this->builder->where('refrigerated', filter_var($value, FILTER_VALIDATE_BOOLEAN));
118
+ }
119
+
120
+ public function devicesCount($value)
121
+ {
122
+ $this->builder->has('devices', '=', (int) $value);
123
+ }
124
+
125
+ public function equipmentCount($value)
126
+ {
127
+ $this->builder->has('equipments', '=', (int) $value);
128
+ }
129
+
130
+ /**
131
+ * Connectivity is derived from `last_online_at`, mirroring
132
+ * Trailer::getConnectivityStatusAttribute(). Multiple states combine with OR.
133
+ */
134
+ public function connectivityStatus($value)
135
+ {
136
+ $states = array_values(array_filter(array_map('strval', (array) $value), 'strlen'));
137
+
138
+ if (empty($states)) {
139
+ return;
140
+ }
141
+
142
+ $this->builder->where(function ($query) use ($states) {
143
+ foreach ($states as $state) {
144
+ $query->orWhere(function ($query) use ($state) {
145
+ match ($state) {
146
+ 'never_connected' => $query->whereNull('last_online_at'),
147
+ 'online' => $query->where('last_online_at', '>=', now()->subMinutes(10)),
148
+ 'recently_offline' => $query->whereBetween('last_online_at', [now()->subDay(), now()->subMinutes(10)]),
149
+ 'offline' => $query->where('last_online_at', '<', now()->subDay()),
150
+ default => $query->whereRaw('0 = 1'),
151
+ };
152
+ });
153
+ }
154
+ });
155
+ }
156
+
157
+ public function attachmentState($value)
158
+ {
159
+ $states = array_unique(array_values(array_filter(array_map('strval', (array) $value), 'strlen')));
160
+
161
+ // Asking for both states is the same as not filtering.
162
+ if (empty($states) || count($states) > 1) {
163
+ return;
164
+ }
165
+
166
+ $method = $states[0] === 'attached' ? 'whereHas' : 'whereDoesntHave';
167
+ $this->builder->{$method}('currentConnection');
168
+ }
169
+
170
+ public function vehicle($value)
171
+ {
172
+ $uuids = $this->resolveRelationUuids(Vehicle::class, $value);
173
+
174
+ $this->builder->whereHas('currentConnection', fn ($query) => $query->whereIn('connector_uuid', $uuids));
175
+ }
176
+
177
+ public function vendor($value)
178
+ {
179
+ $this->builder->whereIn('vendor_uuid', $this->resolveRelationUuids(Vendor::class, $value));
180
+ }
181
+
182
+ /**
183
+ * Trailers with any of the given devices installed.
184
+ */
185
+ public function device($value)
186
+ {
187
+ $uuids = $this->resolveRelationUuids(Device::class, $value);
188
+
189
+ $this->builder->whereHas('devices', fn ($query) => $query->whereIn('uuid', $uuids));
190
+ }
191
+
192
+ public function category($value)
193
+ {
194
+ $this->builder->whereIn('category_uuid', $this->resolveRelationUuids(Category::class, $value, false));
195
+ }
196
+
197
+ public function createdAt($value)
198
+ {
199
+ $this->dateFilter('created_at', $value);
200
+ }
201
+
202
+ public function updatedAt($value)
203
+ {
204
+ $this->dateFilter('updated_at', $value);
205
+ }
206
+
207
+ public function lastOnlineAt($value)
208
+ {
209
+ $this->dateFilter('last_online_at', $value);
210
+ }
211
+
212
+ public function purchasedAt($value)
213
+ {
214
+ $this->dateFilter('purchased_at', $value);
215
+ }
216
+
217
+ /**
218
+ * Console multi-option filters submit arrays; the public API submits scalars.
219
+ */
220
+ private function whereOneOf(string $column, $value): void
221
+ {
222
+ $values = array_values(array_filter(array_map('strval', (array) $value), 'strlen'));
223
+
224
+ if (empty($values)) {
225
+ return;
226
+ }
227
+
228
+ count($values) === 1 ? $this->builder->where($column, $values[0]) : $this->builder->whereIn($column, $values);
229
+ }
230
+
231
+ /**
232
+ * Resolve related record identifiers to UUIDs. The console filters by the record's
233
+ * UUID; the public API filters by public id (or a vehicle's internal id).
234
+ */
235
+ private function resolveRelationUuids(string $modelClass, $identifiers, bool $scopeToCompany = true): array
236
+ {
237
+ $identifiers = array_values(array_filter(array_map('strval', (array) $identifiers), 'strlen'));
238
+
239
+ if (empty($identifiers)) {
240
+ return [];
241
+ }
242
+
243
+ $instance = new $modelClass();
244
+ $query = $modelClass::query()->where(function ($query) use ($identifiers, $instance) {
245
+ $query->whereIn('public_id', $identifiers);
246
+
247
+ if (in_array('internal_id', $instance->getFillable(), true)) {
248
+ $query->orWhereIn('internal_id', $identifiers);
249
+ }
250
+
251
+ if (Http::isInternalRequest($this->request)) {
252
+ $query->orWhereIn('uuid', $identifiers);
253
+ }
254
+ });
255
+
256
+ if ($scopeToCompany) {
257
+ $query->where('company_uuid', $this->session->get('company'));
258
+ }
259
+
260
+ return $query->pluck('uuid')->all();
261
+ }
262
+
263
+ private function dateFilter(string $column, $value): void
264
+ {
265
+ $range = Utils::dateRange($value);
266
+
267
+ is_array($range) ? $this->builder->whereBetween($column, $range) : $this->builder->whereDate($column, $range);
268
+ }
269
+ }
@@ -3,8 +3,10 @@
3
3
  namespace Fleetbase\FleetOps\Http\Filter;
4
4
 
5
5
  use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
6
+ use Fleetbase\FleetOps\Models\Device;
6
7
  use Fleetbase\FleetOps\Models\Driver;
7
8
  use Fleetbase\FleetOps\Models\Fleet;
9
+ use Fleetbase\FleetOps\Models\Trailer;
8
10
  use Fleetbase\FleetOps\Models\Vehicle;
9
11
  use Fleetbase\FleetOps\Models\Vendor;
10
12
  use Fleetbase\FleetOps\Support\Utils;
@@ -55,8 +57,18 @@ class VehicleFilter extends Filter
55
57
  * is the console that gets the exception. An unknown request context is
56
58
  * treated as public: exact is the safe default of the two.
57
59
  */
58
- public function internalId(?string $internalId)
60
+ public function internalId($internalId)
59
61
  {
62
+ // The console's tag-input filter sends several ids at once (batch scans);
63
+ // any list is an exact match on each id.
64
+ $ids = array_values(array_filter(array_map('trim', explode(',', (string) $internalId)), 'strlen'));
65
+
66
+ if (count($ids) > 1) {
67
+ $this->builder->whereIn('internal_id', $ids);
68
+
69
+ return;
70
+ }
71
+
60
72
  if (Http::isInternalRequest($this->request)) {
61
73
  $this->builder->searchWhere('internal_id', $internalId);
62
74
 
@@ -107,7 +119,7 @@ class VehicleFilter extends Filter
107
119
  $this->builder->searchWhere('year', $vehicleYear);
108
120
  }
109
121
 
110
- public function driver(?string $driverId)
122
+ public function driver($driverId)
111
123
  {
112
124
  if ($driverId === 'unassigned') {
113
125
  $this->builder->whereDoesntHave('driver');
@@ -125,7 +137,62 @@ class VehicleFilter extends Filter
125
137
  );
126
138
  }
127
139
 
128
- public function vendor(?string $vendor)
140
+ /**
141
+ * Vehicles currently towing any of the given trailers (public id, internal id or,
142
+ * for the console, uuid). Several trailers combine with OR.
143
+ */
144
+ public function trailer($trailer)
145
+ {
146
+ $uuids = $this->resolvePublicRelationUuids(Trailer::class, $trailer);
147
+
148
+ $this->builder->whereHas('currentTrailers', function ($query) use ($uuids) {
149
+ $query->whereIn('assets.uuid', $uuids);
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Vehicles with any of the given devices installed.
155
+ */
156
+ public function device($device)
157
+ {
158
+ $uuids = $this->resolvePublicRelationUuids(Device::class, $device);
159
+
160
+ $this->builder->whereHas('devices', function ($query) use ($uuids) {
161
+ $query->whereIn('uuid', $uuids);
162
+ });
163
+ }
164
+
165
+ public function hasTrailer($value)
166
+ {
167
+ $this->whereRelationPresence('currentTrailers', $value);
168
+ }
169
+
170
+ public function hasDriver($value)
171
+ {
172
+ $this->whereRelationPresence('driver', $value);
173
+ }
174
+
175
+ public function hasDevice($value)
176
+ {
177
+ $this->whereRelationPresence('devices', $value);
178
+ }
179
+
180
+ /**
181
+ * `true` keeps vehicles that have the relation, `false` those that do not, and
182
+ * anything else (an empty select) leaves the query untouched.
183
+ */
184
+ protected function whereRelationPresence(string $relation, $value): void
185
+ {
186
+ $presence = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
187
+
188
+ if ($presence === true) {
189
+ $this->builder->whereHas($relation, fn ($query) => $query);
190
+ } elseif ($presence === false) {
191
+ $this->builder->whereDoesntHave($relation);
192
+ }
193
+ }
194
+
195
+ public function vendor($vendor)
129
196
  {
130
197
  if (!$vendor) {
131
198
  return;
@@ -41,7 +41,7 @@ class CreateDeviceRequest extends FleetbaseRequest
41
41
  'telematic' => ['nullable', 'string'],
42
42
  'warranty' => ['nullable', 'string'],
43
43
  'photo' => ['nullable', 'string'],
44
- 'attachable_type' => ['nullable', 'string'],
44
+ 'attachable_type' => ['nullable', Rule::in(['fleet-ops:vehicle', 'fleet-ops:trailer', 'vehicle', 'trailer'])],
45
45
  'attachable' => ['nullable', 'required_with:attachable_type', 'string'],
46
46
  'meta' => ['nullable', 'array'],
47
47
  'data' => ['nullable', 'array'],
@@ -24,7 +24,7 @@ class CreateEquipmentRequest extends FleetbaseRequest
24
24
  'model' => ['nullable', 'string'],
25
25
  'warranty' => ['nullable', 'string'],
26
26
  'photo' => ['nullable', 'string'],
27
- 'equipable_type' => ['nullable', 'string'],
27
+ 'equipable_type' => ['nullable', Rule::in(['fleet-ops:vehicle', 'fleet-ops:trailer', 'fleet-ops:driver', 'vehicle', 'trailer', 'driver'])],
28
28
  'equipable' => ['nullable', 'required_with:equipable_type', 'string'],
29
29
  'purchased_at' => ['nullable', 'date'],
30
30
  'purchase_price' => ['nullable'],
@@ -0,0 +1,54 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Requests;
4
+
5
+ use Fleetbase\FleetOps\Models\Trailer;
6
+ use Fleetbase\FleetOps\Rules\ResolvablePoint;
7
+ use Fleetbase\Http\Requests\FleetbaseRequest;
8
+ use Illuminate\Validation\Rule;
9
+
10
+ class CreateTrailerRequest extends FleetbaseRequest
11
+ {
12
+ public function authorize(): bool
13
+ {
14
+ return request()->session()->has('api_credential') || request()->session()->has('is_sanctum_token');
15
+ }
16
+
17
+ public function rules(): array
18
+ {
19
+ return [
20
+ 'name' => [Rule::requiredIf($this->isMethod('POST')), 'string', 'max:255'],
21
+ 'description' => ['nullable', 'string'],
22
+ 'code' => ['nullable', 'string', 'max:255'],
23
+ 'type' => ['nullable', Rule::in(Trailer::TYPES)],
24
+ 'body_type' => ['nullable', 'string', 'max:255'],
25
+ 'status' => ['nullable', Rule::in(Trailer::STATUSES)],
26
+ 'vin' => ['nullable', 'string', 'max:255'],
27
+ 'plate_number' => ['nullable', 'string', 'max:255'],
28
+ 'serial_number' => ['nullable', 'string', 'max:255'],
29
+ 'make' => ['nullable', 'string', 'max:255'],
30
+ 'model' => ['nullable', 'string', 'max:255'],
31
+ 'year' => ['nullable', 'integer', 'min:1880', 'max:' . (now()->year + 2)],
32
+ 'color' => ['nullable', 'string', 'max:255'],
33
+ 'usage_type' => ['nullable', 'string', 'max:255'],
34
+ 'category' => ['nullable', 'string'], 'vendor' => ['nullable', 'string'], 'warranty' => ['nullable', 'string'], 'photo' => ['nullable', 'string'],
35
+ 'location' => ['nullable', new ResolvablePoint()], 'latitude' => ['nullable', 'numeric', 'between:-90,90', 'required_with:longitude'],
36
+ 'longitude' => ['nullable', 'numeric', 'between:-180,180', 'required_with:latitude'],
37
+ 'length' => ['nullable', 'numeric', 'min:0'], 'width' => ['nullable', 'numeric', 'min:0'], 'height' => ['nullable', 'numeric', 'min:0'],
38
+ 'tare_weight' => ['nullable', 'numeric', 'min:0'], 'gvwr' => ['nullable', 'numeric', 'min:0'], 'payload_capacity' => ['nullable', 'numeric', 'min:0'],
39
+ 'cargo_volume' => ['nullable', 'numeric', 'min:0'], 'axle_count' => ['nullable', 'integer', 'min:0'], 'tire_count' => ['nullable', 'integer', 'min:0'],
40
+ 'door_count' => ['nullable', 'integer', 'min:0'], 'abs_equipped' => ['nullable', 'boolean'], 'ebs_equipped' => ['nullable', 'boolean'],
41
+ 'refrigerated' => ['nullable', 'boolean'], 'temperature_min' => ['nullable', 'numeric'], 'temperature_max' => ['nullable', 'numeric', 'gte:temperature_min'],
42
+ 'reefer_engine_hours' => ['nullable', 'numeric', 'min:0'],
43
+ 'measurement_system' => ['nullable', Rule::in(['metric', 'imperial'])], 'odometer' => ['nullable', 'numeric', 'min:0'],
44
+ 'odometer_unit' => ['nullable', Rule::in(['km', 'mi', 'miles', 'kilometers'])],
45
+ 'coupling_type' => ['nullable', 'string', 'max:255'], 'brake_type' => ['nullable', 'string', 'max:255'],
46
+ 'ownership_type' => ['nullable', Rule::in(Trailer::OWNERSHIP_TYPES)],
47
+ 'purchased_at' => ['nullable', 'date'], 'lease_expires_at' => ['nullable', 'date'],
48
+ 'financing_status' => ['nullable', 'string', 'max:255'], 'currency' => ['nullable', 'string', 'size:3'],
49
+ 'acquisition_cost' => ['nullable', 'numeric', 'min:0'], 'current_value' => ['nullable', 'numeric', 'min:0'],
50
+ 'insurance_value' => ['nullable', 'numeric', 'min:0'], 'depreciation_rate' => ['nullable', 'numeric', 'min:0'],
51
+ 'capacity' => ['nullable', 'array'], 'specs' => ['nullable', 'array'], 'attributes' => ['nullable', 'array'], 'notes' => ['nullable', 'string'],
52
+ ];
53
+ }
54
+ }
@@ -0,0 +1,7 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Requests;
4
+
5
+ class UpdateTrailerRequest extends CreateTrailerRequest
6
+ {
7
+ }
@@ -0,0 +1,90 @@
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
+ * An effective-dated towing connection between a Vehicle (connector) and a
13
+ * Trailer (connected asset).
14
+ */
15
+ class AssetConnection extends FleetbaseResource
16
+ {
17
+ use ResolvesPublicRelationFields;
18
+
19
+ /**
20
+ * Transform the resource into an array.
21
+ *
22
+ * @param \Illuminate\Http\Request $request
23
+ */
24
+ public function toArray($request): array
25
+ {
26
+ $internal = Http::isInternalRequest();
27
+
28
+ return [
29
+ 'id' => $this->when($internal, $this->id, $this->public_id),
30
+ 'uuid' => $this->when($internal, $this->uuid),
31
+ 'public_id' => $this->when($internal, $this->public_id),
32
+ 'company_uuid' => $this->when($internal, $this->company_uuid),
33
+ 'connector_type' => $this->when($internal, Utils::toEmberResourceType($this->connector_type)),
34
+ 'connector_uuid' => $this->when($internal, $this->connector_uuid),
35
+ 'connected_type' => $this->when($internal, Utils::toEmberResourceType($this->connected_type)),
36
+ 'connected_uuid' => $this->when($internal, $this->connected_uuid),
37
+ 'relationship_type' => $this->relationship_type,
38
+ 'position' => $this->position,
39
+ // Identifiers are always present so a connection can be followed back to both
40
+ // sides even when the related records were not eager loaded.
41
+ 'vehicle_id' => $this->publicIdForRelation('vehicle', 'connector_uuid'),
42
+ 'trailer_id' => $this->publicIdForRelation('trailer', 'connected_uuid'),
43
+ 'vehicle' => $this->whenLoaded('vehicle', fn () => $this->vehicleRepresentation($internal)),
44
+ 'trailer' => $this->whenLoaded('trailer', fn () => $this->trailerRepresentation($internal)),
45
+ 'connected_at' => $this->connected_at,
46
+ 'disconnected_at' => $this->disconnected_at,
47
+ 'active' => $this->disconnected_at === null,
48
+ 'source' => $this->source,
49
+ 'confidence' => $this->confidence,
50
+ 'notes' => $this->notes,
51
+ 'meta' => data_get($this, 'meta', Utils::createObject()),
52
+ 'created_at' => $this->created_at,
53
+ 'updated_at' => $this->updated_at,
54
+ ];
55
+ }
56
+
57
+ /**
58
+ * Only invoked through whenLoaded(), which already yields null for an absent record.
59
+ */
60
+ protected function vehicleRepresentation(bool $internal): mixed
61
+ {
62
+ $vehicle = $this->vehicle;
63
+
64
+ if ($internal) {
65
+ return Resolve::httpResourceForModel($vehicle);
66
+ }
67
+
68
+ return [
69
+ 'id' => $vehicle->public_id,
70
+ 'name' => $vehicle->display_name,
71
+ 'plate_number' => $vehicle->plate_number,
72
+ ];
73
+ }
74
+
75
+ protected function trailerRepresentation(bool $internal): mixed
76
+ {
77
+ $trailer = $this->trailer;
78
+
79
+ if ($internal) {
80
+ return Resolve::httpResourceForModel($trailer);
81
+ }
82
+
83
+ return [
84
+ 'id' => $trailer->public_id,
85
+ 'name' => $trailer->display_name,
86
+ 'type' => $trailer->type,
87
+ 'plate_number' => $trailer->plate_number,
88
+ ];
89
+ }
90
+ }
@@ -18,7 +18,9 @@ class Device extends FleetbaseResource
18
18
  'company_uuid' => $this->when(Http::isInternalRequest(), $this->company_uuid),
19
19
  'telematic_uuid' => $this->when(Http::isInternalRequest(), $this->telematic_uuid),
20
20
  'attachable_uuid' => $this->when(Http::isInternalRequest(), $this->attachable_uuid),
21
- 'attachable_type' => $this->when(Http::isInternalRequest(), $this->attachable_type ? Utils::toEmberResourceType($this->attachable_type) : null),
21
+ 'attachable_type' => $this->attachable_type ? Utils::toEmberResourceType($this->attachable_type) : null,
22
+ 'attachable_id' => $this->whenLoaded('attachable', fn () => $this->attachable?->public_id),
23
+ 'attachable_name' => $this->whenLoaded('attachable', fn () => $this->attachable?->display_name ?? $this->attachable?->name),
22
24
  'warranty_uuid' => $this->when(Http::isInternalRequest(), $this->warranty_uuid),
23
25
  'photo_uuid' => $this->when(Http::isInternalRequest(), $this->photo_uuid),
24
26
  'telematic' => $this->whenLoaded('telematic', fn () => $this->resolveLoadedRelation($this->telematic)),
@@ -19,7 +19,9 @@ class Equipment extends FleetbaseResource
19
19
  'warranty_uuid' => $this->when(Http::isInternalRequest(), $this->warranty_uuid),
20
20
  'photo_uuid' => $this->when(Http::isInternalRequest(), $this->photo_uuid),
21
21
  'equipable_uuid' => $this->when(Http::isInternalRequest(), $this->equipable_uuid),
22
- 'equipable_type' => $this->when(Http::isInternalRequest(), $this->equipable_type ? Utils::toEmberResourceType($this->equipable_type) : null),
22
+ 'equipable_type' => $this->equipable_type ? Utils::toEmberResourceType($this->equipable_type) : null,
23
+ 'equipable_id' => $this->whenLoaded('equipable', fn () => $this->equipable?->public_id),
24
+ 'equipable_name' => $this->whenLoaded('equipable', fn () => $this->equipable?->display_name ?? $this->equipable?->name),
23
25
  'warranty' => $this->whenLoaded('warranty', fn () => $this->resolveLoadedRelation($this->warranty)),
24
26
  'photo' => $this->whenLoaded('photo', fn () => $this->resolveLoadedRelation($this->photo)),
25
27
  'equipable' => $this->whenLoaded('equipable', fn () => $this->resolveLoadedRelation($this->equipable)),
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\FleetOps\Http\Resources\v1\Index;
4
4
 
5
5
  use Fleetbase\FleetOps\Models\Order;
6
+ use Fleetbase\FleetOps\Support\TrailerSummary;
6
7
  use Fleetbase\FleetOps\Support\Utils;
7
8
  use Fleetbase\Http\Resources\FleetbaseResource;
8
9
  use Fleetbase\Support\Http;
@@ -47,6 +48,7 @@ class Vehicle extends FleetbaseResource
47
48
  'speed' => (int) data_get($this, 'speed', 0),
48
49
  'online' => (bool) data_get($this, 'online', false),
49
50
  'devices' => $this->whenLoaded('devices', fn () => $this->compactDevices()),
51
+ 'trailers' => $this->whenLoaded('currentTrailers', fn () => $this->compactTrailers()),
50
52
  'assigned_orders_count' => $this->when($isInternal, $this->assignedOrdersCount()),
51
53
 
52
54
  // Meta flag to indicate this is an index resource
@@ -84,6 +86,14 @@ class Vehicle extends FleetbaseResource
84
86
  )->values()->all();
85
87
  }
86
88
 
89
+ /**
90
+ * Trailers currently coupled to the vehicle, compacted for map popovers.
91
+ */
92
+ protected function compactTrailers(): array
93
+ {
94
+ return TrailerSummary::collection($this->currentTrailers);
95
+ }
96
+
87
97
  protected function currentOrderReference(): ?string
88
98
  {
89
99
  $this->loadMissing('driver.currentOrder');