diff --git a/packages/admin-next/dashboard/package.json b/packages/admin-next/dashboard/package.json
index 11bd6c40ec..d82741aea5 100644
--- a/packages/admin-next/dashboard/package.json
+++ b/packages/admin-next/dashboard/package.json
@@ -35,6 +35,7 @@
"match-sorter": "^6.3.4",
"medusa-react": "workspace:^",
"react": "18.2.0",
+ "react-country-flag": "^3.1.0",
"react-dom": "18.2.0",
"react-focus-lock": "^2.11.1",
"react-hook-form": "7.49.1",
diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json
index 63df2daa59..e50fcc654b 100644
--- a/packages/admin-next/dashboard/public/locales/en-US/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json
@@ -65,6 +65,11 @@
"defaultTitle": "An error occurred",
"defaultMessage": "An error occurred while rendering this page."
},
+ "addresses": {
+ "shippingAddress": "Shipping Address",
+ "billingAddress": "Billing Address",
+ "sameAsShipping": "Same as shipping address"
+ },
"products": {
"domain": "Products",
"createProductTitle": "Create Product",
@@ -164,27 +169,62 @@
},
"orders": {
"domain": "Orders",
- "paymentStatusLabel": "Payment Status",
- "paymentStatus": {
- "notPaid": "Not Paid",
- "awaiting": "Awaiting",
- "captured": "Captured",
- "partiallyRefunded": "Partially Refunded",
- "refunded": "Refunded",
- "canceled": "Canceled",
- "requresAction": "Requires Action"
+ "cancelWarning": "You are about to cancel the order {{id}}. This action cannot be undone.",
+ "onDateFromSalesChannel": "{{date}} from {{salesChannel}}",
+ "summary": {
+ "allocateItems": "Allocate items",
+ "editItems": "Edit items"
},
- "fulfillmentStatusLabel": "Fulfillment Status",
- "fulfillmentStatus": {
- "notFulfilled": "Not Fulfilled",
- "partiallyFulfilled": "Partially Fulfilled",
- "fulfilled": "Fulfilled",
- "partiallyShipped": "Partially Shipped",
- "shipped": "Shipped",
- "partiallyReturned": "Partially Returned",
- "returned": "Returned",
- "canceled": "Canceled",
- "requresAction": "Requires Action"
+ "payment": {
+ "title": "Payments",
+ "isReadyToBeCaptured": "Payment {{id}} is ready to be captured.",
+ "totalPaidByCustomer": "Total paid by customer",
+ "capture": "Capture",
+ "refund": "Refund",
+ "statusLabel": "Payment status",
+ "statusTitle": "Payment Status",
+ "status": {
+ "notPaid": "Not paid",
+ "awaiting": "Awaiting",
+ "captured": "Captured",
+ "partiallyRefunded": "Partially refunded",
+ "refunded": "Refunded",
+ "canceled": "Canceled",
+ "requresAction": "Requires action"
+ }
+ },
+ "reservations": {
+ "allocatedLabel": "Allocated",
+ "notAllocatedLabel": "Not allocated"
+ },
+ "fulfillment": {
+ "cancelWarning": "You are about to cancel a fulfillment. This action cannot be undone.",
+ "unfulfilledItems": "Unfulfilled Items",
+ "statusLabel": "Fulfillment status",
+ "statusTitle": "Fulfillment Status",
+ "awaitingFullfillmentBadge": "Awaiting fulfillment",
+ "number": "Fulfillment #{{number}}",
+ "status": {
+ "notFulfilled": "Not fulfilled",
+ "partiallyFulfilled": "Partially fulfilled",
+ "fulfilled": "Fulfilled",
+ "partiallyShipped": "Partially shipped",
+ "shipped": "Shipped",
+ "partiallyReturned": "Partially returned",
+ "returned": "Returned",
+ "canceled": "Canceled",
+ "requresAction": "Requires action"
+ },
+ "trackingLabel": "Tracking",
+ "shippingFromLabel": "Shipping from",
+ "itemsLabel": "Items"
+ },
+ "customer": {
+ "contactLabel": "Contact",
+ "editEmail": "Edit email",
+ "transferOwnership": "Transfer ownership",
+ "editBillingAddress": "Edit billing address",
+ "editShippingAddress": "Edit shipping address"
}
},
"draftOrders": {
@@ -429,6 +469,9 @@
"orders": "Orders",
"account": "Account",
"total": "Total",
+ "subtotal": "Subtotal",
+ "shipping": "Shipping",
+ "tax": "Tax",
"created": "Created",
"key": "Key",
"customer": "Customer",
@@ -466,8 +509,10 @@
"thumbnail": "Thumbnail",
"sku": "SKU",
"managedInventory": "Managed inventory",
+ "id": "ID",
"minSubtotal": "Min. Subtotal",
"maxSubtotal": "Max. Subtotal",
- "shippingProfile": "Shipping Profile"
+ "shippingProfile": "Shipping Profile",
+ "summary": "Summary"
}
}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx
index 57a9cf0e66..56a981054d 100644
--- a/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx
@@ -1,5 +1,5 @@
import { clx } from "@medusajs/ui"
-import { getPresentationalAmount } from "../../../../../lib/money-amount-helpers"
+import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
import { PlaceholderCell } from "../placeholder-cell"
type MoneyAmountCellProps = {
@@ -17,18 +17,7 @@ export const MoneyAmountCell = ({
return
}
- const formatted = new Intl.NumberFormat(undefined, {
- style: "currency",
- currency: currencyCode,
- currencyDisplay: "narrowSymbol",
- }).format(0)
-
- const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim()
-
- const presentationAmount = getPresentationalAmount(amount, currencyCode)
- const formattedTotal = new Intl.NumberFormat(undefined, {
- style: "decimal",
- }).format(presentationAmount)
+ const formatted = getStylizedAmount(amount, currencyCode)
return (
-
- {symbol} {formattedTotal} {currencyCode.toUpperCase()}
-
+ {formatted}
)
}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/country-cell/country-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/country-cell/country-cell.tsx
new file mode 100644
index 0000000000..33114a4793
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/country-cell/country-cell.tsx
@@ -0,0 +1,28 @@
+import { Country } from "@medusajs/medusa"
+import { Tooltip } from "@medusajs/ui"
+import ReactCountryFlag from "react-country-flag"
+import { PlaceholderCell } from "../../common/placeholder-cell"
+
+export const CountryCell = ({ country }: { country?: Country | null }) => {
+ if (!country) {
+ return
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/country-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/country-cell/index.ts
new file mode 100644
index 0000000000..81953fd274
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/country-cell/index.ts
@@ -0,0 +1 @@
+export * from "./country-cell"
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx
index af558b959c..5dd516c5c3 100644
--- a/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx
@@ -11,9 +11,7 @@ export const CustomerCell = ({ customer }: { customer: Customer | null }) => {
return (
-
- {name || email}
-
+
{name || email}
)
}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx
index d899e85468..71add7608c 100644
--- a/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx
@@ -12,24 +12,24 @@ export const FulfillmentStatusCell = ({
const { t } = useTranslation()
const [label, color] = {
- not_fulfilled: [t("orders.fulfillmentStatus.notFulfilled"), "red"],
+ not_fulfilled: [t("orders.fulfillment.status.notFulfilled"), "red"],
partially_fulfilled: [
- t("orders.fulfillmentStatus.partiallyFulfilled"),
+ t("orders.fulfillment.status.partiallyFulfilled"),
"orange",
],
- fulfilled: [t("orders.fulfillmentStatus.fulfilled"), "green"],
+ fulfilled: [t("orders.fulfillment.status.fulfilled"), "green"],
partially_shipped: [
- t("orders.fulfillmentStatus.partiallyShipped"),
+ t("orders.fulfillment.status.partiallyShipped"),
"orange",
],
- shipped: [t("orders.fulfillmentStatus.shipped"), "green"],
+ shipped: [t("orders.fulfillment.status.shipped"), "green"],
partially_returned: [
- t("orders.fulfillmentStatus.partiallyReturned"),
+ t("orders.fulfillment.status.partiallyReturned"),
"orange",
],
- returned: [t("orders.fulfillmentStatus.returned"), "green"],
- canceled: [t("orders.fulfillmentStatus.canceled"), "red"],
- requires_action: [t("orders.fulfillmentStatus.requresAction"), "orange"],
+ returned: [t("orders.fulfillment.status.returned"), "green"],
+ canceled: [t("orders.fulfillment.status.canceled"), "red"],
+ requires_action: [t("orders.fulfillment.status.requresAction"), "orange"],
}[status] as [string, "red" | "orange" | "green"]
return {label}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts
deleted file mode 100644
index 8df6791eae..0000000000
--- a/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./items-cell"
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx
deleted file mode 100644
index fc0c3b3403..0000000000
--- a/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { LineItem } from "@medusajs/medusa"
-import { useTranslation } from "react-i18next"
-
-export const ItemsCell = ({ items }: { items: LineItem[] }) => {
- const { t } = useTranslation()
-
- return (
-
-
- {t("general.items", {
- count: items.length,
- })}
-
-
- )
-}
-
-export const ItemsHeader = () => {
- const { t } = useTranslation()
-
- return (
-
- {t("fields.items")}
-
- )
-}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx
index db8828fa26..01eef86a8d 100644
--- a/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx
@@ -10,13 +10,16 @@ export const PaymentStatusCell = ({ status }: PaymentStatusCellProps) => {
const { t } = useTranslation()
const [label, color] = {
- not_paid: [t("orders.paymentStatus.notPaid"), "red"],
- awaiting: [t("orders.paymentStatus.awaiting"), "orange"],
- captured: [t("orders.paymentStatus.captured"), "green"],
- refunded: [t("orders.paymentStatus.refunded"), "green"],
- partially_refunded: [t("orders.paymentStatus.partiallyRefunded"), "orange"],
- canceled: [t("orders.paymentStatus.canceled"), "red"],
- requires_action: [t("orders.paymentStatus.requresAction"), "orange"],
+ not_paid: [t("orders.payment.status.notPaid"), "red"],
+ awaiting: [t("orders.payment.status.awaiting"), "orange"],
+ captured: [t("orders.payment.status.captured"), "green"],
+ refunded: [t("orders.payment.status.refunded"), "green"],
+ partially_refunded: [
+ t("orders.payment.status.partiallyRefunded"),
+ "orange",
+ ],
+ canceled: [t("orders.payment.status.canceled"), "red"],
+ requires_action: [t("orders.payment.status.requresAction"), "orange"],
}[status] as [string, "red" | "orange" | "green"]
return {label}
diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx
index 490db287b1..50ca39028f 100644
--- a/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx
+++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx
@@ -9,6 +9,11 @@ import {
DateCell,
DateHeader,
} from "../../../components/table/table-cells/common/date-cell"
+import { CountryCell } from "../../../components/table/table-cells/order/country-cell"
+import {
+ CustomerCell,
+ CustomerHeader,
+} from "../../../components/table/table-cells/order/customer-cell"
import {
DisplayIdCell,
DisplayIdHeader,
@@ -17,10 +22,6 @@ import {
FulfillmentStatusCell,
FulfillmentStatusHeader,
} from "../../../components/table/table-cells/order/fulfillment-status-cell"
-import {
- ItemsCell,
- ItemsHeader,
-} from "../../../components/table/table-cells/order/items-cell"
import {
PaymentStatusCell,
PaymentStatusHeader,
@@ -62,6 +63,14 @@ export const useOrderTableColumns = (props: UseOrderTableColumnsProps) => {
return
},
}),
+ columnHelper.accessor("customer", {
+ header: () => ,
+ cell: ({ getValue }) => {
+ const customer = getValue()
+
+ return
+ },
+ }),
columnHelper.accessor("sales_channel", {
header: () => ,
cell: ({ getValue }) => {
@@ -86,14 +95,6 @@ export const useOrderTableColumns = (props: UseOrderTableColumnsProps) => {
return
},
}),
- columnHelper.accessor("items", {
- header: () => ,
- cell: ({ getValue }) => {
- const items = getValue()
-
- return
- },
- }),
columnHelper.accessor("total", {
header: () => ,
cell: ({ getValue, row }) => {
@@ -103,6 +104,14 @@ export const useOrderTableColumns = (props: UseOrderTableColumnsProps) => {
return
},
}),
+ columnHelper.display({
+ id: "actions",
+ cell: ({ row }) => {
+ const country = row.original.shipping_address?.country
+
+ return
+ },
+ }),
],
[]
)
diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx
index aa5b309f05..0c2b6348bc 100644
--- a/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx
+++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx
@@ -54,36 +54,36 @@ export const useOrderTableFilters = (): Filter[] => {
const paymentStatusFilter: Filter = {
key: "payment_status",
- label: t("orders.paymentStatusLabel"),
+ label: t("orders.payment.statusLabel"),
type: "select",
multiple: true,
options: [
{
- label: t("orders.paymentStatus.notPaid"),
+ label: t("orders.payment.status.notPaid"),
value: "not_paid",
},
{
- label: t("orders.paymentStatus.awaiting"),
+ label: t("orders.payment.status.awaiting"),
value: "awaiting",
},
{
- label: t("orders.paymentStatus.captured"),
+ label: t("orders.payment.status.captured"),
value: "captured",
},
{
- label: t("orders.paymentStatus.refunded"),
+ label: t("orders.payment.status.refunded"),
value: "refunded",
},
{
- label: t("orders.paymentStatus.partiallyRefunded"),
+ label: t("orders.payment.status.partiallyRefunded"),
value: "partially_refunded",
},
{
- label: t("orders.paymentStatus.canceled"),
+ label: t("orders.payment.status.canceled"),
value: "canceled",
},
{
- label: t("orders.paymentStatus.requresAction"),
+ label: t("orders.payment.status.requresAction"),
value: "requires_action",
},
],
@@ -91,44 +91,44 @@ export const useOrderTableFilters = (): Filter[] => {
const fulfillmentStatusFilter: Filter = {
key: "fulfillment_status",
- label: t("orders.fulfillmentStatusLabel"),
+ label: t("orders.fulfillment.statusLabel"),
type: "select",
multiple: true,
options: [
{
- label: t("orders.fulfillmentStatus.notFulfilled"),
+ label: t("orders.fulfillment.status.notFulfilled"),
value: "not_fulfilled",
},
{
- label: t("orders.fulfillmentStatus.fulfilled"),
+ label: t("orders.fulfillment.status.fulfilled"),
value: "fulfilled",
},
{
- label: t("orders.fulfillmentStatus.partiallyFulfilled"),
+ label: t("orders.fulfillment.status.partiallyFulfilled"),
value: "partially_fulfilled",
},
{
- label: t("orders.fulfillmentStatus.returned"),
+ label: t("orders.fulfillment.status.returned"),
value: "returned",
},
{
- label: t("orders.fulfillmentStatus.partiallyReturned"),
+ label: t("orders.fulfillment.status.partiallyReturned"),
value: "partially_returned",
},
{
- label: t("orders.fulfillmentStatus.shipped"),
+ label: t("orders.fulfillment.status.shipped"),
value: "shipped",
},
{
- label: t("orders.fulfillmentStatus.partiallyShipped"),
+ label: t("orders.fulfillment.status.partiallyShipped"),
value: "partially_shipped",
},
{
- label: t("orders.fulfillmentStatus.canceled"),
+ label: t("orders.fulfillment.status.canceled"),
value: "canceled",
},
{
- label: t("orders.fulfillmentStatus.requresAction"),
+ label: t("orders.fulfillment.status.requresAction"),
value: "requires_action",
},
],
diff --git a/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts b/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts
index d043af4f37..a4de8bf37d 100644
--- a/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts
+++ b/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts
@@ -1,21 +1,95 @@
import { currencies } from "./currencies"
-export const getPresentationalAmount = (amount: number, currency: string) => {
- const decimalDigits = currencies[currency.toUpperCase()].decimal_digits
+const getDecimalDigits = (currency: string) => {
+ return currencies[currency.toUpperCase()]?.decimal_digits
+}
- if (decimalDigits === 0) {
+/**
+ * Converts an amount from the database format (cents) to the presentational format
+ *
+ * @param amount - The amount to format
+ * @param currency - The currency code to format the amount in
+ * @returns The formatted amount
+ *
+ * @example
+ * getPresentationalAmount(1000, "usd") // 10
+ * getPresentationalAmount(1000, "jpy") // 1000
+ */
+export const getPresentationalAmount = (amount: number, currency: string) => {
+ const decimalDigits = getDecimalDigits(currency)
+
+ if (!decimalDigits) {
throw new Error("Currency has no decimal digits")
}
return amount / 10 ** decimalDigits
}
+/**
+ * Converts an amount to the database format (cents)
+ * @param amount - The amount to convert to the database amount
+ * @param currency - The currency code to convert the amount to
+ * @returns The amount in the database format
+ *
+ * @example
+ * getDbAmount(10.5, "usd") // 1050
+ * getDbAmount(10, "jpy") // 10
+ */
export const getDbAmount = (amount: number, currency: string) => {
const decimalDigits = currencies[currency.toUpperCase()].decimal_digits
- if (decimalDigits === 0) {
+ if (!decimalDigits) {
throw new Error("Currency has no decimal digits")
}
return amount * 10 ** decimalDigits
}
+
+/**
+ * Returns a formatted amount based on the currency code using the browser's locale
+ * @param amount - The amount to format
+ * @param currencyCode - The currency code to format the amount in
+ * @returns - The formatted amount
+ *
+ * @example
+ * getFormattedAmount(1000, "usd") // '$10.00' if the browser's locale is en-US
+ * getFormattedAmount(1000, "usd") // '10,00 $' if the browser's locale is fr-FR
+ */
+export const getLocaleAmount = (amount: number, currencyCode: string) => {
+ const formatter = new Intl.NumberFormat(undefined, {
+ style: "currency",
+ currencyDisplay: "narrowSymbol",
+ currency: currencyCode,
+ })
+
+ return formatter.format(getPresentationalAmount(amount, currencyCode))
+}
+
+export const getNativeSymbol = (currencyCode: string) => {
+ const formatted = new Intl.NumberFormat(undefined, {
+ style: "currency",
+ currency: currencyCode,
+ currencyDisplay: "narrowSymbol",
+ }).format(0)
+
+ return formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim()
+}
+
+/**
+ * In some cases we want to display the amount with the currency code and symbol,
+ * in the format of "symbol amount currencyCode". This breaks from the
+ * user's locale and is only used in cases where we want to display the
+ * currency code and symbol explicitly, e.g. for totals.
+ */
+export const getStylizedAmount = (amount: number, currencyCode: string) => {
+ const symbol = getNativeSymbol(currencyCode)
+ const decimalDigits = getDecimalDigits(currencyCode)
+ const presentationAmount = getPresentationalAmount(amount, currencyCode)
+
+ const total = presentationAmount.toLocaleString(undefined, {
+ minimumFractionDigits: decimalDigits,
+ maximumFractionDigits: decimalDigits,
+ })
+
+ return `${symbol} ${total} ${currencyCode.toUpperCase()}`
+}
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
index 31e18640a8..9bfa8a61ca 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
@@ -3,6 +3,7 @@ import type {
AdminCustomerGroupsRes,
AdminCustomersRes,
AdminGiftCardsRes,
+ AdminOrdersRes,
AdminProductsRes,
AdminPublishableApiKeysRes,
AdminRegionsRes,
@@ -87,12 +88,16 @@ const router = createBrowserRouter([
},
children: [
{
- index: true,
+ path: "",
lazy: () => import("../../routes/orders/order-list"),
},
{
path: ":id",
- lazy: () => import("../../routes/orders/details"),
+ lazy: () => import("../../routes/orders/order-detail"),
+ handle: {
+ crumb: (data: AdminOrdersRes) =>
+ `Order #${data.order.display_id}`,
+ },
},
],
},
diff --git a/packages/admin-next/dashboard/src/routes/orders/details/details.tsx b/packages/admin-next/dashboard/src/routes/orders/details/details.tsx
deleted file mode 100644
index 24ef1a5830..0000000000
--- a/packages/admin-next/dashboard/src/routes/orders/details/details.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export const OrderDetails = () => {
- return Order Details
;
-};
diff --git a/packages/admin-next/dashboard/src/routes/orders/details/index.ts b/packages/admin-next/dashboard/src/routes/orders/details/index.ts
deleted file mode 100644
index 0e5b01fb73..0000000000
--- a/packages/admin-next/dashboard/src/routes/orders/details/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { OrderDetails as Component } from "./details";
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-customer-section/index.ts b/packages/admin-next/dashboard/src/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/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/routes/orders/order-detail/components/order-customer-section/order-customer-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-customer-section/order-customer-section.tsx
new file mode 100644
index 0000000000..b941bf80b3
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-customer-section/order-customer-section.tsx
@@ -0,0 +1,319 @@
+import { Address as MedusaAddress, Order } from "@medusajs/medusa"
+import { Avatar, Container, Copy, Heading, Text } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { Link } from "react-router-dom"
+
+import { ArrowPath, CurrencyDollar, Envelope, FlyingBox } from "@medusajs/icons"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+
+type OrderCustomerSectionProps = {
+ order: Order
+}
+
+export const OrderCustomerSection = ({ order }: OrderCustomerSectionProps) => {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+const Header = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
{t("fields.customer")}
+
,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ label: t("orders.customer.editShippingAddress"),
+ to: `#`, // TODO: Open modal to edit shipping address
+ icon:
,
+ },
+ {
+ label: t("orders.customer.editBillingAddress"),
+ to: `#`, // TODO: Open modal to edit billing address
+ icon:
,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ label: t("orders.customer.editEmail"),
+ to: `#`, // TODO: Open modal to edit email
+ icon:
,
+ },
+ ],
+ },
+ ]}
+ />
+
+ )
+}
+
+const getCustomerName = (order: Order) => {
+ const { first_name: sFirstName, last_name: sLastName } =
+ order.shipping_address || {}
+ const { first_name: bFirstName, last_name: bLastName } =
+ order.billing_address || {}
+ const { first_name: cFirstName, last_name: cLastName } = order.customer || {}
+
+ const customerName = [cFirstName, cLastName].filter(Boolean).join(" ")
+ const shippingName = [sFirstName, sLastName].filter(Boolean).join(" ")
+ const billingName = [bFirstName, bLastName].filter(Boolean).join(" ")
+
+ const name = customerName || shippingName || billingName
+
+ return name
+}
+
+const ID = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ const id = order.customer_id
+ const name = getCustomerName(order)
+ const email = order.email
+ const fallback = (name || email).charAt(0).toUpperCase()
+
+ return (
+
+
+ {t("fields.id")}
+
+
+
+
+
+ {name || email}
+
+
+
+
+ )
+}
+
+const Company = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+ const company =
+ order.shipping_address?.company || order.billing_address?.company
+
+ if (!company) {
+ return null
+ }
+
+ return (
+
+
+ {t("fields.company")}
+
+
+ {company}
+
+
+ )
+}
+
+const Contact = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ const phone = order.shipping_address?.phone || order.billing_address?.phone
+ const email = order.email
+
+ return (
+
+
+ {t("orders.customer.contactLabel")}
+
+
+
+ )
+}
+
+const isSameAddress = (a: MedusaAddress | null, b: MedusaAddress | null) => {
+ if (!a || !b) {
+ return false
+ }
+
+ return (
+ a.first_name === b.first_name &&
+ a.last_name === b.last_name &&
+ a.address_1 === b.address_1 &&
+ a.address_2 === b.address_2 &&
+ a.city === b.city &&
+ a.postal_code === b.postal_code &&
+ a.province === b.province &&
+ a.country_code === b.country_code
+ )
+}
+
+const getFormattedAddress = ({ address }: { address: MedusaAddress }) => {
+ const {
+ first_name,
+ last_name,
+ company,
+ address_1,
+ address_2,
+ city,
+ postal_code,
+ province,
+ country,
+ } = address
+
+ const name = [first_name, last_name].filter(Boolean).join(" ")
+
+ const formattedAddress = []
+
+ if (name) {
+ formattedAddress.push(name)
+ }
+
+ if (company) {
+ formattedAddress.push(company)
+ }
+
+ if (address_1) {
+ formattedAddress.push(address_1)
+ }
+
+ if (address_2) {
+ formattedAddress.push(address_2)
+ }
+
+ const cityProvincePostal = [city, province, postal_code]
+ .filter(Boolean)
+ .join(" ")
+
+ if (cityProvincePostal) {
+ formattedAddress.push(cityProvincePostal)
+ }
+
+ if (country) {
+ formattedAddress.push(country.display_name)
+ }
+
+ return formattedAddress
+}
+
+const Address = ({
+ address,
+ type,
+}: {
+ address: MedusaAddress | null
+ type: "shipping" | "billing"
+}) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {type === "shipping"
+ ? t("addresses.shippingAddress")
+ : t("addresses.billingAddress")}
+
+ {address ? (
+
+
+ {getFormattedAddress({ address }).map((line, i) => {
+ return (
+
+ {line}
+
+
+ )
+ })}
+
+
+
+
+
+ ) : (
+
+ -
+
+ )}
+
+ )
+}
+
+const Addresses = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {!isSameAddress(order.shipping_address, order.billing_address) ? (
+
+ ) : (
+
+
+ {t("addresses.billingAddress")}
+
+
+ {t("addresses.sameAsShipping")}
+
+
+ )}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/index.ts b/packages/admin-next/dashboard/src/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/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/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx
new file mode 100644
index 0000000000..0e2b878b43
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx
@@ -0,0 +1,314 @@
+import { XCircle } from "@medusajs/icons"
+import {
+ LineItem,
+ Fulfillment as MedusaFulfillment,
+ Order,
+} from "@medusajs/medusa"
+import {
+ Container,
+ Copy,
+ Heading,
+ StatusBadge,
+ Text,
+ Tooltip,
+ usePrompt,
+} from "@medusajs/ui"
+import { format } from "date-fns"
+import { useAdminCancelFulfillment, useAdminStockLocation } from "medusa-react"
+import { useTranslation } from "react-i18next"
+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 { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
+
+type OrderFulfillmentSectionProps = {
+ order: Order
+}
+
+export const OrderFulfillmentSection = ({
+ order,
+}: OrderFulfillmentSectionProps) => {
+ const fulfillments = order.fulfillments || []
+
+ return (
+
+
+ {fulfillments.map((f, index) => (
+
+ ))}
+
+ )
+}
+
+const UnfulfilledItem = ({
+ item,
+ currencyCode,
+}: {
+ item: LineItem
+ 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,
+ order,
+ index,
+}: {
+ fulfillment: MedusaFulfillment
+ order: Order
+ index: number
+}) => {
+ const { t } = useTranslation()
+ const prompt = usePrompt()
+
+ const showLocation = !!fulfillment.location_id
+
+ const { stock_location, isError, error } = useAdminStockLocation(
+ 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 } = useAdminCancelFulfillment(order.id)
+
+ const handleCancel = async () => {
+ if (fulfillment.shipped_at) {
+ // TODO: When we have implemented Toasts we should show an error toast here
+ return
+ }
+
+ const res = await prompt({
+ title: t("general.areYouSure"),
+ description: t("orders.fulfillment.cancelWarning"),
+ confirmText: t("actions.continue"),
+ cancelText: t("actions.cancel"),
+ })
+
+ if (res) {
+ await mutateAsync(fulfillment.id)
+ }
+ }
+
+ 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("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/routes/orders/order-detail/components/order-general-section/index.ts b/packages/admin-next/dashboard/src/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/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/routes/orders/order-detail/components/order-general-section/order-general-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-general-section/order-general-section.tsx
new file mode 100644
index 0000000000..e6d50e2f7c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-general-section/order-general-section.tsx
@@ -0,0 +1,132 @@
+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 { useAdminCancelOrder } from "medusa-react"
+import { useTranslation } from "react-i18next"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+
+type OrderGeneralSectionProps = {
+ order: Order
+}
+
+export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
+ const { t } = useTranslation()
+ const prompt = usePrompt()
+
+ const { mutateAsync } = useAdminCancelOrder(order.id)
+
+ 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()
+
+ const [label, color] = {
+ not_fulfilled: [t("orders.fulfillment.status.notFulfilled"), "red"],
+ partially_fulfilled: [
+ t("orders.fulfillment.status.partiallyFulfilled"),
+ "orange",
+ ],
+ fulfilled: [t("orders.fulfillment.status.fulfilled"), "green"],
+ partially_shipped: [
+ t("orders.fulfillment.status.partiallyShipped"),
+ "orange",
+ ],
+ shipped: [t("orders.fulfillment.status.shipped"), "green"],
+ partially_returned: [
+ t("orders.fulfillment.status.partiallyReturned"),
+ "orange",
+ ],
+ returned: [t("orders.fulfillment.status.returned"), "green"],
+ canceled: [t("orders.fulfillment.status.canceled"), "red"],
+ requires_action: [t("orders.fulfillment.status.requresAction"), "orange"],
+ }[order.fulfillment_status] as [string, "red" | "orange" | "green"]
+
+ return (
+
+ {label}
+
+ )
+}
+
+const PaymentBadge = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ const [label, color] = {
+ not_paid: [t("orders.payment.status.notPaid"), "red"],
+ awaiting: [t("orders.payment.status.awaiting"), "orange"],
+ captured: [t("orders.payment.status.captured"), "green"],
+ refunded: [t("orders.payment.status.refunded"), "green"],
+ partially_refunded: [
+ t("orders.payment.status.partiallyRefunded"),
+ "orange",
+ ],
+ canceled: [t("orders.payment.status.canceled"), "red"],
+ requires_action: [t("orders.payment.status.requresAction"), "orange"],
+ }[order.payment_status] as [string, "red" | "orange" | "green"]
+
+ return (
+
+ {label}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/index.ts b/packages/admin-next/dashboard/src/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/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/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx
new file mode 100644
index 0000000000..071056dbf3
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx
@@ -0,0 +1,280 @@
+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()
+
+ return (
+
+
{t("orders.payment.title")}
+
,
+ to: "#", // TODO: Go to general refund modal
+ },
+ ],
+ },
+ ]}
+ />
+
+ )
+}
+
+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: "#", // TODO: Go to specific payment refund modal
+ },
+ ],
+ },
+ ]}
+ />
+
+ {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/routes/orders/order-detail/components/order-summary-section/index.ts b/packages/admin-next/dashboard/src/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/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/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
new file mode 100644
index 0000000000..ec6e68a607
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
@@ -0,0 +1,239 @@
+import { Buildings, PencilSquare } from "@medusajs/icons"
+import { LineItem, Order } 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 OrderSummarySectionProps = {
+ order: Order
+}
+
+export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
+ return (
+
+
+
+
+
+
+ )
+}
+
+const Header = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
{t("fields.summary")}
+
,
+ },
+ {
+ label: t("orders.summary.allocateItems"),
+ to: "#", // TODO: Open modal to allocate items
+ icon:
,
+ },
+ ],
+ },
+ ]}
+ />
+
+ )
+}
+
+const Item = ({
+ item,
+ currencyCode,
+ reservation,
+}: {
+ item: LineItem
+ 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: Order }) => {
+ 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: Order }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ 0
+ ? order.discounts.map((d) => d.code).join(", ")
+ : "-"
+ }
+ value={
+ order.discount_total > 0
+ ? `- ${getLocaleAmount(order.discount_total, order.currency_code)}`
+ : "-"
+ }
+ />
+ sm.shipping_option.name)
+ .join(", ")}
+ value={getLocaleAmount(order.shipping_total, order.currency_code)}
+ />
+
+
+ )
+}
+
+const Total = ({ order }: { order: Order }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {t("fields.total")}
+
+
+ {getStylizedAmount(order.total, order.currency_code)}
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts
new file mode 100644
index 0000000000..bd7e8fe093
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts
@@ -0,0 +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"
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-detail/index.ts
new file mode 100644
index 0000000000..72ffee80e5
--- /dev/null
+++ b/packages/admin-next/dashboard/src/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/routes/orders/order-detail/loader.ts b/packages/admin-next/dashboard/src/routes/orders/order-detail/loader.ts
new file mode 100644
index 0000000000..9230da099e
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/loader.ts
@@ -0,0 +1,25 @@
+import { AdminOrdersRes } from "@medusajs/medusa"
+import { Response } from "@medusajs/medusa-js"
+import { adminOrderKeys } from "medusa-react"
+import { LoaderFunctionArgs } from "react-router-dom"
+
+import { medusa, queryClient } from "../../../lib/medusa"
+import { orderExpand } from "./constants"
+
+const orderDetailQuery = (id: string) => ({
+ queryKey: adminOrderKeys.detail(id),
+ queryFn: async () =>
+ medusa.admin.orders.retrieve(id, {
+ expand: orderExpand,
+ }),
+})
+
+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/routes/orders/order-detail/order-detail.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/order-detail.tsx
new file mode 100644
index 0000000000..108b7f5939
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/order-detail.tsx
@@ -0,0 +1,52 @@
+import { useAdminOrder } from "medusa-react"
+import { useLoaderData, useParams } from "react-router-dom"
+import { JsonViewSection } from "../../../components/common/json-view-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 { orderExpand } from "./constants"
+import { orderLoader } from "./loader"
+
+export const OrderDetail = () => {
+ const initialData = useLoaderData() as Awaited>
+
+ const { id } = useParams()
+
+ const { order, isLoading, isError, error } = useAdminOrder(
+ id!,
+ {
+ expand: orderExpand,
+ },
+ {
+ initialData,
+ }
+ )
+
+ if (isLoading || !order) {
+ return Loading...
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx
index 04274e1863..c2716f1840 100644
--- a/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx
+++ b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx
@@ -8,7 +8,8 @@ import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-t
import { useDataTable } from "../../../../../hooks/use-data-table"
const PAGE_SIZE = 20
-const DEFAULT_RELATIONS = "customer,items,sales_channel"
+const DEFAULT_RELATIONS =
+ "customer,items,sales_channel,shipping_address,shipping_address.country"
const DEFAULT_FIELDS =
"id,status,display_id,created_at,email,fulfillment_status,payment_status,total,currency_code"
diff --git a/packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx
index b823d9c715..903819b97d 100644
--- a/packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx
+++ b/packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx
@@ -47,13 +47,13 @@ export const ProductDetail = () => {
)
})}
-
+
-
+
{sideBefore.widgets.map((w, i) => {
return (
@@ -81,7 +81,7 @@ export const ProductDetail = () => {
})}
-
+
{sideBefore.widgets.map((w, i) => {
return (
diff --git a/yarn.lock b/yarn.lock
index 9649071ecf..cc63a681b9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8170,6 +8170,7 @@ __metadata:
postcss: ^8.4.33
prettier: ^3.1.1
react: 18.2.0
+ react-country-flag: ^3.1.0
react-dom: 18.2.0
react-focus-lock: ^2.11.1
react-hook-form: 7.49.1
@@ -43584,7 +43585,7 @@ __metadata:
languageName: node
linkType: hard
-"react-country-flag@npm:^3.0.2":
+"react-country-flag@npm:^3.0.2, react-country-flag@npm:^3.1.0":
version: 3.1.0
resolution: "react-country-flag@npm:3.1.0"
peerDependencies: