feat(dashboard, order, medusa, types, js-sdk): Request return e2e flow (#7848)
This commit is contained in:
@@ -14,7 +14,7 @@ export const LinkButton = ({
|
||||
return (
|
||||
<Link
|
||||
className={clx(
|
||||
" transition-fg txt-compact-small-plus rounded-[4px] outline-none",
|
||||
"transition-fg txt-compact-small-plus rounded-[4px] outline-none",
|
||||
"focus-visible:shadow-borders-focus",
|
||||
{
|
||||
"text-ui-fg-interactive hover:text-ui-fg-interactive-hover":
|
||||
|
||||
@@ -9,10 +9,16 @@ import {
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { sdk } from "../../lib/client"
|
||||
import { AdminCreateOrderShipment, HttpTypes } from "@medusajs/types"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
const ORDERS_QUERY_KEY = "orders" as const
|
||||
export const ordersQueryKeys = queryKeysFactory(ORDERS_QUERY_KEY)
|
||||
const _orderKeys = queryKeysFactory(ORDERS_QUERY_KEY)
|
||||
|
||||
_orderKeys.preview = function (id: string) {
|
||||
return [this.detail(id), "preview"]
|
||||
}
|
||||
|
||||
export const ordersQueryKeys = _orderKeys
|
||||
|
||||
export const useOrder = (
|
||||
id: string,
|
||||
@@ -31,6 +37,22 @@ export const useOrder = (
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useOrderPreview = (
|
||||
id: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, Error, any, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => sdk.admin.order.retrievePreview(id),
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useOrders = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
@@ -49,10 +71,14 @@ export const useOrders = (
|
||||
|
||||
export const useCreateOrderFulfillment = (
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<any, Error, any>
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminOrderResponse,
|
||||
Error,
|
||||
HttpTypes.AdminCreateOrderFulfillment
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: any) =>
|
||||
mutationFn: (payload: HttpTypes.AdminCreateOrderFulfillment) =>
|
||||
sdk.admin.order.createFulfillment(orderId, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const RETURN_REASONS_QUERY_KEY = "return_reasons" as const
|
||||
export const returnReasonQueryKeys = queryKeysFactory(RETURN_REASONS_QUERY_KEY)
|
||||
|
||||
export const useReturnReasons = (
|
||||
query?: HttpTypes.AdminReturnReasonListParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminReturnReasonsResponse,
|
||||
Error,
|
||||
HttpTypes.AdminReturnReasonsResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => sdk.admin.returnReason.list(query),
|
||||
queryKey: returnReasonQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
284
packages/admin-next/dashboard/src/hooks/api/returns.tsx
Normal file
284
packages/admin-next/dashboard/src/hooks/api/returns.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import {
|
||||
QueryKey,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
} from "@tanstack/react-query"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { ordersQueryKeys } from "./orders"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const RETURNS_QUERY_KEY = "returns" as const
|
||||
export const returnsQueryKeys = queryKeysFactory(RETURNS_QUERY_KEY)
|
||||
|
||||
export const useReturn = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, Error, any, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => sdk.admin.return.retrieve(id, query),
|
||||
queryKey: returnsQueryKeys.detail(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useReturns = (
|
||||
query?: HttpTypes.AdminReturnFilters,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminReturnFilters,
|
||||
Error,
|
||||
HttpTypes.AdminReturnsResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => sdk.admin.return.list(query),
|
||||
queryKey: returnsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useInitiateReturn = (
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminReturnResponse,
|
||||
Error,
|
||||
HttpTypes.AdminInitiateReturnRequest
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminInitiateReturnRequest) =>
|
||||
sdk.admin.return.initiateRequest(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 useAddReturnItem = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminReturnResponse,
|
||||
Error,
|
||||
HttpTypes.AdminAddReturnItems
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminAddReturnItems) =>
|
||||
sdk.admin.return.addReturnItem(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateReturnItem = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminReturnResponse,
|
||||
Error,
|
||||
HttpTypes.AdminUpdateReturnItems & { actionId: string }
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
actionId,
|
||||
...payload
|
||||
}: HttpTypes.AdminUpdateReturnItems & { actionId: string }) => {
|
||||
return sdk.admin.return.updateReturnItem(id, actionId, payload)
|
||||
},
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveReturnItem = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminReturnResponse,
|
||||
Error,
|
||||
HttpTypes.AdminAddReturnItems
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (actionId: string) =>
|
||||
sdk.admin.return.removeReturnItem(id, actionId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddReturnShipping = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminReturnResponse,
|
||||
Error,
|
||||
HttpTypes.AdminAddReturnShipping
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminAddReturnShipping) =>
|
||||
sdk.admin.return.addReturnShipping(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateReturnShipping = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminReturnResponse,
|
||||
Error,
|
||||
HttpTypes.AdminAddReturnShipping
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
actionId,
|
||||
...payload
|
||||
}: HttpTypes.AdminAddReturnShipping & { actionId: string }) =>
|
||||
sdk.admin.return.updateReturnShipping(id, actionId, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteReturnShipping = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<HttpTypes.AdminReturnResponse, Error, string>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (actionId: string) =>
|
||||
sdk.admin.return.deleteReturnShipping(id, actionId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: returnsQueryKeys.details(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: returnsQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -107,9 +107,12 @@
|
||||
"close": "Close",
|
||||
"showMore": "Show more",
|
||||
"continue": "Continue",
|
||||
"addReason": "Add Reason",
|
||||
"addNote": "Add Note",
|
||||
"reset": "Reset",
|
||||
"confirm": "Confirm",
|
||||
"edit": "Edit",
|
||||
"addItems": "Add items",
|
||||
"download": "Download",
|
||||
"clearAll": "Clear all",
|
||||
"apply": "Apply",
|
||||
@@ -772,6 +775,7 @@
|
||||
"requiresAction": "Requires action"
|
||||
}
|
||||
},
|
||||
|
||||
"edits": {
|
||||
"title": "Edit order",
|
||||
"currentItems": "Current items",
|
||||
@@ -783,26 +787,25 @@
|
||||
"differenceDue": "Difference due"
|
||||
},
|
||||
"returns": {
|
||||
"details": "Details",
|
||||
"chooseItems": "Choose items",
|
||||
"refundAmount": "Refund amount",
|
||||
"locationDescription": "Choose which location you want to return the items to.",
|
||||
"shippingDescription": "Choose which method you want to use for this return.",
|
||||
"noInventoryLevel": "No inventory level",
|
||||
"create": "Create Return",
|
||||
"inbound": "Inbound",
|
||||
"sendNotification": "Send notification",
|
||||
"sendNotificationHint": "Notify customer of created return.",
|
||||
"customRefund": "Custom refund",
|
||||
"shippingPriceTooltip1": "Custom refund is enabled",
|
||||
"noShippingOptions": "There are no shipping options for the region",
|
||||
"shippingPriceTooltip2": "Shipping needs to be selected",
|
||||
"customRefundHint": "If you want to refund something else instead of the total refund.",
|
||||
"customShippingPrice": "Custom shipping",
|
||||
"customShippingPriceHint": "Custom shipping cost.",
|
||||
"sendNotificationHint": "Notify customer about return.",
|
||||
"returnTotal": "Return total",
|
||||
"refundAmount": "Refund amount",
|
||||
"reason": "Reason",
|
||||
"reasonHint": "Choose why the customer want to return items.",
|
||||
"note": "Note",
|
||||
"noInventoryLevel": "No inventory level",
|
||||
"noInventoryLevelDesc": "The selected location does not have an inventory level for the selected items. The return can be requested but can’t be received until an inventory level is created for the selected location.",
|
||||
"refundableAmountLabel": "Refundable amount",
|
||||
"refundableAmountHeader": "Refundable Amount",
|
||||
"noteHint": "You can type freely if you want to specify something.",
|
||||
"location": "Location",
|
||||
"locationHint": "Choose which location you want to return the items to.",
|
||||
"inboundShipping": "Inbound shipping",
|
||||
"inboundShippingHint": "Choose which method you want to use.",
|
||||
"returnableQuantityLabel": "Returnable quantity",
|
||||
"returnableQuantityHeader": "Returnable Quantity"
|
||||
"refundableAmountLabel": "Refundable amount",
|
||||
"returnRequestedInfo": "{{requestedItemsCount}}x item return requested"
|
||||
},
|
||||
"reservations": {
|
||||
"allocatedLabel": "Allocated",
|
||||
@@ -922,7 +925,9 @@
|
||||
"items_other": "{{count}} items"
|
||||
},
|
||||
"return": {
|
||||
"created": "Return created"
|
||||
"created": "Return created",
|
||||
"items_one": "{{count}} item returned",
|
||||
"items_other": "{{count}} items returned"
|
||||
},
|
||||
"note": {
|
||||
"comment": "Comment",
|
||||
@@ -2256,6 +2261,7 @@
|
||||
"inStock": "In stock",
|
||||
"location": "Location",
|
||||
"quantity": "Quantity",
|
||||
"qty": "Qty",
|
||||
"variant": "Variant",
|
||||
"id": "ID",
|
||||
"parent": "Parent",
|
||||
|
||||
@@ -1,75 +1,15 @@
|
||||
import { ClaimItem, LineItem, Order } from "@medusajs/medusa"
|
||||
import { AdminOrderLineItem } from "@medusajs/types"
|
||||
|
||||
/**
|
||||
* Return line items that are returnable from an order
|
||||
* @param order
|
||||
* @param isClaim
|
||||
*/
|
||||
export const getAllReturnableItems = (
|
||||
order: Omit<Order, "beforeInserts">,
|
||||
isClaim: boolean
|
||||
) => {
|
||||
let orderItems = order.items.reduce(
|
||||
(map, obj) =>
|
||||
map.set(obj.id, {
|
||||
...obj,
|
||||
}),
|
||||
new Map<string, Omit<LineItem, "beforeInsert">>()
|
||||
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_requested_quantity,
|
||||
} = item.detail
|
||||
|
||||
return (
|
||||
shipped_quantity - (return_received_quantity + return_requested_quantity)
|
||||
)
|
||||
|
||||
let claimedItems: ClaimItem[] = []
|
||||
|
||||
if (order.claims && order.claims.length) {
|
||||
for (const claim of order.claims) {
|
||||
if (claim.return_order?.status !== "canceled") {
|
||||
claim.claim_items = claim.claim_items ?? []
|
||||
claimedItems = [...claimedItems, ...claim.claim_items]
|
||||
}
|
||||
|
||||
if (
|
||||
claim.fulfillment_status === "not_fulfilled" &&
|
||||
claim.payment_status === "na"
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (claim.additional_items && claim.additional_items.length) {
|
||||
orderItems = claim.additional_items
|
||||
.filter(
|
||||
(it) =>
|
||||
it.shipped_quantity ||
|
||||
it.shipped_quantity === it.fulfilled_quantity
|
||||
)
|
||||
.reduce((map, obj) => map.set(obj.id, { ...obj }), orderItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isClaim) {
|
||||
if (order.swaps && order.swaps.length) {
|
||||
for (const swap of order.swaps) {
|
||||
if (swap.fulfillment_status === "not_fulfilled") {
|
||||
continue
|
||||
}
|
||||
|
||||
orderItems = swap.additional_items.reduce(
|
||||
(map, obj) =>
|
||||
map.set(obj.id, {
|
||||
...obj,
|
||||
}),
|
||||
orderItems
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of claimedItems) {
|
||||
const i = orderItems.get(item.item_id)
|
||||
if (i) {
|
||||
i.quantity = i.quantity - item.quantity
|
||||
i.quantity !== 0 ? orderItems.set(i.id, i) : orderItems.delete(i.id)
|
||||
}
|
||||
}
|
||||
|
||||
return [...orderItems.values()]
|
||||
}
|
||||
|
||||
@@ -227,6 +227,11 @@ export const RouteMap: RouteObject[] = [
|
||||
lazy: () =>
|
||||
import("../../routes/orders/order-create-shipment"),
|
||||
},
|
||||
{
|
||||
path: "returns",
|
||||
lazy: () =>
|
||||
import("../../routes/orders/order-create-return"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
||||
import { useMemo, useState } from "react"
|
||||
|
||||
import {
|
||||
DateComparisonOperator,
|
||||
NumericalComparisonOperator,
|
||||
} from "@medusajs/types"
|
||||
import { AdminOrderLineItem } from "@medusajs/types"
|
||||
|
||||
import { useReturnItemTableColumns } from "./use-return-item-table-columns"
|
||||
import { useReturnItemTableFilters } from "./use-return-item-table-filters"
|
||||
import { useReturnItemTableQuery } from "./use-return-item-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { getReturnableQuantity } from "../../../../../lib/rma"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PREFIX = "rit"
|
||||
|
||||
type AddReturnItemsTableProps = {
|
||||
onSelectionChange: (ids: string[]) => void
|
||||
selectedItems: string[]
|
||||
items: AdminOrderLineItem[]
|
||||
currencyCode: string
|
||||
}
|
||||
|
||||
export const AddReturnItemsTable = ({
|
||||
onSelectionChange,
|
||||
selectedItems,
|
||||
items,
|
||||
currencyCode,
|
||||
}: AddReturnItemsTableProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
selectedItems.reduce((acc, id) => {
|
||||
acc[id] = true
|
||||
return acc
|
||||
}, {} as RowSelectionState)
|
||||
)
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
setRowSelection(newState)
|
||||
onSelectionChange(Object.keys(newState))
|
||||
}
|
||||
|
||||
const { searchParams, raw } = useReturnItemTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const queriedItems = useMemo(() => {
|
||||
const {
|
||||
order,
|
||||
offset,
|
||||
limit,
|
||||
q,
|
||||
created_at,
|
||||
updated_at,
|
||||
refundable_amount,
|
||||
returnable_quantity,
|
||||
} = searchParams
|
||||
|
||||
let results: AdminOrderLineItem[] = items
|
||||
|
||||
if (q) {
|
||||
results = results.filter((i) => {
|
||||
return (
|
||||
i.variant.product.title.toLowerCase().includes(q.toLowerCase()) ||
|
||||
i.variant.title.toLowerCase().includes(q.toLowerCase()) ||
|
||||
i.variant.sku?.toLowerCase().includes(q.toLowerCase())
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (order) {
|
||||
const direction = order[0] === "-" ? "desc" : "asc"
|
||||
const field = order.replace("-", "")
|
||||
|
||||
results = sortItems(results, field, direction)
|
||||
}
|
||||
|
||||
if (created_at) {
|
||||
results = filterByDate(results, created_at, "created_at")
|
||||
}
|
||||
|
||||
if (updated_at) {
|
||||
results = filterByDate(results, updated_at, "updated_at")
|
||||
}
|
||||
|
||||
if (returnable_quantity) {
|
||||
results = filterByNumber(
|
||||
results,
|
||||
returnable_quantity,
|
||||
"returnable_quantity",
|
||||
currencyCode
|
||||
)
|
||||
}
|
||||
|
||||
if (refundable_amount) {
|
||||
results = filterByNumber(
|
||||
results,
|
||||
refundable_amount,
|
||||
"refundable_amount",
|
||||
currencyCode
|
||||
)
|
||||
}
|
||||
|
||||
return results.slice(offset, offset + limit)
|
||||
}, [items, currencyCode, searchParams])
|
||||
|
||||
const columns = useReturnItemTableColumns(currencyCode)
|
||||
const filters = useReturnItemTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: queriedItems as AdminOrderLineItem[],
|
||||
columns: columns,
|
||||
count: queriedItems.length,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: (row) => {
|
||||
return getReturnableQuantity(row.original) > 0
|
||||
},
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={queriedItems.length}
|
||||
filters={filters}
|
||||
pagination
|
||||
layout="fill"
|
||||
search
|
||||
orderBy={[
|
||||
"product_title",
|
||||
"variant_title",
|
||||
"sku",
|
||||
"returnable_quantity",
|
||||
"refundable_amount",
|
||||
]}
|
||||
prefix={PREFIX}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const sortItems = (
|
||||
items: AdminOrderLineItem[],
|
||||
field: string,
|
||||
direction: "asc" | "desc"
|
||||
) => {
|
||||
return items.sort((a, b) => {
|
||||
let aValue: any
|
||||
let bValue: any
|
||||
|
||||
if (field === "product_title") {
|
||||
aValue = a.variant.product.title
|
||||
bValue = b.variant.product.title
|
||||
} else if (field === "variant_title") {
|
||||
aValue = a.variant.title
|
||||
bValue = b.variant.title
|
||||
} else if (field === "sku") {
|
||||
aValue = a.variant.sku
|
||||
bValue = b.variant.sku
|
||||
} else if (field === "returnable_quantity") {
|
||||
aValue = a.quantity - (a.returned_quantity || 0)
|
||||
bValue = b.quantity - (b.returned_quantity || 0)
|
||||
} else if (field === "refundable_amount") {
|
||||
aValue = a.refundable || 0
|
||||
bValue = b.refundable || 0
|
||||
}
|
||||
|
||||
if (aValue < bValue) {
|
||||
return direction === "asc" ? -1 : 1
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return direction === "asc" ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
const filterByDate = (
|
||||
items: AdminOrderLineItem[],
|
||||
date: DateComparisonOperator,
|
||||
field: "created_at" | "updated_at"
|
||||
) => {
|
||||
const { gt, gte, lt, lte } = date
|
||||
|
||||
return items.filter((i) => {
|
||||
const itemDate = new Date(i[field])
|
||||
let isValid = true
|
||||
|
||||
if (gt) {
|
||||
isValid = isValid && itemDate > new Date(gt)
|
||||
}
|
||||
|
||||
if (gte) {
|
||||
isValid = isValid && itemDate >= new Date(gte)
|
||||
}
|
||||
|
||||
if (lt) {
|
||||
isValid = isValid && itemDate < new Date(lt)
|
||||
}
|
||||
|
||||
if (lte) {
|
||||
isValid = isValid && itemDate <= new Date(lte)
|
||||
}
|
||||
|
||||
return isValid
|
||||
})
|
||||
}
|
||||
|
||||
const defaultOperators = {
|
||||
eq: undefined,
|
||||
gt: undefined,
|
||||
gte: undefined,
|
||||
lt: undefined,
|
||||
lte: undefined,
|
||||
}
|
||||
|
||||
const filterByNumber = (
|
||||
items: AdminOrderLineItem[],
|
||||
value: NumericalComparisonOperator | number,
|
||||
field: "returnable_quantity" | "refundable_amount",
|
||||
currency_code: string
|
||||
) => {
|
||||
const { eq, gt, lt, gte, lte } =
|
||||
typeof value === "object"
|
||||
? { ...defaultOperators, ...value }
|
||||
: { ...defaultOperators, eq: value }
|
||||
|
||||
return items.filter((i) => {
|
||||
const returnableQuantity = i.quantity - (i.returned_quantity || 0)
|
||||
const refundableAmount = getStylizedAmount(i.refundable || 0, currency_code)
|
||||
|
||||
const itemValue =
|
||||
field === "returnable_quantity" ? returnableQuantity : refundableAmount
|
||||
|
||||
if (eq) {
|
||||
return itemValue === eq
|
||||
}
|
||||
|
||||
let isValid = true
|
||||
|
||||
if (gt) {
|
||||
isValid = isValid && itemValue > gt
|
||||
}
|
||||
|
||||
if (gte) {
|
||||
isValid = isValid && itemValue >= gte
|
||||
}
|
||||
|
||||
if (lt) {
|
||||
isValid = isValid && itemValue < lt
|
||||
}
|
||||
|
||||
if (lte) {
|
||||
isValid = isValid && itemValue <= lte
|
||||
}
|
||||
|
||||
return isValid
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./add-return-items-table"
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useMemo } from "react"
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
ProductCell,
|
||||
ProductHeader,
|
||||
} from "../../../../../components/table/table-cells/product/product-cell"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { getReturnableQuantity } from "../../../../../lib/rma"
|
||||
|
||||
const columnHelper = createColumnHelper<any>()
|
||||
|
||||
export const useReturnItemTableColumns = (currencyCode: string) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isSelectable = row.getCanSelect()
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
disabled={!isSelectable}
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "product",
|
||||
header: () => <ProductHeader />,
|
||||
cell: ({ row }) => (
|
||||
<ProductCell product={row.original.variant.product} />
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("variant.sku", {
|
||||
header: t("fields.sku"),
|
||||
cell: ({ getValue }) => {
|
||||
return getValue() || "-"
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("variant.title", {
|
||||
header: t("fields.variant"),
|
||||
}),
|
||||
columnHelper.accessor("quantity", {
|
||||
header: () => (
|
||||
<div className="flex size-full items-center overflow-hidden text-right">
|
||||
<span className="truncate">{t("fields.quantity")}</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ getValue, row }) => {
|
||||
return getReturnableQuantity(row.original)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("refundable_total", {
|
||||
header: () => (
|
||||
<div className="flex size-full items-center justify-end overflow-hidden text-right">
|
||||
<span className="truncate">{t("fields.price")}</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
const amount = getValue() || 0
|
||||
|
||||
const stylized = getStylizedAmount(amount, currencyCode)
|
||||
|
||||
return (
|
||||
<div className="flex size-full items-center justify-end overflow-hidden text-right">
|
||||
<span className="truncate">{stylized}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t, currencyCode]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useReturnItemTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
key: "returnable_quantity",
|
||||
label: t("orders.returns.returnableQuantityLabel"),
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
key: "refundable_amount",
|
||||
label: t("orders.returns.refundableAmountLabel"),
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
key: "created_at",
|
||||
label: t("fields.createdAt"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
key: "updated_at",
|
||||
label: t("fields.updatedAt"),
|
||||
type: "date",
|
||||
},
|
||||
]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
DateComparisonOperator,
|
||||
NumericalComparisonOperator,
|
||||
} from "@medusajs/types"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export type ReturnItemTableQuery = {
|
||||
q?: string
|
||||
offset: number
|
||||
order?: string
|
||||
created_at?: DateComparisonOperator
|
||||
updated_at?: DateComparisonOperator
|
||||
returnable_quantity?: NumericalComparisonOperator | number
|
||||
refundable_amount?: NumericalComparisonOperator | number
|
||||
}
|
||||
|
||||
export const useReturnItemTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
[
|
||||
"q",
|
||||
"offset",
|
||||
"order",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"returnable_quantity",
|
||||
"refundable_amount",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
const {
|
||||
offset,
|
||||
created_at,
|
||||
updated_at,
|
||||
refundable_amount,
|
||||
returnable_quantity,
|
||||
...rest
|
||||
} = raw
|
||||
|
||||
const searchParams = {
|
||||
...rest,
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
refundable_amount: refundable_amount
|
||||
? JSON.parse(refundable_amount)
|
||||
: undefined,
|
||||
returnable_quantity: returnable_quantity
|
||||
? JSON.parse(returnable_quantity)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
return { searchParams, raw }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./return-create-form"
|
||||
@@ -0,0 +1,709 @@
|
||||
import React, { useEffect, useMemo, useState } from "react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
CurrencyInput,
|
||||
Heading,
|
||||
IconButton,
|
||||
Switch,
|
||||
Text,
|
||||
toast,
|
||||
} from "@medusajs/ui"
|
||||
import { useFieldArray, useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { AdminOrder, InventoryLevelDTO, ReturnDTO } from "@medusajs/types"
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
|
||||
import {
|
||||
RouteFocusModal,
|
||||
StackedFocusModal,
|
||||
useRouteModal,
|
||||
useStackedModal,
|
||||
} from "../../../../../components/modals"
|
||||
|
||||
import { ReturnCreateSchema, ReturnCreateSchemaType } from "./schema"
|
||||
import { AddReturnItemsTable } from "../add-return-items-table"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { ReturnItem } from "./return-item"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
import { useShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import {
|
||||
useAddReturnItem,
|
||||
useAddReturnShipping,
|
||||
useCancelReturnRequest,
|
||||
useConfirmReturnRequest,
|
||||
useDeleteReturnShipping,
|
||||
useRemoveReturnItem,
|
||||
useUpdateReturnItem,
|
||||
useUpdateReturnShipping,
|
||||
} from "../../../../../hooks/api/returns"
|
||||
import { currencies } from "../../../../../lib/data/currencies"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
|
||||
type ReturnCreateFormProps = {
|
||||
order: AdminOrder
|
||||
activeReturn: ReturnDTO // TODO: AdminReturn
|
||||
preview: AdminOrder // TODO
|
||||
}
|
||||
|
||||
let selectedItems: string[] = []
|
||||
|
||||
let IS_CANCELING = false
|
||||
|
||||
export const ReturnCreateForm = ({
|
||||
order,
|
||||
preview,
|
||||
activeReturn,
|
||||
}: ReturnCreateFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
/**
|
||||
* STATE
|
||||
*/
|
||||
const { setIsOpen } = useStackedModal()
|
||||
const [isShippingPriceEdit, setIsShippingPriceEdit] = useState(false)
|
||||
const [customShippingAmount, setCustomShippingAmount] = useState(0)
|
||||
const [inventoryMap, setInventoryMap] = useState<
|
||||
Record<string, InventoryLevelDTO[]>
|
||||
>({})
|
||||
|
||||
/**
|
||||
* HOOKS
|
||||
*/
|
||||
const { stock_locations = [] } = useStockLocations({ limit: 999 })
|
||||
const { shipping_options = [] } = useShippingOptions({
|
||||
limit: 999,
|
||||
fields: "*prices,+service_zone.fulfillment_set.location.id",
|
||||
/**
|
||||
* TODO: this should accept filter for location_id
|
||||
*/
|
||||
})
|
||||
|
||||
/**
|
||||
* MUTATIONS
|
||||
*/
|
||||
const { mutateAsync: confirmReturnRequest, isPending: isConfirming } =
|
||||
useConfirmReturnRequest(activeReturn.id, order.id)
|
||||
|
||||
const { mutateAsync: cancelReturnRequest, isPending: isCanceling } =
|
||||
useCancelReturnRequest(activeReturn.id, order.id)
|
||||
|
||||
const { mutateAsync: addReturnShipping, isPending: isAddingReturnShipping } =
|
||||
useAddReturnShipping(activeReturn.id, order.id)
|
||||
|
||||
const {
|
||||
mutateAsync: updateReturnShipping,
|
||||
isPending: isUpdatingReturnShipping,
|
||||
} = useUpdateReturnShipping(activeReturn.id, order.id)
|
||||
|
||||
const {
|
||||
mutateAsync: deleteReturnShipping,
|
||||
isPending: isDeletingReturnShipping,
|
||||
} = useDeleteReturnShipping(activeReturn.id, order.id)
|
||||
|
||||
const { mutateAsync: addReturnItem, isPending: isAddingReturnItem } =
|
||||
useAddReturnItem(activeReturn.id, order.id)
|
||||
|
||||
const { mutateAsync: removeReturnItem, isPending: isRemovingReturnItem } =
|
||||
useRemoveReturnItem(activeReturn.id, order.id)
|
||||
|
||||
const { mutateAsync: updateReturnItem, isPending: isUpdatingReturnItem } =
|
||||
useUpdateReturnItem(activeReturn.id, order.id)
|
||||
|
||||
const isRequestLoading =
|
||||
isConfirming ||
|
||||
isCanceling ||
|
||||
isAddingReturnShipping ||
|
||||
isUpdatingReturnShipping ||
|
||||
isDeletingReturnShipping ||
|
||||
isAddingReturnItem ||
|
||||
isRemovingReturnItem ||
|
||||
isUpdatingReturnItem
|
||||
|
||||
/**
|
||||
* FORM
|
||||
*/
|
||||
|
||||
const form = useForm<ReturnCreateSchemaType>({
|
||||
/**
|
||||
* TODO: reason selection once Return reason settings are added
|
||||
*/
|
||||
defaultValues: () => {
|
||||
const method = preview.shipping_methods.find(
|
||||
(s) => !!s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
)
|
||||
|
||||
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,
|
||||
})),
|
||||
option_id: method ? method.shipping_option_id : "",
|
||||
location_id: "",
|
||||
send_notification: false,
|
||||
})
|
||||
},
|
||||
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,
|
||||
remove,
|
||||
update,
|
||||
} = useFieldArray({
|
||||
name: "items",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const existingItemsMap = {}
|
||||
|
||||
preview.items.forEach((i) => {
|
||||
const ind = items.findIndex((field) => field.item_id === i.id)
|
||||
|
||||
/**
|
||||
* THESE ITEMS ARE REMOVED FROM RETURN REQUEST
|
||||
*/
|
||||
if (!i.detail.return_requested_quantity) {
|
||||
return
|
||||
}
|
||||
|
||||
existingItemsMap[i.id] = true
|
||||
|
||||
if (ind > -1) {
|
||||
if (items[ind].quantity !== i.detail.return_requested_quantity) {
|
||||
const returnItemAction = i.actions?.find(
|
||||
(a) => a.action === "RETURN_ITEM"
|
||||
)
|
||||
|
||||
update(ind, {
|
||||
...items[ind],
|
||||
quantity: i.detail.return_requested_quantity,
|
||||
note: returnItemAction?.internal_note,
|
||||
reason_id: returnItemAction?.details?.reason_id,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
append({ item_id: i.id, quantity: i.detail.return_requested_quantity })
|
||||
}
|
||||
})
|
||||
|
||||
items.forEach((i, ind) => {
|
||||
if (!(i.item_id in existingItemsMap)) {
|
||||
remove(ind)
|
||||
}
|
||||
})
|
||||
}, [preview.items])
|
||||
|
||||
useEffect(() => {
|
||||
const method = preview.shipping_methods.find(
|
||||
(s) => !!s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
)
|
||||
|
||||
if (method) {
|
||||
form.setValue("option_id", method.shipping_option_id)
|
||||
}
|
||||
}, [preview.shipping_methods])
|
||||
|
||||
const showPlaceholder = !items.length
|
||||
const locationId = form.watch("location_id")
|
||||
const shippingOptionId = form.watch("option_id")
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
try {
|
||||
await confirmReturnRequest({ no_notification: !data.send_notification })
|
||||
|
||||
handleSuccess()
|
||||
} catch (e) {
|
||||
toast.error(t("general.error"), {
|
||||
description: e.message,
|
||||
dismissLabel: t("actions.close"),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const onItemsSelected = () => {
|
||||
addReturnItem({
|
||||
items: selectedItems.map((id) => ({
|
||||
id,
|
||||
quantity: 1,
|
||||
})),
|
||||
})
|
||||
|
||||
setIsOpen("items", false)
|
||||
}
|
||||
|
||||
const onShippingOptionChange = async (selectedOptionId: string) => {
|
||||
const promises = preview.shipping_methods
|
||||
.map((s) => s.actions?.find((a) => a.action === "SHIPPING_ADD")?.id)
|
||||
.filter(Boolean)
|
||||
.map(deleteReturnShipping)
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
await addReturnShipping({ shipping_option_id: selectedOptionId })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isShippingPriceEdit) {
|
||||
document.getElementById("js-shipping-input").focus()
|
||||
}
|
||||
}, [isShippingPriceEdit])
|
||||
|
||||
const showLevelsWarning = useMemo(() => {
|
||||
if (!locationId) {
|
||||
return false
|
||||
}
|
||||
|
||||
const allItemsHaveLocation = items
|
||||
.map((_i) => {
|
||||
const item = itemsMap.get(_i.item_id)
|
||||
if (!item?.variant_id) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!item.variant.manage_inventory) {
|
||||
return true
|
||||
}
|
||||
|
||||
return inventoryMap[item.variant_id]?.find(
|
||||
(l) => l.location_id === locationId
|
||||
)
|
||||
})
|
||||
.every(Boolean)
|
||||
|
||||
return !allItemsHaveLocation
|
||||
}, [items, inventoryMap, locationId])
|
||||
|
||||
useEffect(() => {
|
||||
const getInventoryMap = async () => {
|
||||
const ret: Record<string, InventoryLevelDTO[]> = {}
|
||||
|
||||
if (!items.length) {
|
||||
return ret
|
||||
}
|
||||
|
||||
;(
|
||||
await Promise.all(
|
||||
items.map(async (_i) => {
|
||||
const item = itemsMap.get(_i.item_id)
|
||||
|
||||
if (!item.variant_id) {
|
||||
return undefined
|
||||
}
|
||||
return await sdk.admin.product.retrieveVariant(
|
||||
item.variant.product.id,
|
||||
item.variant_id,
|
||||
{ fields: "*inventory,*inventory.location_levels" }
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
.filter((it) => it?.variant)
|
||||
.forEach((item) => {
|
||||
const { variant } = item
|
||||
const levels = variant.inventory[0]?.location_levels
|
||||
|
||||
if (!levels) {
|
||||
return
|
||||
}
|
||||
|
||||
ret[variant.id] = levels
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
getInventoryMap().then((map) => {
|
||||
setInventoryMap(map)
|
||||
})
|
||||
}, [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(() => {
|
||||
const method = preview.shipping_methods.find(
|
||||
(sm) => !!sm.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
)
|
||||
|
||||
return method?.total || 0
|
||||
}, [preview.shipping_methods])
|
||||
|
||||
const refundAmount = returnTotal - shippingTotal
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<RouteFocusModal.Header />
|
||||
|
||||
<RouteFocusModal.Body className="flex size-full justify-center overflow-y-auto">
|
||||
<div className="mt-16 w-[720px] max-w-[100%] px-4 md:p-0">
|
||||
<Heading level="h1">{t("orders.returns.create")}</Heading>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<Heading level="h2">{t("orders.returns.inbound")}</Heading>
|
||||
<StackedFocusModal id="items">
|
||||
<StackedFocusModal.Trigger asChild>
|
||||
<a className="focus-visible:shadow-borders-focus transition-fg txt-compact-small-plus cursor-pointer text-blue-500 outline-none hover:text-blue-400">
|
||||
{t("actions.addItems")}
|
||||
</a>
|
||||
</StackedFocusModal.Trigger>
|
||||
<StackedFocusModal.Content>
|
||||
<StackedFocusModal.Header />
|
||||
|
||||
<AddReturnItemsTable
|
||||
items={order.items!}
|
||||
selectedItems={items.map((i) => i.item_id)}
|
||||
currencyCode={order.currency_code}
|
||||
onSelectionChange={(s) => (selectedItems = s)}
|
||||
/>
|
||||
<StackedFocusModal.Footer>
|
||||
<div className="flex w-full items-center justify-end gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
>
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
key="submit-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
role="button"
|
||||
onClick={() => onItemsSelected()}
|
||||
>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StackedFocusModal.Footer>
|
||||
</StackedFocusModal.Content>
|
||||
</StackedFocusModal>
|
||||
</div>
|
||||
{showPlaceholder && (
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(-45deg, rgb(212, 212, 216, 0.15), rgb(212, 212, 216,.15) 10px, transparent 10px, transparent 20px)",
|
||||
}}
|
||||
className="bg-ui-bg-field mt-4 block h-[56px] w-full rounded-lg border border-dashed"
|
||||
/>
|
||||
)}
|
||||
{items.map((item, index) => (
|
||||
<ReturnItem
|
||||
key={item.id}
|
||||
item={itemsMap.get(item.item_id)!}
|
||||
previewItem={previewItemsMap.get(item.item_id)!}
|
||||
currencyCode={order.currency_code}
|
||||
form={form}
|
||||
onRemove={() => {
|
||||
const actionId = preview.items
|
||||
.find((i) => i.id === item.item_id)
|
||||
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
|
||||
|
||||
if (actionId) {
|
||||
removeReturnItem(actionId)
|
||||
}
|
||||
}}
|
||||
onUpdate={(payload) => {
|
||||
const actionId = preview.items
|
||||
.find((i) => i.id === item.item_id)
|
||||
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
|
||||
|
||||
if (actionId) {
|
||||
updateReturnItem({ ...payload, actionId })
|
||||
}
|
||||
}}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
{!showPlaceholder && (
|
||||
<div className="mt-8 flex flex-col gap-y-4">
|
||||
{/*LOCATION*/}
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<div>
|
||||
<Form.Label>{t("orders.returns.location")}</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.returns.locationHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="location_id"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v)
|
||||
}}
|
||||
{...field}
|
||||
options={(stock_locations ?? []).map(
|
||||
(stockLocation) => ({
|
||||
label: stockLocation.name,
|
||||
value: stockLocation.id,
|
||||
})
|
||||
)}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*INBOUND SHIPPING*/}
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<div>
|
||||
<Form.Label>
|
||||
{t("orders.returns.inboundShipping")}
|
||||
</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.returns.inboundShippingHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
|
||||
{/*TODO: WHAT IF THE RETURN OPTION HAS COMPUTED PRICE*/}
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="option_id"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v)
|
||||
onShippingOptionChange(v)
|
||||
}}
|
||||
{...field}
|
||||
options={(shipping_options ?? [])
|
||||
.filter(
|
||||
(so) =>
|
||||
(locationId
|
||||
? so.service_zone.fulfillment_set!
|
||||
.location.id === locationId
|
||||
: true) &&
|
||||
!!so.rules.find(
|
||||
(r) =>
|
||||
r.attribute === "is_return" &&
|
||||
r.value === "true"
|
||||
)
|
||||
)
|
||||
.map((so) => ({
|
||||
label: so.name,
|
||||
value: so.id,
|
||||
}))}
|
||||
disabled={!locationId}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLevelsWarning && (
|
||||
<Alert variant="warning" dismissible className="mt-4 p-5">
|
||||
<div className="text-ui-fg-subtle txt-small pb-2 font-medium leading-[20px]">
|
||||
{t("orders.returns.noInventoryLevel")}
|
||||
</div>
|
||||
<Text className="text-ui-fg-subtle txt-small leading-normal">
|
||||
{t("orders.returns.noInventoryLevelDesc")}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/*TOTALS SECTION*/}
|
||||
<div className="mt-8 border-y border-dotted py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.returns.returnTotal")}
|
||||
</span>
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{getStylizedAmount(
|
||||
returnTotal ? -1 * returnTotal : returnTotal,
|
||||
order.currency_code
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.returns.inboundShipping")}
|
||||
</span>
|
||||
<span className="txt-small text-ui-fg-subtle flex items-center">
|
||||
{!isShippingPriceEdit && (
|
||||
<IconButton
|
||||
onClick={() => setIsShippingPriceEdit(true)}
|
||||
variant="transparent"
|
||||
className="text-ui-fg-muted"
|
||||
disabled={showPlaceholder || !shippingOptionId}
|
||||
>
|
||||
<PencilSquare />
|
||||
</IconButton>
|
||||
)}
|
||||
{isShippingPriceEdit ? (
|
||||
<CurrencyInput
|
||||
id="js-shipping-input"
|
||||
onBlur={() => {
|
||||
let actionId
|
||||
|
||||
preview.shipping_methods.forEach((s) => {
|
||||
if (s.actions) {
|
||||
for (let a of s.actions) {
|
||||
if (a.action === "SHIPPING_ADD") {
|
||||
actionId = a.id
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (actionId) {
|
||||
updateReturnShipping({
|
||||
actionId,
|
||||
custom_price:
|
||||
typeof customShippingAmount === "string"
|
||||
? null
|
||||
: customShippingAmount,
|
||||
})
|
||||
}
|
||||
setIsShippingPriceEdit(false)
|
||||
}}
|
||||
symbol={
|
||||
currencies[order.currency_code.toUpperCase()]
|
||||
.symbol_native
|
||||
}
|
||||
code={order.currency_code}
|
||||
onValueChange={(value) =>
|
||||
setCustomShippingAmount(value ? parseInt(value) : "")
|
||||
}
|
||||
value={customShippingAmount}
|
||||
disabled={showPlaceholder}
|
||||
/>
|
||||
) : (
|
||||
getStylizedAmount(shippingTotal, 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.refundAmount")}
|
||||
</span>
|
||||
<span className="txt-small font-medium">
|
||||
{getStylizedAmount(
|
||||
refundAmount ? -1 * refundAmount : refundAmount,
|
||||
order.currency_code
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*SEND NOTIFICATION*/}
|
||||
<div className="bg-ui-bg-field mt-8 rounded-lg border py-2 pl-2 pr-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="send_notification"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center">
|
||||
<Form.Control className="mr-4 self-start">
|
||||
<Switch
|
||||
className="mt-[2px]"
|
||||
checked={!!value}
|
||||
onCheckedChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
<div className="block">
|
||||
<Form.Label>
|
||||
{t("orders.returns.sendNotification")}
|
||||
</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.returns.sendNotificationHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
<RouteFocusModal.Footer>
|
||||
<div className="flex w-full items-center justify-end gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => (IS_CANCELING = true)}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
>
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
key="submit-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
isLoading={isRequestLoading}
|
||||
>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Footer>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import React from "react"
|
||||
import { IconButton, Input, Text } from "@medusajs/ui"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import { HttpTypes, AdminOrderLineItem } from "@medusajs/types"
|
||||
import { ChatBubble, DocumentText, XCircle, XMark } from "@medusajs/icons"
|
||||
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { useReturnReasons } from "../../../../../hooks/api/return-reasons"
|
||||
|
||||
type OrderEditItemProps = {
|
||||
item: AdminOrderLineItem
|
||||
previewItem: AdminOrderLineItem
|
||||
currencyCode: string
|
||||
index: number
|
||||
|
||||
onRemove: () => void
|
||||
onUpdate: (payload: HttpTypes.AdminUpdateReturnItems) => void
|
||||
|
||||
form: UseFormReturn<any>
|
||||
}
|
||||
|
||||
function ReturnItem({
|
||||
item,
|
||||
previewItem,
|
||||
currencyCode,
|
||||
form,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
index,
|
||||
}: OrderEditItemProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { return_reasons = [] } = useReturnReasons({ fields: "+label" })
|
||||
|
||||
const formItem = form.watch(`items.${index}`)
|
||||
|
||||
const showReturnReason = typeof formItem.reason_id === "string"
|
||||
const showNote = typeof formItem.note === "string"
|
||||
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl ">
|
||||
<div className="flex flex-col items-center gap-x-2 gap-y-2 border-b p-3 text-sm md:flex-row">
|
||||
<div className="flex flex-1 items-center gap-x-3">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
<Text className="txt-small" as="span" weight="plus">
|
||||
{item.title}{" "}
|
||||
</Text>
|
||||
{item.variant.sku && <span>({item.variant.sku})</span>}
|
||||
</div>
|
||||
<Text as="div" className="text-ui-fg-subtle txt-small">
|
||||
{item.variant.product.title}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 justify-between">
|
||||
<div className="flex flex-grow items-center gap-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`items.${index}.quantity`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Input
|
||||
className="bg-ui-bg-base txt-small w-[67px] rounded-lg"
|
||||
min={1}
|
||||
max={item.quantity}
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
const payload = val === "" ? null : Number(val)
|
||||
|
||||
field.onChange(payload)
|
||||
|
||||
if (payload) {
|
||||
// todo: move on blur
|
||||
onUpdate({ quantity: payload })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Text className="txt-small text-ui-fg-subtle">
|
||||
{t("fields.qty")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle txt-small mr-2 flex flex-shrink-0">
|
||||
<MoneyAmountCell
|
||||
currencyCode={currencyCode}
|
||||
amount={previewItem.return_requested_total}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
!showReturnReason && {
|
||||
label: t("actions.addReason"),
|
||||
onClick: () =>
|
||||
form.setValue(`items.${index}.reason_id`, ""),
|
||||
icon: <ChatBubble />,
|
||||
},
|
||||
!showNote && {
|
||||
label: t("actions.addNote"),
|
||||
onClick: () => form.setValue(`items.${index}.note`, ""),
|
||||
icon: <DocumentText />,
|
||||
},
|
||||
{
|
||||
label: t("actions.remove"),
|
||||
onClick: onRemove,
|
||||
icon: <XCircle />,
|
||||
},
|
||||
].filter(Boolean),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{/*REASON*/}
|
||||
{showReturnReason && (
|
||||
<div className="grid grid-cols-1 gap-2 p-3 md:grid-cols-2">
|
||||
<div>
|
||||
<Form.Label>{t("orders.returns.reason")}</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.returns.reasonHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex-grow">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`items.${index}.reason_id`}
|
||||
render={({ field: { ref, value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onUpdate({ reason_id: v })
|
||||
onChange(v)
|
||||
}}
|
||||
{...field}
|
||||
options={return_reasons.map((reason) => ({
|
||||
label: reason.label,
|
||||
value: reason.id,
|
||||
}))}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
type="button"
|
||||
className="flex-shrink"
|
||||
variant="transparent"
|
||||
onClick={() => {
|
||||
onUpdate({ reason_id: null }) // TODO BE: we should be able to set to unset reason here
|
||||
form.setValue(`items.${index}.reason_id`, "")
|
||||
}}
|
||||
>
|
||||
<XMark className="text-ui-fg-muted" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*NOTE*/}
|
||||
{showNote && (
|
||||
<div className="grid grid-cols-1 gap-2 p-3 md:grid-cols-2">
|
||||
<div>
|
||||
<Form.Label>{t("orders.returns.note")}</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.returns.noteHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex-grow">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`items.${index}.note`}
|
||||
render={({ field: { ref, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Input
|
||||
onChange={onChange}
|
||||
{...field}
|
||||
onBlur={() =>
|
||||
onUpdate({ internal_note: field.value })
|
||||
}
|
||||
className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
type="button"
|
||||
className="flex-shrink"
|
||||
variant="transparent"
|
||||
onClick={() => {
|
||||
form.setValue(`items.${index}.note`, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
onUpdate({ internal_note: null })
|
||||
}}
|
||||
>
|
||||
<XMark className="text-ui-fg-muted" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ReturnItem }
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const ReturnCreateSchema = z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
item_id: z.string(),
|
||||
quantity: z.number(),
|
||||
reason_id: z.string().optional().nullable(),
|
||||
note: z.string().optional().nullable(),
|
||||
})
|
||||
),
|
||||
location_id: z.string().optional(),
|
||||
option_id: z.string(),
|
||||
send_notification: z.boolean().optional(),
|
||||
// TODO: implement this
|
||||
receive_now: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type ReturnCreateSchemaType = z.infer<typeof ReturnCreateSchema>
|
||||
@@ -0,0 +1 @@
|
||||
export { ReturnCreate as Component } from "./return-create"
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import { ReturnCreateForm } from "./components/return-create-form"
|
||||
|
||||
import { useOrder, useOrderPreview } from "../../../hooks/api/orders"
|
||||
import { useInitiateReturn, useReturn } from "../../../hooks/api/returns"
|
||||
import { DEFAULT_FIELDS } from "../order-detail/constants"
|
||||
|
||||
let IS_REQUEST_RUNNING = false
|
||||
|
||||
export const ReturnCreate = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const { order } = useOrder(id!, {
|
||||
fields: DEFAULT_FIELDS,
|
||||
})
|
||||
|
||||
const { order: preview } = useOrderPreview(id!)
|
||||
|
||||
const [activeReturnId, setActiveReturnId] = useState()
|
||||
|
||||
const { mutateAsync: initiateReturn } = useInitiateReturn(order.id)
|
||||
|
||||
const { return: activeReturn } = useReturn(activeReturnId, undefined, {
|
||||
enabled: !!activeReturnId,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
if (IS_REQUEST_RUNNING || !order || !preview) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Active return already exists
|
||||
*/
|
||||
if (preview.order_change?.change_type === "return") {
|
||||
setActiveReturnId(preview.order_change.return.id)
|
||||
return
|
||||
}
|
||||
|
||||
IS_REQUEST_RUNNING = true
|
||||
|
||||
const orderReturn = await initiateReturn({ order_id: order.id })
|
||||
setActiveReturnId(orderReturn.id)
|
||||
|
||||
IS_REQUEST_RUNNING = false
|
||||
}
|
||||
|
||||
run()
|
||||
}, [order, preview])
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{activeReturn && preview && order && (
|
||||
<ReturnCreateForm
|
||||
order={order}
|
||||
activeReturn={activeReturn}
|
||||
preview={preview}
|
||||
/>
|
||||
)}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import { PropsWithChildren, ReactNode, useMemo, useState } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import { XMarkMini } from "@medusajs/icons"
|
||||
import { AdminFulfillment, AdminOrder } from "@medusajs/types"
|
||||
import { AdminFulfillment, AdminOrder, AdminReturn } from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useDate } from "../../../../../hooks/use-date"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { useReturns } from "../../../../../hooks/api/returns"
|
||||
|
||||
type OrderTimelineProps = {
|
||||
order: AdminOrder
|
||||
@@ -80,6 +81,8 @@ type Activity = {
|
||||
const useActivityItems = (order: AdminOrder) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { returns = [] } = useReturns({ order_id: order.id, fields: "*items" })
|
||||
|
||||
const notes = []
|
||||
const isLoading = false
|
||||
// const { notes, isLoading, isError, error } = useNotes(
|
||||
@@ -165,16 +168,13 @@ const useActivityItems = (order: AdminOrder) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: revisit when API is fixed to fetch returns of an order
|
||||
*/
|
||||
|
||||
// for (const ret of order.returns) {
|
||||
// items.push({
|
||||
// title: t("orders.activity.events.return.created"),
|
||||
// timestamp: ret.created_at,
|
||||
// })
|
||||
// }
|
||||
for (const ret of returns) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.return.created"),
|
||||
timestamp: ret.created_at,
|
||||
children: <ReturnCreatedBody orderReturn={ret} />,
|
||||
})
|
||||
}
|
||||
|
||||
// for (const note of notes || []) {
|
||||
// items.push({
|
||||
@@ -391,3 +391,21 @@ const FulfillmentCreatedBody = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ReturnCreatedBody = ({ orderReturn }: { orderReturn: AdminReturn }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const numberOfItems = orderReturn.items.reduce((acc, item) => {
|
||||
return acc + item.quantity
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("orders.activity.events.return.items", {
|
||||
count: numberOfItems,
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import {
|
||||
AdminOrder,
|
||||
OrderLineItemDTO,
|
||||
ReservationItemDTO,
|
||||
} from "@medusajs/types"
|
||||
import { ArrowDownRightMini, ArrowUturnLeft } from "@medusajs/icons"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
@@ -11,9 +16,6 @@ import {
|
||||
StatusBadge,
|
||||
Text,
|
||||
} from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
@@ -22,6 +24,8 @@ import {
|
||||
getStylizedAmount,
|
||||
} from "../../../../../lib/money-amount-helpers"
|
||||
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
||||
import { useReturns } from "../../../../../hooks/api/returns"
|
||||
import { useDate } from "../../../../../hooks/use-date"
|
||||
|
||||
type OrderSummarySectionProps = {
|
||||
order: AdminOrder
|
||||
@@ -67,6 +71,7 @@ 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} />
|
||||
|
||||
@@ -104,11 +109,11 @@ const Header = ({ order }: { order: AdminOrder }) => {
|
||||
// to: "#", // TODO: Open modal to allocate items
|
||||
// icon: <Buildings />,
|
||||
// },
|
||||
// {
|
||||
// label: t("orders.summary.requestReturn"),
|
||||
// to: `/orders/${order.id}/returns`,
|
||||
// icon: <ArrowUturnLeft />,
|
||||
// },
|
||||
{
|
||||
label: t("orders.returns.create"),
|
||||
to: `/orders/${order.id}/returns`,
|
||||
icon: <ArrowUturnLeft />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
@@ -270,6 +275,41 @@ const CostBreakdown = ({ order }: { order: AdminOrder }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const ReturnBreakdown = ({ order }: { order: AdminOrder }) => {
|
||||
const { t } = useTranslation()
|
||||
const { getRelativeDate } = useDate()
|
||||
|
||||
const { returns = [] } = useReturns({
|
||||
order_id: order.id,
|
||||
status: "requested",
|
||||
fields: "*items",
|
||||
})
|
||||
|
||||
if (!returns.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return returns.map((activeReturn) => (
|
||||
<div
|
||||
key={activeReturn.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,
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||
{getRelativeDate(activeReturn.created_at)}
|
||||
</Text>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
const Total = ({ order }: { order: AdminOrder }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
@@ -11,12 +11,16 @@ const DEFAULT_PROPERTIES = [
|
||||
"subtotal",
|
||||
"discounts_total",
|
||||
"shipping_total",
|
||||
"shipping_tax_total",
|
||||
"tax_total",
|
||||
"refundable_total",
|
||||
]
|
||||
|
||||
const DEFAULT_RELATIONS = [
|
||||
"*customer",
|
||||
"*items", // -> we get LineItem here with added `quantity` and `detail` which is actually an OrderItem (which is a parent object to LineItem in the DB)
|
||||
"*items.variant",
|
||||
"*items.variant.product",
|
||||
"*items.variant.options",
|
||||
"+items.variant.manage_inventory",
|
||||
"*shipping_address",
|
||||
|
||||
@@ -25,6 +25,7 @@ import { TaxRate } from "./tax-rate"
|
||||
import { TaxRegion } from "./tax-region"
|
||||
import { Upload } from "./upload"
|
||||
import { User } from "./user"
|
||||
import { ReturnReason } from "./return-reason"
|
||||
|
||||
export class Admin {
|
||||
public invite: Invite
|
||||
@@ -37,6 +38,7 @@ export class Admin {
|
||||
public productType: ProductType
|
||||
public upload: Upload
|
||||
public region: Region
|
||||
public returnReason: ReturnReason
|
||||
public stockLocation: StockLocation
|
||||
public salesChannel: SalesChannel
|
||||
public fulfillmentSet: FulfillmentSet
|
||||
@@ -47,12 +49,12 @@ export class Admin {
|
||||
public inventoryItem: InventoryItem
|
||||
public notification: Notification
|
||||
public order: Order
|
||||
public return: Return
|
||||
public taxRate: TaxRate
|
||||
public taxRegion: TaxRegion
|
||||
public store: Store
|
||||
public productTag: ProductTag
|
||||
public user: User
|
||||
public return: Return
|
||||
|
||||
constructor(client: Client) {
|
||||
this.invite = new Invite(client)
|
||||
@@ -65,6 +67,7 @@ export class Admin {
|
||||
this.productType = new ProductType(client)
|
||||
this.upload = new Upload(client)
|
||||
this.region = new Region(client)
|
||||
this.returnReason = new ReturnReason(client)
|
||||
this.stockLocation = new StockLocation(client)
|
||||
this.salesChannel = new SalesChannel(client)
|
||||
this.fulfillmentSet = new FulfillmentSet(client)
|
||||
@@ -75,11 +78,11 @@ export class Admin {
|
||||
this.inventoryItem = new InventoryItem(client)
|
||||
this.notification = new Notification(client)
|
||||
this.order = new Order(client)
|
||||
this.return = new Return(client)
|
||||
this.taxRate = new TaxRate(client)
|
||||
this.taxRegion = new TaxRegion(client)
|
||||
this.store = new Store(client)
|
||||
this.productTag = new ProductTag(client)
|
||||
this.user = new User(client)
|
||||
this.return = new Return(client)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
AdminCreateOrderShipment,
|
||||
FindParams,
|
||||
HttpTypes,
|
||||
PaginatedResponse,
|
||||
@@ -24,6 +23,15 @@ export class Order {
|
||||
)
|
||||
}
|
||||
|
||||
async retrievePreview(id: string, headers?: ClientHeaders) {
|
||||
return await this.client.fetch<{ order: HttpTypes.AdminOrder }>(
|
||||
`/admin/orders/${id}/preview`,
|
||||
{
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async list(
|
||||
queryParams?: FindParams & HttpTypes.AdminOrderFilters,
|
||||
headers?: ClientHeaders
|
||||
|
||||
24
packages/core/js-sdk/src/admin/return-reason.ts
Normal file
24
packages/core/js-sdk/src/admin/return-reason.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
import { Client } from "../client"
|
||||
import { ClientHeaders } from "../types"
|
||||
|
||||
export class ReturnReason {
|
||||
private client: Client
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async list(
|
||||
queryParams?: HttpTypes.AdminReturnReasonListParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminReturnReasonsResponse>(
|
||||
"/admin/return-reasons",
|
||||
{
|
||||
headers,
|
||||
query: queryParams,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { FindParams, HttpTypes, SelectParams } from "@medusajs/types"
|
||||
|
||||
import { Client } from "../client"
|
||||
import { ClientHeaders } from "../types"
|
||||
|
||||
@@ -8,6 +9,26 @@ export class Return {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async list(query?: HttpTypes.AdminReturnFilters, headers?: ClientHeaders) {
|
||||
return await this.client.fetch<HttpTypes.AdminReturnsResponse>(
|
||||
`/admin/returns`,
|
||||
{
|
||||
query,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async retrieve(id: string, query?: SelectParams, headers?: ClientHeaders) {
|
||||
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
|
||||
`/admin/returns/${id}`,
|
||||
{
|
||||
query,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async initiateRequest(
|
||||
body: HttpTypes.AdminInitiateReturnRequest,
|
||||
query?: HttpTypes.SelectParams,
|
||||
@@ -24,6 +45,21 @@ export class Return {
|
||||
)
|
||||
}
|
||||
|
||||
async cancelRequest(
|
||||
id: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
|
||||
`/admin/returns/${id}/request`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async addReturnItem(
|
||||
id: string,
|
||||
body: HttpTypes.AdminAddReturnItems,
|
||||
@@ -41,6 +77,40 @@ export class Return {
|
||||
)
|
||||
}
|
||||
|
||||
async updateReturnItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
body: HttpTypes.AdminUpdateReturnItems,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
|
||||
`/admin/returns/${id}/request-items/${actionId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async removeReturnItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
|
||||
`/admin/returns/${id}/request-items/${actionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async addReturnShipping(
|
||||
id: string,
|
||||
body: HttpTypes.AdminAddReturnShipping,
|
||||
@@ -58,6 +128,40 @@ export class Return {
|
||||
)
|
||||
}
|
||||
|
||||
async updateReturnShipping(
|
||||
id: string,
|
||||
actionId: string,
|
||||
body: HttpTypes.AdminAddReturnShipping,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
|
||||
`/admin/returns/${id}/shipping-method/${actionId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async deleteReturnShipping(
|
||||
id: string,
|
||||
actionId: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
|
||||
`/admin/returns/${id}/shipping-method/${actionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async confirmRequest(
|
||||
id: string,
|
||||
body: HttpTypes.AdminConfirmReturnRequest,
|
||||
|
||||
@@ -23,6 +23,8 @@ export * from "./product-tag"
|
||||
export * from "./product-type"
|
||||
export * from "./promotion"
|
||||
export * from "./region"
|
||||
export * from "./return"
|
||||
export * from "./return-reason"
|
||||
export * from "./reservation"
|
||||
export * from "./return"
|
||||
export * from "./sales-channel"
|
||||
|
||||
@@ -12,6 +12,14 @@ export interface AdminOrderFilters extends BaseOrderFilters {}
|
||||
export interface AdminOrderAddress extends BaseOrderAddress {}
|
||||
export interface AdminOrderShippingMethod extends BaseOrderShippingMethod {}
|
||||
|
||||
export interface AdminOrderResponse {
|
||||
order: AdminOrder
|
||||
}
|
||||
|
||||
export interface AdminOrdersResponse {
|
||||
orders: AdminOrder[]
|
||||
}
|
||||
|
||||
export interface AdminCreateOrderFulfillment {
|
||||
items: { id: string; quantity: number }[]
|
||||
location_id?: string
|
||||
|
||||
28
packages/core/types/src/http/return-reason/admin.ts
Normal file
28
packages/core/types/src/http/return-reason/admin.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { BaseReturnReason } from "./common"
|
||||
import { FindParams } from "../common"
|
||||
import { BaseFilterable, OperatorMap } from "../../dal"
|
||||
|
||||
export interface AdminReturnReason extends BaseReturnReason {}
|
||||
|
||||
export interface AdminCreateReturnReason {
|
||||
// TODO:
|
||||
value: string
|
||||
label: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface AdminReturnReasonsResponse {
|
||||
return_reasons: AdminReturnReason[]
|
||||
}
|
||||
|
||||
export interface AdminReturnReasonListParams
|
||||
extends FindParams,
|
||||
BaseFilterable<AdminReturnReasonListParams> {
|
||||
id?: string[] | string | OperatorMap<string | string[]>
|
||||
value?: string | OperatorMap<string>
|
||||
label?: string | OperatorMap<string>
|
||||
description?: string | OperatorMap<string>
|
||||
parent_return_reason_id?: string | OperatorMap<string | string[]>
|
||||
created_at?: OperatorMap<string>
|
||||
updated_at?: OperatorMap<string>
|
||||
}
|
||||
9
packages/core/types/src/http/return-reason/common.ts
Normal file
9
packages/core/types/src/http/return-reason/common.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface BaseReturnReason {
|
||||
id: string
|
||||
value: string
|
||||
label: string
|
||||
description?: string | null
|
||||
metadata?: Record<string, any> | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
1
packages/core/types/src/http/return-reason/index.ts
Normal file
1
packages/core/types/src/http/return-reason/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./admin"
|
||||
@@ -1,4 +1,7 @@
|
||||
export interface BaseReturnItem {
|
||||
import { OperatorMap } from "../../dal"
|
||||
import { FindParams } from "../common"
|
||||
|
||||
export interface AdminBaseReturnItem {
|
||||
id: string
|
||||
quantity: number
|
||||
received_quantity: number
|
||||
@@ -9,7 +12,7 @@ export interface BaseReturnItem {
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminReturnResponse {
|
||||
export interface AdminReturn {
|
||||
id: string
|
||||
order_id: string
|
||||
status?: string
|
||||
@@ -19,7 +22,15 @@ export interface AdminReturnResponse {
|
||||
display_id: number
|
||||
no_notification?: boolean
|
||||
refund_amount?: number
|
||||
items: BaseReturnItem[]
|
||||
items: AdminBaseReturnItem[]
|
||||
}
|
||||
|
||||
export interface AdminReturnResponse {
|
||||
return: AdminReturn
|
||||
}
|
||||
|
||||
export interface AdminReturnsResponse {
|
||||
returns: AdminReturn[]
|
||||
}
|
||||
|
||||
export interface AdminInitiateReturnRequest {
|
||||
@@ -42,6 +53,13 @@ export interface AdminAddReturnItem {
|
||||
export interface AdminAddReturnItems {
|
||||
items: AdminAddReturnItem[]
|
||||
}
|
||||
|
||||
export interface AdminUpdateReturnItems {
|
||||
quantity?: number
|
||||
internal_note?: string
|
||||
reason_id?: string
|
||||
}
|
||||
|
||||
export interface AdminAddReturnShipping {
|
||||
shipping_option_id: string
|
||||
custom_price?: number
|
||||
@@ -49,6 +67,25 @@ export interface AdminAddReturnShipping {
|
||||
internal_note?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminUpdateReturnShipping {
|
||||
custom_price?: number
|
||||
internal_note?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminConfirmReturnRequest {
|
||||
no_notification?: boolean
|
||||
}
|
||||
|
||||
export interface AdminReturnFilters extends FindParams {
|
||||
id?: string[] | string | OperatorMap<string | string[]>
|
||||
order_id?: string[] | string | OperatorMap<string | string[]>
|
||||
status?:
|
||||
| string[]
|
||||
| string
|
||||
| Record<string, unknown>
|
||||
| OperatorMap<Record<string, unknown>>
|
||||
created_at?: OperatorMap<string>
|
||||
updated_at?: OperatorMap<string>
|
||||
}
|
||||
|
||||
@@ -450,6 +450,7 @@ export interface UpdateReturnDTO {
|
||||
internal_note?: string | null
|
||||
note?: string | null
|
||||
reason_id?: string | null
|
||||
return_id?: string | null
|
||||
metadata?: Record<string, unknown> | null
|
||||
}[]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AdminGetOrdersParams,
|
||||
AdminPostReceiveReturnItemsReqSchema,
|
||||
AdminPostReceiveReturnsReqSchema,
|
||||
AdminPostCancelReturnReqSchema,
|
||||
AdminPostReturnsConfirmRequestReqSchema,
|
||||
AdminPostReturnsReqSchema,
|
||||
AdminPostReturnsRequestItemsActionReqSchema,
|
||||
@@ -122,6 +123,17 @@ export const adminReturnRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/returns/:id/cancel",
|
||||
middlewares: [
|
||||
validateAndTransformBody(AdminPostCancelReturnReqSchema),
|
||||
validateAndTransformQuery(
|
||||
AdminGetOrdersOrderParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["DELETE"],
|
||||
matcher: "/admin/returns/:id/request",
|
||||
|
||||
@@ -94,10 +94,10 @@ export type AdminPostReceiveReturnItemsReqSchemaType = z.infer<
|
||||
>
|
||||
|
||||
export const AdminPostCancelReturnReqSchema = z.object({
|
||||
return_id: z.string(),
|
||||
no_notification: z.boolean().optional(),
|
||||
internal_note: z.string().nullish(),
|
||||
})
|
||||
|
||||
export type AdminPostCancelReturnReqSchemaType = z.infer<
|
||||
typeof AdminPostCancelReturnReqSchema
|
||||
>
|
||||
|
||||
@@ -67,6 +67,7 @@ export const config: MiddlewaresConfig = {
|
||||
...storeOrderRoutesMiddlewares,
|
||||
...authRoutesMiddlewares,
|
||||
...adminWorkflowsExecutionsMiddlewares,
|
||||
...adminReturnRoutesMiddlewares,
|
||||
...storeRegionRoutesMiddlewares,
|
||||
...adminRegionRoutesMiddlewares,
|
||||
...adminReturnRoutesMiddlewares,
|
||||
@@ -97,7 +98,6 @@ export const config: MiddlewaresConfig = {
|
||||
...adminOrderRoutesMiddlewares,
|
||||
...adminReservationRoutesMiddlewares,
|
||||
...adminProductCategoryRoutesMiddlewares,
|
||||
...adminReservationRoutesMiddlewares,
|
||||
...adminShippingProfilesMiddlewares,
|
||||
...adminFulfillmentsRoutesMiddlewares,
|
||||
...adminFulfillmentProvidersRoutesMiddlewares,
|
||||
|
||||
@@ -129,15 +129,17 @@ export class Migration20240604100512 extends Migration {
|
||||
exchange_id
|
||||
)
|
||||
WHERE exchange_id IS NOT NULL AND deleted_at IS NULL;
|
||||
|
||||
|
||||
|
||||
CREATE TYPE return_status_enum AS ENUM (
|
||||
'requested',
|
||||
'received',
|
||||
'partially_received',
|
||||
'canceled'
|
||||
);
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'return_status_enum') THEN
|
||||
CREATE TYPE return_status_enum AS ENUM (
|
||||
'requested',
|
||||
'received',
|
||||
'partially_received',
|
||||
'canceled');
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "return" (
|
||||
"id" TEXT NOT NULL,
|
||||
@@ -265,12 +267,15 @@ export class Migration20240604100512 extends Migration {
|
||||
CREATE INDEX IF NOT EXISTS "IDX_order_exchange_item_item_id" ON "order_exchange_item" ("item_id")
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
|
||||
|
||||
CREATE TYPE order_claim_type_enum AS ENUM (
|
||||
'refund',
|
||||
'replace'
|
||||
);
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'order_claim_type_enum') THEN
|
||||
CREATE TYPE order_claim_type_enum AS ENUM (
|
||||
'refund',
|
||||
'replace'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "order_claim" (
|
||||
"id" TEXT NOT NULL,
|
||||
@@ -302,14 +307,17 @@ export class Migration20240604100512 extends Migration {
|
||||
CREATE INDEX IF NOT EXISTS "IDX_order_claim_return_id" ON "order_claim" ("return_id")
|
||||
WHERE return_id IS NOT NULL AND deleted_at IS NULL;
|
||||
|
||||
|
||||
|
||||
CREATE TYPE claim_reason_enum AS ENUM (
|
||||
'missing_item',
|
||||
'wrong_item',
|
||||
'production_failure',
|
||||
'other'
|
||||
);
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'claim_reason_enum') THEN
|
||||
CREATE TYPE claim_reason_enum AS ENUM (
|
||||
'missing_item',
|
||||
'wrong_item',
|
||||
'production_failure',
|
||||
'other'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "order_claim_item" (
|
||||
"id" TEXT NOT NULL,
|
||||
@@ -337,7 +345,6 @@ export class Migration20240604100512 extends Migration {
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "order_claim_item_image" (
|
||||
"id" TEXT NOT NULL,
|
||||
"claim_item_id" TEXT NOT NULL,
|
||||
|
||||
Reference in New Issue
Block a user