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:
Philip Korsholm
2023-05-23 05:24:28 +02:00
committed by GitHub
parent 87444488b5
commit 4f3c8f5d70
35 changed files with 1717 additions and 135 deletions
@@ -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
})
}