feat(dashboard): order edits in timeline (#8899)
**What** - add order edit confirmed/created events in the timeline - add order change endpoint clients - panel for active edit and pending edit - few fixes around the edit domain
This commit is contained in:
@@ -50,6 +50,10 @@ export const useRequestOrderEdit = (
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.changes(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
@@ -74,6 +78,10 @@ export const useConfirmOrderEdit = (
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.changes(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
@@ -94,6 +102,10 @@ export const useCancelOrderEdit = (
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.changes(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
|
||||
@@ -18,12 +18,17 @@ const _orderKeys = queryKeysFactory(ORDERS_QUERY_KEY) as TQueryKey<
|
||||
string
|
||||
> & {
|
||||
preview: (orderId: string) => any
|
||||
changes: (orderId: string) => any
|
||||
}
|
||||
|
||||
_orderKeys.preview = function (id: string) {
|
||||
return [this.detail(id), "preview"]
|
||||
}
|
||||
|
||||
_orderKeys.changes = function (id: string) {
|
||||
return [this.detail(id), "changes"]
|
||||
}
|
||||
|
||||
export const ordersQueryKeys = _orderKeys
|
||||
|
||||
export const useOrder = (
|
||||
@@ -81,6 +86,28 @@ export const useOrders = (
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useOrderChanges = (
|
||||
id: string,
|
||||
query?: HttpTypes.AdminOrderChangesFilters,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminOrderChangesResponse,
|
||||
Error,
|
||||
HttpTypes.AdminOrderChangesResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => sdk.admin.order.listChanges(id, query),
|
||||
queryKey: ordersQueryKeys.changes(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateOrderFulfillment = (
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"revoke": "Revoke",
|
||||
"cancel": "Cancel",
|
||||
"forceConfirm": "Force confirm",
|
||||
"continueEdit": "Continue edit",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"undo": "Undo",
|
||||
@@ -837,7 +838,8 @@
|
||||
"summary": {
|
||||
"requestReturn": "Request return",
|
||||
"allocateItems": "Allocate items",
|
||||
"editOrder": "Edit order"
|
||||
"editOrder": "Edit order",
|
||||
"editOrderContinue": "Continue order edit"
|
||||
},
|
||||
"payment": {
|
||||
"title": "Payments",
|
||||
@@ -890,7 +892,8 @@
|
||||
"createSuccessToast": "Order edit request created",
|
||||
"activeChangeError": "There is already active order change on the order (return, claim, exchange etc.). Please finish or cancel the change before editing the order.",
|
||||
"panel": {
|
||||
"title": "Order edit requested"
|
||||
"title": "Order edit requested",
|
||||
"titlePending": "Order edit pending"
|
||||
},
|
||||
"toast": {
|
||||
"canceledSuccessfully": "Order edit cancelled",
|
||||
@@ -1162,6 +1165,11 @@
|
||||
"created": "Exchange #{{exchangeId}} requested",
|
||||
"itemsInbound": "{{count}} item to return",
|
||||
"itemsOutbound": "{{count}} item to send"
|
||||
},
|
||||
"edit": {
|
||||
"requested": "Order edit #{{editId}} requested",
|
||||
"requested": "Order edit #{{editId}} requested",
|
||||
"confirmed": "Order edit #{{editId}} confirmed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { useMemo } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
type OrderActiveEditSectionProps = {
|
||||
order: HttpTypes.AdminOrder
|
||||
@@ -51,12 +52,15 @@ export const OrderActiveEditSection = ({
|
||||
order,
|
||||
}: OrderActiveEditSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { order: orderPreview } = useOrderPreview(order.id)
|
||||
|
||||
const { mutateAsync: cancelOrderEdit } = useCancelOrderEdit(order.id)
|
||||
const { mutateAsync: confirmOrderEdit } = useConfirmOrderEdit(order.id)
|
||||
|
||||
const isPending = orderPreview.order_change?.status === "pending"
|
||||
|
||||
const [addedItems, removedItems] = useMemo(() => {
|
||||
const added = []
|
||||
const removed = []
|
||||
@@ -125,7 +129,13 @@ export const OrderActiveEditSection = ({
|
||||
<div className="flex w-full flex-col divide-y divide-dashed">
|
||||
<div className="flex items-center gap-2 px-6 py-4">
|
||||
<ExclamationCircleSolid className="text-blue-500" />
|
||||
<Heading level="h2">{t("orders.edits.panel.title")}</Heading>
|
||||
<Heading level="h2">
|
||||
{t(
|
||||
isPending
|
||||
? "orders.edits.panel.titlePending"
|
||||
: "orders.edits.panel.title"
|
||||
)}
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
{/*ADDED ITEMS*/}
|
||||
@@ -155,13 +165,23 @@ export const OrderActiveEditSection = ({
|
||||
)}
|
||||
|
||||
<div className="bg-ui-bg-subtle flex items-center justify-end gap-x-2 rounded-b-xl px-4 py-4">
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={onConfirmOrderEdit}
|
||||
>
|
||||
{t("actions.forceConfirm")}
|
||||
</Button>
|
||||
{isPending ? (
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/orders/${order.id}/edits`)}
|
||||
>
|
||||
{t("actions.continueEdit")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={onConfirmOrderEdit}
|
||||
>
|
||||
{t("actions.forceConfirm")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AdminExchange,
|
||||
AdminFulfillment,
|
||||
AdminOrder,
|
||||
AdminOrderChange,
|
||||
AdminReturn,
|
||||
} from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@@ -21,6 +22,7 @@ import { useCancelReturn, useReturns } from "../../../../../hooks/api/returns"
|
||||
import { useDate } from "../../../../../hooks/use-date"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { getPaymentsFromOrder } from "../order-payment-section"
|
||||
import { useOrderChanges } from "../../../../../hooks/api"
|
||||
import ActivityItems from "./activity-items"
|
||||
|
||||
type OrderTimelineProps = {
|
||||
@@ -106,11 +108,16 @@ type Activity = {
|
||||
|
||||
const useActivityItems = (order: AdminOrder): Activity[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const itemsMap = useMemo(
|
||||
() => new Map(order?.items?.map((i) => [i.id, i])),
|
||||
[order.items]
|
||||
)
|
||||
|
||||
const { order_changes: orderChanges = [] } = useOrderChanges(order.id, {
|
||||
change_type: "edit",
|
||||
})
|
||||
|
||||
const { returns = [] } = useReturns({
|
||||
order_id: order.id,
|
||||
fields: "+received_at,*items",
|
||||
@@ -304,6 +311,32 @@ const useActivityItems = (order: AdminOrder): Activity[] => {
|
||||
})
|
||||
}
|
||||
|
||||
for (const edit of orderChanges) {
|
||||
const isConfirmed = edit.status === "confirmed"
|
||||
const isPending = edit.status === "pending"
|
||||
|
||||
if (isPending) {
|
||||
continue
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: t(`orders.activity.events.edit.${edit.status}`, {
|
||||
editId: edit.id.slice(-7),
|
||||
}),
|
||||
timestamp:
|
||||
edit.status === "requested"
|
||||
? edit.requested_at
|
||||
: edit.status === "declined"
|
||||
? edit.declined_at
|
||||
: edit.status === "canceled"
|
||||
? edit.canceled_at
|
||||
: edit.created_at,
|
||||
children: isConfirmed ? (
|
||||
<OrderEditBody edit={edit} itemsMap={itemsMap} />
|
||||
) : null,
|
||||
})
|
||||
}
|
||||
|
||||
// for (const note of notes || []) {
|
||||
// items.push({
|
||||
// title: t("orders.activity.events.note.comment"),
|
||||
@@ -334,7 +367,7 @@ const useActivityItems = (order: AdminOrder): Activity[] => {
|
||||
}
|
||||
|
||||
return [...sortedActivities, createdAt]
|
||||
}, [order, notes, isLoading, t])
|
||||
}, [order, payments, returns, exchanges, orderChanges, notes, isLoading])
|
||||
}
|
||||
|
||||
type OrderActivityItemProps = PropsWithChildren<{
|
||||
@@ -686,3 +719,68 @@ const ExchangeBody = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const OrderEditBody = ({
|
||||
edit,
|
||||
itemsMap,
|
||||
}: {
|
||||
edit: AdminOrderChange
|
||||
isRequested: boolean
|
||||
itemsMap: Record<string, AdminOrderLineItem>
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [itemsAdded, itemsRemoved] = useMemo(
|
||||
() => countItemsChange(edit.actions, itemsMap),
|
||||
[edit]
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{itemsAdded > 0 && (
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("labels.added")}: {itemsAdded}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{itemsRemoved > 0 && (
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("labels.removed")}: {itemsRemoved}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function countItemsChange(
|
||||
actions: AdminOrderChange["actions"],
|
||||
itemsMap: Record<string, AdminOrderLineItem>
|
||||
) {
|
||||
let added = 0
|
||||
let removed = 0
|
||||
|
||||
actions.forEach((action) => {
|
||||
if (action.action === "ITEM_ADD") {
|
||||
added += action.details.quantity
|
||||
}
|
||||
if (action.action === "ITEM_UPDATE") {
|
||||
const newQuantity: number = action.details!.quantity
|
||||
const originalQuantity: number | undefined = itemsMap.get(
|
||||
action.details!.reference_id
|
||||
)?.quantity
|
||||
|
||||
if (typeof originalQuantity === "number") {
|
||||
const diff = Math.abs(newQuantity - originalQuantity)
|
||||
|
||||
if (newQuantity > originalQuantity) {
|
||||
added += diff
|
||||
}
|
||||
if (newQuantity < originalQuantity) {
|
||||
removed += diff
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return [added, removed]
|
||||
}
|
||||
|
||||
@@ -285,6 +285,10 @@ const Header = ({
|
||||
)
|
||||
|
||||
const isOrderEditActive = orderPreview?.order_change?.change_type === "edit"
|
||||
// State where creation of order edit was interrupted i.e. order edit is drafted but not confirmed
|
||||
const isOrderEditPending =
|
||||
orderPreview?.order_change?.change_type === "edit" &&
|
||||
orderPreview?.order_change?.status === "pending"
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
@@ -294,7 +298,11 @@ const Header = ({
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("orders.summary.editOrder"),
|
||||
label: t(
|
||||
isOrderEditPending
|
||||
? "orders.summary.editOrderContinue"
|
||||
: "orders.summary.editOrder"
|
||||
),
|
||||
to: `/orders/${order.id}/edits`,
|
||||
icon: <PencilSquare />,
|
||||
disabled:
|
||||
@@ -557,6 +565,7 @@ const CostBreakdown = ({ order }: { order: AdminOrder }) => {
|
||||
)
|
||||
.map((sm, i) => (
|
||||
<Cost
|
||||
key={sm.id}
|
||||
label={t("fields.shipping") + (i ? ` ${i + 1}` : "")}
|
||||
secondaryValue={sm.name}
|
||||
value={getLocaleAmount(sm.total, order.currency_code)}
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
PaginatedResponse,
|
||||
SelectParams,
|
||||
} from "@medusajs/types"
|
||||
import { AdminOrderChangesResponse } from "@medusajs/types/src/http/order/admin/responses"
|
||||
|
||||
import { Client } from "../client"
|
||||
import { ClientHeaders } from "../types"
|
||||
|
||||
@@ -109,4 +111,17 @@ export class Order {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async listChanges(
|
||||
id: string,
|
||||
queryParams?: FindParams & HttpTypes.AdminOrderChangesFilters,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<
|
||||
PaginatedResponse<AdminOrderChangesResponse>
|
||||
>(`/admin/orders/${id}/changes`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface AdminOrder extends BaseOrder {
|
||||
export interface AdminOrderLineItem extends BaseOrderLineItem {
|
||||
variant?: AdminProductVariant
|
||||
}
|
||||
export interface AdminOrderChange extends BaseOrderChange {}
|
||||
|
||||
export interface AdminOrderFulfillment extends BaseOrderFulfillment {}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OperatorMap } from "../../../dal"
|
||||
import { FindParams } from "../../common"
|
||||
import { BaseOrderFilters } from "../common"
|
||||
import { BaseOrderChangesFilters } from "../common"
|
||||
|
||||
export interface AdminOrderFilters extends FindParams, BaseOrderFilters {
|
||||
id?: string[] | string
|
||||
@@ -13,3 +14,6 @@ export interface AdminOrderFilters extends FindParams, BaseOrderFilters {
|
||||
created_at?: OperatorMap<string>
|
||||
updated_at?: OperatorMap<string>
|
||||
}
|
||||
|
||||
|
||||
export interface AdminOrderChangesFilters extends BaseOrderChangesFilters {}
|
||||
|
||||
@@ -310,6 +310,13 @@ export interface BaseOrderFilters
|
||||
status?: string[] | string | OperatorMap<string | string[]>
|
||||
}
|
||||
|
||||
export interface BaseOrderChangesFilters
|
||||
extends BaseFilterable<BaseOrderChangesFilters> {
|
||||
id?: string[] | string | OperatorMap<string | string[]>
|
||||
status?: string[] | string | OperatorMap<string | string[]>
|
||||
change_type?: string[] | string | OperatorMap<string | string[]>
|
||||
}
|
||||
|
||||
export interface BaseOrderChange {
|
||||
/**
|
||||
* The ID of the order change
|
||||
|
||||
Reference in New Issue
Block a user