@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.
Files changed (115) hide show
  1. package/RELEASE.md +11 -15
  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/fuel-integration/form.hbs +2 -2
  10. package/addon/components/fuel-integration/form.js +8 -0
  11. package/addon/components/fuel-integration/hub.js +1 -1
  12. package/addon/components/modals/driver-form.hbs +2 -2
  13. package/addon/components/modals/reset-profile-credentials.hbs +23 -0
  14. package/addon/components/modals/reset-profile-credentials.js +70 -0
  15. package/addon/components/profile-identity-hint.hbs +13 -0
  16. package/addon/components/profile-identity-hint.js +49 -0
  17. package/addon/controllers/management/drivers/index.js +4 -0
  18. package/addon/extension.js +4 -2
  19. package/addon/services/contact-actions.js +50 -89
  20. package/addon/services/driver-actions.js +98 -0
  21. package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
  22. package/addon/utils/fuel-transaction.js +2 -0
  23. package/addon/utils/profile-login-actions.js +172 -0
  24. package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
  25. package/app/components/profile-identity-hint.js +1 -0
  26. package/composer.json +1 -1
  27. package/docs/SAFEE.md +5 -0
  28. package/docs/TELEMATICS_QUEUES.md +59 -8
  29. package/extension.json +1 -1
  30. package/package.json +3 -3
  31. package/scripts/pest-bootstrap.php +12 -0
  32. package/server/config/fuel-providers.php +53 -0
  33. package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
  34. package/server/resources/views/mail/driver-credentials.blade.php +21 -0
  35. package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
  36. package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
  37. package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
  38. package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
  39. package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
  40. package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
  41. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
  42. package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
  43. package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
  44. package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
  45. package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
  46. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
  47. package/server/src/Http/Resources/v1/Contact.php +3 -0
  48. package/server/src/Http/Resources/v1/Driver.php +3 -0
  49. package/server/src/Jobs/PollTelematicTelemetry.php +36 -0
  50. package/server/src/Jobs/SyncTelematicDevicesJob.php +2 -2
  51. package/server/src/Mail/DriverCredentialsMail.php +55 -0
  52. package/server/src/Models/Contact.php +55 -90
  53. package/server/src/Models/Sensor.php +3 -5
  54. package/server/src/Observers/ContactObserver.php +2 -22
  55. package/server/src/Observers/DriverObserver.php +5 -6
  56. package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
  57. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
  58. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
  59. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
  60. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
  61. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
  62. package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
  63. package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
  64. package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
  65. package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
  66. package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
  67. package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
  68. package/server/src/Support/ProfileAccountManager.php +405 -0
  69. package/server/src/Support/Telematics/TelematicService.php +6 -4
  70. package/server/src/Support/Telematics/Telemetry/Inbox.php +27 -9
  71. package/server/src/routes.php +5 -0
  72. package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
  73. package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
  74. package/server/tests/ApiDriverControllerContractsTest.php +102 -20
  75. package/server/tests/CommandContractsTest.php +4 -4
  76. package/server/tests/ControllerHelperContractsTest.php +3 -11
  77. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
  78. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
  79. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
  80. package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
  81. package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
  82. package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
  83. package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
  84. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
  85. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
  86. package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
  87. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
  88. package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
  89. package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
  90. package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
  91. package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +88 -4
  92. package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
  93. package/server/tests/ModelAccessorContractsTest.php +82 -20
  94. package/server/tests/ObserverContractsTest.php +27 -2
  95. package/server/tests/RequestContractsTest.php +19 -11
  96. package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
  97. package/server/tests/TelematicControllerContractsTest.php +22 -4
  98. package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
  99. package/server/tests/Unit/Models/ContactTest.php +48 -5
  100. package/server/tests/Unit/Models/SensorTest.php +2 -2
  101. package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
  102. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
  103. package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
  104. package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
  105. package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
  106. package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
  107. package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
  108. package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
  109. package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
  110. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
  111. package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
  112. package/translations/en-us.yaml +37 -2
  113. package/translations/uk-ua.yaml +3475 -0
  114. package/addon/components/modals/reset-customer-credentials.hbs +0 -18
  115. package/addon/components/modals/reset-customer-credentials.js +0 -55
package/RELEASE.md CHANGED
@@ -1,27 +1,23 @@
1
- > v0.6.67 ~ "Telematics reliability"
1
+ > v0.6.69 ~ "Fleet-Ops tools for Fleetbase AI"
2
2
 
3
3
  ---
4
4
  ## What's New
5
- - **Safee / DSCO syncs use a few batched requests.** Each sweep fetches every vehicle's current position, speed, heading and odometer in batches of up to 1,000 vehicles, with the vehicle list cached between sweeps. The old sync made three extra requests per vehicle; a 93-vehicle fleet now uses one request per minute instead of about 281. Temperature, door and history lookups stay out of routine polling.
6
- - **Safee sign-in is reused and refreshed.** Access tokens are cached encrypted and refreshed before they expire, requests stay within Safee's 50-per-second account limit, and rate-limit responses are honoured.
7
- - **Telematics can run on dedicated queue workers.** `TELEMATICS_POLL_QUEUE`, `TELEMATICS_INGESTION_QUEUE` and the new `TELEMATICS_BROADCAST_QUEUE` move polling, position processing and live-map broadcasts off the `default` queue for instances that need it. Nothing changes when they are unset. See `docs/TELEMATICS_QUEUES.md`.
8
- - **Live telemetry status is easier to read.** Connection details show telemetry inside the page layout with a compact grid and collapsible setup and failure sections.
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.
9
8
 
10
9
  ---
11
10
  ## Fixes
12
- - Telematics syncs no longer fail with "SyncTelematicDevicesJob has been attempted too many times" for providers on bounded polling. Poll attempts finish within the queue's 90-second reservation, and older queued sync jobs hand off to it instead of running for up to an hour.
13
- - A transient provider or TLS timeout no longer pauses scheduled polling for several minutes, and connections left in an error state keep being polled.
14
- - Vehicles that have never reported a position no longer mark every sweep as partial, quarantine deliveries, or show the connection as degraded.
15
- - Older or delayed samples cannot move a device or attached vehicle position backwards, and partial messages keep existing device identity and metadata.
16
- - AFAQY recovers from unreadable cached tokens and shares one request deadline across sign-in and unit retrieval.
17
- - Pausing Safee polling no longer queues failing sync jobs every minute.
18
- - Device event lookups use a new index instead of full-table scans.
19
- - The Radar dashboard widget links correctly when rendered outside the Fleet-Ops engine.
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.
20
17
 
21
18
  ---
22
19
  ## Testing
23
- - Backend tests cover Safee batching, token refresh, rate limits, ordered ingestion, tenant isolation, delivery checkpoints, paused providers, retry backoff and queue routing.
24
- - A local run on a single default queue worker polled a 93-vehicle Safee fleet: 5 sweeps used 7 API requests and each completed with 91 positions applied and 2 vehicles without a fix.
20
+ - Unit tests cover the new AI tools and console commands, and the AI capability registration.
25
21
 
26
22
  ---
27
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 {
@@ -99,8 +99,8 @@
99
99
  <div class="grid grid-cols-1 gap-2 lg:grid-cols-2 no-input-group-padding text-xs">
100
100
  <InputGroup @name="Environment">
101
101
  <select class="form-select w-full" data-test-fuel-environment {{on "change" this.setEnvironment}}>
102
- <option value="sandbox" selected={{eq this.environment "sandbox"}}>Sandbox (PetroApp staging)</option>
103
- <option value="production" selected={{eq this.environment "production"}}>Production</option>
102
+ <option value="sandbox" selected={{eq this.environment "sandbox"}}>{{this.sandboxLabel}}</option>
103
+ <option value="production" selected={{eq this.environment "production"}}>{{this.productionLabel}}</option>
104
104
  </select>
105
105
  </InputGroup>
106
106
  {{#each this.credentialFields as |field|}}
@@ -69,6 +69,14 @@ export default class FuelIntegrationFormComponent extends Component {
69
69
  return this.args.resource?.environment ?? 'production';
70
70
  }
71
71
 
72
+ get sandboxLabel() {
73
+ return this.selectedProvider?.metadata?.environment_labels?.sandbox ?? 'Sandbox';
74
+ }
75
+
76
+ get productionLabel() {
77
+ return this.selectedProvider?.metadata?.environment_labels?.production ?? 'Production';
78
+ }
79
+
72
80
  get baseUrl() {
73
81
  return this.credentials.base_url?.trim() || this.selectedProvider?.metadata?.base_urls?.[this.environment];
74
82
  }
@@ -116,7 +116,7 @@ export default class FuelIntegrationHubComponent extends Component {
116
116
  get formatSpend() {
117
117
  const currencies = new Map();
118
118
  for (const connection of this.connections) {
119
- const currency = connection.last_sync_state?.summary?.currency || (connection.provider === 'petroapp' ? 'SAR' : null);
119
+ const currency = connection.last_sync_state?.summary?.currency || (['petroapp', 'sasco'].includes(connection.provider) ? 'SAR' : null);
120
120
  if (currency) currencies.set(currency, (currencies.get(currency) || 0) + Number(connection.last_sync_state?.summary?.amount || 0));
121
121
  }
122
122
  return [...currencies].map(([currency, amount]) => fuelMoney(amount, currency)).join(' · ') || 'No spend recorded';
@@ -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>