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:
co-authored by
Frane Polić
Kasper Fabricius Kristensen
parent
347aece924
commit
e4898fb00d
@@ -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 () => {
|
it("should update a reservation item", async () => {
|
||||||
await breaking(null, async () => {
|
await breaking(null, async () => {
|
||||||
const reservationResponse = await api.post(
|
const reservationResponse = await api.post(
|
||||||
|
|||||||
@@ -343,7 +343,25 @@
|
|||||||
"locationLevels": "Location levels",
|
"locationLevels": "Location levels",
|
||||||
"associatedVariants": "Associated variants",
|
"associatedVariants": "Associated variants",
|
||||||
"manageLocations": "Manage locations",
|
"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": {
|
"giftCards": {
|
||||||
"domain": "Gift Cards",
|
"domain": "Gift Cards",
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import { AdminInventoryItemResponse, InventoryNext } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
InventoryItemDeleteRes,
|
InventoryItemDeleteRes,
|
||||||
InventoryItemListRes,
|
InventoryItemListRes,
|
||||||
InventoryItemLocationLevelsRes,
|
InventoryItemLocationLevelsRes,
|
||||||
InventoryItemRes,
|
InventoryItemRes,
|
||||||
ReservationItemDeleteRes,
|
|
||||||
ReservationItemListRes,
|
|
||||||
ReservationItemRes,
|
|
||||||
} from "../../types/api-responses"
|
} from "../../types/api-responses"
|
||||||
import {
|
import {
|
||||||
InventoryItemLocationBatch,
|
InventoryItemLocationBatch,
|
||||||
@@ -20,7 +18,6 @@ import {
|
|||||||
useQuery,
|
useQuery,
|
||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
|
|
||||||
import { InventoryNext } from "@medusajs/types"
|
|
||||||
import { client } from "../../lib/client"
|
import { client } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/medusa"
|
import { queryClient } from "../../lib/medusa"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
@@ -35,11 +32,6 @@ export const inventoryItemLevelsQueryKeys = queryKeysFactory(
|
|||||||
INVENTORY_ITEM_LEVELS_QUERY_KEY
|
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 = (
|
export const useInventoryItems = (
|
||||||
query?: Record<string, any>,
|
query?: Record<string, any>,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
@@ -171,7 +163,7 @@ export const useUpdateInventoryItemLevel = (
|
|||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationId: string,
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
AdminInventoryLevelResponse,
|
AdminInventoryItemResponse,
|
||||||
Error,
|
Error,
|
||||||
UpdateInventoryLevelReq
|
UpdateInventoryLevelReq
|
||||||
>
|
>
|
||||||
@@ -225,67 +217,3 @@ export const useBatchInventoryItemLevels = (
|
|||||||
...options,
|
...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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 { products } from "./products"
|
||||||
import { promotions } from "./promotions"
|
import { promotions } from "./promotions"
|
||||||
import { regions } from "./regions"
|
import { regions } from "./regions"
|
||||||
|
import { reservations } from "./reservations"
|
||||||
import { salesChannels } from "./sales-channels"
|
import { salesChannels } from "./sales-channels"
|
||||||
import { shippingOptions } from "./shipping-options"
|
import { shippingOptions } from "./shipping-options"
|
||||||
import { stockLocations } from "./stock-locations"
|
import { stockLocations } from "./stock-locations"
|
||||||
@@ -45,6 +46,7 @@ export const client = {
|
|||||||
taxes: taxes,
|
taxes: taxes,
|
||||||
invites: invites,
|
invites: invites,
|
||||||
inventoryItems: inventoryItems,
|
inventoryItems: inventoryItems,
|
||||||
|
reservations: reservations,
|
||||||
products: products,
|
products: products,
|
||||||
productTypes: productTypes,
|
productTypes: productTypes,
|
||||||
priceLists: priceLists,
|
priceLists: priceLists,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AdminInventoryItemListResponse,
|
AdminInventoryItemListResponse,
|
||||||
AdminInventoryItemResponse,
|
AdminInventoryItemResponse,
|
||||||
AdminInventoryLevelListResponse,
|
|
||||||
AdminInventoryLevelResponse,
|
AdminInventoryLevelResponse,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
@@ -12,9 +11,7 @@ import {
|
|||||||
} from "../../types/api-payloads"
|
} from "../../types/api-payloads"
|
||||||
import {
|
import {
|
||||||
InventoryItemLevelDeleteRes,
|
InventoryItemLevelDeleteRes,
|
||||||
ReservationItemDeleteRes,
|
InventoryItemLocationLevelsRes,
|
||||||
ReservationItemListRes,
|
|
||||||
ReservationItemRes,
|
|
||||||
} from "../../types/api-responses"
|
} from "../../types/api-responses"
|
||||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||||
|
|
||||||
@@ -59,7 +56,7 @@ async function listInventoryItemLevels(
|
|||||||
id: string,
|
id: string,
|
||||||
query?: Record<string, any>
|
query?: Record<string, any>
|
||||||
) {
|
) {
|
||||||
return getRequest<AdminInventoryLevelListResponse>(
|
return getRequest<InventoryItemLocationLevelsRes>(
|
||||||
`/admin/inventory-items/${id}/location-levels`,
|
`/admin/inventory-items/${id}/location-levels`,
|
||||||
query
|
query
|
||||||
)
|
)
|
||||||
@@ -79,32 +76,12 @@ async function updateInventoryLevel(
|
|||||||
locationId: string,
|
locationId: string,
|
||||||
payload: UpdateInventoryLevelReq
|
payload: UpdateInventoryLevelReq
|
||||||
) {
|
) {
|
||||||
return postRequest<AdminInventoryLevelResponse>(
|
return postRequest<AdminInventoryItemResponse>(
|
||||||
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
|
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
|
||||||
payload
|
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(
|
async function batchPostLocationLevels(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
payload: InventoryItemLocationBatch
|
payload: InventoryItemLocationBatch
|
||||||
@@ -127,8 +104,5 @@ export const inventoryItems = {
|
|||||||
listLocationLevels: listInventoryItemLevels,
|
listLocationLevels: listInventoryItemLevels,
|
||||||
updateInventoryLevel,
|
updateInventoryLevel,
|
||||||
deleteInventoryItemLevel,
|
deleteInventoryItemLevel,
|
||||||
listReservationItems,
|
|
||||||
deleteReservationItem,
|
|
||||||
updateReservationItem,
|
|
||||||
batchPostLocationLevels,
|
batchPostLocationLevels,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
AdminApiKeyResponse,
|
AdminApiKeyResponse,
|
||||||
AdminCustomerGroupResponse,
|
AdminCustomerGroupResponse,
|
||||||
@@ -14,13 +7,20 @@ import {
|
|||||||
SalesChannelDTO,
|
SalesChannelDTO,
|
||||||
UserDTO,
|
UserDTO,
|
||||||
} from "@medusajs/types"
|
} 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 { 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 { ErrorBoundary } from "../../components/error/error-boundary"
|
||||||
import { MainLayout } from "../../components/layout/main-layout"
|
import { MainLayout } from "../../components/layout/main-layout"
|
||||||
|
import { ProtectedRoute } from "../../components/authentication/protected-route"
|
||||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||||
import { InventoryItemRes, PriceListRes } from "../../types/api-responses"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Experimental V2 routes.
|
* Experimental V2 routes.
|
||||||
@@ -383,7 +383,7 @@ export const v2Routes: RouteObject[] = [
|
|||||||
path: "create",
|
path: "create",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
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",
|
path: "/inventory",
|
||||||
handle: {
|
handle: {
|
||||||
@@ -437,7 +482,6 @@ export const v2Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
// TODO: edit item
|
|
||||||
path: "edit",
|
path: "edit",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import(
|
||||||
@@ -445,7 +489,6 @@ export const v2Routes: RouteObject[] = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// TODO: edit item attributes
|
|
||||||
path: "attributes",
|
path: "attributes",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import(
|
||||||
@@ -453,7 +496,6 @@ export const v2Routes: RouteObject[] = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// TODO: manage locations
|
|
||||||
path: "locations",
|
path: "locations",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import(
|
||||||
@@ -461,7 +503,6 @@ export const v2Routes: RouteObject[] = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// TODO: adjust item level
|
|
||||||
path: "locations/:location_id",
|
path: "locations/:location_id",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import(
|
||||||
|
|||||||
+5
-4
@@ -1,6 +1,7 @@
|
|||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
|
||||||
import { ExtendedReservationItem } from "@medusajs/medusa"
|
import { ExtendedReservationItem } from "@medusajs/medusa"
|
||||||
import { Container } from "@medusajs/ui"
|
import { useInventoryItem } from "../../../../../hooks/api/inventory"
|
||||||
import { useAdminInventoryItem } from "medusa-react"
|
|
||||||
|
|
||||||
type ReservationGeneralSectionProps = {
|
type ReservationGeneralSectionProps = {
|
||||||
reservation: ExtendedReservationItem
|
reservation: ExtendedReservationItem
|
||||||
@@ -11,11 +12,11 @@ type ReservationGeneralSectionProps = {
|
|||||||
export const ReservationGeneralSection = ({
|
export const ReservationGeneralSection = ({
|
||||||
reservation,
|
reservation,
|
||||||
}: ReservationGeneralSectionProps) => {
|
}: ReservationGeneralSectionProps) => {
|
||||||
const { inventory_item, isLoading, isError, error } = useAdminInventoryItem(
|
const { inventory_item, isPending, isError, error } = useInventoryItem(
|
||||||
reservation.inventory_item_id
|
reservation.inventory_item_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isLoading || !inventory_item) {
|
if (isPending || !inventory_item) {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { ReservationDetail as Component } from "./reservation-edit"
|
export { ReservationDetail as Component } from "./reservation-detail"
|
||||||
|
|||||||
+14
-6
@@ -1,9 +1,10 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { ExtendedReservationItem } from "@medusajs/medusa"
|
import { toast, usePrompt } from "@medusajs/ui"
|
||||||
import { usePrompt } from "@medusajs/ui"
|
|
||||||
import { useAdminDeleteReservation } from "medusa-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
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 = ({
|
export const ReservationActions = ({
|
||||||
reservation,
|
reservation,
|
||||||
@@ -12,7 +13,7 @@ export const ReservationActions = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const { mutateAsync } = useAdminDeleteReservation(reservation.id)
|
const { mutateAsync } = useDeleteReservationItem(reservation.id)
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
@@ -26,7 +27,14 @@ export const ReservationActions = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await mutateAsync()
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
description: t("inventory.reservation.deleteSuccessToast"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -109,6 +109,13 @@ export type UpdateInventoryItemReq = Omit<
|
|||||||
"id"
|
"id"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
// Reservations
|
||||||
|
export type UpdateReservationReq = Omit<
|
||||||
|
InventoryNext.UpdateReservationItemInput,
|
||||||
|
"id"
|
||||||
|
>
|
||||||
|
export type CreateReservationReq = InventoryNext.CreateReservationItemInput
|
||||||
|
|
||||||
// Inventory Item Levels
|
// Inventory Item Levels
|
||||||
export type InventoryItemLocationBatch = {
|
export type InventoryItemLocationBatch = {
|
||||||
creates: { location_id: string; stocked_quantity?: number }[]
|
creates: { location_id: string; stocked_quantity?: number }[]
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ export type RegionRes = { region: RegionDTO }
|
|||||||
export type RegionListRes = { regions: RegionDTO[] } & ListRes
|
export type RegionListRes = { regions: RegionDTO[] } & ListRes
|
||||||
export type RegionDeleteRes = DeleteRes
|
export type RegionDeleteRes = DeleteRes
|
||||||
|
|
||||||
|
// Reservations
|
||||||
|
export type ReservationRes = { reservation: InventoryNext.ReservationItemDTO }
|
||||||
|
export type ReservationListRes = {
|
||||||
|
reservations: InventoryNext.ReservationItemDTO[]
|
||||||
|
} & ListRes
|
||||||
|
export type ReservationDeleteRes = DeleteRes
|
||||||
|
|
||||||
// Campaigns
|
// Campaigns
|
||||||
export type CampaignRes = { campaign: CampaignDTO }
|
export type CampaignRes = { campaign: CampaignDTO }
|
||||||
export type CampaignListRes = { campaigns: CampaignDTO[] } & ListRes
|
export type CampaignListRes = { campaigns: CampaignDTO[] } & ListRes
|
||||||
|
|||||||
+2
-2
@@ -109,11 +109,11 @@ export const AdjustInventoryForm = ({
|
|||||||
/>
|
/>
|
||||||
<AttributeGridRow
|
<AttributeGridRow
|
||||||
title={t("inventory.reserved")}
|
title={t("inventory.reserved")}
|
||||||
value={item.reserved_quantity}
|
value={level.reserved_quantity}
|
||||||
/>
|
/>
|
||||||
<AttributeGridRow
|
<AttributeGridRow
|
||||||
title={t("inventory.available")}
|
title={t("inventory.available")}
|
||||||
value={stockedQuantityUpdate - item.reserved_quantity}
|
value={stockedQuantityUpdate - level.reserved_quantity}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Form.Field
|
<Form.Field
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ export const InventoryItemReservationsSection = ({
|
|||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading>{t("reservations.domain")}</Heading>
|
<Heading>{t("reservations.domain")}</Heading>
|
||||||
<Button size="small" variant="secondary" asChild>
|
<Button size="small" variant="secondary" asChild>
|
||||||
<Link to="locations">{t("actions.create")}</Link>
|
<Link to="/reservations/create">{t("actions.create")}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ReservationItemTable inventoryItem={inventoryItem} />
|
<ReservationItemTable inventoryItem={inventoryItem} />
|
||||||
|
|||||||
+11
-4
@@ -1,9 +1,9 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { toast, usePrompt } from "@medusajs/ui"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { InventoryNext } from "@medusajs/types"
|
import { InventoryNext } from "@medusajs/types"
|
||||||
import { useDeleteReservationItem } from "../../../../../hooks/api/inventory"
|
import { useDeleteReservationItem } from "../../../../../hooks/api/reservations"
|
||||||
import { usePrompt } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
export const ReservationActions = ({
|
export const ReservationActions = ({
|
||||||
@@ -27,7 +27,14 @@ export const ReservationActions = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await mutateAsync()
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
description: t("inventory.reservation.deleteSuccessToast"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -38,7 +45,7 @@ export const ReservationActions = ({
|
|||||||
{
|
{
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
label: t("actions.edit"),
|
label: t("actions.edit"),
|
||||||
to: `/reservation/${reservation.id}/edit`,
|
to: `/reservations/${reservation.id}/edit`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-17
@@ -1,9 +1,9 @@
|
|||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { InventoryNext } from "@medusajs/types"
|
import { InventoryNext } from "@medusajs/types"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useInventoryTableColumns } from "./use-reservation-list-table-columns"
|
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
||||||
import { useInventoryTableQuery } from "./use-reservation-list-table-query"
|
import { useReservationTableColumn } from "./use-reservation-list-table-columns"
|
||||||
import { useReservationItems } from "../../../../../hooks/api/inventory"
|
import { useReservationsTableQuery } from "./use-reservation-list-table-query"
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
@@ -12,29 +12,24 @@ export const ReservationItemTable = ({
|
|||||||
}: {
|
}: {
|
||||||
inventoryItem: InventoryNext.InventoryItemDTO
|
inventoryItem: InventoryNext.InventoryItemDTO
|
||||||
}) => {
|
}) => {
|
||||||
const { searchParams, raw } = useInventoryTableQuery({
|
const { searchParams, raw } = useReservationsTableQuery({
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const { reservations, count, isPending, isError, error } =
|
||||||
reservations,
|
useReservationItems({
|
||||||
count,
|
...searchParams,
|
||||||
isPending: isLoading,
|
inventory_item_id: [inventoryItem.id],
|
||||||
isError,
|
})
|
||||||
error,
|
|
||||||
} = useReservationItems({
|
|
||||||
...searchParams,
|
|
||||||
inventory_item_id: [inventoryItem.id],
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns = useInventoryTableColumns({ sku: inventoryItem.sku! })
|
const columns = useReservationTableColumn({ sku: inventoryItem.sku! })
|
||||||
|
|
||||||
const { table } = useDataTable({
|
const { table } = useDataTable({
|
||||||
data: reservations ?? [],
|
data: reservations ?? [],
|
||||||
columns,
|
columns,
|
||||||
count,
|
count,
|
||||||
enablePagination: true,
|
enablePagination: true,
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row: InventoryNext.ReservationItemDTO) => row.id,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -48,7 +43,7 @@ export const ReservationItemTable = ({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
count={count}
|
count={count}
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
pagination
|
pagination
|
||||||
queryObject={raw}
|
queryObject={raw}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-9
@@ -9,14 +9,14 @@ import { useTranslation } from "react-i18next"
|
|||||||
/**
|
/**
|
||||||
* Adds missing properties to the InventoryItemDTO type.
|
* Adds missing properties to the InventoryItemDTO type.
|
||||||
*/
|
*/
|
||||||
interface ExtendedInventoryItem extends InventoryNext.ReservationItemDTO {
|
interface ExtendedReservationItem extends InventoryNext.ReservationItemDTO {
|
||||||
line_item: { order_id: string }
|
line_item: { order_id: string }
|
||||||
location: StockLocationDTO
|
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()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
@@ -32,17 +32,17 @@ export const useInventoryTableColumns = ({ sku }: { sku: string }) => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("line_item.order_id", {
|
columnHelper.accessor("line_item.order_id", {
|
||||||
header: t("inventory.reserved"),
|
header: t("inventory.reservation.orderID"),
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const quantity = getValue()
|
const orderId = getValue()
|
||||||
|
|
||||||
if (Number.isNaN(quantity)) {
|
if (!orderId) {
|
||||||
return <PlaceholderCell />
|
return <PlaceholderCell />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center overflow-hidden">
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
<span className="truncate">{quantity}</span>
|
<span className="truncate">{orderId}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -64,7 +64,7 @@ export const useInventoryTableColumns = ({ sku }: { sku: string }) => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("location.name", {
|
columnHelper.accessor("location.name", {
|
||||||
header: t("inventory.location"),
|
header: t("inventory.reservation.location"),
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const location = getValue()
|
const location = getValue()
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export const useInventoryTableColumns = ({ sku }: { sku: string }) => {
|
|||||||
}),
|
}),
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => <ReservationActions item={row.original} />,
|
cell: ({ row }) => <ReservationActions reservation={row.original} />,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[t]
|
[t]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
|
||||||
export const useInventoryTableQuery = ({
|
export const useReservationsTableQuery = ({
|
||||||
pageSize = 20,
|
pageSize = 20,
|
||||||
prefix,
|
prefix,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
+208
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { EditReservationForm as Component } from "./edit-reservation-form"
|
||||||
+53
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ReservationEdit as Component } from "./edit-reservation-modal"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./reservation-general-section"
|
||||||
+89
@@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
+53
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./reservation-list-table"
|
||||||
+57
@@ -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 />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+62
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+104
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+35
@@ -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
|
||||||
|
}
|
||||||
+32
@@ -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"
|
||||||
+286
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-reservation-form"
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { CreateReservationForm } from "./components/create-reservation-form"
|
||||||
|
import { RouteFocusModal } from "../../../../components/route-modal"
|
||||||
|
|
||||||
|
export const CreateReservationModal = () => {
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
<CreateReservationForm />
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { CreateReservationModal as Component } from "./create-reservation-modal"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ReservationList as Component } from "./reservation-list"
|
||||||
+11
@@ -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
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
const levelAdjustmentUpdates = inventoryLevels.map((level) => {
|
const levelAdjustmentUpdates = inventoryLevels
|
||||||
const adjustment = adjustments
|
.map((level) => {
|
||||||
.get(level.inventory_item_id)
|
const adjustment = adjustments
|
||||||
?.get(level.location_id)
|
.get(level.inventory_item_id)
|
||||||
|
?.get(level.location_id)
|
||||||
|
|
||||||
if (!adjustment) {
|
if (!adjustment) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: level.id,
|
id: level.id,
|
||||||
reserved_quantity: level.reserved_quantity + adjustment,
|
reserved_quantity: level.reserved_quantity + adjustment,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
await this.inventoryLevelService_.update(levelAdjustmentUpdates, context)
|
await this.inventoryLevelService_.update(levelAdjustmentUpdates, context)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { defaultAdminInventoryItemFields } from "../inventory-items/query-config"
|
||||||
|
|
||||||
export const defaultAdminReservationFields = [
|
export const defaultAdminReservationFields = [
|
||||||
"id",
|
"id",
|
||||||
"location_id",
|
"location_id",
|
||||||
@@ -8,6 +10,7 @@ export const defaultAdminReservationFields = [
|
|||||||
"metadata",
|
"metadata",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
...defaultAdminInventoryItemFields.map((f) => `inventory_item.${f}`),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const retrieveTransformQueryConfig = {
|
export const retrieveTransformQueryConfig = {
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ export * from "./promotion"
|
|||||||
export * from "./sales-channel"
|
export * from "./sales-channel"
|
||||||
export * from "./stock-locations"
|
export * from "./stock-locations"
|
||||||
export * from "./tax"
|
export * from "./tax"
|
||||||
|
export * from "./product-category"
|
||||||
|
export * from "./reservation"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./reservation"
|
||||||
@@ -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[]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./admin"
|
||||||
Reference in New Issue
Block a user