feat(dashboard,js-sdk,medusa): add ability to add outbound items to claim (#8502)
what: - user can add/remove/update outbound items to a claim - adds API to query variants Note: There are several paths that are not implemented yet correctly, but have some code lying around. Those will be tackled in the followup PRs https://github.com/user-attachments/assets/cadb3f7a-982f-44c7-8d7e-9f4f26949f4f RESOLVES CC-330 RESOLVES CC-296
This commit is contained in:
@@ -11,10 +11,12 @@ export * from "./fulfillment-providers"
|
||||
export * from "./fulfillment-sets"
|
||||
export * from "./inventory"
|
||||
export * from "./invites"
|
||||
export * from "./notification"
|
||||
export * from "./orders"
|
||||
export * from "./payments"
|
||||
export * from "./price-lists"
|
||||
export * from "./product-types"
|
||||
export * from "./product-variants"
|
||||
export * from "./products"
|
||||
export * from "./promotions"
|
||||
export * from "./regions"
|
||||
@@ -29,4 +31,3 @@ export * from "./tax-rates"
|
||||
export * from "./tax-regions"
|
||||
export * from "./users"
|
||||
export * from "./workflow-executions"
|
||||
export * from "./notification"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const PRODUCT_VARIANT_QUERY_KEY = "product_variant" as const
|
||||
export const productVariantQueryKeys = queryKeysFactory(
|
||||
PRODUCT_VARIANT_QUERY_KEY
|
||||
)
|
||||
|
||||
export const useVariants = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, Error, any, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => sdk.admin.productVariant.list(query),
|
||||
queryKey: productVariantQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
@@ -845,6 +845,7 @@
|
||||
"returns": {
|
||||
"create": "Create Return",
|
||||
"inbound": "Inbound",
|
||||
"outbound": "Outbound",
|
||||
"sendNotification": "Send notification",
|
||||
"sendNotificationHint": "Notify customer about return.",
|
||||
"returnTotal": "Return total",
|
||||
@@ -885,6 +886,8 @@
|
||||
"claims": {
|
||||
"create": "Create Claim",
|
||||
"outbound": "Outbound",
|
||||
"outboundShipping": "Outbound shipping",
|
||||
"outboundShippingHint": "Choose which method you want to use.",
|
||||
"refundAmount": "Estimated difference",
|
||||
"activeChangeError": "There is an active order change on this order. Please finish or discard the previous change.",
|
||||
"actions": {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
||||
import { useState } from "react"
|
||||
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useVariants } from "../../../../../hooks/api"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useClaimOutboundItemTableColumns } from "./use-claim-outbound-item-table-columns"
|
||||
import { useClaimOutboundItemTableFilters } from "./use-claim-outbound-item-table-filters"
|
||||
import { useClaimOutboundItemTableQuery } from "./use-claim-outbound-item-table-query"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PREFIX = "rit"
|
||||
|
||||
type AddClaimOutboundItemsTableProps = {
|
||||
onSelectionChange: (ids: string[]) => void
|
||||
selectedItems: string[]
|
||||
currencyCode: string
|
||||
}
|
||||
|
||||
export const AddClaimOutboundItemsTable = ({
|
||||
onSelectionChange,
|
||||
selectedItems,
|
||||
currencyCode,
|
||||
}: AddClaimOutboundItemsTableProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
selectedItems.reduce((acc, id) => {
|
||||
acc[id] = true
|
||||
return acc
|
||||
}, {} as RowSelectionState)
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
setRowSelection(newState)
|
||||
onSelectionChange(Object.keys(newState))
|
||||
}
|
||||
|
||||
const { searchParams, raw } = useClaimOutboundItemTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const { variants = [], count } = useVariants({
|
||||
...searchParams,
|
||||
fields: "*inventory_items.inventory.location_levels,+inventory_quantity",
|
||||
})
|
||||
|
||||
const columns = useClaimOutboundItemTableColumns(currencyCode)
|
||||
const filters = useClaimOutboundItemTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: variants,
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: (_row) => {
|
||||
// TODO: Check inventory here. Check if other validations needs to be made
|
||||
return true
|
||||
},
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
filters={filters}
|
||||
pagination
|
||||
layout="fill"
|
||||
search
|
||||
orderBy={["product_id", "title", "sku"]}
|
||||
prefix={PREFIX}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./add-claim-outbound-items-table"
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
ProductCell,
|
||||
ProductHeader,
|
||||
} from "../../../../../components/table/table-cells/product/product-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<any>()
|
||||
|
||||
export const useClaimOutboundItemTableColumns = (currencyCode: string) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isSelectable = row.getCanSelect()
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
disabled={!isSelectable}
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "product",
|
||||
header: () => <ProductHeader />,
|
||||
cell: ({ row }) => {
|
||||
return <ProductCell product={row.original.product} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sku", {
|
||||
header: t("fields.sku"),
|
||||
cell: ({ getValue }) => {
|
||||
return getValue() || "-"
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
}),
|
||||
],
|
||||
[t, currencyCode]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useClaimOutboundItemTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
key: "created_at",
|
||||
label: t("fields.createdAt"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
key: "updated_at",
|
||||
label: t("fields.updatedAt"),
|
||||
type: "date",
|
||||
},
|
||||
]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
DateComparisonOperator,
|
||||
NumericalComparisonOperator,
|
||||
} from "@medusajs/types"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export type ReturnItemTableQuery = {
|
||||
q?: string
|
||||
offset: number
|
||||
order?: string
|
||||
created_at?: DateComparisonOperator
|
||||
updated_at?: DateComparisonOperator
|
||||
returnable_quantity?: NumericalComparisonOperator | number
|
||||
refundable_amount?: NumericalComparisonOperator | number
|
||||
}
|
||||
|
||||
export const useClaimOutboundItemTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
["q", "offset", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, created_at, updated_at, ...rest } = raw
|
||||
|
||||
const searchParams = {
|
||||
...rest,
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
}
|
||||
|
||||
return { searchParams, raw }
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { AddClaimItemsTable } from "../add-claim-items-table"
|
||||
import { ClaimInboundItem } from "./claim-inbound-item.tsx"
|
||||
import { ClaimCreateSchema, ReturnCreateSchemaType } from "./schema"
|
||||
import { ClaimCreateSchema, CreateClaimSchemaType } from "./schema"
|
||||
|
||||
import {
|
||||
useAddClaimInboundItems,
|
||||
@@ -47,6 +47,8 @@ import {
|
||||
} from "../../../../../hooks/api/claims"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { currencies } from "../../../../../lib/data/currencies"
|
||||
import { ClaimOutboundSection } from "./claim-outbound-section"
|
||||
import { ItemPlaceholder } from "./item-placeholder"
|
||||
|
||||
type ReturnCreateFormProps = {
|
||||
order: AdminOrder
|
||||
@@ -56,7 +58,6 @@ type ReturnCreateFormProps = {
|
||||
|
||||
let itemsToAdd: string[] = []
|
||||
let itemsToRemove: string[] = []
|
||||
|
||||
let IS_CANCELING = false
|
||||
|
||||
export const ClaimCreateForm = ({
|
||||
@@ -92,11 +93,13 @@ export const ClaimCreateForm = ({
|
||||
/**
|
||||
* MUTATIONS
|
||||
*/
|
||||
// TODO: implement confirm claim request
|
||||
const { mutateAsync: confirmClaimRequest, isPending: isConfirming } = {} // useConfirmClaimRequest(claim.id, order.id)
|
||||
|
||||
const { mutateAsync: cancelClaimRequest, isPending: isCanceling } =
|
||||
useCancelClaimRequest(claim.id, order.id)
|
||||
|
||||
// TODO: implement update claim request
|
||||
const { mutateAsync: updateClaimRequest, isPending: isUpdating } = {} // useUpdateClaim(claim.id, order.id)
|
||||
|
||||
const {
|
||||
@@ -145,40 +148,50 @@ export const ClaimCreateForm = ({
|
||||
[preview.items]
|
||||
)
|
||||
|
||||
const inboundPreviewItems = previewItems.filter(
|
||||
(item) => !!item.actions?.find((a) => a.action === "RETURN_ITEM")
|
||||
)
|
||||
|
||||
const outboundPreviewItems = previewItems.filter(
|
||||
(item) => !!item.actions?.find((a) => a.action === "ITEM_ADD")
|
||||
)
|
||||
|
||||
const itemsMap = useMemo(
|
||||
() => new Map(order?.items?.map((i) => [i.id, i])),
|
||||
[order.items]
|
||||
)
|
||||
|
||||
const previewItemsMap = useMemo(
|
||||
() => new Map(previewItems.map((i) => [i.id, i])),
|
||||
[previewItems]
|
||||
)
|
||||
|
||||
/**
|
||||
* FORM
|
||||
*/
|
||||
|
||||
const form = useForm<ReturnCreateSchemaType>({
|
||||
const form = useForm<CreateClaimSchemaType>({
|
||||
defaultValues: () => {
|
||||
const method = preview.shipping_methods.find(
|
||||
(s) => !!s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
)
|
||||
|
||||
return Promise.resolve({
|
||||
inbound_items: previewItems.map((i) => {
|
||||
const returnAction = i.actions?.find(
|
||||
inbound_items: inboundPreviewItems.map((i) => {
|
||||
const inboundAction = i.actions?.find(
|
||||
(a) => a.action === "RETURN_ITEM"
|
||||
)
|
||||
|
||||
return {
|
||||
item_id: i.id,
|
||||
variant_id: i.variant_id,
|
||||
quantity: i.detail.return_requested_quantity,
|
||||
note: returnAction?.internal_note,
|
||||
reason_id: returnAction?.details?.reason_id as string | undefined,
|
||||
note: inboundAction?.internal_note,
|
||||
reason_id: inboundAction?.details?.reason_id as string | undefined,
|
||||
}
|
||||
}),
|
||||
outbound_items: outboundPreviewItems.map((i) => ({
|
||||
item_id: i.id,
|
||||
variant_id: i.variant_id,
|
||||
quantity: i.detail.quantity,
|
||||
})),
|
||||
inbound_option_id: method ? method.shipping_option_id : "",
|
||||
// TODO: pick up shipping method for outbound when available
|
||||
outbound_option_id: method ? method.shipping_option_id : "",
|
||||
location_id: "",
|
||||
send_notification: false,
|
||||
})
|
||||
@@ -187,7 +200,7 @@ export const ClaimCreateForm = ({
|
||||
})
|
||||
|
||||
const {
|
||||
fields: items,
|
||||
fields: inboundItems,
|
||||
append,
|
||||
remove,
|
||||
update,
|
||||
@@ -196,22 +209,27 @@ export const ClaimCreateForm = ({
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const previewItemsMap = useMemo(
|
||||
() => new Map(previewItems.map((i) => [i.id, i])),
|
||||
[previewItems, inboundItems]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const existingItemsMap: Record<string, boolean> = {}
|
||||
|
||||
previewItems.forEach((i) => {
|
||||
const ind = items.findIndex((field) => field.item_id === i.id)
|
||||
inboundPreviewItems.forEach((i) => {
|
||||
const ind = inboundItems.findIndex((field) => field.item_id === i.id)
|
||||
|
||||
existingItemsMap[i.id] = true
|
||||
|
||||
if (ind > -1) {
|
||||
if (items[ind].quantity !== i.detail.return_requested_quantity) {
|
||||
if (inboundItems[ind].quantity !== i.detail.return_requested_quantity) {
|
||||
const returnItemAction = i.actions?.find(
|
||||
(a) => a.action === "RETURN_ITEM"
|
||||
)
|
||||
|
||||
update(ind, {
|
||||
...items[ind],
|
||||
...inboundItems[ind],
|
||||
quantity: i.detail.return_requested_quantity,
|
||||
note: returnItemAction?.internal_note,
|
||||
reason_id: returnItemAction?.details?.reason_id as string,
|
||||
@@ -222,7 +240,7 @@ export const ClaimCreateForm = ({
|
||||
}
|
||||
})
|
||||
|
||||
items.forEach((i, ind) => {
|
||||
inboundItems.forEach((i, ind) => {
|
||||
if (!(i.item_id in existingItemsMap)) {
|
||||
remove(ind)
|
||||
}
|
||||
@@ -239,7 +257,7 @@ export const ClaimCreateForm = ({
|
||||
}
|
||||
}, [preview.shipping_methods])
|
||||
|
||||
const showPlaceholder = !items.length
|
||||
const showInboundItemsPlaceholder = !inboundItems.length
|
||||
const locationId = form.watch("location_id")
|
||||
const shippingOptionId = form.watch("inbound_option_id")
|
||||
|
||||
@@ -285,7 +303,7 @@ export const ClaimCreateForm = ({
|
||||
}
|
||||
}
|
||||
|
||||
setIsOpen("items", false)
|
||||
setIsOpen("inbound-items", false)
|
||||
}
|
||||
|
||||
const onLocationChange = async (selectedLocationId?: string | null) => {
|
||||
@@ -321,7 +339,7 @@ export const ClaimCreateForm = ({
|
||||
return false
|
||||
}
|
||||
|
||||
const allItemsHaveLocation = items
|
||||
const allItemsHaveLocation = inboundItems
|
||||
.map((_i) => {
|
||||
const item = itemsMap.get(_i.item_id)
|
||||
if (!item?.variant_id || !item?.variant) {
|
||||
@@ -339,45 +357,30 @@ export const ClaimCreateForm = ({
|
||||
.every(Boolean)
|
||||
|
||||
return !allItemsHaveLocation
|
||||
}, [items, inventoryMap, locationId])
|
||||
}, [inboundItems, inventoryMap, locationId])
|
||||
|
||||
useEffect(() => {
|
||||
const getInventoryMap = async () => {
|
||||
const ret: Record<string, InventoryLevelDTO[]> = {}
|
||||
|
||||
if (!items.length) {
|
||||
if (!inboundItems.length) {
|
||||
return ret
|
||||
}
|
||||
|
||||
;(
|
||||
await Promise.all(
|
||||
items.map(async (_i) => {
|
||||
const item = itemsMap.get(_i.item_id)!
|
||||
const variantIds = inboundItems
|
||||
.map((item) => item?.variant_id)
|
||||
.filter(Boolean)
|
||||
|
||||
if (!item.variant_id || !item.variant?.product) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return await sdk.admin.product.retrieveVariant(
|
||||
item.variant.product.id,
|
||||
item.variant_id,
|
||||
{ fields: "*inventory,*inventory.location_levels" }
|
||||
)
|
||||
})
|
||||
const variants = (
|
||||
await sdk.admin.productVariant.list(
|
||||
{ id: variantIds },
|
||||
{ fields: "*inventory,*inventory.location_levels" }
|
||||
)
|
||||
)
|
||||
.filter((it) => !!it?.variant)
|
||||
.forEach((item) => {
|
||||
).variants
|
||||
|
||||
const { variant } = item
|
||||
const levels = variant.inventory[0]?.location_levels
|
||||
|
||||
if (!levels) {
|
||||
return
|
||||
}
|
||||
|
||||
ret[variant.id] = levels
|
||||
})
|
||||
variants.forEach((variant) => {
|
||||
ret[variant.id] = variant.inventory[0]?.location_levels || []
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -385,7 +388,7 @@ export const ClaimCreateForm = ({
|
||||
getInventoryMap().then((map) => {
|
||||
setInventoryMap(map)
|
||||
})
|
||||
}, [items])
|
||||
}, [inboundItems])
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -429,7 +432,8 @@ export const ClaimCreateForm = ({
|
||||
<Heading level="h1">{t("orders.claims.create")}</Heading>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<Heading level="h2">{t("orders.returns.inbound")}</Heading>
|
||||
<StackedFocusModal id="items">
|
||||
|
||||
<StackedFocusModal id="inbound-items">
|
||||
<StackedFocusModal.Trigger asChild>
|
||||
<a className="focus-visible:shadow-borders-focus transition-fg txt-compact-small-plus cursor-pointer text-blue-500 outline-none hover:text-blue-400">
|
||||
{t("actions.addItems")}
|
||||
@@ -440,10 +444,10 @@ export const ClaimCreateForm = ({
|
||||
|
||||
<AddClaimItemsTable
|
||||
items={order.items!}
|
||||
selectedItems={items.map((i) => i.item_id)}
|
||||
selectedItems={inboundItems.map((i) => i.item_id)}
|
||||
currencyCode={order.currency_code}
|
||||
onSelectionChange={(finalSelection) => {
|
||||
const alreadySelected = items.map((i) => i.item_id)
|
||||
const alreadySelected = inboundItems.map((i) => i.item_id)
|
||||
|
||||
itemsToAdd = finalSelection.filter(
|
||||
(selection) => !alreadySelected.includes(selection)
|
||||
@@ -482,20 +486,11 @@ export const ClaimCreateForm = ({
|
||||
</StackedFocusModal.Content>
|
||||
</StackedFocusModal>
|
||||
</div>
|
||||
|
||||
{showPlaceholder && (
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(-45deg, rgb(212, 212, 216, 0.15), rgb(212, 212, 216,.15) 10px, transparent 10px, transparent 20px)",
|
||||
}}
|
||||
className="bg-ui-bg-field mt-4 block h-[56px] w-full rounded-lg border border-dashed"
|
||||
/>
|
||||
)}
|
||||
|
||||
{items.map(
|
||||
{showInboundItemsPlaceholder && <ItemPlaceholder />}
|
||||
{inboundItems.map(
|
||||
(item, index) =>
|
||||
previewItemsMap.get(item.item_id) && (
|
||||
previewItemsMap.get(item.item_id) &&
|
||||
itemsMap.get(item.item_id)! && (
|
||||
<ClaimInboundItem
|
||||
key={item.id}
|
||||
item={itemsMap.get(item.item_id)!}
|
||||
@@ -535,8 +530,7 @@ export const ClaimCreateForm = ({
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{!showPlaceholder && (
|
||||
{!showInboundItemsPlaceholder && (
|
||||
<div className="mt-8 flex flex-col gap-y-4">
|
||||
{/*LOCATION*/}
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
@@ -628,7 +622,6 @@ export const ClaimCreateForm = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLevelsWarning && (
|
||||
<Alert variant="warning" dismissible className="mt-4 p-5">
|
||||
<div className="text-ui-fg-subtle txt-small pb-2 font-medium leading-[20px]">
|
||||
@@ -640,6 +633,13 @@ export const ClaimCreateForm = ({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<ClaimOutboundSection
|
||||
form={form}
|
||||
preview={preview}
|
||||
order={order}
|
||||
claim={claim}
|
||||
/>
|
||||
|
||||
{/*TOTALS SECTION*/}
|
||||
<div className="mt-8 border-y border-dotted py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
@@ -664,7 +664,9 @@ export const ClaimCreateForm = ({
|
||||
onClick={() => setIsShippingPriceEdit(true)}
|
||||
variant="transparent"
|
||||
className="text-ui-fg-muted"
|
||||
disabled={showPlaceholder || !shippingOptionId}
|
||||
disabled={
|
||||
showInboundItemsPlaceholder || !shippingOptionId
|
||||
}
|
||||
>
|
||||
<PencilSquare />
|
||||
</IconButton>
|
||||
@@ -712,7 +714,7 @@ export const ClaimCreateForm = ({
|
||||
value && setCustomShippingAmount(parseInt(value))
|
||||
}
|
||||
value={customShippingAmount}
|
||||
disabled={showPlaceholder}
|
||||
disabled={showInboundItemsPlaceholder}
|
||||
/>
|
||||
) : (
|
||||
getStylizedAmount(shippingTotal, order.currency_code)
|
||||
@@ -732,7 +734,6 @@ export const ClaimCreateForm = ({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*SEND NOTIFICATION*/}
|
||||
<div className="bg-ui-bg-field mt-8 rounded-lg border py-2 pl-2 pr-4">
|
||||
<Form.Field
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { XCircle } from "@medusajs/icons"
|
||||
import { AdminOrderLineItem, HttpTypes } from "@medusajs/types"
|
||||
import { Input, Text } from "@medusajs/ui"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
||||
import { CreateClaimSchemaType } from "./schema"
|
||||
|
||||
type ClaimOutboundItemProps = {
|
||||
previewItem: AdminOrderLineItem
|
||||
currencyCode: string
|
||||
index: number
|
||||
|
||||
onRemove: () => void
|
||||
// TODO: create a payload type for outbound updates
|
||||
onUpdate: (payload: HttpTypes.AdminUpdateReturnItems) => void
|
||||
|
||||
form: UseFormReturn<CreateClaimSchemaType>
|
||||
}
|
||||
|
||||
function ClaimOutboundItem({
|
||||
previewItem,
|
||||
currencyCode,
|
||||
form,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
index,
|
||||
}: ClaimOutboundItemProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl ">
|
||||
<div className="flex flex-col items-center gap-x-2 gap-y-2 border-b p-3 text-sm md:flex-row">
|
||||
<div className="flex flex-1 items-center gap-x-3">
|
||||
<Thumbnail src={previewItem.thumbnail} />
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
<Text className="txt-small" as="span" weight="plus">
|
||||
{previewItem.title}{" "}
|
||||
</Text>
|
||||
|
||||
{previewItem.variant_sku && (
|
||||
<span>({previewItem.variant_sku})</span>
|
||||
)}
|
||||
</div>
|
||||
<Text as="div" className="text-ui-fg-subtle txt-small">
|
||||
{previewItem.product_title}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 justify-between">
|
||||
<div className="flex flex-grow items-center gap-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`outbound_items.${index}.quantity`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
className="bg-ui-bg-base txt-small w-[67px] rounded-lg"
|
||||
min={1}
|
||||
// TODO: add max available inventory quantity if present
|
||||
// max={previewItem.quantity}
|
||||
type="number"
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value
|
||||
const payload = val === "" ? null : Number(val)
|
||||
|
||||
field.onChange(payload)
|
||||
|
||||
if (payload) {
|
||||
onUpdate({ quantity: payload })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Text className="txt-small text-ui-fg-subtle">
|
||||
{t("fields.qty")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle txt-small mr-2 flex flex-shrink-0">
|
||||
<MoneyAmountCell
|
||||
currencyCode={currencyCode}
|
||||
amount={previewItem.total}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.remove"),
|
||||
onClick: onRemove,
|
||||
icon: <XCircle />,
|
||||
},
|
||||
].filter(Boolean),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ClaimOutboundItem }
|
||||
@@ -0,0 +1,446 @@
|
||||
import {
|
||||
AdminClaim,
|
||||
AdminOrder,
|
||||
AdminOrderPreview,
|
||||
InventoryLevelDTO,
|
||||
} from "@medusajs/types"
|
||||
import { Alert, Button, Heading, Text, toast } from "@medusajs/ui"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useFieldArray, UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
StackedFocusModal,
|
||||
useStackedModal,
|
||||
} from "../../../../../components/modals"
|
||||
import {
|
||||
useAddClaimOutboundItems,
|
||||
useAddClaimOutboundShipping,
|
||||
useDeleteClaimOutboundShipping,
|
||||
useRemoveClaimOutboundItem,
|
||||
useUpdateClaimOutboundItems,
|
||||
} from "../../../../../hooks/api/claims"
|
||||
import { useShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { AddClaimOutboundItemsTable } from "../add-claim-outbound-items-table"
|
||||
import { ClaimOutboundItem } from "./claim-outbound-item"
|
||||
import { ItemPlaceholder } from "./item-placeholder"
|
||||
import { CreateClaimSchemaType } from "./schema"
|
||||
|
||||
type ClaimOutboundSectionProps = {
|
||||
order: AdminOrder
|
||||
claim: AdminClaim
|
||||
preview: AdminOrderPreview
|
||||
form: UseFormReturn<CreateClaimSchemaType>
|
||||
}
|
||||
|
||||
let itemsToAdd: string[] = []
|
||||
let itemsToRemove: string[] = []
|
||||
|
||||
export const ClaimOutboundSection = ({
|
||||
order,
|
||||
preview,
|
||||
claim,
|
||||
form,
|
||||
}: ClaimOutboundSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { setIsOpen } = useStackedModal()
|
||||
const [inventoryMap, setInventoryMap] = useState<
|
||||
Record<string, InventoryLevelDTO[]>
|
||||
>({})
|
||||
|
||||
/**
|
||||
* HOOKS
|
||||
*/
|
||||
const { stock_locations = [] } = useStockLocations({ limit: 999 })
|
||||
const { shipping_options = [] } = useShippingOptions({
|
||||
limit: 999,
|
||||
fields: "*prices,+service_zone.fulfillment_set.location.id",
|
||||
/**
|
||||
* TODO: this should accept filter for location_id
|
||||
*/
|
||||
})
|
||||
|
||||
const { mutateAsync: addOutboundShipping } = useAddClaimOutboundShipping(
|
||||
claim.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: deleteOutboundShipping } =
|
||||
useDeleteClaimOutboundShipping(claim.id, order.id)
|
||||
|
||||
const { mutateAsync: addOutboundItem } = useAddClaimOutboundItems(
|
||||
claim.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: updateOutboundItem } = useUpdateClaimOutboundItems(
|
||||
claim.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: removeOutboundItem } = useRemoveClaimOutboundItem(
|
||||
claim.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
/**
|
||||
* Only consider items that belong to this claim and is an outbound item
|
||||
*/
|
||||
const previewOutboundItems = useMemo(
|
||||
() =>
|
||||
preview?.items?.filter(
|
||||
(i) =>
|
||||
!!i.actions?.find(
|
||||
(a) => a.claim_id === claim.id && a.action === "ITEM_ADD"
|
||||
)
|
||||
),
|
||||
[preview.items]
|
||||
)
|
||||
|
||||
const variantItemMap = useMemo(
|
||||
() => new Map(order?.items?.map((i) => [i.variant_id, i])),
|
||||
[order.items]
|
||||
)
|
||||
|
||||
const {
|
||||
fields: outboundItems,
|
||||
append,
|
||||
remove,
|
||||
update,
|
||||
} = useFieldArray({
|
||||
name: "outbound_items",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const variantOutboundMap = useMemo(
|
||||
() => new Map(previewOutboundItems.map((i) => [i.variant_id, i])),
|
||||
[previewOutboundItems, outboundItems]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const existingItemsMap: Record<string, boolean> = {}
|
||||
|
||||
previewOutboundItems.forEach((i) => {
|
||||
const ind = outboundItems.findIndex((field) => field.item_id === i.id)
|
||||
|
||||
existingItemsMap[i.id] = true
|
||||
|
||||
if (ind > -1) {
|
||||
if (outboundItems[ind].quantity !== i.detail.quantity) {
|
||||
update(ind, {
|
||||
...outboundItems[ind],
|
||||
quantity: i.detail.quantity,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
append({
|
||||
item_id: i.id,
|
||||
quantity: i.detail.quantity,
|
||||
variant_id: i.variant_id,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
outboundItems.forEach((i, ind) => {
|
||||
if (!(i.item_id in existingItemsMap)) {
|
||||
remove(ind)
|
||||
}
|
||||
})
|
||||
}, [previewOutboundItems])
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Pick the shipping methods from actions where return_id is null for outbound
|
||||
const method = preview.shipping_methods.find(
|
||||
(s) => !!s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
)
|
||||
|
||||
if (method) {
|
||||
form.setValue("outbound_option_id", method.shipping_option_id)
|
||||
}
|
||||
}, [preview.shipping_methods])
|
||||
|
||||
const locationId = form.watch("location_id")
|
||||
const showOutboundItemsPlaceholder = !outboundItems.length
|
||||
|
||||
const onItemsSelected = async () => {
|
||||
itemsToAdd.length &&
|
||||
(await addOutboundItem(
|
||||
{
|
||||
items: itemsToAdd.map((variantId) => ({
|
||||
variant_id: variantId,
|
||||
quantity: 1,
|
||||
})),
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
))
|
||||
|
||||
for (const itemToRemove of itemsToRemove) {
|
||||
const actionId = previewOutboundItems
|
||||
.find((i) => i.variant_id === itemToRemove)
|
||||
?.actions?.find((a) => a.action === "ITEM_ADD")?.id
|
||||
|
||||
if (actionId) {
|
||||
await removeOutboundItem(actionId, {
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setIsOpen("outbound-items", false)
|
||||
}
|
||||
|
||||
// TODO: implement outbound shipping
|
||||
const { mutateAsync: updateClaimRequest, isPending: isUpdating } = {} // useUpdateClaim(claim.id, order.id)
|
||||
const onLocationChange = async (selectedLocationId?: string | null) => {
|
||||
await updateClaimRequest({ location_id: selectedLocationId })
|
||||
}
|
||||
|
||||
const onShippingOptionChange = async (selectedOptionId: string) => {
|
||||
const promises = preview.shipping_methods
|
||||
.map((s) => s.actions?.find((a) => a.action === "SHIPPING_ADD")?.id)
|
||||
.filter(Boolean)
|
||||
.map(deleteOutboundShipping)
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
await addOutboundShipping(
|
||||
{ shipping_option_id: selectedOptionId },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const showLevelsWarning = useMemo(() => {
|
||||
if (!locationId) {
|
||||
return false
|
||||
}
|
||||
|
||||
const allItemsHaveLocation = outboundItems
|
||||
.map((i) => {
|
||||
const item = variantItemMap.get(i.variant_id)
|
||||
if (!item?.variant_id || !item?.variant) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!item.variant.manage_inventory) {
|
||||
return true
|
||||
}
|
||||
|
||||
return inventoryMap[item.variant_id]?.find(
|
||||
(l) => l.location_id === locationId
|
||||
)
|
||||
})
|
||||
.every(Boolean)
|
||||
|
||||
return !allItemsHaveLocation
|
||||
}, [outboundItems, inventoryMap, locationId])
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Ensure inventory validation occurs correctly
|
||||
const getInventoryMap = async () => {
|
||||
const ret: Record<string, InventoryLevelDTO[]> = {}
|
||||
|
||||
if (!outboundItems.length) {
|
||||
return ret
|
||||
}
|
||||
|
||||
const variantIds = outboundItems
|
||||
.map((item) => item?.variant_id)
|
||||
.filter(Boolean)
|
||||
const variants = (
|
||||
await sdk.admin.productVariant.list(
|
||||
{ id: variantIds },
|
||||
{ fields: "*inventory,*inventory.location_levels" }
|
||||
)
|
||||
).variants
|
||||
|
||||
variants.forEach((variant) => {
|
||||
ret[variant.id] = variant.inventory[0]?.location_levels || []
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
getInventoryMap().then((map) => {
|
||||
setInventoryMap(map)
|
||||
})
|
||||
}, [outboundItems])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<Heading level="h2">{t("orders.returns.outbound")}</Heading>
|
||||
|
||||
<StackedFocusModal id="outbound-items">
|
||||
<StackedFocusModal.Trigger asChild>
|
||||
<a className="focus-visible:shadow-borders-focus transition-fg txt-compact-small-plus cursor-pointer text-blue-500 outline-none hover:text-blue-400">
|
||||
{t("actions.addItems")}
|
||||
</a>
|
||||
</StackedFocusModal.Trigger>
|
||||
<StackedFocusModal.Content>
|
||||
<StackedFocusModal.Header />
|
||||
|
||||
<AddClaimOutboundItemsTable
|
||||
selectedItems={outboundItems.map((i) => i.variant_id)}
|
||||
currencyCode={order.currency_code}
|
||||
onSelectionChange={(finalSelection) => {
|
||||
const alreadySelected = outboundItems.map((i) => i.variant_id)
|
||||
|
||||
itemsToAdd = finalSelection.filter(
|
||||
(selection) => !alreadySelected.includes(selection)
|
||||
)
|
||||
itemsToRemove = alreadySelected.filter(
|
||||
(selection) => !finalSelection.includes(selection)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<StackedFocusModal.Footer>
|
||||
<div className="flex w-full items-center justify-end gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button type="button" variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
key="submit-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
role="button"
|
||||
onClick={async () => await onItemsSelected()}
|
||||
>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StackedFocusModal.Footer>
|
||||
</StackedFocusModal.Content>
|
||||
</StackedFocusModal>
|
||||
</div>
|
||||
|
||||
{showOutboundItemsPlaceholder && <ItemPlaceholder />}
|
||||
|
||||
{outboundItems.map(
|
||||
(item, index) =>
|
||||
variantOutboundMap.get(item.variant_id) && (
|
||||
<ClaimOutboundItem
|
||||
key={item.id}
|
||||
previewItem={variantOutboundMap.get(item.variant_id)!}
|
||||
currencyCode={order.currency_code}
|
||||
form={form}
|
||||
onRemove={() => {
|
||||
const actionId = previewOutboundItems
|
||||
.find((i) => i.id === item.item_id)
|
||||
?.actions?.find((a) => a.action === "ITEM_ADD")?.id
|
||||
|
||||
if (actionId) {
|
||||
removeOutboundItem(actionId, {
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
onUpdate={(payload) => {
|
||||
const actionId = previewOutboundItems
|
||||
.find((i) => i.id === item.item_id)
|
||||
?.actions?.find((a) => a.action === "ITEM_ADD")?.id
|
||||
|
||||
if (actionId) {
|
||||
updateOutboundItem(
|
||||
{ ...payload, actionId },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}}
|
||||
index={index}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{!showOutboundItemsPlaceholder && (
|
||||
<div className="mt-8 flex flex-col gap-y-4">
|
||||
{/*OUTBOUND SHIPPING*/}
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<div>
|
||||
<Form.Label>{t("orders.claims.outboundShipping")}</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.claims.outboundShippingHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="outbound_option_id"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
value={value ?? undefined}
|
||||
onChange={(val) => {
|
||||
onChange(val)
|
||||
val && onShippingOptionChange(val)
|
||||
}}
|
||||
{...field}
|
||||
options={(shipping_options ?? [])
|
||||
.filter(
|
||||
(so) =>
|
||||
(locationId
|
||||
? so.service_zone.fulfillment_set!.location
|
||||
.id === locationId
|
||||
: true) &&
|
||||
!!so.rules.find(
|
||||
(r) =>
|
||||
r.attribute === "is_return" &&
|
||||
r.value === "true"
|
||||
)
|
||||
)
|
||||
.map((so) => ({
|
||||
label: so.name,
|
||||
value: so.id,
|
||||
}))}
|
||||
disabled={!locationId}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLevelsWarning && (
|
||||
<Alert variant="warning" dismissible className="mt-4 p-5">
|
||||
<div className="text-ui-fg-subtle txt-small pb-2 font-medium leading-[20px]">
|
||||
{t("orders.returns.noInventoryLevel")}
|
||||
</div>
|
||||
<Text className="text-ui-fg-subtle txt-small leading-normal">
|
||||
{t("orders.returns.noInventoryLevelDesc")}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const ItemPlaceholder = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(-45deg, rgb(212, 212, 216, 0.15), rgb(212, 212, 216,.15) 10px, transparent 10px, transparent 20px)",
|
||||
}}
|
||||
className="bg-ui-bg-field mt-4 block h-[56px] w-full rounded-lg border border-dashed"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -9,16 +9,15 @@ export const ClaimCreateSchema = z.object({
|
||||
note: z.string().nullish(),
|
||||
})
|
||||
),
|
||||
// TODO: Bring back when introducing outbound items
|
||||
// outbound_items: z.array(
|
||||
// z.object({
|
||||
// item_id: z.string(), // TODO: variant id?
|
||||
// quantity: z.number(),
|
||||
// })
|
||||
// ),
|
||||
outbound_items: z.array(
|
||||
z.object({
|
||||
item_id: z.string(), // TODO: variant id?
|
||||
quantity: z.number(),
|
||||
})
|
||||
),
|
||||
location_id: z.string().optional(),
|
||||
inbound_option_id: z.string().nullish(),
|
||||
send_notification: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type ReturnCreateSchemaType = z.infer<typeof ClaimCreateSchema>
|
||||
export type CreateClaimSchemaType = z.infer<typeof ClaimCreateSchema>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Client } from "../client"
|
||||
import { Claim } from "./claim"
|
||||
import { Currency } from "./currency"
|
||||
import { Customer } from "./customer"
|
||||
import { Fulfillment } from "./fulfillment"
|
||||
@@ -16,6 +17,7 @@ import { ProductCategory } from "./product-category"
|
||||
import { ProductCollection } from "./product-collection"
|
||||
import { ProductTag } from "./product-tag"
|
||||
import { ProductType } from "./product-type"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
import { Region } from "./region"
|
||||
import { Return } from "./return"
|
||||
import { ReturnReason } from "./return-reason"
|
||||
@@ -28,7 +30,6 @@ import { TaxRate } from "./tax-rate"
|
||||
import { TaxRegion } from "./tax-region"
|
||||
import { Upload } from "./upload"
|
||||
import { User } from "./user"
|
||||
import { Claim } from "./claim"
|
||||
|
||||
export class Admin {
|
||||
public invite: Invite
|
||||
@@ -61,6 +62,7 @@ export class Admin {
|
||||
public user: User
|
||||
public currency: Currency
|
||||
public payment: Payment
|
||||
public productVariant: ProductVariant
|
||||
|
||||
constructor(client: Client) {
|
||||
this.invite = new Invite(client)
|
||||
@@ -93,5 +95,6 @@ export class Admin {
|
||||
this.user = new User(client)
|
||||
this.currency = new Currency(client)
|
||||
this.payment = new Payment(client)
|
||||
this.productVariant = new ProductVariant(client)
|
||||
}
|
||||
}
|
||||
|
||||
23
packages/core/js-sdk/src/admin/product-variant.ts
Normal file
23
packages/core/js-sdk/src/admin/product-variant.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Client } from "../client"
|
||||
import { ClientHeaders } from "../types"
|
||||
|
||||
export class ProductVariant {
|
||||
private client: Client
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async list(
|
||||
queryParams?: HttpTypes.AdminProductVariantParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminProductVariantListResponse>(
|
||||
`/admin/product-variants`,
|
||||
{
|
||||
headers,
|
||||
query: queryParams,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MiddlewareRoute } from "@medusajs/framework"
|
||||
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||
import * as QueryConfig from "./query-config"
|
||||
import { AdminGetProductVariantsParams } from "./validators"
|
||||
|
||||
export const adminProductVariantRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/product-variants",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetProductVariantsParams,
|
||||
QueryConfig.listProductVariantQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
export const defaultAdminProductVariantFields = [
|
||||
"id",
|
||||
"title",
|
||||
"sku",
|
||||
"barcode",
|
||||
"ean",
|
||||
"upc",
|
||||
"allow_backorder",
|
||||
"manage_inventory",
|
||||
"hs_code",
|
||||
"origin_country",
|
||||
"mid_code",
|
||||
"material",
|
||||
"weight",
|
||||
"length",
|
||||
"height",
|
||||
"width",
|
||||
"metadata",
|
||||
"variant_rank",
|
||||
"product_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"*product",
|
||||
"*prices",
|
||||
"*options",
|
||||
"prices.price_rules.value",
|
||||
"prices.price_rules.attribute",
|
||||
]
|
||||
|
||||
export const retrieveProductVariantQueryConfig = {
|
||||
defaults: defaultAdminProductVariantFields,
|
||||
isList: false,
|
||||
}
|
||||
|
||||
export const listProductVariantQueryConfig = {
|
||||
...retrieveProductVariantQueryConfig,
|
||||
defaultLimit: 50,
|
||||
isList: true,
|
||||
}
|
||||
39
packages/medusa/src/api/admin/product-variants/route.ts
Normal file
39
packages/medusa/src/api/admin/product-variants/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { wrapVariantsWithInventoryQuantity } from "../../utils/middlewares"
|
||||
import { refetchEntities } from "../../utils/refetch-entity"
|
||||
import { remapKeysForVariant, remapVariantResponse } from "../products/helpers"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse<HttpTypes.AdminProductVariantListResponse>
|
||||
) => {
|
||||
const withInventoryQuantity = req.remoteQueryConfig.fields.some((field) =>
|
||||
field.includes("inventory_quantity")
|
||||
)
|
||||
|
||||
if (withInventoryQuantity) {
|
||||
req.remoteQueryConfig.fields = req.remoteQueryConfig.fields.filter(
|
||||
(field) => !field.includes("inventory_quantity")
|
||||
)
|
||||
}
|
||||
|
||||
const { rows: variants, metadata } = await refetchEntities(
|
||||
"variant",
|
||||
{ ...req.filterableFields },
|
||||
req.scope,
|
||||
remapKeysForVariant(req.remoteQueryConfig.fields ?? []),
|
||||
req.remoteQueryConfig.pagination
|
||||
)
|
||||
|
||||
if (withInventoryQuantity) {
|
||||
await wrapVariantsWithInventoryQuantity(req, variants || [])
|
||||
}
|
||||
|
||||
res.json({
|
||||
variants: variants.map(remapVariantResponse),
|
||||
count: metadata.count,
|
||||
offset: metadata.skip,
|
||||
limit: metadata.take,
|
||||
})
|
||||
}
|
||||
22
packages/medusa/src/api/admin/product-variants/validators.ts
Normal file
22
packages/medusa/src/api/admin/product-variants/validators.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod"
|
||||
import { createFindParams, createOperatorMap } from "../../utils/validators"
|
||||
|
||||
export type AdminGetProductVariantsParamsType = z.infer<
|
||||
typeof AdminGetProductVariantsParams
|
||||
>
|
||||
export const AdminGetProductVariantsParams = createFindParams({
|
||||
offset: 0,
|
||||
limit: 50,
|
||||
}).merge(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
manage_inventory: z.boolean().optional(),
|
||||
allow_backorder: z.boolean().optional(),
|
||||
created_at: createOperatorMap().optional(),
|
||||
updated_at: createOperatorMap().optional(),
|
||||
deleted_at: createOperatorMap().optional(),
|
||||
$and: z.lazy(() => AdminGetProductVariantsParams.array()).optional(),
|
||||
$or: z.lazy(() => AdminGetProductVariantsParams.array()).optional(),
|
||||
})
|
||||
)
|
||||
@@ -21,6 +21,7 @@ import { adminPricePreferencesRoutesMiddlewares } from "./admin/price-preference
|
||||
import { adminProductCategoryRoutesMiddlewares } from "./admin/product-categories/middlewares"
|
||||
import { adminProductTagRoutesMiddlewares } from "./admin/product-tags/middlewares"
|
||||
import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlewares"
|
||||
import { adminProductVariantRoutesMiddlewares } from "./admin/product-variants/middlewares"
|
||||
import { adminProductRoutesMiddlewares } from "./admin/products/middlewares"
|
||||
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
|
||||
import { adminRefundReasonsRoutesMiddlewares } from "./admin/refund-reasons/middlewares"
|
||||
@@ -110,4 +111,5 @@ export default defineMiddlewares([
|
||||
...adminClaimRoutesMiddlewares,
|
||||
...adminRefundReasonsRoutesMiddlewares,
|
||||
...adminExchangeRoutesMiddlewares,
|
||||
...adminProductVariantRoutesMiddlewares,
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user