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:
@@ -176,7 +176,10 @@ export const useInventoryItemLevels = (
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => sdk.admin.inventoryItem.listLevels(inventoryItemId, query),
|
||||
queryKey: inventoryItemLevelsQueryKeys.detail(inventoryItemId),
|
||||
queryKey: inventoryItemLevelsQueryKeys.list({
|
||||
...(query || {}),
|
||||
inventoryItemId,
|
||||
}),
|
||||
...options,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import { _DataTable } from "../../../../../components/table/data-table"
|
||||
import { DataTable } from "../../../../../components/data-table"
|
||||
import { useInventoryItemLevels } from "../../../../../hooks/api/inventory"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useLocationListTableColumns } from "./use-location-list-table-columns"
|
||||
import { useLocationLevelTableQuery } from "./use-location-list-table-query"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const PREFIX = "invlvl"
|
||||
|
||||
export const ItemLocationListTable = ({
|
||||
inventory_item_id,
|
||||
}: {
|
||||
inventory_item_id: string
|
||||
}) => {
|
||||
const { searchParams, raw } = useLocationLevelTableQuery({
|
||||
const searchParams = useLocationLevelTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const {
|
||||
@@ -23,33 +24,26 @@ export const ItemLocationListTable = ({
|
||||
error,
|
||||
} = useInventoryItemLevels(inventory_item_id, {
|
||||
...searchParams,
|
||||
fields: "*stock_locations",
|
||||
fields: "+stock_locations.id,+stock_locations.name",
|
||||
})
|
||||
|
||||
const columns = useLocationListTableColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: inventory_levels ?? [],
|
||||
columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<_DataTable
|
||||
table={table}
|
||||
<DataTable
|
||||
data={inventory_levels ?? []}
|
||||
columns={columns}
|
||||
rowCount={count}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
getRowId={(row) => row.id}
|
||||
isLoading={isLoading}
|
||||
pagination
|
||||
queryObject={raw}
|
||||
prefix={PREFIX}
|
||||
layout="fill"
|
||||
enableSearch={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { InventoryTypes, StockLocationDTO } from "@medusajs/types"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { createDataTableColumnHelper, toast, usePrompt } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
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.
|
||||
@@ -16,10 +23,45 @@ interface ExtendedLocationLevel extends InventoryTypes.InventoryLevelDTO {
|
||||
available_quantity: number
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<ExtendedLocationLevel>()
|
||||
const columnHelper = createDataTableColumnHelper<ExtendedLocationLevel>()
|
||||
|
||||
export const useLocationListTableColumns = () => {
|
||||
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(
|
||||
() => [
|
||||
@@ -54,6 +96,7 @@ export const useLocationListTableColumns = () => {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableSorting: true,
|
||||
}),
|
||||
columnHelper.accessor("stocked_quantity", {
|
||||
header: t("fields.inStock"),
|
||||
@@ -70,6 +113,7 @@ export const useLocationListTableColumns = () => {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableSorting: true,
|
||||
}),
|
||||
columnHelper.accessor("available_quantity", {
|
||||
header: t("inventory.available"),
|
||||
@@ -87,9 +131,31 @@ export const useLocationListTableColumns = () => {
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <LocationActions level={row.original} />,
|
||||
columnHelper.action({
|
||||
actions: (ctx) => {
|
||||
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]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useLocationLevelTableQuery = ({
|
||||
@@ -7,38 +8,25 @@ export const useLocationLevelTableQuery = ({
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
const queryObject = useQueryParams(
|
||||
[
|
||||
"id",
|
||||
"order",
|
||||
"offset",
|
||||
"location_id",
|
||||
"stocked_quantity",
|
||||
"reserved_quantity",
|
||||
"incoming_quantity",
|
||||
"available_quantity",
|
||||
"*stock_locations",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { reserved_quantity, stocked_quantity, available_quantity, ...params } =
|
||||
raw
|
||||
const { offset, ...rest } = queryObject
|
||||
|
||||
const searchParams = {
|
||||
const searchParams: HttpTypes.AdminInventoryLevelFilters = {
|
||||
limit: pageSize,
|
||||
reserved_quantity: reserved_quantity
|
||||
? JSON.parse(reserved_quantity)
|
||||
: undefined,
|
||||
stocked_quantity: stocked_quantity
|
||||
? JSON.parse(stocked_quantity)
|
||||
: undefined,
|
||||
available_quantity: available_quantity
|
||||
? JSON.parse(available_quantity)
|
||||
: undefined,
|
||||
...params,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
...rest,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw,
|
||||
}
|
||||
return searchParams
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from "../../../../../components/modals"
|
||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||
import { useCreateOrderFulfillment } from "../../../../../hooks/api/orders"
|
||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
import { getFulfillableQuantity } from "../../../../../lib/order-item"
|
||||
import { CreateFulfillmentSchema } from "./constants"
|
||||
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
|
||||
@@ -24,6 +23,9 @@ import {
|
||||
useShippingOptions,
|
||||
} from "../../../../../hooks/api"
|
||||
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 = {
|
||||
order: AdminOrder
|
||||
@@ -45,6 +47,16 @@ export function OrderCreateFulfillmentForm({
|
||||
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(
|
||||
() =>
|
||||
new Map((reservations || []).map((r) => [r.line_item_id, r.quantity])),
|
||||
@@ -78,8 +90,6 @@ export function OrderCreateFulfillmentForm({
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const { stock_locations = [] } = useStockLocations()
|
||||
|
||||
const { shipping_options = [], isLoading: isShippingOptionsLoading } =
|
||||
useShippingOptions({
|
||||
stock_location_id: selectedLocationId,
|
||||
@@ -155,7 +165,7 @@ export function OrderCreateFulfillmentForm({
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (stock_locations?.length && shipping_options?.length) {
|
||||
if (shipping_options?.length) {
|
||||
const initialShippingOptionId =
|
||||
order.shipping_methods?.[0]?.shipping_option_id
|
||||
|
||||
@@ -176,7 +186,7 @@ export function OrderCreateFulfillmentForm({
|
||||
} // else -> TODO: what if original shipping option is deleted?
|
||||
}
|
||||
}
|
||||
}, [stock_locations?.length, shipping_options?.length])
|
||||
}, [shipping_options])
|
||||
|
||||
const fulfilledQuantityArray = (order.items || []).map(
|
||||
(item) =>
|
||||
@@ -234,7 +244,7 @@ export function OrderCreateFulfillmentForm({
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="location_id"
|
||||
render={({ field: { onChange, ref, ...field } }) => {
|
||||
render={({ field: { ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex flex-col gap-2 xl:flex-row xl:items-center">
|
||||
@@ -246,21 +256,15 @@ export function OrderCreateFulfillmentForm({
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Form.Control>
|
||||
<Select onValueChange={onChange} {...field}>
|
||||
<Select.Trigger
|
||||
className="bg-ui-bg-base"
|
||||
ref={ref}
|
||||
>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{stock_locations.map((l) => (
|
||||
<Select.Item key={l.id} value={l.id}>
|
||||
{l.name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
<Combobox
|
||||
{...field}
|
||||
options={stockLocations.options}
|
||||
searchValue={stockLocations.searchValue}
|
||||
onSearchValueChange={
|
||||
stockLocations.onSearchValueChange
|
||||
}
|
||||
disabled={stockLocations.disabled}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user