feat: carry over promotions toggle on exchanges (#14128)
* feat: carry over promotions toggle on exchanges * fix: inital flag value, return the flag on preview * fix: validation of allocation type * fix: revert client changes * fix: invert condition * feat: recompute adjustments when outbound item is updated * fix: condition again * fix: display more accurate inbound/outbound totals for exchanges * fix: make exchanges specs green * feat: more testing cases * wip: pr feedback * fix: use plural for the flag on Admin * fix: schema test, route refactor * feat: tooltip * feat: refactor to use update workflow * feat: display applied promotion per item on order details, show copy sku on hover * feat: refactor edits and exchanges to have common flag toggle flow * fix: delete empty file * fix: exchange_id param query
This commit is contained in:
@@ -10,8 +10,8 @@ import {
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory, TQueryKey } from "../../lib/query-key-factory"
|
||||
import { inventoryItemsQueryKeys } from "./inventory"
|
||||
import { reservationItemsQueryKeys } from "./reservations"
|
||||
import { inventoryItemsQueryKeys } from "./inventory"
|
||||
|
||||
const ORDERS_QUERY_KEY = "orders" as const
|
||||
const _orderKeys = queryKeysFactory(ORDERS_QUERY_KEY) as TQueryKey<"orders"> & {
|
||||
@@ -406,3 +406,35 @@ export const useCreateOrderCreditLine = (
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateOrderChange = (
|
||||
orderChangeId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminOrderChangeResponse,
|
||||
FetchError,
|
||||
{ carry_over_promotions: boolean }
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: { carry_over_promotions: boolean }) =>
|
||||
sdk.admin.order.updateOrderChange(orderChangeId, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
const orderId = data.order_change.order_id
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.changes(orderId),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2008,6 +2008,7 @@
|
||||
"uploadImagesLabel",
|
||||
"uploadImagesHint",
|
||||
"invalidFileType",
|
||||
"fileTooLarge",
|
||||
"failedToUpload",
|
||||
"deleteWarning_one",
|
||||
"deleteWarning_other",
|
||||
@@ -4907,6 +4908,15 @@
|
||||
"refundAmount": {
|
||||
"type": "string"
|
||||
},
|
||||
"carryOverPromotion": {
|
||||
"type": "string"
|
||||
},
|
||||
"carryOverPromotionHint": {
|
||||
"type": "string"
|
||||
},
|
||||
"carryOverPromotionTooltip": {
|
||||
"type": "string"
|
||||
},
|
||||
"activeChangeError": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4988,6 +4998,9 @@
|
||||
"outboundShipping",
|
||||
"outboundShippingHint",
|
||||
"refundAmount",
|
||||
"carryOverPromotion",
|
||||
"carryOverPromotionHint",
|
||||
"carryOverPromotionTooltip",
|
||||
"activeChangeError",
|
||||
"actions",
|
||||
"cancel",
|
||||
|
||||
@@ -1305,6 +1305,9 @@
|
||||
"outboundShipping": "Outbound shipping",
|
||||
"outboundShippingHint": "Choose which method you want to use.",
|
||||
"refundAmount": "Estimated difference",
|
||||
"carryOverPromotion": "Carry over promotions",
|
||||
"carryOverPromotionHint": "Apply the order's promotions to the exchange items",
|
||||
"carryOverPromotionTooltip": "Only fixed type promotions with EACH allocation and percentage type promotions with EACH or ACROSS allocation can be carried over to outbound exchange items.",
|
||||
"activeChangeError": "There is an active order change on this order. Please finish or discard the previous change.",
|
||||
"actions": {
|
||||
"cancelExchange": {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const sdk = new Medusa({
|
||||
baseUrl: backendUrl,
|
||||
auth: {
|
||||
type: authType,
|
||||
jwtTokenStorageKey
|
||||
jwtTokenStorageKey,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
+84
-6
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { InformationCircleSolid, PencilSquare } from "@medusajs/icons"
|
||||
import { AdminExchange, AdminOrder, AdminOrderPreview } from "@medusajs/types"
|
||||
import {
|
||||
Button,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IconButton,
|
||||
Switch,
|
||||
toast,
|
||||
Tooltip,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
useUpdateExchangeInboundShipping,
|
||||
useUpdateExchangeOutboundShipping,
|
||||
} from "../../../../../hooks/api/exchanges"
|
||||
import { useUpdateOrderChange } from "../../../../../hooks/api/orders"
|
||||
import { currencies } from "../../../../../lib/data/currencies"
|
||||
import { ExchangeInboundSection } from "./exchange-inbound-section.tsx"
|
||||
import { ExchangeOutboundSection } from "./exchange-outbound-section"
|
||||
@@ -92,6 +94,15 @@ export const ExchangeCreateForm = ({
|
||||
isPending: isUpdatingInboundShipping,
|
||||
} = useUpdateExchangeOutboundShipping(exchange.id, order.id)
|
||||
|
||||
const { mutateAsync: updateOrderChange } = useUpdateOrderChange(
|
||||
preview?.order_change?.id!,
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const isRequestLoading =
|
||||
isConfirming ||
|
||||
isCanceling ||
|
||||
@@ -117,6 +128,14 @@ export const ExchangeCreateForm = ({
|
||||
(item) => !!item.actions?.find((a) => a.action === "ITEM_ADD")
|
||||
)
|
||||
|
||||
const hasPromotions = useMemo(() => {
|
||||
return (
|
||||
(order as any).promotions &&
|
||||
Array.isArray((order as any).promotions) &&
|
||||
(order as any).promotions.length > 0
|
||||
)
|
||||
}, [order])
|
||||
|
||||
/**
|
||||
* FORM
|
||||
*/
|
||||
@@ -161,6 +180,8 @@ export const ExchangeCreateForm = ({
|
||||
: "",
|
||||
location_id: orderReturn?.location_id,
|
||||
send_notification: false,
|
||||
carry_over_promotions:
|
||||
preview?.order_change?.carry_over_promotions ?? false,
|
||||
})
|
||||
},
|
||||
resolver: zodResolver(ExchangeCreateSchema),
|
||||
@@ -306,7 +327,14 @@ export const ExchangeCreateForm = ({
|
||||
const action = item.actions?.find(
|
||||
(act) => act.action === "RETURN_ITEM"
|
||||
)
|
||||
acc = acc + (action?.amount || 0)
|
||||
/**
|
||||
* TODO: update this when the change actions return amounts are revamped
|
||||
* it is might not cover all the cases but is more accurate then just using `unit_price` which does't consider adjustments
|
||||
*/
|
||||
acc =
|
||||
acc +
|
||||
((action?.details.quantity || 0) / item.quantity) *
|
||||
item.total
|
||||
|
||||
return acc
|
||||
}, 0) * -1,
|
||||
@@ -323,10 +351,7 @@ export const ExchangeCreateForm = ({
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{getStylizedAmount(
|
||||
outboundPreviewItems.reduce((acc, item) => {
|
||||
const action = item.actions?.find(
|
||||
(act) => act.action === "ITEM_ADD"
|
||||
)
|
||||
acc = acc + (action?.amount || 0)
|
||||
acc = acc + (item.total || 0) // outbound items entire quantity is used for calculating outbound total
|
||||
|
||||
return acc
|
||||
}, 0),
|
||||
@@ -498,6 +523,59 @@ export const ExchangeCreateForm = ({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CARRY OVER PROMOTION */}
|
||||
{hasPromotions && (
|
||||
<div className="bg-ui-bg-field mt-4 rounded-lg border py-2 pl-2 pr-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="carry_over_promotions"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center">
|
||||
<Form.Control className="mr-4 self-start">
|
||||
<Switch
|
||||
dir="ltr"
|
||||
className="mt-[2px] rtl:rotate-180"
|
||||
checked={!!value}
|
||||
onCheckedChange={async (checked) => {
|
||||
onChange(checked)
|
||||
if (preview?.order_change?.id) {
|
||||
await updateOrderChange({
|
||||
carry_over_promotions: checked,
|
||||
})
|
||||
}
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
<div className="block">
|
||||
<Form.Label className="flex items-center gap-x-2">
|
||||
{t("orders.exchanges.carryOverPromotion")}
|
||||
<Form.Hint>
|
||||
<Tooltip
|
||||
content={t(
|
||||
"orders.exchanges.carryOverPromotionTooltip"
|
||||
)}
|
||||
>
|
||||
<InformationCircleSolid />
|
||||
</Tooltip>
|
||||
</Form.Hint>
|
||||
</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.exchanges.carryOverPromotionHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SEND NOTIFICATION*/}
|
||||
<div className="bg-ui-bg-field mt-8 rounded-lg border py-2 pl-2 pr-4">
|
||||
<Form.Field
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ export const ExchangeCreateSchema = z.object({
|
||||
inbound_option_id: z.string().nullish(),
|
||||
outbound_option_id: z.string().nullish(),
|
||||
send_notification: z.boolean().optional(),
|
||||
carry_over_promotions: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type CreateExchangeSchemaType = z.infer<typeof ExchangeCreateSchema>
|
||||
|
||||
+36
-15
@@ -10,6 +10,7 @@ import {
|
||||
DocumentText,
|
||||
ExclamationCircle,
|
||||
PencilSquare,
|
||||
ReceiptPercent,
|
||||
TriangleDownMini,
|
||||
} from "@medusajs/icons"
|
||||
import {
|
||||
@@ -402,29 +403,49 @@ const Item = ({
|
||||
item.variant?.inventory_items?.some((i) => i.required_quantity > 1))
|
||||
const hasUnfulfilledItems = item.quantity - item.detail.fulfilled_quantity > 0
|
||||
|
||||
const appliedPromoCodes = (item.adjustments || []).map((a) => a.code)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={item.id}
|
||||
className="text-ui-fg-subtle grid grid-cols-2 items-center gap-x-4 px-6 py-4"
|
||||
>
|
||||
<div className="flex items-start gap-x-4">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
<div>
|
||||
<Text size="small" leading="compact" className="text-ui-fg-base">
|
||||
{item.title}
|
||||
</Text>
|
||||
<div className=" flex justify-between gap-x-2 ">
|
||||
<div className=" group flex items-start gap-x-4">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
<div>
|
||||
<Text size="small" leading="compact" className="text-ui-fg-base">
|
||||
{item.title}
|
||||
</Text>
|
||||
|
||||
{item.variant_sku && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text size="small">{item.variant_sku}</Text>
|
||||
<Copy content={item.variant_sku} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
)}
|
||||
<Text size="small">
|
||||
{item.variant?.options?.map((o) => o.value).join(" · ")}
|
||||
</Text>
|
||||
{item.variant_sku && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text size="small">{item.variant_sku}</Text>
|
||||
<Copy
|
||||
content={item.variant_sku}
|
||||
className="text-ui-fg-muted hidden group-hover:block"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Text size="small">
|
||||
{item.variant?.options?.map((o) => o.value).join(" · ")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{appliedPromoCodes.length > 0 && (
|
||||
<Tooltip
|
||||
content={
|
||||
<span className="text-pretty">
|
||||
{appliedPromoCodes.map((code) => (
|
||||
<div key={code}>{code}</div>
|
||||
))}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<ReceiptPercent className="text-ui-fg-subtle flex-shrink self-center " />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 items-center gap-x-4">
|
||||
|
||||
Reference in New Issue
Block a user