feat(dashboard): display inventory levels in variants table (#7694)
* feat: display inventory levels in variants table * fix: display conditions and translations * fix: invalidate inventory lists when products are created * fix: translation, fix link definition * fix: revert link * feat: navigation actions * fix: action, refactor * fix: refactor, add check for manage quantity flag * fix: update label
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { inventoryItemsQueryKeys } from "./inventory.tsx"
|
||||
|
||||
const PRODUCTS_QUERY_KEY = "products" as const
|
||||
export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
|
||||
@@ -254,6 +255,10 @@ export const useCreateProduct = (
|
||||
sdk.admin.product.create(payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({ queryKey: productsQueryKeys.lists() })
|
||||
// if `manage_inventory` is true on created variants that will create inventory items automatically
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryItemsQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
|
||||
@@ -324,7 +324,15 @@
|
||||
"create": {
|
||||
"header": "Create Variant"
|
||||
},
|
||||
"tableItemAvailable": "{{availableCount}} available",
|
||||
"tableItem_one": "{{availableCount}} available at {{locationCount}} location",
|
||||
"tableItem_other": "{{availableCount}} available at {{locationCount}} locations",
|
||||
"inventory": {
|
||||
"notManaged": "Not managed",
|
||||
"actions": {
|
||||
"inventoryItems": "Go to inventory item",
|
||||
"inventoryKit": "Show inventory items"
|
||||
},
|
||||
"header": "Stock & Inventory",
|
||||
"editItemDetails": "Edit item details",
|
||||
"manageInventoryLabel": "Manage inventory",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AdminGetInventoryItemsParams } from "@medusajs/medusa"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||
|
||||
export const useInventoryTableQuery = ({
|
||||
@@ -10,14 +11,17 @@ export const useInventoryTableQuery = ({
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
[
|
||||
"id",
|
||||
"location_id",
|
||||
"q",
|
||||
"order",
|
||||
"requires_shipping",
|
||||
"offset",
|
||||
"sku",
|
||||
"origin_country",
|
||||
"material",
|
||||
"mid_code",
|
||||
"hs_code",
|
||||
"order",
|
||||
"weight",
|
||||
"width",
|
||||
@@ -37,7 +41,7 @@ export const useInventoryTableQuery = ({
|
||||
...params
|
||||
} = raw
|
||||
|
||||
const searchParams: AdminGetInventoryItemsParams = {
|
||||
const searchParams: HttpTypes.AdminInventoryItemParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? parseInt(offset) : undefined,
|
||||
weight: weight ? JSON.parse(weight) : undefined,
|
||||
@@ -47,7 +51,16 @@ export const useInventoryTableQuery = ({
|
||||
requires_shipping: requires_shipping
|
||||
? JSON.parse(requires_shipping)
|
||||
: undefined,
|
||||
...params,
|
||||
q: params.q,
|
||||
sku: params.sku,
|
||||
order: params.order,
|
||||
mid_code: params.mid_code,
|
||||
hs_code: params.hs_code,
|
||||
material: params.material,
|
||||
location_levels: {
|
||||
location_id: params.location_id || [],
|
||||
},
|
||||
id: params.id ? params.id.split(",") : undefined,
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PencilSquare, Plus } from "@medusajs/icons"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
@@ -10,7 +11,6 @@ import { useProductVariantTableColumns } from "./use-variant-table-columns"
|
||||
import { useProductVariantTableFilters } from "./use-variant-table-filters"
|
||||
import { useProductVariantTableQuery } from "./use-variant-table-query"
|
||||
import { useProductVariants } from "../../../../../hooks/api/products"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
type ProductVariantSectionProps = {
|
||||
product: HttpTypes.AdminProduct
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { Badge, usePrompt } from "@medusajs/ui"
|
||||
import { Buildings, Component, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { Badge, usePrompt, clx } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { HttpTypes, InventoryItemDTO } from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||
import { useDeleteVariant } from "../../../../../hooks/api/products"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
const VariantActions = ({
|
||||
variant,
|
||||
product,
|
||||
}: {
|
||||
variant: HttpTypes.AdminProductVariant
|
||||
variant: HttpTypes.AdminProductVariant & {
|
||||
inventory_items: { inventory: InventoryItemDTO }[]
|
||||
}
|
||||
product: HttpTypes.AdminProduct
|
||||
}) => {
|
||||
const { mutateAsync } = useDeleteVariant(product.id, variant.id)
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const inventoryItemsCount = variant.inventory_items?.length || 0
|
||||
const hasInventoryItem = inventoryItemsCount === 1
|
||||
const hasInventoryKit = inventoryItemsCount > 1
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
@@ -37,6 +43,23 @@ const VariantActions = ({
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
const [inventoryItemLink, inventoryKitLink] = useMemo(() => {
|
||||
if (!variant.inventory_items?.length) {
|
||||
return ["", ""]
|
||||
}
|
||||
|
||||
const itemId = variant.inventory_items![0].inventory.id
|
||||
const itemLink = `/inventory/${itemId}`
|
||||
|
||||
const itemIds = variant.inventory_items!.map((i) => i.inventory.id)
|
||||
const params = { id: itemIds }
|
||||
const query = new URLSearchParams(params).toString()
|
||||
|
||||
const kitLink = `/inventory?${query}`
|
||||
|
||||
return [itemLink, kitLink]
|
||||
}, [variant.inventory_items])
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
@@ -47,16 +70,26 @@ const VariantActions = ({
|
||||
to: `variants/${variant.id}/edit`,
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
hasInventoryItem
|
||||
? {
|
||||
label: t("products.variant.inventory.actions.inventoryItems"),
|
||||
to: inventoryItemLink,
|
||||
icon: <Buildings />,
|
||||
}
|
||||
: false,
|
||||
hasInventoryKit
|
||||
? {
|
||||
label: t("products.variant.inventory.actions.inventoryKit"),
|
||||
to: inventoryKitLink,
|
||||
icon: <Component />,
|
||||
}
|
||||
: false,
|
||||
].filter(Boolean),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -140,6 +173,60 @@ export const useProductVariantTableColumns = (
|
||||
},
|
||||
}),
|
||||
...optionColumns,
|
||||
columnHelper.accessor("inventory_items", {
|
||||
header: () => (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span className="truncate">{t("fields.inventory")}</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ getValue, row }) => {
|
||||
const variant = row.original
|
||||
|
||||
if (!variant.manage_inventory) {
|
||||
return t("products.variant.inventory.notManaged")
|
||||
}
|
||||
|
||||
const inventory: InventoryItemDTO[] = getValue().map(
|
||||
(i) => i.inventory
|
||||
)
|
||||
|
||||
const hasInventoryKit = inventory.length > 1
|
||||
|
||||
const locations = {}
|
||||
|
||||
inventory.forEach((i) => {
|
||||
i.location_levels.forEach((l) => {
|
||||
locations[l.id] = true
|
||||
})
|
||||
})
|
||||
|
||||
const locationCount = Object.keys(locations).length
|
||||
|
||||
const text = hasInventoryKit
|
||||
? t("products.variant.tableItemAvailable", {
|
||||
availableCount: variant.inventory_quantity,
|
||||
})
|
||||
: t("products.variant.tableItem", {
|
||||
availableCount: variant.inventory_quantity,
|
||||
locationCount,
|
||||
count: locationCount,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center gap-2 overflow-hidden">
|
||||
{hasInventoryKit && <Component style={{ marginTop: 1 }} />}
|
||||
<span
|
||||
className={clx("truncate", {
|
||||
"text-ui-fg-error": !variant.inventory_quantity,
|
||||
})}
|
||||
title={text}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface AdminInventoryItemParams extends FindParams {
|
||||
q?: string
|
||||
sku?: string | string[]
|
||||
origin_country?: string | string[]
|
||||
mid_code?: string | string[]
|
||||
hs_code?: string | string[]
|
||||
material?: string | string[]
|
||||
requires_shipping?: boolean
|
||||
|
||||
Reference in New Issue
Block a user