@fleetbase/fleetops-engine 0.6.70 → 0.6.71

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 (144) hide show
  1. package/RELEASE.md +21 -47
  2. package/addon/components/order/details/activity.hbs +8 -6
  3. package/addon/components/order/details/activity.js +9 -0
  4. package/addon/components/order/details.hbs +29 -0
  5. package/addon/components/order/details.js +26 -0
  6. package/addon/components/order/form/details.hbs +30 -4
  7. package/addon/components/order/form/details.js +46 -0
  8. package/addon/components/order/form/route.hbs +4 -2
  9. package/addon/components/order/form/route.js +11 -0
  10. package/addon/components/order/form.hbs +12 -0
  11. package/addon/components/order/form.js +21 -1
  12. package/addon/components/order/kanban.js +72 -0
  13. package/addon/components/order-config-manager/activity-flow.js +45 -10
  14. package/addon/components/order-config-manager/lifecycle.hbs +250 -0
  15. package/addon/components/order-config-manager/lifecycle.js +358 -0
  16. package/addon/components/order-config-manager.js +5 -1
  17. package/addon/controllers/analytics/reports/index.js +10 -1
  18. package/addon/controllers/connectivity/devices/index/details.js +1 -0
  19. package/addon/controllers/connectivity/devices/index.js +8 -2
  20. package/addon/controllers/connectivity/events/index.js +31 -1
  21. package/addon/controllers/connectivity/sensors/index/details.js +1 -1
  22. package/addon/controllers/connectivity/sensors/index.js +37 -1
  23. package/addon/controllers/maintenance/equipment/index/details.js +2 -2
  24. package/addon/controllers/maintenance/equipment/index.js +28 -9
  25. package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
  26. package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
  27. package/addon/controllers/maintenance/inspection-submissions/index.js +49 -9
  28. package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
  29. package/addon/controllers/maintenance/maintenances/index.js +20 -1
  30. package/addon/controllers/maintenance/parts/index/details.js +2 -2
  31. package/addon/controllers/maintenance/parts/index.js +28 -8
  32. package/addon/controllers/maintenance/schedules/index.js +33 -4
  33. package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
  34. package/addon/controllers/maintenance/work-orders/index.js +39 -8
  35. package/addon/controllers/management/contacts/customers.js +13 -0
  36. package/addon/controllers/management/contacts/index.js +43 -1
  37. package/addon/controllers/management/drivers/index/details.js +1 -0
  38. package/addon/controllers/management/drivers/index.js +32 -2
  39. package/addon/controllers/management/fleets/index/details.js +1 -0
  40. package/addon/controllers/management/fleets/index.js +39 -1
  41. package/addon/controllers/management/fuel-reports/index.js +22 -2
  42. package/addon/controllers/management/fuel-transactions/index.js +43 -7
  43. package/addon/controllers/management/issues/index/details.js +1 -0
  44. package/addon/controllers/management/issues/index.js +22 -2
  45. package/addon/controllers/management/places/index.js +36 -1
  46. package/addon/controllers/management/trailers/index/details.js +1 -0
  47. package/addon/controllers/management/trailers/index.js +50 -2
  48. package/addon/controllers/management/vehicles/index/details.js +1 -0
  49. package/addon/controllers/management/vehicles/index.js +33 -2
  50. package/addon/controllers/management/vendors/index.js +26 -2
  51. package/addon/controllers/operations/orders/index/details.js +31 -4
  52. package/addon/controllers/operations/orders/index.js +59 -4
  53. package/addon/controllers/operations/service-rates/index.js +15 -1
  54. package/addon/extension.js +32 -0
  55. package/addon/routes/operations/orders/index/details.js +1 -0
  56. package/addon/services/contact-actions.js +1 -0
  57. package/addon/services/customer-actions.js +2 -1
  58. package/addon/services/device-actions.js +1 -0
  59. package/addon/services/device-event-actions.js +1 -0
  60. package/addon/services/driver-actions.js +4 -13
  61. package/addon/services/equipment-actions.js +1 -0
  62. package/addon/services/fleet-actions.js +1 -0
  63. package/addon/services/fuel-report-actions.js +1 -0
  64. package/addon/services/inspection-form-actions.js +1 -0
  65. package/addon/services/inspection-submission-actions.js +1 -0
  66. package/addon/services/integrated-vendor-actions.js +1 -0
  67. package/addon/services/issue-actions.js +1 -0
  68. package/addon/services/maintenance-actions.js +1 -0
  69. package/addon/services/maintenance-schedule-actions.js +2 -1
  70. package/addon/services/order-actions.js +7 -0
  71. package/addon/services/order-presentation.js +129 -0
  72. package/addon/services/part-actions.js +1 -0
  73. package/addon/services/place-actions.js +1 -0
  74. package/addon/services/sensor-actions.js +1 -0
  75. package/addon/services/service-area-actions.js +1 -0
  76. package/addon/services/service-rate-actions.js +1 -0
  77. package/addon/services/telematic-actions.js +1 -0
  78. package/addon/services/trailer-actions.js +4 -14
  79. package/addon/services/vehicle-actions.js +4 -13
  80. package/addon/services/vendor-actions.js +1 -0
  81. package/addon/services/work-order-actions.js +1 -0
  82. package/addon/services/zone-actions.js +1 -0
  83. package/addon/templates/analytics/reports/index/details.hbs +1 -0
  84. package/addon/templates/analytics/reports/index.hbs +1 -0
  85. package/addon/templates/connectivity/devices/index/details.hbs +1 -0
  86. package/addon/templates/connectivity/devices/index.hbs +1 -0
  87. package/addon/templates/connectivity/events/index.hbs +1 -0
  88. package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
  89. package/addon/templates/connectivity/sensors/index.hbs +1 -0
  90. package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
  91. package/addon/templates/maintenance/equipment/index.hbs +2 -0
  92. package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
  93. package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
  94. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
  95. package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
  96. package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
  97. package/addon/templates/maintenance/maintenances/index.hbs +1 -0
  98. package/addon/templates/maintenance/parts/index/details.hbs +1 -0
  99. package/addon/templates/maintenance/parts/index.hbs +2 -0
  100. package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
  101. package/addon/templates/maintenance/schedules/index.hbs +2 -0
  102. package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
  103. package/addon/templates/maintenance/work-orders/index.hbs +1 -0
  104. package/addon/templates/management/contacts/customers/details.hbs +1 -0
  105. package/addon/templates/management/contacts/customers.hbs +1 -0
  106. package/addon/templates/management/contacts/index/details.hbs +1 -0
  107. package/addon/templates/management/contacts/index.hbs +1 -0
  108. package/addon/templates/management/contacts.hbs +1 -0
  109. package/addon/templates/management/drivers/index/details.hbs +1 -0
  110. package/addon/templates/management/drivers/index.hbs +2 -0
  111. package/addon/templates/management/fleets/index/details.hbs +1 -0
  112. package/addon/templates/management/fleets/index.hbs +1 -0
  113. package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
  114. package/addon/templates/management/fuel-reports/index.hbs +1 -0
  115. package/addon/templates/management/fuel-transactions/index.hbs +1 -0
  116. package/addon/templates/management/issues/index/details.hbs +1 -0
  117. package/addon/templates/management/issues/index.hbs +1 -0
  118. package/addon/templates/management/places/index/details.hbs +1 -0
  119. package/addon/templates/management/places/index.hbs +1 -0
  120. package/addon/templates/management/trailers/index/details.hbs +1 -0
  121. package/addon/templates/management/trailers/index.hbs +2 -0
  122. package/addon/templates/management/vehicles/index/details.hbs +1 -0
  123. package/addon/templates/management/vehicles/index.hbs +2 -0
  124. package/addon/templates/management/vendors/index/details.hbs +1 -0
  125. package/addon/templates/management/vendors/index.hbs +2 -0
  126. package/addon/templates/management/vendors/integrated.hbs +1 -0
  127. package/addon/templates/operations/orders/index/details.hbs +1 -0
  128. package/addon/templates/operations/orders/index.hbs +1 -0
  129. package/addon/templates/operations/service-rates/index/details.hbs +1 -0
  130. package/addon/templates/operations/service-rates/index.hbs +1 -0
  131. package/addon/utils/normalize-order-config-flow.js +6 -1
  132. package/app/services/order-presentation.js +1 -0
  133. package/composer.json +1 -1
  134. package/extension.json +1 -1
  135. package/package.json +3 -3
  136. package/server/src/Http/Controllers/Internal/v1/OrderController.php +34 -4
  137. package/server/src/Models/Order.php +35 -3
  138. package/server/src/Models/OrderConfig.php +129 -0
  139. package/server/src/Models/TrackingNumber.php +8 -4
  140. package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
  141. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
  142. package/server/tests/OrderConfigLifecycleTest.php +120 -0
  143. package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
  144. package/translations/en-us.yaml +60 -0
package/RELEASE.md CHANGED
@@ -1,58 +1,32 @@
1
- > v0.6.70 ~ "Telematics data retention"
1
+ > v0.6.71 ~ "Extensible tables and panels, configured order lifecycles and order presentation profiles"
2
2
 
3
3
  ---
4
- ## What's New
5
- - **Telematics Data settings.** A new Fleet-Ops → Settings → Telematics Data page lets each organization decide how long telemetry is kept: device events, raw event payloads, positions, processed and quarantined delivery envelopes, and sync run diagnostics. Administrators set system-wide defaults from the admin console (Fleet-Ops Config → Telematics Data). A value of 0 keeps rows forever.
6
- - **Scheduled retention sweep.** `fleetops:prune-telematics-data` runs every fifteen minutes and applies each organization's policy in bounded batches, so a large backlog is drained gradually without stalling ingestion. Defaults: events 30 days, raw payloads stripped after 7 days, positions 90 days, processed deliveries 24 hours, quarantined deliveries 7 days, sync runs 7 days.
7
- - **Storage usage and on-demand cleanup.** The settings page shows rows, oldest row and estimated size per table for your organization, and "Run cleanup now" queues an immediate retention run.
4
+ ## Highlights
8
5
 
9
- ---
10
- ## Fixes
11
- - Device events no longer store the raw provider unit twice. `payload` keeps the raw unit; `meta` holds only the normalized block (speed, heading, odometer, ignition, fuel level, timing). New event rows are roughly half the size.
12
- - Telemetry-driven saves (device, event, position, vehicle, sensors) no longer write to the activity log on every poll. Organizations that want them can enable "Log telemetry activity" in Telematics Data settings. Manual edits are logged as before.
13
- - Delivery inbox and sync run retention moved from the drain command into the retention sweep; `fleetops:drain-telematic-inbox` now only recovers deliveries and finishes interrupted runs.
14
-
15
- ---
16
- ## Upgrade notes
17
- - New indexes on `device_events (company_uuid, created_at)`, `positions (company_uuid, created_at)` and `telematic_sync_runs (telematic_uuid, updated_at)`; on very large tables run `php artisan migrate` in a maintenance window.
18
- - Retention is enabled by default. Existing rows older than the defaults are removed over successive runs after upgrading; set the values to 0 before upgrading if you need to keep history indefinitely.
19
- - Deleting rows frees space inside the database files, not on disk. Run `OPTIMIZE TABLE` off-peak to return space to the operating system.
20
- - Run `php artisan fleetbase:create-permissions` to register the `telematics-settings` permission and the Telematics Settings Manager policy and role.
6
+ - **Resource view registries.** Extensions can add the following through `fleet-ops:<resource>:table:<slot>` and `fleet-ops:<resource>:details:<slot>`:
7
+ - columns, row actions, bulk actions and toolbar buttons on every top-level Fleet-Ops table;
8
+ - header buttons and "…" menu items on its details panels and side panels.
21
9
 
22
- ---
23
- ## Testing
24
- - Backend tests cover the retention policy layering and clamping, the prune command (per-company policies, compaction, inbox tables via connections, orphans, batch caps, dry runs, filters, locking), the settings and storage usage endpoints, the index migrations, the on-demand job, and the meta and activity-log changes to ingestion.
25
- - Ember tests cover the settings route guard, the settings controller, and the admin defaults component.
26
- > v0.6.70 ~ "Order reporting and a cleaner default dashboard"
27
-
28
- ---
29
- ## What's New
30
- - **Report on orders, their items and tracking.** The report builder can answer questions like "Which products sold the most this month?" and "What did orders total this month?".
31
- - Orders expose their ID and Internal ID, and relationships for tracking (tracking number, region, latest status), Order Config, Customer Vendor, Created By, Purchase Rate › Service Quote and Payload Return.
32
- - `Payload` exposes its items with name, SKU, price, dimensions, metadata and destination. Select an item column for one row per item, or group by one to summarise per product.
33
- - Summary columns count orders distinctly, so they stay correct when item rows are selected. Distance, duration and transaction amount have totals and averages.
34
- - `meta` has no fixed shape, so no column assumes a key in it. Read keys with a computed column, for example `CAST(JSON_UNQUOTE(JSON_EXTRACT(payload.entities.meta, '$.quantity')) AS DECIMAL(15,2))`.
35
- - Column labels drop the table name ("Type", not "Order Type"). Distance and duration are labelled in meters and seconds, and money columns are marked "(minor units)".
36
- - **A cleaner default dashboard.** Fleet-Ops widgets declare their place on the default dashboard: Radar first, then Active Orders and Drivers Online, a full-width Live Fleet map, and Revenue Trend, Top Drivers and Maintenance side by side.
37
- - **Live Fleet widget cards match the live map.** Clicking a driver or vehicle on the widget map shows the same card as **Operations › Live Map**.
10
+ Built-in items carry stable ids, so registered items can be placed before or after them.
11
+ - **Configured order lifecycles.** An order config can define its own lifecycle in `meta.lifecycle`: `initial`, `completed`, `canceled` and `terminal` activities, `dispatch: false` and `strict_transitions`. The internal API starts orders at the initial activity, refuses to dispatch when dispatch is off, accepts only the flow's transitions when strict, and cancels to the configured activity. Configs without a lifecycle behave as before.
12
+ - **Lifecycle tab** in Order Configuration, to view and edit a config's lifecycle.
13
+ - **The board** shows a configured lifecycle's activities as its columns.
14
+ - **The activity flow editor** roots at the configured initial activity, and handles flows with cycles.
15
+ - **Order presentation profiles.** An extension can give its order type its own order form sections, extra and hidden fields, details view sections and hidden actions, registered in `fleet-ops:order-presentation`. **Edit details** opens the profiled form. Other order types are unchanged.
16
+ - **The Order Configuration manager lists every config**, including configs created while the console is open.
17
+ - **An order's activity timeline follows status changes made on the server.**
18
+ - **Fix: the Activity Flow crashed on a flow with a cycle.**
19
+ - **Fix: order details now load the assigned vehicle with the order.**
20
+ - **Fix: the order form failed when its custom fields couldn't load.**
21
+ - **Fix: registered details tabs.** Opening a driver, vehicle or trailer side panel removed the route from tabs other extensions had registered, which broke them in the full details view. Side panels also left out the tabs they could render.
22
+ - **Fix: the sensor details view showed the tabs registered for places.**
38
23
 
39
24
  ---
40
- ## Fixes
41
- - Custom field values on fuel reports and service areas are saved. They were dropped because the save hooks read the snake_case payload root, while the console sends `fuelReport` and `serviceArea`.
42
- - The report schema no longer references 13 columns and join keys that don't exist: driver name, email and phone and the driver's vehicle, the vehicle's driver, and the fuel report cost, odometer and date. Transaction summaries sum `transaction.amount`.
43
- - Every report table, and the item join, leaves soft-deleted rows out on core-api v1.6.64 and later.
44
-
45
- ---
46
- ## Dependencies
47
- - Order items, JSON totals and soft deletes in reports need `fleetbase/core-api` v1.6.64. The schema still registers on older versions.
48
- - The report builder and the default dashboard order ship in `@fleetbase/ember-ui` v0.4.4.
49
-
50
- ---
51
- ## Testing
52
- - Every declared report column and join key was checked against the database schema, and order reports were run against real storefront orders on MySQL in strict mode.
53
- - Tests cover the report schema at 100% line coverage, the Live Fleet map cards and the default dashboard order.
25
+ ## Upgrading
26
+ Needs fleetbase/ember-core v0.3.25 and fleetbase/ember-ui v0.4.5.
54
27
 
55
28
  ---
56
29
  ## Need help?
57
30
  - [GitHub Discussions](https://github.com/fleetbase/fleetbase/discussions)
58
31
  - [Discord](https://discord.gg/HnTqQ6zAVn)
32
+ ---
@@ -5,9 +5,11 @@
5
5
  @isLoading={{or @isLoadingActivity this.loadActivity.isRunning @isLoading}}
6
6
  @wrapperClass="bordered-top"
7
7
  >
8
- {{#if (eq this.layout "timeline")}}
9
- <Order::ActivityTimeline @resource={{@resource}} @activity={{this.parentOrderActivity}} />
10
- {{else}}
11
- <Order::ActivityList @resource={{@resource}} @activity={{this.parentOrderActivity}} />
12
- {{/if}}
13
- </ContentPanel>
8
+ <div {{did-update this.onStatusChanged @resource.status}}>
9
+ {{#if (eq this.layout "timeline")}}
10
+ <Order::ActivityTimeline @resource={{@resource}} @activity={{this.parentOrderActivity}} />
11
+ {{else}}
12
+ <Order::ActivityList @resource={{@resource}} @activity={{this.parentOrderActivity}} />
13
+ {{/if}}
14
+ </div>
15
+ </ContentPanel>
@@ -1,6 +1,7 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
+ import { action } from '@ember/object';
4
5
  import { task } from 'ember-concurrency';
5
6
 
6
7
  export default class OrderDetailsActivityComponent extends Component {
@@ -64,6 +65,14 @@ export default class OrderDetailsActivityComponent extends Component {
64
65
  ];
65
66
  }
66
67
 
68
+ /**
69
+ * A status change recorded outside this panel (a configured lifecycle advancing on save, an
70
+ * extension action, a realtime update) means new tracking statuses exist server-side.
71
+ */
72
+ @action onStatusChanged() {
73
+ this.loadActivity.perform();
74
+ }
75
+
67
76
  @task *loadActivity() {
68
77
  if (!this.args.resource) {
69
78
  return;
@@ -44,6 +44,35 @@
44
44
  )
45
45
  )
46
46
  }}
47
+ {{else if this.profile}}
48
+ {{#each this.profileSections key="key" as |section|}}
49
+ {{#if section.native}}
50
+ {{#let
51
+ (component
52
+ section.componentName
53
+ resource=@resource
54
+ onChange=section.onChange
55
+ isLoading=@isLoading
56
+ isLoadingActivity=@isLoadingActivity
57
+ reloadToken=this.effectiveProofReloadToken
58
+ )
59
+ as |NativeSection|
60
+ }}
61
+ <NativeSection />
62
+ {{/let}}
63
+ {{else}}
64
+ <LazyEngineComponent @component={{section.component}} as |ProfileSection|>
65
+ <ProfileSection
66
+ @resource={{@resource}}
67
+ @controller={{@controller}}
68
+ @permission={{get-write-permission @resource}}
69
+ @isLoading={{@isLoading}}
70
+ @profile={{this.profile}}
71
+ @onChange={{@onDetailsChanged}}
72
+ />
73
+ </LazyEngineComponent>
74
+ {{/if}}
75
+ {{/each}}
47
76
  {{else}}
48
77
  <RegistryYield @registry="fleet-ops:component:order:details:start" as |RegistryComponent|>
49
78
  <RegistryComponent @resource={{@resource}} @controller={{@controller}} @permission={{get-write-permission @resource}} @isLoading={{@isLoading}} />
@@ -1,14 +1,40 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { action } from '@ember/object';
3
3
  import { tracked } from '@glimmer/tracking';
4
+ import { inject as service } from '@ember/service';
4
5
 
5
6
  export default class OrderDetailsComponent extends Component {
7
+ @service orderPresentation;
6
8
  @tracked proofReloadToken = 0;
7
9
 
8
10
  get effectiveProofReloadToken() {
9
11
  return `${this.args.proofReloadToken ?? 0}:${this.proofReloadToken}`;
10
12
  }
11
13
 
14
+ /** The extension presentation profile applying to this order's config, if any. */
15
+ get profile() {
16
+ return this.orderPresentation.profileFor(this.args.resource);
17
+ }
18
+
19
+ /** Profile sections with the change handler each native section expects. */
20
+ get profileSections() {
21
+ const handlers = {
22
+ activity: this.handleActivityChanged,
23
+ detail: this.args.onDetailsChanged,
24
+ 'custom-fields': this.args.onCustomFieldsChanged,
25
+ notes: this.args.onNotesChanged,
26
+ route: this.args.onRouteChanged,
27
+ payload: this.args.onPayloadChanged,
28
+ documents: this.args.onDocumentsChanged,
29
+ comments: this.args.onCommentsChanged,
30
+ metadata: this.args.onMetadataChanged,
31
+ };
32
+
33
+ return this.orderPresentation.renderableSectionsFor(this.profile, 'details').map((section) => {
34
+ return section.native ? { ...section, onChange: handlers[section.name] } : section;
35
+ });
36
+ }
37
+
12
38
  @action handleActivityChanged(optionsOrActivity) {
13
39
  if (typeof this.args.onActivityChanged === 'function') {
14
40
  this.args.onActivityChanged(optionsOrActivity?.activityCreated ?? optionsOrActivity);
@@ -9,7 +9,7 @@
9
9
  @onChange={{perform this.selectOrderConfig}}
10
10
  @placeholder={{t "order.fields.order-type-placeholder"}}
11
11
  @triggerClass="form-select form-input"
12
- @disabled={{cannot-write @resource}}
12
+ @disabled={{or (cannot-write @resource) this.orderTypeLocked}}
13
13
  as |orderConfig|
14
14
  >
15
15
  <SelectOption::OrderConfig @option={{orderConfig}} />
@@ -17,8 +17,19 @@
17
17
  </div>
18
18
  </InputGroup>
19
19
 
20
+ {{#if this.profileDetailFields}}
21
+ <div class="contents">
22
+ <LazyEngineComponent @component={{this.profileDetailFields}} as |ProfileDetailFields|>
23
+ <ProfileDetailFields @resource={{@resource}} @order={{@resource}} @profile={{this.profile}} />
24
+ </LazyEngineComponent>
25
+ </div>
26
+ {{/if}}
27
+
28
+ {{#if this.show.internalId}}
20
29
  <InputGroup @name={{t "order.fields.internal-id"}} @value={{@resource.internal_id}} @helpText={{t "order.fields.internal-id-help-text"}} @disabled={{cannot-write @resource}} />
30
+ {{/if}}
21
31
 
32
+ {{#if this.show.schedule}}
22
33
  <InputGroup>
23
34
  <InputLabel @labelText={{t "order.fields.schedule"}} @helpText={{t "order.fields.schedule-help-text"}} />
24
35
  <DateTimeInput
@@ -30,7 +41,9 @@
30
41
  @disabled={{or @resource.dispatched (cannot-write @resource)}}
31
42
  />
32
43
  </InputGroup>
44
+ {{/if}}
33
45
 
46
+ {{#if this.show.customer}}
34
47
  <InputGroup @name={{t "order.fields.customer"}} @helpText={{t "order.fields.customer-help-text"}}>
35
48
  <ModelSelect
36
49
  @modelName="customer"
@@ -48,7 +61,9 @@
48
61
  <SelectOption::Customer @option={{model}} />
49
62
  </ModelSelect>
50
63
  </InputGroup>
64
+ {{/if}}
51
65
 
66
+ {{#if this.show.facilitator}}
52
67
  <InputGroup @name={{t "order.fields.facilitator"}} @helpText={{t "order.fields.facilitator-help-text"}}>
53
68
  <ModelSelect
54
69
  @modelName="facilitator"
@@ -66,8 +81,9 @@
66
81
  <SelectOption::Facilitator @option={{model}} />
67
82
  </ModelSelect>
68
83
  </InputGroup>
84
+ {{/if}}
69
85
 
70
- {{#if (and @resource.facilitator.isIntegratedVendor @resource.facilitator.service_types.length)}}
86
+ {{#if (and this.show.serviceType @resource.facilitator.isIntegratedVendor @resource.facilitator.service_types.length)}}
71
87
  <InputGroup @name={{t "order.fields.service-type"}} @helpText={{concat (t "order.fields.service-type-help-text") @resource.facilitator.name "."}}>
72
88
  <Select
73
89
  class="w-full"
@@ -82,6 +98,7 @@
82
98
  </InputGroup>
83
99
  {{/if}}
84
100
 
101
+ {{#if this.show.driver}}
85
102
  <InputGroup @name={{t "order.fields.assign-driver"}} @helpText={{t "order.fields.assign-driver-help-text"}}>
86
103
  <ModelSelect
87
104
  @modelName="driver"
@@ -101,7 +118,9 @@
101
118
  <SelectOption::Driver @option={{model}} />
102
119
  </ModelSelect>
103
120
  </InputGroup>
121
+ {{/if}}
104
122
 
123
+ {{#if this.show.vehicle}}
105
124
  <InputGroup @name={{t "order.fields.assign-vehicle"}} @helpText={{t "order.fields.assign-vehicle-help-text"}}>
106
125
  <ModelSelect
107
126
  @modelName="vehicle"
@@ -120,8 +139,9 @@
120
139
  <SelectOption::Vehicle @option={{model}} />
121
140
  </ModelSelect>
122
141
  </InputGroup>
142
+ {{/if}}
123
143
 
124
- {{#if (and @resource.pod_required (not @resource.facilitator.isIntegratedVendor))}}
144
+ {{#if (and this.show.pod @resource.pod_required (not @resource.facilitator.isIntegratedVendor))}}
125
145
  <InputGroup @name={{t "order.fields.proof-delivery"}} @helpText={{t "order.fields.proof-delivery-help-text"}} @required={{true}}>
126
146
  <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
127
147
  <PowerSelect
@@ -142,7 +162,7 @@
142
162
  </InputGroup>
143
163
  {{/if}}
144
164
 
145
- {{#if @resource.adhoc}}
165
+ {{#if (and this.show.adhoc @resource.adhoc)}}
146
166
  <div>
147
167
  <InputGroup
148
168
  @name={{t "order.fields.adhoc-ping"}}
@@ -158,6 +178,7 @@
158
178
  </div>
159
179
  {{/if}}
160
180
 
181
+ {{#if this.show.adhoc}}
161
182
  <div class="lg:col-span-2">
162
183
  <InputGroup @wrapperClass="pt-2">
163
184
  <Toggle
@@ -170,7 +191,9 @@
170
191
  />
171
192
  </InputGroup>
172
193
  </div>
194
+ {{/if}}
173
195
 
196
+ {{#if this.show.dispatch}}
174
197
  <div class="lg:col-span-2">
175
198
  <InputGroup @wrapperClass="pt-1">
176
199
  <Toggle
@@ -183,7 +206,9 @@
183
206
  />
184
207
  </InputGroup>
185
208
  </div>
209
+ {{/if}}
186
210
 
211
+ {{#if this.show.pod}}
187
212
  <div class="lg:col-span-2">
188
213
  <InputGroup @wrapperClass="pt-1">
189
214
  <Toggle
@@ -196,6 +221,7 @@
196
221
  />
197
222
  </InputGroup>
198
223
  </div>
224
+ {{/if}}
199
225
 
200
226
  <RegistryYield @registry="fleet-ops:component:order:form:details" as |RegistryComponent|>
201
227
  <RegistryComponent @order={{@resource}} @resource={{@resource}} />
@@ -13,6 +13,7 @@ export default class OrderFormDetailsComponent extends Component {
13
13
  @service leafletMapManager;
14
14
  @service leafletLayerVisibilityManager;
15
15
  @service currentUser;
16
+ @service orderPresentation;
16
17
  @tracked customFields;
17
18
 
18
19
  constructor() {
@@ -20,6 +21,40 @@ export default class OrderFormDetailsComponent extends Component {
20
21
  this.orderConfigActions.loadAll.perform();
21
22
  }
22
23
 
24
+ /** The presentation profile applying to the order's current config, if any. */
25
+ get profile() {
26
+ return this.orderPresentation.profileFor(this.args.resource);
27
+ }
28
+
29
+ /** Extension fields rendered inside the details grid when a profile applies. */
30
+ get profileDetailFields() {
31
+ return this.profile?.form?.detailFields ?? null;
32
+ }
33
+
34
+ /** A persisted order keeps its profiled order type; reclassification is not supported. */
35
+ get orderTypeLocked() {
36
+ return Boolean(this.profile) && this.args.resource?.isNew === false;
37
+ }
38
+
39
+ /** Field visibility; everything is visible unless a presentation profile hides it. */
40
+ get show() {
41
+ const hidden = this.args.hiddenFields ?? this.orderPresentation.hiddenFieldsFor(this.args.resource);
42
+ const visible = (key) => !hidden.includes(key);
43
+
44
+ return {
45
+ internalId: visible('internal-id'),
46
+ schedule: visible('schedule'),
47
+ customer: visible('customer'),
48
+ facilitator: visible('facilitator'),
49
+ serviceType: visible('service-type'),
50
+ driver: visible('driver'),
51
+ vehicle: visible('vehicle'),
52
+ pod: visible('pod'),
53
+ adhoc: visible('adhoc'),
54
+ dispatch: visible('dispatch'),
55
+ };
56
+ }
57
+
23
58
  get integratedVendorServiceType() {
24
59
  return this.args.resource?.type;
25
60
  }
@@ -38,12 +73,23 @@ export default class OrderFormDetailsComponent extends Component {
38
73
 
39
74
  @task *selectOrderConfig(orderConfig) {
40
75
  if (!orderConfig) return;
76
+ const previousProfile = this.orderPresentation.profileFor(this.args.resource);
41
77
  this.args.resource.setProperties({
42
78
  order_config_uuid: orderConfig.id,
43
79
  order_config: orderConfig,
44
80
  type: orderConfig.key,
45
81
  });
46
82
  this.args.resource.payload.set('type', orderConfig.key);
83
+
84
+ // Let presentation profiles clean up or prepare the draft when the order type changes.
85
+ const nextProfile = this.orderPresentation.profileFor(this.args.resource);
86
+ if (previousProfile && previousProfile !== nextProfile && typeof previousProfile.release === 'function') {
87
+ previousProfile.release(this.args.resource);
88
+ }
89
+ if (nextProfile && nextProfile !== previousProfile) {
90
+ this.orderPresentation.prepare(this.args.resource);
91
+ }
92
+
47
93
  this.requestServiceQuoteRefresh('details.order_config.changed');
48
94
 
49
95
  try {
@@ -1,7 +1,9 @@
1
- <ContentPanel @title={{t "order.fields.route"}} @open={{true}} @wrapperClass="bordered-top">
1
+ <ContentPanel @title={{or this.profileTitle (t "order.fields.route")}} @open={{true}} @wrapperClass="bordered-top">
2
2
  <div class="flex flex-row items-center justify-between mb-4">
3
3
  <div class="flex">
4
- <Toggle @isToggled={{this.multipleWaypoints}} @onToggle={{this.toggleWaypoints}} @label={{t "order.fields.route-label"}} />
4
+ {{#if this.allowsMultipleWaypoints}}
5
+ <Toggle @isToggled={{this.multipleWaypoints}} @onToggle={{this.toggleWaypoints}} @label={{t "order.fields.route-label"}} />
6
+ {{/if}}
5
7
  </div>
6
8
  <div class="flex flex-1 justify-end space-x-2">
7
9
  {{#if this.multipleWaypoints}}
@@ -22,6 +22,7 @@ export default class OrderFormRouteComponent extends Component {
22
22
  @service notifications;
23
23
  @service placeActions;
24
24
  @service orderCreation;
25
+ @service orderPresentation;
25
26
  @tracked multipleWaypoints = false;
26
27
  @tracked routingControl;
27
28
  @tracked route;
@@ -35,6 +36,16 @@ export default class OrderFormRouteComponent extends Component {
35
36
  }
36
37
  }
37
38
 
39
+ /** A presentation profile may hide multi-drop routing and rename the route panel. */
40
+ get allowsMultipleWaypoints() {
41
+ const hidden = this.args.hiddenFields ?? this.orderPresentation.hiddenFieldsFor(this.args.resource);
42
+ return !hidden.includes('multiple-waypoints');
43
+ }
44
+
45
+ get profileTitle() {
46
+ return this.orderPresentation.profileFor(this.args.resource)?.form?.titles?.route ?? null;
47
+ }
48
+
38
49
  get coordinates() {
39
50
  return this.routePoints.map(({ place }) => [place.latitude, place.longitude]);
40
51
  }
@@ -16,6 +16,18 @@
16
16
  RegistryYieldEnd=(component "registry-yield" registry="fleet-ops:component:order:form:end" order=@resource)
17
17
  )
18
18
  }}
19
+ {{else if this.profile}}
20
+ {{#each this.profileSections key="key" as |section|}}
21
+ {{#if section.native}}
22
+ {{#let (component section.componentName resource=@resource hiddenFields=this.hiddenFields) as |NativeSection|}}
23
+ <NativeSection />
24
+ {{/let}}
25
+ {{else}}
26
+ <LazyEngineComponent @component={{section.component}} as |ProfileSection|>
27
+ <ProfileSection @resource={{@resource}} @order={{@resource}} @controller={{@controller}} @profile={{this.profile}} @hiddenFields={{this.hiddenFields}} />
28
+ </LazyEngineComponent>
29
+ {{/if}}
30
+ {{/each}}
19
31
  {{else}}
20
32
  <RegistryYield @registry="fleet-ops:component:order:form:start" as |RegistryComponent|>
21
33
  <RegistryComponent @order={{@resource}} @resource={{@resource}} />
@@ -11,6 +11,7 @@ export default class OrderFormComponent extends Component {
11
11
  @service customFieldsRegistry;
12
12
  @service mapManager;
13
13
  @service currentUser;
14
+ @service orderPresentation;
14
15
  @tracked customFields;
15
16
 
16
17
  constructor() {
@@ -18,6 +19,19 @@ export default class OrderFormComponent extends Component {
18
19
  this.orderConfigActions.loadAll.perform();
19
20
  }
20
21
 
22
+ /** The extension presentation profile applying to this order's config, if any. */
23
+ get profile() {
24
+ return this.orderPresentation.profileFor(this.args.resource);
25
+ }
26
+
27
+ get profileSections() {
28
+ return this.orderPresentation.renderableSectionsFor(this.profile, 'form');
29
+ }
30
+
31
+ get hiddenFields() {
32
+ return this.orderPresentation.hiddenFieldsFor(this.args.resource);
33
+ }
34
+
21
35
  @action selectFacilitator(model) {
22
36
  this.args.resource.set('facilitator', model);
23
37
  this.args.resource.set('driver', null);
@@ -32,7 +46,13 @@ export default class OrderFormComponent extends Component {
32
46
  });
33
47
  this.args.resource.payload.set('type', orderConfig.key);
34
48
 
35
- this.customFields = yield this.customFieldsRegistry.loadSubjectCustomFields.perform(orderConfig);
49
+ // loadSubjectCustomFields rejects when the custom fields cannot be loaded (ember-core 0.3.25).
50
+ try {
51
+ this.customFields = yield this.customFieldsRegistry.loadSubjectCustomFields.perform(orderConfig);
52
+ } catch (err) {
53
+ this.customFields = undefined;
54
+ debug('Unable to load order custom fields: ' + err.message);
55
+ }
36
56
  }
37
57
 
38
58
  @task *selectDriver(driver) {
@@ -13,7 +13,9 @@ export default class OrderKanbanComponent extends Component {
13
13
  @service fetch;
14
14
  @service notifications;
15
15
  @service intl;
16
+ @service store;
16
17
  @tracked statuses = [];
18
+ @tracked orderConfigRecord = null;
17
19
  @tracked orders = this.args.orders ?? [];
18
20
  @tracked orderConfig = this.args.orderConfig ?? null;
19
21
 
@@ -23,6 +25,11 @@ export default class OrderKanbanComponent extends Component {
23
25
  };
24
26
 
25
27
  get columns() {
28
+ const lifecycleColumns = this.#lifecycleColumns();
29
+ if (lifecycleColumns) {
30
+ return lifecycleColumns;
31
+ }
32
+
26
33
  const start = this.#defaultStatuses.start ?? [];
27
34
  const end = this.#defaultStatuses.end ?? [];
28
35
  const loaded = isArray(this.statuses) ? this.statuses : [];
@@ -112,6 +119,64 @@ export default class OrderKanbanComponent extends Component {
112
119
  this.loadStatuses.perform();
113
120
  }
114
121
 
122
+ /**
123
+ * Columns for an order config with a configured lifecycle (`meta.lifecycle`): the flow's own
124
+ * activities in graph order from the initial activity, titled with their status labels.
125
+ * No default dispatch columns are injected. Returns null for configs without a lifecycle.
126
+ */
127
+ #lifecycleColumns() {
128
+ const config = this.orderConfigRecord;
129
+ const lifecycle = config?.meta?.lifecycle;
130
+ const flow = config?.flow;
131
+ if (!lifecycle || !flow || typeof flow !== 'object') {
132
+ return null;
133
+ }
134
+
135
+ const activities = Object.values(flow).filter((activity) => activity && activity.code);
136
+ const byKeyOrCode = new Map();
137
+ for (const activity of activities) {
138
+ byKeyOrCode.set(activity.key ?? activity.code, activity);
139
+ byKeyOrCode.set(activity.code, activity);
140
+ }
141
+
142
+ // Breadth-first walk from the initial activity, then any activities not reachable from it.
143
+ const ordered = [];
144
+ const seen = new Set();
145
+ const queue = [byKeyOrCode.get(lifecycle.initial)].filter(Boolean);
146
+ while (queue.length) {
147
+ const activity = queue.shift();
148
+ if (seen.has(activity.code)) continue;
149
+ seen.add(activity.code);
150
+ ordered.push(activity);
151
+ for (const child of activity.activities ?? []) {
152
+ const next = byKeyOrCode.get(typeof child === 'string' ? child : (child?.key ?? child?.code));
153
+ if (next && !seen.has(next.code)) queue.push(next);
154
+ }
155
+ }
156
+ for (const activity of activities.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0))) {
157
+ if (!seen.has(activity.code)) {
158
+ seen.add(activity.code);
159
+ ordered.push(activity);
160
+ }
161
+ }
162
+
163
+ // Keep orders whose status is not part of the flow visible rather than silently dropping them.
164
+ const columns = ordered.map((activity) => ({ id: activity.code, title: this.#activityTitle(activity) }));
165
+ for (const status of isArray(this.statuses) ? this.statuses : []) {
166
+ if (status && !seen.has(status)) {
167
+ seen.add(status);
168
+ columns.push({ id: status, title: titleize(smartHumanize(status)) });
169
+ }
170
+ }
171
+
172
+ return columns.map((column, index) => ({ ...column, position: index, cards: this.#getOrdersByStatus(column.id, this.orders) }));
173
+ }
174
+
175
+ #activityTitle(activity) {
176
+ const label = typeof activity.status === 'string' ? activity.status.trim() : '';
177
+ return label && !label.includes('{') ? label : titleize(smartHumanize(activity.code));
178
+ }
179
+
115
180
  #getOrdersByStatus(status, orders = []) {
116
181
  let filteredOrders = orders.filter((order) => order.status === status);
117
182
  if (this.orderConfig) {
@@ -125,6 +190,13 @@ export default class OrderKanbanComponent extends Component {
125
190
  const params = {};
126
191
  if (this.orderConfig) params.order_config_uuid = this.orderConfig;
127
192
 
193
+ try {
194
+ this.orderConfigRecord = this.orderConfig ? yield this.store.findRecord('order-config', this.orderConfig) : null;
195
+ } catch (err) {
196
+ this.orderConfigRecord = null;
197
+ debug('Unable to load order config for board: ' + err.message);
198
+ }
199
+
128
200
  try {
129
201
  const statuses = yield this.fetch.get('orders/statuses', params);
130
202
  this.statuses = isArray(statuses) ? statuses : [];