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 { sdk } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
import { inventoryItemsQueryKeys } from "./inventory.tsx"
|
||||||
|
|
||||||
const PRODUCTS_QUERY_KEY = "products" as const
|
const PRODUCTS_QUERY_KEY = "products" as const
|
||||||
export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
|
export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
|
||||||
@@ -254,6 +255,10 @@ export const useCreateProduct = (
|
|||||||
sdk.admin.product.create(payload),
|
sdk.admin.product.create(payload),
|
||||||
onSuccess: (data: any, variables: any, context: any) => {
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
queryClient.invalidateQueries({ queryKey: productsQueryKeys.lists() })
|
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?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -324,7 +324,15 @@
|
|||||||
"create": {
|
"create": {
|
||||||
"header": "Create Variant"
|
"header": "Create Variant"
|
||||||
},
|
},
|
||||||
|
"tableItemAvailable": "{{availableCount}} available",
|
||||||
|
"tableItem_one": "{{availableCount}} available at {{locationCount}} location",
|
||||||
|
"tableItem_other": "{{availableCount}} available at {{locationCount}} locations",
|
||||||
"inventory": {
|
"inventory": {
|
||||||
|
"notManaged": "Not managed",
|
||||||
|
"actions": {
|
||||||
|
"inventoryItems": "Go to inventory item",
|
||||||
|
"inventoryKit": "Show inventory items"
|
||||||
|
},
|
||||||
"header": "Stock & Inventory",
|
"header": "Stock & Inventory",
|
||||||
"editItemDetails": "Edit item details",
|
"editItemDetails": "Edit item details",
|
||||||
"manageInventoryLabel": "Manage inventory",
|
"manageInventoryLabel": "Manage inventory",
|
||||||
|
|||||||
+16
-3
@@ -1,4 +1,5 @@
|
|||||||
import { AdminGetInventoryItemsParams } from "@medusajs/medusa"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
import { useQueryParams } from "../../../../hooks/use-query-params"
|
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||||
|
|
||||||
export const useInventoryTableQuery = ({
|
export const useInventoryTableQuery = ({
|
||||||
@@ -10,14 +11,17 @@ export const useInventoryTableQuery = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const raw = useQueryParams(
|
const raw = useQueryParams(
|
||||||
[
|
[
|
||||||
|
"id",
|
||||||
"location_id",
|
"location_id",
|
||||||
"q",
|
"q",
|
||||||
"order",
|
"order",
|
||||||
"requires_shipping",
|
"requires_shipping",
|
||||||
"offset",
|
"offset",
|
||||||
"sku",
|
"sku",
|
||||||
|
"origin_country",
|
||||||
"material",
|
"material",
|
||||||
"mid_code",
|
"mid_code",
|
||||||
|
"hs_code",
|
||||||
"order",
|
"order",
|
||||||
"weight",
|
"weight",
|
||||||
"width",
|
"width",
|
||||||
@@ -37,7 +41,7 @@ export const useInventoryTableQuery = ({
|
|||||||
...params
|
...params
|
||||||
} = raw
|
} = raw
|
||||||
|
|
||||||
const searchParams: AdminGetInventoryItemsParams = {
|
const searchParams: HttpTypes.AdminInventoryItemParams = {
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: offset ? parseInt(offset) : undefined,
|
offset: offset ? parseInt(offset) : undefined,
|
||||||
weight: weight ? JSON.parse(weight) : undefined,
|
weight: weight ? JSON.parse(weight) : undefined,
|
||||||
@@ -47,7 +51,16 @@ export const useInventoryTableQuery = ({
|
|||||||
requires_shipping: requires_shipping
|
requires_shipping: requires_shipping
|
||||||
? JSON.parse(requires_shipping)
|
? JSON.parse(requires_shipping)
|
||||||
: undefined,
|
: 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 {
|
return {
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,7 @@ import { PencilSquare, Plus } from "@medusajs/icons"
|
|||||||
import { Container, Heading } from "@medusajs/ui"
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { keepPreviousData } from "@tanstack/react-query"
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
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 { useProductVariantTableFilters } from "./use-variant-table-filters"
|
||||||
import { useProductVariantTableQuery } from "./use-variant-table-query"
|
import { useProductVariantTableQuery } from "./use-variant-table-query"
|
||||||
import { useProductVariants } from "../../../../../hooks/api/products"
|
import { useProductVariants } from "../../../../../hooks/api/products"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
|
|
||||||
type ProductVariantSectionProps = {
|
type ProductVariantSectionProps = {
|
||||||
product: HttpTypes.AdminProduct
|
product: HttpTypes.AdminProduct
|
||||||
|
|||||||
+97
-10
@@ -1,25 +1,31 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { Buildings, Component, PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { Badge, usePrompt } from "@medusajs/ui"
|
import { Badge, usePrompt, clx } from "@medusajs/ui"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { HttpTypes, InventoryItemDTO } from "@medusajs/types"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
import { useDeleteVariant } from "../../../../../hooks/api/products"
|
import { useDeleteVariant } from "../../../../../hooks/api/products"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
|
|
||||||
const VariantActions = ({
|
const VariantActions = ({
|
||||||
variant,
|
variant,
|
||||||
product,
|
product,
|
||||||
}: {
|
}: {
|
||||||
variant: HttpTypes.AdminProductVariant
|
variant: HttpTypes.AdminProductVariant & {
|
||||||
|
inventory_items: { inventory: InventoryItemDTO }[]
|
||||||
|
}
|
||||||
product: HttpTypes.AdminProduct
|
product: HttpTypes.AdminProduct
|
||||||
}) => {
|
}) => {
|
||||||
const { mutateAsync } = useDeleteVariant(product.id, variant.id)
|
const { mutateAsync } = useDeleteVariant(product.id, variant.id)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const inventoryItemsCount = variant.inventory_items?.length || 0
|
||||||
|
const hasInventoryItem = inventoryItemsCount === 1
|
||||||
|
const hasInventoryKit = inventoryItemsCount > 1
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("general.areYouSure"),
|
||||||
@@ -37,6 +43,23 @@ const VariantActions = ({
|
|||||||
await mutateAsync()
|
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 (
|
return (
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
groups={[
|
groups={[
|
||||||
@@ -47,16 +70,26 @@ const VariantActions = ({
|
|||||||
to: `variants/${variant.id}/edit`,
|
to: `variants/${variant.id}/edit`,
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
{
|
||||||
label: t("actions.delete"),
|
label: t("actions.delete"),
|
||||||
onClick: handleDelete,
|
onClick: handleDelete,
|
||||||
icon: <Trash />,
|
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,
|
...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({
|
columnHelper.display({
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row, table }) => {
|
cell: ({ row, table }) => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface AdminInventoryItemParams extends FindParams {
|
|||||||
q?: string
|
q?: string
|
||||||
sku?: string | string[]
|
sku?: string | string[]
|
||||||
origin_country?: string | string[]
|
origin_country?: string | string[]
|
||||||
|
mid_code?: string | string[]
|
||||||
hs_code?: string | string[]
|
hs_code?: string | string[]
|
||||||
material?: string | string[]
|
material?: string | string[]
|
||||||
requires_shipping?: boolean
|
requires_shipping?: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user