Skip to content

Commit f26158a

Browse files
authored
Unify customer KPIs with Pulse (#6703)
* Align customer overview KPIs with net-sales analytics Scope order stats per channel, compute recent total and AOV from subtotal.net, and surface shipping and refunds via a value tooltip while keeping lifetime counts server-filtered. * Refine customer KPIs with channel-scoped orders and net/gross table Filter KPI order fetches by channel in the API, exclude drafts from counts, show net and gross in recent orders with guidance, and tighten provider loading for channel switches. * Extract messages
1 parent 3de2a34 commit f26158a

24 files changed

Lines changed: 1278 additions & 227 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"saleor-dashboard": patch
3+
---
4+
5+
Customer profiles now show channel-scoped order statistics: total orders, recent net sales, and average order value. Net sales use product revenue after discounts and exclude shipping and tax; hover the amount for shipping and refund details from the same orders. The recent orders table shows net and gross amounts per order, with a hint explaining how net relates to the overview figures.

locale/defaultMessages.json

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,10 @@
398398
"context": "grant refund within return, structured reason select label",
399399
"string": "Reason"
400400
},
401+
"02j7gU": {
402+
"context": "customer recent orders table, net column tooltip aria label",
403+
"string": "Net order value explanation"
404+
},
401405
"033/zW": {
402406
"context": "explore extensions section name",
403407
"string": "Explore"
@@ -2321,10 +2325,6 @@
23212325
"context": "scope note for model type tab grouping help tooltip",
23222326
"string": "Browser-only display — does not rename, merge, or change your model types or data."
23232327
},
2324-
"AJMlmz": {
2325-
"context": "customer overview stat label: sum of recent orders",
2326-
"string": "Recent orders total"
2327-
},
23282328
"ALe+of": {
23292329
"string": "You don't have permission to manage webhooks. Contact your administrator for access."
23302330
},
@@ -2899,6 +2899,10 @@
28992899
"context": "variant created error message",
29002900
"string": "Variant creation failed"
29012901
},
2902+
"DGgIIw": {
2903+
"context": "customer recent orders table column: checkout total including shipping and tax",
2904+
"string": "Gross"
2905+
},
29022906
"DILs4b": {
29032907
"string": "Unsupported media provider or incorrect URL"
29042908
},
@@ -5543,6 +5547,10 @@
55435547
"Qb2XN5": {
55445548
"string": "\"Mark as paid\" feature creates a {link} - used by Payment Apps"
55455549
},
5550+
"QcXdj6": {
5551+
"context": "customer recent orders table column: net product sales after discounts",
5552+
"string": "Net"
5553+
},
55465554
"QclvqG": {
55475555
"context": "input label",
55485556
"string": "Checkout line limit"
@@ -6072,10 +6080,6 @@
60726080
"context": "intro for model type tab grouping help tooltip",
60736081
"string": "Each model type name is split at the first matching separator. The part before becomes the tab name; the part after appears in the tab menu."
60746082
},
6075-
"TZq1Tm": {
6076-
"context": "customer overview, subtitle scoping a money stat when every fetched recent order belongs to this currency",
6077-
"string": "Across last {count, plural, one {# order} other {# orders}} in {currency}"
6078-
},
60796083
"TZtvTG": {
60806084
"context": "table header required stock label",
60816085
"string": "Required"
@@ -7548,6 +7552,10 @@
75487552
"bIAY+o": {
75497553
"string": "Address updated"
75507554
},
7555+
"bIttOt": {
7556+
"context": "customer overview, AOV tooltip",
7557+
"string": "Average product revenue per order in this channel, from the last {windowSize} orders. Excludes shipping and tax."
7558+
},
75517559
"bKCoN5": {
75527560
"context": "order line discount updated title",
75537561
"string": "{productName} discount was updated"
@@ -8061,6 +8069,10 @@
80618069
"dnbJKr": {
80628070
"string": "This transaction doesn't have any events"
80638071
},
8072+
"doQvFl": {
8073+
"context": "customer overview, subtitle scoping recent money stats to the selected channel window",
8074+
"string": "{count} of last {windowSize, plural, one {# order} other {# orders}} in {channelName}"
8075+
},
80648076
"dpXfQL": {
80658077
"context": "channel link title and a11y label",
80668078
"string": "View orders from {channelName}"
@@ -8128,10 +8140,6 @@
81288140
"context": "product inventory, checkbox",
81298141
"string": "Variant currently in preorder"
81308142
},
8131-
"eAvs2Y": {
8132-
"context": "customer overview, subtitle scoping a money stat when only part of the fetched recent orders belong to this currency",
8133-
"string": "{count} of last {total, plural, one {# order} other {# orders}} in {currency}"
8134-
},
81358143
"eB6oHa": {
81368144
"context": "Message when product is not in any channel",
81378145
"string": "Product is not listed in any channel"
@@ -8760,6 +8768,10 @@
87608768
"context": "table head",
87618769
"string": "Collection Name"
87628770
},
8771+
"htV3fN": {
8772+
"context": "customer overview, prefix before channel name or channel selector",
8773+
"string": "Statistics from"
8774+
},
87638775
"htvX+Z": {
87648776
"string": "Deny"
87658777
},
@@ -9697,10 +9709,18 @@
96979709
"context": "button",
96989710
"string": "Create shipping zone"
96999711
},
9712+
"mKu838": {
9713+
"context": "customer overview, value tooltip heading for shipping and refund breakdown",
9714+
"string": "From these orders"
9715+
},
97009716
"mLZMb6": {
97019717
"context": "ChannelsSection select field label",
97029718
"string": "Channel"
97039719
},
9720+
"mMEpFC": {
9721+
"context": "customer overview, refunded row label in value tooltip",
9722+
"string": "Refunded"
9723+
},
97049724
"mMOskm": {
97059725
"string": "Open product detail"
97069726
},
@@ -9816,6 +9836,10 @@
98169836
"context": "customer's address book, header",
98179837
"string": "{fullName}'s Address Book"
98189838
},
9839+
"n8tEIV": {
9840+
"context": "customer overview, last login tooltip",
9841+
"string": "Most recent time this customer signed in to their account."
9842+
},
98199843
"n97Ii0": {
98209844
"context": "export selected items to csv file",
98219845
"string": "Selected giftCards ({number})"
@@ -10509,6 +10533,10 @@
1050910533
"context": "Badge label showing number of errors",
1051010534
"string": "{count, plural, one {{count} error} other {{count} errors}}"
1051110535
},
10536+
"qXjR1X": {
10537+
"context": "customer overview, shipping row label in value tooltip",
10538+
"string": "Shipping"
10539+
},
1051210540
"qYukJi": {
1051310541
"context": "Status when product is partially configured",
1051410542
"string": "Scheduled"
@@ -11018,6 +11046,10 @@
1101811046
"tLM9Jr": {
1101911047
"string": "The operation cannot be performed right now. This might be due to a pending installation with the same identifier. {docsLink} ({errorCode})"
1102011048
},
11049+
"tQA//+": {
11050+
"context": "customer overview, recent net sales tooltip",
11051+
"string": "Sum of product revenue from this customer's last {windowSize} orders in this channel. Excludes shipping and tax. Discounts are already applied."
11052+
},
1102111053
"tQxBXs": {
1102211054
"context": "PageTypeDeleteWarningDialog single assigned items description",
1102311055
"string": "You are about to delete page type <b>{typeName}</b>. It is assigned to {assignedItemsCount} {assignedItemsCount,plural,one{page} other{pages}}. Deleting this page type will also delete those pages. Are you sure you want to do this?"
@@ -11068,10 +11100,6 @@
1106811100
"context": "variant stocks section subtitle",
1106911101
"string": "Assign this variant to a channel in the product channel manager to define warehouses allocation"
1107011102
},
11071-
"taX/V3": {
11072-
"context": "order total amount",
11073-
"string": "Total"
11074-
},
1107511103
"tf+OkY": {
1107611104
"context": "link to edit variant metadata",
1107711105
"string": "Edit on variant page"
@@ -11752,6 +11780,10 @@
1175211780
"context": "relative time",
1175311781
"string": "just now"
1175411782
},
11783+
"wPhgkN": {
11784+
"context": "customer recent orders table, net column tooltip",
11785+
"string": "Product revenue after discounts. Excludes shipping and tax. Matches the net sales figures in the overview above."
11786+
},
1175511787
"wQdR8M": {
1175611788
"string": "Add search engine title and description to make this category easier to find"
1175711789
},
@@ -11801,6 +11833,10 @@
1180111833
"context": "deactivate customer dialog, confirm button label",
1180211834
"string": "Deactivate"
1180311835
},
11836+
"wek82T": {
11837+
"context": "customer overview stat label: sum of recent orders net product sales",
11838+
"string": "Recent orders total"
11839+
},
1180411840
"werrDz": {
1180511841
"context": "refunded event title",
1180611842
"string": "Products were refunded"
@@ -12040,6 +12076,10 @@
1204012076
"context": "Info message when user sets a past publication date",
1204112077
"string": "This date is in the past. The product will be visible immediately when you save."
1204212078
},
12079+
"xwXFUB": {
12080+
"context": "customer overview, total orders tooltip",
12081+
"string": "Number of orders placed by this customer in the selected channel. Draft and cancelled orders are not counted."
12082+
},
1204312083
"xxQxLE": {
1204412084
"string": "Email Address"
1204512085
},
@@ -12261,10 +12301,6 @@
1226112301
"context": "create new structure, header",
1226212302
"string": "Add Item"
1226312303
},
12264-
"zJuyv/": {
12265-
"context": "customer overview, AOV tooltip clarifying that the average is computed only over the fetched recent orders, not the customer's lifetime",
12266-
"string": "Sum of the recent orders shown divided by their count."
12267-
},
1226812304
"zKEVOg": {
1226912305
"context": "error message when required attributes are not filled",
1227012306
"string": "Please fill in all required attributes before generating variants."

src/components/KpiCard/KpiCard.module.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,19 @@
2626
.subtitleSlot {
2727
min-height: 1rem;
2828
}
29+
30+
.tooltipContent {
31+
display: block;
32+
width: max-content;
33+
max-width: min(52.5rem, calc(100vw - 2rem));
34+
white-space: normal;
35+
text-align: left;
36+
line-height: 1.4;
37+
}
38+
39+
.valueWithTooltip {
40+
cursor: help;
41+
text-decoration: underline dotted;
42+
text-decoration-color: var(--mu-colors-border-default2);
43+
text-underline-offset: 0.2em;
44+
}

src/components/KpiCard/KpiCard.stories.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ export const WithTooltip: Story = {
6262
},
6363
};
6464

65+
export const WithValueTooltip: Story = {
66+
args: {
67+
title: "Recent orders total",
68+
value: "$142.68 USD",
69+
icon: <ShoppingCart size={16} />,
70+
tooltip: "Net product sales from the last 10 orders. Excludes shipping and tax.",
71+
valueTooltip: (
72+
<>
73+
Shipping: $34.20
74+
<br />
75+
Refunded: $25.00
76+
</>
77+
),
78+
subtitle: "3 of last 3 orders in USD",
79+
},
80+
};
81+
6582
export const SubtitleOnly: Story = {
6683
args: {
6784
title: "Recent orders total",

src/components/KpiCard/KpiCard.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ describe("KpiCard", () => {
6969
expect(onSelect).toHaveBeenCalledTimes(1);
7070
});
7171

72+
it("does not invoke onSelect when value tooltip trigger is used", () => {
73+
// Arrange
74+
const onSelect = jest.fn();
75+
76+
renderCard(
77+
<KpiCard
78+
title="Orders"
79+
value="28"
80+
valueTooltip="Shipping: $10"
81+
onSelect={onSelect}
82+
dataTestId="kpi-clickable"
83+
/>,
84+
);
85+
86+
// Act
87+
const valueTooltipTrigger = screen.getByTestId("kpi-value-tooltip-trigger");
88+
89+
fireEvent.pointerDown(valueTooltipTrigger);
90+
fireEvent.click(valueTooltipTrigger);
91+
92+
// Assert
93+
expect(onSelect).not.toHaveBeenCalled();
94+
});
95+
7296
it("does not invoke onSelect when tooltip trigger is used", () => {
7397
// Arrange
7498
const onSelect = jest.fn();

src/components/KpiCard/KpiCard.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface KpiCardProps {
1616
icon?: ReactNode;
1717
subtitle?: ReactNode;
1818
tooltip?: ReactNode;
19+
valueTooltip?: ReactNode;
1920
delta?: KpiDelta;
2021
loading?: boolean;
2122
active?: boolean;
@@ -51,6 +52,7 @@ export const KpiCard = ({
5152
icon,
5253
subtitle,
5354
tooltip,
55+
valueTooltip,
5456
delta,
5557
loading,
5658
active,
@@ -131,19 +133,51 @@ export const KpiCard = ({
131133
<Info size={13} />
132134
</Box>
133135
</Tooltip.Trigger>
134-
<Tooltip.Content side="top">
136+
<Tooltip.Content side="top" className={styles.tooltipContent}>
135137
<Tooltip.Arrow />
136-
{tooltip}
138+
<Text size={2} color="default1">
139+
{tooltip}
140+
</Text>
137141
</Tooltip.Content>
138142
</Tooltip>
139143
</Box>
140144
)}
141145
</Box>
142146

143147
<Box display="flex" flexDirection="column" gap={1}>
144-
<Text size={7} fontWeight="bold" __lineHeight="1.1">
145-
{value}
146-
</Text>
148+
{valueTooltip ? (
149+
<Box
150+
display="inline-flex"
151+
alignSelf="start"
152+
onClick={interactive ? stopEventPropagation : undefined}
153+
onKeyDown={interactive ? stopEventPropagation : undefined}
154+
onPointerDown={interactive ? stopEventPropagation : undefined}
155+
data-test-id="kpi-value-tooltip-trigger"
156+
>
157+
<Tooltip>
158+
<Tooltip.Trigger>
159+
<Text
160+
size={7}
161+
fontWeight="bold"
162+
__lineHeight="1.1"
163+
className={styles.valueWithTooltip}
164+
>
165+
{value}
166+
</Text>
167+
</Tooltip.Trigger>
168+
<Tooltip.Content side="top" className={styles.tooltipContent}>
169+
<Tooltip.Arrow />
170+
<Text size={2} color="default1">
171+
{valueTooltip}
172+
</Text>
173+
</Tooltip.Content>
174+
</Tooltip>
175+
</Box>
176+
) : (
177+
<Text size={7} fontWeight="bold" __lineHeight="1.1">
178+
{value}
179+
</Text>
180+
)}
147181

148182
<Box display="flex" alignItems="center" gap={1} className={styles.subtitleSlot}>
149183
{delta && (

src/customers/components/CustomerOrders/CustomerOrders.module.css

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,23 @@
33
* `.table th:last-child` !important padding rules (specificity 0,2,1)
44
* by adding `:last-child` AND chaining the class (specificity 0,3,1).
55
*/
6-
td.totalCell.totalCell:last-child,
7-
th.totalCell.totalCell:last-child {
6+
td.lastAmountCell.lastAmountCell:last-child,
7+
th.lastAmountCell.lastAmountCell:last-child {
88
padding-right: var(--mu-spacing-6) !important;
99
}
10+
11+
.amountHeaderCell {
12+
text-align: right !important;
13+
}
14+
15+
.amountHeaderContent {
16+
display: inline-flex;
17+
align-items: center;
18+
justify-content: flex-end;
19+
gap: var(--mu-spacing-1);
20+
width: 100%;
21+
}
22+
23+
.tooltipContent {
24+
max-width: 16.25rem;
25+
}

0 commit comments

Comments
 (0)