@fleetbase/fleetops-engine 0.6.67 → 0.6.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE.md +11 -15
- package/addon/components/contact/form.hbs +13 -2
- package/addon/components/customer/details.hbs +17 -17
- package/addon/components/customer/form.hbs +13 -2
- package/addon/components/customer/form.js +0 -56
- package/addon/components/driver/details.hbs +13 -19
- package/addon/components/driver/form.hbs +20 -35
- package/addon/components/driver/form.js +0 -58
- package/addon/components/fuel-integration/form.hbs +2 -2
- package/addon/components/fuel-integration/form.js +8 -0
- package/addon/components/fuel-integration/hub.js +1 -1
- package/addon/components/modals/driver-form.hbs +2 -2
- package/addon/components/modals/reset-profile-credentials.hbs +23 -0
- package/addon/components/modals/reset-profile-credentials.js +70 -0
- package/addon/components/profile-identity-hint.hbs +13 -0
- package/addon/components/profile-identity-hint.js +49 -0
- package/addon/controllers/management/drivers/index.js +4 -0
- package/addon/extension.js +4 -2
- package/addon/services/contact-actions.js +50 -89
- package/addon/services/driver-actions.js +98 -0
- package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
- package/addon/utils/fuel-transaction.js +2 -0
- package/addon/utils/profile-login-actions.js +172 -0
- package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
- package/app/components/profile-identity-hint.js +1 -0
- package/composer.json +1 -1
- package/docs/SAFEE.md +5 -0
- package/docs/TELEMATICS_QUEUES.md +59 -8
- package/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -0
- package/server/config/fuel-providers.php +53 -0
- package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
- package/server/resources/views/mail/driver-credentials.blade.php +21 -0
- package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
- package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
- package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
- package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
- package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
- package/server/src/Http/Resources/v1/Contact.php +3 -0
- package/server/src/Http/Resources/v1/Driver.php +3 -0
- package/server/src/Jobs/PollTelematicTelemetry.php +36 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +2 -2
- package/server/src/Mail/DriverCredentialsMail.php +55 -0
- package/server/src/Models/Contact.php +55 -90
- package/server/src/Models/Sensor.php +3 -5
- package/server/src/Observers/ContactObserver.php +2 -22
- package/server/src/Observers/DriverObserver.php +5 -6
- package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
- package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
- package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
- package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
- package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
- package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
- package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
- package/server/src/Support/ProfileAccountManager.php +405 -0
- package/server/src/Support/Telematics/TelematicService.php +6 -4
- package/server/src/Support/Telematics/Telemetry/Inbox.php +27 -9
- package/server/src/routes.php +5 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
- package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
- package/server/tests/ApiDriverControllerContractsTest.php +102 -20
- package/server/tests/CommandContractsTest.php +4 -4
- package/server/tests/ControllerHelperContractsTest.php +3 -11
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
- package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
- package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
- package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
- package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
- package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
- package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
- package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
- package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
- package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
- package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +88 -4
- package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
- package/server/tests/ModelAccessorContractsTest.php +82 -20
- package/server/tests/ObserverContractsTest.php +27 -2
- package/server/tests/RequestContractsTest.php +19 -11
- package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
- package/server/tests/TelematicControllerContractsTest.php +22 -4
- package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
- package/server/tests/Unit/Models/ContactTest.php +48 -5
- package/server/tests/Unit/Models/SensorTest.php +2 -2
- package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
- package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
- package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
- package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
- package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
- package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
- package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
- package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
- package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
- package/translations/en-us.yaml +37 -2
- package/translations/uk-ua.yaml +3475 -0
- package/addon/components/modals/reset-customer-credentials.hbs +0 -18
- package/addon/components/modals/reset-customer-credentials.js +0 -55
package/RELEASE.md
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
|
-
> v0.6.
|
|
1
|
+
> v0.6.69 ~ "Fleet-Ops tools for Fleetbase AI"
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
## What's New
|
|
5
|
-
- **
|
|
6
|
-
- **
|
|
7
|
-
- **
|
|
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
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
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
|
|
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 "
|
|
3
|
-
<div class="grid grid-cols-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
33
|
+
<div class="field-name">{{t "resource.vehicle"}}</div>
|
|
39
34
|
<div class="field-value">
|
|
40
|
-
<
|
|
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.
|
|
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
|
-
<
|
|
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"}}>
|
|
103
|
-
<option value="production" selected={{eq this.environment "production"}}>
|
|
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
|
|
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>
|