@fleetbase/fleetops-engine 0.6.23 → 0.6.25
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/addon/components/map/drawer/position-listing.hbs +1 -1
- package/addon/components/map/leaflet-live-map.js +96 -1
- package/addon/controllers/connectivity/events/index.js +0 -1
- package/addon/controllers/management/contacts.js +13 -11
- package/addon/services/location.js +10 -2
- package/addon/templates/management/contacts.hbs +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/migrations/2025_11_01_103634_add_performance_indexes_to_fleetops_tables.php +245 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +39 -29
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +9 -9
- package/server/src/Http/Filter/OrderFilter.php +42 -25
- package/server/src/Http/Resources/v1/Driver.php +1 -1
- package/server/src/Http/Resources/v1/Payload.php +1 -0
- package/server/src/Models/Driver.php +3 -0
- package/server/src/Models/Vendor.php +0 -1
|
@@ -34,7 +34,7 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
34
34
|
|
|
35
35
|
/** tracked properties */
|
|
36
36
|
@tracked ready = false;
|
|
37
|
-
@tracked zoom = this.
|
|
37
|
+
@tracked zoom = this.getValidZoom();
|
|
38
38
|
@tracked latitude = this.location.getLatitude();
|
|
39
39
|
@tracked longitude = this.location.getLongitude();
|
|
40
40
|
@tracked contextmenuItems = [];
|
|
@@ -45,6 +45,29 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
45
45
|
@tracked vehicles = [];
|
|
46
46
|
@tracked places = [];
|
|
47
47
|
|
|
48
|
+
constructor() {
|
|
49
|
+
super(...arguments);
|
|
50
|
+
|
|
51
|
+
// Store bound function reference for proper cleanup
|
|
52
|
+
this._locationUpdateHandler = this.#handleLocationUpdate.bind(this);
|
|
53
|
+
|
|
54
|
+
// Listen for location updates from the location service
|
|
55
|
+
this.universe.on('user.located', this._locationUpdateHandler);
|
|
56
|
+
|
|
57
|
+
// Ensure we have valid coordinates on initialization
|
|
58
|
+
this.#updateCoordinatesFromLocation();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
willDestroy() {
|
|
62
|
+
super.willDestroy();
|
|
63
|
+
|
|
64
|
+
// Clean up event listener using stored reference
|
|
65
|
+
if (this._locationUpdateHandler) {
|
|
66
|
+
this.universe.off('user.located', this._locationUpdateHandler);
|
|
67
|
+
this._locationUpdateHandler = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
@action didLoad({ target: map }) {
|
|
49
72
|
this.#setMap(map);
|
|
50
73
|
this.#createMapContextMenu(map);
|
|
@@ -189,6 +212,46 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
189
212
|
return this.ready === true;
|
|
190
213
|
}
|
|
191
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Get valid zoom level for map initialization
|
|
217
|
+
* @returns {number} Valid zoom level between 1-20
|
|
218
|
+
*/
|
|
219
|
+
getValidZoom() {
|
|
220
|
+
const zoom = this.args.zoom;
|
|
221
|
+
// Validate zoom is a valid number within Leaflet bounds (1-20)
|
|
222
|
+
if (typeof zoom === 'number' && !isNaN(zoom) && zoom >= 1 && zoom <= 20) {
|
|
223
|
+
return zoom;
|
|
224
|
+
}
|
|
225
|
+
// Return default zoom of 14 if invalid
|
|
226
|
+
return 14;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Handles location updates from the location service
|
|
231
|
+
* @param {Object} coordinates - The new coordinates
|
|
232
|
+
*/
|
|
233
|
+
#handleLocationUpdate(coordinates) {
|
|
234
|
+
if (coordinates && typeof coordinates.latitude === 'number' && typeof coordinates.longitude === 'number') {
|
|
235
|
+
this.latitude = coordinates.latitude;
|
|
236
|
+
this.longitude = coordinates.longitude;
|
|
237
|
+
|
|
238
|
+
// Update map position if map is loaded
|
|
239
|
+
if (this.map && this.map.setView) {
|
|
240
|
+
this.map.setView([coordinates.latitude, coordinates.longitude], this.zoom);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Updates coordinates from location service on initialization
|
|
247
|
+
*/
|
|
248
|
+
#updateCoordinatesFromLocation() {
|
|
249
|
+
// Initial coordinates are already set via tracked properties
|
|
250
|
+
// This method ensures we have the latest location service values
|
|
251
|
+
this.latitude = this.location.getLatitude();
|
|
252
|
+
this.longitude = this.location.getLongitude();
|
|
253
|
+
}
|
|
254
|
+
|
|
192
255
|
#setMap(map) {
|
|
193
256
|
set(map, 'livemap', this);
|
|
194
257
|
this.map = map;
|
|
@@ -417,6 +480,38 @@ export default class MapLeafletLiveMapComponent extends Component {
|
|
|
417
480
|
return contextmenuRegistry;
|
|
418
481
|
}
|
|
419
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Safely gets a valid latitude value with fallback to default
|
|
485
|
+
* @returns {number} Valid latitude value
|
|
486
|
+
*/
|
|
487
|
+
#getValidLatitude() {
|
|
488
|
+
const lat = this.location.getLatitude();
|
|
489
|
+
|
|
490
|
+
// Validate latitude is a number and within valid range (-90 to 90)
|
|
491
|
+
if (typeof lat === 'number' && !isNaN(lat) && lat >= -90 && lat <= 90) {
|
|
492
|
+
return lat;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Fallback to default Singapore latitude
|
|
496
|
+
return 1.369;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Safely gets a valid longitude value with fallback to default
|
|
501
|
+
* @returns {number} Valid longitude value
|
|
502
|
+
*/
|
|
503
|
+
#getValidLongitude() {
|
|
504
|
+
const lng = this.location.getLongitude();
|
|
505
|
+
|
|
506
|
+
// Validate longitude is a number and within valid range (-180 to 180)
|
|
507
|
+
if (typeof lng === 'number' && !isNaN(lng) && lng >= -180 && lng <= 180) {
|
|
508
|
+
return lng;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Fallback to default Singapore longitude
|
|
512
|
+
return 103.8864;
|
|
513
|
+
}
|
|
514
|
+
|
|
420
515
|
#changeTileSource(source) {
|
|
421
516
|
switch (source) {
|
|
422
517
|
case 'dark':
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import Controller from '@ember/controller';
|
|
2
|
-
import { tracked } from '@glimmer/tracking';
|
|
3
2
|
import { inject as service } from '@ember/service';
|
|
4
3
|
import { getOwner } from '@ember/application';
|
|
5
4
|
import getCurrentNestedController from '@fleetbase/ember-core/utils/get-current-nested-controller';
|
|
6
5
|
|
|
7
6
|
export default class ManagementContactsController extends Controller {
|
|
8
7
|
@service hostRouter;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
|
|
9
|
+
get tabs() {
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
route: 'management.contacts.index',
|
|
13
|
+
label: 'Contacts',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
route: 'management.contacts.customers',
|
|
17
|
+
label: 'Customers',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
}
|
|
19
21
|
|
|
20
22
|
get childController() {
|
|
21
23
|
return getCurrentNestedController(getOwner(this), this.hostRouter.currentRouteName);
|
|
@@ -69,7 +69,11 @@ export default class LocationService extends Service {
|
|
|
69
69
|
* @returns {number} The current latitude.
|
|
70
70
|
*/
|
|
71
71
|
getLatitude() {
|
|
72
|
-
return
|
|
72
|
+
// Ensure we always return a valid number within geographic bounds
|
|
73
|
+
if (typeof this.latitude === 'number' && !isNaN(this.latitude) && this.latitude >= -90 && this.latitude <= 90) {
|
|
74
|
+
return this.latitude;
|
|
75
|
+
}
|
|
76
|
+
return this.constructor.DEFAULT_LATITUDE;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/**
|
|
@@ -77,7 +81,11 @@ export default class LocationService extends Service {
|
|
|
77
81
|
* @returns {number} The current longitude.
|
|
78
82
|
*/
|
|
79
83
|
getLongitude() {
|
|
80
|
-
return
|
|
84
|
+
// Ensure we always return a valid number within geographic bounds
|
|
85
|
+
if (typeof this.longitude === 'number' && !isNaN(this.longitude) && this.longitude >= -180 && this.longitude <= 180) {
|
|
86
|
+
return this.longitude;
|
|
87
|
+
}
|
|
88
|
+
return this.constructor.DEFAULT_LONGITUDE;
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
/**
|
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.25",
|
|
4
4
|
"description": "Fleet & Transport Management Extension for Fleetbase",
|
|
5
5
|
"fleetbase": {
|
|
6
6
|
"route": "fleet-ops"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@babel/core": "^7.23.2",
|
|
45
45
|
"@fleetbase/ember-core": "^0.3.6",
|
|
46
|
-
"@fleetbase/ember-ui": "^0.3.
|
|
46
|
+
"@fleetbase/ember-ui": "^0.3.9",
|
|
47
47
|
"@fleetbase/fleetops-data": "^0.1.21",
|
|
48
48
|
"@fleetbase/leaflet-routing-machine": "^3.2.17",
|
|
49
49
|
"@fortawesome/ember-fontawesome": "^2.0.0",
|
|
@@ -0,0 +1,245 @@
|
|
|
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
|
+
// ============================================================
|
|
14
|
+
// ORDERS TABLE INDEXES
|
|
15
|
+
// ============================================================
|
|
16
|
+
Schema::table('orders', function (Blueprint $table) {
|
|
17
|
+
/**
|
|
18
|
+
* Index 1: company_uuid + status.
|
|
19
|
+
*
|
|
20
|
+
* WHY: The most common query is "get all orders for company X with status Y"
|
|
21
|
+
* USED BY: OrderFilter status filtering, dashboard queries
|
|
22
|
+
* QUERY: WHERE company_uuid = ? AND status IN (?, ?, ?)
|
|
23
|
+
*
|
|
24
|
+
* This index allows MySQL to:
|
|
25
|
+
* 1. Jump to all rows for the company (first column)
|
|
26
|
+
* 2. Filter by status within that company (second column)
|
|
27
|
+
*
|
|
28
|
+
* Without this, MySQL would use company_uuid index, then scan all
|
|
29
|
+
* matching rows to filter by status (slow for large datasets).
|
|
30
|
+
*/
|
|
31
|
+
if (!$this->indexExists('orders', 'idx_orders_company_status')) {
|
|
32
|
+
$table->index(['company_uuid', 'status'], 'idx_orders_company_status');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Index 2: company_uuid + driver_assigned_uuid.
|
|
37
|
+
*
|
|
38
|
+
* WHY: Finding unassigned orders is a critical operation
|
|
39
|
+
* USED BY: OrderFilter unassigned() method, driver assignment UI
|
|
40
|
+
* QUERY: WHERE company_uuid = ? AND driver_assigned_uuid IS NULL
|
|
41
|
+
*
|
|
42
|
+
* This index is particularly efficient for IS NULL checks because
|
|
43
|
+
* MySQL stores NULL values in indexes (unlike some databases).
|
|
44
|
+
*/
|
|
45
|
+
if (!$this->indexExists('orders', 'idx_orders_company_driver')) {
|
|
46
|
+
$table->index(['company_uuid', 'driver_assigned_uuid'], 'idx_orders_company_driver');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Index 3: company_uuid + created_at.
|
|
51
|
+
*
|
|
52
|
+
* WHY: Date-based filtering and sorting is extremely common
|
|
53
|
+
* USED BY: Date range filters, "recent orders" queries
|
|
54
|
+
* QUERY: WHERE company_uuid = ? AND created_at >= ? AND created_at <= ?
|
|
55
|
+
* ORDER BY: created_at DESC
|
|
56
|
+
*
|
|
57
|
+
* This index supports both filtering AND sorting efficiently.
|
|
58
|
+
* The ORDER BY can use the index for sorting without a filesort operation.
|
|
59
|
+
*/
|
|
60
|
+
if (!$this->indexExists('orders', 'idx_orders_company_created')) {
|
|
61
|
+
$table->index(['company_uuid', 'created_at'], 'idx_orders_company_created');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Index 4: company_uuid + scheduled_at.
|
|
66
|
+
*
|
|
67
|
+
* WHY: Scheduled orders need to be queried efficiently
|
|
68
|
+
* USED BY: Scheduler, upcoming deliveries view
|
|
69
|
+
* QUERY: WHERE company_uuid = ? AND scheduled_at >= ? AND scheduled_at <= ?
|
|
70
|
+
*/
|
|
71
|
+
if (!$this->indexExists('orders', 'idx_orders_company_scheduled')) {
|
|
72
|
+
$table->index(['company_uuid', 'scheduled_at'], 'idx_orders_company_scheduled');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Index 5: company_uuid + dispatched + status.
|
|
77
|
+
*
|
|
78
|
+
* WHY: Finding dispatched orders with specific statuses
|
|
79
|
+
* USED BY: Active orders view, driver assignment
|
|
80
|
+
* QUERY: WHERE company_uuid = ? AND dispatched = 1 AND status IN (?, ?)
|
|
81
|
+
*
|
|
82
|
+
* This is a 3-column composite index. MySQL will use it for:
|
|
83
|
+
* - company_uuid alone
|
|
84
|
+
* - company_uuid + dispatched
|
|
85
|
+
* - company_uuid + dispatched + status (full index)
|
|
86
|
+
*
|
|
87
|
+
* But NOT for dispatched alone or status alone.
|
|
88
|
+
*/
|
|
89
|
+
if (!$this->indexExists('orders', 'idx_orders_company_dispatched_status')) {
|
|
90
|
+
$table->index(['company_uuid', 'dispatched', 'status'], 'idx_orders_company_dispatched_status');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Index 6: company_uuid + tracking_number_uuid.
|
|
95
|
+
*
|
|
96
|
+
* WHY: Common join key and filtering key for tracking data.
|
|
97
|
+
* USED BY: OrderFilter, tracking joins
|
|
98
|
+
* QUERY: WHERE company_uuid = ? AND tracking_number_uuid = ?
|
|
99
|
+
*/
|
|
100
|
+
if (!$this->indexExists('orders', 'idx_orders_company_tracking')) {
|
|
101
|
+
$table->index(['company_uuid', 'tracking_number_uuid'], 'idx_orders_company_tracking');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ============================================================
|
|
106
|
+
// PAYLOADS TABLE INDEXES
|
|
107
|
+
// ============================================================
|
|
108
|
+
Schema::table('payloads', function (Blueprint $table) {
|
|
109
|
+
/**
|
|
110
|
+
* Index 6-8: Foreign key indexes.
|
|
111
|
+
*
|
|
112
|
+
* WHY: JOINs require indexes on both sides of the join condition
|
|
113
|
+
* USED BY: All queries that JOIN orders to payloads to places
|
|
114
|
+
*
|
|
115
|
+
* When we JOIN payloads to places:
|
|
116
|
+
* JOIN places ON places.uuid = payloads.pickup_uuid
|
|
117
|
+
*
|
|
118
|
+
* MySQL needs an index on payloads.pickup_uuid to efficiently
|
|
119
|
+
* find matching rows. Without it, MySQL does a full table scan
|
|
120
|
+
* of the payloads table for each place.
|
|
121
|
+
*/
|
|
122
|
+
if (!$this->indexExists('payloads', 'pickup_uuid')) {
|
|
123
|
+
$table->index('pickup_uuid', 'idx_payloads_pickup');
|
|
124
|
+
}
|
|
125
|
+
if (!$this->indexExists('payloads', 'dropoff_uuid')) {
|
|
126
|
+
$table->index('dropoff_uuid', 'idx_payloads_dropoff');
|
|
127
|
+
}
|
|
128
|
+
if (!$this->indexExists('payloads', 'return_uuid')) {
|
|
129
|
+
$table->index('return_uuid', 'idx_payloads_return');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ============================================================
|
|
134
|
+
// WAYPOINTS TABLE INDEXES
|
|
135
|
+
// ============================================================
|
|
136
|
+
Schema::table('waypoints', function (Blueprint $table) {
|
|
137
|
+
/**
|
|
138
|
+
* Index 9: payload_uuid.
|
|
139
|
+
*
|
|
140
|
+
* WHY: Loading waypoints for a payload is a 1-to-many relationship
|
|
141
|
+
* USED BY: Eager loading payload.waypoints
|
|
142
|
+
* QUERY: WHERE payload_uuid = ?
|
|
143
|
+
*
|
|
144
|
+
* Without this index, loading waypoints for 378 payloads would
|
|
145
|
+
* require 378 table scans of the waypoints table.
|
|
146
|
+
*/
|
|
147
|
+
if (!$this->indexExists('waypoints', 'payload_uuid')) {
|
|
148
|
+
$table->index('payload_uuid', 'idx_waypoints_payload');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Index 10: Composite index for deleted waypoints.
|
|
153
|
+
*
|
|
154
|
+
* WHY: We frequently query non-deleted waypoints for a payload
|
|
155
|
+
* QUERY: WHERE payload_uuid = ? AND deleted_at IS NULL
|
|
156
|
+
*
|
|
157
|
+
* This composite index is more efficient than two separate indexes
|
|
158
|
+
* because it allows MySQL to filter on both conditions using one index.
|
|
159
|
+
*/
|
|
160
|
+
if (!$this->indexExists('waypoints', 'idx_waypoints_payload_deleted')) {
|
|
161
|
+
$table->index(['payload_uuid', 'deleted_at'], 'idx_waypoints_payload_deleted');
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ============================================================
|
|
166
|
+
// TRACKING_STATUSES TABLE INDEXES
|
|
167
|
+
// ============================================================
|
|
168
|
+
Schema::table('tracking_statuses', function (Blueprint $table) {
|
|
169
|
+
/**
|
|
170
|
+
* Index 11: tracking_number_uuid.
|
|
171
|
+
*
|
|
172
|
+
* WHY: Loading tracking statuses for an order
|
|
173
|
+
* USED BY: Eager loading order.trackingStatuses
|
|
174
|
+
* QUERY: WHERE tracking_number_uuid = ?
|
|
175
|
+
*/
|
|
176
|
+
if (!$this->indexExists('tracking_statuses', 'tracking_number_uuid')) {
|
|
177
|
+
$table->index('tracking_number_uuid', 'idx_tracking_statuses_tracking_number');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ============================================================
|
|
182
|
+
// ENTITIES TABLE INDEXES
|
|
183
|
+
// ============================================================
|
|
184
|
+
|
|
185
|
+
Schema::table('entities', function (Blueprint $table) {
|
|
186
|
+
/**
|
|
187
|
+
* Index 12: payload_uuid.
|
|
188
|
+
*
|
|
189
|
+
* WHY: Loading entities for a payload
|
|
190
|
+
* USED BY: Eager loading payload.entities
|
|
191
|
+
* QUERY: WHERE payload_uuid = ?
|
|
192
|
+
*/
|
|
193
|
+
if (!$this->indexExists('entities', 'payload_uuid')) {
|
|
194
|
+
$table->index('payload_uuid', 'idx_entities_payload');
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Reverse the migrations.
|
|
201
|
+
*/
|
|
202
|
+
public function down(): void
|
|
203
|
+
{
|
|
204
|
+
Schema::table('orders', function (Blueprint $table) {
|
|
205
|
+
$table->dropIndex('idx_orders_company_status');
|
|
206
|
+
$table->dropIndex('idx_orders_company_driver');
|
|
207
|
+
$table->dropIndex('idx_orders_company_created');
|
|
208
|
+
$table->dropIndex('idx_orders_company_scheduled');
|
|
209
|
+
$table->dropIndex('idx_orders_company_dispatched_status');
|
|
210
|
+
$table->dropIndex('idx_orders_company_tracking');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
Schema::table('payloads', function (Blueprint $table) {
|
|
214
|
+
$table->dropIndex('idx_payloads_pickup');
|
|
215
|
+
$table->dropIndex('idx_payloads_dropoff');
|
|
216
|
+
$table->dropIndex('idx_payloads_return');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
Schema::table('waypoints', function (Blueprint $table) {
|
|
220
|
+
$table->dropIndex('idx_waypoints_payload');
|
|
221
|
+
$table->dropIndex('idx_waypoints_payload_deleted');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
Schema::table('tracking_statuses', function (Blueprint $table) {
|
|
225
|
+
$table->dropIndex('idx_tracking_statuses_tracking_number');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
Schema::table('entities', function (Blueprint $table) {
|
|
229
|
+
$table->dropIndex('idx_entities_payload');
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check if an index exists on a table.
|
|
235
|
+
*
|
|
236
|
+
* This helper method prevents errors when running migrations multiple times
|
|
237
|
+
* or when indexes already exist from previous manual additions.
|
|
238
|
+
*/
|
|
239
|
+
private function indexExists(string $table, string $indexName): bool
|
|
240
|
+
{
|
|
241
|
+
$indexes = DB::select("SHOW INDEX FROM {$table} WHERE Key_name = ?", [$indexName]);
|
|
242
|
+
|
|
243
|
+
return count($indexes) > 0;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
@@ -323,8 +323,8 @@ class DriverController extends Controller
|
|
|
323
323
|
*/
|
|
324
324
|
public function track(string $id, Request $request)
|
|
325
325
|
{
|
|
326
|
-
$latitude = $request->input('latitude');
|
|
327
|
-
$longitude = $request->input('longitude');
|
|
326
|
+
$latitude = (float) $request->input('latitude');
|
|
327
|
+
$longitude = (float) $request->input('longitude');
|
|
328
328
|
$altitude = $request->input('altitude');
|
|
329
329
|
$heading = $request->input('heading');
|
|
330
330
|
$speed = $request->input('speed');
|
|
@@ -332,15 +332,14 @@ class DriverController extends Controller
|
|
|
332
332
|
try {
|
|
333
333
|
$driver = Driver::findRecordOrFail($id);
|
|
334
334
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $exception) {
|
|
335
|
-
return response()->
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
);
|
|
335
|
+
return response()->apiError('Driver resource not found.', 404);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// If no lat/lng provided, maintain compatibility and just return existing driver resource
|
|
339
|
+
if (empty($latitude) && empty($longitude)) {
|
|
340
|
+
return new DriverResource($driver);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
// check if driver needs a geocoded update to set city and country they are currently in
|
|
344
343
|
$isGeocodable = Carbon::parse($driver->updated_at)->diffInMinutes(Carbon::now(), false) > 10 || empty($driver->country) || empty($driver->city);
|
|
345
344
|
|
|
346
345
|
$positionData = [
|
|
@@ -352,37 +351,42 @@ class DriverController extends Controller
|
|
|
352
351
|
'speed' => $speed,
|
|
353
352
|
];
|
|
354
353
|
|
|
355
|
-
|
|
356
|
-
$order = $driver->getCurrentOrder();
|
|
357
|
-
if ($order) {
|
|
354
|
+
if ($order = $driver->getCurrentOrder()) {
|
|
358
355
|
$positionData['order_uuid'] = $order->uuid;
|
|
359
|
-
|
|
360
|
-
|
|
356
|
+
$destination = $order->payload?->getPickupOrCurrentWaypoint();
|
|
357
|
+
|
|
361
358
|
if ($destination) {
|
|
362
359
|
$positionData['destination_uuid'] = $destination->uuid;
|
|
363
360
|
}
|
|
364
361
|
}
|
|
365
362
|
|
|
366
|
-
$driver->
|
|
363
|
+
$driver->updateQuietly($positionData);
|
|
367
364
|
$driver->createPosition($positionData);
|
|
368
365
|
|
|
369
|
-
// If vehicle is assigned to driver load it and sync position data
|
|
370
366
|
$driver->loadMissing('vehicle');
|
|
371
|
-
if ($driver->vehicle) {
|
|
372
|
-
$
|
|
373
|
-
$
|
|
374
|
-
|
|
367
|
+
if ($vehicle = $driver->vehicle) {
|
|
368
|
+
$vehicleUpdateData = [...$positionData];
|
|
369
|
+
if ($vehicle->online !== $driver->online) {
|
|
370
|
+
$vehicleUpdateData['online'] = $driver->online;
|
|
371
|
+
}
|
|
372
|
+
$vehicle->updateQuietly($vehicleUpdateData);
|
|
373
|
+
$vehicle->createPosition($positionData);
|
|
374
|
+
broadcast(new VehicleLocationChanged($vehicle, ['driver' => $driver->public_id]));
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
if ($isGeocodable) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
378
|
+
try {
|
|
379
|
+
$geocoded = Geocoder::reverse($latitude, $longitude)->get()->first();
|
|
380
|
+
if ($geocoded) {
|
|
381
|
+
$driver->updateQuietly([
|
|
382
|
+
'city' => $geocoded->getLocality(),
|
|
383
|
+
'country' => $geocoded->getCountry()->getCode(),
|
|
384
|
+
]);
|
|
385
|
+
}
|
|
386
|
+
} catch (\Throwable $e) {
|
|
387
|
+
if (app()->bound('sentry')) {
|
|
388
|
+
app('sentry')->captureException($e);
|
|
389
|
+
}
|
|
386
390
|
}
|
|
387
391
|
}
|
|
388
392
|
|
|
@@ -420,7 +424,13 @@ class DriverController extends Controller
|
|
|
420
424
|
$onlineValue = is_null($onlineParam) ? !$driver->online : Utils::castBoolean($onlineParam);
|
|
421
425
|
|
|
422
426
|
// Perform a single update call
|
|
423
|
-
$driver->
|
|
427
|
+
$driver->updateQuietly(['online' => $onlineValue]);
|
|
428
|
+
|
|
429
|
+
// Update vehicle online too
|
|
430
|
+
$driver->loadMissing('vehicle');
|
|
431
|
+
if ($vehicle = $driver->vehicle) {
|
|
432
|
+
$vehicle->updateQuietly(['online' => $onlineValue]);
|
|
433
|
+
}
|
|
424
434
|
|
|
425
435
|
// Return the updated resource
|
|
426
436
|
return new DriverResource($driver);
|
|
@@ -218,8 +218,8 @@ class VehicleController extends Controller
|
|
|
218
218
|
*/
|
|
219
219
|
public function track(string $id, Request $request)
|
|
220
220
|
{
|
|
221
|
-
$latitude = $request->input('latitude');
|
|
222
|
-
$longitude = $request->input('longitude');
|
|
221
|
+
$latitude = (float) $request->input('latitude');
|
|
222
|
+
$longitude = (float) $request->input('longitude');
|
|
223
223
|
$altitude = $request->input('altitude');
|
|
224
224
|
$heading = $request->input('heading');
|
|
225
225
|
$speed = $request->input('speed');
|
|
@@ -227,12 +227,12 @@ class VehicleController extends Controller
|
|
|
227
227
|
try {
|
|
228
228
|
$vehicle = Vehicle::findRecordOrFail($id);
|
|
229
229
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $exception) {
|
|
230
|
-
return response()->
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
230
|
+
return response()->apiError('Vehicle resource not found.', 404);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// If no lat/lng provided, maintain compatibility and just return existing driver resource
|
|
234
|
+
if (empty($latitude) && empty($longitude)) {
|
|
235
|
+
return new VehicleResource($vehicle);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
$positionData = [
|
|
@@ -260,7 +260,7 @@ class VehicleController extends Controller
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
$vehicle->
|
|
263
|
+
$vehicle->updateQuietly($positionData);
|
|
264
264
|
$vehicle->createPosition($positionData);
|
|
265
265
|
|
|
266
266
|
broadcast(new VehicleLocationChanged($vehicle));
|
|
@@ -12,31 +12,46 @@ class OrderFilter extends Filter
|
|
|
12
12
|
{
|
|
13
13
|
public function queryForInternal()
|
|
14
14
|
{
|
|
15
|
-
$this->
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
$
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
$companyUuid = $this->request->session()->get('company');
|
|
16
|
+
|
|
17
|
+
// apply company scope first for indexed filtering
|
|
18
|
+
$this->builder->where('orders.company_uuid', $companyUuid);
|
|
19
|
+
|
|
20
|
+
// replace ambiguous whereRelation with qualified whereHas to avoid alias clashes
|
|
21
|
+
$this->builder->whereHas('payload', function ($payloadQuery) {
|
|
22
|
+
$payloadQuery->where(function ($q) {
|
|
23
|
+
$q->whereHas('waypoints', function ($w) {
|
|
24
|
+
$w->whereNotNull('waypoints.uuid');
|
|
25
|
+
});
|
|
26
|
+
$q->orWhereHas('pickup', function ($p) {
|
|
27
|
+
$p->whereNotNull('places.uuid');
|
|
28
|
+
});
|
|
29
|
+
$q->orWhereHas('dropoff', function ($d) {
|
|
30
|
+
$d->whereNotNull('places.uuid');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ensure associated tracking data exists
|
|
36
|
+
$this->builder->whereHas('trackingNumber', function ($q) {
|
|
37
|
+
$q->select('uuid');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
$this->builder->whereHas('trackingStatuses', function ($q) {
|
|
41
|
+
$q->select('uuid');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// eager load main relationships to reduce N+1 overhead
|
|
45
|
+
$this->builder->with([
|
|
46
|
+
'payload.entities',
|
|
47
|
+
'payload.waypoints',
|
|
48
|
+
'payload.pickup',
|
|
49
|
+
'payload.dropoff',
|
|
50
|
+
'payload.return',
|
|
51
|
+
'trackingNumber',
|
|
52
|
+
'trackingStatuses',
|
|
53
|
+
'driverAssigned',
|
|
54
|
+
]);
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
public function queryForPublic()
|
|
@@ -108,6 +123,8 @@ class OrderFilter extends Filter
|
|
|
108
123
|
$this->builder->whereNotIn('status', ['created', 'completed', 'expired', 'order_canceled', 'canceled', 'pending']);
|
|
109
124
|
// remove the searchBuilder where clause
|
|
110
125
|
$this->builder->removeWhereFromQuery('status', 'active');
|
|
126
|
+
|
|
127
|
+
return;
|
|
111
128
|
}
|
|
112
129
|
|
|
113
130
|
$status = Utils::arrayFrom($status);
|
|
@@ -46,7 +46,7 @@ class Driver extends FleetbaseResource
|
|
|
46
46
|
'vendor_name' => $this->when(Http::isInternalRequest(), $this->vendor_name),
|
|
47
47
|
'vehicle' => $this->whenLoaded('vehicle', fn () => new VehicleWithoutDriver($this->vehicle)),
|
|
48
48
|
'current_job' => $this->whenLoaded('currentJob', fn () => new Order($this->currentJob)),
|
|
49
|
-
'current_job_id' => $this->when(Http::isInternalRequest(), data_get($this, 'currentJob.
|
|
49
|
+
'current_job_id' => $this->when(Http::isInternalRequest(), data_get($this, 'currentJob.tracking')),
|
|
50
50
|
'jobs' => $this->whenLoaded('jobs', fn () => $this->getJobs()),
|
|
51
51
|
'vendor' => $this->whenLoaded('vendor', fn () => new Vendor($this->vendor)),
|
|
52
52
|
'fleets' => $this->whenLoaded('fleets', fn () => Fleet::collection($this->fleets()->without('drivers')->get())),
|
|
@@ -38,6 +38,7 @@ class Payload extends FleetbaseResource
|
|
|
38
38
|
'cod_amount' => $this->cod_amount ?? null,
|
|
39
39
|
'cod_currency' => $this->cod_currency ?? null,
|
|
40
40
|
'cod_payment_method' => $this->cod_payment_method ?? null,
|
|
41
|
+
'payment_method' => $this->payment_method ?? null,
|
|
41
42
|
'meta' => data_get($this, 'meta', Utils::createObject()),
|
|
42
43
|
'updated_at' => $this->updated_at,
|
|
43
44
|
'created_at' => $this->created_at,
|