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:
1 parent
ba34c53151
commit
82c147b91e
36 files changed
+3660
-61
No files matched your search
@@ -11,6 +11,7 @@ import { sdk } from "../../lib/client"
|
|||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
import { ordersQueryKeys } from "./orders"
|
import { ordersQueryKeys } from "./orders"
|
||||||
|
import { returnsQueryKeys } from "./returns"
|
||||||
|
|
||||||
const CLAIMS_QUERY_KEY = "claims" as const
|
const CLAIMS_QUERY_KEY = "claims" as const
|
||||||
export const claimsQueryKeys = queryKeysFactory(CLAIMS_QUERY_KEY)
|
export const claimsQueryKeys = queryKeysFactory(CLAIMS_QUERY_KEY)
|
||||||
@@ -499,6 +500,10 @@ export const useClaimConfirmRequest = (
|
|||||||
mutationFn: (payload: HttpTypes.AdminRequestClaim) =>
|
mutationFn: (payload: HttpTypes.AdminRequestClaim) =>
|
||||||
sdk.admin.claim.request(id, payload),
|
sdk.admin.claim.request(id, payload),
|
||||||
onSuccess: (data: any, variables: any, context: any) => {
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: returnsQueryKeys.all,
|
||||||
|
})
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ordersQueryKeys.details(),
|
queryKey: ordersQueryKeys.details(),
|
||||||
})
|
})
|
||||||
@@ -509,6 +514,11 @@ export const useClaimConfirmRequest = (
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ordersQueryKeys.preview(orderId),
|
queryKey: ordersQueryKeys.preview(orderId),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: claimsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -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",
|
"returnTotal": "Return total",
|
||||||
"inboundTotal": "Inbound total",
|
"inboundTotal": "Inbound total",
|
||||||
"refundAmount": "Refund amount",
|
"refundAmount": "Refund amount",
|
||||||
"outstandingAmount": "Difference amount",
|
"outstandingAmount": "Outstanding amount",
|
||||||
"reason": "Reason",
|
"reason": "Reason",
|
||||||
"reasonHint": "Choose why the customer want to return items.",
|
"reasonHint": "Choose why the customer want to return items.",
|
||||||
"note": "Note",
|
"note": "Note",
|
||||||
@@ -904,6 +904,25 @@
|
|||||||
"onlyReturnShippingOptions": "This list will consist of only return shipping options."
|
"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": {
|
"reservations": {
|
||||||
"allocatedLabel": "Allocated",
|
"allocatedLabel": "Allocated",
|
||||||
"notAllocatedLabel": "Not allocated"
|
"notAllocatedLabel": "Not allocated"
|
||||||
|
|||||||
@@ -248,6 +248,11 @@ export const RouteMap: RouteObject[] = [
|
|||||||
lazy: () =>
|
lazy: () =>
|
||||||
import("../../routes/orders/order-create-claim"),
|
import("../../routes/orders/order-create-claim"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "exchanges",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/orders/order-create-exchange"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "payments/:paymentId/refund",
|
path: "payments/:paymentId/refund",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
|
|||||||
+186
@@ -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,
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./add-exchange-inbound-items-table"
|
||||||
+98
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+22
@@ -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
|
||||||
|
}
|
||||||
+26
@@ -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 }
|
||||||
|
}
|
||||||
+87
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./add-exchange-outbound-items-table"
|
||||||
+68
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+22
@@ -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
|
||||||
|
}
|
||||||
+26
@@ -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 }
|
||||||
|
}
|
||||||
+423
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+244
@@ -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 }
|
||||||
+527
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+121
@@ -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 }
|
||||||
+423
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./exchange-create-form"
|
||||||
+23
@@ -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>
|
||||||
+84
@@ -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"
|
||||||
+15
-2
@@ -42,10 +42,13 @@ export function OrderCreateFulfillmentForm({
|
|||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateFulfillmentSchema>>({
|
const form = useForm<zod.infer<typeof CreateFulfillmentSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
quantity: fulfillableItems.reduce((acc, item) => {
|
quantity: fulfillableItems.reduce(
|
||||||
|
(acc, item) => {
|
||||||
acc[item.id] = getFulfillableQuantity(item)
|
acc[item.id] = getFulfillableQuantity(item)
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, number>),
|
},
|
||||||
|
{} as Record<string, number>
|
||||||
|
),
|
||||||
send_notification: !order.no_notification,
|
send_notification: !order.no_notification,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateFulfillmentSchema),
|
resolver: zodResolver(CreateFulfillmentSchema),
|
||||||
@@ -108,6 +111,16 @@ export function OrderCreateFulfillmentForm({
|
|||||||
}
|
}
|
||||||
}, [fulfillableItems.length])
|
}, [fulfillableItems.length])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const itemsToFulfill = order?.items?.filter(
|
||||||
|
(item) => getFulfillableQuantity(item) > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if (itemsToFulfill?.length) {
|
||||||
|
setFulfillableItems(itemsToFulfill)
|
||||||
|
}
|
||||||
|
}, [order.items])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
+102
-34
@@ -5,16 +5,18 @@ import { useNavigate } from "react-router-dom"
|
|||||||
import {
|
import {
|
||||||
ArrowDownRightMini,
|
ArrowDownRightMini,
|
||||||
ArrowLongRight,
|
ArrowLongRight,
|
||||||
|
ArrowPath,
|
||||||
ArrowUturnLeft,
|
ArrowUturnLeft,
|
||||||
DocumentText,
|
DocumentText,
|
||||||
ExclamationCircle,
|
ExclamationCircle,
|
||||||
} from "@medusajs/icons"
|
} from "@medusajs/icons"
|
||||||
import {
|
import {
|
||||||
AdminClaim,
|
AdminClaim,
|
||||||
|
AdminExchange,
|
||||||
AdminOrder,
|
AdminOrder,
|
||||||
|
AdminOrderLineItem,
|
||||||
AdminOrderPreview,
|
AdminOrderPreview,
|
||||||
AdminReturn,
|
AdminReturn,
|
||||||
OrderLineItemDTO,
|
|
||||||
ReservationItemDTO,
|
ReservationItemDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +34,7 @@ import { ActionMenu } from "../../../../../components/common/action-menu"
|
|||||||
import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx"
|
import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx"
|
||||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||||
import { useClaims } from "../../../../../hooks/api/claims.tsx"
|
import { useClaims } from "../../../../../hooks/api/claims.tsx"
|
||||||
|
import { useExchanges } from "../../../../../hooks/api/exchanges.tsx"
|
||||||
import { useOrderPreview } from "../../../../../hooks/api/orders.tsx"
|
import { useOrderPreview } from "../../../../../hooks/api/orders.tsx"
|
||||||
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
||||||
import { useReturns } from "../../../../../hooks/api/returns"
|
import { useReturns } from "../../../../../hooks/api/returns"
|
||||||
@@ -165,13 +168,35 @@ const Header = ({
|
|||||||
label: t("orders.returns.create"),
|
label: t("orders.returns.create"),
|
||||||
to: `/orders/${order.id}/returns`,
|
to: `/orders/${order.id}/returns`,
|
||||||
icon: <ArrowUturnLeft />,
|
icon: <ArrowUturnLeft />,
|
||||||
|
disabled:
|
||||||
|
!!orderPreview?.order_change?.exchange_id ||
|
||||||
|
!!orderPreview?.order_change?.claim_id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: orderPreview?.order_change?.id
|
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.manage")
|
||||||
: t("orders.claims.create"),
|
: t("orders.claims.create"),
|
||||||
to: `/orders/${order.id}/claims`,
|
to: `/orders/${order.id}/claims`,
|
||||||
icon: <ExclamationCircle />,
|
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,
|
reservation,
|
||||||
returns,
|
returns,
|
||||||
claims,
|
claims,
|
||||||
|
exchanges,
|
||||||
}: {
|
}: {
|
||||||
item: OrderLineItemDTO
|
item: AdminOrderLineItem
|
||||||
currencyCode: string
|
currencyCode: string
|
||||||
reservation?: ReservationItemDTO | null
|
reservation?: ReservationItemDTO | null
|
||||||
returns: AdminReturn[]
|
returns: AdminReturn[]
|
||||||
claims: AdminClaim[]
|
claims: AdminClaim[]
|
||||||
|
exchanges: AdminExchange[]
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isInventoryManaged = item.variant.manage_inventory
|
const isInventoryManaged = item.variant?.manage_inventory
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -214,6 +241,7 @@ const Item = ({
|
|||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{item.variant_sku && (
|
{item.variant_sku && (
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<Text size="small">{item.variant_sku}</Text>
|
<Text size="small">{item.variant_sku}</Text>
|
||||||
@@ -221,22 +249,25 @@ const Item = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Text size="small">
|
<Text size="small">
|
||||||
{item.variant?.options.map((o) => o.value).join(" · ")}
|
{item.variant?.options?.map((o) => o.value).join(" · ")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 items-center gap-x-4">
|
<div className="grid grid-cols-3 items-center gap-x-4">
|
||||||
<div className="flex items-center justify-end gap-x-4">
|
<div className="flex items-center justify-end gap-x-4">
|
||||||
<Text size="small">
|
<Text size="small">
|
||||||
{getLocaleAmount(item.unit_price, currencyCode)}
|
{getLocaleAmount(item.unit_price, currencyCode)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<div className="w-fit min-w-[27px]">
|
<div className="w-fit min-w-[27px]">
|
||||||
<Text size="small">
|
<Text size="small">
|
||||||
<span className="tabular-nums">{item.quantity}</span>x
|
<span className="tabular-nums">{item.quantity}</span>x
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-visible">
|
<div className="overflow-visible">
|
||||||
{isInventoryManaged && (
|
{isInventoryManaged && (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
@@ -250,6 +281,7 @@ const Item = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<Text size="small" className="pt-[1px]">
|
<Text size="small" className="pt-[1px]">
|
||||||
{getLocaleAmount(item.subtotal || 0, currencyCode)}
|
{getLocaleAmount(item.subtotal || 0, currencyCode)}
|
||||||
@@ -257,6 +289,7 @@ const Item = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{returns.map((r) => (
|
{returns.map((r) => (
|
||||||
<ReturnBreakdown key={r.id} orderReturn={r} itemId={item.id} />
|
<ReturnBreakdown key={r.id} orderReturn={r} itemId={item.id} />
|
||||||
))}
|
))}
|
||||||
@@ -264,6 +297,14 @@ const Item = ({
|
|||||||
{claims.map((claim) => (
|
{claims.map((claim) => (
|
||||||
<ClaimBreakdown key={claim.id} claim={claim} itemId={item.id} />
|
<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),
|
line_item_id: order.items.map((i) => i.id),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { claims } = useClaims({
|
const { claims = [] } = useClaims({
|
||||||
order_id: order.id,
|
order_id: order.id,
|
||||||
fields: "*additional_items",
|
fields: "*additional_items",
|
||||||
})
|
})
|
||||||
|
|
||||||
const { returns } = useReturns({
|
const { exchanges = [] } = useExchanges({
|
||||||
|
order_id: order.id,
|
||||||
|
fields: "*additional_items",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { returns = [] } = useReturns({
|
||||||
order_id: order.id,
|
order_id: order.id,
|
||||||
fields: "*items,*items.reason",
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{order.items.map((item) => {
|
{order.items?.map((item) => {
|
||||||
const reservation = reservations
|
const reservation = reservations
|
||||||
? reservations.find((r) => r.line_item_id === item.id)
|
? reservations.find((r) => r.line_item_id === item.id)
|
||||||
: null
|
: null
|
||||||
@@ -318,8 +342,9 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => {
|
|||||||
item={item}
|
item={item}
|
||||||
currencyCode={order.currency_code}
|
currencyCode={order.currency_code}
|
||||||
reservation={reservation}
|
reservation={reservation}
|
||||||
returns={itemsReturnsMap[item.id] || []}
|
returns={returns}
|
||||||
claims={claims || []}
|
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 Total = ({ order }: { order: AdminOrder }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -524,9 +588,13 @@ const Total = ({ order }: { order: AdminOrder }) => {
|
|||||||
>
|
>
|
||||||
{t("orders.returns.outstandingAmount")}
|
{t("orders.returns.outstandingAmount")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-ui-fg-subtle" size="small" leading="compact">
|
<Text
|
||||||
|
className="text-ui-fg-subtle text-bold"
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
>
|
||||||
{getStylizedAmount(
|
{getStylizedAmount(
|
||||||
order.summary.difference_sum || 0,
|
order.summary.pending_difference || 0,
|
||||||
order.currency_code
|
order.currency_code
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import {
|
|||||||
OrderExchangeDTO,
|
OrderExchangeDTO,
|
||||||
OrderPreviewDTO,
|
OrderPreviewDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { ChangeActionType, Modules, OrderChangeStatus } from "@medusajs/utils"
|
import {
|
||||||
|
ChangeActionType,
|
||||||
|
Modules,
|
||||||
|
OrderChangeStatus,
|
||||||
|
ReturnStatus,
|
||||||
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
createStep,
|
createStep,
|
||||||
@@ -19,7 +24,7 @@ import { createRemoteLinkStep, useRemoteQueryStep } from "../../../common"
|
|||||||
import { reserveInventoryStep } from "../../../definition/cart/steps/reserve-inventory"
|
import { reserveInventoryStep } from "../../../definition/cart/steps/reserve-inventory"
|
||||||
import { prepareConfirmInventoryInput } from "../../../definition/cart/utils/prepare-confirm-inventory-input"
|
import { prepareConfirmInventoryInput } from "../../../definition/cart/utils/prepare-confirm-inventory-input"
|
||||||
import { createReturnFulfillmentWorkflow } from "../../../fulfillment/workflows/create-return-fulfillment"
|
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 { confirmOrderChanges } from "../../steps/confirm-order-changes"
|
||||||
import { createOrderExchangeItemsFromActionsStep } from "../../steps/exchange/create-exchange-items-from-actions"
|
import { createOrderExchangeItemsFromActionsStep } from "../../steps/exchange/create-exchange-items-from-actions"
|
||||||
import { createReturnItemsFromActionsStep } from "../../steps/return/create-return-items-from-actions"
|
import { createReturnItemsFromActionsStep } from "../../steps/return/create-return-items-from-actions"
|
||||||
@@ -57,7 +62,6 @@ function prepareFulfillmentData({
|
|||||||
items,
|
items,
|
||||||
shippingOption,
|
shippingOption,
|
||||||
deliveryAddress,
|
deliveryAddress,
|
||||||
isReturn,
|
|
||||||
}: {
|
}: {
|
||||||
order: OrderDTO
|
order: OrderDTO
|
||||||
items: any[]
|
items: any[]
|
||||||
@@ -74,17 +78,15 @@ function prepareFulfillmentData({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
deliveryAddress?: Record<string, any>
|
deliveryAddress?: Record<string, any>
|
||||||
isReturn?: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const orderItemsMap = new Map<string, Required<OrderDTO>["items"][0]>(
|
const orderItemsMap = new Map<string, Required<OrderDTO>["items"][0]>(
|
||||||
order.items!.map((i) => [i.id, i])
|
order.items!.map((i) => [i.id, i])
|
||||||
)
|
)
|
||||||
const fulfillmentItems = items.map((i) => {
|
const fulfillmentItems = items.map((i) => {
|
||||||
const orderItem = orderItemsMap.get(i.item_id) ?? i.item
|
const orderItem = orderItemsMap.get(i.id) ?? i.item
|
||||||
return {
|
return {
|
||||||
line_item_id: i.item_id,
|
line_item_id: i.item_id,
|
||||||
quantity: !isReturn ? i.quantity : undefined,
|
quantity: i.quantity,
|
||||||
return_quantity: isReturn ? i.quantity : undefined,
|
|
||||||
title: orderItem.variant_title ?? orderItem.title,
|
title: orderItem.variant_title ?? orderItem.title,
|
||||||
sku: orderItem.variant_sku || "",
|
sku: orderItem.variant_sku || "",
|
||||||
barcode: orderItem.variant_barcode || "",
|
barcode: orderItem.variant_barcode || "",
|
||||||
@@ -191,11 +193,8 @@ export const confirmExchangeRequestWorkflow = createWorkflow(
|
|||||||
"id",
|
"id",
|
||||||
"version",
|
"version",
|
||||||
"canceled_at",
|
"canceled_at",
|
||||||
"items.id",
|
"items.*",
|
||||||
"items.title",
|
"items.item.id",
|
||||||
"items.variant_title",
|
|
||||||
"items.variant_sku",
|
|
||||||
"items.variant_barcode",
|
|
||||||
"shipping_address.*",
|
"shipping_address.*",
|
||||||
],
|
],
|
||||||
variables: { id: orderExchange.order_id },
|
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(
|
const exchangeId = transform(
|
||||||
{ createExchangeItems },
|
{ createExchangeItems },
|
||||||
({ createExchangeItems }) => {
|
({ createExchangeItems }) => {
|
||||||
@@ -320,9 +331,12 @@ export const confirmExchangeRequestWorkflow = createWorkflow(
|
|||||||
reserveInventoryStep(formatedInventoryItems)
|
reserveInventoryStep(formatedInventoryItems)
|
||||||
})
|
})
|
||||||
|
|
||||||
when({ returnShippingMethod }, ({ returnShippingMethod }) => {
|
when(
|
||||||
return !!returnShippingMethod
|
{ returnShippingMethod, returnId },
|
||||||
}).then(() => {
|
({ returnShippingMethod, returnId }) => {
|
||||||
|
return !!returnShippingMethod && !!returnId
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
const returnShippingOption = useRemoteQueryStep({
|
const returnShippingOption = useRemoteQueryStep({
|
||||||
entry_point: "shipping_options",
|
entry_point: "shipping_options",
|
||||||
fields: [
|
fields: [
|
||||||
@@ -343,7 +357,6 @@ export const confirmExchangeRequestWorkflow = createWorkflow(
|
|||||||
order,
|
order,
|
||||||
items: order.items!,
|
items: order.items!,
|
||||||
shippingOption: returnShippingOption,
|
shippingOption: returnShippingOption,
|
||||||
isReturn: true,
|
|
||||||
},
|
},
|
||||||
prepareFulfillmentData
|
prepareFulfillmentData
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { useRemoteQueryStep } from "../../../common"
|
|||||||
import { updateOrderExchangesStep } from "../../steps/exchange/update-order-exchanges"
|
import { updateOrderExchangesStep } from "../../steps/exchange/update-order-exchanges"
|
||||||
import { previewOrderChangeStep } from "../../steps/preview-order-change"
|
import { previewOrderChangeStep } from "../../steps/preview-order-change"
|
||||||
import { createReturnsStep } from "../../steps/return/create-returns"
|
import { createReturnsStep } from "../../steps/return/create-returns"
|
||||||
|
import { updateOrderChangesStep } from "../../steps/update-order-changes"
|
||||||
import {
|
import {
|
||||||
throwIfIsCancelled,
|
throwIfIsCancelled,
|
||||||
throwIfItemsDoesNotExistsInOrder,
|
throwIfItemsDoesNotExistsInOrder,
|
||||||
@@ -124,6 +125,17 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow(
|
|||||||
status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED],
|
status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
when({ createdReturn }, ({ createdReturn }) => {
|
||||||
|
return !!createdReturn?.length
|
||||||
|
}).then(() => {
|
||||||
|
updateOrderChangesStep([
|
||||||
|
{
|
||||||
|
id: orderChange.id,
|
||||||
|
return_id: createdReturn?.[0]?.id,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
exchangeRequestItemReturnValidationStep({
|
exchangeRequestItemReturnValidationStep({
|
||||||
order,
|
order,
|
||||||
items: input.items,
|
items: input.items,
|
||||||
|
|||||||
@@ -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 { Claim } from "./claim"
|
||||||
import { Currency } from "./currency"
|
import { Currency } from "./currency"
|
||||||
import { Customer } from "./customer"
|
import { Customer } from "./customer"
|
||||||
|
import { Exchange } from "./exchange"
|
||||||
import { Fulfillment } from "./fulfillment"
|
import { Fulfillment } from "./fulfillment"
|
||||||
import { FulfillmentProvider } from "./fulfillment-provider"
|
import { FulfillmentProvider } from "./fulfillment-provider"
|
||||||
import { FulfillmentSet } from "./fulfillment-set"
|
import { FulfillmentSet } from "./fulfillment-set"
|
||||||
@@ -56,6 +57,7 @@ export class Admin {
|
|||||||
public order: Order
|
public order: Order
|
||||||
public return: Return
|
public return: Return
|
||||||
public claim: Claim
|
public claim: Claim
|
||||||
|
public exchange: Exchange
|
||||||
public taxRate: TaxRate
|
public taxRate: TaxRate
|
||||||
public taxRegion: TaxRegion
|
public taxRegion: TaxRegion
|
||||||
public store: Store
|
public store: Store
|
||||||
@@ -99,5 +101,6 @@ export class Admin {
|
|||||||
this.payment = new Payment(client)
|
this.payment = new Payment(client)
|
||||||
this.productVariant = new ProductVariant(client)
|
this.productVariant = new ProductVariant(client)
|
||||||
this.refundReason = new RefundReason(client)
|
this.refundReason = new RefundReason(client)
|
||||||
|
this.exchange = new Exchange(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from "./entities"
|
export * from "./entities"
|
||||||
|
export * from "./payloads"
|
||||||
|
export * from "./queries"
|
||||||
export * from "./responses"
|
export * from "./responses"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 { OrderDTO, OrderPreviewDTO } from "../../../order"
|
||||||
import { PaginatedResponse } from "../../common"
|
import { DeleteResponse, PaginatedResponse } from "../../common"
|
||||||
import { AdminReturn } from "../../return"
|
import { AdminReturn } from "../../return"
|
||||||
import { AdminExchange } from "./entities"
|
import { AdminExchange } from "./entities"
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ export interface AdminExchangeResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AdminExchangeListResponse = PaginatedResponse<{
|
export type AdminExchangeListResponse = PaginatedResponse<{
|
||||||
exchanges: AdminExchange
|
exchanges: AdminExchange[]
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export interface AdminExchangeOrderResponse {
|
export interface AdminExchangeOrderResponse {
|
||||||
@@ -30,3 +30,6 @@ export interface AdminExchangeReturnResponse {
|
|||||||
order_preview: OrderPreviewDTO
|
order_preview: OrderPreviewDTO
|
||||||
return: AdminReturn
|
return: AdminReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminExchangeDeleteResponse
|
||||||
|
extends DeleteResponse<"exchange"> {}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { BaseOrder } from "../order/common";
|
import { OperatorMap } from "../../dal"
|
||||||
import { AdminReturnItem, AdminReturn } from "../return";
|
import { FindParams } from "../common"
|
||||||
|
import { BaseOrder } from "../order/common"
|
||||||
|
import { AdminReturn, AdminReturnItem } from "../return"
|
||||||
|
|
||||||
export interface BaseExchangeItem {
|
export interface BaseExchangeItem {
|
||||||
id: string
|
id: string
|
||||||
@@ -12,7 +14,8 @@ export interface BaseExchangeItem {
|
|||||||
updated_at: string | null
|
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
|
order_id: string
|
||||||
return_items: AdminReturnItem[]
|
return_items: AdminReturnItem[]
|
||||||
additional_items: BaseExchangeItem[]
|
additional_items: BaseExchangeItem[]
|
||||||
@@ -21,3 +24,13 @@ export interface BaseExchange extends Omit<BaseOrder, "status" | "version" | "it
|
|||||||
return?: AdminReturn
|
return?: AdminReturn
|
||||||
return_id?: string
|
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,
|
removeItemReturnActionWorkflow,
|
||||||
updateRequestItemReturnWorkflow,
|
updateRequestItemReturnWorkflow,
|
||||||
} from "@medusajs/core-flows"
|
} from "@medusajs/core-flows"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
remoteQueryObjectFromString,
|
remoteQueryObjectFromString,
|
||||||
@@ -10,9 +11,9 @@ import {
|
|||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../../../../../types/routing"
|
} from "../../../../../../../types/routing"
|
||||||
|
import { refetchEntity } from "../../../../../../utils/refetch-entity"
|
||||||
import { defaultAdminDetailsReturnFields } from "../../../../../returns/query-config"
|
import { defaultAdminDetailsReturnFields } from "../../../../../returns/query-config"
|
||||||
import { AdminPostExchangesRequestItemsReturnActionReqSchemaType } from "../../../../validators"
|
import { AdminPostExchangesRequestItemsReturnActionReqSchemaType } from "../../../../validators"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: AuthenticatedMedusaRequest<AdminPostExchangesRequestItemsReturnActionReqSchemaType>,
|
req: AuthenticatedMedusaRequest<AdminPostExchangesRequestItemsReturnActionReqSchemaType>,
|
||||||
@@ -69,11 +70,15 @@ export const DELETE = async (
|
|||||||
|
|
||||||
const { id, action_id } = req.params
|
const { id, action_id } = req.params
|
||||||
|
|
||||||
|
const exchange = await refetchEntity("order_exchange", id, req.scope, [
|
||||||
|
"return_id",
|
||||||
|
])
|
||||||
|
|
||||||
const { result: orderPreview } = await removeItemReturnActionWorkflow(
|
const { result: orderPreview } = await removeItemReturnActionWorkflow(
|
||||||
req.scope
|
req.scope
|
||||||
).run({
|
).run({
|
||||||
input: {
|
input: {
|
||||||
return_id: id,
|
return_id: exchange.return_id,
|
||||||
action_id,
|
action_id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -81,7 +86,7 @@ export const DELETE = async (
|
|||||||
const queryObject = remoteQueryObjectFromString({
|
const queryObject = remoteQueryObjectFromString({
|
||||||
entryPoint: "return",
|
entryPoint: "return",
|
||||||
variables: {
|
variables: {
|
||||||
id,
|
id: exchange.return_id,
|
||||||
filters: {
|
filters: {
|
||||||
...req.filterableFields,
|
...req.filterableFields,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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