@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.
Files changed (159) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/details.hbs +1 -2
  4. package/addon/components/customer/form.hbs +63 -51
  5. package/addon/components/customer/form.js +27 -1
  6. package/addon/components/customer/orders.hbs +48 -0
  7. package/addon/components/customer/orders.js +67 -5
  8. package/addon/components/driver/form.hbs +1 -1
  9. package/addon/components/home/getting-started-guidance.hbs +113 -0
  10. package/addon/components/home/getting-started-guidance.js +107 -0
  11. package/addon/components/issue/details/comments.hbs +3 -0
  12. package/addon/components/issue/details/comments.js +3 -0
  13. package/addon/components/issue/details/correspondence.hbs +9 -0
  14. package/addon/components/issue/details/correspondence.js +83 -0
  15. package/addon/components/issue/details/documents.hbs +69 -0
  16. package/addon/components/issue/details/documents.js +85 -0
  17. package/addon/components/issue/details.hbs +220 -59
  18. package/addon/components/issue/details.js +245 -0
  19. package/addon/components/issue/form.hbs +19 -1
  20. package/addon/components/issue/panel-header.hbs +43 -0
  21. package/addon/components/issue/panel-header.js +22 -0
  22. package/addon/components/issue/timeline.hbs +40 -0
  23. package/addon/components/issue/timeline.js +45 -0
  24. package/addon/components/leaflet-tracking-marker.js +3 -0
  25. package/addon/components/map/drawer/position-listing.js +3 -0
  26. package/addon/components/map/leaflet-live-map.hbs +2 -2
  27. package/addon/components/map/leaflet-live-map.js +17 -0
  28. package/addon/components/map/order-list-overlay.hbs +4 -4
  29. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  30. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  31. package/addon/components/modals/issue-assign.hbs +20 -0
  32. package/addon/components/modals/issue-assign.js +49 -0
  33. package/addon/components/modals/issue-close.hbs +30 -0
  34. package/addon/components/modals/issue-close.js +73 -0
  35. package/addon/components/modals/issue-status.hbs +20 -0
  36. package/addon/components/modals/issue-status.js +43 -0
  37. package/addon/components/modals/reset-customer-credentials.js +12 -6
  38. package/addon/components/modals/update-order-activity.hbs +51 -17
  39. package/addon/components/modals/update-order-activity.js +59 -5
  40. package/addon/components/order/details/activity.js +2 -2
  41. package/addon/components/order/details/proof.hbs +40 -38
  42. package/addon/components/order/details/proof.js +31 -4
  43. package/addon/components/order/details.hbs +5 -5
  44. package/addon/components/order/details.js +19 -1
  45. package/addon/components/order/header.hbs +1 -1
  46. package/addon/components/route-list.hbs +0 -12
  47. package/addon/components/service-rate/details.hbs +1 -1
  48. package/addon/components/service-rate/form.hbs +1 -1
  49. package/addon/components/service-rate/form.js +4 -0
  50. package/addon/components/vendor/personnel-manager.hbs +166 -0
  51. package/addon/components/vendor/personnel-manager.js +143 -0
  52. package/addon/components/widget/live-fleet.hbs +1 -0
  53. package/addon/components/widget/live-fleet.js +2 -7
  54. package/addon/components/widget/revenue-trend.hbs +1 -1
  55. package/addon/components/widget/revenue-trend.js +75 -2
  56. package/addon/controllers/management/contacts/customers/details.js +31 -13
  57. package/addon/controllers/management/contacts/customers.js +16 -5
  58. package/addon/controllers/management/contacts/index/details.js +31 -13
  59. package/addon/controllers/management/contacts/index.js +7 -0
  60. package/addon/controllers/management/issues/index/details.js +52 -6
  61. package/addon/controllers/management/vendors/index/details.js +4 -0
  62. package/addon/controllers/operations/orders/index/details.js +11 -3
  63. package/addon/controllers/operations/orders/index.js +1 -1
  64. package/addon/extension.js +19 -12
  65. package/addon/helpers/format-duration.js +1 -38
  66. package/addon/initializers/load-leaflet-assets.js +5 -6
  67. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  68. package/addon/routes/management/issues/index/details.js +1 -1
  69. package/addon/routes.js +1 -0
  70. package/addon/services/contact-actions.js +231 -1
  71. package/addon/services/getting-started.js +44 -0
  72. package/addon/services/issue-actions.js +71 -0
  73. package/addon/services/leaflet-map-manager.js +2 -0
  74. package/addon/services/map-adapter/leaflet.js +19 -0
  75. package/addon/services/service-areas.js +3 -0
  76. package/addon/styles/fleetops-engine.css +1036 -0
  77. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  78. package/addon/templates/management/contacts/index/details.hbs +1 -1
  79. package/addon/templates/management/contacts.hbs +2 -2
  80. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  81. package/addon/templates/management/issues/index/details.hbs +3 -2
  82. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  83. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  84. package/addon/templates/operations/orders/index.hbs +1 -1
  85. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  86. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  87. package/addon/templates/settings/payments/onboard.hbs +8 -8
  88. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  89. package/addon/utils/leaflet-plugin-loader.js +219 -0
  90. package/addon/utils/setup-customer-portal.js +4 -64
  91. package/app/components/home/getting-started-guidance.js +1 -0
  92. package/app/components/issue/details/comments.js +1 -0
  93. package/app/components/issue/details/correspondence.js +1 -0
  94. package/app/components/issue/details/documents.js +1 -0
  95. package/app/components/issue/panel-header.js +1 -0
  96. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  97. package/app/components/modals/issue-assign.js +1 -0
  98. package/app/components/modals/issue-close.js +1 -0
  99. package/app/components/modals/issue-status.js +1 -0
  100. package/app/components/vendor/personnel-manager.js +1 -0
  101. package/app/helpers/format-duration.js +1 -1
  102. package/app/services/getting-started.js +1 -0
  103. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  104. package/app/utils/leaflet-plugin-loader.js +1 -0
  105. package/composer.json +1 -1
  106. package/extension.json +1 -1
  107. package/package.json +5 -5
  108. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  109. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  110. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  111. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  112. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  113. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  114. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  115. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  116. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -25
  117. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  118. package/server/src/Http/Controllers/Internal/v1/ContactController.php +135 -0
  119. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  120. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  121. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  122. package/server/src/Http/Controllers/Internal/v1/OrderController.php +172 -97
  123. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  124. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  125. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  126. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  127. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  128. package/server/src/Http/Resources/v1/Issue.php +25 -0
  129. package/server/src/Http/Resources/v1/Order.php +20 -18
  130. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  131. package/server/src/Models/Contact.php +30 -51
  132. package/server/src/Models/Issue.php +47 -6
  133. package/server/src/Models/Order.php +33 -6
  134. package/server/src/Models/ServiceArea.php +1 -1
  135. package/server/src/Models/VendorPersonnel.php +10 -1
  136. package/server/src/Models/Zone.php +1 -1
  137. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  138. package/server/src/Observers/ZoneObserver.php +56 -0
  139. package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
  140. package/server/src/Support/Analytics/TopDrivers.php +9 -1
  141. package/server/src/Support/GettingStarted.php +144 -0
  142. package/server/src/Support/PlaceSearch.php +282 -0
  143. package/server/src/Support/ResolvesOrderServiceStops.php +22 -12
  144. package/server/src/routes.php +33 -1
  145. package/server/tests/AnalyticsRoutesTest.php +8 -0
  146. package/server/tests/CustomerContactIdentitySafetyTest.php +41 -0
  147. package/server/tests/DriverVehicleStatusDefaultTest.php +2 -2
  148. package/server/tests/GettingStartedStatusTest.php +21 -0
  149. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  150. package/server/tests/ServiceAreaZoneApiShapeTest.php +44 -0
  151. package/translations/ar-ae.yml +9 -0
  152. package/translations/bg-bg.yaml +9 -0
  153. package/translations/en-us.yaml +11 -0
  154. package/translations/es-pa.yaml +9 -0
  155. package/translations/fr-fr.yaml +9 -0
  156. package/translations/mn-mn.yaml +9 -0
  157. package/translations/pt-br.yaml +9 -0
  158. package/translations/ru-ru.yaml +9 -0
  159. 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
+ }
@@ -20,6 +20,7 @@
20
20
  @zoom={{this.zoom}}
21
21
  @zoomControl={{true}}
22
22
  @onLoad={{this.onMapLoad}}
23
+ @scrollWheelZoom={{false}}
23
24
  {{set-container-dimensions}}
24
25
  as |layers|
25
26
  >
@@ -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
- const L = typeof window !== 'undefined' ? (window.L ?? window.leaflet) : null;
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.data.datasets}} @options={{this.chartOptions}} />
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: { mode: 'index', intersect: false },
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: { beginAtZero: true, ticks: { precision: 0 } },
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
- @tracked tabs = [
8
- {
9
- route: 'management.contacts.customers.details.index',
10
- label: 'Overview',
11
- },
12
- ];
13
- @tracked actionButtons = [
14
- {
15
- icon: 'pencil',
16
- fn: () => this.hostRouter.transitionTo('console.fleet-ops.management.contacts.customers.edit', this.model),
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.contactActions.transition.view,
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.contactActions.viewPlace,
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
- fn: this.contactActions.transition.view,
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
- fn: this.contactActions.transition.edit,
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
- fn: this.contactActions.delete,
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
- @tracked tabs = [
8
- {
9
- route: 'management.contacts.index.details.index',
10
- label: 'Overview',
11
- },
12
- ];
13
- @tracked actionButtons = [
14
- {
15
- icon: 'pencil',
16
- fn: () => this.hostRouter.transitionTo('console.fleet-ops.management.contacts.index.edit', this.model),
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
  },