@fleetbase/fleetops-engine 0.6.55 → 0.6.57

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 (161) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/ai/create-order-preview.hbs +297 -0
  3. package/addon/components/ai/create-order-preview.js +362 -0
  4. package/addon/components/ai/route-preview-map.hbs +79 -0
  5. package/addon/components/ai/route-preview-map.js +527 -0
  6. package/addon/components/device/details.js +1 -1
  7. package/addon/components/device-event/details.hbs +192 -0
  8. package/addon/components/device-event/details.js +151 -0
  9. package/addon/components/driver/schedule.js +1 -1
  10. package/addon/components/fleet/details.hbs +21 -8
  11. package/addon/components/fleet/details.js +55 -1
  12. package/addon/components/fleet/hierarchy-tree.hbs +137 -0
  13. package/addon/components/fleet/hierarchy-tree.js +528 -0
  14. package/addon/components/layout/fleet-ops-sidebar.js +15 -9
  15. package/addon/components/map/drawer/device-event-listing.js +1 -0
  16. package/addon/components/map/drawer/driver-listing.js +1 -0
  17. package/addon/components/map/drawer/place-listing.js +1 -0
  18. package/addon/components/map/drawer/vehicle-listing.js +1 -0
  19. package/addon/components/order/details.hbs +34 -1
  20. package/addon/components/order/form/details.hbs +4 -0
  21. package/addon/components/order/form.hbs +9 -1
  22. package/addon/components/telematic/details.hbs +1 -1
  23. package/addon/components/telematic/form.hbs +2 -2
  24. package/addon/components/telematic/settings.hbs +1 -1
  25. package/addon/components/vehicle/card.hbs +14 -2
  26. package/addon/components/vehicle/form.hbs +13 -1
  27. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  28. package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
  29. package/addon/controllers/connectivity/events/details.js +68 -1
  30. package/addon/controllers/connectivity/sensors/index.js +4 -0
  31. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  32. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  33. package/addon/controllers/settings/map.js +19 -0
  34. package/addon/extension.js +14 -0
  35. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  36. package/addon/routes/connectivity/events/details.js +26 -1
  37. package/addon/routes/management/fleets/index/details.js +1 -1
  38. package/addon/routes/management/vehicles/index/details.js +3 -1
  39. package/addon/services/map-adapter/google.js +66 -1
  40. package/addon/services/map-adapter/leaflet.js +1 -1
  41. package/addon/services/map-adapter-interface.js +1 -1
  42. package/addon/services/map-manager.js +16 -1
  43. package/addon/services/map-settings.js +15 -0
  44. package/addon/styles/fleetops-engine.css +487 -1
  45. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
  46. package/addon/templates/connectivity/events/details.hbs +40 -1
  47. package/addon/templates/settings/map.hbs +38 -9
  48. package/addon/utils/leaflet-plugin-loader.js +5 -5
  49. package/addon/utils/map-drawer-dropdown-position.js +11 -13
  50. package/addon/utils/route-colors.js +1 -1
  51. package/app/components/ai/create-order-preview.js +1 -0
  52. package/app/components/ai/route-preview-map.js +1 -0
  53. package/app/components/device-event/details.hbs +192 -0
  54. package/app/components/device-event/details.js +1 -0
  55. package/app/components/fleet/hierarchy-tree.js +1 -0
  56. package/app/services/route-engine.js +1 -0
  57. package/composer.json +1 -1
  58. package/extension.json +1 -1
  59. package/package.json +6 -4
  60. package/scripts/normalize-rgba-alpha.js +90 -0
  61. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  62. package/server/src/Exports/ContactExport.php +12 -5
  63. package/server/src/Exports/DeviceExport.php +92 -0
  64. package/server/src/Exports/DriverExport.php +28 -3
  65. package/server/src/Exports/EquipmentExport.php +94 -0
  66. package/server/src/Exports/FleetExport.php +22 -7
  67. package/server/src/Exports/FuelReportExport.php +16 -1
  68. package/server/src/Exports/IssueExport.php +25 -3
  69. package/server/src/Exports/MaintenanceExport.php +106 -0
  70. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  71. package/server/src/Exports/OrderExport.php +24 -5
  72. package/server/src/Exports/PartExport.php +99 -0
  73. package/server/src/Exports/PlaceExport.php +33 -7
  74. package/server/src/Exports/SensorExport.php +94 -0
  75. package/server/src/Exports/ServiceAreaExport.php +31 -7
  76. package/server/src/Exports/ServiceRateExport.php +41 -1
  77. package/server/src/Exports/TelematicExport.php +90 -0
  78. package/server/src/Exports/VehicleExport.php +274 -8
  79. package/server/src/Exports/VendorExport.php +15 -2
  80. package/server/src/Exports/WorkOrderExport.php +92 -0
  81. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  82. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  83. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  84. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  85. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  86. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  87. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  88. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
  89. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  90. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  91. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  92. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  93. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  94. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  95. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  96. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  97. package/server/src/Http/Filter/DeviceFilter.php +33 -10
  98. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  99. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  100. package/server/src/Http/Filter/PartFilter.php +47 -0
  101. package/server/src/Http/Filter/SensorFilter.php +34 -6
  102. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  103. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  104. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  105. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  106. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  107. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  108. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  109. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  110. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  111. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  112. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  113. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  114. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  115. package/server/src/Http/Resources/v1/Device.php +72 -0
  116. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  117. package/server/src/Http/Resources/v1/Fleet.php +8 -2
  118. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  119. package/server/src/Http/Resources/v1/Part.php +64 -0
  120. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  121. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  122. package/server/src/Models/Driver.php +6 -0
  123. package/server/src/Models/Order.php +20 -7
  124. package/server/src/Models/Place.php +11 -3
  125. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  126. package/server/src/Providers/FleetOpsServiceProvider.php +26 -0
  127. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +88 -0
  128. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +108 -0
  129. package/server/src/Support/Ai/Capabilities/ConsoleNavigationCapability.php +55 -0
  130. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +508 -0
  131. package/server/src/Support/Ai/Capabilities/DocsHelpCapability.php +49 -0
  132. package/server/src/Support/Ai/Capabilities/ImportOrdersPreviewCapability.php +66 -0
  133. package/server/src/Support/Ai/Capabilities/OperationalQueryCapability.php +319 -0
  134. package/server/src/Support/Ai/Capabilities/OptimizeOrderRouteCapability.php +240 -0
  135. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +95 -0
  136. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +195 -0
  137. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +150 -0
  138. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  139. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  140. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  141. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  142. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
  143. package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
  144. package/server/src/Support/Telematics/TelematicService.php +28 -9
  145. package/server/src/Support/Utils.php +52 -0
  146. package/server/src/routes.php +67 -14
  147. package/server/tests/AiCreateOrderPreviewCapabilityTest.php +131 -0
  148. package/server/tests/AiOperationalQueryCapabilityTest.php +75 -0
  149. package/server/tests/ExportCoverageTest.php +79 -0
  150. package/server/tests/FleetHierarchyResourceTest.php +12 -0
  151. package/server/tests/MetricsRegistryTest.php +45 -5
  152. package/server/tests/OrderPayloadPlaceResolutionTest.php +53 -0
  153. package/server/tests/PointResolutionTest.php +63 -0
  154. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  155. package/server/tests/TelematicsHardeningTest.php +636 -1
  156. package/addon/components/table/cell/dropdown/action-item.hbs +0 -31
  157. package/addon/components/table/cell/dropdown/action-item.js +0 -62
  158. package/addon/components/table/cell/dropdown.hbs +0 -28
  159. package/addon/components/table/cell/dropdown.js +0 -91
  160. package/app/components/table/cell/dropdown/action-item.js +0 -1
  161. package/app/components/table/cell/dropdown.js +0 -1
@@ -79,7 +79,7 @@ function waitForLeafletGlobal({ timeoutMs = 8000 } = {}) {
79
79
 
80
80
  if (timeoutMs != null && Date.now() - startedAt >= timeoutMs) {
81
81
  clearInterval(interval);
82
- reject(new Error('[FleetOps Leaflet] Leaflet global is not available'));
82
+ reject(new Error('[Fleet-Ops Leaflet] Leaflet global is not available'));
83
83
  }
84
84
  }, 50);
85
85
  });
@@ -105,7 +105,7 @@ function appendStylesheet(href) {
105
105
 
106
106
  function loadScript(src, { timeoutMs = 8000, isReady = null } = {}) {
107
107
  if (typeof document === 'undefined') {
108
- return Promise.reject(new Error('[FleetOps Leaflet] document is not available'));
108
+ return Promise.reject(new Error('[Fleet-Ops Leaflet] document is not available'));
109
109
  }
110
110
 
111
111
  const existing = findAssetElement('script', 'src', src);
@@ -137,7 +137,7 @@ function loadScript(src, { timeoutMs = 8000, isReady = null } = {}) {
137
137
 
138
138
  const onError = () => {
139
139
  cleanup();
140
- reject(new Error(`[FleetOps Leaflet] Failed to load ${src}`));
140
+ reject(new Error(`[Fleet-Ops Leaflet] Failed to load ${src}`));
141
141
  };
142
142
 
143
143
  script.addEventListener('load', onLoad);
@@ -146,7 +146,7 @@ function loadScript(src, { timeoutMs = 8000, isReady = null } = {}) {
146
146
  if (timeoutMs != null) {
147
147
  timeoutId = setTimeout(() => {
148
148
  cleanup();
149
- reject(new Error(`[FleetOps Leaflet] Timed out loading ${src}`));
149
+ reject(new Error(`[Fleet-Ops Leaflet] Timed out loading ${src}`));
150
150
  }, timeoutMs);
151
151
  }
152
152
 
@@ -202,7 +202,7 @@ export default function ensureLeafletPluginsReady(options = {}) {
202
202
  .then(() => {
203
203
  const readyLeaflet = normalizeLeafletGlobal();
204
204
  if (!hasLeafletPluginsReady()) {
205
- throw new Error('[FleetOps Leaflet] Leaflet plugins loaded but required Draw/contextmenu APIs are missing');
205
+ throw new Error('[Fleet-Ops Leaflet] Leaflet plugins loaded but required Draw/contextmenu APIs are missing');
206
206
  }
207
207
 
208
208
  window.fleetopsLeafletPluginsLoaded = true;
@@ -1,17 +1,15 @@
1
1
  export default function calculateMapDrawerDropdownPosition(trigger, content) {
2
- const dropdownRoot = trigger?.closest?.('.ember-basic-dropdown');
3
2
  const drawerPanel = trigger?.closest?.('.next-drawer-panel');
4
3
 
5
- if (!trigger || !dropdownRoot) {
4
+ if (!trigger) {
6
5
  return { style: {} };
7
6
  }
8
7
 
9
8
  const triggerRect = trigger.getBoundingClientRect();
10
- const rootRect = dropdownRoot.getBoundingClientRect();
11
9
  const drawerRect = drawerPanel?.getBoundingClientRect?.();
12
10
  const contentRect = content?.getBoundingClientRect?.();
13
11
  const contentWidth = contentRect?.width || 224;
14
- // const contentHeight = contentRect?.height || 240;
12
+ const contentHeight = contentRect?.height || 240;
15
13
  const gap = 6;
16
14
 
17
15
  const viewportRect = {
@@ -21,21 +19,21 @@ export default function calculateMapDrawerDropdownPosition(trigger, content) {
21
19
  left: 0,
22
20
  };
23
21
  const boundaryRect = drawerRect ?? viewportRect;
24
- const minLeft = boundaryRect.left - rootRect.left + gap;
25
- const maxLeft = boundaryRect.right - rootRect.left - contentWidth - gap;
26
- // const minTop = boundaryRect.top - rootRect.top + gap;
27
- // const maxTop = boundaryRect.bottom - rootRect.top - contentHeight - gap;
22
+ const minLeft = boundaryRect.left + gap;
23
+ const maxLeft = boundaryRect.right - contentWidth - gap;
24
+ const minTop = boundaryRect.top;
25
+ const maxTop = boundaryRect.bottom - contentHeight + 8;
28
26
 
29
- const preferredLeft = triggerRect.left - rootRect.left - contentWidth - gap;
30
- // const preferredTop = triggerRect.top - rootRect.top;
27
+ const preferredLeft = triggerRect.left - contentWidth - gap;
28
+ const preferredTop = triggerRect.top;
31
29
  const left = clamp(preferredLeft, minLeft, Math.max(minLeft, maxLeft));
32
- const top = 0; //clamp(preferredTop, minTop, Math.max(minTop, maxTop));
30
+ const top = clamp(preferredTop, minTop, Math.max(minTop, maxTop));
33
31
 
34
32
  return {
35
33
  style: {
36
34
  position: 'absolute',
37
- left: `${left}px`,
38
- top: `${top}px`,
35
+ left,
36
+ top,
39
37
  marginTop: '0px',
40
38
  zIndex: '10000',
41
39
  },
@@ -2,7 +2,7 @@
2
2
  * Route Colors Utility
3
3
  *
4
4
  * Provides deterministic color assignment, color manipulation helpers,
5
- * and status-based route line styling for the FleetOps map visualization.
5
+ * and status-based route line styling for the Fleet-Ops map visualization.
6
6
  */
7
7
 
8
8
  /**
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/ai/create-order-preview';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/ai/route-preview-map';
@@ -0,0 +1,192 @@
1
+ <div class="flex flex-col gap-3 p-3" ...attributes>
2
+ <section class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
3
+ <div class="flex flex-col gap-3 p-3 lg:flex-row lg:items-start lg:justify-between">
4
+ <div class="min-w-0">
5
+ <div class="flex flex-wrap items-center gap-2">
6
+ <h2 class="truncate text-base font-bold text-gray-900 dark:text-gray-100">{{smart-humanize this.eventType}}</h2>
7
+ <Badge @status={{this.severity}} @hideStatusDot={{true}}>{{smart-humanize this.severity}}</Badge>
8
+ <Badge @status={{this.processedStatus}} @hideStatusDot={{true}}>{{this.processedLabel}}</Badge>
9
+ </div>
10
+
11
+ <p class="mt-2 max-w-4xl text-sm leading-5 text-gray-700 dark:text-gray-200">{{n-a this.message}}</p>
12
+
13
+ <div class="mt-2 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-300">
14
+ <span>Occurred: {{n-a (format-date-fns this.occurredAt "dd MMM yyyy, HH:mm")}}</span>
15
+ <span>Processed: {{n-a (format-date-fns this.processedAt "dd MMM yyyy, HH:mm")}}</span>
16
+ <span>Public ID: {{n-a @resource.public_id}}</span>
17
+ <span>UUID: {{n-a @resource.id}}</span>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </section>
22
+
23
+ <section class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-4">
24
+ {{#each this.metrics as |metric|}}
25
+ <div class="fleet-ops-widget fleet-ops-kpi-tile fleetops-connectivity-kpi-tile {{metric.accentClass}} relative min-h-[82px] overflow-hidden rounded-md border bg-white shadow-sm dark:bg-gray-800">
26
+ <div class="flex h-full items-start justify-between gap-2 p-3">
27
+ <div class="min-w-0">
28
+ <div class="truncate text-[10px] font-bold uppercase text-gray-500 dark:text-gray-400">{{metric.label}}</div>
29
+ <div class="mt-1 truncate text-lg font-extrabold leading-none text-gray-900 dark:text-gray-50">{{n-a metric.value}}</div>
30
+ {{#if metric.meta}}
31
+ <div class="mt-1 truncate text-xs text-gray-500 dark:text-gray-300">{{metric.meta}}</div>
32
+ {{/if}}
33
+ </div>
34
+ <div class="fleetops-connectivity-kpi-icon flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md text-[13px]">
35
+ <FaIcon @icon={{metric.icon}} />
36
+ </div>
37
+ </div>
38
+ </div>
39
+ {{/each}}
40
+ </section>
41
+
42
+ <section class="grid grid-cols-1 gap-3 xl:grid-cols-2">
43
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
44
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
45
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Event Details</h3>
46
+ </div>
47
+ <div class="grid grid-cols-1 gap-2 p-3 sm:grid-cols-2 lg:grid-cols-3">
48
+ <div class="field-info-container">
49
+ <div class="field-name">Event Type</div>
50
+ <div class="field-value">{{n-a (smart-humanize this.eventType)}}</div>
51
+ </div>
52
+ <div class="field-info-container">
53
+ <div class="field-name">Severity</div>
54
+ <div class="field-value"><Badge @status={{this.severity}}>{{smart-humanize this.severity}}</Badge></div>
55
+ </div>
56
+ <div class="field-info-container">
57
+ <div class="field-name">State</div>
58
+ <div class="field-value">{{n-a (smart-humanize @resource.state)}}</div>
59
+ </div>
60
+ <div class="field-info-container">
61
+ <div class="field-name">Code</div>
62
+ <div class="field-value">{{n-a @resource.code}}</div>
63
+ </div>
64
+ <div class="field-info-container">
65
+ <div class="field-name">Reason</div>
66
+ <div class="field-value">{{n-a @resource.reason}}</div>
67
+ </div>
68
+ <div class="field-info-container">
69
+ <div class="field-name">Mileage</div>
70
+ <div class="field-value">{{n-a @resource.mileage}}</div>
71
+ </div>
72
+ <div class="field-info-container">
73
+ <div class="field-name">Age</div>
74
+ <div class="field-value">{{#if this.ageMinutes}}{{this.ageMinutes}} minutes{{else}}{{n-a null}}{{/if}}</div>
75
+ </div>
76
+ <div class="field-info-container">
77
+ <div class="field-name">Processing Delay</div>
78
+ <div class="field-value">{{#if this.processingDelay}}{{this.processingDelay}} minutes{{else}}{{n-a null}}{{/if}}</div>
79
+ </div>
80
+ <div class="field-info-container sm:col-span-2 lg:col-span-3">
81
+ <div class="field-name">Comment</div>
82
+ <div class="field-value whitespace-pre-line">{{n-a @resource.comment}}</div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
88
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
89
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Device & Provider</h3>
90
+ </div>
91
+ <div class="grid grid-cols-1 gap-2 p-3 sm:grid-cols-2 lg:grid-cols-3">
92
+ <div class="field-info-container">
93
+ <div class="field-name">Device</div>
94
+ <div class="field-value">
95
+ {{#if this.deviceRouteModel}}
96
+ <button
97
+ type="button"
98
+ class="text-blue-600 hover:text-blue-700 hover:underline dark:text-blue-300 dark:hover:text-blue-200"
99
+ data-test-device-event-device-link
100
+ {{on "click" this.openDevice}}
101
+ >
102
+ {{n-a this.deviceName}}
103
+ </button>
104
+ {{else}}
105
+ {{n-a this.deviceName}}
106
+ {{/if}}
107
+ </div>
108
+ </div>
109
+ <div class="field-info-container">
110
+ <div class="field-name">Device UUID</div>
111
+ <div class="field-value">{{n-a @resource.device_uuid}}</div>
112
+ </div>
113
+ <div class="field-info-container">
114
+ <div class="field-name">Device ID</div>
115
+ <div class="field-value">{{n-a @resource.device_id}}</div>
116
+ </div>
117
+ <div class="field-info-container">
118
+ <div class="field-name">IMEI</div>
119
+ <div class="field-value">{{n-a @resource.device_imei}}</div>
120
+ </div>
121
+ <div class="field-info-container">
122
+ <div class="field-name">Serial Number</div>
123
+ <div class="field-value">{{n-a @resource.device_serial_number}}</div>
124
+ </div>
125
+ <div class="field-info-container">
126
+ <div class="field-name">IDENT</div>
127
+ <div class="field-value">{{n-a @resource.ident}}</div>
128
+ </div>
129
+ <div class="field-info-container">
130
+ <div class="field-name">Provider</div>
131
+ <div class="field-value">
132
+ {{#if this.telematicRouteModel}}
133
+ <button
134
+ type="button"
135
+ class="text-blue-600 hover:text-blue-700 hover:underline dark:text-blue-300 dark:hover:text-blue-200"
136
+ data-test-device-event-telematic-link
137
+ {{on "click" this.openTelematic}}
138
+ >
139
+ {{n-a this.providerLabel}}
140
+ </button>
141
+ {{else}}
142
+ {{n-a this.providerLabel}}
143
+ {{/if}}
144
+ </div>
145
+ </div>
146
+ <div class="field-info-container">
147
+ <div class="field-name">Protocol</div>
148
+ <div class="field-value">{{n-a @resource.protocol}}</div>
149
+ </div>
150
+ <div class="field-info-container">
151
+ <div class="field-name">Telematic UUID</div>
152
+ <div class="field-value">{{n-a @resource.telematic_uuid}}</div>
153
+ </div>
154
+ <div class="field-info-container">
155
+ <div class="field-name">Device Status</div>
156
+ <div class="field-value" data-test-device-event-device-status>
157
+ {{#if this.deviceStatus}}
158
+ <Badge @status={{this.deviceStatus}}>{{smart-humanize this.deviceStatus}}</Badge>
159
+ {{else}}
160
+ {{n-a null}}
161
+ {{/if}}
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </section>
167
+
168
+ <section class="grid grid-cols-1 gap-3 xl:grid-cols-3">
169
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
170
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
171
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Payload</h3>
172
+ </div>
173
+ <pre class="max-h-80 overflow-auto bg-gray-900 p-3 text-[11px] leading-5 text-gray-100">{{n-a this.payloadJson}}</pre>
174
+ </div>
175
+
176
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
177
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
178
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Data</h3>
179
+ </div>
180
+ <pre class="max-h-80 overflow-auto bg-gray-900 p-3 text-[11px] leading-5 text-gray-100">{{n-a this.dataJson}}</pre>
181
+ </div>
182
+
183
+ <div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
184
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
185
+ <h3 class="text-sm font-bold text-gray-900 dark:text-gray-100">Meta</h3>
186
+ </div>
187
+ <pre class="max-h-80 overflow-auto bg-gray-900 p-3 text-[11px] leading-5 text-gray-100">{{n-a this.metaJson}}</pre>
188
+ </div>
189
+ </section>
190
+
191
+ <CustomField::Yield @subject={{@resource}} @viewMode={{true}} @wrapperClass="bordered-top" />
192
+ </div>
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/device-event/details';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/fleet/hierarchy-tree';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/services/route-engine';
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.55",
3
+ "version": "0.6.57",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Fleet-Ops",
3
- "version": "0.6.55",
3
+ "version": "0.6.57",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/fleetops",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/fleetops-engine",
3
- "version": "0.6.55",
3
+ "version": "0.6.57",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -25,7 +25,9 @@
25
25
  "scripts": {
26
26
  "build": "ember build --environment=production",
27
27
  "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
28
- "lint:css": "stylelint \"**/*.css\"",
28
+ "check:css-alpha": "node scripts/normalize-rgba-alpha.js --check",
29
+ "fix:css-alpha": "node scripts/normalize-rgba-alpha.js",
30
+ "lint:css": "npm run check:css-alpha && stylelint \"**/*.css\"",
29
31
  "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"",
30
32
  "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
31
33
  "lint:hbs": "ember-template-lint .",
@@ -42,8 +44,8 @@
42
44
  },
43
45
  "dependencies": {
44
46
  "@babel/core": "^7.23.2",
45
- "@fleetbase/ember-core": "^0.3.23",
46
- "@fleetbase/ember-ui": "^0.3.37",
47
+ "@fleetbase/ember-core": "^0.3.24",
48
+ "@fleetbase/ember-ui": "^0.3.39",
47
49
  "@fleetbase/fleetops-data": "^0.1.40",
48
50
  "@fleetbase/leaflet-routing-machine": "^3.2.17",
49
51
  "@fortawesome/ember-fontawesome": "^2.0.0",
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const root = process.cwd();
8
+ const checkOnly = process.argv.includes('--check');
9
+ const cssRoot = path.join(root, 'addon');
10
+ const rgbaPercentAlphaPattern = /rgba\(\s*([+-]?(?:\d*\.)?\d+)\s*,\s*([+-]?(?:\d*\.)?\d+)\s*,\s*([+-]?(?:\d*\.)?\d+)\s*,\s*([+-]?(?:\d*\.)?\d+)%\s*\)/g;
11
+
12
+ function collectCssFiles(dir, files = []) {
13
+ if (!fs.existsSync(dir)) {
14
+ return files;
15
+ }
16
+
17
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
18
+ const fullPath = path.join(dir, entry.name);
19
+
20
+ if (entry.isDirectory()) {
21
+ collectCssFiles(fullPath, files);
22
+ continue;
23
+ }
24
+
25
+ if (entry.isFile() && entry.name.endsWith('.css')) {
26
+ files.push(fullPath);
27
+ }
28
+ }
29
+
30
+ return files;
31
+ }
32
+
33
+ function formatAlpha(percent) {
34
+ const alpha = Number(percent) / 100;
35
+ return Number.isInteger(alpha) ? String(alpha) : String(Number(alpha.toFixed(4)));
36
+ }
37
+
38
+ function lineForIndex(source, index) {
39
+ return source.slice(0, index).split('\n').length;
40
+ }
41
+
42
+ const changes = [];
43
+
44
+ for (const filePath of collectCssFiles(cssRoot)) {
45
+ const source = fs.readFileSync(filePath, 'utf8');
46
+ const matches = [];
47
+
48
+ const next = source.replace(rgbaPercentAlphaPattern, (match, red, green, blue, alphaPercent, offset) => {
49
+ const replacement = `rgba(${red}, ${green}, ${blue}, ${formatAlpha(alphaPercent)})`;
50
+ matches.push({
51
+ line: lineForIndex(source, offset),
52
+ before: match,
53
+ after: replacement,
54
+ });
55
+ return replacement;
56
+ });
57
+
58
+ if (matches.length === 0) {
59
+ continue;
60
+ }
61
+
62
+ changes.push({
63
+ filePath,
64
+ matches,
65
+ });
66
+
67
+ if (!checkOnly) {
68
+ fs.writeFileSync(filePath, next);
69
+ }
70
+ }
71
+
72
+ if (changes.length === 0) {
73
+ console.log('No comma-form rgba() percent-alpha values found.');
74
+ process.exit(0);
75
+ }
76
+
77
+ for (const change of changes) {
78
+ const relativePath = path.relative(root, change.filePath);
79
+
80
+ for (const match of change.matches) {
81
+ console.log(`${relativePath}:${match.line} ${match.before} -> ${match.after}`);
82
+ }
83
+ }
84
+
85
+ if (checkOnly) {
86
+ console.error('\nRun `npm run fix:css-alpha` to normalize these values before production builds.');
87
+ process.exit(1);
88
+ }
89
+
90
+ console.log(`\nNormalized ${changes.reduce((total, change) => total + change.matches.length, 0)} rgba() alpha value(s).`);
@@ -0,0 +1,34 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Support\Facades\DB;
5
+
6
+ return new class extends Migration {
7
+ /**
8
+ * Delete malformed AFAQY telematics sensor rows created before sensor identities
9
+ * were normalized. AFAQY remains the source of truth and the next sync recreates
10
+ * one clean latest-state row per real provider sensor.
11
+ */
12
+ public function up(): void
13
+ {
14
+ DB::table('sensors')
15
+ ->whereIn('telematic_uuid', function ($query) {
16
+ $query->select('uuid')
17
+ ->from('telematics')
18
+ ->where('provider', 'afaqy');
19
+ })
20
+ ->delete();
21
+
22
+ DB::table('sensors')
23
+ ->whereRaw("LOWER(JSON_UNQUOTE(JSON_EXTRACT(meta, '$.provider'))) = ?", ['afaqy'])
24
+ ->delete();
25
+ }
26
+
27
+ /**
28
+ * This cleanup cannot restore deleted rows. Run an AFAQY sync to recreate
29
+ * current sensor state from the provider.
30
+ */
31
+ public function down(): void
32
+ {
33
+ }
34
+ };
@@ -25,10 +25,13 @@ class ContactExport implements FromCollection, WithHeadings, WithMapping, WithCo
25
25
  $contact->public_id,
26
26
  $contact->internal_id,
27
27
  $contact->name,
28
- $contact->address ? $contact->address->address : null,
28
+ $contact->title,
29
+ $contact->type,
30
+ $contact->address,
29
31
  $contact->email,
30
32
  $contact->phone,
31
33
  $contact->created_at,
34
+ $contact->updated_at,
32
35
  ];
33
36
  }
34
37
 
@@ -38,18 +41,22 @@ class ContactExport implements FromCollection, WithHeadings, WithMapping, WithCo
38
41
  'ID',
39
42
  'Internal ID',
40
43
  'Name',
44
+ 'Title',
45
+ 'Type',
41
46
  'Address',
42
47
  'Email',
43
48
  'Phone',
44
49
  'Date Created',
50
+ 'Date Updated',
45
51
  ];
46
52
  }
47
53
 
48
54
  public function columnFormats(): array
49
55
  {
50
56
  return [
51
- 'F' => '+#',
52
- 'G' => NumberFormat::FORMAT_DATE_DDMMYYYY,
57
+ 'H' => '+#',
58
+ 'I' => NumberFormat::FORMAT_DATE_DDMMYYYY,
59
+ 'J' => NumberFormat::FORMAT_DATE_DDMMYYYY,
53
60
  ];
54
61
  }
55
62
 
@@ -59,9 +66,9 @@ class ContactExport implements FromCollection, WithHeadings, WithMapping, WithCo
59
66
  public function collection()
60
67
  {
61
68
  if ($this->selections) {
62
- return Contact::where('company_uuid', session('company'))->whereIn('uuid', $this->selections)->get();
69
+ return Contact::where('company_uuid', session('company'))->whereIn('uuid', $this->selections)->with(['place'])->get();
63
70
  }
64
71
 
65
- return Contact::where('company_uuid', session('company'))->get();
72
+ return Contact::where('company_uuid', session('company'))->with(['place'])->get();
66
73
  }
67
74
  }
@@ -0,0 +1,92 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Exports;
4
+
5
+ use Fleetbase\FleetOps\Models\Device;
6
+ use Maatwebsite\Excel\Concerns\FromCollection;
7
+ use Maatwebsite\Excel\Concerns\ShouldAutoSize;
8
+ use Maatwebsite\Excel\Concerns\WithColumnFormatting;
9
+ use Maatwebsite\Excel\Concerns\WithHeadings;
10
+ use Maatwebsite\Excel\Concerns\WithMapping;
11
+ use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
12
+
13
+ class DeviceExport implements FromCollection, WithHeadings, WithMapping, WithColumnFormatting, ShouldAutoSize
14
+ {
15
+ protected array $selections = [];
16
+
17
+ public function __construct(array $selections = [])
18
+ {
19
+ $this->selections = $selections;
20
+ }
21
+
22
+ public function map($device): array
23
+ {
24
+ return [
25
+ $device->public_id,
26
+ $device->name,
27
+ $device->device_id,
28
+ $device->connection_status,
29
+ $device->attached_to_name,
30
+ $device->telematic_name,
31
+ $device->sensors_count,
32
+ $device->last_online_at,
33
+ $device->provider,
34
+ $device->type,
35
+ $device->serial_number,
36
+ $device->imei,
37
+ $device->status,
38
+ $device->attachable_uuid ? 'Attached' : 'Unattached',
39
+ $this->yesNo($device->online),
40
+ $device->created_at,
41
+ $device->updated_at,
42
+ ];
43
+ }
44
+
45
+ public function headings(): array
46
+ {
47
+ return [
48
+ 'ID',
49
+ 'Name',
50
+ 'Provider ID / IMEI',
51
+ 'Connection Status',
52
+ 'Attached To',
53
+ 'Telematic Provider',
54
+ 'Sensors Count',
55
+ 'Last Seen',
56
+ 'Provider',
57
+ 'Type',
58
+ 'Serial Number',
59
+ 'IMEI',
60
+ 'Status',
61
+ 'Attachment State',
62
+ 'Online',
63
+ 'Date Created',
64
+ 'Date Updated',
65
+ ];
66
+ }
67
+
68
+ public function columnFormats(): array
69
+ {
70
+ return [
71
+ 'H' => NumberFormat::FORMAT_DATE_DDMMYYYY,
72
+ 'P' => NumberFormat::FORMAT_DATE_DDMMYYYY,
73
+ 'Q' => NumberFormat::FORMAT_DATE_DDMMYYYY,
74
+ ];
75
+ }
76
+
77
+ public function collection()
78
+ {
79
+ $query = Device::where('company_uuid', session('company'))->with(['telematic', 'attachable'])->withCount('sensors');
80
+
81
+ if ($this->selections) {
82
+ $query->whereIn('uuid', $this->selections);
83
+ }
84
+
85
+ return $query->get();
86
+ }
87
+
88
+ protected function yesNo($value): string
89
+ {
90
+ return $value ? 'Yes' : 'No';
91
+ }
92
+ }