feat(dashboard): basic Order UI (#7225)
**WHAT** - rudimentary list and details pages - fix Order<>Promotion link - fix Order<>SalesChannel link **NOTE** - displaying basic info since we don't have Fulfillments & Payments linked ATM - `disaply_id` needs to be added to order - `tax_rate` needs to be added to order --- https://github.com/medusajs/medusa/assets/16856471/cd4e98c7-345e-4193-8c1e-ad4ed1584993 Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"is": "is",
|
||||
"timeline": "Timeline",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"error": "Error",
|
||||
"select": "Select",
|
||||
"selected": "Selected",
|
||||
@@ -540,6 +541,10 @@
|
||||
"canceled": "Canceled",
|
||||
"requiresAction": "Requires action"
|
||||
},
|
||||
"toast": {
|
||||
"canceled": "Fulfillment successfully canceled",
|
||||
"fulfillmentShipped": "Cannot cancel an already shipped fulfillment"
|
||||
},
|
||||
"trackingLabel": "Tracking",
|
||||
"shippingFromLabel": "Shipping from",
|
||||
"itemsLabel": "Items"
|
||||
|
||||
@@ -12,6 +12,11 @@ export const FulfillmentStatusCell = ({
|
||||
}: FulfillmentStatusCellProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!status) {
|
||||
// TODO: remove this once fulfillment<>order link is added
|
||||
return "-"
|
||||
}
|
||||
|
||||
const { label, color } = getOrderFulfillmentStatus(t, status)
|
||||
|
||||
return <StatusCell color={color}>{label}</StatusCell>
|
||||
|
||||
@@ -10,6 +10,9 @@ type PaymentStatusCellProps = {
|
||||
export const PaymentStatusCell = ({ status }: PaymentStatusCellProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// TODO: remove this when Order<>Payments are linked
|
||||
return "-"
|
||||
|
||||
const { label, color } = getOrderPaymentStatus(t, status)
|
||||
|
||||
return <StatusCell color={color}>{label}</StatusCell>
|
||||
|
||||
40
packages/admin-next/dashboard/src/hooks/api/orders.ts
Normal file
40
packages/admin-next/dashboard/src/hooks/api/orders.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
|
||||
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { client } from "../../lib/client"
|
||||
|
||||
const ORDERS_QUERY_KEY = "orders" as const
|
||||
export const ordersQueryKeys = queryKeysFactory(ORDERS_QUERY_KEY)
|
||||
|
||||
export const useOrder = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, Error, any, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.orders.retrieve(id, query),
|
||||
queryKey: ordersQueryKeys.detail(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useOrders = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<any, Error, any, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.orders.list(query),
|
||||
queryKey: ordersQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import { useAdminRegions, useAdminSalesChannels } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import type { Filter } from "../../../components/table/data-table"
|
||||
import { useRegions } from "../../api/regions"
|
||||
import { useSalesChannels } from "../../api/sales-channels"
|
||||
|
||||
export const useOrderTableFilters = (): Filter[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { regions } = useAdminRegions({
|
||||
const { regions } = useRegions({
|
||||
limit: 1000,
|
||||
fields: "id,name",
|
||||
expand: "",
|
||||
})
|
||||
|
||||
const { sales_channels } = useAdminSalesChannels({
|
||||
const { sales_channels } = useSalesChannels({
|
||||
limit: 1000,
|
||||
fields: "id,name",
|
||||
expand: "",
|
||||
@@ -145,8 +146,9 @@ export const useOrderTableFilters = (): Filter[] => {
|
||||
|
||||
filters = [
|
||||
...filters,
|
||||
paymentStatusFilter,
|
||||
fulfillmentStatusFilter,
|
||||
// TODO: enable when Payment, Fulfillments <> Orders are linked
|
||||
// paymentStatusFilter,
|
||||
// fulfillmentStatusFilter,
|
||||
...dateFilters,
|
||||
]
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { stores } from "./stores"
|
||||
import { tags } from "./tags"
|
||||
import { taxes } from "./taxes"
|
||||
import { users } from "./users"
|
||||
import { orders } from "./orders"
|
||||
import { workflowExecutions } from "./workflow-executions"
|
||||
import { shippingProfiles } from "./shipping-profiles"
|
||||
|
||||
@@ -43,6 +44,7 @@ export const client = {
|
||||
shippingProfiles: shippingProfiles,
|
||||
tags: tags,
|
||||
users: users,
|
||||
orders: orders,
|
||||
regions: regions,
|
||||
taxes: taxes,
|
||||
invites: invites,
|
||||
|
||||
15
packages/admin-next/dashboard/src/lib/client/orders.ts
Normal file
15
packages/admin-next/dashboard/src/lib/client/orders.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getRequest } from "./common"
|
||||
import { OrderListRes, OrderRes } from "../../types/api-responses"
|
||||
|
||||
async function retrieveOrder(id: string, query?: Record<string, any>) {
|
||||
return getRequest<OrderRes, Record<string, any>>(`/admin/orders/${id}`, query)
|
||||
}
|
||||
|
||||
async function listOrders(query?: Record<string, any>) {
|
||||
return getRequest<OrderListRes, Record<string, any>>(`/admin/orders`, query)
|
||||
}
|
||||
|
||||
export const orders = {
|
||||
list: listOrders,
|
||||
retrieve: retrieveOrder,
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export const getOrderPaymentStatus = (
|
||||
t: TFunction<"translation">,
|
||||
status: PaymentStatus
|
||||
) => {
|
||||
const [label, color] = {
|
||||
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"],
|
||||
@@ -16,7 +16,9 @@ export const getOrderPaymentStatus = (
|
||||
],
|
||||
canceled: [t("orders.payment.status.canceled"), "red"],
|
||||
requires_action: [t("orders.payment.status.requiresAction"), "orange"],
|
||||
}[status] as [string, "red" | "orange" | "green"]
|
||||
}[status] ||
|
||||
// TODO: remove this when Order<>Payment are linked
|
||||
"not_paid") as [string, "red" | "orange" | "green"]
|
||||
|
||||
return { label, color }
|
||||
}
|
||||
|
||||
@@ -156,6 +156,10 @@ export const RouteMap: RouteObject[] = [
|
||||
path: "",
|
||||
lazy: () => import("../../v2-routes/orders/order-list"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../v2-routes/orders/order-detail"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -99,6 +99,11 @@ const FulfillmentBadge = ({ order }: { order: Order }) => {
|
||||
const PaymentBadge = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* TODO: revisit when Order<>Payment are linked
|
||||
*/
|
||||
return null
|
||||
|
||||
const { label, color } = getOrderPaymentStatus(t, order.payment_status)
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
FulfillmentProviderDTO,
|
||||
InventoryNext,
|
||||
InviteDTO,
|
||||
OrderDTO,
|
||||
PaymentProviderDTO,
|
||||
PriceListDTO,
|
||||
ProductCategoryDTO,
|
||||
@@ -99,6 +100,10 @@ export type InviteRes = { invite: InviteDTO }
|
||||
export type InviteListRes = { invites: InviteDTO[] } & ListRes
|
||||
export type InviteDeleteRes = DeleteRes
|
||||
|
||||
// Orders
|
||||
export type OrderRes = { order: OrderDTO }
|
||||
export type OrderListRes = { orders: OrderDTO[] } & ListRes
|
||||
|
||||
// Products
|
||||
export type ExtendedProductDTO = ProductDTO & {
|
||||
variants: ProductVariantDTO[] | null
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-activity-section"
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { OrderNoteForm } from "./order-note-form"
|
||||
import { OrderTimeline } from "./order-timeline"
|
||||
import { OrderDTO } from "@medusajs/types"
|
||||
|
||||
type OrderActivityProps = {
|
||||
order: OrderDTO
|
||||
}
|
||||
|
||||
export const OrderActivitySection = ({ order }: OrderActivityProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="flex flex-col gap-y-8 px-6 py-4">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading level="h2">{t("orders.activity.header")}</Heading>
|
||||
</div>
|
||||
<OrderNoteForm order={order} />
|
||||
</div>
|
||||
<OrderTimeline order={order} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { ArrowUpCircleSolid } from "@medusajs/icons"
|
||||
import { IconButton } from "@medusajs/ui"
|
||||
import { useRef } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { OrderDTO } from "@medusajs/types"
|
||||
|
||||
type OrderNoteFormProps = {
|
||||
order: OrderDTO
|
||||
}
|
||||
|
||||
const OrderNoteSchema = z.object({
|
||||
value: z.string().min(1),
|
||||
})
|
||||
|
||||
export const OrderNoteForm = ({ order }: OrderNoteFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const form = useForm<z.infer<typeof OrderNoteSchema>>({
|
||||
defaultValues: {
|
||||
value: "",
|
||||
},
|
||||
resolver: zodResolver(OrderNoteSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = {}
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
mutateAsync(
|
||||
{
|
||||
resource_id: order.id,
|
||||
resource_type: "order",
|
||||
value: values.value,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
form.reset()
|
||||
handleResetSize()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const handleResize = () => {
|
||||
const textarea = textareaRef.current
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto"
|
||||
textarea.style.height = textarea.scrollHeight + "px"
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetSize = () => {
|
||||
const textarea = textareaRef.current
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="bg-ui-bg-field shadow-borders-base flex flex-col gap-y-2 rounded-md px-2 py-1.5">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label hidden>
|
||||
{t("orders.activity.comment.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<textarea
|
||||
{...field}
|
||||
ref={textareaRef}
|
||||
onInput={handleResize}
|
||||
className="txt-small text-ui-fg-base placeholder:text-ui-fg-muted resize-none overflow-hidden bg-transparent outline-none"
|
||||
placeholder={t("orders.activity.comment.placeholder")}
|
||||
rows={1}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-end">
|
||||
<IconButton
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
variant="transparent"
|
||||
size="small"
|
||||
className="text-ui-fg-muted hover:text-ui-fg-subtle active:text-ui-fg-subtle"
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("orders.activity.comment.addButtonText")}
|
||||
</span>
|
||||
<ArrowUpCircleSolid />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
import { Fulfillment, Note, Order } from "@medusajs/medusa"
|
||||
import { IconButton, Text, Tooltip, clx, usePrompt } from "@medusajs/ui"
|
||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||
|
||||
import { PropsWithChildren, ReactNode, useMemo, useState } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import { XMarkMini } from "@medusajs/icons"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||
import { useDate } from "../../../../../hooks/use-date"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { OrderDTO } from "@medusajs/types"
|
||||
|
||||
type OrderTimelineProps = {
|
||||
order: OrderDTO
|
||||
}
|
||||
|
||||
/**
|
||||
* Arbitrary high limit to ensure all notes are fetched
|
||||
*/
|
||||
const NOTE_LIMIT = 9999
|
||||
|
||||
export const OrderTimeline = ({ order }: OrderTimelineProps) => {
|
||||
const items = useActivityItems(order)
|
||||
|
||||
if (items.length <= 3) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<OrderActivityItem
|
||||
key={index}
|
||||
title={item.title}
|
||||
timestamp={item.timestamp}
|
||||
isFirst={index === items.length - 1}
|
||||
>
|
||||
{item.children}
|
||||
</OrderActivityItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const lastItems = items.slice(0, 2)
|
||||
const collapsibleItems = items.slice(2, items.length - 1)
|
||||
const firstItem = items[items.length - 1]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
{lastItems.map((item, index) => {
|
||||
return (
|
||||
<OrderActivityItem
|
||||
key={index}
|
||||
title={item.title}
|
||||
timestamp={item.timestamp}
|
||||
>
|
||||
{item.children}
|
||||
</OrderActivityItem>
|
||||
)
|
||||
})}
|
||||
<OrderActivityCollapsible activities={collapsibleItems} />
|
||||
<OrderActivityItem
|
||||
title={firstItem.title}
|
||||
timestamp={firstItem.timestamp}
|
||||
isFirst
|
||||
>
|
||||
{firstItem.children}
|
||||
</OrderActivityItem>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Activity = {
|
||||
title: string
|
||||
timestamp: string | Date
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const useActivityItems = (order: Order) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const notes = []
|
||||
const isLoading = false
|
||||
// const { notes, isLoading, isError, error } = useNotes(
|
||||
// {
|
||||
// resource_id: order.id,
|
||||
// limit: NOTE_LIMIT,
|
||||
// offset: 0,
|
||||
// },
|
||||
// {
|
||||
// keepPreviousData: true,
|
||||
// }
|
||||
// )
|
||||
//
|
||||
// if (isError) {
|
||||
// throw error
|
||||
// }
|
||||
|
||||
return useMemo(() => {
|
||||
if (isLoading) {
|
||||
return []
|
||||
}
|
||||
|
||||
const items: Activity[] = []
|
||||
|
||||
// for (const payment of order.payments) {
|
||||
// items.push({
|
||||
// title: t("orders.activity.events.payment.awaiting"),
|
||||
// timestamp: payment.created_at,
|
||||
// children: (
|
||||
// <Text size="small" className="text-ui-fg-subtle">
|
||||
// {getStylizedAmount(payment.amount, payment.currency_code)}
|
||||
// </Text>
|
||||
// ),
|
||||
// })
|
||||
//
|
||||
// if (payment.canceled_at) {
|
||||
// items.push({
|
||||
// title: t("orders.activity.events.payment.canceled"),
|
||||
// timestamp: payment.canceled_at,
|
||||
// children: (
|
||||
// <Text size="small" className="text-ui-fg-subtle">
|
||||
// {getStylizedAmount(payment.amount, payment.currency_code)}
|
||||
// </Text>
|
||||
// ),
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// if (payment.captured_at) {
|
||||
// items.push({
|
||||
// title: t("orders.activity.events.payment.captured"),
|
||||
// timestamp: payment.captured_at,
|
||||
// children: (
|
||||
// <Text size="small" className="text-ui-fg-subtle">
|
||||
// {getStylizedAmount(payment.amount, payment.currency_code)}
|
||||
// </Text>
|
||||
// ),
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// for (const fulfillment of order.fulfillments) {
|
||||
// items.push({
|
||||
// title: t("orders.activity.events.fulfillment.created"),
|
||||
// timestamp: fulfillment.created_at,
|
||||
// children: <FulfillmentCreatedBody fulfillment={fulfillment} />,
|
||||
// })
|
||||
//
|
||||
// if (fulfillment.shipped_at) {
|
||||
// items.push({
|
||||
// title: t("orders.activity.events.fulfillment.shipped"),
|
||||
// timestamp: fulfillment.shipped_at,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// for (const ret of order.returns) {
|
||||
// items.push({
|
||||
// title: t("orders.activity.events.return.created"),
|
||||
// timestamp: ret.created_at,
|
||||
// })
|
||||
// }
|
||||
|
||||
// for (const note of notes || []) {
|
||||
// items.push({
|
||||
// title: t("orders.activity.events.note.comment"),
|
||||
// timestamp: note.created_at,
|
||||
// children: <NoteBody note={note} />,
|
||||
// })
|
||||
// }
|
||||
|
||||
if (order.canceled_at) {
|
||||
items.push({
|
||||
title: t("orders.activity.events.canceled.title"),
|
||||
timestamp: order.canceled_at,
|
||||
})
|
||||
}
|
||||
|
||||
const sortedActivities = items.sort((a, b) => {
|
||||
return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
})
|
||||
|
||||
const createdAt = {
|
||||
title: t("orders.activity.events.placed.title"),
|
||||
timestamp: order.created_at,
|
||||
children: (
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("orders.activity.events.placed.fromSalesChannel", {
|
||||
salesChannel: order.sales_channel.name,
|
||||
})}
|
||||
</Text>
|
||||
),
|
||||
}
|
||||
|
||||
return [...sortedActivities, createdAt]
|
||||
}, [order, notes, isLoading, t])
|
||||
}
|
||||
|
||||
type OrderActivityItemProps = PropsWithChildren<{
|
||||
title: string
|
||||
timestamp: string | Date
|
||||
isFirst?: boolean
|
||||
}>
|
||||
|
||||
const OrderActivityItem = ({
|
||||
title,
|
||||
timestamp,
|
||||
isFirst = false,
|
||||
children,
|
||||
}: OrderActivityItemProps) => {
|
||||
const { getFullDate, getRelativeDate } = useDate()
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[20px_1fr] items-start gap-2">
|
||||
<div className="flex size-full flex-col items-center gap-y-0.5">
|
||||
<div className="flex size-5 items-center justify-center">
|
||||
<div className="bg-ui-bg-base shadow-borders-base flex size-2.5 items-center justify-center rounded-full">
|
||||
<div className="bg-ui-tag-neutral-icon size-1.5 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
{!isFirst && <div className="bg-ui-border-base w-px flex-1" />}
|
||||
</div>
|
||||
<div
|
||||
className={clx({
|
||||
"pb-4": !isFirst,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{title}
|
||||
</Text>
|
||||
<Tooltip
|
||||
content={getFullDate({ date: timestamp, includeTime: true })}
|
||||
>
|
||||
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||
{getRelativeDate(timestamp)}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const OrderActivityCollapsible = ({
|
||||
activities,
|
||||
}: {
|
||||
activities: Activity[]
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!activities.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
{!open && (
|
||||
<div className="grid grid-cols-[20px_1fr] items-start gap-2">
|
||||
<div className="flex size-full flex-col items-center">
|
||||
<div className="border-ui-border-strong w-px flex-1 bg-[linear-gradient(var(--border-strong)_33%,rgba(255,255,255,0)_0%)] bg-[length:1px_3px] bg-right bg-repeat-y" />
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<Collapsible.Trigger className="text-left">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-muted"
|
||||
>
|
||||
{t("orders.activity.showMoreActivities", {
|
||||
count: activities.length,
|
||||
})}
|
||||
</Text>
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Collapsible.Content>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
{activities.map((item, index) => {
|
||||
return (
|
||||
<OrderActivityItem
|
||||
key={index}
|
||||
title={item.title}
|
||||
timestamp={item.timestamp}
|
||||
>
|
||||
{item.children}
|
||||
</OrderActivityItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const NoteBody = ({ note }: { note: Note }) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { first_name, last_name, email } = note.author || {}
|
||||
const name = [first_name, last_name].filter(Boolean).join(" ")
|
||||
|
||||
const byLine = t("orders.activity.events.note.byLine", {
|
||||
author: name || email,
|
||||
})
|
||||
|
||||
const { mutateAsync } = useAdminDeleteNote(note.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: "This action cannot be undone",
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2 pt-2">
|
||||
<div className="bg-ui-bg-component shadow-borders-base group grid grid-cols-[1fr_20px] items-start gap-x-2 text-pretty rounded-r-2xl rounded-bl-md rounded-tl-xl px-3 py-1.5">
|
||||
<div className="flex h-full min-h-7 items-center">
|
||||
<Text size="xsmall" className="text-ui-fg-subtle">
|
||||
{note.value}
|
||||
</Text>
|
||||
</div>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="transparent"
|
||||
className="transition-fg invisible opacity-0 group-hover:visible group-hover:opacity-100"
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("orders.activity.comment.deleteButtonText")}
|
||||
</span>
|
||||
<XMarkMini className="text-ui-fg-muted" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Link
|
||||
to={`/settings/users/${note.author_id}`}
|
||||
className="text-ui-fg-subtle hover:text-ui-fg-base transition-fg w-fit"
|
||||
>
|
||||
<Text size="small">{byLine}</Text>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const FulfillmentCreatedBody = ({
|
||||
fulfillment,
|
||||
}: {
|
||||
fulfillment: Fulfillment
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { stock_location, isLoading, isError, error } = useAdminStockLocation(
|
||||
fulfillment.location_id!,
|
||||
{
|
||||
enabled: !!fulfillment.location_id,
|
||||
}
|
||||
)
|
||||
|
||||
const numberOfItems = fulfillment.items.reduce((acc, item) => {
|
||||
return acc + item.quantity
|
||||
}, 0)
|
||||
|
||||
const triggerText = stock_location
|
||||
? t("orders.activity.events.fulfillment.itemsFulfilledFrom", {
|
||||
count: numberOfItems,
|
||||
location: stock_location.name,
|
||||
})
|
||||
: t("orders.activity.events.fulfillment.itemsFulfilled", {
|
||||
count: numberOfItems,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-7 w-full" />
|
||||
) : (
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{triggerText}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-customer-section"
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Order } from "@medusajs/medusa"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ArrowPath, CurrencyDollar, Envelope, FlyingBox } from "@medusajs/icons"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { CustomerInfo } from "../../../../../components/common/customer-info"
|
||||
|
||||
type OrderCustomerSectionProps = {
|
||||
order: Order
|
||||
}
|
||||
|
||||
export const OrderCustomerSection = ({ order }: OrderCustomerSectionProps) => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<Header />
|
||||
<CustomerInfo.ID data={order} />
|
||||
<CustomerInfo.Contact data={order} />
|
||||
<CustomerInfo.Company data={order} />
|
||||
<CustomerInfo.Addresses data={order} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = () => {
|
||||
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("transferOwnership.label"),
|
||||
to: `transfer-ownership`,
|
||||
icon: <ArrowPath />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("addresses.shippingAddress.editLabel"),
|
||||
to: "shipping-address",
|
||||
icon: <FlyingBox />,
|
||||
},
|
||||
{
|
||||
label: t("addresses.billingAddress.editLabel"),
|
||||
to: "billing-address",
|
||||
icon: <CurrencyDollar />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("email.editLabel"),
|
||||
to: `email`,
|
||||
icon: <Envelope />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-fulfillment-section"
|
||||
@@ -0,0 +1,340 @@
|
||||
import { XCircle } from "@medusajs/icons"
|
||||
import {
|
||||
LineItem,
|
||||
Fulfillment as MedusaFulfillment,
|
||||
Order,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
Container,
|
||||
Copy,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Text,
|
||||
toast,
|
||||
Tooltip,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { format } from "date-fns"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { FulfillmentDTO, OrderDTO, OrderItemDTO } from "@medusajs/types"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { formatProvider } from "../../../../../lib/format-provider"
|
||||
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { useStockLocation } from "../../../../../hooks/api/stock-locations"
|
||||
|
||||
type OrderFulfillmentSectionProps = {
|
||||
order: OrderDTO
|
||||
}
|
||||
|
||||
export const OrderFulfillmentSection = ({
|
||||
order,
|
||||
}: OrderFulfillmentSectionProps) => {
|
||||
const fulfillments = order.fulfillments || []
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<UnfulfilledItemBreakdown order={order} />
|
||||
{fulfillments.map((f, index) => (
|
||||
<Fulfillment key={f.id} index={index} fulfillment={f} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const UnfulfilledItem = ({
|
||||
item,
|
||||
currencyCode,
|
||||
}: {
|
||||
item: OrderItemDTO
|
||||
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,
|
||||
index,
|
||||
}: {
|
||||
fulfillment: FulfillmentDTO
|
||||
index: number
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const showLocation = !!fulfillment.location_id
|
||||
|
||||
const { stock_location, isError, error } = useStockLocation(
|
||||
fulfillment.location_id!,
|
||||
{
|
||||
enabled: showLocation,
|
||||
}
|
||||
)
|
||||
|
||||
let statusText = "Fulfilled"
|
||||
let statusColor: "orange" | "green" | "red" = "orange"
|
||||
let statusTimestamp = fulfillment.created_at
|
||||
|
||||
if (fulfillment.canceled_at) {
|
||||
statusText = "Canceled"
|
||||
statusColor = "red"
|
||||
statusTimestamp = fulfillment.canceled_at
|
||||
} else if (fulfillment.shipped_at) {
|
||||
statusText = "Shipped"
|
||||
statusColor = "green"
|
||||
statusTimestamp = fulfillment.shipped_at
|
||||
}
|
||||
|
||||
const { mutateAsync } = {} // useCancelFulfillment(order.id)
|
||||
|
||||
const handleCancel = async () => {
|
||||
if (fulfillment.shipped_at) {
|
||||
toast.warning(t("general.warning"), {
|
||||
description: t("orders.fulfillment.toast.fulfillmentShipped"),
|
||||
dismissLabel: t("actions.close"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("orders.fulfillment.cancelWarning"),
|
||||
confirmText: t("actions.continue"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (res) {
|
||||
try {
|
||||
await mutateAsync(fulfillment.id)
|
||||
|
||||
toast.success(t("general.success"), {
|
||||
description: t("orders.fulfillment.toast.canceled"),
|
||||
dismissLabel: t("actions.close"),
|
||||
})
|
||||
} catch (e) {
|
||||
toast.error(t("general.error"), {
|
||||
description: e.message,
|
||||
dismissLabel: t("actions.close"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<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-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.provider")}
|
||||
</Text>
|
||||
|
||||
<Text size="small" leading="compact">
|
||||
{formatProvider(fulfillment.provider_id)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("orders.fulfillment.trackingLabel")}
|
||||
</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,118 @@
|
||||
import { XCircle } from "@medusajs/icons"
|
||||
import { Order } from "@medusajs/medusa"
|
||||
import {
|
||||
Container,
|
||||
Copy,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Text,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { format } from "date-fns"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
getOrderFulfillmentStatus,
|
||||
getOrderPaymentStatus,
|
||||
} from "../../../../../lib/order-helpers"
|
||||
|
||||
type OrderGeneralSectionProps = {
|
||||
order: Order
|
||||
}
|
||||
|
||||
export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = { mutateAsync: () => {} } // cancel order
|
||||
|
||||
const handleCancel = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("orders.cancelWarning", {
|
||||
id: `#${order.display_id}`,
|
||||
}),
|
||||
confirmText: t("actions.continue"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<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()
|
||||
|
||||
/**
|
||||
* TODO: revisit when Order<>Fulfillment are linked
|
||||
*/
|
||||
return null
|
||||
|
||||
const { label, color } = getOrderFulfillmentStatus(
|
||||
t,
|
||||
order.fulfillment_status
|
||||
)
|
||||
|
||||
return (
|
||||
<StatusBadge color={color} className="text-nowrap">
|
||||
{label}
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
|
||||
const PaymentBadge = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* TODO: revisit when Order<>Payment are linked
|
||||
*/
|
||||
return null
|
||||
|
||||
const { label, color } = getOrderPaymentStatus(t, order.payment_status)
|
||||
|
||||
return (
|
||||
<StatusBadge color={color} className="text-nowrap">
|
||||
{label}
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-payment-section"
|
||||
@@ -0,0 +1,284 @@
|
||||
import { ArrowDownRightMini, XCircle } from "@medusajs/icons"
|
||||
import {
|
||||
Payment as MedusaPayment,
|
||||
Refund as MedusaRefund,
|
||||
Order,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Container,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@medusajs/ui"
|
||||
import { format } from "date-fns"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
getLocaleAmount,
|
||||
getStylizedAmount,
|
||||
} from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
type OrderPaymentSectionProps = {
|
||||
order: Order
|
||||
}
|
||||
|
||||
export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => {
|
||||
return (
|
||||
<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()
|
||||
|
||||
const hasCapturedPayment = order.payments.some((p) => !!p.captured_at)
|
||||
|
||||
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: `/orders/${order.id}/refund`,
|
||||
disabled: !hasCapturedPayment,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</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: `/orders/${payment.order_id}/refund?paymentId=${payment.id}`,
|
||||
disabled: !payment.captured_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</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,246 @@
|
||||
import { Buildings, PencilSquare, ArrowUturnLeft } from "@medusajs/icons"
|
||||
import { OrderDTO, OrderLineItemDTO, ReservationItemDTO } from "@medusajs/types"
|
||||
import { Container, Copy, Heading, StatusBadge, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import {
|
||||
getLocaleAmount,
|
||||
getStylizedAmount,
|
||||
} from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
type OrderSummarySectionProps = {
|
||||
order: OrderDTO
|
||||
}
|
||||
|
||||
export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
||||
return (
|
||||
<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: OrderDTO }) => {
|
||||
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: `/orders/${order.id}/edit`,
|
||||
// icon: <PencilSquare />,
|
||||
// },
|
||||
// {
|
||||
// label: t("orders.summary.allocateItems"),
|
||||
// to: "#", // TODO: Open modal to allocate items
|
||||
// icon: <Buildings />,
|
||||
// },
|
||||
// {
|
||||
// label: t("orders.summary.requestReturn"),
|
||||
// to: `/orders/${order.id}/returns`,
|
||||
// icon: <ArrowUturnLeft />,
|
||||
// },
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
item,
|
||||
currencyCode,
|
||||
reservation,
|
||||
}: {
|
||||
item: OrderLineItemDTO
|
||||
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: OrderDTO }) => {
|
||||
// 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={null /* TODO: fetch reservation for this item */}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</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: OrderDTO }) => {
|
||||
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")}
|
||||
// TODO: ORDER<>DISCOUNTS link
|
||||
// secondaryValue={
|
||||
// order.discounts.length > 0
|
||||
// ? order.discounts.map((d) => d.code).join(", ")
|
||||
// : "-"
|
||||
// }
|
||||
value={
|
||||
order.discount_total > 0
|
||||
? `- ${getLocaleAmount(order.discount_total, order.currency_code)}`
|
||||
: "-"
|
||||
}
|
||||
/>
|
||||
<Cost
|
||||
label={t("fields.shipping")}
|
||||
// TODO: ORDER<>SHIPPING link
|
||||
// secondaryValue={order.shipping_methods
|
||||
// .map((sm) => sm.shipping_option.name)
|
||||
// .join(", ")}
|
||||
value={getLocaleAmount(order.shipping_total, order.currency_code)}
|
||||
/>
|
||||
<Cost
|
||||
label={t("fields.tax")}
|
||||
// TODO: TAX_RATE is missing on order
|
||||
secondaryValue={`${order.tax_rate || 0}%`}
|
||||
value={
|
||||
order.tax_total
|
||||
? getLocaleAmount(order.tax_total, order.currency_code)
|
||||
: "-"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Total = ({ order }: { order: OrderDTO }) => {
|
||||
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,30 @@
|
||||
const DEFAULT_PROPERTIES = [
|
||||
"id",
|
||||
"status",
|
||||
"created_at",
|
||||
"email",
|
||||
// "fulfillment_status", // -> TODO replacement for this
|
||||
// "payment_status", // -> TODO replacement for this
|
||||
"display_id",
|
||||
"currency_code",
|
||||
// --- TOTALS ---
|
||||
"total",
|
||||
"subtotal",
|
||||
"discounts_total",
|
||||
"shipping_total",
|
||||
"tax_total",
|
||||
]
|
||||
|
||||
const DEFAULT_RELATIONS = [
|
||||
"*customer",
|
||||
"*items", // -> we get LineItem here with added `quantity` and `detail` which is actually an OrderItem (which is a parent object to LineItem in the DB)
|
||||
"*items.variant.options",
|
||||
"*shipping_address",
|
||||
"*billing_address",
|
||||
"*sales_channel",
|
||||
"*promotion",
|
||||
]
|
||||
|
||||
export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
|
||||
","
|
||||
)},${DEFAULT_RELATIONS.join(",")}`
|
||||
@@ -0,0 +1,2 @@
|
||||
export { orderLoader as loader } from "./loader"
|
||||
export { OrderDetail as Component } from "./order-detail"
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AdminOrdersRes } from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import { adminOrderKeys } from "medusa-react"
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { queryClient } from "../../../lib/medusa"
|
||||
import { DEFAULT_FIELDS } from "./constants"
|
||||
import { client } from "../../../lib/client"
|
||||
|
||||
const orderDetailQuery = (id: string) => ({
|
||||
queryKey: adminOrderKeys.detail(id),
|
||||
queryFn: async () =>
|
||||
client.orders.retrieve(id, {
|
||||
fields: DEFAULT_FIELDS,
|
||||
}),
|
||||
})
|
||||
|
||||
export const orderLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = orderDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<Response<AdminOrdersRes>>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { OrderActivitySection } from "./components/order-activity-section"
|
||||
import { OrderCustomerSection } from "./components/order-customer-section"
|
||||
import { OrderFulfillmentSection } from "./components/order-fulfillment-section"
|
||||
import { OrderGeneralSection } from "./components/order-general-section"
|
||||
import { OrderPaymentSection } from "./components/order-payment-section"
|
||||
import { OrderSummarySection } from "./components/order-summary-section"
|
||||
import { DEFAULT_FIELDS } from "./constants"
|
||||
import { orderLoader } from "./loader"
|
||||
import { useOrder } from "../../../hooks/api/orders"
|
||||
|
||||
export const OrderDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<ReturnType<typeof orderLoader>>
|
||||
|
||||
const { id } = useParams()
|
||||
|
||||
const { order, isLoading, isError, error } = useOrder(
|
||||
id!,
|
||||
{
|
||||
fields: DEFAULT_FIELDS,
|
||||
},
|
||||
{
|
||||
initialData,
|
||||
}
|
||||
)
|
||||
|
||||
if (isLoading || !order) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
||||
<div className="flex w-full 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 xl:hidden">
|
||||
<OrderCustomerSection order={order} />
|
||||
<OrderActivitySection order={order} />
|
||||
</div>
|
||||
<JsonViewSection data={order} />
|
||||
</div>
|
||||
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
|
||||
<OrderCustomerSection order={order} />
|
||||
<OrderActivitySection order={order} />
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-list-table"
|
||||
@@ -0,0 +1,67 @@
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { DataTable } from "../../../../../components/table/data-table/data-table"
|
||||
import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns"
|
||||
import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters"
|
||||
import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useOrders } from "../../../../../hooks/api/orders"
|
||||
|
||||
import { DEFAULT_FIELDS } from "../../const"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const OrderListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
const { searchParams, raw } = useOrderTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const { orders, count, isError, error, isLoading } = useOrders(
|
||||
{
|
||||
fields: DEFAULT_FIELDS,
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
}
|
||||
)
|
||||
|
||||
const filters = useOrderTableFilters()
|
||||
const columns = useOrderTableColumns({})
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: orders ?? [],
|
||||
columns,
|
||||
enablePagination: true,
|
||||
count,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{t("orders.domain")}</Heading>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
table={table}
|
||||
pagination
|
||||
navigateTo={(row) => `/orders/${row.original.id}`}
|
||||
filters={filters}
|
||||
count={count}
|
||||
search
|
||||
isLoading={isLoading}
|
||||
pageSize={PAGE_SIZE}
|
||||
orderBy={["display_id", "created_at", "updated_at"]}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
const DEFAULT_PROPERTIES = [
|
||||
"id",
|
||||
"status",
|
||||
"created_at",
|
||||
"email",
|
||||
"display_id",
|
||||
// "fulfillment_status", // -> TODO replacement for this
|
||||
// "payment_status", // -> TODO replacement for this
|
||||
"total",
|
||||
"currency_code",
|
||||
]
|
||||
|
||||
const DEFAULT_RELATIONS = ["*customer", "*sales_channel"]
|
||||
|
||||
export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
|
||||
","
|
||||
)},${DEFAULT_RELATIONS.join(",")}`
|
||||
@@ -1,5 +1,9 @@
|
||||
// TODO: This is just a placeholder to render the actual "home" page
|
||||
// after logging in. Replace this with the actual order components when ready
|
||||
import { OrderListTable } from "./components/order-list-table"
|
||||
|
||||
export const OrderList = () => {
|
||||
return <div className="flex w-full flex-col gap-y-2"></div>
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-y-2">
|
||||
<OrderListTable />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user