@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.
- package/RELEASE.md +11 -9
- 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/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/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/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -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/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/ProfileAccountManager.php +405 -0
- 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/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/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,21 +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
|
-
- **
|
|
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
|
-
-
|
|
12
|
-
|
|
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
|
-
-
|
|
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
|
|
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 {
|
|
@@ -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',
|
package/addon/extension.js
CHANGED
|
@@ -115,8 +115,10 @@ export default {
|
|
|
115
115
|
route: 'virtual',
|
|
116
116
|
slug: 'track-order',
|
|
117
117
|
icon: 'barcode',
|
|
118
|
-
type: '
|
|
119
|
-
|
|
118
|
+
type: 'default',
|
|
119
|
+
// btn-auth (ember-ui): the sign-in page's neutral button, matching the
|
|
120
|
+
// console's "Continue with ..." provider buttons, hover included.
|
|
121
|
+
wrapperClass: 'btn-block btn-auth',
|
|
120
122
|
component: new ExtensionComponent('@fleetbase/fleetops-engine', 'order-tracking-lookup'),
|
|
121
123
|
onClick: (menuItem) => {
|
|
122
124
|
universe.transitionMenuItem('virtual', menuItem);
|