diff --git a/integration-tests/http/__tests__/order/admin/order.spec.ts b/integration-tests/http/__tests__/order/admin/order.spec.ts index 8d6de95a28..1bb66dd8f5 100644 --- a/integration-tests/http/__tests__/order/admin/order.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order.spec.ts @@ -76,18 +76,41 @@ medusaIntegrationTestRunner({ version: 1, change_type: "update_order", status: "confirmed", + created_by: expect.any(String), + confirmed_by: expect.any(String), confirmed_at: expect.any(String), actions: expect.arrayContaining([ expect.objectContaining({ version: 1, applied: true, - reference_id: addressBefore.id, - reference: "shipping_address", action: "UPDATE_ORDER_PROPERTIES", - details: { - city: "New New York", - address_1: "New Main street 123", - }, + details: expect.objectContaining({ + type: "shipping_address", + old: expect.objectContaining({ + address_1: addressBefore.address_1, + city: addressBefore.city, + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + company: addressBefore.company, + first_name: addressBefore.first_name, + last_name: addressBefore.last_name, + address_2: addressBefore.address_2, + }), + new: expect.objectContaining({ + address_1: "New Main street 123", + city: "New New York", + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + company: addressBefore.company, + first_name: addressBefore.first_name, + last_name: addressBefore.last_name, + address_2: addressBefore.address_2, + }), + }), }), ]), }) @@ -162,18 +185,33 @@ medusaIntegrationTestRunner({ version: 1, change_type: "update_order", status: "confirmed", + created_by: expect.any(String), + confirmed_by: expect.any(String), confirmed_at: expect.any(String), actions: expect.arrayContaining([ expect.objectContaining({ version: 1, applied: true, - reference_id: addressBefore.id, - reference: "billing_address", action: "UPDATE_ORDER_PROPERTIES", - details: { - city: "New New York", - address_1: "New Main street 123", - }, + details: expect.objectContaining({ + type: "billing_address", + old: expect.objectContaining({ + address_1: addressBefore.address_1, + city: addressBefore.city, + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + }), + new: expect.objectContaining({ + address_1: "New Main street 123", + city: "New New York", + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + }), + }), }), ]), }) @@ -239,16 +277,23 @@ medusaIntegrationTestRunner({ change_type: "update_order", status: "confirmed", confirmed_at: expect.any(String), + created_by: expect.any(String), + confirmed_by: expect.any(String), actions: expect.arrayContaining([ expect.objectContaining({ version: 1, applied: true, - reference_id: order.shipping_address.id, - reference: "shipping_address", action: "UPDATE_ORDER_PROPERTIES", - details: { - address_1: "New Main street 123", - }, + details: expect.objectContaining({ + type: "shipping_address", + old: expect.objectContaining({ + address_1: order.shipping_address.address_1, + city: order.shipping_address.city, + }), + new: expect.objectContaining({ + address_1: "New Main street 123", + }), + }), }), ]), }), @@ -257,16 +302,18 @@ medusaIntegrationTestRunner({ change_type: "update_order", status: "confirmed", confirmed_at: expect.any(String), + created_by: expect.any(String), + confirmed_by: expect.any(String), actions: expect.arrayContaining([ expect.objectContaining({ version: 1, applied: true, - reference_id: order.email, - reference: "email", action: "UPDATE_ORDER_PROPERTIES", - details: { - email: "new-email@example.com", - }, + details: expect.objectContaining({ + type: "email", + old: order.email, + new: "new-email@example.com", + }), }), ]), }), diff --git a/packages/admin/dashboard/src/components/common/user-link/user-link.tsx b/packages/admin/dashboard/src/components/common/user-link/user-link.tsx index 1a23adcf61..239abb8b08 100644 --- a/packages/admin/dashboard/src/components/common/user-link/user-link.tsx +++ b/packages/admin/dashboard/src/components/common/user-link/user-link.tsx @@ -1,5 +1,6 @@ import { Avatar, Text } from "@medusajs/ui" import { Link } from "react-router-dom" +import { useUser } from "../../../hooks/api/users" type UserLinkProps = { id: string @@ -32,3 +33,13 @@ export const UserLink = ({ ) } + +export const By = ({ id }: { id: string }) => { + const { user } = useUser(id) // todo: extend to support customers + + if (!user) { + return null + } + + return +} diff --git a/packages/admin/dashboard/src/hooks/api/orders.tsx b/packages/admin/dashboard/src/hooks/api/orders.tsx index 4e092a4b70..4440bb7942 100644 --- a/packages/admin/dashboard/src/hooks/api/orders.tsx +++ b/packages/admin/dashboard/src/hooks/api/orders.tsx @@ -51,6 +51,37 @@ export const useOrder = ( return { ...data, ...rest } } +export const useUpdateOrder = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminOrderResponse, + FetchError, + HttpTypes.AdminUpdateOrder + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminUpdateOrder) => + sdk.admin.order.update(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.detail(id), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.changes(id), + }) + + // TODO: enable when needed + // queryClient.invalidateQueries({ + // queryKey: ordersQueryKeys.lists(), + // }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + export const useOrderPreview = ( id: string, query?: HttpTypes.AdminOrderFilters, diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index b4c6e4c51d..ff3a6d5af3 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -4146,6 +4146,65 @@ ], "additionalProperties": false }, + "edit": { + "type": "object", + "properties": { + "email": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "requestSuccess": { + "type": "string" + } + }, + "required": [ + "title", + "requestSuccess" + ], + "additionalProperties": false + }, + "shippingAddress": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "requestSuccess": { + "type": "string" + } + }, + "required": [ + "title", + "requestSuccess" + ], + "additionalProperties": false + }, + "billingAddress": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "requestSuccess": { + "type": "string" + } + }, + "required": [ + "title", + "requestSuccess" + ], + "additionalProperties": false + } + }, + "required": [ + "email", + "shippingAddress", + "billingAddress" + ], + "additionalProperties": false + }, "returns": { "type": "object", "properties": { @@ -5364,6 +5423,26 @@ "declined" ], "additionalProperties": false + }, + "update_order": { + "type": "object", + "properties": { + "shipping_address": { + "type": "string" + }, + "billing_address": { + "type": "string" + }, + "email": { + "type": "string" + } + }, + "required": [ + "shipping_address", + "billing_address", + "email" + ], + "additionalProperties": false } }, "required": [ @@ -5377,7 +5456,8 @@ "claim", "exchange", "edit", - "transfer" + "transfer", + "update_order" ], "additionalProperties": false } @@ -5426,6 +5506,7 @@ "transfer", "payment", "edits", + "edit", "returns", "claims", "exchanges", @@ -10849,6 +10930,12 @@ }, "removed": { "type": "string" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" } }, "required": [ @@ -10857,7 +10944,9 @@ "available", "inStock", "added", - "removed" + "removed", + "from", + "to" ], "additionalProperties": false }, @@ -10951,6 +11040,9 @@ "subtitle": { "type": "string" }, + "by": { + "type": "string" + }, "item": { "type": "string" }, @@ -11360,6 +11452,7 @@ "discountable", "handle", "subtitle", + "by", "item", "qty", "limit", diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index feaf8f0381..5677801063 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -1002,6 +1002,20 @@ "quantityLowerThanFulfillment": "Cannot set quantity to be less then or equal to fulfilled quantity" } }, + "edit": { + "email": { + "title": "Edit email", + "requestSuccess": "Order email updated to {{email}}." + }, + "shippingAddress": { + "title": "Edit shipping address", + "requestSuccess": "Order shipping address updated." + }, + "billingAddress": { + "title": "Edit billing address", + "requestSuccess": "Order billing address updated." + } + }, "returns": { "create": "Create Return", "confirm": "Confirm Return", @@ -1301,6 +1315,11 @@ "requested": "Order transfer #{{transferId}} requested", "confirmed": "Order transfer #{{transferId}} confirmed", "declined": "Order transfer #{{transferId}} declined" + }, + "update_order": { + "shipping_address": "Shipping address updated", + "billing_address": "Billing address updated", + "email": "Email updated" } } }, @@ -2598,7 +2617,9 @@ "available": "Available", "inStock": "In stock", "added": "Added", - "removed": "Removed" + "removed": "Removed", + "from": "From", + "to": "To" }, "fields": { "amount": "Amount", @@ -2630,6 +2651,7 @@ "discountable": "Discountable", "handle": "Handle", "subtitle": "Subtitle", + "by": "By", "item": "Item", "qty": "qty.", "limit": "Limit", 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 2ee8c9d14f..e9fc3ca912 100644 --- a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx @@ -329,6 +329,20 @@ export const RouteMap: RouteObject[] = [ lazy: () => import("../../routes/orders/order-request-transfer"), }, + { + path: "email", + lazy: () => import("../../routes/orders/order-edit-email"), + }, + { + path: "shipping-address", + lazy: () => + import("../../routes/orders/order-edit-shipping-address"), + }, + { + path: "billing-address", + lazy: () => + import("../../routes/orders/order-edit-billing-address"), + }, ], }, ], diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx new file mode 100644 index 0000000000..25ac34b4e8 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx @@ -0,0 +1,74 @@ +import { Popover, Text } from "@medusajs/ui" +import { ReactNode, useState } from "react" +import { useTranslation } from "react-i18next" + +type ChangeDetailsTooltipProps = { + previous: ReactNode + next: ReactNode + title: string +} + +function ChangeDetailsTooltip(props: ChangeDetailsTooltipProps) { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + const previous = props.previous + const next = props.next + const title = props.title + + const handleMouseEnter = () => { + setOpen(true) + } + + const handleMouseLeave = () => { + setOpen(false) + } + + if (!previous && !next) { + return null + } + + return ( + + + + {title} + + + + +
+ {!!previous && ( +
+
+ {t("labels.from")} +
+ +

{previous}

+
+ )} + + {!!next && ( +
+
+ {t("labels.to")} +
+ +

{next}

+
+ )} +
+
+
+ ) +} + +export default ChangeDetailsTooltip 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 5542246d25..6563fb6b9b 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 @@ -32,6 +32,9 @@ import { useDate } from "../../../../../hooks/use-date" import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" import { getPaymentsFromOrder } from "../order-payment-section" import ActivityItems from "./activity-items" +import { By, UserLink } from "../../../../../components/common/user-link" +import ChangeDetailsTooltip from "./change-details-tooltip" +import { getFormattedAddress } from "../../../../../lib/addresses" type OrderTimelineProps = { order: AdminOrder @@ -42,6 +45,11 @@ type OrderTimelineProps = { */ const NOTE_LIMIT = 9999 +/** + * Order Changes that are not related to RMA flows + */ +const NON_RMA_CHANGE_TYPES = ["transfer", "update_order"] + export const OrderTimeline = ({ order }: OrderTimelineProps) => { const items = useActivityItems(order) @@ -118,10 +126,19 @@ const useActivityItems = (order: AdminOrder): Activity[] => { const { t } = useTranslation() const { order_changes: orderChanges = [] } = useOrderChanges(order.id, { - change_type: ["edit", "claim", "exchange", "return", "transfer"], + change_type: [ + "edit", + "claim", + "exchange", + "return", + "transfer", + "update_order", + ], }) - const rmaChanges = orderChanges.filter((oc) => oc.change_type !== "transfer") + const rmaChanges = orderChanges.filter( + (oc) => !NON_RMA_CHANGE_TYPES.includes(oc.change_type) + ) const missingLineItemIds = getMissingLineItemIds(order, rmaChanges) const { order_items: removedLineItems = [] } = useOrderLineItems( @@ -418,6 +435,74 @@ const useActivityItems = (order: AdminOrder): Activity[] => { } } + for (const update of orderChanges.filter( + (oc) => oc.change_type === "update_order" + )) { + const updateType = update.actions[0]?.details?.type + + if (updateType === "shipping_address") { + items.push({ + title: ( + + ), + timestamp: update.created_at, + children: ( +
+ {t("fields.by")} +
+ ), + }) + } + + if (updateType === "billing_address") { + items.push({ + title: ( + + ), + timestamp: update.created_at, + children: ( +
+ {t("fields.by")} +
+ ), + }) + } + + if (updateType === "email") { + items.push({ + title: ( + + ), + timestamp: update.created_at, + children: ( +
+ {t("fields.by")} +
+ ), + }) + } + } + // for (const note of notes || []) { // items.push({ // title: t("orders.activity.events.note.comment"), diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx new file mode 100644 index 0000000000..b20e97b395 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx @@ -0,0 +1,216 @@ +import * as zod from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, Input, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { Form } from "../../../../../components/common/form" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useUpdateOrder } from "../../../../../hooks/api/orders" +import { CountrySelect } from "../../../../../components/inputs/country-select" + +type EditOrderBillingAddressFormProps = { + order: HttpTypes.AdminOrder +} + +const EditOrderBillingAddressSchema = zod.object({ + address_1: zod.string().min(1), + address_2: zod.string().optional(), + country_code: zod.string().min(2).max(2), + city: zod.string().optional(), + postal_code: zod.string().optional(), + province: zod.string().optional(), + company: zod.string().optional(), + phone: zod.string().optional(), +}) + +export function EditOrderBillingAddressForm({ + order, +}: EditOrderBillingAddressFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + address_1: order.billing_address?.address_1 || "", + address_2: order.billing_address?.address_2 || "", + city: order.billing_address?.city || "", + company: order.billing_address?.company || "", + country_code: order.billing_address?.country_code || "", + phone: order.billing_address?.phone || "", + postal_code: order.billing_address?.postal_code || "", + province: order.billing_address?.province || "", + }, + resolver: zodResolver(EditOrderBillingAddressSchema), + }) + + const { mutateAsync, isPending } = useUpdateOrder(order.id) + + const handleSubmit = form.handleSubmit(async (data) => { + try { + await mutateAsync({ + billing_address: data, + }) + toast.success(t("orders.edit.billingAddress.requestSuccess")) + handleSuccess() + } catch (error) { + toast.error((error as Error).message) + } + }) + + return ( + + + +
+ { + return ( + + {t("fields.address")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.address2")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.postalCode")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.city")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.country")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.state")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.company")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+ + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts new file mode 100644 index 0000000000..1c1b9f5a33 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-order-billing-address-form" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts new file mode 100644 index 0000000000..dab858990a --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts @@ -0,0 +1 @@ +export { OrderEditBillingAddress as Component } from "./order-edit-billing-address" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx new file mode 100644 index 0000000000..88327223bf --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx @@ -0,0 +1,31 @@ +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" +import { DEFAULT_FIELDS } from "../order-detail/constants" +import { EditOrderBillingAddressForm } from "./components/edit-order-billing-address-form" + +export const OrderEditBillingAddress = () => { + const { t } = useTranslation() + const params = useParams() + + const { order, isPending, isError, error } = useOrder(params.id!, { + fields: DEFAULT_FIELDS, + }) + + if (!isPending && isError) { + throw error + } + + return ( + + + {t("orders.edit.billingAddress.title")} + + + {order && } + + ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx new file mode 100644 index 0000000000..db86181971 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx @@ -0,0 +1,95 @@ +import * as zod from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, Input, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { Form } from "../../../../../components/common/form" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useUpdateOrder } from "../../../../../hooks/api/orders" + +type EditOrderEmailFormProps = { + order: HttpTypes.AdminOrder +} + +const EditOrderEmailSchema = zod.object({ + email: zod.string().email(), +}) + +export function EditOrderEmailForm({ order }: EditOrderEmailFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + email: order.email || "", + }, + resolver: zodResolver(EditOrderEmailSchema), + }) + + const { mutateAsync, isPending } = useUpdateOrder(order.id) + + const handleSubmit = form.handleSubmit(async (data) => { + try { + await mutateAsync({ + email: data.email, + }) + toast.success( + t("orders.edit.email.requestSuccess", { email: data.email }) + ) + handleSuccess() + } catch (error) { + toast.error((error as Error).message) + } + }) + + return ( + + + + { + return ( + + {t("fields.email")} + + + + + + + + ) + }} + /> + + + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts new file mode 100644 index 0000000000..55443133a7 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-order-email-form" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts new file mode 100644 index 0000000000..014a490fc1 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts @@ -0,0 +1 @@ +export { OrderEditEmail as Component } from "./order-edit-email" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx new file mode 100644 index 0000000000..3411a91ac7 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx @@ -0,0 +1,31 @@ +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" +import { DEFAULT_FIELDS } from "../order-detail/constants" +import { EditOrderEmailForm } from "./components/edit-order-email-form" + +export const OrderEditEmail = () => { + const { t } = useTranslation() + const params = useParams() + + const { order, isPending, isError, error } = useOrder(params.id!, { + fields: DEFAULT_FIELDS, + }) + + if (!isPending && isError) { + throw error + } + + return ( + + + {t("orders.edit.email.title")} + + + {order && } + + ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx new file mode 100644 index 0000000000..f6be220cb4 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx @@ -0,0 +1,215 @@ +import * as zod from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, Input, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { Form } from "../../../../../components/common/form" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useUpdateOrder } from "../../../../../hooks/api/orders" +import { CountrySelect } from "../../../../../components/inputs/country-select" + +type EditOrderShippingAddressFormProps = { + order: HttpTypes.AdminOrder +} + +const EditOrderShippingAddressSchema = zod.object({ + address_1: zod.string().min(1), + address_2: zod.string().optional(), + country_code: zod.string().min(2).max(2), + city: zod.string().optional(), + postal_code: zod.string().optional(), + province: zod.string().optional(), + company: zod.string().optional(), + phone: zod.string().optional(), +}) + +export function EditOrderShippingAddressForm({ + order, +}: EditOrderShippingAddressFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + address_1: order.shipping_address?.address_1 || "", + address_2: order.shipping_address?.address_2 || "", + city: order.shipping_address?.city || "", + company: order.shipping_address?.company || "", + country_code: order.shipping_address?.country_code || "", + phone: order.shipping_address?.phone || "", + postal_code: order.shipping_address?.postal_code || "", + province: order.shipping_address?.province || "", + }, + resolver: zodResolver(EditOrderShippingAddressSchema), + }) + + const { mutateAsync, isPending } = useUpdateOrder(order.id) + + const handleSubmit = form.handleSubmit(async (data) => { + try { + await mutateAsync({ + shipping_address: data, + }) + toast.success(t("orders.edit.shippingAddress.requestSuccess")) + handleSuccess() + } catch (error) { + toast.error((error as Error).message) + } + }) + + return ( + + + +
+ { + return ( + + {t("fields.address")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.address2")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.postalCode")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.city")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.country")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.state")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.company")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+ + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts new file mode 100644 index 0000000000..b317ef3184 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-order-shipping-address-form" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts new file mode 100644 index 0000000000..0dca73c20e --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts @@ -0,0 +1 @@ +export { OrderEditShippingAddress as Component } from "./order-edit-shipping-address" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx new file mode 100644 index 0000000000..dbb982a073 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx @@ -0,0 +1,31 @@ +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" +import { DEFAULT_FIELDS } from "../order-detail/constants" +import { EditOrderShippingAddressForm } from "./components/edit-order-shipping-address-form" + +export const OrderEditShippingAddress = () => { + const { t } = useTranslation() + const params = useParams() + + const { order, isPending, isError } = useOrder(params.id!, { + fields: DEFAULT_FIELDS, + }) + + if (!isPending && isError) { + throw new Error("Order not found") + } + + return ( + + + {t("orders.edit.shippingAddress.title")} + + + {order && } + + ) +} diff --git a/packages/core/core-flows/src/order/workflows/update-order.ts b/packages/core/core-flows/src/order/workflows/update-order.ts index 54a265cebc..b78ac81e9d 100644 --- a/packages/core/core-flows/src/order/workflows/update-order.ts +++ b/packages/core/core-flows/src/order/workflows/update-order.ts @@ -121,45 +121,62 @@ export const updateOrderWorkflow = createWorkflow( return { ...input, ...update } }) - updateOrdersStep({ + const updatedOrders = updateOrdersStep({ selector: { id: input.id }, update: updateInput, }) - const orderChangeInput = transform({ input, order }, ({ input, order }) => { - const changes: RegisterOrderChangeDTO[] = [] - if (input.shipping_address) { - changes.push({ - change_type: "update_order" as const, - order_id: input.id, - reference: "shipping_address", - reference_id: order.shipping_address?.id, // save previous address id as reference - details: input.shipping_address as Record, // save what changed on the address - }) - } + const orderChangeInput = transform( + { input, updatedOrders, order }, + ({ input, updatedOrders, order }) => { + const updatedOrder = updatedOrders[0] - if (input.billing_address) { - changes.push({ - change_type: "update_order" as const, - order_id: input.id, - reference: "billing_address", - reference_id: order.billing_address?.id, - details: input.billing_address as Record, - }) - } + const changes: RegisterOrderChangeDTO[] = [] + if (input.shipping_address) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "shipping_address", + old: order.shipping_address, + new: updatedOrder.shipping_address, + }, + }) + } - if (input.email) { - changes.push({ - change_type: "update_order" as const, - order_id: input.id, - reference: "email", - reference_id: order.email, - details: { email: input.email }, - }) - } + if (input.billing_address) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "billing_address", + old: order.billing_address, + new: updatedOrder.billing_address, + }, + }) + } - return changes - }) + if (input.email) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "email", + old: order.email, + new: input.email, + }, + }) + } + + return changes + } + ) registerOrderChangesStep(orderChangeInput) diff --git a/packages/core/js-sdk/src/admin/order.ts b/packages/core/js-sdk/src/admin/order.ts index 550126a5ee..096a31ef49 100644 --- a/packages/core/js-sdk/src/admin/order.ts +++ b/packages/core/js-sdk/src/admin/order.ts @@ -64,6 +64,47 @@ export class Order { ) } + /** + * This method updates an order. It sends a request to the + * [Update Order Email](https://docs.medusajs.com/api/admin#orders_postordersid) + * API route. + * + * @param id - The order's ID. + * @param body - The update details. + * @param headers - Headers to pass in the request + * @returns The order's details. + * + * @example + * sdk.admin.order.update( + * "order_123", + * { + * email: "new_email@example.com", + * shipping_address: { + * first_name: "John", + * last_name: "Doe", + * address_1: "123 Main St", + * } + * } + * ) + * .then(({ order }) => { + * console.log(order) + * }) + */ + async update( + id: string, + body: HttpTypes.AdminUpdateOrder, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/orders/${id}`, + { + method: "POST", + headers, + body, + } + ) + } + /** * This method retrieves the preview of an order based on its last associated change. It sends a request to the * [Get Order Preview](https://docs.medusajs.com/api/admin#orders_getordersidpreview) API route. diff --git a/packages/core/types/src/http/order/admin/payload.ts b/packages/core/types/src/http/order/admin/payload.ts index e46edf2c7a..d0f601318a 100644 --- a/packages/core/types/src/http/order/admin/payload.ts +++ b/packages/core/types/src/http/order/admin/payload.ts @@ -1,3 +1,18 @@ +export interface AdminUpdateOrder { + /** + * The order's email. + */ + email?: string + /** + * The order's shipping address. + */ + shipping_address?: OrderAddress + /** + * The order's billing address. + */ + billing_address?: OrderAddress +} + export interface AdminCreateOrderFulfillment { /** * The items to add to the fulfillment. @@ -82,3 +97,60 @@ export interface AdminRequestOrderTransfer { internal_note?: string description?: string } + +export interface OrderAddress { + /** + * The first name of the address. + */ + first_name?: string + + /** + * The last name of the address. + */ + last_name?: string + + /** + * The phone number of the address. + */ + phone?: string + + /** + * The company of the address. + */ + company?: string + + /** + * The first address line of the address. + */ + address_1?: string + + /** + * The second address line of the address. + */ + address_2?: string + + /** + * The city of the address. + */ + city?: string + + /** + * The country code of the address. + */ + country_code?: string + + /** + * The province/state of the address. + */ + province?: string + + /** + * The postal code of the address. + */ + postal_code?: string + + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record | null +} diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index 358aefa56b..f8c6b6bd2a 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -1077,9 +1077,14 @@ export interface RegisterOrderChangeDTO { internal_note?: string | null /** - * The user or customer that requested the order change. + * The user that created the order change. */ - requested_by?: string + created_by?: string + + /** + * The user or customer that confirmed the order change. + */ + confirmed_by?: string /** * Holds custom data in key-value pairs. diff --git a/packages/core/types/src/workflow/order/update-order.ts b/packages/core/types/src/workflow/order/update-order.ts index 32cdd47bc2..e9c0b0357f 100644 --- a/packages/core/types/src/workflow/order/update-order.ts +++ b/packages/core/types/src/workflow/order/update-order.ts @@ -2,6 +2,7 @@ import { UpsertOrderAddressDTO } from "../../order" export type UpdateOrderWorkflowInput = { id: string + user_id: string shipping_address?: UpsertOrderAddressDTO billing_address?: UpsertOrderAddressDTO email?: string diff --git a/packages/medusa/src/api/admin/orders/[id]/route.ts b/packages/medusa/src/api/admin/orders/[id]/route.ts index 715b097197..4fce9e3f29 100644 --- a/packages/medusa/src/api/admin/orders/[id]/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/route.ts @@ -38,6 +38,7 @@ export const POST = async ( await updateOrderWorkflow(req.scope).run({ input: { ...req.validatedBody, + user_id: req.auth_context.actor_id, id: req.params.id, }, }) diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 149d4e77e2..3a15c3cd7f 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -2268,14 +2268,14 @@ export default class OrderModuleService< description: d.description, metadata: d.metadata, confirmed_at: new Date(), + created_by: d.created_by, + confirmed_by: d.confirmed_by, status: OrderChangeStatus.CONFIRMED, version: orderVersionsMap.get(d.order_id)!, actions: [ { action: ChangeActionType.UPDATE_ORDER_PROPERTIES, details: d.details, - reference: d.reference, - reference_id: d.reference_id, version: orderVersionsMap.get(d.order_id)!, applied: true, },