@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.
- package/DRIVER_SCHEDULING.md +186 -0
- package/addon/components/driver/schedule.hbs +100 -0
- package/addon/components/driver/schedule.js +267 -0
- package/addon/components/order/kanban-card.hbs +2 -2
- package/addon/components/vehicle/details.hbs +594 -4
- package/addon/components/vehicle/form.hbs +467 -41
- package/addon/controllers/analytics/reports/index.js +3 -2
- package/addon/controllers/connectivity/devices/index.js +3 -3
- package/addon/controllers/connectivity/events/index.js +3 -2
- package/addon/controllers/connectivity/sensors/index.js +3 -5
- package/addon/controllers/connectivity/telematics/index.js +3 -1
- package/addon/controllers/maintenance/equipment/index.js +4 -4
- package/addon/controllers/maintenance/parts/index.js +4 -4
- package/addon/controllers/maintenance/work-orders/index.js +4 -4
- package/addon/controllers/management/contacts/customers.js +12 -10
- package/addon/controllers/management/contacts/index.js +3 -10
- package/addon/controllers/management/drivers/index/details.js +26 -13
- package/addon/controllers/management/drivers/index.js +4 -16
- package/addon/controllers/management/fleets/index.js +3 -13
- package/addon/controllers/management/fuel-reports/index.js +3 -10
- package/addon/controllers/management/issues/index.js +3 -12
- package/addon/controllers/management/places/index.js +4 -12
- package/addon/controllers/management/vehicles/index.js +3 -13
- package/addon/controllers/management/vendors/index.js +3 -13
- package/addon/controllers/operations/orders/index.js +5 -22
- package/addon/controllers/operations/scheduler/index.js +195 -6
- package/addon/controllers/operations/service-rates/index.js +34 -34
- package/addon/controllers/settings/payments/index.js +0 -6
- package/addon/routes.js +1 -0
- package/addon/services/driver-scheduling.js +171 -0
- package/addon/services/leaflet-layer-visibility-manager.js +4 -1
- package/addon/services/service-rate-actions.js +5 -1
- package/addon/templates/management/drivers/index/details/positions.hbs +1 -2
- package/addon/templates/management/drivers/index/details/schedule.hbs +1 -2
- package/addon/templates/operations/scheduler/index.hbs +48 -10
- package/addon/utils/fleet-ops-options.js +86 -0
- package/app/services/driver-scheduling.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2025_11_17_033648_add_additional_columns_to_vehicles_table.php +142 -0
- package/server/src/Constraints/HOSConstraint.php +244 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +8 -3
- package/server/src/Http/Resources/v1/Vehicle.php +197 -19
- package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +211 -28
- package/server/src/Models/Driver.php +12 -8
- 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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetbase/fleetops-engine",
|
|
3
|
-
"version": "0.6.
|
|
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.
|
|
47
|
-
"@fleetbase/fleetops-data": "^0.1.
|
|
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->
|
|
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
|
|
169
|
-
$
|
|
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(
|
|
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) {
|