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ć authored and GitHub committed 2024-09-01 07:49:14 +00:00
1 parent 7e92aa412c
commit dbb10ff051
10 files changed
+213 -12

No files matched your search

@@ -50,6 +50,10 @@ export const useRequestOrderEdit = (
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(id), queryKey: ordersQueryKeys.preview(id),
}) })
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.changes(id),
})
options?.onSuccess?.(data, variables, context) options?.onSuccess?.(data, variables, context)
}, },
...options, ...options,
@@ -74,6 +78,10 @@ export const useConfirmOrderEdit = (
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(id), queryKey: ordersQueryKeys.preview(id),
}) })
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.changes(id),
})
options?.onSuccess?.(data, variables, context) options?.onSuccess?.(data, variables, context)
}, },
...options, ...options,
@@ -94,6 +102,10 @@ export const useCancelOrderEdit = (
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ordersQueryKeys.preview(orderId), queryKey: ordersQueryKeys.preview(orderId),
}) })
queryClient.invalidateQueries({
queryKey: ordersQueryKeys.changes(id),
})
options?.onSuccess?.(data, variables, context) options?.onSuccess?.(data, variables, context)
}, },
...options, ...options,
@@ -18,12 +18,17 @@ const _orderKeys = queryKeysFactory(ORDERS_QUERY_KEY) as TQueryKey<
string string
> & { > & {
preview: (orderId: string) => any preview: (orderId: string) => any
changes: (orderId: string) => any
} }
_orderKeys.preview = function (id: string) { _orderKeys.preview = function (id: string) {
return [this.detail(id), "preview"] return [this.detail(id), "preview"]
} }
_orderKeys.changes = function (id: string) {
return [this.detail(id), "changes"]
}
export const ordersQueryKeys = _orderKeys export const ordersQueryKeys = _orderKeys
export const useOrder = ( export const useOrder = (
@@ -81,6 +86,28 @@ export const useOrders = (
return { ...data, ...rest } 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 = ( export const useCreateOrderFulfillment = (
orderId: string, orderId: string,
options?: UseMutationOptions< options?: UseMutationOptions<
@@ -105,6 +105,7 @@
"revoke": "Revoke", "revoke": "Revoke",
"cancel": "Cancel", "cancel": "Cancel",
"forceConfirm": "Force confirm", "forceConfirm": "Force confirm",
"continueEdit": "Continue edit",
"enable": "Enable", "enable": "Enable",
"disable": "Disable", "disable": "Disable",
"undo": "Undo", "undo": "Undo",
@@ -837,7 +838,8 @@
"summary": { "summary": {
"requestReturn": "Request return", "requestReturn": "Request return",
"allocateItems": "Allocate items", "allocateItems": "Allocate items",
"editOrder": "Edit order" "editOrder": "Edit order",
"editOrderContinue": "Continue order edit"
}, },
"payment": { "payment": {
"title": "Payments", "title": "Payments",
@@ -890,7 +892,8 @@
"createSuccessToast": "Order edit request created", "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.", "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": { "panel": {
"title": "Order edit requested" "title": "Order edit requested",
"titlePending": "Order edit pending"
}, },
"toast": { "toast": {
"canceledSuccessfully": "Order edit cancelled", "canceledSuccessfully": "Order edit cancelled",
@@ -1162,6 +1165,11 @@
"created": "Exchange #{{exchangeId}} requested", "created": "Exchange #{{exchangeId}} requested",
"itemsInbound": "{{count}} item to return", "itemsInbound": "{{count}} item to return",
"itemsOutbound": "{{count}} item to send" "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 { useMemo } from "react"
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { Thumbnail } from "../../../../../components/common/thumbnail" import { Thumbnail } from "../../../../../components/common/thumbnail"
import { useNavigate } from "react-router-dom"
type OrderActiveEditSectionProps = { type OrderActiveEditSectionProps = {
order: HttpTypes.AdminOrder order: HttpTypes.AdminOrder
@@ -51,12 +52,15 @@ export const OrderActiveEditSection = ({
order, order,
}: OrderActiveEditSectionProps) => { }: OrderActiveEditSectionProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate()
const { order: orderPreview } = useOrderPreview(order.id) const { order: orderPreview } = useOrderPreview(order.id)
const { mutateAsync: cancelOrderEdit } = useCancelOrderEdit(order.id) const { mutateAsync: cancelOrderEdit } = useCancelOrderEdit(order.id)
const { mutateAsync: confirmOrderEdit } = useConfirmOrderEdit(order.id) const { mutateAsync: confirmOrderEdit } = useConfirmOrderEdit(order.id)
const isPending = orderPreview.order_change?.status === "pending"
const [addedItems, removedItems] = useMemo(() => { const [addedItems, removedItems] = useMemo(() => {
const added = [] const added = []
const removed = [] const removed = []
@@ -125,7 +129,13 @@ export const OrderActiveEditSection = ({
<div className="flex w-full flex-col divide-y divide-dashed"> <div className="flex w-full flex-col divide-y divide-dashed">
<div className="flex items-center gap-2 px-6 py-4"> <div className="flex items-center gap-2 px-6 py-4">
<ExclamationCircleSolid className="text-blue-500" /> <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> </div>
{/*ADDED ITEMS*/} {/*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"> <div className="bg-ui-bg-subtle flex items-center justify-end gap-x-2 rounded-b-xl px-4 py-4">
<Button {isPending ? (
size="small" <Button
variant="secondary" size="small"
onClick={onConfirmOrderEdit} variant="secondary"
> onClick={() => navigate(`/orders/${order.id}/edits`)}
{t("actions.forceConfirm")} >
</Button> {t("actions.continueEdit")}
</Button>
) : (
<Button
size="small"
variant="secondary"
onClick={onConfirmOrderEdit}
>
{t("actions.forceConfirm")}
</Button>
)}
<Button <Button
size="small" size="small"
variant="secondary" variant="secondary"
@@ -10,6 +10,7 @@ import {
AdminExchange, AdminExchange,
AdminFulfillment, AdminFulfillment,
AdminOrder, AdminOrder,
AdminOrderChange,
AdminReturn, AdminReturn,
} from "@medusajs/types" } from "@medusajs/types"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
@@ -21,6 +22,7 @@ import { useCancelReturn, useReturns } from "../../../../../hooks/api/returns"
import { useDate } from "../../../../../hooks/use-date" import { useDate } from "../../../../../hooks/use-date"
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
import { getPaymentsFromOrder } from "../order-payment-section" import { getPaymentsFromOrder } from "../order-payment-section"
import { useOrderChanges } from "../../../../../hooks/api"
import ActivityItems from "./activity-items" import ActivityItems from "./activity-items"
type OrderTimelineProps = { type OrderTimelineProps = {
@@ -106,11 +108,16 @@ type Activity = {
const useActivityItems = (order: AdminOrder): Activity[] => { const useActivityItems = (order: AdminOrder): Activity[] => {
const { t } = useTranslation() const { t } = useTranslation()
const itemsMap = useMemo( const itemsMap = useMemo(
() => new Map(order?.items?.map((i) => [i.id, i])), () => new Map(order?.items?.map((i) => [i.id, i])),
[order.items] [order.items]
) )
const { order_changes: orderChanges = [] } = useOrderChanges(order.id, {
change_type: "edit",
})
const { returns = [] } = useReturns({ const { returns = [] } = useReturns({
order_id: order.id, order_id: order.id,
fields: "+received_at,*items", 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 || []) { // for (const note of notes || []) {
// items.push({ // items.push({
// title: t("orders.activity.events.note.comment"), // title: t("orders.activity.events.note.comment"),
@@ -334,7 +367,7 @@ const useActivityItems = (order: AdminOrder): Activity[] => {
} }
return [...sortedActivities, createdAt] return [...sortedActivities, createdAt]
}, [order, notes, isLoading, t]) }, [order, payments, returns, exchanges, orderChanges, notes, isLoading])
} }
type OrderActivityItemProps = PropsWithChildren<{ type OrderActivityItemProps = PropsWithChildren<{
@@ -686,3 +719,68 @@ const ExchangeBody = ({
</div> </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" 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 ( return (
<div className="flex items-center justify-between px-6 py-4"> <div className="flex items-center justify-between px-6 py-4">
@@ -294,7 +298,11 @@ const Header = ({
{ {
actions: [ actions: [
{ {
label: t("orders.summary.editOrder"), label: t(
isOrderEditPending
? "orders.summary.editOrderContinue"
: "orders.summary.editOrder"
),
to: `/orders/${order.id}/edits`, to: `/orders/${order.id}/edits`,
icon: <PencilSquare />, icon: <PencilSquare />,
disabled: disabled:
@@ -557,6 +565,7 @@ const CostBreakdown = ({ order }: { order: AdminOrder }) => {
) )
.map((sm, i) => ( .map((sm, i) => (
<Cost <Cost
key={sm.id}
label={t("fields.shipping") + (i ? ` ${i + 1}` : "")} label={t("fields.shipping") + (i ? ` ${i + 1}` : "")}
secondaryValue={sm.name} secondaryValue={sm.name}
value={getLocaleAmount(sm.total, order.currency_code)} value={getLocaleAmount(sm.total, order.currency_code)}
+15
View File
@@ -4,6 +4,8 @@ import {
PaginatedResponse, PaginatedResponse,
SelectParams, SelectParams,
} from "@medusajs/types" } from "@medusajs/types"
import { AdminOrderChangesResponse } from "@medusajs/types/src/http/order/admin/responses"
import { Client } from "../client" import { Client } from "../client"
import { ClientHeaders } from "../types" 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 { export interface AdminOrderLineItem extends BaseOrderLineItem {
variant?: AdminProductVariant variant?: AdminProductVariant
} }
export interface AdminOrderChange extends BaseOrderChange {}
export interface AdminOrderFulfillment extends BaseOrderFulfillment {} export interface AdminOrderFulfillment extends BaseOrderFulfillment {}
@@ -1,6 +1,7 @@
import { OperatorMap } from "../../../dal" import { OperatorMap } from "../../../dal"
import { FindParams } from "../../common" import { FindParams } from "../../common"
import { BaseOrderFilters } from "../common" import { BaseOrderFilters } from "../common"
import { BaseOrderChangesFilters } from "../common"
export interface AdminOrderFilters extends FindParams, BaseOrderFilters { export interface AdminOrderFilters extends FindParams, BaseOrderFilters {
id?: string[] | string id?: string[] | string
@@ -13,3 +14,6 @@ export interface AdminOrderFilters extends FindParams, BaseOrderFilters {
created_at?: OperatorMap<string> created_at?: OperatorMap<string>
updated_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[]> 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 { export interface BaseOrderChange {
/** /**
* The ID of the order change * The ID of the order change