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:
+98
@@ -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 ?? "",
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./edit-address-form"
|
||||
+50
@@ -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>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { DraftOrderBillingAddress as Component } from "./draft-order-billing-address"
|
||||
+71
@@ -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>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./draft-order-customer-section"
|
||||
+228
@@ -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()
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./draft-order-general-section"
|
||||
+269
@@ -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>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./draft-order-summary-section"
|
||||
@@ -1,3 +0,0 @@
|
||||
export const DraftOrderDetails = () => {
|
||||
return <div>Draft Order Details</div>
|
||||
}
|
||||
+43
@@ -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))
|
||||
)
|
||||
}
|
||||
+66
@@ -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>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./edit-draft-order-email-form"
|
||||
+28
@@ -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"
|
||||
+1
-1
@@ -20,7 +20,7 @@ export const DraftOrderListTable = () => {
|
||||
useAdminDraftOrders(
|
||||
{
|
||||
...searchParams,
|
||||
expand: "cart,cart.customer",
|
||||
// expand: "cart,cart.customer",
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
|
||||
+50
@@ -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>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { DraftOrderShippingAddress as Component } from "./draft-order-shipping-address"
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./transfer-draft-order-ownership-form"
|
||||
+72
@@ -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>
|
||||
)
|
||||
}
|
||||
+28
@@ -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>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { DraftOrderTransferOwnership as Component } from "./draft-order-transfer-ownership"
|
||||
Reference in New Issue
Block a user