@fleetbase/fleetops-engine 0.6.68 → 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.
Files changed (100) hide show
  1. package/RELEASE.md +11 -9
  2. package/addon/components/contact/form.hbs +13 -2
  3. package/addon/components/customer/details.hbs +17 -17
  4. package/addon/components/customer/form.hbs +13 -2
  5. package/addon/components/customer/form.js +0 -56
  6. package/addon/components/driver/details.hbs +13 -19
  7. package/addon/components/driver/form.hbs +20 -35
  8. package/addon/components/driver/form.js +0 -58
  9. package/addon/components/modals/driver-form.hbs +2 -2
  10. package/addon/components/modals/reset-profile-credentials.hbs +23 -0
  11. package/addon/components/modals/reset-profile-credentials.js +70 -0
  12. package/addon/components/profile-identity-hint.hbs +13 -0
  13. package/addon/components/profile-identity-hint.js +49 -0
  14. package/addon/controllers/management/drivers/index.js +4 -0
  15. package/addon/extension.js +4 -2
  16. package/addon/services/contact-actions.js +50 -89
  17. package/addon/services/driver-actions.js +98 -0
  18. package/addon/utils/profile-login-actions.js +172 -0
  19. package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
  20. package/app/components/profile-identity-hint.js +1 -0
  21. package/composer.json +1 -1
  22. package/extension.json +1 -1
  23. package/package.json +3 -3
  24. package/scripts/pest-bootstrap.php +12 -0
  25. package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
  26. package/server/resources/views/mail/driver-credentials.blade.php +21 -0
  27. package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
  28. package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
  29. package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
  30. package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
  31. package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
  32. package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
  33. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
  34. package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
  35. package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
  36. package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
  37. package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
  38. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
  39. package/server/src/Http/Resources/v1/Contact.php +3 -0
  40. package/server/src/Http/Resources/v1/Driver.php +3 -0
  41. package/server/src/Mail/DriverCredentialsMail.php +55 -0
  42. package/server/src/Models/Contact.php +55 -90
  43. package/server/src/Models/Sensor.php +3 -5
  44. package/server/src/Observers/ContactObserver.php +2 -22
  45. package/server/src/Observers/DriverObserver.php +5 -6
  46. package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
  47. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
  48. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
  49. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
  50. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
  51. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
  52. package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
  53. package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
  54. package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
  55. package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
  56. package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
  57. package/server/src/Support/ProfileAccountManager.php +405 -0
  58. package/server/src/routes.php +5 -0
  59. package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
  60. package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
  61. package/server/tests/ApiDriverControllerContractsTest.php +102 -20
  62. package/server/tests/CommandContractsTest.php +4 -4
  63. package/server/tests/ControllerHelperContractsTest.php +3 -11
  64. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
  65. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
  66. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
  67. package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
  68. package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
  69. package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
  70. package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
  71. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
  72. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
  73. package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
  74. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
  75. package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
  76. package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
  77. package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
  78. package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
  79. package/server/tests/ModelAccessorContractsTest.php +82 -20
  80. package/server/tests/ObserverContractsTest.php +27 -2
  81. package/server/tests/RequestContractsTest.php +19 -11
  82. package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
  83. package/server/tests/TelematicControllerContractsTest.php +22 -4
  84. package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
  85. package/server/tests/Unit/Models/ContactTest.php +48 -5
  86. package/server/tests/Unit/Models/SensorTest.php +2 -2
  87. package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
  88. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
  89. package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
  90. package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
  91. package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
  92. package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
  93. package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
  94. package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
  95. package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
  96. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
  97. package/translations/en-us.yaml +37 -2
  98. package/translations/uk-ua.yaml +3475 -0
  99. package/addon/components/modals/reset-customer-credentials.hbs +0 -18
  100. package/addon/components/modals/reset-customer-credentials.js +0 -55
@@ -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, get } from '@ember/object';
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
- const user = contact?.user;
128
- return Boolean(contact?.user_uuid || (user && (get(user, 'id') || get(user, 'uuid'))));
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
- const user = contact?.user;
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 ['inactive', 'disabled', 'suspended'].includes(this.linkedUserStatus(contact));
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.hasLinkedUser(contact)) {
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: 'Reset Password',
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: 'Send Credentials',
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: 'Reactivate Login',
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: 'Deactivate Login',
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: 'Convert to Vendor',
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 hasLinkedUser = (contact) => this.hasLinkedUser(contact);
220
+ const hasManagedLogin = (contact) => this.hasManagedLogin(contact);
216
221
 
217
222
  const actions = [
218
223
  {
219
- label: 'Reset Password',
224
+ label: this.intl.t('profile-account.actions.reset-password'),
220
225
  icon: 'key',
221
226
  fn: (contact) => this.openResetPasswordModal(contact),
222
- isVisible: hasLinkedUser,
227
+ isVisible: hasManagedLogin,
223
228
  },
224
229
  {
225
- label: 'Send Credentials',
230
+ label: this.intl.t('profile-account.actions.send-credentials'),
226
231
  icon: 'paper-plane',
227
232
  fn: (contact) => this.confirmSendCredentials(contact),
228
- isVisible: hasLinkedUser,
233
+ isVisible: hasManagedLogin,
229
234
  },
230
235
  {
231
- label: 'Deactivate Login',
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) => hasLinkedUser(contact) && !this.hasInactiveLogin(contact),
240
+ isVisible: (contact) => hasManagedLogin(contact) && !this.hasInactiveLogin(contact),
236
241
  },
237
242
  {
238
- label: 'Reactivate Login',
243
+ label: this.intl.t('profile-account.actions.reactivate-login'),
239
244
  icon: 'unlock',
240
245
  fn: (contact) => this.confirmReactivatePortalLogin(contact),
241
- isVisible: (contact) => hasLinkedUser(contact) && this.hasInactiveLogin(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: 'Convert to Vendor',
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: hasLinkedUser,
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-customer-credentials', {
259
- customer: contact,
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: 'Send Portal Credentials',
282
- body: 'Generate a new temporary password and email this contact their portal credentials.',
283
- acceptButtonText: 'Send Credentials',
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: 'Portal credentials sent.',
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: 'Deactivate Login',
292
- body: 'Deactivate portal access for this contact. Their profile and history will be preserved.',
293
- acceptButtonText: 'Deactivate Login',
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: 'Portal login deactivated.',
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: 'Reactivate Login',
303
- body: 'Reactivate portal access for this contact.',
304
- acceptButtonText: 'Reactivate Login',
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: 'Portal login reactivated.',
313
+ successMessage: this.intl.t('profile-account.prompts.reactivate-portal-login-success'),
307
314
  });
308
315
  }
309
316
 
310
- confirmLoginAction(contact, { title, body, acceptButtonText, acceptButtonScheme, endpoint, successMessage }) {
311
- this.modalsManager.confirm({
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
- async updateContactFromResponse(contact, response) {
333
- const payload = response.customer ?? response.contact;
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,
@@ -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
+ }
@@ -1 +1 @@
1
- export { default } from '@fleetbase/fleetops-engine/components/modals/reset-customer-credentials';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.68",
3
+ "version": "0.6.69",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Fleet-Ops",
3
- "version": "0.6.68",
3
+ "version": "0.6.69",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/fleetops",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/fleetops-engine",
3
- "version": "0.6.68",
3
+ "version": "0.6.69",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -45,8 +45,8 @@
45
45
  "dependencies": {
46
46
  "@babel/core": "^7.23.2",
47
47
  "@fleetbase/ember-core": "^0.3.24",
48
- "@fleetbase/ember-ui": "^0.4.1",
49
- "@fleetbase/fleetops-data": "^0.2.1",
48
+ "@fleetbase/ember-ui": "^0.4.3",
49
+ "@fleetbase/fleetops-data": "^0.2.2",
50
50
  "@fleetbase/leaflet-routing-machine": "^3.2.17",
51
51
  "@fortawesome/ember-fontawesome": "^2.0.0",
52
52
  "@fortawesome/fontawesome-svg-core": "6.4.0",
@@ -382,6 +382,18 @@ if (!interface_exists('Fleetbase\Ai\Contracts\AIActionCapabilityInterface')) {
382
382
  eval('namespace Fleetbase\Ai\Contracts; interface AIActionCapabilityInterface {}');
383
383
  }
384
384
 
385
+ if (!interface_exists('Fleetbase\Ai\Contracts\AIToolCapabilityInterface')) {
386
+ eval('namespace Fleetbase\Ai\Contracts; interface AIToolCapabilityInterface {}');
387
+ }
388
+
389
+ if (!class_exists('Fleetbase\Ai\Support\AiAudience')) {
390
+ eval('namespace Fleetbase\Ai\Support; class AiAudience { public function __construct(public bool $isSystemAdmin = false, public array $granted = []) {} public function can(string $permission): bool { return $this->isSystemAdmin || in_array($permission, $this->granted, true); } public function canAll(array $permissions): bool { foreach ($permissions as $permission) { if (!$this->can($permission)) { return false; } } return true; } }');
391
+ }
392
+
393
+ if (!class_exists('Fleetbase\Ai\Support\AiToolContext')) {
394
+ eval('namespace Fleetbase\Ai\Support; class AiToolContext { public array $actionPreviews = []; public array $uiActions = []; public function __construct(public $task, public $audience) {} public function addActionPreview($capability, array $preview): array { $preview = array_merge(["preview_id" => "preview-" . (count($this->actionPreviews) + 1), "key" => $capability->key()], $preview); $this->actionPreviews[] = $preview; return $preview; } }');
395
+ }
396
+
385
397
  if (!class_exists('Fleetbase\Ai\Models\AiTask')) {
386
398
  eval('namespace Fleetbase\Ai\Models; class AiTask { public function __construct(array $attributes = []) { foreach ($attributes as $key => $value) { $this->{$key} = $value; } } }');
387
399
  }