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:
@@ -30,6 +30,7 @@
|
||||
"items_one": "{{count}} item",
|
||||
"items_other": "{{count}} items",
|
||||
"countSelected": "{{count}} selected",
|
||||
"countOfTotalSelected": "{{count}} of {{total}} selected",
|
||||
"plusCount": "+ {{count}}",
|
||||
"plusCountMore": "+ {{count}} more",
|
||||
"areYouSure": "Are you sure?",
|
||||
@@ -265,6 +266,7 @@
|
||||
},
|
||||
"inventory": {
|
||||
"header": "Stock & Inventory",
|
||||
"editItemDetails": "Edit item details",
|
||||
"manageInventoryLabel": "Manage inventory",
|
||||
"manageInventoryHint": "When enabled the inventory level will be regulated when orders and returns are created.",
|
||||
"allowBackordersLabel": "Allow backorders",
|
||||
@@ -311,6 +313,10 @@
|
||||
"inventory": {
|
||||
"domain": "Inventory",
|
||||
"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."
|
||||
},
|
||||
"giftCards": {
|
||||
@@ -943,6 +949,7 @@
|
||||
"addSalesChannels": "Add sales channels",
|
||||
"detailsHint": "Specify the details of the location.",
|
||||
"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.",
|
||||
"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."
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { EllipsisHorizontal } from "@medusajs/icons"
|
||||
import { DropdownMenu, IconButton } from "@medusajs/ui"
|
||||
import { ReactNode } from "react"
|
||||
|
||||
import { EllipsisHorizontal } from "@medusajs/icons"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ReactNode } from "react"
|
||||
|
||||
type Action = {
|
||||
icon: ReactNode
|
||||
|
||||
291
packages/admin-next/dashboard/src/hooks/api/inventory.tsx
Normal file
291
packages/admin-next/dashboard/src/hooks/api/inventory.tsx
Normal file
@@ -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 { customerGroups } from "./customer-groups"
|
||||
import { customers } from "./customers"
|
||||
import { inventoryItems } from "./inventory"
|
||||
import { invites } from "./invites"
|
||||
import { payments } from "./payments"
|
||||
import { priceLists } from "./price-lists"
|
||||
@@ -39,6 +40,7 @@ export const client = {
|
||||
regions: regions,
|
||||
taxes: taxes,
|
||||
invites: invites,
|
||||
inventoryItems: inventoryItems,
|
||||
products: products,
|
||||
productTypes: productTypes,
|
||||
priceLists: priceLists,
|
||||
|
||||
131
packages/admin-next/dashboard/src/lib/client/inventory.ts
Normal file
131
packages/admin-next/dashboard/src/lib/client/inventory.ts
Normal file
@@ -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 {
|
||||
AdminApiKeyResponse,
|
||||
AdminCustomerGroupResponse,
|
||||
@@ -13,14 +5,24 @@ import {
|
||||
SalesChannelDTO,
|
||||
UserDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
AdminCollectionsRes,
|
||||
AdminProductsRes,
|
||||
AdminPromotionRes,
|
||||
AdminRegionsRes,
|
||||
} from "@medusajs/medusa"
|
||||
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
|
||||
|
||||
import { AdminCustomersRes } from "@medusajs/client-types"
|
||||
import { ErrorBoundary } from "../../components/error/error-boundary"
|
||||
import { InventoryItemRes } from "../../types/api-responses"
|
||||
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 { SearchProvider } from "../search-provider"
|
||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||
import { SidebarProvider } from "../sidebar-provider"
|
||||
import { Spinner } from "@medusajs/icons"
|
||||
import { useMe } from "../../hooks/api/users"
|
||||
|
||||
export const ProtectedRoute = () => {
|
||||
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"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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 { 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 { 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 }) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -54,8 +53,8 @@ export const DetailsSection = ({ discount }: { discount: Discount }) => {
|
||||
{discount.rule.type === "percentage"
|
||||
? t("discounts.percentageDiscount")
|
||||
: discount.rule.type === "free_shipping"
|
||||
? t("discounts.freeShipping")
|
||||
: t("discounts.fixedDiscount")}
|
||||
? t("discounts.freeShipping")
|
||||
: t("discounts.fixedDiscount")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
CreateRegionDTO,
|
||||
CreateSalesChannelDTO,
|
||||
CreateStockLocationInput,
|
||||
InventoryNext,
|
||||
UpdateApiKeyDTO,
|
||||
UpdateCampaignDTO,
|
||||
UpdateCustomerDTO,
|
||||
@@ -88,3 +89,21 @@ export type BatchUpdatePromotionRulesReq = { rules: UpdatePromotionRuleDTO[] }
|
||||
// Campaign
|
||||
export type CreateCampaignReq = CreateCampaignDTO
|
||||
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,
|
||||
CurrencyDTO,
|
||||
CustomerGroupDTO,
|
||||
InventoryNext,
|
||||
InviteDTO,
|
||||
PaymentProviderDTO,
|
||||
PriceListDTO,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
StoreDTO,
|
||||
UserDTO,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { ProductTagDTO } from "@medusajs/types/dist/product"
|
||||
import { WorkflowExecutionDTO } from "../v2-routes/workflow-executions/types"
|
||||
|
||||
@@ -140,6 +142,36 @@ export type ProductCollectionListRes = {
|
||||
} & ListRes
|
||||
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
|
||||
export type PriceListRes = { price_list: PriceListDTO }
|
||||
export type PriceListListRes = { price_lists: PriceListDTO[] } & ListRes
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { AdjustInventoryDrawer as Component } from "./adjust-inventory-drawer"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { InventoryItemAttributesEdit as Component } from "./edit-item-attributes-drawer"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { InventoryItemEdit as Component } from "./edit-item-drawer"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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` : ""}`
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ManageLocationsDrawer as Component } from "./manage-locations-drawer"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user