Feat(admin-next, core-flows, link-modules, medusa, types): Inventory end to end flows (#7020)
* add reservation endpoints * add changeset * initial * add reservations table * add edit-item modal * udpate inventory item attributes * manage locations skeleton * add combi batch endpoint * cleanup * fix manage locations * add adjust inventory * prep for pr * update versions * fix for pr * fix for pr * cleanup * Update packages/core-flows/src/inventory/workflows/bulk-create-delete-levels.ts Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> * Update packages/core-flows/src/inventory/steps/delete-levels-by-item-and-location.ts Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> * rm wack import * fix build --------- Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
1 parent
276278cbe7
commit
ab7ff64c4a
66 files changed
+2892
-41
No files matched your search
@@ -413,7 +413,6 @@ medusaIntegrationTestRunner({
|
|||||||
ContainerRegistrationKeys.REMOTE_LINK
|
ContainerRegistrationKeys.REMOTE_LINK
|
||||||
)
|
)
|
||||||
|
|
||||||
console.warn("testing")
|
|
||||||
await remoteLink.create([
|
await remoteLink.create([
|
||||||
{
|
{
|
||||||
[Modules.SALES_CHANNEL]: {
|
[Modules.SALES_CHANNEL]: {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IInventoryServiceNext, IStockLocationService } from "@medusajs/types"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
remoteQueryObjectFromString,
|
remoteQueryObjectFromString,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
import { IInventoryServiceNext, IStockLocationService } from "@medusajs/types"
|
||||||
|
|
||||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
import { createAdminUser } from "../../../helpers/create-admin-user"
|
import { createAdminUser } from "../../../helpers/create-admin-user"
|
||||||
|
|
||||||
@@ -314,6 +314,49 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Bulk create/delete inventory levels", () => {
|
||||||
|
const locationId = "loc_1"
|
||||||
|
let inventoryItem
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
inventoryItem = await service.create({
|
||||||
|
sku: "MY_SKU",
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.createInventoryLevels([
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete an inventory location level and create a new one", async () => {
|
||||||
|
const result = await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItem.id}/location-levels/batch/combi`,
|
||||||
|
{
|
||||||
|
creates: [
|
||||||
|
{
|
||||||
|
location_id: "location_2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
deletes: [locationId],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.status).toEqual(200)
|
||||||
|
|
||||||
|
const levelsListResult = await api.get(
|
||||||
|
`/admin/inventory-items/${inventoryItem.id}/location-levels`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
expect(levelsListResult.status).toEqual(200)
|
||||||
|
expect(levelsListResult.data.inventory_levels).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Delete inventory levels", () => {
|
describe("Delete inventory levels", () => {
|
||||||
const locationId = "loc_1"
|
const locationId = "loc_1"
|
||||||
let inventoryItem
|
let inventoryItem
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"items_one": "{{count}} item",
|
"items_one": "{{count}} item",
|
||||||
"items_other": "{{count}} items",
|
"items_other": "{{count}} items",
|
||||||
"countSelected": "{{count}} selected",
|
"countSelected": "{{count}} selected",
|
||||||
|
"countOfTotalSelected": "{{count}} of {{total}} selected",
|
||||||
"plusCount": "+ {{count}}",
|
"plusCount": "+ {{count}}",
|
||||||
"plusCountMore": "+ {{count}} more",
|
"plusCountMore": "+ {{count}} more",
|
||||||
"areYouSure": "Are you sure?",
|
"areYouSure": "Are you sure?",
|
||||||
@@ -265,6 +266,7 @@
|
|||||||
},
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"header": "Stock & Inventory",
|
"header": "Stock & Inventory",
|
||||||
|
"editItemDetails": "Edit item details",
|
||||||
"manageInventoryLabel": "Manage inventory",
|
"manageInventoryLabel": "Manage inventory",
|
||||||
"manageInventoryHint": "When enabled the inventory level will be regulated when orders and returns are created.",
|
"manageInventoryHint": "When enabled the inventory level will be regulated when orders and returns are created.",
|
||||||
"allowBackordersLabel": "Allow backorders",
|
"allowBackordersLabel": "Allow backorders",
|
||||||
@@ -311,6 +313,10 @@
|
|||||||
"inventory": {
|
"inventory": {
|
||||||
"domain": "Inventory",
|
"domain": "Inventory",
|
||||||
"reserved": "Reserved",
|
"reserved": "Reserved",
|
||||||
|
"available": "Available",
|
||||||
|
"locationLevels": "Location levels",
|
||||||
|
"associatedVariants": "Associated variants",
|
||||||
|
"manageLocations": "Manage locations",
|
||||||
"deleteWarning": "You are about to delete an inventory item. This action cannot be undone."
|
"deleteWarning": "You are about to delete an inventory item. This action cannot be undone."
|
||||||
},
|
},
|
||||||
"giftCards": {
|
"giftCards": {
|
||||||
@@ -943,6 +949,7 @@
|
|||||||
"addSalesChannels": "Add sales channels",
|
"addSalesChannels": "Add sales channels",
|
||||||
"detailsHint": "Specify the details of the location.",
|
"detailsHint": "Specify the details of the location.",
|
||||||
"noLocationsFound": "No locations found",
|
"noLocationsFound": "No locations found",
|
||||||
|
"selectLocations": "Select locations that stock the item.",
|
||||||
"deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone.",
|
"deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone.",
|
||||||
"removeSalesChannelsWarning_one": "You are about to remove {{count}} sales channel from the location.",
|
"removeSalesChannelsWarning_one": "You are about to remove {{count}} sales channel from the location.",
|
||||||
"removeSalesChannelsWarning_other": "You are about to remove {{count}} sales channels from the location."
|
"removeSalesChannelsWarning_other": "You are about to remove {{count}} sales channels from the location."
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { EllipsisHorizontal } from "@medusajs/icons"
|
|
||||||
import { DropdownMenu, IconButton } from "@medusajs/ui"
|
import { DropdownMenu, IconButton } from "@medusajs/ui"
|
||||||
import { ReactNode } from "react"
|
|
||||||
|
import { EllipsisHorizontal } from "@medusajs/icons"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
|
import { ReactNode } from "react"
|
||||||
|
|
||||||
type Action = {
|
type Action = {
|
||||||
icon: ReactNode
|
icon: ReactNode
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import {
|
||||||
|
InventoryItemDeleteRes,
|
||||||
|
InventoryItemListRes,
|
||||||
|
InventoryItemLocationLevelsRes,
|
||||||
|
InventoryItemRes,
|
||||||
|
ReservationItemDeleteRes,
|
||||||
|
ReservationItemListRes,
|
||||||
|
ReservationItemRes,
|
||||||
|
} from "../../types/api-responses"
|
||||||
|
import {
|
||||||
|
InventoryItemLocationBatch,
|
||||||
|
UpdateInventoryItemReq,
|
||||||
|
UpdateInventoryLevelReq,
|
||||||
|
} from "../../types/api-payloads"
|
||||||
|
import {
|
||||||
|
QueryKey,
|
||||||
|
UseMutationOptions,
|
||||||
|
UseQueryOptions,
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query"
|
||||||
|
|
||||||
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { client } from "../../lib/client"
|
||||||
|
import { queryClient } from "../../lib/medusa"
|
||||||
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
|
||||||
|
const INVENTORY_ITEMS_QUERY_KEY = "inventory_items" as const
|
||||||
|
export const inventoryItemsQueryKeys = queryKeysFactory(
|
||||||
|
INVENTORY_ITEMS_QUERY_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
const INVENTORY_ITEM_LEVELS_QUERY_KEY = "inventory_item_levels" as const
|
||||||
|
export const inventoryItemLevelsQueryKeys = queryKeysFactory(
|
||||||
|
INVENTORY_ITEM_LEVELS_QUERY_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
const RESERVATION_ITEMS_QUERY_KEY = "reservation_items" as const
|
||||||
|
export const reservationItemsQueryKeys = queryKeysFactory(
|
||||||
|
RESERVATION_ITEMS_QUERY_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
export const useInventoryItems = (
|
||||||
|
query?: Record<string, any>,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
InventoryItemListRes,
|
||||||
|
Error,
|
||||||
|
InventoryItemListRes,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () => client.inventoryItems.list(query),
|
||||||
|
queryKey: inventoryItemsQueryKeys.list(query),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInventoryItem = (
|
||||||
|
id: string,
|
||||||
|
query?: Record<string, any>,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<InventoryItemRes, Error, InventoryItemRes, QueryKey>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () => client.inventoryItems.retrieve(id, query),
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(id),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateInventoryItem = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<InventoryItemRes, Error, UpdateInventoryItemReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: InventoryNext.UpdateInventoryItemInput) =>
|
||||||
|
client.inventoryItems.update(id, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteInventoryItem = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<InventoryItemDeleteRes, Error, void>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => client.inventoryItems.delete(id),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteInventoryItemLevel = (
|
||||||
|
inventoryItemId: string,
|
||||||
|
locationId: string,
|
||||||
|
options?: UseMutationOptions<InventoryItemDeleteRes, Error, void>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
client.inventoryItems.deleteInventoryItemLevel(
|
||||||
|
inventoryItemId,
|
||||||
|
locationId
|
||||||
|
),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(inventoryItemId),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemLevelsQueryKeys.detail(inventoryItemId),
|
||||||
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInventoryItemLevels = (
|
||||||
|
inventoryItemId: string,
|
||||||
|
query?: Record<string, any>,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
InventoryItemLocationLevelsRes,
|
||||||
|
Error,
|
||||||
|
InventoryItemLocationLevelsRes,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () =>
|
||||||
|
client.inventoryItems.listLocationLevels(inventoryItemId, query),
|
||||||
|
queryKey: inventoryItemLevelsQueryKeys.detail(inventoryItemId),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateInventoryItemLevel = (
|
||||||
|
inventoryItemId: string,
|
||||||
|
locationId: string,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
AdminInventoryLevelResponse,
|
||||||
|
Error,
|
||||||
|
UpdateInventoryLevelReq
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: UpdateInventoryLevelReq) =>
|
||||||
|
client.inventoryItems.updateInventoryLevel(
|
||||||
|
inventoryItemId,
|
||||||
|
locationId,
|
||||||
|
payload
|
||||||
|
),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(inventoryItemId),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemLevelsQueryKeys.detail(inventoryItemId),
|
||||||
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBatchInventoryItemLevels = (
|
||||||
|
inventoryItemId: string,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
InventoryItemLocationLevelsRes,
|
||||||
|
Error,
|
||||||
|
InventoryItemLocationBatch
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: InventoryItemLocationBatch) =>
|
||||||
|
client.inventoryItems.batchPostLocationLevels(inventoryItemId, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(inventoryItemId),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemLevelsQueryKeys.detail(inventoryItemId),
|
||||||
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReservationItems = (
|
||||||
|
query?: Record<string, any>,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
ReservationItemListRes,
|
||||||
|
Error,
|
||||||
|
ReservationItemListRes,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: () => client.inventoryItems.listReservationItems(query),
|
||||||
|
queryKey: reservationItemsQueryKeys.list(query),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateReservationItem = (
|
||||||
|
id: string,
|
||||||
|
payload: InventoryNext.UpdateInventoryItemInput,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
ReservationItemRes,
|
||||||
|
Error,
|
||||||
|
UpdateInventoryItemReq
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => client.inventoryItems.updateReservationItem(id, payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteReservationItem = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<ReservationItemDeleteRes, Error, void>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => client.inventoryItems.deleteReservationItem(id),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { collections } from "./collections"
|
|||||||
import { currencies } from "./currencies"
|
import { currencies } from "./currencies"
|
||||||
import { customerGroups } from "./customer-groups"
|
import { customerGroups } from "./customer-groups"
|
||||||
import { customers } from "./customers"
|
import { customers } from "./customers"
|
||||||
|
import { inventoryItems } from "./inventory"
|
||||||
import { invites } from "./invites"
|
import { invites } from "./invites"
|
||||||
import { payments } from "./payments"
|
import { payments } from "./payments"
|
||||||
import { priceLists } from "./price-lists"
|
import { priceLists } from "./price-lists"
|
||||||
@@ -39,6 +40,7 @@ export const client = {
|
|||||||
regions: regions,
|
regions: regions,
|
||||||
taxes: taxes,
|
taxes: taxes,
|
||||||
invites: invites,
|
invites: invites,
|
||||||
|
inventoryItems: inventoryItems,
|
||||||
products: products,
|
products: products,
|
||||||
productTypes: productTypes,
|
productTypes: productTypes,
|
||||||
priceLists: priceLists,
|
priceLists: priceLists,
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import {
|
||||||
|
AdminInventoryItemListResponse,
|
||||||
|
AdminInventoryItemResponse,
|
||||||
|
AdminInventoryLevelListResponse,
|
||||||
|
AdminInventoryLevelResponse,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
CreateInventoryItemReq,
|
||||||
|
InventoryItemLocationBatch,
|
||||||
|
UpdateInventoryItemReq,
|
||||||
|
UpdateInventoryLevelReq,
|
||||||
|
} from "../../types/api-payloads"
|
||||||
|
import {
|
||||||
|
InventoryItemLevelDeleteRes,
|
||||||
|
ReservationItemDeleteRes,
|
||||||
|
ReservationItemListRes,
|
||||||
|
ReservationItemRes,
|
||||||
|
} 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<AdminInventoryLevelListResponse>(
|
||||||
|
`/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<AdminInventoryLevelResponse>(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listReservationItems(query?: Record<string, any>) {
|
||||||
|
return getRequest<ReservationItemListRes>(`/admin/reservations`, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteReservationItem(reservationId: string) {
|
||||||
|
return deleteRequest<ReservationItemDeleteRes>(
|
||||||
|
`/admin/reservations/${reservationId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateReservationItem(
|
||||||
|
reservationId: string,
|
||||||
|
payload: UpdateInventoryItemReq
|
||||||
|
) {
|
||||||
|
return postRequest<ReservationItemRes>(
|
||||||
|
`/admin/reservatinos/${reservationId}`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchPostLocationLevels(
|
||||||
|
inventoryItemId: string,
|
||||||
|
payload: InventoryItemLocationBatch
|
||||||
|
) {
|
||||||
|
return postRequest<AdminInventoryLevelResponse>(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels/batch/combi`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const inventoryItems = {
|
||||||
|
retrieve: retrieveInventoryItem,
|
||||||
|
list: listInventoryItems,
|
||||||
|
create: createInventoryItem,
|
||||||
|
update: updateInventoryItem,
|
||||||
|
delete: deleteInventoryItem,
|
||||||
|
listLocationLevels: listInventoryItemLevels,
|
||||||
|
updateInventoryLevel,
|
||||||
|
deleteInventoryItemLevel,
|
||||||
|
listReservationItems,
|
||||||
|
deleteReservationItem,
|
||||||
|
updateReservationItem,
|
||||||
|
batchPostLocationLevels,
|
||||||
|
}
|
||||||
@@ -1,11 +1,3 @@
|
|||||||
import { AdminCustomersRes } from "@medusajs/client-types"
|
|
||||||
import { Spinner } from "@medusajs/icons"
|
|
||||||
import {
|
|
||||||
AdminCollectionsRes,
|
|
||||||
AdminProductsRes,
|
|
||||||
AdminPromotionRes,
|
|
||||||
AdminRegionsRes,
|
|
||||||
} from "@medusajs/medusa"
|
|
||||||
import {
|
import {
|
||||||
AdminApiKeyResponse,
|
AdminApiKeyResponse,
|
||||||
AdminCustomerGroupResponse,
|
AdminCustomerGroupResponse,
|
||||||
@@ -13,14 +5,24 @@ import {
|
|||||||
SalesChannelDTO,
|
SalesChannelDTO,
|
||||||
UserDTO,
|
UserDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
AdminCollectionsRes,
|
||||||
|
AdminProductsRes,
|
||||||
|
AdminPromotionRes,
|
||||||
|
AdminRegionsRes,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
|
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
|
import { AdminCustomersRes } from "@medusajs/client-types"
|
||||||
import { ErrorBoundary } from "../../components/error/error-boundary"
|
import { ErrorBoundary } from "../../components/error/error-boundary"
|
||||||
|
import { InventoryItemRes } from "../../types/api-responses"
|
||||||
import { MainLayout } from "../../components/layout-v2/main-layout"
|
import { MainLayout } from "../../components/layout-v2/main-layout"
|
||||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
|
||||||
import { useMe } from "../../hooks/api/users"
|
|
||||||
import { PriceListRes } from "../../types/api-responses"
|
import { PriceListRes } from "../../types/api-responses"
|
||||||
import { SearchProvider } from "../search-provider"
|
import { SearchProvider } from "../search-provider"
|
||||||
|
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||||
import { SidebarProvider } from "../sidebar-provider"
|
import { SidebarProvider } from "../sidebar-provider"
|
||||||
|
import { Spinner } from "@medusajs/icons"
|
||||||
|
import { useMe } from "../../hooks/api/users"
|
||||||
|
|
||||||
export const ProtectedRoute = () => {
|
export const ProtectedRoute = () => {
|
||||||
const { user, isLoading } = useMe()
|
const { user, isLoading } = useMe()
|
||||||
@@ -393,6 +395,73 @@ export const v2Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/inventory",
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Inventory",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
lazy: () => import("../../v2-routes/inventory/inventory-list"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/inventory/inventory-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: InventoryItemRes) =>
|
||||||
|
data.inventory_item.title ?? data.inventory_item.sku,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
// TODO: edit item
|
||||||
|
path: "edit",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/inventory/inventory-detail/components/edit-inventory-item"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// TODO: edit item attributes
|
||||||
|
path: "attributes",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/inventory/inventory-detail/components/edit-inventory-item-attributes"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// TODO: manage locations
|
||||||
|
path: "locations",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/inventory/inventory-detail/components/manage-locations"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// TODO: adjust item level
|
||||||
|
path: "locations/:location_id",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../v2-routes/inventory/inventory-detail/components/adjust-inventory"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// TODO: create reservation
|
||||||
|
path: "reservations",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/customers/customer-edit"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// TODO: edit reservation
|
||||||
|
path: "reservations/:reservation_id",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../v2-routes/customers/customer-edit"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+4
-5
@@ -1,12 +1,11 @@
|
|||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
import { PencilSquare } from "@medusajs/icons"
|
|
||||||
import type { Discount } from "@medusajs/medusa"
|
|
||||||
import { Container, Copy, Heading, Text } from "@medusajs/ui"
|
import { Container, Copy, Heading, Text } from "@medusajs/ui"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
import type { Discount } from "@medusajs/medusa"
|
||||||
import { ListSummary } from "../../../../../components/common/list-summary"
|
import { ListSummary } from "../../../../../components/common/list-summary"
|
||||||
|
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
||||||
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
export const DetailsSection = ({ discount }: { discount: Discount }) => {
|
export const DetailsSection = ({ discount }: { discount: Discount }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
CreateRegionDTO,
|
CreateRegionDTO,
|
||||||
CreateSalesChannelDTO,
|
CreateSalesChannelDTO,
|
||||||
CreateStockLocationInput,
|
CreateStockLocationInput,
|
||||||
|
InventoryNext,
|
||||||
UpdateApiKeyDTO,
|
UpdateApiKeyDTO,
|
||||||
UpdateCampaignDTO,
|
UpdateCampaignDTO,
|
||||||
UpdateCustomerDTO,
|
UpdateCustomerDTO,
|
||||||
@@ -88,3 +89,21 @@ export type BatchUpdatePromotionRulesReq = { rules: UpdatePromotionRuleDTO[] }
|
|||||||
// Campaign
|
// Campaign
|
||||||
export type CreateCampaignReq = CreateCampaignDTO
|
export type CreateCampaignReq = CreateCampaignDTO
|
||||||
export type UpdateCampaignReq = UpdateCampaignDTO
|
export type UpdateCampaignReq = UpdateCampaignDTO
|
||||||
|
|
||||||
|
// Inventory Items
|
||||||
|
export type CreateInventoryItemReq = InventoryNext.CreateInventoryItemInput
|
||||||
|
export type UpdateInventoryItemReq = Omit<
|
||||||
|
InventoryNext.UpdateInventoryItemInput,
|
||||||
|
"id"
|
||||||
|
>
|
||||||
|
|
||||||
|
// Inventory Item Levels
|
||||||
|
export type InventoryItemLocationBatch = {
|
||||||
|
creates: { location_id: string; stocked_quantity?: number }[]
|
||||||
|
deletes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateInventoryLevelReq = {
|
||||||
|
reserved_quantity?: number
|
||||||
|
stocked_quantity?: number
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CampaignDTO,
|
CampaignDTO,
|
||||||
CurrencyDTO,
|
CurrencyDTO,
|
||||||
CustomerGroupDTO,
|
CustomerGroupDTO,
|
||||||
|
InventoryNext,
|
||||||
InviteDTO,
|
InviteDTO,
|
||||||
PaymentProviderDTO,
|
PaymentProviderDTO,
|
||||||
PriceListDTO,
|
PriceListDTO,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
StoreDTO,
|
StoreDTO,
|
||||||
UserDTO,
|
UserDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
|
||||||
import { ProductTagDTO } from "@medusajs/types/dist/product"
|
import { ProductTagDTO } from "@medusajs/types/dist/product"
|
||||||
import { WorkflowExecutionDTO } from "../v2-routes/workflow-executions/types"
|
import { WorkflowExecutionDTO } from "../v2-routes/workflow-executions/types"
|
||||||
|
|
||||||
@@ -140,6 +142,36 @@ export type ProductCollectionListRes = {
|
|||||||
} & ListRes
|
} & ListRes
|
||||||
export type ProductCollectionDeleteRes = DeleteRes
|
export type ProductCollectionDeleteRes = DeleteRes
|
||||||
|
|
||||||
|
// Inventory Items
|
||||||
|
export type InventoryItemRes = {
|
||||||
|
inventory_item: InventoryNext.InventoryItemDTO & {
|
||||||
|
stocked_quantity: number
|
||||||
|
reserved_quantity: number
|
||||||
|
location_levels?: InventoryNext.InventoryLevelDTO[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
export type ReservationItemListRes = {
|
||||||
|
reservations: InventoryNext.ReservationItemDTO[]
|
||||||
|
} & ListRes
|
||||||
|
|
||||||
|
export type ReservationItemRes = {
|
||||||
|
reservation: InventoryNext.ReservationItemDTO
|
||||||
|
}
|
||||||
// Price Lists
|
// Price Lists
|
||||||
export type PriceListRes = { price_list: PriceListDTO }
|
export type PriceListRes = { price_list: PriceListDTO }
|
||||||
export type PriceListListRes = { price_lists: PriceListDTO[] } & ListRes
|
export type PriceListListRes = { price_lists: PriceListDTO[] } & ListRes
|
||||||
|
|||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import { AdjustInventoryForm } from "./components/adjust-inventory-form"
|
||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { RouteDrawer } from "../../../../../components/route-modal"
|
||||||
|
import { useInventoryItem } from "../../../../../hooks/api/inventory"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { useStockLocation } from "../../../../../hooks/api/stock-locations"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const AdjustInventoryDrawer = () => {
|
||||||
|
const { id, location_id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const {
|
||||||
|
inventory_item: inventoryItem,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useInventoryItem(id!)
|
||||||
|
|
||||||
|
const inventoryLevel = inventoryItem?.location_levels!.find(
|
||||||
|
(level: InventoryNext.InventoryLevelDTO) =>
|
||||||
|
level.location_id === location_id
|
||||||
|
)
|
||||||
|
|
||||||
|
const { stock_location, isLoading: isLoadingLocation } = useStockLocation(
|
||||||
|
location_id!
|
||||||
|
)
|
||||||
|
|
||||||
|
const ready =
|
||||||
|
!isLoading &&
|
||||||
|
inventoryItem &&
|
||||||
|
inventoryLevel &&
|
||||||
|
!isLoadingLocation &&
|
||||||
|
stock_location
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("inventory.manageLocations")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{ready && (
|
||||||
|
<AdjustInventoryForm
|
||||||
|
item={inventoryItem}
|
||||||
|
level={inventoryLevel}
|
||||||
|
location={stock_location}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Button, Input, Text } from "@medusajs/ui"
|
||||||
|
import { InventoryLevelDTO, StockLocationDTO } 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"
|
||||||
|
|
||||||
|
type AdjustInventoryFormProps = {
|
||||||
|
item: InventoryItemRes["inventory_item"]
|
||||||
|
level: InventoryLevelDTO
|
||||||
|
location: StockLocationDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
const AttributeGridRow = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
value: string | number
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 divide-x">
|
||||||
|
<Text className="px-2 py-1.5" size="small" leading="compact">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text className="px-2 py-1.5" size="small" leading="compact">
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdjustInventoryForm = ({
|
||||||
|
item,
|
||||||
|
level,
|
||||||
|
location,
|
||||||
|
}: AdjustInventoryFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const AdjustInventorySchema = z.object({
|
||||||
|
stocked_quantity: z.number().min(level.reserved_quantity),
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof AdjustInventorySchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
stocked_quantity: level.stocked_quantity,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(AdjustInventorySchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const stockedQuantityUpdate = form.watch("stocked_quantity")
|
||||||
|
|
||||||
|
const { mutateAsync } = useUpdateInventoryItemLevel(
|
||||||
|
item.id,
|
||||||
|
level.location_id
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (value) => {
|
||||||
|
if (value.stocked_quantity === level.stocked_quantity) {
|
||||||
|
return handleSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
stocked_quantity: value.stocked_quantity,
|
||||||
|
})
|
||||||
|
|
||||||
|
return handleSuccess()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto">
|
||||||
|
<div className="text-ui-fg-subtle shadow-elevation-card-rest grid grid-rows-4 divide-y rounded-lg border">
|
||||||
|
<AttributeGridRow
|
||||||
|
title={t("fields.title")}
|
||||||
|
value={item.title ?? "-"}
|
||||||
|
/>
|
||||||
|
<AttributeGridRow title={t("fields.sku")} value={item.sku!} />
|
||||||
|
<AttributeGridRow
|
||||||
|
title={t("locations.domain")}
|
||||||
|
value={location.name}
|
||||||
|
/>
|
||||||
|
<AttributeGridRow
|
||||||
|
title={t("inventory.reserved")}
|
||||||
|
value={item.reserved_quantity}
|
||||||
|
/>
|
||||||
|
<AttributeGridRow
|
||||||
|
title={t("inventory.available")}
|
||||||
|
value={stockedQuantityUpdate - item.reserved_quantity}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="stocked_quantity"
|
||||||
|
render={({ field: { onChange, value, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.inStock")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={level.reserved_quantity}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
onChange(null)
|
||||||
|
} else {
|
||||||
|
onChange(parseFloat(value))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button type="submit" size="small" isLoading={false}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { AdjustInventoryDrawer as Component } from "./adjust-inventory-drawer"
|
||||||
+252
@@ -0,0 +1,252 @@
|
|||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Button, Input } from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../../components/route-modal"
|
||||||
|
|
||||||
|
import { CountrySelect } from "../../../../../../components/common/country-select"
|
||||||
|
import { Form } from "../../../../../../components/common/form"
|
||||||
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useUpdateInventoryItem } from "../../../../../../hooks/api/inventory"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
|
||||||
|
type EditInventoryItemAttributeFormProps = {
|
||||||
|
item: InventoryNext.InventoryItemDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditInventoryItemAttributesSchema = z.object({
|
||||||
|
height: z.number().positive().optional(),
|
||||||
|
width: z.number().positive().optional(),
|
||||||
|
length: z.number().positive().optional(),
|
||||||
|
weight: z.number().positive().optional(),
|
||||||
|
mid_code: z.string().optional(),
|
||||||
|
hs_code: z.string().optional(),
|
||||||
|
origin_country: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDefaultValues = (item: InventoryNext.InventoryItemDTO) => {
|
||||||
|
return {
|
||||||
|
height: item.height ?? undefined,
|
||||||
|
width: item.width ?? undefined,
|
||||||
|
length: item.length ?? undefined,
|
||||||
|
weight: item.weight ?? undefined,
|
||||||
|
mid_code: item.mid_code ?? undefined,
|
||||||
|
hs_code: item.hs_code ?? undefined,
|
||||||
|
origin_country: item.origin_country ?? undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditInventoryItemAttributesForm = ({
|
||||||
|
item,
|
||||||
|
}: EditInventoryItemAttributeFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditInventoryItemAttributesSchema>>({
|
||||||
|
defaultValues: getDefaultValues(item),
|
||||||
|
resolver: zodResolver(EditInventoryItemAttributesSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync } = useUpdateInventoryItem(item.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
mutateAsync(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-4 overflow-auto">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="height"
|
||||||
|
render={({ field: { onChange, value, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.height")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
onChange(null)
|
||||||
|
} else {
|
||||||
|
onChange(parseFloat(value))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="width"
|
||||||
|
render={({ field: { onChange, value, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.width")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
onChange(null)
|
||||||
|
} else {
|
||||||
|
onChange(parseFloat(value))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="length"
|
||||||
|
render={({ field: { onChange, value, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.length")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
onChange(null)
|
||||||
|
} else {
|
||||||
|
onChange(parseFloat(value))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="weight"
|
||||||
|
render={({ field: { onChange, value, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.weight")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
onChange(null)
|
||||||
|
} else {
|
||||||
|
onChange(parseFloat(value))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="mid_code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.midCode")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="hs_code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.hsCode")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="origin_country"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>
|
||||||
|
{t("fields.countryOfOrigin")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CountrySelect {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button type="submit" size="small" isLoading={false}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { EditInventoryItemAttributesForm } from "./components/edit-item-attributes-form"
|
||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { RouteDrawer } from "../../../../../components/route-modal"
|
||||||
|
import { useInventoryItem } from "../../../../../hooks/api/inventory"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const InventoryItemAttributesEdit = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const {
|
||||||
|
inventory_item: inventoryItem,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useInventoryItem(id!)
|
||||||
|
|
||||||
|
const ready = !isLoading && inventoryItem
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("products.editAttributes")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{ready && <EditInventoryItemAttributesForm item={inventoryItem} />}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { InventoryItemAttributesEdit as Component } from "./edit-item-attributes-drawer"
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Button, Input } from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../../components/route-modal"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../../components/common/form"
|
||||||
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useUpdateInventoryItem } from "../../../../../../hooks/api/inventory"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
|
||||||
|
type EditInventoryItemFormProps = {
|
||||||
|
item: InventoryNext.InventoryItemDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditInventoryItemSchema = z.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
sku: z.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDefaultValues = (item: InventoryNext.InventoryItemDTO) => {
|
||||||
|
return {
|
||||||
|
title: item.title ?? undefined,
|
||||||
|
sku: item.sku ?? undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditInventoryItemForm = ({ item }: EditInventoryItemFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditInventoryItemSchema>>({
|
||||||
|
defaultValues: getDefaultValues(item),
|
||||||
|
resolver: zodResolver(EditInventoryItemSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync } = useUpdateInventoryItem(item.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
mutateAsync(values as any, {
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.title")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</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} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button type="submit" size="small" isLoading={false}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { EditInventoryItemForm } from "./components/edit-item-form"
|
||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { RouteDrawer } from "../../../../../components/route-modal"
|
||||||
|
import { useInventoryItem } from "../../../../../hooks/api/inventory"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const InventoryItemEdit = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const {
|
||||||
|
inventory_item: inventoryItem,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useInventoryItem(id!)
|
||||||
|
|
||||||
|
const ready = !isLoading && inventoryItem
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("inventory.editItemDetails")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{ready && <EditInventoryItemForm item={inventoryItem} />}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { InventoryItemEdit as Component } from "./edit-item-drawer"
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { SectionRow } from "../../../../../components/common/section"
|
||||||
|
import { getFormattedCountry } from "../../../../../lib/addresses"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type InventoryItemAttributeSectionProps = {
|
||||||
|
inventoryItem: InventoryNext.InventoryItemDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InventoryItemAttributeSection = ({
|
||||||
|
inventoryItem,
|
||||||
|
}: InventoryItemAttributeSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("products.attributes")}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: "attributes",
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SectionRow title={t("fields.height")} value={inventoryItem.height} />
|
||||||
|
<SectionRow title={t("fields.width")} value={inventoryItem.width} />
|
||||||
|
<SectionRow title={t("fields.length")} value={inventoryItem.length} />
|
||||||
|
<SectionRow title={t("fields.weight")} value={inventoryItem.weight} />
|
||||||
|
<SectionRow title={t("fields.midCode")} value={inventoryItem.mid_code} />
|
||||||
|
<SectionRow title={t("fields.hsCode")} value={inventoryItem.hs_code} />
|
||||||
|
<SectionRow
|
||||||
|
title={t("fields.countryOfOrigin")}
|
||||||
|
value={getFormattedCountry(inventoryItem.origin_country)}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
import { Container, Heading, Text } from "@medusajs/ui"
|
||||||
|
import { InventoryNext, ProductVariantDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../components/common/action-menu"
|
||||||
|
import { InventoryItemRes } from "../../../../types/api-responses"
|
||||||
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { SectionRow } from "../../../../components/common/section"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type InventoryItemGeneralSectionProps = {
|
||||||
|
inventoryItem: InventoryItemRes["inventory_item"] & {
|
||||||
|
variant: ProductVariantDTO | ProductVariantDTO[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const InventoryItemGeneralSection = ({
|
||||||
|
inventoryItem,
|
||||||
|
}: InventoryItemGeneralSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const variantArray = inventoryItem.variant
|
||||||
|
? Array.isArray(inventoryItem.variant)
|
||||||
|
? inventoryItem.variant
|
||||||
|
: [inventoryItem.variant]
|
||||||
|
: []
|
||||||
|
|
||||||
|
const variantTitles = variantArray.map((variant) => variant.title)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{inventoryItem.title ?? inventoryItem.sku}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: "edit",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SectionRow title={t("fields.sku")} value={inventoryItem.sku ?? "-"} />
|
||||||
|
<SectionRow
|
||||||
|
title={t("inventory.associatedVariants")}
|
||||||
|
value={variantTitles?.length ? variantTitles.join(", ") : "-"}
|
||||||
|
/>
|
||||||
|
<SectionRow
|
||||||
|
title={t("fields.inStock")}
|
||||||
|
value={getQuantityFormat(
|
||||||
|
inventoryItem.stocked_quantity,
|
||||||
|
inventoryItem.location_levels.length
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SectionRow
|
||||||
|
title={t("inventory.reserved")}
|
||||||
|
value={getQuantityFormat(
|
||||||
|
inventoryItem.reserved_quantity,
|
||||||
|
inventoryItem.location_levels.length
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<SectionRow
|
||||||
|
title={t("inventory.available")}
|
||||||
|
value={getQuantityFormat(
|
||||||
|
inventoryItem.stocked_quantity - inventoryItem.reserved_quantity,
|
||||||
|
inventoryItem.location_levels.length
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getQuantityFormat = (quantity: number, locations: number) => {
|
||||||
|
return `${quantity ?? "-"}
|
||||||
|
${quantity ? `across ${locations} locations` : ""}`
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { Button, Container, Heading } from "@medusajs/ui"
|
||||||
|
|
||||||
|
import { InventoryItemRes } from "../../../../types/api-responses"
|
||||||
|
import { ItemLocationListTable } from "./location-levels-table/location-list-table"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { ReservationItemTable } from "./reservations-table/reservation-list-table"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type InventoryItemLocationLevelsSectionProps = {
|
||||||
|
inventoryItem: InventoryItemRes["inventory_item"]
|
||||||
|
}
|
||||||
|
export const InventoryItemLocationLevelsSection = ({
|
||||||
|
inventoryItem,
|
||||||
|
}: InventoryItemLocationLevelsSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{t("inventory.locationLevels")}</Heading>
|
||||||
|
<Button size="small" variant="secondary" asChild>
|
||||||
|
<Link to="locations">{t("inventory.manageLocations")}</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ItemLocationListTable inventory_item_id={inventoryItem.id} />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { Button, Container, Heading } from "@medusajs/ui"
|
||||||
|
|
||||||
|
import { InventoryItemRes } from "../../../../types/api-responses"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { ReservationItemTable } from "./reservations-table/reservation-list-table"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type InventoryItemLocationLevelsSectionProps = {
|
||||||
|
inventoryItem: InventoryItemRes["inventory_item"]
|
||||||
|
}
|
||||||
|
export const InventoryItemReservationsSection = ({
|
||||||
|
inventoryItem,
|
||||||
|
}: InventoryItemLocationLevelsSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{t("reservations.domain")}</Heading>
|
||||||
|
<Button size="small" variant="secondary" asChild>
|
||||||
|
<Link to="locations">{t("actions.create")}</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ReservationItemTable inventoryItem={inventoryItem} />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { useDeleteInventoryItemLevel } from "../../../../../hooks/api/inventory"
|
||||||
|
import { usePrompt } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const LocationActions = ({
|
||||||
|
level,
|
||||||
|
}: {
|
||||||
|
level: InventoryNext.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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useInventoryItemLevels } from "../../../../../hooks/api/inventory"
|
||||||
|
import { useLocationLevelTableQuery } from "./use-location-list-table-query"
|
||||||
|
import { useLocationListTableColumns } from "./use-location-list-table-columns"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export const ItemLocationListTable = ({
|
||||||
|
inventory_item_id,
|
||||||
|
}: {
|
||||||
|
inventory_item_id: string
|
||||||
|
}) => {
|
||||||
|
const { searchParams, raw } = useLocationLevelTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { inventory_levels, count, isLoading, isError, error } =
|
||||||
|
useInventoryItemLevels(inventory_item_id, {
|
||||||
|
...searchParams,
|
||||||
|
fields: "*stock_locations",
|
||||||
|
})
|
||||||
|
|
||||||
|
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}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
isLoading={isLoading}
|
||||||
|
pagination
|
||||||
|
queryObject={raw}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
import { InventoryNext, StockLocationDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { LocationActions } from "./location-actions"
|
||||||
|
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds missing properties to the InventoryLevelDTO type.
|
||||||
|
*/
|
||||||
|
interface ExtendedLocationLevel extends InventoryNext.InventoryLevelDTO {
|
||||||
|
stock_locations: StockLocationDTO[]
|
||||||
|
reserved_quantity: number
|
||||||
|
stocked_quantity: number
|
||||||
|
available_quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ExtendedLocationLevel>()
|
||||||
|
|
||||||
|
export const useLocationListTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("stock_locations.0.name", {
|
||||||
|
header: t("fields.location"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const locationName = getValue()
|
||||||
|
|
||||||
|
if (!locationName) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{locationName.toString()}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("reserved_quantity", {
|
||||||
|
header: t("inventory.reserved"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const quantity = getValue()
|
||||||
|
|
||||||
|
if (Number.isNaN(quantity)) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{quantity}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("stocked_quantity", {
|
||||||
|
header: t("fields.inStock"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const stockedQuantity = getValue()
|
||||||
|
|
||||||
|
if (Number.isNaN(stockedQuantity)) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{stockedQuantity}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("available_quantity", {
|
||||||
|
header: t("inventory.available"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const availableQuantity = getValue()
|
||||||
|
|
||||||
|
if (Number.isNaN(availableQuantity)) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{availableQuantity}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => <LocationActions level={row.original} />,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
export const useLocationLevelTableQuery = ({
|
||||||
|
pageSize = 20,
|
||||||
|
prefix,
|
||||||
|
}: {
|
||||||
|
pageSize?: number
|
||||||
|
prefix?: string
|
||||||
|
}) => {
|
||||||
|
const raw = useQueryParams(
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"location_id",
|
||||||
|
"stocked_quantity",
|
||||||
|
"reserved_quantity",
|
||||||
|
"incoming_quantity",
|
||||||
|
"available_quantity",
|
||||||
|
"*stock_locations",
|
||||||
|
],
|
||||||
|
prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
const { reserved_quantity, stocked_quantity, available_quantity, ...params } =
|
||||||
|
raw
|
||||||
|
|
||||||
|
const searchParams = {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParams,
|
||||||
|
raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
import { Checkbox, Text, clx } from "@medusajs/ui"
|
||||||
|
|
||||||
|
import { StockLocationDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
type LocationItemProps = {
|
||||||
|
selected: boolean
|
||||||
|
onSelect: (selected: boolean) => void
|
||||||
|
location: StockLocationDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LocationItem = ({
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
location,
|
||||||
|
}: LocationItemProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clx("flex w-full rounded-lg border px-2 py-2 gap-x-2", {
|
||||||
|
"border-ui-border-interactive ": selected,
|
||||||
|
})}
|
||||||
|
onClick={() => onSelect(!selected)}
|
||||||
|
>
|
||||||
|
<div className="h-5 w-5">
|
||||||
|
<Checkbox checked={selected} />
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{location.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||||
|
{[
|
||||||
|
location.address?.address_1,
|
||||||
|
location.address?.city,
|
||||||
|
location.address?.country_code,
|
||||||
|
]
|
||||||
|
.filter((el) => !!el)
|
||||||
|
.join(", ")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Button, Table, Text } from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../../components/route-modal"
|
||||||
|
import {
|
||||||
|
useBatchInventoryItemLevels,
|
||||||
|
useUpdateInventoryItem,
|
||||||
|
} from "../../../../../../hooks/api/inventory"
|
||||||
|
import { useFieldArray, useForm } from "react-hook-form"
|
||||||
|
|
||||||
|
import { InventoryItemRes } from "../../../../../../types/api-responses"
|
||||||
|
import { LocationItem } from "./location-item"
|
||||||
|
import { StockLocationDTO } from "@medusajs/types"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
|
||||||
|
type EditInventoryItemAttributeFormProps = {
|
||||||
|
item: InventoryItemRes["inventory_item"]
|
||||||
|
locations: StockLocationDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditInventoryItemAttributesSchema = z.object({
|
||||||
|
locations: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
location_id: z.string(),
|
||||||
|
selected: z.boolean(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDefaultValues = (
|
||||||
|
allLocations: StockLocationDTO[],
|
||||||
|
existinLevels: Set<string>
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
locations: allLocations.map((location) => ({
|
||||||
|
...location,
|
||||||
|
location_id: location.id,
|
||||||
|
selected: existinLevels.has(location.id),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ManageLocationsForm = ({
|
||||||
|
item,
|
||||||
|
locations,
|
||||||
|
}: EditInventoryItemAttributeFormProps) => {
|
||||||
|
const existingLocationLevels = new Set(
|
||||||
|
item.location_levels?.map((l) => l.location_id) ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditInventoryItemAttributesSchema>>({
|
||||||
|
defaultValues: getDefaultValues(locations, existingLocationLevels),
|
||||||
|
resolver: zodResolver(EditInventoryItemAttributesSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { fields: locationFields, update: updateField } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "locations",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync } = useBatchInventoryItemLevels(item.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async ({ locations }) => {
|
||||||
|
// Changes in selected locations
|
||||||
|
const [selectedLocations, unselectedLocations] = locations.reduce(
|
||||||
|
(acc, location) => {
|
||||||
|
// If the location is not changed do nothing
|
||||||
|
if (
|
||||||
|
(!location.selected &&
|
||||||
|
!existingLocationLevels.has(location.location_id)) ||
|
||||||
|
(location.selected &&
|
||||||
|
existingLocationLevels.has(location.location_id))
|
||||||
|
) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.selected) {
|
||||||
|
acc[0].push(location.location_id)
|
||||||
|
} else {
|
||||||
|
acc[1].push(location.location_id)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
[[], []] as [string[], string[]]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedLocations.length === 0 && unselectedLocations.length === 0) {
|
||||||
|
return handleSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
creates: selectedLocations.map((location_id) => ({
|
||||||
|
location_id,
|
||||||
|
})),
|
||||||
|
deletes: unselectedLocations,
|
||||||
|
})
|
||||||
|
|
||||||
|
return handleSuccess()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-4 overflow-auto">
|
||||||
|
<div className="grid grid-rows-2 divide-y rounded-lg border text-ui-fg-subtle shadow-elevation-card-rest">
|
||||||
|
<div className="grid grid-cols-2 divide-x">
|
||||||
|
<Text className="px-2 py-1.5" size="small" leading="compact">
|
||||||
|
{t("fields.title")}
|
||||||
|
</Text>
|
||||||
|
<Text className="px-2 py-1.5" size="small" leading="compact">
|
||||||
|
{item.title ?? "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 divide-x">
|
||||||
|
<Text className="px-2 py-1.5" size="small" leading="compact">
|
||||||
|
{t("fields.sku")}
|
||||||
|
</Text>
|
||||||
|
<Text className="px-2 py-1.5" size="small" leading="compact">
|
||||||
|
{item.sku}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{t("locations.domain")}
|
||||||
|
</Text>
|
||||||
|
<div className="flex w-full justify-between text-ui-fg-subtle">
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{t("locations.selectLocations")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{"("}
|
||||||
|
{t("general.countOfTotalSelected", {
|
||||||
|
count: locationFields.filter((l) => l.selected).length,
|
||||||
|
total: locations.length,
|
||||||
|
})}
|
||||||
|
{")"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{locationFields.map((location, idx) => {
|
||||||
|
return (
|
||||||
|
<LocationItem
|
||||||
|
selected={location.selected}
|
||||||
|
location={location as any}
|
||||||
|
onSelect={() =>
|
||||||
|
updateField(idx, {
|
||||||
|
...location,
|
||||||
|
selected: !location.selected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
key={location.id}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button type="submit" size="small" isLoading={false}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ManageLocationsDrawer as Component } from "./manage-locations-drawer"
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { ManageLocationsForm } from "./components/manage-locations-form"
|
||||||
|
import { RouteDrawer } from "../../../../../components/route-modal"
|
||||||
|
import { useInventoryItem } from "../../../../../hooks/api/inventory"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const ManageLocationsDrawer = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const {
|
||||||
|
inventory_item: inventoryItem,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useInventoryItem(id!)
|
||||||
|
|
||||||
|
const { stock_locations, isLoading: loadingLocations } = useStockLocations()
|
||||||
|
|
||||||
|
const ready =
|
||||||
|
!isLoading && !loadingLocations && inventoryItem && stock_locations
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("inventory.manageLocations")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{ready && (
|
||||||
|
<ManageLocationsForm item={inventoryItem} locations={stock_locations} />
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { useDeleteReservationItem } from "../../../../../hooks/api/inventory"
|
||||||
|
import { usePrompt } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const ReservationActions = ({
|
||||||
|
reservation,
|
||||||
|
}: {
|
||||||
|
reservation: InventoryNext.ReservationItemDTO
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const { mutateAsync } = useDeleteReservationItem(reservation.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: `/reservation/${reservation.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useInventoryTableColumns } from "./use-reservation-list-table-columns"
|
||||||
|
import { useInventoryTableQuery } from "./use-reservation-list-table-query"
|
||||||
|
import { useReservationItems } from "../../../../../hooks/api/inventory"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export const ReservationItemTable = ({
|
||||||
|
inventoryItem,
|
||||||
|
}: {
|
||||||
|
inventoryItem: InventoryNext.InventoryItemDTO
|
||||||
|
}) => {
|
||||||
|
const { searchParams, raw } = useInventoryTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { reservations, count, isLoading, isError, error } =
|
||||||
|
useReservationItems({
|
||||||
|
...searchParams,
|
||||||
|
inventory_item_id: [inventoryItem.id],
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = useInventoryTableColumns({ sku: inventoryItem.sku! })
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: reservations ?? [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
isLoading={isLoading}
|
||||||
|
pagination
|
||||||
|
queryObject={raw}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
import { InventoryNext, StockLocationDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
|
import { ReservationActions } from "./reservation-actions"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds missing properties to the InventoryItemDTO type.
|
||||||
|
*/
|
||||||
|
interface ExtendedInventoryItem extends InventoryNext.ReservationItemDTO {
|
||||||
|
line_item: { order_id: string }
|
||||||
|
location: StockLocationDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ExtendedInventoryItem>()
|
||||||
|
|
||||||
|
export const useInventoryTableColumns = ({ sku }: { sku: string }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
header: t("fields.sku"),
|
||||||
|
cell: () => {
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{sku}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("line_item.order_id", {
|
||||||
|
header: t("inventory.reserved"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const quantity = getValue()
|
||||||
|
|
||||||
|
if (Number.isNaN(quantity)) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{quantity}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("description", {
|
||||||
|
header: t("fields.description"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const description = getValue()
|
||||||
|
|
||||||
|
if (!description) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{description}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("location.name", {
|
||||||
|
header: t("inventory.location"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const location = getValue()
|
||||||
|
|
||||||
|
if (!location) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{location}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("created_at", {
|
||||||
|
header: t("fields.createdAt"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const createdAt = getValue()
|
||||||
|
|
||||||
|
if (!createdAt) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">
|
||||||
|
{createdAt instanceof Date ? createdAt.toString() : createdAt}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => <ReservationActions item={row.original} />,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
export const useInventoryTableQuery = ({
|
||||||
|
pageSize = 20,
|
||||||
|
prefix,
|
||||||
|
}: {
|
||||||
|
pageSize?: number
|
||||||
|
prefix?: string
|
||||||
|
}) => {
|
||||||
|
const raw = useQueryParams(
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
"location_id",
|
||||||
|
"inventory_item_id",
|
||||||
|
"quantity",
|
||||||
|
"line_item_id",
|
||||||
|
"description",
|
||||||
|
"created_by",
|
||||||
|
],
|
||||||
|
prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
const { quantity, ...params } = raw
|
||||||
|
|
||||||
|
const searchParams = {
|
||||||
|
limit: pageSize,
|
||||||
|
quantity: quantity ? JSON.parse(quantity) : undefined,
|
||||||
|
...params,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParams,
|
||||||
|
raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { InventoryDeatil as Component } from "./inventory-detail"
|
||||||
|
export { inventoryItemLoader as loader } from "./loader"
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { InventoryItemAttributeSection } from "./components/inventory-item-attributes/attributes-section"
|
||||||
|
import { InventoryItemGeneralSection } from "./components/inventory-item-general-section"
|
||||||
|
import { InventoryItemLocationLevelsSection } from "./components/inventory-item-location-levels"
|
||||||
|
import { InventoryItemReservationsSection } from "./components/inventory-item-reservations"
|
||||||
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { inventoryItemLoader } from "./loader"
|
||||||
|
import { useInventoryItem } from "../../../hooks/api/inventory"
|
||||||
|
|
||||||
|
export const InventoryDeatil = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof inventoryItemLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { inventory_item, isLoading, isError, error } = useInventoryItem(
|
||||||
|
id!,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
initialData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !inventory_item) {
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
throw json("An unknown error occurred", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start">
|
||||||
|
<div className="w-full flex flex-col gap-y-2">
|
||||||
|
<InventoryItemGeneralSection inventoryItem={inventory_item} />
|
||||||
|
<InventoryItemLocationLevelsSection inventoryItem={inventory_item} />
|
||||||
|
<InventoryItemReservationsSection inventoryItem={inventory_item} />
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<JsonViewSection data={inventory_item} />
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
<div className="w-full lg:max-w-[400px] max-w-[100%] mt-2 lg:mt-0 flex flex-col gap-y-2">
|
||||||
|
<InventoryItemAttributeSection inventoryItem={inventory_item} />
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<JsonViewSection data={inventory_item} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { InventoryItemRes } from "../../../types/api-responses"
|
||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
import { client } from "../../../lib/client"
|
||||||
|
import { inventoryItemsQueryKeys } from "../../../hooks/api/inventory"
|
||||||
|
import { queryClient } from "../../../lib/medusa"
|
||||||
|
|
||||||
|
const inventoryDetailQuery = (id: string) => ({
|
||||||
|
queryKey: inventoryItemsQueryKeys.detail(id),
|
||||||
|
queryFn: async () =>
|
||||||
|
client.inventoryItems.retrieve(id, {
|
||||||
|
fields: "*variants",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const inventoryItemLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id
|
||||||
|
const query = inventoryDetailQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<InventoryItemRes>(query.queryKey) ??
|
||||||
|
(await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../components/common/action-menu"
|
||||||
|
import { InventoryItemDTO } from "@medusajs/types"
|
||||||
|
import { useDeleteInventoryItem } from "../../../../hooks/api/inventory"
|
||||||
|
import { usePrompt } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const InventoryActions = ({ item }: { item: InventoryItemDTO }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const { mutateAsync } = useDeleteInventoryItem(item.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: `${item.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
|
||||||
|
import { DataTable } from "../../../../components/table/data-table"
|
||||||
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { useDataTable } from "../../../../hooks/use-data-table"
|
||||||
|
import { useInventoryItems } from "../../../../hooks/api/inventory"
|
||||||
|
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"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export const InventoryListTable = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { searchParams, raw } = useInventoryTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
const { inventory_items, count, isLoading, isError, error } =
|
||||||
|
useInventoryItems({
|
||||||
|
...searchParams,
|
||||||
|
})
|
||||||
|
|
||||||
|
const filters = useInventoryTableFilters()
|
||||||
|
const columns = useInventoryTableColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: (inventory_items ?? []) as InventoryNext.InventoryItemDTO[],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{t("inventory.domain")}</Heading>
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
isLoading={isLoading}
|
||||||
|
pagination
|
||||||
|
search
|
||||||
|
filters={filters}
|
||||||
|
queryObject={raw}
|
||||||
|
orderBy={["title", "sku", "stocked_quantity", "reserved_quantity"]}
|
||||||
|
navigateTo={(row) => `${row.id}`}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
import { InventoryNext, ProductVariantDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { InventoryActions } from "./inventory-actions"
|
||||||
|
import { PlaceholderCell } from "../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds missing properties to the InventoryItemDTO type.
|
||||||
|
*/
|
||||||
|
interface ExtendedInventoryItem extends InventoryNext.InventoryItemDTO {
|
||||||
|
variants?: ProductVariantDTO[] | null
|
||||||
|
stocked_quantity?: number
|
||||||
|
reserved_quantity?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ExtendedInventoryItem>()
|
||||||
|
|
||||||
|
export const useInventoryTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("title", {
|
||||||
|
header: t("fields.title"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const title = getValue()
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{title}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("sku", {
|
||||||
|
header: t("fields.sku"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const sku = getValue() as string
|
||||||
|
|
||||||
|
if (!sku) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{sku}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("reserved_quantity", {
|
||||||
|
header: t("inventory.reserved"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const quantity = getValue()
|
||||||
|
|
||||||
|
if (Number.isNaN(quantity)) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{quantity}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("stocked_quantity", {
|
||||||
|
header: t("fields.inStock"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const quantity = getValue()
|
||||||
|
|
||||||
|
if (Number.isNaN(quantity)) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{quantity}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => <InventoryActions item={row.original} />,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
import { Filter } from "../../../../components/table/data-table"
|
||||||
|
import { useStockLocations } from "../../../../hooks/api/stock-locations"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const useInventoryTableFilters = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { stock_locations } = useStockLocations({
|
||||||
|
limit: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const filters: Filter[] = []
|
||||||
|
|
||||||
|
if (stock_locations) {
|
||||||
|
const stockLocationFilter: Filter = {
|
||||||
|
type: "select",
|
||||||
|
options: stock_locations.map((s) => ({
|
||||||
|
label: s.name,
|
||||||
|
value: s.id,
|
||||||
|
})),
|
||||||
|
key: "location_id",
|
||||||
|
searchable: true,
|
||||||
|
label: t("fields.location"),
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.push(stockLocationFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "string",
|
||||||
|
key: "material",
|
||||||
|
label: t("fields.material"),
|
||||||
|
})
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "string",
|
||||||
|
key: "sku",
|
||||||
|
label: t("fields.sku"),
|
||||||
|
})
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "string",
|
||||||
|
key: "mid_code",
|
||||||
|
label: t("fields.midCode"),
|
||||||
|
})
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "number",
|
||||||
|
key: "height",
|
||||||
|
label: t("fields.height"),
|
||||||
|
})
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "number",
|
||||||
|
key: "width",
|
||||||
|
label: t("fields.width"),
|
||||||
|
})
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "number",
|
||||||
|
key: "length",
|
||||||
|
label: t("fields.length"),
|
||||||
|
})
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "number",
|
||||||
|
key: "weight",
|
||||||
|
label: t("fields.weight"),
|
||||||
|
})
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: t("fields.true"), value: "true" },
|
||||||
|
{ label: t("fields.false"), value: "false" },
|
||||||
|
],
|
||||||
|
key: "requires_shipping",
|
||||||
|
multiple: false,
|
||||||
|
label: t("fields.requiresShipping"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return filters
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { AdminGetInventoryItemsParams } from "@medusajs/medusa"
|
||||||
|
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
export const useInventoryTableQuery = ({
|
||||||
|
pageSize = 20,
|
||||||
|
prefix,
|
||||||
|
}: {
|
||||||
|
pageSize?: number
|
||||||
|
prefix?: string
|
||||||
|
}) => {
|
||||||
|
const raw = useQueryParams(
|
||||||
|
[
|
||||||
|
"location_id",
|
||||||
|
"q",
|
||||||
|
"order",
|
||||||
|
"requires_shipping",
|
||||||
|
"offset",
|
||||||
|
"sku",
|
||||||
|
"material",
|
||||||
|
"mid_code",
|
||||||
|
"order",
|
||||||
|
"weight",
|
||||||
|
"width",
|
||||||
|
"length",
|
||||||
|
"height",
|
||||||
|
],
|
||||||
|
prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
offset,
|
||||||
|
weight,
|
||||||
|
width,
|
||||||
|
length,
|
||||||
|
height,
|
||||||
|
requires_shipping,
|
||||||
|
...params
|
||||||
|
} = raw
|
||||||
|
|
||||||
|
const searchParams: AdminGetInventoryItemsParams = {
|
||||||
|
limit: pageSize,
|
||||||
|
offset: offset ? parseInt(offset) : undefined,
|
||||||
|
weight: weight ? JSON.parse(weight) : undefined,
|
||||||
|
width: width ? JSON.parse(width) : undefined,
|
||||||
|
length: length ? JSON.parse(length) : undefined,
|
||||||
|
height: height ? JSON.parse(height) : undefined,
|
||||||
|
requires_shipping: requires_shipping
|
||||||
|
? JSON.parse(requires_shipping)
|
||||||
|
: undefined,
|
||||||
|
...params,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParams,
|
||||||
|
raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { InventoryItemListTable as Component } from "./inventory-list"
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { InventoryListTable } from "./components/inventory-list-table"
|
||||||
|
import { Outlet } from "react-router-dom"
|
||||||
|
|
||||||
|
export const InventoryItemListTable = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<InventoryListTable />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { IInventoryServiceNext, InventoryNext } from "@medusajs/types"
|
||||||
CreateInventoryLevelInput,
|
|
||||||
IInventoryServiceNext,
|
|
||||||
} from "@medusajs/types"
|
|
||||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
@@ -9,7 +6,7 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
|||||||
export const createInventoryLevelsStepId = "create-inventory-levels"
|
export const createInventoryLevelsStepId = "create-inventory-levels"
|
||||||
export const createInventoryLevelsStep = createStep(
|
export const createInventoryLevelsStep = createStep(
|
||||||
createInventoryLevelsStepId,
|
createInventoryLevelsStepId,
|
||||||
async (data: CreateInventoryLevelInput[], { container }) => {
|
async (data: InventoryNext.CreateInventoryLevelInput[], { container }) => {
|
||||||
const service = container.resolve<IInventoryServiceNext>(
|
const service = container.resolve<IInventoryServiceNext>(
|
||||||
ModuleRegistrationName.INVENTORY
|
ModuleRegistrationName.INVENTORY
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
DeleteEntityInput,
|
||||||
|
ModuleRegistrationName,
|
||||||
|
Modules,
|
||||||
|
} from "@medusajs/modules-sdk"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
import { IInventoryServiceNext } from "@medusajs/types"
|
||||||
|
import { MedusaError } from "@medusajs/utils"
|
||||||
|
|
||||||
|
export const deleteInventoryLevelsFromItemAndLocationsStepId =
|
||||||
|
"delete-inventory-levels-from-item-and-location-step"
|
||||||
|
export const deleteInventoryLevelsFromItemAndLocationsStep = createStep(
|
||||||
|
deleteInventoryLevelsFromItemAndLocationsStepId,
|
||||||
|
async (
|
||||||
|
input: { inventory_item_id: string; location_id: string }[],
|
||||||
|
{ container }
|
||||||
|
) => {
|
||||||
|
if (!input.length) {
|
||||||
|
return new StepResponse(void 0, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = container.resolve<IInventoryServiceNext>(
|
||||||
|
ModuleRegistrationName.INVENTORY
|
||||||
|
)
|
||||||
|
|
||||||
|
const items = await service.listInventoryLevels({ $or: input }, {})
|
||||||
|
|
||||||
|
if (items.some((i) => i.reserved_quantity > 0)) {
|
||||||
|
const invalidDeletes = items.filter((i) => i.reserved_quantity > 0)
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
`Cannot remove Inventory Levels for ${invalidDeletes
|
||||||
|
.map((i) => `Inventory Level ${i.id} at Location ${i.location_id}`)
|
||||||
|
.join(
|
||||||
|
", "
|
||||||
|
)} because there are reserved quantities for items at locations`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedIds = items.map((i) => i.id)
|
||||||
|
const deleted = await service.softDeleteInventoryLevels(deletedIds)
|
||||||
|
|
||||||
|
return new StepResponse(
|
||||||
|
{
|
||||||
|
[Modules.INVENTORY]: deleted,
|
||||||
|
} as DeleteEntityInput,
|
||||||
|
deletedIds
|
||||||
|
)
|
||||||
|
},
|
||||||
|
async (prevLevelIds, { container }) => {
|
||||||
|
if (!prevLevelIds?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = container.resolve<IInventoryServiceNext>(
|
||||||
|
ModuleRegistrationName.INVENTORY
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.restoreInventoryLevels(prevLevelIds)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -7,3 +7,4 @@ export * from "./validate-inventory-locations"
|
|||||||
export * from "./update-inventory-items"
|
export * from "./update-inventory-items"
|
||||||
export * from "./delete-inventory-levels"
|
export * from "./delete-inventory-levels"
|
||||||
export * from "./update-inventory-levels"
|
export * from "./update-inventory-levels"
|
||||||
|
export * from "./delete-levels-by-item-and-location"
|
||||||
@@ -5,13 +5,13 @@ import {
|
|||||||
remoteQueryObjectFromString,
|
remoteQueryObjectFromString,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
import { CreateInventoryLevelInput } from "@medusajs/types"
|
import { InventoryNext } from "@medusajs/types"
|
||||||
import { createStep } from "@medusajs/workflows-sdk"
|
import { createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
export const validateInventoryLocationsStepId = "validate-inventory-levels-step"
|
export const validateInventoryLocationsStepId = "validate-inventory-levels-step"
|
||||||
export const validateInventoryLocationsStep = createStep(
|
export const validateInventoryLocationsStep = createStep(
|
||||||
validateInventoryLocationsStepId,
|
validateInventoryLocationsStepId,
|
||||||
async (data: CreateInventoryLevelInput[], { container }) => {
|
async (data: InventoryNext.CreateInventoryLevelInput[], { container }) => {
|
||||||
const remoteQuery = container.resolve(
|
const remoteQuery = container.resolve(
|
||||||
ContainerRegistrationKeys.REMOTE_QUERY
|
ContainerRegistrationKeys.REMOTE_QUERY
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { InventoryLevelDTO, InventoryNext } from "@medusajs/types"
|
||||||
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
|
import {
|
||||||
|
createInventoryLevelsStep,
|
||||||
|
deleteInventoryLevelsFromItemAndLocationsStep,
|
||||||
|
} from "../steps"
|
||||||
|
|
||||||
|
import { removeRemoteLinkStep } from "../../common"
|
||||||
|
|
||||||
|
interface WorkflowInput {
|
||||||
|
creates: InventoryNext.CreateInventoryLevelInput[]
|
||||||
|
deletes: { inventory_item_id: string; location_id: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bulkCreateDeleteLevelsWorkflowId =
|
||||||
|
"bulk-create-delete-levels-workflow"
|
||||||
|
export const bulkCreateDeleteLevelsWorkflow = createWorkflow(
|
||||||
|
bulkCreateDeleteLevelsWorkflowId,
|
||||||
|
(input: WorkflowData<WorkflowInput>): WorkflowData<InventoryLevelDTO[]> => {
|
||||||
|
const deleted = deleteInventoryLevelsFromItemAndLocationsStep(input.deletes)
|
||||||
|
|
||||||
|
removeRemoteLinkStep(deleted)
|
||||||
|
|
||||||
|
return createInventoryLevelsStep(input.creates)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CreateInventoryLevelInput, InventoryLevelDTO } from "@medusajs/types"
|
import { InventoryLevelDTO, InventoryNext } from "@medusajs/types"
|
||||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
createInventoryLevelsStep,
|
createInventoryLevelsStep,
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "../steps"
|
} from "../steps"
|
||||||
|
|
||||||
interface WorkflowInput {
|
interface WorkflowInput {
|
||||||
inventory_levels: CreateInventoryLevelInput[]
|
inventory_levels: InventoryNext.CreateInventoryLevelInput[]
|
||||||
}
|
}
|
||||||
export const createInventoryLevelsWorkflowId =
|
export const createInventoryLevelsWorkflowId =
|
||||||
"create-inventory-levels-workflow"
|
"create-inventory-levels-workflow"
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from "./create-inventory-levels"
|
|||||||
export * from "./update-inventory-items"
|
export * from "./update-inventory-items"
|
||||||
export * from "./delete-inventory-levels"
|
export * from "./delete-inventory-levels"
|
||||||
export * from "./update-inventory-levels"
|
export * from "./update-inventory-levels"
|
||||||
|
export * from "./bulk-create-delete-levels"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Modules } from "@medusajs/modules-sdk"
|
|
||||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
|
||||||
import { LINKS } from "@medusajs/utils"
|
import { LINKS } from "@medusajs/utils"
|
||||||
|
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||||
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
|
|
||||||
export const ProductVariantInventoryItem: ModuleJoinerConfig = {
|
export const ProductVariantInventoryItem: ModuleJoinerConfig = {
|
||||||
serviceName: LINKS.ProductVariantInventoryItem,
|
serviceName: LINKS.ProductVariantInventoryItem,
|
||||||
@@ -58,6 +58,9 @@ export const ProductVariantInventoryItem: ModuleJoinerConfig = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
serviceName: Modules.INVENTORY,
|
serviceName: Modules.INVENTORY,
|
||||||
|
fieldAlias: {
|
||||||
|
variant: "variant_link.variant",
|
||||||
|
},
|
||||||
relationship: {
|
relationship: {
|
||||||
serviceName: LINKS.ProductVariantInventoryItem,
|
serviceName: LINKS.ProductVariantInventoryItem,
|
||||||
primaryKey: "inventory_item_id",
|
primaryKey: "inventory_item_id",
|
||||||
|
|||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
AdminPostInventoryItemsItemLocationLevelsBatchReq,
|
||||||
|
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||||
|
} from "../../../../validators"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
MedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../../../../types/routing"
|
||||||
|
|
||||||
|
import { bulkCreateDeleteLevelsWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { defaultAdminInventoryItemFields } from "../../../../query-config"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: MedusaRequest<AdminPostInventoryItemsItemLocationLevelsBatchReq>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
|
||||||
|
const workflow = bulkCreateDeleteLevelsWorkflow(req.scope)
|
||||||
|
const { errors } = await workflow.run({
|
||||||
|
input: {
|
||||||
|
deletes: req.validatedBody.deletes.map((location_id) => ({
|
||||||
|
location_id,
|
||||||
|
inventory_item_id: id,
|
||||||
|
})),
|
||||||
|
creates: req.validatedBody.creates.map((c) => ({
|
||||||
|
...c,
|
||||||
|
inventory_item_id: id,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(errors) && errors[0]) {
|
||||||
|
throw errors[0].error
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemQuery = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "inventory_items",
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
fields: defaultAdminInventoryItemFields,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [inventory_item] = await remoteQuery(itemQuery)
|
||||||
|
|
||||||
|
res.status(200).json({ inventory_item })
|
||||||
|
}
|
||||||
@@ -54,9 +54,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|||||||
entryPoint: "inventory_levels",
|
entryPoint: "inventory_levels",
|
||||||
variables: {
|
variables: {
|
||||||
filters: req.filterableFields,
|
filters: req.filterableFields,
|
||||||
order: req.listConfig.order,
|
...req.remoteQueryConfig.pagination,
|
||||||
skip: req.listConfig.skip,
|
|
||||||
take: req.listConfig.take,
|
|
||||||
},
|
},
|
||||||
fields: req.remoteQueryConfig.fields,
|
fields: req.remoteQueryConfig.fields,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
AdminGetInventoryItemsParams,
|
AdminGetInventoryItemsParams,
|
||||||
AdminPostInventoryItemsInventoryItemParams,
|
AdminPostInventoryItemsInventoryItemParams,
|
||||||
AdminPostInventoryItemsInventoryItemReq,
|
AdminPostInventoryItemsInventoryItemReq,
|
||||||
|
AdminPostInventoryItemsItemLocationLevelsBatchReq,
|
||||||
AdminPostInventoryItemsItemLocationLevelsLevelParams,
|
AdminPostInventoryItemsItemLocationLevelsLevelParams,
|
||||||
AdminPostInventoryItemsItemLocationLevelsLevelReq,
|
AdminPostInventoryItemsItemLocationLevelsLevelReq,
|
||||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||||
@@ -53,6 +54,13 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/inventory-items/:id/location-levels/batch/combi",
|
||||||
|
middlewares: [
|
||||||
|
transformBody(AdminPostInventoryItemsItemLocationLevelsBatchReq),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: ["GET"],
|
method: ["GET"],
|
||||||
matcher: "/admin/inventory-items/:id/location-levels",
|
matcher: "/admin/inventory-items/:id/location-levels",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { InventoryNext } from "@medusajs/types"
|
import { InventoryNext } from "@medusajs/types"
|
||||||
|
import { defaultAdminProductsVariantFields } from "../products/query-config"
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
export const defaultAdminLocationLevelFields = [
|
export const defaultAdminLocationLevelFields = [
|
||||||
@@ -37,17 +38,18 @@ export const defaultAdminInventoryItemFields = [
|
|||||||
...defaultAdminLocationLevelFields.map(
|
...defaultAdminLocationLevelFields.map(
|
||||||
(field) => `location_levels.${field.toString()}`
|
(field) => `location_levels.${field.toString()}`
|
||||||
),
|
),
|
||||||
|
...defaultAdminProductsVariantFields
|
||||||
|
.filter((field) => !field.startsWith("*"))
|
||||||
|
.map((field) => `variant.${field}`),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const retrieveTransformQueryConfig = {
|
export const retrieveTransformQueryConfig = {
|
||||||
defaults: defaultAdminInventoryItemFields,
|
defaults: defaultAdminInventoryItemFields,
|
||||||
allowed: defaultAdminInventoryItemFields,
|
|
||||||
isList: false,
|
isList: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const retrieveLocationLevelsTransformQueryConfig = {
|
export const retrieveLocationLevelsTransformQueryConfig = {
|
||||||
defaults: defaultAdminLocationLevelFields,
|
defaults: defaultAdminLocationLevelFields,
|
||||||
allowed: defaultAdminLocationLevelFields,
|
|
||||||
isList: false,
|
isList: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,13 +127,23 @@ export class AdminPostInventoryItemsItemLocationLevelsReq {
|
|||||||
location_id: string
|
location_id: string
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
stocked_quantity: number
|
@IsOptional()
|
||||||
|
stocked_quantity?: number
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
incoming_quantity?: number
|
incoming_quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AdminPostInventoryItemsItemLocationLevelsBatchReq {
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => AdminPostInventoryItemsItemLocationLevelsReq)
|
||||||
|
creates: AdminPostInventoryItemsItemLocationLevelsReq[]
|
||||||
|
|
||||||
|
@IsString({ each: true })
|
||||||
|
deletes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
export class AdminPostInventoryItemsItemLocationLevelsParams extends FindParams {}
|
export class AdminPostInventoryItemsItemLocationLevelsParams extends FindParams {}
|
||||||
|
|
||||||
@@ -259,7 +269,13 @@ export class AdminPostInventoryItemsReq {
|
|||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdminGetInventoryItemsItemLocationLevelsParams extends FindParams {
|
// eslint-disable-next-line max-len
|
||||||
|
export class AdminGetInventoryItemsItemLocationLevelsParams extends extendedFindParamsMixin(
|
||||||
|
{
|
||||||
|
limit: 50,
|
||||||
|
offset: 0,
|
||||||
|
}
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* Location IDs to filter location levels.
|
* Location IDs to filter location levels.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export const config: MiddlewaresConfig = {
|
|||||||
...adminProductTypeRoutesMiddlewares,
|
...adminProductTypeRoutesMiddlewares,
|
||||||
...adminUploadRoutesMiddlewares,
|
...adminUploadRoutesMiddlewares,
|
||||||
...adminFulfillmentSetsRoutesMiddlewares,
|
...adminFulfillmentSetsRoutesMiddlewares,
|
||||||
|
...adminReservationRoutesMiddlewares,
|
||||||
...adminProductCategoryRoutesMiddlewares,
|
...adminProductCategoryRoutesMiddlewares,
|
||||||
...adminReservationRoutesMiddlewares,
|
...adminReservationRoutesMiddlewares,
|
||||||
...adminShippingProfilesMiddlewares,
|
...adminShippingProfilesMiddlewares,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from "./api-key"
|
export * from "./api-key"
|
||||||
export * from "./customer"
|
export * from "./customer"
|
||||||
export * from "./fulfillment"
|
export * from "./fulfillment"
|
||||||
|
export * from "./inventory"
|
||||||
export * from "./pricing"
|
export * from "./pricing"
|
||||||
export * from "./sales-channel"
|
export * from "./sales-channel"
|
||||||
export * from "./stock-locations"
|
export * from "./stock-locations"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./inventory"
|
||||||
|
export * from "./inventory-level"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface AdminInventoryLevelResponse {
|
||||||
|
inventory_item: AdminInventoryItemResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface AdminInventoryLevelListResponse extends PaginatedResponse {
|
||||||
|
inventory_levels: InventoryLevelResponse[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { PaginatedResponse } from "../common"
|
||||||
|
|
||||||
|
interface InventoryItemResponse {
|
||||||
|
id: string
|
||||||
|
sku?: string | null
|
||||||
|
origin_country?: string | null
|
||||||
|
hs_code?: string | null
|
||||||
|
requires_shipping: boolean
|
||||||
|
mid_code?: string | null
|
||||||
|
material?: string | null
|
||||||
|
weight?: number | null
|
||||||
|
length?: number | null
|
||||||
|
height?: number | null
|
||||||
|
width?: number | null
|
||||||
|
title?: string | null
|
||||||
|
description?: string | null
|
||||||
|
thumbnail?: string | null
|
||||||
|
metadata?: Record<string, unknown> | null
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface AdminInventoryItemResponse {
|
||||||
|
inventory_item: InventoryItemResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface AdminInventoryItemListResponse extends PaginatedResponse {
|
||||||
|
inventory_items: InventoryItemResponse[]
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { BaseFilterable, OperatorMap } from "../../dal"
|
import { BaseFilterable, OperatorMap } from "../../dal"
|
||||||
|
|
||||||
import { NumericalComparisonOperator } from "../../common"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The inventory level details.
|
* The inventory level details.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface CreateInventoryLevelInput {
|
|||||||
/**
|
/**
|
||||||
* The stocked quantity of the associated inventory item in the associated location.
|
* The stocked quantity of the associated inventory item in the associated location.
|
||||||
*/
|
*/
|
||||||
stocked_quantity: number
|
stocked_quantity?: number
|
||||||
/**
|
/**
|
||||||
* The reserved quantity of the associated inventory item in the associated location.
|
* The reserved quantity of the associated inventory item in the associated location.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in new issue
Block a user