feat(admin-ui,medusa): Reservations management (#4081)
* add location filtering to list-location levels * cleanup * add location filtering to list-location levels * cleanup * Initial work on route,table,new reservation form * generated types * add block * udpate clients * initial create reservation * update actionables for reservation table * update edit-allocation modal * misc naming updates * update reservations table * add expand capabilities for list-reservations * expand fields and show columns * update oas * make remove item work in focus modal * add yarn lock * add integration test * Fix display when label doesn't match search term * remove unused file * Update packages/admin-ui/ui/src/components/templates/reservations-table/components/reservation-form/index.tsx Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * Update packages/admin-ui/ui/src/domain/orders/details/allocations/edit-allocation-modal.tsx Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * Update packages/admin-ui/ui/src/components/templates/reservations-table/new/index.tsx Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * initial changes * add changeset * update font size * cleanup reservations table + select * add decorated inventory item type * use type * feedback changes * Update packages/admin-ui/ui/src/components/molecules/item-search/index.tsx Co-authored-by: Riqwan Thamir <rmthamir@gmail.com> * decorate response for list inventory item to include total quantities * update decorated properties * decorate type * adrien feedback * Update packages/generated/client-types/src/lib/models/DecoratedInventoryItemDTO.ts Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * update join-utils * fix caching --------- Co-authored-by: Rares Capilnar <rares.capilnar@gmail.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
@@ -222,6 +222,38 @@ export type AdminInventoryItemsListRes = PaginatedResponse & {
|
||||
inventory_items: InventoryItemDTO[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema DecoratedInventoryItemDTO
|
||||
* type: object
|
||||
* allOf:
|
||||
* - $ref: "#/components/schemas/InventoryItemDTO"
|
||||
* - type: object
|
||||
* required:
|
||||
* - stocked_quantity
|
||||
* - reserved_quantity
|
||||
* properties:
|
||||
* location_levels:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/InventoryLevelDTO"
|
||||
* variants:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/ProductVariant"
|
||||
* stocked_quantity:
|
||||
* type: number
|
||||
* description: The total quantity of the item in stock across levels
|
||||
* reserved_quantity:
|
||||
* type: number
|
||||
* description: The total quantity of the item available across levels
|
||||
*/
|
||||
export type DecoratedInventoryItemDTO = InventoryItemDTO & {
|
||||
location_levels?: InventoryLevelDTO[]
|
||||
variants?: ProductVariant[]
|
||||
stocked_quantity: number
|
||||
reserved_quantity: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema AdminInventoryItemsListWithVariantsAndLocationLevelsRes
|
||||
* type: object
|
||||
@@ -234,20 +266,7 @@ export type AdminInventoryItemsListRes = PaginatedResponse & {
|
||||
* inventory_items:
|
||||
* type: array
|
||||
* items:
|
||||
* allOf:
|
||||
* - $ref: "#/components/schemas/InventoryItemDTO"
|
||||
* - type: object
|
||||
* properties:
|
||||
* location_levels:
|
||||
* type: array
|
||||
* items:
|
||||
* allOf:
|
||||
* - $ref: "#/components/schemas/InventoryLevelDTO"
|
||||
* variants:
|
||||
* type: array
|
||||
* items:
|
||||
* allOf:
|
||||
* - $ref: "#/components/schemas/ProductVariant"
|
||||
* $ref: "#/components/schemas/DecoratedInventoryItemDTO"
|
||||
* count:
|
||||
* type: integer
|
||||
* description: The total number of items available
|
||||
@@ -260,11 +279,9 @@ export type AdminInventoryItemsListRes = PaginatedResponse & {
|
||||
*/
|
||||
export type AdminInventoryItemsListWithVariantsAndLocationLevelsRes =
|
||||
PaginatedResponse & {
|
||||
inventory_items: (Partial<InventoryItemDTO> & {
|
||||
location_levels?: InventoryLevelDTO[]
|
||||
variants?: ProductVariant[]
|
||||
})[]
|
||||
inventory_items: DecoratedInventoryItemDTO[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema AdminInventoryItemsLocationLevelsRes
|
||||
* type: object
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import { Transform } from "class-transformer"
|
||||
import { IsBoolean, IsOptional, IsString } from "class-validator"
|
||||
import { Request, Response } from "express"
|
||||
import {
|
||||
NumericalComparisonOperator,
|
||||
StringComparisonOperator,
|
||||
extendedFindParamsMixin,
|
||||
} from "../../../../types/common"
|
||||
import {
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
} from "../../../../services"
|
||||
import { Request, Response } from "express"
|
||||
import { getLevelsByInventoryItemId, joinLevels } from "./utils/join-levels"
|
||||
import {
|
||||
extendedFindParamsMixin,
|
||||
NumericalComparisonOperator,
|
||||
StringComparisonOperator,
|
||||
} from "../../../../types/common"
|
||||
getVariantsByInventoryItemId,
|
||||
joinVariants,
|
||||
} from "./utils/join-variants"
|
||||
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
import { getLevelsByInventoryItemId } from "./utils/join-levels"
|
||||
import { getVariantsByInventoryItemId } from "./utils/join-variants"
|
||||
import { Transform } from "class-transformer"
|
||||
|
||||
/**
|
||||
* @oas [get] /admin/inventory-items
|
||||
@@ -117,30 +121,16 @@ export default async (req: Request, res: Response) => {
|
||||
listConfig
|
||||
)
|
||||
|
||||
const levelsByItemId = await getLevelsByInventoryItemId(
|
||||
inventoryItems,
|
||||
locationIds,
|
||||
inventoryService
|
||||
)
|
||||
|
||||
const variantsByInventoryItemId = await getVariantsByInventoryItemId(
|
||||
const inventory_items = await joinVariants(
|
||||
inventoryItems,
|
||||
productVariantInventoryService,
|
||||
productVariantService
|
||||
)
|
||||
|
||||
const inventoryItemsWithVariantsAndLocationLevels = inventoryItems.map(
|
||||
(inventoryItem) => {
|
||||
return {
|
||||
...inventoryItem,
|
||||
variants: variantsByInventoryItemId[inventoryItem.id] ?? [],
|
||||
location_levels: levelsByItemId[inventoryItem.id] ?? [],
|
||||
}
|
||||
}
|
||||
)
|
||||
).then(async (res) => {
|
||||
return await joinLevels(res, locationIds, inventoryService)
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
inventory_items: inventoryItemsWithVariantsAndLocationLevels,
|
||||
inventory_items,
|
||||
count,
|
||||
offset: skip,
|
||||
limit: take,
|
||||
|
||||
@@ -62,8 +62,22 @@ export const joinLevels = async (
|
||||
inventoryService
|
||||
)
|
||||
|
||||
return inventoryItems.map((inventoryItem) => ({
|
||||
...inventoryItem,
|
||||
location_levels: levelsByItemId[inventoryItem.id] || [],
|
||||
}))
|
||||
return inventoryItems.map((inventoryItem) => {
|
||||
const levels = levelsByItemId[inventoryItem.id] ?? []
|
||||
const itemAvailability = levels.reduce(
|
||||
(acc, curr) => {
|
||||
return {
|
||||
reserved_quantity: acc.reserved_quantity + curr.reserved_quantity,
|
||||
stocked_quantity: acc.stocked_quantity + curr.stocked_quantity,
|
||||
}
|
||||
},
|
||||
{ reserved_quantity: 0, stocked_quantity: 0 }
|
||||
)
|
||||
|
||||
return {
|
||||
...inventoryItem,
|
||||
...itemAvailability,
|
||||
location_levels: levels,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { InventoryItemDTO } from "@medusajs/types"
|
||||
import { ProductVariant } from "../../../../../models"
|
||||
import {
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
} from "../../../../../services"
|
||||
|
||||
import { InventoryItemDTO } from "@medusajs/types"
|
||||
import { ProductVariant } from "../../../../../models"
|
||||
|
||||
export type InventoryItemsWithVariants = Partial<InventoryItemDTO> & {
|
||||
variants?: ProductVariant[]
|
||||
}
|
||||
@@ -34,3 +35,22 @@ export const getVariantsByInventoryItemId = async (
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const joinVariants = async (
|
||||
inventoryItems: InventoryItemDTO[],
|
||||
productVariantInventoryService: ProductVariantInventoryService,
|
||||
productVariantService: ProductVariantService
|
||||
) => {
|
||||
const variantsByInventoryItemId = await getVariantsByInventoryItemId(
|
||||
inventoryItems,
|
||||
productVariantInventoryService,
|
||||
productVariantService
|
||||
)
|
||||
|
||||
return inventoryItems.map((inventoryItem) => {
|
||||
return {
|
||||
...inventoryItem,
|
||||
variants: variantsByInventoryItemId[inventoryItem.id] ?? [],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ export default async (req, res) => {
|
||||
*/
|
||||
export class AdminPostReservationsReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
line_item_id?: string
|
||||
|
||||
@IsString()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import { InventoryItemDTO, ReservationItemDTO } from "@medusajs/types"
|
||||
import middlewares, {
|
||||
transformBody,
|
||||
transformQuery,
|
||||
@@ -7,7 +8,7 @@ import middlewares, {
|
||||
import { AdminGetReservationsParams } from "./list-reservations"
|
||||
import { AdminPostReservationsReq } from "./create-reservation"
|
||||
import { AdminPostReservationsReservationReq } from "./update-reservation"
|
||||
import { ReservationItemDTO } from "@medusajs/types"
|
||||
import { LineItem } from "../../../../models"
|
||||
import { Router } from "express"
|
||||
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
|
||||
|
||||
@@ -68,6 +69,25 @@ export type AdminReservationsRes = {
|
||||
reservation: ReservationItemDTO
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema ExtendedReservationItem
|
||||
* type: object
|
||||
* allOf:
|
||||
* - $ref: "#/components/schemas/ReservationItemDTO"
|
||||
* - type: object
|
||||
* properties:
|
||||
* line_item:
|
||||
* description: optional line item
|
||||
* $ref: "#/components/schemas/LineItem"
|
||||
* inventory_item:
|
||||
* description: inventory item from inventory module
|
||||
* $ref: "#/components/schemas/InventoryItemDTO"
|
||||
*/
|
||||
export type ExtendedReservationItem = ReservationItemDTO & {
|
||||
line_item?: LineItem
|
||||
inventory_item?: InventoryItemDTO
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema AdminReservationsListRes
|
||||
* type: object
|
||||
@@ -80,7 +100,7 @@ export type AdminReservationsRes = {
|
||||
* reservations:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/ReservationItemDTO"
|
||||
* $ref: "#/components/schemas/ExtendedReservationItem"
|
||||
* count:
|
||||
* type: integer
|
||||
* description: The total number of items available
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import { Type } from "class-transformer"
|
||||
import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator"
|
||||
import { Request, Response } from "express"
|
||||
import {
|
||||
extendedFindParamsMixin,
|
||||
NumericalComparisonOperator,
|
||||
extendedFindParamsMixin,
|
||||
} from "../../../../types/common"
|
||||
import { Request, Response } from "express"
|
||||
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
import { LineItemService } from "../../../../services"
|
||||
import { Type } from "class-transformer"
|
||||
import { joinInventoryItems } from "./utils/join-inventory-items"
|
||||
import { joinLineItems } from "./utils/join-line-items"
|
||||
|
||||
/**
|
||||
* @oas [get] /admin/reservations
|
||||
@@ -110,12 +116,47 @@ import {
|
||||
export default async (req: Request, res: Response) => {
|
||||
const inventoryService: IInventoryService =
|
||||
req.scope.resolve("inventoryService")
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
const { filterableFields, listConfig } = req
|
||||
|
||||
const relations = new Set(listConfig.relations ?? [])
|
||||
|
||||
const includeItems = relations.delete("line_item")
|
||||
const includeInventoryItems = relations.delete("inventory_item")
|
||||
|
||||
if (listConfig.relations?.length) {
|
||||
listConfig.relations = [...relations]
|
||||
}
|
||||
|
||||
const [reservations, count] = await inventoryService.listReservationItems(
|
||||
req.filterableFields,
|
||||
req.listConfig
|
||||
filterableFields,
|
||||
listConfig,
|
||||
{
|
||||
transactionManager: manager,
|
||||
}
|
||||
)
|
||||
|
||||
const promises: Promise<any>[] = []
|
||||
|
||||
if (includeInventoryItems) {
|
||||
promises.push(
|
||||
joinInventoryItems(reservations, {
|
||||
inventoryService,
|
||||
manager,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (includeItems) {
|
||||
const lineItemService: LineItemService =
|
||||
req.scope.resolve("lineItemService")
|
||||
|
||||
promises.push(joinLineItems(reservations, lineItemService))
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
const { limit, offset } = req.validatedQuery
|
||||
|
||||
res.json({ reservations, count, limit, offset })
|
||||
@@ -125,10 +166,9 @@ export class AdminGetReservationsParams extends extendedFindParamsMixin({
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
}) {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
location_id?: string[]
|
||||
@IsType([String, [String]])
|
||||
location_id?: string | string[]
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { ExtendedReservationItem } from ".."
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
|
||||
export const joinInventoryItems = async (
|
||||
reservations: ExtendedReservationItem[],
|
||||
dependencies: {
|
||||
inventoryService: IInventoryService
|
||||
manager: EntityManager
|
||||
}
|
||||
): Promise<ExtendedReservationItem[]> => {
|
||||
const [inventoryItems] =
|
||||
await dependencies.inventoryService.listInventoryItems(
|
||||
{
|
||||
id: reservations.map((r) => r.inventory_item_id),
|
||||
},
|
||||
{},
|
||||
{
|
||||
transactionManager: dependencies.manager,
|
||||
}
|
||||
)
|
||||
|
||||
const inventoryItemMap = new Map(inventoryItems.map((i) => [i.id, i]))
|
||||
|
||||
return reservations.map((reservation) => {
|
||||
reservation.inventory_item = inventoryItemMap.get(
|
||||
reservation.inventory_item_id
|
||||
)
|
||||
|
||||
return reservation
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ExtendedReservationItem } from ".."
|
||||
import { LineItemService } from "../../../../../services"
|
||||
|
||||
export const joinLineItems = async (
|
||||
reservations: ExtendedReservationItem[],
|
||||
lineItemService: LineItemService
|
||||
): Promise<ExtendedReservationItem[]> => {
|
||||
const lineItems = await lineItemService.list(
|
||||
{
|
||||
id: reservations
|
||||
.map((r) => r.line_item_id)
|
||||
.filter((lId: string | null | undefined): lId is string => !!lId),
|
||||
},
|
||||
{
|
||||
relations: ["order"],
|
||||
}
|
||||
)
|
||||
|
||||
const lineItemMap = new Map(lineItems.map((i) => [i.id, i]))
|
||||
|
||||
return reservations.map((reservation) => {
|
||||
if (!reservation.line_item_id) {
|
||||
return reservation
|
||||
}
|
||||
|
||||
reservation.line_item = lineItemMap.get(reservation.line_item_id)
|
||||
|
||||
return reservation
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user