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:
1 parent
0430e63b0b
commit
39c3f6d92a
40 files changed
+1953
-81
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/link-modules": patch
|
||||||
|
"@medusajs/modules-sdk": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Default MedusaApp schema scalar
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"is": "is",
|
"is": "is",
|
||||||
"timeline": "Timeline",
|
"timeline": "Timeline",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
|
"warning": "Warning",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
@@ -540,6 +541,10 @@
|
|||||||
"canceled": "Canceled",
|
"canceled": "Canceled",
|
||||||
"requiresAction": "Requires action"
|
"requiresAction": "Requires action"
|
||||||
},
|
},
|
||||||
|
"toast": {
|
||||||
|
"canceled": "Fulfillment successfully canceled",
|
||||||
|
"fulfillmentShipped": "Cannot cancel an already shipped fulfillment"
|
||||||
|
},
|
||||||
"trackingLabel": "Tracking",
|
"trackingLabel": "Tracking",
|
||||||
"shippingFromLabel": "Shipping from",
|
"shippingFromLabel": "Shipping from",
|
||||||
"itemsLabel": "Items"
|
"itemsLabel": "Items"
|
||||||
|
|||||||
+5
@@ -12,6 +12,11 @@ export const FulfillmentStatusCell = ({
|
|||||||
}: FulfillmentStatusCellProps) => {
|
}: FulfillmentStatusCellProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
// TODO: remove this once fulfillment<>order link is added
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
const { label, color } = getOrderFulfillmentStatus(t, status)
|
const { label, color } = getOrderFulfillmentStatus(t, status)
|
||||||
|
|
||||||
return <StatusCell color={color}>{label}</StatusCell>
|
return <StatusCell color={color}>{label}</StatusCell>
|
||||||
|
|||||||
+3
@@ -10,6 +10,9 @@ type PaymentStatusCellProps = {
|
|||||||
export const PaymentStatusCell = ({ status }: PaymentStatusCellProps) => {
|
export const PaymentStatusCell = ({ status }: PaymentStatusCellProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// TODO: remove this when Order<>Payments are linked
|
||||||
|
return "-"
|
||||||
|
|
||||||
const { label, color } = getOrderPaymentStatus(t, status)
|
const { label, color } = getOrderPaymentStatus(t, status)
|
||||||
|
|
||||||
return <StatusCell color={color}>{label}</StatusCell>
|
return <StatusCell color={color}>{label}</StatusCell>
|
||||||
|
|||||||
@@ -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 { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import type { Filter } from "../../../components/table/data-table"
|
import type { Filter } from "../../../components/table/data-table"
|
||||||
|
import { useRegions } from "../../api/regions"
|
||||||
|
import { useSalesChannels } from "../../api/sales-channels"
|
||||||
|
|
||||||
export const useOrderTableFilters = (): Filter[] => {
|
export const useOrderTableFilters = (): Filter[] => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { regions } = useAdminRegions({
|
const { regions } = useRegions({
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
fields: "id,name",
|
fields: "id,name",
|
||||||
expand: "",
|
expand: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const { sales_channels } = useAdminSalesChannels({
|
const { sales_channels } = useSalesChannels({
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
fields: "id,name",
|
fields: "id,name",
|
||||||
expand: "",
|
expand: "",
|
||||||
@@ -145,8 +146,9 @@ export const useOrderTableFilters = (): Filter[] => {
|
|||||||
|
|
||||||
filters = [
|
filters = [
|
||||||
...filters,
|
...filters,
|
||||||
paymentStatusFilter,
|
// TODO: enable when Payment, Fulfillments <> Orders are linked
|
||||||
fulfillmentStatusFilter,
|
// paymentStatusFilter,
|
||||||
|
// fulfillmentStatusFilter,
|
||||||
...dateFilters,
|
...dateFilters,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { stores } from "./stores"
|
|||||||
import { tags } from "./tags"
|
import { tags } from "./tags"
|
||||||
import { taxes } from "./taxes"
|
import { taxes } from "./taxes"
|
||||||
import { users } from "./users"
|
import { users } from "./users"
|
||||||
|
import { orders } from "./orders"
|
||||||
import { workflowExecutions } from "./workflow-executions"
|
import { workflowExecutions } from "./workflow-executions"
|
||||||
import { shippingProfiles } from "./shipping-profiles"
|
import { shippingProfiles } from "./shipping-profiles"
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ export const client = {
|
|||||||
shippingProfiles: shippingProfiles,
|
shippingProfiles: shippingProfiles,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
users: users,
|
users: users,
|
||||||
|
orders: orders,
|
||||||
regions: regions,
|
regions: regions,
|
||||||
taxes: taxes,
|
taxes: taxes,
|
||||||
invites: invites,
|
invites: invites,
|
||||||
|
|||||||
@@ -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">,
|
t: TFunction<"translation">,
|
||||||
status: PaymentStatus
|
status: PaymentStatus
|
||||||
) => {
|
) => {
|
||||||
const [label, color] = {
|
const [label, color] = ({
|
||||||
not_paid: [t("orders.payment.status.notPaid"), "red"],
|
not_paid: [t("orders.payment.status.notPaid"), "red"],
|
||||||
awaiting: [t("orders.payment.status.awaiting"), "orange"],
|
awaiting: [t("orders.payment.status.awaiting"), "orange"],
|
||||||
captured: [t("orders.payment.status.captured"), "green"],
|
captured: [t("orders.payment.status.captured"), "green"],
|
||||||
@@ -16,7 +16,9 @@ export const getOrderPaymentStatus = (
|
|||||||
],
|
],
|
||||||
canceled: [t("orders.payment.status.canceled"), "red"],
|
canceled: [t("orders.payment.status.canceled"), "red"],
|
||||||
requires_action: [t("orders.payment.status.requiresAction"), "orange"],
|
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 }
|
return { label, color }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@ export const RouteMap: RouteObject[] = [
|
|||||||
path: "",
|
path: "",
|
||||||
lazy: () => import("../../v2-routes/orders/order-list"),
|
lazy: () => import("../../v2-routes/orders/order-list"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ":id",
|
||||||
|
lazy: () => import("../../v2-routes/orders/order-detail"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+5
@@ -99,6 +99,11 @@ const FulfillmentBadge = ({ order }: { order: Order }) => {
|
|||||||
const PaymentBadge = ({ order }: { order: Order }) => {
|
const PaymentBadge = ({ order }: { order: Order }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: revisit when Order<>Payment are linked
|
||||||
|
*/
|
||||||
|
return null
|
||||||
|
|
||||||
const { label, color } = getOrderPaymentStatus(t, order.payment_status)
|
const { label, color } = getOrderPaymentStatus(t, order.payment_status)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
FulfillmentProviderDTO,
|
FulfillmentProviderDTO,
|
||||||
InventoryNext,
|
InventoryNext,
|
||||||
InviteDTO,
|
InviteDTO,
|
||||||
|
OrderDTO,
|
||||||
PaymentProviderDTO,
|
PaymentProviderDTO,
|
||||||
PriceListDTO,
|
PriceListDTO,
|
||||||
ProductCategoryDTO,
|
ProductCategoryDTO,
|
||||||
@@ -99,6 +100,10 @@ export type InviteRes = { invite: InviteDTO }
|
|||||||
export type InviteListRes = { invites: InviteDTO[] } & ListRes
|
export type InviteListRes = { invites: InviteDTO[] } & ListRes
|
||||||
export type InviteDeleteRes = DeleteRes
|
export type InviteDeleteRes = DeleteRes
|
||||||
|
|
||||||
|
// Orders
|
||||||
|
export type OrderRes = { order: OrderDTO }
|
||||||
|
export type OrderListRes = { orders: OrderDTO[] } & ListRes
|
||||||
|
|
||||||
// Products
|
// Products
|
||||||
export type ExtendedProductDTO = ProductDTO & {
|
export type ExtendedProductDTO = ProductDTO & {
|
||||||
variants: ProductVariantDTO[] | null
|
variants: ProductVariantDTO[] | null
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./order-activity-section"
|
||||||
+25
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+111
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+404
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./order-customer-section"
|
||||||
+69
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./order-fulfillment-section"
|
||||||
+340
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./order-general-section"
|
||||||
+118
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./order-payment-section"
|
||||||
+284
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./order-summary-section"
|
||||||
+246
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./order-list-table"
|
||||||
+67
@@ -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
|
import { OrderListTable } from "./components/order-list-table"
|
||||||
// after logging in. Replace this with the actual order components when ready
|
|
||||||
export const OrderList = () => {
|
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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,13 @@ function isMedusaModule(mod) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanAndMergeSchema(loadedSchema) {
|
function cleanAndMergeSchema(loadedSchema) {
|
||||||
const { schema: cleanedSchema, notFound } = cleanGraphQLSchema(loadedSchema)
|
const defaultMedusaSchema = `
|
||||||
|
scalar DateTime
|
||||||
|
scalar JSON
|
||||||
|
`
|
||||||
|
const { schema: cleanedSchema, notFound } = cleanGraphQLSchema(
|
||||||
|
defaultMedusaSchema + loadedSchema
|
||||||
|
)
|
||||||
const mergedSchema = mergeTypeDefs(cleanedSchema)
|
const mergedSchema = mergeTypeDefs(cleanedSchema)
|
||||||
return { schema: makeExecutableSchema({ typeDefs: mergedSchema }), notFound }
|
return { schema: makeExecutableSchema({ typeDefs: mergedSchema }), notFound }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export * from "./cart-promotion"
|
|||||||
export * from "./fulfillment-set-location"
|
export * from "./fulfillment-set-location"
|
||||||
export * from "./order-promotion"
|
export * from "./order-promotion"
|
||||||
export * from "./order-region"
|
export * from "./order-region"
|
||||||
export * from "./order-sales-channel"
|
|
||||||
export * from "./product-sales-channel"
|
export * from "./product-sales-channel"
|
||||||
export * from "./product-shipping-profile"
|
export * from "./product-shipping-profile"
|
||||||
export * from "./product-variant-inventory-item"
|
export * from "./product-variant-inventory-item"
|
||||||
|
|||||||
@@ -35,11 +35,14 @@ export const OrderPromotion: ModuleJoinerConfig = {
|
|||||||
extends: [
|
extends: [
|
||||||
{
|
{
|
||||||
serviceName: Modules.ORDER,
|
serviceName: Modules.ORDER,
|
||||||
|
fieldAlias: {
|
||||||
|
promotion: "promotion_link.promotion",
|
||||||
|
},
|
||||||
relationship: {
|
relationship: {
|
||||||
serviceName: LINKS.OrderPromotion,
|
serviceName: LINKS.OrderPromotion,
|
||||||
primaryKey: "order_id",
|
primaryKey: "order_id",
|
||||||
foreignKey: "id",
|
foreignKey: "id",
|
||||||
alias: "order_link",
|
alias: "promotion_link",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -48,7 +51,7 @@ export const OrderPromotion: ModuleJoinerConfig = {
|
|||||||
serviceName: LINKS.OrderPromotion,
|
serviceName: LINKS.OrderPromotion,
|
||||||
primaryKey: "promotion_id",
|
primaryKey: "promotion_id",
|
||||||
foreignKey: "id",
|
foreignKey: "id",
|
||||||
alias: "promotion_link",
|
alias: "order_link",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
|
||||||
|
|
||||||
import { Modules } from "@medusajs/modules-sdk"
|
|
||||||
import { LINKS } from "@medusajs/utils"
|
|
||||||
|
|
||||||
export const OrderSalesChannel: ModuleJoinerConfig = {
|
|
||||||
serviceName: LINKS.OrderSalesChannel,
|
|
||||||
isLink: true,
|
|
||||||
databaseConfig: {
|
|
||||||
tableName: "order_sales_channel",
|
|
||||||
idPrefix: "ordersc",
|
|
||||||
},
|
|
||||||
alias: [
|
|
||||||
{
|
|
||||||
name: "order_sales_channel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "order_sales_channels",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
primaryKeys: ["id", "order_id", "sales_channel_id"],
|
|
||||||
relationships: [
|
|
||||||
{
|
|
||||||
serviceName: Modules.ORDER,
|
|
||||||
isInternalService: true,
|
|
||||||
primaryKey: "id",
|
|
||||||
foreignKey: "order_id",
|
|
||||||
alias: "order",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
serviceName: "salesChannelService",
|
|
||||||
isInternalService: true,
|
|
||||||
primaryKey: "id",
|
|
||||||
foreignKey: "sales_channel_id",
|
|
||||||
alias: "sales_channel",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extends: [
|
|
||||||
{
|
|
||||||
serviceName: Modules.ORDER,
|
|
||||||
fieldAlias: {
|
|
||||||
sales_channel: "sales_channel_link.sales_channel",
|
|
||||||
},
|
|
||||||
relationship: {
|
|
||||||
serviceName: LINKS.OrderSalesChannel,
|
|
||||||
isInternalService: true,
|
|
||||||
primaryKey: "order_id",
|
|
||||||
foreignKey: "id",
|
|
||||||
alias: "sales_channel_link",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
serviceName: "salesChannelService",
|
|
||||||
fieldAlias: {
|
|
||||||
orders: "order_link.order",
|
|
||||||
},
|
|
||||||
relationship: {
|
|
||||||
serviceName: LINKS.OrderSalesChannel,
|
|
||||||
isInternalService: true,
|
|
||||||
primaryKey: "sales_channel_id",
|
|
||||||
foreignKey: "id",
|
|
||||||
alias: "order_link",
|
|
||||||
isList: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -5,4 +5,5 @@ export * from "./cart-sales-channel"
|
|||||||
export * from "./inventory-level-stock-location"
|
export * from "./inventory-level-stock-location"
|
||||||
export * from "./order-customer"
|
export * from "./order-customer"
|
||||||
export * from "./order-product"
|
export * from "./order-product"
|
||||||
|
export * from "./order-sales-channel"
|
||||||
export * from "./store-default-currency"
|
export * from "./store-default-currency"
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
|
|
||||||
|
export const OrderSalesChannel: ModuleJoinerConfig = {
|
||||||
|
isLink: true,
|
||||||
|
isReadOnlyLink: true,
|
||||||
|
extends: [
|
||||||
|
{
|
||||||
|
serviceName: Modules.ORDER,
|
||||||
|
relationship: {
|
||||||
|
serviceName: Modules.SALES_CHANNEL,
|
||||||
|
primaryKey: "id",
|
||||||
|
foreignKey: "sales_channel_id",
|
||||||
|
alias: "sales_channel",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: Modules.SALES_CHANNEL,
|
||||||
|
relationship: {
|
||||||
|
serviceName: Modules.ORDER,
|
||||||
|
primaryKey: "sales_channel_id",
|
||||||
|
foreignKey: "id",
|
||||||
|
alias: "orders",
|
||||||
|
isList: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
Reference in new issue
Block a user