diff --git a/.changeset/loud-adults-report.md b/.changeset/loud-adults-report.md new file mode 100644 index 0000000000..254bae192e --- /dev/null +++ b/.changeset/loud-adults-report.md @@ -0,0 +1,6 @@ +--- +"@medusajs/client-types": patch +"@medusajs/medusa": patch +--- + +fix(medusa): Adds updated_at query param to list-reservations. Fixes OAS for list-inventory-items. diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index 8b6f9602e2..250e9b82c4 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -57,7 +57,28 @@ "continue": "Continue", "edit": "Edit", "download": "Download", - "clearAll": "Clear all" + "clearAll": "Clear all", + "apply": "Apply" + }, + "filters": { + "date": { + "today": "Today", + "lastSevenDays": "Last 7 days", + "lastThirtyDays": "Last 30 days", + "lastNinetyDays": "Last 90 days", + "lastTwelveMonths": "Last 12 months", + "custom": "Custom" + }, + "compare": { + "lessThan": "Less than", + "greaterThan": "Greater than", + "exact": "Exact", + "range": "Range", + "lessThanLabel": "less than {{value}}", + "greaterThanLabel": "greater than {{value}}", + "andLabel": "and" + }, + "addFilter": "Add filter" }, "errorBoundary": { "badRequestTitle": "Bad request", @@ -125,7 +146,9 @@ "domain": "Categories" }, "inventory": { - "domain": "Inventory" + "domain": "Inventory", + "reserved": "Reserved", + "deleteWarning": "You are about to delete an inventory item. This action cannot be undone." }, "giftCards": { "domain": "Gift Cards", @@ -451,6 +474,10 @@ "removeSalesChannelsWarning_one": "You are about to remove {{count}} sales channel from the location.", "removeSalesChannelsWarning_other": "You are about to remove {{count}} sales channels from the location." }, + "reservations": { + "domain": "Reservations", + "deleteWarning": "You are about to delete a reservation. This action cannot be undone." + }, "salesChannels": { "domain": "Sales Channels", "createSalesChannel": "Create Sales Channel", @@ -660,12 +687,17 @@ "thumbnail": "Thumbnail", "sku": "SKU", "managedInventory": "Managed inventory", + "inStock": "In stock", + "location": "Location", + "quantity": "Quantity", + "variant": "Variant", "id": "ID", "minSubtotal": "Min. Subtotal", "maxSubtotal": "Max. Subtotal", "shippingProfile": "Shipping Profile", "summary": "Summary", - "rate": "Rate" + "rate": "Rate", + "requiresShipping": "Requires shipping" }, "dateTime": { "years_one": "Year", diff --git a/packages/admin-next/dashboard/src/components/common/inline-link/index.ts b/packages/admin-next/dashboard/src/components/common/inline-link/index.ts new file mode 100644 index 0000000000..0a2021c6aa --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/inline-link/index.ts @@ -0,0 +1 @@ +export * from "./inline-link" diff --git a/packages/admin-next/dashboard/src/components/common/inline-link/inline-link.tsx b/packages/admin-next/dashboard/src/components/common/inline-link/inline-link.tsx new file mode 100644 index 0000000000..2acdccbf5a --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/inline-link/inline-link.tsx @@ -0,0 +1,21 @@ +import { clx } from "@medusajs/ui" +import { ComponentPropsWithoutRef } from "react" +import { Link } from "react-router-dom" + +export const InlineLink = ({ + className, + ...props +}: ComponentPropsWithoutRef) => { + return ( + { + e.stopPropagation() + }} + className={clx( + "text-ui-fg-interactive transition-fg hover:text-ui-fg-interactive-hover focus-visible:text-ui-fg-interactive-hover rounded-md outline-none", + className + )} + {...props} + /> + ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx b/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx index 0d76bd7dd4..40bf61c1f9 100644 --- a/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx +++ b/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx @@ -1,4 +1,5 @@ import { + Buildings, ChevronDownMini, CurrencyDollar, MinusMini, @@ -61,14 +62,14 @@ const Header = () => { {fallback ? ( ) : ( - + )} {name ? ( {store.name} ) : ( - + )} @@ -108,9 +109,16 @@ const useCoreRoutes = (): Omit[] => { label: t("giftCards.domain"), to: "/gift-cards", }, + ], + }, + { + icon: , + label: t("inventory.domain"), + to: "/inventory", + items: [ { - label: t("inventory.domain"), - to: "/inventory", + label: t("reservations.domain"), + to: "/reservations", }, ], }, diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx index c5ac5f6be9..8405f46f4c 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx @@ -3,9 +3,12 @@ import * as Popover from "@radix-ui/react-popover" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useSearchParams } from "react-router-dom" +import { useTranslation } from "react-i18next" import { DataTableFilterContext, useDataTableFilterContext } from "./context" import { DateFilter } from "./date-filter" +import { NumberFilter } from "./number-filter" import { SelectFilter } from "./select-filter" +import { StringFilter } from "./string-filter" type Option = { label: string @@ -26,6 +29,14 @@ export type Filter = { type: "date" options?: never } + | { + type: "string" + options?: never + } + | { + type: "number" + options?: never + } ) type DataTableFilterProps = { @@ -34,6 +45,7 @@ type DataTableFilterProps = { } export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { + const { t } = useTranslation() const [searchParams] = useSearchParams() const [open, setOpen] = useState(false) @@ -60,7 +72,6 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { const key = prefix ? `${prefix}_${filter.key}` : filter.key const value = params.get(key) if (value && !activeFilters.find((af) => af.key === filter.key)) { - console.log("adding filter", filter.key, "to active filters") if (filter.type === "select") { setActiveFilters((prev) => [ ...prev, @@ -109,40 +120,61 @@ export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { >
{activeFilters.map((filter) => { - if (filter.type === "select") { - return ( - - ) + switch (filter.type) { + case "select": + return ( + + ) + case "date": + return ( + + ) + case "string": + return ( + + ) + case "number": + return ( + + ) + default: + break } - - return ( - - ) })} {availableFilters.length > 0 && ( { const [open, setOpen] = useState(openOnMount) const [showCustom, setShowCustom] = useState(false) + const { key, label } = filter + const { removeFilter } = useDataTableFilterContext() const selectedParams = useSelectedParams({ param: key, prefix }) + const presets = usePresets() + const handleSelectPreset = (value: DateComparisonOperator) => { selectedParams.add(JSON.stringify(value)) setShowCustom(false) @@ -100,7 +118,7 @@ export const DateFilter = ({ } return ( - +
- Custom + {t("filters.date.custom")} @@ -275,38 +293,53 @@ const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => { const today = new Date() today.setHours(0, 0, 0, 0) -const presets: { label: string; value: DateComparisonOperator }[] = [ - { - label: "Today", - value: { - gte: today.toISOString(), - }, - }, - { - label: "Last 7 days", - value: { - gte: new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days ago - }, - }, - { - label: "Last 30 days", - value: { - gte: new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days ago - }, - }, - { - label: "Last 90 days", - value: { - gte: new Date(today.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(), // 90 days ago - }, - }, - { - label: "Last 12 months", - value: { - gte: new Date(today.getTime() - 365 * 24 * 60 * 60 * 1000).toISOString(), // 365 days ago - }, - }, -] +const usePresets = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + { + label: t("filters.date.today"), + value: { + gte: today.toISOString(), + }, + }, + { + label: t("filters.date.lastSevenDays"), + value: { + gte: new Date( + today.getTime() - 7 * 24 * 60 * 60 * 1000 + ).toISOString(), // 7 days ago + }, + }, + { + label: t("filters.date.lastThirtyDays"), + value: { + gte: new Date( + today.getTime() - 30 * 24 * 60 * 60 * 1000 + ).toISOString(), // 30 days ago + }, + }, + { + label: t("filters.date.lastNinetyDays"), + value: { + gte: new Date( + today.getTime() - 90 * 24 * 60 * 60 * 1000 + ).toISOString(), // 90 days ago + }, + }, + { + label: t("filters.date.lastTwelveMonths"), + value: { + gte: new Date( + today.getTime() - 365 * 24 * 60 * 60 * 1000 + ).toISOString(), // 365 days ago + }, + }, + ], + [t] + ) +} const parseDateComparison = (value: string[]) => { return value?.length diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/number-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/number-filter.tsx new file mode 100644 index 0000000000..30fde6c43e --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/number-filter.tsx @@ -0,0 +1,372 @@ +import { EllipseMiniSolid, XMarkMini } from "@medusajs/icons" +import { Input, Label, Text, clx } from "@medusajs/ui" +import * as Popover from "@radix-ui/react-popover" +import * as RadioGroup from "@radix-ui/react-radio-group" +import { debounce } from "lodash" +import { + ChangeEvent, + MouseEvent, + useCallback, + useEffect, + useState, +} from "react" +import { useTranslation } from "react-i18next" + +import { useSelectedParams } from "../hooks" +import { useDataTableFilterContext } from "./context" +import { IFilter } from "./types" + +type NumberFilterProps = IFilter + +type Comparison = "exact" | "range" +type Operator = "lt" | "gt" | "eq" + +export const NumberFilter = ({ + filter, + prefix, + openOnMount, +}: NumberFilterProps) => { + const { t } = useTranslation() + const [open, setOpen] = useState(openOnMount) + + const { key, label } = filter + + const { removeFilter } = useDataTableFilterContext() + const selectedParams = useSelectedParams({ + param: key, + prefix, + multiple: false, + }) + + const currentValue = selectedParams.get() + + const [operator, setOperator] = useState( + getOperator(currentValue) + ) + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedOnChange = useCallback( + debounce((e: ChangeEvent, operator: Operator) => { + const value = e.target.value + const curr = JSON.parse(currentValue?.join(",") || "{}") + const isCurrentNumber = !isNaN(Number(curr)) + + const handleValue = (operator: Operator) => { + if (!value && isCurrentNumber) { + selectedParams.delete() + return + } + + if (curr && !value) { + delete curr[operator] + selectedParams.add(JSON.stringify(curr)) + return + } + + if (!curr) { + selectedParams.add(JSON.stringify({ [operator]: value })) + return + } + + selectedParams.add(JSON.stringify({ ...curr, [operator]: value })) + } + + switch (operator) { + case "eq": + if (!value) { + selectedParams.delete() + } else { + selectedParams.add(value) + } + break + case "lt": + case "gt": + handleValue(operator) + break + } + }, 500), + [selectedParams, currentValue] + ) + + useEffect(() => { + return () => { + debouncedOnChange.cancel() + } + }, [debouncedOnChange]) + + let timeoutId: ReturnType | null = null + + const handleOpenChange = (open: boolean) => { + setOpen(open) + + if (timeoutId) { + clearTimeout(timeoutId) + } + + if (!open && !currentValue.length) { + timeoutId = setTimeout(() => { + removeFilter(key) + }, 200) + } + } + + const handleRemove = () => { + selectedParams.delete() + removeFilter(key) + } + + const operators: { operator: Comparison; label: string }[] = [ + { + operator: "exact", + label: t("filters.compare.exact"), + }, + { + operator: "range", + label: t("filters.compare.range"), + }, + ] + + const GT_KEY = `${key}-gt` + const LT_KEY = `${key}-lt` + const EQ_KEY = key + + return ( + + + + { + if (e.target instanceof HTMLElement) { + if ( + e.target.attributes.getNamedItem("data-name")?.value === + "filters_menu_content" + ) { + e.preventDefault() + } + } + }} + > +
+ setOperator(val as Comparison)} + className="flex flex-col items-start" + orientation="vertical" + autoFocus + > + {operators.map((o) => ( + +
+ + + +
+ {o.label} +
+ ))} +
+
+
+ {operator === "range" ? ( +
+
+ +
+
+ debouncedOnChange(e, "gt")} + /> +
+
+ +
+
+ debouncedOnChange(e, "lt")} + /> +
+
+ ) : ( +
+
+ +
+
+ debouncedOnChange(e, "eq")} + /> +
+
+ )} +
+
+
+
+ ) +} + +const NumberDisplay = ({ + label, + value, + onRemove, +}: { + label: string + value?: string[] + onRemove: () => void +}) => { + const { t } = useTranslation() + const handleRemove = (e: MouseEvent) => { + e.stopPropagation() + onRemove() + } + + const parsed = JSON.parse(value?.join(",") || "{}") + let displayValue = "" + + if (typeof parsed === "object") { + const parts = [] + if (parsed.gt) { + parts.push(t("filters.compare.greaterThanLabel", { value: parsed.gt })) + } + + if (parsed.lt) { + parts.push( + t("filters.compare.lessThanLabel", { + value: parsed.lt, + }) + ) + } + + displayValue = parts.join(` ${t("filters.compare.andLabel")} `) + } + + if (typeof parsed === "number") { + displayValue = parsed.toString() + } + + return ( + +
+
+ + {label} + +
+ {!!value && ( +
+ + {t("general.is")} + +
+ )} + {value && ( +
+
+ + {displayValue} + +
+
+ )} + {value && ( +
+ +
+ )} +
+
+ ) +} + +const parseValue = (value: string[] | null | undefined) => { + if (!value) { + return undefined + } + + const val = value.join(",") + if (!val) { + return undefined + } + + return JSON.parse(val) +} + +const getValue = ( + value: string[] | null | undefined, + key: Operator +): number | undefined => { + const parsed = parseValue(value) + + if (typeof parsed === "object") { + return parsed[key] + } + if (typeof parsed === "number" && key === "eq") { + return parsed + } + + return undefined +} + +const getOperator = (value?: string[] | null): Comparison | undefined => { + const parsed = parseValue(value) + + return typeof parsed === "object" ? "range" : "exact" +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx index cf4a1d784f..a9d27c79a6 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx @@ -61,6 +61,7 @@ export const SelectFilter = ({ const handleClearSearch = () => { setSearch("") + if (searchRef) { searchRef.focus() } @@ -112,7 +113,7 @@ export const SelectFilter = ({ ref={setSearchRef} value={search} onValueChange={setSearch} - className="txt-compact-small placeholder:text-ui-fg-muted outline-none" + className="txt-compact-small placeholder:text-ui-fg-muted bg-transparent outline-none" placeholder="Search" />
diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/string-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/string-filter.tsx new file mode 100644 index 0000000000..62c088f75d --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/string-filter.tsx @@ -0,0 +1,188 @@ +import { XMarkMini } from "@medusajs/icons" +import { Input, Label, Text, clx } from "@medusajs/ui" +import * as Popover from "@radix-ui/react-popover" +import { debounce } from "lodash" +import { ChangeEvent, useCallback, useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { useSelectedParams } from "../hooks" +import { useDataTableFilterContext } from "./context" +import { IFilter } from "./types" + +type StringFilterProps = IFilter + +export const StringFilter = ({ + filter, + prefix, + openOnMount, +}: StringFilterProps) => { + const [open, setOpen] = useState(openOnMount) + + const { key, label } = filter + + const { removeFilter } = useDataTableFilterContext() + const selectedParams = useSelectedParams({ param: key, prefix }) + + const query = selectedParams.get() + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedOnChange = useCallback( + debounce((e: ChangeEvent) => { + const value = e.target.value + + if (!value) { + selectedParams.delete() + } else { + selectedParams.add(value) + } + }, 500), + [selectedParams] + ) + + useEffect(() => { + return () => { + debouncedOnChange.cancel() + } + }, [debouncedOnChange]) + + let timeoutId: ReturnType | null = null + + const handleOpenChange = (open: boolean) => { + setOpen(open) + + if (timeoutId) { + clearTimeout(timeoutId) + } + + if (!open && !query.length) { + timeoutId = setTimeout(() => { + removeFilter(key) + }, 200) + } + } + + const handleRemove = () => { + selectedParams.delete() + removeFilter(key) + } + + return ( + + + + { + if (e.target instanceof HTMLElement) { + if ( + e.target.attributes.getNamedItem("data-name")?.value === + "filters_menu_content" + ) { + e.preventDefault() + e.stopPropagation() + } + } + }} + > +
+
+ +
+
+ +
+
+
+
+
+ ) +} + +const StringDisplay = ({ + label, + value, + onRemove, +}: { + label: string + value?: string + onRemove: () => void +}) => { + const { t } = useTranslation() + + return ( + +
+
+ + {label} + +
+
+ {!!value && ( +
+ + {t("general.is")} + +
+ )} + {!!value && ( +
+ + {value} + +
+ )} +
+ {!!value && ( +
+ +
+ )} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx index 2a95a8da0a..59de680244 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx @@ -188,7 +188,6 @@ export const DataTableRoot = ({ data-selected={row.getIsSelected()} className={clx( "transition-fg group/row [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap", - "[&:has(td_a:focus-visible)_td]:bg-ui-bg-base-pressed", { "cursor-pointer": !!to, "bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover": @@ -215,8 +214,8 @@ export const DataTableRoot = ({ return ( { diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index 694aaf19b9..77d0986f53 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -223,6 +223,38 @@ const router = createBrowserRouter([ }, ], }, + { + path: "/inventory", + handle: { + crumb: () => "Inventory", + }, + lazy: () => import("../../routes/inventory/inventory-list"), + }, + { + path: "/reservations", + handle: { + crumb: () => "Reservations", + }, + children: [ + { + path: "", + lazy: () => + import("../../routes/reservations/reservation-list"), + }, + { + path: ":id", + lazy: () => + import("../../routes/reservations/reservation-detail"), + // children: [ + // { + // path: "edit", + // lazy: () => + // import("../../routes/reservations/reservation-edit"), + // }, + // ], + }, + ], + }, { path: "/customers", handle: { @@ -335,13 +367,6 @@ const router = createBrowserRouter([ }, ], }, - { - path: "/inventory", - handle: { - crumb: () => "Inventory", - }, - lazy: () => import("../../routes/inventory/list"), - }, { path: "/discounts", handle: { diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/index.ts b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/index.ts new file mode 100644 index 0000000000..2eb5c78069 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/index.ts @@ -0,0 +1 @@ +export * from "./inventory-list-table" diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/inventory-actions.tsx b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/inventory-actions.tsx new file mode 100644 index 0000000000..d03e315b5a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/inventory-actions.tsx @@ -0,0 +1,52 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { InventoryItemDTO } from "@medusajs/types" +import { usePrompt } from "@medusajs/ui" +import { useAdminDeleteInventoryItem } from "medusa-react" +import { useTranslation } from "react-i18next" +import { ActionMenu } from "../../../../../components/common/action-menu" + +export const InventoryActions = ({ item }: { item: InventoryItemDTO }) => { + const { t } = useTranslation() + const prompt = usePrompt() + const { mutateAsync } = useAdminDeleteInventoryItem(item.id) + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("inventory.deleteWarning"), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync() + } + + return ( + , + label: t("actions.edit"), + to: `${item.id}/edit`, + }, + ], + }, + { + actions: [ + { + icon: , + label: t("actions.delete"), + onClick: handleDelete, + }, + ], + }, + ]} + /> + ) +} diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/inventory-list-table.tsx b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/inventory-list-table.tsx new file mode 100644 index 0000000000..8711341c82 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/inventory-list-table.tsx @@ -0,0 +1,64 @@ +import { Container, Heading } from "@medusajs/ui" +import { useAdminInventoryItems } from "medusa-react" +import { useTranslation } from "react-i18next" +import { DataTable } from "../../../../../components/table/data-table" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { useInventoryTableColumns } from "./use-inventory-table-columns" +import { useInventoryTableFilters } from "./use-inventory-table-filters" +import { useInventoryTableQuery } from "./use-inventory-table-query" + +const PAGE_SIZE = 20 + +export const InventoryListTable = () => { + const { t } = useTranslation() + + const { searchParams, raw } = useInventoryTableQuery({ + pageSize: PAGE_SIZE, + }) + const { inventory_items, count, isLoading, isError, error } = + useAdminInventoryItems( + { + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const filters = useInventoryTableFilters() + const columns = useInventoryTableColumns() + + const { table } = useDataTable({ + data: inventory_items || [], + columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + }) + + if (isError) { + throw error + } + + return ( + +
+ {t("inventory.domain")} +
+ `${row.id}`} + /> +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/use-inventory-table-columns.tsx b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/use-inventory-table-columns.tsx new file mode 100644 index 0000000000..e8726f6921 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/use-inventory-table-columns.tsx @@ -0,0 +1,118 @@ +import { ProductVariant } from "@medusajs/medusa" +import { InventoryItemDTO } from "@medusajs/types" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell" +import { InventoryActions } from "./inventory-actions" + +/** + * Adds missing properties to the InventoryItemDTO type. + */ +interface ExtendedInventoryItem extends InventoryItemDTO { + variants?: ProductVariant[] | null + stocked_quantity?: number + reserved_quantity?: number +} + +const columnHelper = createColumnHelper() + +export const useInventoryTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("title", { + header: t("fields.title"), + cell: ({ getValue }) => { + const title = getValue() + + if (!title) { + return + } + + return ( +
+ {title} +
+ ) + }, + }), + columnHelper.accessor("sku", { + header: t("fields.sku"), + cell: ({ getValue }) => { + const sku = getValue() + + if (!sku) { + return + } + + return ( +
+ {sku} +
+ ) + }, + }), + columnHelper.accessor("variants", { + header: t("fields.variant"), + cell: ({ getValue }) => { + const variants = getValue() + + if (!variants || variants.length === 0) { + return + } + + /** + * There is always only one variant despite it being an array, + * so we can safely access the first element. + */ + const variant = variants[0] + + return ( +
+ {variant.title} +
+ ) + }, + }), + columnHelper.accessor("reserved_quantity", { + header: t("inventory.reserved"), + cell: ({ getValue }) => { + const quantity = getValue() + + if (!quantity) { + return + } + + return ( +
+ {quantity} +
+ ) + }, + }), + columnHelper.accessor("stocked_quantity", { + header: t("fields.inStock"), + cell: ({ getValue }) => { + const quantity = getValue() + + if (!quantity) { + return + } + + return ( +
+ {quantity} +
+ ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/use-inventory-table-filters.tsx b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/use-inventory-table-filters.tsx new file mode 100644 index 0000000000..e906f7cfb1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/use-inventory-table-filters.tsx @@ -0,0 +1,82 @@ +import { useAdminStockLocations } from "medusa-react" +import { useTranslation } from "react-i18next" +import { Filter } from "../../../../../components/table/data-table" + +export const useInventoryTableFilters = () => { + const { t } = useTranslation() + const { stock_locations } = useAdminStockLocations({ + limit: 1000, + }) + + const filters: Filter[] = [] + + if (stock_locations) { + const stockLocationFilter: Filter = { + type: "select", + options: stock_locations.map((s) => ({ + label: s.name, + value: s.id, + })), + key: "location_id", + searchable: true, + label: t("fields.location"), + } + + filters.push(stockLocationFilter) + } + + filters.push({ + type: "string", + key: "material", + label: t("fields.material"), + }) + + filters.push({ + type: "string", + key: "sku", + label: t("fields.sku"), + }) + + filters.push({ + type: "string", + key: "mid_code", + label: t("fields.midCode"), + }) + + filters.push({ + type: "number", + key: "height", + label: t("fields.height"), + }) + + filters.push({ + type: "number", + key: "width", + label: t("fields.width"), + }) + + filters.push({ + type: "number", + key: "length", + label: t("fields.length"), + }) + + filters.push({ + type: "number", + key: "weight", + label: t("fields.weight"), + }) + + filters.push({ + type: "select", + options: [ + { label: t("fields.true"), value: "true" }, + { label: t("fields.false"), value: "false" }, + ], + key: "requires_shipping", + multiple: false, + label: t("fields.requiresShipping"), + }) + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/use-inventory-table-query.tsx b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/use-inventory-table-query.tsx new file mode 100644 index 0000000000..286b2528d8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/components/inventory-list-table/use-inventory-table-query.tsx @@ -0,0 +1,57 @@ +import { AdminGetInventoryItemsParams } from "@medusajs/medusa" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useInventoryTableQuery = ({ + pageSize = 20, + prefix, +}: { + pageSize?: number + prefix?: string +}) => { + const raw = useQueryParams( + [ + "location_id", + "q", + "order", + "requires_shipping", + "offset", + "sku", + "material", + "mid_code", + "order", + "weight", + "width", + "length", + "height", + ], + prefix + ) + + const { + offset, + weight, + width, + length, + height, + requires_shipping, + ...params + } = raw + + const searchParams: AdminGetInventoryItemsParams = { + limit: pageSize, + offset: offset ? parseInt(offset) : undefined, + weight: weight ? JSON.parse(weight) : undefined, + width: width ? JSON.parse(width) : undefined, + length: length ? JSON.parse(length) : undefined, + height: height ? JSON.parse(height) : undefined, + requires_shipping: requires_shipping + ? JSON.parse(requires_shipping) + : undefined, + ...params, + } + + return { + searchParams, + raw, + } +} diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-list/index.ts b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/index.ts new file mode 100644 index 0000000000..7f1f3e9fe3 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/index.ts @@ -0,0 +1 @@ +export { InventoryList as Component } from "./inventory-list" diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-list/inventory-list.tsx b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/inventory-list.tsx new file mode 100644 index 0000000000..d4d5405bb5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-list/inventory-list.tsx @@ -0,0 +1,9 @@ +import { InventoryListTable } from "./components/inventory-list-table" + +export const InventoryList = () => { + return ( +
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/inventory/list/index.ts b/packages/admin-next/dashboard/src/routes/inventory/list/index.ts deleted file mode 100644 index cc22dde9bc..0000000000 --- a/packages/admin-next/dashboard/src/routes/inventory/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { InventoryList as Component } from "./list"; diff --git a/packages/admin-next/dashboard/src/routes/inventory/list/list.tsx b/packages/admin-next/dashboard/src/routes/inventory/list/list.tsx deleted file mode 100644 index 792b2186be..0000000000 --- a/packages/admin-next/dashboard/src/routes/inventory/list/list.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const InventoryList = () => { - return ( - - Inventory - - ); -}; diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/components/reservation-general-section/index.ts b/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/components/reservation-general-section/index.ts new file mode 100644 index 0000000000..61935f829f --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/components/reservation-general-section/index.ts @@ -0,0 +1 @@ +export * from "./reservation-general-section" diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/components/reservation-general-section/reservation-general-section.tsx b/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/components/reservation-general-section/reservation-general-section.tsx new file mode 100644 index 0000000000..e0fc97daf3 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/components/reservation-general-section/reservation-general-section.tsx @@ -0,0 +1,27 @@ +import { ExtendedReservationItem } from "@medusajs/medusa" +import { Container } from "@medusajs/ui" +import { useAdminInventoryItem } from "medusa-react" + +type ReservationGeneralSectionProps = { + reservation: ExtendedReservationItem +} + +// TODO: Its not possible to get the data necessary for this component with the current API + +export const ReservationGeneralSection = ({ + reservation, +}: ReservationGeneralSectionProps) => { + const { inventory_item, isLoading, isError, error } = useAdminInventoryItem( + reservation.inventory_item_id + ) + + if (isLoading || !inventory_item) { + return
Loading...
+ } + + if (isError) { + throw error + } + + return +} diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/index.ts b/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/index.ts new file mode 100644 index 0000000000..233de04c8c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/index.ts @@ -0,0 +1 @@ +export { ReservationDetail as Component } from "./reservation-edit" diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/reservation-edit.tsx b/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/reservation-edit.tsx new file mode 100644 index 0000000000..bedfb5fccc --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-detail/reservation-edit.tsx @@ -0,0 +1,25 @@ +import { useAdminReservation } from "medusa-react" +import { useParams } from "react-router-dom" +import { JsonViewSection } from "../../../components/common/json-view-section" +import { ReservationGeneralSection } from "./components/reservation-general-section" + +export const ReservationDetail = () => { + const { id } = useParams() + + const { reservation, isLoading, isError, error } = useAdminReservation(id!) + + if (isLoading || !reservation) { + return
Loading...
+ } + + if (isError) { + throw error + } + + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/index.ts b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/index.ts new file mode 100644 index 0000000000..8248f5647d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/index.ts @@ -0,0 +1 @@ +export * from "./reservation-list-table" diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/reservation-actions.tsx b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/reservation-actions.tsx new file mode 100644 index 0000000000..edf50937c6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/reservation-actions.tsx @@ -0,0 +1,56 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { ExtendedReservationItem } from "@medusajs/medusa" +import { usePrompt } from "@medusajs/ui" +import { useAdminDeleteReservation } from "medusa-react" +import { useTranslation } from "react-i18next" +import { ActionMenu } from "../../../../../components/common/action-menu" + +export const ReservationActions = ({ + reservation, +}: { + reservation: ExtendedReservationItem +}) => { + const { t } = useTranslation() + const prompt = usePrompt() + const { mutateAsync } = useAdminDeleteReservation(reservation.id) + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("reservations.deleteWarning"), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync() + } + + return ( + , + }, + ], + }, + { + actions: [ + { + label: t("actions.delete"), + onClick: handleDelete, + icon: , + }, + ], + }, + ]} + /> + ) +} diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/reservation-list-table.tsx b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/reservation-list-table.tsx new file mode 100644 index 0000000000..2ed378d75b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/reservation-list-table.tsx @@ -0,0 +1,71 @@ +import { useAdminReservations } from "medusa-react" + +import { Button, Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" +import { DataTable } from "../../../../../components/table/data-table" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { reservationListExpand } from "../../constants" +import { useReservationTableColumns } from "./use-reservation-table-columns" +import { useReservationTableFilters } from "./use-reservation-table-filters" +import { useReservationTableQuery } from "./use-reservation-table-query" + +const PAGE_SIZE = 20 + +export const ReservationListTable = () => { + const { t } = useTranslation() + + const { searchParams, raw } = useReservationTableQuery({ + pageSize: PAGE_SIZE, + }) + const { reservations, count, isLoading, isError, error } = + useAdminReservations( + { + expand: reservationListExpand, + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const filters = useReservationTableFilters() + const columns = useReservationTableColumns() + + const { table } = useDataTable({ + data: reservations || [], + columns, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + }) + + if (isError) { + throw error + } + + return ( + +
+ {t("reservations.domain")} + +
+ `${row.id}`} + orderBy={["created_at", "updated_at"]} + queryObject={raw} + search={false} + /> +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-columns.tsx b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-columns.tsx new file mode 100644 index 0000000000..317295ab4b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-columns.tsx @@ -0,0 +1,104 @@ +import { ExtendedReservationItem } from "@medusajs/medusa" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { InlineLink } from "../../../../../components/common/inline-link" +import { DateCell } from "../../../../../components/table/table-cells/common/date-cell" +import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell" +import { ReservationActions } from "./reservation-actions" + +const columnHelper = createColumnHelper() + +export const useReservationTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("inventory_item", { + header: t("fields.sku"), + cell: ({ getValue }) => { + const inventoryItem = getValue() + + if (!inventoryItem || !inventoryItem.sku) { + return + } + + return ( +
+ {inventoryItem.sku} +
+ ) + }, + }), + columnHelper.accessor("line_item", { + header: t("fields.order"), + cell: ({ getValue }) => { + const inventoryItem = getValue() + + if (!inventoryItem || !inventoryItem.order?.display_id) { + return + } + + return ( +
+ + + #{inventoryItem.order.display_id} + + +
+ ) + }, + }), + columnHelper.accessor("description", { + header: t("fields.description"), + cell: ({ getValue }) => { + const description = getValue() + + if (!description) { + return + } + + return ( +
+ {description} +
+ ) + }, + }), + columnHelper.accessor("created_at", { + header: t("fields.created"), + cell: ({ getValue }) => { + const created = getValue() + + return + }, + }), + columnHelper.accessor("quantity", { + header: () => ( +
+ {t("fields.quantity")} +
+ ), + cell: ({ getValue }) => { + const quantity = getValue() + + return ( +
+ {quantity} +
+ ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => { + const reservation = row.original + + return + }, + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-filters.tsx b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-filters.tsx new file mode 100644 index 0000000000..6c12ae5d1e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-filters.tsx @@ -0,0 +1,35 @@ +import { useAdminStockLocations } from "medusa-react" +import { useTranslation } from "react-i18next" +import { Filter } from "../../../../../components/table/data-table" + +export const useReservationTableFilters = () => { + const { t } = useTranslation() + const { stock_locations } = useAdminStockLocations({ + limit: 1000, + }) + + const filters: Filter[] = [] + + if (stock_locations) { + const stockLocationFilter: Filter = { + type: "select", + options: stock_locations.map((s) => ({ + label: s.name, + value: s.id, + })), + key: "location_id", + searchable: true, + label: t("fields.location"), + } + + filters.push(stockLocationFilter) + } + + filters.push({ + type: "date", + key: "created_at", + label: t("fields.createdAt"), + }) + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-query.tsx b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-query.tsx new file mode 100644 index 0000000000..7d2478049a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-query.tsx @@ -0,0 +1,32 @@ +import { AdminGetReservationsParams } from "@medusajs/medusa" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useReservationTableQuery = ({ + pageSize = 20, + prefix, +}: { + pageSize?: number + prefix?: string +}) => { + const raw = useQueryParams( + ["location_id", "offset", "created_at", "quantity", "updated_at", "order"], + prefix + ) + + const { location_id, created_at, updated_at, quantity, offset, ...rest } = raw + + const searchParams: AdminGetReservationsParams = { + limit: pageSize, + offset: offset ? parseInt(offset) : undefined, + location_id: location_id, + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + quantity: quantity ? JSON.parse(quantity) : undefined, + ...rest, + } + + return { + searchParams, + raw, + } +} diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/constants.ts b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/constants.ts new file mode 100644 index 0000000000..af36e02261 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/constants.ts @@ -0,0 +1 @@ +export const reservationListExpand = "line_item,inventory_item" diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/index.ts b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/index.ts new file mode 100644 index 0000000000..40b8ea104c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/index.ts @@ -0,0 +1 @@ +export { ReservationList as Component } from "./reservation-list" diff --git a/packages/admin-next/dashboard/src/routes/reservations/reservation-list/reservation-list.tsx b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/reservation-list.tsx new file mode 100644 index 0000000000..d5abda70c8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/reservations/reservation-list/reservation-list.tsx @@ -0,0 +1,9 @@ +import { ReservationListTable } from "./components/reservation-list-table" + +export const ReservationList = () => { + return ( +
+ +
+ ) +} diff --git a/packages/generated/client-types/src/lib/models/AdminGetInventoryItemsParams.ts b/packages/generated/client-types/src/lib/models/AdminGetInventoryItemsParams.ts index cbd0a5c72b..dd07c92c51 100644 --- a/packages/generated/client-types/src/lib/models/AdminGetInventoryItemsParams.ts +++ b/packages/generated/client-types/src/lib/models/AdminGetInventoryItemsParams.ts @@ -24,6 +24,10 @@ export interface AdminGetInventoryItemsParams { * term to search inventory item's sku, title, and description. */ q?: string + /** + * Field to sort-order inventory items by. + */ + order?: string /** * Filter by location IDs. */ diff --git a/packages/medusa/src/api/routes/admin/inventory-items/list-inventory-items.ts b/packages/medusa/src/api/routes/admin/inventory-items/list-inventory-items.ts index b96f371ea3..1c09ba1066 100644 --- a/packages/medusa/src/api/routes/admin/inventory-items/list-inventory-items.ts +++ b/packages/medusa/src/api/routes/admin/inventory-items/list-inventory-items.ts @@ -1,23 +1,20 @@ import { IsBoolean, IsOptional, IsString } from "class-validator" +import { Request, Response } from "express" +import { + ProductVariantInventoryService, + ProductVariantService, +} from "../../../../services" 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 { - getVariantsByInventoryItemId, - joinVariants, -} from "./utils/join-variants" +import { joinLevels } from "./utils/join-levels" +import { joinVariants } from "./utils/join-variants" import { IInventoryService } from "@medusajs/types" -import { IsType } from "../../../../utils/validators/is-type" import { Transform } from "class-transformer" +import { IsType } from "../../../../utils/validators/is-type" /** * @oas [get] /admin/inventory-items @@ -31,6 +28,7 @@ import { Transform } from "class-transformer" * - (query) expand {string} Comma-separated relations that should be expanded in each returned inventory item. * - (query) fields {string} Comma-separated fields that should be included in the returned inventory item. * - (query) q {string} term to search inventory item's sku, title, and description. + * - (query) order {string} Field to sort-order inventory items by. * - in: query * name: location_id * style: form diff --git a/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts b/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts index fcc17481fa..7234a59cfd 100644 --- a/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts +++ b/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts @@ -1,20 +1,20 @@ -import { - DateComparisonOperator, - extendedFindParamsMixin, - NumericalComparisonOperator, - StringComparisonOperator, -} from "../../../../types/common" import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator" import { Request, Response } from "express" +import { + DateComparisonOperator, + NumericalComparisonOperator, + StringComparisonOperator, + extendedFindParamsMixin, +} from "../../../../types/common" -import { EntityManager } from "typeorm" import { IInventoryService } from "@medusajs/types" -import { IsType } from "../../../../utils/validators/is-type" -import { LineItemService } from "../../../../services" +import { promiseAll } from "@medusajs/utils" import { Type } from "class-transformer" +import { EntityManager } from "typeorm" +import { LineItemService } from "../../../../services" +import { IsType } from "../../../../utils/validators/is-type" import { joinInventoryItems } from "./utils/join-inventory-items" import { joinLineItems } from "./utils/join-line-items" -import { promiseAll } from "@medusajs/utils" /** * @oas [get] /admin/reservations @@ -288,7 +288,15 @@ export class AdminGetReservationsParams extends extendedFindParamsMixin({ created_at?: DateComparisonOperator /** - * String filters tp apply on the reservations' `description` field. + * Date filters to apply on the reservations' `updated_at` field. + */ + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + updated_at?: DateComparisonOperator + + /** + * String filters to apply on the reservations' `description` field. */ @IsOptional() @IsType([StringComparisonOperator, String])