@fleetbase/fleetops-engine 0.6.50 → 0.6.51
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/README.md +185 -67
- package/addon/components/contact/panel-header.hbs +11 -48
- package/addon/components/customer/details.hbs +1 -2
- package/addon/components/customer/form.hbs +63 -51
- package/addon/components/customer/form.js +27 -1
- package/addon/components/customer/orders.hbs +48 -0
- package/addon/components/customer/orders.js +67 -5
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/home/getting-started-guidance.hbs +113 -0
- package/addon/components/home/getting-started-guidance.js +107 -0
- package/addon/components/issue/details/comments.hbs +3 -0
- package/addon/components/issue/details/comments.js +3 -0
- package/addon/components/issue/details/correspondence.hbs +9 -0
- package/addon/components/issue/details/correspondence.js +83 -0
- package/addon/components/issue/details/documents.hbs +69 -0
- package/addon/components/issue/details/documents.js +85 -0
- package/addon/components/issue/details.hbs +220 -59
- package/addon/components/issue/details.js +245 -0
- package/addon/components/issue/form.hbs +19 -1
- package/addon/components/issue/panel-header.hbs +43 -0
- package/addon/components/issue/panel-header.js +22 -0
- package/addon/components/issue/timeline.hbs +40 -0
- package/addon/components/issue/timeline.js +45 -0
- package/addon/components/leaflet-tracking-marker.js +3 -0
- package/addon/components/map/drawer/position-listing.js +3 -0
- package/addon/components/map/leaflet-live-map.hbs +2 -2
- package/addon/components/map/leaflet-live-map.js +17 -0
- package/addon/components/map/order-list-overlay.hbs +4 -4
- package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
- package/addon/components/modals/convert-customer-to-vendor.js +55 -0
- package/addon/components/modals/issue-assign.hbs +20 -0
- package/addon/components/modals/issue-assign.js +49 -0
- package/addon/components/modals/issue-close.hbs +30 -0
- package/addon/components/modals/issue-close.js +73 -0
- package/addon/components/modals/issue-status.hbs +20 -0
- package/addon/components/modals/issue-status.js +43 -0
- package/addon/components/modals/reset-customer-credentials.js +12 -6
- package/addon/components/modals/update-order-activity.hbs +51 -17
- package/addon/components/modals/update-order-activity.js +59 -5
- package/addon/components/order/details/activity.js +2 -2
- package/addon/components/order/details/proof.hbs +40 -38
- package/addon/components/order/details/proof.js +31 -4
- package/addon/components/order/details.hbs +5 -5
- package/addon/components/order/details.js +19 -1
- package/addon/components/order/header.hbs +1 -1
- package/addon/components/route-list.hbs +0 -12
- package/addon/components/service-rate/details.hbs +1 -1
- package/addon/components/service-rate/form.hbs +1 -1
- package/addon/components/service-rate/form.js +4 -0
- package/addon/components/vendor/personnel-manager.hbs +166 -0
- package/addon/components/vendor/personnel-manager.js +143 -0
- package/addon/components/widget/live-fleet.hbs +1 -0
- package/addon/components/widget/live-fleet.js +2 -7
- package/addon/components/widget/revenue-trend.hbs +1 -1
- package/addon/components/widget/revenue-trend.js +75 -2
- package/addon/controllers/management/contacts/customers/details.js +31 -13
- package/addon/controllers/management/contacts/customers.js +16 -5
- package/addon/controllers/management/contacts/index/details.js +31 -13
- package/addon/controllers/management/contacts/index.js +7 -0
- package/addon/controllers/management/issues/index/details.js +52 -6
- package/addon/controllers/management/vendors/index/details.js +4 -0
- package/addon/controllers/operations/orders/index/details.js +11 -3
- package/addon/controllers/operations/orders/index.js +1 -1
- package/addon/extension.js +19 -12
- package/addon/helpers/format-duration.js +1 -38
- package/addon/initializers/load-leaflet-assets.js +5 -6
- package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
- package/addon/routes/management/issues/index/details.js +1 -1
- package/addon/routes.js +1 -0
- package/addon/services/contact-actions.js +231 -1
- package/addon/services/getting-started.js +44 -0
- package/addon/services/issue-actions.js +71 -0
- package/addon/services/leaflet-map-manager.js +2 -0
- package/addon/services/map-adapter/leaflet.js +19 -0
- package/addon/services/service-areas.js +3 -0
- package/addon/styles/fleetops-engine.css +1036 -0
- package/addon/templates/management/contacts/customers/details.hbs +1 -1
- package/addon/templates/management/contacts/index/details.hbs +1 -1
- package/addon/templates/management/contacts.hbs +2 -2
- package/addon/templates/management/issues/index/details/index.hbs +3 -1
- package/addon/templates/management/issues/index/details.hbs +3 -2
- package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
- package/addon/templates/operations/orders/index/details/index.hbs +2 -2
- package/addon/templates/operations/orders/index.hbs +1 -1
- package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
- package/addon/templates/operations/service-rates/index/new.hbs +1 -0
- package/addon/templates/settings/payments/onboard.hbs +8 -8
- package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
- package/addon/utils/leaflet-plugin-loader.js +219 -0
- package/addon/utils/setup-customer-portal.js +4 -64
- package/app/components/home/getting-started-guidance.js +1 -0
- package/app/components/issue/details/comments.js +1 -0
- package/app/components/issue/details/correspondence.js +1 -0
- package/app/components/issue/details/documents.js +1 -0
- package/app/components/issue/panel-header.js +1 -0
- package/app/components/modals/convert-customer-to-vendor.js +1 -0
- package/app/components/modals/issue-assign.js +1 -0
- package/app/components/modals/issue-close.js +1 -0
- package/app/components/modals/issue-status.js +1 -0
- package/app/components/vendor/personnel-manager.js +1 -0
- package/app/helpers/format-duration.js +1 -1
- package/app/services/getting-started.js +1 -0
- package/app/utils/leaflet-draw-namespace-guard.js +1 -0
- package/app/utils/leaflet-plugin-loader.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +5 -5
- package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
- package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
- package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
- package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
- package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
- package/server/resources/views/mail/customer-credentials.blade.php +16 -13
- package/server/src/Expansions/UserFilterExpansion.php +28 -0
- package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -25
- package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +135 -0
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
- package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +172 -97
- package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
- package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
- package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
- package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
- package/server/src/Http/Resources/v1/Index/Order.php +12 -9
- package/server/src/Http/Resources/v1/Issue.php +25 -0
- package/server/src/Http/Resources/v1/Order.php +20 -18
- package/server/src/Mail/CustomerCredentialsMail.php +5 -6
- package/server/src/Models/Contact.php +30 -51
- package/server/src/Models/Issue.php +47 -6
- package/server/src/Models/Order.php +33 -6
- package/server/src/Models/ServiceArea.php +1 -1
- package/server/src/Models/VendorPersonnel.php +10 -1
- package/server/src/Models/Zone.php +1 -1
- package/server/src/Observers/PurchaseRateObserver.php +20 -0
- package/server/src/Observers/ZoneObserver.php +56 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
- package/server/src/Support/Analytics/TopDrivers.php +9 -1
- package/server/src/Support/GettingStarted.php +144 -0
- package/server/src/Support/PlaceSearch.php +282 -0
- package/server/src/Support/ResolvesOrderServiceStops.php +22 -12
- package/server/src/routes.php +33 -1
- package/server/tests/AnalyticsRoutesTest.php +8 -0
- package/server/tests/CustomerContactIdentitySafetyTest.php +41 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +2 -2
- package/server/tests/GettingStartedStatusTest.php +21 -0
- package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +44 -0
- package/translations/ar-ae.yml +9 -0
- package/translations/bg-bg.yaml +9 -0
- package/translations/en-us.yaml +11 -0
- package/translations/es-pa.yaml +9 -0
- package/translations/fr-fr.yaml +9 -0
- package/translations/mn-mn.yaml +9 -0
- package/translations/pt-br.yaml +9 -0
- package/translations/ru-ru.yaml +9 -0
- package/translations/vi-vn.yaml +9 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<div class="details-wrapper" ...attributes>
|
|
2
|
+
<ContentPanel @title="Personnel" @open={{true}} @wrapperClass="bordered-top">
|
|
3
|
+
<div class="space-y-3">
|
|
4
|
+
<div class="flex flex-col space-y-2 lg:flex-row lg:items-center lg:justify-between lg:space-y-0">
|
|
5
|
+
<div>
|
|
6
|
+
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Vendor personnel</h3>
|
|
7
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">Manage contacts assigned to this vendor account.</p>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="inline-flex overflow-hidden rounded-md border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800">
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
class="inline-flex w-36 items-center justify-center whitespace-nowrap border-r border-gray-200 px-3 py-2 text-xs font-semibold dark:border-gray-700 {{if this.isAddingExistingContact 'bg-blue-600 text-white' 'text-gray-700 dark:text-gray-200'}}"
|
|
13
|
+
{{on "click" (fn this.setMode "existing")}}
|
|
14
|
+
>
|
|
15
|
+
<FaIcon @icon="address-book" class="mr-1.5" />
|
|
16
|
+
Existing contact
|
|
17
|
+
</button>
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
class="inline-flex w-36 items-center justify-center whitespace-nowrap px-3 py-2 text-xs font-semibold {{if this.isCreatingContact 'bg-blue-600 text-white' 'text-gray-700 dark:text-gray-200'}}"
|
|
21
|
+
{{on "click" (fn this.setMode "new")}}
|
|
22
|
+
>
|
|
23
|
+
<FaIcon @icon="user-plus" class="mr-1.5" />
|
|
24
|
+
New contact
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<form {{on "submit" (perform this.addPersonnel)}} class="rounded-md border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
|
30
|
+
<div class="space-y-3">
|
|
31
|
+
{{#if this.isAddingExistingContact}}
|
|
32
|
+
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
|
33
|
+
<InputGroup @name="Contact" @wrapperClass="mb-0i">
|
|
34
|
+
<ModelSelect
|
|
35
|
+
@modelName="contact"
|
|
36
|
+
@selectedModel={{this.selectedContact}}
|
|
37
|
+
@placeholder="Select existing customer contact"
|
|
38
|
+
@query={{hash type="customer"}}
|
|
39
|
+
@triggerClass="form-select form-input"
|
|
40
|
+
@infiniteScroll={{false}}
|
|
41
|
+
@renderInPlace={{true}}
|
|
42
|
+
@onChange={{this.selectContact}}
|
|
43
|
+
as |model|
|
|
44
|
+
>
|
|
45
|
+
<div class="flex flex-row items-center">
|
|
46
|
+
<div class="mr-2 w-6">
|
|
47
|
+
<Image src={{model.photo_url}} @fallbackSrc={{config "defaultValues.contactImage"}} alt={{model.name}} class="h-6 w-6 rounded-md" />
|
|
48
|
+
</div>
|
|
49
|
+
<div class="min-w-0">
|
|
50
|
+
<div class="truncate font-semibold">{{model.name}}</div>
|
|
51
|
+
<div class="truncate text-xs text-gray-500">{{n-a model.email}}</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</ModelSelect>
|
|
55
|
+
</InputGroup>
|
|
56
|
+
|
|
57
|
+
<InputGroup @name="Role" @wrapperClass="mb-0i">
|
|
58
|
+
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
|
|
59
|
+
<PowerSelect
|
|
60
|
+
@options={{this.roleOptions}}
|
|
61
|
+
@selected={{this.selectedRole}}
|
|
62
|
+
@onChange={{this.selectRole}}
|
|
63
|
+
@placeholder="Select role"
|
|
64
|
+
@triggerClass="form-select form-input"
|
|
65
|
+
as |role|
|
|
66
|
+
>
|
|
67
|
+
<div class="text-sm">
|
|
68
|
+
<div class="font-semibold normalize-in-trigger">{{role.label}}</div>
|
|
69
|
+
<div class="hide-from-trigger">{{role.description}}</div>
|
|
70
|
+
</div>
|
|
71
|
+
</PowerSelect>
|
|
72
|
+
</div>
|
|
73
|
+
</InputGroup>
|
|
74
|
+
</div>
|
|
75
|
+
{{else}}
|
|
76
|
+
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
|
77
|
+
<InputGroup @name="Name" @wrapperClass="mb-0i" @value={{this.name}} @placeholder="Personnel name" />
|
|
78
|
+
<InputGroup @name="Email" @wrapperClass="mb-0i" @type="email" @value={{this.email}} @placeholder="email@example.com" />
|
|
79
|
+
</div>
|
|
80
|
+
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
|
81
|
+
<InputGroup @name="Phone" @wrapperClass="mb-0i">
|
|
82
|
+
<PhoneInput @value={{this.phone}} @onInput={{fn (mut this.phone)}} class="form-input w-full" />
|
|
83
|
+
</InputGroup>
|
|
84
|
+
<InputGroup @name="Role" @wrapperClass="mb-0i">
|
|
85
|
+
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
|
|
86
|
+
<PowerSelect
|
|
87
|
+
@options={{this.roleOptions}}
|
|
88
|
+
@selected={{this.selectedRole}}
|
|
89
|
+
@onChange={{this.selectRole}}
|
|
90
|
+
@placeholder="Select role"
|
|
91
|
+
@triggerClass="form-select form-input"
|
|
92
|
+
as |role|
|
|
93
|
+
>
|
|
94
|
+
<div class="text-sm">
|
|
95
|
+
<div class="font-semibold normalize-in-trigger">{{role.label}}</div>
|
|
96
|
+
<div class="hide-from-trigger">{{role.description}}</div>
|
|
97
|
+
</div>
|
|
98
|
+
</PowerSelect>
|
|
99
|
+
</div>
|
|
100
|
+
</InputGroup>
|
|
101
|
+
</div>
|
|
102
|
+
{{/if}}
|
|
103
|
+
|
|
104
|
+
<div>
|
|
105
|
+
<Checkbox @value={{this.createLogin}} @onToggle={{this.toggleCreateLogin}}>Create login</Checkbox>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="flex justify-end">
|
|
109
|
+
<Button
|
|
110
|
+
@buttonType="submit"
|
|
111
|
+
@type="primary"
|
|
112
|
+
@icon="plus"
|
|
113
|
+
@text="Add Personnel"
|
|
114
|
+
@isLoading={{this.addPersonnel.isRunning}}
|
|
115
|
+
@disabled={{not this.canSubmit}}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</form>
|
|
120
|
+
|
|
121
|
+
<div class="overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
|
122
|
+
<div class="grid grid-cols-12 border-b border-gray-200 bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-800 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100">
|
|
123
|
+
<div class="col-span-6">Contact</div>
|
|
124
|
+
<div class="col-span-2">Role</div>
|
|
125
|
+
<div class="col-span-2">Status</div>
|
|
126
|
+
<div class="col-span-2 text-right">Actions</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{{#if this.loadPersonnel.isRunning}}
|
|
130
|
+
<div class="flex items-center justify-center p-4 text-sm text-gray-500 dark:text-gray-300">
|
|
131
|
+
<Spinner @iconClass="text-sky-500" />
|
|
132
|
+
<span class="ml-2">Loading personnel...</span>
|
|
133
|
+
</div>
|
|
134
|
+
{{else if this.hasPersonnel}}
|
|
135
|
+
{{#each this.personnels as |personnel|}}
|
|
136
|
+
<div class="grid grid-cols-12 items-center border-b border-gray-100 px-3 py-2 text-xs last:border-b-0 dark:border-gray-700 dark:text-gray-100">
|
|
137
|
+
<div class="col-span-6 flex min-w-0 items-center">
|
|
138
|
+
<Image src={{personnel.photo_url}} @fallbackSrc={{config "defaultValues.contactImage"}} alt={{personnel.name}} class="mr-2 h-7 w-7 rounded-md" />
|
|
139
|
+
<div class="min-w-0">
|
|
140
|
+
<div class="truncate font-semibold">{{or personnel.name personnel.public_id}}</div>
|
|
141
|
+
<div class="truncate text-gray-500 dark:text-gray-400">{{n-a personnel.email}} {{n-a personnel.phone}}</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="col-span-2 capitalize">{{personnel.role}}</div>
|
|
145
|
+
<div class="col-span-2">
|
|
146
|
+
<Badge @status={{or personnel.status "active"}} />
|
|
147
|
+
</div>
|
|
148
|
+
<div class="col-span-2 flex justify-end">
|
|
149
|
+
<Button
|
|
150
|
+
@type="danger"
|
|
151
|
+
@size="xs"
|
|
152
|
+
@icon="trash"
|
|
153
|
+
@text="Remove"
|
|
154
|
+
@onClick={{perform this.removePersonnel personnel}}
|
|
155
|
+
@isLoading={{this.removePersonnel.isRunning}}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
{{/each}}
|
|
160
|
+
{{else}}
|
|
161
|
+
<div class="p-4 text-sm text-gray-500 dark:text-gray-400">No personnel added yet.</div>
|
|
162
|
+
{{/if}}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</ContentPanel>
|
|
166
|
+
</div>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { inject as service } from '@ember/service';
|
|
3
|
+
import { tracked } from '@glimmer/tracking';
|
|
4
|
+
import { action } from '@ember/object';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
|
|
7
|
+
export default class VendorPersonnelManagerComponent extends Component {
|
|
8
|
+
@service fetch;
|
|
9
|
+
@service notifications;
|
|
10
|
+
|
|
11
|
+
@tracked personnels = [];
|
|
12
|
+
@tracked selectedContact;
|
|
13
|
+
@tracked mode = 'existing';
|
|
14
|
+
@tracked name;
|
|
15
|
+
@tracked email;
|
|
16
|
+
@tracked phone;
|
|
17
|
+
@tracked role = 'member';
|
|
18
|
+
@tracked createLogin = true;
|
|
19
|
+
|
|
20
|
+
roleOptions = [
|
|
21
|
+
{
|
|
22
|
+
label: 'Admin',
|
|
23
|
+
value: 'admin',
|
|
24
|
+
description: 'Can manage vendor account settings and personnel.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: 'Member',
|
|
28
|
+
value: 'member',
|
|
29
|
+
description: 'Can access the vendor customer workspace.',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
constructor(owner, args) {
|
|
34
|
+
super(owner, args);
|
|
35
|
+
this.loadPersonnel.perform();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get selectedRole() {
|
|
39
|
+
return this.roleOptions.find((option) => option.value === this.role);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get isAddingExistingContact() {
|
|
43
|
+
return this.mode === 'existing';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get isCreatingContact() {
|
|
47
|
+
return this.mode === 'new';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get hasPersonnel() {
|
|
51
|
+
return this.personnels.length > 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get canSubmit() {
|
|
55
|
+
if (this.isAddingExistingContact) {
|
|
56
|
+
return this.selectedContact;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this.name && this.email;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@action setMode(mode) {
|
|
63
|
+
this.mode = mode;
|
|
64
|
+
this.clearForm();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@action selectContact(contact) {
|
|
68
|
+
this.selectedContact = contact;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@action selectRole(role) {
|
|
72
|
+
this.role = role?.value ?? 'member';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@action toggleCreateLogin(value) {
|
|
76
|
+
this.createLogin = value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clearForm() {
|
|
80
|
+
this.selectedContact = null;
|
|
81
|
+
this.name = '';
|
|
82
|
+
this.email = '';
|
|
83
|
+
this.phone = '';
|
|
84
|
+
this.role = 'member';
|
|
85
|
+
this.createLogin = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@task *loadPersonnel() {
|
|
89
|
+
try {
|
|
90
|
+
const response = yield this.fetch.get(`vendors/${this.args.vendor.id}/personnels`, {}, { namespace: 'int/v1' });
|
|
91
|
+
this.personnels = response.personnels ?? [];
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.notifications.serverError(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@task *addPersonnel(event) {
|
|
98
|
+
if (event instanceof Event) {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!this.canSubmit) {
|
|
103
|
+
this.notifications.warning(this.isAddingExistingContact ? 'Select a contact to add.' : 'Name and email are required.');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const payload = {
|
|
109
|
+
role: this.role,
|
|
110
|
+
create_login: this.createLogin,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (this.isAddingExistingContact) {
|
|
114
|
+
payload.contact = this.selectedContact?.public_id ?? this.selectedContact?.uuid ?? this.selectedContact?.id;
|
|
115
|
+
} else {
|
|
116
|
+
payload.name = this.name;
|
|
117
|
+
payload.email = this.email;
|
|
118
|
+
payload.phone = this.phone;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const response = yield this.fetch.post(`vendors/${this.args.vendor.id}/personnels`, payload, { namespace: 'int/v1' });
|
|
122
|
+
const personnel = response.personnel;
|
|
123
|
+
if (personnel) {
|
|
124
|
+
this.personnels = [...this.personnels.filter((item) => item.contact_uuid !== personnel.contact_uuid), personnel];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.clearForm();
|
|
128
|
+
this.notifications.success('Personnel added.');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.notifications.serverError(error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@task *removePersonnel(personnel) {
|
|
135
|
+
try {
|
|
136
|
+
yield this.fetch.delete(`vendors/${this.args.vendor.id}/personnels/${personnel.id}`, {}, { namespace: 'int/v1' });
|
|
137
|
+
this.personnels = this.personnels.filter((item) => item.id !== personnel.id);
|
|
138
|
+
this.notifications.success('Personnel removed.');
|
|
139
|
+
} catch (error) {
|
|
140
|
+
this.notifications.serverError(error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -7,6 +7,7 @@ import { debug } from '@ember/debug';
|
|
|
7
7
|
import { task } from 'ember-concurrency';
|
|
8
8
|
import { renderCompleted, waitForInsertedAndSized } from '@fleetbase/ember-ui/utils/dom';
|
|
9
9
|
import LeafletTrackingMarkerComponent from '../leaflet-tracking-marker';
|
|
10
|
+
import ensureLeafletDrawEditNamespace from '../../utils/leaflet-draw-namespace-guard';
|
|
10
11
|
|
|
11
12
|
const DEFAULT_CENTER = [1.31, 103.85];
|
|
12
13
|
const DEFAULT_ZOOM = 11;
|
|
@@ -101,13 +102,7 @@ export default class WidgetLiveFleetComponent extends Component {
|
|
|
101
102
|
* widget is read-only.
|
|
102
103
|
*/
|
|
103
104
|
#patchLeafletDrawInitHook() {
|
|
104
|
-
|
|
105
|
-
if (L && !L.Edit) {
|
|
106
|
-
L.Edit = {};
|
|
107
|
-
}
|
|
108
|
-
if (L && !L.Marker) {
|
|
109
|
-
L.Marker = {};
|
|
110
|
-
}
|
|
105
|
+
ensureLeafletDrawEditNamespace();
|
|
111
106
|
}
|
|
112
107
|
|
|
113
108
|
/**
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{{/if}}
|
|
28
28
|
</div>
|
|
29
29
|
<div class="flex-1 min-h-0">
|
|
30
|
-
<Chart @type="line" @labels={{this.data.labels}} @datasets={{this.
|
|
30
|
+
<Chart @type="line" @labels={{this.data.labels}} @datasets={{this.chartDatasets}} @options={{this.chartOptions}} />
|
|
31
31
|
</div>
|
|
32
32
|
{{/if}}
|
|
33
33
|
</div>
|
|
@@ -3,6 +3,8 @@ import { tracked } from '@glimmer/tracking';
|
|
|
3
3
|
import { action } from '@ember/object';
|
|
4
4
|
import { inject as service } from '@ember/service';
|
|
5
5
|
import { task } from 'ember-concurrency';
|
|
6
|
+
import getCurrency from '@fleetbase/ember-ui/utils/get-currency';
|
|
7
|
+
import formatMoney from '@fleetbase/ember-accounting/utils/format-money';
|
|
6
8
|
|
|
7
9
|
const PERIODS = ['7d', '30d', '90d'];
|
|
8
10
|
|
|
@@ -22,17 +24,88 @@ export default class WidgetRevenueTrendComponent extends Component {
|
|
|
22
24
|
this.load.perform();
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
get currencyCode() {
|
|
28
|
+
return this.data?.summary?.currency ?? 'USD';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get currency() {
|
|
32
|
+
return getCurrency(this.currencyCode) ?? getCurrency('USD');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get currencyDivisor() {
|
|
36
|
+
const precision = Number(this.currency?.precision ?? 2);
|
|
37
|
+
|
|
38
|
+
return precision > 0 ? 10 ** precision : 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get chartDatasets() {
|
|
42
|
+
return (
|
|
43
|
+
this.data?.datasets?.map((dataset) => {
|
|
44
|
+
if (!this.isRevenueDataset(dataset)) {
|
|
45
|
+
return dataset;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...dataset,
|
|
50
|
+
data: dataset.data?.map((value) => this.normalizeRevenueValue(value)) ?? [],
|
|
51
|
+
};
|
|
52
|
+
}) ?? []
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isRevenueDataset(dataset) {
|
|
57
|
+
return dataset?.label === 'Revenue' || dataset?.yAxisID === 'y';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
normalizeRevenueValue(value) {
|
|
61
|
+
const numericValue = Number(value);
|
|
62
|
+
|
|
63
|
+
if (!Number.isFinite(numericValue)) {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return numericValue / this.currencyDivisor;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
formatChartCurrency(value) {
|
|
71
|
+
const numericValue = Number(value);
|
|
72
|
+
|
|
73
|
+
if (!Number.isFinite(numericValue)) {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return formatMoney(numericValue, this.currency.symbol, this.currency.precision, this.currency.thousandSeparator, this.currency.decimalSeparator);
|
|
78
|
+
}
|
|
79
|
+
|
|
25
80
|
get chartOptions() {
|
|
26
81
|
return {
|
|
27
82
|
responsive: true,
|
|
28
83
|
maintainAspectRatio: false,
|
|
29
84
|
plugins: {
|
|
30
85
|
legend: { display: false },
|
|
31
|
-
tooltip: {
|
|
86
|
+
tooltip: {
|
|
87
|
+
mode: 'index',
|
|
88
|
+
intersect: false,
|
|
89
|
+
callbacks: {
|
|
90
|
+
label: (context) => {
|
|
91
|
+
if (this.isRevenueDataset(context.dataset)) {
|
|
92
|
+
return `Revenue: ${this.formatChartCurrency(context.parsed?.y ?? context.raw)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return `${context.dataset?.label ?? 'Value'}: ${context.parsed?.y ?? context.raw}`;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
32
99
|
},
|
|
33
100
|
scales: {
|
|
34
101
|
x: { grid: { display: false } },
|
|
35
|
-
y: {
|
|
102
|
+
y: {
|
|
103
|
+
beginAtZero: true,
|
|
104
|
+
ticks: {
|
|
105
|
+
precision: this.currency.precision,
|
|
106
|
+
callback: (value) => this.formatChartCurrency(value),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
36
109
|
},
|
|
37
110
|
elements: { point: { radius: 0, hoverRadius: 4 } },
|
|
38
111
|
};
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
import Controller from '@ember/controller';
|
|
2
|
-
import { tracked } from '@glimmer/tracking';
|
|
3
2
|
import { inject as service } from '@ember/service';
|
|
3
|
+
import { isArray } from '@ember/array';
|
|
4
4
|
|
|
5
5
|
export default class ManagementContactsCustomersDetailsController extends Controller {
|
|
6
|
+
@service('universe/menu-service') menuService;
|
|
7
|
+
@service customerActions;
|
|
6
8
|
@service hostRouter;
|
|
7
|
-
@
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
@service intl;
|
|
10
|
+
|
|
11
|
+
get tabs() {
|
|
12
|
+
const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:customer:details');
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
route: 'management.contacts.index.details.index',
|
|
16
|
+
label: 'Overview',
|
|
17
|
+
},
|
|
18
|
+
...(isArray(registeredTabs) ? registeredTabs : []),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get actionButtons() {
|
|
23
|
+
const buttons = [
|
|
24
|
+
{
|
|
25
|
+
icon: 'pencil',
|
|
26
|
+
fn: () => this.hostRouter.transitionTo('console.fleet-ops.management.contacts.customers.edit', this.model),
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const accountActionButton = this.customerActions.accountActionButton(this.model);
|
|
31
|
+
if (accountActionButton) {
|
|
32
|
+
buttons.push(accountActionButton);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return buttons;
|
|
36
|
+
}
|
|
19
37
|
}
|
|
@@ -4,6 +4,10 @@ import { inject as service } from '@ember/service';
|
|
|
4
4
|
export default class ManagementContactsCustomersController extends ManagementContactsIndexController {
|
|
5
5
|
@service('customerActions') contactActions;
|
|
6
6
|
|
|
7
|
+
get customerActions() {
|
|
8
|
+
return this.contactActions;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
/** columns */
|
|
8
12
|
get columns() {
|
|
9
13
|
return [
|
|
@@ -13,7 +17,7 @@ export default class ManagementContactsCustomersController extends ManagementCon
|
|
|
13
17
|
valuePath: 'name',
|
|
14
18
|
|
|
15
19
|
cellComponent: 'table/cell/media-name',
|
|
16
|
-
action: this.
|
|
20
|
+
action: this.customerActions.transition.view,
|
|
17
21
|
permission: 'fleet-ops view contact',
|
|
18
22
|
resizable: true,
|
|
19
23
|
sortable: true,
|
|
@@ -75,7 +79,7 @@ export default class ManagementContactsCustomersController extends ManagementCon
|
|
|
75
79
|
label: this.intl.t('column.address'),
|
|
76
80
|
valuePath: 'address',
|
|
77
81
|
cellComponent: 'table/cell/anchor',
|
|
78
|
-
action: this.
|
|
82
|
+
action: this.customerActions.viewPlace,
|
|
79
83
|
|
|
80
84
|
resizable: true,
|
|
81
85
|
sortable: true,
|
|
@@ -118,20 +122,27 @@ export default class ManagementContactsCustomersController extends ManagementCon
|
|
|
118
122
|
actions: [
|
|
119
123
|
{
|
|
120
124
|
label: this.intl.t('common.view-resource', { resource: this.intl.t('resource.customer') }),
|
|
121
|
-
|
|
125
|
+
icon: 'eye',
|
|
126
|
+
fn: this.customerActions.transition.view,
|
|
122
127
|
permission: 'fleet-ops view contact',
|
|
123
128
|
},
|
|
124
129
|
{
|
|
125
130
|
label: this.intl.t('common.edit-resource', { resource: this.intl.t('resource.customer') }),
|
|
126
|
-
|
|
131
|
+
icon: 'pencil',
|
|
132
|
+
fn: this.customerActions.transition.edit,
|
|
127
133
|
permission: 'fleet-ops update contact',
|
|
128
134
|
},
|
|
129
135
|
{
|
|
130
136
|
separator: true,
|
|
131
137
|
},
|
|
138
|
+
...this.customerActions.accountRowActionItems(),
|
|
139
|
+
{
|
|
140
|
+
separator: true,
|
|
141
|
+
},
|
|
132
142
|
{
|
|
133
143
|
label: this.intl.t('common.delete-resource', { resource: this.intl.t('resource.customer') }),
|
|
134
|
-
|
|
144
|
+
icon: 'trash',
|
|
145
|
+
fn: this.customerActions.delete,
|
|
135
146
|
permission: 'fleet-ops delete contact',
|
|
136
147
|
},
|
|
137
148
|
],
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
import Controller from '@ember/controller';
|
|
2
|
-
import { tracked } from '@glimmer/tracking';
|
|
3
2
|
import { inject as service } from '@ember/service';
|
|
3
|
+
import { isArray } from '@ember/array';
|
|
4
4
|
|
|
5
5
|
export default class ManagementContactsIndexDetailsController extends Controller {
|
|
6
|
+
@service('universe/menu-service') menuService;
|
|
7
|
+
@service contactActions;
|
|
6
8
|
@service hostRouter;
|
|
7
|
-
@
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
@service intl;
|
|
10
|
+
|
|
11
|
+
get tabs() {
|
|
12
|
+
const registeredTabs = this.menuService.getMenuItems('fleet-ops:component:contact:details');
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
route: 'management.contacts.index.details.index',
|
|
16
|
+
label: 'Overview',
|
|
17
|
+
},
|
|
18
|
+
...(isArray(registeredTabs) ? registeredTabs : []),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get actionButtons() {
|
|
23
|
+
const buttons = [
|
|
24
|
+
{
|
|
25
|
+
icon: 'pencil',
|
|
26
|
+
fn: () => this.hostRouter.transitionTo('console.fleet-ops.management.contacts.index.edit', this.model),
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const accountActionButton = this.contactActions.accountActionButton(this.model);
|
|
31
|
+
if (accountActionButton) {
|
|
32
|
+
buttons.push(accountActionButton);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return buttons;
|
|
36
|
+
}
|
|
19
37
|
}
|
|
@@ -168,19 +168,26 @@ export default class ManagementContactsIndexController extends Controller {
|
|
|
168
168
|
actions: [
|
|
169
169
|
{
|
|
170
170
|
label: this.intl.t('common.view-resource', { resource: this.intl.t('resource.contact') }),
|
|
171
|
+
icon: 'eye',
|
|
171
172
|
fn: this.contactActions.transition.view,
|
|
172
173
|
permission: 'fleet-ops view contact',
|
|
173
174
|
},
|
|
174
175
|
{
|
|
175
176
|
label: this.intl.t('common.edit-resource', { resource: this.intl.t('resource.contact') }),
|
|
177
|
+
icon: 'pencil',
|
|
176
178
|
fn: this.contactActions.transition.edit,
|
|
177
179
|
permission: 'fleet-ops update contact',
|
|
178
180
|
},
|
|
179
181
|
{
|
|
180
182
|
separator: true,
|
|
181
183
|
},
|
|
184
|
+
...this.contactActions.accountRowActionItems(),
|
|
185
|
+
{
|
|
186
|
+
separator: true,
|
|
187
|
+
},
|
|
182
188
|
{
|
|
183
189
|
label: this.intl.t('common.delete-resource', { resource: this.intl.t('resource.contact') }),
|
|
190
|
+
icon: 'trash',
|
|
184
191
|
fn: this.contactActions.delete,
|
|
185
192
|
permission: 'fleet-ops delete contact',
|
|
186
193
|
},
|