feat(admin-next, inventory-next, medusa, types): Add admin reservations flow (#7080)
* 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 * minor fixes to region domain and api (#7042) * initial reservation * init * update reservation * create reservation * polishing * minor fix * prep for pr * prep for pr * polishing * inventory items reservations * Update packages/admin-next/dashboard/src/v2-routes/reservations/reservation-list/components/reservation-list-table/reservation-list-table.tsx Co-authored-by: Frane Polić <16856471+fPolic@users.noreply.github.com> * fix feedback * rename to ispending --------- Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Co-authored-by: Frane Polić <16856471+fPolic@users.noreply.github.com>
This commit is contained in:
@@ -190,6 +190,25 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
})
|
||||
|
||||
it("should update a reservation item description", async () => {
|
||||
await breaking(null, async () => {
|
||||
const reservationResponse = await api.post(
|
||||
`/admin/reservations/${reservationId}`,
|
||||
{
|
||||
description: "test description 1",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(reservationResponse.status).toEqual(200)
|
||||
expect(reservationResponse.data.reservation).toEqual(
|
||||
expect.objectContaining({
|
||||
description: "test description 1",
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("should update a reservation item", async () => {
|
||||
await breaking(null, async () => {
|
||||
const reservationResponse = await api.post(
|
||||
|
||||
@@ -343,7 +343,25 @@
|
||||
"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.",
|
||||
"reservation": {
|
||||
"header": "Reservation of {{itemName}}",
|
||||
"editItemDetails": "Edit item details",
|
||||
"orderID": "Order ID",
|
||||
"description": "Description",
|
||||
"location": "Location",
|
||||
"inStockAtLocation": "In stock at this location",
|
||||
"availableAtLocation": "Available at this location",
|
||||
"reservedAtLocation": "Reserved at this location",
|
||||
"reservedAmount": "Reserve amount",
|
||||
"create": "Create reservation",
|
||||
"itemToReserve": "Item to reserve",
|
||||
"quantityPlaceholder": "How many do you want to reserve?",
|
||||
"descriptionPlaceholder": "What type of reservation is this?",
|
||||
"successToast": "Reservation was successfully created.",
|
||||
"updateSuccessToast": "Reservation was successfully updated.",
|
||||
"deleteSuccessToast": "Reservation was successfully deleted."
|
||||
}
|
||||
},
|
||||
"giftCards": {
|
||||
"domain": "Gift Cards",
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { AdminInventoryItemResponse, InventoryNext } from "@medusajs/types"
|
||||
import {
|
||||
InventoryItemDeleteRes,
|
||||
InventoryItemListRes,
|
||||
InventoryItemLocationLevelsRes,
|
||||
InventoryItemRes,
|
||||
ReservationItemDeleteRes,
|
||||
ReservationItemListRes,
|
||||
ReservationItemRes,
|
||||
} from "../../types/api-responses"
|
||||
import {
|
||||
InventoryItemLocationBatch,
|
||||
@@ -20,7 +18,6 @@ import {
|
||||
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"
|
||||
@@ -35,11 +32,6 @@ 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<
|
||||
@@ -171,7 +163,7 @@ export const useUpdateInventoryItemLevel = (
|
||||
inventoryItemId: string,
|
||||
locationId: string,
|
||||
options?: UseMutationOptions<
|
||||
AdminInventoryLevelResponse,
|
||||
AdminInventoryItemResponse,
|
||||
Error,
|
||||
UpdateInventoryLevelReq
|
||||
>
|
||||
@@ -225,67 +217,3 @@ export const useBatchInventoryItemLevels = (
|
||||
...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,
|
||||
})
|
||||
}
|
||||
|
||||
119
packages/admin-next/dashboard/src/hooks/api/reservations.tsx
Normal file
119
packages/admin-next/dashboard/src/hooks/api/reservations.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
CreateReservationReq,
|
||||
UpdateReservationReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import {
|
||||
ReservationItemDeleteRes,
|
||||
ReservationItemListRes,
|
||||
ReservationItemRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
import { InventoryNext } from "@medusajs/types"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const RESERVATION_ITEMS_QUERY_KEY = "reservation_items" as const
|
||||
export const reservationItemsQueryKeys = queryKeysFactory(
|
||||
RESERVATION_ITEMS_QUERY_KEY
|
||||
)
|
||||
|
||||
export const useReservationItem = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<ReservationItemRes, Error, ReservationItemRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: reservationItemsQueryKeys.detail(id),
|
||||
queryFn: async () => client.reservations.retrieve(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useReservationItems = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
ReservationItemListRes,
|
||||
Error,
|
||||
ReservationItemListRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.reservations.list(query),
|
||||
queryKey: reservationItemsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useUpdateReservationItem = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<ReservationItemRes, Error, UpdateReservationReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: InventoryNext.UpdateReservationItemInput) =>
|
||||
client.reservations.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: reservationItemsQueryKeys.detail(id),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: reservationItemsQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateReservationItem = (
|
||||
options?: UseMutationOptions<ReservationItemRes, Error, CreateReservationReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: CreateReservationReq) =>
|
||||
client.reservations.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: reservationItemsQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteReservationItem = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<ReservationItemDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.reservations.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: reservationItemsQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: reservationItemsQueryKeys.detail(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { productTypes } from "./product-types"
|
||||
import { products } from "./products"
|
||||
import { promotions } from "./promotions"
|
||||
import { regions } from "./regions"
|
||||
import { reservations } from "./reservations"
|
||||
import { salesChannels } from "./sales-channels"
|
||||
import { shippingOptions } from "./shipping-options"
|
||||
import { stockLocations } from "./stock-locations"
|
||||
@@ -45,6 +46,7 @@ export const client = {
|
||||
taxes: taxes,
|
||||
invites: invites,
|
||||
inventoryItems: inventoryItems,
|
||||
reservations: reservations,
|
||||
products: products,
|
||||
productTypes: productTypes,
|
||||
priceLists: priceLists,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
AdminInventoryItemListResponse,
|
||||
AdminInventoryItemResponse,
|
||||
AdminInventoryLevelListResponse,
|
||||
AdminInventoryLevelResponse,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
@@ -12,9 +11,7 @@ import {
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
InventoryItemLevelDeleteRes,
|
||||
ReservationItemDeleteRes,
|
||||
ReservationItemListRes,
|
||||
ReservationItemRes,
|
||||
InventoryItemLocationLevelsRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
@@ -59,7 +56,7 @@ async function listInventoryItemLevels(
|
||||
id: string,
|
||||
query?: Record<string, any>
|
||||
) {
|
||||
return getRequest<AdminInventoryLevelListResponse>(
|
||||
return getRequest<InventoryItemLocationLevelsRes>(
|
||||
`/admin/inventory-items/${id}/location-levels`,
|
||||
query
|
||||
)
|
||||
@@ -79,32 +76,12 @@ async function updateInventoryLevel(
|
||||
locationId: string,
|
||||
payload: UpdateInventoryLevelReq
|
||||
) {
|
||||
return postRequest<AdminInventoryLevelResponse>(
|
||||
return postRequest<AdminInventoryItemResponse>(
|
||||
`/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
|
||||
@@ -127,8 +104,5 @@ export const inventoryItems = {
|
||||
listLocationLevels: listInventoryItemLevels,
|
||||
updateInventoryLevel,
|
||||
deleteInventoryItemLevel,
|
||||
listReservationItems,
|
||||
deleteReservationItem,
|
||||
updateReservationItem,
|
||||
batchPostLocationLevels,
|
||||
}
|
||||
|
||||
41
packages/admin-next/dashboard/src/lib/client/reservations.ts
Normal file
41
packages/admin-next/dashboard/src/lib/client/reservations.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
ReservationDeleteRes,
|
||||
ReservationListRes,
|
||||
ReservationRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
import { InventoryNext } from "@medusajs/types"
|
||||
|
||||
async function retrieveReservation(id: string, query?: Record<string, any>) {
|
||||
return getRequest<ReservationRes>(`/admin/reservations/${id}`, query)
|
||||
}
|
||||
|
||||
async function listReservations(query?: Record<string, any>) {
|
||||
return getRequest<ReservationListRes>(`/admin/reservations`, query)
|
||||
}
|
||||
|
||||
async function createReservation(
|
||||
payload: InventoryNext.CreateReservationItemInput
|
||||
) {
|
||||
return postRequest<ReservationRes>(`/admin/reservations`, payload)
|
||||
}
|
||||
|
||||
async function updateReservation(
|
||||
id: string,
|
||||
payload: InventoryNext.UpdateReservationItemInput
|
||||
) {
|
||||
return postRequest<ReservationRes>(`/admin/reservations/${id}`, payload)
|
||||
}
|
||||
|
||||
async function deleteReservation(id: string) {
|
||||
return deleteRequest<ReservationDeleteRes>(`/admin/reservations/${id}`)
|
||||
}
|
||||
|
||||
export const reservations = {
|
||||
retrieve: retrieveReservation,
|
||||
list: listReservations,
|
||||
create: createReservation,
|
||||
update: updateReservation,
|
||||
delete: deleteReservation,
|
||||
}
|
||||
@@ -1,10 +1,3 @@
|
||||
import { AdminCustomersRes } from "@medusajs/client-types"
|
||||
import {
|
||||
AdminCollectionsRes,
|
||||
AdminProductsRes,
|
||||
AdminPromotionRes,
|
||||
AdminRegionsRes,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
AdminApiKeyResponse,
|
||||
AdminCustomerGroupResponse,
|
||||
@@ -14,13 +7,20 @@ import {
|
||||
SalesChannelDTO,
|
||||
UserDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
AdminCollectionsRes,
|
||||
AdminProductsRes,
|
||||
AdminPromotionRes,
|
||||
AdminRegionsRes,
|
||||
} from "@medusajs/medusa"
|
||||
import { InventoryItemRes, PriceListRes } from "../../types/api-responses"
|
||||
import { Outlet, RouteObject } from "react-router-dom"
|
||||
|
||||
import { ProtectedRoute } from "../../components/authentication/protected-route"
|
||||
import { AdminCustomersRes } from "@medusajs/client-types"
|
||||
import { ErrorBoundary } from "../../components/error/error-boundary"
|
||||
import { MainLayout } from "../../components/layout/main-layout"
|
||||
import { ProtectedRoute } from "../../components/authentication/protected-route"
|
||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||
import { InventoryItemRes, PriceListRes } from "../../types/api-responses"
|
||||
|
||||
/**
|
||||
* Experimental V2 routes.
|
||||
@@ -383,7 +383,7 @@ export const v2Routes: RouteObject[] = [
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../v2-routes/customer-groups/customer-group-create"
|
||||
"../../v2-routes/reservations/reservation-list/create-reservation"
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -417,6 +417,51 @@ export const v2Routes: RouteObject[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/reservations",
|
||||
handle: {
|
||||
crumb: () => "Reservations",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/reservations/reservation-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../v2-routes/reservations/reservation-list/create-reservation"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/reservations/reservation-detail"),
|
||||
handle: {
|
||||
crumb: ({ reservation }: any) => {
|
||||
return (
|
||||
reservation?.inventory_item?.title ??
|
||||
reservation?.inventory_item?.sku ??
|
||||
reservation?.id
|
||||
)
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../v2-routes/reservations/reservation-detail/components/edit-reservation"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/inventory",
|
||||
handle: {
|
||||
@@ -437,7 +482,6 @@ export const v2Routes: RouteObject[] = [
|
||||
},
|
||||
children: [
|
||||
{
|
||||
// TODO: edit item
|
||||
path: "edit",
|
||||
lazy: () =>
|
||||
import(
|
||||
@@ -445,7 +489,6 @@ export const v2Routes: RouteObject[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
// TODO: edit item attributes
|
||||
path: "attributes",
|
||||
lazy: () =>
|
||||
import(
|
||||
@@ -453,7 +496,6 @@ export const v2Routes: RouteObject[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
// TODO: manage locations
|
||||
path: "locations",
|
||||
lazy: () =>
|
||||
import(
|
||||
@@ -461,7 +503,6 @@ export const v2Routes: RouteObject[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
// TODO: adjust item level
|
||||
path: "locations/:location_id",
|
||||
lazy: () =>
|
||||
import(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
import { ExtendedReservationItem } from "@medusajs/medusa"
|
||||
import { Container } from "@medusajs/ui"
|
||||
import { useAdminInventoryItem } from "medusa-react"
|
||||
import { useInventoryItem } from "../../../../../hooks/api/inventory"
|
||||
|
||||
type ReservationGeneralSectionProps = {
|
||||
reservation: ExtendedReservationItem
|
||||
@@ -11,11 +12,11 @@ type ReservationGeneralSectionProps = {
|
||||
export const ReservationGeneralSection = ({
|
||||
reservation,
|
||||
}: ReservationGeneralSectionProps) => {
|
||||
const { inventory_item, isLoading, isError, error } = useAdminInventoryItem(
|
||||
const { inventory_item, isPending, isError, error } = useInventoryItem(
|
||||
reservation.inventory_item_id
|
||||
)
|
||||
|
||||
if (isLoading || !inventory_item) {
|
||||
if (isPending || !inventory_item) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { ReservationDetail as Component } from "./reservation-edit"
|
||||
export { ReservationDetail as Component } from "./reservation-detail"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { ExtendedReservationItem } from "@medusajs/medusa"
|
||||
import { usePrompt } from "@medusajs/ui"
|
||||
import { useAdminDeleteReservation } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast, usePrompt } from "@medusajs/ui"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { ExtendedReservationItem } from "@medusajs/medusa"
|
||||
import { useDeleteReservationItem } from "../../../../../hooks/api/reservations"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export const ReservationActions = ({
|
||||
reservation,
|
||||
@@ -12,7 +13,7 @@ export const ReservationActions = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const { mutateAsync } = useAdminDeleteReservation(reservation.id)
|
||||
const { mutateAsync } = useDeleteReservationItem(reservation.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
@@ -26,7 +27,14 @@ export const ReservationActions = ({
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
await mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
toast.success(t("general.success"), {
|
||||
dismissLabel: t("actions.close"),
|
||||
description: t("inventory.reservation.deleteSuccessToast"),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -109,6 +109,13 @@ export type UpdateInventoryItemReq = Omit<
|
||||
"id"
|
||||
>
|
||||
|
||||
// Reservations
|
||||
export type UpdateReservationReq = Omit<
|
||||
InventoryNext.UpdateReservationItemInput,
|
||||
"id"
|
||||
>
|
||||
export type CreateReservationReq = InventoryNext.CreateReservationItemInput
|
||||
|
||||
// Inventory Item Levels
|
||||
export type InventoryItemLocationBatch = {
|
||||
creates: { location_id: string; stocked_quantity?: number }[]
|
||||
|
||||
@@ -71,6 +71,13 @@ export type RegionRes = { region: RegionDTO }
|
||||
export type RegionListRes = { regions: RegionDTO[] } & ListRes
|
||||
export type RegionDeleteRes = DeleteRes
|
||||
|
||||
// Reservations
|
||||
export type ReservationRes = { reservation: InventoryNext.ReservationItemDTO }
|
||||
export type ReservationListRes = {
|
||||
reservations: InventoryNext.ReservationItemDTO[]
|
||||
} & ListRes
|
||||
export type ReservationDeleteRes = DeleteRes
|
||||
|
||||
// Campaigns
|
||||
export type CampaignRes = { campaign: CampaignDTO }
|
||||
export type CampaignListRes = { campaigns: CampaignDTO[] } & ListRes
|
||||
|
||||
@@ -109,11 +109,11 @@ export const AdjustInventoryForm = ({
|
||||
/>
|
||||
<AttributeGridRow
|
||||
title={t("inventory.reserved")}
|
||||
value={item.reserved_quantity}
|
||||
value={level.reserved_quantity}
|
||||
/>
|
||||
<AttributeGridRow
|
||||
title={t("inventory.available")}
|
||||
value={stockedQuantityUpdate - item.reserved_quantity}
|
||||
value={stockedQuantityUpdate - level.reserved_quantity}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
|
||||
@@ -18,7 +18,7 @@ export const InventoryItemReservationsSection = ({
|
||||
<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>
|
||||
<Link to="/reservations/create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<ReservationItemTable inventoryItem={inventoryItem} />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { toast, usePrompt } from "@medusajs/ui"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { InventoryNext } from "@medusajs/types"
|
||||
import { useDeleteReservationItem } from "../../../../../hooks/api/inventory"
|
||||
import { usePrompt } from "@medusajs/ui"
|
||||
import { useDeleteReservationItem } from "../../../../../hooks/api/reservations"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export const ReservationActions = ({
|
||||
@@ -27,7 +27,14 @@ export const ReservationActions = ({
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
await mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
toast.success(t("general.success"), {
|
||||
dismissLabel: t("actions.close"),
|
||||
description: t("inventory.reservation.deleteSuccessToast"),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -38,7 +45,7 @@ export const ReservationActions = ({
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/reservation/${reservation.id}/edit`,
|
||||
to: `/reservations/${reservation.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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"
|
||||
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
||||
import { useReservationTableColumn } from "./use-reservation-list-table-columns"
|
||||
import { useReservationsTableQuery } from "./use-reservation-list-table-query"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
@@ -12,29 +12,24 @@ export const ReservationItemTable = ({
|
||||
}: {
|
||||
inventoryItem: InventoryNext.InventoryItemDTO
|
||||
}) => {
|
||||
const { searchParams, raw } = useInventoryTableQuery({
|
||||
const { searchParams, raw } = useReservationsTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const {
|
||||
reservations,
|
||||
count,
|
||||
isPending: isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useReservationItems({
|
||||
...searchParams,
|
||||
inventory_item_id: [inventoryItem.id],
|
||||
})
|
||||
const { reservations, count, isPending, isError, error } =
|
||||
useReservationItems({
|
||||
...searchParams,
|
||||
inventory_item_id: [inventoryItem.id],
|
||||
})
|
||||
|
||||
const columns = useInventoryTableColumns({ sku: inventoryItem.sku! })
|
||||
const columns = useReservationTableColumn({ sku: inventoryItem.sku! })
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: reservations ?? [],
|
||||
columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
getRowId: (row: InventoryNext.ReservationItemDTO) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
@@ -48,7 +43,7 @@ export const ReservationItemTable = ({
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
isLoading={isLoading}
|
||||
isLoading={isPending}
|
||||
pagination
|
||||
queryObject={raw}
|
||||
/>
|
||||
|
||||
@@ -9,14 +9,14 @@ import { useTranslation } from "react-i18next"
|
||||
/**
|
||||
* Adds missing properties to the InventoryItemDTO type.
|
||||
*/
|
||||
interface ExtendedInventoryItem extends InventoryNext.ReservationItemDTO {
|
||||
interface ExtendedReservationItem extends InventoryNext.ReservationItemDTO {
|
||||
line_item: { order_id: string }
|
||||
location: StockLocationDTO
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<ExtendedInventoryItem>()
|
||||
const columnHelper = createColumnHelper<ExtendedReservationItem>()
|
||||
|
||||
export const useInventoryTableColumns = ({ sku }: { sku: string }) => {
|
||||
export const useReservationTableColumn = ({ sku }: { sku: string }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
@@ -32,17 +32,17 @@ export const useInventoryTableColumns = ({ sku }: { sku: string }) => {
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("line_item.order_id", {
|
||||
header: t("inventory.reserved"),
|
||||
header: t("inventory.reservation.orderID"),
|
||||
cell: ({ getValue }) => {
|
||||
const quantity = getValue()
|
||||
const orderId = getValue()
|
||||
|
||||
if (Number.isNaN(quantity)) {
|
||||
if (!orderId) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full items-center overflow-hidden">
|
||||
<span className="truncate">{quantity}</span>
|
||||
<span className="truncate">{orderId}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -64,7 +64,7 @@ export const useInventoryTableColumns = ({ sku }: { sku: string }) => {
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("location.name", {
|
||||
header: t("inventory.location"),
|
||||
header: t("inventory.reservation.location"),
|
||||
cell: ({ getValue }) => {
|
||||
const location = getValue()
|
||||
|
||||
@@ -99,7 +99,7 @@ export const useInventoryTableColumns = ({ sku }: { sku: string }) => {
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <ReservationActions item={row.original} />,
|
||||
cell: ({ row }) => <ReservationActions reservation={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useInventoryTableQuery = ({
|
||||
export const useReservationsTableQuery = ({
|
||||
pageSize = 20,
|
||||
prefix,
|
||||
}: {
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Button, Input, Select, Text, Textarea, toast } from "@medusajs/ui"
|
||||
import { InventoryNext, 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 { useUpdateReservationItem } from "../../../../../../hooks/api/reservations"
|
||||
import { z } from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
|
||||
type EditReservationFormProps = {
|
||||
reservation: InventoryNext.ReservationItemDTO
|
||||
locations: StockLocationDTO[]
|
||||
item: InventoryItemRes["inventory_item"]
|
||||
}
|
||||
|
||||
const EditReservationSchema = z.object({
|
||||
location_id: z.string(),
|
||||
description: z.string().optional(),
|
||||
quantity: z.number().min(1),
|
||||
})
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
const getDefaultValues = (reservation: InventoryNext.ReservationItemDTO) => {
|
||||
return {
|
||||
quantity: reservation.quantity,
|
||||
location_id: reservation.location_id,
|
||||
description: reservation.description ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export const EditReservationForm = ({
|
||||
reservation,
|
||||
item,
|
||||
locations,
|
||||
}: EditReservationFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof EditReservationSchema>>({
|
||||
defaultValues: getDefaultValues(reservation),
|
||||
resolver: zodResolver(EditReservationSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync } = useUpdateReservationItem(reservation.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
mutateAsync(values as any, {
|
||||
onSuccess: () => {
|
||||
handleSuccess()
|
||||
toast.success(t("general.success"), {
|
||||
dismissLabel: t("actions.close"),
|
||||
description: t("inventory.reservation.updateSuccessToast"),
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const reservedQuantity = form.watch("quantity")
|
||||
const locationId = form.watch("location_id")
|
||||
|
||||
const level = item.location_levels!.find(
|
||||
(level: InventoryNext.InventoryLevelDTO) => level.location_id === locationId
|
||||
)
|
||||
|
||||
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="location_id"
|
||||
render={({ field: { onChange, value, ref, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("inventory.reservation.location")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(v) => {
|
||||
onChange(v)
|
||||
}}
|
||||
{...field}
|
||||
>
|
||||
<Select.Trigger ref={ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{(locations || []).map((r) => (
|
||||
<Select.Item key={r.id} value={r.id}>
|
||||
{r.name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<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 ?? item.sku!}
|
||||
/>
|
||||
<AttributeGridRow title={t("fields.sku")} value={item.sku!} />
|
||||
<AttributeGridRow
|
||||
title={t("fields.inStock")}
|
||||
value={level!.stocked_quantity}
|
||||
/>
|
||||
<AttributeGridRow
|
||||
title={t("inventory.available")}
|
||||
value={level!.stocked_quantity - reservedQuantity}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="quantity"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("inventory.reservation.reservedAmount")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={level!.available_quantity + reservation.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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.description")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea {...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 { EditReservationForm as Component } from "./edit-reservation-form"
|
||||
@@ -0,0 +1,53 @@
|
||||
import { EditReservationForm } from "./components/edit-reservation-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 { useReservationItem } from "../../../../../hooks/api/reservations"
|
||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export const ReservationEdit = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { reservation, isPending, isError, error } = useReservationItem(id!)
|
||||
const { inventory_item: inventoryItem } = useInventoryItem(
|
||||
reservation?.inventory_item_id!,
|
||||
{
|
||||
enabled: !!reservation,
|
||||
}
|
||||
)
|
||||
|
||||
const { stock_locations } = useStockLocations(
|
||||
{
|
||||
id: inventoryItem?.location_levels?.map(
|
||||
(l: InventoryNext.InventoryLevelDTO) => l.location_id
|
||||
),
|
||||
},
|
||||
{
|
||||
enabled: !!inventoryItem?.location_levels,
|
||||
}
|
||||
)
|
||||
|
||||
const ready = !isPending && reservation && inventoryItem && stock_locations
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("inventory.reservation.editItemDetails")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{ready && (
|
||||
<EditReservationForm
|
||||
locations={stock_locations}
|
||||
reservation={reservation}
|
||||
item={inventoryItem}
|
||||
/>
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ReservationEdit as Component } from "./edit-reservation-modal"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./reservation-general-section"
|
||||
@@ -0,0 +1,89 @@
|
||||
import { AdminReservationResponse, StockLocationDTO } from "@medusajs/types"
|
||||
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 { useInventoryItem } from "../../../../../hooks/api/inventory"
|
||||
import { useStockLocation } from "../../../../../hooks/api/stock-locations"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
type ReservationGeneralSectionProps = {
|
||||
reservation: AdminReservationResponse["reservation"]
|
||||
}
|
||||
|
||||
export const ReservationGeneralSection = ({
|
||||
reservation,
|
||||
}: ReservationGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { inventory_item: inventoryItem, isPending: isLoadingInventoryItem } =
|
||||
useInventoryItem(reservation.inventory_item_id)
|
||||
|
||||
const { stock_location: location, isPending: isLoadingLocation } =
|
||||
useStockLocation(reservation.location_id)
|
||||
|
||||
if (
|
||||
isLoadingInventoryItem ||
|
||||
!inventoryItem ||
|
||||
isLoadingLocation ||
|
||||
!location
|
||||
) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
const locationLevel = inventoryItem.location_levels!.find(
|
||||
(l: InventoryNext.InventoryLevelDTO) =>
|
||||
l.location_id === reservation.location_id
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>
|
||||
{t("inventory.reservation.header", {
|
||||
itemName: inventoryItem.title ?? inventoryItem.sku,
|
||||
})}
|
||||
</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<SectionRow
|
||||
title={t("inventory.reservation.orderID")}
|
||||
value={reservation.line_item_id} // TODO fetch order
|
||||
/>
|
||||
<SectionRow
|
||||
title={t("inventory.reservation.description")}
|
||||
value={reservation.description}
|
||||
/>
|
||||
<SectionRow
|
||||
title={t("inventory.reservation.location")}
|
||||
value={location?.name}
|
||||
/>
|
||||
<SectionRow
|
||||
title={t("inventory.reservation.inStockAtLocation")}
|
||||
value={locationLevel?.stocked_quantity}
|
||||
/>
|
||||
<SectionRow
|
||||
title={t("inventory.reservation.availableAtLocation")}
|
||||
value={locationLevel?.available_quantity}
|
||||
/>
|
||||
<SectionRow
|
||||
title={t("inventory.reservation.reservedAtLocation")}
|
||||
value={reservation.quantity}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ReservationDetail as Component } from "./reservation-edit"
|
||||
export { reservationItemLoader as loader } from "./loader"
|
||||
@@ -0,0 +1,20 @@
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
import { ReservationItemRes } from "../../../types/api-responses"
|
||||
import { client } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/medusa"
|
||||
import { reservationItemsQueryKeys } from "../../../hooks/api/reservations"
|
||||
|
||||
const reservationDetailQuery = (id: string) => ({
|
||||
queryKey: reservationItemsQueryKeys.detail(id),
|
||||
queryFn: async () => client.reservations.retrieve(id),
|
||||
})
|
||||
|
||||
export const reservationItemLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = reservationDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<ReservationItemRes>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { InventoryItemGeneralSection } from "../../inventory/inventory-detail/components/inventory-item-general-section"
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { ReservationGeneralSection } from "./components/reservation-general-section"
|
||||
import { reservationItemLoader } from "./loader"
|
||||
import { useReservationItem } from "../../../hooks/api/reservations"
|
||||
|
||||
export const ReservationDetail = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof reservationItemLoader>
|
||||
>
|
||||
|
||||
const { reservation, isLoading, isError, error } = useReservationItem(
|
||||
id!,
|
||||
{},
|
||||
{
|
||||
initialData,
|
||||
}
|
||||
)
|
||||
|
||||
if (isLoading || !reservation) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
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="flex w-full flex-col gap-y-2">
|
||||
<ReservationGeneralSection reservation={reservation} />
|
||||
<div className="hidden lg:block">
|
||||
<JsonViewSection data={reservation} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 lg:mt-0 lg:max-w-[400px]">
|
||||
<InventoryItemGeneralSection
|
||||
inventoryItem={reservation.inventory_item}
|
||||
/>
|
||||
<div className="lg:hidden">
|
||||
<JsonViewSection data={reservation} />
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./reservation-list-table"
|
||||
@@ -0,0 +1,57 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { ExtendedReservationItem } from "@medusajs/medusa"
|
||||
import { useDeleteReservationItem } from "../../../../../hooks/api/reservations"
|
||||
import { usePrompt } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export const ReservationActions = ({
|
||||
reservation,
|
||||
}: {
|
||||
reservation: ExtendedReservationItem
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const { mutateAsync } = useDeleteReservationItem(reservation.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("reservations.deleteWarning"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: `${reservation.id}/edit`,
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { Link } from "react-router-dom"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
||||
import { useReservationTableColumns } from "./use-reservation-table-columns"
|
||||
import { useReservationTableFilters } from "./use-reservation-table-filters"
|
||||
import { useReservationTableQuery } from "./use-reservation-table-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const ReservationListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { searchParams } = useReservationTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
const { reservations, count, isPending, isError, error } =
|
||||
useReservationItems({
|
||||
...searchParams,
|
||||
})
|
||||
|
||||
const filters = useReservationTableFilters()
|
||||
const columns = useReservationTableColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: reservations || [],
|
||||
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("reservations.domain")}</Heading>
|
||||
<Button variant="secondary" size="small" asChild>
|
||||
<Link to="create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
isLoading={isPending}
|
||||
filters={filters}
|
||||
pagination
|
||||
navigateTo={(row) => row.id}
|
||||
search={false}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { ExtendedReservationItem } from "@medusajs/medusa"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { InlineLink } from "../../../../../components/common/inline-link"
|
||||
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
|
||||
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||
import { ReservationActions } from "./reservation-actions"
|
||||
|
||||
const columnHelper = createColumnHelper<ExtendedReservationItem>()
|
||||
|
||||
export const useReservationTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("inventory_item", {
|
||||
header: t("fields.sku"),
|
||||
cell: ({ getValue }) => {
|
||||
const inventoryItem = getValue()
|
||||
|
||||
if (!inventoryItem || !inventoryItem.sku) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full items-center overflow-hidden">
|
||||
<span className="truncate">{inventoryItem.sku}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("line_item", {
|
||||
header: t("fields.order"),
|
||||
cell: ({ getValue }) => {
|
||||
const inventoryItem = getValue()
|
||||
|
||||
if (!inventoryItem || !inventoryItem.order?.display_id) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full items-center overflow-hidden">
|
||||
<InlineLink to={`/orders/${inventoryItem.order.id}`}>
|
||||
<span className="truncate">
|
||||
#{inventoryItem.order.display_id}
|
||||
</span>
|
||||
</InlineLink>
|
||||
</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("created_at", {
|
||||
header: t("fields.created"),
|
||||
cell: ({ getValue }) => {
|
||||
const created = getValue()
|
||||
|
||||
return <DateCell date={created} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("quantity", {
|
||||
header: () => (
|
||||
<div className="flex size-full items-center justify-end overflow-hidden text-right">
|
||||
<span className="truncate">{t("fields.quantity")}</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
const quantity = getValue()
|
||||
|
||||
return (
|
||||
<div className="flex size-full items-center justify-end overflow-hidden text-right">
|
||||
<span className="truncate">{quantity}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const reservation = row.original
|
||||
|
||||
return <ReservationActions reservation={reservation} />
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export const useReservationTableFilters = () => {
|
||||
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: "date",
|
||||
key: "created_at",
|
||||
label: t("fields.createdAt"),
|
||||
})
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { AdminGetReservationsParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useReservationTableQuery = ({
|
||||
pageSize = 20,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
["location_id", "offset", "created_at", "quantity", "updated_at", "order"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { location_id, created_at, updated_at, quantity, offset, ...rest } = raw
|
||||
|
||||
const searchParams: AdminGetReservationsParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? parseInt(offset) : undefined,
|
||||
location_id: location_id,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
quantity: quantity ? JSON.parse(quantity) : undefined,
|
||||
...rest,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const reservationListExpand = "line_item"
|
||||
@@ -0,0 +1,286 @@
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Button, Heading, Input, Text, Textarea, toast } from "@medusajs/ui"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../../components/route-modal"
|
||||
|
||||
import { Combobox } from "../../../../../../components/common/combobox"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { InventoryItemRes } from "../../../../../../types/api-responses"
|
||||
import { InventoryNext } from "@medusajs/types"
|
||||
import React from "react"
|
||||
import { useCreateReservationItem } from "../../../../../../hooks/api/reservations"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useInventoryItems } from "../../../../../../hooks/api/inventory"
|
||||
import { useStockLocations } from "../../../../../../hooks/api/stock-locations"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
|
||||
export const CreateReservationSchema = zod.object({
|
||||
inventory_item_id: zod.string().min(1),
|
||||
location_id: zod.string().min(1),
|
||||
quantity: zod.number().min(1),
|
||||
description: zod.string().optional(),
|
||||
})
|
||||
|
||||
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 CreateReservationForm = () => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const [inventorySearch, setInventorySearch] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateReservationSchema>>({
|
||||
defaultValues: {
|
||||
inventory_item_id: "",
|
||||
location_id: "",
|
||||
quantity: 0,
|
||||
description: "",
|
||||
},
|
||||
resolver: zodResolver(CreateReservationSchema),
|
||||
})
|
||||
|
||||
const { inventory_items } = useInventoryItems(
|
||||
{
|
||||
q: inventorySearch,
|
||||
},
|
||||
{
|
||||
enabled: !!inventorySearch,
|
||||
}
|
||||
)
|
||||
|
||||
const inventoryItemId = form.watch("inventory_item_id")
|
||||
const selectedInventoryItem = inventory_items?.find(
|
||||
(it) => it.id === inventoryItemId
|
||||
) as InventoryItemRes["inventory_item"] | undefined
|
||||
|
||||
const locationId = form.watch("location_id")
|
||||
const selectedLocationLevel = selectedInventoryItem?.location_levels?.find(
|
||||
(it) => it.location_id === locationId
|
||||
)
|
||||
|
||||
const quantity = form.watch("quantity")
|
||||
|
||||
const { stock_locations } = useStockLocations(
|
||||
{
|
||||
id:
|
||||
selectedInventoryItem?.location_levels?.map(
|
||||
(level: InventoryNext.InventoryLevelDTO) => level.location_id
|
||||
) ?? [],
|
||||
},
|
||||
{
|
||||
enabled: !!selectedInventoryItem,
|
||||
}
|
||||
)
|
||||
|
||||
const { mutateAsync, isPending } = useCreateReservationItem()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: ({ reservation }) => {
|
||||
toast.success(t("general.success"), {
|
||||
dismissLabel: t("actions.close"),
|
||||
description: t("inventory.reservation.successToast"),
|
||||
})
|
||||
handleSuccess(`/reservations/${reservation.id}`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
isLoading={isPending}
|
||||
>
|
||||
{t("actions.reservation")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex flex-col items-center pt-[72px]">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<Heading>{t("inventory.reservation.create")}</Heading>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
key={"inventory_item_id"}
|
||||
control={form.control}
|
||||
name={"inventory_item_id"}
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("inventory.reservation.itemToReserve")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
onSearchValueChange={(value: string) =>
|
||||
setInventorySearch(value)
|
||||
}
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v)
|
||||
}}
|
||||
{...field}
|
||||
options={(inventory_items ?? []).map(
|
||||
(inventoryItem) => ({
|
||||
label: inventoryItem.title ?? inventoryItem.sku!,
|
||||
value: inventoryItem.id,
|
||||
})
|
||||
)}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
key={"location_id"}
|
||||
control={form.control}
|
||||
name={"location_id"}
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.location")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
disabled={!!selectedInventoryItem}
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v)
|
||||
}}
|
||||
{...field}
|
||||
options={(stock_locations ?? []).map(
|
||||
(stockLocation) => ({
|
||||
label: stockLocation.name,
|
||||
value: stockLocation.id,
|
||||
})
|
||||
)}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<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={
|
||||
selectedInventoryItem?.title ??
|
||||
selectedInventoryItem?.sku ??
|
||||
"-"
|
||||
}
|
||||
/>
|
||||
<AttributeGridRow
|
||||
title={t("fields.sku")}
|
||||
value={selectedInventoryItem?.sku ?? "-"}
|
||||
/>
|
||||
<AttributeGridRow
|
||||
title={t("fields.inStock")}
|
||||
value={selectedLocationLevel?.stocked_quantity ?? "-"}
|
||||
/>
|
||||
<AttributeGridRow
|
||||
title={t("inventory.available")}
|
||||
value={
|
||||
selectedLocationLevel
|
||||
? selectedLocationLevel.available_quantity - quantity
|
||||
: "-"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full lg:w-1/2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="quantity"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.quantity")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t(
|
||||
"inventory.reservation.quantityPlaceholder"
|
||||
)}
|
||||
min={0}
|
||||
max={
|
||||
selectedLocationLevel
|
||||
? selectedLocationLevel.available_quantity
|
||||
: 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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.description")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"inventory.reservation.descriptionPlaceholder"
|
||||
)}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./create-reservation-form"
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CreateReservationForm } from "./components/create-reservation-form"
|
||||
import { RouteFocusModal } from "../../../../components/route-modal"
|
||||
|
||||
export const CreateReservationModal = () => {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateReservationForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateReservationModal as Component } from "./create-reservation-modal"
|
||||
@@ -0,0 +1 @@
|
||||
export { ReservationList as Component } from "./reservation-list"
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { ReservationListTable } from "./components/reservation-list-table"
|
||||
|
||||
export const ReservationList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ReservationListTable />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -676,20 +676,22 @@ export default class InventoryModuleService<
|
||||
context
|
||||
)
|
||||
|
||||
const levelAdjustmentUpdates = inventoryLevels.map((level) => {
|
||||
const adjustment = adjustments
|
||||
.get(level.inventory_item_id)
|
||||
?.get(level.location_id)
|
||||
const levelAdjustmentUpdates = inventoryLevels
|
||||
.map((level) => {
|
||||
const adjustment = adjustments
|
||||
.get(level.inventory_item_id)
|
||||
?.get(level.location_id)
|
||||
|
||||
if (!adjustment) {
|
||||
return
|
||||
}
|
||||
if (!adjustment) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
id: level.id,
|
||||
reserved_quantity: level.reserved_quantity + adjustment,
|
||||
}
|
||||
})
|
||||
return {
|
||||
id: level.id,
|
||||
reserved_quantity: level.reserved_quantity + adjustment,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
await this.inventoryLevelService_.update(levelAdjustmentUpdates, context)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defaultAdminInventoryItemFields } from "../inventory-items/query-config"
|
||||
|
||||
export const defaultAdminReservationFields = [
|
||||
"id",
|
||||
"location_id",
|
||||
@@ -8,6 +10,7 @@ export const defaultAdminReservationFields = [
|
||||
"metadata",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
...defaultAdminInventoryItemFields.map((f) => `inventory_item.${f}`),
|
||||
]
|
||||
|
||||
export const retrieveTransformQueryConfig = {
|
||||
|
||||
@@ -10,3 +10,5 @@ export * from "./promotion"
|
||||
export * from "./sales-channel"
|
||||
export * from "./stock-locations"
|
||||
export * from "./tax"
|
||||
export * from "./product-category"
|
||||
export * from "./reservation"
|
||||
|
||||
1
packages/types/src/http/reservation/admin/index.ts
Normal file
1
packages/types/src/http/reservation/admin/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./reservation"
|
||||
34
packages/types/src/http/reservation/admin/reservation.ts
Normal file
34
packages/types/src/http/reservation/admin/reservation.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PaginatedResponse } from "../../../common"
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
interface ReservationResponse {
|
||||
id: string
|
||||
line_item_id: string | null
|
||||
location_id: string
|
||||
quantity: string
|
||||
external_id: string | null
|
||||
description: string | null
|
||||
inventory_item_id: string
|
||||
inventory_item: Record<string, unknown> // TODO: add InventoryItemResponse
|
||||
metadata?: Record<string, unknown>
|
||||
created_by?: string | null
|
||||
deleted_at?: Date | string | null
|
||||
created_at?: Date | string
|
||||
updated_at?: Date | string
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface AdminReservationResponse {
|
||||
reservation: ReservationResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export interface AdminReservationListResponse extends PaginatedResponse {
|
||||
reservations: ReservationResponse[]
|
||||
}
|
||||
1
packages/types/src/http/reservation/index.ts
Normal file
1
packages/types/src/http/reservation/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./admin"
|
||||
Reference in New Issue
Block a user