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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./action-menu"
|
||||
@@ -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 (
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: () =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-receive-return-form.tsx"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
export { OrderReceiveReturn as Component } from "./order-receive-return"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]>
|
||||
|
||||
37
packages/design-system/icons/src/components/heart-broken.tsx
Normal file
37
packages/design-system/icons/src/components/heart-broken.tsx
Normal 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
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user