feat(dashboard): inventory create flow (#7650)
This commit is contained in:
@@ -5,7 +5,12 @@ import { DataGridCellContainer } from "./data-grid-cell-container"
|
||||
export const DataGridNumberCell = <TData, TValue = any>({
|
||||
field,
|
||||
context,
|
||||
}: DataGridCellProps<TData, TValue>) => {
|
||||
...rest
|
||||
}: DataGridCellProps<TData, TValue> & {
|
||||
min?: number
|
||||
max?: number
|
||||
placeholder?: string
|
||||
}) => {
|
||||
const { register, attributes, container } = useDataGridCell({
|
||||
field,
|
||||
context,
|
||||
@@ -18,7 +23,19 @@ export const DataGridNumberCell = <TData, TValue = any>({
|
||||
type="number"
|
||||
{...register(field, {
|
||||
valueAsNumber: true,
|
||||
onChange: (e) => {
|
||||
if (e.target.value) {
|
||||
const parsedValue = Number(e.target.value)
|
||||
if (Number.isNaN(parsedValue)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return parsedValue
|
||||
}
|
||||
},
|
||||
})}
|
||||
className="h-full w-full bg-transparent p-2 text-right [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
{...rest}
|
||||
/>
|
||||
</DataGridCellContainer>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AdminInventoryItemResponse, InventoryNext } from "@medusajs/types"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
@@ -6,19 +6,9 @@ import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import {
|
||||
InventoryItemLocationBatch,
|
||||
UpdateInventoryItemReq,
|
||||
UpdateInventoryLevelReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
InventoryItemDeleteRes,
|
||||
InventoryItemListRes,
|
||||
InventoryItemLocationLevelsRes,
|
||||
InventoryItemRes,
|
||||
} from "../../types/api-responses"
|
||||
import { FetchError } from "@medusajs/js-sdk"
|
||||
|
||||
import { client } from "../../lib/client"
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
@@ -36,16 +26,16 @@ export const useInventoryItems = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
InventoryItemListRes,
|
||||
Error,
|
||||
InventoryItemListRes,
|
||||
HttpTypes.AdminInventoryItemListResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminInventoryItemListResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.inventoryItems.list(query),
|
||||
queryFn: () => sdk.admin.inventoryItem.list(query),
|
||||
queryKey: inventoryItemsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
@@ -57,12 +47,17 @@ export const useInventoryItem = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<InventoryItemRes, Error, InventoryItemRes, QueryKey>,
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminInventoryItemResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminInventoryItemResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.inventoryItems.retrieve(id, query),
|
||||
queryFn: () => sdk.admin.inventoryItem.retrieve(id, query),
|
||||
queryKey: inventoryItemsQueryKeys.detail(id),
|
||||
...options,
|
||||
})
|
||||
@@ -70,13 +65,37 @@ export const useInventoryItem = (
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useUpdateInventoryItem = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<InventoryItemRes, Error, UpdateInventoryItemReq>
|
||||
export const useCreateInventoryItem = (
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminInventoryItemResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminCreateInventoryItem
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: InventoryNext.UpdateInventoryItemInput) =>
|
||||
client.inventoryItems.update(id, payload),
|
||||
mutationFn: (payload: HttpTypes.AdminCreateInventoryItem) =>
|
||||
sdk.admin.inventoryItem.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryItemsQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateInventoryItem = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminInventoryItemResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminUpdateInventoryItem
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminUpdateInventoryItem) =>
|
||||
sdk.admin.inventoryItem.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryItemsQueryKeys.lists(),
|
||||
@@ -92,10 +111,14 @@ export const useUpdateInventoryItem = (
|
||||
|
||||
export const useDeleteInventoryItem = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<InventoryItemDeleteRes, Error, void>
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminInventoryItemDeleteResponse,
|
||||
FetchError,
|
||||
void
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.inventoryItems.delete(id),
|
||||
mutationFn: () => sdk.admin.inventoryItem.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryItemsQueryKeys.lists(),
|
||||
@@ -112,14 +135,15 @@ export const useDeleteInventoryItem = (
|
||||
export const useDeleteInventoryItemLevel = (
|
||||
inventoryItemId: string,
|
||||
locationId: string,
|
||||
options?: UseMutationOptions<InventoryItemDeleteRes, Error, void>
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminInventoryItemDeleteResponse,
|
||||
FetchError,
|
||||
void
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () =>
|
||||
client.inventoryItems.deleteInventoryItemLevel(
|
||||
inventoryItemId,
|
||||
locationId
|
||||
),
|
||||
sdk.admin.inventoryItem.deleteLevel(inventoryItemId, locationId),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryItemsQueryKeys.lists(),
|
||||
@@ -141,17 +165,16 @@ export const useInventoryItemLevels = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
InventoryItemLocationLevelsRes,
|
||||
Error,
|
||||
InventoryItemLocationLevelsRes,
|
||||
HttpTypes.AdminInventoryLevelListResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminInventoryLevelListResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () =>
|
||||
client.inventoryItems.listLocationLevels(inventoryItemId, query),
|
||||
queryFn: () => sdk.admin.inventoryItem.listLevels(inventoryItemId, query),
|
||||
queryKey: inventoryItemLevelsQueryKeys.detail(inventoryItemId),
|
||||
...options,
|
||||
})
|
||||
@@ -159,22 +182,18 @@ export const useInventoryItemLevels = (
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useUpdateInventoryItemLevel = (
|
||||
export const useUpdateInventoryLevel = (
|
||||
inventoryItemId: string,
|
||||
locationId: string,
|
||||
options?: UseMutationOptions<
|
||||
AdminInventoryItemResponse,
|
||||
Error,
|
||||
UpdateInventoryLevelReq
|
||||
HttpTypes.AdminInventoryItemResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminUpdateInventoryLevel
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: UpdateInventoryLevelReq) =>
|
||||
client.inventoryItems.updateInventoryLevel(
|
||||
inventoryItemId,
|
||||
locationId,
|
||||
payload
|
||||
),
|
||||
mutationFn: (payload: HttpTypes.AdminUpdateInventoryLevel) =>
|
||||
sdk.admin.inventoryItem.updateLevel(inventoryItemId, locationId, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryItemsQueryKeys.lists(),
|
||||
@@ -191,17 +210,17 @@ export const useUpdateInventoryItemLevel = (
|
||||
})
|
||||
}
|
||||
|
||||
export const useBatchInventoryItemLevels = (
|
||||
export const useBatchUpdateInventoryLevels = (
|
||||
inventoryItemId: string,
|
||||
options?: UseMutationOptions<
|
||||
InventoryItemLocationLevelsRes,
|
||||
Error,
|
||||
InventoryItemLocationBatch
|
||||
HttpTypes.AdminInventoryItemResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminBatchUpdateInventoryLevelLocation
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: InventoryItemLocationBatch) =>
|
||||
client.inventoryItems.batchPostLocationLevels(inventoryItemId, payload),
|
||||
mutationFn: (payload: HttpTypes.AdminBatchUpdateInventoryLevelLocation) =>
|
||||
sdk.admin.inventoryItem.batchUpdateLevels(inventoryItemId, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryItemsQueryKeys.lists(),
|
||||
|
||||
@@ -403,6 +403,15 @@
|
||||
"associatedVariants": "Associated variants",
|
||||
"manageLocations": "Manage locations",
|
||||
"deleteWarning": "You are about to delete an inventory item. This action cannot be undone.",
|
||||
"create": {
|
||||
"title": "Add item",
|
||||
"details": "Details",
|
||||
"availability": "Availability",
|
||||
"locations": "Locations",
|
||||
"attributes": "Attributes",
|
||||
"requiresShipping": "Requires shipping",
|
||||
"requiresShippingHint": "Does the inventory item require shipping?"
|
||||
},
|
||||
"reservation": {
|
||||
"header": "Reservation of {{itemName}}",
|
||||
"editItemDetails": "Edit item details",
|
||||
|
||||
@@ -6,7 +6,6 @@ import { currencies } from "./currencies"
|
||||
import { customerGroups } from "./customer-groups"
|
||||
import { fulfillmentProviders } from "./fulfillment-providers"
|
||||
import { fulfillments } from "./fulfillments"
|
||||
import { inventoryItems } from "./inventory"
|
||||
import { invites } from "./invites"
|
||||
import { payments } from "./payments"
|
||||
import { priceLists } from "./price-lists"
|
||||
@@ -40,7 +39,6 @@ export const client = {
|
||||
productTags: tags,
|
||||
users: users,
|
||||
invites: invites,
|
||||
inventoryItems: inventoryItems,
|
||||
reservations: reservations,
|
||||
fulfillments: fulfillments,
|
||||
fulfillmentProviders: fulfillmentProviders,
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import {
|
||||
AdminInventoryItemListResponse,
|
||||
AdminInventoryItemResponse,
|
||||
AdminInventoryLevelResponse,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
CreateInventoryItemReq,
|
||||
InventoryItemLocationBatch,
|
||||
UpdateInventoryItemReq,
|
||||
UpdateInventoryLevelReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
InventoryItemLevelDeleteRes,
|
||||
InventoryItemLocationLevelsRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
async function retrieveInventoryItem(id: string, query?: Record<string, any>) {
|
||||
return getRequest<AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items/${id}`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
async function listInventoryItems(query?: Record<string, any>) {
|
||||
return getRequest<AdminInventoryItemListResponse>(
|
||||
`/admin/inventory-items`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
async function createInventoryItem(payload: CreateInventoryItemReq) {
|
||||
return postRequest<AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function updateInventoryItem(
|
||||
id: string,
|
||||
payload: UpdateInventoryItemReq
|
||||
) {
|
||||
return postRequest<AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items/${id}`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function deleteInventoryItem(id: string) {
|
||||
return deleteRequest<AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
async function listInventoryItemLevels(
|
||||
id: string,
|
||||
query?: Record<string, any>
|
||||
) {
|
||||
return getRequest<InventoryItemLocationLevelsRes>(
|
||||
`/admin/inventory-items/${id}/location-levels`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
async function deleteInventoryItemLevel(
|
||||
inventoryItemId: string,
|
||||
locationId: string
|
||||
) {
|
||||
return deleteRequest<InventoryItemLevelDeleteRes>(
|
||||
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`
|
||||
)
|
||||
}
|
||||
|
||||
async function updateInventoryLevel(
|
||||
inventoryItemId: string,
|
||||
locationId: string,
|
||||
payload: UpdateInventoryLevelReq
|
||||
) {
|
||||
return postRequest<AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function batchPostLocationLevels(
|
||||
inventoryItemId: string,
|
||||
payload: InventoryItemLocationBatch
|
||||
) {
|
||||
return postRequest<AdminInventoryLevelResponse>(
|
||||
`/admin/inventory-items/${inventoryItemId}/location-levels/batch`,
|
||||
{
|
||||
create: payload.creates,
|
||||
delete: payload.deletes,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const inventoryItems = {
|
||||
retrieve: retrieveInventoryItem,
|
||||
list: listInventoryItems,
|
||||
create: createInventoryItem,
|
||||
update: updateInventoryItem,
|
||||
delete: deleteInventoryItem,
|
||||
listLocationLevels: listInventoryItemLevels,
|
||||
updateInventoryLevel,
|
||||
deleteInventoryItemLevel,
|
||||
batchPostLocationLevels,
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { ProtectedRoute } from "../../components/authentication/protected-route"
|
||||
import { MainLayout } from "../../components/layout/main-layout"
|
||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||
import { ErrorBoundary } from "../../components/utilities/error-boundary"
|
||||
import { InventoryItemRes, PriceListRes } from "../../types/api-responses"
|
||||
import { PriceListRes } from "../../types/api-responses"
|
||||
|
||||
import { RouteExtensions } from "./route-extensions"
|
||||
import { SettingsExtensions } from "./settings-extensions"
|
||||
@@ -467,12 +467,19 @@ export const RouteMap: RouteObject[] = [
|
||||
{
|
||||
path: "",
|
||||
lazy: () => import("../../routes/inventory/inventory-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import("../../routes/inventory/inventory-create"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/inventory/inventory-detail"),
|
||||
handle: {
|
||||
crumb: (data: InventoryItemRes) =>
|
||||
crumb: (data: HttpTypes.AdminInventoryItemResponse) =>
|
||||
data.inventory_item.title ?? data.inventory_item.sku,
|
||||
},
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { StockLocationDTO } from "@medusajs/types"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root"
|
||||
import { createDataGridHelper } from "../../../../../components/data-grid/utils"
|
||||
import { DataGridReadOnlyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
|
||||
import { DataGridNumberCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-number-cell"
|
||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<{}>
|
||||
}
|
||||
|
||||
export const CreateInventoryAvailabilityForm = ({ form }: Props) => {
|
||||
const { isPending, stock_locations = [] } = useStockLocations({ limit: 999 })
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col divide-y overflow-hidden">
|
||||
{isPending ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<DataGridRoot columns={columns} data={stock_locations} state={form} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createDataGridHelper<StockLocationDTO>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.column({
|
||||
id: "location",
|
||||
header: () => (
|
||||
<div className="flex size-full items-center overflow-hidden">
|
||||
<span className="truncate">{t("locations.domain")}</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<DataGridReadOnlyCell>{row.original.name}</DataGridReadOnlyCell>
|
||||
)
|
||||
},
|
||||
disableHidding: true,
|
||||
}),
|
||||
columnHelper.column({
|
||||
id: "in-stock",
|
||||
name: t("fields.inStock"),
|
||||
header: t("fields.inStock"),
|
||||
cell: (context) => {
|
||||
return (
|
||||
<DataGridNumberCell
|
||||
min={0}
|
||||
placeholder="0"
|
||||
context={context}
|
||||
field={`locations.${context.row.original.id}`}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import React, { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as zod from "zod"
|
||||
|
||||
import {
|
||||
Button,
|
||||
Heading,
|
||||
ProgressStatus,
|
||||
ProgressTabs,
|
||||
clx,
|
||||
Input,
|
||||
Textarea,
|
||||
Switch,
|
||||
toast,
|
||||
} from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { CreateInventoryAvailabilityForm } from "./create-inventory-availability-form"
|
||||
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
inventoryItemsQueryKeys,
|
||||
useCreateInventoryItem,
|
||||
} from "../../../../../hooks/api/inventory"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { optionalInt } from "../../../../../lib/validation"
|
||||
import { queryClient } from "../../../../../lib/query-client"
|
||||
|
||||
enum Tab {
|
||||
DETAILS = "details",
|
||||
AVAILABILITY = "availability",
|
||||
}
|
||||
|
||||
type StepStatus = {
|
||||
[key in Tab]: ProgressStatus
|
||||
}
|
||||
|
||||
const CreateInventoryItemSchema = zod.object({
|
||||
title: zod.string().min(1),
|
||||
|
||||
sku: zod.string().optional(),
|
||||
hs_code: zod.string().optional(),
|
||||
weight: optionalInt,
|
||||
length: optionalInt,
|
||||
height: optionalInt,
|
||||
width: optionalInt,
|
||||
origin_country: zod.string().optional(),
|
||||
mid_code: zod.string().optional(),
|
||||
material: zod.string().optional(),
|
||||
description: zod.string().optional(),
|
||||
requires_shipping: zod.boolean().optional(),
|
||||
thumbnail: zod.string().optional(),
|
||||
locations: zod.record(zod.string(), zod.number().optional()).optional(),
|
||||
// metadata: zod.record(zod.string(), zod.unknown()).optional(),
|
||||
})
|
||||
|
||||
type CreateInventoryItemFormProps = {}
|
||||
|
||||
export function CreateInventoryItemForm({}: CreateInventoryItemFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const [tab, setTab] = React.useState<Tab>(Tab.DETAILS)
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateInventoryItemSchema>>({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
sku: "",
|
||||
hs_code: "",
|
||||
weight: "",
|
||||
length: "",
|
||||
height: "",
|
||||
width: "",
|
||||
origin_country: "",
|
||||
mid_code: "",
|
||||
material: "",
|
||||
description: "",
|
||||
requires_shipping: true,
|
||||
thumbnail: "",
|
||||
},
|
||||
resolver: zodResolver(CreateInventoryItemSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync: createInventoryItem, isPending: isLoading } =
|
||||
useCreateInventoryItem()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
let { locations, ...payload } = data
|
||||
|
||||
for (const k in payload) {
|
||||
if (payload[k] === "") {
|
||||
delete payload[k]
|
||||
continue
|
||||
}
|
||||
|
||||
if (["weight", "length", "height", "width"].includes(k)) {
|
||||
payload[k] = parseInt(payload[k])
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { inventory_item } = await createInventoryItem(payload)
|
||||
|
||||
try {
|
||||
await sdk.admin.inventoryItem.batchUpdateLevels(inventory_item.id, {
|
||||
create: Object.entries(locations)
|
||||
.filter(([_, quantiy]) => !!quantiy)
|
||||
.map(([location_id, stocked_quantity]) => ({
|
||||
location_id,
|
||||
stocked_quantity,
|
||||
})),
|
||||
})
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: inventoryItemsQueryKeys.lists(),
|
||||
})
|
||||
} catch (e) {
|
||||
toast.error(t("general.error"), {
|
||||
description: e.message,
|
||||
dismissLabel: t("actions.close"),
|
||||
})
|
||||
}
|
||||
|
||||
handleSuccess()
|
||||
} catch (e) {
|
||||
toast.error(t("general.error"), {
|
||||
description: e.message,
|
||||
dismissLabel: t("actions.close"),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const [status, setStatus] = React.useState<StepStatus>({
|
||||
[Tab.AVAILABILITY]: "not-started",
|
||||
[Tab.DETAILS]: "not-started",
|
||||
})
|
||||
|
||||
const onTabChange = React.useCallback(async (value: Tab) => {
|
||||
const result = await form.trigger()
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
setTab(value)
|
||||
}, [])
|
||||
|
||||
const onNext = React.useCallback(async () => {
|
||||
const result = await form.trigger()
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (tab) {
|
||||
case Tab.DETAILS: {
|
||||
setTab(Tab.AVAILABILITY)
|
||||
break
|
||||
}
|
||||
case Tab.AVAILABILITY:
|
||||
break
|
||||
}
|
||||
}, [tab])
|
||||
|
||||
useEffect(() => {
|
||||
if (form.formState.isDirty) {
|
||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
||||
} else {
|
||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "not-started" }))
|
||||
}
|
||||
}, [form.formState.isDirty])
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === Tab.DETAILS && form.formState.isDirty) {
|
||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
||||
}
|
||||
|
||||
if (tab === Tab.AVAILABILITY) {
|
||||
setStatus((prev) => ({
|
||||
...prev,
|
||||
[Tab.DETAILS]: "completed",
|
||||
[Tab.AVAILABILITY]: "in-progress",
|
||||
}))
|
||||
}
|
||||
}, [tab])
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<ProgressTabs
|
||||
value={tab}
|
||||
className="h-full"
|
||||
onValueChange={(tab) => onTabChange(tab as Tab)}
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<ProgressTabs.List className="border-ui-border-base -my-2 ml-2 min-w-0 flex-1 border-l">
|
||||
<ProgressTabs.Trigger
|
||||
value={Tab.DETAILS}
|
||||
status={status[Tab.DETAILS]}
|
||||
className="w-full max-w-[200px]"
|
||||
>
|
||||
<span className="w-full cursor-auto overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{t("inventory.create.details")}
|
||||
</span>
|
||||
</ProgressTabs.Trigger>
|
||||
<ProgressTabs.Trigger
|
||||
value={Tab.AVAILABILITY}
|
||||
className="w-full max-w-[200px]"
|
||||
status={status[Tab.AVAILABILITY]}
|
||||
>
|
||||
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{t("inventory.create.availability")}
|
||||
</span>
|
||||
</ProgressTabs.Trigger>
|
||||
</ProgressTabs.List>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
size="small"
|
||||
className="whitespace-nowrap"
|
||||
isLoading={isLoading}
|
||||
onClick={tab !== Tab.AVAILABILITY ? onNext : undefined}
|
||||
key={tab === Tab.AVAILABILITY ? "details" : "pricing"}
|
||||
type={tab === Tab.AVAILABILITY ? "submit" : "button"}
|
||||
>
|
||||
{tab === Tab.AVAILABILITY
|
||||
? t("actions.save")
|
||||
: t("general.next")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
|
||||
<RouteFocusModal.Body
|
||||
className={clx(
|
||||
"flex h-full w-fit flex-col items-center divide-y overflow-hidden",
|
||||
{ "mx-auto": tab === Tab.DETAILS }
|
||||
)}
|
||||
>
|
||||
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
|
||||
<div className="container mx-auto w-[720px] px-1 py-8">
|
||||
<Heading level="h2" className="mb-12 mt-8 text-2xl">
|
||||
{t("inventory.create.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="grid grid-cols-1 gap-x-3 gap-y-6 lg:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("fields.title")}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="sku"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.sku")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} placeholder="sku-123" />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.description.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder="The item description"
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="requires_shipping"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>
|
||||
{t("inventory.create.requiresShipping")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange(!!checked)
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>
|
||||
{t("inventory.create.requiresShippingHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*<Form.Field*/}
|
||||
{/* className="col-span-1"*/}
|
||||
{/* control={form.control}*/}
|
||||
{/* name="location_ids"*/}
|
||||
{/* render={({ field }) => {*/}
|
||||
{/* return (*/}
|
||||
{/* <Form.Item>*/}
|
||||
{/* <Form.Label optional>*/}
|
||||
{/* {t("inventory.create.locations")}*/}
|
||||
{/* </Form.Label>*/}
|
||||
{/* <Form.Control>*/}
|
||||
{/* <Combobox*/}
|
||||
{/* {...field}*/}
|
||||
{/* multiple*/}
|
||||
{/* options={locations.options}*/}
|
||||
{/* searchValue={locations.searchValue}*/}
|
||||
{/* onSearchValueChange={*/}
|
||||
{/* locations.onSearchValueChange*/}
|
||||
{/* }*/}
|
||||
{/* fetchNextPage={locations.fetchNextPage}*/}
|
||||
{/* />*/}
|
||||
{/* </Form.Control>*/}
|
||||
{/* <Form.ErrorMessage />*/}
|
||||
{/* </Form.Item>*/}
|
||||
{/* )*/}
|
||||
{/* }}*/}
|
||||
{/*/>*/}
|
||||
</div>
|
||||
|
||||
<Heading level="h3" className="my-6">
|
||||
{t("inventory.create.attributes")}
|
||||
</Heading>
|
||||
|
||||
<div className="grid grid-cols-1 gap-x-4 gap-y-8 lg:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="width"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.width.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="100"
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="length"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.length.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="100"
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="height"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.height.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="100"
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="weight"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.weight.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="100"
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="mid_code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.mid_code.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="hs_code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.hs_code.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="origin_country"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.countryOrigin.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<CountrySelect {...field} />
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="material"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.material.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProgressTabs.Content>
|
||||
|
||||
<ProgressTabs.Content
|
||||
value={Tab.AVAILABILITY}
|
||||
className="h-full w-full"
|
||||
style={{ width: "100vw" }}
|
||||
>
|
||||
<CreateInventoryAvailabilityForm form={form} />
|
||||
</ProgressTabs.Content>
|
||||
</RouteFocusModal.Body>
|
||||
</ProgressTabs>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./create-inventory-item-form.tsx"
|
||||
@@ -0,0 +1 @@
|
||||
export { InventoryCreate as Component } from "./inventory-create"
|
||||
@@ -0,0 +1,10 @@
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { CreateInventoryItemForm } from "./components/create-inventory-item-form"
|
||||
|
||||
export function InventoryCreate() {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateInventoryItemForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
import * as zod from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
|
||||
import { Button, Input, Text, toast } from "@medusajs/ui"
|
||||
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/types"
|
||||
import { InventoryLevelDTO, StockLocationDTO, HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../../components/route-modal"
|
||||
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { InventoryItemRes } from "../../../../../../types/api-responses"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useUpdateInventoryItemLevel } from "../../../../../../hooks/api/inventory"
|
||||
import { z } from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useUpdateInventoryLevel } from "../../../../../../hooks/api/inventory"
|
||||
|
||||
type AdjustInventoryFormProps = {
|
||||
item: InventoryItemRes["inventory_item"]
|
||||
item: HttpTypes.AdminInventoryItem
|
||||
level: InventoryLevelDTO
|
||||
location: StockLocationDTO
|
||||
}
|
||||
@@ -48,8 +46,8 @@ export const AdjustInventoryForm = ({
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const AdjustInventorySchema = z.object({
|
||||
stocked_quantity: z.number().min(level.reserved_quantity),
|
||||
const AdjustInventorySchema = zod.object({
|
||||
stocked_quantity: zod.number().min(level.reserved_quantity),
|
||||
})
|
||||
|
||||
const form = useForm<zod.infer<typeof AdjustInventorySchema>>({
|
||||
@@ -61,7 +59,7 @@ export const AdjustInventoryForm = ({
|
||||
|
||||
const stockedQuantityUpdate = form.watch("stocked_quantity")
|
||||
|
||||
const { mutateAsync, isPending: isLoading } = useUpdateInventoryItemLevel(
|
||||
const { mutateAsync, isPending: isLoading } = useUpdateInventoryLevel(
|
||||
item.id,
|
||||
level.location_id
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../../components/route-modal"
|
||||
import { useBatchInventoryItemLevels } from "../../../../../../hooks/api/inventory"
|
||||
import { useBatchUpdateInventoryLevels } from "../../../../../../hooks/api/inventory"
|
||||
import { useFieldArray, useForm } from "react-hook-form"
|
||||
|
||||
import { InventoryItemRes } from "../../../../../../types/api-responses"
|
||||
@@ -64,7 +64,7 @@ export const ManageLocationsForm = ({
|
||||
name: "locations",
|
||||
})
|
||||
|
||||
const { mutateAsync } = useBatchInventoryItemLevels(item.id)
|
||||
const { mutateAsync } = useBatchUpdateInventoryLevels(item.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async ({ locations }) => {
|
||||
// Changes in selected locations
|
||||
@@ -96,10 +96,10 @@ export const ManageLocationsForm = ({
|
||||
|
||||
try {
|
||||
await mutateAsync({
|
||||
creates: selectedLocations.map((location_id) => ({
|
||||
create: selectedLocations.map((location_id) => ({
|
||||
location_id,
|
||||
})),
|
||||
deletes: unselectedLocations,
|
||||
delete: unselectedLocations,
|
||||
})
|
||||
|
||||
handleSuccess()
|
||||
|
||||
@@ -33,7 +33,6 @@ export const InventoryDetail = () => {
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError || !inventory_item) {
|
||||
if (error) {
|
||||
throw error
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
import { inventoryItemsQueryKeys } from "../../../hooks/api/inventory"
|
||||
import { client } from "../../../lib/client"
|
||||
import { sdk } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/query-client"
|
||||
import { InventoryItemRes } from "../../../types/api-responses"
|
||||
|
||||
const inventoryDetailQuery = (id: string) => ({
|
||||
queryKey: inventoryItemsQueryKeys.detail(id),
|
||||
queryFn: async () =>
|
||||
client.inventoryItems.retrieve(id, {
|
||||
sdk.admin.inventoryItem.retrieve(id, {
|
||||
fields: "*variant",
|
||||
}),
|
||||
})
|
||||
@@ -17,7 +18,8 @@ export const inventoryItemLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const query = inventoryDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<InventoryItemRes>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
queryClient.getQueryData<HttpTypes.AdminInventoryItemResponse>(
|
||||
query.queryKey
|
||||
) ?? (await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { InventoryNext } from "@medusajs/types"
|
||||
|
||||
import { DataTable } from "../../../../components/table/data-table"
|
||||
@@ -8,6 +8,7 @@ import { useInventoryTableColumns } from "./use-inventory-table-columns"
|
||||
import { useInventoryTableFilters } from "./use-inventory-table-filters"
|
||||
import { useInventoryTableQuery } from "./use-inventory-table-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
@@ -48,6 +49,9 @@ export const InventoryListTable = () => {
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{t("inventory.domain")}</Heading>
|
||||
<Button size="small" variant="secondary" asChild>
|
||||
<Link to="create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
|
||||
@@ -110,27 +110,9 @@ export type BatchUpdatePromotionRulesReq = { rules: UpdatePromotionRuleDTO[] }
|
||||
export type CreateCampaignReq = CreateCampaignDTO
|
||||
export type UpdateCampaignReq = UpdateCampaignDTO
|
||||
|
||||
// Inventory Items
|
||||
export type CreateInventoryItemReq = InventoryNext.CreateInventoryItemInput
|
||||
export type UpdateInventoryItemReq = Omit<
|
||||
InventoryNext.UpdateInventoryItemInput,
|
||||
"id"
|
||||
>
|
||||
|
||||
// Reservations
|
||||
export type UpdateReservationReq = Omit<
|
||||
InventoryNext.UpdateReservationItemInput,
|
||||
"id"
|
||||
>
|
||||
export type CreateReservationReq = InventoryNext.CreateReservationItemInput
|
||||
|
||||
// Inventory Item Levels
|
||||
export type InventoryItemLocationBatch = {
|
||||
creates: { location_id: string; stocked_quantity?: number }[]
|
||||
deletes: string[]
|
||||
}
|
||||
|
||||
export type UpdateInventoryLevelReq = {
|
||||
reserved_quantity?: number
|
||||
stocked_quantity?: number
|
||||
}
|
||||
|
||||
@@ -176,27 +176,6 @@ export type WorkflowExecutionListRes = {
|
||||
export type TaxRegionDeleteRes = DeleteRes
|
||||
export type TaxRateDeleteRes = DeleteRes
|
||||
|
||||
// Inventory Items
|
||||
export type InventoryItemRes = {
|
||||
inventory_item: InventoryNext.InventoryItemDTO & {
|
||||
stocked_quantity: number
|
||||
reserved_quantity: number
|
||||
location_levels?: InventoryNext.InventoryLevelDTO[]
|
||||
variant?: ProductVariantDTO | ProductVariantDTO[]
|
||||
}
|
||||
}
|
||||
|
||||
export type InventoryItemListRes = {
|
||||
inventory_items: InventoryNext.InventoryItemDTO[]
|
||||
} & ListRes
|
||||
export type InventoryItemDeleteRes = DeleteRes
|
||||
|
||||
export type InventoryItemLocationLevelsRes = {
|
||||
inventory_levels: InventoryNext.InventoryLevelDTO[]
|
||||
} & ListRes
|
||||
|
||||
export type InventoryItemLevelDeleteRes = DeleteRes
|
||||
|
||||
// Reservations
|
||||
export type ReservationItemDeleteRes = DeleteRes
|
||||
|
||||
|
||||
@@ -39,14 +39,9 @@ export const deleteInventoryLevelsFromItemAndLocationsStep = createStep(
|
||||
}
|
||||
|
||||
const deletedIds = items.map((i) => i.id)
|
||||
const deleted = await service.softDeleteInventoryLevels(deletedIds)
|
||||
await service.softDeleteInventoryLevels(deletedIds)
|
||||
|
||||
return new StepResponse(
|
||||
{
|
||||
[Modules.INVENTORY]: deleted,
|
||||
} as DeleteEntityInput,
|
||||
deletedIds
|
||||
)
|
||||
return new StepResponse(void 0, deletedIds)
|
||||
},
|
||||
async (prevLevelIds, { container }) => {
|
||||
if (!prevLevelIds?.length) {
|
||||
|
||||
@@ -5,8 +5,6 @@ import {
|
||||
deleteInventoryLevelsFromItemAndLocationsStep,
|
||||
} from "../steps"
|
||||
|
||||
import { removeRemoteLinkStep } from "../../common"
|
||||
|
||||
interface WorkflowInput {
|
||||
creates: InventoryNext.CreateInventoryLevelInput[]
|
||||
deletes: { inventory_item_id: string; location_id: string }[]
|
||||
@@ -17,9 +15,7 @@ export const bulkCreateDeleteLevelsWorkflowId =
|
||||
export const bulkCreateDeleteLevelsWorkflow = createWorkflow(
|
||||
bulkCreateDeleteLevelsWorkflowId,
|
||||
(input: WorkflowData<WorkflowInput>): WorkflowData<InventoryLevelDTO[]> => {
|
||||
const deleted = deleteInventoryLevelsFromItemAndLocationsStep(input.deletes)
|
||||
|
||||
removeRemoteLinkStep(deleted)
|
||||
deleteInventoryLevelsFromItemAndLocationsStep(input.deletes)
|
||||
|
||||
return createInventoryLevelsStep(input.creates)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { StockLocation } from "./stock-location"
|
||||
import { TaxRate } from "./tax-rate"
|
||||
import { TaxRegion } from "./tax-region"
|
||||
import { Upload } from "./upload"
|
||||
import { InventoryItem } from "./inventory-item"
|
||||
|
||||
export class Admin {
|
||||
public invite: Invite
|
||||
@@ -28,6 +29,7 @@ export class Admin {
|
||||
public fulfillment: Fulfillment
|
||||
public shippingOption: ShippingOption
|
||||
public shippingProfile: ShippingProfile
|
||||
public inventoryItem: InventoryItem
|
||||
public order: Order
|
||||
public taxRate: TaxRate
|
||||
public taxRegion: TaxRegion
|
||||
@@ -45,6 +47,7 @@ export class Admin {
|
||||
this.fulfillment = new Fulfillment(client)
|
||||
this.shippingOption = new ShippingOption(client)
|
||||
this.shippingProfile = new ShippingProfile(client)
|
||||
this.inventoryItem = new InventoryItem(client)
|
||||
this.order = new Order(client)
|
||||
this.taxRate = new TaxRate(client)
|
||||
this.taxRegion = new TaxRegion(client)
|
||||
|
||||
135
packages/core/js-sdk/src/admin/inventory-item.ts
Normal file
135
packages/core/js-sdk/src/admin/inventory-item.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { HttpTypes, SelectParams } from "@medusajs/types"
|
||||
import { Client } from "../client"
|
||||
import { ClientHeaders } from "../types"
|
||||
|
||||
export class InventoryItem {
|
||||
private client: Client
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async create(
|
||||
body: HttpTypes.AdminCreateInventoryItem,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
body: HttpTypes.AdminUpdateInventoryItem,
|
||||
query?: SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items/${id}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async list(
|
||||
query?: HttpTypes.AdminInventoryItemParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminInventoryItemListResponse>(
|
||||
`/admin/inventory-items`,
|
||||
{
|
||||
query,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async retrieve(id: string, query?: SelectParams, headers?: ClientHeaders) {
|
||||
return await this.client.fetch<HttpTypes.AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items/${id}`,
|
||||
{
|
||||
query,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async delete(id: string, headers?: ClientHeaders) {
|
||||
return await this.client.fetch<HttpTypes.AdminInventoryItemDeleteResponse>(
|
||||
`/admin/inventory-items/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async listLevels(
|
||||
id: string,
|
||||
query?: HttpTypes.AdminInventoryLevelFilters,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminInventoryLevelListResponse>(
|
||||
`/admin/inventory-items/${id}/location-levels`,
|
||||
{
|
||||
query,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async updateLevel(
|
||||
id: string,
|
||||
locationId: string,
|
||||
body: HttpTypes.AdminUpdateInventoryLevel,
|
||||
query?: SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items/${id}/location-levels/${locationId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async deleteLevel(id: string, locationId: string, headers?: ClientHeaders) {
|
||||
return await this.client.fetch<HttpTypes.AdminInventoryItemDeleteResponse>(
|
||||
`/admin/inventory-items/${id}/location-levels/${locationId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async batchUpdateLevels(
|
||||
id: string,
|
||||
body: HttpTypes.AdminBatchUpdateInventoryLevelLocation,
|
||||
query?: SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminInventoryItemResponse>(
|
||||
`/admin/inventory-items/${id}/location-levels/batch`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export * from "./fulfillment"
|
||||
export * from "./fulfillment-provider"
|
||||
export * from "./fulfillment-set"
|
||||
export * from "./inventory"
|
||||
export * from "./inventory-level"
|
||||
export * from "./invite"
|
||||
export * from "./order"
|
||||
export * from "./payment"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface InventoryLevel {
|
||||
id: string
|
||||
inventory_item_id: string
|
||||
location_id: string
|
||||
stocked_quantity: number
|
||||
reserved_quantity: number
|
||||
available_quantity: number
|
||||
incoming_quantity: number
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./entities"
|
||||
export * from "./payloads"
|
||||
export * from "./queries"
|
||||
export * from "./responses"
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface AdminUpdateInventoryLevel {
|
||||
stocked_quantity?: number
|
||||
incoming_quantity?: number
|
||||
}
|
||||
|
||||
export interface AdminCreateInventoryLevel {
|
||||
location_id: string
|
||||
stocked_quantity?: number
|
||||
incoming_quantity?: number
|
||||
}
|
||||
|
||||
export interface AdminBatchUpdateInventoryLevelLocation {
|
||||
delete?: string[] // a list of location_ids
|
||||
update?: never // TODO - not implemented // AdminUpdateInventoryLevel[]
|
||||
create?: AdminCreateInventoryLevel[]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { FindParams } from "../../common"
|
||||
|
||||
export interface AdminInventoryLevelFilters extends FindParams {
|
||||
location_id?: string | string[]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PaginatedResponse } from "../../common"
|
||||
import { InventoryLevel } from "./entities"
|
||||
|
||||
export interface AdminInventoryLevelResponse {
|
||||
inventory_level: InventoryLevel
|
||||
}
|
||||
|
||||
export type AdminInventoryLevelListResponse = PaginatedResponse<{
|
||||
inventory_levels: InventoryLevel[]
|
||||
}>
|
||||
1
packages/core/types/src/http/inventory-level/index.ts
Normal file
1
packages/core/types/src/http/inventory-level/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./admin"
|
||||
@@ -1,6 +1,4 @@
|
||||
import { PaginatedResponse } from "../common"
|
||||
|
||||
interface InventoryItemResponse {
|
||||
export interface AdminInventoryItem {
|
||||
id: string
|
||||
sku?: string | null
|
||||
origin_country?: string | null
|
||||
@@ -17,11 +15,3 @@ interface InventoryItemResponse {
|
||||
thumbnail?: string | null
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface AdminInventoryItemResponse {
|
||||
inventory_item: InventoryItemResponse
|
||||
}
|
||||
|
||||
export type AdminInventoryItemListResponse = PaginatedResponse<{
|
||||
inventory_items: InventoryItemResponse[]
|
||||
}>
|
||||
4
packages/core/types/src/http/inventory/admin/index.ts
Normal file
4
packages/core/types/src/http/inventory/admin/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./entities"
|
||||
export * from "./payloads"
|
||||
export * from "./queries"
|
||||
export * from "./responses"
|
||||
18
packages/core/types/src/http/inventory/admin/payloads.ts
Normal file
18
packages/core/types/src/http/inventory/admin/payloads.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface AdminCreateInventoryItem {
|
||||
sku?: string
|
||||
hs_code?: string
|
||||
weight?: number
|
||||
length?: number
|
||||
height?: number
|
||||
width?: number
|
||||
origin_country?: string
|
||||
mid_code?: string
|
||||
material?: string
|
||||
title?: string
|
||||
description?: string
|
||||
requires_shipping?: boolean
|
||||
thumbnail?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminUpdateInventoryItem extends AdminCreateInventoryItem {}
|
||||
17
packages/core/types/src/http/inventory/admin/queries.ts
Normal file
17
packages/core/types/src/http/inventory/admin/queries.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { FindParams } from "../../common"
|
||||
import { OperatorMap } from "../../../dal"
|
||||
|
||||
export interface AdminInventoryItemParams extends FindParams {
|
||||
id?: string | string[]
|
||||
q?: string
|
||||
sku?: string | string[]
|
||||
origin_country?: string | string[]
|
||||
hs_code?: string | string[]
|
||||
material?: string | string[]
|
||||
requires_shipping?: boolean
|
||||
weight?: number | OperatorMap<number>
|
||||
length?: number | OperatorMap<number>
|
||||
height?: number | OperatorMap<number>
|
||||
width?: number | OperatorMap<number>
|
||||
location_levels?: Record<"location_id", string | string[]>
|
||||
}
|
||||
12
packages/core/types/src/http/inventory/admin/responses.ts
Normal file
12
packages/core/types/src/http/inventory/admin/responses.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { DeleteResponse, PaginatedResponse } from "../../common"
|
||||
import { AdminInventoryItem } from "./entities"
|
||||
|
||||
export interface AdminInventoryItemResponse {
|
||||
inventory_item: AdminInventoryItem
|
||||
}
|
||||
|
||||
export type AdminInventoryItemListResponse = PaginatedResponse<{
|
||||
inventory_items: AdminInventoryItem[]
|
||||
}>
|
||||
|
||||
export type AdminInventoryItemDeleteResponse = DeleteResponse<"inventory_item">
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./inventory"
|
||||
export * from "./inventory-level"
|
||||
export * from "./admin"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { AdminInventoryItemResponse } from "./inventory"
|
||||
import { PaginatedResponse } from "../common"
|
||||
|
||||
interface InventoryLevelResponse {
|
||||
id: string
|
||||
inventory_item_id: string
|
||||
location_id: string
|
||||
stocked_quantity: number
|
||||
reserved_quantity: number
|
||||
available_quantity: number
|
||||
incoming_quantity: number
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface AdminInventoryLevelResponse {
|
||||
inventory_item: AdminInventoryItemResponse
|
||||
}
|
||||
|
||||
export type AdminInventoryLevelListResponse = PaginatedResponse<{
|
||||
inventory_levels: InventoryLevelResponse[]
|
||||
}>
|
||||
@@ -41,12 +41,14 @@ export const GET = async (
|
||||
req: MedusaRequest<AdminGetInventoryLocationLevelsParamsType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const { id } = req.params
|
||||
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "inventory_levels",
|
||||
variables: {
|
||||
filters: req.filterableFields,
|
||||
filters: { ...req.filterableFields, inventory_item_id: id },
|
||||
...req.remoteQueryConfig.pagination,
|
||||
},
|
||||
fields: req.remoteQueryConfig.fields,
|
||||
|
||||
Reference in New Issue
Block a user