import { zodResolver } from "@hookform/resolvers/zod" import { InformationCircleSolid, PencilSquare } from "@medusajs/icons" import { AdminExchange, AdminOrder, AdminOrderPreview } from "@medusajs/types" import { Button, CurrencyInput, Heading, IconButton, Switch, toast, Tooltip, usePrompt, } from "@medusajs/ui" import { useEffect, useMemo, useState } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import { RouteFocusModal, useRouteModal, } from "../../../../../components/modals" import { Form } from "../../../../../components/common/form" import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" import { CreateExchangeSchemaType, ExchangeCreateSchema } from "./schema" import { AdminReturn } from "@medusajs/types" import { KeyboundForm } from "../../../../../components/utilities/keybound-form/keybound-form.tsx" import { useCancelExchangeRequest, useExchangeConfirmRequest, 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" type ReturnCreateFormProps = { order: AdminOrder exchange: AdminExchange preview: AdminOrderPreview orderReturn?: AdminReturn } let IS_CANCELING = false export const ExchangeCreateForm = ({ order, preview, exchange, orderReturn, }: ReturnCreateFormProps) => { const { t } = useTranslation() const { handleSuccess } = useRouteModal() /** * STATE */ const [isInboundShippingPriceEdit, setIsInboundShippingPriceEdit] = useState(false) const [isOutboundShippingPriceEdit, setIsOutboundShippingPriceEdit] = useState(false) const [customInboundShippingAmount, setCustomInboundShippingAmount] = useState<{ value: string; float: number | null }>({ value: "0", float: 0, }) const [customOutboundShippingAmount, setCustomOutboundShippingAmount] = useState<{ value: string; float: number | null }>({ value: "0", float: 0, }) /** * MUTATIONS */ const { mutateAsync: confirmExchangeRequest, isPending: isConfirming } = useExchangeConfirmRequest(exchange.id, order.id) const { mutateAsync: cancelExchangeRequest, isPending: isCanceling } = useCancelExchangeRequest(exchange.id, order.id) const { mutateAsync: updateInboundShipping, isPending: isUpdatingOutboundShipping, } = useUpdateExchangeInboundShipping(exchange.id, order.id) const { mutateAsync: updateOutboundShipping, 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 || isUpdatingInboundShipping || isUpdatingOutboundShipping /** * Only consider items that belong to this exchange. */ const previewItems = useMemo( () => preview?.items?.filter( (i) => !!i.actions?.find((a) => a.exchange_id === exchange.id) ), [preview.items] ) const inboundPreviewItems = previewItems.filter( (item) => !!item.actions?.find((a) => a.action === "RETURN_ITEM") ) const outboundPreviewItems = previewItems.filter( (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 */ const form = useForm({ defaultValues: () => { const inboundShippingMethod = preview.shipping_methods.find((s) => { return !!s.actions?.find( (a) => a.action === "SHIPPING_ADD" && !!a.return_id ) }) const outboundShippingMethod = preview.shipping_methods.find((s) => { return !!s.actions?.find( (a) => a.action === "SHIPPING_ADD" && !a.return_id ) }) return Promise.resolve({ inbound_items: inboundPreviewItems.map((i) => { const inboundAction = i.actions?.find( (a) => a.action === "RETURN_ITEM" ) return { item_id: i.id, variant_id: i.variant_id, quantity: i.detail.return_requested_quantity, note: inboundAction?.internal_note, reason_id: inboundAction?.details?.reason_id as string | undefined, } }), outbound_items: outboundPreviewItems.map((i) => ({ item_id: i.id, variant_id: i.variant_id, quantity: i.detail.quantity, })), inbound_option_id: inboundShippingMethod ? inboundShippingMethod.shipping_option_id : "", outbound_option_id: outboundShippingMethod ? outboundShippingMethod.shipping_option_id : "", location_id: orderReturn?.location_id, send_notification: false, carry_over_promotions: preview?.order_change?.carry_over_promotions ?? false, }) }, resolver: zodResolver(ExchangeCreateSchema), }) const inboundShipping = preview.shipping_methods.find((s) => { return !!s.actions?.find( (a) => a.action === "SHIPPING_ADD" && !!a.return_id ) }) const outboundShipping = preview.shipping_methods.find((s) => { return !!s.actions?.find((a) => a.action === "SHIPPING_ADD" && !a.return_id) }) useEffect(() => { if (inboundShipping) { setCustomInboundShippingAmount({ value: String(inboundShipping.total), float: inboundShipping.total, }) } }, [inboundShipping]) useEffect(() => { if (outboundShipping) { setCustomOutboundShippingAmount({ value: String(outboundShipping.total), float: outboundShipping.total, }) } }, [outboundShipping]) const inboundShippingOptionId = form.watch("inbound_option_id") const outboundShippingOptionId = form.watch("outbound_option_id") const prompt = usePrompt() const handleSubmit = form.handleSubmit(async (data) => { try { const res = await prompt({ title: t("general.areYouSure"), description: t("orders.exchanges.confirmText"), confirmText: t("actions.continue"), cancelText: t("actions.cancel"), variant: "confirmation", }) if (!res) { return } await confirmExchangeRequest({ no_notification: !data.send_notification }) handleSuccess() } catch (e) { toast.error(t("general.error"), { description: e instanceof Error ? e.message : t("errorBoundary.defaultTitle"), }) } }) useEffect(() => { if (isInboundShippingPriceEdit) { document.getElementById("js-inbound-shipping-input")?.focus() } }, [isInboundShippingPriceEdit]) useEffect(() => { if (isOutboundShippingPriceEdit) { document.getElementById("js-outbound-shipping-input")?.focus() } }, [isOutboundShippingPriceEdit]) useEffect(() => { /** * Unmount hook */ return () => { if (IS_CANCELING) { cancelExchangeRequest(undefined, { onSuccess: () => { toast.success( t("orders.exchanges.actions.cancelExchange.successToast") ) }, onError: (error) => { toast.error(error.message) }, }) IS_CANCELING = false } } }, []) /** * For estimated difference show pending difference and subtract the total of inbound items (assume all items will be returned correctly) * We don't include inbound total in the pending difference because it will be considered returned when the receive flow is completed */ const estimatedDifference = preview.summary.pending_difference - inboundPreviewItems.reduce((acc, item) => { return acc + item.total }, 0) const inboundShippingTotal = useMemo(() => { const method = preview.shipping_methods.find( (sm) => !!sm.actions?.find((a) => a.action === "SHIPPING_ADD" && !!a.return_id) ) return (method?.total as number) || 0 }, [preview.shipping_methods]) const outboundShippingTotal = useMemo(() => { const method = preview.shipping_methods.find( (sm) => !!sm.actions?.find((a) => a.action === "SHIPPING_ADD" && !a.return_id) ) return (method?.total as number) || 0 }, [preview.shipping_methods]) return (
{t("orders.exchanges.create")} {/* TOTALS SECTION*/}
{t("orders.returns.inboundTotal")} {getStylizedAmount( inboundPreviewItems.reduce((acc, item) => { const action = item.actions?.find( (act) => act.action === "RETURN_ITEM" ) /** * 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 + (Number(action?.details?.quantity || 0) / item.quantity) * item.total return acc }, 0) * -1, order.currency_code )}
{t("orders.exchanges.outboundTotal")} {getStylizedAmount( outboundPreviewItems.reduce((acc, item) => { acc = acc + (item.total || 0) // outbound items entire quantity is used for calculating outbound total return acc }, 0), order.currency_code )}
{t("orders.returns.inboundShipping")} {!isInboundShippingPriceEdit && ( setIsInboundShippingPriceEdit(true)} variant="transparent" className="text-ui-fg-muted" disabled={ !inboundPreviewItems?.length || !inboundShippingOptionId } > )} {isInboundShippingPriceEdit ? ( { let actionId preview.shipping_methods.forEach((s) => { if (s.actions) { for (const a of s.actions) { if ( a.action === "SHIPPING_ADD" && !!a.return_id ) { actionId = a.id } } } }) const customPrice = customInboundShippingAmount.float if (actionId) { updateInboundShipping( { actionId, custom_amount: customPrice, }, { onError: (error) => { toast.error(error.message) }, } ) } setIsInboundShippingPriceEdit(false) }} symbol={ currencies[order.currency_code.toUpperCase()] .symbol_native } code={order.currency_code} onValueChange={(_value, _name, values) => setCustomInboundShippingAmount({ value: values?.value ?? "", float: values?.float ?? null, }) } value={customInboundShippingAmount.value} disabled={!inboundPreviewItems?.length} /> ) : ( getStylizedAmount(inboundShippingTotal, order.currency_code) )}
{t("orders.exchanges.outboundShipping")} {!isOutboundShippingPriceEdit && ( setIsOutboundShippingPriceEdit(true)} variant="transparent" className="text-ui-fg-muted" disabled={ !outboundPreviewItems?.length || !outboundShippingOptionId } > )} {isOutboundShippingPriceEdit ? ( { let actionId preview.shipping_methods.forEach((s) => { if (s.actions) { for (const a of s.actions) { if (a.action === "SHIPPING_ADD" && !a.return_id) { actionId = a.id } } } }) const customPrice = customOutboundShippingAmount.float if (actionId) { updateOutboundShipping( { actionId, custom_amount: customPrice, }, { onError: (error) => { toast.error(error.message) }, } ) } setIsOutboundShippingPriceEdit(false) }} symbol={ currencies[order.currency_code.toUpperCase()] .symbol_native } code={order.currency_code} onValueChange={(_value, _name, values) => setCustomOutboundShippingAmount({ value: values?.value ?? "", float: values?.float ?? null, }) } value={customOutboundShippingAmount.value} disabled={!outboundPreviewItems?.length} /> ) : ( getStylizedAmount( outboundShippingTotal, order.currency_code ) )}
{t("orders.exchanges.refundAmount")} {getStylizedAmount(estimatedDifference, order.currency_code)}
{/* CARRY OVER PROMOTION */} {hasPromotions && (
{ return (
{ onChange(checked) if (preview?.order_change?.id) { await updateOrderChange({ carry_over_promotions: checked, }) } }} {...field} />
{t("orders.exchanges.carryOverPromotion")} {t("orders.exchanges.carryOverPromotionHint")}
) }} />
)} {/* SEND NOTIFICATION*/}
{ return (
{t("orders.returns.sendNotification")} {t("orders.returns.sendNotificationHint")}
) }} />
) }