Feat(admin-ui): Request return flow warnings and errors (#3473)
**What** - Add warning to request-return modal if no inventory level exists for the combination of items that is being requested - Block receive return at location with the same condition
This commit is contained in:
1 parent
4a7bdc917a
commit
332a9b686b
5 files changed
+200
-36
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(admin-ui): add errors and block receiving returns dependent on existing inventory item levels
|
||||||
+7
-6
@@ -1,8 +1,3 @@
|
|||||||
import { Product } from "@medusajs/medusa"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
|
||||||
import { nestedForm } from "../../../utils/nested-form"
|
|
||||||
import DiscountableForm, {
|
import DiscountableForm, {
|
||||||
DiscountableFormType,
|
DiscountableFormType,
|
||||||
} from "../../forms/product/discountable-form"
|
} from "../../forms/product/discountable-form"
|
||||||
@@ -10,8 +5,14 @@ import GeneralForm, { GeneralFormType } from "../../forms/product/general-form"
|
|||||||
import OrganizeForm, {
|
import OrganizeForm, {
|
||||||
OrganizeFormType,
|
OrganizeFormType,
|
||||||
} from "../../forms/product/organize-form"
|
} from "../../forms/product/organize-form"
|
||||||
|
|
||||||
import Button from "../../fundamentals/button"
|
import Button from "../../fundamentals/button"
|
||||||
import Modal from "../../molecules/modal"
|
import Modal from "../../molecules/modal"
|
||||||
|
import { Product } from "@medusajs/medusa"
|
||||||
|
import { nestedForm } from "../../../utils/nested-form"
|
||||||
|
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -149,7 +150,7 @@ const getDefaultValues = (product: Product): GeneralFormWrapper => {
|
|||||||
? { label: product.type.value, value: product.type.id }
|
? { label: product.type.value, value: product.type.id }
|
||||||
: null,
|
: null,
|
||||||
tags: product.tags ? product.tags.map((t) => t.value) : null,
|
tags: product.tags ? product.tags.map((t) => t.value) : null,
|
||||||
categories: product?.categories?.map((c) => c.id),
|
categories: product.categories?.map((c) => c.id),
|
||||||
},
|
},
|
||||||
discountable: {
|
discountable: {
|
||||||
value: product.discountable,
|
value: product.discountable,
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react"
|
import React, { useState } from "react"
|
||||||
import {
|
import {
|
||||||
|
AdminGetVariantsVariantInventoryRes,
|
||||||
AdminPostReturnsReturnReceiveReq,
|
AdminPostReturnsReturnReceiveReq,
|
||||||
|
InventoryLevelDTO,
|
||||||
|
LineItem,
|
||||||
Order,
|
Order,
|
||||||
Return,
|
Return,
|
||||||
StockLocationDTO,
|
StockLocationDTO,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { useAdminOrder, useAdminReceiveReturn } from "medusa-react"
|
import { useAdminOrder, useAdminReceiveReturn, useMedusa } from "medusa-react"
|
||||||
import { useEffect, useMemo } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
import Button from "../../../../components/fundamentals/button"
|
import Button from "../../../../components/fundamentals/button"
|
||||||
import Modal from "../../../../components/molecules/modal"
|
import Modal from "../../../../components/molecules/modal"
|
||||||
import useNotification from "../../../../hooks/use-notification"
|
import useNotification from "../../../../hooks/use-notification"
|
||||||
@@ -36,6 +39,7 @@ export type ReceiveReturnFormType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => {
|
export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => {
|
||||||
|
const { client } = useMedusa()
|
||||||
const { isFeatureEnabled } = useFeatureFlag()
|
const { isFeatureEnabled } = useFeatureFlag()
|
||||||
const isLocationFulfillmentEnabled =
|
const isLocationFulfillmentEnabled =
|
||||||
isFeatureEnabled("inventoryService") &&
|
isFeatureEnabled("inventoryService") &&
|
||||||
@@ -47,6 +51,18 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => {
|
|||||||
expand: orderRelations,
|
expand: orderRelations,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const form = useForm<ReceiveReturnFormType>({
|
||||||
|
defaultValues: getDefaultReceiveReturnValues(order, returnRequest),
|
||||||
|
reValidateMode: "onBlur",
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
setError,
|
||||||
|
formState: { isDirty },
|
||||||
|
} = form
|
||||||
|
|
||||||
const {
|
const {
|
||||||
stock_locations,
|
stock_locations,
|
||||||
refetch: refetchLocations,
|
refetch: refetchLocations,
|
||||||
@@ -69,6 +85,79 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => {
|
|||||||
label: string
|
label: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
const itemMap = React.useMemo(() => {
|
||||||
|
return new Map<string, LineItem>(order.items.map((i) => [i.id, i]))
|
||||||
|
}, [order.items])
|
||||||
|
|
||||||
|
const [inventoryMap, setInventoryMap] = useState<
|
||||||
|
Map<string, InventoryLevelDTO[]>
|
||||||
|
>(new Map())
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const getInventoryMap = async () => {
|
||||||
|
if (!returnRequest.items?.length || !isLocationFulfillmentEnabled) {
|
||||||
|
return new Map()
|
||||||
|
}
|
||||||
|
const itemInventoryList = await Promise.all(
|
||||||
|
returnRequest.items.map(async (item) => {
|
||||||
|
const orderItem = itemMap.get(item.item_id)
|
||||||
|
if (!orderItem?.variant_id) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return await client.admin.variants.getInventory(orderItem.variant_id)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Map(
|
||||||
|
itemInventoryList
|
||||||
|
.filter((it) => !!it)
|
||||||
|
.map((item) => {
|
||||||
|
const { variant } = item as AdminGetVariantsVariantInventoryRes
|
||||||
|
return [variant.id, variant.inventory[0].location_levels]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getInventoryMap().then((map) => {
|
||||||
|
setInventoryMap(map)
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
client.admin.variants,
|
||||||
|
isLocationFulfillmentEnabled,
|
||||||
|
itemMap,
|
||||||
|
returnRequest.items,
|
||||||
|
])
|
||||||
|
|
||||||
|
const { items: receiveItems } = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "receive_items",
|
||||||
|
})
|
||||||
|
|
||||||
|
const noOfReturnItems = receiveItems.filter((item) => item.receive).length
|
||||||
|
|
||||||
|
const locationsHasInventoryLevels = React.useMemo(() => {
|
||||||
|
if (!noOfReturnItems) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return receiveItems
|
||||||
|
.map((returnItem) => {
|
||||||
|
const item = itemMap.get(returnItem.item_id)
|
||||||
|
if (!item?.variant_id || !returnItem.receive) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const hasInventoryLevel = inventoryMap
|
||||||
|
.get(item.variant_id)
|
||||||
|
?.find((l) => l.location_id === selectedLocation?.value)
|
||||||
|
|
||||||
|
if (!hasInventoryLevel && selectedLocation?.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.every(Boolean)
|
||||||
|
}, [receiveItems, itemMap, noOfReturnItems, inventoryMap, selectedLocation])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLocationFulfillmentEnabled && stock_locations?.length) {
|
if (isLocationFulfillmentEnabled && stock_locations?.length) {
|
||||||
const location = stock_locations.find(
|
const location = stock_locations.find(
|
||||||
@@ -110,17 +199,6 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => {
|
|||||||
return isRefundedClaim || Boolean(returnRequest.swap_id)
|
return isRefundedClaim || Boolean(returnRequest.swap_id)
|
||||||
}, [isRefundedClaim, returnRequest.swap_id])
|
}, [isRefundedClaim, returnRequest.swap_id])
|
||||||
|
|
||||||
const form = useForm<ReceiveReturnFormType>({
|
|
||||||
defaultValues: getDefaultReceiveReturnValues(order, returnRequest),
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
setError,
|
|
||||||
formState: { isDirty },
|
|
||||||
} = form
|
|
||||||
|
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -218,6 +296,17 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => {
|
|||||||
placeholder="Select Location to Return to"
|
placeholder="Select Location to Return to"
|
||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
isMulti={false}
|
isMulti={false}
|
||||||
|
name={"location_id"}
|
||||||
|
errors={
|
||||||
|
locationsHasInventoryLevels
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
location_id: {
|
||||||
|
message:
|
||||||
|
"No inventory levels exist for the items at the selected location",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
onChange={setSelectedLocation}
|
onChange={setSelectedLocation}
|
||||||
options={
|
options={
|
||||||
stock_locations?.map((sl: StockLocationDTO) => ({
|
stock_locations?.map((sl: StockLocationDTO) => ({
|
||||||
@@ -247,7 +336,7 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!isDirty || isLoading}
|
disabled={!isDirty || isLoading || !locationsHasInventoryLevels}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
Save and close
|
Save and close
|
||||||
|
|||||||
@@ -1,36 +1,40 @@
|
|||||||
import {
|
import {
|
||||||
|
AdminGetVariantsVariantInventoryRes,
|
||||||
AdminPostOrdersOrderReturnsReq,
|
AdminPostOrdersOrderReturnsReq,
|
||||||
LineItem as RawLineItem,
|
InventoryLevelDTO,
|
||||||
Order,
|
Order,
|
||||||
|
LineItem as RawLineItem,
|
||||||
StockLocationDTO,
|
StockLocationDTO,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
|
import LayeredModal, {
|
||||||
|
LayeredModalContext,
|
||||||
|
} from "../../../../components/molecules/modal/layered-modal"
|
||||||
|
import React, { useContext, useEffect, useState } from "react"
|
||||||
import {
|
import {
|
||||||
useAdminRequestReturn,
|
useAdminRequestReturn,
|
||||||
useAdminShippingOptions,
|
useAdminShippingOptions,
|
||||||
useAdminStockLocations,
|
useAdminStockLocations,
|
||||||
|
useMedusa,
|
||||||
} from "medusa-react"
|
} from "medusa-react"
|
||||||
import React, { useContext, useEffect, useState } from "react"
|
|
||||||
import Spinner from "../../../../components/atoms/spinner"
|
|
||||||
import LayeredModal, {
|
|
||||||
LayeredModalContext,
|
|
||||||
} from "../../../../components/molecules/modal/layered-modal"
|
|
||||||
|
|
||||||
import Button from "../../../../components/fundamentals/button"
|
import Button from "../../../../components/fundamentals/button"
|
||||||
import CheckIcon from "../../../../components/fundamentals/icons/check-icon"
|
import CheckIcon from "../../../../components/fundamentals/icons/check-icon"
|
||||||
|
import CurrencyInput from "../../../../components/organisms/currency-input"
|
||||||
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
|
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
|
||||||
import IconTooltip from "../../../../components/molecules/icon-tooltip"
|
import IconTooltip from "../../../../components/molecules/icon-tooltip"
|
||||||
import Modal from "../../../../components/molecules/modal"
|
import Modal from "../../../../components/molecules/modal"
|
||||||
|
import { Option } from "../../../../types/shared"
|
||||||
|
import RMASelectProductTable from "../../../../components/organisms/rma-select-product-table"
|
||||||
import RMAShippingPrice from "../../../../components/molecules/rma-select-shipping"
|
import RMAShippingPrice from "../../../../components/molecules/rma-select-shipping"
|
||||||
import Select from "../../../../components/molecules/select/next-select/select"
|
import Select from "../../../../components/molecules/select/next-select/select"
|
||||||
import CurrencyInput from "../../../../components/organisms/currency-input"
|
import Spinner from "../../../../components/atoms/spinner"
|
||||||
import RMASelectProductTable from "../../../../components/organisms/rma-select-product-table"
|
import WarningCircleIcon from "../../../../components/fundamentals/icons/warning-circle"
|
||||||
import useNotification from "../../../../hooks/use-notification"
|
|
||||||
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
|
|
||||||
import { Option } from "../../../../types/shared"
|
|
||||||
import { getErrorMessage } from "../../../../utils/error-messages"
|
|
||||||
import { displayAmount } from "../../../../utils/prices"
|
import { displayAmount } from "../../../../utils/prices"
|
||||||
import { removeNullish } from "../../../../utils/remove-nullish"
|
|
||||||
import { getAllReturnableItems } from "../utils/create-filtering"
|
import { getAllReturnableItems } from "../utils/create-filtering"
|
||||||
|
import { getErrorMessage } from "../../../../utils/error-messages"
|
||||||
|
import { removeNullish } from "../../../../utils/remove-nullish"
|
||||||
|
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
|
||||||
|
import useNotification from "../../../../hooks/use-notification"
|
||||||
|
|
||||||
type ReturnMenuProps = {
|
type ReturnMenuProps = {
|
||||||
order: Order
|
order: Order
|
||||||
@@ -40,6 +44,7 @@ type ReturnMenuProps = {
|
|||||||
type LineItem = Omit<RawLineItem, "beforeInsert">
|
type LineItem = Omit<RawLineItem, "beforeInsert">
|
||||||
|
|
||||||
const ReturnMenu: React.FC<ReturnMenuProps> = ({ order, onDismiss }) => {
|
const ReturnMenu: React.FC<ReturnMenuProps> = ({ order, onDismiss }) => {
|
||||||
|
const { client } = useMedusa()
|
||||||
const layeredModalContext = useContext(LayeredModalContext)
|
const layeredModalContext = useContext(LayeredModalContext)
|
||||||
const { isFeatureEnabled } = useFeatureFlag()
|
const { isFeatureEnabled } = useFeatureFlag()
|
||||||
const isLocationFulfillmentEnabled =
|
const isLocationFulfillmentEnabled =
|
||||||
@@ -88,6 +93,62 @@ const ReturnMenu: React.FC<ReturnMenuProps> = ({ order, onDismiss }) => {
|
|||||||
}
|
}
|
||||||
}, [order])
|
}, [order])
|
||||||
|
|
||||||
|
const itemMap = React.useMemo(() => {
|
||||||
|
return new Map<string, LineItem>(order.items.map((i) => [i.id, i]))
|
||||||
|
}, [order.items])
|
||||||
|
|
||||||
|
const [inventoryMap, setInventoryMap] = useState<
|
||||||
|
Map<string, InventoryLevelDTO[]>
|
||||||
|
>(new Map())
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const getInventoryMap = async () => {
|
||||||
|
if (!allItems.length || !isLocationFulfillmentEnabled) {
|
||||||
|
return new Map()
|
||||||
|
}
|
||||||
|
const itemInventoryList = await Promise.all(
|
||||||
|
allItems.map(async (item) => {
|
||||||
|
if (!item.variant_id) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return await client.admin.variants.getInventory(item.variant_id)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Map(
|
||||||
|
itemInventoryList
|
||||||
|
.filter((it) => !!it)
|
||||||
|
.map((item) => {
|
||||||
|
const { variant } = item as AdminGetVariantsVariantInventoryRes
|
||||||
|
return [variant.id, variant.inventory[0].location_levels]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getInventoryMap().then((map) => {
|
||||||
|
setInventoryMap(map)
|
||||||
|
})
|
||||||
|
}, [allItems, client.admin.variants, isLocationFulfillmentEnabled])
|
||||||
|
|
||||||
|
const locationsHasInventoryLevels = React.useMemo(() => {
|
||||||
|
return Object.entries(toReturn)
|
||||||
|
.map(([itemId]) => {
|
||||||
|
const item = itemMap.get(itemId)
|
||||||
|
if (!item?.variant_id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const hasInventoryLevel = inventoryMap
|
||||||
|
.get(item.variant_id)
|
||||||
|
?.find((l) => l.location_id === selectedLocation?.value)
|
||||||
|
|
||||||
|
if (!hasInventoryLevel && selectedLocation?.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.every(Boolean)
|
||||||
|
}, [toReturn, itemMap, selectedLocation?.value, inventoryMap])
|
||||||
|
|
||||||
const { isLoading: shippingLoading, shipping_options: shippingOptions } =
|
const { isLoading: shippingLoading, shipping_options: shippingOptions } =
|
||||||
useAdminShippingOptions({
|
useAdminShippingOptions({
|
||||||
region_id: order.region_id,
|
region_id: order.region_id,
|
||||||
@@ -227,6 +288,16 @@ const ReturnMenu: React.FC<ReturnMenuProps> = ({ order, onDismiss }) => {
|
|||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{!locationsHasInventoryLevels && (
|
||||||
|
<div className="bg-orange-10 border-orange-20 rounded-rounded text-yellow-60 gap-x-base mt-4 flex border p-4">
|
||||||
|
<div className="text-orange-40">
|
||||||
|
<WarningCircleIcon size={20} fillType="solid" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{`The selected location does not have inventory levels for the selected items. The return can be requested but can't be received until an inventory level is created for the selected location.`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ export const getAllReturnableItems = (
|
|||||||
order: Omit<Order, "beforeInserts">,
|
order: Omit<Order, "beforeInserts">,
|
||||||
isClaim: boolean
|
isClaim: boolean
|
||||||
) => {
|
) => {
|
||||||
console.log(JSON.stringify(order, null, 2))
|
|
||||||
|
|
||||||
let orderItems = order.items.reduce(
|
let orderItems = order.items.reduce(
|
||||||
(map, obj) =>
|
(map, obj) =>
|
||||||
map.set(obj.id, {
|
map.set(obj.id, {
|
||||||
|
|||||||
Reference in new issue
Block a user