@fleetbase/fleetops-engine 0.6.19 → 0.6.20
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/order/kanban.hbs +12 -10
- package/addon/components/order/kanban.js +27 -3
- package/addon/controllers/operations/orders/index/new.js +4 -2
- package/addon/controllers/operations/orders/index.js +50 -45
- package/addon/routes/operations/orders/index.js +0 -3
- package/addon/services/order-creation.js +4 -8
- package/addon/services/order-validation.js +3 -3
- package/addon/styles/fleetops-engine.css +35 -0
- package/addon/templates/operations/orders/index.hbs +26 -2
- package/addon/utils/setup-customer-portal.js +7 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +50 -68
- package/server/src/Models/Payload.php +12 -3
- package/server/src/Models/Place.php +5 -1
- package/server/src/Models/Vehicle.php +107 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
<div {{did-update this.handleArgsChange @orderConfig @orders}}>
|
|
2
|
+
<Kanban
|
|
3
|
+
@columns={{this.columns}}
|
|
4
|
+
@onCardMove={{this.handleCardMove}}
|
|
5
|
+
@headerOffset={{@headerOffset}}
|
|
6
|
+
@columnIdPath="status"
|
|
7
|
+
@subject="order"
|
|
8
|
+
@cardTemplate="order/kanban-card"
|
|
9
|
+
@onCreateCard={{transition-to "operations.orders.index.new"}}
|
|
10
|
+
...attributes
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
@@ -6,6 +6,8 @@ import { isArray } from '@ember/array';
|
|
|
6
6
|
import { debug } from '@ember/debug';
|
|
7
7
|
import { task } from 'ember-concurrency';
|
|
8
8
|
import titleize from 'ember-cli-string-helpers/utils/titleize';
|
|
9
|
+
import smartHumanize from '@fleetbase/ember-ui/utils/smart-humanize';
|
|
10
|
+
import isUuid from '@fleetbase/ember-core/utils/is-uuid';
|
|
9
11
|
|
|
10
12
|
export default class OrderKanbanComponent extends Component {
|
|
11
13
|
@service fetch;
|
|
@@ -13,6 +15,7 @@ export default class OrderKanbanComponent extends Component {
|
|
|
13
15
|
@service intl;
|
|
14
16
|
@tracked statuses = [];
|
|
15
17
|
@tracked orders = this.args.orders ?? [];
|
|
18
|
+
@tracked orderConfig = this.args.orderConfig ?? null;
|
|
16
19
|
|
|
17
20
|
#defaultStatuses = {
|
|
18
21
|
start: ['created', 'dispatched', 'started'],
|
|
@@ -48,7 +51,7 @@ export default class OrderKanbanComponent extends Component {
|
|
|
48
51
|
|
|
49
52
|
return final.map((status, index) => ({
|
|
50
53
|
id: status,
|
|
51
|
-
title: titleize(status),
|
|
54
|
+
title: titleize(smartHumanize(status)),
|
|
52
55
|
position: index,
|
|
53
56
|
cards: this.#getOrdersByStatus(status, this.orders),
|
|
54
57
|
}));
|
|
@@ -96,13 +99,34 @@ export default class OrderKanbanComponent extends Component {
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
@action handleArgsChange(el, [orderConfig, orders = []]) {
|
|
103
|
+
if (isArray(orders)) {
|
|
104
|
+
this.orders = orders;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isUuid(orderConfig)) {
|
|
108
|
+
this.orderConfig = orderConfig;
|
|
109
|
+
} else {
|
|
110
|
+
this.orderConfig = null;
|
|
111
|
+
}
|
|
112
|
+
this.loadStatuses.perform();
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
#getOrdersByStatus(status, orders = []) {
|
|
100
|
-
|
|
116
|
+
let filteredOrders = orders.filter((order) => order.status === status);
|
|
117
|
+
if (this.orderConfig) {
|
|
118
|
+
filteredOrders = filteredOrders.filter((order) => order.order_config_uuid === this.orderConfig);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return filteredOrders;
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
@task *loadStatuses() {
|
|
125
|
+
const params = {};
|
|
126
|
+
if (this.orderConfig) params.order_config_uuid = this.orderConfig;
|
|
127
|
+
|
|
104
128
|
try {
|
|
105
|
-
const statuses = yield this.fetch.get('orders/statuses');
|
|
129
|
+
const statuses = yield this.fetch.get('orders/statuses', params);
|
|
106
130
|
this.statuses = isArray(statuses) ? statuses : [];
|
|
107
131
|
} catch (err) {
|
|
108
132
|
debug('Unable to load order statuses: ' + err.message);
|
|
@@ -66,8 +66,10 @@ export default class OperationsOrdersIndexNewController extends Controller {
|
|
|
66
66
|
this.universe.trigger('fleet-ops.order.creating', order);
|
|
67
67
|
|
|
68
68
|
// Save custom field values
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
if (order.cfManager) {
|
|
70
|
+
const { created: customFieldValues } = yield order.cfManager.saveTo(order);
|
|
71
|
+
order.custom_field_values.pushObjects(customFieldValues);
|
|
72
|
+
}
|
|
71
73
|
|
|
72
74
|
// Save order
|
|
73
75
|
const createdOrder = yield order.save();
|
|
@@ -63,57 +63,62 @@ export default class OperationsOrdersIndexController extends Controller {
|
|
|
63
63
|
@tracked without_driver;
|
|
64
64
|
@tracked status;
|
|
65
65
|
@tracked type;
|
|
66
|
+
@tracked orderConfig;
|
|
66
67
|
@tracked bulkSearchValue = '';
|
|
67
68
|
@tracked bulk_query = '';
|
|
68
69
|
@tracked layout = 'map';
|
|
69
70
|
|
|
70
71
|
/** action buttons */
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
72
|
+
get actionButtons() {
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
icon: 'refresh',
|
|
76
|
+
onClick: this.orderActions.refresh,
|
|
77
|
+
helpText: this.intl.t('common.refresh'),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
text: this.intl.t('common.new'),
|
|
81
|
+
type: 'primary',
|
|
82
|
+
icon: 'plus',
|
|
83
|
+
onClick: this.orderActions.transition.create,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
text: this.intl.t('common.export'),
|
|
87
|
+
icon: 'long-arrow-up',
|
|
88
|
+
iconClass: 'rotate-icon-45',
|
|
89
|
+
wrapperClass: 'hidden md:flex',
|
|
90
|
+
onClick: this.orderActions.export,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
91
94
|
|
|
92
95
|
/** bulk actions */
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
96
|
+
get bulkActions() {
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
label: this.intl.t('common.cancel-resource', { resource: this.intl.t('resource.orders') }),
|
|
100
|
+
icon: 'ban',
|
|
101
|
+
fn: this.orderActions.bulkCancel,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
label: this.intl.t('common.delete-resource', { resource: this.intl.t('resource.orders') }),
|
|
105
|
+
icon: 'trash',
|
|
106
|
+
class: 'text-red-500',
|
|
107
|
+
fn: this.orderActions.bulkDelete,
|
|
108
|
+
},
|
|
109
|
+
{ separator: true },
|
|
110
|
+
{
|
|
111
|
+
label: this.intl.t('common.dispatch-orders'),
|
|
112
|
+
icon: 'rocket',
|
|
113
|
+
fn: this.orderActions.bulkDispatch,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
label: this.intl.t('common.assign-driver'),
|
|
117
|
+
icon: 'user-plus',
|
|
118
|
+
fn: this.orderActions.bulkAssignDriver,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
}
|
|
117
122
|
|
|
118
123
|
/** columns */
|
|
119
124
|
get columns() {
|
|
@@ -366,7 +371,7 @@ export default class OperationsOrdersIndexController extends Controller {
|
|
|
366
371
|
ddButtonText: false,
|
|
367
372
|
ddButtonIcon: 'ellipsis-h',
|
|
368
373
|
ddButtonIconPrefix: 'fas',
|
|
369
|
-
ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.
|
|
374
|
+
ddMenuLabel: this.intl.t('common.resource-actions', { resource: this.intl.t('resource.order') }),
|
|
370
375
|
cellClassNames: 'overflow-visible',
|
|
371
376
|
wrapperClass: 'flex items-center justify-end mx-2',
|
|
372
377
|
width: '12%',
|
|
@@ -30,9 +30,6 @@ export default class OperationsOrdersIndexRoute extends Route {
|
|
|
30
30
|
before: { refreshModel: true },
|
|
31
31
|
type: { refreshModel: true },
|
|
32
32
|
layout: { refreshModel: false },
|
|
33
|
-
drawerOpen: { refreshModel: false },
|
|
34
|
-
drawerTab: { refreshModel: false },
|
|
35
|
-
orderPanelOpen: { refreshModel: false },
|
|
36
33
|
};
|
|
37
34
|
|
|
38
35
|
model(params) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Service, { inject as service } from '@ember/service';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
|
-
import {
|
|
3
|
+
import { next } from '@ember/runloop';
|
|
4
4
|
|
|
5
5
|
export default class OrderCreationService extends Service {
|
|
6
6
|
@service orderActions;
|
|
@@ -12,13 +12,9 @@ export default class OrderCreationService extends Service {
|
|
|
12
12
|
const order = this.orderActions.createNewInstance(attrs);
|
|
13
13
|
this.order = order;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
this,
|
|
17
|
-
|
|
18
|
-
this.addContext('order', order);
|
|
19
|
-
},
|
|
20
|
-
0
|
|
21
|
-
);
|
|
15
|
+
next(() => {
|
|
16
|
+
this.addContext('order', order);
|
|
17
|
+
});
|
|
22
18
|
|
|
23
19
|
return order;
|
|
24
20
|
}
|
|
@@ -23,7 +23,7 @@ export default class OrderValidationService extends Service {
|
|
|
23
23
|
const hasWaypoints = order.payload.waypoints.length >= 2;
|
|
24
24
|
const hasPickup = isNotEmpty(order.payload.pickup);
|
|
25
25
|
const hasDropoff = isNotEmpty(order.payload.dropoff);
|
|
26
|
-
const hasValidCustomFields = cfManager ? this.isCustomFieldsValid(cfManager) :
|
|
26
|
+
const hasValidCustomFields = cfManager ? this.isCustomFieldsValid(cfManager) : true;
|
|
27
27
|
|
|
28
28
|
if (hasWaypoints) {
|
|
29
29
|
return hasOrderConfig && hasOrderType && hasValidCustomFields;
|
|
@@ -37,13 +37,13 @@ export default class OrderValidationService extends Service {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
validateCustomFields(cfManager) {
|
|
40
|
-
if (!cfManager) return
|
|
40
|
+
if (!cfManager) return true;
|
|
41
41
|
|
|
42
42
|
return cfManager.validateRequired();
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
isCustomFieldsValid(cfManager) {
|
|
46
|
-
if (!cfManager) return
|
|
46
|
+
if (!cfManager) return true;
|
|
47
47
|
|
|
48
48
|
const { isValid } = this.validateCustomFields(cfManager);
|
|
49
49
|
return isValid;
|
|
@@ -1654,3 +1654,38 @@ button.fleetops-btn-xxs,
|
|
|
1654
1654
|
padding-top: 0.2rem !important;
|
|
1655
1655
|
padding-bottom: 0.2rem !important;
|
|
1656
1656
|
}
|
|
1657
|
+
|
|
1658
|
+
/** css fix for operations index/kanban */
|
|
1659
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section {
|
|
1660
|
+
max-width: 100vw;
|
|
1661
|
+
min-width: 0;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container {
|
|
1665
|
+
display: flex;
|
|
1666
|
+
flex-direction: column;
|
|
1667
|
+
max-width: 100vw;
|
|
1668
|
+
min-width: 0;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container > .next-view-section-subheader {
|
|
1672
|
+
flex: 0 0 auto;
|
|
1673
|
+
width: 100%;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container > .next-view-section-body {
|
|
1677
|
+
flex: 1 1 auto;
|
|
1678
|
+
min-width: 0;
|
|
1679
|
+
overflow: auto;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
main.console-fleet-ops-operations-orders-index-index section.next-view-section > .next-view-section-container > .next-view-section-body > .kanban-board {
|
|
1683
|
+
display: flex;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.next-view-section-subheader .next-view-section-subheader-actions .order-board-type-filter.ember-power-select-trigger.ember-basic-dropdown-trigger {
|
|
1687
|
+
height: 2rem;
|
|
1688
|
+
align-items: center;
|
|
1689
|
+
padding-left: 0.5rem;
|
|
1690
|
+
padding-right: 2rem;
|
|
1691
|
+
}
|
|
@@ -53,9 +53,33 @@
|
|
|
53
53
|
{{/if}}
|
|
54
54
|
|
|
55
55
|
{{#if (eq this.layout "kanban")}}
|
|
56
|
-
<Layout::Section::Header @title={{t "menu.order-board"}} @actionsWrapperClass="space-x-1"
|
|
56
|
+
<Layout::Section::Header @title={{t "menu.order-board"}} @subtitle={{@model.length}} @subtitleClass="text-xs text-center font-semibold ml-2 w-6 h-6 rounded-full bg-blue-100 text-blue-900 dark:bg-blue-900 dark:text-blue-100 flex items-center justify-center" @actionsWrapperClass="space-x-1">
|
|
57
|
+
<div class="flex flex-row items-center space-x-2">
|
|
58
|
+
<div class="w-64">
|
|
59
|
+
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
|
|
60
|
+
<ModelSelect
|
|
61
|
+
@modelName="order-config"
|
|
62
|
+
@selectedModel={{or this.orderConfig this.type}}
|
|
63
|
+
@placeholder={{t "order.fields.order-type-placeholder"}}
|
|
64
|
+
@triggerClass="form-select form-input order-board-type-filter"
|
|
65
|
+
@infiniteScroll={{false}}
|
|
66
|
+
@renderInPlace={{true}}
|
|
67
|
+
@allowClear={{true}}
|
|
68
|
+
@onChange={{fn (mut this.orderConfig)}}
|
|
69
|
+
@onChangeId={{fn (mut this.type)}}
|
|
70
|
+
as |orderConfig|
|
|
71
|
+
>
|
|
72
|
+
<div class="text-sm">
|
|
73
|
+
<div class="font-semibold normalize-in-trigger">{{orderConfig.name}}</div>
|
|
74
|
+
<div class="hide-from-trigger">{{n-a orderConfig.description}}</div>
|
|
75
|
+
</div>
|
|
76
|
+
</ModelSelect>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</Layout::Section::Header>
|
|
57
81
|
<Layout::Section::Body>
|
|
58
|
-
<Order::Kanban @orders={{@model}} @headerOffset={{160}} />
|
|
82
|
+
<Order::Kanban @orders={{@model}} @headerOffset={{160}} @orderConfig={{this.type}} />
|
|
59
83
|
</Layout::Section::Body>
|
|
60
84
|
{{/if}}
|
|
61
85
|
|
|
@@ -4,6 +4,8 @@ import { setOwner } from '@ember/application';
|
|
|
4
4
|
import { debug } from '@ember/debug';
|
|
5
5
|
|
|
6
6
|
export default function setupCustomerPortal(app, engine, universe) {
|
|
7
|
+
if (!customerPortalInstalled(app)) return;
|
|
8
|
+
|
|
7
9
|
universe.afterBoot(function (u) {
|
|
8
10
|
const portal = u.getEngineInstance('@fleetbase/customer-portal-engine');
|
|
9
11
|
if (!portal) {
|
|
@@ -58,3 +60,8 @@ function createEngineBoundComponent(engineInstance, ComponentClass) {
|
|
|
58
60
|
}
|
|
59
61
|
};
|
|
60
62
|
}
|
|
63
|
+
|
|
64
|
+
function customerPortalInstalled(app) {
|
|
65
|
+
const extensions = app.extensions ?? [];
|
|
66
|
+
return extensions.find(({ name }) => name === '@fleetbase/customer-portal-engine');
|
|
67
|
+
}
|
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.20",
|
|
4
4
|
"description": "Fleet & Transport Management Extension for Fleetbase",
|
|
5
5
|
"fleetbase": {
|
|
6
6
|
"route": "fleet-ops"
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@babel/core": "^7.23.2",
|
|
45
|
-
"@fleetbase/ember-core": "
|
|
46
|
-
"@fleetbase/ember-ui": "
|
|
45
|
+
"@fleetbase/ember-core": "^0.3.4",
|
|
46
|
+
"@fleetbase/ember-ui": "^0.3.6",
|
|
47
47
|
"@fleetbase/fleetops-data": "^0.1.20",
|
|
48
48
|
"@fleetbase/leaflet-routing-machine": "^3.2.17",
|
|
49
49
|
"@fortawesome/ember-fontawesome": "^2.0.0",
|
|
@@ -774,55 +774,30 @@ class OrderController extends FleetOpsController
|
|
|
774
774
|
}
|
|
775
775
|
|
|
776
776
|
/**
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
* This endpoint compiles:
|
|
780
|
-
* 1. All unique `status` values from the `orders` table for the current company.
|
|
781
|
-
* 2. Optionally, all `Activity` codes defined in `OrderConfig` records that are
|
|
782
|
-
* actually referenced by existing orders (via `order_config_uuid`).
|
|
783
|
-
*
|
|
784
|
-
* ---
|
|
785
|
-
* ### Query Parameters
|
|
786
|
-
* - `include_order_config_activities` (bool, optional)
|
|
787
|
-
* When true, includes `Activity` codes from relevant `OrderConfig` instances.
|
|
788
|
-
*
|
|
789
|
-
* - `order_config_key` (string, optional)
|
|
790
|
-
* Restricts both `orders` and `order_configs` to a specific configuration key.
|
|
791
|
-
*
|
|
792
|
-
* ---
|
|
793
|
-
* ### Behavior
|
|
794
|
-
* - Only includes order configs that are actually used by orders.
|
|
795
|
-
* - Uses the `activities()` method on each `OrderConfig` to extract all activity codes.
|
|
796
|
-
* - Merges and deduplicates both order statuses and activity codes.
|
|
797
|
-
*
|
|
798
|
-
* ---
|
|
799
|
-
* ### Example Response
|
|
800
|
-
* ```json
|
|
801
|
-
* [
|
|
802
|
-
* "created",
|
|
803
|
-
* "dispatched",
|
|
804
|
-
* "completed",
|
|
805
|
-
* "canceled",
|
|
806
|
-
* "pickup_ready",
|
|
807
|
-
* "awaiting_payment"
|
|
808
|
-
* ]
|
|
809
|
-
* ```
|
|
810
|
-
*
|
|
811
|
-
* @return \Illuminate\Http\JsonResponse
|
|
777
|
+
* Return distinct order statuses (and optionally activity codes) for a company,
|
|
778
|
+
* filtered by order_config_uuid or order_config_key if provided.
|
|
812
779
|
*/
|
|
813
780
|
public function statuses(Request $request)
|
|
814
781
|
{
|
|
815
|
-
$companyUuid
|
|
816
|
-
|
|
782
|
+
$companyUuid = $request->user()->company_uuid ?? session('company');
|
|
817
783
|
$includeActivities = $request->boolean('include_order_config_activities', true);
|
|
818
|
-
$orderConfigKey = trim((string) $request->string('order_config_key'));
|
|
819
784
|
|
|
820
|
-
//
|
|
785
|
+
// Use input() + trim to get plain strings (Request::string() returns Stringable in newer Laravel)
|
|
786
|
+
$orderConfigKey = trim((string) $request->input('order_config_key', ''));
|
|
787
|
+
$orderConfigId = trim((string) $request->input('order_config_uuid', ''));
|
|
788
|
+
|
|
789
|
+
// ---------------------------
|
|
790
|
+
// Build base orders query
|
|
791
|
+
// ---------------------------
|
|
821
792
|
$ordersQuery = DB::table('orders')
|
|
822
793
|
->where('company_uuid', $companyUuid)
|
|
823
|
-
->whereNotNull('status')
|
|
794
|
+
->whereNotNull('status')
|
|
795
|
+
->whereNull('deleted_at');
|
|
824
796
|
|
|
825
|
-
|
|
797
|
+
// Prefer filtering by UUID (most precise), else by key
|
|
798
|
+
if ($orderConfigId !== '') {
|
|
799
|
+
$ordersQuery->where('order_config_uuid', $orderConfigId);
|
|
800
|
+
} elseif ($orderConfigKey !== '') {
|
|
826
801
|
$ordersQuery->whereExists(function ($q) use ($companyUuid, $orderConfigKey) {
|
|
827
802
|
$q->select(DB::raw(1))
|
|
828
803
|
->from('order_configs as oc')
|
|
@@ -832,34 +807,38 @@ class OrderController extends FleetOpsController
|
|
|
832
807
|
});
|
|
833
808
|
}
|
|
834
809
|
|
|
810
|
+
// Distinct order statuses
|
|
835
811
|
$orderStatuses = $ordersQuery->distinct()->pluck('status')->filter();
|
|
836
812
|
|
|
837
|
-
//
|
|
813
|
+
// ---------------------------------------
|
|
814
|
+
// Optionally include activity codes
|
|
815
|
+
// (must use the SAME target config set)
|
|
816
|
+
// ---------------------------------------
|
|
838
817
|
$activityCodes = collect();
|
|
839
818
|
|
|
840
819
|
if ($includeActivities) {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
820
|
+
// Determine target config UUIDs once, honoring UUID > key > all-on-company
|
|
821
|
+
if ($orderConfigId !== '') {
|
|
822
|
+
$targetConfigUuids = collect([$orderConfigId]);
|
|
823
|
+
} elseif ($orderConfigKey !== '') {
|
|
824
|
+
$targetConfigUuids = DB::table('order_configs')
|
|
825
|
+
->where('company_uuid', $companyUuid)
|
|
826
|
+
->where('key', $orderConfigKey)
|
|
827
|
+
->pluck('uuid');
|
|
828
|
+
} else {
|
|
829
|
+
// No filter given; derive from orders in this company
|
|
830
|
+
$targetConfigUuids = DB::table('orders')
|
|
831
|
+
->where('company_uuid', $companyUuid)
|
|
832
|
+
->whereNotNull('order_config_uuid')
|
|
833
|
+
->distinct()
|
|
834
|
+
->pluck('order_config_uuid');
|
|
835
|
+
}
|
|
856
836
|
|
|
857
|
-
if ($
|
|
837
|
+
if ($targetConfigUuids->isNotEmpty()) {
|
|
858
838
|
$orderConfigs = OrderConfig::where('company_uuid', $companyUuid)
|
|
859
|
-
->whereIn('uuid', $
|
|
839
|
+
->whereIn('uuid', $targetConfigUuids)
|
|
860
840
|
->get();
|
|
861
841
|
|
|
862
|
-
/** @var OrderConfig $config */
|
|
863
842
|
foreach ($orderConfigs as $config) {
|
|
864
843
|
if (!method_exists($config, 'activities')) {
|
|
865
844
|
continue;
|
|
@@ -867,19 +846,22 @@ class OrderController extends FleetOpsController
|
|
|
867
846
|
|
|
868
847
|
$activities = $config->activities();
|
|
869
848
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
849
|
+
// Handle Collection/array gracefully
|
|
850
|
+
$codes = collect($activities)
|
|
851
|
+
->map(function ($activity) {
|
|
852
|
+
return data_get($activity, 'code');
|
|
853
|
+
})
|
|
854
|
+
->filter()
|
|
855
|
+
->values();
|
|
875
856
|
|
|
876
|
-
|
|
877
|
-
}
|
|
857
|
+
$activityCodes = $activityCodes->merge($codes);
|
|
878
858
|
}
|
|
879
859
|
}
|
|
880
860
|
}
|
|
881
861
|
|
|
882
|
-
//
|
|
862
|
+
// ---------------------------------------
|
|
863
|
+
// Merge & return
|
|
864
|
+
// ---------------------------------------
|
|
883
865
|
$result = $orderStatuses
|
|
884
866
|
->merge($activityCodes)
|
|
885
867
|
->unique()
|
|
@@ -368,7 +368,15 @@ class Payload extends Model
|
|
|
368
368
|
) {
|
|
369
369
|
$placeUuid = $attributes['place_uuid'];
|
|
370
370
|
|
|
371
|
-
// Path 2: public_id under "
|
|
371
|
+
// Path 2: public_id under "uuid" -> resolve to uuid
|
|
372
|
+
} elseif (
|
|
373
|
+
is_array($attributes)
|
|
374
|
+
&& isset($attributes['uuid'])
|
|
375
|
+
&& ($resolvedUuid = Place::where('uuid', $attributes['uuid'])->value('uuid'))
|
|
376
|
+
) {
|
|
377
|
+
$placeUuid = $resolvedUuid;
|
|
378
|
+
|
|
379
|
+
// Path 3: public_id under "id" -> resolve to uuid
|
|
372
380
|
} elseif (
|
|
373
381
|
is_array($attributes)
|
|
374
382
|
&& isset($attributes['id'])
|
|
@@ -376,10 +384,11 @@ class Payload extends Model
|
|
|
376
384
|
) {
|
|
377
385
|
$placeUuid = $resolvedUuid;
|
|
378
386
|
|
|
379
|
-
// Path
|
|
387
|
+
// Path 4: create from mixed payload
|
|
380
388
|
} else {
|
|
381
389
|
$place = Place::createFromMixed($attributes);
|
|
382
390
|
|
|
391
|
+
|
|
383
392
|
// Store temp search UUID for traceability if present and different
|
|
384
393
|
if ($place instanceof Place && isset($attributes['uuid']) && $place->uuid !== $attributes['uuid']) {
|
|
385
394
|
$place->updateMeta('search_uuid', $attributes['uuid']);
|
|
@@ -420,7 +429,7 @@ class Payload extends Model
|
|
|
420
429
|
// -------- Upsert Waypoint --------
|
|
421
430
|
// Uniqueness: payload + place + order for deterministic row per position.
|
|
422
431
|
$unique = [
|
|
423
|
-
'payload_uuid' => $this->
|
|
432
|
+
'payload_uuid' => $this->uuid,
|
|
424
433
|
'place_uuid' => $placeUuid,
|
|
425
434
|
'order' => $index,
|
|
426
435
|
];
|
|
@@ -362,7 +362,11 @@ class Place extends Model
|
|
|
362
362
|
$results = \Geocoder\Laravel\Facades\Geocoder::geocode($address)->get();
|
|
363
363
|
|
|
364
364
|
if ($results->isEmpty() || !$results->first()) {
|
|
365
|
-
|
|
365
|
+
$place = (new static())->newInstance(['street1' => $address, 'location' => new SpatialPoint(0, 0)]);
|
|
366
|
+
if ($saveInstance) {
|
|
367
|
+
$place->save();
|
|
368
|
+
}
|
|
369
|
+
return $place;
|
|
366
370
|
}
|
|
367
371
|
|
|
368
372
|
return static::createFromGoogleAddress($results->first(), $saveInstance);
|
|
@@ -8,6 +8,7 @@ use Fleetbase\FleetOps\Casts\Point;
|
|
|
8
8
|
use Fleetbase\FleetOps\Support\Utils;
|
|
9
9
|
use Fleetbase\FleetOps\Support\VehicleData;
|
|
10
10
|
use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
|
|
11
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
|
|
11
12
|
use Fleetbase\Models\Category;
|
|
12
13
|
use Fleetbase\Models\File;
|
|
13
14
|
use Fleetbase\Models\Model;
|
|
@@ -23,6 +24,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
23
24
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
|
24
25
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
|
25
26
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
|
27
|
+
use Illuminate\Support\Arr;
|
|
26
28
|
use Illuminate\Support\Str;
|
|
27
29
|
use Spatie\Activitylog\LogOptions;
|
|
28
30
|
use Spatie\Activitylog\Traits\LogsActivity;
|
|
@@ -61,7 +63,7 @@ class Vehicle extends Model
|
|
|
61
63
|
*
|
|
62
64
|
* @var array
|
|
63
65
|
*/
|
|
64
|
-
protected $searchableColumns = ['make', 'model', 'year', 'plate_number', 'vin', 'public_id'];
|
|
66
|
+
protected $searchableColumns = ['name', 'description', 'make', 'model', 'trim', 'model_type', 'body_type', 'body_sub_type', 'year', 'plate_number', 'vin', 'call_sign', 'public_id'];
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Attributes that is filterable on this model.
|
|
@@ -533,6 +535,34 @@ class Vehicle extends Model
|
|
|
533
535
|
return ($isFirstPosition || $isPast50Meters) ? Position::create($positionData) : null;
|
|
534
536
|
}
|
|
535
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Creates a new position for the vehicle
|
|
540
|
+
*
|
|
541
|
+
* @param array $attributes
|
|
542
|
+
* @return Position|null
|
|
543
|
+
*/
|
|
544
|
+
public function createPosition(array $attributes = [], Model|string|null $destination = null): ?Position
|
|
545
|
+
{
|
|
546
|
+
if (!isset($attributes['coordinates']) && isset($attributes['location'])) {
|
|
547
|
+
$attributes['coordinates'] = $attributes['location'];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!isset($attributes['coordinates']) && isset($attributes['latitude']) && isset($attributes['longitude'])) {
|
|
551
|
+
$attributes['coordinates'] = new SpatialPoint($attributes['latitude'], $attributes['longitude']);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// handle destination if set
|
|
555
|
+
$destinationUuid = Str::isUuid($destination) ? $destination : data_get($destination, 'uuid');
|
|
556
|
+
|
|
557
|
+
return Position::create([
|
|
558
|
+
...Arr::only($attributes, ['coordinates', 'heading', 'bearing', 'speed', 'altitude']),
|
|
559
|
+
'subject_uuid' => $this->uuid,
|
|
560
|
+
'subject_type' => $this->getMorphClass(),
|
|
561
|
+
'company_uuid' => $this->company_uuid,
|
|
562
|
+
'destination_uuid' => $destinationUuid
|
|
563
|
+
]);
|
|
564
|
+
}
|
|
565
|
+
|
|
536
566
|
public static function createFromImport(array $row, bool $saveInstance = false): Vehicle
|
|
537
567
|
{
|
|
538
568
|
// Filter array for null key values
|
|
@@ -660,4 +690,80 @@ class Vehicle extends Model
|
|
|
660
690
|
|
|
661
691
|
return $details;
|
|
662
692
|
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Set or update a single key/value pair in the `specs` JSON column.
|
|
696
|
+
*
|
|
697
|
+
* Uses Laravel's `data_set` helper to allow dot notation for nested keys.
|
|
698
|
+
*
|
|
699
|
+
* @param string|array $key the key (or array path) to set within the specs
|
|
700
|
+
* @param mixed $value the value to assign to the given key
|
|
701
|
+
*
|
|
702
|
+
* @return array the updated specs array
|
|
703
|
+
*/
|
|
704
|
+
public function setSpec(string|array $key, mixed $value): array
|
|
705
|
+
{
|
|
706
|
+
$specs = is_array($this->specs) ? $this->specs : (array) $this->specs;
|
|
707
|
+
data_set($specs, $key, $value);
|
|
708
|
+
$this->specs = $specs;
|
|
709
|
+
|
|
710
|
+
return $specs;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Merge multiple values into the `specs` JSON column.
|
|
715
|
+
*
|
|
716
|
+
* By default this performs a shallow merge (overwrites duplicate keys).
|
|
717
|
+
* Use `array_replace_recursive` if you need nested merges.
|
|
718
|
+
*
|
|
719
|
+
* @param array $newSpecs key/value pairs to merge into specs
|
|
720
|
+
*
|
|
721
|
+
* @return array the updated specs array
|
|
722
|
+
*/
|
|
723
|
+
public function setSpecs(array $newSpecs = []): array
|
|
724
|
+
{
|
|
725
|
+
$specs = is_array($this->specs) ? $this->specs : (array) $this->specs;
|
|
726
|
+
$specs = array_merge($specs, $newSpecs);
|
|
727
|
+
$this->specs = $specs;
|
|
728
|
+
|
|
729
|
+
return $specs;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Set or update a single key/value pair in the `vin_data` JSON column.
|
|
734
|
+
*
|
|
735
|
+
* Uses Laravel's `data_set` helper to allow dot notation for nested keys.
|
|
736
|
+
*
|
|
737
|
+
* @param string|array $key the key (or array path) to set within the VIN data
|
|
738
|
+
* @param mixed $value the value to assign to the given key
|
|
739
|
+
*
|
|
740
|
+
* @return array the updated vin_data array
|
|
741
|
+
*/
|
|
742
|
+
public function setVinData(string|array $key, mixed $value): array
|
|
743
|
+
{
|
|
744
|
+
$vinData = is_array($this->vin_data) ? $this->vin_data : (array) $this->vin_data;
|
|
745
|
+
data_set($vinData, $key, $value);
|
|
746
|
+
$this->vin_data = $vinData;
|
|
747
|
+
|
|
748
|
+
return $vinData;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Merge multiple values into the `vin_data` JSON column.
|
|
753
|
+
*
|
|
754
|
+
* By default this performs a shallow merge (overwrites duplicate keys).
|
|
755
|
+
* Use `array_replace_recursive` if you need nested merges.
|
|
756
|
+
*
|
|
757
|
+
* @param array $newVinData key/value pairs to merge into vin_data
|
|
758
|
+
*
|
|
759
|
+
* @return array the updated vin_data array
|
|
760
|
+
*/
|
|
761
|
+
public function setVinDatas(array $newVinData = []): array
|
|
762
|
+
{
|
|
763
|
+
$vinData = is_array($this->vin_data) ? $this->vin_data : (array) $this->vin_data;
|
|
764
|
+
$vinData = array_merge($vinData, $newVinData);
|
|
765
|
+
$this->vin_data = $vinData;
|
|
766
|
+
|
|
767
|
+
return $vinData;
|
|
768
|
+
}
|
|
663
769
|
}
|