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:
co-authored by
Oliver Windall Juhl
Riqwan Thamir
Rares Capilnar
parent
87444488b5
commit
4f3c8f5d70
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/client-types": patch
|
||||||
|
"medusa-react": patch
|
||||||
|
"@medusajs/medusa-js": patch
|
||||||
|
"@medusajs/admin-ui": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa,client-types,medusa-js,admin-ui,medusa-react): add reservation table and creation
|
||||||
@@ -533,6 +533,8 @@ describe("Inventory Items endpoints", () => {
|
|||||||
available_quantity: 5,
|
available_quantity: 5,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
|
reserved_quantity: 0,
|
||||||
|
stocked_quantity: 15,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -203,5 +203,27 @@ describe("Inventory Items endpoints", () => {
|
|||||||
"The reservation quantity cannot be greater than the unfulfilled line item quantity",
|
"The reservation quantity cannot be greater than the unfulfilled line item quantity",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("lists reservations with inventory_items and line items", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const res = await api.get(
|
||||||
|
`/admin/reservations?expand=line_item,inventory_item`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.reservations.length).toEqual(1)
|
||||||
|
expect(res.data.reservations).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item: expect.objectContaining({}),
|
||||||
|
line_item: expect.objectContaining({
|
||||||
|
order: expect.objectContaining({}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.2",
|
"@radix-ui/react-dialog": "^1.0.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.3",
|
"@radix-ui/react-popover": "^1.0.3",
|
||||||
|
"@radix-ui/react-portal": "^1.0.2",
|
||||||
"@radix-ui/react-radio-group": "^1.1.1",
|
"@radix-ui/react-radio-group": "^1.1.1",
|
||||||
"@radix-ui/react-select": "^1.2.0",
|
"@radix-ui/react-select": "^1.2.0",
|
||||||
"@radix-ui/react-switch": "^1.0.1",
|
"@radix-ui/react-switch": "^1.0.1",
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import {
|
||||||
|
AdminGetInventoryItemsParams,
|
||||||
|
DecoratedInventoryItemDTO,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import { ControlProps, OptionProps, SingleValue } from "react-select"
|
||||||
|
|
||||||
|
import Control from "../select/next-select/components/control"
|
||||||
|
import { NextSelect } from "../select/next-select"
|
||||||
|
import SearchIcon from "../../fundamentals/icons/search-icon"
|
||||||
|
import { useAdminInventoryItems } from "medusa-react"
|
||||||
|
import { useDebounce } from "../../../hooks/use-debounce"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onItemSelect: (item: itemType) => void
|
||||||
|
clearOnSelect?: boolean
|
||||||
|
filters?: AdminGetInventoryItemsParams
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemOption = {
|
||||||
|
label: string | undefined
|
||||||
|
value: string | undefined
|
||||||
|
inventoryItem: DecoratedInventoryItemDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemSearch = ({ onItemSelect, clearOnSelect, filters = {} }: Props) => {
|
||||||
|
const [itemSearchTerm, setItemSearchTerm] = useState<string | undefined>()
|
||||||
|
|
||||||
|
const debouncedItemSearchTerm = useDebounce(itemSearchTerm, 500)
|
||||||
|
|
||||||
|
const queryEnabled = !!debouncedItemSearchTerm?.length
|
||||||
|
|
||||||
|
const { isLoading, inventory_items } = useAdminInventoryItems(
|
||||||
|
{
|
||||||
|
q: debouncedItemSearchTerm,
|
||||||
|
...filters,
|
||||||
|
},
|
||||||
|
{ enabled: queryEnabled }
|
||||||
|
)
|
||||||
|
|
||||||
|
const onChange = (item: SingleValue<ItemOption>) => {
|
||||||
|
if (item) {
|
||||||
|
onItemSelect(item.inventoryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = inventory_items?.map(
|
||||||
|
(inventoryItem: DecoratedInventoryItemDTO) => ({
|
||||||
|
label:
|
||||||
|
inventoryItem.title ||
|
||||||
|
inventoryItem.variants?.[0]?.product?.title ||
|
||||||
|
inventoryItem.sku,
|
||||||
|
value: inventoryItem.id,
|
||||||
|
inventoryItem,
|
||||||
|
})
|
||||||
|
) as ItemOption[]
|
||||||
|
|
||||||
|
const filterOptions = () => true
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<NextSelect
|
||||||
|
isMulti={false}
|
||||||
|
components={{ Option: ProductOption, Control: SearchControl }}
|
||||||
|
onInputChange={setItemSearchTerm}
|
||||||
|
options={options}
|
||||||
|
placeholder="Choose an item"
|
||||||
|
isSearchable={true}
|
||||||
|
noOptionsMessage={() => "No items found"}
|
||||||
|
openMenuOnClick={!!inventory_items?.length}
|
||||||
|
onChange={onChange}
|
||||||
|
value={null}
|
||||||
|
isLoading={queryEnabled && isLoading}
|
||||||
|
filterOption={filterOptions} // TODO: Remove this when we can q for inventory item titles
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductOption = ({ innerProps, data }: OptionProps<ItemOption>) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...innerProps}
|
||||||
|
className="text-small grid w-full cursor-pointer grid-cols-2 place-content-between px-4 py-2 transition-all hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>{data.label}</p>
|
||||||
|
<p className="text-grey-50">{data.inventoryItem.sku}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-grey-50">{`${data.inventoryItem.stocked_quantity} stock`}</p>
|
||||||
|
<p className="text-grey-50">{`${
|
||||||
|
data.inventoryItem.stocked_quantity -
|
||||||
|
data.inventoryItem.reserved_quantity
|
||||||
|
} available`}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchControl = ({ children, ...props }: ControlProps<ItemOption>) => (
|
||||||
|
<Control {...props}>
|
||||||
|
<span className="mr-4">
|
||||||
|
<SearchIcon size={16} className="text-grey-50" />
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</Control>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ItemSearch
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect, useMemo } from "react"
|
||||||
|
import { useAdminStockLocations } from "medusa-react"
|
||||||
|
import { NextSelect } from "../select/next-select"
|
||||||
|
|
||||||
|
const LocationDropdown = ({
|
||||||
|
selectedLocation,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
selectedLocation?: string
|
||||||
|
onChange: (id: string) => void
|
||||||
|
}) => {
|
||||||
|
const { stock_locations: locations, isLoading } = useAdminStockLocations()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedLocation && !isLoading && locations?.length) {
|
||||||
|
onChange(locations[0].id)
|
||||||
|
}
|
||||||
|
}, [isLoading, locations, onChange, selectedLocation])
|
||||||
|
|
||||||
|
const selectedLocObj = useMemo(() => {
|
||||||
|
if (!isLoading && locations) {
|
||||||
|
return locations.find((l) => l.id === selectedLocation) ?? locations[0]
|
||||||
|
}
|
||||||
|
}, [selectedLocation, locations, isLoading])
|
||||||
|
|
||||||
|
if (isLoading || !locations || !selectedLocObj) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NextSelect
|
||||||
|
isMulti={false}
|
||||||
|
onChange={(loc) => {
|
||||||
|
onChange(loc!.value)
|
||||||
|
}}
|
||||||
|
options={locations.map((l) => ({
|
||||||
|
label: l.name,
|
||||||
|
value: l.id,
|
||||||
|
}))}
|
||||||
|
value={{ value: selectedLocObj.id, label: selectedLocObj.name }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LocationDropdown
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PropsWithChildren } from "react"
|
import { PropsWithChildren } from "react"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
import * as Portal from "@radix-ui/react-portal"
|
||||||
|
|
||||||
const MODAL_WIDTH = 560
|
const MODAL_WIDTH = 560
|
||||||
|
|
||||||
@@ -14,46 +15,48 @@ type SideModalProps = PropsWithChildren<{
|
|||||||
function SideModal(props: SideModalProps) {
|
function SideModal(props: SideModalProps) {
|
||||||
const { isVisible, children, close } = props
|
const { isVisible, children, close } = props
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<Portal.Root>
|
||||||
{isVisible && (
|
<AnimatePresence>
|
||||||
<>
|
{isVisible && (
|
||||||
<motion.div
|
<>
|
||||||
onClick={close}
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
onClick={close}
|
||||||
animate={{ opacity: 1 }}
|
initial={{ opacity: 0 }}
|
||||||
exit={{ opacity: 0 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ ease: "easeInOut" }}
|
exit={{ opacity: 0 }}
|
||||||
style={{
|
transition={{ ease: "easeInOut" }}
|
||||||
position: "absolute",
|
style={{
|
||||||
top: 0,
|
position: "fixed",
|
||||||
left: 0,
|
top: 0,
|
||||||
width: "100%",
|
left: 0,
|
||||||
height: "100%",
|
width: "100%",
|
||||||
zIndex: 99,
|
height: "100%",
|
||||||
background: "rgba(0,0,0,.3)",
|
zIndex: 99,
|
||||||
}}
|
background: "rgba(0,0,0,.3)",
|
||||||
></motion.div>
|
}}
|
||||||
<motion.div
|
></motion.div>
|
||||||
transition={{ ease: "easeInOut" }}
|
<motion.div
|
||||||
initial={{ right: -MODAL_WIDTH }}
|
transition={{ ease: "easeInOut" }}
|
||||||
style={{
|
initial={{ right: -MODAL_WIDTH }}
|
||||||
position: "fixed",
|
style={{
|
||||||
height: "100%",
|
position: "fixed",
|
||||||
width: MODAL_WIDTH,
|
height: "100%",
|
||||||
background: "white",
|
width: MODAL_WIDTH,
|
||||||
right: 0,
|
background: "white",
|
||||||
top: 0,
|
right: 0,
|
||||||
zIndex: 200,
|
top: 0,
|
||||||
}}
|
zIndex: 200,
|
||||||
className="overflow-hidden rounded border"
|
}}
|
||||||
animate={{ right: 0 }}
|
className="overflow-hidden rounded border"
|
||||||
exit={{ right: -MODAL_WIDTH }}
|
animate={{ right: 0 }}
|
||||||
>
|
exit={{ right: -MODAL_WIDTH }}
|
||||||
{children}
|
>
|
||||||
</motion.div>
|
{children}
|
||||||
</>
|
</motion.div>
|
||||||
)}
|
</>
|
||||||
</AnimatePresence>
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Portal.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
|
import type { GroupBase, Props, SelectInstance } from "react-select"
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
ReactElement,
|
ReactElement,
|
||||||
RefAttributes,
|
RefAttributes,
|
||||||
|
forwardRef,
|
||||||
useContext,
|
useContext,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react"
|
} from "react"
|
||||||
import type { GroupBase, Props, SelectInstance } from "react-select"
|
|
||||||
import ReactSelect from "react-select"
|
|
||||||
import { ModalContext } from "../../../modal"
|
|
||||||
import { AdjacentContainer } from "../components"
|
import { AdjacentContainer } from "../components"
|
||||||
|
import { ModalContext } from "../../../modal"
|
||||||
|
import ReactSelect from "react-select"
|
||||||
import { useSelectProps } from "../use-select-props"
|
import { useSelectProps } from "../use-select-props"
|
||||||
|
|
||||||
export type SelectComponent = <
|
export type SelectComponent = <
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { ActionMeta, GroupBase, OnChangeValue, Props } from "react-select"
|
import { ActionMeta, GroupBase, OnChangeValue, Props } from "react-select"
|
||||||
import Components from "./components"
|
import BaseComponents from "./components"
|
||||||
import { formatOptionLabel, hasLabel } from "./utils"
|
import { formatOptionLabel, hasLabel } from "./utils"
|
||||||
|
|
||||||
export const useSelectProps = <
|
export const useSelectProps = <
|
||||||
@@ -64,7 +64,7 @@ export const useSelectProps = <
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
components: Components,
|
components: { ...BaseComponents, ...components },
|
||||||
styles: {
|
styles: {
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 60 }),
|
menuPortal: (base) => ({ ...base, zIndex: 60 }),
|
||||||
...styles,
|
...styles,
|
||||||
|
|||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
import Button from "../../../../fundamentals/button"
|
||||||
|
import { Controller } from "react-hook-form"
|
||||||
|
import InputField from "../../../../molecules/input"
|
||||||
|
import ItemSearch from "../../../../molecules/item-search"
|
||||||
|
import LocationDropdown from "../../../../molecules/location-dropdown"
|
||||||
|
import { NestedForm } from "../../../../../utils/nested-form"
|
||||||
|
import { DecoratedInventoryItemDTO } from "@medusajs/medusa"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export type GeneralFormType = {
|
||||||
|
location: string
|
||||||
|
items: Partial<DecoratedInventoryItemDTO>
|
||||||
|
description: string
|
||||||
|
quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: NestedForm<GeneralFormType>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReservationForm: React.FC<Props> = ({ form }) => {
|
||||||
|
const { register, path, watch, control, setValue } = form
|
||||||
|
|
||||||
|
const selectedItem = watch(path("item"))
|
||||||
|
const selectedLocation = watch(path("location"))
|
||||||
|
|
||||||
|
const locationLevel = selectedItem?.location_levels?.find(
|
||||||
|
(l) => l.location_id === selectedLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-6">
|
||||||
|
<div className="grid w-full grid-cols-2 items-center">
|
||||||
|
<div>
|
||||||
|
<p className="inter-base-semibold mb-1">Location</p>
|
||||||
|
<p className="text-grey-50">Choose where you wish to reserve from.</p>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={path("location")}
|
||||||
|
render={({ field: { onChange } }) => {
|
||||||
|
return (
|
||||||
|
<LocationDropdown
|
||||||
|
onChange={onChange}
|
||||||
|
selectedLocation={selectedLocation}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full grid-cols-2 items-center">
|
||||||
|
<div>
|
||||||
|
<p className="inter-base-semibold mb-1">Item to reserve</p>
|
||||||
|
<p className="text-grey-50">
|
||||||
|
Select the item that you wish to reserve.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={path("item")}
|
||||||
|
render={({ field: { onChange } }) => {
|
||||||
|
return (
|
||||||
|
<ItemSearch
|
||||||
|
onItemSelect={onChange}
|
||||||
|
clearOnSelect={true}
|
||||||
|
filters={{ location_id: selectedLocation }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{selectedItem && locationLevel && (
|
||||||
|
<div className="col-span-2 flex w-full flex-col">
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
bg-grey-5 text-grey-50 border-grey-20
|
||||||
|
mt-8
|
||||||
|
grid border-collapse grid-cols-2 grid-rows-5
|
||||||
|
[&>*]:border-r [&>*]:border-b [&>*]:py-2
|
||||||
|
[&>*:nth-child(odd)]:border-l [&>*:nth-child(odd)]:pl-4
|
||||||
|
[&>*:nth-child(even)]:pr-4 [&>*:nth-child(even)]:text-right
|
||||||
|
[&>*:nth-child(-n+2)]:border-t`}
|
||||||
|
>
|
||||||
|
<div className="rounded-tl-rounded">Item</div>
|
||||||
|
<div className="rounded-tr-rounded">
|
||||||
|
{selectedItem!.title ?? "N/A"}
|
||||||
|
</div>
|
||||||
|
<div>SKU</div>
|
||||||
|
<div>{selectedItem.sku ?? "N/A"}</div>
|
||||||
|
<div>In stock</div>
|
||||||
|
<div>{locationLevel?.stocked_quantity}</div>
|
||||||
|
<div>Available</div>
|
||||||
|
<div>
|
||||||
|
{locationLevel?.stocked_quantity -
|
||||||
|
locationLevel?.reserved_quantity}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-bl-rounded">Reserve</div>
|
||||||
|
<div className="bg-grey-0 rounded-br-rounded text-grey-80 flex items-center">
|
||||||
|
<input
|
||||||
|
className="remove-number-spinner inter-base-regular w-full shrink border-none bg-transparent text-right font-normal outline-none outline-0"
|
||||||
|
{...register(path("quantity"), {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={
|
||||||
|
locationLevel?.stocked_quantity -
|
||||||
|
locationLevel?.reserved_quantity
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex w-full justify-end">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="border"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setValue(path("item"), undefined)}
|
||||||
|
>
|
||||||
|
Remove item
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="border-grey border-grey-20 grid w-full grid-cols-2 items-center border-t py-6">
|
||||||
|
<div>
|
||||||
|
<p className="inter-base-semibold mb-1">Description</p>
|
||||||
|
<p className="text-grey-50">What type of reservation is this?</p>
|
||||||
|
</div>
|
||||||
|
<InputField
|
||||||
|
{...register(path("description"))}
|
||||||
|
placeholder="Description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReservationForm
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
import { Cell, Row, TableRowProps, usePagination, useTable } from "react-table"
|
||||||
|
import React, { useEffect, useMemo, useState } from "react"
|
||||||
|
import {
|
||||||
|
useAdminReservations,
|
||||||
|
useAdminStockLocations,
|
||||||
|
useAdminStore,
|
||||||
|
} from "medusa-react"
|
||||||
|
|
||||||
|
import Button from "../../fundamentals/button"
|
||||||
|
import DeletePrompt from "../../organisms/delete-prompt"
|
||||||
|
import EditAllocationDrawer from "../../../domain/orders/details/allocations/edit-allocation-modal"
|
||||||
|
import EditIcon from "../../fundamentals/icons/edit-icon"
|
||||||
|
import Fade from "../../atoms/fade-wrapper"
|
||||||
|
import NewReservation from "./new"
|
||||||
|
import { NextSelect } from "../../molecules/select/next-select"
|
||||||
|
import { ReservationItemDTO } from "@medusajs/types"
|
||||||
|
import Table from "../../molecules/table"
|
||||||
|
import TableContainer from "../../../components/organisms/table-container"
|
||||||
|
import TrashIcon from "../../fundamentals/icons/trash-icon"
|
||||||
|
import { isEmpty } from "lodash"
|
||||||
|
import qs from "qs"
|
||||||
|
import { useAdminDeleteReservation } from "medusa-react"
|
||||||
|
import { useLocation } from "react-router-dom"
|
||||||
|
import { useReservationFilters } from "./use-reservation-filters"
|
||||||
|
import useReservationsTableColumns from "./use-reservations-columns"
|
||||||
|
import useToggleState from "../../../hooks/use-toggle-state"
|
||||||
|
|
||||||
|
const DEFAULT_PAGE_SIZE = 15
|
||||||
|
|
||||||
|
type ReservationsTableProps = {}
|
||||||
|
|
||||||
|
const LocationDropdown = ({
|
||||||
|
selectedLocation,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
selectedLocation: string
|
||||||
|
onChange: (id: string) => void
|
||||||
|
}) => {
|
||||||
|
const { stock_locations: locations, isLoading } = useAdminStockLocations()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedLocation && !isLoading && locations?.length) {
|
||||||
|
onChange(locations[0].id)
|
||||||
|
}
|
||||||
|
}, [isLoading, locations, onChange, selectedLocation])
|
||||||
|
|
||||||
|
const selectedLocObj = useMemo(() => {
|
||||||
|
if (!isLoading && locations) {
|
||||||
|
return locations.find((l) => l.id === selectedLocation)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}, [selectedLocation, locations, isLoading])
|
||||||
|
|
||||||
|
if (isLoading || !locations || !selectedLocObj) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[40px] w-[200px]">
|
||||||
|
<NextSelect
|
||||||
|
isMulti={false}
|
||||||
|
onChange={(loc) => {
|
||||||
|
onChange(loc!.value)
|
||||||
|
}}
|
||||||
|
options={locations.map((l) => ({
|
||||||
|
label: l.name,
|
||||||
|
value: l.id,
|
||||||
|
}))}
|
||||||
|
value={{ value: selectedLocObj.id, label: selectedLocObj.name }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReservationsTable: React.FC<ReservationsTableProps> = () => {
|
||||||
|
const { store } = useAdminStore()
|
||||||
|
const {
|
||||||
|
state: createReservationState,
|
||||||
|
close: closeReservationCreate,
|
||||||
|
open: openReservationCreate,
|
||||||
|
} = useToggleState()
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const defaultQuery = useMemo(() => {
|
||||||
|
if (store) {
|
||||||
|
return {
|
||||||
|
location_id: store.default_location_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}, [store])
|
||||||
|
|
||||||
|
const {
|
||||||
|
reset,
|
||||||
|
paginate,
|
||||||
|
setLocationFilter,
|
||||||
|
setQuery: setFreeText,
|
||||||
|
queryObject,
|
||||||
|
representationObject,
|
||||||
|
} = useReservationFilters(location.search, defaultQuery)
|
||||||
|
|
||||||
|
const offs = parseInt(queryObject.offset) || 0
|
||||||
|
const limit = parseInt(queryObject.limit)
|
||||||
|
|
||||||
|
const [query, setQuery] = useState(queryObject.query)
|
||||||
|
const [numPages, setNumPages] = useState(0)
|
||||||
|
|
||||||
|
const { reservations, isLoading, count } = useAdminReservations(
|
||||||
|
{
|
||||||
|
...queryObject,
|
||||||
|
expand: "line_item,inventory_item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!store,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controlledPageCount = Math.ceil(count! / queryObject.limit)
|
||||||
|
setNumPages(controlledPageCount)
|
||||||
|
}, [reservations])
|
||||||
|
|
||||||
|
const updateUrlFromFilter = (obj = {}) => {
|
||||||
|
const stringified = qs.stringify(obj)
|
||||||
|
window.history.replaceState(`/a/reservations`, "", `${`?${stringified}`}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshWithFilters = () => {
|
||||||
|
const filterObj = representationObject
|
||||||
|
|
||||||
|
if (isEmpty(filterObj)) {
|
||||||
|
updateUrlFromFilter({ offset: 0, limit: DEFAULT_PAGE_SIZE })
|
||||||
|
} else {
|
||||||
|
updateUrlFromFilter(filterObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshWithFilters()
|
||||||
|
}, [representationObject])
|
||||||
|
|
||||||
|
const [columns] = useReservationsTableColumns()
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
prepareRow,
|
||||||
|
gotoPage,
|
||||||
|
canPreviousPage,
|
||||||
|
canNextPage,
|
||||||
|
pageCount,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
// Get the state from the instance
|
||||||
|
state: { pageIndex },
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns,
|
||||||
|
data: reservations || [],
|
||||||
|
manualPagination: true,
|
||||||
|
initialState: {
|
||||||
|
pageIndex: Math.floor(offs / limit),
|
||||||
|
pageSize: limit,
|
||||||
|
},
|
||||||
|
pageCount: numPages,
|
||||||
|
autoResetPage: false,
|
||||||
|
},
|
||||||
|
usePagination
|
||||||
|
)
|
||||||
|
|
||||||
|
// Debounced search
|
||||||
|
useEffect(() => {
|
||||||
|
const delayDebounceFn = setTimeout(() => {
|
||||||
|
if (query) {
|
||||||
|
setFreeText(query)
|
||||||
|
gotoPage(0)
|
||||||
|
} else {
|
||||||
|
if (typeof query !== "undefined") {
|
||||||
|
// if we delete query string, we reset the table view
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 400)
|
||||||
|
|
||||||
|
return () => clearTimeout(delayDebounceFn)
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (canNextPage) {
|
||||||
|
paginate(1)
|
||||||
|
nextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePrev = () => {
|
||||||
|
if (canPreviousPage) {
|
||||||
|
paginate(-1)
|
||||||
|
previousPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableContainer
|
||||||
|
hasPagination
|
||||||
|
pagingState={{
|
||||||
|
count: count || 0,
|
||||||
|
offset: offs,
|
||||||
|
pageSize: offs + rows.length,
|
||||||
|
title: "Reservations",
|
||||||
|
currentPage: pageIndex + 1,
|
||||||
|
pageCount: pageCount,
|
||||||
|
nextPage: handleNext,
|
||||||
|
prevPage: handlePrev,
|
||||||
|
hasNext: canNextPage,
|
||||||
|
hasPrev: canPreviousPage,
|
||||||
|
}}
|
||||||
|
numberOfRows={limit}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
enableSearch
|
||||||
|
searchClassName="h-[40px]"
|
||||||
|
handleSearch={setQuery}
|
||||||
|
searchValue={query}
|
||||||
|
tableActions={
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<LocationDropdown
|
||||||
|
selectedLocation={
|
||||||
|
queryObject.location_id || store?.default_location_id
|
||||||
|
}
|
||||||
|
onChange={(id) => {
|
||||||
|
setLocationFilter(id)
|
||||||
|
gotoPage(0)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onClick={openReservationCreate}
|
||||||
|
>
|
||||||
|
Create reservation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{...getTableProps()}
|
||||||
|
>
|
||||||
|
<Table.Head>
|
||||||
|
{headerGroups?.map((headerGroup) => {
|
||||||
|
const { key, ...rest } = headerGroup.getHeaderGroupProps()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.HeadRow key={key} {...rest}>
|
||||||
|
{headerGroup.headers.map((col) => {
|
||||||
|
const { key, ...rest } = col.getHeaderProps()
|
||||||
|
return (
|
||||||
|
<Table.HeadCell
|
||||||
|
className="min-w-[100px]"
|
||||||
|
key={key}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{col.render("Header")}
|
||||||
|
</Table.HeadCell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.HeadRow>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Head>
|
||||||
|
|
||||||
|
<Table.Body {...getTableBodyProps()}>
|
||||||
|
{rows.map((row) => {
|
||||||
|
prepareRow(row)
|
||||||
|
const { key, ...rest } = row.getRowProps()
|
||||||
|
return <ReservationRow row={row} key={key} {...rest} />
|
||||||
|
})}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<Fade isVisible={createReservationState} isFullScreen={true}>
|
||||||
|
<NewReservation
|
||||||
|
locationId={queryObject.location_id}
|
||||||
|
onClose={closeReservationCreate}
|
||||||
|
/>
|
||||||
|
</Fade>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReservationRow = ({
|
||||||
|
row,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
row: Row<ReservationItemDTO>
|
||||||
|
} & TableRowProps) => {
|
||||||
|
const inventory = row.original
|
||||||
|
|
||||||
|
const { mutate: deleteReservation } = useAdminDeleteReservation(inventory.id)
|
||||||
|
|
||||||
|
const [showEditReservation, setShowEditReservation] =
|
||||||
|
useState<ReservationItemDTO | null>(null)
|
||||||
|
const [showDeleteReservation, setShowDeleteReservation] = useState(false)
|
||||||
|
|
||||||
|
const getRowActionables = () => {
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
onClick: () => setShowEditReservation(row.original),
|
||||||
|
icon: <EditIcon size={20} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
variant: "danger",
|
||||||
|
icon: <TrashIcon size={20} />,
|
||||||
|
onClick: () => setShowDeleteReservation(true),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table.Row
|
||||||
|
color={"inherit"}
|
||||||
|
forceDropdown
|
||||||
|
actions={getRowActionables()}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{row.cells.map((cell: Cell, index: number) => {
|
||||||
|
const { key, ...rest } = cell.getCellProps()
|
||||||
|
return (
|
||||||
|
<Table.Cell {...rest} key={key}>
|
||||||
|
{cell.render("Cell", { index })}
|
||||||
|
</Table.Cell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Row>
|
||||||
|
{showEditReservation && (
|
||||||
|
<EditAllocationDrawer
|
||||||
|
close={() => setShowEditReservation(null)}
|
||||||
|
reservation={row.original}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showDeleteReservation && (
|
||||||
|
<DeletePrompt
|
||||||
|
text={"Are you sure you want to remove this reservation?"}
|
||||||
|
heading={"Remove reservation"}
|
||||||
|
successText={"Reservation has been removed"}
|
||||||
|
onDelete={async () => await deleteReservation(undefined)}
|
||||||
|
handleClose={() => setShowDeleteReservation(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReservationsTable
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import MetadataForm, {
|
||||||
|
MetadataFormType,
|
||||||
|
getSubmittableMetadata,
|
||||||
|
} from "../../../forms/general/metadata-form"
|
||||||
|
import ReservationForm, {
|
||||||
|
GeneralFormType,
|
||||||
|
} from "../components/reservation-form"
|
||||||
|
|
||||||
|
import { AdminPostReservationsReq } from "@medusajs/medusa"
|
||||||
|
import Button from "../../../fundamentals/button"
|
||||||
|
import CrossIcon from "../../../fundamentals/icons/cross-icon"
|
||||||
|
import FocusModal from "../../../molecules/modal/focus-modal"
|
||||||
|
import { getErrorMessage } from "../../../../utils/error-messages"
|
||||||
|
import { nestedForm } from "../../../../utils/nested-form"
|
||||||
|
import { useAdminCreateReservation } from "medusa-react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import useNotification from "../../../../hooks/use-notification"
|
||||||
|
|
||||||
|
type NewReservationFormType = {
|
||||||
|
general: GeneralFormType
|
||||||
|
metadata: MetadataFormType
|
||||||
|
}
|
||||||
|
|
||||||
|
const NewReservation = ({
|
||||||
|
onClose,
|
||||||
|
locationId,
|
||||||
|
}: {
|
||||||
|
onClose: () => void
|
||||||
|
locationId?: string
|
||||||
|
}) => {
|
||||||
|
const { mutateAsync: createReservation } = useAdminCreateReservation()
|
||||||
|
const form = useForm<NewReservationFormType>({
|
||||||
|
defaultValues: {
|
||||||
|
general: {
|
||||||
|
location: locationId,
|
||||||
|
item: undefined,
|
||||||
|
description: undefined,
|
||||||
|
quantity: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reValidateMode: "onBlur",
|
||||||
|
mode: "onBlur",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { handleSubmit } = form
|
||||||
|
|
||||||
|
const notification = useNotification()
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
const payload = await createPayload(data)
|
||||||
|
|
||||||
|
createReservation(payload, {
|
||||||
|
onSuccess: () => {
|
||||||
|
notification("Success", "Successfully created reservation", "success")
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
notification("Error", getErrorMessage(err), "error")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
|
||||||
|
<FocusModal>
|
||||||
|
<FocusModal.Header>
|
||||||
|
<div className="medium:w-8/12 flex w-full justify-between px-8">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<CrossIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
<div className="gap-x-small flex">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="small" variant="primary" type="submit">
|
||||||
|
Save reservation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Header>
|
||||||
|
<FocusModal.Main className="no-scrollbar flex w-full justify-center">
|
||||||
|
<div className="medium:w-7/12 large:w-6/12 small:w-4/5 my-16 max-w-[700px]">
|
||||||
|
<h1 className="mb-base text-grey-90 text-xlarge font-semibold">
|
||||||
|
Reserve Item
|
||||||
|
</h1>
|
||||||
|
<div className="mt-xlarge gap-y-xlarge flex w-full pb-0.5">
|
||||||
|
<ReservationForm form={nestedForm(form, "general")} />
|
||||||
|
</div>
|
||||||
|
<div className="border-grey border-grey-20 w-full items-center border-t pt-6">
|
||||||
|
<p className="inter-base-semibold mb-2">Metadata</p>
|
||||||
|
<MetadataForm form={nestedForm(form, "metadata")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Main>
|
||||||
|
</FocusModal>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPayload = (
|
||||||
|
data: NewReservationFormType
|
||||||
|
): AdminPostReservationsReq => {
|
||||||
|
return {
|
||||||
|
location_id: data.general.location,
|
||||||
|
inventory_item_id: data.general.item.id!,
|
||||||
|
quantity: data.general.quantity,
|
||||||
|
description: data.general.description,
|
||||||
|
metadata: getSubmittableMetadata(data.metadata),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewReservation
|
||||||
+470
@@ -0,0 +1,470 @@
|
|||||||
|
import { useMemo, useReducer, useState } from "react"
|
||||||
|
|
||||||
|
import { omit } from "lodash"
|
||||||
|
import qs from "qs"
|
||||||
|
import { relativeDateFormatToTimestamp } from "../../../utils/time"
|
||||||
|
|
||||||
|
type ReservationDateFilter = null | {
|
||||||
|
gt?: string
|
||||||
|
lt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type reservationFilterAction =
|
||||||
|
| { type: "setQuery"; payload: string | null }
|
||||||
|
| { type: "setFilters"; payload: ReservationFilterState }
|
||||||
|
| { type: "reset"; payload: ReservationFilterState }
|
||||||
|
| { type: "setOffset"; payload: number }
|
||||||
|
| { type: "setDefaults"; payload: ReservationDefaultFilters | null }
|
||||||
|
| { type: "setLocation"; payload: string }
|
||||||
|
| { type: "setLimit"; payload: number }
|
||||||
|
|
||||||
|
interface ReservationFilterState {
|
||||||
|
query?: string | null
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
location: string
|
||||||
|
additionalFilters: ReservationDefaultFilters | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedFilters = ["q", "offset", "limit", "location_id"]
|
||||||
|
|
||||||
|
const DefaultTabs = {}
|
||||||
|
|
||||||
|
const formatDateFilter = (filter: ReservationDateFilter) => {
|
||||||
|
if (filter === null) {
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateFormatted = Object.entries(filter).reduce((acc, [key, value]) => {
|
||||||
|
if (value.includes("|")) {
|
||||||
|
acc[key] = relativeDateFormatToTimestamp(value)
|
||||||
|
} else {
|
||||||
|
acc[key] = value
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return dateFormatted
|
||||||
|
}
|
||||||
|
|
||||||
|
const reducer = (
|
||||||
|
state: ReservationFilterState,
|
||||||
|
action: reservationFilterAction
|
||||||
|
): ReservationFilterState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "setFilters": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
query: action?.payload?.query,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "setQuery": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
query: action.payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "setLimit": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
limit: action.payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "setOffset": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
offset: action.payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "setLocation": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
location: action.payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "reset": {
|
||||||
|
return action.payload
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReservationDefaultFilters = {
|
||||||
|
expand?: string
|
||||||
|
fields?: string
|
||||||
|
location_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const eqSet = (as: Set<string>, bs: Set<string>) => {
|
||||||
|
if (as.size !== bs.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (const a of as) {
|
||||||
|
if (!bs.has(a)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReservationFilters = (
|
||||||
|
existing?: string,
|
||||||
|
defaultFilters: ReservationDefaultFilters | null = null
|
||||||
|
) => {
|
||||||
|
if (existing && existing[0] === "?") {
|
||||||
|
existing = existing.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initial = useMemo(
|
||||||
|
() => parseQueryString(existing, defaultFilters),
|
||||||
|
[existing, defaultFilters]
|
||||||
|
)
|
||||||
|
|
||||||
|
const initialTabs = useMemo(() => {
|
||||||
|
const storageString = localStorage.getItem("reservation::filters")
|
||||||
|
if (storageString) {
|
||||||
|
const savedTabs = JSON.parse(storageString)
|
||||||
|
|
||||||
|
if (savedTabs) {
|
||||||
|
return Object.entries(savedTabs).map(([key, value]) => {
|
||||||
|
return {
|
||||||
|
label: key,
|
||||||
|
value: key,
|
||||||
|
removable: true,
|
||||||
|
representationString: value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(reducer, initial)
|
||||||
|
const [tabs, setTabs] = useState(initialTabs)
|
||||||
|
|
||||||
|
const setDefaultFilters = (filters: ReservationDefaultFilters | null) => {
|
||||||
|
dispatch({ type: "setDefaults", payload: filters })
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLimit = (limit: number) => {
|
||||||
|
dispatch({ type: "setLimit", payload: limit })
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLocationFilter = (loc: string) => {
|
||||||
|
dispatch({ type: "setLocation", payload: loc })
|
||||||
|
dispatch({ type: "setOffset", payload: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const paginate = (direction: 1 | -1) => {
|
||||||
|
if (direction > 0) {
|
||||||
|
const nextOffset = state.offset + state.limit
|
||||||
|
|
||||||
|
dispatch({ type: "setOffset", payload: nextOffset })
|
||||||
|
} else {
|
||||||
|
const nextOffset = Math.max(state.offset - state.limit, 0)
|
||||||
|
dispatch({ type: "setOffset", payload: nextOffset })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
dispatch({
|
||||||
|
type: "setFilters",
|
||||||
|
payload: {
|
||||||
|
...state,
|
||||||
|
offset: 0,
|
||||||
|
query: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFilters = (filters: ReservationFilterState) => {
|
||||||
|
dispatch({ type: "setFilters", payload: filters })
|
||||||
|
}
|
||||||
|
|
||||||
|
const setQuery = (queryString: string | null) => {
|
||||||
|
dispatch({ type: "setQuery", payload: queryString })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getQueryObject = () => {
|
||||||
|
const toQuery: any = { ...state.additionalFilters }
|
||||||
|
for (const [key, value] of Object.entries(state)) {
|
||||||
|
if (key === "query") {
|
||||||
|
if (value && typeof value === "string") {
|
||||||
|
toQuery["q"] = value
|
||||||
|
}
|
||||||
|
} else if (key === "offset" || key === "limit") {
|
||||||
|
toQuery[key] = value
|
||||||
|
} else if (value.open) {
|
||||||
|
if (key === "date") {
|
||||||
|
toQuery[stateFilterMap[key]] = formatDateFilter(
|
||||||
|
value.filter as ReservationDateFilter
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
toQuery[stateFilterMap[key]] = value.filter
|
||||||
|
}
|
||||||
|
} else if (key === "location") {
|
||||||
|
toQuery[stateFilterMap[key]] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toQuery["expand"] = "line_item,inventory_item"
|
||||||
|
|
||||||
|
return toQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
const getQueryString = () => {
|
||||||
|
const obj = getQueryObject()
|
||||||
|
return qs.stringify(obj, { skipNulls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRepresentationObject = (fromObject?: ReservationFilterState) => {
|
||||||
|
const objToUse = fromObject ?? state
|
||||||
|
|
||||||
|
const toQuery: any = {}
|
||||||
|
for (const [key, value] of Object.entries(objToUse)) {
|
||||||
|
if (key === "query") {
|
||||||
|
if (value && typeof value === "string") {
|
||||||
|
toQuery["q"] = value
|
||||||
|
}
|
||||||
|
} else if (key === "offset" || key === "limit" || key === "location") {
|
||||||
|
toQuery[stateFilterMap[key] || key] = value
|
||||||
|
} else if (value.open) {
|
||||||
|
toQuery[stateFilterMap[key]] = value.filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRepresentationString = () => {
|
||||||
|
const obj = getRepresentationObject()
|
||||||
|
return qs.stringify(obj, { skipNulls: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryObject = useMemo(() => getQueryObject(), [state])
|
||||||
|
const representationObject = useMemo(() => getRepresentationObject(), [state])
|
||||||
|
const representationString = useMemo(() => getRepresentationString(), [state])
|
||||||
|
|
||||||
|
const activeFilterTab = useMemo(() => {
|
||||||
|
const clean = omit(representationObject, ["limit", "offset"])
|
||||||
|
const stringified = qs.stringify(clean)
|
||||||
|
|
||||||
|
const existsInSaved = tabs.find(
|
||||||
|
(el) => el.representationString === stringified
|
||||||
|
)
|
||||||
|
if (existsInSaved) {
|
||||||
|
return existsInSaved.value
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [tab, conditions] of Object.entries(DefaultTabs)) {
|
||||||
|
let match = true
|
||||||
|
|
||||||
|
if (Object.keys(clean).length !== Object.keys(conditions).length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [filter, value] of Object.entries(conditions)) {
|
||||||
|
if (filter in clean) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
match =
|
||||||
|
Array.isArray(clean[filter]) &&
|
||||||
|
eqSet(new Set(clean[filter]), new Set(value))
|
||||||
|
} else {
|
||||||
|
match = clean[filter] === value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return tab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}, [representationObject, tabs])
|
||||||
|
|
||||||
|
const availableTabs = useMemo(() => {
|
||||||
|
return [...tabs]
|
||||||
|
}, [tabs])
|
||||||
|
|
||||||
|
const setTab = (tabName: string) => {
|
||||||
|
let tabToUse: object | null = null
|
||||||
|
if (tabName in DefaultTabs) {
|
||||||
|
tabToUse = DefaultTabs[tabName]
|
||||||
|
} else {
|
||||||
|
const tabFound = tabs.find((t) => t.value === tabName)
|
||||||
|
if (tabFound) {
|
||||||
|
tabToUse = qs.parse(tabFound.representationString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabToUse) {
|
||||||
|
const toSubmit = {
|
||||||
|
...state,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [filter, val] of Object.entries(tabToUse)) {
|
||||||
|
toSubmit[filterStateMap[filter]] = {
|
||||||
|
open: true,
|
||||||
|
filter: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch({ type: "setFilters", payload: toSubmit })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveTab = (tabName: string, filters: ReservationFilterState) => {
|
||||||
|
const repObj = getRepresentationObject({ ...filters })
|
||||||
|
const clean = omit(repObj, ["limit", "offset"])
|
||||||
|
const repString = qs.stringify(clean, { skipNulls: true })
|
||||||
|
|
||||||
|
const storedString = localStorage.getItem("inventory::filters")
|
||||||
|
|
||||||
|
let existing: null | object = null
|
||||||
|
|
||||||
|
if (storedString) {
|
||||||
|
existing = JSON.parse(storedString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing[tabName] = repString
|
||||||
|
localStorage.setItem("inventory::filters", JSON.stringify(existing))
|
||||||
|
} else {
|
||||||
|
const newFilters = {}
|
||||||
|
newFilters[tabName] = repString
|
||||||
|
localStorage.setItem("inventory::filters", JSON.stringify(newFilters))
|
||||||
|
}
|
||||||
|
|
||||||
|
setTabs((prev) => {
|
||||||
|
const duplicate = prev.findIndex(
|
||||||
|
(prev) => prev.label?.toLowerCase() === tabName.toLowerCase()
|
||||||
|
)
|
||||||
|
if (duplicate !== -1) {
|
||||||
|
prev.splice(duplicate, 1)
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
label: tabName,
|
||||||
|
value: tabName,
|
||||||
|
representationString: repString,
|
||||||
|
removable: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch({ type: "setFilters", payload: filters })
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTab = (tabValue: string) => {
|
||||||
|
const storedString = localStorage.getItem("products::filters")
|
||||||
|
|
||||||
|
let existing: null | object = null
|
||||||
|
|
||||||
|
if (storedString) {
|
||||||
|
existing = JSON.parse(storedString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
delete existing[tabValue]
|
||||||
|
localStorage.setItem("products::filters", JSON.stringify(existing))
|
||||||
|
}
|
||||||
|
|
||||||
|
setTabs((prev) => {
|
||||||
|
const newTabs = prev.filter((p) => p.value !== tabValue)
|
||||||
|
return newTabs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filters: {
|
||||||
|
...state,
|
||||||
|
},
|
||||||
|
removeTab,
|
||||||
|
saveTab,
|
||||||
|
setTab,
|
||||||
|
availableTabs,
|
||||||
|
activeFilterTab,
|
||||||
|
representationObject,
|
||||||
|
representationString,
|
||||||
|
queryObject,
|
||||||
|
paginate,
|
||||||
|
getQueryObject,
|
||||||
|
getQueryString,
|
||||||
|
setQuery,
|
||||||
|
setFilters,
|
||||||
|
setDefaultFilters,
|
||||||
|
setLocationFilter,
|
||||||
|
setLimit,
|
||||||
|
reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterStateMap = {
|
||||||
|
location_id: "location",
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateFilterMap = {
|
||||||
|
location: "location_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseQueryString = (
|
||||||
|
queryString?: string,
|
||||||
|
additionals: ReservationDefaultFilters | null = null
|
||||||
|
): ReservationFilterState => {
|
||||||
|
const defaultVal: ReservationFilterState = {
|
||||||
|
location: additionals?.location_id ?? "",
|
||||||
|
offset: 0,
|
||||||
|
limit: 15,
|
||||||
|
additionalFilters: additionals,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryString) {
|
||||||
|
const filters = qs.parse(queryString)
|
||||||
|
for (const [key, value] of Object.entries(filters)) {
|
||||||
|
if (allowedFilters.includes(key)) {
|
||||||
|
switch (key) {
|
||||||
|
case "offset": {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
defaultVal.offset = parseInt(value)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "limit": {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
defaultVal.limit = parseInt(value)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "location_id": {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
defaultVal.location = value
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "q": {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
defaultVal.query = value
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import moment from "moment"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
const useReservationsTableColumns = () => {
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: "SKU",
|
||||||
|
accessor: "inventory_item.sku",
|
||||||
|
Cell: ({ cell: { value } }) => value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Order ID",
|
||||||
|
accessor: "line_item.order.display_id",
|
||||||
|
Cell: ({ cell: { value } }) => value ?? "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Description",
|
||||||
|
accessor: "description",
|
||||||
|
Cell: ({ cell: { value } }) => value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Created",
|
||||||
|
accessor: "created_at",
|
||||||
|
Cell: ({ cell: { value } }) => moment(value).format("MMM Do YYYY"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: () => <div className="pr-2 text-right">Quantity</div>,
|
||||||
|
accessor: "quantity",
|
||||||
|
Cell: ({ cell: { value } }) => (
|
||||||
|
<div className="w-full pr-2 text-right">{value}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
return [columns] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useReservationsTableColumns
|
||||||
@@ -2,7 +2,7 @@ import TableViewHeader from "../../components/organisms/custom-table-header"
|
|||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
type P = {
|
type P = {
|
||||||
activeView: "inventory" | "locations"
|
activeView: "inventory" | "locations" | "reservations"
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -17,10 +17,10 @@ function InventoryPageTableHeader(props: P) {
|
|||||||
if (v === "inventory") {
|
if (v === "inventory") {
|
||||||
navigate(`/a/inventory`)
|
navigate(`/a/inventory`)
|
||||||
} else {
|
} else {
|
||||||
navigate(`/a/inventory/locations`)
|
navigate(`/a/inventory/${v}`)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
views={["inventory", "locations"]}
|
views={["inventory", "locations", "reservations"]}
|
||||||
activeView={props.activeView}
|
activeView={props.activeView}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Route, Routes } from "react-router-dom"
|
import { Route, Routes } from "react-router-dom"
|
||||||
import InventoryView from "./inventory"
|
import InventoryView from "./inventory"
|
||||||
import Locations from "./locations"
|
import Locations from "./locations"
|
||||||
|
import Reservations from "./reservations"
|
||||||
|
|
||||||
const Inventory = () => {
|
const Inventory = () => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<InventoryView />} />
|
<Route index element={<InventoryView />} />
|
||||||
<Route path="/locations/*" element={<Locations />} />
|
<Route path="/locations/*" element={<Locations />} />
|
||||||
|
<Route path="/reservations/*" element={<Reservations />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import Spacer from "../../../components/atoms/spacer"
|
||||||
|
import BodyCard from "../../../components/organisms/body-card"
|
||||||
|
import ReservationsTable from "../../../components/templates/reservations-table"
|
||||||
|
import InventoryPageTableHeader from "../header"
|
||||||
|
|
||||||
|
const Reservations = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full grow flex-col">
|
||||||
|
<div className="flex w-full grow flex-col">
|
||||||
|
<BodyCard
|
||||||
|
customHeader={<InventoryPageTableHeader activeView="reservations" />}
|
||||||
|
className="h-fit"
|
||||||
|
>
|
||||||
|
<ReservationsTable />
|
||||||
|
</BodyCard>
|
||||||
|
<Spacer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Reservations
|
||||||
+2
-9
@@ -3,17 +3,10 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { SetRelation, Merge } from "../core/ModelUtils"
|
import { SetRelation, Merge } from "../core/ModelUtils"
|
||||||
|
|
||||||
import type { InventoryItemDTO } from "./InventoryItemDTO"
|
import type { DecoratedInventoryItemDTO } from "./DecoratedInventoryItemDTO"
|
||||||
import type { InventoryLevelDTO } from "./InventoryLevelDTO"
|
|
||||||
import type { ProductVariant } from "./ProductVariant"
|
|
||||||
|
|
||||||
export interface AdminInventoryItemsListWithVariantsAndLocationLevelsRes {
|
export interface AdminInventoryItemsListWithVariantsAndLocationLevelsRes {
|
||||||
inventory_items: Array<
|
inventory_items: Array<DecoratedInventoryItemDTO>
|
||||||
InventoryItemDTO & {
|
|
||||||
location_levels?: Array<InventoryLevelDTO>
|
|
||||||
variants?: Array<ProductVariant>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
/**
|
/**
|
||||||
* The total number of items available
|
* The total number of items available
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { SetRelation, Merge } from "../core/ModelUtils"
|
import { SetRelation, Merge } from "../core/ModelUtils"
|
||||||
|
|
||||||
import type { ReservationItemDTO } from "./ReservationItemDTO"
|
import type { ExtendedReservationItem } from "./ExtendedReservationItem"
|
||||||
|
|
||||||
export interface AdminReservationsListRes {
|
export interface AdminReservationsListRes {
|
||||||
reservations: Array<ReservationItemDTO>
|
reservations: Array<ExtendedReservationItem>
|
||||||
/**
|
/**
|
||||||
* The total number of items available
|
* The total number of items available
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import { SetRelation, Merge } from "../core/ModelUtils"
|
||||||
|
|
||||||
|
import type { InventoryItemDTO } from "./InventoryItemDTO"
|
||||||
|
import type { InventoryLevelDTO } from "./InventoryLevelDTO"
|
||||||
|
import type { ProductVariant } from "./ProductVariant"
|
||||||
|
|
||||||
|
export type DecoratedInventoryItemDTO = InventoryItemDTO & {
|
||||||
|
location_levels?: Array<InventoryLevelDTO>
|
||||||
|
variants?: Array<ProductVariant>
|
||||||
|
/**
|
||||||
|
* The total quantity of the item in stock across levels
|
||||||
|
*/
|
||||||
|
stocked_quantity: number
|
||||||
|
/**
|
||||||
|
* The total quantity of the item available across levels
|
||||||
|
*/
|
||||||
|
reserved_quantity: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import { SetRelation, Merge } from "../core/ModelUtils"
|
||||||
|
|
||||||
|
import type { InventoryItemDTO } from "./InventoryItemDTO"
|
||||||
|
import type { LineItem } from "./LineItem"
|
||||||
|
import type { ReservationItemDTO } from "./ReservationItemDTO"
|
||||||
|
|
||||||
|
export type ExtendedReservationItem = ReservationItemDTO & {
|
||||||
|
/**
|
||||||
|
* optional line item
|
||||||
|
*/
|
||||||
|
line_item?: LineItem
|
||||||
|
/**
|
||||||
|
* inventory item from inventory module
|
||||||
|
*/
|
||||||
|
inventory_item?: InventoryItemDTO
|
||||||
|
}
|
||||||
@@ -310,6 +310,7 @@ export type { Currency } from "./Currency"
|
|||||||
export type { Customer } from "./Customer"
|
export type { Customer } from "./Customer"
|
||||||
export type { CustomerGroup } from "./CustomerGroup"
|
export type { CustomerGroup } from "./CustomerGroup"
|
||||||
export type { CustomShippingOption } from "./CustomShippingOption"
|
export type { CustomShippingOption } from "./CustomShippingOption"
|
||||||
|
export type { DecoratedInventoryItemDTO } from "./DecoratedInventoryItemDTO"
|
||||||
export type { Discount } from "./Discount"
|
export type { Discount } from "./Discount"
|
||||||
export type { DiscountCondition } from "./DiscountCondition"
|
export type { DiscountCondition } from "./DiscountCondition"
|
||||||
export type { DiscountConditionCustomerGroup } from "./DiscountConditionCustomerGroup"
|
export type { DiscountConditionCustomerGroup } from "./DiscountConditionCustomerGroup"
|
||||||
@@ -320,6 +321,7 @@ export type { DiscountConditionProductType } from "./DiscountConditionProductTyp
|
|||||||
export type { DiscountRule } from "./DiscountRule"
|
export type { DiscountRule } from "./DiscountRule"
|
||||||
export type { DraftOrder } from "./DraftOrder"
|
export type { DraftOrder } from "./DraftOrder"
|
||||||
export type { Error } from "./Error"
|
export type { Error } from "./Error"
|
||||||
|
export type { ExtendedReservationItem } from "./ExtendedReservationItem"
|
||||||
export type { ExtendedStoreDTO } from "./ExtendedStoreDTO"
|
export type { ExtendedStoreDTO } from "./ExtendedStoreDTO"
|
||||||
export type { FeatureFlagsResponse } from "./FeatureFlagsResponse"
|
export type { FeatureFlagsResponse } from "./FeatureFlagsResponse"
|
||||||
export type { Fulfillment } from "./Fulfillment"
|
export type { Fulfillment } from "./Fulfillment"
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
AdminGetInventoryItemsParams,
|
|
||||||
AdminInventoryItemsRes,
|
|
||||||
AdminPostInventoryItemsInventoryItemReq,
|
|
||||||
AdminGetInventoryItemsItemLocationLevelsParams,
|
AdminGetInventoryItemsItemLocationLevelsParams,
|
||||||
AdminPostInventoryItemsItemLocationLevelsLevelReq,
|
|
||||||
AdminInventoryItemsDeleteRes,
|
|
||||||
AdminGetInventoryItemsItemParams,
|
AdminGetInventoryItemsItemParams,
|
||||||
|
AdminGetInventoryItemsParams,
|
||||||
|
AdminInventoryItemsDeleteRes,
|
||||||
AdminInventoryItemsListWithVariantsAndLocationLevelsRes,
|
AdminInventoryItemsListWithVariantsAndLocationLevelsRes,
|
||||||
AdminInventoryItemsLocationLevelsRes,
|
AdminInventoryItemsLocationLevelsRes,
|
||||||
|
AdminInventoryItemsRes,
|
||||||
|
AdminPostInventoryItemsInventoryItemReq,
|
||||||
|
AdminPostInventoryItemsItemLocationLevelsLevelReq,
|
||||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||||
AdminPostInventoryItemsReq,
|
|
||||||
AdminPostInventoryItemsParams,
|
AdminPostInventoryItemsParams,
|
||||||
|
AdminPostInventoryItemsReq,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { ResponsePromise } from "../../typings"
|
|
||||||
import BaseResource from "../base"
|
import BaseResource from "../base"
|
||||||
|
import { ResponsePromise } from "../../typings"
|
||||||
import qs from "qs"
|
import qs from "qs"
|
||||||
|
|
||||||
class AdminInventoryItemsResource extends BaseResource {
|
class AdminInventoryItemsResource extends BaseResource {
|
||||||
@@ -202,7 +203,7 @@ class AdminInventoryItemsResource extends BaseResource {
|
|||||||
query?: AdminGetInventoryItemsItemLocationLevelsParams,
|
query?: AdminGetInventoryItemsItemLocationLevelsParams,
|
||||||
customHeaders: Record<string, any> = {}
|
customHeaders: Record<string, any> = {}
|
||||||
): ResponsePromise<AdminInventoryItemsLocationLevelsRes> {
|
): ResponsePromise<AdminInventoryItemsLocationLevelsRes> {
|
||||||
let path = `/admin/inventory-items/${inventoryItemId}`
|
let path = `/admin/inventory-items/${inventoryItemId}/location-levels`
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
const queryString = qs.stringify(query)
|
const queryString = qs.stringify(query)
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
|
AdminGetInventoryItemsItemLocationLevelsParams,
|
||||||
AdminGetStockLocationsParams,
|
AdminGetStockLocationsParams,
|
||||||
AdminInventoryItemsListWithVariantsAndLocationLevelsRes,
|
AdminInventoryItemsListWithVariantsAndLocationLevelsRes,
|
||||||
AdminInventoryItemsLocationLevelsRes,
|
AdminInventoryItemsLocationLevelsRes,
|
||||||
AdminInventoryItemsRes,
|
AdminInventoryItemsRes,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
|
|
||||||
import { Response } from "@medusajs/medusa-js"
|
import { Response } from "@medusajs/medusa-js"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
|
||||||
import { useMedusa } from "../../../contexts"
|
|
||||||
import { UseQueryOptionsWrapper } from "../../../types"
|
import { UseQueryOptionsWrapper } from "../../../types"
|
||||||
import { queryKeysFactory } from "../../utils"
|
import { queryKeysFactory } from "../../utils"
|
||||||
|
import { useMedusa } from "../../../contexts"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
|
||||||
const ADMIN_INVENTORY_ITEMS_QUERY_KEY = `admin_inventory_items` as const
|
const ADMIN_INVENTORY_ITEMS_QUERY_KEY = `admin_inventory_items` as const
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ export const useAdminInventoryItem = (
|
|||||||
|
|
||||||
export const useAdminInventoryItemLocationLevels = (
|
export const useAdminInventoryItemLocationLevels = (
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
query?: AdminGetStockLocationsParams,
|
query?: AdminGetInventoryItemsItemLocationLevelsParams,
|
||||||
options?: UseQueryOptionsWrapper<
|
options?: UseQueryOptionsWrapper<
|
||||||
Response<AdminInventoryItemsLocationLevelsRes>,
|
Response<AdminInventoryItemsLocationLevelsRes>,
|
||||||
Error,
|
Error,
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import {
|
|||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
|
|
||||||
import { Response } from "@medusajs/medusa-js/src"
|
import { Response } from "@medusajs/medusa-js/src"
|
||||||
|
import { adminInventoryItemsKeys } from "../inventory-item"
|
||||||
import { adminReservationsKeys } from "./queries"
|
import { adminReservationsKeys } from "./queries"
|
||||||
|
import { adminVariantKeys } from "../variants"
|
||||||
import { buildOptions } from "../../utils/buildOptions"
|
import { buildOptions } from "../../utils/buildOptions"
|
||||||
import { useMedusa } from "../../../contexts"
|
import { useMedusa } from "../../../contexts"
|
||||||
import { adminVariantKeys } from "../variants"
|
|
||||||
|
|
||||||
export const useAdminCreateReservation = (
|
export const useAdminCreateReservation = (
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
@@ -57,6 +58,7 @@ export const useAdminUpdateReservation = (
|
|||||||
adminReservationsKeys.lists(),
|
adminReservationsKeys.lists(),
|
||||||
adminReservationsKeys.detail(id),
|
adminReservationsKeys.detail(id),
|
||||||
adminVariantKeys.all,
|
adminVariantKeys.all,
|
||||||
|
adminInventoryItemsKeys.details()
|
||||||
],
|
],
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -222,6 +222,38 @@ export type AdminInventoryItemsListRes = PaginatedResponse & {
|
|||||||
inventory_items: InventoryItemDTO[]
|
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
|
* @schema AdminInventoryItemsListWithVariantsAndLocationLevelsRes
|
||||||
* type: object
|
* type: object
|
||||||
@@ -234,20 +266,7 @@ export type AdminInventoryItemsListRes = PaginatedResponse & {
|
|||||||
* inventory_items:
|
* inventory_items:
|
||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* allOf:
|
* $ref: "#/components/schemas/DecoratedInventoryItemDTO"
|
||||||
* - $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"
|
|
||||||
* count:
|
* count:
|
||||||
* type: integer
|
* type: integer
|
||||||
* description: The total number of items available
|
* description: The total number of items available
|
||||||
@@ -260,11 +279,9 @@ export type AdminInventoryItemsListRes = PaginatedResponse & {
|
|||||||
*/
|
*/
|
||||||
export type AdminInventoryItemsListWithVariantsAndLocationLevelsRes =
|
export type AdminInventoryItemsListWithVariantsAndLocationLevelsRes =
|
||||||
PaginatedResponse & {
|
PaginatedResponse & {
|
||||||
inventory_items: (Partial<InventoryItemDTO> & {
|
inventory_items: DecoratedInventoryItemDTO[]
|
||||||
location_levels?: InventoryLevelDTO[]
|
|
||||||
variants?: ProductVariant[]
|
|
||||||
})[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @schema AdminInventoryItemsLocationLevelsRes
|
* @schema AdminInventoryItemsLocationLevelsRes
|
||||||
* type: object
|
* type: object
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { IInventoryService } from "@medusajs/types"
|
|
||||||
import { Transform } from "class-transformer"
|
|
||||||
import { IsBoolean, IsOptional, IsString } from "class-validator"
|
import { IsBoolean, IsOptional, IsString } from "class-validator"
|
||||||
import { Request, Response } from "express"
|
import {
|
||||||
|
NumericalComparisonOperator,
|
||||||
|
StringComparisonOperator,
|
||||||
|
extendedFindParamsMixin,
|
||||||
|
} from "../../../../types/common"
|
||||||
import {
|
import {
|
||||||
ProductVariantInventoryService,
|
ProductVariantInventoryService,
|
||||||
ProductVariantService,
|
ProductVariantService,
|
||||||
} from "../../../../services"
|
} from "../../../../services"
|
||||||
|
import { Request, Response } from "express"
|
||||||
|
import { getLevelsByInventoryItemId, joinLevels } from "./utils/join-levels"
|
||||||
import {
|
import {
|
||||||
extendedFindParamsMixin,
|
getVariantsByInventoryItemId,
|
||||||
NumericalComparisonOperator,
|
joinVariants,
|
||||||
StringComparisonOperator,
|
} from "./utils/join-variants"
|
||||||
} from "../../../../types/common"
|
|
||||||
|
import { IInventoryService } from "@medusajs/types"
|
||||||
import { IsType } from "../../../../utils/validators/is-type"
|
import { IsType } from "../../../../utils/validators/is-type"
|
||||||
import { getLevelsByInventoryItemId } from "./utils/join-levels"
|
import { Transform } from "class-transformer"
|
||||||
import { getVariantsByInventoryItemId } from "./utils/join-variants"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /admin/inventory-items
|
* @oas [get] /admin/inventory-items
|
||||||
@@ -117,30 +121,16 @@ export default async (req: Request, res: Response) => {
|
|||||||
listConfig
|
listConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
const levelsByItemId = await getLevelsByInventoryItemId(
|
const inventory_items = await joinVariants(
|
||||||
inventoryItems,
|
|
||||||
locationIds,
|
|
||||||
inventoryService
|
|
||||||
)
|
|
||||||
|
|
||||||
const variantsByInventoryItemId = await getVariantsByInventoryItemId(
|
|
||||||
inventoryItems,
|
inventoryItems,
|
||||||
productVariantInventoryService,
|
productVariantInventoryService,
|
||||||
productVariantService
|
productVariantService
|
||||||
)
|
).then(async (res) => {
|
||||||
|
return await joinLevels(res, locationIds, inventoryService)
|
||||||
const inventoryItemsWithVariantsAndLocationLevels = inventoryItems.map(
|
})
|
||||||
(inventoryItem) => {
|
|
||||||
return {
|
|
||||||
...inventoryItem,
|
|
||||||
variants: variantsByInventoryItemId[inventoryItem.id] ?? [],
|
|
||||||
location_levels: levelsByItemId[inventoryItem.id] ?? [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
inventory_items: inventoryItemsWithVariantsAndLocationLevels,
|
inventory_items,
|
||||||
count,
|
count,
|
||||||
offset: skip,
|
offset: skip,
|
||||||
limit: take,
|
limit: take,
|
||||||
|
|||||||
@@ -62,8 +62,22 @@ export const joinLevels = async (
|
|||||||
inventoryService
|
inventoryService
|
||||||
)
|
)
|
||||||
|
|
||||||
return inventoryItems.map((inventoryItem) => ({
|
return inventoryItems.map((inventoryItem) => {
|
||||||
...inventoryItem,
|
const levels = levelsByItemId[inventoryItem.id] ?? []
|
||||||
location_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 {
|
import {
|
||||||
ProductVariantInventoryService,
|
ProductVariantInventoryService,
|
||||||
ProductVariantService,
|
ProductVariantService,
|
||||||
} from "../../../../../services"
|
} from "../../../../../services"
|
||||||
|
|
||||||
|
import { InventoryItemDTO } from "@medusajs/types"
|
||||||
|
import { ProductVariant } from "../../../../../models"
|
||||||
|
|
||||||
export type InventoryItemsWithVariants = Partial<InventoryItemDTO> & {
|
export type InventoryItemsWithVariants = Partial<InventoryItemDTO> & {
|
||||||
variants?: ProductVariant[]
|
variants?: ProductVariant[]
|
||||||
}
|
}
|
||||||
@@ -34,3 +35,22 @@ export const getVariantsByInventoryItemId = async (
|
|||||||
return acc
|
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 {
|
export class AdminPostReservationsReq {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
line_item_id?: string
|
line_item_id?: string
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||||
|
import { InventoryItemDTO, ReservationItemDTO } from "@medusajs/types"
|
||||||
import middlewares, {
|
import middlewares, {
|
||||||
transformBody,
|
transformBody,
|
||||||
transformQuery,
|
transformQuery,
|
||||||
@@ -7,7 +8,7 @@ import middlewares, {
|
|||||||
import { AdminGetReservationsParams } from "./list-reservations"
|
import { AdminGetReservationsParams } from "./list-reservations"
|
||||||
import { AdminPostReservationsReq } from "./create-reservation"
|
import { AdminPostReservationsReq } from "./create-reservation"
|
||||||
import { AdminPostReservationsReservationReq } from "./update-reservation"
|
import { AdminPostReservationsReservationReq } from "./update-reservation"
|
||||||
import { ReservationItemDTO } from "@medusajs/types"
|
import { LineItem } from "../../../../models"
|
||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
|
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
|
||||||
|
|
||||||
@@ -68,6 +69,25 @@ export type AdminReservationsRes = {
|
|||||||
reservation: ReservationItemDTO
|
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
|
* @schema AdminReservationsListRes
|
||||||
* type: object
|
* type: object
|
||||||
@@ -80,7 +100,7 @@ export type AdminReservationsRes = {
|
|||||||
* reservations:
|
* reservations:
|
||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* $ref: "#/components/schemas/ReservationItemDTO"
|
* $ref: "#/components/schemas/ExtendedReservationItem"
|
||||||
* count:
|
* count:
|
||||||
* type: integer
|
* type: integer
|
||||||
* description: The total number of items available
|
* 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 { IsArray, IsOptional, IsString, ValidateNested } from "class-validator"
|
||||||
import { Request, Response } from "express"
|
|
||||||
import {
|
import {
|
||||||
extendedFindParamsMixin,
|
|
||||||
NumericalComparisonOperator,
|
NumericalComparisonOperator,
|
||||||
|
extendedFindParamsMixin,
|
||||||
} from "../../../../types/common"
|
} 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
|
* @oas [get] /admin/reservations
|
||||||
@@ -110,12 +116,47 @@ import {
|
|||||||
export default async (req: Request, res: Response) => {
|
export default async (req: Request, res: Response) => {
|
||||||
const inventoryService: IInventoryService =
|
const inventoryService: IInventoryService =
|
||||||
req.scope.resolve("inventoryService")
|
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(
|
const [reservations, count] = await inventoryService.listReservationItems(
|
||||||
req.filterableFields,
|
filterableFields,
|
||||||
req.listConfig
|
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
|
const { limit, offset } = req.validatedQuery
|
||||||
|
|
||||||
res.json({ reservations, count, limit, offset })
|
res.json({ reservations, count, limit, offset })
|
||||||
@@ -125,10 +166,9 @@ export class AdminGetReservationsParams extends extendedFindParamsMixin({
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}) {
|
}) {
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
location_id?: string[]
|
@IsType([String, [String]])
|
||||||
|
location_id?: string | string[]
|
||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5856,6 +5856,7 @@ __metadata:
|
|||||||
"@radix-ui/react-dialog": ^1.0.2
|
"@radix-ui/react-dialog": ^1.0.2
|
||||||
"@radix-ui/react-dropdown-menu": ^2.0.2
|
"@radix-ui/react-dropdown-menu": ^2.0.2
|
||||||
"@radix-ui/react-popover": ^1.0.3
|
"@radix-ui/react-popover": ^1.0.3
|
||||||
|
"@radix-ui/react-portal": ^1.0.2
|
||||||
"@radix-ui/react-radio-group": ^1.1.1
|
"@radix-ui/react-radio-group": ^1.1.1
|
||||||
"@radix-ui/react-select": ^1.2.0
|
"@radix-ui/react-select": ^1.2.0
|
||||||
"@radix-ui/react-switch": ^1.0.1
|
"@radix-ui/react-switch": ^1.0.1
|
||||||
@@ -7726,6 +7727,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@radix-ui/react-portal@npm:^1.0.2":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "@radix-ui/react-portal@npm:1.0.2"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.13.10
|
||||||
|
"@radix-ui/react-primitive": 1.0.2
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
checksum: 2397b9a3fe5e1f7b4982e0995fb088f77221754e42ebf5aa52374794ed7d35048ec530cc486927e65b1975fdb5f5caa1d8d2f87e8aa15004dfa011511fe8c606
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-presence@npm:1.0.0":
|
"@radix-ui/react-presence@npm:1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "@radix-ui/react-presence@npm:1.0.0"
|
resolution: "@radix-ui/react-presence@npm:1.0.0"
|
||||||
@@ -7753,6 +7767,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive@npm:1.0.2":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "@radix-ui/react-primitive@npm:1.0.2"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.13.10
|
||||||
|
"@radix-ui/react-slot": 1.0.1
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
checksum: bc9dc28568a9d4e0343363e62428fc13685667e470a663a69413a23145264f7a114de9d45c1ce33e6ccdeb7ae5bbfdbc198ee4b2505cd71da8f8d470c4f88d68
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-radio-group@npm:^1.1.1":
|
"@radix-ui/react-radio-group@npm:^1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "@radix-ui/react-radio-group@npm:1.1.1"
|
resolution: "@radix-ui/react-radio-group@npm:1.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user