@fleetbase/fleetops-engine 0.6.25 → 0.6.27
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/Place.php +2 -2
- package/server/src/Models/Vehicle.php +101 -15
|
@@ -1,21 +1,105 @@
|
|
|
1
1
|
import Controller from '@ember/controller';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
|
-
import { action } from '@ember/object';
|
|
4
|
+
import { action, computed } from '@ember/object';
|
|
5
|
+
import { later } from '@ember/runloop';
|
|
6
|
+
import { task } from 'ember-concurrency';
|
|
5
7
|
import { format, isValid as isValidDate } from 'date-fns';
|
|
6
8
|
import isObject from '@fleetbase/ember-core/utils/is-object';
|
|
7
9
|
import isJson from '@fleetbase/ember-core/utils/is-json';
|
|
8
10
|
import createFullCalendarEventFromOrder, { createOrderEventTitle } from '../../../utils/create-full-calendar-event-from-order';
|
|
9
11
|
|
|
12
|
+
function createFullCalendarEventFromScheduleItem(item, driver) {
|
|
13
|
+
return {
|
|
14
|
+
id: item.id,
|
|
15
|
+
resourceId: driver.id,
|
|
16
|
+
title: `${driver.name} - Shift`,
|
|
17
|
+
start: item.start_at,
|
|
18
|
+
end: item.end_at,
|
|
19
|
+
backgroundColor: getScheduleItemColor(item),
|
|
20
|
+
extendedProps: {
|
|
21
|
+
scheduleItem: item,
|
|
22
|
+
driver: driver,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getScheduleItemColor(item) {
|
|
28
|
+
const statusColors = {
|
|
29
|
+
pending: '#FFA500',
|
|
30
|
+
confirmed: '#4CAF50',
|
|
31
|
+
in_progress: '#2196F3',
|
|
32
|
+
completed: '#9E9E9E',
|
|
33
|
+
cancelled: '#F44336',
|
|
34
|
+
no_show: '#FF5722',
|
|
35
|
+
};
|
|
36
|
+
return statusColors[item.status] || '#4CAF50';
|
|
37
|
+
}
|
|
38
|
+
|
|
10
39
|
export default class OperationsSchedulerIndexController extends Controller {
|
|
11
40
|
@service modalsManager;
|
|
12
41
|
@service notifications;
|
|
13
42
|
@service store;
|
|
14
43
|
@service intl;
|
|
15
44
|
@service hostRouter;
|
|
45
|
+
@service scheduling;
|
|
16
46
|
@tracked scheduledOrders = [];
|
|
17
47
|
@tracked unscheduledOrders = [];
|
|
18
|
-
@tracked
|
|
48
|
+
@tracked drivers = [];
|
|
49
|
+
@tracked scheduleItems = [];
|
|
50
|
+
@tracked viewMode = 'orders'; // 'orders' or 'drivers'
|
|
51
|
+
|
|
52
|
+
@computed('drivers', 'scheduleItems.[]', 'scheduledOrders.[]', 'viewMode') get events() {
|
|
53
|
+
if (this.viewMode === 'drivers') {
|
|
54
|
+
return this.scheduleItems.map((item) => {
|
|
55
|
+
const driver = this.drivers.find((d) => d.id === item.assignee_uuid);
|
|
56
|
+
return createFullCalendarEventFromScheduleItem(item, driver);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return this.scheduledOrders.map(createFullCalendarEventFromOrder);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@computed('drivers.[]') get calendarResources() {
|
|
63
|
+
return this.drivers.map((driver) => ({
|
|
64
|
+
id: driver.id,
|
|
65
|
+
title: driver.name,
|
|
66
|
+
extendedProps: { driver },
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get calendarStartDate() {
|
|
71
|
+
const now = new Date();
|
|
72
|
+
const dayOfWeek = now.getDay();
|
|
73
|
+
const diff = now.getDate() - dayOfWeek;
|
|
74
|
+
return new Date(now.setDate(diff)).toISOString();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get calendarEndDate() {
|
|
78
|
+
const now = new Date();
|
|
79
|
+
return new Date(now.setDate(now.getDate() + 28)).toISOString();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@task *loadDrivers() {
|
|
83
|
+
try {
|
|
84
|
+
const drivers = yield this.store.query('driver', { limit: 100 });
|
|
85
|
+
this.drivers = drivers.toArray();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.notifications.serverError(error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@task *loadScheduleItems() {
|
|
92
|
+
try {
|
|
93
|
+
const items = yield this.store.query('schedule-item', {
|
|
94
|
+
assignee_type: 'driver',
|
|
95
|
+
start_at_after: this.calendarStartDate,
|
|
96
|
+
end_at_before: this.calendarEndDate,
|
|
97
|
+
});
|
|
98
|
+
this.scheduleItems = items.toArray();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.notifications.serverError(error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
19
103
|
|
|
20
104
|
@action setCalendarApi(calendar) {
|
|
21
105
|
this.calendar = calendar;
|
|
@@ -79,13 +163,70 @@ export default class OperationsSchedulerIndexController extends Controller {
|
|
|
79
163
|
});
|
|
80
164
|
}
|
|
81
165
|
|
|
166
|
+
@action async switchViewMode(mode) {
|
|
167
|
+
this.viewMode = mode;
|
|
168
|
+
if (mode === 'drivers') {
|
|
169
|
+
await this.loadDrivers.perform();
|
|
170
|
+
await this.loadScheduleItems.perform();
|
|
171
|
+
later(() => {
|
|
172
|
+
if (this.calendar) {
|
|
173
|
+
this.calendar.changeView('resourceTimelineWeek');
|
|
174
|
+
}
|
|
175
|
+
}, 100);
|
|
176
|
+
} else {
|
|
177
|
+
later(() => {
|
|
178
|
+
if (this.calendar) {
|
|
179
|
+
this.calendar.changeView('dayGridMonth');
|
|
180
|
+
}
|
|
181
|
+
}, 100);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
82
185
|
@action viewOrderAsEvent(eventClickInfo) {
|
|
83
186
|
const { event } = eventClickInfo;
|
|
187
|
+
if (event.extendedProps && event.extendedProps.scheduleItem) {
|
|
188
|
+
return this.viewScheduleItem(event.extendedProps.scheduleItem, event.extendedProps.driver);
|
|
189
|
+
}
|
|
84
190
|
const order = this.store.peekRecord('order', event.id);
|
|
85
|
-
|
|
86
191
|
this.viewEvent(order, eventClickInfo);
|
|
87
192
|
}
|
|
88
193
|
|
|
194
|
+
@action viewScheduleItem(scheduleItem, driver) {
|
|
195
|
+
this.modalsManager.show('modals/driver-shift', {
|
|
196
|
+
title: `${driver.name} - Shift Details`,
|
|
197
|
+
acceptButtonText: 'Save Changes',
|
|
198
|
+
acceptButtonIcon: 'save',
|
|
199
|
+
scheduleItem,
|
|
200
|
+
driver,
|
|
201
|
+
confirm: async (modal) => {
|
|
202
|
+
modal.startLoading();
|
|
203
|
+
try {
|
|
204
|
+
await scheduleItem.save();
|
|
205
|
+
this.notifications.success('Shift updated successfully');
|
|
206
|
+
await this.loadScheduleItems.perform();
|
|
207
|
+
modal.done();
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.notifications.serverError(error);
|
|
210
|
+
modal.stopLoading();
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
delete: async (modal) => {
|
|
214
|
+
if (confirm('Are you sure you want to delete this shift?')) {
|
|
215
|
+
modal.startLoading();
|
|
216
|
+
try {
|
|
217
|
+
await scheduleItem.destroyRecord();
|
|
218
|
+
this.notifications.success('Shift deleted successfully');
|
|
219
|
+
await this.loadScheduleItems.perform();
|
|
220
|
+
modal.done();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.notifications.serverError(error);
|
|
223
|
+
modal.stopLoading();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
89
230
|
@action async scheduleEventFromDrop(dropInfo) {
|
|
90
231
|
const { draggedEl, date } = dropInfo;
|
|
91
232
|
const { dataset } = draggedEl;
|
|
@@ -112,17 +253,35 @@ export default class OperationsSchedulerIndexController extends Controller {
|
|
|
112
253
|
|
|
113
254
|
@action async rescheduleEventFromDrag(eventDropInfo) {
|
|
114
255
|
const { event } = eventDropInfo;
|
|
115
|
-
const { start } = event;
|
|
256
|
+
const { start, end } = event;
|
|
257
|
+
|
|
258
|
+
if (event.extendedProps && event.extendedProps.scheduleItem) {
|
|
259
|
+
const scheduleItem = event.extendedProps.scheduleItem;
|
|
260
|
+
const newResourceId = event.getResources()[0]?.id;
|
|
261
|
+
try {
|
|
262
|
+
scheduleItem.set('start_at', start);
|
|
263
|
+
scheduleItem.set('end_at', end || start);
|
|
264
|
+
if (newResourceId && newResourceId !== scheduleItem.assignee_uuid) {
|
|
265
|
+
scheduleItem.set('assignee_uuid', newResourceId);
|
|
266
|
+
}
|
|
267
|
+
await scheduleItem.save();
|
|
268
|
+
this.notifications.success('Shift rescheduled successfully');
|
|
269
|
+
await this.loadScheduleItems.perform();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
this.notifications.serverError(error);
|
|
272
|
+
eventDropInfo.revert();
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
116
277
|
const order = this.store.peekRecord('order', event.id);
|
|
117
278
|
const scheduledTime = order.scheduledAtTime;
|
|
118
279
|
const newDate = new Date(`${format(start, 'PP')} ${scheduledTime}`);
|
|
119
280
|
|
|
120
281
|
try {
|
|
121
|
-
// set and save order props
|
|
122
282
|
order.set('scheduled_at', isValidDate(newDate) ? newDate : start);
|
|
123
283
|
await order.save();
|
|
124
284
|
this.setEventProperty(event, 'title', createOrderEventTitle(order));
|
|
125
|
-
|
|
126
285
|
return this.hostRouter.refresh();
|
|
127
286
|
} catch (error) {
|
|
128
287
|
this.notifications.serverError(error);
|
|
@@ -130,6 +289,36 @@ export default class OperationsSchedulerIndexController extends Controller {
|
|
|
130
289
|
}
|
|
131
290
|
}
|
|
132
291
|
|
|
292
|
+
@action async addDriverShift() {
|
|
293
|
+
this.modalsManager.show('modals/add-driver-shift', {
|
|
294
|
+
title: 'Add Driver Shift',
|
|
295
|
+
acceptButtonText: 'Create Shift',
|
|
296
|
+
acceptButtonIcon: 'plus',
|
|
297
|
+
drivers: this.drivers,
|
|
298
|
+
confirm: async (modal) => {
|
|
299
|
+
modal.startLoading();
|
|
300
|
+
const { driver, startAt, endAt, duration } = modal.getOptions();
|
|
301
|
+
try {
|
|
302
|
+
const scheduleItem = this.store.createRecord('schedule-item', {
|
|
303
|
+
assignee_type: 'driver',
|
|
304
|
+
assignee_uuid: driver.id,
|
|
305
|
+
start_at: startAt,
|
|
306
|
+
end_at: endAt,
|
|
307
|
+
duration: duration,
|
|
308
|
+
status: 'pending',
|
|
309
|
+
});
|
|
310
|
+
await scheduleItem.save();
|
|
311
|
+
this.notifications.success('Shift created successfully');
|
|
312
|
+
await this.loadScheduleItems.perform();
|
|
313
|
+
modal.done();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
this.notifications.serverError(error);
|
|
316
|
+
modal.stopLoading();
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
133
322
|
removeEvent(event) {
|
|
134
323
|
if (isObject(event) && typeof event.remove === 'function') {
|
|
135
324
|
event.remove();
|
|
@@ -14,43 +14,47 @@ export default class OperationsServiceRatesIndexController extends Controller {
|
|
|
14
14
|
@tracked sort = '-created_at';
|
|
15
15
|
|
|
16
16
|
/** action buttons */
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
get actionButtons() {
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
icon: 'refresh',
|
|
21
|
+
onClick: this.serviceRateActions.refresh,
|
|
22
|
+
helpText: this.intl.t('common.refresh'),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
text: this.intl.t('common.new'),
|
|
26
|
+
type: 'primary',
|
|
27
|
+
icon: 'plus',
|
|
28
|
+
onClick: this.serviceRateActions.transition.create,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
text: this.intl.t('common.export'),
|
|
32
|
+
icon: 'long-arrow-up',
|
|
33
|
+
iconClass: 'rotate-icon-45',
|
|
34
|
+
wrapperClass: 'hidden md:flex',
|
|
35
|
+
onClick: this.serviceRateActions.export,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
37
39
|
|
|
38
40
|
/** bulk action buttons */
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
get bulkActions() {
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
label: 'Delete selected...',
|
|
45
|
+
class: 'text-red-500',
|
|
46
|
+
fn: this.serviceRateActions.bulkDelete,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
46
50
|
|
|
47
51
|
/** columns **/
|
|
48
52
|
get columns() {
|
|
49
53
|
return [
|
|
50
54
|
{
|
|
55
|
+
sticky: true,
|
|
51
56
|
label: this.intl.t('column.id'),
|
|
52
57
|
valuePath: 'public_id',
|
|
53
|
-
width: '150px',
|
|
54
58
|
cellComponent: 'table/cell/anchor',
|
|
55
59
|
permission: 'fleet-ops view service-rate',
|
|
56
60
|
onClick: this.serviceRateActions.transition.view,
|
|
@@ -63,7 +67,6 @@ export default class OperationsServiceRatesIndexController extends Controller {
|
|
|
63
67
|
label: this.intl.t('column.service'),
|
|
64
68
|
valuePath: 'service_name',
|
|
65
69
|
cellComponent: 'table/cell/base',
|
|
66
|
-
width: '125px',
|
|
67
70
|
resizable: true,
|
|
68
71
|
sortable: true,
|
|
69
72
|
filterable: false,
|
|
@@ -72,7 +75,6 @@ export default class OperationsServiceRatesIndexController extends Controller {
|
|
|
72
75
|
label: this.intl.t('column.service-area'),
|
|
73
76
|
valuePath: 'service_area.name',
|
|
74
77
|
cellComponent: 'table/cell/base',
|
|
75
|
-
width: '125px',
|
|
76
78
|
resizable: true,
|
|
77
79
|
sortable: true,
|
|
78
80
|
filterable: true,
|
|
@@ -85,7 +87,6 @@ export default class OperationsServiceRatesIndexController extends Controller {
|
|
|
85
87
|
label: this.intl.t('column.zone'),
|
|
86
88
|
valuePath: 'zone.name',
|
|
87
89
|
cellComponent: 'table/cell/base',
|
|
88
|
-
width: '125px',
|
|
89
90
|
resizable: true,
|
|
90
91
|
sortable: true,
|
|
91
92
|
filterable: true,
|
|
@@ -98,7 +99,6 @@ export default class OperationsServiceRatesIndexController extends Controller {
|
|
|
98
99
|
label: this.intl.t('column.created-at'),
|
|
99
100
|
valuePath: 'createdAt',
|
|
100
101
|
sortParam: 'created_at',
|
|
101
|
-
width: '125px',
|
|
102
102
|
resizable: true,
|
|
103
103
|
sortable: true,
|
|
104
104
|
filterable: true,
|
|
@@ -108,7 +108,6 @@ export default class OperationsServiceRatesIndexController extends Controller {
|
|
|
108
108
|
label: this.intl.t('column.updated-at'),
|
|
109
109
|
valuePath: 'updatedAt',
|
|
110
110
|
sortParam: 'updated_at',
|
|
111
|
-
width: '125px',
|
|
112
111
|
resizable: true,
|
|
113
112
|
sortable: true,
|
|
114
113
|
hidden: true,
|
|
@@ -123,7 +122,8 @@ export default class OperationsServiceRatesIndexController extends Controller {
|
|
|
123
122
|
ddButtonIconPrefix: 'fas',
|
|
124
123
|
cellClassNames: 'overflow-visible',
|
|
125
124
|
wrapperClass: 'flex items-center justify-end mx-2',
|
|
126
|
-
|
|
125
|
+
sticky: 'right',
|
|
126
|
+
width: 60,
|
|
127
127
|
actions: [
|
|
128
128
|
{
|
|
129
129
|
label: this.intl.t('column.edit-service'),
|
|
@@ -19,35 +19,29 @@ export default class SettingsPaymentsIndexController extends Controller {
|
|
|
19
19
|
label: 'Purchase Rate ID',
|
|
20
20
|
valuePath: 'public_id',
|
|
21
21
|
cellComponent: 'click-to-copy',
|
|
22
|
-
width: '20%',
|
|
23
22
|
},
|
|
24
23
|
{
|
|
25
24
|
label: 'Service Quote',
|
|
26
25
|
valuePath: 'service_quote_id',
|
|
27
26
|
cellComponent: 'click-to-copy',
|
|
28
|
-
width: '20%',
|
|
29
27
|
},
|
|
30
28
|
{
|
|
31
29
|
label: 'Order',
|
|
32
30
|
valuePath: 'order_id',
|
|
33
31
|
cellComponent: 'click-to-copy',
|
|
34
|
-
width: '20%',
|
|
35
32
|
},
|
|
36
33
|
{
|
|
37
34
|
label: 'Customer',
|
|
38
35
|
valuePath: 'customer.name',
|
|
39
|
-
width: '20%',
|
|
40
36
|
},
|
|
41
37
|
{
|
|
42
38
|
label: 'Amount',
|
|
43
39
|
valuePath: 'amount',
|
|
44
40
|
cellComponent: 'table/cell/currency',
|
|
45
|
-
width: '20%',
|
|
46
41
|
},
|
|
47
42
|
{
|
|
48
43
|
label: 'Date',
|
|
49
44
|
valuePath: 'created_at',
|
|
50
|
-
width: '20%',
|
|
51
45
|
},
|
|
52
46
|
];
|
|
53
47
|
|
package/addon/routes.js
CHANGED
|
@@ -63,6 +63,7 @@ export default buildRoutes(function () {
|
|
|
63
63
|
this.route('details', { path: '/:public_id' }, function () {
|
|
64
64
|
this.route('index', { path: '/' });
|
|
65
65
|
this.route('positions');
|
|
66
|
+
this.route('schedule');
|
|
66
67
|
});
|
|
67
68
|
this.route('edit', { path: '/edit/:public_id' });
|
|
68
69
|
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import Service, { inject as service } from '@ember/service';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { task } from 'ember-concurrency';
|
|
4
|
+
|
|
5
|
+
export default class DriverSchedulingService extends Service {
|
|
6
|
+
@service store;
|
|
7
|
+
@service fetch;
|
|
8
|
+
@service notifications;
|
|
9
|
+
@tracked currentSchedule = null;
|
|
10
|
+
@tracked scheduleItems = [];
|
|
11
|
+
@tracked constraints = [];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Load a schedule by ID
|
|
15
|
+
*/
|
|
16
|
+
@task *loadSchedule(scheduleId) {
|
|
17
|
+
try {
|
|
18
|
+
const schedule = yield this.store.findRecord('schedule', scheduleId, {
|
|
19
|
+
include: 'items',
|
|
20
|
+
reload: true,
|
|
21
|
+
});
|
|
22
|
+
this.currentSchedule = schedule;
|
|
23
|
+
return schedule;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
this.notifications.serverError(error);
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a new schedule
|
|
32
|
+
*/
|
|
33
|
+
@task *createSchedule(data) {
|
|
34
|
+
try {
|
|
35
|
+
const schedule = this.store.createRecord('schedule', data);
|
|
36
|
+
yield schedule.save();
|
|
37
|
+
this.notifications.success('Schedule created successfully');
|
|
38
|
+
return schedule;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
this.notifications.serverError(error);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a new schedule item
|
|
47
|
+
*/
|
|
48
|
+
@task *createScheduleItem(data) {
|
|
49
|
+
try {
|
|
50
|
+
const item = this.store.createRecord('schedule-item', data);
|
|
51
|
+
yield item.save();
|
|
52
|
+
this.notifications.success('Schedule item created successfully');
|
|
53
|
+
return item;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.notifications.serverError(error);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update a schedule item
|
|
62
|
+
*/
|
|
63
|
+
@task *updateScheduleItem(item, data) {
|
|
64
|
+
try {
|
|
65
|
+
item.setProperties(data);
|
|
66
|
+
yield item.save();
|
|
67
|
+
this.notifications.success('Schedule item updated successfully');
|
|
68
|
+
return item;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.notifications.serverError(error);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Delete a schedule item
|
|
77
|
+
*/
|
|
78
|
+
@task *deleteScheduleItem(item) {
|
|
79
|
+
try {
|
|
80
|
+
yield item.destroyRecord();
|
|
81
|
+
this.notifications.success('Schedule item deleted successfully');
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.notifications.serverError(error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get schedule items for an assignee
|
|
90
|
+
*/
|
|
91
|
+
@task *getScheduleItemsForAssignee(assigneeType, assigneeUuid, filters = {}) {
|
|
92
|
+
try {
|
|
93
|
+
const items = yield this.store.query('schedule-item', {
|
|
94
|
+
assignee_type: assigneeType,
|
|
95
|
+
assignee_uuid: assigneeUuid,
|
|
96
|
+
...filters,
|
|
97
|
+
});
|
|
98
|
+
this.scheduleItems = items.toArray();
|
|
99
|
+
return items;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.notifications.serverError(error);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check availability for a subject
|
|
108
|
+
*/
|
|
109
|
+
@task *checkAvailability(subjectType, subjectUuid, startAt, endAt) {
|
|
110
|
+
try {
|
|
111
|
+
const response = yield this.fetch.get('schedule-availability/check', {
|
|
112
|
+
subject_type: subjectType,
|
|
113
|
+
subject_uuid: subjectUuid,
|
|
114
|
+
start_at: startAt,
|
|
115
|
+
end_at: endAt,
|
|
116
|
+
});
|
|
117
|
+
return response.available;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
this.notifications.serverError(error);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Set availability for a subject
|
|
126
|
+
*/
|
|
127
|
+
@task *setAvailability(data) {
|
|
128
|
+
try {
|
|
129
|
+
const availability = this.store.createRecord('schedule-availability', data);
|
|
130
|
+
yield availability.save();
|
|
131
|
+
this.notifications.success('Availability set successfully');
|
|
132
|
+
return availability;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
this.notifications.serverError(error);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Load constraints for a subject
|
|
141
|
+
*/
|
|
142
|
+
@task *loadConstraints(subjectType, subjectUuid) {
|
|
143
|
+
try {
|
|
144
|
+
const constraints = yield this.store.query('schedule-constraint', {
|
|
145
|
+
subject_type: subjectType,
|
|
146
|
+
subject_uuid: subjectUuid,
|
|
147
|
+
is_active: true,
|
|
148
|
+
});
|
|
149
|
+
this.constraints = constraints.toArray();
|
|
150
|
+
return constraints;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
this.notifications.serverError(error);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validate a schedule item against constraints
|
|
159
|
+
*/
|
|
160
|
+
@task *validateScheduleItem(item) {
|
|
161
|
+
try {
|
|
162
|
+
const response = yield this.fetch.post('schedule-items/validate', {
|
|
163
|
+
schedule_item: item.serialize(),
|
|
164
|
+
});
|
|
165
|
+
return response.violations || [];
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.notifications.serverError(error);
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Service, { inject as service } from '@ember/service';
|
|
2
2
|
import { next } from '@ember/runloop';
|
|
3
|
+
import { isNone } from '@ember/utils';
|
|
3
4
|
|
|
4
5
|
const L = window.L || window.leaflet;
|
|
5
6
|
export default class LeafletLayerVisibilityService extends Service {
|
|
@@ -203,7 +204,9 @@ export default class LeafletLayerVisibilityService extends Service {
|
|
|
203
204
|
if (layer.setStyle) {
|
|
204
205
|
const base = { opacity: 1 };
|
|
205
206
|
// leaflets default fillOpacity if fill:true is ~0.2
|
|
206
|
-
|
|
207
|
+
const fillOpacity = layer.options?.fillOpacity ?? 0.2;
|
|
208
|
+
const hasFillOpacity = !isNone(layer.options?.fill);
|
|
209
|
+
base.fillOpacity = hasFillOpacity ? fillOpacity : 0;
|
|
207
210
|
layer.setStyle(base);
|
|
208
211
|
} else if (layer.setOpacity) {
|
|
209
212
|
layer.setOpacity(1);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
|
|
2
2
|
import { task } from 'ember-concurrency';
|
|
3
|
+
import { isNone } from '@ember/utils';
|
|
3
4
|
import serializePayload from '../utils/serialize-payload';
|
|
4
5
|
|
|
5
6
|
export default class ServiceRateActionsService extends ResourceActionService {
|
|
@@ -115,12 +116,15 @@ export default class ServiceRateActionsService extends ResourceActionService {
|
|
|
115
116
|
@task *getServiceQuotes(serviceRate, order) {
|
|
116
117
|
if (order.payload.payloadCoordinates.length < 2) return;
|
|
117
118
|
|
|
119
|
+
const hasFacilitator = !isNone(order.facilitator);
|
|
120
|
+
const facilitatorServiceType = order.facilitator?.get('service_types.firstObject.key') ?? order.type;
|
|
121
|
+
|
|
118
122
|
try {
|
|
119
123
|
const serviceQuotes = yield this.fetch.post('service-quotes/preliminary', {
|
|
120
124
|
payload: serializePayload(order.payload),
|
|
121
125
|
distance: order.route.summary?.totalDistance,
|
|
122
126
|
time: order.route.summary?.totalTime,
|
|
123
|
-
service_type:
|
|
127
|
+
service_type: hasFacilitator ? facilitatorServiceType : order.type,
|
|
124
128
|
facilitator: order.facilitator?.public_id,
|
|
125
129
|
scheduled_at: order.scheduled_at,
|
|
126
130
|
is_route_optimized: order.optimized,
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
{{
|
|
2
|
-
{{outlet}}
|
|
1
|
+
<PositionsReplay @resource={{@model}} />
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
{{outlet}}
|
|
1
|
+
<Driver::Schedule @resource={{@model}} />
|