@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
package/RELEASE.md CHANGED
@@ -1,21 +1,23 @@
1
- > v0.6.68 ~ "Telematics sync status and SASCO fuel"
1
+ > v0.6.69 ~ "Fleet-Ops tools for Fleetbase AI"
2
2
 
3
3
  ---
4
4
  ## What's New
5
- - **SASCO is a native fuel provider.** Connect a SASCO B2B account alongside PetroApp to import fuel transactions, with sandbox and production environments, amounts in SAR, driver name and phone, and receipt images. Transactions are matched to vehicles by plate.
6
- - **"Sync Devices" works while telematics polling is running.** A manual sync requested while a scheduled sweep is queued or running is recorded and completed by the next sweep, instead of failing with "already queued or running".
7
- - **Telematics connection status stays current.** Each completed scheduled sweep updates the connection status and last sync time, so a connection that has recovered no longer shows "Needs attention" or an old last sync date.
5
+ - **Fleet-Ops works with Fleetbase AI tool calling.** Creating orders from the AI prompt works again with the tool-calling assistant, and the assistant can also propose an optimized waypoint sequence for an order and explain what an order or resource import needs. Every change is a preview card the user confirms.
6
+ - **Fleet-Ops console actions.** The assistant can offer to open Fleet-Ops pages and dialogs, such as **Operations › Orders** and **New Order**, as confirmation cards.
7
+ - **Resource search for the assistant.** A `fleetops_search` tool finds orders, vehicles, drivers, work orders, maintenances, devices, sensors and telematics records by id, name, plate, VIN, email or phone.
8
8
 
9
9
  ---
10
10
  ## Fixes
11
- - Manual telematics sync requests can no longer stay queued or failed indefinitely after their job is lost; the next scheduled sweep completes them.
12
- - Fuel provider environment labels and sync run details no longer refer to PetroApp for other providers.
11
+ - AI resource search no longer fails on every call with `Unknown column 'sensor_type'`, and database errors are no longer passed to the model.
12
+
13
+ ---
14
+ ## Dependencies
15
+ - `@fleetbase/fleetops-data` upgraded to `^0.2.2`, which adds the read-only login state (`is_staff_linked`, `login_status`) to the driver and contact models.
16
+ - `@fleetbase/ember-ui` upgraded to `^0.4.3`, which provides the `btn-auth` style the Track Order button on the login page uses.
13
17
 
14
18
  ---
15
19
  ## Testing
16
- - Backend tests cover manual syncs during scheduled sweeps, stale failure recovery, partial sweeps, and adoption of abandoned requests.
17
- - SASCO provider tests cover login, token caching, re-login after a 401, connection tests, pagination limits, error handling and transaction normalization.
18
- - `docs/TELEMATICS_QUEUES.md` now documents that dedicated telematics workers must share the queue worker's image and `APP_KEY`, with verification commands and troubleshooting.
20
+ - Unit tests cover the new AI tools and console commands, and the AI capability registration.
19
21
 
20
22
  ---
21
23
  ## Need help?
@@ -30,11 +30,22 @@
30
30
  <Input @value={{@resource.title}} @type="text" class="w-full form-input" placeholder={{t "common.title"}} disabled={{cannot-write @resource}} />
31
31
  </InputGroup>
32
32
  <InputGroup @name={{t "common.email"}}>
33
- <Input @value={{@resource.email}} @type="text" class="w-full form-input" placeholder={{t "common.email"}} disabled={{cannot-write @resource}} />
33
+ <Input
34
+ @value={{@resource.email}}
35
+ @type="text"
36
+ class="w-full form-input"
37
+ placeholder={{t "common.email"}}
38
+ disabled={{or (cannot-write @resource) @resource.is_staff_linked}}
39
+ />
34
40
  </InputGroup>
35
41
  <InputGroup @name={{t "common.phone"}}>
36
- <PhoneInput @value={{@resource.phone}} @onInput={{fn (mut @resource.phone)}} class="form-input w-full" disabled={{cannot-write @resource}} />
42
+ <PhoneInput @value={{@resource.phone}} @onInput={{fn (mut @resource.phone)}} class="form-input w-full" disabled={{or (cannot-write @resource) @resource.is_staff_linked}} />
37
43
  </InputGroup>
44
+ {{#if @resource.is_staff_linked}}
45
+ <div class="lg:col-span-2 -mt-1 mb-2 text-xs text-gray-500 dark:text-gray-400" data-test-staff-linked-helper>{{t "profile-account.staff-linked-helper"}}</div>
46
+ {{else if @resource.isNew}}
47
+ <ProfileIdentityHint @email={{@resource.email}} @phone={{@resource.phone}} @type="contact" class="lg:col-span-2" />
48
+ {{/if}}
38
49
  <InputGroup @name={{t "common.internal-id"}}>
39
50
  <Input @value={{@resource.internal_id}} @type="text" class="w-full form-input" placeholder={{t "common.internal-id"}} disabled={{cannot-write @resource}} />
40
51
  </InputGroup>
@@ -1,20 +1,4 @@
1
1
  <div class="details-wrapper" ...attributes>
2
- <ContentPanel @title={{t "customer.fields.user-account"}} @open={{true}} @wrapperClass="bordered-top">
3
- <div class="grid grid-cols-2 gap-2 text-xs dark:text-gray-100">
4
- <div class="field-info-container md:col-span-3">
5
- <div class="field-name">{{t "common.name"}}</div>
6
- <div class="field-value">{{n-a @resource.name}}</div>
7
- </div>
8
- <div class="field-info-container">
9
- <div class="field-name">{{t "common.email"}}</div>
10
- <ClickToCopy @value={{@resource.email}} class="field-value">{{n-a @resource.email}}</ClickToCopy>
11
- </div>
12
- <div class="field-info-container">
13
- <div class="field-name">{{t "common.phone"}}</div>
14
- <ClickToCopy @value={{@resource.phone}} class="field-value">{{n-a @resource.phone}}</ClickToCopy>
15
- </div>
16
- </div>
17
- </ContentPanel>
18
2
  <ContentPanel @title={{t "common.details"}} @open={{true}} @wrapperClass="bordered-top">
19
3
  <div class="grid grid-cols-2 gap-2 text-xs dark:text-gray-100">
20
4
  <div class="field-info-container">
@@ -30,7 +14,7 @@
30
14
  <ClickToCopy @value={{@resource.email}} class="field-value">{{n-a @resource.email}}</ClickToCopy>
31
15
  </div>
32
16
  <div class="field-info-container">
33
- <div class="field-name">{{t "common.email"}}</div>
17
+ <div class="field-name">{{t "common.phone"}}</div>
34
18
  <ClickToCopy @value={{@resource.phone}} class="field-value">{{n-a @resource.phone}}</ClickToCopy>
35
19
  </div>
36
20
  <div class="field-info-container">
@@ -47,5 +31,21 @@
47
31
  </div>
48
32
  </div>
49
33
  </ContentPanel>
34
+ <ContentPanel @title={{t "customer.fields.addresses"}} @open={{true}} @wrapperClass="bordered-top">
35
+ {{#if @resource.places.length}}
36
+ <div class="space-y-2 text-xs dark:text-gray-100">
37
+ {{#each @resource.places as |place|}}
38
+ <div class="flex items-center justify-between gap-2">
39
+ <Place::Pill @place={{place}} @subtitle={{place.address}} />
40
+ {{#if (eq place.id @resource.place_uuid)}}
41
+ <Badge @status="info" @hideStatusDot={{true}}>{{t "customer.fields.primary-address"}}</Badge>
42
+ {{/if}}
43
+ </div>
44
+ {{/each}}
45
+ </div>
46
+ {{else}}
47
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{t "customer.fields.no-addresses"}}</div>
48
+ {{/if}}
49
+ </ContentPanel>
50
50
  <CustomField::Yield @subject={{@resource}} @viewMode={{true}} @wrapperClass="bordered-top" />
51
51
  </div>
@@ -37,11 +37,22 @@
37
37
  <Input @value={{@resource.title}} @type="text" class="w-full form-input" placeholder={{t "common.title"}} disabled={{cannot-write @resource}} />
38
38
  </InputGroup>
39
39
  <InputGroup @name={{t "common.email"}}>
40
- <Input @value={{@resource.email}} @type="text" class="w-full form-input" placeholder={{t "common.email"}} disabled={{cannot-write @resource}} />
40
+ <Input
41
+ @value={{@resource.email}}
42
+ @type="text"
43
+ class="w-full form-input"
44
+ placeholder={{t "common.email"}}
45
+ disabled={{or (cannot-write @resource) @resource.is_staff_linked}}
46
+ />
41
47
  </InputGroup>
42
48
  <InputGroup @name={{t "common.phone"}}>
43
- <PhoneInput @value={{@resource.phone}} @onInput={{fn (mut @resource.phone)}} class="form-input w-full" disabled={{cannot-write @resource}} />
49
+ <PhoneInput @value={{@resource.phone}} @onInput={{fn (mut @resource.phone)}} class="form-input w-full" disabled={{or (cannot-write @resource) @resource.is_staff_linked}} />
44
50
  </InputGroup>
51
+ {{#if @resource.is_staff_linked}}
52
+ <div class="lg:col-span-2 -mt-1 mb-2 text-xs text-gray-500 dark:text-gray-400" data-test-staff-linked-helper>{{t "profile-account.staff-linked-helper"}}</div>
53
+ {{else if @resource.isNew}}
54
+ <ProfileIdentityHint @email={{@resource.email}} @phone={{@resource.phone}} @type="customer" class="lg:col-span-2" />
55
+ {{/if}}
45
56
  <InputGroup @name={{t "common.internal-id"}}>
46
57
  <Input @value={{@resource.internal_id}} @type="text" class="w-full form-input" placeholder={{t "common.internal-id"}} disabled={{cannot-write @resource}} />
47
58
  </InputGroup>
@@ -1,71 +1,15 @@
1
1
  import Component from '@glimmer/component';
2
- import { tracked } from '@glimmer/tracking';
3
2
  import { inject as service } from '@ember/service';
4
3
  import { action } from '@ember/object';
5
4
  import { task } from 'ember-concurrency';
6
5
 
7
6
  export default class CustomerFormComponent extends Component {
8
7
  @service customerActions;
9
- @service store;
10
8
  @service fetch;
11
9
  @service currentUser;
12
10
  @service notifications;
13
- @service modalsManager;
14
11
  @service('universe/extension-manager') extensionManager;
15
12
 
16
- @tracked userAccountActionButtons = [
17
- {
18
- icon: 'user-plus',
19
- size: 'xs',
20
- permission: 'iam create user',
21
- onClick: async () => {
22
- // Load IAM engine for user-form modal component
23
- await this.extensionManager.ensureEngineLoaded('@fleetbase/iam-engine');
24
-
25
- const user = this.store.createRecord('user', {
26
- status: 'pending',
27
- type: 'user',
28
- });
29
-
30
- this.modalsManager.show('modals/user-form', {
31
- title: 'Create a new user',
32
- user,
33
- formPermission: 'iam create user',
34
- uploadNewPhoto: (file) => {
35
- this.fetch.uploadFile.perform(
36
- file,
37
- {
38
- path: `uploads/${this.currentUser.companyId}/users/${user.slug}`,
39
- subject_uui: user.id,
40
- subject_type: 'user',
41
- type: 'user_photo',
42
- },
43
- (uploadedFile) => {
44
- user.setProperties({
45
- avatar_uuid: uploadedFile.id,
46
- avatar_url: uploadedFile.url,
47
- avatar: uploadedFile,
48
- });
49
- }
50
- );
51
- },
52
- confirm: async (modal) => {
53
- modal.startLoading();
54
-
55
- try {
56
- await user.save();
57
- this.notifications.success('New user created successfully!');
58
- modal.done();
59
- } catch (error) {
60
- this.notifications.serverError(error);
61
- modal.stopLoading();
62
- }
63
- },
64
- });
65
- },
66
- },
67
- ];
68
-
69
13
  get showWelcomeEmailOption() {
70
14
  return this.args.resource?.isNew && this.extensionManager.isInstalled('@fleetbase/customer-portal-engine');
71
15
  }
@@ -1,23 +1,18 @@
1
1
  <div class="details-wrapper" ...attributes>
2
- <ContentPanel @title={{t "driver.fields.user-account"}} @open={{true}} @wrapperClass="bordered-top">
3
- <div class="grid grid-cols-2 gap-2 text-xs dark:text-gray-100">
2
+ <ContentPanel @title={{t "common.details"}} @open={{true}} @wrapperClass="bordered-top">
3
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs dark:text-gray-100">
4
4
  <div class="field-info-container md:col-span-3">
5
5
  <div class="field-name">{{t "common.name"}}</div>
6
6
  <div class="field-value">{{n-a @resource.name}}</div>
7
7
  </div>
8
- <div class="field-info-container">
9
- <div class="field-name">{{t "common.email"}}</div>
10
- <ClickToCopy @value={{@resource.email}} class="field-value">{{n-a @resource.email}}</ClickToCopy>
11
- </div>
12
8
  <div class="field-info-container">
13
9
  <div class="field-name">{{t "common.phone"}}</div>
14
10
  <ClickToCopy @value={{@resource.phone}} class="field-value">{{n-a @resource.phone}}</ClickToCopy>
15
11
  </div>
16
- </div>
17
- </ContentPanel>
18
-
19
- <ContentPanel @title={{t "driver.fields.driver-details"}} @open={{true}} @wrapperClass="bordered-top">
20
- <div class="grid grid-cols-2 gap-2 text-xs dark:text-gray-100">
12
+ <div class="field-info-container md:col-span-2">
13
+ <div class="field-name">{{t "common.email"}}</div>
14
+ <ClickToCopy @value={{@resource.email}} class="field-value">{{n-a @resource.email}}</ClickToCopy>
15
+ </div>
21
16
  <div class="field-info-container">
22
17
  <div class="field-name">{{t "common.id"}}</div>
23
18
  <ClickToCopy @value={{@resource.public_id}} class="field-value">{{n-a @resource.public_id}}</ClickToCopy>
@@ -30,28 +25,27 @@
30
25
  <div class="field-name">{{t "driver.fields.driver-license"}}</div>
31
26
  <div class="field-value">{{n-a @resource.drivers_license_number}}</div>
32
27
  </div>
33
- <div class="field-info-container">
28
+ <div class="field-info-container md:col-span-3">
34
29
  <div class="field-name">{{t "driver.fields.license-expiry"}}</div>
35
30
  <div class="field-value">{{n-a (format-date-fns @resource.license_expiry)}}</div>
36
31
  </div>
37
32
  <div class="field-info-container">
38
- <div class="field-name">{{t "resource.vendor"}}</div>
33
+ <div class="field-name">{{t "resource.vehicle"}}</div>
39
34
  <div class="field-value">
40
- <Vendor::Pill @vendor={{resource-relation @resource "vendor"}} @titleFallback={{n-a @resource.vendor_name}} />
35
+ <Vehicle::Pill @vehicle={{resource-relation @resource "vehicle"}} @titleFallback={{n-a @resource.vehicle_name (t "driver.fields.no-vehicle-assigned")}} />
41
36
  </div>
42
37
  </div>
43
- <div class="field-info-container">
44
- <div class="field-name">{{t "resource.vehicle"}}</div>
38
+ <div class="field-info-container md:col-span-2">
39
+ <div class="field-name">{{t "resource.vendor"}}</div>
45
40
  <div class="field-value">
46
- <Vehicle::Pill @vehicle={{resource-relation @resource "vehicle"}} @titleFallback={{n-a @resource.vehicle_name (t "driver.fields.no-vehicle-assigned")}} />
41
+ <Vendor::Pill @vendor={{resource-relation @resource "vendor"}} @titleFallback={{n-a @resource.vendor_name}} />
47
42
  </div>
48
43
  </div>
49
- <div class="col-span-3"></div>
50
44
  <div class="field-info-container">
51
45
  <div class="field-name">{{t "common.city"}}</div>
52
46
  <div class="field-value">{{n-a @resource.city}}</div>
53
47
  </div>
54
- <div class="field-info-container">
48
+ <div class="field-info-container md:col-span-2">
55
49
  <div class="field-name">{{t "common.country"}}</div>
56
50
  <div class="field-value">
57
51
  <CountryName @country={{@resource.country}} />
@@ -1,39 +1,4 @@
1
1
  <div class="form-wrapper" ...attributes>
2
- <ContentPanel @title={{t "driver.fields.user-account"}} @open={{true}} @actionButtons={{this.userAccountActionButtons}} @wrapperClass="bordered-top">
3
- <div class="space-y-2">
4
- <InputGroup @name={{t "driver.fields.user-account"}} @helpText={{t "driver.fields.user-account-help-text"}}>
5
- <ModelSelect
6
- @modelName="user"
7
- @selectedItemComponent={{component "select-option/user" compact=true}}
8
- @selectedModel={{@resource.user}}
9
- @placeholder={{t "driver.fields.select-user"}}
10
- @query={{hash doesnt_have_driver=true can_be_driver=true}}
11
- @triggerClass="form-select form-input"
12
- @infiniteScroll={{false}}
13
- @renderInPlace={{true}}
14
- @onChange={{fn (mut @resource.user)}}
15
- @permission="fleet-ops update-user-for driver"
16
- as |model|
17
- >
18
- <SelectOption::User @option={{model}} />
19
- </ModelSelect>
20
- </InputGroup>
21
- {{#if @resource.user}}
22
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-2 no-input-group-padding">
23
- <InputGroup @name={{t "common.name"}} @wrapperClass="col-span-2 mb-0i">
24
- <Input @value={{@resource.user.name}} @type="text" class="w-full form-input" placeholder={{t "common.name"}} disabled={{true}} />
25
- </InputGroup>
26
- <InputGroup @name={{t "common.email"}} @wrapperClass="mb-0i">
27
- <Input @value={{@resource.user.email}} @type="text" class="w-full form-input" placeholder={{t "common.email"}} disabled={{true}} />
28
- </InputGroup>
29
- <InputGroup @name={{t "common.phone"}} @wrapperClass="mb-0i">
30
- <PhoneInput @value={{@resource.user.phone}} @onInput={{fn (mut @resource.phone)}} class="form-input w-full" disabled={{true}} />
31
- </InputGroup>
32
- </div>
33
- {{/if}}
34
- </div>
35
- </ContentPanel>
36
-
37
2
  <ContentPanel @title={{t "driver.fields.driver-details"}} @open={{true}} @wrapperClass="bordered-top">
38
3
  <div class="flex flex-row space-x-3 mb-3">
39
4
  <div class="flex items-center justify-start rounded-full">
@@ -65,6 +30,26 @@
65
30
  </div>
66
31
  </div>
67
32
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-2 text-xs no-input-group-padding">
33
+ <InputGroup @name={{t "common.name"}} @required={{true}}>
34
+ <Input @value={{@resource.name}} @type="text" class="w-full form-input" placeholder={{t "common.name"}} disabled={{cannot-write @resource}} />
35
+ </InputGroup>
36
+ <InputGroup @name={{t "common.email"}}>
37
+ <Input
38
+ @value={{@resource.email}}
39
+ @type="text"
40
+ class="w-full form-input"
41
+ placeholder={{t "common.email"}}
42
+ disabled={{or (cannot-write @resource) @resource.is_staff_linked}}
43
+ />
44
+ </InputGroup>
45
+ <InputGroup @name={{t "common.phone"}}>
46
+ <PhoneInput @value={{@resource.phone}} @onInput={{fn (mut @resource.phone)}} class="form-input w-full" disabled={{or (cannot-write @resource) @resource.is_staff_linked}} />
47
+ </InputGroup>
48
+ {{#if @resource.is_staff_linked}}
49
+ <div class="lg:col-span-2 -mt-1 mb-2 text-xs text-gray-500 dark:text-gray-400" data-test-staff-linked-helper>{{t "profile-account.staff-linked-helper"}}</div>
50
+ {{else if @resource.isNew}}
51
+ <ProfileIdentityHint @email={{@resource.email}} @phone={{@resource.phone}} @type="driver" class="lg:col-span-2" />
52
+ {{/if}}
68
53
  <InputGroup @name={{t "common.internal-id"}}>
69
54
  <Input @value={{@resource.internal_id}} @type="text" class="w-full form-input" placeholder={{t "common.internal-id"}} disabled={{cannot-write @resource}} />
70
55
  </InputGroup>
@@ -3,67 +3,9 @@ import { inject as service } from '@ember/service';
3
3
  import { task } from 'ember-concurrency';
4
4
 
5
5
  export default class DriverFormComponent extends Component {
6
- @service store;
7
6
  @service fetch;
8
7
  @service currentUser;
9
8
  @service notifications;
10
- @service modalsManager;
11
- @service('universe/extension-manager') extensionManager;
12
-
13
- get userAccountActionButtons() {
14
- return [
15
- {
16
- icon: 'user-plus',
17
- size: 'xs',
18
- permission: 'iam create user',
19
- onClick: async () => {
20
- // Load IAM engine for user-form modal component
21
- await this.extensionManager.ensureEngineLoaded('@fleetbase/iam-engine');
22
-
23
- const user = this.store.createRecord('user', {
24
- status: 'pending',
25
- type: 'user',
26
- });
27
-
28
- this.modalsManager.show('modals/user-form', {
29
- title: 'Create a new user',
30
- user,
31
- formPermission: 'iam create user',
32
- uploadNewPhoto: (file) => {
33
- this.fetch.uploadFile.perform(
34
- file,
35
- {
36
- path: `uploads/${this.currentUser.companyId}/users/${user.slug}`,
37
- subject_uui: user.id,
38
- subject_type: 'user',
39
- type: 'user_photo',
40
- },
41
- (uploadedFile) => {
42
- user.setProperties({
43
- avatar_uuid: uploadedFile.id,
44
- avatar_url: uploadedFile.url,
45
- avatar: uploadedFile,
46
- });
47
- }
48
- );
49
- },
50
- confirm: async (modal) => {
51
- modal.startLoading();
52
-
53
- try {
54
- await user.save();
55
- this.notifications.success('New user created successfully!');
56
- modal.done();
57
- } catch (error) {
58
- this.notifications.serverError(error);
59
- modal.stopLoading();
60
- }
61
- },
62
- });
63
- },
64
- },
65
- ];
66
- }
67
9
 
68
10
  @task *handlePhotoUpload(file) {
69
11
  try {
@@ -41,14 +41,14 @@
41
41
  <label>
42
42
  {{t "common.email"}}
43
43
  </label>
44
- <Input @value={{@options.driver.email}} @type="text" class="w-full form-input" placeholder={{t "common.email"}} />
44
+ <Input @value={{@options.driver.email}} @type="text" class="w-full form-input" placeholder={{t "common.email"}} disabled={{@options.driver.is_staff_linked}} />
45
45
  </div>
46
46
 
47
47
  <div class="input-group">
48
48
  <label>
49
49
  {{t "common.phone"}}
50
50
  </label>
51
- <PhoneInput @value={{@options.driver.phone}} @onInput={{fn (mut @options.driver.phone)}} class="form-input w-full" />
51
+ <PhoneInput @value={{@options.driver.phone}} @onInput={{fn (mut @options.driver.phone)}} class="form-input w-full" disabled={{@options.driver.is_staff_linked}} />
52
52
  </div>
53
53
 
54
54
  <div class="input-group">
@@ -0,0 +1,23 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="px-4 text-gray-900 dark:text-gray-50">
3
+ <div class="mb-1 text-base font-semibold">
4
+ {{t "profile-account.prompts.reset-password-heading" name=this.profile.name}}
5
+ </div>
6
+ <div class="text-sm mb-4">{{t "profile-account.prompts.reset-password-body"}}</div>
7
+ <InputGroup>
8
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
9
+ <InputGroup @name={{t "profile-account.prompts.new-password"}} @type="password" @value={{this.password}} @wrapperClass="mb-0i" />
10
+ <InputGroup @name={{t "profile-account.prompts.confirm-new-password"}} @type="password" @value={{this.confirmPassword}} @wrapperClass="mb-0i" />
11
+ </div>
12
+ </InputGroup>
13
+ <InputGroup>
14
+ <Checkbox
15
+ @value={{this.sendCredentials}}
16
+ @label={{t "profile-account.prompts.send-new-credentials"}}
17
+ @onToggle={{fn (mut this.sendCredentials)}}
18
+ @alignItems="center"
19
+ @labelClass="mb-0i"
20
+ />
21
+ </InputGroup>
22
+ </div>
23
+ </Modal::Default>
@@ -0,0 +1,70 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { INTERNAL_NAMESPACE, updateProfileFromResponse } from '../../utils/profile-login-actions';
5
+
6
+ /**
7
+ * Resets the login password of a driver, contact or customer profile.
8
+ *
9
+ * Options:
10
+ * - `profile`: the driver or contact record
11
+ * - `endpoint`: the reset credentials endpoint (internal namespace)
12
+ * - `payload`: extra body params, or a function returning them
13
+ * - `onPasswordResetComplete(response)`: optional callback
14
+ */
15
+ export default class ModalsResetProfileCredentialsComponent extends Component {
16
+ @service fetch;
17
+ @service intl;
18
+ @service notifications;
19
+ @tracked options = {};
20
+ @tracked password;
21
+ @tracked confirmPassword;
22
+ @tracked sendCredentials = true;
23
+ @tracked profile;
24
+
25
+ constructor(owner, { options }) {
26
+ super(...arguments);
27
+ this.profile = options.profile;
28
+ this.options = options;
29
+ this.setupOptions();
30
+ }
31
+
32
+ get extraPayload() {
33
+ const { payload } = this.options;
34
+ return (typeof payload === 'function' ? payload(this.profile) : payload) ?? {};
35
+ }
36
+
37
+ setupOptions() {
38
+ this.options.title = this.options.title ?? this.intl.t('profile-account.prompts.reset-password-title');
39
+ this.options.acceptButtonText = this.options.acceptButtonText ?? this.intl.t('profile-account.prompts.reset-password-accept');
40
+ this.options.declineButtonHidden = true;
41
+ this.options.confirm = async (modal) => {
42
+ modal.startLoading();
43
+
44
+ try {
45
+ const response = await this.fetch.post(
46
+ this.options.endpoint,
47
+ {
48
+ ...this.extraPayload,
49
+ password: this.password,
50
+ password_confirmation: this.confirmPassword,
51
+ send_credentials: this.sendCredentials,
52
+ },
53
+ { namespace: INTERNAL_NAMESPACE }
54
+ );
55
+
56
+ updateProfileFromResponse(this.profile, response);
57
+ this.notifications.success(this.intl.t('profile-account.prompts.reset-password-success'));
58
+
59
+ if (typeof this.options.onPasswordResetComplete === 'function') {
60
+ this.options.onPasswordResetComplete(response);
61
+ }
62
+
63
+ modal.done();
64
+ } catch (error) {
65
+ this.notifications.serverError(error);
66
+ modal.stopLoading();
67
+ }
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,13 @@
1
+ <div class="profile-identity-hint text-xs" {{did-insert this.lookupIdentity}} {{did-update this.lookupIdentity @email @phone}} ...attributes>
2
+ {{#if this.conflict}}
3
+ <div class="flex items-center space-x-1 text-yellow-600 dark:text-yellow-400" data-test-profile-identity-conflict>
4
+ <FaIcon @icon="triangle-exclamation" @size="xs" />
5
+ <span>{{this.conflict}}</span>
6
+ </div>
7
+ {{else if this.staff}}
8
+ <div class="flex items-center space-x-1 text-blue-600 dark:text-blue-400" data-test-profile-identity-staff>
9
+ <FaIcon @icon="circle-info" @size="xs" />
10
+ <span>{{t "profile-account.will-link-to-staff" name=(or this.staff.name this.staff.email this.staff.phone)}}</span>
11
+ </div>
12
+ {{/if}}
13
+ </div>
@@ -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',
@@ -115,8 +115,10 @@ export default {
115
115
  route: 'virtual',
116
116
  slug: 'track-order',
117
117
  icon: 'barcode',
118
- type: 'link',
119
- wrapperClass: 'btn-block py-1 border dark:border-gray-700 border-gray-200 hover:opacity-50',
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);