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:
Frane Polić
2024-09-01 09:49:14 +02:00
committed by GitHub
parent 7e92aa412c
commit dbb10ff051
10 changed files with 213 additions and 12 deletions

View File

@@ -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,

View File

@@ -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<

View File

@@ -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"
}
}
}

View File

@@ -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"

View File

@@ -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]
}

View File

@@ -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)}

View File

@@ -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,
})
}
}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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