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:
Frane Polić
2024-06-17 11:23:18 +02:00
committed by GitHub
parent 0b9a6d5a52
commit 4e86caba30
6 changed files with 128 additions and 14 deletions

View File

@@ -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,

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 }) => {

View File

@@ -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