feat(dashboard, js-sdk): customer page transfer order + cancel request in timeline (#10250)

**What**
- request order transfer from admin customers details page
- cancel transfer request from order timeline

---

CLOSES CMRC-730
This commit is contained in:
Frane Polić
2024-11-26 12:42:47 +01:00
committed by GitHub
parent 344a6c9ea0
commit 1bf60c7a7d
15 changed files with 127 additions and 380 deletions

View File

@@ -1 +0,0 @@
export * from "./transfer-ownership-form"

View File

@@ -1,264 +0,0 @@
import { Select, Text, clx } from "@medusajs/ui"
import { useInfiniteQuery } from "@tanstack/react-query"
import { format } from "date-fns"
import { debounce } from "lodash"
import { PropsWithChildren, useCallback, useEffect, useState } from "react"
import { Control, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { useCustomer } from "../../../hooks/api/customers"
import { sdk } from "../../../lib/client"
import { getStylizedAmount } from "../../../lib/money-amount-helpers"
import {
getOrderFulfillmentStatus,
getOrderPaymentStatus,
} from "../../../lib/order-helpers"
import { TransferOwnershipSchema } from "../../../lib/schemas"
import { Form } from "../../common/form"
import { Skeleton } from "../../common/skeleton"
import { Combobox } from "../../inputs/combobox"
import { HttpTypes } from "@medusajs/types"
type TransferOwnerShipFieldValues = z.infer<typeof TransferOwnershipSchema>
type TransferOwnerShipFormProps = {
/**
* The Order or DraftOrder to transfer ownership of.
*/
order: HttpTypes.AdminOrder
/**
* React Hook Form control object.
*/
control: Control<TransferOwnerShipFieldValues>
}
const isOrder = (
order: HttpTypes.AdminOrder
): order is HttpTypes.AdminOrder => {
return "customer" in order
}
export const TransferOwnerShipForm = ({
order,
control,
}: TransferOwnerShipFormProps) => {
const { t } = useTranslation()
const [query, setQuery] = useState("")
const [debouncedQuery, setDebouncedQuery] = useState("")
const isOrderType = isOrder(order)
const currentOwnerId = useWatch({
control,
name: "current_owner_id",
})
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedUpdate = useCallback(
debounce((query) => setDebouncedQuery(query), 300),
[]
)
useEffect(() => {
debouncedUpdate(query)
return () => debouncedUpdate.cancel()
}, [query, debouncedUpdate])
const {
customer: owner,
isLoading: isLoadingOwner,
isError: isOwnerError,
error: ownerError,
} = useCustomer(currentOwnerId)
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ["customers", debouncedQuery],
queryFn: async ({ pageParam = 0 }) => {
const res = await sdk.admin.customer.list({
q: debouncedQuery,
limit: 10,
offset: pageParam,
has_account: true, // Only show customers with confirmed accounts
})
return res
},
initialPageParam: 0,
getNextPageParam: (lastPage) => {
const moreCustomersExist =
lastPage.count > lastPage.offset + lastPage.limit
return moreCustomersExist ? lastPage.offset + lastPage.limit : undefined
},
})
const createLabel = (customer?: HttpTypes.AdminCustomer) => {
if (!customer) {
return ""
}
const { first_name, last_name, email } = customer
const name = [first_name, last_name].filter(Boolean).join(" ")
if (name) {
return `${name} (${email})`
}
return email
}
const ownerReady = !isLoadingOwner && owner
const options =
data?.pages
.map((p) =>
p.customers.map((c) => ({
label: createLabel(c),
value: c.id,
}))
)
.flat() || []
if (isOwnerError) {
throw ownerError
}
return (
<div className="flex flex-col gap-y-8">
<div className="flex flex-col gap-y-2">
<Text size="small" leading="compact" weight="plus">
{isOrderType
? t("transferOwnership.details.order")
: t("transferOwnership.details.draft")}
</Text>
{isOrderType ? (
<OrderDetailsTable order={order} />
) : (
<DraftOrderDetailsTable draft={order} />
)}
</div>
<div className="flex flex-col gap-y-2">
<div>
<Text size="small" leading="compact" weight="plus">
{t("transferOwnership.currentOwner.label")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("transferOwnership.currentOwner.hint")}
</Text>
</div>
{ownerReady ? (
<Select defaultValue={owner.id} disabled>
<Select.Trigger>
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Item value={owner.id}>{createLabel(owner)}</Select.Item>
</Select.Content>
</Select>
) : (
<Skeleton className="h-8 w-full rounded-md" />
)}
</div>
<Form.Field
control={control}
name="new_owner_id"
render={({ field }) => {
return (
<Form.Item>
<div className="flex flex-col">
<Form.Label>{t("transferOwnership.newOwner.label")}</Form.Label>
<Form.Hint>{t("transferOwnership.newOwner.hint")}</Form.Hint>
</div>
<Form.Control>
<Combobox
{...field}
searchValue={query}
onSearchValueChange={setQuery}
fetchNextPage={fetchNextPage}
isFetchingNextPage={isFetchingNextPage}
options={options}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
)
}
const OrderDetailsTable = ({ order }: { order: HttpTypes.AdminOrder }) => {
const { t } = useTranslation()
const { label: fulfillmentLabel } = getOrderFulfillmentStatus(
t,
order.fulfillment_status
)
const { label: paymentLabel } = getOrderPaymentStatus(t, order.payment_status)
return (
<Table>
<Row label={t("fields.order")} value={`#${order.display_id}`} />
<DateRow date={order.created_at} />
<Row label={t("fields.fulfillment")} value={fulfillmentLabel} />
<Row label={t("fields.payment")} value={paymentLabel} />
<TotalRow total={order.total || 0} currencyCode={order.currency_code} />
</Table>
)
}
// TODO: Create type for Draft Order when we have it
const DraftOrderDetailsTable = ({ draft }: { draft: HttpTypes.AdminOrder }) => {
const { t } = useTranslation()
return (
<Table>
<Row label={t("fields.draft")} value={`#${draft.display_id}`} />
<DateRow date={draft.created_at} />
<Row
label={t("fields.status")}
value={t(`draftOrders.status.${draft.status}`)}
/>
{/* TODO: This will likely change. We don't use carts for draft orders any longer. */}
{/* <TotalRow
total={draft.cart.total || 0}
currencyCode={draft.cart.region.currency_code}
/> */}
</Table>
)
}
const DateRow = ({ date }: { date: string | Date }) => {
const { t } = useTranslation()
const formattedDate = format(new Date(date), "dd MMM yyyy")
return <Row label={t("fields.date")} value={formattedDate} />
}
const TotalRow = ({
total,
currencyCode,
}: {
total: number
currencyCode: string
}) => {
return <Row label="Total" value={getStylizedAmount(total, currencyCode)} />
}
const Row = ({ label, value }: { label: string; value: string }) => {
return (
<div className="txt-compact-small grid grid-cols-2 divide-x">
<div className="text-ui-fg-muted px-2 py-1.5">{label}</div>
<div className="text-ui-fg-subtle px-2 py-1.5">{value}</div>
</div>
)
}
const Table = ({ children }: PropsWithChildren) => {
return <div className={clx("divide-y rounded-lg border")}>{children}</div>
}