@fleetbase/fleetops-engine 0.6.25 → 0.6.26

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 (48) hide show
  1. package/DRIVER_SCHEDULING.md +186 -0
  2. package/addon/components/driver/schedule.hbs +100 -0
  3. package/addon/components/driver/schedule.js +267 -0
  4. package/addon/components/order/kanban-card.hbs +2 -2
  5. package/addon/components/vehicle/details.hbs +594 -4
  6. package/addon/components/vehicle/form.hbs +467 -41
  7. package/addon/controllers/analytics/reports/index.js +3 -2
  8. package/addon/controllers/connectivity/devices/index.js +3 -3
  9. package/addon/controllers/connectivity/events/index.js +3 -2
  10. package/addon/controllers/connectivity/sensors/index.js +3 -5
  11. package/addon/controllers/connectivity/telematics/index.js +3 -1
  12. package/addon/controllers/maintenance/equipment/index.js +4 -4
  13. package/addon/controllers/maintenance/parts/index.js +4 -4
  14. package/addon/controllers/maintenance/work-orders/index.js +4 -4
  15. package/addon/controllers/management/contacts/customers.js +12 -10
  16. package/addon/controllers/management/contacts/index.js +3 -10
  17. package/addon/controllers/management/drivers/index/details.js +26 -13
  18. package/addon/controllers/management/drivers/index.js +4 -16
  19. package/addon/controllers/management/fleets/index.js +3 -13
  20. package/addon/controllers/management/fuel-reports/index.js +3 -10
  21. package/addon/controllers/management/issues/index.js +3 -12
  22. package/addon/controllers/management/places/index.js +4 -12
  23. package/addon/controllers/management/vehicles/index.js +3 -13
  24. package/addon/controllers/management/vendors/index.js +3 -13
  25. package/addon/controllers/operations/orders/index.js +5 -22
  26. package/addon/controllers/operations/scheduler/index.js +195 -6
  27. package/addon/controllers/operations/service-rates/index.js +34 -34
  28. package/addon/controllers/settings/payments/index.js +0 -6
  29. package/addon/routes.js +1 -0
  30. package/addon/services/driver-scheduling.js +171 -0
  31. package/addon/services/leaflet-layer-visibility-manager.js +4 -1
  32. package/addon/services/service-rate-actions.js +5 -1
  33. package/addon/templates/management/drivers/index/details/positions.hbs +1 -2
  34. package/addon/templates/management/drivers/index/details/schedule.hbs +1 -2
  35. package/addon/templates/operations/scheduler/index.hbs +48 -10
  36. package/addon/utils/fleet-ops-options.js +86 -0
  37. package/app/services/driver-scheduling.js +1 -0
  38. package/composer.json +1 -1
  39. package/extension.json +1 -1
  40. package/package.json +3 -3
  41. package/server/migrations/2025_11_17_033648_add_additional_columns_to_vehicles_table.php +142 -0
  42. package/server/src/Constraints/HOSConstraint.php +244 -0
  43. package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
  44. package/server/src/Http/Controllers/Internal/v1/OrderController.php +8 -3
  45. package/server/src/Http/Resources/v1/Vehicle.php +197 -19
  46. package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +211 -28
  47. package/server/src/Models/Driver.php +12 -8
  48. package/server/src/Models/Vehicle.php +101 -15
@@ -1,4 +1,30 @@
1
- <Layout::Section::Header @title={{t "menu.scheduler"}} />
1
+ <Layout::Section::Header @title={{t "menu.scheduler"}}>
2
+ <div class="flex items-center gap-2">
3
+ <Button
4
+ @type={{if (eq this.viewMode "orders") "primary" "default"}}
5
+ @text="Orders"
6
+ @icon="box"
7
+ @onClick={{fn this.switchViewMode "orders"}}
8
+ @size="sm"
9
+ />
10
+ <Button
11
+ @type={{if (eq this.viewMode "drivers") "primary" "default"}}
12
+ @text="Driver Schedules"
13
+ @icon="id-card"
14
+ @onClick={{fn this.switchViewMode "drivers"}}
15
+ @size="sm"
16
+ />
17
+ {{#if (eq this.viewMode "drivers")}}
18
+ <Button
19
+ @type="success"
20
+ @text="Add Shift"
21
+ @icon="plus"
22
+ @onClick={{this.addDriverShift}}
23
+ @size="sm"
24
+ />
25
+ {{/if}}
26
+ </div>
27
+ </Layout::Section::Header>
2
28
 
3
29
  <Layout::Section::Body>
4
30
  <div id="fleet-ops-scheduler-container">
@@ -46,15 +72,27 @@
46
72
  </div>
47
73
  </div>
48
74
  {{/if}}
49
- <FullCalendar
50
- @events={{this.events}}
51
- @editable={{true}}
52
- @onDrop={{this.scheduleEventFromDrop}}
53
- @onEventReceive={{this.receivedEvent}}
54
- @onEventDrop={{this.rescheduleEventFromDrag}}
55
- @onEventClick={{this.viewOrderAsEvent}}
56
- @onInit={{this.setCalendarApi}}
57
- />
75
+ {{#if (eq this.viewMode "drivers")}}
76
+ <FullCalendar
77
+ @events={{this.events}}
78
+ @resources={{this.calendarResources}}
79
+ @editable={{true}}
80
+ @onEventDrop={{this.rescheduleEventFromDrag}}
81
+ @onEventClick={{this.viewOrderAsEvent}}
82
+ @onInit={{this.setCalendarApi}}
83
+ @schedulerLicenseKey="GPL-My-Project-Is-Open-Source"
84
+ />
85
+ {{else}}
86
+ <FullCalendar
87
+ @events={{this.events}}
88
+ @editable={{true}}
89
+ @onDrop={{this.scheduleEventFromDrop}}
90
+ @onEventReceive={{this.receivedEvent}}
91
+ @onEventDrop={{this.rescheduleEventFromDrag}}
92
+ @onEventClick={{this.viewOrderAsEvent}}
93
+ @onInit={{this.setCalendarApi}}
94
+ />
95
+ {{/if}}
58
96
  <Spacer @height="200px" />
59
97
  </div>
60
98
  </div>
@@ -492,6 +492,83 @@ export const sensorStatuses = [
492
492
  { label: 'Decommissioned', value: 'decommissioned', description: 'Sensor permanently retired, replaced, or removed from service.' },
493
493
  ];
494
494
 
495
+ export const measurementSystems = [
496
+ { label: 'Metric System', value: 'metric', description: 'Internationally adopted system based on meters, liters, and kilograms; used by most countries.' },
497
+ { label: 'Imperial System', value: 'imperial', description: 'Traditional British system using miles, gallons, and pounds; still widely used in the United States.' },
498
+ ];
499
+
500
+ export const fuelVolumeUnits = [
501
+ { label: 'Liters (L)', value: 'liters', description: 'Metric unit for measuring liquid fuel; standard in most of the world.' },
502
+ { label: 'Gallons (US gal)', value: 'gallons_us', description: 'US customary gallon, equal to 3.785 liters; commonly used in the United States.' },
503
+ { label: 'Gallons (Imperial gal)', value: 'gallons_imperial', description: 'UK Imperial gallon, equal to 4.546 liters; historically used in the UK and some Commonwealth countries.' },
504
+ ];
505
+
506
+ export const fuelTypes = [
507
+ { label: 'Petrol (Gasoline)', value: 'petrol', description: 'Traditional internal combustion engine fuel, widely available.' },
508
+ { label: 'Diesel', value: 'diesel', description: 'Fuel for compression ignition engines, offering higher efficiency and torque.' },
509
+ { label: 'Electric', value: 'electric', description: 'Battery-powered vehicles producing zero tailpipe emissions.' },
510
+ { label: 'Hybrid', value: 'hybrid', description: 'Combines petrol/diesel engine with an electric motor for better fuel economy.' },
511
+ { label: 'Liquefied Petroleum Gas (LPG)', value: 'lpg', description: 'Alternative fuel derived from propane or butane, lower emissions.' },
512
+ { label: 'Compressed Natural Gas (CNG)', value: 'cng', description: 'Fuel stored at high pressure, cleaner than petrol or diesel.' },
513
+ ];
514
+
515
+ export const vehicleUsageTypes = [
516
+ { label: 'Commercial', value: 'commercial', description: 'Used for business operations such as deliveries, services, or company activities.' },
517
+ { label: 'Personal', value: 'personal', description: 'Used by an individual for private, non-business purposes.' },
518
+ { label: 'Mixed', value: 'mixed', description: 'Used for both business and personal purposes.' },
519
+ { label: 'Rental', value: 'rental', description: 'Provided for short or long-term rental to third parties.' },
520
+ { label: 'Fleet', value: 'fleet', description: 'Part of a company-managed group of vehicles or assets for organizational use.' },
521
+ { label: 'Operational', value: 'operational', description: 'Vehicles actively in use for company operations or service delivery.' },
522
+ { label: 'Standby', value: 'standby', description: 'Assets kept on standby for future or emergency use.' },
523
+ { label: 'Under Maintenance', value: 'under_maintenance', description: 'Vehicles currently undergoing repair, inspection, or service.' },
524
+ { label: 'Decommissioned', value: 'decommissioned', description: 'Retired assets no longer part of active operations.' },
525
+ { label: 'In Transit', value: 'in_transit', description: 'Assets currently being transported between locations.' },
526
+ { label: 'On Loan', value: 'on_loan', description: 'Assets temporarily loaned to another department or client.' },
527
+ ];
528
+
529
+ export const vehicleOwnershipTypes = [
530
+ { label: 'Company Owned', value: 'company_owned', description: 'Vehicles fully owned and managed by the company.' },
531
+ { label: 'Leased', value: 'leased', description: 'Vehicles leased from a third-party vendor or lessor under contract.' },
532
+ { label: 'Rented', value: 'rented', description: 'Short-term rental assets used for temporary fleet expansion or projects.' },
533
+ { label: 'Financed', value: 'financed', description: 'Vehicles purchased through financing or loan agreements.' },
534
+ { label: 'Vendor Supplied', value: 'vendor_supplied', description: 'Assets provided and maintained by external vendors.' },
535
+ { label: 'Customer Owned', value: 'customer_owned', description: 'Assets owned by a customer but operated or tracked within the system.' },
536
+ ];
537
+
538
+ export const vehicleBodyTypes = [
539
+ { label: 'Sedan', value: 'sedan', description: 'Standard passenger car body with a fixed roof and trunk.' },
540
+ { label: 'SUV', value: 'suv', description: 'Sport utility vehicle designed for passenger and cargo versatility.' },
541
+ { label: 'Pickup Truck', value: 'pickup_truck', description: 'Truck with an open cargo area and enclosed cab.' },
542
+ { label: 'Van', value: 'van', description: 'Multi-purpose vehicle for transporting passengers or goods.' },
543
+ { label: 'Box Truck', value: 'box_truck', description: 'Enclosed cargo truck for logistics or delivery operations.' },
544
+ { label: 'Flatbed', value: 'flatbed', description: 'Truck with a flat, open platform for hauling oversized loads.' },
545
+ { label: 'Trailer', value: 'trailer', description: 'Unpowered vehicle towed for freight transport.' },
546
+ { label: 'Bus', value: 'bus', description: 'Passenger transport vehicle with multiple seating rows.' },
547
+ ];
548
+
549
+ export const vehicleBodySubTypes = [
550
+ { label: 'Refrigerated Truck', value: 'refrigerated_truck', description: 'Temperature-controlled truck for transporting perishable goods.' },
551
+ { label: 'Tanker', value: 'tanker', description: 'Vehicle designed for transporting liquids such as fuel or water.' },
552
+ { label: 'Tipper Truck', value: 'tipper_truck', description: 'Truck equipped with a hydraulic bed for dumping bulk materials.' },
553
+ { label: 'Car Carrier', value: 'car_carrier', description: 'Trailer or truck configured for vehicle transport.' },
554
+ { label: 'Mini Van', value: 'mini_van', description: 'Compact van suitable for urban transport or light cargo.' },
555
+ { label: 'Panel Van', value: 'panel_van', description: 'Enclosed van used for deliveries or small logistics operations.' },
556
+ { label: 'Chassis Cab', value: 'chassis_cab', description: 'Truck base with customizable rear body configurations.' },
557
+ { label: 'Electric Bus', value: 'electric_bus', description: 'Battery-powered bus used for sustainable public or private transport.' },
558
+ { label: 'Motorbike', value: 'motorbike', description: 'Two-wheeled asset for rapid, lightweight transportation.' },
559
+ ];
560
+
561
+ export const transmissionTypes = [
562
+ { label: 'Manual', value: 'manual', description: 'Vehicles requiring manual gear shifting by the driver.' },
563
+ { label: 'Automatic', value: 'automatic', description: 'Vehicles equipped with fully automatic transmission systems.' },
564
+ { label: 'Semi-Automatic', value: 'semi_automatic', description: 'Combines manual control with automatic clutch operation.' },
565
+ { label: 'CVT (Continuously Variable Transmission)', value: 'cvt', description: 'Uses a belt and pulley system for seamless gear ratio transitions.' },
566
+ { label: 'Dual-Clutch', value: 'dual_clutch', description: 'High-performance transmission with two clutches for faster shifting.' },
567
+ { label: 'Electric Drive', value: 'electric_drive', description: 'Single-speed transmission system used in electric vehicles (EVs).' },
568
+ ];
569
+
570
+ export const odometerUnits = [...distanceUnits, { label: 'Hours', value: 'hours', description: 'Unit of time measurement, commonly used worldwide.' }];
571
+
495
572
  export default function fleetOpsOptions(key) {
496
573
  const allOptions = {
497
574
  driverTypes,
@@ -529,6 +606,15 @@ export default function fleetOpsOptions(key) {
529
606
  sensorTypes,
530
607
  deviceStatuses,
531
608
  sensorStatuses,
609
+ fuelTypes,
610
+ fuelVolumeUnits,
611
+ vehicleUsageTypes,
612
+ vehicleOwnershipTypes,
613
+ vehicleBodyTypes,
614
+ vehicleBodySubTypes,
615
+ transmissionTypes,
616
+ odometerUnits,
617
+ measurementSystems,
532
618
  };
533
619
 
534
620
  return allOptions[key] ?? [];
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/fleetops-engine/services/driver-scheduling';
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.25",
3
+ "version": "0.6.26",
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.25",
3
+ "version": "0.6.26",
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.25",
3
+ "version": "0.6.26",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -43,8 +43,8 @@
43
43
  "dependencies": {
44
44
  "@babel/core": "^7.23.2",
45
45
  "@fleetbase/ember-core": "^0.3.6",
46
- "@fleetbase/ember-ui": "^0.3.9",
47
- "@fleetbase/fleetops-data": "^0.1.21",
46
+ "@fleetbase/ember-ui": "^0.3.11",
47
+ "@fleetbase/fleetops-data": "^0.1.23",
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,142 @@
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('vehicles', function (Blueprint $table) {
14
+ // Financing details
15
+ $table->unsignedInteger('loan_number_of_payments')->nullable()->after('acquisition_cost');
16
+ $table->date('loan_first_payment')->nullable()->after('acquisition_cost');
17
+ $table->decimal('loan_amount', 12, 2)->nullable()->after('acquisition_cost');
18
+
19
+ // Service life details
20
+ $table->string('estimated_service_life_distance_unit', 16)->nullable()->after('acquisition_cost');
21
+ $table->unsignedInteger('estimated_service_life_distance')->nullable()->after('acquisition_cost');
22
+ $table->unsignedInteger('estimated_service_life_months')->nullable()->after('acquisition_cost');
23
+
24
+ // Odometer at purchase (lifecycle context)
25
+ $table->unsignedInteger('odometer_at_purchase')->nullable()->after('odometer');
26
+
27
+ // Capacity and Dimensions
28
+ $table->decimal('cargo_volume', 10, 2)->nullable()->after('fuel_volume_unit');
29
+ $table->decimal('passenger_volume', 10, 2)->nullable()->after('fuel_volume_unit');
30
+ $table->decimal('interior_volume', 10, 2)->nullable()->after('fuel_volume_unit');
31
+ $table->decimal('weight', 10, 2)->nullable()->after('fuel_volume_unit');
32
+ $table->decimal('width', 10, 2)->nullable()->after('fuel_volume_unit');
33
+ $table->decimal('length', 10, 2)->nullable()->after('fuel_volume_unit');
34
+ $table->decimal('height', 10, 2)->nullable()->after('fuel_volume_unit');
35
+ $table->decimal('towing_capacity', 10, 2)->nullable()->after('fuel_volume_unit');
36
+ $table->decimal('payload_capacity', 10, 2)->nullable()->after('fuel_volume_unit');
37
+ $table->unsignedTinyInteger('seating_capacity')->nullable()->after('fuel_volume_unit');
38
+ $table->decimal('ground_clearance', 10, 2)->nullable()->after('fuel_volume_unit');
39
+ $table->decimal('bed_length', 10, 2)->nullable()->after('fuel_volume_unit');
40
+ $table->decimal('fuel_capacity', 10, 2)->nullable()->after('fuel_volume_unit');
41
+
42
+ // Regulatory / compliance
43
+ $table->string('emission_standard', 32)->nullable()->after('payload_capacity');
44
+ $table->boolean('dpf_equipped')->nullable()->after('emission_standard');
45
+ $table->boolean('scr_equipped')->nullable()->after('dpf_equipped');
46
+ $table->decimal('gvwr', 10, 2)->nullable()->after('scr_equipped'); // gross vehicle weight rating
47
+ $table->decimal('gcwr', 10, 2)->nullable()->after('gvwr'); // gross combined weight rating
48
+
49
+ // Engine specs
50
+ $table->string('cylinder_arrangement', 8)->nullable()->after('serial_number');
51
+ $table->unsignedTinyInteger('number_of_cylinders')->nullable()->after('serial_number');
52
+ $table->unsignedInteger('torque_rpm')->nullable()->after('serial_number');
53
+ $table->decimal('torque', 10, 2)->nullable()->after('serial_number');
54
+ $table->unsignedInteger('horsepower_rpm')->nullable()->after('serial_number');
55
+ $table->decimal('horsepower', 10, 2)->nullable()->after('serial_number');
56
+ $table->decimal('engine_size', 5, 2)->nullable()->after('serial_number'); // e.g. 2.0 (L)
57
+ $table->decimal('engine_displacement', 7, 2)->nullable()->after('serial_number'); // e.g. 1998.00 cc
58
+ $table->string('engine_configuration', 32)->nullable()->after('serial_number'); // e.g. Inline-4
59
+ $table->string('engine_family', 64)->nullable()->after('serial_number');
60
+ $table->string('engine_make', 64)->nullable()->after('serial_number');
61
+ $table->string('engine_model', 64)->nullable()->after('serial_number');
62
+ $table->string('engine_number', 128)->nullable()->after('serial_number');
63
+
64
+ // Indexes (engine + financing + compliance)
65
+ $table->unique('engine_number', 'vehicles_engine_number_unique');
66
+ $table->index('engine_make', 'vehicles_engine_make_index');
67
+ $table->index('engine_model', 'vehicles_engine_model_index');
68
+ $table->index('engine_family', 'vehicles_engine_family_index');
69
+ $table->index('loan_first_payment', 'vehicles_loan_first_payment_index');
70
+ $table->index('emission_standard', 'vehicles_emission_standard_index');
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Reverse the migrations.
76
+ */
77
+ public function down(): void
78
+ {
79
+ Schema::table('vehicles', function (Blueprint $table) {
80
+ // Drop indexes first
81
+ $table->dropUnique('vehicles_engine_number_unique');
82
+ $table->dropIndex('vehicles_engine_make_index');
83
+ $table->dropIndex('vehicles_engine_model_index');
84
+ $table->dropIndex('vehicles_engine_family_index');
85
+ $table->dropIndex('vehicles_loan_first_payment_index');
86
+ $table->dropIndex('vehicles_emission_standard_index');
87
+
88
+ // Then drop columns
89
+ $table->dropColumn([
90
+ // Financing details
91
+ 'loan_number_of_payments',
92
+ 'loan_first_payment',
93
+ 'loan_amount',
94
+
95
+ // Service life details
96
+ 'estimated_service_life_distance_unit',
97
+ 'estimated_service_life_distance',
98
+ 'estimated_service_life_months',
99
+
100
+ // Odometer at purchase
101
+ 'odometer_at_purchase',
102
+
103
+ // Capacity and Dimensions
104
+ 'cargo_volume',
105
+ 'passenger_volume',
106
+ 'interior_volume',
107
+ 'weight',
108
+ 'width',
109
+ 'length',
110
+ 'height',
111
+ 'towing_capacity',
112
+ 'payload_capacity',
113
+ 'seating_capacity',
114
+ 'ground_clearance',
115
+ 'bed_length',
116
+ 'fuel_capacity',
117
+
118
+ // Regulatory / compliance
119
+ 'emission_standard',
120
+ 'dpf_equipped',
121
+ 'scr_equipped',
122
+ 'gvwr',
123
+ 'gcwr',
124
+
125
+ // Engine specs
126
+ 'cylinder_arrangement',
127
+ 'number_of_cylinders',
128
+ 'torque_rpm',
129
+ 'torque',
130
+ 'horsepower_rpm',
131
+ 'horsepower',
132
+ 'engine_size',
133
+ 'engine_displacement',
134
+ 'engine_configuration',
135
+ 'engine_family',
136
+ 'engine_make',
137
+ 'engine_model',
138
+ 'engine_number',
139
+ ]);
140
+ });
141
+ }
142
+ };
@@ -0,0 +1,244 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Constraints;
4
+
5
+ use Fleetbase\Models\ScheduleItem;
6
+ use Fleetbase\Support\Scheduling\ConstraintResult;
7
+ use Illuminate\Support\Carbon;
8
+
9
+ /**
10
+ * Hours of Service (HOS) Constraint.
11
+ *
12
+ * Validates schedule items against FMCSA Hours of Service regulations.
13
+ *
14
+ * Key Regulations:
15
+ * - 11-hour driving limit: May drive a maximum of 11 hours after 10 consecutive hours off duty
16
+ * - 14-hour duty window: May not drive beyond the 14th consecutive hour after coming on duty
17
+ * - 60/70-hour weekly limit: May not drive after 60/70 hours on duty in 7/8 consecutive days
18
+ * - 30-minute break: Required after 8 cumulative hours of driving without at least a 30-minute break
19
+ */
20
+ class HOSConstraint
21
+ {
22
+ /**
23
+ * Validate a schedule item against HOS regulations.
24
+ */
25
+ public function validate(ScheduleItem $item): ConstraintResult
26
+ {
27
+ $violations = [];
28
+
29
+ // Get the driver's recent schedule items
30
+ $recentItems = $this->getRecentScheduleItems($item);
31
+
32
+ // Check 11-hour driving limit
33
+ if (!$this->check11HourDrivingLimit($item, $recentItems)) {
34
+ $violations[] = [
35
+ 'constraint_key' => 'hos_11_hour_driving_limit',
36
+ 'message' => 'Exceeds 11-hour driving limit. Driver must have 10 consecutive hours off duty.',
37
+ 'severity' => 'critical',
38
+ ];
39
+ }
40
+
41
+ // Check 14-hour duty window
42
+ if (!$this->check14HourDutyWindow($item, $recentItems)) {
43
+ $violations[] = [
44
+ 'constraint_key' => 'hos_14_hour_duty_window',
45
+ 'message' => 'Exceeds 14-hour duty window. Driver cannot drive beyond 14 hours after coming on duty.',
46
+ 'severity' => 'critical',
47
+ ];
48
+ }
49
+
50
+ // Check 60/70-hour weekly limit
51
+ if (!$this->check60_70HourWeeklyLimit($item, $recentItems)) {
52
+ $violations[] = [
53
+ 'constraint_key' => 'hos_60_70_hour_weekly_limit',
54
+ 'message' => 'Exceeds 60/70-hour weekly limit. Driver has exceeded maximum hours in 7/8 consecutive days.',
55
+ 'severity' => 'critical',
56
+ ];
57
+ }
58
+
59
+ // Check 30-minute break requirement
60
+ if (!$this->check30MinuteBreak($item, $recentItems)) {
61
+ $violations[] = [
62
+ 'constraint_key' => 'hos_30_minute_break',
63
+ 'message' => 'Missing required 30-minute break after 8 cumulative hours of driving.',
64
+ 'severity' => 'warning',
65
+ ];
66
+ }
67
+
68
+ if (empty($violations)) {
69
+ return ConstraintResult::pass();
70
+ }
71
+
72
+ return ConstraintResult::fail($violations);
73
+ }
74
+
75
+ /**
76
+ * Get recent schedule items for the driver.
77
+ *
78
+ * @return \Illuminate\Database\Eloquent\Collection
79
+ */
80
+ protected function getRecentScheduleItems(ScheduleItem $item)
81
+ {
82
+ $sevenDaysAgo = Carbon::parse($item->start_at)->subDays(7);
83
+
84
+ return ScheduleItem::where('assignee_uuid', $item->assignee_uuid)
85
+ ->where('assignee_type', $item->assignee_type)
86
+ ->where('start_at', '>=', $sevenDaysAgo)
87
+ ->where('id', '!=', $item->id)
88
+ ->orderBy('start_at', 'asc')
89
+ ->get();
90
+ }
91
+
92
+ /**
93
+ * Check 11-hour driving limit.
94
+ *
95
+ * @param \Illuminate\Database\Eloquent\Collection $recentItems
96
+ */
97
+ protected function check11HourDrivingLimit(ScheduleItem $item, $recentItems): bool
98
+ {
99
+ // Get the last 10-hour off-duty period
100
+ $lastOffDutyPeriod = $this->getLastOffDutyPeriod($item, $recentItems, 10);
101
+
102
+ if (!$lastOffDutyPeriod) {
103
+ // If no 10-hour off-duty period found, check total driving hours
104
+ $totalDrivingHours = $this->calculateTotalDrivingHours($item, $recentItems);
105
+
106
+ return $totalDrivingHours <= 11;
107
+ }
108
+
109
+ // Calculate driving hours since last 10-hour off-duty period
110
+ $drivingHoursSinceRest = $this->calculateDrivingHoursSince($item, $recentItems, $lastOffDutyPeriod);
111
+
112
+ return $drivingHoursSinceRest <= 11;
113
+ }
114
+
115
+ /**
116
+ * Check 14-hour duty window.
117
+ *
118
+ * @param \Illuminate\Database\Eloquent\Collection $recentItems
119
+ */
120
+ protected function check14HourDutyWindow(ScheduleItem $item, $recentItems): bool
121
+ {
122
+ $lastOffDutyPeriod = $this->getLastOffDutyPeriod($item, $recentItems, 10);
123
+
124
+ if (!$lastOffDutyPeriod) {
125
+ return true; // Cannot determine, allow for now
126
+ }
127
+
128
+ $dutyStart = Carbon::parse($lastOffDutyPeriod);
129
+ $itemEnd = Carbon::parse($item->end_at);
130
+
131
+ $hoursSinceDutyStart = $dutyStart->diffInHours($itemEnd);
132
+
133
+ return $hoursSinceDutyStart <= 14;
134
+ }
135
+
136
+ /**
137
+ * Check 60/70-hour weekly limit.
138
+ *
139
+ * @param \Illuminate\Database\Eloquent\Collection $recentItems
140
+ */
141
+ protected function check60_70HourWeeklyLimit(ScheduleItem $item, $recentItems): bool
142
+ {
143
+ // Use 70-hour/8-day limit as default (can be configured)
144
+ $limit = 70;
145
+ $days = 8;
146
+
147
+ $startDate = Carbon::parse($item->start_at)->subDays($days);
148
+ $totalHours = 0;
149
+
150
+ foreach ($recentItems as $recentItem) {
151
+ if (Carbon::parse($recentItem->start_at)->gte($startDate)) {
152
+ $totalHours += $recentItem->duration / 60; // Convert minutes to hours
153
+ }
154
+ }
155
+
156
+ // Add current item duration
157
+ $totalHours += $item->duration / 60;
158
+
159
+ return $totalHours <= $limit;
160
+ }
161
+
162
+ /**
163
+ * Check 30-minute break requirement.
164
+ *
165
+ * @param \Illuminate\Database\Eloquent\Collection $recentItems
166
+ */
167
+ protected function check30MinuteBreak(ScheduleItem $item, $recentItems): bool
168
+ {
169
+ // Get driving hours in the current duty period
170
+ $drivingHours = 0;
171
+ $lastBreak = null;
172
+
173
+ foreach ($recentItems as $recentItem) {
174
+ if ($recentItem->break_start_at && $recentItem->break_end_at) {
175
+ $breakDuration = Carbon::parse($recentItem->break_start_at)->diffInMinutes($recentItem->break_end_at);
176
+ if ($breakDuration >= 30) {
177
+ $lastBreak = $recentItem->break_end_at;
178
+ $drivingHours = 0; // Reset driving hours after break
179
+ }
180
+ }
181
+
182
+ $drivingHours += $recentItem->duration / 60;
183
+ }
184
+
185
+ // Add current item
186
+ $drivingHours += $item->duration / 60;
187
+
188
+ // If more than 8 hours of driving, require a 30-minute break
189
+ if ($drivingHours > 8) {
190
+ return $item->break_start_at && $item->break_end_at
191
+ && Carbon::parse($item->break_start_at)->diffInMinutes($item->break_end_at) >= 30;
192
+ }
193
+
194
+ return true;
195
+ }
196
+
197
+ /**
198
+ * Get the last off-duty period of specified duration.
199
+ *
200
+ * @param \Illuminate\Database\Eloquent\Collection $recentItems
201
+ */
202
+ protected function getLastOffDutyPeriod(ScheduleItem $item, $recentItems, int $hours): ?Carbon
203
+ {
204
+ // This is a simplified implementation
205
+ // In production, this would check actual off-duty periods from driver logs
206
+ return null;
207
+ }
208
+
209
+ /**
210
+ * Calculate total driving hours.
211
+ *
212
+ * @param \Illuminate\Database\Eloquent\Collection $recentItems
213
+ */
214
+ protected function calculateTotalDrivingHours(ScheduleItem $item, $recentItems): float
215
+ {
216
+ $totalMinutes = $item->duration;
217
+
218
+ foreach ($recentItems as $recentItem) {
219
+ $totalMinutes += $recentItem->duration;
220
+ }
221
+
222
+ return $totalMinutes / 60;
223
+ }
224
+
225
+ /**
226
+ * Calculate driving hours since a specific time.
227
+ *
228
+ * @param \Illuminate\Database\Eloquent\Collection $recentItems
229
+ */
230
+ protected function calculateDrivingHoursSince(ScheduleItem $item, $recentItems, Carbon $since): float
231
+ {
232
+ $totalMinutes = 0;
233
+
234
+ foreach ($recentItems as $recentItem) {
235
+ if (Carbon::parse($recentItem->start_at)->gte($since)) {
236
+ $totalMinutes += $recentItem->duration;
237
+ }
238
+ }
239
+
240
+ $totalMinutes += $item->duration;
241
+
242
+ return $totalMinutes / 60;
243
+ }
244
+ }
@@ -1060,7 +1060,7 @@ class OrderController extends Controller
1060
1060
  // Handle order completion
1061
1061
  if (Utils::isActivity($activity) && $activity->completesOrder()) {
1062
1062
  // unset from driver current job
1063
- $order->driverAssigned->unassignCurrentOrder();
1063
+ $order->driverAssigned->unassignCurrentJob();
1064
1064
  $order->complete();
1065
1065
  }
1066
1066
 
@@ -165,8 +165,13 @@ class OrderController extends FleetOpsController
165
165
  );
166
166
  }
167
167
 
168
- // dispatch if flagged true
169
- $order->firstDispatchWithActivity();
168
+ // Check dispatch flag with backward compatibility (default true)
169
+ $shouldDispatch = isset($input['dispatched']) ? (bool) $input['dispatched'] : true;
170
+
171
+ // dispatch if flagged true, otherwise ensure order stays in created state
172
+ if ($shouldDispatch) {
173
+ $order->firstDispatchWithActivity();
174
+ }
170
175
 
171
176
  // set driving distance and time
172
177
  $order->setPreliminaryDistanceAndTime();
@@ -190,7 +195,7 @@ class OrderController extends FleetOpsController
190
195
 
191
196
  return ['order' => new $this->resource($record)];
192
197
  } catch (QueryException $e) {
193
- return response()->error(env('DEBUG') ? $e->getMessage() : 'Error occurred while trying to create a ' . $this->resourceSingularlName);
198
+ return response()->error(app()->hasDebugModeEnabled() ? $e->getMessage() : 'Error occurred while trying to create a ' . $this->resourceSingularlName);
194
199
  } catch (FleetbaseRequestValidationException $e) {
195
200
  return response()->error($e->getErrors());
196
201
  } catch (\Exception $e) {