feat(dashboard, order, medusa, types, js-sdk): Request return e2e flow (#7848)

This commit is contained in:
Frane Polić
2024-07-24 19:19:00 +02:00
committed by GitHub
parent 0a482e972f
commit f7d1cd259e
36 changed files with 2251 additions and 146 deletions
@@ -14,7 +14,7 @@ export const LinkButton = ({
return ( return (
<Link <Link
className={clx( 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", "focus-visible:shadow-borders-focus",
{ {
"text-ui-fg-interactive hover:text-ui-fg-interactive-hover": "text-ui-fg-interactive hover:text-ui-fg-interactive-hover":
@@ -9,10 +9,16 @@ import {
import { queryClient } from "../../lib/query-client" import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory" import { queryKeysFactory } from "../../lib/query-key-factory"
import { sdk } from "../../lib/client" import { sdk } from "../../lib/client"
import { AdminCreateOrderShipment, HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
const ORDERS_QUERY_KEY = "orders" as const 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 = ( export const useOrder = (
id: string, id: string,
@@ -31,6 +37,22 @@ export const useOrder = (
return { ...data, ...rest } 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 = ( export const useOrders = (
query?: Record<string, any>, query?: Record<string, any>,
options?: Omit< options?: Omit<
@@ -49,10 +71,14 @@ export const useOrders = (
export const useCreateOrderFulfillment = ( export const useCreateOrderFulfillment = (
orderId: string, orderId: string,
options?: UseMutationOptions<any, Error, any> options?: UseMutationOptions<
HttpTypes.AdminOrderResponse,
Error,
HttpTypes.AdminCreateOrderFulfillment
>
) => { ) => {
return useMutation({ return useMutation({
mutationFn: (payload: any) => mutationFn: (payload: HttpTypes.AdminCreateOrderFulfillment) =>
sdk.admin.order.createFulfillment(orderId, payload), sdk.admin.order.createFulfillment(orderId, payload),
onSuccess: (data: any, variables: any, context: any) => { onSuccess: (data: any, variables: any, context: any) => {
queryClient.invalidateQueries({ 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 }
}
@@ -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", "close": "Close",
"showMore": "Show more", "showMore": "Show more",
"continue": "Continue", "continue": "Continue",
"addReason": "Add Reason",
"addNote": "Add Note",
"reset": "Reset", "reset": "Reset",
"confirm": "Confirm", "confirm": "Confirm",
"edit": "Edit", "edit": "Edit",
"addItems": "Add items",
"download": "Download", "download": "Download",
"clearAll": "Clear all", "clearAll": "Clear all",
"apply": "Apply", "apply": "Apply",
@@ -772,6 +775,7 @@
"requiresAction": "Requires action" "requiresAction": "Requires action"
} }
}, },
"edits": { "edits": {
"title": "Edit order", "title": "Edit order",
"currentItems": "Current items", "currentItems": "Current items",
@@ -783,26 +787,25 @@
"differenceDue": "Difference due" "differenceDue": "Difference due"
}, },
"returns": { "returns": {
"details": "Details", "create": "Create Return",
"chooseItems": "Choose items", "inbound": "Inbound",
"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",
"sendNotification": "Send notification", "sendNotification": "Send notification",
"sendNotificationHint": "Notify customer of created return.", "sendNotificationHint": "Notify customer about return.",
"customRefund": "Custom refund", "returnTotal": "Return total",
"shippingPriceTooltip1": "Custom refund is enabled", "refundAmount": "Refund amount",
"noShippingOptions": "There are no shipping options for the region", "reason": "Reason",
"shippingPriceTooltip2": "Shipping needs to be selected", "reasonHint": "Choose why the customer want to return items.",
"customRefundHint": "If you want to refund something else instead of the total refund.", "note": "Note",
"customShippingPrice": "Custom shipping", "noInventoryLevel": "No inventory level",
"customShippingPriceHint": "Custom shipping cost.",
"noInventoryLevelDesc": "The selected location does not have an inventory level for the selected items. The return can be requested but cant be received until an inventory level is created for the selected location.", "noInventoryLevelDesc": "The selected location does not have an inventory level for the selected items. The return can be requested but cant be received until an inventory level is created for the selected location.",
"refundableAmountLabel": "Refundable amount", "noteHint": "You can type freely if you want to specify something.",
"refundableAmountHeader": "Refundable Amount", "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", "returnableQuantityLabel": "Returnable quantity",
"returnableQuantityHeader": "Returnable Quantity" "refundableAmountLabel": "Refundable amount",
"returnRequestedInfo": "{{requestedItemsCount}}x item return requested"
}, },
"reservations": { "reservations": {
"allocatedLabel": "Allocated", "allocatedLabel": "Allocated",
@@ -922,7 +925,9 @@
"items_other": "{{count}} items" "items_other": "{{count}} items"
}, },
"return": { "return": {
"created": "Return created" "created": "Return created",
"items_one": "{{count}} item returned",
"items_other": "{{count}} items returned"
}, },
"note": { "note": {
"comment": "Comment", "comment": "Comment",
@@ -2256,6 +2261,7 @@
"inStock": "In stock", "inStock": "In stock",
"location": "Location", "location": "Location",
"quantity": "Quantity", "quantity": "Quantity",
"qty": "Qty",
"variant": "Variant", "variant": "Variant",
"id": "ID", "id": "ID",
"parent": "Parent", "parent": "Parent",
+12 -72
View File
@@ -1,75 +1,15 @@
import { ClaimItem, LineItem, Order } from "@medusajs/medusa" import { AdminOrderLineItem } from "@medusajs/types"
/** export function getReturnableQuantity(item: AdminOrderLineItem): number {
* Return line items that are returnable from an order const {
* @param order // 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
* @param isClaim shipped_quantity,
*/ return_received_quantity,
export const getAllReturnableItems = ( return_dismissed_quantity, // TODO: check this
order: Omit<Order, "beforeInserts">, return_requested_quantity,
isClaim: boolean } = item.detail
) => {
let orderItems = order.items.reduce( return (
(map, obj) => shipped_quantity - (return_received_quantity + return_requested_quantity)
map.set(obj.id, {
...obj,
}),
new Map<string, Omit<LineItem, "beforeInsert">>()
) )
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: () => lazy: () =>
import("../../routes/orders/order-create-shipment"), 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 { Link } from "react-router-dom"
import { XMarkMini } from "@medusajs/icons" import { XMarkMini } from "@medusajs/icons"
import { AdminFulfillment, AdminOrder } from "@medusajs/types" import { AdminFulfillment, AdminOrder, AdminReturn } from "@medusajs/types"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useDate } from "../../../../../hooks/use-date" import { useDate } from "../../../../../hooks/use-date"
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
import { useReturns } from "../../../../../hooks/api/returns"
type OrderTimelineProps = { type OrderTimelineProps = {
order: AdminOrder order: AdminOrder
@@ -80,6 +81,8 @@ type Activity = {
const useActivityItems = (order: AdminOrder) => { const useActivityItems = (order: AdminOrder) => {
const { t } = useTranslation() const { t } = useTranslation()
const { returns = [] } = useReturns({ order_id: order.id, fields: "*items" })
const notes = [] const notes = []
const isLoading = false const isLoading = false
// const { notes, isLoading, isError, error } = useNotes( // const { notes, isLoading, isError, error } = useNotes(
@@ -165,16 +168,13 @@ const useActivityItems = (order: AdminOrder) => {
} }
} }
/** for (const ret of returns) {
* TODO: revisit when API is fixed to fetch returns of an order items.push({
*/ title: t("orders.activity.events.return.created"),
timestamp: ret.created_at,
// for (const ret of order.returns) { children: <ReturnCreatedBody orderReturn={ret} />,
// items.push({ })
// title: t("orders.activity.events.return.created"), }
// timestamp: ret.created_at,
// })
// }
// for (const note of notes || []) { // for (const note of notes || []) {
// items.push({ // items.push({
@@ -391,3 +391,21 @@ const FulfillmentCreatedBody = ({
</div> </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 { import {
AdminOrder, AdminOrder,
OrderLineItemDTO, OrderLineItemDTO,
ReservationItemDTO, ReservationItemDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { ArrowDownRightMini, ArrowUturnLeft } from "@medusajs/icons"
import { import {
Button, Button,
Container, Container,
@@ -11,9 +16,6 @@ import {
StatusBadge, StatusBadge,
Text, Text,
} from "@medusajs/ui" } 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 { ActionMenu } from "../../../../../components/common/action-menu"
import { Thumbnail } from "../../../../../components/common/thumbnail" import { Thumbnail } from "../../../../../components/common/thumbnail"
@@ -22,6 +24,8 @@ import {
getStylizedAmount, getStylizedAmount,
} from "../../../../../lib/money-amount-helpers" } from "../../../../../lib/money-amount-helpers"
import { useReservationItems } from "../../../../../hooks/api/reservations" import { useReservationItems } from "../../../../../hooks/api/reservations"
import { useReturns } from "../../../../../hooks/api/returns"
import { useDate } from "../../../../../hooks/use-date"
type OrderSummarySectionProps = { type OrderSummarySectionProps = {
order: AdminOrder order: AdminOrder
@@ -67,6 +71,7 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
<Container className="divide-y divide-dashed p-0"> <Container className="divide-y divide-dashed p-0">
<Header order={order} /> <Header order={order} />
<ItemBreakdown order={order} /> <ItemBreakdown order={order} />
<ReturnBreakdown order={order} />
<CostBreakdown order={order} /> <CostBreakdown order={order} />
<Total order={order} /> <Total order={order} />
@@ -104,11 +109,11 @@ const Header = ({ order }: { order: AdminOrder }) => {
// to: "#", // TODO: Open modal to allocate items // to: "#", // TODO: Open modal to allocate items
// icon: <Buildings />, // icon: <Buildings />,
// }, // },
// { {
// label: t("orders.summary.requestReturn"), label: t("orders.returns.create"),
// to: `/orders/${order.id}/returns`, to: `/orders/${order.id}/returns`,
// icon: <ArrowUturnLeft />, 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 Total = ({ order }: { order: AdminOrder }) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -11,12 +11,16 @@ const DEFAULT_PROPERTIES = [
"subtotal", "subtotal",
"discounts_total", "discounts_total",
"shipping_total", "shipping_total",
"shipping_tax_total",
"tax_total", "tax_total",
"refundable_total",
] ]
const DEFAULT_RELATIONS = [ const DEFAULT_RELATIONS = [
"*customer", "*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", // -> 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.options",
"+items.variant.manage_inventory", "+items.variant.manage_inventory",
"*shipping_address", "*shipping_address",
+5 -2
View File
@@ -25,6 +25,7 @@ import { TaxRate } from "./tax-rate"
import { TaxRegion } from "./tax-region" import { TaxRegion } from "./tax-region"
import { Upload } from "./upload" import { Upload } from "./upload"
import { User } from "./user" import { User } from "./user"
import { ReturnReason } from "./return-reason"
export class Admin { export class Admin {
public invite: Invite public invite: Invite
@@ -37,6 +38,7 @@ export class Admin {
public productType: ProductType public productType: ProductType
public upload: Upload public upload: Upload
public region: Region public region: Region
public returnReason: ReturnReason
public stockLocation: StockLocation public stockLocation: StockLocation
public salesChannel: SalesChannel public salesChannel: SalesChannel
public fulfillmentSet: FulfillmentSet public fulfillmentSet: FulfillmentSet
@@ -47,12 +49,12 @@ export class Admin {
public inventoryItem: InventoryItem public inventoryItem: InventoryItem
public notification: Notification public notification: Notification
public order: Order public order: Order
public return: Return
public taxRate: TaxRate public taxRate: TaxRate
public taxRegion: TaxRegion public taxRegion: TaxRegion
public store: Store public store: Store
public productTag: ProductTag public productTag: ProductTag
public user: User public user: User
public return: Return
constructor(client: Client) { constructor(client: Client) {
this.invite = new Invite(client) this.invite = new Invite(client)
@@ -65,6 +67,7 @@ export class Admin {
this.productType = new ProductType(client) this.productType = new ProductType(client)
this.upload = new Upload(client) this.upload = new Upload(client)
this.region = new Region(client) this.region = new Region(client)
this.returnReason = new ReturnReason(client)
this.stockLocation = new StockLocation(client) this.stockLocation = new StockLocation(client)
this.salesChannel = new SalesChannel(client) this.salesChannel = new SalesChannel(client)
this.fulfillmentSet = new FulfillmentSet(client) this.fulfillmentSet = new FulfillmentSet(client)
@@ -75,11 +78,11 @@ export class Admin {
this.inventoryItem = new InventoryItem(client) this.inventoryItem = new InventoryItem(client)
this.notification = new Notification(client) this.notification = new Notification(client)
this.order = new Order(client) this.order = new Order(client)
this.return = new Return(client)
this.taxRate = new TaxRate(client) this.taxRate = new TaxRate(client)
this.taxRegion = new TaxRegion(client) this.taxRegion = new TaxRegion(client)
this.store = new Store(client) this.store = new Store(client)
this.productTag = new ProductTag(client) this.productTag = new ProductTag(client)
this.user = new User(client) this.user = new User(client)
this.return = new Return(client)
} }
} }
+9 -1
View File
@@ -1,5 +1,4 @@
import { import {
AdminCreateOrderShipment,
FindParams, FindParams,
HttpTypes, HttpTypes,
PaginatedResponse, 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( async list(
queryParams?: FindParams & HttpTypes.AdminOrderFilters, queryParams?: FindParams & HttpTypes.AdminOrderFilters,
headers?: ClientHeaders headers?: ClientHeaders
@@ -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,
}
)
}
}
+105 -1
View File
@@ -1,4 +1,5 @@
import { HttpTypes } from "@medusajs/types" import { FindParams, HttpTypes, SelectParams } from "@medusajs/types"
import { Client } from "../client" import { Client } from "../client"
import { ClientHeaders } from "../types" import { ClientHeaders } from "../types"
@@ -8,6 +9,26 @@ export class Return {
this.client = client 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( async initiateRequest(
body: HttpTypes.AdminInitiateReturnRequest, body: HttpTypes.AdminInitiateReturnRequest,
query?: HttpTypes.SelectParams, 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( async addReturnItem(
id: string, id: string,
body: HttpTypes.AdminAddReturnItems, 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( async addReturnShipping(
id: string, id: string,
body: HttpTypes.AdminAddReturnShipping, 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( async confirmRequest(
id: string, id: string,
body: HttpTypes.AdminConfirmReturnRequest, body: HttpTypes.AdminConfirmReturnRequest,
+2
View File
@@ -23,6 +23,8 @@ export * from "./product-tag"
export * from "./product-type" export * from "./product-type"
export * from "./promotion" export * from "./promotion"
export * from "./region" export * from "./region"
export * from "./return"
export * from "./return-reason"
export * from "./reservation" export * from "./reservation"
export * from "./return" export * from "./return"
export * from "./sales-channel" export * from "./sales-channel"
@@ -12,6 +12,14 @@ export interface AdminOrderFilters extends BaseOrderFilters {}
export interface AdminOrderAddress extends BaseOrderAddress {} export interface AdminOrderAddress extends BaseOrderAddress {}
export interface AdminOrderShippingMethod extends BaseOrderShippingMethod {} export interface AdminOrderShippingMethod extends BaseOrderShippingMethod {}
export interface AdminOrderResponse {
order: AdminOrder
}
export interface AdminOrdersResponse {
orders: AdminOrder[]
}
export interface AdminCreateOrderFulfillment { export interface AdminCreateOrderFulfillment {
items: { id: string; quantity: number }[] items: { id: string; quantity: number }[]
location_id?: string location_id?: string
@@ -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>
}
@@ -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
}
@@ -0,0 +1 @@
export * from "./admin"
+40 -3
View File
@@ -1,4 +1,7 @@
export interface BaseReturnItem { import { OperatorMap } from "../../dal"
import { FindParams } from "../common"
export interface AdminBaseReturnItem {
id: string id: string
quantity: number quantity: number
received_quantity: number received_quantity: number
@@ -9,7 +12,7 @@ export interface BaseReturnItem {
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
} }
export interface AdminReturnResponse { export interface AdminReturn {
id: string id: string
order_id: string order_id: string
status?: string status?: string
@@ -19,7 +22,15 @@ export interface AdminReturnResponse {
display_id: number display_id: number
no_notification?: boolean no_notification?: boolean
refund_amount?: number refund_amount?: number
items: BaseReturnItem[] items: AdminBaseReturnItem[]
}
export interface AdminReturnResponse {
return: AdminReturn
}
export interface AdminReturnsResponse {
returns: AdminReturn[]
} }
export interface AdminInitiateReturnRequest { export interface AdminInitiateReturnRequest {
@@ -42,6 +53,13 @@ export interface AdminAddReturnItem {
export interface AdminAddReturnItems { export interface AdminAddReturnItems {
items: AdminAddReturnItem[] items: AdminAddReturnItem[]
} }
export interface AdminUpdateReturnItems {
quantity?: number
internal_note?: string
reason_id?: string
}
export interface AdminAddReturnShipping { export interface AdminAddReturnShipping {
shipping_option_id: string shipping_option_id: string
custom_price?: number custom_price?: number
@@ -49,6 +67,25 @@ export interface AdminAddReturnShipping {
internal_note?: string internal_note?: string
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
} }
export interface AdminUpdateReturnShipping {
custom_price?: number
internal_note?: string
metadata?: Record<string, unknown>
}
export interface AdminConfirmReturnRequest { export interface AdminConfirmReturnRequest {
no_notification?: boolean 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 internal_note?: string | null
note?: string | null note?: string | null
reason_id?: string | null reason_id?: string | null
return_id?: string | null
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
}[] }[]
} }
@@ -7,6 +7,7 @@ import {
AdminGetOrdersParams, AdminGetOrdersParams,
AdminPostReceiveReturnItemsReqSchema, AdminPostReceiveReturnItemsReqSchema,
AdminPostReceiveReturnsReqSchema, AdminPostReceiveReturnsReqSchema,
AdminPostCancelReturnReqSchema,
AdminPostReturnsConfirmRequestReqSchema, AdminPostReturnsConfirmRequestReqSchema,
AdminPostReturnsReqSchema, AdminPostReturnsReqSchema,
AdminPostReturnsRequestItemsActionReqSchema, 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"], method: ["DELETE"],
matcher: "/admin/returns/:id/request", matcher: "/admin/returns/:id/request",
@@ -94,10 +94,10 @@ export type AdminPostReceiveReturnItemsReqSchemaType = z.infer<
> >
export const AdminPostCancelReturnReqSchema = z.object({ export const AdminPostCancelReturnReqSchema = z.object({
return_id: z.string(),
no_notification: z.boolean().optional(), no_notification: z.boolean().optional(),
internal_note: z.string().nullish(), internal_note: z.string().nullish(),
}) })
export type AdminPostCancelReturnReqSchemaType = z.infer< export type AdminPostCancelReturnReqSchemaType = z.infer<
typeof AdminPostCancelReturnReqSchema typeof AdminPostCancelReturnReqSchema
> >
+1 -1
View File
@@ -67,6 +67,7 @@ export const config: MiddlewaresConfig = {
...storeOrderRoutesMiddlewares, ...storeOrderRoutesMiddlewares,
...authRoutesMiddlewares, ...authRoutesMiddlewares,
...adminWorkflowsExecutionsMiddlewares, ...adminWorkflowsExecutionsMiddlewares,
...adminReturnRoutesMiddlewares,
...storeRegionRoutesMiddlewares, ...storeRegionRoutesMiddlewares,
...adminRegionRoutesMiddlewares, ...adminRegionRoutesMiddlewares,
...adminReturnRoutesMiddlewares, ...adminReturnRoutesMiddlewares,
@@ -97,7 +98,6 @@ export const config: MiddlewaresConfig = {
...adminOrderRoutesMiddlewares, ...adminOrderRoutesMiddlewares,
...adminReservationRoutesMiddlewares, ...adminReservationRoutesMiddlewares,
...adminProductCategoryRoutesMiddlewares, ...adminProductCategoryRoutesMiddlewares,
...adminReservationRoutesMiddlewares,
...adminShippingProfilesMiddlewares, ...adminShippingProfilesMiddlewares,
...adminFulfillmentsRoutesMiddlewares, ...adminFulfillmentsRoutesMiddlewares,
...adminFulfillmentProvidersRoutesMiddlewares, ...adminFulfillmentProvidersRoutesMiddlewares,
@@ -129,15 +129,17 @@ export class Migration20240604100512 extends Migration {
exchange_id exchange_id
) )
WHERE exchange_id IS NOT NULL AND deleted_at IS NULL; WHERE exchange_id IS NOT NULL AND deleted_at IS NULL;
CREATE TYPE return_status_enum AS ENUM ( DO $$
'requested', BEGIN
'received', IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'return_status_enum') THEN
'partially_received', CREATE TYPE return_status_enum AS ENUM (
'canceled' 'requested',
); 'received',
'partially_received',
'canceled');
END IF;
END$$;
CREATE TABLE IF NOT EXISTS "return" ( CREATE TABLE IF NOT EXISTS "return" (
"id" TEXT NOT NULL, "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") CREATE INDEX IF NOT EXISTS "IDX_order_exchange_item_item_id" ON "order_exchange_item" ("item_id")
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
DO $$
BEGIN
CREATE TYPE order_claim_type_enum AS ENUM ( IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'order_claim_type_enum') THEN
'refund', CREATE TYPE order_claim_type_enum AS ENUM (
'replace' 'refund',
); 'replace'
);
END IF;
END$$;
CREATE TABLE IF NOT EXISTS "order_claim" ( CREATE TABLE IF NOT EXISTS "order_claim" (
"id" TEXT NOT NULL, "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") 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; WHERE return_id IS NOT NULL AND deleted_at IS NULL;
DO $$
BEGIN
CREATE TYPE claim_reason_enum AS ENUM ( IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'claim_reason_enum') THEN
'missing_item', CREATE TYPE claim_reason_enum AS ENUM (
'wrong_item', 'missing_item',
'production_failure', 'wrong_item',
'other' 'production_failure',
); 'other'
);
END IF;
END$$;
CREATE TABLE IF NOT EXISTS "order_claim_item" ( CREATE TABLE IF NOT EXISTS "order_claim_item" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
@@ -337,7 +345,6 @@ export class Migration20240604100512 extends Migration {
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS "order_claim_item_image" ( CREATE TABLE IF NOT EXISTS "order_claim_item_image" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"claim_item_id" TEXT NOT NULL, "claim_item_id" TEXT NOT NULL,