Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
134 commits
Select commit Hold shift + click to select a range
b89a946
test: make the engine's own test suite bootable
roncodes Oct 9, 2026
3c6ad10
feat(food-trucks): networks show their member stores' trucks; a truck…
roncodes Oct 10, 2026
e60a706
feat(cards): catalogs, trucks and networks render on the shared resou…
roncodes Oct 10, 2026
672665c
fix(truck-card): the online dot comes from the vehicle; a truck witho…
roncodes Oct 10, 2026
4900d9e
fix(truck): a model attribute named store shadowed Ember Data's store…
roncodes Oct 10, 2026
b409f73
feat(context): the console is scoped to a store or a network, with it…
roncodes Oct 10, 2026
41506ec
fix(switcher): the context switcher the design shows, and the store m…
roncodes Oct 10, 2026
56b5f95
fix(switcher): expose the active context through native getters so th…
roncodes Oct 10, 2026
f535fdc
feat(network): overview, stores with status tabs and bulk actions, an…
roncodes Oct 10, 2026
f4b6dbb
fix(i18n): drop the duplicate common.store key that broke the transla…
roncodes Oct 10, 2026
184b5e3
fix(engine): the descriptor initializer runs inside the engine; its h…
roncodes Oct 10, 2026
7182093
feat(network-orders): orders grouped by checkout with status tabs, st…
roncodes Oct 10, 2026
bde3613
fix(network-orders): alias the orders query params so they cannot col…
roncodes Oct 10, 2026
3a5e69c
feat(order-panel): the order details panel answers the dispatcher's f…
roncodes Oct 10, 2026
10f3347
feat(settings): one settings page with a section rail, labelled check…
roncodes Oct 10, 2026
5ab0bc3
feat(catalogs): the catalog editor, a catalog edited as a container o…
roncodes Oct 10, 2026
ba1b4b7
feat(products): the product editor's ten panels become four tabs with…
roncodes Oct 10, 2026
eb11a33
feat(seeders): Mongolian testing network with QPay sandbox gateways, …
roncodes Oct 10, 2026
76a110e
feat(customers): the customer panel answers a support agent's questio…
roncodes Oct 10, 2026
ad11d02
feat(promotions): the promotions hub with counts and tabs, a stepped …
roncodes Oct 10, 2026
7cfed01
feat(trucks): trucks page with status tabs and a side panel; networks…
roncodes Oct 10, 2026
c92484c
feat(products): nested category sidebar with counts and smart groups,…
roncodes Oct 10, 2026
0219d38
fix(products): pass counts, smart groups and the group handler to the…
roncodes Oct 10, 2026
d622404
feat(dashboard): a needs-attention strip counts what the store acts o…
roncodes Oct 10, 2026
3489f83
feat(seeders): Mongolian network and store promotions, segments and a…
roncodes Oct 10, 2026
85fdf71
feat(network-join): invited stores can review, accept or decline a ne…
roncodes Oct 10, 2026
9267ff9
feat(orders): a commerce-first create order form that runs the app's …
roncodes Oct 10, 2026
acf09b6
fix(seeders): re-seeding keeps product, vehicle and food truck ids, s…
roncodes Oct 10, 2026
143aa81
fix(quotes): carts with lines that can no longer be ordered get a car…
roncodes Oct 10, 2026
c87e0c5
fix(qpay): retry an invoice without the receiver email when QPay reje…
roncodes Oct 10, 2026
4ffc99b
revert(qpay): drop the retry-without-email added in ea4d524
roncodes Oct 10, 2026
affbcd3
fix(qpay): follow QPay API v2: paid status and amount before ordering…
roncodes Oct 10, 2026
ac59746
fix(seeders): Mongolian amounts in whole tögrög, a 1,000₮ test item, …
roncodes Oct 10, 2026
d29ee7f
fix(ui): section headers follow the console pattern: no inline subtit…
roncodes Oct 10, 2026
721e913
fix(qpay): exact VAT amounts; floating point truncated e.g. 3190 → 28…
roncodes Oct 10, 2026
c0b1062
fix(checkout): QPay's callback creates the order; the app hears about…
roncodes Oct 10, 2026
f8eb78f
fix(ui): promotions hub tabs sit under each page's section header, pu…
roncodes Oct 10, 2026
3aa5013
fix(ui): Settings is one sidebar entry; the settings page rail holds …
roncodes Oct 10, 2026
a9963be
fix(customers): the customer panel injects intl and notifications; op…
roncodes Oct 10, 2026
9b2e5bc
fix(customers): panel loads insights (storefront service injected), c…
roncodes Oct 10, 2026
38627da
fix(customers): panel insights are tracked and scoped like the order …
roncodes Oct 10, 2026
9266df9
fix(order-panel): wire the Items/Route/Payment/Activity/Customer/Data…
roncodes Oct 10, 2026
f013b88
feat(carts): carts keep what they held: open, checked out and cleared…
roncodes Oct 10, 2026
d0b0629
fix(orders): New order button on the orders page opens the create ord…
roncodes Oct 10, 2026
a83414b
fix(orders): the create order route's customer query param is aliased…
roncodes Oct 10, 2026
996b2ac
fix(customers): New order from the panel passes the aliased for_custo…
roncodes Oct 10, 2026
bc4b92a
fix(networks): the network Stores page drops the category picker row …
roncodes Oct 10, 2026
4c24b99
feat(qpay): payment recorded on the checkout the moment it's verified…
roncodes Oct 10, 2026
3fff409
fix(network-context): truck panel and customer panel no longer crash …
roncodes Oct 10, 2026
3d9c798
feat(network-settings): network settings on the settings shell (rail,…
roncodes Oct 10, 2026
09d43bd
style(storefront): one palette and type scale for the engine stylesheet
roncodes Oct 10, 2026
2297131
feat(network-stores): the stores table matches the board: Status, Ord…
roncodes Oct 10, 2026
f67843d
refactor(ui): one Storefront::ViewToggle for every segmented view swi…
roncodes Oct 10, 2026
d82c184
refactor(products): the product grid card is built on Layout::Resourc…
roncodes Oct 10, 2026
697156b
feat(network-categories): a Categories page in the network nav (table…
roncodes Oct 10, 2026
6123280
feat(catalogs): catalog cards get an actions menu (duplicate, assign …
roncodes Oct 10, 2026
2a13e2a
fix(promotions): duplicate and view-redemptions menu labels; workflow…
roncodes Oct 10, 2026
715e999
fix(create-order): line configurator reads add-on category relations …
roncodes Oct 10, 2026
a0ee921
feat(settings): one section anatomy for every settings page (location…
roncodes Oct 10, 2026
e3db965
feat(trucks): Map button on both trucks pages opens the Fleet-Ops liv…
roncodes Oct 10, 2026
827833f
test(server): network_stores test schemas carry the status column
roncodes Oct 10, 2026
010e584
fix(sidebar): the Member of / Your stores group keeps a stable root i…
roncodes Oct 10, 2026
782aeaf
style(tests): prettier
roncodes Oct 10, 2026
d0fbe9b
fix(network): membership changes (suspend, reinstate, add, remove) cl…
roncodes Oct 10, 2026
5d0de7e
fix(network): membership changes invalidate the core query cache for …
roncodes Oct 10, 2026
b636c4b
fix(i18n): every storefront.* key used in templates and controllers e…
roncodes Oct 10, 2026
52907f2
feat(network): approval queue for memberships; accepting an invitatio…
roncodes Oct 10, 2026
1dc1d38
feat(catalogs): per-catalog price and availability overrides; catalog…
roncodes Oct 10, 2026
ce47ba5
test(server): cart stub and controller expectation follow the catalog…
roncodes Oct 10, 2026
2898eae
fix(catalogs): the editor's unsaved summary counts price and availabi…
roncodes Oct 10, 2026
3824bce
fix(catalogs): the console keeps store values on catalog products (on…
roncodes Oct 10, 2026
f121223
feat(create-order): orders can be created from the network context; a…
roncodes Oct 10, 2026
985362f
feat(board-parity): dashboard header leads with the store name and op…
roncodes Oct 11, 2026
40ceb04
feat(settings): map marker option for stores and networks: how stores…
roncodes Oct 11, 2026
3a239a8
feat(board-parity): promotions table is selectable with bulk pause, r…
roncodes Oct 11, 2026
0903f28
feat(network): the shareable join link works end to end; the link car…
roncodes Oct 11, 2026
a11552a
feat(customers): a customer card on the shared anatomy (pill identity…
roncodes Oct 11, 2026
ec39760
fix(customers): the card shows saved places only when the listing car…
roncodes Oct 11, 2026
5375726
fix(trucks): the card's last-online reading guards a truck without a …
roncodes Oct 11, 2026
1fc6dd6
feat(audit): screens match their boards after the 11 Oct review
roncodes Oct 11, 2026
4343dcc
feat(orders): the create order form follows its board
roncodes Oct 11, 2026
d0b9ce1
fix(orders): the create order overlay body stops at the window edge s…
roncodes Oct 11, 2026
c27e207
fix(panels): storefront overlay bodies stop at the window edge (truck…
roncodes Oct 11, 2026
8d98c52
fix(panels): storefront overlays lay out as a column so the body take…
roncodes Oct 11, 2026
e602374
feat(catalog-editor, product-form): both follow their boards at 1440
roncodes Oct 11, 2026
cc2f71f
fix(ui): button labels never wrap on storefront screens, panels and m…
roncodes Oct 11, 2026
1b693f6
feat(catalogs, customers): both pages follow their boards at 1440
roncodes Oct 11, 2026
8e0b191
feat(products, dashboard): products page follows its board at 1440; a…
roncodes Oct 11, 2026
325f7ff
feat(orders, trucks): the store orders table reads like the board's o…
roncodes Oct 11, 2026
9c5de83
fix(network): the stores row menu reads in sentence case as the board…
roncodes Oct 11, 2026
d407a2d
feat(network): orders group rows and the categories page follow the b…
roncodes Oct 11, 2026
1ad2570
fix(network): overview lays out as the board does; customers and sett…
roncodes Oct 11, 2026
8ec8df3
fix(ui): overview stores section truly unboxed in dark mode; gutters,…
roncodes Oct 11, 2026
739f1c5
fix(network): overview stacks as the canvas renders it (stores full w…
roncodes Oct 11, 2026
19c22d1
feat(identity, tables, i18n): storefront resources get identity compo…
roncodes Oct 11, 2026
50570af
feat(loading, palette, audit): skeleton loading and error substates, …
roncodes Oct 11, 2026
75b2bea
test(promotions): panel form tests grant abilities and avoid the date…
roncodes Oct 11, 2026
decde08
fix(routes, sidebar): panels open without reloading the list; product…
roncodes Oct 12, 2026
6f62a8b
fix(dashboard): needs-attention fits its grid cell as one compact strip
roncodes Oct 12, 2026
7873b6f
fix(create order): preset customer is selected without filtering the …
roncodes Oct 12, 2026
b94c10c
feat(products): rebuilt product card, list view as a real table, flus…
roncodes Oct 12, 2026
c3efd96
feat(products): product form as one page with a rail; add-ons manager…
roncodes Oct 12, 2026
e2e26f5
chore(products): remove the product form sections the one-page form i…
roncodes Oct 12, 2026
10384cc
fix(catalogs): editor products get the width, rows line up and stay o…
roncodes Oct 12, 2026
55098fc
feat(customers): customer panel rebuilt: compact header, flush sectio…
roncodes Oct 12, 2026
002a198
fix(create order): the selected customer's saved places load from the…
roncodes Oct 12, 2026
0aa0a3a
fix(trucks, ui): new truck panel reads cleanly; checkboxes that never…
roncodes Oct 12, 2026
3b14fbb
fix(promotions): status tabs filter instantly with counts and stay in…
roncodes Oct 12, 2026
64c54c4
fix(promotions): builder choices on the step surface, aligned BOGO fi…
roncodes Oct 12, 2026
530e037
fix(settings): compact uppercase section headings on the panel surfac…
roncodes Oct 12, 2026
8e7c477
fix(network): overview stores four to a row; recent orders list each …
roncodes Oct 12, 2026
f221706
fix(customers): store and network lists show the people who ordered t…
roncodes Oct 12, 2026
424413c
fix(promotions): a promotion past its end date reads Ended in the tab…
roncodes Oct 12, 2026
f87ea3d
fix(ui): table footers keep the row rhythm; rows sit on the card surf…
roncodes Oct 12, 2026
1634be6
fix(dashboard): recent orders and customers rows read on one line, wi…
roncodes Oct 12, 2026
e7bd7ee
feat(loading): every page loads with a skeleton drawn from that page:…
roncodes Oct 12, 2026
9943c13
test: error page tests register the engine's error controller; widget…
roncodes Oct 12, 2026
ab77e44
fix(orders): an order opened from a list loads its details, so items …
roncodes Oct 12, 2026
1dd3768
fix(products): the page's view mode and other list params no longer r…
roncodes Oct 12, 2026
a903c0a
feat(order panel): tracking number leads the header with the id besid…
roncodes Oct 12, 2026
b6a9957
fix(settings): section headings lose the console border on every side…
roncodes Oct 12, 2026
95500b8
feat(addresses): a customer's address is added in a modal from the cu…
roncodes Oct 12, 2026
a321a5b
feat(orders): the tracking number identifies an order everywhere (Ord…
roncodes Oct 12, 2026
fe43070
fix(products): cards keep their full height in a scrolling grid; the …
roncodes Oct 12, 2026
db1a800
feat(product form): placeholders on name and SKU; video links as rows…
roncodes Oct 12, 2026
950e6ce
fix(cards, catalogs): a long card badge ellipsizes before the buttons…
roncodes Oct 12, 2026
9c59e67
test: schedule manager rows, the order store profile and the customer…
roncodes Oct 12, 2026
a062d03
fix(store): order ids line up with their row; identity strip columns …
roncodes Oct 12, 2026
a94605c
fix(network orders): the store cell is one line; a multi-store checko…
roncodes Oct 12, 2026
3160a75
feat(network): a store details page with its week, orders and custome…
roncodes Oct 12, 2026
c595e6f
fix(network orders): every order row is one line: the tracking number…
roncodes Oct 12, 2026
fed8eef
fix(trucks): saved catalogs stay on the truck: the sync reloads the r…
roncodes Oct 12, 2026
f5c7df9
feat(incoming order): the modal reads as a ticket: store, total and p…
roncodes Oct 12, 2026
d85db2d
Merge release/v0.4.25 into feat/storefront-ux-overhaul
roncodes Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions addon/adapters/promotion-redemption.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './storefront';
2 changes: 1 addition & 1 deletion addon/components/add-product-as-entity-button.hbs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{{#if (eq @order.type "storefront")}}
<Button @type="magic" @icon="square-plus" @text="Add Product from Storefront" @onClick={{this.promptProductSelection}} />
<Button @type="magic" @icon="square-plus" @text={{t "storefront.orders.create.add-product-from-storefront"}} @onClick={{this.promptProductSelection}} />
{{/if}}
3 changes: 2 additions & 1 deletion addon/components/add-product-as-entity-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { isArray } from '@ember/array';
import { inject as service } from '@ember/service';

export default class AddProductAsEntityButtonComponent extends Component {
@service intl;
@service modalsManager;
@service fetch;
@service notifications;
Expand All @@ -19,7 +20,7 @@ export default class AddProductAsEntityButtonComponent extends Component {

@action promptProductSelection() {
this.modalsManager.show('modals/select-product', {
title: 'Select Product to Add to Order',
title: this.intl.t('storefront.orders.create.select-product-title'),
modalClass: 'modal-lg',
acceptButtonText: 'Add Products',
acceptButtonDisabled: true,
Expand Down
24 changes: 24 additions & 0 deletions addon/components/catalog/pill.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Resource::Pill
@resource={{(or @catalog @resource)}}
@resourceType="catalog"
@title={{@title}}
@titleFallback={{@titleFallback}}
@subtitle={{@subtitle}}
@showOnlineIndicator={{@showOnlineIndicator}}
@noPopover={{@noPopover}}
@popoverPlacement={{@popoverPlacement}}
@static={{@static}}
@onClick={{@onClick}}
@onView={{@onView}}
@anchorClass={{@anchorClass}}
@imageClass={{@imageClass}}
@imageWrapperClass={{@imageWrapperClass}}
@contentWrapperClass={{@contentWrapperClass}}
@titleClass={{@titleClass}}
@subtitleClass={{@subtitleClass}}
@useBlock={{(has-block)}}
...attributes
as |resource|
>
{{yield resource}}
</Resource::Pill>
1 change: 1 addition & 0 deletions addon/components/catalog/summary.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Resource::Summary @resource={{(or @catalog @resource)}} @resourceType="catalog" @onView={{@onView}} @showView={{@showView}} @onClose={{@onClose}} @titleFallback={{@titleFallback}} ...attributes />
1 change: 1 addition & 0 deletions addon/components/cell/catalog-identity.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Table::Cell::Identity @row={{@row}} @column={{@column}} @value={{@value}} @onClick={{@onClick}} @resourceType="catalog" ...attributes />
1 change: 1 addition & 0 deletions addon/components/cell/food-truck-identity.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Table::Cell::Identity @row={{@row}} @column={{@column}} @value={{@value}} @onClick={{@onClick}} @resourceType="food-truck" ...attributes />
1 change: 1 addition & 0 deletions addon/components/cell/network-identity.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Table::Cell::Identity @row={{@row}} @column={{@column}} @value={{@value}} @onClick={{@onClick}} @resourceType="network" ...attributes />
1 change: 1 addition & 0 deletions addon/components/cell/product-identity.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Table::Cell::Identity @row={{@row}} @column={{@column}} @value={{@value}} @onClick={{@onClick}} @resourceType="product" ...attributes />
1 change: 1 addition & 0 deletions addon/components/cell/promotion-identity.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Table::Cell::Identity @row={{@row}} @column={{@column}} @value={{@value}} @onClick={{@onClick}} @resourceType="promotion" ...attributes />
1 change: 1 addition & 0 deletions addon/components/cell/store-identity.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Table::Cell::Identity @row={{@row}} @column={{@column}} @value={{@value}} @onClick={{@onClick}} @resourceType="store" ...attributes />
16 changes: 10 additions & 6 deletions addon/components/customer-panel.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
@registry="storefront:customer:details"
@resource={{this.customer}}
@controller={{this}}
@headerComponent={{component "customer-panel/panel-header" resource=this.customer actionButtons=this.actionButtons}}
@headerComponent={{component "customer-panel/panel-header" resource=this.customer actionButtons=this.actionButtons insights=this.insights isLoadingInsights=this.loadInsights.isRunning}}
@headerTitle={{or this.customer.name}}
@actionButtons={{this.actionButtons}}
@onPressCancel={{this.onPressCancel}}
@onOverlayReady={{this.setOverlayContext}}
@headerClass="no-bottom-border"
@bodyClass="no-scroll"
@width={{or this.width @width "570px"}}
@bodyClass="no-scroll storefront-overlay-body"
@width={{or this.width @width "600px"}}
@isResizable={{or this.isResizable @isResizable}}
>
<TabNavigation @tabs={{this.tabs}} @activeTabId={{this.tab.id}} @onTabChange={{this.onTabChanged}} @contentClass="scrollable" @tablistClass="pl-2" as |activeTab|>
{{component activeTab.component customer=this.customer tabOptions=activeTab options=activeTab.componentParams}}
</TabNavigation>
{{#if this.isEditing}}
<CustomerPanel::EditForm @customer={{this.customer}} @onDone={{this.doneEditing}} />
{{else}}
<TabNavigation @tabs={{this.tabs}} @activeTabId={{this.tab.id}} @onTabChange={{this.onTabChanged}} @contentClass="scrollable" @tablistClass="pl-2" as |activeTab|>
{{component activeTab.component customer=this.customer tabOptions=activeTab options=activeTab.componentParams insights=this.insights onChange=this.reload}}
</TabNavigation>
{{/if}}
</Layout::Resource::Panel>
117 changes: 105 additions & 12 deletions addon/components/customer-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { isArray } from '@ember/array';
import { dasherize } from '@ember/string';
import CustomerPanelDetailsComponent from './customer-panel/details';
import CustomerPanelOverviewComponent from './customer-panel/overview';
import CustomerPanelOrdersComponent from './customer-panel/orders';
import CustomerPanelPlacesComponent from './customer-panel/places';
import CustomerPanelActivityComponent from './customer-panel/activity';
import { task } from 'ember-concurrency';
import contextComponentCallback from '@fleetbase/ember-core/utils/context-component-callback';
import applyContextComponentArguments from '@fleetbase/ember-core/utils/apply-context-component-arguments';

Expand Down Expand Up @@ -50,7 +53,13 @@ export default class CustomerPanelComponent extends Component {
*
* @type {Service}
*/
@service contextPanel;
@service intl;
@service notifications;
@service storefront;
@tracked insights = null;

/** Whether the panel shows the details form instead of the tabs. */
@tracked isEditing = false;

/**
* The current active tab.
Expand Down Expand Up @@ -80,9 +89,13 @@ export default class CustomerPanelComponent extends Component {
constructor() {
super(...arguments);
this.customer = this.args.customer;

this.tab = this.getTabUsingSlug(this.args.tab);
// The context panel may pass the customer and tab as component arguments; apply them
// before anything reads `customer`, or Glimmer asserts on an update after a read.
applyContextComponentArguments(this);
// Read the requested tab from the arguments, not from the tracked field the helper just wrote.
this.tab = this.getTabUsingSlug(this.args.tab ?? this.args.dynamicArgs?.tab);
this.isEditing = Boolean(this.args.startEditing);
this.loadInsights.perform();
}

/**
Expand All @@ -95,8 +108,22 @@ export default class CustomerPanelComponent extends Component {
const registeredTabs = this.universe.getMenuItemsFromRegistry('component:customer-panel');

const defaultTabs = [
this.universe._createMenuItem('Details', null, { icon: 'circle-info', component: CustomerPanelDetailsComponent }),
this.universe._createMenuItem('Orders', null, { icon: 'circle-info', component: CustomerPanelOrdersComponent }),
this.universe._createMenuItem(this.intl.t('storefront.customers.panel.tabs.overview'), null, {
icon: 'circle-info',
component: CustomerPanelOverviewComponent,
componentParams: { insights: this.insights, onChange: this.reload },
}),
this.universe._createMenuItem(this.intl.t('storefront.customers.panel.tabs.orders'), null, { icon: 'file-invoice-dollar', component: CustomerPanelOrdersComponent }),
this.universe._createMenuItem(this.intl.t('storefront.customers.panel.tabs.places'), null, {
icon: 'map-marker-alt',
component: CustomerPanelPlacesComponent,
componentParams: { onChange: this.reload },
}),
this.universe._createMenuItem(this.intl.t('storefront.customers.panel.tabs.activity'), null, {
icon: 'wave-square',
component: CustomerPanelActivityComponent,
componentParams: { insights: this.insights },
}),
];

if (isArray(registeredTabs)) {
Expand All @@ -108,14 +135,78 @@ export default class CustomerPanelComponent extends Component {

get actionButtons() {
return [
{
icon: 'plus',
text: this.intl.t('storefront.customers.panel.new-order'),
size: 'sm',
type: 'primary',
onClick: this.newOrder,
permission: 'storefront create order',
},
{
icon: 'pencil',
helpText: 'Edit customer',
text: this.intl.t('common.edit'),
size: 'sm',
onClick: this.onEdit,
permission: 'storefront update customer',
},
{
items: [
{ text: this.intl.t('storefront.customers.panel.copy-id'), icon: 'copy', fn: this.copyId },
{ text: this.intl.t('storefront.customers.panel.copy-email'), icon: 'at', fn: this.copyEmail, disabled: !this.customer?.email },
],
},
];
}

@task *loadInsights() {
const customer = this.customer;

if (!customer?.id) {
return;
}

const scope = this.storefront.contextScope();

try {
this.insights = yield this.fetch.get(`customers/${customer.id}/insights`, scope, { namespace: 'storefront/int/v1' });
} catch {
this.insights = null;
}
}

@action reload() {
return this.loadInsights.perform();
}

@action newOrder() {
try {
return this.hostRouter.transitionTo('console.storefront.orders.index.new', { queryParams: { for_customer: this.customer?.public_id } });
} catch {
return this.hostRouter.transitionTo('console.storefront.orders.index.new');
}
}

@action async copyId() {
return this.copy(this.customer?.public_id);
}

@action async copyEmail() {
return this.copy(this.customer?.email);
}

async copy(value) {
if (!value) {
return;
}

try {
await navigator.clipboard.writeText(value);
this.notifications.success(this.intl.t('storefront.customers.panel.copied', { value }));
} catch {
this.notifications.info(value);
}
}
/**
* Sets the overlay context.
*
Expand Down Expand Up @@ -149,14 +240,16 @@ export default class CustomerPanelComponent extends Component {
const isActionOverrided = contextComponentCallback(this, 'onEdit', this.customer);

if (!isActionOverrided) {
this.contextPanel.focus(this.customer, 'editing', {
onAfterSave: () => {
this.contextPanel.clear();
},
});
this.isEditing = true;
}
}

@action doneEditing() {
this.isEditing = false;
contextComponentCallback(this, 'onDoneEditing', this.customer);
this.loadInsights.perform();
}

/**
* Handles the cancel action.
*
Expand Down
23 changes: 23 additions & 0 deletions addon/components/customer-panel/activity.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="storefront-panel-sections" data-test-customer-activity>
<div class="storefront-customer-facts">
<div class="field-info-container">
<div class="field-name">{{t "storefront.customers.panel.first-order"}}</div>
<div class="field-value">{{#if @insights.first_order_at}}{{format-date-fns @insights.first_order_at "d MMM yyyy"}}{{else}}{{t "storefront.customers.panel.never"}}{{/if}}</div>
</div>
<div class="field-info-container">
<div class="field-name">{{t "storefront.customers.panel.last-order"}}</div>
<div class="field-value">{{#if @insights.last_order_at}}{{format-date-fns @insights.last_order_at "d MMM yyyy"}} · {{@insights.last_order_public_id}}{{else}}{{t "storefront.customers.panel.never"}}{{/if}}</div>
</div>
<div class="field-info-container">
<div class="field-name">{{t "storefront.customers.panel.canceled-orders"}}</div>
<div class="field-value">{{n-a @insights.canceled 0}}</div>
</div>
<div class="field-info-container">
<div class="field-name">{{t "storefront.common.created"}}</div>
<div class="field-value">{{n-a (format-date-fns @customer.created_at "d MMM yyyy HH:mm")}}</div>
</div>
</div>
<ContentPanel @title={{t "storefront.customers.panel.notes"}} @open={{true}} @wrapperClass="bordered-top">
<CommentThread @subject={{@customer}} @subjectType="fleet-ops:contact" />
</ContentPanel>
</div>
41 changes: 41 additions & 0 deletions addon/components/customer-panel/edit-form.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<div class="storefront-form-panel" data-test-customer-edit-form>
<div class="storefront-form-panel__body">
<div class="storefront-create-order__sec-title mb-3">{{t "storefront.customers.panel.edit-details"}}</div>
<div class="storefront-product-editor__grid-3 storefront-customer-edit__grid">
<div class="storefront-create-order__field">
<label class="storefront-create-order__label" for="customer-name">{{t "storefront.common.name"}} <span class="text-red-500">*</span></label>
<Input id="customer-name" class="form-input w-full" @value={{this.customer.name}} data-test-customer-edit-name />
</div>
<div class="storefront-create-order__field">
<label class="storefront-create-order__label" for="customer-title">{{t "storefront.common.title"}}</label>
<Input id="customer-title" class="form-input w-full" @value={{this.customer.title}} />
</div>
<div class="storefront-create-order__field">
<label class="storefront-create-order__label" for="customer-email">{{t "storefront.common.email"}}</label>
<Input id="customer-email" @type="email" class="form-input w-full" @value={{this.customer.email}} />
</div>
<div class="storefront-create-order__field">
<span class="storefront-create-order__label">{{t "storefront.common.phone"}}</span>
<PhoneInput @value={{this.customer.phone}} @onInput={{fn (mut this.customer.phone)}} class="form-input w-full" />
</div>
<div class="storefront-create-order__field">
<label class="storefront-create-order__label" for="customer-internal-id">{{t "storefront.common.internal-id"}}</label>
<Input id="customer-internal-id" class="form-input w-full" @value={{this.customer.internal_id}} />
</div>
</div>
<p class="storefront-create-order__help mt-3">{{t "storefront.customers.panel.edit-help"}}</p>
</div>
<div class="storefront-save-bar storefront-form-panel__save-bar {{if this.isDirty 'is-dirty'}}" data-test-customer-edit-save-bar>
<div class="storefront-save-bar__status">
{{#if this.isDirty}}
<FaIcon @icon="circle" @size="xs" class="text-amber-500 mr-2" />{{t "storefront.trucks.panel.unsaved"}}
{{else}}
<FaIcon @icon="check" @size="xs" class="text-green-500 mr-2" />{{t "storefront.settings.save-bar.saved"}}
{{/if}}
</div>
<div class="storefront-save-bar__actions">
<Button @text={{t "common.cancel"}} @onClick={{this.cancel}} data-test-customer-edit-cancel />
<Button @type="primary" @icon="save" @text={{t "storefront.common.save"}} @isLoading={{this.save.isRunning}} @onClick={{perform this.save}} @permission="storefront update customer" data-test-customer-edit-save />
</div>
</div>
</div>
40 changes: 40 additions & 0 deletions addon/components/customer-panel/edit-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';

/**
* Edits a customer's details inside their panel: name, title, contact details and the
* internal id, with the shared save bar. Places keep their own tab.
*/
export default class CustomerPanelEditFormComponent extends Component {
@service intl;
@service notifications;

get customer() {
return this.args.customer;
}

get isDirty() {
return Boolean(this.customer?.hasDirtyAttributes);
}

@task *save() {
if (!String(this.customer?.name ?? '').trim()) {
return this.notifications.warning(this.intl.t('storefront.customers.panel.name-required'));
}

try {
yield this.customer.save();
this.notifications.success(this.intl.t('storefront.customers.panel.saved', { name: this.customer.name }));
this.args.onDone?.(this.customer);
} catch (error) {
this.notifications.serverError(error);
}
}

@action cancel() {
this.customer?.rollbackAttributes?.();
this.args.onDone?.();
}
}
Loading