import { AdminExchange, AdminInventoryLevel, AdminOrder, AdminOrderPreview, } from "@medusajs/types" import { Alert, Button, Heading, Text, toast } from "@medusajs/ui" import { useEffect, useMemo, useState } from "react" import { useFieldArray, UseFormReturn } from "react-hook-form" import { useTranslation } from "react-i18next" import { Form } from "../../../../../components/common/form" import { Combobox } from "../../../../../components/inputs/combobox" import { RouteFocusModal, StackedFocusModal, useStackedModal, } from "../../../../../components/modals" import { useAddExchangeOutboundItems, useAddExchangeOutboundShipping, useDeleteExchangeOutboundShipping, useRemoveExchangeOutboundItem, useUpdateExchangeOutboundItems, } from "../../../../../hooks/api/exchanges" import { sdk } from "../../../../../lib/client" import { OutboundShippingPlaceholder } from "../../../common/placeholders" import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder" import { AddExchangeOutboundItemsTable } from "../add-exchange-outbound-items-table" import { ExchangeOutboundItem } from "./exchange-outbound-item" import { useOrderShippingOptions } from "../../../../../hooks/api/orders" import { CreateExchangeSchemaType } from "./schema" import { getFormattedShippingOptionLocationName } from "../../../../../lib/shipping-options" import { ExtendedVariant } from "../../../../product-variants/product-variant-detail/constants" type ExchangeOutboundSectionProps = { order: AdminOrder exchange: AdminExchange preview: AdminOrderPreview form: UseFormReturn } let itemsToAdd: string[] = [] let itemsToRemove: string[] = [] export const ExchangeOutboundSection = ({ order, preview, exchange, form, }: ExchangeOutboundSectionProps) => { const { t } = useTranslation() const { setIsOpen } = useStackedModal() const [inventoryMap, setInventoryMap] = useState< Record >({}) /** * HOOKS */ const { shipping_options = [] } = useOrderShippingOptions(order.id) // TODO: filter in the API when boolean filter is supported and fulfillment module support partial rule SO filtering const outboundShippingOptions = shipping_options.filter( (so) => !so.rules?.find((r) => r.attribute === "is_return" && r.value === "true") ) const { mutateAsync: addOutboundShipping } = useAddExchangeOutboundShipping( exchange.id, order.id ) const { mutateAsync: deleteOutboundShipping } = useDeleteExchangeOutboundShipping(exchange.id, order.id) const { mutateAsync: addOutboundItem } = useAddExchangeOutboundItems( exchange.id, order.id ) const { mutateAsync: updateOutboundItem } = useUpdateExchangeOutboundItems( exchange.id, order.id ) const { mutateAsync: removeOutboundItem } = useRemoveExchangeOutboundItem( exchange.id, order.id ) /** * Only consider items that belong to this exchange and is an outbound item */ const previewOutboundItems = useMemo( () => preview?.items?.filter( (i) => !!i.actions?.find( (a) => a.exchange_id === exchange.id && a.action === "ITEM_ADD" ) ), [preview.items] ) const variantItemMap = useMemo( () => new Map(order?.items?.map((i) => [i.variant_id, i])), [order.items] ) const { fields: outboundItems, append, remove, update, } = useFieldArray({ name: "outbound_items", control: form.control, }) const variantOutboundMap = useMemo( () => new Map(previewOutboundItems.map((i) => [i.variant_id, i])), [previewOutboundItems, outboundItems] ) useEffect(() => { const existingItemsMap: Record = {} previewOutboundItems.forEach((i) => { const ind = outboundItems.findIndex((field) => field.item_id === i.id) existingItemsMap[i.id] = true if (ind > -1) { if (outboundItems[ind].quantity !== i.detail.quantity) { update(ind, { ...outboundItems[ind], quantity: i.detail.quantity, }) } } else { append( { item_id: i.id, quantity: i.detail.quantity, variant_id: i.variant_id, }, { shouldFocus: false } ) } }) outboundItems.forEach((i, ind) => { if (!(i.item_id in existingItemsMap)) { remove(ind) } }) }, [previewOutboundItems]) const locationId = form.watch("location_id") const showOutboundItemsPlaceholder = !outboundItems.length const onItemsSelected = async () => { itemsToAdd.length && (await addOutboundItem( { items: itemsToAdd.map((variantId) => ({ variant_id: variantId, quantity: 1, })), }, { onError: (error) => { toast.error(error.message) }, } )) for (const itemToRemove of itemsToRemove) { const action = previewOutboundItems .find((i) => i.variant_id === itemToRemove) ?.actions?.find((a) => a.action === "ITEM_ADD") if (action?.id) { await removeOutboundItem(action?.id, { onError: (error) => { toast.error(error.message) }, }) } } setIsOpen("outbound-items", false) } useEffect(() => { const outboundShipping = preview.shipping_methods.find( (s) => !!s.actions?.find((a) => a.action === "SHIPPING_ADD" && !a.return_id) ) if (outboundShipping) { form.setValue("outbound_option_id", outboundShipping.shipping_option_id) } else { form.setValue("outbound_option_id", "") } }, [preview.shipping_methods]) const onShippingOptionChange = async ( selectedOptionId: string | undefined ) => { const outboundShippingMethods = preview.shipping_methods.filter( (s) => !!s.actions?.find((a) => a.action === "SHIPPING_ADD" && !a.return_id) ) const promises = outboundShippingMethods .filter(Boolean) .map((outboundShippingMethod) => { const action = outboundShippingMethod.actions?.find( (a) => a.action === "SHIPPING_ADD" && !a.return_id ) if (action) { return deleteOutboundShipping(action.id) } }) await Promise.all(promises) if (selectedOptionId) { await addOutboundShipping( { shipping_option_id: selectedOptionId }, { onError: (error) => { toast.error(error.message) }, } ) } } const showLevelsWarning = useMemo(() => { if (!locationId) { return false } const allItemsHaveLocation = outboundItems .map((i) => { if (!i.variant_id) { return true } const item = variantItemMap.get(i.variant_id) if (!item?.variant_id || !item?.variant) { return true } if (!item.variant?.manage_inventory) { return true } return inventoryMap[item.variant_id]?.find( (l) => l.location_id === locationId ) }) .every(Boolean) return !allItemsHaveLocation }, [outboundItems, inventoryMap, locationId]) useEffect(() => { const getInventoryMap = async () => { const ret: Record = {} if (!outboundItems.length) { return ret } const variantIds = outboundItems .map((item) => item?.variant_id) .filter(Boolean) as string[] const variants = ( await sdk.admin.productVariant.list({ id: variantIds, fields: "*inventory.location_levels", }) ).variants as ExtendedVariant[] variants.forEach((variant) => { ret[variant.id] = variant.inventory?.[0]?.location_levels || [] }) return ret } getInventoryMap().then((map) => { setInventoryMap(map) }) }, [outboundItems]) return (
{t("orders.returns.outbound")} {t("actions.addItems")} i.variant_id) .filter(Boolean) as string[] } currencyCode={order.currency_code} onSelectionChange={(finalSelection) => { const alreadySelected = outboundItems .map((i) => i.variant_id) .filter(Boolean) as string[] itemsToAdd = finalSelection.filter( (selection) => !alreadySelected.includes(selection) ) itemsToRemove = alreadySelected.filter( (selection) => !finalSelection.includes(selection) ) }} />
{showOutboundItemsPlaceholder && } {outboundItems.map( (item, index) => item.variant_id && variantOutboundMap.get(item.variant_id) && ( { const actionId = previewOutboundItems .find((i) => i.id === item.item_id) ?.actions?.find((a) => a.action === "ITEM_ADD")?.id if (actionId) { removeOutboundItem(actionId, { onError: (error) => { toast.error(error.message) }, }) } }} onUpdate={(payload) => { const actionId = previewOutboundItems .find((i) => i.id === item.item_id) ?.actions?.find((a) => a.action === "ITEM_ADD")?.id if (actionId) { updateOutboundItem( { ...payload, actionId }, { onError: (error) => { toast.error(error.message) }, } ) } }} index={index} /> ) )} {!showOutboundItemsPlaceholder && (
{/* OUTBOUND SHIPPING*/}
{t("orders.exchanges.outboundShipping")} {t("orders.exchanges.outboundShippingHint")}
{ return ( } value={value ?? undefined} onChange={(val) => { onChange(val) onShippingOptionChange(val) }} {...field} options={outboundShippingOptions.map((so) => ({ label: `${ so.name } (${getFormattedShippingOptionLocationName(so)})`, value: so.id, }))} disabled={!outboundShippingOptions.length} /> ) }} />
)} {showLevelsWarning && (
{t("orders.returns.noInventoryLevel")}
{t("orders.returns.noInventoryLevelDesc")}
)}
) }