@fleetbase/fleetops-engine 0.6.67 → 0.6.69
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/RELEASE.md +11 -15
- package/addon/components/contact/form.hbs +13 -2
- package/addon/components/customer/details.hbs +17 -17
- package/addon/components/customer/form.hbs +13 -2
- package/addon/components/customer/form.js +0 -56
- package/addon/components/driver/details.hbs +13 -19
- package/addon/components/driver/form.hbs +20 -35
- package/addon/components/driver/form.js +0 -58
- package/addon/components/fuel-integration/form.hbs +2 -2
- package/addon/components/fuel-integration/form.js +8 -0
- package/addon/components/fuel-integration/hub.js +1 -1
- package/addon/components/modals/driver-form.hbs +2 -2
- package/addon/components/modals/reset-profile-credentials.hbs +23 -0
- package/addon/components/modals/reset-profile-credentials.js +70 -0
- package/addon/components/profile-identity-hint.hbs +13 -0
- package/addon/components/profile-identity-hint.js +49 -0
- package/addon/controllers/management/drivers/index.js +4 -0
- package/addon/extension.js +4 -2
- package/addon/services/contact-actions.js +50 -89
- package/addon/services/driver-actions.js +98 -0
- package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
- package/addon/utils/fuel-transaction.js +2 -0
- package/addon/utils/profile-login-actions.js +172 -0
- package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
- package/app/components/profile-identity-hint.js +1 -0
- package/composer.json +1 -1
- package/docs/SAFEE.md +5 -0
- package/docs/TELEMATICS_QUEUES.md +59 -8
- package/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -0
- package/server/config/fuel-providers.php +53 -0
- package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
- package/server/resources/views/mail/driver-credentials.blade.php +21 -0
- package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
- package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
- package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
- package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
- package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
- package/server/src/Http/Resources/v1/Contact.php +3 -0
- package/server/src/Http/Resources/v1/Driver.php +3 -0
- package/server/src/Jobs/PollTelematicTelemetry.php +36 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +2 -2
- package/server/src/Mail/DriverCredentialsMail.php +55 -0
- package/server/src/Models/Contact.php +55 -90
- package/server/src/Models/Sensor.php +3 -5
- package/server/src/Observers/ContactObserver.php +2 -22
- package/server/src/Observers/DriverObserver.php +5 -6
- package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
- package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
- package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
- package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
- package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
- package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
- package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
- package/server/src/Support/ProfileAccountManager.php +405 -0
- package/server/src/Support/Telematics/TelematicService.php +6 -4
- package/server/src/Support/Telematics/Telemetry/Inbox.php +27 -9
- package/server/src/routes.php +5 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
- package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
- package/server/tests/ApiDriverControllerContractsTest.php +102 -20
- package/server/tests/CommandContractsTest.php +4 -4
- package/server/tests/ControllerHelperContractsTest.php +3 -11
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
- package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
- package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
- package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
- package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
- package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
- package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
- package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
- package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
- package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
- package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +88 -4
- package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
- package/server/tests/ModelAccessorContractsTest.php +82 -20
- package/server/tests/ObserverContractsTest.php +27 -2
- package/server/tests/RequestContractsTest.php +19 -11
- package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
- package/server/tests/TelematicControllerContractsTest.php +22 -4
- package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
- package/server/tests/Unit/Models/ContactTest.php +48 -5
- package/server/tests/Unit/Models/SensorTest.php +2 -2
- package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
- package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
- package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
- package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
- package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
- package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
- package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
- package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
- package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
- package/translations/en-us.yaml +37 -2
- package/translations/uk-ua.yaml +3475 -0
- package/addon/components/modals/reset-customer-credentials.hbs +0 -18
- package/addon/components/modals/reset-customer-credentials.js +0 -55
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
import { action } from '@ember/object';
|
|
5
|
+
import { task, timeout } from 'ember-concurrency';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Inline hint shown while creating a driver, contact or customer profile:
|
|
9
|
+
* tells the user when the entered email/phone belongs to an existing team
|
|
10
|
+
* member (the profile will be linked to that account) or cannot be used.
|
|
11
|
+
*
|
|
12
|
+
* Args: `@email`, `@phone`, `@type` ('driver' | 'customer' | 'contact'),
|
|
13
|
+
* `@ignore` (optional profile id).
|
|
14
|
+
*/
|
|
15
|
+
export default class ProfileIdentityHintComponent extends Component {
|
|
16
|
+
@service fetch;
|
|
17
|
+
@tracked staff = null;
|
|
18
|
+
@tracked conflict = null;
|
|
19
|
+
|
|
20
|
+
@action lookupIdentity() {
|
|
21
|
+
this.lookup.perform();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@task({ restartable: true }) *lookup() {
|
|
25
|
+
const { email, phone, type, ignore } = this.args;
|
|
26
|
+
|
|
27
|
+
if (!email && !phone) {
|
|
28
|
+
this.staff = null;
|
|
29
|
+
this.conflict = null;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
yield timeout(400);
|
|
34
|
+
|
|
35
|
+
const query = { type };
|
|
36
|
+
if (email) query.email = email;
|
|
37
|
+
if (phone) query.phone = phone;
|
|
38
|
+
if (ignore) query.ignore = ignore;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = yield this.fetch.get('fleet-ops/lookup/profile-identity', query, { namespace: 'int/v1' });
|
|
42
|
+
this.staff = response?.staff ?? null;
|
|
43
|
+
this.conflict = response?.conflict ?? null;
|
|
44
|
+
} catch {
|
|
45
|
+
this.staff = null;
|
|
46
|
+
this.conflict = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -358,6 +358,10 @@ export default class ManagementDriversIndexController extends Controller {
|
|
|
358
358
|
{
|
|
359
359
|
separator: true,
|
|
360
360
|
},
|
|
361
|
+
...this.driverActions.accountRowActionItems(),
|
|
362
|
+
{
|
|
363
|
+
separator: true,
|
|
364
|
+
},
|
|
361
365
|
{
|
|
362
366
|
label: this.intl.t('common.delete-resource', { resource: this.intl.t('resource.driver') }),
|
|
363
367
|
icon: 'trash',
|
package/addon/extension.js
CHANGED
|
@@ -115,8 +115,10 @@ export default {
|
|
|
115
115
|
route: 'virtual',
|
|
116
116
|
slug: 'track-order',
|
|
117
117
|
icon: 'barcode',
|
|
118
|
-
type: '
|
|
119
|
-
|
|
118
|
+
type: 'default',
|
|
119
|
+
// btn-auth (ember-ui): the sign-in page's neutral button, matching the
|
|
120
|
+
// console's "Continue with ..." provider buttons, hover included.
|
|
121
|
+
wrapperClass: 'btn-block btn-auth',
|
|
120
122
|
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'order-tracking-lookup'),
|
|
121
123
|
onClick: (menuItem) => {
|
|
122
124
|
universe.transitionMenuItem('virtual', menuItem);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import ResourceActionService from '@fleetbase/ember-core/services/resource-action';
|
|
2
2
|
import { inject as service } from '@ember/service';
|
|
3
|
-
import { action
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
4
|
import { PANEL_DEFAULTS, closePanelsThen, registeredPanelTabs } from '../utils/context-panel';
|
|
5
|
-
|
|
6
|
-
const INTERNAL_NAMESPACE = 'int/v1';
|
|
5
|
+
import { confirmLoginAction, hasInactiveLogin, hasLinkedUser, hasManagedLogin, linkedUserStatus, updateProfileFromResponse } from '../utils/profile-login-actions';
|
|
7
6
|
|
|
8
7
|
export default class ContactActionsService extends ResourceActionService {
|
|
9
8
|
@service fetch;
|
|
@@ -124,17 +123,23 @@ export default class ContactActionsService extends ResourceActionService {
|
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
hasLinkedUser(contact) {
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
return hasLinkedUser(contact);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* True when the contact has a login account managed from Fleet-Ops. A
|
|
131
|
+
* contact owned by a staff member is managed from IAM instead.
|
|
132
|
+
*/
|
|
133
|
+
hasManagedLogin(contact) {
|
|
134
|
+
return hasManagedLogin(contact);
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
linkedUserStatus(contact) {
|
|
132
|
-
|
|
133
|
-
return (user ? (get(user, 'status') ?? get(user, 'session_status')) : null) ?? 'active';
|
|
138
|
+
return linkedUserStatus(contact);
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
hasInactiveLogin(contact) {
|
|
137
|
-
return
|
|
142
|
+
return hasInactiveLogin(contact);
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
isCustomerPortalInstalled() {
|
|
@@ -156,7 +161,7 @@ export default class ContactActionsService extends ResourceActionService {
|
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
accountActionButton(contact, options = {}) {
|
|
159
|
-
if (!this.
|
|
164
|
+
if (!this.hasManagedLogin(contact)) {
|
|
160
165
|
return null;
|
|
161
166
|
}
|
|
162
167
|
|
|
@@ -171,23 +176,23 @@ export default class ContactActionsService extends ResourceActionService {
|
|
|
171
176
|
accountActionItems(contact, options = {}) {
|
|
172
177
|
const actions = [
|
|
173
178
|
{
|
|
174
|
-
text: '
|
|
179
|
+
text: this.intl.t('profile-account.actions.reset-password'),
|
|
175
180
|
icon: 'key',
|
|
176
181
|
fn: () => this.openResetPasswordModal(contact),
|
|
177
182
|
},
|
|
178
183
|
{
|
|
179
|
-
text: '
|
|
184
|
+
text: this.intl.t('profile-account.actions.send-credentials'),
|
|
180
185
|
icon: 'paper-plane',
|
|
181
186
|
fn: () => this.confirmSendCredentials(contact),
|
|
182
187
|
},
|
|
183
188
|
this.hasInactiveLogin(contact)
|
|
184
189
|
? {
|
|
185
|
-
text: '
|
|
190
|
+
text: this.intl.t('profile-account.actions.reactivate-login'),
|
|
186
191
|
icon: 'unlock',
|
|
187
192
|
fn: () => this.confirmReactivatePortalLogin(contact),
|
|
188
193
|
}
|
|
189
194
|
: {
|
|
190
|
-
text: '
|
|
195
|
+
text: this.intl.t('profile-account.actions.deactivate-login'),
|
|
191
196
|
icon: 'lock',
|
|
192
197
|
fn: () => this.confirmDeactivatePortalLogin(contact),
|
|
193
198
|
class: 'text-red-500 hover:text-red-600',
|
|
@@ -200,7 +205,7 @@ export default class ContactActionsService extends ResourceActionService {
|
|
|
200
205
|
separator: true,
|
|
201
206
|
},
|
|
202
207
|
{
|
|
203
|
-
text: '
|
|
208
|
+
text: this.intl.t('profile-account.actions.convert-to-vendor'),
|
|
204
209
|
icon: 'building',
|
|
205
210
|
fn: () => this.openConvertToVendorModal(contact, options),
|
|
206
211
|
}
|
|
@@ -212,42 +217,42 @@ export default class ContactActionsService extends ResourceActionService {
|
|
|
212
217
|
|
|
213
218
|
accountRowActionItems(options = {}) {
|
|
214
219
|
const hasCustomerPortal = this.isCustomerPortalInstalled();
|
|
215
|
-
const
|
|
220
|
+
const hasManagedLogin = (contact) => this.hasManagedLogin(contact);
|
|
216
221
|
|
|
217
222
|
const actions = [
|
|
218
223
|
{
|
|
219
|
-
label: '
|
|
224
|
+
label: this.intl.t('profile-account.actions.reset-password'),
|
|
220
225
|
icon: 'key',
|
|
221
226
|
fn: (contact) => this.openResetPasswordModal(contact),
|
|
222
|
-
isVisible:
|
|
227
|
+
isVisible: hasManagedLogin,
|
|
223
228
|
},
|
|
224
229
|
{
|
|
225
|
-
label: '
|
|
230
|
+
label: this.intl.t('profile-account.actions.send-credentials'),
|
|
226
231
|
icon: 'paper-plane',
|
|
227
232
|
fn: (contact) => this.confirmSendCredentials(contact),
|
|
228
|
-
isVisible:
|
|
233
|
+
isVisible: hasManagedLogin,
|
|
229
234
|
},
|
|
230
235
|
{
|
|
231
|
-
label: '
|
|
236
|
+
label: this.intl.t('profile-account.actions.deactivate-login'),
|
|
232
237
|
icon: 'lock',
|
|
233
238
|
class: 'text-red-500 hover:text-red-600',
|
|
234
239
|
fn: (contact) => this.confirmDeactivatePortalLogin(contact),
|
|
235
|
-
isVisible: (contact) =>
|
|
240
|
+
isVisible: (contact) => hasManagedLogin(contact) && !this.hasInactiveLogin(contact),
|
|
236
241
|
},
|
|
237
242
|
{
|
|
238
|
-
label: '
|
|
243
|
+
label: this.intl.t('profile-account.actions.reactivate-login'),
|
|
239
244
|
icon: 'unlock',
|
|
240
245
|
fn: (contact) => this.confirmReactivatePortalLogin(contact),
|
|
241
|
-
isVisible: (contact) =>
|
|
246
|
+
isVisible: (contact) => hasManagedLogin(contact) && this.hasInactiveLogin(contact),
|
|
242
247
|
},
|
|
243
248
|
];
|
|
244
249
|
|
|
245
250
|
if (hasCustomerPortal) {
|
|
246
251
|
actions.push({
|
|
247
|
-
label: '
|
|
252
|
+
label: this.intl.t('profile-account.actions.convert-to-vendor'),
|
|
248
253
|
icon: 'building',
|
|
249
254
|
fn: (contact) => this.openConvertToVendorModal(contact, options),
|
|
250
|
-
isVisible:
|
|
255
|
+
isVisible: hasManagedLogin,
|
|
251
256
|
});
|
|
252
257
|
}
|
|
253
258
|
|
|
@@ -255,8 +260,10 @@ export default class ContactActionsService extends ResourceActionService {
|
|
|
255
260
|
}
|
|
256
261
|
|
|
257
262
|
openResetPasswordModal(contact) {
|
|
258
|
-
this.modalsManager.show('modals/reset-
|
|
259
|
-
|
|
263
|
+
this.modalsManager.show('modals/reset-profile-credentials', {
|
|
264
|
+
profile: contact,
|
|
265
|
+
endpoint: 'customers/reset-credentials',
|
|
266
|
+
payload: (profile) => ({ customer: profile?.id }),
|
|
260
267
|
});
|
|
261
268
|
}
|
|
262
269
|
|
|
@@ -278,86 +285,40 @@ export default class ContactActionsService extends ResourceActionService {
|
|
|
278
285
|
|
|
279
286
|
confirmSendCredentials(contact) {
|
|
280
287
|
this.confirmLoginAction(contact, {
|
|
281
|
-
title: '
|
|
282
|
-
body: '
|
|
283
|
-
acceptButtonText: '
|
|
288
|
+
title: this.intl.t('profile-account.prompts.send-portal-credentials-title'),
|
|
289
|
+
body: this.intl.t('profile-account.prompts.send-portal-credentials-body'),
|
|
290
|
+
acceptButtonText: this.intl.t('profile-account.actions.send-credentials'),
|
|
284
291
|
endpoint: 'customers/send-credentials',
|
|
285
|
-
successMessage: '
|
|
292
|
+
successMessage: this.intl.t('profile-account.prompts.send-portal-credentials-success'),
|
|
286
293
|
});
|
|
287
294
|
}
|
|
288
295
|
|
|
289
296
|
confirmDeactivatePortalLogin(contact) {
|
|
290
297
|
this.confirmLoginAction(contact, {
|
|
291
|
-
title: '
|
|
292
|
-
body:
|
|
293
|
-
acceptButtonText: '
|
|
298
|
+
title: this.intl.t('profile-account.actions.deactivate-login'),
|
|
299
|
+
body: this.intl.t('profile-account.prompts.deactivate-portal-login-body'),
|
|
300
|
+
acceptButtonText: this.intl.t('profile-account.actions.deactivate-login'),
|
|
294
301
|
acceptButtonScheme: 'danger',
|
|
295
302
|
endpoint: 'customers/deactivate-portal-login',
|
|
296
|
-
successMessage: '
|
|
303
|
+
successMessage: this.intl.t('profile-account.prompts.deactivate-portal-login-success'),
|
|
297
304
|
});
|
|
298
305
|
}
|
|
299
306
|
|
|
300
307
|
confirmReactivatePortalLogin(contact) {
|
|
301
308
|
this.confirmLoginAction(contact, {
|
|
302
|
-
title: '
|
|
303
|
-
body:
|
|
304
|
-
acceptButtonText: '
|
|
309
|
+
title: this.intl.t('profile-account.actions.reactivate-login'),
|
|
310
|
+
body: this.intl.t('profile-account.prompts.reactivate-portal-login-body'),
|
|
311
|
+
acceptButtonText: this.intl.t('profile-account.actions.reactivate-login'),
|
|
305
312
|
endpoint: 'customers/reactivate-portal-login',
|
|
306
|
-
successMessage: '
|
|
313
|
+
successMessage: this.intl.t('profile-account.prompts.reactivate-portal-login-success'),
|
|
307
314
|
});
|
|
308
315
|
}
|
|
309
316
|
|
|
310
|
-
confirmLoginAction(contact,
|
|
311
|
-
this
|
|
312
|
-
title,
|
|
313
|
-
body,
|
|
314
|
-
acceptButtonText,
|
|
315
|
-
acceptButtonScheme,
|
|
316
|
-
confirm: async (modal) => {
|
|
317
|
-
modal.startLoading();
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
const response = await this.fetch.post(endpoint, { customer: contact?.id }, { namespace: INTERNAL_NAMESPACE });
|
|
321
|
-
await this.updateContactFromResponse(contact, response);
|
|
322
|
-
this.notifications.success(successMessage);
|
|
323
|
-
modal.done();
|
|
324
|
-
} catch (error) {
|
|
325
|
-
this.notifications.serverError(error);
|
|
326
|
-
modal.stopLoading();
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
});
|
|
317
|
+
confirmLoginAction(contact, options = {}) {
|
|
318
|
+
return confirmLoginAction(this, contact, { payload: { customer: contact?.id }, ...options });
|
|
330
319
|
}
|
|
331
320
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (!payload || typeof contact?.setProperties !== 'function') {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
contact.setProperties(
|
|
340
|
-
this.withoutIdentityFields({
|
|
341
|
-
user_uuid: payload.user_uuid,
|
|
342
|
-
})
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
if (payload.user) {
|
|
346
|
-
const user = await contact.user;
|
|
347
|
-
|
|
348
|
-
if (typeof user?.setProperties === 'function') {
|
|
349
|
-
user.setProperties(this.withoutIdentityFields(payload.user));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
withoutIdentityFields(payload = {}) {
|
|
355
|
-
const attributes = { ...payload };
|
|
356
|
-
|
|
357
|
-
delete attributes.id;
|
|
358
|
-
delete attributes.uuid;
|
|
359
|
-
delete attributes.public_id;
|
|
360
|
-
|
|
361
|
-
return attributes;
|
|
321
|
+
updateContactFromResponse(contact, response) {
|
|
322
|
+
return updateProfileFromResponse(contact, response);
|
|
362
323
|
}
|
|
363
324
|
}
|
|
@@ -5,6 +5,7 @@ import { action } from '@ember/object';
|
|
|
5
5
|
import { isArray } from '@ember/array';
|
|
6
6
|
import { dasherize } from '@ember/string';
|
|
7
7
|
import { PANEL_DEFAULTS, closePanelsThen } from '../utils/context-panel';
|
|
8
|
+
import { confirmLoginAction, hasInactiveLogin, hasManagedLogin } from '../utils/profile-login-actions';
|
|
8
9
|
|
|
9
10
|
export default class DriverActionsService extends ResourceActionService {
|
|
10
11
|
@service('universe/menu-service') menuService;
|
|
@@ -422,9 +423,106 @@ export default class DriverActionsService extends ResourceActionService {
|
|
|
422
423
|
{ separator: true },
|
|
423
424
|
{ text: this.intl.t('driver.actions.locate-driver'), icon: 'location-dot', fn: () => this.locate(driver), permission: 'fleet-ops view driver' },
|
|
424
425
|
{ text: this.intl.t('driver.actions.create-issue'), icon: 'triangle-exclamation', fn: () => this.createIssue(driver), permission: 'fleet-ops create issue' },
|
|
426
|
+
...this.accountMenuItems(driver),
|
|
425
427
|
];
|
|
426
428
|
}
|
|
427
429
|
|
|
430
|
+
/**
|
|
431
|
+
* The login account section of a driver's actions menu. Only offered when
|
|
432
|
+
* the driver has a login managed from Fleet-Ops; a staff-linked driver's
|
|
433
|
+
* login is managed from IAM.
|
|
434
|
+
*/
|
|
435
|
+
accountMenuItems(driver) {
|
|
436
|
+
if (!hasManagedLogin(driver)) {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const items = this.accountRowActionItems()
|
|
441
|
+
.filter(({ isVisible }) => isVisible(driver))
|
|
442
|
+
// eslint-disable-next-line no-unused-vars
|
|
443
|
+
.map(({ label, fn, isVisible, ...item }) => ({ ...item, text: label, fn: () => fn(driver) }));
|
|
444
|
+
|
|
445
|
+
return [{ separator: true }, ...items];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Login account actions for the drivers table row menu.
|
|
450
|
+
*/
|
|
451
|
+
accountRowActionItems() {
|
|
452
|
+
const permission = 'fleet-ops update driver';
|
|
453
|
+
|
|
454
|
+
return [
|
|
455
|
+
{
|
|
456
|
+
label: this.intl.t('profile-account.actions.reset-password'),
|
|
457
|
+
icon: 'key',
|
|
458
|
+
fn: (driver) => this.resetCredentials(driver),
|
|
459
|
+
permission,
|
|
460
|
+
isVisible: (driver) => hasManagedLogin(driver),
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
label: this.intl.t('profile-account.actions.send-credentials'),
|
|
464
|
+
icon: 'paper-plane',
|
|
465
|
+
fn: (driver) => this.sendCredentials(driver),
|
|
466
|
+
permission,
|
|
467
|
+
isVisible: (driver) => hasManagedLogin(driver),
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
label: this.intl.t('profile-account.actions.deactivate-login'),
|
|
471
|
+
icon: 'lock',
|
|
472
|
+
class: 'text-red-500 hover:text-red-600',
|
|
473
|
+
fn: (driver) => this.deactivateLogin(driver),
|
|
474
|
+
permission,
|
|
475
|
+
isVisible: (driver) => hasManagedLogin(driver) && !hasInactiveLogin(driver),
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
label: this.intl.t('profile-account.actions.reactivate-login'),
|
|
479
|
+
icon: 'unlock',
|
|
480
|
+
fn: (driver) => this.reactivateLogin(driver),
|
|
481
|
+
permission,
|
|
482
|
+
isVisible: (driver) => hasManagedLogin(driver) && hasInactiveLogin(driver),
|
|
483
|
+
},
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@action resetCredentials(driver, options = {}) {
|
|
488
|
+
return this.modalsManager.show('modals/reset-profile-credentials', {
|
|
489
|
+
profile: driver,
|
|
490
|
+
endpoint: `drivers/${driver.id}/reset-credentials`,
|
|
491
|
+
...options,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
@action sendCredentials(driver) {
|
|
496
|
+
return confirmLoginAction(this, driver, {
|
|
497
|
+
title: this.intl.t('profile-account.prompts.send-credentials-title'),
|
|
498
|
+
body: this.intl.t('profile-account.prompts.send-credentials-body', { name: driver.name }),
|
|
499
|
+
acceptButtonText: this.intl.t('profile-account.actions.send-credentials'),
|
|
500
|
+
endpoint: `drivers/${driver.id}/send-credentials`,
|
|
501
|
+
successMessage: this.intl.t('profile-account.prompts.send-credentials-success', { name: driver.name }),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@action deactivateLogin(driver) {
|
|
506
|
+
return confirmLoginAction(this, driver, {
|
|
507
|
+
title: this.intl.t('profile-account.actions.deactivate-login'),
|
|
508
|
+
body: this.intl.t('profile-account.prompts.deactivate-login-body', { name: driver.name }),
|
|
509
|
+
acceptButtonText: this.intl.t('profile-account.actions.deactivate-login'),
|
|
510
|
+
acceptButtonScheme: 'danger',
|
|
511
|
+
endpoint: `drivers/${driver.id}/deactivate-login`,
|
|
512
|
+
successMessage: this.intl.t('profile-account.prompts.deactivate-login-success', { name: driver.name }),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
@action reactivateLogin(driver) {
|
|
517
|
+
return confirmLoginAction(this, driver, {
|
|
518
|
+
title: this.intl.t('profile-account.actions.reactivate-login'),
|
|
519
|
+
body: this.intl.t('profile-account.prompts.reactivate-login-body', { name: driver.name }),
|
|
520
|
+
acceptButtonText: this.intl.t('profile-account.actions.reactivate-login'),
|
|
521
|
+
endpoint: `drivers/${driver.id}/reactivate-login`,
|
|
522
|
+
successMessage: this.intl.t('profile-account.prompts.reactivate-login-success', { name: driver.name }),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
428
526
|
@action createIssue(driver) {
|
|
429
527
|
return this.issueActions.modal.create({
|
|
430
528
|
driver,
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">Requested dates: {{this.details.latestRun.window}}</p>
|
|
31
31
|
{{#unless this.details.latestRun.received}}
|
|
32
32
|
<p class="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-400">If you expected purchases, choose their dates on the Sync tab and confirm that they belong to this
|
|
33
|
-
|
|
33
|
+
provider account.</p>
|
|
34
34
|
{{/unless}}
|
|
35
35
|
{{else}}
|
|
36
36
|
<p class="text-sm text-gray-900 dark:text-gray-100">No syncs yet</p>
|
|
@@ -99,6 +99,8 @@ export function purchaseGroups(transaction) {
|
|
|
99
99
|
['Account branch', value('branch_name')],
|
|
100
100
|
['Representative', value('delegate_name')],
|
|
101
101
|
['Representative phone', petroapp ? value('delegate_mobile') : null],
|
|
102
|
+
['Driver', value('driver_name')],
|
|
103
|
+
['Driver phone', value('driver_phone')],
|
|
102
104
|
]),
|
|
103
105
|
group('Vehicle & trip references', [
|
|
104
106
|
['Plate number', transaction.plate_number],
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { get } from '@ember/object';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared helpers for the login account behind a driver, contact or customer
|
|
5
|
+
* profile. The account itself is created, updated and removed by the server
|
|
6
|
+
* from the profile's name/email/phone; the console only offers the account
|
|
7
|
+
* actions (reset password, send credentials, deactivate/reactivate login).
|
|
8
|
+
*
|
|
9
|
+
* A profile owned by a staff member (`is_staff_linked`) has its login
|
|
10
|
+
* managed from IAM, so no account actions are offered for it here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const INTERNAL_NAMESPACE = 'int/v1';
|
|
14
|
+
export const INACTIVE_LOGIN_STATUSES = ['inactive', 'disabled', 'suspended'];
|
|
15
|
+
|
|
16
|
+
function read(subject, key) {
|
|
17
|
+
if (!subject) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
return get(subject, key);
|
|
23
|
+
} catch {
|
|
24
|
+
return subject[key];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The linked user record when it is already loaded, without triggering a
|
|
30
|
+
* fetch for an async relationship.
|
|
31
|
+
*/
|
|
32
|
+
export function loadedUser(profile) {
|
|
33
|
+
if (typeof profile?.belongsTo === 'function') {
|
|
34
|
+
try {
|
|
35
|
+
return profile.belongsTo('user').value();
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const user = read(profile, 'user');
|
|
42
|
+
if (!user) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return typeof user.then === 'function' ? (read(user, 'content') ?? null) : user;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isStaffLinked(profile) {
|
|
50
|
+
return Boolean(read(profile, 'is_staff_linked'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function hasLinkedUser(profile) {
|
|
54
|
+
if (!profile) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (read(profile, 'user_uuid') || read(profile, 'login_status')) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const user = loadedUser(profile);
|
|
63
|
+
return Boolean(user && (read(user, 'id') || read(user, 'uuid')));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* True when the profile has a login account that is managed from Fleet-Ops,
|
|
68
|
+
* i.e. a linked user which is not a staff member's account.
|
|
69
|
+
*/
|
|
70
|
+
export function hasManagedLogin(profile) {
|
|
71
|
+
return hasLinkedUser(profile) && !isStaffLinked(profile);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The login status, preferring the server-provided `login_status` over the
|
|
76
|
+
* embedded user's status.
|
|
77
|
+
*/
|
|
78
|
+
export function linkedUserStatus(profile) {
|
|
79
|
+
const loginStatus = read(profile, 'login_status');
|
|
80
|
+
if (loginStatus) {
|
|
81
|
+
return loginStatus;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const user = loadedUser(profile);
|
|
85
|
+
return (user ? (read(user, 'status') ?? read(user, 'session_status')) : null) ?? 'active';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function hasInactiveLogin(profile) {
|
|
89
|
+
return INACTIVE_LOGIN_STATUSES.includes(linkedUserStatus(profile));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function withoutIdentityFields(payload = {}) {
|
|
93
|
+
const attributes = { ...payload };
|
|
94
|
+
|
|
95
|
+
delete attributes.id;
|
|
96
|
+
delete attributes.uuid;
|
|
97
|
+
delete attributes.public_id;
|
|
98
|
+
|
|
99
|
+
return attributes;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Applies the profile payload returned by an account action endpoint
|
|
104
|
+
* (`{ driver: {...} }`, `{ customer: {...} }` or `{ contact: {...} }`).
|
|
105
|
+
*/
|
|
106
|
+
export function updateProfileFromResponse(profile, response = {}) {
|
|
107
|
+
const payload = response?.driver ?? response?.customer ?? response?.contact;
|
|
108
|
+
|
|
109
|
+
if (!payload || typeof profile?.setProperties !== 'function') {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const attributes = {};
|
|
114
|
+
if ('user_uuid' in payload) {
|
|
115
|
+
attributes.user_uuid = payload.user_uuid;
|
|
116
|
+
}
|
|
117
|
+
if ('is_staff_linked' in payload) {
|
|
118
|
+
attributes.is_staff_linked = Boolean(payload.is_staff_linked);
|
|
119
|
+
}
|
|
120
|
+
if ('login_status' in payload) {
|
|
121
|
+
attributes.login_status = payload.login_status;
|
|
122
|
+
} else if (payload.user && 'status' in payload.user) {
|
|
123
|
+
attributes.login_status = payload.user.status;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
profile.setProperties(attributes);
|
|
127
|
+
|
|
128
|
+
if (payload.user) {
|
|
129
|
+
const user = loadedUser(profile);
|
|
130
|
+
|
|
131
|
+
if (typeof user?.setProperties === 'function') {
|
|
132
|
+
user.setProperties(withoutIdentityFields(payload.user));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Opens a confirm modal which POSTs to an account action endpoint and applies
|
|
139
|
+
* the response to the profile.
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} services { modalsManager, fetch, notifications }
|
|
142
|
+
* @param {Object} profile the driver or contact record
|
|
143
|
+
* @param {Object} options { title, body, acceptButtonText, acceptButtonScheme, endpoint, payload, successMessage, onSuccess }
|
|
144
|
+
*/
|
|
145
|
+
export function confirmLoginAction({ modalsManager, fetch, notifications }, profile, options = {}) {
|
|
146
|
+
const { title, body, acceptButtonText, acceptButtonScheme, endpoint, payload = {}, successMessage, onSuccess } = options;
|
|
147
|
+
|
|
148
|
+
return modalsManager.confirm({
|
|
149
|
+
title,
|
|
150
|
+
body,
|
|
151
|
+
acceptButtonText,
|
|
152
|
+
acceptButtonScheme,
|
|
153
|
+
confirm: async (modal) => {
|
|
154
|
+
modal.startLoading();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch.post(endpoint, payload, { namespace: INTERNAL_NAMESPACE });
|
|
158
|
+
updateProfileFromResponse(profile, response);
|
|
159
|
+
notifications.success(successMessage);
|
|
160
|
+
|
|
161
|
+
if (typeof onSuccess === 'function') {
|
|
162
|
+
onSuccess(response);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
modal.done();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
notifications.serverError(error);
|
|
168
|
+
modal.stopLoading();
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from '@fleetbase/fleetops-engine/components/modals/reset-
|
|
1
|
+
export { default } from '@fleetbase/fleetops-engine/components/modals/reset-profile-credentials';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@fleetbase/fleetops-engine/components/profile-identity-hint';
|
package/composer.json
CHANGED
package/docs/SAFEE.md
CHANGED
|
@@ -86,6 +86,11 @@ cached configuration as part of the application's normal deployment process.
|
|
|
86
86
|
Do not retry an old failed monolithic job to recover a connection; start a new
|
|
87
87
|
manual sync or let scheduled polling recover it.
|
|
88
88
|
|
|
89
|
+
A completed scheduled sweep updates the connection's status and last sync time, so
|
|
90
|
+
a recovered connection no longer shows an old failure. Clicking "Sync Devices" while
|
|
91
|
+
a sweep is queued or running records the request for the next sweep instead of
|
|
92
|
+
returning an error. A partial sweep leaves the last reported state unchanged.
|
|
93
|
+
|
|
89
94
|
Inspect the shared telemetry diagnostics for fetched/applied/failed counts,
|
|
90
95
|
incomplete sweeps, pending deliveries, queue delay, and source age. Manual sync
|
|
91
96
|
completion is tied to its ingestion run; queueing or fetching alone is not
|