feat(dashboard): Order details page (#6538)
**What** - Adds the different display cards to the order details page. This does not include any of the different forms for editing various aspects of an order.
This commit is contained in:
committed by
GitHub
parent
62a7bcc30c
commit
e5dc918be5
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <PlaceholderCell />
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
@@ -37,9 +26,7 @@ export const MoneyAmountCell = ({
|
||||
"justify-end text-right": align === "right",
|
||||
})}
|
||||
>
|
||||
<span className="truncate">
|
||||
{symbol} {formattedTotal} {currencyCode.toUpperCase()}
|
||||
</span>
|
||||
<span className="truncate">{formatted}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-5 items-center justify-center">
|
||||
<Tooltip content={country.display_name}>
|
||||
<div className="flex size-4 items-center justify-center overflow-hidden rounded-sm">
|
||||
<ReactCountryFlag
|
||||
countryCode={country.iso_2.toUpperCase()}
|
||||
svg
|
||||
style={{
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
}}
|
||||
aria-label={country.display_name}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./country-cell"
|
||||
@@ -11,9 +11,7 @@ export const CustomerCell = ({ customer }: { customer: Customer | null }) => {
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<div>
|
||||
<span className="truncate">{name || email}</span>
|
||||
</div>
|
||||
<span className="truncate">{name || email}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 <StatusCell color={color}>{label}</StatusCell>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./items-cell"
|
||||
@@ -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 (
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<span className="truncate">
|
||||
{t("general.items", {
|
||||
count: items.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ItemsHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span className="truncate">{t("fields.items")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 <StatusCell color={color}>{label}</StatusCell>
|
||||
|
||||
@@ -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 <DateCell date={date} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("customer", {
|
||||
header: () => <CustomerHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const customer = getValue()
|
||||
|
||||
return <CustomerCell customer={customer} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sales_channel", {
|
||||
header: () => <SalesChannelHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
@@ -86,14 +95,6 @@ export const useOrderTableColumns = (props: UseOrderTableColumnsProps) => {
|
||||
return <FulfillmentStatusCell status={status} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("items", {
|
||||
header: () => <ItemsHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const items = getValue()
|
||||
|
||||
return <ItemsCell items={items} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("total", {
|
||||
header: () => <TotalHeader />,
|
||||
cell: ({ getValue, row }) => {
|
||||
@@ -103,6 +104,14 @@ export const useOrderTableColumns = (props: UseOrderTableColumnsProps) => {
|
||||
return <TotalCell currencyCode={currencyCode} total={total} />
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const country = row.original.shipping_address?.country
|
||||
|
||||
return <CountryCell country={country} />
|
||||
},
|
||||
}),
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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()}`
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const OrderDetails = () => {
|
||||
return <div>Order Details</div>;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { OrderDetails as Component } from "./details";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-customer-section"
|
||||
@@ -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 (
|
||||
<Container className="divide-y p-0">
|
||||
<Header order={order} />
|
||||
<ID order={order} />
|
||||
<Contact order={order} />
|
||||
<Company order={order} />
|
||||
<Addresses order={order} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("fields.customer")}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("orders.customer.transferOwnership"),
|
||||
to: `#`, // TODO: Open modal to transfer ownership
|
||||
icon: <ArrowPath />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("orders.customer.editShippingAddress"),
|
||||
to: `#`, // TODO: Open modal to edit shipping address
|
||||
icon: <FlyingBox />,
|
||||
},
|
||||
{
|
||||
label: t("orders.customer.editBillingAddress"),
|
||||
to: `#`, // TODO: Open modal to edit billing address
|
||||
icon: <CurrencyDollar />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("orders.customer.editEmail"),
|
||||
to: `#`, // TODO: Open modal to edit email
|
||||
icon: <Envelope />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<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.id")}
|
||||
</Text>
|
||||
<Link
|
||||
to={`/customers/${id}`}
|
||||
className="focus:shadow-borders-focus rounded-[4px] outline-none transition-shadow"
|
||||
>
|
||||
<div className="flex items-center gap-x-2 overflow-hidden">
|
||||
<Avatar size="2xsmall" fallback={fallback} />
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle hover:text-ui-fg-base transition-fg truncate"
|
||||
>
|
||||
{name || email}
|
||||
</Text>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Company = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
const company =
|
||||
order.shipping_address?.company || order.billing_address?.company
|
||||
|
||||
if (!company) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<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.company")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="truncate">
|
||||
{company}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Contact = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const phone = order.shipping_address?.phone || order.billing_address?.phone
|
||||
const email = order.email
|
||||
|
||||
return (
|
||||
<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.customer.contactLabel")}
|
||||
</Text>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-[1fr_20px] items-start gap-x-2">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-pretty break-all"
|
||||
>
|
||||
{email}
|
||||
</Text>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Copy content={email} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
{phone && (
|
||||
<div className="grid grid-cols-[1fr_20px] items-start gap-x-2">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-pretty break-all"
|
||||
>
|
||||
{phone}
|
||||
</Text>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Copy content={email} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{type === "shipping"
|
||||
? t("addresses.shippingAddress")
|
||||
: t("addresses.billingAddress")}
|
||||
</Text>
|
||||
{address ? (
|
||||
<div className="grid grid-cols-[1fr_20px] items-start gap-x-2">
|
||||
<Text size="small" leading="compact">
|
||||
{getFormattedAddress({ address }).map((line, i) => {
|
||||
return (
|
||||
<span key={i} className="break-words">
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</Text>
|
||||
<div className="flex justify-end">
|
||||
<Copy
|
||||
content={getFormattedAddress({ address }).join("\n")}
|
||||
className="text-ui-fg-muted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="small" leading="compact">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Addresses = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="divide-y">
|
||||
<Address address={order.shipping_address} type="shipping" />
|
||||
{!isSameAddress(order.shipping_address, order.billing_address) ? (
|
||||
<Address address={order.billing_address} type="billing" />
|
||||
) : (
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
{t("addresses.billingAddress")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||
{t("addresses.sameAsShipping")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-fulfillment-section"
|
||||
@@ -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 (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<UnfulfilledItemBreakdown order={order} />
|
||||
{fulfillments.map((f, index) => (
|
||||
<Fulfillment key={f.id} index={index} order={order} fulfillment={f} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const UnfulfilledItem = ({
|
||||
item,
|
||||
currencyCode,
|
||||
}: {
|
||||
item: LineItem
|
||||
currencyCode: string
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4"
|
||||
>
|
||||
<div className="flex items-start gap-x-4">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
<div>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-base"
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.variant.sku && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text size="small">{item.variant.sku}</Text>
|
||||
<Copy content={item.variant.sku} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
)}
|
||||
<Text size="small">
|
||||
{item.variant.options.map((o) => o.value).join(" · ")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-x-4">
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(item.unit_price, currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text>
|
||||
<span className="tabular-nums">{item.quantity}</span>x
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(item.subtotal || 0, currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("orders.fulfillment.unfulfilledItems")}</Heading>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<StatusBadge color="red" className="text-nowrap">
|
||||
{t("orders.fulfillment.awaitingFullfillmentBadge")}
|
||||
</StatusBadge>
|
||||
<ActionMenu groups={[]} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{unfulfilledItems.map((item) => (
|
||||
<UnfulfilledItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
currencyCode={order.currency_code}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">
|
||||
{t("orders.fulfillment.number", {
|
||||
number: index + 1,
|
||||
})}
|
||||
</Heading>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Tooltip
|
||||
content={format(
|
||||
new Date(statusTimestamp),
|
||||
"dd MMM, yyyy, HH:mm:ss"
|
||||
)}
|
||||
>
|
||||
<StatusBadge color={statusColor} className="text-nowrap">
|
||||
{statusText}
|
||||
</StatusBadge>
|
||||
</Tooltip>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.cancel"),
|
||||
icon: <XCircle />,
|
||||
onClick: handleCancel,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</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.itemsLabel")}
|
||||
</Text>
|
||||
<ul>
|
||||
{fulfillment.items.map((f_item) => (
|
||||
<li key={f_item.item_id}>
|
||||
<Text size="small" leading="compact">
|
||||
{f_item.item.quantity}x {f_item.item.title}
|
||||
</Text>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{showLocation && (
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("orders.fulfillment.shippingFromLabel")}
|
||||
</Text>
|
||||
{stock_location ? (
|
||||
<Link
|
||||
to={`/settings/locations/${stock_location.id}`}
|
||||
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg"
|
||||
>
|
||||
<Text size="small" leading="compact">
|
||||
{stock_location.name}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<Skeleton className="w-16" />
|
||||
)}
|
||||
</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")}
|
||||
</Text>
|
||||
<div>
|
||||
{fulfillment.tracking_links &&
|
||||
fulfillment.tracking_links.length > 0 ? (
|
||||
<ul>
|
||||
{fulfillment.tracking_links.map((tlink) => {
|
||||
const hasUrl = tlink.url && tlink.url.length > 0
|
||||
|
||||
if (hasUrl) {
|
||||
return (
|
||||
<li key={tlink.tracking_number}>
|
||||
<a
|
||||
href={tlink.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg"
|
||||
>
|
||||
<Text size="small" leading="compact">
|
||||
{tlink.tracking_number}
|
||||
</Text>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={tlink.tracking_number}>
|
||||
<Text size="small" leading="compact">
|
||||
{tlink.tracking_number}
|
||||
</Text>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<Text size="small" leading="compact">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-general-section"
|
||||
@@ -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 (
|
||||
<Container className="flex items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Heading>#{order.display_id}</Heading>
|
||||
<Copy content={`#${order.display_id}`} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("orders.onDateFromSalesChannel", {
|
||||
date: format(new Date(order.created_at), "dd MMM, yyyy, HH:mm:ss"),
|
||||
salesChannel: order.sales_channel.name,
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<PaymentBadge order={order} />
|
||||
<FulfillmentBadge order={order} />
|
||||
</div>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.cancel"),
|
||||
onClick: handleCancel,
|
||||
icon: <XCircle />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<StatusBadge color={color} className="text-nowrap">
|
||||
{label}
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<StatusBadge color={color} className="text-nowrap">
|
||||
{label}
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-payment-section"
|
||||
@@ -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 (
|
||||
<Container className="divide-y divide-dashed p-0">
|
||||
<Header order={order} />
|
||||
<PaymentBreakdown
|
||||
payments={order.payments}
|
||||
refunds={order.refunds}
|
||||
currencyCode={order.currency_code}
|
||||
/>
|
||||
<Total payments={order.payments} currencyCode={order.currency_code} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("orders.payment.title")}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("orders.payment.refund"),
|
||||
icon: <ArrowDownRightMini />,
|
||||
to: "#", // TODO: Go to general refund modal
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Refund = ({
|
||||
refund,
|
||||
currencyCode,
|
||||
}: {
|
||||
refund: MedusaRefund
|
||||
currencyCode: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const hasPayment = refund.payment_id !== null
|
||||
|
||||
const BadgeComponent = (
|
||||
<Badge size="2xsmall" className="cursor-default select-none capitalize">
|
||||
{refund.reason}
|
||||
</Badge>
|
||||
)
|
||||
|
||||
const Render = refund.note ? (
|
||||
<Tooltip content={refund.note}>{BadgeComponent}</Tooltip>
|
||||
) : (
|
||||
BadgeComponent
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle text-ui-fg-subtle grid grid-cols-[1fr_1fr_1fr_1fr_20px] items-center gap-x-4 px-6 py-4">
|
||||
<div>
|
||||
{hasPayment && <ArrowDownRightMini className="text-ui-fg-muted" />}
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("orders.payment.refund")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small" leading="compact">
|
||||
{format(new Date(refund.created_at), "dd MMM, yyyy, HH:mm:ss")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">{Render}</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small" leading="compact">
|
||||
- {getLocaleAmount(refund.amount, currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="divide-y divide-dashed">
|
||||
<div className="text-ui-fg-subtle grid grid-cols-[1fr_1fr_1fr_1fr_20px] items-center gap-x-4 px-6 py-4">
|
||||
<div className="w-full overflow-hidden">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="truncate"
|
||||
>
|
||||
{cleanId}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{format(new Date(payment.created_at), "dd MMM, yyyy, HH:mm:ss")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small" leading="compact" className="capitalize">
|
||||
{payment.provider_id}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<StatusBadge color={color} className="text-nowrap">
|
||||
{status}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small" leading="compact">
|
||||
{getLocaleAmount(payment.amount, payment.currency_code)}
|
||||
</Text>
|
||||
</div>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("orders.payment.refund"),
|
||||
icon: <XCircle />,
|
||||
to: "#", // TODO: Go to specific payment refund modal
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{showCapture && (
|
||||
<div className="bg-ui-bg-subtle flex items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<ArrowDownRightMini className="text-ui-fg-muted" />
|
||||
<Text size="small" leading="compact">
|
||||
{t("orders.payment.isReadyToBeCaptured", {
|
||||
id: cleanId,
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("orders.payment.capture")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{refunds.map((refund) => (
|
||||
<Refund key={refund.id} refund={refund} currencyCode={currencyCode} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col divide-y divide-dashed">
|
||||
{entries.map(({ type, event }) => {
|
||||
switch (type) {
|
||||
case "payment":
|
||||
return (
|
||||
<Payment
|
||||
key={event.id}
|
||||
payment={event}
|
||||
refunds={refunds.filter(
|
||||
(refund) => refund.payment_id === event.id
|
||||
)}
|
||||
currencyCode={currencyCode}
|
||||
/>
|
||||
)
|
||||
case "refund":
|
||||
return (
|
||||
<Refund
|
||||
key={event.id}
|
||||
refund={event}
|
||||
currencyCode={currencyCode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{t("orders.payment.totalPaidByCustomer")}
|
||||
</Text>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{getStylizedAmount(total, currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-summary-section"
|
||||
@@ -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 (
|
||||
<Container className="divide-y divide-dashed p-0">
|
||||
<Header order={order} />
|
||||
<ItemBreakdown order={order} />
|
||||
<CostBreakdown order={order} />
|
||||
<Total order={order} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("fields.summary")}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("orders.summary.editItems"),
|
||||
to: "#", // TODO: Open modal to edit items
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
{
|
||||
label: t("orders.summary.allocateItems"),
|
||||
to: "#", // TODO: Open modal to allocate items
|
||||
icon: <Buildings />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
item,
|
||||
currencyCode,
|
||||
reservation,
|
||||
}: {
|
||||
item: LineItem
|
||||
currencyCode: string
|
||||
reservation?: ReservationItemDTO | null
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="text-ui-fg-subtle grid grid-cols-2 items-start gap-x-4 px-6 py-4"
|
||||
>
|
||||
<div className="flex items-start gap-x-4">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
<div>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-base"
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.variant.sku && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text size="small">{item.variant.sku}</Text>
|
||||
<Copy content={item.variant.sku} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
)}
|
||||
<Text size="small">
|
||||
{item.variant.options.map((o) => o.value).join(" · ")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-4">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(item.unit_price, currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="w-fit min-w-[27px]">
|
||||
<Text>
|
||||
<span className="tabular-nums">{item.quantity}</span>x
|
||||
</Text>
|
||||
</div>
|
||||
<div className="overflow-visible">
|
||||
<StatusBadge
|
||||
color={reservation ? "green" : "orange"}
|
||||
className="text-nowrap"
|
||||
>
|
||||
{reservation
|
||||
? t("orders.reservations.allocatedLabel")
|
||||
: t("orders.reservations.notAllocatedLabel")}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(item.subtotal || 0, currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ItemBreakdown = ({ order }: { order: Order }) => {
|
||||
const { reservations, isError, error } = useAdminReservations({
|
||||
line_item_id: order.items.map((i) => i.id),
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{order.items.map((item) => {
|
||||
const reservation = reservations
|
||||
? reservations.find((r) => r.line_item_id === item.id)
|
||||
: null
|
||||
|
||||
return (
|
||||
<Item
|
||||
key={item.id}
|
||||
item={item}
|
||||
currencyCode={order.currency_code}
|
||||
reservation={reservation}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Cost = ({
|
||||
label,
|
||||
value,
|
||||
secondaryValue,
|
||||
}: {
|
||||
label: string
|
||||
value: string | number
|
||||
secondaryValue: string
|
||||
}) => (
|
||||
<div className="grid grid-cols-3 items-center">
|
||||
<Text size="small" leading="compact">
|
||||
{label}
|
||||
</Text>
|
||||
<div className="text-right">
|
||||
<Text size="small" leading="compact">
|
||||
{secondaryValue}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Text size="small" leading="compact">
|
||||
{value}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const CostBreakdown = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-y-2 px-6 py-4">
|
||||
<Cost
|
||||
label={t("fields.subtotal")}
|
||||
secondaryValue={t("general.items", { count: order.items.length })}
|
||||
value={getLocaleAmount(order.subtotal, order.currency_code)}
|
||||
/>
|
||||
<Cost
|
||||
label={t("fields.discount")}
|
||||
secondaryValue={
|
||||
order.discounts.length > 0
|
||||
? order.discounts.map((d) => d.code).join(", ")
|
||||
: "-"
|
||||
}
|
||||
value={
|
||||
order.discount_total > 0
|
||||
? `- ${getLocaleAmount(order.discount_total, order.currency_code)}`
|
||||
: "-"
|
||||
}
|
||||
/>
|
||||
<Cost
|
||||
label={t("fields.shipping")}
|
||||
secondaryValue={order.shipping_methods
|
||||
.map((sm) => sm.shipping_option.name)
|
||||
.join(", ")}
|
||||
value={getLocaleAmount(order.shipping_total, order.currency_code)}
|
||||
/>
|
||||
<Cost
|
||||
label={t("fields.tax")}
|
||||
secondaryValue={`${order.tax_rate || 0}%`}
|
||||
value={
|
||||
order.tax_total
|
||||
? getLocaleAmount(order.tax_total, order.currency_code)
|
||||
: "-"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Total = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="text-ui-fg-base flex items-center justify-between px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.total")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{getStylizedAmount(order.total, order.currency_code)}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { orderLoader as loader } from "./loader"
|
||||
export { OrderDetail as Component } from "./order-detail"
|
||||
@@ -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<Response<AdminOrdersRes>>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -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<ReturnType<typeof orderLoader>>
|
||||
|
||||
const { id } = useParams()
|
||||
|
||||
const { order, isLoading, isError, error } = useAdminOrder(
|
||||
id!,
|
||||
{
|
||||
expand: orderExpand,
|
||||
},
|
||||
{
|
||||
initialData,
|
||||
}
|
||||
)
|
||||
|
||||
if (isLoading || !order) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-x-4 xl:grid-cols-[1fr,400px]">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<OrderGeneralSection order={order} />
|
||||
<OrderSummarySection order={order} />
|
||||
<OrderPaymentSection order={order} />
|
||||
<OrderFulfillmentSection order={order} />
|
||||
<div className="flex flex-col gap-y-2 lg:hidden">
|
||||
<OrderCustomerSection order={order} />
|
||||
</div>
|
||||
<JsonViewSection data={order} />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-y-2 lg:flex">
|
||||
<OrderCustomerSection order={order} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -47,13 +47,13 @@ export const ProductDetail = () => {
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="grid grid-cols-1 gap-x-4 lg:grid-cols-[1fr,400px]">
|
||||
<div className="grid grid-cols-1 gap-x-4 xl:grid-cols-[1fr,400px]">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ProductGeneralSection product={product} />
|
||||
<ProductMediaSection product={product} />
|
||||
<ProductOptionSection product={product} />
|
||||
<ProductVariantSection product={product} />
|
||||
<div className="flex flex-col gap-y-2 lg:hidden">
|
||||
<div className="flex flex-col gap-y-2 xl:hidden">
|
||||
{sideBefore.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
@@ -81,7 +81,7 @@ export const ProductDetail = () => {
|
||||
})}
|
||||
<JsonViewSection data={product} root="product" />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-y-2 lg:flex">
|
||||
<div className="hidden flex-col gap-y-2 xl:flex">
|
||||
{sideBefore.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user