feat(dashboard, js-sdk, types): receive return e2e (#8305)

**What**
- receive return flow
- order summary section adjustments
- fix received item in summary
- activity event

---

CLOSES TRI-79 CC-256
This commit is contained in:
Frane Polić
2024-08-01 20:29:11 +02:00
committed by GitHub
parent 7ae1d80380
commit 2280d31396
21 changed files with 1536 additions and 167 deletions

View File

@@ -0,0 +1,96 @@
import { DropdownMenu, clx } from "@medusajs/ui"
import { PropsWithChildren, ReactNode } from "react"
import { Link } from "react-router-dom"
type Action = {
icon: ReactNode
label: string
disabled?: boolean
} & (
| {
to: string
onClick?: never
}
| {
onClick: () => void
to?: never
}
)
type ActionGroup = {
actions: Action[]
}
type ActionMenuProps = {
groups: ActionGroup[]
}
export const ButtonMenu = ({
groups,
children,
}: PropsWithChildren<ActionMenuProps>) => {
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>{children}</DropdownMenu.Trigger>
<DropdownMenu.Content>
{groups.map((group, index) => {
if (!group.actions.length) {
return null
}
const isLast = index === groups.length - 1
return (
<DropdownMenu.Group key={index}>
{group.actions.map((action, index) => {
if (action.onClick) {
return (
<DropdownMenu.Item
disabled={action.disabled}
key={index}
onClick={(e) => {
e.stopPropagation()
action.onClick()
}}
className={clx(
"[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
{
"[&_svg]:text-ui-fg-disabled": action.disabled,
}
)}
>
{action.icon}
<span>{action.label}</span>
</DropdownMenu.Item>
)
}
return (
<div key={index}>
<DropdownMenu.Item
className={clx(
"[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
{
"[&_svg]:text-ui-fg-disabled": action.disabled,
}
)}
asChild
disabled={action.disabled}
>
<Link to={action.to} onClick={(e) => e.stopPropagation()}>
{action.icon}
<span>{action.label}</span>
</Link>
</DropdownMenu.Item>
</div>
)
})}
{!isLast && <DropdownMenu.Separator />}
</DropdownMenu.Group>
)
})}
</DropdownMenu.Content>
</DropdownMenu>
)
}

View File

@@ -0,0 +1 @@
export * from "./action-menu"

View File

@@ -8,12 +8,14 @@ import { Form } from "../../common/form"
type RouteModalFormProps<TFieldValues extends FieldValues> = PropsWithChildren<{
form: UseFormReturn<TFieldValues>
blockSearch?: boolean
onClose?: (isSubmitSuccessful: boolean) => void
}>
export const RouteModalForm = <TFieldValues extends FieldValues = any>({
form,
blockSearch = false,
children,
onClose,
}: RouteModalFormProps<TFieldValues>) => {
const { t } = useTranslation()
@@ -25,6 +27,7 @@ export const RouteModalForm = <TFieldValues extends FieldValues = any>({
const { isSubmitSuccessful } = nextLocation.state || {}
if (isSubmitSuccessful) {
onClose?.(true)
return false
}
@@ -32,10 +35,22 @@ export const RouteModalForm = <TFieldValues extends FieldValues = any>({
const isSearchChanged = currentLocation.search !== nextLocation.search
if (blockSearch) {
return isDirty && (isPathChanged || isSearchChanged)
const ret = isDirty && (isPathChanged || isSearchChanged)
if (!ret) {
onClose?.(isSubmitSuccessful)
}
return ret
}
return isDirty && isPathChanged
const ret = isDirty && isPathChanged
if (!ret) {
onClose?.(isSubmitSuccessful)
}
return ret
})
const handleCancel = () => {
@@ -44,6 +59,7 @@ export const RouteModalForm = <TFieldValues extends FieldValues = any>({
const handleContinue = () => {
blocker?.proceed?.()
onClose?.(false)
}
return (

View File

@@ -53,6 +53,10 @@ export const useReturns = (
return { ...data, ...rest }
}
/**
* REQUEST RETURN
*/
export const useInitiateReturn = (
orderId: string,
options?: UseMutationOptions<
@@ -81,6 +85,74 @@ export const useInitiateReturn = (
})
}
export const useConfirmReturnRequest = (
id: string,
orderId: string,
options?: UseMutationOptions<
HttpTypes.AdminReturnResponse,
Error,
HttpTypes.AdminConfirmReturnRequest
>
) => {
return useMutation({
mutationFn: (payload: HttpTypes.AdminConfirmReturnRequest) =>
sdk.admin.return.confirmRequest(id, payload),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.details(),
})
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
})
queryClient.invalidateQueries({
queryKey: returnsQueryKeys.details(),
})
queryClient.invalidateQueries({
queryKey: returnsQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useCancelReturnRequest = (
id: string,
orderId: string,
options?: UseMutationOptions<HttpTypes.AdminReturnResponse, Error>
) => {
return useMutation({
mutationFn: () => sdk.admin.return.cancelRequest(id),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.details(),
})
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
refetchType: "all",
})
queryClient.invalidateQueries({
queryKey: returnsQueryKeys.details(),
})
queryClient.invalidateQueries({
queryKey: returnsQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useAddReturnItem = (
id: string,
orderId: string,
@@ -235,18 +307,185 @@ export const useDeleteReturnShipping = (
})
}
export const useConfirmReturnRequest = (
/**
* RECEIVE RETURN
*/
export const useInitiateReceiveReturn = (
id: string,
orderId: string,
options?: UseMutationOptions<
HttpTypes.AdminReturnResponse,
Error,
HttpTypes.AdminConfirmReturnRequest
HttpTypes.AdminInitiateReceiveReturn
>
) => {
return useMutation({
mutationFn: (payload: HttpTypes.AdminConfirmReturnRequest) =>
sdk.admin.return.confirmRequest(id, payload),
mutationFn: (payload: HttpTypes.AdminInitiateReceiveReturn) =>
sdk.admin.return.initiateReceive(id, payload),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.details(),
})
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useAddReceiveItems = (
id: string,
orderId: string,
options?: UseMutationOptions<
HttpTypes.AdminReturnResponse,
Error,
HttpTypes.AdminReceiveItems
>
) => {
return useMutation({
mutationFn: (payload: HttpTypes.AdminReceiveItems) =>
sdk.admin.return.receiveItems(id, payload),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useUpdateReceiveItem = (
id: string,
orderId: string,
options?: UseMutationOptions<
HttpTypes.AdminReturnResponse,
Error,
HttpTypes.AdminUpdateReceiveItems & { actionId: string }
>
) => {
return useMutation({
mutationFn: ({
actionId,
...payload
}: HttpTypes.AdminUpdateReceiveItems & { actionId: string }) => {
return sdk.admin.return.updateReceiveItem(id, actionId, payload)
},
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useRemoveReceiveItems = (
id: string,
orderId: string,
options?: UseMutationOptions<HttpTypes.AdminReturnResponse, Error, string>
) => {
return useMutation({
mutationFn: (actionId: string) => {
return sdk.admin.return.removeReceiveItem(id, actionId)
},
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useAddDismissItems = (
id: string,
orderId: string,
options?: UseMutationOptions<
HttpTypes.AdminReturnResponse,
Error,
HttpTypes.AdminDismissItems
>
) => {
return useMutation({
mutationFn: (payload: HttpTypes.AdminDismissItems) =>
sdk.admin.return.dismissItems(id, payload),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useUpdateDismissItem = (
id: string,
orderId: string,
options?: UseMutationOptions<
HttpTypes.AdminReturnResponse,
Error,
HttpTypes.AdminUpdateDismissItems & { actionId: string }
>
) => {
return useMutation({
mutationFn: ({
actionId,
...payload
}: HttpTypes.AdminUpdateReceiveItems & { actionId: string }) => {
return sdk.admin.return.updateDismissItem(id, actionId, payload)
},
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useRemoveDismissItem = (
id: string,
orderId: string,
options?: UseMutationOptions<HttpTypes.AdminReturnResponse, Error, string>
) => {
return useMutation({
mutationFn: (actionId: string) => {
return sdk.admin.return.removeDismissItem(id, actionId)
},
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useConfirmReturnReceive = (
id: string,
orderId: string,
options?: UseMutationOptions<
HttpTypes.AdminReturnResponse,
Error,
HttpTypes.AdminConfirmReceiveReturn
>
) => {
return useMutation({
mutationFn: (payload: HttpTypes.AdminConfirmReceiveReturn) =>
sdk.admin.return.confirmReceive(id, payload),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.details(),
@@ -271,13 +510,13 @@ export const useConfirmReturnRequest = (
})
}
export const useCancelReturnRequest = (
export const useCancelReceiveReturn = (
id: string,
orderId: string,
options?: UseMutationOptions<HttpTypes.AdminReturnResponse, Error>
) => {
return useMutation({
mutationFn: () => sdk.admin.return.cancelRequest(id),
mutationFn: () => sdk.admin.return.cancelReceive(id),
onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.details(),
@@ -288,6 +527,7 @@ export const useCancelReturnRequest = (
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId),
refetchType: "all", // For some reason RQ will refetch this but will return stale record from the cache
})
queryClient.invalidateQueries({

View File

@@ -849,6 +849,7 @@
"sendNotificationHint": "Notify customer about return.",
"returnTotal": "Return total",
"refundAmount": "Refund amount",
"outstandingAmount": "Outstanding amount",
"reason": "Reason",
"reasonHint": "Choose why the customer want to return items.",
"note": "Note",
@@ -861,7 +862,25 @@
"inboundShippingHint": "Choose which method you want to use.",
"returnableQuantityLabel": "Returnable quantity",
"refundableAmountLabel": "Refundable amount",
"returnRequestedInfo": "{{requestedItemsCount}}x item return requested"
"returnRequestedInfo": "{{requestedItemsCount}}x item return requested",
"returnReceivedInfo": "{{requestedItemsCount}}x item return received",
"activeChangeError": "There is an active order change in progress on this order. Please finish or discard the change first.",
"receive": {
"action": "Receive items",
"receive": "Return {{label}}",
"restockAll": "Restock all items",
"itemsLabel": "Items received",
"title": "Receive items for #{{returnId}}",
"sendNotificationHint": "Notify customer about received return.",
"inventoryWarning": "Please note that we will automatically adjust the inventory levels based on your input above.",
"writeOffInputLabel": "How many of the items are damaged?",
"toast": {
"success": "Return received successfully.",
"errorLargeValue": "Quantity is greater than requested item quantity.",
"errorNegativeValue": "Quantity cannot be a negative value.",
"errorLargeDamagedValue": "Damaged items quantity + non damaged received quantity exceeds total item quantity on the return. Please decrease quantity of non damaged items."
}
}
},
"reservations": {
"allocatedLabel": "Allocated",
@@ -981,7 +1000,8 @@
"items_other": "{{count}} items"
},
"return": {
"created": "Return created",
"created": "Return #{{returnId}} created",
"received": "Return #{{returnId}} received",
"items_one": "{{count}} item returned",
"items_other": "{{count}} items returned"
},

View File

@@ -2,14 +2,16 @@ import { AdminOrderLineItem } from "@medusajs/types"
export function getReturnableQuantity(item: AdminOrderLineItem): number {
const {
// TODO: this should be `fulfilled_quantity`? now there is check on the BD that we can't return more quantity than we shipped but some items don't require shipping
shipped_quantity,
return_received_quantity,
return_dismissed_quantity, // TODO: check this
return_dismissed_quantity,
return_requested_quantity,
} = item.detail
return (
shipped_quantity - (return_received_quantity + return_requested_quantity)
shipped_quantity -
(return_received_quantity +
return_requested_quantity +
return_dismissed_quantity)
)
}

View File

@@ -223,6 +223,11 @@ export const RouteMap: RouteObject[] = [
lazy: () =>
import("../../routes/orders/order-create-fulfillment"),
},
{
path: "returns/:return_id/receive",
lazy: () =>
import("../../routes/orders/order-receive-return"),
},
{
path: "allocate-items",
lazy: () =>

View File

@@ -52,8 +52,6 @@ type ReturnCreateFormProps = {
let selectedItems: string[] = []
let IS_CANCELING = false
export const ReturnCreateForm = ({
order,
preview,
@@ -62,6 +60,27 @@ export const ReturnCreateForm = ({
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const itemsMap = useMemo(
() => new Map(order.items.map((i) => [i.id, i])),
[order.items]
)
/**
* Only consider items that belong to this return.
*/
const previewItems = useMemo(
() =>
preview.items.filter(
(i) => !!i.actions?.find((a) => a.return_id === activeReturn.id)
),
[preview.items]
)
const previewItemsMap = useMemo(
() => new Map(previewItems.map((i) => [i.id, i])),
[previewItems]
)
/**
* STATE
*/
@@ -142,16 +161,14 @@ export const ReturnCreateForm = ({
)
return Promise.resolve({
items: preview.items
.filter((i) => !!i.detail.return_requested_quantity)
.map((i) => ({
item_id: i.id,
quantity: i.detail.return_requested_quantity,
note: i.actions?.find((a) => a.action === "RETURN_ITEM")
?.internal_note,
reason_id: i.actions?.find((a) => a.action === "RETURN_ITEM")
?.details?.reason_id,
})),
items: previewItems.map((i) => ({
item_id: i.id,
quantity: i.detail.return_requested_quantity,
note: i.actions?.find((a) => a.action === "RETURN_ITEM")
?.internal_note,
reason_id: i.actions?.find((a) => a.action === "RETURN_ITEM")?.details
?.reason_id,
})),
option_id: method ? method.shipping_option_id : "",
location_id: "",
send_notification: false,
@@ -160,16 +177,6 @@ export const ReturnCreateForm = ({
resolver: zodResolver(ReturnCreateSchema),
})
const itemsMap = useMemo(
() => new Map(order.items.map((i) => [i.id, i])),
[order.items]
)
const previewItemsMap = useMemo(
() => new Map(preview.items.map((i) => [i.id, i])),
[preview.items]
)
const {
fields: items,
append,
@@ -183,7 +190,7 @@ export const ReturnCreateForm = ({
useEffect(() => {
const existingItemsMap = {}
preview.items.forEach((i) => {
previewItems.forEach((i) => {
const ind = items.findIndex((field) => field.item_id === i.id)
/**
@@ -218,7 +225,7 @@ export const ReturnCreateForm = ({
remove(ind)
}
})
}, [preview.items])
}, [previewItems])
useEffect(() => {
const method = preview.shipping_methods.find(
@@ -348,19 +355,6 @@ export const ReturnCreateForm = ({
})
}, [items])
useEffect(() => {
/**
* Unmount hook
*/
return () => {
if (IS_CANCELING) {
cancelReturnRequest()
// TODO: add this on ESC press
IS_CANCELING = false
}
}
}, [])
const returnTotal = preview.return_requested_total
const shippingTotal = useMemo(() => {
@@ -374,7 +368,14 @@ export const ReturnCreateForm = ({
const refundAmount = returnTotal - shippingTotal
return (
<RouteFocusModal.Form form={form}>
<RouteFocusModal.Form
form={form}
onClose={(isSubmitSuccessful) => {
if (!isSubmitSuccessful) {
cancelReturnRequest()
}
}}
>
<form onSubmit={handleSubmit} className="flex h-full flex-col">
<RouteFocusModal.Header />
@@ -443,7 +444,7 @@ export const ReturnCreateForm = ({
currencyCode={order.currency_code}
form={form}
onRemove={() => {
const actionId = preview.items
const actionId = previewItems
.find((i) => i.id === item.item_id)
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
@@ -452,7 +453,7 @@ export const ReturnCreateForm = ({
}
}}
onUpdate={(payload) => {
const actionId = preview.items
const actionId = previewItems
.find((i) => i.id === item.item_id)
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
@@ -691,12 +692,7 @@ export const ReturnCreateForm = ({
<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"
onClick={() => (IS_CANCELING = true)}
variant="secondary"
size="small"
>
<Button type="button" variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>

View File

@@ -1,5 +1,8 @@
import { useParams } from "react-router-dom"
import { useNavigate, useParams } from "react-router-dom"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "@medusajs/ui"
import { RouteFocusModal } from "../../../components/modals"
import { ReturnCreateForm } from "./components/return-create-form"
@@ -13,11 +16,14 @@ let IS_REQUEST_RUNNING = false
export const ReturnCreate = () => {
const { id } = useParams()
const navigate = useNavigate()
const { t } = useTranslation()
const { order } = useOrder(id!, {
fields: DEFAULT_FIELDS,
})
const { order: preview } = useOrderPreview(id!)
const { order: preview } = useOrderPreview(id!, undefined, {})
const [activeReturnId, setActiveReturnId] = useState()
@@ -29,28 +35,36 @@ export const ReturnCreate = () => {
useEffect(() => {
async function run() {
if (IS_REQUEST_RUNNING || !order || !preview) {
if (IS_REQUEST_RUNNING || !preview) {
return
}
/**
* Active return already exists
*/
if (preview.order_change?.change_type === "return") {
setActiveReturnId(preview.order_change.return.id)
if (preview.order_change) {
if (preview.order_change.change_type === "return_request") {
setActiveReturnId(preview.order_change.return_id)
} else {
navigate(`/orders/${order.id}`, { replace: true })
toast.error(t("orders.returns.activeChangeError"))
}
return
}
IS_REQUEST_RUNNING = true
const orderReturn = await initiateReturn({ order_id: order.id })
setActiveReturnId(orderReturn.id)
IS_REQUEST_RUNNING = false
try {
const orderReturn = await initiateReturn({ order_id: order.id })
setActiveReturnId(orderReturn.id)
} catch (e) {
navigate(`/orders/${order.id}`, { replace: true })
toast.error(e.message)
} finally {
IS_REQUEST_RUNNING = false
}
}
run()
}, [order, preview])
}, [preview])
return (
<RouteFocusModal>

View File

@@ -81,7 +81,10 @@ type Activity = {
const useActivityItems = (order: AdminOrder) => {
const { t } = useTranslation()
const { returns = [] } = useReturns({ order_id: order.id, fields: "*items" })
const { returns = [] } = useReturns({
order_id: order.id,
fields: "+received_at,*items",
})
const notes = []
const isLoading = false
@@ -169,11 +172,24 @@ const useActivityItems = (order: AdminOrder) => {
}
for (const ret of returns) {
// Always display created action
items.push({
title: t("orders.activity.events.return.created"),
title: t("orders.activity.events.return.created", {
returnId: ret.id.slice(-7),
}),
timestamp: ret.created_at,
children: <ReturnCreatedBody orderReturn={ret} />,
children: <ReturnBody orderReturn={ret} />,
})
if (ret.status === "received" || ret.status === "partially_received") {
items.push({
title: t("orders.activity.events.return.received", {
returnId: ret.id.slice(-7),
}),
timestamp: ret.received_at,
children: <ReturnBody orderReturn={ret} />,
})
}
}
// for (const note of notes || []) {
@@ -242,13 +258,19 @@ const OrderActivityItem = ({
<Text size="small" leading="compact" weight="plus">
{title}
</Text>
<Tooltip
content={getFullDate({ date: timestamp, includeTime: true })}
>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{getRelativeDate(timestamp)}
</Text>
</Tooltip>
{timestamp && (
<Tooltip
content={getFullDate({ date: timestamp, includeTime: true })}
>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
{getRelativeDate(timestamp)}
</Text>
</Tooltip>
)}
</div>
<div>{children}</div>
</div>
@@ -392,7 +414,7 @@ const FulfillmentCreatedBody = ({
)
}
const ReturnCreatedBody = ({ orderReturn }: { orderReturn: AdminReturn }) => {
const ReturnBody = ({ orderReturn }: { orderReturn: AdminReturn }) => {
const { t } = useTranslation()
const numberOfItems = orderReturn.items.reduce((acc, item) => {

View File

@@ -4,15 +4,21 @@ import { useMemo } from "react"
import {
AdminOrder,
AdminReturn,
OrderLineItemDTO,
ReservationItemDTO,
} from "@medusajs/types"
import { ArrowDownRightMini, ArrowUturnLeft } from "@medusajs/icons"
import {
ArrowDownRightMini,
ArrowLongRight,
ArrowUturnLeft,
} from "@medusajs/icons"
import {
Button,
Container,
Copy,
Heading,
IconButton,
StatusBadge,
Text,
} from "@medusajs/ui"
@@ -26,6 +32,7 @@ import {
import { useReservationItems } from "../../../../../hooks/api/reservations"
import { useReturns } from "../../../../../hooks/api/returns"
import { useDate } from "../../../../../hooks/use-date"
import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx"
type OrderSummarySectionProps = {
order: AdminOrder
@@ -39,6 +46,14 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
line_item_id: order.items.map((i) => i.id),
})
const { returns = [] } = useReturns({
status: "requested",
order_id: order.id,
fields: "+received_at",
})
const showReturns = !!returns.length
/**
* Show Allocation button only if there are unfulfilled items that don't have reservations
*/
@@ -71,20 +86,41 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
<Container className="divide-y divide-dashed p-0">
<Header order={order} />
<ItemBreakdown order={order} />
<ReturnBreakdown order={order} />
<CostBreakdown order={order} />
<Total order={order} />
{showAllocateButton && (
<div className="bg-ui-bg-subtle flex items-center justify-end rounded-b-xl px-4 py-4">
<Button
onClick={() => navigate(`./allocate-items`)}
variant="secondary"
>
{t("orders.allocateItems.action")}
</Button>
</div>
)}
{showAllocateButton ||
(showReturns && (
<div className="bg-ui-bg-subtle flex items-center justify-end rounded-b-xl px-4 py-4">
{showReturns && (
<ButtonMenu
groups={[
{
actions: returns.map((r) => ({
label: t("orders.returns.receive.receive", {
label: `#${r.id.slice(-7)}`,
}),
icon: <ArrowLongRight />,
to: `/orders/${order.id}/returns/${r.id}/receive`,
})),
},
]}
>
<Button variant="secondary" size="small">
{t("orders.returns.receive.action")}
</Button>
</ButtonMenu>
)}
{showAllocateButton && (
<Button
onClick={() => navigate(`./allocate-items`)}
variant="secondary"
>
{t("orders.allocateItems.action")}
</Button>
)}
</div>
))}
</Container>
)
}
@@ -126,73 +162,80 @@ const Item = ({
item,
currencyCode,
reservation,
returns,
}: {
item: OrderLineItemDTO
currencyCode: string
reservation?: ReservationItemDTO | null
returns: AdminReturn[]
}) => {
const { t } = useTranslation()
const isInventoryManaged = item.variant.manage_inventory
return (
<div
key={item.id}
className="text-ui-fg-subtle grid grid-cols-2 items-center gap-x-4 px-6 py-4"
>
<div className="flex items-start gap-x-4">
<Thumbnail src={item.thumbnail} />
<div>
<Text
size="small"
leading="compact"
weight="plus"
className="text-ui-fg-base"
>
{item.title}
</Text>
{item.variant_sku && (
<div className="flex items-center gap-x-1">
<Text size="small">{item.variant_sku}</Text>
<Copy content={item.variant_sku} className="text-ui-fg-muted" />
</div>
)}
<Text size="small">
{item.variant?.options.map((o) => o.value).join(" · ")}
</Text>
</div>
</div>
<div className="grid grid-cols-3 items-center gap-x-4">
<div className="flex items-center justify-end gap-x-4">
<Text size="small">
{getLocaleAmount(item.unit_price, currencyCode)}
</Text>
</div>
<div className="flex items-center gap-x-2">
<div className="w-fit min-w-[27px]">
<>
<div
key={item.id}
className="text-ui-fg-subtle grid grid-cols-2 items-center gap-x-4 px-6 py-4"
>
<div className="flex items-start gap-x-4">
<Thumbnail src={item.thumbnail} />
<div>
<Text
size="small"
leading="compact"
weight="plus"
className="text-ui-fg-base"
>
{item.title}
</Text>
{item.variant_sku && (
<div className="flex items-center gap-x-1">
<Text size="small">{item.variant_sku}</Text>
<Copy content={item.variant_sku} className="text-ui-fg-muted" />
</div>
)}
<Text size="small">
<span className="tabular-nums">{item.quantity}</span>x
{item.variant?.options.map((o) => o.value).join(" · ")}
</Text>
</div>
<div className="overflow-visible">
{isInventoryManaged && (
<StatusBadge
color={reservation ? "green" : "orange"}
className="text-nowrap"
>
{reservation
? t("orders.reservations.allocatedLabel")
: t("orders.reservations.notAllocatedLabel")}
</StatusBadge>
)}
</div>
<div className="grid grid-cols-3 items-center gap-x-4">
<div className="flex items-center justify-end gap-x-4">
<Text size="small">
{getLocaleAmount(item.unit_price, currencyCode)}
</Text>
</div>
<div className="flex items-center gap-x-2">
<div className="w-fit min-w-[27px]">
<Text size="small">
<span className="tabular-nums">{item.quantity}</span>x
</Text>
</div>
<div className="overflow-visible">
{isInventoryManaged && (
<StatusBadge
color={reservation ? "green" : "orange"}
className="text-nowrap"
>
{reservation
? t("orders.reservations.allocatedLabel")
: t("orders.reservations.notAllocatedLabel")}
</StatusBadge>
)}
</div>
</div>
<div className="flex items-center justify-end">
<Text size="small" className="pt-[1px]">
{getLocaleAmount(item.subtotal || 0, currencyCode)}
</Text>
</div>
</div>
<div className="flex items-center justify-end">
<Text size="small" className="pt-[1px]">
{getLocaleAmount(item.subtotal || 0, currencyCode)}
</Text>
</div>
</div>
</div>
{returns.map((r) => (
<ReturnBreakdown key={r.id} orderReturn={r} itemId={item.id} />
))}
</>
)
}
@@ -201,6 +244,33 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => {
line_item_id: order.items.map((i) => i.id),
})
const { returns } = useReturns({
order_id: order.id,
fields: "*items",
})
const itemsReturnsMap = useMemo(() => {
if (!returns) {
return {}
}
const ret = {}
order.items?.forEach((i) => {
returns.forEach((r) => {
if (r.items.some((ri) => ri.item_id === i.id)) {
if (ret[i.id]) {
ret[i.id].push(r)
} else {
ret[i.id] = [r]
}
}
})
})
return ret
}, [returns])
return (
<div>
{order.items.map((item) => {
@@ -214,6 +284,7 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => {
item={item}
currencyCode={order.currency_code}
reservation={reservation}
returns={itemsReturnsMap[item.id] || []}
/>
)
})}
@@ -275,39 +346,56 @@ const CostBreakdown = ({ order }: { order: AdminOrder }) => {
)
}
const ReturnBreakdown = ({ order }: { order: AdminOrder }) => {
const ReturnBreakdown = ({
orderReturn,
itemId,
}: {
orderReturn: AdminReturn
itemId: string
}) => {
const { t } = useTranslation()
const { getRelativeDate } = useDate()
const { returns = [] } = useReturns({
order_id: order.id,
status: "requested",
fields: "*items",
})
if (!returns.length) {
if (
!["requested", "received", "partially_received"].includes(
orderReturn.status
)
) {
return null
}
return returns.map((activeReturn) => (
const isRequested = orderReturn.status === "requested"
return (
<div
key={activeReturn.id}
key={orderReturn.id}
className="text-ui-fg-subtle bg-ui-bg-subtle flex flex-row justify-between gap-y-2 px-6 py-4"
>
<div className="flex items-center gap-2">
<ArrowDownRightMini className="text-ui-fg-muted" />
<Text size="small" className="text-ui-fg-subtle">
{t("orders.returns.returnRequestedInfo", {
requestedItemsCount: activeReturn.items.length,
})}
{t(
`orders.returns.${
isRequested ? "returnRequestedInfo" : "returnReceivedInfo"
}`,
{
requestedItemsCount: orderReturn.items.find(
(ri) => ri.item_id === itemId
)[isRequested ? "quantity" : "received_quantity"],
}
)}
</Text>
</div>
<Text size="small" leading="compact" className="text-ui-fg-muted">
{getRelativeDate(activeReturn.created_at)}
</Text>
{isRequested && (
<Text size="small" leading="compact" className="text-ui-fg-muted">
{getRelativeDate(
isRequested ? orderReturn.created_at : orderReturn.received_at
)}
</Text>
)}
</div>
))
)
}
const Total = ({ order }: { order: AdminOrder }) => {

View File

@@ -0,0 +1,12 @@
import { z } from "zod"
export const ReceiveReturnSchema = z.object({
items: z.array(
z.object({
quantity: z.number().nullable(),
written_off_quantity: z.number().nullable(),
item_id: z.string(),
})
),
send_notification: z.boolean().optional(),
})

View File

@@ -0,0 +1 @@
export * from "./order-receive-return-form.tsx"

View File

@@ -0,0 +1,379 @@
import { useTranslation } from "react-i18next"
import { AdminOrder, AdminReturn } from "@medusajs/types"
import { Alert, Button, Input, Switch, Text, toast } from "@medusajs/ui"
import React, { useEffect, useMemo } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { ArrrowRight } from "@medusajs/icons"
import * as zod from "zod"
import { Thumbnail } from "../../../../../components/common/thumbnail"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { useStockLocation } from "../../../../../hooks/api"
import { ReceiveReturnSchema } from "./constants"
import { Form } from "../../../../../components/common/form"
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
import {
useAddReceiveItems,
useCancelReceiveReturn,
useConfirmReturnReceive,
useRemoveReceiveItems,
useUpdateReceiveItem,
} from "../../../../../hooks/api/returns"
import WrittenOffQuantity from "./written-off-quantity"
type OrderAllocateItemsFormProps = {
order: AdminOrder
preview: AdminOrder
orderReturn: AdminReturn
}
export function OrderReceiveReturnForm({
order,
preview,
orderReturn,
}: OrderAllocateItemsFormProps) {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
/**
* Items on the preview order that are part of the return we are receiving currently.
*/
const previewItems = useMemo(() => {
const idsMap = {}
orderReturn.items.forEach((i) => (idsMap[i.item_id] = true))
return preview.items.filter((i) => idsMap[i.id])
}, [preview.items, orderReturn])
const { mutateAsync: confirmReturnReceive } = useConfirmReturnReceive(
orderReturn.id,
order.id
)
const { mutateAsync: cancelReceiveReturn } = useCancelReceiveReturn(
orderReturn.id,
order.id
)
const { mutateAsync: addReceiveItems } = useAddReceiveItems(
orderReturn.id,
order.id
)
const { mutateAsync: updateReceiveItem } = useUpdateReceiveItem(
orderReturn.id,
order.id
)
const { mutateAsync: removeReceiveItem } = useRemoveReceiveItems(
orderReturn.id,
order.id
)
const { stock_location } = useStockLocation(
orderReturn.location_id,
undefined,
{
enabled: !!orderReturn.location_id,
}
)
const itemsMap = useMemo(() => {
const ret = {}
order.items.forEach((i) => (ret[i.id] = i))
return ret
}, [order.items])
const form = useForm<zod.infer<typeof ReceiveReturnSchema>>({
defaultValues: {
items: previewItems
?.sort((i1, i2) => i1.id.localeCompare(i2.id))
.map((i) => ({
item_id: i.id,
quantity: i.detail.return_received_quantity,
written_off_quantity: i.detail.written_off_quantity,
})),
send_notification: false,
},
resolver: zodResolver(ReceiveReturnSchema),
})
useEffect(() => {
previewItems
?.sort((i1, i2) => i1.id.localeCompare(i2.id))
.forEach((item, index) => {
form.setValue(
`items.${index}.quantity`,
item.detail.return_received_quantity,
{ shouldTouch: true, shouldDirty: true }
)
form.setValue(
`items.${index}.written_off_quantity`,
item.detail.written_off_quantity,
{ shouldTouch: true, shouldDirty: true }
)
})
}, [previewItems])
/**
* HANDLERS
*/
const handleSubmit = form.handleSubmit(async (data) => {
try {
await confirmReturnReceive({ no_notification: !data.send_notification })
handleSuccess(`/orders/${order.id}`)
toast.success(t("general.success"), {
description: t("orders.returns.receive.toast.success"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
})
const handleQuantityChange = async (
itemId: string,
value: number | null,
index: number
) => {
const item = previewItems?.find((i) => i.id === itemId)
const action = item?.actions?.find(
(a) => a.action === "RECEIVE_RETURN_ITEM"
)
if (typeof value === "number" && value < 0) {
form.setValue(
`items.${index}.quantity`,
item.detail.return_received_quantity,
{ shouldTouch: true, shouldDirty: true }
)
toast.error(t("orders.returns.receive.toast.errorNegativeValue"))
return
}
if (typeof value === "number" && value > item.quantity) {
// reset value in the form and notify the user to be aware that we didn't chang anything
form.setValue(
`items.${index}.quantity`,
item.detail.return_received_quantity,
{ shouldTouch: true, shouldDirty: true }
)
toast.error(t("orders.returns.receive.toast.errorLargeValue"))
return
}
try {
if (action) {
if (value === null || value === 0) {
await removeReceiveItem(action.id)
return
}
await updateReceiveItem({ actionId: action.id, quantity: value })
} else {
if (typeof value === "number" && value > 0 && value <= item.quantity) {
await addReceiveItems({ items: [{ id: item.id, quantity: value }] })
}
}
} catch (e) {
toast.error(e.message)
}
}
const onFormClose = async (isSubmitSuccessful: boolean) => {
try {
if (!isSubmitSuccessful) {
await cancelReceiveReturn()
}
} catch (e) {
toast.error(e.message)
}
}
return (
<RouteDrawer.Form form={form} onClose={onFormClose}>
<form
onSubmit={handleSubmit}
className="flex size-full flex-col overflow-hidden"
>
<RouteDrawer.Body className="flex size-full flex-col overflow-auto">
<div className="flex justify-between">
<div>
{stock_location && (
<div className="flex items-center gap-2">
<ArrrowRight className="text-ui-fg-subtle" />{" "}
<span className="text-ui-fg-base txt-small font-medium">
{stock_location.name}
</span>
</div>
)}
</div>
<span className="text-ui-fg-muted txt-small text-right">
{t("orders.returns.receive.itemsLabel")}
</span>
</div>
{previewItems.map((item, ind) => {
const originalItem = itemsMap[item.id]
return (
<div
key={item.id}
className="bg-ui-bg-subtle shadow-elevation-card-rest mt-2 rounded-xl"
>
<div className="flex flex-col items-center gap-x-2 gap-y-2 p-3 text-sm md:flex-row">
<div className="flex flex-1 items-center gap-x-3">
<Text size="small" className="text-ui-fg-subtle">
{item.quantity}x
</Text>
<Thumbnail src={item.thumbnail} />
<div className="flex flex-col">
<div>
<Text className="txt-small" as="span" weight="plus">
{item.title}{" "}
</Text>
{originalItem.variant.sku && (
<span>({originalItem.variant.sku})</span>
)}
</div>
<Text as="div" className="text-ui-fg-subtle txt-small">
{originalItem.variant.product.title}
</Text>
</div>
</div>
<div className="flex flex-1 flex-row items-center gap-2">
<WrittenOffQuantity
form={form}
item={item}
index={ind}
returnId={orderReturn.id}
orderId={order.id}
/>
<Form.Field
control={form.control}
name={`items.${ind}.quantity`}
render={({ field: { onChange, value, ...field } }) => {
return (
<Form.Item className="w-full">
<Form.Control>
<Input
min={0}
max={item.quantity}
type="number"
value={value}
className="bg-ui-bg-field-component text-right [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
onChange={(e) => {
const value =
e.target.value === ""
? null
: parseFloat(e.target.value)
onChange(value)
}}
{...field}
onBlur={() => {
field.onBlur()
handleQuantityChange(item.id, value, ind)
}}
/>
</Form.Control>
</Form.Item>
)
}}
/>
</div>
</div>
</div>
)
})}
{/*TOTALS*/}
<div className="my-6 border-b border-t border-dashed py-4">
<div className="mb-2 flex items-center justify-between">
<span className="txt-small text-ui-fg-subtle">
{t("fields.total")}
</span>
<span className="txt-small text-ui-fg-subtle">
{getStylizedAmount(preview.total, order.currency_code)}
</span>
</div>
<div className="mt-4 flex items-center justify-between border-t border-dotted pt-4">
<span className="txt-small font-medium">
{t("orders.returns.outstandingAmount")}
</span>
<span className="txt-small font-medium">
{getStylizedAmount(
preview.summary.difference_sum || 0,
order.currency_code
)}
</span>
</div>
</div>
<Alert className="rounded-xl" variant="warning">
{t("orders.returns.receive.inventoryWarning")}
</Alert>
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl p-3">
<Form.Field
control={form.control}
name="send_notification"
render={({ field: { onChange, value, ...field } }) => {
return (
<Form.Item>
<div className="flex items-center gap-3">
<Form.Control>
<Switch
className="mt-1 self-start"
checked={!!value}
onCheckedChange={onChange}
{...field}
/>
</Form.Control>
<div className="flex flex-col">
<Form.Label>
{t("orders.returns.sendNotification")}
</Form.Label>
<Form.Hint className="!mt-1">
{t("orders.returns.receive.sendNotificationHint")}
</Form.Hint>
</div>
</div>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</RouteDrawer.Body>
<RouteDrawer.Footer className="overflow-hidden">
<div className="flex items-center gap-x-2">
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={false}>
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</form>
</RouteDrawer.Form>
)
}

View File

@@ -0,0 +1,159 @@
import React, { useState } from "react"
import { HeartBroken } from "@medusajs/icons"
import { UseFormReturn } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { AdminOrderLineItem } from "@medusajs/types"
import { Button, Input, Popover, toast } from "@medusajs/ui"
import { ReceiveReturnSchema } from "./constants"
import { Form } from "../../../../../components/common/form"
import {
useAddDismissItems,
useRemoveDismissItem,
useUpdateDismissItem,
} from "../../../../../hooks/api/returns"
type WriteOffQuantityProps = {
returnId: string
orderId: string
index: number
item: AdminOrderLineItem
form: UseFormReturn<typeof ReceiveReturnSchema>
}
function WrittenOffQuantity({
form,
item,
index,
returnId,
orderId,
}: WriteOffQuantityProps) {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const { mutateAsync: addDismissedItems } = useAddDismissItems(
returnId,
orderId
)
const { mutateAsync: updateDismissedItems } = useUpdateDismissItem(
returnId,
orderId
)
const { mutateAsync: removeDismissedItems } = useRemoveDismissItem(
returnId,
orderId
)
const onDismissedQuantityChanged = async (value: number | null) => {
// TODO: if out of bounds prevent sending and notify user
const action = item.actions?.find(
(a) => a.action === "RECEIVE_DAMAGED_RETURN_ITEM"
)
if (typeof value === "number" && value < 0) {
form.setValue(
`items.${index}.written_off_quantity`,
item.detail.written_off_quantity,
{ shouldTouch: true, shouldDirty: true }
)
toast.error(t("orders.returns.receive.toast.errorNegativeValue"))
return
}
if (
typeof value === "number" &&
value > item.quantity - item.detail.return_received_quantity
) {
form.setValue(
`items.${index}.written_off_quantity`,
item.detail.written_off_quantity,
{ shouldTouch: true, shouldDirty: true }
)
toast.error(t("orders.returns.receive.toast.errorLargeDamagedValue"))
return
}
try {
if (value) {
if (!action) {
await addDismissedItems({
items: [{ id: item.id, quantity: value }],
})
} else {
await updateDismissedItems({ actionId: action.id, quantity: value })
}
} else {
if (action) {
// remove damaged item if value is unset and it was added before
await removeDismissedItems(action.id)
}
}
} catch (e) {
toast.error(e.message)
}
}
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<Popover.Trigger asChild>
<Button className="flex gap-2 px-2" variant="secondary" type="button">
<div>
<HeartBroken />
</div>
{!!item.detail.written_off_quantity && (
<span>{item.detail.written_off_quantity}</span>
)}
</Button>
</Popover.Trigger>
<Popover.Content align="center">
<div className="flex flex-col p-2">
<span className="txt-small text-ui-fg-subtle mb-2 font-medium">
{t("orders.returns.receive.writeOffInputLabel")}
</span>
<Form.Field
control={form.control}
name={`items.${index}.written_off_quantity`}
render={({ field: { onChange, value, ...field } }) => {
return (
<Form.Item className="w-full">
<Form.Control>
<Input
min={0}
max={item.quantity}
type="number"
value={value}
className="bg-ui-bg-field-component text-right [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
onChange={(e) => {
const value =
e.target.value === ""
? null
: parseFloat(e.target.value)
onChange(value)
}}
{...field}
onBlur={() => {
field.onBlur()
onDismissedQuantityChanged(value)
}}
/>
</Form.Control>
</Form.Item>
)
}}
/>
</div>
</Popover.Content>
</Popover>
)
}
export default WrittenOffQuantity

View File

@@ -0,0 +1 @@
export { OrderReceiveReturn as Component } from "./order-receive-return"

View File

@@ -0,0 +1,96 @@
import { useNavigate, useParams } from "react-router-dom"
import { useTranslation } from "react-i18next"
import { Heading, toast } from "@medusajs/ui"
import { useEffect } from "react"
import { useOrder, useOrderPreview } from "../../../hooks/api/orders"
import { RouteDrawer } from "../../../components/modals"
import { OrderReceiveReturnForm } from "./components/order-receive-return-form"
import {
useAddReceiveItems,
useInitiateReceiveReturn,
useReturn,
} from "../../../hooks/api/returns"
let IS_REQUEST_RUNNING = false
export function OrderReceiveReturn() {
const { id, return_id } = useParams()
const { t } = useTranslation()
const navigate = useNavigate()
/**
* HOOKS
*/
const { order } = useOrder(id!, { fields: "+currency_code,*items" })
const { order: preview } = useOrderPreview(id!)
const { return: orderReturn } = useReturn(return_id, {
fields: "*items.item,*items.item.variant,*items.item.variant.product",
}) // TODO: fix API needs to return 404 if return not exists and not an empty object
/**
* MUTATIONS
*/
const { mutateAsync: initiateReceiveReturn } = useInitiateReceiveReturn(
return_id,
id
)
const { mutateAsync: addReceiveItems } = useAddReceiveItems(return_id, id)
useEffect(() => {
;(async function () {
if (IS_REQUEST_RUNNING || !preview) {
return
}
if (preview.order_change) {
if (preview.order_change.change_type !== "return_receive") {
navigate(`/orders/${order.id}`, { replace: true })
toast.error(t("orders.returns.activeChangeError"))
}
return
}
IS_REQUEST_RUNNING = true
try {
const { return: _return } = await initiateReceiveReturn({})
await addReceiveItems({
items: _return.items.map((i) => ({
id: i.item_id,
quantity: i.quantity,
})),
})
} catch (e) {
toast.error(e.message)
} finally {
IS_REQUEST_RUNNING = false
}
})()
}, [preview])
const ready = order && orderReturn && preview
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>
{t("orders.returns.receive.title", {
returnId: return_id?.slice(-7),
})}
</Heading>
</RouteDrawer.Header>
{ready && (
<OrderReceiveReturnForm
order={order}
orderReturn={orderReturn}
preview={preview}
/>
)}
</RouteDrawer>
)
}

View File

@@ -162,6 +162,23 @@ export class Return {
)
}
async updateRequest(
id: string,
body: HttpTypes.AdminUpdateReturnRequest,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}`,
{
method: "POST",
headers,
body,
query,
}
)
}
async confirmRequest(
id: string,
body: HttpTypes.AdminConfirmReturnRequest,
@@ -179,14 +196,14 @@ export class Return {
)
}
async updateRequest(
async initiateReceive(
id: string,
body: HttpTypes.AdminUpdateReturnRequest,
body: HttpTypes.AdminInitiateReceiveReturn,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}`,
`/admin/returns/${id}/receive`,
{
method: "POST",
headers,
@@ -195,4 +212,138 @@ export class Return {
}
)
}
async receiveItems(
id: string,
body: HttpTypes.AdminReceiveItems,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}/receive-items`,
{
method: "POST",
headers,
body,
query,
}
)
}
async updateReceiveItem(
id: string,
actionId: string,
body: HttpTypes.AdminUpdateReceiveItems,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}/receive-items/${actionId}`,
{
method: "POST",
headers,
body,
query,
}
)
}
async removeReceiveItem(
id: string,
actionId: string,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}/receive-items/${actionId}`,
{
method: "DELETE",
headers,
query,
}
)
}
async dismissItems(
id: string,
body: HttpTypes.AdminDismissItems,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}/dismiss-items`,
{
method: "POST",
headers,
body,
query,
}
)
}
async updateDismissItem(
id: string,
actionId: string,
body: HttpTypes.AdminUpdateDismissItems,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}/dismiss-items/${actionId}`,
{
method: "POST",
headers,
body,
query,
}
)
}
async removeDismissItem(
id: string,
actionId: string,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}/dismiss-items/${actionId}`,
{
method: "DELETE",
headers,
query,
}
)
}
async confirmReceive(
id: string,
body: HttpTypes.AdminConfirmReceiveReturn,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}/receive/confirm`,
{
method: "POST",
headers,
body,
query,
}
)
}
async cancelReceive(
id: string,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
`/admin/returns/${id}/receive`,
{
method: "DELETE",
headers,
query,
}
)
}
}

View File

@@ -84,6 +84,38 @@ export interface AdminUpdateReturnRequest {
metadata?: Record<string, unknown> | null
}
export interface AdminConfirmReceiveReturn {
no_notification?: boolean
}
export interface AdminInitiateReceiveReturn {
internal_note?: string
description?: string
metadata?: Record<string, unknown>
}
export interface AdminReceiveItems {
items: { id: string; quantity: number; internal_note?: string }[]
}
export interface AdminDismissItems {
items: { id: string; quantity: number; internal_note?: string }[]
}
export interface AdminUpdateReceiveItems {
quantity?: number
internal_note?: string
reason_id?: string
metadata?: Record<string, unknown>
}
export interface AdminUpdateDismissItems {
quantity?: number
internal_note?: string
reason_id?: string
metadata?: Record<string, unknown>
}
export interface AdminReturnFilters extends FindParams {
id?: string[] | string | OperatorMap<string | string[]>
order_id?: string[] | string | OperatorMap<string | string[]>

View File

@@ -0,0 +1,37 @@
import * as React from "react"
import type { IconProps } from "../types"
const HeartBroken = React.forwardRef<SVGSVGElement, IconProps>(
({ color = "currentColor", ...props }, ref) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
fill="none"
ref={ref}
{...props}
>
<g id="heart-broken">
<path
d="M8.24998 3.47314L6.4722 5.72203L10.0278 7.49981L8.24998 9.27759"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
<path
d="M7.83132 13.0306C8.09532 13.1683 8.40376 13.1683 8.66776 13.0306C10.0633 12.3026 14.4713 9.66434 14.4713 5.37456C14.4784 3.49011 12.9567 1.95589 11.0704 1.94434C9.93532 1.95856 8.88021 2.531 8.24998 3.47322C7.61887 2.531 6.56376 1.95856 5.42954 1.94434C3.54243 1.95589 2.02154 3.49011 2.02865 5.37456C2.02865 9.66434 6.43576 12.3026 7.83132 13.0306Z"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
</g>
</svg>
)
}
)
HeartBroken.displayName = "HeartBroken"
export default HeartBroken

View File

@@ -164,6 +164,7 @@ export { default as GridList } from "./grid-list"
export { default as HandTruck } from "./hand-truck"
export { default as Hashtag } from "./hashtag"
export { default as Heart } from "./heart"
export { default as HeartBroken } from "./heart-broken"
export { default as History } from "./history"
export { default as InboxSolid } from "./inbox-solid"
export { default as InformationCircle } from "./information-circle"