feat(dashboard,medusa): Draft order detail (#6703)

**What**
- Adds draft order details page
- Adds Shipping and Billing address forms to both draft order and order pages
- Adds Email form to both draft order and order pages
- Adds transfer ownership form to draft order, order and customer pages
- Update Combobox component allowing it to work with async data (`useInfiniteQuery`)

**@medusajs/medusa**
- Include country as a default relation of draft order addresses
This commit is contained in:
Kasper Fabricius Kristensen
2024-03-15 10:29:59 +00:00
committed by GitHub
parent 68d869607f
commit c3f26a6826
70 changed files with 2884 additions and 573 deletions
@@ -0,0 +1,98 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Country, DraftOrder } from "@medusajs/medusa"
import { Button } from "@medusajs/ui"
import { useAdminUpdateDraftOrder } from "medusa-react"
import { DefaultValues, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { AddressForm } from "../../../../components/forms/address-form"
import { RouteDrawer, useRouteModal } from "../../../../components/route-modal"
import { AddressSchema } from "../../../../lib/schemas"
type AddressType = "shipping" | "billing"
type EditDraftOrderAddressFormProps = {
draftOrder: DraftOrder
countries: Country[]
type: AddressType
}
export const EditDraftOrderAddressForm = ({
draftOrder,
countries,
type,
}: EditDraftOrderAddressFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof AddressSchema>>({
defaultValues: getDefaultValues(draftOrder, type),
resolver: zodResolver(AddressSchema),
})
const { mutateAsync, isLoading } = useAdminUpdateDraftOrder(draftOrder.id)
const handleSumbit = form.handleSubmit(async (values) => {
const update = {
[type === "shipping" ? "shipping_address" : "billing_address"]: values,
}
mutateAsync(update, {
onSuccess: () => {
handleSuccess()
},
})
})
return (
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSumbit}
className="flex size-full flex-col overflow-hidden"
>
<RouteDrawer.Body className="size-full flex-1 overflow-auto">
<AddressForm
control={form.control}
countries={countries}
layout="stack"
/>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button type="submit" isLoading={isLoading} size="small">
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</form>
</RouteDrawer.Form>
)
}
const getDefaultValues = (
draftOrder: DraftOrder,
type: AddressType
): DefaultValues<z.infer<typeof AddressSchema>> => {
const address =
type === "shipping"
? draftOrder.cart.shipping_address
: draftOrder.cart.billing_address
return {
first_name: address?.first_name ?? "",
last_name: address?.last_name ?? "",
address_1: address?.address_1 ?? "",
address_2: address?.address_2 ?? "",
city: address?.city ?? "",
postal_code: address?.postal_code ?? "",
province: address?.province ?? "",
country_code: address?.country_code ?? "",
phone: address?.phone ?? "",
company: address?.company ?? "",
}
}
@@ -0,0 +1 @@
export * from "./edit-address-form"
@@ -0,0 +1,50 @@
import { Heading } from "@medusajs/ui"
import { useAdminDraftOrder, useAdminRegion } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { EditDraftOrderAddressForm } from "../common/edit-address-form"
export const DraftOrderBillingAddress = () => {
const { id } = useParams()
const { t } = useTranslation()
const { draft_order, isLoading, isError, error } = useAdminDraftOrder(id!)
const regionId = draft_order?.cart?.region_id
const {
region,
isLoading: isLoadingRegion,
isError: isErrorRegion,
error: errorRegion,
} = useAdminRegion(regionId!, {
enabled: !!regionId,
})
const ready = !isLoading && draft_order && !isLoadingRegion && region
const countries = region?.countries || []
if (isError) {
throw error
}
if (isErrorRegion) {
throw errorRegion
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("addresses.billingAddress.editHeader")}</Heading>
</RouteDrawer.Header>
{ready && (
<EditDraftOrderAddressForm
draftOrder={draft_order}
countries={countries}
type="billing"
/>
)}
</RouteDrawer>
)
}
@@ -0,0 +1 @@
export { DraftOrderBillingAddress as Component } from "./draft-order-billing-address"
@@ -0,0 +1,71 @@
import { DraftOrder } from "@medusajs/medusa"
import { Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { ArrowPath, CurrencyDollar, Envelope, FlyingBox } from "@medusajs/icons"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { CustomerInfo } from "../../../../../components/common/customer-info"
type DraftOrderCustomerSectionProps = {
draftOrder: DraftOrder
}
export const DraftOrderCustomerSection = ({
draftOrder,
}: DraftOrderCustomerSectionProps) => {
return (
<Container className="divide-y p-0">
<Header />
<CustomerInfo.ID data={draftOrder.cart} />
<CustomerInfo.Contact data={draftOrder.cart} />
<CustomerInfo.Company data={draftOrder.cart} />
<CustomerInfo.Addresses data={draftOrder.cart} />
</Container>
)
}
const Header = () => {
const { t } = useTranslation()
return (
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("fields.customer")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("transferOwnership.label"),
to: `transfer-ownership`,
icon: <ArrowPath />,
},
],
},
{
actions: [
{
label: t("addresses.shippingAddress.editLabel"),
to: "shipping-address",
icon: <FlyingBox />,
},
{
label: t("addresses.billingAddress.editLabel"),
to: "billing-address",
icon: <CurrencyDollar />,
},
],
},
{
actions: [
{
label: t("email.editLabel"),
to: `email`,
icon: <Envelope />,
},
],
},
]}
/>
</div>
)
}
@@ -0,0 +1 @@
export * from "./draft-order-customer-section"
@@ -0,0 +1,228 @@
import { CreditCard, Trash } from "@medusajs/icons"
import { DraftOrder, Order } from "@medusajs/medusa"
import {
Container,
Copy,
Heading,
StatusBadge,
Text,
usePrompt,
} from "@medusajs/ui"
import { format } from "date-fns"
import {
useAdminDeleteDraftOrder,
useAdminDraftOrderRegisterPayment,
useAdminStore,
} from "medusa-react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { InlineLink } from "../../../../../components/common/inline-link"
import { Skeleton } from "../../../../../components/common/skeleton"
type DraftOrderGeneralSectionProps = {
draftOrder: DraftOrder
}
export const DraftOrderGeneralSection = ({
draftOrder,
}: DraftOrderGeneralSectionProps) => {
const { t } = useTranslation()
const prompt = usePrompt()
const navigate = useNavigate()
const { color, label } = {
open: {
color: "orange",
label: t("draftOrders.status.open"),
},
completed: {
color: "green",
label: t("draftOrders.status.completed"),
},
}[draftOrder.status] as { color: "green" | "orange"; label: string }
const { mutateAsync } = useAdminDeleteDraftOrder(draftOrder.id)
const { mutateAsync: markAsPaid } = useAdminDraftOrderRegisterPayment(
draftOrder.id
)
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("orders.cancelWarning", {
id: `#${draftOrder.display_id}`,
}),
confirmText: t("actions.continue"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(undefined)
}
const handleMarkAsPaid = async () => {
const res = await prompt({
title: t("draftOrders.markAsPaid.warningTitle"),
description: t("draftOrders.markAsPaid.warningDescription"),
confirmText: t("actions.continue"),
cancelText: t("actions.cancel"),
variant: "confirmation",
})
if (!res) {
return
}
await markAsPaid(undefined, {
onSuccess: ({ order }) => {
navigate(`/orders/${order.id}`)
},
})
}
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<div>
<div className="flex items-center gap-x-1">
<Heading>#{draftOrder.display_id}</Heading>
<Copy
content={`#${draftOrder.display_id}`}
className="text-ui-fg-muted"
/>
</div>
<Text size="small" className="text-ui-fg-subtle">
{format(new Date(draftOrder.created_at), "dd MMM, yyyy, HH:mm:ss")}
</Text>
</div>
<div className="flex items-center gap-x-4">
<StatusBadge color={color}>{label}</StatusBadge>
<ActionMenu
groups={[
{
actions: [
{
label: t("draftOrders.markAsPaid.label"),
onClick: handleMarkAsPaid,
icon: <CreditCard />,
},
],
},
{
actions: [
{
label: t("actions.delete"),
onClick: handleDelete,
icon: <Trash />,
},
],
},
]}
/>
</div>
</div>
<OrderLink order={draftOrder.order} />
{!draftOrder.order && <PaymentLink cartId={draftOrder.cart_id} />}
</Container>
)
}
const OrderLink = ({ order }: { order: Order | null }) => {
const { t } = useTranslation()
if (!order) {
return null
}
return (
<div className="text-ui-fg-subtle grid grid-cols-2 items-center gap-x-4 px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("fields.order")}
</Text>
<InlineLink to={`/orders/${order.id}`}>
<Text size="small">{`#${order.display_id}`}</Text>
</InlineLink>
</div>
)
}
const formatPaymentLink = (paymentLink: string, cartId: string) => {
// Validate that the payment link template is valid
if (!/{.*?}/.test(paymentLink)) {
return null
}
return paymentLink.replace(/{.*?}/, cartId)
}
const PaymentLink = ({ cartId }: { cartId: string }) => {
const { t } = useTranslation()
const { store, isLoading, isError, error } = useAdminStore()
/**
* If the store has a payment link template, we format the payment link.
* Otherwise, we display the cart id.
*/
const paymentLink = store?.payment_link_template
? formatPaymentLink(store.payment_link_template, cartId)
: null
if (isError) {
throw error
}
const renderContent = () => {
if (isLoading) {
return (
<div className="text-ui-fg-subtle grid grid-cols-2 items-center gap-x-4 px-6 py-4">
<Skeleton className="w-[120px]" />
<Skeleton className="w-full max-w-[190px]" />
</div>
)
}
if (paymentLink) {
return (
<div className="text-ui-fg-subtle grid grid-cols-2 items-center gap-x-4 px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("draftOrders.paymentLinkLabel")}
</Text>
<div className="flex w-full items-start gap-x-1 overflow-hidden">
<InlineLink
to={paymentLink}
rel="noopener"
target="_blank"
className="w-fit overflow-hidden"
>
<Text size="small" className="truncate">
{paymentLink}
</Text>
</InlineLink>
<Copy className="text-ui-fg-muted" content={cartId} />
</div>
</div>
)
}
return (
<div className="text-ui-fg-subtle grid grid-cols-2 items-start gap-x-4 px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("draftOrders.cartIdLabel")}
</Text>
<div className="flex w-full items-start gap-x-1 overflow-hidden">
<Text size="small" className="truncate">
{cartId}
</Text>
<Copy className="text-ui-fg-muted" content={cartId} />
</div>
</div>
)
}
return renderContent()
}
@@ -0,0 +1 @@
export * from "./draft-order-general-section"
@@ -0,0 +1,269 @@
import { Buildings } from "@medusajs/icons"
import { Cart, DraftOrder, LineItem } from "@medusajs/medusa"
import { ReservationItemDTO } from "@medusajs/types"
import { Container, Copy, Heading, StatusBadge, Text } from "@medusajs/ui"
import { useAdminReservations } from "medusa-react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { Thumbnail } from "../../../../../components/common/thumbnail"
import {
getLocaleAmount,
getStylizedAmount,
} from "../../../../../lib/money-amount-helpers"
type DraftOrderSummarySectionProps = {
draftOrder: DraftOrder
}
export const DraftOrderSummarySection = ({
draftOrder,
}: DraftOrderSummarySectionProps) => {
return (
<Container className="divide-y divide-dashed p-0">
<Header />
<ItemBreakdown draftOrder={draftOrder} />
<CostBreakdown draftOrder={draftOrder} />
<Total draftOrder={draftOrder} />
</Container>
)
}
const Header = () => {
const { t } = useTranslation()
return (
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("fields.summary")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("orders.summary.allocateItems"),
to: "#", // TODO: Open modal to allocate items
icon: <Buildings />,
},
],
},
]}
/>
</div>
)
}
const Item = ({
item,
currencyCode,
reservation,
}: {
item: LineItem
currencyCode: string
reservation?: ReservationItemDTO | null
}) => {
const { t } = useTranslation()
return (
<div
key={item.id}
className="text-ui-fg-subtle grid grid-cols-2 items-start gap-x-4 px-6 py-4"
>
<div className="flex items-start gap-x-4">
<Thumbnail src={item.thumbnail} />
<div>
<Text
size="small"
leading="compact"
weight="plus"
className="text-ui-fg-base"
>
{item.title}
</Text>
{item.variant.sku && (
<div className="flex items-center gap-x-1">
<Text size="small">{item.variant.sku}</Text>
<Copy content={item.variant.sku} className="text-ui-fg-muted" />
</div>
)}
<Text size="small">{item.variant.title}</Text>
</div>
</div>
<div className="grid grid-cols-3 items-center gap-x-4">
<div className="flex items-center justify-end gap-x-4">
<Text size="small">
{getLocaleAmount(item.unit_price, currencyCode)}
</Text>
</div>
<div className="flex items-center gap-x-2">
<div className="w-fit min-w-[27px]">
<Text>
<span className="tabular-nums">{item.quantity}</span>x
</Text>
</div>
<div className="overflow-visible">
<StatusBadge
color={reservation ? "green" : "orange"}
className="text-nowrap"
>
{reservation
? t("orders.reservations.allocatedLabel")
: t("orders.reservations.notAllocatedLabel")}
</StatusBadge>
</div>
</div>
<div className="flex items-center justify-end">
<Text size="small">
{getLocaleAmount(item.subtotal || 0, currencyCode)}
</Text>
</div>
</div>
</div>
)
}
const ItemBreakdown = ({ draftOrder }: { draftOrder: DraftOrder }) => {
const { reservations, isError, error } = useAdminReservations({
line_item_id: draftOrder.cart.items.map((i) => i.id),
})
if (isError) {
throw error
}
return (
<div>
{draftOrder.cart.items.map((item) => {
const reservation = reservations
? reservations.find((r) => r.line_item_id === item.id)
: null
return (
<Item
key={item.id}
item={item}
currencyCode={draftOrder.cart.region.currency_code}
reservation={reservation}
/>
)
})}
</div>
)
}
const Cost = ({
label,
value,
secondaryValue,
}: {
label: string
value: string | number
secondaryValue: string
}) => (
<div className="grid grid-cols-3 items-center">
<Text size="small" leading="compact">
{label}
</Text>
<div className="text-right">
<Text size="small" leading="compact">
{secondaryValue}
</Text>
</div>
<div className="text-right">
<Text size="small" leading="compact">
{value}
</Text>
</div>
</div>
)
const calculateCartTaxRate = (cart: Cart) => {
const {
tax_total,
subtotal = 0,
discount_total = 0,
gift_card_total = 0,
shipping_total = 0,
} = cart
const preTaxTotal =
subtotal - discount_total - gift_card_total + shipping_total
return ((tax_total || 0) / preTaxTotal) * 100
}
const CostBreakdown = ({ draftOrder }: { draftOrder: DraftOrder }) => {
const { t } = useTranslation()
// Calculate tax rate since it's not included in the cart
const taxRate = calculateCartTaxRate(draftOrder.cart)
return (
<div className="text-ui-fg-subtle flex flex-col gap-y-2 px-6 py-4">
<Cost
label={t("fields.subtotal")}
secondaryValue={t("general.items", {
count: draftOrder.cart.items.length,
})}
value={getLocaleAmount(
draftOrder.cart.subtotal || 0,
draftOrder.cart.region.currency_code
)}
/>
<Cost
label={t("fields.discount")}
secondaryValue={
draftOrder.cart.discounts.length > 0
? draftOrder.cart.discounts.map((d) => d.code).join(", ")
: "-"
}
value={
(draftOrder.cart.discount_total || 0) > 0
? `- ${getLocaleAmount(
draftOrder.cart.discount_total || 0,
draftOrder.cart.region.currency_code
)}`
: "-"
}
/>
<Cost
label={t("fields.shipping")}
secondaryValue={draftOrder.cart.shipping_methods
.map((sm) => sm.shipping_option.name)
.join(", ")}
value={getLocaleAmount(
draftOrder.cart.shipping_total || 0,
draftOrder.cart.region.currency_code
)}
/>
<Cost
label={t("fields.tax")}
secondaryValue={`${taxRate || 0}%`}
value={
draftOrder.cart.tax_total
? getLocaleAmount(
draftOrder.cart.tax_total || 0,
draftOrder.cart.region.currency_code
)
: "-"
}
/>
</div>
)
}
const Total = ({ draftOrder }: { draftOrder: DraftOrder }) => {
const { t } = useTranslation()
return (
<div className="text-ui-fg-base flex items-center justify-between px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("fields.total")}
</Text>
<Text size="small" leading="compact" weight="plus">
{getStylizedAmount(
draftOrder.cart.total || 0,
draftOrder.cart.region.currency_code
)}
</Text>
</div>
)
}
@@ -0,0 +1 @@
export * from "./draft-order-summary-section"
@@ -1,3 +0,0 @@
export const DraftOrderDetails = () => {
return <div>Draft Order Details</div>
}
@@ -0,0 +1,43 @@
import { useAdminDraftOrder } from "medusa-react"
import { Outlet, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { DraftOrderCustomerSection } from "./components/draft-order-customer-section"
import { DraftOrderGeneralSection } from "./components/draft-order-general-section"
import { DraftOrderSummarySection } from "./components/draft-order-summary-section"
import { draftOrderLoader } from "./loader"
export const DraftOrderDetail = () => {
const { id } = useParams()
const initialData = useLoaderData() as Awaited<
ReturnType<typeof draftOrderLoader>
>
const { draft_order, isLoading, isError, error } = useAdminDraftOrder(id!, {
initialData,
})
if (isLoading || !draft_order) {
return <div>Loading...</div>
}
if (isError) {
throw error
}
return (
<div className="grid grid-cols-1 gap-x-4 xl:grid-cols-[1fr,400px]">
<div className="flex flex-col gap-y-2">
<DraftOrderGeneralSection draftOrder={draft_order} />
<DraftOrderSummarySection draftOrder={draft_order} />
<div className="flex flex-col gap-y-2 xl:hidden">
<DraftOrderCustomerSection draftOrder={draft_order} />
</div>
<JsonViewSection data={draft_order} />
</div>
<div className="hidden flex-col gap-y-2 xl:flex">
<DraftOrderCustomerSection draftOrder={draft_order} />
</div>
<Outlet />
</div>
)
}
@@ -1 +1,2 @@
export { DraftOrderDetails as Component } from "./details"
export { DraftOrderDetail as Component } from "./draft-order-detail"
export { draftOrderLoader as loader } from "./loader"
@@ -0,0 +1,21 @@
import { AdminDraftOrdersRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { adminDraftOrderKeys } from "medusa-react"
import { LoaderFunctionArgs } from "react-router-dom"
import { medusa, queryClient } from "../../../lib/medusa"
const draftOrderDetailQuery = (id: string) => ({
queryKey: adminDraftOrderKeys.detail(id),
queryFn: async () => medusa.admin.draftOrders.retrieve(id),
})
export const draftOrderLoader = async ({ params }: LoaderFunctionArgs) => {
const id = params.id
const query = draftOrderDetailQuery(id!)
return (
queryClient.getQueryData<Response<AdminDraftOrdersRes>>(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}
@@ -0,0 +1,66 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { DraftOrder } from "@medusajs/medusa"
import { Button } from "@medusajs/ui"
import { useAdminUpdateDraftOrder } from "medusa-react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { EmailForm } from "../../../../../components/forms/email-form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { EmailSchema } from "../../../../../lib/schemas"
type EditDraftOrderEmailFormProps = {
draftOrder: DraftOrder
}
export const EditDraftOrderEmailForm = ({
draftOrder,
}: EditDraftOrderEmailFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof EmailSchema>>({
defaultValues: {
email: draftOrder.cart.email,
},
resolver: zodResolver(EmailSchema),
})
const { mutateAsync, isLoading } = useAdminUpdateDraftOrder(draftOrder.id)
const handleSumbit = form.handleSubmit(async (values) => {
mutateAsync(values, {
onSuccess: () => {
handleSuccess()
},
})
})
return (
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSumbit}
className="flex size-full flex-col overflow-hidden"
>
<RouteDrawer.Body className="size-full flex-1 overflow-auto">
<EmailForm control={form.control} layout="stack" />
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button type="submit" isLoading={isLoading} size="small">
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</form>
</RouteDrawer.Form>
)
}
@@ -0,0 +1 @@
export * from "./edit-draft-order-email-form"
@@ -0,0 +1,28 @@
import { Heading } from "@medusajs/ui"
import { useAdminDraftOrder } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { EditDraftOrderEmailForm } from "./components/edit-draft-order-email-form/edit-draft-order-email-form"
export const DraftOrderEmail = () => {
const { id } = useParams()
const { t } = useTranslation()
const { draft_order, isLoading, isError, error } = useAdminDraftOrder(id!)
const ready = !isLoading && draft_order
if (isError) {
throw error
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("email.editHeader")}</Heading>
</RouteDrawer.Header>
{ready && <EditDraftOrderEmailForm draftOrder={draft_order} />}
</RouteDrawer>
)
}
@@ -0,0 +1 @@
export { DraftOrderEmail as Component } from "./draft-order-email"
@@ -20,7 +20,7 @@ export const DraftOrderListTable = () => {
useAdminDraftOrders(
{
...searchParams,
expand: "cart,cart.customer",
// expand: "cart,cart.customer",
},
{
keepPreviousData: true,
@@ -0,0 +1,50 @@
import { Heading } from "@medusajs/ui"
import { useAdminDraftOrder, useAdminRegion } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { EditDraftOrderAddressForm } from "../common/edit-address-form"
export const DraftOrderShippingAddress = () => {
const { id } = useParams()
const { t } = useTranslation()
const { draft_order, isLoading, isError, error } = useAdminDraftOrder(id!)
const regionId = draft_order?.cart?.region_id
const {
region,
isLoading: isLoadingRegion,
isError: isErrorRegion,
error: errorRegion,
} = useAdminRegion(regionId!, {
enabled: !!regionId,
})
const ready = !isLoading && draft_order && !isLoadingRegion && region
const countries = region?.countries || []
if (isError) {
throw error
}
if (isErrorRegion) {
throw errorRegion
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("addresses.shippingAddress.editHeader")}</Heading>
</RouteDrawer.Header>
{ready && (
<EditDraftOrderAddressForm
draftOrder={draft_order}
countries={countries}
type="shipping"
/>
)}
</RouteDrawer>
)
}
@@ -0,0 +1 @@
export { DraftOrderShippingAddress as Component } from "./draft-order-shipping-address"
@@ -0,0 +1 @@
export * from "./transfer-draft-order-ownership-form"
@@ -0,0 +1,72 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { DraftOrder } from "@medusajs/medusa"
import { Button } from "@medusajs/ui"
import { useAdminUpdateDraftOrder } from "medusa-react"
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/route-modal"
import { TransferOwnershipSchema } from "../../../../../lib/schemas"
type TransferDraftOrderOwnershipFormProps = {
draftOrder: DraftOrder
}
export const TransferDraftOrderOwnershipForm = ({
draftOrder,
}: TransferDraftOrderOwnershipFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof TransferOwnershipSchema>>({
defaultValues: {
current_owner_id: draftOrder.cart.customer_id,
new_owner_id: "",
},
resolver: zodResolver(TransferOwnershipSchema),
})
const { mutateAsync, isLoading } = useAdminUpdateDraftOrder(draftOrder.id)
const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync(
{
customer_id: values.new_owner_id,
},
{
onSuccess: () => {
handleSuccess()
},
}
)
})
return (
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex size-full flex-col overflow-hidden"
>
<RouteDrawer.Body className="size-full flex-1 overflow-auto">
<TransferOwnerShipForm order={draftOrder} control={form.control} />
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button type="submit" isLoading={isLoading} size="small">
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</form>
</RouteDrawer.Form>
)
}
@@ -0,0 +1,28 @@
import { Heading } from "@medusajs/ui"
import { useAdminDraftOrder } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { TransferDraftOrderOwnershipForm } from "./components/transfer-draft-order-ownership-form"
export const DraftOrderTransferOwnership = () => {
const { id } = useParams()
const { t } = useTranslation()
const { draft_order, isLoading, isError, error } = useAdminDraftOrder(id!)
const ready = !isLoading && draft_order
if (isError) {
throw error
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("transferOwnership.header")}</Heading>
</RouteDrawer.Header>
{ready && <TransferDraftOrderOwnershipForm draftOrder={draft_order} />}
</RouteDrawer>
)
}
@@ -0,0 +1 @@
export { DraftOrderTransferOwnership as Component } from "./draft-order-transfer-ownership"