fix(admin-ui, medusa): Allocations and fulfillments for variants without inventory items (#3660)
* fix fulfillment for order lines without inventory items * fix summary card on order for variants without inventory items * add changeset * minor fixes * remove variants without inventory items from allocate modal * naming * Update .changeset/kind-rings-wave.md Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * remove line item indicator * cleanup --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
Oliver Windall Juhl
parent
ca3b32d53c
commit
eed784d7d0
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/admin-ui": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(admin-ui, medusa): resolve bugs for orders with variants without inventory items
|
||||||
@@ -246,6 +246,10 @@ export const AllocationLineItem: React.FC<{
|
|||||||
}
|
}
|
||||||
}, [variant, locationId, isLoading])
|
}, [variant, locationId, isLoading])
|
||||||
|
|
||||||
|
if (!variant.inventory?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const lineItemReservationCapacity =
|
const lineItemReservationCapacity =
|
||||||
getFulfillableQuantity(item) - (reservedQuantity || 0)
|
getFulfillableQuantity(item) - (reservedQuantity || 0)
|
||||||
|
|
||||||
@@ -256,6 +260,7 @@ export const AllocationLineItem: React.FC<{
|
|||||||
lineItemReservationCapacity,
|
lineItemReservationCapacity,
|
||||||
inventoryItemReservationCapacity
|
inventoryItemReservationCapacity
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-8 flex w-full items-start justify-between">
|
<div className="mt-8 flex w-full items-start justify-between">
|
||||||
<div className="gap-x-base flex w-7/12">
|
<div className="gap-x-base flex w-7/12">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const CreateFulfillmentItemsTable = ({
|
|||||||
items: LineItem[]
|
items: LineItem[]
|
||||||
quantities: Record<string, number>
|
quantities: Record<string, number>
|
||||||
setQuantities: (quantities: Record<string, number>) => void
|
setQuantities: (quantities: Record<string, number>) => void
|
||||||
locationId: string
|
locationId?: string
|
||||||
setErrors: (errors: React.SetStateAction<{}>) => void
|
setErrors: (errors: React.SetStateAction<{}>) => void
|
||||||
}) => {
|
}) => {
|
||||||
const handleQuantityUpdate = React.useCallback(
|
const handleQuantityUpdate = React.useCallback(
|
||||||
@@ -80,6 +80,8 @@ const FulfillmentLine = ({
|
|||||||
{ enabled: isLocationFulfillmentEnabled }
|
{ enabled: isLocationFulfillmentEnabled }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const hasInventoryItem = !!variant?.inventory.length
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isLocationFulfillmentEnabled) {
|
if (isLocationFulfillmentEnabled) {
|
||||||
refetch()
|
refetch()
|
||||||
@@ -100,7 +102,7 @@ const FulfillmentLine = ({
|
|||||||
|
|
||||||
const { inventory } = variant
|
const { inventory } = variant
|
||||||
|
|
||||||
const locationInventory = inventory[0].location_levels?.find(
|
const locationInventory = inventory[0]?.location_levels?.find(
|
||||||
(inv) => inv.location_id === locationId
|
(inv) => inv.location_id === locationId
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -138,11 +140,14 @@ const FulfillmentLine = ({
|
|||||||
}, [validQuantity, setErrors, item.id])
|
}, [validQuantity, setErrors, item.id])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!availableQuantity) {
|
if (!availableQuantity && hasInventoryItem) {
|
||||||
handleQuantityUpdate(0, item.id)
|
handleQuantityUpdate(0, item.id)
|
||||||
} else {
|
} else {
|
||||||
handleQuantityUpdate(
|
handleQuantityUpdate(
|
||||||
Math.min(getFulfillableQuantity(item), availableQuantity),
|
Math.min(
|
||||||
|
getFulfillableQuantity(item),
|
||||||
|
...[hasInventoryItem ? availableQuantity : Number.MAX_VALUE]
|
||||||
|
),
|
||||||
item.id
|
item.id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -158,7 +163,9 @@ const FulfillmentLine = ({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"rounded-rounded hover:bg-grey-5 mx-[-5px] mb-1 flex h-[64px] justify-between py-2 px-[5px]",
|
"rounded-rounded hover:bg-grey-5 mx-[-5px] mb-1 flex h-[64px] justify-between py-2 px-[5px]",
|
||||||
{
|
{
|
||||||
"pointer-events-none opacity-50": !availableQuantity,
|
"pointer-events-none opacity-50":
|
||||||
|
(!availableQuantity && hasInventoryItem) ||
|
||||||
|
(!locationId && isLocationFulfillmentEnabled),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -185,10 +192,12 @@ const FulfillmentLine = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FeatureToggle featureFlag="inventoryService">
|
<FeatureToggle featureFlag="inventoryService">
|
||||||
<div className="inter-base-regular text-grey-50 mr-6 flex flex-col items-end whitespace-nowrap">
|
{hasInventoryItem && (
|
||||||
<p>{availableQuantity || 0} available</p>
|
<div className="inter-base-regular text-grey-50 mr-6 flex flex-col items-end whitespace-nowrap">
|
||||||
<p>({inStockQuantity || 0} in stock)</p>
|
<p>{availableQuantity || 0} available</p>
|
||||||
</div>
|
<p>({inStockQuantity || 0} in stock)</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</FeatureToggle>
|
</FeatureToggle>
|
||||||
<InputField
|
<InputField
|
||||||
type="number"
|
type="number"
|
||||||
@@ -202,7 +211,10 @@ const FulfillmentLine = ({
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
value={quantities[item.id]}
|
value={quantities[item.id]}
|
||||||
max={Math.min(availableQuantity || 0, getFulfillableQuantity(item))}
|
max={Math.min(
|
||||||
|
getFulfillableQuantity(item),
|
||||||
|
...[hasInventoryItem ? availableQuantity || 0 : Number.MAX_VALUE]
|
||||||
|
)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleQuantityUpdate(e.target.valueAsNumber, item.id)
|
handleQuantityUpdate(e.target.valueAsNumber, item.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import {
|
||||||
|
AdminGetVariantsVariantInventoryRes,
|
||||||
|
Order,
|
||||||
|
VariantInventory,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
import { DisplayTotal, PaymentDetails } from "../templates"
|
import { DisplayTotal, PaymentDetails } from "../templates"
|
||||||
import { Order } from "@medusajs/medusa"
|
|
||||||
import React, { useContext, useMemo } from "react"
|
import React, { useContext, useMemo } from "react"
|
||||||
|
|
||||||
import { ActionType } from "../../../../components/molecules/actionables"
|
import { ActionType } from "../../../../components/molecules/actionables"
|
||||||
@@ -9,11 +13,13 @@ import BodyCard from "../../../../components/organisms/body-card"
|
|||||||
import CopyToClipboard from "../../../../components/atoms/copy-to-clipboard"
|
import CopyToClipboard from "../../../../components/atoms/copy-to-clipboard"
|
||||||
import { OrderEditContext } from "../../edit/context"
|
import { OrderEditContext } from "../../edit/context"
|
||||||
import OrderLine from "../order-line"
|
import OrderLine from "../order-line"
|
||||||
|
import { ReservationItemDTO } from "@medusajs/types"
|
||||||
|
import { Response } from "@medusajs/medusa-js"
|
||||||
import StatusIndicator from "../../../../components/fundamentals/status-indicator"
|
import StatusIndicator from "../../../../components/fundamentals/status-indicator"
|
||||||
import { sum } from "lodash"
|
import { sum } from "lodash"
|
||||||
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
|
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
|
||||||
|
import { useMedusa } from "medusa-react"
|
||||||
import useToggleState from "../../../../hooks/use-toggle-state"
|
import useToggleState from "../../../../hooks/use-toggle-state"
|
||||||
import { ReservationItemDTO } from "@medusajs/types"
|
|
||||||
|
|
||||||
type SummaryCardProps = {
|
type SummaryCardProps = {
|
||||||
order: Order
|
order: Order
|
||||||
@@ -28,9 +34,49 @@ const SummaryCard: React.FC<SummaryCardProps> = ({ order, reservations }) => {
|
|||||||
} = useToggleState()
|
} = useToggleState()
|
||||||
|
|
||||||
const { showModal } = useContext(OrderEditContext)
|
const { showModal } = useContext(OrderEditContext)
|
||||||
|
const { client } = useMedusa()
|
||||||
const { isFeatureEnabled } = useFeatureFlag()
|
const { isFeatureEnabled } = useFeatureFlag()
|
||||||
const inventoryEnabled = isFeatureEnabled("inventoryService")
|
const inventoryEnabled = isFeatureEnabled("inventoryService")
|
||||||
|
|
||||||
|
const [variantInventoryMap, setVariantInventoryMap] = React.useState<
|
||||||
|
Map<string, VariantInventory>
|
||||||
|
>(new Map())
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!inventoryEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchInventory = async () => {
|
||||||
|
const inventory = await Promise.all(
|
||||||
|
order.items.map(async (item) => {
|
||||||
|
if (!item.variant_id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return await client.admin.variants.getInventory(item.variant_id)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
setVariantInventoryMap(
|
||||||
|
new Map(
|
||||||
|
inventory
|
||||||
|
.filter(
|
||||||
|
(
|
||||||
|
inventoryItem
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
): inventoryItem is Response<AdminGetVariantsVariantInventoryRes> =>
|
||||||
|
!!inventoryItem
|
||||||
|
)
|
||||||
|
.map((i) => {
|
||||||
|
return [i.variant.id, i.variant]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchInventory()
|
||||||
|
}, [order.items, inventoryEnabled, client.admin.variants])
|
||||||
|
|
||||||
const reservationItemsMap = useMemo(() => {
|
const reservationItemsMap = useMemo(() => {
|
||||||
if (!reservations?.length || !inventoryEnabled) {
|
if (!reservations?.length || !inventoryEnabled) {
|
||||||
return {}
|
return {}
|
||||||
@@ -52,6 +98,13 @@ const SummaryCard: React.FC<SummaryCardProps> = ({ order, reservations }) => {
|
|||||||
|
|
||||||
const allItemsReserved = useMemo(() => {
|
const allItemsReserved = useMemo(() => {
|
||||||
return order.items.every((item) => {
|
return order.items.every((item) => {
|
||||||
|
if (
|
||||||
|
!item.variant_id ||
|
||||||
|
!variantInventoryMap.get(item.variant_id)?.inventory.length
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const reservations = reservationItemsMap[item.id]
|
const reservations = reservationItemsMap[item.id]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,7 +114,7 @@ const SummaryCard: React.FC<SummaryCardProps> = ({ order, reservations }) => {
|
|||||||
item.quantity - (item.fulfilled_quantity || 0))
|
item.quantity - (item.fulfilled_quantity || 0))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}, [reservationItemsMap, order])
|
}, [order.items, variantInventoryMap, reservationItemsMap])
|
||||||
|
|
||||||
const { hasMovements, swapAmount, manualRefund, swapRefund, returnRefund } =
|
const { hasMovements, swapAmount, manualRefund, swapRefund, returnRefund } =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LineItem, ReservationItemDTO } from "@medusajs/medusa"
|
import { LineItem, ReservationItemDTO } from "@medusajs/medusa"
|
||||||
|
import { useAdminStockLocations, useAdminVariantsInventory } from "medusa-react"
|
||||||
|
|
||||||
import Button from "../../../../components/fundamentals/button"
|
import Button from "../../../../components/fundamentals/button"
|
||||||
import CheckCircleFillIcon from "../../../../components/fundamentals/icons/check-circle-fill-icon"
|
import CheckCircleFillIcon from "../../../../components/fundamentals/icons/check-circle-fill-icon"
|
||||||
@@ -10,7 +11,6 @@ import Tooltip from "../../../../components/atoms/tooltip"
|
|||||||
import WarningCircleIcon from "../../../../components/fundamentals/icons/warning-circle"
|
import WarningCircleIcon from "../../../../components/fundamentals/icons/warning-circle"
|
||||||
import { formatAmountWithSymbol } from "../../../../utils/prices"
|
import { formatAmountWithSymbol } from "../../../../utils/prices"
|
||||||
import { sum } from "lodash"
|
import { sum } from "lodash"
|
||||||
import { useAdminStockLocations } from "medusa-react"
|
|
||||||
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
|
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
|
||||||
|
|
||||||
type OrderLineProps = {
|
type OrderLineProps = {
|
||||||
@@ -84,6 +84,13 @@ const ReservationIndicator = ({
|
|||||||
reservations?: ReservationItemDTO[]
|
reservations?: ReservationItemDTO[]
|
||||||
lineItem: LineItem
|
lineItem: LineItem
|
||||||
}) => {
|
}) => {
|
||||||
|
const { variant, isLoading, isFetching } = useAdminVariantsInventory(
|
||||||
|
lineItem.variant_id!,
|
||||||
|
{
|
||||||
|
enabled: !!lineItem?.variant_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const { stock_locations } = useAdminStockLocations({
|
const { stock_locations } = useAdminStockLocations({
|
||||||
id: reservations?.map((r) => r.location_id) || [],
|
id: reservations?.map((r) => r.location_id) || [],
|
||||||
})
|
})
|
||||||
@@ -99,6 +106,15 @@ const ReservationIndicator = ({
|
|||||||
|
|
||||||
const awaitingAllocation = allocatableSum - reservationsSum
|
const awaitingAllocation = allocatableSum - reservationsSum
|
||||||
|
|
||||||
|
if (
|
||||||
|
isLoading ||
|
||||||
|
isFetching ||
|
||||||
|
!lineItem.variant_id ||
|
||||||
|
!variant?.inventory.length
|
||||||
|
) {
|
||||||
|
return <div className="w-[20px]" />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={awaitingAllocation ? "text-rose-50" : "text-grey-40"}>
|
<div className={awaitingAllocation ? "text-rose-50" : "text-grey-40"}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -514,11 +514,15 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemsToValidate = items.filter((item) => item.variant_id)
|
const itemsToValidate = items.filter((item) => !!item.variant_id)
|
||||||
|
|
||||||
for (const item of itemsToValidate) {
|
for (const item of itemsToValidate) {
|
||||||
const pvInventoryItems = await this.listByVariant(item.variant_id!)
|
const pvInventoryItems = await this.listByVariant(item.variant_id!)
|
||||||
|
|
||||||
|
if (!pvInventoryItems.length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const [inventoryLevels, inventoryLevelCount] =
|
const [inventoryLevels, inventoryLevelCount] =
|
||||||
await this.inventoryService_.listInventoryLevels({
|
await this.inventoryService_.listInventoryLevels({
|
||||||
inventory_item_id: pvInventoryItems.map((i) => i.inventory_item_id),
|
inventory_item_id: pvInventoryItems.map((i) => i.inventory_item_id),
|
||||||
|
|||||||
Reference in New Issue
Block a user