fix(dashboard): handle deleted products/variants in the order domain (#9841)

* fix: optional variants and products

* fix: more cases with inventory

* fix: fulfillment creation modal

* fix: move create fulfillment actions to footer

* refactor: use properties of the LineItem

* fix: remove N/A

* fix: remove ||

* fix: show confirmed status properly

* fix: pick needed product props

* fix: typo
This commit is contained in:
Frane Polić authored and GitHub committed 2024-11-12 09:05:11 +01:00
1 parent 713e428ec2
commit 81208b6e1d
23 files changed
+111 -94

No files matched your search

@@ -4,12 +4,12 @@ import { Thumbnail } from "../../../../common/thumbnail"
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
type ProductCellProps = { type ProductCellProps = {
product: HttpTypes.AdminProduct product: Pick<HttpTypes.AdminProduct, "thumbnail" | "title">
} }
export const ProductCell = ({ product }: ProductCellProps) => { export const ProductCell = ({ product }: ProductCellProps) => {
return ( return (
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden max-w-[250px]"> <div className="flex h-full w-full max-w-[250px] items-center gap-x-3 overflow-hidden">
<div className="w-fit flex-shrink-0"> <div className="w-fit flex-shrink-0">
<Thumbnail src={product.thumbnail} /> <Thumbnail src={product.thumbnail} />
</div> </div>
@@ -38,8 +38,8 @@ export function OrderAllocateItemsForm({ order }: OrderAllocateItemsFormProps) {
() => () =>
order.items.filter( order.items.filter(
(item) => (item) =>
item.variant.manage_inventory && item.variant?.manage_inventory &&
item.variant.inventory.length && item.variant?.inventory.length &&
item.quantity - item.detail.fulfilled_quantity > 0 item.quantity - item.detail.fulfilled_quantity > 0
), ),
[order.items] [order.items]
@@ -48,8 +48,8 @@ export function OrderAllocateItemsForm({ order }: OrderAllocateItemsFormProps) {
const filteredItems = useMemo(() => { const filteredItems = useMemo(() => {
return itemsToAllocate.filter( return itemsToAllocate.filter(
(i) => (i) =>
i.variant.title.toLowerCase().includes(filterTerm) || i.variant_title.toLowerCase().includes(filterTerm) ||
i.variant.product.title.toLowerCase().includes(filterTerm) i.product_title.toLowerCase().includes(filterTerm)
) )
}, [itemsToAllocate, filterTerm]) }, [itemsToAllocate, filterTerm])
@@ -151,9 +151,9 @@ export function OrderAllocateItemsForm({ order }: OrderAllocateItemsFormProps) {
const item = itemsToAllocate.find((i) => i.id === lineItem.id) const item = itemsToAllocate.find((i) => i.id === lineItem.id)
item.variant.inventory_items.forEach((ii, ind) => { item.variant?.inventory_items.forEach((ii, ind) => {
const num = value || 0 const num = value || 0
const inventory = item.variant.inventory[ind] const inventory = item.variant?.inventory[ind]
form.setValue( form.setValue(
`quantity.${lineItem.id}-${inventory.id}`, `quantity.${lineItem.id}-${inventory.id}`,
@@ -161,7 +161,7 @@ export function OrderAllocateItemsForm({ order }: OrderAllocateItemsFormProps) {
) )
if (value) { if (value) {
const location = inventory.location_levels.find( const location = inventory?.location_levels.find(
(l) => l.location_id === selectedLocationId (l) => l.location_id === selectedLocationId
) )
if (location) { if (location) {
@@ -313,16 +313,16 @@ function defaultAllocations(items: OrderLineItemDTO) {
const ret = {} const ret = {}
items.forEach((item) => { items.forEach((item) => {
const hasInventoryKit = item.variant.inventory_items.length > 1 const hasInventoryKit = item.variant?.inventory_items.length > 1
ret[ ret[
hasInventoryKit hasInventoryKit
? `${item.id}-` ? `${item.id}-`
: `${item.id}-${item.variant.inventory[0].id}` : `${item.id}-${item.variant?.inventory[0].id}`
] = "" ] = ""
if (hasInventoryKit) { if (hasInventoryKit) {
item.variant.inventory.forEach((i) => { item.variant?.inventory.forEach((i) => {
ret[`${item.id}-${i.id}`] = "" ret[`${item.id}-${i.id}`] = ""
}) })
} }
@@ -37,7 +37,7 @@ export function OrderAllocateItemsItem({
const { t } = useTranslation() const { t } = useTranslation()
const variant = item.variant const variant = item.variant
const inventory = item.variant.inventory const inventory = item.variant?.inventory || []
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
@@ -54,8 +54,6 @@ export function OrderAllocateItemsItem({
return {} return {}
} }
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
) )
@@ -73,8 +71,8 @@ export function OrderAllocateItemsItem({
const hasQuantityError = const hasQuantityError =
!hasInventoryKit && !hasInventoryKit &&
availableQuantity && availableQuantity &&
quantityField[`${item.id}-${item.variant.inventory[0].id}`] && quantityField[`${item.id}-${item.variant?.inventory[0].id}`] &&
quantityField[`${item.id}-${item.variant.inventory[0].id}`] > quantityField[`${item.id}-${item.variant?.inventory[0].id}`] >
availableQuantity availableQuantity
const minValue = 0 const minValue = 0
@@ -95,12 +93,12 @@ export function OrderAllocateItemsItem({
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-row"> <div className="flex flex-row">
<Text className="txt-small flex" as="span" weight="plus"> <Text className="txt-small flex" as="span" weight="plus">
{item.variant.product.title} {item.product_title}
</Text> </Text>
{item.variant.sku && ( {item.variant_sku && (
<span className="text-ui-fg-subtle"> <span className="text-ui-fg-subtle">
{" "} {" "}
({item.variant.sku}) ({item.variant_sku})
</span> </span>
)} )}
{hasInventoryKit && ( {hasInventoryKit && (
@@ -134,13 +132,13 @@ export function OrderAllocateItemsItem({
{availableQuantity && {availableQuantity &&
!hasInventoryKit && !hasInventoryKit &&
quantityField[ quantityField[
`${item.id}-${item.variant.inventory[0].id}` `${item.id}-${item.variant?.inventory[0].id}`
] && ( ] && (
<span className="text-ui-fg-error txt-small ml-1"> <span className="text-ui-fg-error txt-small ml-1">
- -
{ {
quantityField[ quantityField[
`${item.id}-${item.variant.inventory[0].id}` `${item.id}-${item.variant?.inventory[0].id}`
] ]
} }
</span> </span>
@@ -173,7 +171,7 @@ export function OrderAllocateItemsItem({
name={ name={
hasInventoryKit hasInventoryKit
? `quantity.${item.id}-` ? `quantity.${item.id}-`
: `quantity.${item.id}-${item.variant.inventory[0].id}` : `quantity.${item.id}-${item.variant?.inventory[0].id}`
} }
rules={{ rules={{
required: !hasInventoryKit, required: !hasInventoryKit,
@@ -196,7 +194,7 @@ export function OrderAllocateItemsItem({
: Number(e.target.value) : Number(e.target.value)
onQuantityChange( onQuantityChange(
item.variant.inventory[0], item.variant?.inventory[0],
item, item,
hasInventoryKit, hasInventoryKit,
val, val,
@@ -70,9 +70,9 @@ export const AddClaimItemsTable = ({
if (q) { if (q) {
results = results.filter((i) => { results = results.filter((i) => {
return ( return (
i.variant.product.title.toLowerCase().includes(q.toLowerCase()) || i.product_title.toLowerCase().includes(q.toLowerCase()) ||
i.variant.title.toLowerCase().includes(q.toLowerCase()) || i.variant_title.toLowerCase().includes(q.toLowerCase()) ||
i.variant.sku?.toLowerCase().includes(q.toLowerCase()) i.variant_sku?.toLowerCase().includes(q.toLowerCase())
) )
}) })
} }
@@ -173,14 +173,14 @@ const sortItems = (
let bValue: any let bValue: any
if (field === "product_title") { if (field === "product_title") {
aValue = a.variant.product.title aValue = a.product_title
bValue = b.variant.product.title bValue = b.product_title
} else if (field === "variant_title") { } else if (field === "variant_title") {
aValue = a.variant.title aValue = a.variant_title
bValue = b.variant.title bValue = b.variant_title
} else if (field === "sku") { } else if (field === "sku") {
aValue = a.variant.sku aValue = a.variant_sku
bValue = b.variant.sku bValue = b.variant_sku
} else if (field === "returnable_quantity") { } else if (field === "returnable_quantity") {
aValue = a.quantity - (a.returned_quantity || 0) aValue = a.quantity - (a.returned_quantity || 0)
bValue = b.quantity - (b.returned_quantity || 0) bValue = b.quantity - (b.returned_quantity || 0)
@@ -52,7 +52,12 @@ export const useClaimItemTableColumns = (currencyCode: string) => {
id: "product", id: "product",
header: () => <ProductHeader />, header: () => <ProductHeader />,
cell: ({ row }) => ( cell: ({ row }) => (
<ProductCell product={row.original.variant.product} /> <ProductCell
product={{
thumbnail: row.original.thumbnail,
title: row.original.product_title,
}}
/>
), ),
}), }),
columnHelper.accessor("variant.sku", { columnHelper.accessor("variant.sku", {
@@ -482,7 +482,7 @@ export const ClaimCreateForm = ({
return true return true
} }
if (!item.variant.manage_inventory) { if (!item.variant?.manage_inventory) {
return true return true
} }
@@ -51,10 +51,10 @@ function ClaimInboundItem({
{item.title}{" "} {item.title}{" "}
</Text> </Text>
{item.variant?.sku && <span>({item.variant.sku})</span>} {item.variant_sku && <span>({item.variant_sku})</span>}
</div> </div>
<Text as="div" className="text-ui-fg-subtle txt-small"> <Text as="div" className="text-ui-fg-subtle txt-small">
{item.variant?.product?.title} {item.product_title}
</Text> </Text>
</div> </div>
</div> </div>
@@ -235,7 +235,7 @@ export const ClaimOutboundSection = ({
return true return true
} }
if (!item.variant.manage_inventory) { if (!item.variant?.manage_inventory) {
return true return true
} }
@@ -56,9 +56,9 @@ export const AddExchangeInboundItemsTable = ({
if (q) { if (q) {
results = results.filter((i) => { results = results.filter((i) => {
return ( return (
i.variant.product.title.toLowerCase().includes(q.toLowerCase()) || i.product_title.toLowerCase().includes(q.toLowerCase()) ||
i.variant.title.toLowerCase().includes(q.toLowerCase()) || i.variant_title.toLowerCase().includes(q.toLowerCase()) ||
i.variant.sku?.toLowerCase().includes(q.toLowerCase()) i.variant_sku?.toLowerCase().includes(q.toLowerCase())
) )
}) })
} }
@@ -133,14 +133,14 @@ const sortItems = (
let bValue: any let bValue: any
if (field === "product_title") { if (field === "product_title") {
aValue = a.variant.product.title aValue = a.product_title
bValue = b.variant.product.title bValue = b.product_title
} else if (field === "variant_title") { } else if (field === "variant_title") {
aValue = a.variant.title aValue = a.variant_title
bValue = b.variant.title bValue = b.variant_title
} else if (field === "sku") { } else if (field === "sku") {
aValue = a.variant.sku aValue = a.variant_sku
bValue = b.variant.sku bValue = b.variant_sku
} }
if (aValue < bValue) { if (aValue < bValue) {
@@ -52,7 +52,12 @@ export const useExchangeItemTableColumns = (currencyCode: string) => {
id: "product", id: "product",
header: () => <ProductHeader />, header: () => <ProductHeader />,
cell: ({ row }) => ( cell: ({ row }) => (
<ProductCell product={row.original.variant.product} /> <ProductCell
product={{
thumbnail: row.original.thumbnail,
title: row.original.product_title,
}}
/>
), ),
}), }),
columnHelper.accessor("variant.sku", { columnHelper.accessor("variant.sku", {
@@ -51,10 +51,10 @@ function ExchangeInboundItem({
{item.title}{" "} {item.title}{" "}
</Text> </Text>
{item.variant?.sku && <span>({item.variant.sku})</span>} {item.variant_sku && <span>({item.variant_sku})</span>}
</div> </div>
<Text as="div" className="text-ui-fg-subtle txt-small"> <Text as="div" className="text-ui-fg-subtle txt-small">
{item.variant?.product?.title} {item.product_title}
</Text> </Text>
</div> </div>
</div> </div>
@@ -287,7 +287,7 @@ export const ExchangeInboundSection = ({
return true return true
} }
if (!item.variant.manage_inventory) { if (!item.variant?.manage_inventory) {
return true return true
} }
@@ -486,7 +486,7 @@ export const ExchangeInboundSection = ({
<Text <Text
size="small" size="small"
leading="compact" leading="compact"
className="text-ui-fg-muted inline ml-1" className="text-ui-fg-muted ml-1 inline"
> >
({t("fields.optional")}) ({t("fields.optional")})
</Text> </Text>
@@ -244,7 +244,7 @@ export const ExchangeOutboundSection = ({
return true return true
} }
if (!item.variant.manage_inventory) { if (!item.variant?.manage_inventory) {
return true return true
} }
@@ -138,18 +138,7 @@ export function OrderCreateFulfillmentForm({
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden" className="flex h-full flex-col overflow-hidden"
> >
<RouteFocusModal.Header> <RouteFocusModal.Header />
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}>
{t("orders.fulfillment.create")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto"> <RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
<div className="flex size-full flex-col items-center overflow-auto p-16"> <div className="flex size-full flex-col items-center overflow-auto p-16">
@@ -309,6 +298,18 @@ export function OrderCreateFulfillmentForm({
</div> </div>
</div> </div>
</RouteFocusModal.Body> </RouteFocusModal.Body>
<RouteFocusModal.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}>
{t("orders.fulfillment.create")}
</Button>
</div>
</RouteFocusModal.Footer>
</KeyboundForm> </KeyboundForm>
</RouteFocusModal.Form> </RouteFocusModal.Form>
) )
@@ -10,7 +10,6 @@ import { Thumbnail } from "../../../../../components/common/thumbnail/index"
import { useProductVariant } from "../../../../../hooks/api/products" import { useProductVariant } from "../../../../../hooks/api/products"
import { getFulfillableQuantity } from "../../../../../lib/order-item" import { getFulfillableQuantity } from "../../../../../lib/order-item"
import { CreateFulfillmentSchema } from "./constants" import { CreateFulfillmentSchema } from "./constants"
import { useReservationItems } from "../../../../../hooks/api/reservations"
type OrderEditItemProps = { type OrderEditItemProps = {
item: HttpTypes.AdminOrderLineItem item: HttpTypes.AdminOrderLineItem
@@ -30,10 +29,13 @@ export function OrderCreateFulfillmentItem({
const { t } = useTranslation() const { t } = useTranslation()
const { variant } = useProductVariant( const { variant } = useProductVariant(
item.variant!.product_id!, item.product_id,
item.variant_id!, item.variant_id,
{ {
fields: "*inventory,*inventory.location_levels", fields: "*inventory,*inventory.location_levels",
},
{
enabled: !!item.variant,
} }
) )
@@ -77,10 +79,10 @@ export function OrderCreateFulfillmentItem({
<Text className="txt-small" as="span" weight="plus"> <Text className="txt-small" as="span" weight="plus">
{item.title} {item.title}
</Text> </Text>
{item.variant?.sku && <span>({item.variant.sku})</span>} {item.variant_sku && <span>({item.variant_sku})</span>}
</div> </div>
<Text as="div" className="text-ui-fg-subtle txt-small"> <Text as="div" className="text-ui-fg-subtle txt-small">
{item.variant?.title ?? ""} {item.variant_title}
</Text> </Text>
</div> </div>
</div> </div>
@@ -71,9 +71,9 @@ export const AddReturnItemsTable = ({
if (q) { if (q) {
results = results.filter((i) => { results = results.filter((i) => {
return ( return (
i.variant.product.title.toLowerCase().includes(q.toLowerCase()) || i.product_title.toLowerCase().includes(q.toLowerCase()) ||
i.variant.title.toLowerCase().includes(q.toLowerCase()) || i.variant_title.toLowerCase().includes(q.toLowerCase()) ||
i.variant.sku?.toLowerCase().includes(q.toLowerCase()) i.variant_sku?.toLowerCase().includes(q.toLowerCase())
) )
}) })
} }
@@ -174,14 +174,14 @@ const sortItems = (
let bValue: any let bValue: any
if (field === "product_title") { if (field === "product_title") {
aValue = a.variant.product.title aValue = a.product_title
bValue = b.variant.product.title bValue = b.product_title
} else if (field === "variant_title") { } else if (field === "variant_title") {
aValue = a.variant.title aValue = a.variant_title
bValue = b.variant.title bValue = b.variant_title
} else if (field === "sku") { } else if (field === "sku") {
aValue = a.variant.sku aValue = a.variant_sku
bValue = b.variant.sku bValue = b.variant_sku
} else if (field === "returnable_quantity") { } else if (field === "returnable_quantity") {
aValue = a.quantity - (a.returned_quantity || 0) aValue = a.quantity - (a.returned_quantity || 0)
bValue = b.quantity - (b.returned_quantity || 0) bValue = b.quantity - (b.returned_quantity || 0)
@@ -52,7 +52,12 @@ export const useReturnItemTableColumns = (currencyCode: string) => {
id: "product", id: "product",
header: () => <ProductHeader />, header: () => <ProductHeader />,
cell: ({ row }) => ( cell: ({ row }) => (
<ProductCell product={row.original.variant.product} /> <ProductCell
product={{
thumbnail: row.original.thumbnail,
title: row.original.product_title,
}}
/>
), ),
}), }),
columnHelper.accessor("variant.sku", { columnHelper.accessor("variant.sku", {
@@ -325,7 +325,7 @@ export const ReturnCreateForm = ({
return true return true
} }
if (!item.variant.manage_inventory) { if (!item.variant?.manage_inventory) {
return true return true
} }
@@ -355,7 +355,7 @@ export const ReturnCreateForm = ({
return undefined return undefined
} }
return await sdk.admin.product.retrieveVariant( return await sdk.admin.product.retrieveVariant(
item.variant.product.id, item.product_id,
item.variant_id, item.variant_id,
{ fields: "*inventory,*inventory.location_levels" } { fields: "*inventory,*inventory.location_levels" }
) )
@@ -53,10 +53,10 @@ function ReturnItem({
<Text className="txt-small" as="span" weight="plus"> <Text className="txt-small" as="span" weight="plus">
{item.title}{" "} {item.title}{" "}
</Text> </Text>
{item.variant.sku && <span>({item.variant.sku})</span>} {item.variant_sku && <span>({item.variant_sku})</span>}
</div> </div>
<Text as="div" className="text-ui-fg-subtle txt-small"> <Text as="div" className="text-ui-fg-subtle txt-small">
{item.variant.product.title} {item.product_title}
</Text> </Text>
</div> </div>
</div> </div>
@@ -369,6 +369,8 @@ const useActivityItems = (order: AdminOrder): Activity[] => {
timestamp: timestamp:
edit.status === "requested" edit.status === "requested"
? edit.requested_at ? edit.requested_at
: edit.status === "confirmed"
? edit.confirmed_at
: edit.status === "declined" : edit.status === "declined"
? edit.declined_at ? edit.declined_at
: edit.status === "canceled" : edit.status === "canceled"
@@ -73,14 +73,14 @@ const UnfulfilledItem = ({
> >
{item.title} {item.title}
</Text> </Text>
{item.variant.sku && ( {item.variant_sku && (
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
<Text size="small">{item.variant.sku}</Text> <Text size="small">{item.variant_sku}</Text>
<Copy content={item.variant.sku} className="text-ui-fg-muted" /> <Copy content={item.variant_sku} className="text-ui-fg-muted" />
</div> </div>
)} )}
<Text size="small"> <Text size="small">
{item.variant.options.map((o) => o.value).join(" · ")} {item.variant?.options.map((o) => o.value).join(" · ")}
</Text> </Text>
</div> </div>
</div> </div>
@@ -118,7 +118,6 @@ const UnfulfilledItemBreakdown = ({ order }: { order: AdminOrder }) => {
(i) => !i.requires_shipping && i.detail.fulfilled_quantity < i.quantity (i) => !i.requires_shipping && i.detail.fulfilled_quantity < i.quantity
) )
return ( return (
<> <>
{!!unfulfilledItemsWithShipping.length && ( {!!unfulfilledItemsWithShipping.length && (
@@ -428,7 +427,7 @@ const Fulfillment = ({
</div> </div>
{(showShippingButton || showDeliveryButton) && ( {(showShippingButton || showDeliveryButton) && (
<div className="bg-ui-bg-subtle flex items-center justify-end rounded-b-xl px-4 py-4 gap-x-2"> <div className="bg-ui-bg-subtle flex items-center justify-end gap-x-2 rounded-b-xl px-4 py-4">
{showDeliveryButton && ( {showDeliveryButton && (
<Button onClick={handleMarkAsDelivered} variant="secondary"> <Button onClick={handleMarkAsDelivered} variant="secondary">
{t("orders.fulfillment.markAsDelivered")} {t("orders.fulfillment.markAsDelivered")}
@@ -746,7 +746,7 @@ const InventoryKitBreakdown = ({ item }: { item: AdminOrderLineItem }) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const inventory = item.variant.inventory_items const inventory = item.variant?.inventory_items || []
return ( return (
<> <>
@@ -250,12 +250,12 @@ export function OrderReceiveReturnForm({
<Text className="txt-small" as="span" weight="plus"> <Text className="txt-small" as="span" weight="plus">
{item.title}{" "} {item.title}{" "}
</Text> </Text>
{originalItem.variant.sku && ( {originalItem.variant_sku && (
<span>({originalItem.variant.sku})</span> <span>({originalItem.variant_sku})</span>
)} )}
</div> </div>
<Text as="div" className="text-ui-fg-subtle txt-small"> <Text as="div" className="text-ui-fg-subtle txt-small">
{originalItem.variant.product.title} {originalItem.product_title}
</Text> </Text>
</div> </div>
</div> </div>