feat(dashboard,core-flows,js-sdk,types,medusa): Add exchange UI + fixes (#8606)
what: - adds exchange UI - fixes bugs on core-flows and endpoints - random set of tiny fixes https://github.com/user-attachments/assets/b163b9c1-4475-4936-ae98-20795760cc55
This commit is contained in:
@@ -11,6 +11,7 @@ import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { ordersQueryKeys } from "./orders"
|
||||
import { returnsQueryKeys } from "./returns"
|
||||
|
||||
const CLAIMS_QUERY_KEY = "claims" as const
|
||||
export const claimsQueryKeys = queryKeysFactory(CLAIMS_QUERY_KEY)
|
||||
@@ -499,6 +500,10 @@ export const useClaimConfirmRequest = (
|
||||
mutationFn: (payload: HttpTypes.AdminRequestClaim) =>
|
||||
sdk.admin.claim.request(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: returnsQueryKeys.all,
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
@@ -509,6 +514,11 @@ export const useClaimConfirmRequest = (
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: claimsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
|
||||
559
packages/admin-next/dashboard/src/hooks/api/exchanges.tsx
Normal file
559
packages/admin-next/dashboard/src/hooks/api/exchanges.tsx
Normal file
@@ -0,0 +1,559 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
QueryKey,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
} from "@tanstack/react-query"
|
||||
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { ordersQueryKeys } from "./orders"
|
||||
import { returnsQueryKeys } from "./returns"
|
||||
|
||||
const EXCHANGES_QUERY_KEY = "exchanges" as const
|
||||
export const exchangesQueryKeys = queryKeysFactory(EXCHANGES_QUERY_KEY)
|
||||
|
||||
export const useExchange = (
|
||||
id: string,
|
||||
query?: HttpTypes.AdminExchangeListParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => sdk.admin.exchange.retrieve(id, query),
|
||||
queryKey: exchangesQueryKeys.detail(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useExchanges = (
|
||||
query?: HttpTypes.AdminExchangeListParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminExchangeListParams,
|
||||
Error,
|
||||
HttpTypes.AdminExchangeListResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => sdk.admin.exchange.list(query),
|
||||
queryKey: exchangesQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateExchange = (
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminCreateExchange
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminCreateExchange) =>
|
||||
sdk.admin.exchange.create(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: exchangesQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCancelExchange = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<HttpTypes.AdminExchangeResponse, Error>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => sdk.admin.exchange.cancel(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: exchangesQueryKeys.details(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: exchangesQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteExchange = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<HttpTypes.AdminExchangeDeleteResponse, Error>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => sdk.admin.exchange.delete(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: exchangesQueryKeys.details(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: exchangesQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddExchangeItems = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminAddExchangeItems
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminAddExchangeItems) =>
|
||||
sdk.admin.exchange.addItems(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateExchangeItems = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminUpdateExchangeItem & { actionId: string }
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
actionId,
|
||||
...payload
|
||||
}: HttpTypes.AdminUpdateExchangeItem & { actionId: string }) => {
|
||||
return sdk.admin.exchange.updateItem(id, actionId, payload)
|
||||
},
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveExchangeItem = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<HttpTypes.AdminReturnResponse, Error, string>
|
||||
) => {
|
||||
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 useAddExchangeInboundItems = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminAddExchangeInboundItems
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminAddExchangeInboundItems) =>
|
||||
sdk.admin.exchange.addInboundItems(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateExchangeInboundItem = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminUpdateExchangeInboundItem & { actionId: string }
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
actionId,
|
||||
...payload
|
||||
}: HttpTypes.AdminUpdateExchangeInboundItem & { actionId: string }) => {
|
||||
return sdk.admin.exchange.updateInboundItem(id, actionId, payload)
|
||||
},
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveExchangeInboundItem = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<HttpTypes.AdminExchangeResponse, Error, string>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (actionId: string) =>
|
||||
sdk.admin.exchange.removeInboundItem(id, actionId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.all,
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddExchangeInboundShipping = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminExchangeAddInboundShipping
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminExchangeAddInboundShipping) =>
|
||||
sdk.admin.exchange.addInboundShipping(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateExchangeInboundShipping = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminExchangeUpdateInboundShipping
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
actionId,
|
||||
...payload
|
||||
}: HttpTypes.AdminExchangeUpdateInboundShipping & { actionId: string }) =>
|
||||
sdk.admin.exchange.updateInboundShipping(id, actionId, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteExchangeInboundShipping = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<HttpTypes.AdminExchangeResponse, Error, string>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (actionId: string) =>
|
||||
sdk.admin.exchange.deleteInboundShipping(id, actionId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddExchangeOutboundItems = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminAddExchangeOutboundItems
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminAddExchangeOutboundItems) =>
|
||||
sdk.admin.exchange.addOutboundItems(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateExchangeOutboundItems = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminUpdateExchangeOutboundItem & { actionId: string }
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
actionId,
|
||||
...payload
|
||||
}: HttpTypes.AdminUpdateExchangeOutboundItem & { actionId: string }) => {
|
||||
return sdk.admin.exchange.updateOutboundItem(id, actionId, payload)
|
||||
},
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveExchangeOutboundItem = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<HttpTypes.AdminExchangeResponse, Error, string>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (actionId: string) =>
|
||||
sdk.admin.exchange.removeOutboundItem(id, actionId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddExchangeOutboundShipping = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminExchangeAddOutboundShipping
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminExchangeAddOutboundShipping) =>
|
||||
sdk.admin.exchange.addOutboundShipping(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateExchangeOutboundShipping = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminExchangeUpdateOutboundShipping
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
actionId,
|
||||
...payload
|
||||
}: HttpTypes.AdminExchangeUpdateOutboundShipping & { actionId: string }) =>
|
||||
sdk.admin.exchange.updateOutboundShipping(id, actionId, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteExchangeOutboundShipping = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<HttpTypes.AdminExchangeResponse, Error, string>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (actionId: string) =>
|
||||
sdk.admin.exchange.deleteOutboundShipping(id, actionId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useExchangeConfirmRequest = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminExchangeResponse,
|
||||
Error,
|
||||
HttpTypes.AdminRequestExchange
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminRequestExchange) =>
|
||||
sdk.admin.exchange.request(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: returnsQueryKeys.all,
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.lists(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: exchangesQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCancelExchangeRequest = (
|
||||
id: string,
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<HttpTypes.AdminExchangeResponse, Error>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => sdk.admin.exchange.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: exchangesQueryKeys.details(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: exchangesQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -852,7 +852,7 @@
|
||||
"returnTotal": "Return total",
|
||||
"inboundTotal": "Inbound total",
|
||||
"refundAmount": "Refund amount",
|
||||
"outstandingAmount": "Difference amount",
|
||||
"outstandingAmount": "Outstanding amount",
|
||||
"reason": "Reason",
|
||||
"reasonHint": "Choose why the customer want to return items.",
|
||||
"note": "Note",
|
||||
@@ -904,6 +904,25 @@
|
||||
"onlyReturnShippingOptions": "This list will consist of only return shipping options."
|
||||
}
|
||||
},
|
||||
"exchanges": {
|
||||
"create": "Create Exchange",
|
||||
"manage": "Manage Exchange",
|
||||
"outbound": "Outbound",
|
||||
"outboundItemAdded": "{{itemsCount}}x added through exchange",
|
||||
"outboundTotal": "Outbound total",
|
||||
"outboundShipping": "Outbound shipping",
|
||||
"outboundShippingHint": "Choose which method you want to use.",
|
||||
"refundAmount": "Estimated difference",
|
||||
"activeChangeError": "There is an active order change on this order. Please finish or discard the previous change.",
|
||||
"actions": {
|
||||
"cancelExchange": {
|
||||
"successToast": "Exchange was successfully canceled."
|
||||
}
|
||||
},
|
||||
"tooltips": {
|
||||
"onlyReturnShippingOptions": "This list will consist of only return shipping options."
|
||||
}
|
||||
},
|
||||
"reservations": {
|
||||
"allocatedLabel": "Allocated",
|
||||
"notAllocatedLabel": "Not allocated"
|
||||
|
||||
@@ -248,6 +248,11 @@ export const RouteMap: RouteObject[] = [
|
||||
lazy: () =>
|
||||
import("../../routes/orders/order-create-claim"),
|
||||
},
|
||||
{
|
||||
path: "exchanges",
|
||||
lazy: () =>
|
||||
import("../../routes/orders/order-create-exchange"),
|
||||
},
|
||||
{
|
||||
path: "payments/:paymentId/refund",
|
||||
lazy: () =>
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { AdminOrderLineItem, DateComparisonOperator } from "@medusajs/types"
|
||||
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
||||
import { useMemo, useState } from "react"
|
||||
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { getReturnableQuantity } from "../../../../../lib/rma"
|
||||
import { useExchangeItemTableColumns } from "./use-exchange-item-table-columns"
|
||||
import { useExchangeItemTableFilters } from "./use-exchange-item-table-filters"
|
||||
import { useExchangeItemTableQuery } from "./use-exchange-item-table-query"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PREFIX = "rit"
|
||||
|
||||
type AddExchangeInboundItemsTableProps = {
|
||||
onSelectionChange: (ids: string[]) => void
|
||||
selectedItems: string[]
|
||||
items: AdminOrderLineItem[]
|
||||
currencyCode: string
|
||||
}
|
||||
|
||||
export const AddExchangeInboundItemsTable = ({
|
||||
onSelectionChange,
|
||||
selectedItems,
|
||||
items,
|
||||
currencyCode,
|
||||
}: AddExchangeInboundItemsTableProps) => {
|
||||
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 } = useExchangeItemTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const queriedItems = useMemo(() => {
|
||||
const { order, offset, limit, q, created_at, updated_at } = 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")
|
||||
}
|
||||
|
||||
return results.slice(offset, offset + limit)
|
||||
}, [items, currencyCode, searchParams])
|
||||
|
||||
const columns = useExchangeItemTableColumns(currencyCode)
|
||||
const filters = useExchangeItemTableFilters()
|
||||
|
||||
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"]}
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./add-exchange-inbound-items-table"
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
ProductCell,
|
||||
ProductHeader,
|
||||
} from "../../../../../components/table/table-cells/product/product-cell"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { getReturnableQuantity } from "../../../../../lib/rma"
|
||||
|
||||
const columnHelper = createColumnHelper<any>()
|
||||
|
||||
export const useExchangeItemTableColumns = (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,22 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useExchangeItemTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
key: "created_at",
|
||||
label: t("fields.createdAt"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
key: "updated_at",
|
||||
label: t("fields.updatedAt"),
|
||||
type: "date",
|
||||
},
|
||||
]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useExchangeItemTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
["q", "offset", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, created_at, updated_at, ...rest } = raw
|
||||
|
||||
const searchParams = {
|
||||
...rest,
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
}
|
||||
|
||||
return { searchParams, raw }
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
||||
import { useState } from "react"
|
||||
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useVariants } from "../../../../../hooks/api"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useExchangeOutboundItemTableColumns } from "./use-exchange-outbound-item-table-columns"
|
||||
import { useExchangeOutboundItemTableFilters } from "./use-exchange-outbound-item-table-filters"
|
||||
import { useExchangeOutboundItemTableQuery } from "./use-exchange-outbound-item-table-query"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PREFIX = "rit"
|
||||
|
||||
type AddExchangeOutboundItemsTableProps = {
|
||||
onSelectionChange: (ids: string[]) => void
|
||||
selectedItems: string[]
|
||||
currencyCode: string
|
||||
}
|
||||
|
||||
export const AddExchangeOutboundItemsTable = ({
|
||||
onSelectionChange,
|
||||
selectedItems,
|
||||
currencyCode,
|
||||
}: AddExchangeOutboundItemsTableProps) => {
|
||||
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 } = useExchangeOutboundItemTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const { variants = [], count } = useVariants({
|
||||
...searchParams,
|
||||
fields: "*inventory_items.inventory.location_levels,+inventory_quantity",
|
||||
})
|
||||
|
||||
const columns = useExchangeOutboundItemTableColumns(currencyCode)
|
||||
const filters = useExchangeOutboundItemTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: variants,
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: (_row) => {
|
||||
// TODO: Check inventory here. Check if other validations needs to be made
|
||||
return true
|
||||
},
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
filters={filters}
|
||||
pagination
|
||||
layout="fill"
|
||||
search
|
||||
orderBy={["product_id", "title", "sku"]}
|
||||
prefix={PREFIX}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./add-exchange-outbound-items-table"
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
ProductCell,
|
||||
ProductHeader,
|
||||
} from "../../../../../components/table/table-cells/product/product-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<any>()
|
||||
|
||||
export const useExchangeOutboundItemTableColumns = (currencyCode: string) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isSelectable = row.getCanSelect()
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
disabled={!isSelectable}
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "product",
|
||||
header: () => <ProductHeader />,
|
||||
cell: ({ row }) => {
|
||||
return <ProductCell product={row.original.product} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sku", {
|
||||
header: t("fields.sku"),
|
||||
cell: ({ getValue }) => {
|
||||
return getValue() || "-"
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
}),
|
||||
],
|
||||
[t, currencyCode]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useExchangeOutboundItemTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
key: "created_at",
|
||||
label: t("fields.createdAt"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
key: "updated_at",
|
||||
label: t("fields.updatedAt"),
|
||||
type: "date",
|
||||
},
|
||||
]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useExchangeOutboundItemTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
["q", "offset", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, created_at, updated_at, ...rest } = raw
|
||||
|
||||
const searchParams = {
|
||||
...rest,
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
}
|
||||
|
||||
return { searchParams, raw }
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { AdminExchange, AdminOrder, AdminOrderPreview } from "@medusajs/types"
|
||||
import {
|
||||
Button,
|
||||
CurrencyInput,
|
||||
Heading,
|
||||
IconButton,
|
||||
Switch,
|
||||
toast,
|
||||
} from "@medusajs/ui"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/modals"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { CreateExchangeSchemaType, ExchangeCreateSchema } from "./schema"
|
||||
|
||||
import { AdminReturn } from "@medusajs/types"
|
||||
import {
|
||||
useCancelExchangeRequest,
|
||||
useExchangeConfirmRequest,
|
||||
useUpdateExchangeInboundShipping,
|
||||
} from "../../../../../hooks/api/exchanges"
|
||||
import { currencies } from "../../../../../lib/data/currencies"
|
||||
import { ExchangeInboundSection } from "./exchange-inbound-section.tsx"
|
||||
import { ExchangeOutboundSection } from "./exchange-outbound-section"
|
||||
|
||||
type ReturnCreateFormProps = {
|
||||
order: AdminOrder
|
||||
exchange: AdminExchange
|
||||
preview: AdminOrderPreview
|
||||
orderReturn?: AdminReturn
|
||||
}
|
||||
|
||||
let IS_CANCELING = false
|
||||
|
||||
export const ExchangeCreateForm = ({
|
||||
order,
|
||||
preview,
|
||||
exchange,
|
||||
orderReturn,
|
||||
}: ReturnCreateFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
/**
|
||||
* STATE
|
||||
*/
|
||||
const [isShippingPriceEdit, setIsShippingPriceEdit] = useState(false)
|
||||
const [customShippingAmount, setCustomShippingAmount] = useState(0)
|
||||
|
||||
/**
|
||||
* MUTATIONS
|
||||
*/
|
||||
const { mutateAsync: confirmExchangeRequest, isPending: isConfirming } =
|
||||
useExchangeConfirmRequest(exchange.id, order.id)
|
||||
|
||||
const { mutateAsync: cancelExchangeRequest, isPending: isCanceling } =
|
||||
useCancelExchangeRequest(exchange.id, order.id)
|
||||
|
||||
const {
|
||||
mutateAsync: updateInboundShipping,
|
||||
isPending: isUpdatingInboundShipping,
|
||||
} = useUpdateExchangeInboundShipping(exchange.id, order.id)
|
||||
|
||||
const isRequestLoading =
|
||||
isConfirming || isCanceling || isUpdatingInboundShipping
|
||||
|
||||
/**
|
||||
* Only consider items that belong to this exchange.
|
||||
*/
|
||||
const previewItems = useMemo(
|
||||
() =>
|
||||
preview?.items?.filter(
|
||||
(i) => !!i.actions?.find((a) => a.exchange_id === exchange.id)
|
||||
),
|
||||
[preview.items]
|
||||
)
|
||||
|
||||
const inboundPreviewItems = previewItems.filter(
|
||||
(item) => !!item.actions?.find((a) => a.action === "RETURN_ITEM")
|
||||
)
|
||||
|
||||
const outboundPreviewItems = previewItems.filter(
|
||||
(item) => !!item.actions?.find((a) => a.action === "ITEM_ADD")
|
||||
)
|
||||
|
||||
/**
|
||||
* FORM
|
||||
*/
|
||||
const form = useForm<CreateExchangeSchemaType>({
|
||||
defaultValues: () => {
|
||||
const inboundShippingMethod = preview.shipping_methods.find((s) => {
|
||||
const action = s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
|
||||
return !!action?.return?.id
|
||||
})
|
||||
|
||||
const outboundShippingMethod = preview.shipping_methods.find((s) => {
|
||||
const action = s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
|
||||
return action && !!!action?.return?.id
|
||||
})
|
||||
|
||||
return Promise.resolve({
|
||||
inbound_items: inboundPreviewItems.map((i) => {
|
||||
const inboundAction = i.actions?.find(
|
||||
(a) => a.action === "RETURN_ITEM"
|
||||
)
|
||||
|
||||
return {
|
||||
item_id: i.id,
|
||||
variant_id: i.variant_id,
|
||||
quantity: i.detail.return_requested_quantity,
|
||||
note: inboundAction?.internal_note,
|
||||
reason_id: inboundAction?.details?.reason_id as string | undefined,
|
||||
}
|
||||
}),
|
||||
outbound_items: outboundPreviewItems.map((i) => ({
|
||||
item_id: i.id,
|
||||
variant_id: i.variant_id,
|
||||
quantity: i.detail.quantity,
|
||||
})),
|
||||
inbound_option_id: inboundShippingMethod
|
||||
? inboundShippingMethod.shipping_option_id
|
||||
: "",
|
||||
outbound_option_id: outboundShippingMethod
|
||||
? outboundShippingMethod.shipping_option_id
|
||||
: "",
|
||||
location_id: orderReturn?.location_id,
|
||||
send_notification: false,
|
||||
})
|
||||
},
|
||||
resolver: zodResolver(ExchangeCreateSchema),
|
||||
})
|
||||
|
||||
const outboundShipping = preview.shipping_methods.find((s) => {
|
||||
const action = s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
|
||||
return action && !!!action?.return?.id
|
||||
})
|
||||
|
||||
const shippingOptionId = form.watch("inbound_option_id")
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
try {
|
||||
await confirmExchangeRequest({ no_notification: !data.send_notification })
|
||||
|
||||
handleSuccess()
|
||||
} catch (e) {
|
||||
toast.error(t("general.error"), {
|
||||
description: e.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isShippingPriceEdit) {
|
||||
document.getElementById("js-shipping-input")?.focus()
|
||||
}
|
||||
}, [isShippingPriceEdit])
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Unmount hook
|
||||
*/
|
||||
return () => {
|
||||
if (IS_CANCELING) {
|
||||
cancelExchangeRequest(undefined, {
|
||||
onSuccess: () => {
|
||||
toast.success(
|
||||
t("orders.exchanges.actions.cancelExchange.successToast")
|
||||
)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
|
||||
IS_CANCELING = false
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const shippingTotal = useMemo(() => {
|
||||
const method = preview.shipping_methods.find(
|
||||
(sm) => !!sm.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
)
|
||||
|
||||
return (method?.total as number) || 0
|
||||
}, [preview.shipping_methods])
|
||||
|
||||
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.exchanges.create")}</Heading>
|
||||
|
||||
<ExchangeInboundSection
|
||||
form={form}
|
||||
preview={preview}
|
||||
order={order}
|
||||
exchange={exchange}
|
||||
orderReturn={orderReturn}
|
||||
/>
|
||||
|
||||
<ExchangeOutboundSection
|
||||
form={form}
|
||||
preview={preview}
|
||||
order={order}
|
||||
exchange={exchange}
|
||||
/>
|
||||
|
||||
{/*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.inboundTotal")}
|
||||
</span>
|
||||
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{getStylizedAmount(
|
||||
inboundPreviewItems.reduce((acc, item) => {
|
||||
const action = item.actions?.find(
|
||||
(act) => act.action === "RETURN_ITEM"
|
||||
)
|
||||
acc = acc + (action?.amount || 0)
|
||||
|
||||
return acc
|
||||
}, 0) * -1,
|
||||
order.currency_code
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.exchanges.outboundTotal")}
|
||||
</span>
|
||||
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{getStylizedAmount(
|
||||
outboundPreviewItems.reduce((acc, item) => {
|
||||
const action = item.actions?.find(
|
||||
(act) => act.action === "ITEM_ADD"
|
||||
)
|
||||
acc = acc + (action?.amount || 0)
|
||||
|
||||
return acc
|
||||
}, 0),
|
||||
order.currency_code
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 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={
|
||||
!inboundPreviewItems?.length || !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) {
|
||||
updateInboundShipping(
|
||||
{
|
||||
actionId,
|
||||
custom_price:
|
||||
typeof customShippingAmount === "string"
|
||||
? null
|
||||
: customShippingAmount,
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
setIsShippingPriceEdit(false)
|
||||
}}
|
||||
symbol={
|
||||
currencies[order.currency_code.toUpperCase()]
|
||||
.symbol_native
|
||||
}
|
||||
code={order.currency_code}
|
||||
onValueChange={(value) =>
|
||||
value && setCustomShippingAmount(parseInt(value))
|
||||
}
|
||||
value={customShippingAmount}
|
||||
disabled={!inboundPreviewItems?.length}
|
||||
/>
|
||||
) : (
|
||||
getStylizedAmount(shippingTotal, order.currency_code)
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.exchanges.outboundShipping")}
|
||||
</span>
|
||||
|
||||
<span className="txt-small text-ui-fg-subtle flex items-center">
|
||||
{getStylizedAmount(
|
||||
outboundShipping?.amount ?? 0,
|
||||
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.exchanges.refundAmount")}
|
||||
</span>
|
||||
<span className="txt-small font-medium">
|
||||
{getStylizedAmount(
|
||||
preview.summary.pending_difference,
|
||||
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,244 @@
|
||||
import { ChatBubble, DocumentText, XCircle, XMark } from "@medusajs/icons"
|
||||
import { AdminOrderLineItem, HttpTypes } from "@medusajs/types"
|
||||
import { IconButton, Input, Text } from "@medusajs/ui"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
||||
import { useReturnReasons } from "../../../../../hooks/api/return-reasons"
|
||||
|
||||
type ExchangeInboundItemProps = {
|
||||
item: AdminOrderLineItem
|
||||
previewItem: AdminOrderLineItem
|
||||
currencyCode: string
|
||||
index: number
|
||||
|
||||
onRemove: () => void
|
||||
onUpdate: (payload: HttpTypes.AdminUpdateReturnItems) => void
|
||||
|
||||
form: UseFormReturn<any>
|
||||
}
|
||||
|
||||
function ExchangeInboundItem({
|
||||
item,
|
||||
previewItem,
|
||||
currencyCode,
|
||||
form,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
index,
|
||||
}: ExchangeInboundItemProps) {
|
||||
const { t } = useTranslation()
|
||||
const { return_reasons = [] } = useReturnReasons({ fields: "+label" })
|
||||
|
||||
const formItem = form.watch(`inbound_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={`inbound_items.${index}.quantity`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
className="bg-ui-bg-base txt-small w-[67px] rounded-lg"
|
||||
min={1}
|
||||
max={item.quantity}
|
||||
type="number"
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value
|
||||
const payload = val === "" ? null : Number(val)
|
||||
|
||||
field.onChange(payload)
|
||||
|
||||
if (payload) {
|
||||
onUpdate({ quantity: payload })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Text className="txt-small text-ui-fg-subtle">
|
||||
{t("fields.qty")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle txt-small mr-2 flex flex-shrink-0">
|
||||
<MoneyAmountCell
|
||||
currencyCode={currencyCode}
|
||||
amount={previewItem.return_requested_total}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
!showReturnReason && {
|
||||
label: t("actions.addReason"),
|
||||
onClick: () =>
|
||||
form.setValue(`inbound_items.${index}.reason_id`, ""),
|
||||
icon: <ChatBubble />,
|
||||
},
|
||||
!showNote && {
|
||||
label: t("actions.addNote"),
|
||||
onClick: () =>
|
||||
form.setValue(`inbound_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={`inbound_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={() => {
|
||||
form.setValue(`inbound_items.${index}.reason_id`, null)
|
||||
|
||||
onUpdate({ reason_id: null })
|
||||
}}
|
||||
>
|
||||
<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={`inbound_items.${index}.note`}
|
||||
render={({ field: { ref, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
onBlur={() => {
|
||||
field.onChange(field.value)
|
||||
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(`inbound_items.${index}.note`, null)
|
||||
|
||||
onUpdate({ internal_note: null })
|
||||
}}
|
||||
>
|
||||
<XMark className="text-ui-fg-muted" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ExchangeInboundItem }
|
||||
@@ -0,0 +1,527 @@
|
||||
import {
|
||||
AdminExchange,
|
||||
AdminOrder,
|
||||
AdminOrderPreview,
|
||||
AdminReturn,
|
||||
InventoryLevelDTO,
|
||||
} from "@medusajs/types"
|
||||
import { Alert, Button, Heading, Text, toast } from "@medusajs/ui"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useFieldArray, UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
StackedFocusModal,
|
||||
useStackedModal,
|
||||
} from "../../../../../components/modals"
|
||||
import { useShippingOptions, useStockLocations } from "../../../../../hooks/api"
|
||||
import {
|
||||
useAddExchangeInboundItems,
|
||||
useAddExchangeInboundShipping,
|
||||
useDeleteExchangeInboundShipping,
|
||||
useRemoveExchangeInboundItem,
|
||||
useUpdateExchangeInboundItem,
|
||||
} from "../../../../../hooks/api/exchanges"
|
||||
import { useUpdateReturn } from "../../../../../hooks/api/returns"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder"
|
||||
import { AddExchangeInboundItemsTable } from "../add-exchange-inbound-items-table"
|
||||
import { ExchangeInboundItem } from "./exchange-inbound-item"
|
||||
import { CreateExchangeSchemaType } from "./schema"
|
||||
|
||||
type ExchangeInboundSectionProps = {
|
||||
order: AdminOrder
|
||||
orderReturn?: AdminReturn
|
||||
exchange: AdminExchange
|
||||
preview: AdminOrderPreview
|
||||
form: UseFormReturn<CreateExchangeSchemaType>
|
||||
}
|
||||
|
||||
let itemsToAdd: string[] = []
|
||||
let itemsToRemove: string[] = []
|
||||
|
||||
export const ExchangeInboundSection = ({
|
||||
order,
|
||||
preview,
|
||||
exchange,
|
||||
form,
|
||||
orderReturn,
|
||||
}: ExchangeInboundSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* STATE
|
||||
*/
|
||||
const { setIsOpen } = useStackedModal()
|
||||
const [inventoryMap, setInventoryMap] = useState<
|
||||
Record<string, InventoryLevelDTO[]>
|
||||
>({})
|
||||
|
||||
/**
|
||||
* MUTATIONS
|
||||
*/
|
||||
const { mutateAsync: updateReturn } = useUpdateReturn(
|
||||
preview?.order_change?.return_id!,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: addInboundShipping } = useAddExchangeInboundShipping(
|
||||
exchange.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: deleteInboundShipping } =
|
||||
useDeleteExchangeInboundShipping(exchange.id, order.id)
|
||||
|
||||
const { mutateAsync: addInboundItem } = useAddExchangeInboundItems(
|
||||
exchange.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: updateInboundItem } = useUpdateExchangeInboundItem(
|
||||
exchange.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: removeInboundItem } = useRemoveExchangeInboundItem(
|
||||
exchange.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
/**
|
||||
* Only consider items that belong to this exchange.
|
||||
*/
|
||||
const previewInboundItems = useMemo(
|
||||
() =>
|
||||
preview?.items?.filter(
|
||||
(i) => !!i.actions?.find((a) => a.exchange_id === exchange.id)
|
||||
),
|
||||
[preview.items]
|
||||
)
|
||||
|
||||
const inboundPreviewItems = previewInboundItems.filter(
|
||||
(item) => !!item.actions?.find((a) => a.action === "RETURN_ITEM")
|
||||
)
|
||||
|
||||
const itemsMap = useMemo(
|
||||
() => new Map(order?.items?.map((i) => [i.id, i])),
|
||||
[order.items]
|
||||
)
|
||||
|
||||
const locationId = form.watch("location_id")
|
||||
|
||||
/**
|
||||
* HOOKS
|
||||
*/
|
||||
const { stock_locations = [] } = useStockLocations({ limit: 999 })
|
||||
const { shipping_options = [] } = useShippingOptions(
|
||||
{
|
||||
limit: 999,
|
||||
fields: "*prices,+service_zone.fulfillment_set.location.id",
|
||||
stock_location_id: locationId,
|
||||
},
|
||||
{
|
||||
enabled: !!locationId,
|
||||
}
|
||||
)
|
||||
|
||||
const inboundShippingOptions = shipping_options.filter(
|
||||
(shippingOption) =>
|
||||
!!shippingOption.rules.find(
|
||||
(r) => r.attribute === "is_return" && r.value === "true"
|
||||
)
|
||||
)
|
||||
|
||||
const {
|
||||
fields: inboundItems,
|
||||
append,
|
||||
remove,
|
||||
update,
|
||||
} = useFieldArray({
|
||||
name: "inbound_items",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const inboundItemsMap = useMemo(
|
||||
() => new Map(previewInboundItems.map((i) => [i.id, i])),
|
||||
[previewInboundItems, inboundItems]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const existingItemsMap: Record<string, boolean> = {}
|
||||
|
||||
inboundPreviewItems.forEach((i) => {
|
||||
const ind = inboundItems.findIndex((field) => field.item_id === i.id)
|
||||
|
||||
existingItemsMap[i.id] = true
|
||||
|
||||
if (ind > -1) {
|
||||
if (inboundItems[ind].quantity !== i.detail.return_requested_quantity) {
|
||||
const returnItemAction = i.actions?.find(
|
||||
(a) => a.action === "RETURN_ITEM"
|
||||
)
|
||||
|
||||
update(ind, {
|
||||
...inboundItems[ind],
|
||||
quantity: i.detail.return_requested_quantity,
|
||||
note: returnItemAction?.internal_note,
|
||||
reason_id: returnItemAction?.details?.reason_id as string,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
append({ item_id: i.id, quantity: i.detail.return_requested_quantity })
|
||||
}
|
||||
})
|
||||
|
||||
inboundItems.forEach((i, ind) => {
|
||||
if (!(i.item_id in existingItemsMap)) {
|
||||
remove(ind)
|
||||
}
|
||||
})
|
||||
}, [previewInboundItems])
|
||||
|
||||
useEffect(() => {
|
||||
const inboundShippingMethod = preview.shipping_methods.find((s) => {
|
||||
const action = s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
|
||||
return !!action?.return?.id
|
||||
})
|
||||
|
||||
if (inboundShippingMethod) {
|
||||
form.setValue(
|
||||
"inbound_option_id",
|
||||
inboundShippingMethod.shipping_option_id
|
||||
)
|
||||
}
|
||||
}, [preview.shipping_methods])
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue("location_id", orderReturn?.location_id)
|
||||
}, [orderReturn])
|
||||
|
||||
const showInboundItemsPlaceholder = !inboundItems.length
|
||||
|
||||
const onItemsSelected = async () => {
|
||||
itemsToAdd.length &&
|
||||
(await addInboundItem(
|
||||
{
|
||||
items: itemsToAdd.map((id) => ({
|
||||
id,
|
||||
quantity: 1,
|
||||
})),
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
))
|
||||
|
||||
for (const itemToRemove of itemsToRemove) {
|
||||
const actionId = previewInboundItems
|
||||
.find((i) => i.id === itemToRemove)
|
||||
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
|
||||
|
||||
if (actionId) {
|
||||
await removeInboundItem(actionId, {
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setIsOpen("inbound-items", false)
|
||||
}
|
||||
|
||||
const onLocationChange = async (selectedLocationId?: string | null) => {
|
||||
await updateReturn({ location_id: selectedLocationId })
|
||||
}
|
||||
|
||||
const onShippingOptionChange = async (selectedOptionId: string) => {
|
||||
const inboundShippingMethods = preview.shipping_methods.filter((s) => {
|
||||
const action = s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
|
||||
return action && !!action?.return?.id
|
||||
})
|
||||
|
||||
const promises = inboundShippingMethods
|
||||
.filter(Boolean)
|
||||
.map((inboundShippingMethod) => {
|
||||
const action = inboundShippingMethod.actions?.find(
|
||||
(a) => a.action === "SHIPPING_ADD"
|
||||
)
|
||||
|
||||
if (action) {
|
||||
deleteInboundShipping(action.id)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
await addInboundShipping(
|
||||
{ shipping_option_id: selectedOptionId },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const showLevelsWarning = useMemo(() => {
|
||||
if (!locationId) {
|
||||
return false
|
||||
}
|
||||
|
||||
const allItemsHaveLocation = inboundItems
|
||||
.map((_i) => {
|
||||
const item = itemsMap.get(_i.item_id)
|
||||
if (!item?.variant_id || !item?.variant) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!item.variant.manage_inventory) {
|
||||
return true
|
||||
}
|
||||
|
||||
return inventoryMap[item.variant_id]?.find(
|
||||
(l) => l.location_id === locationId
|
||||
)
|
||||
})
|
||||
.every(Boolean)
|
||||
|
||||
return !allItemsHaveLocation
|
||||
}, [inboundItems, inventoryMap, locationId])
|
||||
|
||||
useEffect(() => {
|
||||
const getInventoryMap = async () => {
|
||||
const ret: Record<string, InventoryLevelDTO[]> = {}
|
||||
|
||||
if (!inboundItems.length) {
|
||||
return ret
|
||||
}
|
||||
|
||||
const variantIds = inboundItems
|
||||
.map((item) => item?.variant_id)
|
||||
.filter(Boolean)
|
||||
|
||||
const variants = (
|
||||
await sdk.admin.productVariant.list(
|
||||
{ id: variantIds },
|
||||
{ fields: "*inventory,*inventory.location_levels" }
|
||||
)
|
||||
).variants
|
||||
|
||||
variants.forEach((variant) => {
|
||||
ret[variant.id] = variant.inventory?.[0]?.location_levels || []
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
getInventoryMap().then((map) => {
|
||||
setInventoryMap(map)
|
||||
})
|
||||
}, [inboundItems])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<Heading level="h2">{t("orders.returns.inbound")}</Heading>
|
||||
|
||||
<StackedFocusModal id="inbound-items">
|
||||
<StackedFocusModal.Trigger asChild>
|
||||
<a className="focus-visible:shadow-borders-focus transition-fg txt-compact-small-plus cursor-pointer text-blue-500 outline-none hover:text-blue-400">
|
||||
{t("actions.addItems")}
|
||||
</a>
|
||||
</StackedFocusModal.Trigger>
|
||||
|
||||
<StackedFocusModal.Content>
|
||||
<StackedFocusModal.Header />
|
||||
|
||||
<AddExchangeInboundItemsTable
|
||||
items={order.items!}
|
||||
selectedItems={inboundItems.map((i) => i.item_id)}
|
||||
currencyCode={order.currency_code}
|
||||
onSelectionChange={(finalSelection) => {
|
||||
const alreadySelected = inboundItems.map((i) => i.item_id)
|
||||
|
||||
itemsToAdd = finalSelection.filter(
|
||||
(selection) => !alreadySelected.includes(selection)
|
||||
)
|
||||
itemsToRemove = alreadySelected.filter(
|
||||
(selection) => !finalSelection.includes(selection)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<StackedFocusModal.Footer>
|
||||
<div className="flex w-full items-center justify-end gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button type="button" variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
key="submit-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
role="button"
|
||||
onClick={async () => await onItemsSelected()}
|
||||
>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StackedFocusModal.Footer>
|
||||
</StackedFocusModal.Content>
|
||||
</StackedFocusModal>
|
||||
</div>
|
||||
|
||||
{showInboundItemsPlaceholder && <ItemPlaceholder />}
|
||||
|
||||
{inboundItems.map(
|
||||
(item, index) =>
|
||||
inboundItemsMap.get(item.item_id) &&
|
||||
itemsMap.get(item.item_id)! && (
|
||||
<ExchangeInboundItem
|
||||
key={item.id}
|
||||
item={itemsMap.get(item.item_id)!}
|
||||
previewItem={inboundItemsMap.get(item.item_id)!}
|
||||
currencyCode={order.currency_code}
|
||||
form={form}
|
||||
onRemove={() => {
|
||||
const actionId = previewInboundItems
|
||||
.find((i) => i.id === item.item_id)
|
||||
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
|
||||
|
||||
if (actionId) {
|
||||
removeInboundItem(actionId, {
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
onUpdate={(payload) => {
|
||||
const actionId = previewInboundItems
|
||||
.find((i) => i.id === item.item_id)
|
||||
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
|
||||
|
||||
if (actionId) {
|
||||
updateInboundItem(
|
||||
{ ...payload, actionId },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}}
|
||||
index={index}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{!showInboundItemsPlaceholder && (
|
||||
<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
|
||||
{...field}
|
||||
value={value ?? undefined}
|
||||
onChange={(v) => {
|
||||
onChange(v)
|
||||
onLocationChange(v)
|
||||
}}
|
||||
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
|
||||
tooltip={t(
|
||||
"orders.exchanges.tooltips.onlyReturnShippingOptions"
|
||||
)}
|
||||
>
|
||||
{t("orders.returns.inboundShipping")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.returns.inboundShippingHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="inbound_option_id"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
value={value ?? undefined}
|
||||
onChange={(val) => {
|
||||
onChange(val)
|
||||
val && onShippingOptionChange(val)
|
||||
}}
|
||||
{...field}
|
||||
options={inboundShippingOptions.map((so) => ({
|
||||
label: so.name,
|
||||
value: so.id,
|
||||
}))}
|
||||
disabled={!locationId}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showLevelsWarning && (
|
||||
<Alert variant="warning" dismissible className="mt-4 p-5">
|
||||
<div className="text-ui-fg-subtle txt-small pb-2 font-medium leading-[20px]">
|
||||
{t("orders.returns.noInventoryLevel")}
|
||||
</div>
|
||||
<Text className="text-ui-fg-subtle txt-small leading-normal">
|
||||
{t("orders.returns.noInventoryLevelDesc")}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { XCircle } from "@medusajs/icons"
|
||||
import { AdminOrderLineItem, HttpTypes } from "@medusajs/types"
|
||||
import { Input, Text } from "@medusajs/ui"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
||||
import { CreateExchangeSchemaType } from "./schema"
|
||||
|
||||
type ExchangeOutboundItemProps = {
|
||||
previewItem: AdminOrderLineItem
|
||||
currencyCode: string
|
||||
index: number
|
||||
|
||||
onRemove: () => void
|
||||
// TODO: create a payload type for outbound updates
|
||||
onUpdate: (payload: HttpTypes.AdminUpdateReturnItems) => void
|
||||
|
||||
form: UseFormReturn<CreateExchangeSchemaType>
|
||||
}
|
||||
|
||||
function ExchangeOutboundItem({
|
||||
previewItem,
|
||||
currencyCode,
|
||||
form,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
index,
|
||||
}: ExchangeOutboundItemProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl ">
|
||||
<div className="flex flex-col items-center gap-x-2 gap-y-2 border-b p-3 text-sm md:flex-row">
|
||||
<div className="flex flex-1 items-center gap-x-3">
|
||||
<Thumbnail src={previewItem.thumbnail} />
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
<Text className="txt-small" as="span" weight="plus">
|
||||
{previewItem.title}{" "}
|
||||
</Text>
|
||||
|
||||
{previewItem.variant_sku && (
|
||||
<span>({previewItem.variant_sku})</span>
|
||||
)}
|
||||
</div>
|
||||
<Text as="div" className="text-ui-fg-subtle txt-small">
|
||||
{previewItem.product_title}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 justify-between">
|
||||
<div className="flex flex-grow items-center gap-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`outbound_items.${index}.quantity`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
className="bg-ui-bg-base txt-small w-[67px] rounded-lg"
|
||||
min={1}
|
||||
// TODO: add max available inventory quantity if present
|
||||
// max={previewItem.quantity}
|
||||
type="number"
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value
|
||||
const payload = val === "" ? null : Number(val)
|
||||
|
||||
field.onChange(payload)
|
||||
|
||||
if (payload) {
|
||||
onUpdate({ quantity: payload })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Text className="txt-small text-ui-fg-subtle">
|
||||
{t("fields.qty")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle txt-small mr-2 flex flex-shrink-0">
|
||||
<MoneyAmountCell
|
||||
currencyCode={currencyCode}
|
||||
amount={previewItem.total}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.remove"),
|
||||
onClick: onRemove,
|
||||
icon: <XCircle />,
|
||||
},
|
||||
].filter(Boolean),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ExchangeOutboundItem }
|
||||
@@ -0,0 +1,423 @@
|
||||
import {
|
||||
AdminExchange,
|
||||
AdminOrder,
|
||||
AdminOrderPreview,
|
||||
InventoryLevelDTO,
|
||||
} from "@medusajs/types"
|
||||
import { Alert, Button, Heading, Text, toast } from "@medusajs/ui"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useFieldArray, UseFormReturn } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
StackedFocusModal,
|
||||
useStackedModal,
|
||||
} from "../../../../../components/modals"
|
||||
import {
|
||||
useAddExchangeOutboundItems,
|
||||
useAddExchangeOutboundShipping,
|
||||
useDeleteExchangeOutboundShipping,
|
||||
useRemoveExchangeOutboundItem,
|
||||
useUpdateExchangeOutboundItems,
|
||||
} from "../../../../../hooks/api/exchanges"
|
||||
import { useShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder"
|
||||
import { AddExchangeOutboundItemsTable } from "../add-exchange-outbound-items-table"
|
||||
import { ExchangeOutboundItem } from "./exchange-outbound-item"
|
||||
import { CreateExchangeSchemaType } from "./schema"
|
||||
|
||||
type ExchangeOutboundSectionProps = {
|
||||
order: AdminOrder
|
||||
exchange: AdminExchange
|
||||
preview: AdminOrderPreview
|
||||
form: UseFormReturn<CreateExchangeSchemaType>
|
||||
}
|
||||
|
||||
let itemsToAdd: string[] = []
|
||||
let itemsToRemove: string[] = []
|
||||
|
||||
export const ExchangeOutboundSection = ({
|
||||
order,
|
||||
preview,
|
||||
exchange,
|
||||
form,
|
||||
}: ExchangeOutboundSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { setIsOpen } = useStackedModal()
|
||||
const [inventoryMap, setInventoryMap] = useState<
|
||||
Record<string, InventoryLevelDTO[]>
|
||||
>({})
|
||||
|
||||
/**
|
||||
* HOOKS
|
||||
*/
|
||||
const { shipping_options = [] } = useShippingOptions({
|
||||
limit: 999,
|
||||
fields: "*prices,+service_zone.fulfillment_set.location.id",
|
||||
})
|
||||
|
||||
const { mutateAsync: addOutboundShipping } = useAddExchangeOutboundShipping(
|
||||
exchange.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: deleteOutboundShipping } =
|
||||
useDeleteExchangeOutboundShipping(exchange.id, order.id)
|
||||
|
||||
const { mutateAsync: addOutboundItem } = useAddExchangeOutboundItems(
|
||||
exchange.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: updateOutboundItem } = useUpdateExchangeOutboundItems(
|
||||
exchange.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
const { mutateAsync: removeOutboundItem } = useRemoveExchangeOutboundItem(
|
||||
exchange.id,
|
||||
order.id
|
||||
)
|
||||
|
||||
/**
|
||||
* Only consider items that belong to this exchange and is an outbound item
|
||||
*/
|
||||
const previewOutboundItems = useMemo(
|
||||
() =>
|
||||
preview?.items?.filter(
|
||||
(i) =>
|
||||
!!i.actions?.find(
|
||||
(a) => a.exchange_id === exchange.id && a.action === "ITEM_ADD"
|
||||
)
|
||||
),
|
||||
[preview.items]
|
||||
)
|
||||
|
||||
const variantItemMap = useMemo(
|
||||
() => new Map(order?.items?.map((i) => [i.variant_id, i])),
|
||||
[order.items]
|
||||
)
|
||||
|
||||
const {
|
||||
fields: outboundItems,
|
||||
append,
|
||||
remove,
|
||||
update,
|
||||
} = useFieldArray({
|
||||
name: "outbound_items",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
const variantOutboundMap = useMemo(
|
||||
() => new Map(previewOutboundItems.map((i) => [i.variant_id, i])),
|
||||
[previewOutboundItems, outboundItems]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const existingItemsMap: Record<string, boolean> = {}
|
||||
|
||||
previewOutboundItems.forEach((i) => {
|
||||
const ind = outboundItems.findIndex((field) => field.item_id === i.id)
|
||||
|
||||
existingItemsMap[i.id] = true
|
||||
|
||||
if (ind > -1) {
|
||||
if (outboundItems[ind].quantity !== i.detail.quantity) {
|
||||
update(ind, {
|
||||
...outboundItems[ind],
|
||||
quantity: i.detail.quantity,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
append({
|
||||
item_id: i.id,
|
||||
quantity: i.detail.quantity,
|
||||
variant_id: i.variant_id,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
outboundItems.forEach((i, ind) => {
|
||||
if (!(i.item_id in existingItemsMap)) {
|
||||
remove(ind)
|
||||
}
|
||||
})
|
||||
}, [previewOutboundItems])
|
||||
|
||||
const locationId = form.watch("location_id")
|
||||
const showOutboundItemsPlaceholder = !outboundItems.length
|
||||
|
||||
const onItemsSelected = async () => {
|
||||
itemsToAdd.length &&
|
||||
(await addOutboundItem(
|
||||
{
|
||||
items: itemsToAdd.map((variantId) => ({
|
||||
variant_id: variantId,
|
||||
quantity: 1,
|
||||
})),
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
))
|
||||
|
||||
for (const itemToRemove of itemsToRemove) {
|
||||
const action = previewOutboundItems
|
||||
.find((i) => i.variant_id === itemToRemove)
|
||||
?.actions?.find((a) => a.action === "ITEM_ADD")
|
||||
|
||||
if (action?.id) {
|
||||
await removeOutboundItem(action?.id, {
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setIsOpen("outbound-items", false)
|
||||
}
|
||||
|
||||
const onShippingOptionChange = async (selectedOptionId: string) => {
|
||||
const outboundShippingMethods = preview.shipping_methods.filter((s) => {
|
||||
const action = s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
|
||||
return action && !!!action?.return?.id
|
||||
})
|
||||
|
||||
const promises = outboundShippingMethods
|
||||
.filter(Boolean)
|
||||
.map((outboundShippingMethod) => {
|
||||
const action = outboundShippingMethod.actions?.find(
|
||||
(a) => a.action === "SHIPPING_ADD"
|
||||
)
|
||||
|
||||
if (action) {
|
||||
deleteOutboundShipping(action.id)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
await addOutboundShipping(
|
||||
{ shipping_option_id: selectedOptionId },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const showLevelsWarning = useMemo(() => {
|
||||
if (!locationId) {
|
||||
return false
|
||||
}
|
||||
|
||||
const allItemsHaveLocation = outboundItems
|
||||
.map((i) => {
|
||||
const item = variantItemMap.get(i.variant_id)
|
||||
if (!item?.variant_id || !item?.variant) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!item.variant.manage_inventory) {
|
||||
return true
|
||||
}
|
||||
|
||||
return inventoryMap[item.variant_id]?.find(
|
||||
(l) => l.location_id === locationId
|
||||
)
|
||||
})
|
||||
.every(Boolean)
|
||||
|
||||
return !allItemsHaveLocation
|
||||
}, [outboundItems, inventoryMap, locationId])
|
||||
|
||||
useEffect(() => {
|
||||
const getInventoryMap = async () => {
|
||||
const ret: Record<string, InventoryLevelDTO[]> = {}
|
||||
|
||||
if (!outboundItems.length) {
|
||||
return ret
|
||||
}
|
||||
|
||||
const variantIds = outboundItems
|
||||
.map((item) => item?.variant_id)
|
||||
.filter(Boolean)
|
||||
const variants = (
|
||||
await sdk.admin.productVariant.list(
|
||||
{ id: variantIds },
|
||||
{ fields: "*inventory,*inventory.location_levels" }
|
||||
)
|
||||
).variants
|
||||
|
||||
variants.forEach((variant) => {
|
||||
ret[variant.id] = variant.inventory?.[0]?.location_levels || []
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
getInventoryMap().then((map) => {
|
||||
setInventoryMap(map)
|
||||
})
|
||||
}, [outboundItems])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<Heading level="h2">{t("orders.returns.outbound")}</Heading>
|
||||
|
||||
<StackedFocusModal id="outbound-items">
|
||||
<StackedFocusModal.Trigger asChild>
|
||||
<a className="focus-visible:shadow-borders-focus transition-fg txt-compact-small-plus cursor-pointer text-blue-500 outline-none hover:text-blue-400">
|
||||
{t("actions.addItems")}
|
||||
</a>
|
||||
</StackedFocusModal.Trigger>
|
||||
<StackedFocusModal.Content>
|
||||
<StackedFocusModal.Header />
|
||||
|
||||
<AddExchangeOutboundItemsTable
|
||||
selectedItems={outboundItems.map((i) => i.variant_id)}
|
||||
currencyCode={order.currency_code}
|
||||
onSelectionChange={(finalSelection) => {
|
||||
const alreadySelected = outboundItems.map((i) => i.variant_id)
|
||||
|
||||
itemsToAdd = finalSelection.filter(
|
||||
(selection) => !alreadySelected.includes(selection)
|
||||
)
|
||||
itemsToRemove = alreadySelected.filter(
|
||||
(selection) => !finalSelection.includes(selection)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<StackedFocusModal.Footer>
|
||||
<div className="flex w-full items-center justify-end gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button type="button" variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
key="submit-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
role="button"
|
||||
onClick={async () => await onItemsSelected()}
|
||||
>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StackedFocusModal.Footer>
|
||||
</StackedFocusModal.Content>
|
||||
</StackedFocusModal>
|
||||
</div>
|
||||
|
||||
{showOutboundItemsPlaceholder && <ItemPlaceholder />}
|
||||
|
||||
{outboundItems.map(
|
||||
(item, index) =>
|
||||
variantOutboundMap.get(item.variant_id) && (
|
||||
<ExchangeOutboundItem
|
||||
key={item.id}
|
||||
previewItem={variantOutboundMap.get(item.variant_id)!}
|
||||
currencyCode={order.currency_code}
|
||||
form={form}
|
||||
onRemove={() => {
|
||||
const actionId = previewOutboundItems
|
||||
.find((i) => i.id === item.item_id)
|
||||
?.actions?.find((a) => a.action === "ITEM_ADD")?.id
|
||||
|
||||
if (actionId) {
|
||||
removeOutboundItem(actionId, {
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
onUpdate={(payload) => {
|
||||
const actionId = previewOutboundItems
|
||||
.find((i) => i.id === item.item_id)
|
||||
?.actions?.find((a) => a.action === "ITEM_ADD")?.id
|
||||
|
||||
if (actionId) {
|
||||
updateOutboundItem(
|
||||
{ ...payload, actionId },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}}
|
||||
index={index}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{!showOutboundItemsPlaceholder && (
|
||||
<div className="mt-8 flex flex-col gap-y-4">
|
||||
{/*OUTBOUND SHIPPING*/}
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<div>
|
||||
<Form.Label>{t("orders.exchanges.outboundShipping")}</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.exchanges.outboundShippingHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="outbound_option_id"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
value={value ?? undefined}
|
||||
onChange={(val) => {
|
||||
onChange(val)
|
||||
val && onShippingOptionChange(val)
|
||||
}}
|
||||
{...field}
|
||||
options={shipping_options.map((so) => ({
|
||||
label: so.name,
|
||||
value: so.id,
|
||||
}))}
|
||||
disabled={!shipping_options.length}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLevelsWarning && (
|
||||
<Alert variant="warning" dismissible className="mt-4 p-5">
|
||||
<div className="text-ui-fg-subtle txt-small pb-2 font-medium leading-[20px]">
|
||||
{t("orders.returns.noInventoryLevel")}
|
||||
</div>
|
||||
<Text className="text-ui-fg-subtle txt-small leading-normal">
|
||||
{t("orders.returns.noInventoryLevelDesc")}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./exchange-create-form"
|
||||
@@ -0,0 +1,23 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const ExchangeCreateSchema = z.object({
|
||||
inbound_items: z.array(
|
||||
z.object({
|
||||
item_id: z.string(),
|
||||
quantity: z.number(),
|
||||
reason_id: z.string().nullish(),
|
||||
note: z.string().nullish(),
|
||||
})
|
||||
),
|
||||
outbound_items: z.array(
|
||||
z.object({
|
||||
item_id: z.string(), // TODO: variant id?
|
||||
quantity: z.number(),
|
||||
})
|
||||
),
|
||||
location_id: z.string().optional(),
|
||||
inbound_option_id: z.string().nullish(),
|
||||
send_notification: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type CreateExchangeSchemaType = z.infer<typeof ExchangeCreateSchema>
|
||||
@@ -0,0 +1,84 @@
|
||||
import { toast } from "@medusajs/ui"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate, useParams } from "react-router-dom"
|
||||
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import { useCreateExchange, useExchange } from "../../../hooks/api/exchanges"
|
||||
import { useOrder, useOrderPreview } from "../../../hooks/api/orders"
|
||||
import { useReturn } from "../../../hooks/api/returns"
|
||||
import { DEFAULT_FIELDS } from "../order-detail/constants"
|
||||
import { ExchangeCreateForm } from "./components/exchange-create-form"
|
||||
|
||||
let IS_REQUEST_RUNNING = false
|
||||
|
||||
export const ExchangeCreate = () => {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { order } = useOrder(id!, {
|
||||
fields: DEFAULT_FIELDS,
|
||||
})
|
||||
|
||||
const { order: preview } = useOrderPreview(id!)
|
||||
const [activeExchangeId, setActiveExchangeId] = useState<string>()
|
||||
const { mutateAsync: createExchange } = useCreateExchange(order.id)
|
||||
|
||||
const { exchange } = useExchange(activeExchangeId!, undefined, {
|
||||
enabled: !!activeExchangeId,
|
||||
})
|
||||
|
||||
const { return: orderReturn } = useReturn(exchange?.return_id!, undefined, {
|
||||
enabled: !!exchange?.return_id,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
if (IS_REQUEST_RUNNING || !preview) {
|
||||
return
|
||||
}
|
||||
|
||||
if (preview.order_change) {
|
||||
if (preview.order_change.change_type === "exchange") {
|
||||
setActiveExchangeId(preview.order_change.exchange_id)
|
||||
} else {
|
||||
navigate(`/orders/${preview.id}`, { replace: true })
|
||||
toast.error(t("orders.exchanges.activeChangeError"))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
IS_REQUEST_RUNNING = true
|
||||
|
||||
try {
|
||||
const { exchange: createdExchange } = await createExchange({
|
||||
order_id: preview.id,
|
||||
})
|
||||
|
||||
setActiveExchangeId(createdExchange.id)
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
navigate(`/orders/${preview.id}`, { replace: true })
|
||||
} finally {
|
||||
IS_REQUEST_RUNNING = false
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
}, [preview])
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{exchange && preview && order && (
|
||||
<ExchangeCreateForm
|
||||
order={order}
|
||||
exchange={exchange}
|
||||
preview={preview}
|
||||
orderReturn={orderReturn}
|
||||
/>
|
||||
)}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ExchangeCreate as Component } from "./exchange-create"
|
||||
@@ -42,10 +42,13 @@ export function OrderCreateFulfillmentForm({
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateFulfillmentSchema>>({
|
||||
defaultValues: {
|
||||
quantity: fulfillableItems.reduce((acc, item) => {
|
||||
acc[item.id] = getFulfillableQuantity(item)
|
||||
return acc
|
||||
}, {} as Record<string, number>),
|
||||
quantity: fulfillableItems.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.id] = getFulfillableQuantity(item)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, number>
|
||||
),
|
||||
send_notification: !order.no_notification,
|
||||
},
|
||||
resolver: zodResolver(CreateFulfillmentSchema),
|
||||
@@ -108,6 +111,16 @@ export function OrderCreateFulfillmentForm({
|
||||
}
|
||||
}, [fulfillableItems.length])
|
||||
|
||||
useEffect(() => {
|
||||
const itemsToFulfill = order?.items?.filter(
|
||||
(item) => getFulfillableQuantity(item) > 0
|
||||
)
|
||||
|
||||
if (itemsToFulfill?.length) {
|
||||
setFulfillableItems(itemsToFulfill)
|
||||
}
|
||||
}, [order.items])
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
|
||||
@@ -5,16 +5,18 @@ import { useNavigate } from "react-router-dom"
|
||||
import {
|
||||
ArrowDownRightMini,
|
||||
ArrowLongRight,
|
||||
ArrowPath,
|
||||
ArrowUturnLeft,
|
||||
DocumentText,
|
||||
ExclamationCircle,
|
||||
} from "@medusajs/icons"
|
||||
import {
|
||||
AdminClaim,
|
||||
AdminExchange,
|
||||
AdminOrder,
|
||||
AdminOrderLineItem,
|
||||
AdminOrderPreview,
|
||||
AdminReturn,
|
||||
OrderLineItemDTO,
|
||||
ReservationItemDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
@@ -32,6 +34,7 @@ import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { useClaims } from "../../../../../hooks/api/claims.tsx"
|
||||
import { useExchanges } from "../../../../../hooks/api/exchanges.tsx"
|
||||
import { useOrderPreview } from "../../../../../hooks/api/orders.tsx"
|
||||
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
||||
import { useReturns } from "../../../../../hooks/api/returns"
|
||||
@@ -165,13 +168,35 @@ const Header = ({
|
||||
label: t("orders.returns.create"),
|
||||
to: `/orders/${order.id}/returns`,
|
||||
icon: <ArrowUturnLeft />,
|
||||
disabled:
|
||||
!!orderPreview?.order_change?.exchange_id ||
|
||||
!!orderPreview?.order_change?.claim_id,
|
||||
},
|
||||
{
|
||||
label: orderPreview?.order_change?.id
|
||||
? t("orders.claims.manage")
|
||||
: t("orders.claims.create"),
|
||||
label:
|
||||
orderPreview?.order_change?.id &&
|
||||
orderPreview?.order_change?.exchange_id
|
||||
? t("orders.exchanges.manage")
|
||||
: t("orders.exchanges.create"),
|
||||
to: `/orders/${order.id}/exchanges`,
|
||||
icon: <ArrowPath />,
|
||||
disabled:
|
||||
(!!orderPreview?.order_change?.return_id &&
|
||||
!!!orderPreview?.order_change?.exchange_id) ||
|
||||
!!orderPreview?.order_change?.claim_id,
|
||||
},
|
||||
{
|
||||
label:
|
||||
orderPreview?.order_change?.id &&
|
||||
orderPreview?.order_change?.claim_id
|
||||
? t("orders.claims.manage")
|
||||
: t("orders.claims.create"),
|
||||
to: `/orders/${order.id}/claims`,
|
||||
icon: <ExclamationCircle />,
|
||||
disabled:
|
||||
(!!orderPreview?.order_change?.return_id &&
|
||||
!!!orderPreview?.order_change?.claim_id) ||
|
||||
!!orderPreview?.order_change?.exchange_id,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -187,15 +212,17 @@ const Item = ({
|
||||
reservation,
|
||||
returns,
|
||||
claims,
|
||||
exchanges,
|
||||
}: {
|
||||
item: OrderLineItemDTO
|
||||
item: AdminOrderLineItem
|
||||
currencyCode: string
|
||||
reservation?: ReservationItemDTO | null
|
||||
returns: AdminReturn[]
|
||||
claims: AdminClaim[]
|
||||
exchanges: AdminExchange[]
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isInventoryManaged = item.variant.manage_inventory
|
||||
const isInventoryManaged = item.variant?.manage_inventory
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -214,6 +241,7 @@ const Item = ({
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
|
||||
{item.variant_sku && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text size="small">{item.variant_sku}</Text>
|
||||
@@ -221,22 +249,25 @@ const Item = ({
|
||||
</div>
|
||||
)}
|
||||
<Text size="small">
|
||||
{item.variant?.options.map((o) => o.value).join(" · ")}
|
||||
{item.variant?.options?.map((o) => o.value).join(" · ")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 items-center gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-4">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(item.unit_price, currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="w-fit min-w-[27px]">
|
||||
<Text size="small">
|
||||
<span className="tabular-nums">{item.quantity}</span>x
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="overflow-visible">
|
||||
{isInventoryManaged && (
|
||||
<StatusBadge
|
||||
@@ -250,6 +281,7 @@ const Item = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small" className="pt-[1px]">
|
||||
{getLocaleAmount(item.subtotal || 0, currencyCode)}
|
||||
@@ -257,6 +289,7 @@ const Item = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{returns.map((r) => (
|
||||
<ReturnBreakdown key={r.id} orderReturn={r} itemId={item.id} />
|
||||
))}
|
||||
@@ -264,6 +297,14 @@ const Item = ({
|
||||
{claims.map((claim) => (
|
||||
<ClaimBreakdown key={claim.id} claim={claim} itemId={item.id} />
|
||||
))}
|
||||
|
||||
{exchanges.map((exchange) => (
|
||||
<ExchangeBreakdown
|
||||
key={exchange.id}
|
||||
exchange={exchange}
|
||||
itemId={item.id}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -273,41 +314,24 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => {
|
||||
line_item_id: order.items.map((i) => i.id),
|
||||
})
|
||||
|
||||
const { claims } = useClaims({
|
||||
const { claims = [] } = useClaims({
|
||||
order_id: order.id,
|
||||
fields: "*additional_items",
|
||||
})
|
||||
|
||||
const { returns } = useReturns({
|
||||
const { exchanges = [] } = useExchanges({
|
||||
order_id: order.id,
|
||||
fields: "*additional_items",
|
||||
})
|
||||
|
||||
const { returns = [] } = useReturns({
|
||||
order_id: order.id,
|
||||
fields: "*items,*items.reason",
|
||||
})
|
||||
|
||||
const itemsReturnsMap = useMemo(() => {
|
||||
if (!returns) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const ret = {}
|
||||
|
||||
order.items?.forEach((i) => {
|
||||
returns.forEach((r) => {
|
||||
if (r.items.some((ri) => ri.item_id === i.id)) {
|
||||
if (ret[i.id]) {
|
||||
ret[i.id].push(r)
|
||||
} else {
|
||||
ret[i.id] = [r]
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return ret
|
||||
}, [returns])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{order.items.map((item) => {
|
||||
{order.items?.map((item) => {
|
||||
const reservation = reservations
|
||||
? reservations.find((r) => r.line_item_id === item.id)
|
||||
: null
|
||||
@@ -318,8 +342,9 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => {
|
||||
item={item}
|
||||
currencyCode={order.currency_code}
|
||||
reservation={reservation}
|
||||
returns={itemsReturnsMap[item.id] || []}
|
||||
claims={claims || []}
|
||||
returns={returns}
|
||||
exchanges={exchanges}
|
||||
claims={claims}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -490,6 +515,45 @@ const ClaimBreakdown = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ExchangeBreakdown = ({
|
||||
exchange,
|
||||
itemId,
|
||||
}: {
|
||||
exchange: AdminExchange
|
||||
itemId: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { getRelativeDate } = useDate()
|
||||
const items = exchange.additional_items.filter(
|
||||
(item) => item?.item?.id === itemId
|
||||
)
|
||||
|
||||
return (
|
||||
!!items.length && (
|
||||
<div
|
||||
key={exchange.id}
|
||||
className="txt-compact-small-plus text-ui-fg-subtle bg-ui-bg-subtle border-dotted border-t-2 border-b-2 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>
|
||||
{t(`orders.exchanges.outboundItemAdded`, {
|
||||
itemsCount: items.reduce(
|
||||
(acc, item) => (acc = acc + item.quantity),
|
||||
0
|
||||
),
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||
{getRelativeDate(exchange.created_at)}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const Total = ({ order }: { order: AdminOrder }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -524,9 +588,13 @@ const Total = ({ order }: { order: AdminOrder }) => {
|
||||
>
|
||||
{t("orders.returns.outstandingAmount")}
|
||||
</Text>
|
||||
<Text className="text-ui-fg-subtle" size="small" leading="compact">
|
||||
<Text
|
||||
className="text-ui-fg-subtle text-bold"
|
||||
size="small"
|
||||
leading="compact"
|
||||
>
|
||||
{getStylizedAmount(
|
||||
order.summary.difference_sum || 0,
|
||||
order.summary.pending_difference || 0,
|
||||
order.currency_code
|
||||
)}
|
||||
</Text>
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
OrderExchangeDTO,
|
||||
OrderPreviewDTO,
|
||||
} from "@medusajs/types"
|
||||
import { ChangeActionType, Modules, OrderChangeStatus } from "@medusajs/utils"
|
||||
import {
|
||||
ChangeActionType,
|
||||
Modules,
|
||||
OrderChangeStatus,
|
||||
ReturnStatus,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
WorkflowResponse,
|
||||
createStep,
|
||||
@@ -19,7 +24,7 @@ import { createRemoteLinkStep, useRemoteQueryStep } from "../../../common"
|
||||
import { reserveInventoryStep } from "../../../definition/cart/steps/reserve-inventory"
|
||||
import { prepareConfirmInventoryInput } from "../../../definition/cart/utils/prepare-confirm-inventory-input"
|
||||
import { createReturnFulfillmentWorkflow } from "../../../fulfillment/workflows/create-return-fulfillment"
|
||||
import { previewOrderChangeStep } from "../../steps"
|
||||
import { previewOrderChangeStep, updateReturnsStep } from "../../steps"
|
||||
import { confirmOrderChanges } from "../../steps/confirm-order-changes"
|
||||
import { createOrderExchangeItemsFromActionsStep } from "../../steps/exchange/create-exchange-items-from-actions"
|
||||
import { createReturnItemsFromActionsStep } from "../../steps/return/create-return-items-from-actions"
|
||||
@@ -57,7 +62,6 @@ function prepareFulfillmentData({
|
||||
items,
|
||||
shippingOption,
|
||||
deliveryAddress,
|
||||
isReturn,
|
||||
}: {
|
||||
order: OrderDTO
|
||||
items: any[]
|
||||
@@ -74,17 +78,15 @@ function prepareFulfillmentData({
|
||||
}
|
||||
}
|
||||
deliveryAddress?: Record<string, any>
|
||||
isReturn?: boolean
|
||||
}) {
|
||||
const orderItemsMap = new Map<string, Required<OrderDTO>["items"][0]>(
|
||||
order.items!.map((i) => [i.id, i])
|
||||
)
|
||||
const fulfillmentItems = items.map((i) => {
|
||||
const orderItem = orderItemsMap.get(i.item_id) ?? i.item
|
||||
const orderItem = orderItemsMap.get(i.id) ?? i.item
|
||||
return {
|
||||
line_item_id: i.item_id,
|
||||
quantity: !isReturn ? i.quantity : undefined,
|
||||
return_quantity: isReturn ? i.quantity : undefined,
|
||||
quantity: i.quantity,
|
||||
title: orderItem.variant_title ?? orderItem.title,
|
||||
sku: orderItem.variant_sku || "",
|
||||
barcode: orderItem.variant_barcode || "",
|
||||
@@ -191,11 +193,8 @@ export const confirmExchangeRequestWorkflow = createWorkflow(
|
||||
"id",
|
||||
"version",
|
||||
"canceled_at",
|
||||
"items.id",
|
||||
"items.title",
|
||||
"items.variant_title",
|
||||
"items.variant_sku",
|
||||
"items.variant_barcode",
|
||||
"items.*",
|
||||
"items.item.id",
|
||||
"shipping_address.*",
|
||||
],
|
||||
variables: { id: orderExchange.order_id },
|
||||
@@ -248,6 +247,18 @@ export const confirmExchangeRequestWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
when({ returnId }, ({ returnId }) => {
|
||||
return !!returnId
|
||||
}).then(() => {
|
||||
updateReturnsStep([
|
||||
{
|
||||
id: returnId,
|
||||
status: ReturnStatus.REQUESTED,
|
||||
requested_at: new Date(),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
const exchangeId = transform(
|
||||
{ createExchangeItems },
|
||||
({ createExchangeItems }) => {
|
||||
@@ -320,9 +331,12 @@ export const confirmExchangeRequestWorkflow = createWorkflow(
|
||||
reserveInventoryStep(formatedInventoryItems)
|
||||
})
|
||||
|
||||
when({ returnShippingMethod }, ({ returnShippingMethod }) => {
|
||||
return !!returnShippingMethod
|
||||
}).then(() => {
|
||||
when(
|
||||
{ returnShippingMethod, returnId },
|
||||
({ returnShippingMethod, returnId }) => {
|
||||
return !!returnShippingMethod && !!returnId
|
||||
}
|
||||
).then(() => {
|
||||
const returnShippingOption = useRemoteQueryStep({
|
||||
entry_point: "shipping_options",
|
||||
fields: [
|
||||
@@ -343,7 +357,6 @@ export const confirmExchangeRequestWorkflow = createWorkflow(
|
||||
order,
|
||||
items: order.items!,
|
||||
shippingOption: returnShippingOption,
|
||||
isReturn: true,
|
||||
},
|
||||
prepareFulfillmentData
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useRemoteQueryStep } from "../../../common"
|
||||
import { updateOrderExchangesStep } from "../../steps/exchange/update-order-exchanges"
|
||||
import { previewOrderChangeStep } from "../../steps/preview-order-change"
|
||||
import { createReturnsStep } from "../../steps/return/create-returns"
|
||||
import { updateOrderChangesStep } from "../../steps/update-order-changes"
|
||||
import {
|
||||
throwIfIsCancelled,
|
||||
throwIfItemsDoesNotExistsInOrder,
|
||||
@@ -124,6 +125,17 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow(
|
||||
status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED],
|
||||
})
|
||||
|
||||
when({ createdReturn }, ({ createdReturn }) => {
|
||||
return !!createdReturn?.length
|
||||
}).then(() => {
|
||||
updateOrderChangesStep([
|
||||
{
|
||||
id: orderChange.id,
|
||||
return_id: createdReturn?.[0]?.id,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
exchangeRequestItemReturnValidationStep({
|
||||
order,
|
||||
items: input.items,
|
||||
|
||||
371
packages/core/js-sdk/src/admin/exchange.ts
Normal file
371
packages/core/js-sdk/src/admin/exchange.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
import { Client } from "../client"
|
||||
import { ClientHeaders } from "../types"
|
||||
|
||||
export class Exchange {
|
||||
private client: Client
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async list(
|
||||
query?: HttpTypes.AdminExchangeListParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeListResponse>(
|
||||
`/admin/exchanges`,
|
||||
{
|
||||
query,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
id: string,
|
||||
query?: HttpTypes.AdminExchangeParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}`,
|
||||
{
|
||||
query,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async create(
|
||||
body: HttpTypes.AdminCreateExchange,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async cancel(
|
||||
id: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/cancel`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async delete(
|
||||
id: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeDeleteResponse>(
|
||||
`/admin/exchanges/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async addItems(
|
||||
id: string,
|
||||
body: HttpTypes.AdminAddExchangeItems,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/exchange-items`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async updateItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
body: HttpTypes.AdminUpdateExchangeItem,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/exchange-items/${actionId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async removeItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminReturnResponse>(
|
||||
`/admin/exchanges/${id}/exchange-items/${actionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async addInboundItems(
|
||||
id: string,
|
||||
body: HttpTypes.AdminAddExchangeInboundItems,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/inbound/items`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async updateInboundItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
body: HttpTypes.AdminUpdateExchangeInboundItem,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/inbound/items/${actionId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async removeInboundItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/inbound/items/${actionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async addInboundShipping(
|
||||
id: string,
|
||||
body: HttpTypes.AdminExchangeAddInboundShipping,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/inbound/shipping-method`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async updateInboundShipping(
|
||||
id: string,
|
||||
actionId: string,
|
||||
body: HttpTypes.AdminExchangeUpdateInboundShipping,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/inbound/shipping-method/${actionId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async deleteInboundShipping(
|
||||
id: string,
|
||||
actionId: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/inbound/shipping-method/${actionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async addOutboundItems(
|
||||
id: string,
|
||||
body: HttpTypes.AdminAddExchangeOutboundItems,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/outbound/items`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async updateOutboundItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
body: HttpTypes.AdminUpdateExchangeOutboundItem,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/outbound/items/${actionId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async removeOutboundItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/outbound/items/${actionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async addOutboundShipping(
|
||||
id: string,
|
||||
body: HttpTypes.AdminExchangeAddOutboundShipping,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/outbound/shipping-method`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async updateOutboundShipping(
|
||||
id: string,
|
||||
actionId: string,
|
||||
body: HttpTypes.AdminExchangeUpdateOutboundShipping,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/outbound/shipping-method/${actionId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async deleteOutboundShipping(
|
||||
id: string,
|
||||
actionId: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/outbound/shipping-method/${actionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async request(
|
||||
id: string,
|
||||
body: HttpTypes.AdminRequestExchange,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/request`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async cancelRequest(
|
||||
id: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminExchangeResponse>(
|
||||
`/admin/exchanges/${id}/request`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Client } from "../client"
|
||||
import { Claim } from "./claim"
|
||||
import { Currency } from "./currency"
|
||||
import { Customer } from "./customer"
|
||||
import { Exchange } from "./exchange"
|
||||
import { Fulfillment } from "./fulfillment"
|
||||
import { FulfillmentProvider } from "./fulfillment-provider"
|
||||
import { FulfillmentSet } from "./fulfillment-set"
|
||||
@@ -56,6 +57,7 @@ export class Admin {
|
||||
public order: Order
|
||||
public return: Return
|
||||
public claim: Claim
|
||||
public exchange: Exchange
|
||||
public taxRate: TaxRate
|
||||
public taxRegion: TaxRegion
|
||||
public store: Store
|
||||
@@ -99,5 +101,6 @@ export class Admin {
|
||||
this.payment = new Payment(client)
|
||||
this.productVariant = new ProductVariant(client)
|
||||
this.refundReason = new RefundReason(client)
|
||||
this.exchange = new Exchange(client)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./entities"
|
||||
export * from "./responses"
|
||||
export * from "./payloads"
|
||||
export * from "./queries"
|
||||
export * from "./responses"
|
||||
|
||||
74
packages/core/types/src/http/exchange/admin/payloads.ts
Normal file
74
packages/core/types/src/http/exchange/admin/payloads.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
enum ExchangeReason {
|
||||
MISSING_ITEM = "missing_item",
|
||||
WRONG_ITEM = "wrong_item",
|
||||
PRODUCTION_FAILURE = "production_failure",
|
||||
OTHER = "other",
|
||||
}
|
||||
|
||||
interface AdminExchangeAddItems {
|
||||
items: {
|
||||
id: string
|
||||
quantity: number
|
||||
reason?: ExchangeReason
|
||||
description?: string
|
||||
internal_note?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
interface AdminExchangeUpdateItem {
|
||||
quantity?: number
|
||||
reason_id?: string | null
|
||||
description?: string
|
||||
internal_note?: string | null
|
||||
}
|
||||
|
||||
interface AdminExchangeAddShippingMethod {
|
||||
shipping_option_id: string
|
||||
custom_price?: number
|
||||
description?: string
|
||||
internal_note?: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
interface AdminExchangeUpdateShippingMethod {
|
||||
custom_price?: number | null
|
||||
internal_note?: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface AdminCreateExchange {
|
||||
type: "refund" | "replace"
|
||||
order_id: string
|
||||
description?: string
|
||||
internal_note?: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface AdminAddExchangeItems extends AdminExchangeAddItems {}
|
||||
export interface AdminUpdateExchangeItem extends AdminExchangeUpdateItem {}
|
||||
|
||||
export interface AdminAddExchangeInboundItems extends AdminExchangeAddItems {}
|
||||
export interface AdminUpdateExchangeInboundItem
|
||||
extends AdminExchangeUpdateItem {}
|
||||
|
||||
export interface AdminAddExchangeOutboundItems extends AdminExchangeAddItems {}
|
||||
export interface AdminUpdateExchangeOutboundItem
|
||||
extends AdminExchangeUpdateItem {}
|
||||
|
||||
export interface AdminExchangeAddInboundShipping
|
||||
extends AdminExchangeAddShippingMethod {}
|
||||
export interface AdminExchangeUpdateInboundShipping
|
||||
extends AdminExchangeUpdateShippingMethod {}
|
||||
|
||||
export interface AdminExchangeAddOutboundShipping
|
||||
extends AdminExchangeAddShippingMethod {}
|
||||
export interface AdminExchangeUpdateOutboundShipping
|
||||
extends AdminExchangeUpdateShippingMethod {}
|
||||
|
||||
export interface AdminRequestExchange {
|
||||
no_notification?: boolean
|
||||
}
|
||||
|
||||
export interface AdminCancelExchange {
|
||||
no_notification?: boolean
|
||||
}
|
||||
17
packages/core/types/src/http/exchange/admin/queries.ts
Normal file
17
packages/core/types/src/http/exchange/admin/queries.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { BaseFilterable, OperatorMap } from "../../../dal"
|
||||
import { SelectParams } from "../../common"
|
||||
import { BaseExchangeListParams } from "../common"
|
||||
|
||||
export interface AdminExchangeListParams
|
||||
extends BaseExchangeListParams,
|
||||
BaseFilterable<AdminExchangeListParams> {
|
||||
deleted_at?: OperatorMap<string>
|
||||
}
|
||||
|
||||
export interface AdminExchangeParams extends SelectParams {
|
||||
id?: string | string[]
|
||||
status?: string | string[]
|
||||
created_at?: OperatorMap<string>
|
||||
updated_at?: OperatorMap<string>
|
||||
deleted_at?: OperatorMap<string>
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OrderDTO, OrderPreviewDTO } from "../../../order"
|
||||
import { PaginatedResponse } from "../../common"
|
||||
import { DeleteResponse, PaginatedResponse } from "../../common"
|
||||
import { AdminReturn } from "../../return"
|
||||
import { AdminExchange } from "./entities"
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface AdminExchangeResponse {
|
||||
}
|
||||
|
||||
export type AdminExchangeListResponse = PaginatedResponse<{
|
||||
exchanges: AdminExchange
|
||||
exchanges: AdminExchange[]
|
||||
}>
|
||||
|
||||
export interface AdminExchangeOrderResponse {
|
||||
@@ -30,3 +30,6 @@ export interface AdminExchangeReturnResponse {
|
||||
order_preview: OrderPreviewDTO
|
||||
return: AdminReturn
|
||||
}
|
||||
|
||||
export interface AdminExchangeDeleteResponse
|
||||
extends DeleteResponse<"exchange"> {}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { BaseOrder } from "../order/common";
|
||||
import { AdminReturnItem, AdminReturn } from "../return";
|
||||
import { OperatorMap } from "../../dal"
|
||||
import { FindParams } from "../common"
|
||||
import { BaseOrder } from "../order/common"
|
||||
import { AdminReturn, AdminReturnItem } from "../return"
|
||||
|
||||
export interface BaseExchangeItem {
|
||||
id: string
|
||||
@@ -12,7 +14,8 @@ export interface BaseExchangeItem {
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface BaseExchange extends Omit<BaseOrder, "status" | "version" | "items"> {
|
||||
export interface BaseExchange
|
||||
extends Omit<BaseOrder, "status" | "version" | "items"> {
|
||||
order_id: string
|
||||
return_items: AdminReturnItem[]
|
||||
additional_items: BaseExchangeItem[]
|
||||
@@ -20,4 +23,14 @@ export interface BaseExchange extends Omit<BaseOrder, "status" | "version" | "it
|
||||
difference_due?: number
|
||||
return?: AdminReturn
|
||||
return_id?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface BaseExchangeListParams extends FindParams {
|
||||
q?: string
|
||||
id?: string | string[]
|
||||
order_id?: string | string[]
|
||||
status?: string | string[]
|
||||
created_at?: OperatorMap<string>
|
||||
updated_at?: OperatorMap<string>
|
||||
deleted_at?: OperatorMap<string>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
removeItemReturnActionWorkflow,
|
||||
updateRequestItemReturnWorkflow,
|
||||
} from "@medusajs/core-flows"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
@@ -10,9 +11,9 @@ import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../../../../types/routing"
|
||||
import { refetchEntity } from "../../../../../../utils/refetch-entity"
|
||||
import { defaultAdminDetailsReturnFields } from "../../../../../returns/query-config"
|
||||
import { AdminPostExchangesRequestItemsReturnActionReqSchemaType } from "../../../../validators"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminPostExchangesRequestItemsReturnActionReqSchemaType>,
|
||||
@@ -69,11 +70,15 @@ export const DELETE = async (
|
||||
|
||||
const { id, action_id } = req.params
|
||||
|
||||
const exchange = await refetchEntity("order_exchange", id, req.scope, [
|
||||
"return_id",
|
||||
])
|
||||
|
||||
const { result: orderPreview } = await removeItemReturnActionWorkflow(
|
||||
req.scope
|
||||
).run({
|
||||
input: {
|
||||
return_id: id,
|
||||
return_id: exchange.return_id,
|
||||
action_id,
|
||||
},
|
||||
})
|
||||
@@ -81,7 +86,7 @@ export const DELETE = async (
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "return",
|
||||
variables: {
|
||||
id,
|
||||
id: exchange.return_id,
|
||||
filters: {
|
||||
...req.filterableFields,
|
||||
},
|
||||
|
||||
28
packages/medusa/src/api/admin/exchanges/[id]/route.ts
Normal file
28
packages/medusa/src/api/admin/exchanges/[id]/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { AdminExchangeResponse } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../types/routing"
|
||||
import { refetchEntity } from "../../../utils/refetch-entity"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse<AdminExchangeResponse>
|
||||
) => {
|
||||
const exchange = await refetchEntity(
|
||||
"order_exchange",
|
||||
req.params.id,
|
||||
req.scope,
|
||||
req.remoteQueryConfig.fields
|
||||
)
|
||||
|
||||
if (!exchange) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Exchange with id: ${req.params.id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
res.status(200).json({ exchange })
|
||||
}
|
||||
Reference in New Issue
Block a user