diff --git a/packages/admin/dashboard/src/components/forms/transfer-ownership-form/index.ts b/packages/admin/dashboard/src/components/forms/transfer-ownership-form/index.ts deleted file mode 100644 index b8021da3f9..0000000000 --- a/packages/admin/dashboard/src/components/forms/transfer-ownership-form/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./transfer-ownership-form" diff --git a/packages/admin/dashboard/src/components/forms/transfer-ownership-form/transfer-ownership-form.tsx b/packages/admin/dashboard/src/components/forms/transfer-ownership-form/transfer-ownership-form.tsx deleted file mode 100644 index 7c76549c0a..0000000000 --- a/packages/admin/dashboard/src/components/forms/transfer-ownership-form/transfer-ownership-form.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { Select, Text, clx } from "@medusajs/ui" -import { useInfiniteQuery } from "@tanstack/react-query" -import { format } from "date-fns" -import { debounce } from "lodash" -import { PropsWithChildren, useCallback, useEffect, useState } from "react" -import { Control, useWatch } from "react-hook-form" -import { useTranslation } from "react-i18next" -import { z } from "zod" - -import { useCustomer } from "../../../hooks/api/customers" -import { sdk } from "../../../lib/client" -import { getStylizedAmount } from "../../../lib/money-amount-helpers" -import { - getOrderFulfillmentStatus, - getOrderPaymentStatus, -} from "../../../lib/order-helpers" -import { TransferOwnershipSchema } from "../../../lib/schemas" -import { Form } from "../../common/form" -import { Skeleton } from "../../common/skeleton" -import { Combobox } from "../../inputs/combobox" -import { HttpTypes } from "@medusajs/types" - -type TransferOwnerShipFieldValues = z.infer - -type TransferOwnerShipFormProps = { - /** - * The Order or DraftOrder to transfer ownership of. - */ - order: HttpTypes.AdminOrder - /** - * React Hook Form control object. - */ - control: Control -} - -const isOrder = ( - order: HttpTypes.AdminOrder -): order is HttpTypes.AdminOrder => { - return "customer" in order -} - -export const TransferOwnerShipForm = ({ - order, - control, -}: TransferOwnerShipFormProps) => { - const { t } = useTranslation() - - const [query, setQuery] = useState("") - const [debouncedQuery, setDebouncedQuery] = useState("") - - const isOrderType = isOrder(order) - const currentOwnerId = useWatch({ - control, - name: "current_owner_id", - }) - - // eslint-disable-next-line react-hooks/exhaustive-deps - const debouncedUpdate = useCallback( - debounce((query) => setDebouncedQuery(query), 300), - [] - ) - - useEffect(() => { - debouncedUpdate(query) - - return () => debouncedUpdate.cancel() - }, [query, debouncedUpdate]) - - const { - customer: owner, - isLoading: isLoadingOwner, - isError: isOwnerError, - error: ownerError, - } = useCustomer(currentOwnerId) - - const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({ - queryKey: ["customers", debouncedQuery], - queryFn: async ({ pageParam = 0 }) => { - const res = await sdk.admin.customer.list({ - q: debouncedQuery, - limit: 10, - offset: pageParam, - has_account: true, // Only show customers with confirmed accounts - }) - return res - }, - initialPageParam: 0, - getNextPageParam: (lastPage) => { - const moreCustomersExist = - lastPage.count > lastPage.offset + lastPage.limit - return moreCustomersExist ? lastPage.offset + lastPage.limit : undefined - }, - }) - - const createLabel = (customer?: HttpTypes.AdminCustomer) => { - if (!customer) { - return "" - } - - const { first_name, last_name, email } = customer - - const name = [first_name, last_name].filter(Boolean).join(" ") - - if (name) { - return `${name} (${email})` - } - - return email - } - - const ownerReady = !isLoadingOwner && owner - - const options = - data?.pages - .map((p) => - p.customers.map((c) => ({ - label: createLabel(c), - value: c.id, - })) - ) - .flat() || [] - - if (isOwnerError) { - throw ownerError - } - - return ( -
-
- - {isOrderType - ? t("transferOwnership.details.order") - : t("transferOwnership.details.draft")} - - {isOrderType ? ( - - ) : ( - - )} -
-
-
- - {t("transferOwnership.currentOwner.label")} - - - {t("transferOwnership.currentOwner.hint")} - -
- {ownerReady ? ( - - ) : ( - - )} -
- { - return ( - -
- {t("transferOwnership.newOwner.label")} - {t("transferOwnership.newOwner.hint")} -
- - - - -
- ) - }} - /> -
- ) -} - -const OrderDetailsTable = ({ order }: { order: HttpTypes.AdminOrder }) => { - const { t } = useTranslation() - - const { label: fulfillmentLabel } = getOrderFulfillmentStatus( - t, - order.fulfillment_status - ) - - const { label: paymentLabel } = getOrderPaymentStatus(t, order.payment_status) - - return ( - - - - - - -
- ) -} - -// TODO: Create type for Draft Order when we have it -const DraftOrderDetailsTable = ({ draft }: { draft: HttpTypes.AdminOrder }) => { - const { t } = useTranslation() - - return ( - - - - - - {/* TODO: This will likely change. We don't use carts for draft orders any longer. */} - {/* */} -
- ) -} - -const DateRow = ({ date }: { date: string | Date }) => { - const { t } = useTranslation() - - const formattedDate = format(new Date(date), "dd MMM yyyy") - - return -} - -const TotalRow = ({ - total, - currencyCode, -}: { - total: number - currencyCode: string -}) => { - return -} - -const Row = ({ label, value }: { label: string; value: string }) => { - return ( -
-
{label}
-
{value}
-
- ) -} - -const Table = ({ children }: PropsWithChildren) => { - return
{children}
-} diff --git a/packages/admin/dashboard/src/hooks/api/orders.tsx b/packages/admin/dashboard/src/hooks/api/orders.tsx index 17eddc4b5f..4e092a4b70 100644 --- a/packages/admin/dashboard/src/hooks/api/orders.tsx +++ b/packages/admin/dashboard/src/hooks/api/orders.tsx @@ -286,6 +286,31 @@ export const useRequestTransferOrder = ( queryKey: ordersQueryKeys.preview(orderId), }) + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.changes(orderId), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useCancelOrderTransfer = ( + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: () => sdk.admin.order.cancelTransfer(orderId), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.changes(orderId), + }) + options?.onSuccess?.(data, variables, context) }, ...options, diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index 925f1d2830..53a017191a 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -515,6 +515,9 @@ }, "import": { "type": "string" + }, + "cannotUndo": { + "type": "string" } }, "required": [ @@ -558,7 +561,8 @@ "logout", "hide", "export", - "import" + "import", + "cannotUndo" ], "additionalProperties": false }, @@ -5327,11 +5331,15 @@ }, "confirmed": { "type": "string" + }, + "declined": { + "type": "string" } }, "required": [ "requested", - "confirmed" + "confirmed", + "declined" ], "additionalProperties": false } diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 1bd01a0842..3648ef378d 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -136,7 +136,8 @@ "logout": "Logout", "hide": "Hide", "export": "Export", - "import": "Import" + "import": "Import", + "cannotUndo": "This action cannot be undone" }, "operators": { "in": "In" @@ -1293,7 +1294,8 @@ }, "transfer": { "requested": "Order transfer #{{transferId}} requested", - "confirmed": "Order transfer #{{transferId}} confirmed" + "confirmed": "Order transfer #{{transferId}} confirmed", + "declined": "Order transfer #{{transferId}} declined" } } }, diff --git a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx index bd593ad93e..2ee8c9d14f 100644 --- a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx @@ -603,6 +603,11 @@ export const RouteMap: RouteObject[] = [ "../../routes/customers/customers-add-customer-group" ), }, + { + path: ":order_id/transfer", + lazy: () => + import("../../routes/orders/order-request-transfer"), + }, { path: "metadata/edit", lazy: () => diff --git a/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx index 9a0caecad9..9dc1ff7725 100644 --- a/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx +++ b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx @@ -120,11 +120,10 @@ const useColumns = () => { return useMemo( () => [ ...base, - // TODO: REENABLE WHEN TRANSFER OWNERSHIP IS IMPLEMENTED - // columnHelper.display({ - // id: "actions", - // cell: ({ row }) => , - // }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), ], [base] ) diff --git a/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/index.ts b/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/index.ts deleted file mode 100644 index 252fcf8d47..0000000000 --- a/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./transfer-customer-order-ownership-form" diff --git a/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/transfer-customer-order-ownership-form.tsx b/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/transfer-customer-order-ownership-form.tsx deleted file mode 100644 index f58c1a27aa..0000000000 --- a/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/components/transfer-customer-order-ownership-form/transfer-customer-order-ownership-form.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { HttpTypes } from "@medusajs/types" -import { Button } from "@medusajs/ui" -import { useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" -import { z } from "zod" -import { TransferOwnerShipForm } from "../../../../../components/forms/transfer-ownership-form" -import { RouteDrawer, useRouteModal } from "../../../../../components/modals" -import { KeyboundForm } from "../../../../../components/utilities/keybound-form" -import { TransferOwnershipSchema } from "../../../../../lib/schemas" - -type TransferCustomerOrderOwnershipFormProps = { - order: HttpTypes.AdminOrder -} - -export const TransferCustomerOrderOwnershipForm = ({ - order, -}: TransferCustomerOrderOwnershipFormProps) => { - const { t } = useTranslation() - const { handleSuccess } = useRouteModal() - - const form = useForm>({ - defaultValues: { - current_owner_id: order.customer_id ?? undefined, - new_owner_id: "", - }, - resolver: zodResolver(TransferOwnershipSchema), - }) - - const { mutateAsync, isLoading } = { - mutateAsync: async (args: any) => {}, - isLoading: false, - } - - const handleSubmit = form.handleSubmit(async (values) => { - mutateAsync( - { - customer_id: values.new_owner_id, - } - // { - // onSuccess: () => { - // handleSuccess() - // }, - // } - ) - }) - - return ( - - - - - - -
- - - - -
-
-
-
- ) -} diff --git a/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/customer-transfer-ownership.tsx b/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/customer-transfer-ownership.tsx deleted file mode 100644 index d9e17738c9..0000000000 --- a/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/customer-transfer-ownership.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Heading } from "@medusajs/ui" -import { useTranslation } from "react-i18next" -import { useParams } from "react-router-dom" - -import { RouteDrawer } from "../../../components/modals" -import { useOrder } from "../../../hooks/api/orders" -import { TransferCustomerOrderOwnershipForm } from "./components/transfer-customer-order-ownership-form" - -export const CustomerTransferOwnership = () => { - const { t } = useTranslation() - const { order_id } = useParams() - - const { order, isLoading, isError, error } = useOrder(order_id!) - - const ready = !isLoading && order - - if (isError) { - throw error - } - - return ( - - - {t("transferOwnership.header")} - - {ready && } - - ) -} diff --git a/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/index.ts b/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/index.ts deleted file mode 100644 index 447df1676c..0000000000 --- a/packages/admin/dashboard/src/routes/customers/customer-transfer-ownership/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomerTransferOwnership as Component } from "./customer-transfer-ownership" diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx index 8e2b17691a..5542246d25 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx @@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next" import { AdminOrderLineItem } from "@medusajs/types" import { + useCancelOrderTransfer, useCustomer, useOrderChanges, useOrderLineItems, @@ -407,6 +408,14 @@ const useActivityItems = (order: AdminOrder): Activity[] => { timestamp: transfer.confirmed_at, }) } + if (transfer.declined_at) { + items.push({ + title: t(`orders.activity.events.transfer.declined`, { + transferId: transfer.id.slice(-7), + }), + timestamp: transfer.declined_at, + }) + } } // for (const note of notes || []) { @@ -896,11 +905,33 @@ const TransferOrderRequestBody = ({ }: { transfer: AdminOrderChange }) => { + const prompt = usePrompt() const { t } = useTranslation() const action = transfer.actions[0] const { customer } = useCustomer(action.reference_id) + const isCompleted = !!transfer.confirmed_at + + const { mutateAsync: cancelTransfer } = useCancelOrderTransfer( + transfer.order_id + ) + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("actions.cannotUndo"), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await cancelTransfer() + } + /** * TODO: change original_email to customer info when action details is changed */ @@ -917,6 +948,16 @@ const TransferOrderRequestBody = ({ ? `${customer?.first_name} ${customer?.last_name}` : customer?.email} + {!isCompleted && ( + + )} ) } diff --git a/packages/admin/dashboard/src/routes/orders/order-request-transfer/components/create-order-transfer-form/create-order-transfer-form.tsx b/packages/admin/dashboard/src/routes/orders/order-request-transfer/components/create-order-transfer-form/create-order-transfer-form.tsx index 39eaa2e904..523eb3b917 100644 --- a/packages/admin/dashboard/src/routes/orders/order-request-transfer/components/create-order-transfer-form/create-order-transfer-form.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-request-transfer/components/create-order-transfer-form/create-order-transfer-form.tsx @@ -72,7 +72,9 @@ export function CreateOrderTransferForm({ >
- +
+ +
{ const { t } = useTranslation() const params = useParams() - const { order } = useOrder(params.id!, { + + // Form is rendered bot on the order details page and customer page so we need to pick the correct param from URL + const orderId = (params.order_id || params.id) as string + + const { order, isPending, isError } = useOrder(orderId, { fields: DEFAULT_FIELDS, }) + if (!isPending && isError) { + throw new Error("Order not found") + } + return ( {t("orders.transfer.title")} - + {order && } ) } diff --git a/packages/core/js-sdk/src/admin/order.ts b/packages/core/js-sdk/src/admin/order.ts index 331024e762..550126a5ee 100644 --- a/packages/core/js-sdk/src/admin/order.ts +++ b/packages/core/js-sdk/src/admin/order.ts @@ -211,6 +211,31 @@ export class Order { ) } + /** + * This method cancels an order transfer request. It sends a request to the + * [Cancel Order Transfer Request](https://docs.medusajs.com/api/admin#orders_postordersidcanceltransferrequest) + * API route. + * + * @param id - The order's ID. + * @param headers - Headers to pass in the request. + * @returns The order's details. + * + * @example + * sdk.admin.order.cancelTransfer("order_123") + * .then(({ order }) => { + * console.log(order) + * }) + */ + async cancelTransfer(id: string, headers?: ClientHeaders) { + return await this.client.fetch( + `/admin/orders/${id}/transfer/cancel`, + { + method: "POST", + headers, + } + ) + } + /** * This method creates a fulfillment for an order. It sends a request to the * [Create Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments)