feat(dashboard,cart,types,utils): refine order details summary (#13313)

* feat(dashboard,types,utils): refine order details summary

* fix tests

* changeset

* ui corrections

* tests again

* weird tests failing

* revert update to subtotal

* revert http tests too

* comments

* move credit lines so it makes more sense

* remove currency codes and add bold prices

* add new properties in default for storefront

* minor to patch

* remove bold on things that should be

* olis comment about taxes

* remove bold from shipping
This commit is contained in:
William Bouchard
2025-09-02 14:50:37 -04:00
committed by GitHub
parent 753e8081c4
commit 2f6edf367a
13 changed files with 348 additions and 214 deletions

View File

@@ -3987,6 +3987,9 @@
},
"taxTotal": {
"type": "string"
},
"totalAfterDiscount": {
"type": "string"
}
},
"required": [
@@ -4002,7 +4005,8 @@
"itemSubtotal",
"shippingSubtotal",
"discountSubtotal",
"taxTotal"
"taxTotal",
"totalAfterDiscount"
],
"additionalProperties": false
},

View File

@@ -1067,7 +1067,8 @@
"itemSubtotal": "Item Subtotal",
"shippingSubtotal": "Shipping Subtotal",
"discountSubtotal": "Discount Subtotal",
"taxTotal": "Tax Total"
"taxTotal": "Tax Total",
"totalAfterDiscount": "Total After Discount"
},
"transfer": {
"title": "Transfer ownership",

View File

@@ -63,6 +63,7 @@ import { getReturnableQuantity } from "../../../../../lib/rma"
import { CopyPaymentLink } from "../copy-payment-link/copy-payment-link"
import ReturnInfoPopover from "./return-info-popover"
import ShippingInfoPopover from "./shipping-info-popover"
import { formatPercentage } from "../../../../../lib/percentage-helpers.ts"
type OrderSummarySectionProps = {
order: AdminOrder
@@ -191,7 +192,7 @@ export const OrderSummarySection = ({
<Header order={order} orderPreview={orderPreview} />
<ItemBreakdown order={order} reservations={reservations!} />
<CostBreakdown order={order} />
<CreditLinesBreakdown order={order} plugins={plugins} />
<DiscountAndTotalBreakdown order={order} plugins={plugins} />
<Total order={order} />
{(showAllocateButton || showReturns || showPayment || showRefund) && (
@@ -569,51 +570,39 @@ const CostBreakdown = ({
const [isTaxOpen, setIsTaxOpen] = useState(false)
const [isShippingOpen, setIsShippingOpen] = useState(false)
const discountCodes = useMemo(() => {
const codes = new Set()
order.items.forEach((item) =>
item.adjustments?.forEach((adj) => {
codes.add(adj.code)
})
)
return Array.from(codes).sort()
}, [order])
const taxCodes = useMemo(() => {
const taxCodeMap = {}
const taxCodeMap: { [key: string]: { total: number; rate: number } } = {}
order.items.forEach((item) => {
item.tax_lines?.forEach((line) => {
taxCodeMap[line.code] = (taxCodeMap[line.code] || 0) + line.total
const prevTotal = taxCodeMap[line.code]?.total || 0
taxCodeMap[line.code] = {
total: prevTotal + line.subtotal,
rate: line.rate,
}
})
})
order.shipping_methods.forEach((sm) => {
sm.tax_lines?.forEach((line) => {
taxCodeMap[line.code] = (taxCodeMap[line.code] || 0) + line.total
const prevTotal = taxCodeMap[line.code]?.total || 0
taxCodeMap[line.code] = {
total: prevTotal + line.subtotal,
rate: line.rate,
}
})
})
return taxCodeMap
}, [order])
const automaticTaxesOn = !!order.region?.automatic_taxes
const hasTaxLines = !!Object.keys(taxCodes).length
const discountTotal = automaticTaxesOn
? order.discount_total
: order.discount_subtotal
const hasTaxes = !!Object.keys(taxCodes).length
return (
<div className="text-ui-fg-subtle flex flex-col gap-y-2 px-6 py-4">
<Cost
label={t(
automaticTaxesOn
? "orders.summary.itemTotal"
: "orders.summary.itemSubtotal"
)}
value={getLocaleAmount(order.item_total, order.currency_code)}
label={t("orders.summary.itemSubtotal")}
value={getLocaleAmount(order.item_subtotal, order.currency_code)}
/>
<Cost
label={
@@ -621,13 +610,7 @@ const CostBreakdown = ({
onClick={() => setIsShippingOpen((o) => !o)}
className="flex cursor-pointer items-center gap-1"
>
<span>
{t(
automaticTaxesOn
? "orders.summary.shippingTotal"
: "orders.summary.shippingSubtotal"
)}
</span>
<span>{t("orders.summary.shippingSubtotal")}</span>
<TriangleDownMini
style={{
transform: `rotate(${isShippingOpen ? 0 : -90}deg)`,
@@ -635,10 +618,7 @@ const CostBreakdown = ({
/>
</div>
}
value={getLocaleAmount(
automaticTaxesOn ? order.shipping_total : order.shipping_subtotal,
order.currency_code
)}
value={getLocaleAmount(order.shipping_subtotal, order.currency_code)}
/>
{isShippingOpen && (
@@ -654,7 +634,7 @@ const CostBreakdown = ({
className="flex items-center justify-between gap-x-2"
>
<div>
<span className="txt-small text-ui-fg-subtle font-medium">
<span className="txt-small">
{sm.name}
{sm.detail.return_id &&
` (${t("fields.returnShipping")})`}{" "}
@@ -665,10 +645,7 @@ const CostBreakdown = ({
<div className="bottom-[calc(50% - 2px)] absolute h-[1px] w-full border-b border-dashed" />
</div>
<span className="txt-small text-ui-fg-muted">
{getLocaleAmount(
automaticTaxesOn ? sm.total : sm.subtotal,
order.currency_code
)}
{getLocaleAmount(sm.subtotal, order.currency_code)}
</span>
</div>
)
@@ -676,36 +653,18 @@ const CostBreakdown = ({
</div>
)}
<Cost
label={t(
automaticTaxesOn
? "orders.summary.discountTotal"
: "orders.summary.discountSubtotal"
)}
secondaryValue={discountCodes.join(", ")}
value={
discountTotal > 0
? `- ${getLocaleAmount(discountTotal, order.currency_code)}`
: "-"
}
/>
<>
<div className="flex justify-between">
<div
onClick={() => hasTaxLines && setIsTaxOpen((o) => !o)}
onClick={() => hasTaxes && setIsTaxOpen((o) => !o)}
className={clx("flex items-center gap-1", {
"cursor-pointer": hasTaxLines,
"cursor-pointer": hasTaxes,
})}
>
<span className="txt-small select-none">
{t(
automaticTaxesOn
? "orders.summary.taxTotalIncl"
: "orders.summary.taxTotal"
)}
{t("orders.summary.taxTotal")}
</span>
{hasTaxLines && (
{hasTaxes && (
<TriangleDownMini
style={{
transform: `rotate(${isTaxOpen ? 0 : -90}deg)`,
@@ -716,21 +675,22 @@ const CostBreakdown = ({
<div className="text-right">
<Text size="small" leading="compact">
{getLocaleAmount(order.tax_total, order.currency_code)}
{getLocaleAmount(order.original_tax_total, order.currency_code)}
</Text>
</div>
</div>
{isTaxOpen && (
<div className="flex flex-col gap-1 pl-5">
{Object.entries(taxCodes).map(([code, total]) => {
{Object.entries(taxCodes).map(([code, { total, rate }]) => {
return (
<div
key={code}
className="flex items-center justify-between gap-x-2"
>
<div>
<span className="txt-small text-ui-fg-subtle font-medium">
{code}
<div className="flex gap-1">
<span className="txt-small">{code}</span>
<span className="txt-small">
({formatPercentage(rate)})
</span>
</div>
<div className="relative flex-1">
@@ -745,11 +705,23 @@ const CostBreakdown = ({
</div>
)}
</>
<div className="text-ui-fg-base flex items-center justify-between">
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{t("fields.total")}
</Text>
<Text
className="text-ui-fg-subtle text-bold"
size="small"
leading="compact"
>
{getLocaleAmount(order.original_total, order.currency_code)}
</Text>
</div>
</div>
)
}
const CreditLinesBreakdown = ({
const DiscountAndTotalBreakdown = ({
order,
plugins,
}: {
@@ -757,97 +729,208 @@ const CreditLinesBreakdown = ({
plugins: AdminPlugin[]
}) => {
const { t } = useTranslation()
const [isDiscountOpen, setIsDiscountOpen] = useState(false)
const [isCreditLinesOpen, setIsCreditLinesOpen] = useState(false)
const creditLines = order.credit_lines ?? []
const loyaltyPlugin = getLoyaltyPlugin(plugins)
if (creditLines.length === 0) {
return null
}
const discounts = useMemo(() => {
const discounts: {
type: "item" | "shipping"
total: number
codes: string[]
}[] = []
if (order.item_discount_total) {
discounts.push({
type: "item",
total: order.item_discount_total,
codes: Array.from(
new Set(
order.items
.flatMap((item) => item.adjustments || [])
.map((adjustment) => adjustment.code!)
)
).sort(),
})
}
if (order.shipping_discount_total) {
discounts.push({
type: "shipping",
total: order.shipping_discount_total,
codes: Array.from(
new Set(
order.shipping_methods
.flatMap((shippingMethod) => shippingMethod.adjustments || [])
.map((adjustment) => adjustment.code!)
)
).sort(),
})
}
return discounts
}, [order])
const hasDiscount = discounts.length > 0
const hasCreditLines = creditLines.length > 0
return (
<div className="text-ui-fg-subtle flex flex-col">
<>
<div
onClick={() => setIsCreditLinesOpen((o) => !o)}
className="bg-ui-bg-component flex cursor-pointer items-center justify-between border border-dashed px-6 py-4"
>
<div className="flex items-center gap-2">
<TriangleDownMini
style={{
transform: `rotate(${isCreditLinesOpen ? 0 : -90}deg)`,
}}
/>
<span className="text-ui-fg-muted txt-small select-none">
{loyaltyPlugin
? t("orders.giftCardsStoreCreditLines")
: t("orders.creditLines.title")}
</span>
</div>
<div>
<Text size="small" leading="compact">
{getLocaleAmount(order.credit_line_total, order.currency_code)}
</Text>
<div className="text-ui-fg-subtle flex flex-col gap-y-2 px-6 py-4">
<Cost
label={
<div
onClick={() => hasDiscount && setIsDiscountOpen((o) => !o)}
className={clx("flex items-center gap-1", {
"cursor-pointer": hasDiscount,
})}
>
<span>{t("orders.summary.discountTotal")}</span>
{hasDiscount && (
<TriangleDownMini
style={{
transform: `rotate(${isDiscountOpen ? 0 : -90}deg)`,
}}
/>
)}
</div>
}
value={getLocaleAmount(order.discount_total, order.currency_code)}
/>
{isDiscountOpen && (
<div className="flex flex-col gap-1 pl-5">
{discounts.map(({ type, total, codes }) => {
return (
<div
key={type}
className="flex items-center justify-between gap-x-2"
>
<div className="flex gap-1">
<span className="txt-small">{t(`fields.${type}`)}</span>
<span className="txt-small">({codes.join(", ")})</span>
</div>
<div className="relative flex-1">
<div className="bottom-[calc(50% - 2px)] absolute h-[1px] w-full border-b border-dashed" />
</div>
<span className="txt-small text-ui-fg-muted">
{getLocaleAmount(total, order.currency_code)}
</span>
</div>
)
})}
</div>
)}
{isCreditLinesOpen && (
<div className="flex flex-col">
{creditLines.map((creditLine) => {
const prettyReference = creditLine.reference
?.split("_")
.join(" ")
.split("-")
.join(" ")
{hasCreditLines && (
<>
<Cost
label={
<div
onClick={() => setIsCreditLinesOpen((o) => !o)}
className="flex cursor-pointer items-center gap-1"
>
<span>
{loyaltyPlugin
? t("orders.giftCardsStoreCreditLines")
: t("orders.creditLines.title")}
</span>
<TriangleDownMini
style={{
transform: `rotate(${isCreditLinesOpen ? 0 : -90}deg)`,
}}
/>
</div>
}
value={getLocaleAmount(
order.credit_line_total,
order.currency_code
)}
/>
{isCreditLinesOpen && (
<div className="flex flex-col gap-1 pl-5">
{creditLines.map((creditLine) => {
const prettyReference = creditLine.reference
?.split("_")
.join(" ")
.split("-")
.join(" ")
const prettyReferenceId = creditLine.reference_id ? (
<DisplayId id={creditLine.reference_id} />
) : null
const prettyReferenceId = creditLine.reference_id ? (
<DisplayId id={creditLine.reference_id} />
) : null
return (
<div
className="text-ui-fg-subtle grid grid-cols-[1fr_1fr_1fr] items-center px-6 py-4 py-4 sm:grid-cols-[1fr_1fr_1fr]"
key={creditLine.id}
>
<div className="w-full min-w-[60px] overflow-hidden">
<Text
size="small"
leading="compact"
weight="plus"
className="truncate"
>
<DisplayId id={creditLine.id} />
</Text>
<Text size="small" leading="compact">
{format(
new Date(creditLine.created_at),
"dd MMM, yyyy, HH:mm:ss"
)}
</Text>
</div>
<div className="hidden items-center justify-end gap-x-2 sm:flex">
<Text size="small" leading="compact" className="capitalize">
{prettyReference} ({prettyReferenceId})
</Text>
</div>
<div className="flex items-center justify-end">
<Text size="small" leading="compact">
return (
<div
key={creditLine.id}
className="flex items-center justify-between gap-x-2"
>
<div className="flex items-center">
<Text
size="small"
leading="compact"
weight="plus"
className="txt-small text-ui-fg-subtle font-medium"
>
<DisplayId id={creditLine.id} />
</Text>
<span className="txt-small text-ui-fg-subtle mx-1">
-
</span>
<Tooltip
content={format(
new Date(creditLine.created_at),
"dd MMM, yyyy, HH:mm:ss"
)}
>
<Text
size="small"
leading="compact"
className="txt-small text-ui-fg-subtle"
>
{format(
new Date(creditLine.created_at),
"dd MMM, yyyy"
)}
</Text>
</Tooltip>
<span className="txt-small text-ui-fg-subtle mx-1">
-
</span>
<Text
size="small"
leading="compact"
className="txt-small text-ui-fg-subtle capitalize"
>
({prettyReference} {prettyReferenceId})
</Text>
</div>
<div className="relative flex-1">
<div className="bottom-[calc(50% - 2px)] absolute h-[1px] w-full border-b border-dashed" />
</div>
<span className="txt-small text-ui-fg-muted">
{getLocaleAmount(
creditLine.amount as number,
order.currency_code
)}
</Text>
</span>
</div>
</div>
)
})}
</div>
)}
</>
)
})}
</div>
)}
</>
)}
<div className="text-ui-fg-base flex items-center justify-between">
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{t("orders.summary.totalAfterDiscount")}
</Text>
<Text
className="text-ui-fg-subtle text-bold"
size="small"
leading="compact"
>
{getLocaleAmount(order.total, order.currency_code)}
</Text>
</div>
</div>
)
}
@@ -1138,15 +1221,6 @@ const Total = ({ order }: { order: AdminOrder }) => {
return (
<div className=" flex flex-col gap-y-2 px-6 py-4">
<div className="text-ui-fg-base flex items-center justify-between">
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{t("fields.total")}
</Text>
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{getStylizedAmount(order.original_total, order.currency_code)}
</Text>
</div>
<div className="text-ui-fg-base flex items-center justify-between">
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{t("fields.paidTotal")}
@@ -1159,18 +1233,20 @@ const Total = ({ order }: { order: AdminOrder }) => {
</Text>
</div>
<div className="text-ui-fg-base flex items-center justify-between">
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{t("fields.creditTotal")}
</Text>
{getTotalCreditLines(order.credit_lines ?? []) > 0 && (
<div className="text-ui-fg-base flex items-center justify-between">
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{t("fields.creditTotal")}
</Text>
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{getStylizedAmount(
getTotalCreditLines(order.credit_lines ?? []),
order.currency_code
)}
</Text>
</div>
<Text className="text-ui-fg-subtle" size="small" leading="compact">
{getStylizedAmount(
getTotalCreditLines(order.credit_lines ?? []),
order.currency_code
)}
</Text>
</div>
)}
<div className="text-ui-fg-base flex items-center justify-between">
<Text
@@ -1181,7 +1257,7 @@ const Total = ({ order }: { order: AdminOrder }) => {
{t("orders.returns.outstandingAmount")}
</Text>
<Text
className="text-ui-fg-subtle text-bold"
className="text-ui-fg-subtle text-bold" // ici
size="small"
leading="compact"
>

View File

@@ -10,14 +10,21 @@ const DEFAULT_PROPERTIES = [
// --- TOTALS ---
"total",
"credit_line_total",
"item_subtotal",
"item_total",
"item_tax_total",
"original_item_tax_total",
"item_discount_total",
"shipping_subtotal",
"original_total",
"original_tax_total",
"subtotal",
"discount_total",
"discount_subtotal",
"shipping_total",
"shipping_tax_total",
"original_shipping_tax_total",
"shipping_discount_total",
"tax_total",
"refundable_total",
"order_change",