@fleetbase/fleetops-engine 0.6.43 → 0.6.45

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 (82) hide show
  1. package/addon/components/entity/form.hbs +24 -0
  2. package/addon/components/entity/form.js +30 -0
  3. package/addon/components/leaflet-draw-control.js +1 -1
  4. package/addon/components/map/drawer/geofence-event-listing.hbs +123 -0
  5. package/addon/components/map/drawer/geofence-event-listing.js +103 -0
  6. package/addon/components/map/drawer.js +1 -0
  7. package/addon/components/map/leaflet-live-map.js +83 -0
  8. package/addon/components/modals/orchestrator-import.js +2 -78
  9. package/addon/components/order/form/service-rate.hbs +1 -1
  10. package/addon/components/service-area/form.hbs +59 -0
  11. package/addon/components/service-rate/details.hbs +6 -6
  12. package/addon/components/service-rate/form.hbs +28 -15
  13. package/addon/components/zone/form.hbs +60 -1
  14. package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
  15. package/addon/controllers/operations/orders/index/details.js +27 -2
  16. package/addon/controllers/operations/service-rates/index/edit.js +1 -0
  17. package/addon/controllers/operations/service-rates/index/new.js +1 -0
  18. package/addon/helpers/f-to-int.js +6 -0
  19. package/addon/routes/application.js +3 -3
  20. package/addon/routes/operations/orders/index/details/virtual.js +1 -0
  21. package/addon/routes/operations/orders/index/details.js +19 -3
  22. package/addon/services/equipment-actions.js +4 -6
  23. package/addon/services/geofence-event-bus.js +223 -0
  24. package/addon/services/leaflet-map-manager.js +4 -0
  25. package/addon/services/maintenance-actions.js +4 -6
  26. package/addon/services/part-actions.js +4 -6
  27. package/addon/services/service-rate-actions.js +8 -3
  28. package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
  29. package/addon/utils/fleet-ops-options.js +33 -0
  30. package/addon/utils/serialize-payload.js +1 -1
  31. package/app/components/map/drawer/geofence-event-listing.js +1 -0
  32. package/app/controllers/operations/orders/index/details/virtual.js +1 -0
  33. package/app/helpers/f-to-int.js +1 -0
  34. package/app/services/geofence-event-bus.js +1 -0
  35. package/composer.json +1 -1
  36. package/extension.json +1 -1
  37. package/package.json +2 -2
  38. package/server/migrations/2026_04_05_000001_add_geofence_config_to_zones_table.php +48 -0
  39. package/server/migrations/2026_04_05_000002_add_geofence_config_to_service_areas_table.php +38 -0
  40. package/server/migrations/2026_04_05_000003_create_driver_geofence_states_table.php +62 -0
  41. package/server/migrations/2026_04_05_000004_create_geofence_events_log_table.php +66 -0
  42. package/server/migrations/2026_04_22_000001_add_vehicle_support_to_geofence_events_log.php +31 -0
  43. package/server/migrations/2026_04_22_000002_create_vehicle_geofence_states_table.php +39 -0
  44. package/server/src/Console/Commands/SimulateGeofenceEvents.php +278 -0
  45. package/server/src/Events/GeofenceDwelled.php +208 -0
  46. package/server/src/Events/GeofenceEntered.php +207 -0
  47. package/server/src/Events/GeofenceExited.php +213 -0
  48. package/server/src/Http/Controllers/Api/v1/DriverController.php +100 -0
  49. package/server/src/Http/Controllers/Api/v1/GeofenceController.php +263 -0
  50. package/server/src/Http/Controllers/Api/v1/OrderController.php +13 -8
  51. package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
  52. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  53. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +4 -0
  54. package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
  55. package/server/src/Http/Controllers/Api/v1/VehicleController.php +88 -0
  56. package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -2
  57. package/server/src/Http/Controllers/Internal/v1/GeofenceController.php +7 -0
  58. package/server/src/Http/Requests/CreateServiceAreaRequest.php +8 -4
  59. package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
  60. package/server/src/Http/Requests/CreateZoneRequest.php +8 -4
  61. package/server/src/Http/Resources/v1/Order.php +0 -1
  62. package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
  63. package/server/src/Jobs/CheckGeofenceDwell.php +139 -0
  64. package/server/src/Listeners/HandleGeofenceDwelled.php +56 -0
  65. package/server/src/Listeners/HandleGeofenceEntered.php +187 -0
  66. package/server/src/Listeners/HandleGeofenceExited.php +56 -0
  67. package/server/src/Models/Entity.php +7 -0
  68. package/server/src/Models/GeofenceEventLog.php +155 -0
  69. package/server/src/Models/Order.php +39 -7
  70. package/server/src/Models/ServiceArea.php +21 -2
  71. package/server/src/Models/ServiceQuote.php +3 -1
  72. package/server/src/Models/ServiceRate.php +122 -30
  73. package/server/src/Models/Zone.php +20 -2
  74. package/server/src/Notifications/DriverArrivedAtGeofence.php +86 -0
  75. package/server/src/Observers/PurchaseRateObserver.php +3 -5
  76. package/server/src/Providers/EventServiceProvider.php +19 -0
  77. package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
  78. package/server/src/Rules/ComputableAlgo.php +1 -3
  79. package/server/src/Support/Algo.php +87 -1
  80. package/server/src/Support/GeofenceIntersectionService.php +206 -0
  81. package/server/src/Support/Utils.php +35 -0
  82. package/server/src/routes.php +16 -0
@@ -123,6 +123,38 @@ export const contactTypes = [
123
123
  { label: 'Dispatcher', value: 'dispatcher', description: 'Scheduling and operations contact' },
124
124
  ];
125
125
 
126
+ export const entityTypes = [
127
+ { label: 'Parcel', value: 'parcel', description: 'Standard parcel or package used by parcel-based pricing.' },
128
+ { label: 'Package', value: 'package', description: 'Generic package or boxed item.' },
129
+ { label: 'Product', value: 'product', description: 'Sellable product or catalog item.' },
130
+ { label: 'Item', value: 'item', description: 'Generic inventory item when no stronger type applies.' },
131
+ { label: 'SKU / Inventory Unit', value: 'sku', description: 'Stock keeping unit or tracked inventory piece.' },
132
+ { label: 'Document', value: 'document', description: 'Envelopes, paperwork, and lightweight documents.' },
133
+ { label: 'Envelope', value: 'envelope', description: 'Flat document mailer or envelope.' },
134
+ { label: 'Box', value: 'box', description: 'Single boxed shipment unit.' },
135
+ { label: 'Carton', value: 'carton', description: 'Single carton, box, or case.' },
136
+ { label: 'Case', value: 'case', description: 'Case-packed goods or packaged case.' },
137
+ { label: 'Bundle', value: 'bundle', description: 'Bundled or strapped items shipped together.' },
138
+ { label: 'Bag', value: 'bag', description: 'Bagged goods, sacks, or pouches.' },
139
+ { label: 'Tote', value: 'tote', description: 'Reusable tote, bin, or container.' },
140
+ { label: 'Crate', value: 'crate', description: 'Rigid crate or caged shipment unit.' },
141
+ { label: 'Pallet', value: 'pallet', description: 'Palletized freight or grouped cargo.' },
142
+ { label: 'Container', value: 'container', description: 'Containerized shipment or storage container.' },
143
+ { label: 'Freight', value: 'freight', description: 'Loose freight or larger shipment unit.' },
144
+ { label: 'Cargo', value: 'cargo', description: 'General cargo not classified more specifically.' },
145
+ { label: 'Equipment', value: 'equipment', description: 'Tools, machines, or operational equipment.' },
146
+ { label: 'Machinery', value: 'machinery', description: 'Industrial machinery or large mechanical unit.' },
147
+ { label: 'Asset', value: 'asset', description: 'Tracked business asset or returnable item.' },
148
+ { label: 'Return', value: 'return', description: 'Returned goods or reverse-logistics item.' },
149
+ { label: 'Sample', value: 'sample', description: 'Sample item for trial, demo, or inspection.' },
150
+ { label: 'Spare Part', value: 'spare_part', description: 'Replacement component or service part.' },
151
+ { label: 'Medical Supply', value: 'medical_supply', description: 'Healthcare, lab, or medical delivery item.' },
152
+ { label: 'Food', value: 'food', description: 'Prepared food, meal, or grocery item.' },
153
+ { label: 'Beverage', value: 'beverage', description: 'Drink, liquid refreshment, or bottled shipment.' },
154
+ { label: 'Retail Goods', value: 'retail_goods', description: 'General consumer or retail merchandise.' },
155
+ { label: 'Passenger', value: 'passenger', description: 'Person transported as part of the order payload.' },
156
+ ];
157
+
126
158
  export const contactStatuses = [
127
159
  { label: 'Active', value: 'active', description: 'Contact is valid and in use' },
128
160
  { label: 'Inactive', value: 'inactive', description: 'Contact is no longer valid' },
@@ -821,6 +853,7 @@ export default function fleetOpsOptions(key) {
821
853
  fleetTypes,
822
854
  fleetStatuses,
823
855
  contactTypes,
856
+ entityTypes,
824
857
  contactStatuses,
825
858
  fuelReportTypes,
826
859
  fuelReportStatuses,
@@ -5,7 +5,7 @@ export default function serializePayload(payload) {
5
5
  const serialized = {
6
6
  pickup: serializeModel(payload.pickup),
7
7
  dropoff: serializeModel(payload.dropoff),
8
- entitities: serializeArray(payload.entities),
8
+ entities: serializeArray(payload.entities),
9
9
  waypoints: serializeArray(payload.waypoints),
10
10
  };
11
11
 
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/components/map/drawer/geofence-event-listing';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/controllers/operations/orders/index/details/virtual';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/helpers/f-to-int';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/services/geofence-event-bus';
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.43",
3
+ "version": "0.6.45",
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.43",
3
+ "version": "0.6.45",
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.43",
3
+ "version": "0.6.45",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -44,7 +44,7 @@
44
44
  "@babel/core": "^7.23.2",
45
45
  "@fleetbase/ember-core": "^0.3.18",
46
46
  "@fleetbase/ember-ui": "^0.3.26",
47
- "@fleetbase/fleetops-data": "^0.1.28",
47
+ "@fleetbase/fleetops-data": "^0.1.30",
48
48
  "@fleetbase/leaflet-routing-machine": "^3.2.17",
49
49
  "@fortawesome/ember-fontawesome": "^2.0.0",
50
50
  "@fortawesome/fontawesome-svg-core": "6.4.0",
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ /**
9
+ * Run the migrations.
10
+ *
11
+ * Adds geofence trigger configuration columns to the zones table,
12
+ * enabling each zone to be configured as an active geofence with
13
+ * entry/exit/dwell event triggers.
14
+ */
15
+ public function up(): void
16
+ {
17
+ Schema::table('zones', function (Blueprint $table) {
18
+ // Whether to fire a GeofenceEntered event when a driver enters this zone
19
+ $table->boolean('trigger_on_entry')->default(true)->after('stroke_color');
20
+
21
+ // Whether to fire a GeofenceExited event when a driver exits this zone
22
+ $table->boolean('trigger_on_exit')->default(true)->after('trigger_on_entry');
23
+
24
+ // Minutes a driver must remain inside before a GeofenceDwelled event fires.
25
+ // NULL disables dwell tracking for this zone.
26
+ $table->unsignedSmallInteger('dwell_threshold_minutes')->nullable()->after('trigger_on_exit');
27
+
28
+ // Optional speed limit (km/h) enforced within this zone.
29
+ // NULL means no speed limit is configured.
30
+ $table->unsignedSmallInteger('speed_limit_kmh')->nullable()->after('dwell_threshold_minutes');
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Reverse the migrations.
36
+ */
37
+ public function down(): void
38
+ {
39
+ Schema::table('zones', function (Blueprint $table) {
40
+ $table->dropColumn([
41
+ 'trigger_on_entry',
42
+ 'trigger_on_exit',
43
+ 'dwell_threshold_minutes',
44
+ 'speed_limit_kmh',
45
+ ]);
46
+ });
47
+ }
48
+ };
@@ -0,0 +1,38 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ /**
9
+ * Run the migrations.
10
+ *
11
+ * Adds geofence trigger configuration columns to the service_areas table,
12
+ * enabling each service area to act as an active geofence boundary.
13
+ */
14
+ public function up(): void
15
+ {
16
+ Schema::table('service_areas', function (Blueprint $table) {
17
+ $table->boolean('trigger_on_entry')->default(true)->after('stroke_color');
18
+ $table->boolean('trigger_on_exit')->default(true)->after('trigger_on_entry');
19
+ $table->unsignedSmallInteger('dwell_threshold_minutes')->nullable()->after('trigger_on_exit');
20
+ $table->unsignedSmallInteger('speed_limit_kmh')->nullable()->after('dwell_threshold_minutes');
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Reverse the migrations.
26
+ */
27
+ public function down(): void
28
+ {
29
+ Schema::table('service_areas', function (Blueprint $table) {
30
+ $table->dropColumn([
31
+ 'trigger_on_entry',
32
+ 'trigger_on_exit',
33
+ 'dwell_threshold_minutes',
34
+ 'speed_limit_kmh',
35
+ ]);
36
+ });
37
+ }
38
+ };
@@ -0,0 +1,62 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ /**
9
+ * Run the migrations.
10
+ *
11
+ * Creates the driver_geofence_states table which acts as the state machine
12
+ * store for the geofencing engine. It tracks whether each driver is currently
13
+ * inside each geofence, enabling the system to detect transitions (entry/exit)
14
+ * rather than just evaluating current position in isolation.
15
+ *
16
+ * One record exists per driver-geofence pair. Records are upserted on each
17
+ * location update where a crossing is detected.
18
+ */
19
+ public function up(): void
20
+ {
21
+ Schema::create('driver_geofence_states', function (Blueprint $table) {
22
+ $table->id();
23
+ $table->uuid('driver_uuid')->index();
24
+ $table->uuid('geofence_uuid')->index();
25
+
26
+ // Discriminator: 'zone' or 'service_area'
27
+ $table->string('geofence_type', 50)->default('zone');
28
+
29
+ // Whether the driver is currently inside this geofence
30
+ $table->boolean('is_inside')->default(false)->index();
31
+
32
+ // Timestamp of when the driver most recently entered this geofence
33
+ $table->timestamp('entered_at')->nullable();
34
+
35
+ // Timestamp of when the driver most recently exited this geofence
36
+ $table->timestamp('exited_at')->nullable();
37
+
38
+ // Queue job ID of the pending CheckGeofenceDwell job.
39
+ // Stored so it can be cancelled if the driver exits before the dwell threshold.
40
+ $table->string('dwell_job_id')->nullable();
41
+
42
+ $table->timestamps();
43
+
44
+ // Composite unique key: one state record per driver-geofence pair
45
+ $table->unique(['driver_uuid', 'geofence_uuid'], 'driver_geofence_unique');
46
+
47
+ // Cascade delete when the driver is deleted
48
+ $table->foreign('driver_uuid')
49
+ ->references('uuid')
50
+ ->on('drivers')
51
+ ->onDelete('cascade');
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Reverse the migrations.
57
+ */
58
+ public function down(): void
59
+ {
60
+ Schema::dropIfExists('driver_geofence_states');
61
+ }
62
+ };
@@ -0,0 +1,66 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ /**
9
+ * Run the migrations.
10
+ *
11
+ * Creates the geofence_events_log table which provides a persistent,
12
+ * queryable audit trail of all geofence events (entered, exited, dwelled).
13
+ * This table powers the reporting dashboard, dwell time analytics, and
14
+ * geofence activity history views.
15
+ */
16
+ public function up(): void
17
+ {
18
+ Schema::create('geofence_events_log', function (Blueprint $table) {
19
+ $table->id();
20
+ $table->uuid('uuid')->unique()->index();
21
+ $table->uuid('company_uuid')->index();
22
+ $table->uuid('driver_uuid')->index();
23
+ $table->uuid('vehicle_uuid')->nullable()->index();
24
+ $table->uuid('order_uuid')->nullable()->index();
25
+ $table->uuid('geofence_uuid')->index();
26
+
27
+ // Discriminator: 'zone' or 'service_area'
28
+ $table->string('geofence_type', 50)->default('zone');
29
+
30
+ // Snapshot of the geofence name at the time of the event
31
+ $table->string('geofence_name')->nullable();
32
+
33
+ // The type of geofence event
34
+ $table->enum('event_type', ['entered', 'exited', 'dwelled'])->index();
35
+
36
+ // Driver location at the time of the event
37
+ $table->decimal('latitude', 10, 7)->nullable();
38
+ $table->decimal('longitude', 10, 7)->nullable();
39
+
40
+ // Driver speed at the time of the event (km/h)
41
+ $table->decimal('speed_kmh', 8, 2)->nullable();
42
+
43
+ // Populated for 'exited' and 'dwelled' events
44
+ $table->unsignedInteger('dwell_duration_minutes')->nullable();
45
+
46
+ // The precise time the event occurred (not the DB insert time)
47
+ $table->timestamp('occurred_at')->index();
48
+
49
+ $table->timestamps();
50
+
51
+ // Composite indexes for common query patterns
52
+ $table->index(['company_uuid', 'occurred_at'], 'gel_company_occurred_idx');
53
+ $table->index(['geofence_uuid', 'occurred_at'], 'gel_geofence_occurred_idx');
54
+ $table->index(['driver_uuid', 'occurred_at'], 'gel_driver_occurred_idx');
55
+ $table->index(['order_uuid', 'event_type'], 'gel_order_event_idx');
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Reverse the migrations.
61
+ */
62
+ public function down(): void
63
+ {
64
+ Schema::dropIfExists('geofence_events_log');
65
+ }
66
+ };
@@ -0,0 +1,31 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ /**
9
+ * Run the migrations.
10
+ */
11
+ public function up(): void
12
+ {
13
+ Schema::table('geofence_events_log', function (Blueprint $table) {
14
+ $table->uuid('subject_uuid')->nullable()->after('order_uuid')->index();
15
+ $table->string('subject_type', 50)->default('driver')->after('subject_uuid')->index();
16
+ $table->string('subject_name')->nullable()->after('subject_type');
17
+ $table->uuid('driver_uuid')->nullable()->change();
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Reverse the migrations.
23
+ */
24
+ public function down(): void
25
+ {
26
+ Schema::table('geofence_events_log', function (Blueprint $table) {
27
+ $table->dropColumn(['subject_uuid', 'subject_type', 'subject_name']);
28
+ $table->uuid('driver_uuid')->nullable(false)->change();
29
+ });
30
+ }
31
+ };
@@ -0,0 +1,39 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ /**
9
+ * Run the migrations.
10
+ */
11
+ public function up(): void
12
+ {
13
+ Schema::create('vehicle_geofence_states', function (Blueprint $table) {
14
+ $table->id();
15
+ $table->uuid('vehicle_uuid')->index();
16
+ $table->uuid('geofence_uuid')->index();
17
+ $table->string('geofence_type', 50)->default('zone');
18
+ $table->boolean('is_inside')->default(false)->index();
19
+ $table->timestamp('entered_at')->nullable();
20
+ $table->timestamp('exited_at')->nullable();
21
+ $table->string('dwell_job_id')->nullable();
22
+ $table->timestamps();
23
+
24
+ $table->unique(['vehicle_uuid', 'geofence_uuid'], 'vehicle_geofence_unique');
25
+ $table->foreign('vehicle_uuid')
26
+ ->references('uuid')
27
+ ->on('vehicles')
28
+ ->onDelete('cascade');
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Reverse the migrations.
34
+ */
35
+ public function down(): void
36
+ {
37
+ Schema::dropIfExists('vehicle_geofence_states');
38
+ }
39
+ };