chore(dashboard): migrate location levels table (#12624)
* chore: migrate location levels table * fix: page size * fix: disable sorting by available quantity * wip: migrate fulfillment location combobox --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore(dashboard): migrate inventory location levels table
|
||||||
@@ -176,7 +176,10 @@ export const useInventoryItemLevels = (
|
|||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => sdk.admin.inventoryItem.listLevels(inventoryItemId, query),
|
queryFn: () => sdk.admin.inventoryItem.listLevels(inventoryItemId, query),
|
||||||
queryKey: inventoryItemLevelsQueryKeys.detail(inventoryItemId),
|
queryKey: inventoryItemLevelsQueryKeys.list({
|
||||||
|
...(query || {}),
|
||||||
|
inventoryItemId,
|
||||||
|
}),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
-62
@@ -1,62 +0,0 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
|
||||||
|
|
||||||
import { InventoryTypes } from "@medusajs/types"
|
|
||||||
import { usePrompt } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
|
||||||
import { useDeleteInventoryItemLevel } from "../../../../../hooks/api/inventory"
|
|
||||||
|
|
||||||
export const LocationActions = ({
|
|
||||||
level,
|
|
||||||
}: {
|
|
||||||
level: InventoryTypes.InventoryLevelDTO
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const prompt = usePrompt()
|
|
||||||
const { mutateAsync } = useDeleteInventoryItemLevel(
|
|
||||||
level.inventory_item_id,
|
|
||||||
level.location_id
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
const res = await prompt({
|
|
||||||
title: t("general.areYouSure"),
|
|
||||||
description: t("inventory.deleteWarning"),
|
|
||||||
confirmText: t("actions.delete"),
|
|
||||||
cancelText: t("actions.cancel"),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutateAsync()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
label: t("actions.edit"),
|
|
||||||
to: `locations/${level.location_id}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: <Trash />,
|
|
||||||
label: t("actions.delete"),
|
|
||||||
onClick: handleDelete,
|
|
||||||
disabled:
|
|
||||||
level.reserved_quantity > 0 || level.stocked_quantity > 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+12
-18
@@ -1,18 +1,19 @@
|
|||||||
import { _DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/data-table"
|
||||||
import { useInventoryItemLevels } from "../../../../../hooks/api/inventory"
|
import { useInventoryItemLevels } from "../../../../../hooks/api/inventory"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
|
||||||
import { useLocationListTableColumns } from "./use-location-list-table-columns"
|
import { useLocationListTableColumns } from "./use-location-list-table-columns"
|
||||||
import { useLocationLevelTableQuery } from "./use-location-list-table-query"
|
import { useLocationLevelTableQuery } from "./use-location-list-table-query"
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
const PREFIX = "invlvl"
|
||||||
|
|
||||||
export const ItemLocationListTable = ({
|
export const ItemLocationListTable = ({
|
||||||
inventory_item_id,
|
inventory_item_id,
|
||||||
}: {
|
}: {
|
||||||
inventory_item_id: string
|
inventory_item_id: string
|
||||||
}) => {
|
}) => {
|
||||||
const { searchParams, raw } = useLocationLevelTableQuery({
|
const searchParams = useLocationLevelTableQuery({
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -23,33 +24,26 @@ export const ItemLocationListTable = ({
|
|||||||
error,
|
error,
|
||||||
} = useInventoryItemLevels(inventory_item_id, {
|
} = useInventoryItemLevels(inventory_item_id, {
|
||||||
...searchParams,
|
...searchParams,
|
||||||
fields: "*stock_locations",
|
fields: "+stock_locations.id,+stock_locations.name",
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = useLocationListTableColumns()
|
const columns = useLocationListTableColumns()
|
||||||
|
|
||||||
const { table } = useDataTable({
|
|
||||||
data: inventory_levels ?? [],
|
|
||||||
columns,
|
|
||||||
count,
|
|
||||||
enablePagination: true,
|
|
||||||
getRowId: (row) => row.id,
|
|
||||||
pageSize: PAGE_SIZE,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<_DataTable
|
<DataTable
|
||||||
table={table}
|
data={inventory_levels ?? []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
rowCount={count}
|
||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
count={count}
|
getRowId={(row) => row.id}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
pagination
|
prefix={PREFIX}
|
||||||
queryObject={raw}
|
layout="fill"
|
||||||
|
enableSearch={false}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-6
@@ -1,10 +1,17 @@
|
|||||||
import { InventoryTypes, StockLocationDTO } from "@medusajs/types"
|
import { InventoryTypes, StockLocationDTO } from "@medusajs/types"
|
||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
|
import { createDataTableColumnHelper, toast, usePrompt } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
import { LocationActions } from "./location-actions"
|
import {
|
||||||
|
inventoryItemLevelsQueryKeys,
|
||||||
|
inventoryItemsQueryKeys,
|
||||||
|
} from "../../../../../hooks/api"
|
||||||
|
import { sdk } from "../../../../../lib/client"
|
||||||
|
import { queryClient } from "../../../../../lib/query-client"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds missing properties to the InventoryLevelDTO type.
|
* Adds missing properties to the InventoryLevelDTO type.
|
||||||
@@ -16,10 +23,45 @@ interface ExtendedLocationLevel extends InventoryTypes.InventoryLevelDTO {
|
|||||||
available_quantity: number
|
available_quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<ExtendedLocationLevel>()
|
const columnHelper = createDataTableColumnHelper<ExtendedLocationLevel>()
|
||||||
|
|
||||||
export const useLocationListTableColumns = () => {
|
export const useLocationListTableColumns = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleDelete = async (level: ExtendedLocationLevel) => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("inventory.deleteWarning"),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sdk.admin.inventoryItem.deleteLevel(
|
||||||
|
level.inventory_item_id,
|
||||||
|
level.location_id
|
||||||
|
)
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(level.inventory_item_id),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemLevelsQueryKeys.detail(level.inventory_item_id),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -54,6 +96,7 @@ export const useLocationListTableColumns = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("stocked_quantity", {
|
columnHelper.accessor("stocked_quantity", {
|
||||||
header: t("fields.inStock"),
|
header: t("fields.inStock"),
|
||||||
@@ -70,6 +113,7 @@ export const useLocationListTableColumns = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("available_quantity", {
|
columnHelper.accessor("available_quantity", {
|
||||||
header: t("inventory.available"),
|
header: t("inventory.available"),
|
||||||
@@ -87,9 +131,31 @@ export const useLocationListTableColumns = () => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.display({
|
columnHelper.action({
|
||||||
id: "actions",
|
actions: (ctx) => {
|
||||||
cell: ({ row }) => <LocationActions level={row.original} />,
|
const level = ctx.row.original
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("actions.edit"),
|
||||||
|
|
||||||
|
onClick: (row) => {
|
||||||
|
navigate(`locations/${level.location_id}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: () => handleDelete(level),
|
||||||
|
disabled:
|
||||||
|
level.reserved_quantity > 0 || level.stocked_quantity > 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[t]
|
[t]
|
||||||
|
|||||||
+9
-21
@@ -1,3 +1,4 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
|
||||||
export const useLocationLevelTableQuery = ({
|
export const useLocationLevelTableQuery = ({
|
||||||
@@ -7,38 +8,25 @@ export const useLocationLevelTableQuery = ({
|
|||||||
pageSize?: number
|
pageSize?: number
|
||||||
prefix?: string
|
prefix?: string
|
||||||
}) => {
|
}) => {
|
||||||
const raw = useQueryParams(
|
const queryObject = useQueryParams(
|
||||||
[
|
[
|
||||||
"id",
|
"order",
|
||||||
|
"offset",
|
||||||
"location_id",
|
"location_id",
|
||||||
"stocked_quantity",
|
"stocked_quantity",
|
||||||
"reserved_quantity",
|
"reserved_quantity",
|
||||||
"incoming_quantity",
|
"incoming_quantity",
|
||||||
"available_quantity",
|
|
||||||
"*stock_locations",
|
|
||||||
],
|
],
|
||||||
prefix
|
prefix
|
||||||
)
|
)
|
||||||
|
|
||||||
const { reserved_quantity, stocked_quantity, available_quantity, ...params } =
|
const { offset, ...rest } = queryObject
|
||||||
raw
|
|
||||||
|
|
||||||
const searchParams = {
|
const searchParams: HttpTypes.AdminInventoryLevelFilters = {
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
reserved_quantity: reserved_quantity
|
offset: offset ? Number(offset) : 0,
|
||||||
? JSON.parse(reserved_quantity)
|
...rest,
|
||||||
: undefined,
|
|
||||||
stocked_quantity: stocked_quantity
|
|
||||||
? JSON.parse(stocked_quantity)
|
|
||||||
: undefined,
|
|
||||||
available_quantity: available_quantity
|
|
||||||
? JSON.parse(available_quantity)
|
|
||||||
: undefined,
|
|
||||||
...params,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return searchParams
|
||||||
searchParams,
|
|
||||||
raw,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-21
@@ -15,7 +15,6 @@ import {
|
|||||||
} from "../../../../../components/modals"
|
} from "../../../../../components/modals"
|
||||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||||
import { useCreateOrderFulfillment } from "../../../../../hooks/api/orders"
|
import { useCreateOrderFulfillment } from "../../../../../hooks/api/orders"
|
||||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
|
||||||
import { getFulfillableQuantity } from "../../../../../lib/order-item"
|
import { getFulfillableQuantity } from "../../../../../lib/order-item"
|
||||||
import { CreateFulfillmentSchema } from "./constants"
|
import { CreateFulfillmentSchema } from "./constants"
|
||||||
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
|
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
|
||||||
@@ -24,6 +23,9 @@ import {
|
|||||||
useShippingOptions,
|
useShippingOptions,
|
||||||
} from "../../../../../hooks/api"
|
} from "../../../../../hooks/api"
|
||||||
import { getReservationsLimitCount } from "../../../../../lib/orders"
|
import { getReservationsLimitCount } from "../../../../../lib/orders"
|
||||||
|
import { sdk } from "../../../../../lib/client"
|
||||||
|
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
|
||||||
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
|
|
||||||
type OrderCreateFulfillmentFormProps = {
|
type OrderCreateFulfillmentFormProps = {
|
||||||
order: AdminOrder
|
order: AdminOrder
|
||||||
@@ -45,6 +47,16 @@ export function OrderCreateFulfillmentForm({
|
|||||||
limit: getReservationsLimitCount(order),
|
limit: getReservationsLimitCount(order),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const stockLocations = useComboboxData({
|
||||||
|
queryFn: (params) => sdk.admin.stockLocation.list(params),
|
||||||
|
queryKey: ["stock_locations"],
|
||||||
|
getOptions: (data) =>
|
||||||
|
data.stock_locations.map((location) => ({
|
||||||
|
label: location.name,
|
||||||
|
value: location.id,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
const itemReservedQuantitiesMap = useMemo(
|
const itemReservedQuantitiesMap = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map((reservations || []).map((r) => [r.line_item_id, r.quantity])),
|
new Map((reservations || []).map((r) => [r.line_item_id, r.quantity])),
|
||||||
@@ -78,8 +90,6 @@ export function OrderCreateFulfillmentForm({
|
|||||||
control: form.control,
|
control: form.control,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { stock_locations = [] } = useStockLocations()
|
|
||||||
|
|
||||||
const { shipping_options = [], isLoading: isShippingOptionsLoading } =
|
const { shipping_options = [], isLoading: isShippingOptionsLoading } =
|
||||||
useShippingOptions({
|
useShippingOptions({
|
||||||
stock_location_id: selectedLocationId,
|
stock_location_id: selectedLocationId,
|
||||||
@@ -155,7 +165,7 @@ export function OrderCreateFulfillmentForm({
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stock_locations?.length && shipping_options?.length) {
|
if (shipping_options?.length) {
|
||||||
const initialShippingOptionId =
|
const initialShippingOptionId =
|
||||||
order.shipping_methods?.[0]?.shipping_option_id
|
order.shipping_methods?.[0]?.shipping_option_id
|
||||||
|
|
||||||
@@ -176,7 +186,7 @@ export function OrderCreateFulfillmentForm({
|
|||||||
} // else -> TODO: what if original shipping option is deleted?
|
} // else -> TODO: what if original shipping option is deleted?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [stock_locations?.length, shipping_options?.length])
|
}, [shipping_options])
|
||||||
|
|
||||||
const fulfilledQuantityArray = (order.items || []).map(
|
const fulfilledQuantityArray = (order.items || []).map(
|
||||||
(item) =>
|
(item) =>
|
||||||
@@ -234,7 +244,7 @@ export function OrderCreateFulfillmentForm({
|
|||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="location_id"
|
name="location_id"
|
||||||
render={({ field: { onChange, ref, ...field } }) => {
|
render={({ field: { ...field } }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="flex flex-col gap-2 xl:flex-row xl:items-center">
|
<div className="flex flex-col gap-2 xl:flex-row xl:items-center">
|
||||||
@@ -246,21 +256,15 @@ export function OrderCreateFulfillmentForm({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Select onValueChange={onChange} {...field}>
|
<Combobox
|
||||||
<Select.Trigger
|
{...field}
|
||||||
className="bg-ui-bg-base"
|
options={stockLocations.options}
|
||||||
ref={ref}
|
searchValue={stockLocations.searchValue}
|
||||||
>
|
onSearchValueChange={
|
||||||
<Select.Value />
|
stockLocations.onSearchValueChange
|
||||||
</Select.Trigger>
|
}
|
||||||
<Select.Content>
|
disabled={stockLocations.disabled}
|
||||||
{stock_locations.map((l) => (
|
/>
|
||||||
<Select.Item key={l.id} value={l.id}>
|
|
||||||
{l.name}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user