>({
+ defaultValues: {
+ value: "",
+ },
+ resolver: zodResolver(OrderNoteSchema),
+ })
+
+ const { mutateAsync, isLoading } = {}
+
+ 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 (
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-activity-section/order-timeline.tsx b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-activity-section/order-timeline.tsx
new file mode 100644
index 0000000000..0520714196
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-activity-section/order-timeline.tsx
@@ -0,0 +1,404 @@
+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 { 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"
+import { OrderDTO } from "@medusajs/types"
+
+type OrderTimelineProps = {
+ order: OrderDTO
+}
+
+/**
+ * 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 (
+
+ {items.map((item, index) => {
+ return (
+
+ {item.children}
+
+ )
+ })}
+
+ )
+ }
+
+ const lastItems = items.slice(0, 2)
+ const collapsibleItems = items.slice(2, items.length - 1)
+ const firstItem = items[items.length - 1]
+
+ return (
+
+ {lastItems.map((item, index) => {
+ return (
+
+ {item.children}
+
+ )
+ })}
+
+
+ {firstItem.children}
+
+
+ )
+}
+
+type Activity = {
+ title: string
+ timestamp: string | Date
+ children?: ReactNode
+}
+
+const useActivityItems = (order: Order) => {
+ const { t } = useTranslation()
+
+ const notes = []
+ const isLoading = false
+ // const { notes, isLoading, isError, error } = useNotes(
+ // {
+ // 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: (
+ //
+ // {getStylizedAmount(payment.amount, payment.currency_code)}
+ //
+ // ),
+ // })
+ //
+ // if (payment.canceled_at) {
+ // items.push({
+ // title: t("orders.activity.events.payment.canceled"),
+ // timestamp: payment.canceled_at,
+ // children: (
+ //
+ // {getStylizedAmount(payment.amount, payment.currency_code)}
+ //
+ // ),
+ // })
+ // }
+ //
+ // if (payment.captured_at) {
+ // items.push({
+ // title: t("orders.activity.events.payment.captured"),
+ // timestamp: payment.captured_at,
+ // children: (
+ //
+ // {getStylizedAmount(payment.amount, payment.currency_code)}
+ //
+ // ),
+ // })
+ // }
+ // }
+
+ // for (const fulfillment of order.fulfillments) {
+ // items.push({
+ // title: t("orders.activity.events.fulfillment.created"),
+ // timestamp: fulfillment.created_at,
+ // children: ,
+ // })
+ //
+ // 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: ,
+ // })
+ // }
+
+ 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: (
+
+ {t("orders.activity.events.placed.fromSalesChannel", {
+ salesChannel: order.sales_channel.name,
+ })}
+
+ ),
+ }
+
+ 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 (
+
+
+
+
+
+ {title}
+
+
+
+ {getRelativeDate(timestamp)}
+
+
+
+
{children}
+
+
+ )
+}
+
+const OrderActivityCollapsible = ({
+ activities,
+}: {
+ activities: Activity[]
+}) => {
+ const [open, setOpen] = useState(false)
+
+ const { t } = useTranslation()
+
+ if (!activities.length) {
+ return null
+ }
+
+ return (
+
+ {!open && (
+
+
+
+
+
+ {t("orders.activity.showMoreActivities", {
+ count: activities.length,
+ })}
+
+
+
+
+ )}
+
+
+ {activities.map((item, index) => {
+ return (
+
+ {item.children}
+
+ )
+ })}
+
+
+
+ )
+}
+
+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 (
+
+
+
+
+ {note.value}
+
+
+
+
+ {t("orders.activity.comment.deleteButtonText")}
+
+
+
+
+
+
{byLine}
+
+
+ )
+}
+
+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 (
+
+ {isLoading ? (
+
+ ) : (
+
+ {triggerText}
+
+ )}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-customer-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-customer-section/index.ts
new file mode 100644
index 0000000000..84cacaaaed
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-customer-section/index.ts
@@ -0,0 +1 @@
+export * from "./order-customer-section"
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-customer-section/order-customer-section.tsx b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-customer-section/order-customer-section.tsx
new file mode 100644
index 0000000000..818f943496
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-customer-section/order-customer-section.tsx
@@ -0,0 +1,69 @@
+import { Order } 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 OrderCustomerSectionProps = {
+ order: Order
+}
+
+export const OrderCustomerSection = ({ order }: OrderCustomerSectionProps) => {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+const Header = () => {
+ const { t } = useTranslation()
+
+ return (
+
+
{t("fields.customer")}
+
,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ label: t("addresses.shippingAddress.editLabel"),
+ to: "shipping-address",
+ icon:
,
+ },
+ {
+ label: t("addresses.billingAddress.editLabel"),
+ to: "billing-address",
+ icon:
,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ label: t("email.editLabel"),
+ to: `email`,
+ icon:
,
+ },
+ ],
+ },
+ ]}
+ />
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-fulfillment-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-fulfillment-section/index.ts
new file mode 100644
index 0000000000..6945fbefaf
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-fulfillment-section/index.ts
@@ -0,0 +1 @@
+export * from "./order-fulfillment-section"
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx
new file mode 100644
index 0000000000..939f3919c1
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx
@@ -0,0 +1,340 @@
+import { XCircle } from "@medusajs/icons"
+import {
+ LineItem,
+ Fulfillment as MedusaFulfillment,
+ Order,
+} from "@medusajs/medusa"
+import {
+ Container,
+ Copy,
+ Heading,
+ StatusBadge,
+ Text,
+ toast,
+ Tooltip,
+ usePrompt,
+} from "@medusajs/ui"
+import { format } from "date-fns"
+import { useTranslation } from "react-i18next"
+import { Link } from "react-router-dom"
+import { FulfillmentDTO, OrderDTO, OrderItemDTO } from "@medusajs/types"
+
+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"
+import { useStockLocation } from "../../../../../hooks/api/stock-locations"
+
+type OrderFulfillmentSectionProps = {
+ order: OrderDTO
+}
+
+export const OrderFulfillmentSection = ({
+ order,
+}: OrderFulfillmentSectionProps) => {
+ const fulfillments = order.fulfillments || []
+
+ return (
+
+
+ {fulfillments.map((f, index) => (
+
+ ))}
+
+ )
+}
+
+const UnfulfilledItem = ({
+ item,
+ currencyCode,
+}: {
+ item: OrderItemDTO
+ currencyCode: string
+}) => {
+ return (
+
+
+
+
+
+ {item.title}
+
+ {item.variant.sku && (
+
+ {item.variant.sku}
+
+
+ )}
+
+ {item.variant.options.map((o) => o.value).join(" · ")}
+
+
+
+
+
+
+ {getLocaleAmount(item.unit_price, currencyCode)}
+
+
+
+
+ {item.quantity}x
+
+
+
+
+ {getLocaleAmount(item.subtotal || 0, currencyCode)}
+
+
+
+
+ )
+}
+
+const UnfulfilledItemBreakdown = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ const fulfillmentItems = order.fulfillments?.map((f) =>
+ f.items.map((i) => ({ id: i.item_id, quantity: i.quantity }))
+ )
+
+ // Create an array of order items that haven't been fulfilled or at least not fully fulfilled
+ const unfulfilledItems = order.items.filter(
+ (i) =>
+ !fulfillmentItems?.some((fi) =>
+ fi.some((f) => f.id === i.id && f.quantity === i.quantity)
+ )
+ )
+
+ if (!unfulfilledItems.length) {
+ return null
+ }
+
+ return (
+
+
+
{t("orders.fulfillment.unfulfilledItems")}
+
+
+ {t("orders.fulfillment.awaitingFullfillmentBadge")}
+
+
+
+
+
+ {unfulfilledItems.map((item) => (
+
+ ))}
+
+
+ )
+}
+
+const Fulfillment = ({
+ fulfillment,
+ index,
+}: {
+ fulfillment: FulfillmentDTO
+ index: number
+}) => {
+ const { t } = useTranslation()
+ const prompt = usePrompt()
+
+ const showLocation = !!fulfillment.location_id
+
+ const { stock_location, isError, error } = useStockLocation(
+ fulfillment.location_id!,
+ {
+ enabled: showLocation,
+ }
+ )
+
+ let statusText = "Fulfilled"
+ let statusColor: "orange" | "green" | "red" = "orange"
+ let statusTimestamp = fulfillment.created_at
+
+ if (fulfillment.canceled_at) {
+ statusText = "Canceled"
+ statusColor = "red"
+ statusTimestamp = fulfillment.canceled_at
+ } else if (fulfillment.shipped_at) {
+ statusText = "Shipped"
+ statusColor = "green"
+ statusTimestamp = fulfillment.shipped_at
+ }
+
+ const { mutateAsync } = {} // useCancelFulfillment(order.id)
+
+ const handleCancel = async () => {
+ if (fulfillment.shipped_at) {
+ toast.warning(t("general.warning"), {
+ description: t("orders.fulfillment.toast.fulfillmentShipped"),
+ dismissLabel: t("actions.close"),
+ })
+ return
+ }
+
+ const res = await prompt({
+ title: t("general.areYouSure"),
+ description: t("orders.fulfillment.cancelWarning"),
+ confirmText: t("actions.continue"),
+ cancelText: t("actions.cancel"),
+ })
+
+ if (res) {
+ try {
+ await mutateAsync(fulfillment.id)
+
+ toast.success(t("general.success"), {
+ description: t("orders.fulfillment.toast.canceled"),
+ dismissLabel: t("actions.close"),
+ })
+ } catch (e) {
+ toast.error(t("general.error"), {
+ description: e.message,
+ dismissLabel: t("actions.close"),
+ })
+ }
+ }
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+ {t("orders.fulfillment.number", {
+ number: index + 1,
+ })}
+
+
+
+
+ {statusText}
+
+
+
,
+ onClick: handleCancel,
+ },
+ ],
+ },
+ ]}
+ />
+
+
+
+
+ {t("orders.fulfillment.itemsLabel")}
+
+
+ {fulfillment.items.map((f_item) => (
+ -
+
+ {f_item.item.quantity}x {f_item.item.title}
+
+
+ ))}
+
+
+ {showLocation && (
+
+
+ {t("orders.fulfillment.shippingFromLabel")}
+
+ {stock_location ? (
+
+
+ {stock_location.name}
+
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {t("fields.provider")}
+
+
+
+ {formatProvider(fulfillment.provider_id)}
+
+
+
+
+ {t("orders.fulfillment.trackingLabel")}
+
+
+ {fulfillment.tracking_links &&
+ fulfillment.tracking_links.length > 0 ? (
+
+ {fulfillment.tracking_links.map((tlink) => {
+ const hasUrl = tlink.url && tlink.url.length > 0
+
+ if (hasUrl) {
+ return (
+ -
+
+
+ {tlink.tracking_number}
+
+
+
+ )
+ }
+
+ return (
+ -
+
+ {tlink.tracking_number}
+
+
+ )
+ })}
+
+ ) : (
+
+ -
+
+ )}
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-general-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-general-section/index.ts
new file mode 100644
index 0000000000..583807cc1b
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-general-section/index.ts
@@ -0,0 +1 @@
+export * from "./order-general-section"
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-general-section/order-general-section.tsx b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-general-section/order-general-section.tsx
new file mode 100644
index 0000000000..16b6b59f76
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-general-section/order-general-section.tsx
@@ -0,0 +1,118 @@
+import { XCircle } from "@medusajs/icons"
+import { Order } from "@medusajs/medusa"
+import {
+ Container,
+ Copy,
+ Heading,
+ StatusBadge,
+ Text,
+ usePrompt,
+} from "@medusajs/ui"
+import { format } from "date-fns"
+import { useTranslation } from "react-i18next"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+import {
+ getOrderFulfillmentStatus,
+ getOrderPaymentStatus,
+} from "../../../../../lib/order-helpers"
+
+type OrderGeneralSectionProps = {
+ order: Order
+}
+
+export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
+ const { t } = useTranslation()
+ const prompt = usePrompt()
+
+ const { mutateAsync } = { mutateAsync: () => {} } // cancel order
+
+ const handleCancel = async () => {
+ const res = await prompt({
+ title: t("general.areYouSure"),
+ description: t("orders.cancelWarning", {
+ id: `#${order.display_id}`,
+ }),
+ confirmText: t("actions.continue"),
+ cancelText: t("actions.cancel"),
+ })
+
+ if (!res) {
+ return
+ }
+
+ await mutateAsync(undefined)
+ }
+
+ return (
+
+
+
+ #{order.display_id}
+
+
+
+ {t("orders.onDateFromSalesChannel", {
+ date: format(new Date(order.created_at), "dd MMM, yyyy, HH:mm:ss"),
+ salesChannel: order.sales_channel?.name,
+ })}
+
+
+
+
+
,
+ // },
+ ],
+ },
+ ]}
+ />
+
+
+ )
+}
+
+const FulfillmentBadge = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ /**
+ * TODO: revisit when Order<>Fulfillment are linked
+ */
+ return null
+
+ const { label, color } = getOrderFulfillmentStatus(
+ t,
+ order.fulfillment_status
+ )
+
+ return (
+
+ {label}
+
+ )
+}
+
+const PaymentBadge = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ /**
+ * TODO: revisit when Order<>Payment are linked
+ */
+ return null
+
+ const { label, color } = getOrderPaymentStatus(t, order.payment_status)
+
+ return (
+
+ {label}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-payment-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-payment-section/index.ts
new file mode 100644
index 0000000000..4b0c66897c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-payment-section/index.ts
@@ -0,0 +1 @@
+export * from "./order-payment-section"
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx
new file mode 100644
index 0000000000..afc2d6e652
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx
@@ -0,0 +1,284 @@
+import { ArrowDownRightMini, XCircle } from "@medusajs/icons"
+import {
+ Payment as MedusaPayment,
+ Refund as MedusaRefund,
+ Order,
+} from "@medusajs/medusa"
+import {
+ Badge,
+ Button,
+ Container,
+ Heading,
+ StatusBadge,
+ Text,
+ Tooltip,
+} from "@medusajs/ui"
+import { format } from "date-fns"
+import { useTranslation } from "react-i18next"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+import {
+ getLocaleAmount,
+ getStylizedAmount,
+} from "../../../../../lib/money-amount-helpers"
+
+type OrderPaymentSectionProps = {
+ order: Order
+}
+
+export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => {
+ return (
+
+
+
+
+
+ )
+}
+
+const Header = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ const hasCapturedPayment = order.payments.some((p) => !!p.captured_at)
+
+ return (
+
+
{t("orders.payment.title")}
+
,
+ to: `/orders/${order.id}/refund`,
+ disabled: !hasCapturedPayment,
+ },
+ ],
+ },
+ ]}
+ />
+
+ )
+}
+
+const Refund = ({
+ refund,
+ currencyCode,
+}: {
+ refund: MedusaRefund
+ currencyCode: string
+}) => {
+ const { t } = useTranslation()
+ const hasPayment = refund.payment_id !== null
+
+ const BadgeComponent = (
+
+ {refund.reason}
+
+ )
+
+ const Render = refund.note ? (
+ {BadgeComponent}
+ ) : (
+ BadgeComponent
+ )
+
+ return (
+
+
+ {hasPayment &&
}
+
+ {t("orders.payment.refund")}
+
+
+
+
+ {format(new Date(refund.created_at), "dd MMM, yyyy, HH:mm:ss")}
+
+
+
{Render}
+
+
+ - {getLocaleAmount(refund.amount, currencyCode)}
+
+
+
+ )
+}
+
+const Payment = ({
+ payment,
+ refunds,
+ currencyCode,
+}: {
+ payment: MedusaPayment
+ refunds: MedusaRefund[]
+ currencyCode: string
+}) => {
+ const { t } = useTranslation()
+
+ const [status, color] = (
+ payment.captured_at ? ["Captured", "green"] : ["Pending", "orange"]
+ ) as [string, "green" | "orange"]
+
+ const cleanId = payment.id.replace("pay_", "")
+ const showCapture =
+ payment.captured_at === null && payment.canceled_at === null
+
+ return (
+
+
+
+
+ {cleanId}
+
+
+ {format(new Date(payment.created_at), "dd MMM, yyyy, HH:mm:ss")}
+
+
+
+
+ {payment.provider_id}
+
+
+
+
+ {status}
+
+
+
+
+ {getLocaleAmount(payment.amount, payment.currency_code)}
+
+
+
,
+ to: `/orders/${payment.order_id}/refund?paymentId=${payment.id}`,
+ disabled: !payment.captured_at,
+ },
+ ],
+ },
+ ]}
+ />
+
+ {showCapture && (
+
+
+
+
+ {t("orders.payment.isReadyToBeCaptured", {
+ id: cleanId,
+ })}
+
+
+
+
+ )}
+ {refunds.map((refund) => (
+
+ ))}
+
+ )
+}
+
+const PaymentBreakdown = ({
+ payments,
+ refunds,
+ currencyCode,
+}: {
+ payments: MedusaPayment[]
+ refunds: MedusaRefund[]
+ currencyCode: string
+}) => {
+ /**
+ * Refunds that are not associated with a payment.
+ */
+ const orderRefunds = refunds.filter((refund) => refund.payment_id === null)
+
+ const entries = [...orderRefunds, ...payments]
+ .sort((a, b) => {
+ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
+ })
+ .map((entry) => {
+ return {
+ event: entry,
+ type: entry.id.startsWith("pay_") ? "payment" : "refund",
+ }
+ }) as (
+ | { type: "payment"; event: MedusaPayment }
+ | { type: "refund"; event: MedusaRefund }
+ )[]
+
+ return (
+
+ {entries.map(({ type, event }) => {
+ switch (type) {
+ case "payment":
+ return (
+
refund.payment_id === event.id
+ )}
+ currencyCode={currencyCode}
+ />
+ )
+ case "refund":
+ return (
+
+ )
+ }
+ })}
+
+ )
+}
+
+const Total = ({
+ payments,
+ currencyCode,
+}: {
+ payments: MedusaPayment[]
+ currencyCode: string
+}) => {
+ const { t } = useTranslation()
+
+ const paid = payments.reduce((acc, payment) => acc + payment.amount, 0)
+ const refunded = payments.reduce(
+ (acc, payment) => acc + (payment.amount_refunded || 0),
+ 0
+ )
+
+ const total = paid - refunded
+
+ return (
+
+
+ {t("orders.payment.totalPaidByCustomer")}
+
+
+ {getStylizedAmount(total, currencyCode)}
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-summary-section/index.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-summary-section/index.ts
new file mode 100644
index 0000000000..65fbe542a2
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-summary-section/index.ts
@@ -0,0 +1 @@
+export * from "./order-summary-section"
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
new file mode 100644
index 0000000000..e3019aea9c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
@@ -0,0 +1,246 @@
+import { Buildings, PencilSquare, ArrowUturnLeft } from "@medusajs/icons"
+import { OrderDTO, OrderLineItemDTO, ReservationItemDTO } from "@medusajs/types"
+import { Container, Copy, Heading, StatusBadge, Text } from "@medusajs/ui"
+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 OrderSummarySectionProps = {
+ order: OrderDTO
+}
+
+export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
+ return (
+
+
+
+
+
+
+ )
+}
+
+const Header = ({ order }: { order: OrderDTO }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
{t("fields.summary")}
+
,
+ // },
+ // {
+ // label: t("orders.summary.allocateItems"),
+ // to: "#", // TODO: Open modal to allocate items
+ // icon:
,
+ // },
+ // {
+ // label: t("orders.summary.requestReturn"),
+ // to: `/orders/${order.id}/returns`,
+ // icon:
,
+ // },
+ ],
+ },
+ ]}
+ />
+
+ )
+}
+
+const Item = ({
+ item,
+ currencyCode,
+ reservation,
+}: {
+ item: OrderLineItemDTO
+ currencyCode: string
+ reservation?: ReservationItemDTO | null
+}) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+
+ {item.title}
+
+ {item.variant_sku && (
+
+ {item.variant_sku}
+
+
+ )}
+
+ {item.variant?.options.map((o) => o.value).join(" · ")}
+
+
+
+
+
+
+ {getLocaleAmount(item.unit_price, currencyCode)}
+
+
+
+
+
+ {item.quantity}x
+
+
+
+
+ {reservation
+ ? t("orders.reservations.allocatedLabel")
+ : t("orders.reservations.notAllocatedLabel")}
+
+
+
+
+
+ {getLocaleAmount(item.subtotal || 0, currencyCode)}
+
+
+
+
+ )
+}
+
+const ItemBreakdown = ({ order }: { order: OrderDTO }) => {
+ // const { reservations, isError, error } = useAdminReservations({
+ // line_item_id: order.items.map((i) => i.id),
+ // })
+
+ // if (isError) {
+ // throw error
+ // }
+
+ return (
+
+ {order.items.map((item) => {
+ // const reservation = reservations
+ // ? reservations.find((r) => r.line_item_id === item.id)
+ // : null
+
+ return (
+
+ )
+ })}
+
+ )
+}
+
+const Cost = ({
+ label,
+ value,
+ secondaryValue,
+}: {
+ label: string
+ value: string | number
+ secondaryValue: string
+}) => (
+
+
+ {label}
+
+
+
+ {secondaryValue}
+
+
+
+
+ {value}
+
+
+
+)
+
+const CostBreakdown = ({ order }: { order: OrderDTO }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ DISCOUNTS link
+ // secondaryValue={
+ // order.discounts.length > 0
+ // ? order.discounts.map((d) => d.code).join(", ")
+ // : "-"
+ // }
+ value={
+ order.discount_total > 0
+ ? `- ${getLocaleAmount(order.discount_total, order.currency_code)}`
+ : "-"
+ }
+ />
+ SHIPPING link
+ // secondaryValue={order.shipping_methods
+ // .map((sm) => sm.shipping_option.name)
+ // .join(", ")}
+ value={getLocaleAmount(order.shipping_total, order.currency_code)}
+ />
+
+
+ )
+}
+
+const Total = ({ order }: { order: OrderDTO }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {t("fields.total")}
+
+
+ {getStylizedAmount(order.total, order.currency_code)}
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/constants.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/constants.ts
new file mode 100644
index 0000000000..d1725847a1
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/constants.ts
@@ -0,0 +1,30 @@
+const DEFAULT_PROPERTIES = [
+ "id",
+ "status",
+ "created_at",
+ "email",
+ // "fulfillment_status", // -> TODO replacement for this
+ // "payment_status", // -> TODO replacement for this
+ "display_id",
+ "currency_code",
+ // --- TOTALS ---
+ "total",
+ "subtotal",
+ "discounts_total",
+ "shipping_total",
+ "tax_total",
+]
+
+const DEFAULT_RELATIONS = [
+ "*customer",
+ "*items", // -> we get LineItem here with added `quantity` and `detail` which is actually an OrderItem (which is a parent object to LineItem in the DB)
+ "*items.variant.options",
+ "*shipping_address",
+ "*billing_address",
+ "*sales_channel",
+ "*promotion",
+]
+
+export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
+ ","
+)},${DEFAULT_RELATIONS.join(",")}`
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/index.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/index.ts
new file mode 100644
index 0000000000..72ffee80e5
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/index.ts
@@ -0,0 +1,2 @@
+export { orderLoader as loader } from "./loader"
+export { OrderDetail as Component } from "./order-detail"
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/loader.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/loader.ts
new file mode 100644
index 0000000000..738aa1348b
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/loader.ts
@@ -0,0 +1,26 @@
+import { AdminOrdersRes } from "@medusajs/medusa"
+import { Response } from "@medusajs/medusa-js"
+import { adminOrderKeys } from "medusa-react"
+import { LoaderFunctionArgs } from "react-router-dom"
+
+import { queryClient } from "../../../lib/medusa"
+import { DEFAULT_FIELDS } from "./constants"
+import { client } from "../../../lib/client"
+
+const orderDetailQuery = (id: string) => ({
+ queryKey: adminOrderKeys.detail(id),
+ queryFn: async () =>
+ client.orders.retrieve(id, {
+ fields: DEFAULT_FIELDS,
+ }),
+})
+
+export const orderLoader = async ({ params }: LoaderFunctionArgs) => {
+ const id = params.id
+ const query = orderDetailQuery(id!)
+
+ return (
+ queryClient.getQueryData>(query.queryKey) ??
+ (await queryClient.fetchQuery(query))
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/order-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/order-detail.tsx
new file mode 100644
index 0000000000..e02fb3e9fe
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-detail/order-detail.tsx
@@ -0,0 +1,57 @@
+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"
+import { OrderPaymentSection } from "./components/order-payment-section"
+import { OrderSummarySection } from "./components/order-summary-section"
+import { DEFAULT_FIELDS } from "./constants"
+import { orderLoader } from "./loader"
+import { useOrder } from "../../../hooks/api/orders"
+
+export const OrderDetail = () => {
+ const initialData = useLoaderData() as Awaited>
+
+ const { id } = useParams()
+
+ const { order, isLoading, isError, error } = useOrder(
+ id!,
+ {
+ fields: DEFAULT_FIELDS,
+ },
+ {
+ initialData,
+ }
+ )
+
+ if (isLoading || !order) {
+ return Loading...
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+
+ {/*
*/}
+ {/*
*/}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-list/components/order-list-table/index.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-list/components/order-list-table/index.ts
new file mode 100644
index 0000000000..f652070734
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-list/components/order-list-table/index.ts
@@ -0,0 +1 @@
+export * from "./order-list-table"
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-list/components/order-list-table/order-list-table.tsx b/packages/admin-next/dashboard/src/v2-routes/orders/order-list/components/order-list-table/order-list-table.tsx
new file mode 100644
index 0000000000..0a643018eb
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-list/components/order-list-table/order-list-table.tsx
@@ -0,0 +1,67 @@
+import { keepPreviousData } from "@tanstack/react-query"
+import { Container, Heading } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+
+import { DataTable } from "../../../../../components/table/data-table/data-table"
+import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns"
+import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters"
+import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query"
+import { useDataTable } from "../../../../../hooks/use-data-table"
+import { useOrders } from "../../../../../hooks/api/orders"
+
+import { DEFAULT_FIELDS } from "../../const"
+
+const PAGE_SIZE = 20
+
+export const OrderListTable = () => {
+ const { t } = useTranslation()
+ const { searchParams, raw } = useOrderTableQuery({
+ pageSize: PAGE_SIZE,
+ })
+
+ const { orders, count, isError, error, isLoading } = useOrders(
+ {
+ fields: DEFAULT_FIELDS,
+ ...searchParams,
+ },
+ {
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const filters = useOrderTableFilters()
+ const columns = useOrderTableColumns({})
+
+ const { table } = useDataTable({
+ data: orders ?? [],
+ columns,
+ enablePagination: true,
+ count,
+ pageSize: PAGE_SIZE,
+ })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+ {t("orders.domain")}
+
+ `/orders/${row.original.id}`}
+ filters={filters}
+ count={count}
+ search
+ isLoading={isLoading}
+ pageSize={PAGE_SIZE}
+ orderBy={["display_id", "created_at", "updated_at"]}
+ queryObject={raw}
+ />
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-list/const.ts b/packages/admin-next/dashboard/src/v2-routes/orders/order-list/const.ts
new file mode 100644
index 0000000000..bc6a37e659
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-list/const.ts
@@ -0,0 +1,17 @@
+const DEFAULT_PROPERTIES = [
+ "id",
+ "status",
+ "created_at",
+ "email",
+ "display_id",
+ // "fulfillment_status", // -> TODO replacement for this
+ // "payment_status", // -> TODO replacement for this
+ "total",
+ "currency_code",
+]
+
+const DEFAULT_RELATIONS = ["*customer", "*sales_channel"]
+
+export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
+ ","
+)},${DEFAULT_RELATIONS.join(",")}`
diff --git a/packages/admin-next/dashboard/src/v2-routes/orders/order-list/order-list.tsx b/packages/admin-next/dashboard/src/v2-routes/orders/order-list/order-list.tsx
index 1edbc2cee1..587cb88bf5 100644
--- a/packages/admin-next/dashboard/src/v2-routes/orders/order-list/order-list.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/orders/order-list/order-list.tsx
@@ -1,5 +1,9 @@
-// TODO: This is just a placeholder to render the actual "home" page
-// after logging in. Replace this with the actual order components when ready
+import { OrderListTable } from "./components/order-list-table"
+
export const OrderList = () => {
- return
+ return (
+
+
+
+ )
}
diff --git a/packages/core/modules-sdk/src/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts
index 9972dabcf6..4670264f6d 100644
--- a/packages/core/modules-sdk/src/medusa-app.ts
+++ b/packages/core/modules-sdk/src/medusa-app.ts
@@ -175,7 +175,13 @@ function isMedusaModule(mod) {
}
function cleanAndMergeSchema(loadedSchema) {
- const { schema: cleanedSchema, notFound } = cleanGraphQLSchema(loadedSchema)
+ const defaultMedusaSchema = `
+ scalar DateTime
+ scalar JSON
+ `
+ const { schema: cleanedSchema, notFound } = cleanGraphQLSchema(
+ defaultMedusaSchema + loadedSchema
+ )
const mergedSchema = mergeTypeDefs(cleanedSchema)
return { schema: makeExecutableSchema({ typeDefs: mergedSchema }), notFound }
}
diff --git a/packages/modules/link-modules/src/definitions/index.ts b/packages/modules/link-modules/src/definitions/index.ts
index c09af37fc2..a6ce650479 100644
--- a/packages/modules/link-modules/src/definitions/index.ts
+++ b/packages/modules/link-modules/src/definitions/index.ts
@@ -3,7 +3,6 @@ export * from "./cart-promotion"
export * from "./fulfillment-set-location"
export * from "./order-promotion"
export * from "./order-region"
-export * from "./order-sales-channel"
export * from "./product-sales-channel"
export * from "./product-shipping-profile"
export * from "./product-variant-inventory-item"
diff --git a/packages/modules/link-modules/src/definitions/order-promotion.ts b/packages/modules/link-modules/src/definitions/order-promotion.ts
index 5ce92b1de8..feaf8d65e5 100644
--- a/packages/modules/link-modules/src/definitions/order-promotion.ts
+++ b/packages/modules/link-modules/src/definitions/order-promotion.ts
@@ -35,11 +35,14 @@ export const OrderPromotion: ModuleJoinerConfig = {
extends: [
{
serviceName: Modules.ORDER,
+ fieldAlias: {
+ promotion: "promotion_link.promotion",
+ },
relationship: {
serviceName: LINKS.OrderPromotion,
primaryKey: "order_id",
foreignKey: "id",
- alias: "order_link",
+ alias: "promotion_link",
},
},
{
@@ -48,7 +51,7 @@ export const OrderPromotion: ModuleJoinerConfig = {
serviceName: LINKS.OrderPromotion,
primaryKey: "promotion_id",
foreignKey: "id",
- alias: "promotion_link",
+ alias: "order_link",
},
},
],
diff --git a/packages/modules/link-modules/src/definitions/order-sales-channel.ts b/packages/modules/link-modules/src/definitions/order-sales-channel.ts
deleted file mode 100644
index 0204b278ee..0000000000
--- a/packages/modules/link-modules/src/definitions/order-sales-channel.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { ModuleJoinerConfig } from "@medusajs/types"
-
-import { Modules } from "@medusajs/modules-sdk"
-import { LINKS } from "@medusajs/utils"
-
-export const OrderSalesChannel: ModuleJoinerConfig = {
- serviceName: LINKS.OrderSalesChannel,
- isLink: true,
- databaseConfig: {
- tableName: "order_sales_channel",
- idPrefix: "ordersc",
- },
- alias: [
- {
- name: "order_sales_channel",
- },
- {
- name: "order_sales_channels",
- },
- ],
- primaryKeys: ["id", "order_id", "sales_channel_id"],
- relationships: [
- {
- serviceName: Modules.ORDER,
- isInternalService: true,
- primaryKey: "id",
- foreignKey: "order_id",
- alias: "order",
- },
- {
- serviceName: "salesChannelService",
- isInternalService: true,
- primaryKey: "id",
- foreignKey: "sales_channel_id",
- alias: "sales_channel",
- },
- ],
- extends: [
- {
- serviceName: Modules.ORDER,
- fieldAlias: {
- sales_channel: "sales_channel_link.sales_channel",
- },
- relationship: {
- serviceName: LINKS.OrderSalesChannel,
- isInternalService: true,
- primaryKey: "order_id",
- foreignKey: "id",
- alias: "sales_channel_link",
- },
- },
- {
- serviceName: "salesChannelService",
- fieldAlias: {
- orders: "order_link.order",
- },
- relationship: {
- serviceName: LINKS.OrderSalesChannel,
- isInternalService: true,
- primaryKey: "sales_channel_id",
- foreignKey: "id",
- alias: "order_link",
- isList: true,
- },
- },
- ],
-}
diff --git a/packages/modules/link-modules/src/definitions/readonly/index.ts b/packages/modules/link-modules/src/definitions/readonly/index.ts
index 9e3ca86a1d..f1df878f68 100644
--- a/packages/modules/link-modules/src/definitions/readonly/index.ts
+++ b/packages/modules/link-modules/src/definitions/readonly/index.ts
@@ -5,4 +5,5 @@ export * from "./cart-sales-channel"
export * from "./inventory-level-stock-location"
export * from "./order-customer"
export * from "./order-product"
+export * from "./order-sales-channel"
export * from "./store-default-currency"
diff --git a/packages/modules/link-modules/src/definitions/readonly/order-sales-channel.ts b/packages/modules/link-modules/src/definitions/readonly/order-sales-channel.ts
new file mode 100644
index 0000000000..1e86614f66
--- /dev/null
+++ b/packages/modules/link-modules/src/definitions/readonly/order-sales-channel.ts
@@ -0,0 +1,29 @@
+import { ModuleJoinerConfig } from "@medusajs/types"
+
+import { Modules } from "@medusajs/modules-sdk"
+
+export const OrderSalesChannel: ModuleJoinerConfig = {
+ isLink: true,
+ isReadOnlyLink: true,
+ extends: [
+ {
+ serviceName: Modules.ORDER,
+ relationship: {
+ serviceName: Modules.SALES_CHANNEL,
+ primaryKey: "id",
+ foreignKey: "sales_channel_id",
+ alias: "sales_channel",
+ },
+ },
+ {
+ serviceName: Modules.SALES_CHANNEL,
+ relationship: {
+ serviceName: Modules.ORDER,
+ primaryKey: "sales_channel_id",
+ foreignKey: "id",
+ alias: "orders",
+ isList: true,
+ },
+ },
+ ],
+}