feat(dashboard): Order timeline (#6815)
**What** - Adds the initial Timeline component. - Not all events have been added, as it makes sense to add them with their respective RMA flows. - Emoji picker is omitted from initial PR as it's a nice-to-have that we can add later.
This commit is contained in:
committed by
GitHub
parent
bacfa3e17b
commit
71efa15088
@@ -335,6 +335,47 @@
|
||||
"transferOwnership": "Transfer ownership",
|
||||
"editBillingAddress": "Edit billing address",
|
||||
"editShippingAddress": "Edit shipping address"
|
||||
},
|
||||
"activity": {
|
||||
"header": "Activity",
|
||||
"showMoreActivities_one": "Show {{count}} more activity",
|
||||
"showMoreActivities_other": "Show {{count}} more activities",
|
||||
"comment": {
|
||||
"label": "Comment",
|
||||
"placeholder": "Leave a comment",
|
||||
"addButtonText": "Add comment",
|
||||
"deleteButtonText": "Delete comment"
|
||||
},
|
||||
"events": {
|
||||
"placed": {
|
||||
"title": "Order placed",
|
||||
"fromSalesChannel": "from {{salesChannel}}"
|
||||
},
|
||||
"canceled": {
|
||||
"title": "Order canceled"
|
||||
},
|
||||
"payment": {
|
||||
"awaiting": "Awaiting payment",
|
||||
"captured": "Payment captured",
|
||||
"canceled": "Payment canceled"
|
||||
},
|
||||
"fulfillment": {
|
||||
"created": "Fulfillment created",
|
||||
"canceled": "Fulfillment canceled",
|
||||
"shipped": "Fulfillment shipped",
|
||||
"itemsFulfilledFrom_one": "{{count}} item fulfilled from {{location}}",
|
||||
"itemsFulfilledFrom_other": "{{count}} items fulfilled from {{location}}",
|
||||
"itemsFulfilled_one": "{{count}} item fulfilled",
|
||||
"itemsFulfilled_other": "{{count}} items fulfilled"
|
||||
},
|
||||
"return": {
|
||||
"created": "Return created"
|
||||
},
|
||||
"note": {
|
||||
"comment": "Comment",
|
||||
"byLine": "by {{author}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"draftOrders": {
|
||||
@@ -814,6 +855,7 @@
|
||||
"date": "Date",
|
||||
"order": "Order",
|
||||
"fulfillment": "Fulfillment",
|
||||
"provider": "Provider",
|
||||
"payment": "Payment",
|
||||
"items": "Items",
|
||||
"salesChannel": "Sales Channel",
|
||||
|
||||
46
packages/admin-next/dashboard/src/hooks/use-date.tsx
Normal file
46
packages/admin-next/dashboard/src/hooks/use-date.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { format, formatDistance, sub } from "date-fns"
|
||||
import { enUS } from "date-fns/locale"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { languages } from "../i18n/config"
|
||||
|
||||
export const useDate = () => {
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const locale =
|
||||
languages.find((l) => l.code === i18n.language)?.date_locale || enUS
|
||||
|
||||
const getFullDate = ({
|
||||
date,
|
||||
includeTime = false,
|
||||
}: {
|
||||
date: string | Date
|
||||
includeTime?: boolean
|
||||
}) => {
|
||||
const ensuredDate = new Date(date)
|
||||
|
||||
if (isNaN(ensuredDate.getTime())) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const timeFormat = includeTime ? "p" : ""
|
||||
|
||||
return format(ensuredDate, `PP ${timeFormat}`, {
|
||||
locale,
|
||||
})
|
||||
}
|
||||
|
||||
function getRelativeDate(date: string | Date): string {
|
||||
const now = new Date()
|
||||
|
||||
return formatDistance(sub(new Date(date), { minutes: 0 }), now, {
|
||||
addSuffix: true,
|
||||
locale,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getFullDate,
|
||||
getRelativeDate,
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ void i18n
|
||||
escapeValue: false,
|
||||
},
|
||||
backend: {
|
||||
// for all available options read the backend's repository readme file
|
||||
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-activity-section"
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Order } from "@medusajs/medusa"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { OrderNoteForm } from "./order-note-form"
|
||||
import { OrderTimeline } from "./order-timeline"
|
||||
|
||||
type OrderActivityProps = {
|
||||
order: Order
|
||||
}
|
||||
|
||||
export const OrderActivitySection = ({ order }: OrderActivityProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="flex flex-col gap-y-8 px-6 py-4">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading level="h2">{t("orders.activity.header")}</Heading>
|
||||
</div>
|
||||
<OrderNoteForm order={order} />
|
||||
</div>
|
||||
<OrderTimeline order={order} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { ArrowUpCircleSolid } from "@medusajs/icons"
|
||||
import { Order } from "@medusajs/medusa"
|
||||
import { IconButton } from "@medusajs/ui"
|
||||
import { useAdminCreateNote } from "medusa-react"
|
||||
import { useRef } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
|
||||
type OrderNoteFormProps = {
|
||||
order: Order
|
||||
}
|
||||
|
||||
const OrderNoteSchema = z.object({
|
||||
value: z.string().min(1),
|
||||
})
|
||||
|
||||
export const OrderNoteForm = ({ order }: OrderNoteFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const form = useForm<z.infer<typeof OrderNoteSchema>>({
|
||||
defaultValues: {
|
||||
value: "",
|
||||
},
|
||||
resolver: zodResolver(OrderNoteSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreateNote()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
mutateAsync(
|
||||
{
|
||||
resource_id: order.id,
|
||||
resource_type: "order",
|
||||
value: values.value,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
form.reset()
|
||||
handleResetSize()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const handleResize = () => {
|
||||
const textarea = textareaRef.current
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto"
|
||||
textarea.style.height = textarea.scrollHeight + "px"
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetSize = () => {
|
||||
const textarea = textareaRef.current
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="bg-ui-bg-field shadow-borders-base flex flex-col gap-y-2 rounded-md px-2 py-1.5">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label hidden>
|
||||
{t("orders.activity.comment.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<textarea
|
||||
{...field}
|
||||
ref={textareaRef}
|
||||
onInput={handleResize}
|
||||
className="txt-small text-ui-fg-base placeholder:text-ui-fg-muted resize-none overflow-hidden bg-transparent outline-none"
|
||||
placeholder={t("orders.activity.comment.placeholder")}
|
||||
rows={1}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-end">
|
||||
<IconButton
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
variant="transparent"
|
||||
size="small"
|
||||
className="text-ui-fg-muted hover:text-ui-fg-subtle active:text-ui-fg-subtle"
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("orders.activity.comment.addButtonText")}
|
||||
</span>
|
||||
<ArrowUpCircleSolid />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
import { Fulfillment, Note, Order } from "@medusajs/medusa"
|
||||
import { IconButton, Text, Tooltip, clx, usePrompt } from "@medusajs/ui"
|
||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||
import {
|
||||
useAdminDeleteNote,
|
||||
useAdminNotes,
|
||||
useAdminStockLocation,
|
||||
} from "medusa-react"
|
||||
import { PropsWithChildren, ReactNode, useMemo, useState } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import { XMarkMini } from "@medusajs/icons"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||
import { useDate } from "../../../../../hooks/use-date"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
type OrderTimelineProps = {
|
||||
order: Order
|
||||
}
|
||||
|
||||
/**
|
||||
* Arbitrary high limit to ensure all notes are fetched
|
||||
*/
|
||||
const NOTE_LIMIT = 9999
|
||||
|
||||
export const OrderTimeline = ({ order }: OrderTimelineProps) => {
|
||||
const items = useActivityItems(order)
|
||||
|
||||
if (items.length <= 3) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<OrderActivityItem
|
||||
key={index}
|
||||
title={item.title}
|
||||
timestamp={item.timestamp}
|
||||
isFirst={index === items.length - 1}
|
||||
>
|
||||
{item.children}
|
||||
</OrderActivityItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const lastItems = items.slice(0, 2)
|
||||
const collapsibleItems = items.slice(2, items.length - 1)
|
||||
const firstItem = items[items.length - 1]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
{lastItems.map((item, index) => {
|
||||
return (
|
||||
<OrderActivityItem
|
||||
key={index}
|
||||
title={item.title}
|
||||
timestamp={item.timestamp}
|
||||
>
|
||||
{item.children}
|
||||
</OrderActivityItem>
|
||||
)
|
||||
})}
|
||||
<OrderActivityCollapsible activities={collapsibleItems} />
|
||||
<OrderActivityItem
|
||||
title={firstItem.title}
|
||||
timestamp={firstItem.timestamp}
|
||||
isFirst
|
||||
>
|
||||
{firstItem.children}
|
||||
</OrderActivityItem>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Activity = {
|
||||
title: string
|
||||
timestamp: string | Date
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const useActivityItems = (order: Order) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { notes, isLoading, isError, error } = useAdminNotes(
|
||||
{
|
||||
resource_id: order.id,
|
||||
limit: NOTE_LIMIT,
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return useMemo(() => {
|
||||
if (isLoading) {
|
||||
return []
|
||||
}
|
||||
|
||||
const items: Activity[] = []
|
||||
|
||||
for (const payment of order.payments) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.payment.awaiting"),
|
||||
timestamp: payment.created_at,
|
||||
children: (
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{getStylizedAmount(payment.amount, payment.currency_code)}
|
||||
</Text>
|
||||
),
|
||||
})
|
||||
|
||||
if (payment.canceled_at) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.payment.canceled"),
|
||||
timestamp: payment.canceled_at,
|
||||
children: (
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{getStylizedAmount(payment.amount, payment.currency_code)}
|
||||
</Text>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (payment.captured_at) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.payment.captured"),
|
||||
timestamp: payment.captured_at,
|
||||
children: (
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{getStylizedAmount(payment.amount, payment.currency_code)}
|
||||
</Text>
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const fulfillment of order.fulfillments) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.fulfillment.created"),
|
||||
timestamp: fulfillment.created_at,
|
||||
children: <FulfillmentCreatedBody fulfillment={fulfillment} />,
|
||||
})
|
||||
|
||||
if (fulfillment.shipped_at) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.fulfillment.shipped"),
|
||||
timestamp: fulfillment.shipped_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const ret of order.returns) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.return.created"),
|
||||
timestamp: ret.created_at,
|
||||
})
|
||||
}
|
||||
|
||||
for (const note of notes || []) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.note.comment"),
|
||||
timestamp: note.created_at,
|
||||
children: <NoteBody note={note} />,
|
||||
})
|
||||
}
|
||||
|
||||
if (order.canceled_at) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.canceled.title"),
|
||||
timestamp: order.canceled_at,
|
||||
})
|
||||
}
|
||||
|
||||
const sortedActivities = items.sort((a, b) => {
|
||||
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
})
|
||||
|
||||
const createdAt = {
|
||||
title: t("orders.activity.events.placed.title"),
|
||||
timestamp: order.created_at,
|
||||
children: (
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("orders.activity.events.placed.fromSalesChannel", {
|
||||
salesChannel: order.sales_channel.name,
|
||||
})}
|
||||
</Text>
|
||||
),
|
||||
}
|
||||
|
||||
return [...sortedActivities, createdAt]
|
||||
}, [order, notes, isLoading, t])
|
||||
}
|
||||
|
||||
type OrderActivityItemProps = PropsWithChildren<{
|
||||
title: string
|
||||
timestamp: string | Date
|
||||
isFirst?: boolean
|
||||
}>
|
||||
|
||||
const OrderActivityItem = ({
|
||||
title,
|
||||
timestamp,
|
||||
isFirst = false,
|
||||
children,
|
||||
}: OrderActivityItemProps) => {
|
||||
const { getFullDate, getRelativeDate } = useDate()
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[20px_1fr] items-start gap-2">
|
||||
<div className="flex size-full flex-col items-center gap-y-0.5">
|
||||
<div className="flex size-5 items-center justify-center">
|
||||
<div className="bg-ui-bg-base shadow-borders-base flex size-2.5 items-center justify-center rounded-full">
|
||||
<div className="bg-ui-tag-neutral-icon size-1.5 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
{!isFirst && <div className="bg-ui-border-base w-px flex-1" />}
|
||||
</div>
|
||||
<div
|
||||
className={clx({
|
||||
"pb-4": !isFirst,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{title}
|
||||
</Text>
|
||||
<Tooltip
|
||||
content={getFullDate({ date: timestamp, includeTime: true })}
|
||||
>
|
||||
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||
{getRelativeDate(timestamp)}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const OrderActivityCollapsible = ({
|
||||
activities,
|
||||
}: {
|
||||
activities: Activity[]
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!activities.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
{!open && (
|
||||
<div className="grid grid-cols-[20px_1fr] items-start gap-2">
|
||||
<div className="flex size-full flex-col items-center">
|
||||
<div className="border-ui-border-strong w-px flex-1 bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-right bg-repeat-y" />
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<Collapsible.Trigger className="text-left">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-muted"
|
||||
>
|
||||
{t("orders.activity.showMoreActivities", {
|
||||
count: activities.length,
|
||||
})}
|
||||
</Text>
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Collapsible.Content>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
{activities.map((item, index) => {
|
||||
return (
|
||||
<OrderActivityItem
|
||||
key={index}
|
||||
title={item.title}
|
||||
timestamp={item.timestamp}
|
||||
>
|
||||
{item.children}
|
||||
</OrderActivityItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const NoteBody = ({ note }: { note: Note }) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { first_name, last_name, email } = note.author || {}
|
||||
const name = [first_name, last_name].filter(Boolean).join(" ")
|
||||
|
||||
const byLine = t("orders.activity.events.note.byLine", {
|
||||
author: name || email,
|
||||
})
|
||||
|
||||
const { mutateAsync } = useAdminDeleteNote(note.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: "This action cannot be undone",
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2 pt-2">
|
||||
<div className="bg-ui-bg-component shadow-borders-base group grid grid-cols-[1fr_20px] items-start gap-x-2 text-pretty rounded-r-2xl rounded-bl-md rounded-tl-xl px-3 py-1.5">
|
||||
<div className="flex h-full min-h-7 items-center">
|
||||
<Text size="xsmall" className="text-ui-fg-subtle">
|
||||
{note.value}
|
||||
</Text>
|
||||
</div>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="transparent"
|
||||
className="transition-fg invisible opacity-0 group-hover:visible group-hover:opacity-100"
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("orders.activity.comment.deleteButtonText")}
|
||||
</span>
|
||||
<XMarkMini className="text-ui-fg-muted" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Link
|
||||
to={`/settings/users/${note.author_id}`}
|
||||
className="text-ui-fg-subtle hover:text-ui-fg-base transition-fg w-fit"
|
||||
>
|
||||
<Text size="small">{byLine}</Text>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const FulfillmentCreatedBody = ({
|
||||
fulfillment,
|
||||
}: {
|
||||
fulfillment: Fulfillment
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { stock_location, isLoading, isError, error } = useAdminStockLocation(
|
||||
fulfillment.location_id!,
|
||||
{
|
||||
enabled: !!fulfillment.location_id,
|
||||
}
|
||||
)
|
||||
|
||||
const numberOfItems = fulfillment.items.reduce((acc, item) => {
|
||||
return acc + item.quantity
|
||||
}, 0)
|
||||
|
||||
const triggerText = stock_location
|
||||
? t("orders.activity.events.fulfillment.itemsFulfilledFrom", {
|
||||
count: numberOfItems,
|
||||
location: stock_location.name,
|
||||
})
|
||||
: t("orders.activity.events.fulfillment.itemsFulfilled", {
|
||||
count: numberOfItems,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-7 w-full" />
|
||||
) : (
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{triggerText}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { formatProvider } from "../../../../../lib/format-provider"
|
||||
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
type OrderFulfillmentSectionProps = {
|
||||
@@ -265,6 +266,15 @@ const Fulfillment = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.provider")}
|
||||
</Text>
|
||||
|
||||
<Text size="small" leading="compact">
|
||||
{formatProvider(fulfillment.provider_id)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("orders.fulfillment.trackingLabel")}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const orderExpand =
|
||||
"items,items.variant,items.variant.options,sales_channel,shipping_methods,shipping_methods.shipping_option,discounts,payments,customer,shipping_address,shipping_address.country,billing_address,billing_address.country,fulfillments,fulfillments.items,fulfillments.items.item,fulfillments.tracking_links,refunds,edits,edits.items,edits.items.variant,edits.items.variant.product"
|
||||
"items,items.variant,items.variant.options,sales_channel,shipping_methods,shipping_methods.shipping_option,discounts,payments,customer,shipping_address,shipping_address.country,billing_address,billing_address.country,fulfillments,fulfillments.items,fulfillments.items.item,fulfillments.tracking_links,refunds,edits,edits.items,edits.items.variant,edits.items.variant.product,returns"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAdminOrder } from "medusa-react"
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { OrderActivitySection } from "./components/order-activity-section"
|
||||
import { OrderCustomerSection } from "./components/order-customer-section"
|
||||
import { OrderFulfillmentSection } from "./components/order-fulfillment-section"
|
||||
import { OrderGeneralSection } from "./components/order-general-section"
|
||||
@@ -41,11 +42,13 @@ export const OrderDetail = () => {
|
||||
<OrderFulfillmentSection order={order} />
|
||||
<div className="flex flex-col gap-y-2 xl:hidden">
|
||||
<OrderCustomerSection order={order} />
|
||||
<OrderActivitySection order={order} />
|
||||
</div>
|
||||
<JsonViewSection data={order} />
|
||||
</div>
|
||||
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
|
||||
<OrderCustomerSection order={order} />
|
||||
<OrderActivitySection order={order} />
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user