From 332a9b686bd0d224855215213dd11b4704283f62 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:01:31 +0100 Subject: [PATCH] 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 --- .changeset/tricky-grapes-flow.md | 5 + .../product-general-section/general-modal.tsx | 13 +- .../orders/details/receive-return/index.tsx | 119 +++++++++++++++--- .../domain/orders/details/returns/index.tsx | 97 ++++++++++++-- .../orders/details/utils/create-filtering.ts | 2 - 5 files changed, 200 insertions(+), 36 deletions(-) create mode 100644 .changeset/tricky-grapes-flow.md diff --git a/.changeset/tricky-grapes-flow.md b/.changeset/tricky-grapes-flow.md new file mode 100644 index 0000000000..785d5374e9 --- /dev/null +++ b/.changeset/tricky-grapes-flow.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(admin-ui): add errors and block receiving returns dependent on existing inventory item levels diff --git a/packages/admin-ui/ui/src/components/organisms/product-general-section/general-modal.tsx b/packages/admin-ui/ui/src/components/organisms/product-general-section/general-modal.tsx index b8f7dd17b2..f595125eb3 100644 --- a/packages/admin-ui/ui/src/components/organisms/product-general-section/general-modal.tsx +++ b/packages/admin-ui/ui/src/components/organisms/product-general-section/general-modal.tsx @@ -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, { DiscountableFormType, } from "../../forms/product/discountable-form" @@ -10,8 +5,14 @@ import GeneralForm, { GeneralFormType } from "../../forms/product/general-form" import OrganizeForm, { OrganizeFormType, } from "../../forms/product/organize-form" + import Button from "../../fundamentals/button" 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 = { product: Product @@ -149,7 +150,7 @@ const getDefaultValues = (product: Product): GeneralFormWrapper => { ? { label: product.type.value, value: product.type.id } : 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: { value: product.discountable, diff --git a/packages/admin-ui/ui/src/domain/orders/details/receive-return/index.tsx b/packages/admin-ui/ui/src/domain/orders/details/receive-return/index.tsx index ecce3f06bd..92709b512d 100644 --- a/packages/admin-ui/ui/src/domain/orders/details/receive-return/index.tsx +++ b/packages/admin-ui/ui/src/domain/orders/details/receive-return/index.tsx @@ -1,13 +1,16 @@ -import React from "react" +import React, { useState } from "react" import { + AdminGetVariantsVariantInventoryRes, AdminPostReturnsReturnReceiveReq, + InventoryLevelDTO, + LineItem, Order, Return, StockLocationDTO, } from "@medusajs/medusa" -import { useAdminOrder, useAdminReceiveReturn } from "medusa-react" +import { useAdminOrder, useAdminReceiveReturn, useMedusa } from "medusa-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 Modal from "../../../../components/molecules/modal" import useNotification from "../../../../hooks/use-notification" @@ -36,6 +39,7 @@ export type ReceiveReturnFormType = { } export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => { + const { client } = useMedusa() const { isFeatureEnabled } = useFeatureFlag() const isLocationFulfillmentEnabled = isFeatureEnabled("inventoryService") && @@ -47,6 +51,18 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => { expand: orderRelations, }) + const form = useForm({ + defaultValues: getDefaultReceiveReturnValues(order, returnRequest), + reValidateMode: "onBlur", + }) + + const { + handleSubmit, + reset, + setError, + formState: { isDirty }, + } = form + const { stock_locations, refetch: refetchLocations, @@ -69,6 +85,79 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => { label: string } | null>(null) + const itemMap = React.useMemo(() => { + return new Map(order.items.map((i) => [i.id, i])) + }, [order.items]) + + const [inventoryMap, setInventoryMap] = useState< + Map + >(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(() => { if (isLocationFulfillmentEnabled && stock_locations?.length) { const location = stock_locations.find( @@ -110,17 +199,6 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => { return isRefundedClaim || Boolean(returnRequest.swap_id) }, [isRefundedClaim, returnRequest.swap_id]) - const form = useForm({ - defaultValues: getDefaultReceiveReturnValues(order, returnRequest), - }) - - const { - handleSubmit, - reset, - setError, - formState: { isDirty }, - } = form - const notification = useNotification() useEffect(() => { @@ -218,6 +296,17 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => { placeholder="Select Location to Return to" value={selectedLocation} isMulti={false} + name={"location_id"} + errors={ + locationsHasInventoryLevels + ? {} + : { + location_id: { + message: + "No inventory levels exist for the items at the selected location", + }, + } + } onChange={setSelectedLocation} options={ stock_locations?.map((sl: StockLocationDTO) => ({ @@ -247,7 +336,7 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => {