@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,40 @@
1
+ <div class="issue-timeline" {{did-update this.reload this.issueId}} ...attributes>
2
+ {{#if this.loadTimeline.isRunning}}
3
+ <div class="issue-timeline-loading">
4
+ <Spinner @loadingMessage="Loading timeline..." />
5
+ </div>
6
+ {{else if this.hasEvents}}
7
+ <ol class="issue-timeline-list">
8
+ {{#each this.events as |event|}}
9
+ <li class="issue-timeline-event tone-{{event.tone}}">
10
+ <div class="issue-timeline-marker">
11
+ <FaIcon @icon={{event.icon}} />
12
+ </div>
13
+ <div class="issue-timeline-card">
14
+ <div class="issue-timeline-card-header">
15
+ <div>
16
+ <div class="issue-timeline-label">{{event.label}}</div>
17
+ <div class="issue-timeline-actor">{{n-a event.actor_name "Someone"}}</div>
18
+ </div>
19
+ <time>{{n-a (format-date-fns event.created_at "dd MMM yyyy HH:mm")}}</time>
20
+ </div>
21
+ {{#if event.description}}
22
+ <p>{{event.description}}</p>
23
+ {{/if}}
24
+ {{#if event.meta.file_url}}
25
+ <a href={{event.meta.file_url}} target="_blank" rel="noopener noreferrer" class="issue-timeline-link">
26
+ <FaIcon @icon="download" />
27
+ <span>{{n-a event.meta.file_name "Download file"}}</span>
28
+ </a>
29
+ {{/if}}
30
+ </div>
31
+ </li>
32
+ {{/each}}
33
+ </ol>
34
+ {{else}}
35
+ <div class="issue-detail-empty issue-detail-empty-compact">
36
+ <FaIcon @icon="timeline" />
37
+ <span>No issue activity yet.</span>
38
+ </div>
39
+ {{/if}}
40
+ </div>
@@ -0,0 +1,45 @@
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 { debug } from '@ember/debug';
6
+ import { task } from 'ember-concurrency';
7
+
8
+ export default class IssueTimelineComponent extends Component {
9
+ @service fetch;
10
+ @service notifications;
11
+
12
+ @tracked events = [];
13
+
14
+ constructor() {
15
+ super(...arguments);
16
+ this.loadTimeline.perform();
17
+ }
18
+
19
+ get issueId() {
20
+ return this.args.resource?.id || this.args.resource?.uuid || this.args.resource?.public_id;
21
+ }
22
+
23
+ get hasEvents() {
24
+ return this.events.length > 0;
25
+ }
26
+
27
+ @task *loadTimeline() {
28
+ if (!this.issueId) {
29
+ this.events = [];
30
+ return;
31
+ }
32
+
33
+ try {
34
+ const response = yield this.fetch.get(`issues/${this.issueId}/timeline`, {}, { namespace: 'int/v1' });
35
+ this.events = response.events ?? response.timeline ?? [];
36
+ } catch (err) {
37
+ debug('Failed to load issue timeline: ' + err.message);
38
+ this.notifications.serverError(err);
39
+ }
40
+ }
41
+
42
+ @action reload() {
43
+ this.loadTimeline.perform();
44
+ }
45
+ }
@@ -1,5 +1,6 @@
1
1
  import MarkerLayer from 'ember-leaflet/components/marker-layer';
2
2
  import { isArray } from '@ember/array';
3
+ import ensureLeafletDrawEditNamespace from '../utils/leaflet-draw-namespace-guard';
3
4
 
4
5
  const __draggingHotfix = (layer) => {
5
6
  if (!layer.dragging) {
@@ -23,6 +24,7 @@ const arrayFromLatLng = (latlng) => {
23
24
  };
24
25
 
25
26
  const L = window.leaflet || window.L;
27
+ ensureLeafletDrawEditNamespace(L);
26
28
  const oldIE = L.DomUtil.TRANSFORM === 'msTransform';
27
29
  L.TrackingMarker = L.Marker.extend({
28
30
  // eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects
@@ -291,6 +293,7 @@ export default class LeafletTrackingMarkerComponent extends MarkerLayer {
291
293
  }
292
294
 
293
295
  createLayer() {
296
+ ensureLeafletDrawEditNamespace(L);
294
297
  const { rotationAngle, location } = this.args;
295
298
  const bearingAngle = rotationAngle ?? computeBearing([0, 0], location);
296
299
  return new L.TrackingMarker(...this.requiredOptions, { ...this.options, bearingAngle });
@@ -7,6 +7,7 @@ import { isArray } from '@ember/array';
7
7
  import { htmlSafe } from '@ember/template';
8
8
  import { startOfWeek, endOfWeek, format } from 'date-fns';
9
9
  import getModelName from '@fleetbase/ember-core/utils/get-model-name';
10
+ import ensureLeafletDrawEditNamespace from '../../../utils/leaflet-draw-namespace-guard';
10
11
 
11
12
  const L = window.leaflet || window.L;
12
13
 
@@ -412,6 +413,8 @@ export default class MapDrawerPositionListingComponent extends Component {
412
413
  return;
413
414
  }
414
415
 
416
+ ensureLeafletDrawEditNamespace(L);
417
+ ensureLeafletDrawEditNamespace();
415
418
  const marker = L.circleMarker([lat, lng], {
416
419
  radius: 3,
417
420
  color: '#3b82f6',
@@ -39,7 +39,7 @@
39
39
  @mapClass={{@mapClass}}
40
40
  ...attributes
41
41
  />
42
- {{else}}
42
+ {{else if this.leafletPluginsReady}}
43
43
  {{! ─── Leaflet branch (default) ────────────────────────────────────── }}
44
44
  <LeafletMap
45
45
  id={{this.id}}
@@ -374,4 +374,4 @@
374
374
  @liveMap={{this}}
375
375
  @onDrawerReady={{this.setDrawerContext}}
376
376
  />
377
- {{/if}} --}}
377
+ {{/if}} --}}
@@ -11,6 +11,7 @@ import { all } from 'rsvp';
11
11
  import { task, timeout } from 'ember-concurrency';
12
12
  import { next } from '@ember/runloop';
13
13
  import getModelName from '@fleetbase/ember-core/utils/get-model-name';
14
+ import ensureLeafletPluginsReady, { hasLeafletPluginsReady } from '../../utils/leaflet-plugin-loader';
14
15
 
15
16
  export default class MapLeafletLiveMapComponent extends Component {
16
17
  liveViewportReloadDebounceMs = 300;
@@ -53,6 +54,7 @@ export default class MapLeafletLiveMapComponent extends Component {
53
54
  @tracked drivers = [];
54
55
  @tracked vehicles = [];
55
56
  @tracked places = [];
57
+ @tracked leafletPluginsReady = hasLeafletPluginsReady();
56
58
  _viewportReloadLocks = new Set();
57
59
 
58
60
  constructor() {
@@ -79,6 +81,10 @@ export default class MapLeafletLiveMapComponent extends Component {
79
81
  this._geofenceExitedHandler = this.#handleGeofenceExited.bind(this);
80
82
  this.universe.on('fleet-ops.geofence.entered', this._geofenceEnteredHandler);
81
83
  this.universe.on('fleet-ops.geofence.exited', this._geofenceExitedHandler);
84
+
85
+ if (!this.shouldUseGoogleMaps && !this.leafletPluginsReady) {
86
+ this.prepareLeafletPlugins();
87
+ }
82
88
  }
83
89
 
84
90
  willDestroy() {
@@ -329,6 +335,17 @@ export default class MapLeafletLiveMapComponent extends Component {
329
335
  return this.mapSettings.isGoogleMaps;
330
336
  }
331
337
 
338
+ async prepareLeafletPlugins() {
339
+ try {
340
+ await ensureLeafletPluginsReady();
341
+ if (!this.isDestroying && !this.isDestroyed) {
342
+ this.leafletPluginsReady = true;
343
+ }
344
+ } catch (error) {
345
+ debug('Failed to prepare Leaflet plugins: ' + error.message);
346
+ }
347
+ }
348
+
332
349
  /**
333
350
  * Get valid zoom level for map initialization
334
351
  * @returns {number} Valid zoom level between 1-20
@@ -44,10 +44,10 @@
44
44
  <div class="next-dd-menu-seperator"></div>
45
45
  <div class="px-1">
46
46
  <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.orderActions.bulkCancel this.overlay.selectedOrders)}}>{{t
47
- "fleet-ops.component.order-list-overlay.cancel"
47
+ "order-list-overlay.cancel"
48
48
  }}</a>
49
49
  <a href="javascript:;" class="next-dd-item text-danger" {{on "click" (dropdown-fn dd this.orderActions.bulkDelete this.overlay.selectedOrders)}}>{{t
50
- "fleet-ops.component.order-list-overlay.delete"
50
+ "order-list-overlay.delete"
51
51
  }}</a>
52
52
  </div>
53
53
  </div>
@@ -70,10 +70,10 @@
70
70
  <div class="next-dd-menu-seperator"></div>
71
71
  <div class="px-1">
72
72
  <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd (transition-to "operations.orders.index.new"))}}>{{t
73
- "fleet-ops.component.order-list-overlay.create-order"
73
+ "order-list-overlay.create-order"
74
74
  }}</a>
75
75
  <a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.fleetActions.modal.create)}}>{{t
76
- "fleet-ops.component.order-list-overlay.create-fleet"
76
+ "order-list-overlay.create-fleet"
77
77
  }}</a>
78
78
  </div>
79
79
  </div>
@@ -0,0 +1,30 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <div class="mb-4 rounded-md border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-900 dark:border-yellow-700 dark:bg-yellow-900 dark:text-yellow-100">
4
+ <div class="mb-1 font-semibold">Create a company customer account</div>
5
+ <div>
6
+ This action will create a vendor, assign
7
+ {{this.customer.name}}
8
+ as admin personnel, and migrate any customer-owned portal history to the vendor account.
9
+ </div>
10
+ </div>
11
+
12
+ <InputGroup @name="Primary Personnel" @wrapperClass="mb-0i">
13
+ <div class="flex items-start rounded-md border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-700 dark:bg-gray-800">
14
+ <Image src={{this.customer.photo_url}} @fallbackSrc={{config "defaultValues.contactImage"}} alt={{this.customer.name}} class="mr-2 h-6 w-6 rounded-md" />
15
+ <div class="min-w-0">
16
+ <div class="leading-4 text-sm truncate font-semibold dark:text-gray-100">{{this.customer.name}}</div>
17
+ <div class="truncate text-sm text-gray-500 dark:text-gray-400">{{n-a this.customer.email}}</div>
18
+ </div>
19
+ </div>
20
+ </InputGroup>
21
+
22
+ <div class="mt-4 grid grid-cols-1 gap-3 text-xs lg:grid-cols-2">
23
+ <InputGroup @name="Vendor Name" @value={{this.vendorName}} @placeholder="Company name" @wrapperClass="mb-0i" />
24
+ <InputGroup @name="Email" @type="email" @value={{this.vendorEmail}} @placeholder="billing@example.com" @wrapperClass="mb-0i" />
25
+ <InputGroup @name="Phone" @wrapperClass="mb-0i">
26
+ <PhoneInput @value={{this.vendorPhone}} @onInput={{fn (mut this.vendorPhone)}} class="form-input w-full" />
27
+ </InputGroup>
28
+ </div>
29
+ </div>
30
+ </Modal::Default>
@@ -0,0 +1,55 @@
1
+ import Component from '@glimmer/component';
2
+ import { inject as service } from '@ember/service';
3
+ import { tracked } from '@glimmer/tracking';
4
+
5
+ export default class ModalsConvertCustomerToVendorComponent extends Component {
6
+ @service fetch;
7
+ @service notifications;
8
+
9
+ @tracked customer;
10
+ @tracked vendorName;
11
+ @tracked vendorEmail;
12
+ @tracked vendorPhone;
13
+ @tracked options = {};
14
+
15
+ constructor(owner, { options }) {
16
+ super(...arguments);
17
+ this.customer = options.customer;
18
+ this.vendorName = this.customer?.name;
19
+ this.vendorEmail = this.customer?.email;
20
+ this.vendorPhone = this.customer?.phone;
21
+ this.options = options;
22
+ this.setupOptions();
23
+ }
24
+
25
+ setupOptions() {
26
+ this.options.title = 'Convert Contact to Vendor';
27
+ this.options.acceptButtonText = 'Create Vendor';
28
+ this.options.confirm = async (modal) => {
29
+ modal.startLoading();
30
+
31
+ try {
32
+ const response = await this.fetch.post(
33
+ `contacts/${this.customer.id}/convert-to-vendor`,
34
+ {
35
+ name: this.vendorName,
36
+ email: this.vendorEmail,
37
+ phone: this.vendorPhone,
38
+ },
39
+ { namespace: 'int/v1' }
40
+ );
41
+
42
+ this.notifications.success('Customer converted to vendor account.');
43
+
44
+ if (typeof this.options.onConverted === 'function') {
45
+ this.options.onConverted(response.vendor);
46
+ }
47
+
48
+ modal.done();
49
+ } catch (error) {
50
+ this.notifications.serverError(error);
51
+ modal.stopLoading();
52
+ }
53
+ };
54
+ }
55
+ }
@@ -0,0 +1,20 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <InputGroup @name="Assignee" @helpText="Choose the operator responsible for following up on this issue.">
4
+ <ModelSelect
5
+ @modelName="user"
6
+ @selectedModel={{this.selectedAssignee}}
7
+ @placeholder="Select assignee"
8
+ @triggerClass="form-select form-input"
9
+ @infiniteScroll={{false}}
10
+ @renderInPlace={{true}}
11
+ @query={{hash is_not_customer=true}}
12
+ @allowClear={{true}}
13
+ @onChange={{fn (mut this.selectedAssignee)}}
14
+ as |model|
15
+ >
16
+ {{model.name}}
17
+ </ModelSelect>
18
+ </InputGroup>
19
+ </div>
20
+ </Modal::Default>
@@ -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
+
5
+ export default class ModalsIssueAssignComponent extends Component {
6
+ @service notifications;
7
+
8
+ @tracked options = {};
9
+ @tracked issue;
10
+ @tracked selectedAssignee;
11
+
12
+ constructor(owner, { options }) {
13
+ super(...arguments);
14
+ this.options = options;
15
+ this.issue = options.issue;
16
+ this.selectedAssignee = this.issue?.assignee;
17
+ this.setupOptions();
18
+ }
19
+
20
+ setupOptions() {
21
+ this.options.title = 'Assign Issue';
22
+ this.options.acceptButtonText = 'Assign Issue';
23
+ this.options.acceptButtonIcon = 'user-check';
24
+ this.options.confirm = async (modal) => {
25
+ modal.startLoading();
26
+
27
+ try {
28
+ if (this.selectedAssignee?.type === 'customer') {
29
+ this.notifications.warning('Customers cannot be assigned to issues.');
30
+ modal.stopLoading();
31
+ return;
32
+ }
33
+
34
+ this.issue.assignee = this.selectedAssignee;
35
+ await this.issue.save();
36
+ this.notifications.success('Issue assignment updated.');
37
+
38
+ if (typeof this.options.onSaved === 'function') {
39
+ await this.options.onSaved(this.issue);
40
+ }
41
+
42
+ modal.done();
43
+ } catch (error) {
44
+ this.notifications.serverError(error);
45
+ modal.stopLoading();
46
+ }
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,30 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <div class="issue-close-modal-summary">
4
+ {{#if this.vehicleLabel}}
5
+ <div>
6
+ <span>Vehicle</span>
7
+ <strong>{{this.vehicleLabel}}</strong>
8
+ </div>
9
+ {{/if}}
10
+ {{#if this.orderLabel}}
11
+ <div>
12
+ <span>Order</span>
13
+ <strong>{{this.orderLabel}}</strong>
14
+ </div>
15
+ {{/if}}
16
+ <div>
17
+ <span>Summary</span>
18
+ <strong>{{n-a this.issue.title}}</strong>
19
+ </div>
20
+ <div>
21
+ <span>Description</span>
22
+ <strong>{{n-a this.issue.report "No report was provided."}}</strong>
23
+ </div>
24
+ </div>
25
+
26
+ <InputGroup @name="Close Note" @helpText="Describe why this issue is being closed.">
27
+ <Textarea @value={{this.note}} class="form-input w-full" rows={{5}} placeholder="Resolution note" />
28
+ </InputGroup>
29
+ </div>
30
+ </Modal::Default>
@@ -0,0 +1,73 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+
5
+ export default class ModalsIssueCloseComponent extends Component {
6
+ @service currentUser;
7
+ @service notifications;
8
+
9
+ @tracked options = {};
10
+ @tracked issue;
11
+ @tracked note;
12
+
13
+ constructor(owner, { options }) {
14
+ super(...arguments);
15
+ this.options = options;
16
+ this.issue = options.issue;
17
+ this.setupOptions();
18
+ }
19
+
20
+ get orderLabel() {
21
+ return this.issue?.order?.tracking || this.issue?.order?.public_id;
22
+ }
23
+
24
+ get vehicleLabel() {
25
+ return this.issue?.vehicle?.displayName || this.issue?.vehicle_name;
26
+ }
27
+
28
+ setupOptions() {
29
+ this.options.title = `Close ${this.issue?.public_id || 'Issue'}`;
30
+ this.options.acceptButtonText = 'Close Issue';
31
+ this.options.acceptButtonIcon = 'circle-check';
32
+ this.options.acceptButtonScheme = 'success';
33
+ this.options.confirm = async (modal) => {
34
+ if (!this.note) {
35
+ this.notifications.warning('Add a close note before closing the issue.');
36
+ return;
37
+ }
38
+
39
+ modal.startLoading();
40
+
41
+ try {
42
+ const closedAt = new Date().toISOString();
43
+ const user = this.currentUser.user;
44
+ const meta = this.issue.meta ?? {};
45
+
46
+ this.issue.status = 'closed';
47
+ this.issue.resolved_at = new Date();
48
+ this.issue.meta = {
49
+ ...meta,
50
+ resolution: {
51
+ ...(meta.resolution ?? {}),
52
+ note: this.note,
53
+ closed_at: closedAt,
54
+ closed_by_uuid: user?.id,
55
+ closed_by_name: user?.name,
56
+ },
57
+ };
58
+
59
+ await this.issue.save();
60
+ this.notifications.success('Issue closed.');
61
+
62
+ if (typeof this.options.onSaved === 'function') {
63
+ await this.options.onSaved(this.issue);
64
+ }
65
+
66
+ modal.done();
67
+ } catch (error) {
68
+ this.notifications.serverError(error);
69
+ modal.stopLoading();
70
+ }
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,20 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <InputGroup @name="Status" @helpText="Choose the current workflow state for this issue.">
4
+ <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
5
+ <PowerSelect
6
+ @options={{@options.statusOptions}}
7
+ @selected={{this.selectedStatus}}
8
+ @onChange={{fn (mut this.selectedStatus)}}
9
+ @placeholder="Select status"
10
+ @triggerClass="form-select form-input"
11
+ @renderInPlace={{true}}
12
+ as |status|
13
+ >
14
+ <div class="font-semibold normalize-in-trigger">{{status.label}}</div>
15
+ <div class="hide-from-trigger text-xs text-gray-500 dark:text-gray-400">{{status.description}}</div>
16
+ </PowerSelect>
17
+ </div>
18
+ </InputGroup>
19
+ </div>
20
+ </Modal::Default>
@@ -0,0 +1,43 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+
5
+ export default class ModalsIssueStatusComponent extends Component {
6
+ @service notifications;
7
+
8
+ @tracked options = {};
9
+ @tracked issue;
10
+ @tracked selectedStatus;
11
+
12
+ constructor(owner, { options }) {
13
+ super(...arguments);
14
+ this.options = options;
15
+ this.issue = options.issue;
16
+ this.selectedStatus = options.statusOptions?.find((status) => status.value === this.issue?.status);
17
+ this.setupOptions();
18
+ }
19
+
20
+ setupOptions() {
21
+ this.options.title = 'Change Issue Status';
22
+ this.options.acceptButtonText = 'Update Status';
23
+ this.options.acceptButtonIcon = 'save';
24
+ this.options.confirm = async (modal) => {
25
+ modal.startLoading();
26
+
27
+ try {
28
+ this.issue.status = this.selectedStatus?.value ?? this.issue.status;
29
+ await this.issue.save();
30
+ this.notifications.success('Issue status updated.');
31
+
32
+ if (typeof this.options.onSaved === 'function') {
33
+ await this.options.onSaved(this.issue);
34
+ }
35
+
36
+ modal.done();
37
+ } catch (error) {
38
+ this.notifications.serverError(error);
39
+ modal.stopLoading();
40
+ }
41
+ };
42
+ }
43
+ }
@@ -2,6 +2,8 @@ import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
 
5
+ const INTERNAL_NAMESPACE = 'int/v1';
6
+
5
7
  export default class ModalsResetCustomerCredentialsComponent extends Component {
6
8
  @service fetch;
7
9
  @service notifications;
@@ -26,12 +28,16 @@ export default class ModalsResetCustomerCredentialsComponent extends Component {
26
28
  modal.startLoading();
27
29
 
28
30
  try {
29
- await this.fetch.post('customers/reset-credentials', {
30
- customer: this.customer.id,
31
- password: this.password,
32
- password_confirmation: this.confirmPassword,
33
- send_credentials: this.sendCredentials,
34
- });
31
+ await this.fetch.post(
32
+ 'customers/reset-credentials',
33
+ {
34
+ customer: this.customer.id,
35
+ password: this.password,
36
+ password_confirmation: this.confirmPassword,
37
+ send_credentials: this.sendCredentials,
38
+ },
39
+ { namespace: INTERNAL_NAMESPACE }
40
+ );
35
41
 
36
42
  this.notifications.success('Customer password reset.');
37
43