feat(dashboard): order edit UI (#8700)
**What** - order edit create flow - active order edit panel **Note** - basic implementation of the flow, edge cases from the design such as fulfilled quantities validation will be added in a followup
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import { useMutation, UseMutationOptions } from "@tanstack/react-query"
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { ordersQueryKeys } from "./orders"
|
||||
|
||||
export const useCreateOrderEdit = (
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminOrderEditPreviewResponse,
|
||||
Error,
|
||||
HttpTypes.AdminInitiateOrderEditRequest
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminInitiateOrderEditRequest) =>
|
||||
sdk.admin.orderEdit.initiateRequest(payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRequestOrderEdit = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminOrderEditPreviewResponse,
|
||||
Error,
|
||||
void
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => sdk.admin.orderEdit.request(id),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useConfirmOrderEdit = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminOrderEditPreviewResponse,
|
||||
Error,
|
||||
void
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => sdk.admin.orderEdit.confirm(id),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCancelOrderEdit = (
|
||||
orderId: string,
|
||||
options?: UseMutationOptions<any, Error, any>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => sdk.admin.orderEdit.cancelRequest(orderId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.details(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(orderId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddOrderEditItems = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminOrderEditPreviewResponse,
|
||||
Error,
|
||||
HttpTypes.AdminAddOrderEditItems
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: HttpTypes.AdminAddOrderEditItems) =>
|
||||
sdk.admin.orderEdit.addItems(id, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (quantity) of an item that was originally on the order.
|
||||
*/
|
||||
export const useUpdateOrderEditOriginalItem = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminOrderEditPreviewResponse,
|
||||
Error,
|
||||
HttpTypes.AdminUpdateOrderEditItem & { itemId: string }
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
itemId,
|
||||
...payload
|
||||
}: HttpTypes.AdminUpdateOrderEditItem & { itemId: string }) => {
|
||||
return sdk.admin.orderEdit.updateOriginalItem(id, itemId, payload)
|
||||
},
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (quantity) of an item that was added to the order edit.
|
||||
*/
|
||||
export const useUpdateOrderEditAddedItem = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminOrderEditPreviewResponse,
|
||||
Error,
|
||||
HttpTypes.AdminUpdateOrderEditItem & { actionId: string }
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
actionId,
|
||||
...payload
|
||||
}: HttpTypes.AdminUpdateOrderEditItem & { actionId: string }) => {
|
||||
return sdk.admin.orderEdit.updateAddedItem(id, actionId, payload)
|
||||
},
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item that was added to the edit.
|
||||
* To remove an original item on the order, set quantity to 0.
|
||||
*/
|
||||
export const useRemoveOrderEditItem = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminOrderEditPreviewResponse,
|
||||
Error,
|
||||
string
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (actionId: string) =>
|
||||
sdk.admin.orderEdit.removeAddedItem(id, actionId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -45,6 +45,7 @@ export const useOrder = (
|
||||
|
||||
export const useOrderPreview = (
|
||||
id: string,
|
||||
query?: HttpTypes.AdminOrderFilters,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminOrderPreviewResponse,
|
||||
@@ -56,7 +57,7 @@ export const useOrderPreview = (
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: async () => sdk.admin.order.retrievePreview(id),
|
||||
queryFn: async () => sdk.admin.order.retrievePreview(id, query),
|
||||
queryKey: ordersQueryKeys.preview(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
"expired": "Expired",
|
||||
"active": "Active",
|
||||
"revoked": "Revoked",
|
||||
"new": "New",
|
||||
"modified": "Modified",
|
||||
"removed": "Removed",
|
||||
"admin": "Admin",
|
||||
"store": "Store",
|
||||
"details": "Details",
|
||||
@@ -94,14 +97,17 @@
|
||||
"actions": {
|
||||
"save": "Save",
|
||||
"saveAsDraft": "Save as draft",
|
||||
"duplicate": "Duplicate",
|
||||
"publish": "Publish",
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"remove": "Remove",
|
||||
"revoke": "Revoke",
|
||||
"cancel": "Cancel",
|
||||
"forceConfirm": "Force confirm",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"undo": "Undo",
|
||||
"complete": "Complete",
|
||||
"viewDetails": "View details",
|
||||
"back": "Back",
|
||||
@@ -806,7 +812,7 @@
|
||||
"summary": {
|
||||
"requestReturn": "Request return",
|
||||
"allocateItems": "Allocate items",
|
||||
"editItems": "Edit items"
|
||||
"editOrder": "Edit order"
|
||||
},
|
||||
"payment": {
|
||||
"title": "Payments",
|
||||
@@ -840,7 +846,6 @@
|
||||
"paymentLink": "Copy payment link for {{ amount }}",
|
||||
"selectPaymentToRefund": "Select payment to refund"
|
||||
},
|
||||
|
||||
"edits": {
|
||||
"title": "Edit order",
|
||||
"currentItems": "Current items",
|
||||
@@ -849,7 +854,23 @@
|
||||
"addItems": "Add items",
|
||||
"amountPaid": "Amount paid",
|
||||
"newTotal": "New total",
|
||||
"differenceDue": "Difference due"
|
||||
"differenceDue": "Difference due",
|
||||
"create": "Edit Order",
|
||||
"currentTotal": "Current total",
|
||||
"noteHint": "Add an internal note for the edit",
|
||||
"cancelSuccessToast": "Order edit canceled",
|
||||
"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"
|
||||
},
|
||||
"toast": {
|
||||
"canceledSuccessfully": "Order edit cancelled",
|
||||
"confirmedSuccessfully": "Order edit confirmed"
|
||||
},
|
||||
"validation": {
|
||||
"quantityLowerThanFulfillment": "Cannot set quantity to be less then or equal to fulfilled quantity"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"create": "Create Return",
|
||||
@@ -2326,7 +2347,9 @@
|
||||
"productVariant": "Product Variant",
|
||||
"prices": "Prices",
|
||||
"available": "Available",
|
||||
"inStock": "In stock"
|
||||
"inStock": "In stock",
|
||||
"added": "Added",
|
||||
"removed": "Removed"
|
||||
},
|
||||
"fields": {
|
||||
"amount": "Amount",
|
||||
@@ -2342,7 +2365,6 @@
|
||||
"inventoryItems": "Inventory items",
|
||||
"inventoryItem": "Inventory item",
|
||||
"requiredQuantity": "Required quantity",
|
||||
"qty": "Qty",
|
||||
"description": "Description",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
@@ -2365,6 +2387,7 @@
|
||||
"reason": "Reason",
|
||||
"none": "none",
|
||||
"all": "all",
|
||||
"search": "Search",
|
||||
"percentage": "Percentage",
|
||||
"sales_channels": "Sales Channels",
|
||||
"customer_groups": "Customer Groups",
|
||||
|
||||
@@ -257,6 +257,10 @@ export const RouteMap: RouteObject[] = [
|
||||
lazy: () =>
|
||||
import("../../routes/orders/order-create-exchange"),
|
||||
},
|
||||
{
|
||||
path: "edits",
|
||||
lazy: () => import("../../routes/orders/order-create-edit"),
|
||||
},
|
||||
{
|
||||
path: "refund",
|
||||
lazy: () =>
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
|
||||
import { useState } from "react"
|
||||
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useVariants } from "../../../../../hooks/api"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useOrderEditItemTableFilters } from "./use-order-edit-item-table-filters"
|
||||
import { useOrderEditItemTableQuery } from "./use-order-edit-item-table-query"
|
||||
import { useOrderEditItemsTableColumns } from "./use-order-edit-item-table-columns"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PREFIX = "rit"
|
||||
|
||||
type AddExchangeOutboundItemsTableProps = {
|
||||
onSelectionChange: (ids: string[]) => void
|
||||
currencyCode: string
|
||||
}
|
||||
|
||||
export const AddOrderEditItemsTable = ({
|
||||
onSelectionChange,
|
||||
currencyCode,
|
||||
}: AddExchangeOutboundItemsTableProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
setRowSelection(newState)
|
||||
onSelectionChange(Object.keys(newState))
|
||||
}
|
||||
|
||||
const { searchParams, raw } = useOrderEditItemTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const { variants = [], count } = useVariants({
|
||||
...searchParams,
|
||||
fields: "*inventory_items.inventory.location_levels,+inventory_quantity",
|
||||
})
|
||||
|
||||
const columns = useOrderEditItemsTableColumns(currencyCode)
|
||||
const filters = useOrderEditItemTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: variants,
|
||||
columns: columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
enableRowSelection: (_row) => {
|
||||
// TODO: Check inventory here. Check if other validations needs to be made
|
||||
return true
|
||||
},
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
filters={filters}
|
||||
pagination
|
||||
layout="fill"
|
||||
search
|
||||
orderBy={["product_id", "title", "sku"]}
|
||||
prefix={PREFIX}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./add-order-edit-items-table.tsx"
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { Checkbox } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
ProductCell,
|
||||
ProductHeader,
|
||||
} from "../../../../../components/table/table-cells/product/product-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<any>()
|
||||
|
||||
export const useOrderEditItemsTableColumns = (currencyCode: string) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isSelectable = row.getCanSelect()
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
disabled={!isSelectable}
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "product",
|
||||
header: () => <ProductHeader />,
|
||||
cell: ({ row }) => {
|
||||
return <ProductCell product={row.original.product} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sku", {
|
||||
header: t("fields.sku"),
|
||||
cell: ({ getValue }) => {
|
||||
return getValue() || "-"
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
}),
|
||||
],
|
||||
[t, currencyCode]
|
||||
)
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useOrderEditItemTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
key: "created_at",
|
||||
label: t("fields.createdAt"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
key: "updated_at",
|
||||
label: t("fields.updatedAt"),
|
||||
type: "date",
|
||||
},
|
||||
]
|
||||
|
||||
return filters
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useOrderEditItemTableQuery = ({
|
||||
pageSize = 50,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(
|
||||
["q", "offset", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, created_at, updated_at, ...rest } = raw
|
||||
|
||||
const searchParams = {
|
||||
...rest,
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
}
|
||||
|
||||
return { searchParams, raw }
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./order-edit-create-form.tsx"
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { AdminOrder, AdminOrderPreview } from "@medusajs/types"
|
||||
import { Button, Heading, Input, Switch, toast } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/modals"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { CreateOrderEditSchemaType, OrderEditCreateSchema } from "./schema"
|
||||
import {
|
||||
useCancelOrderEdit,
|
||||
useRequestOrderEdit,
|
||||
} from "../../../../../hooks/api/order-edits"
|
||||
import { OrderEditItemsSection } from "./order-edit-items-section"
|
||||
|
||||
type ReturnCreateFormProps = {
|
||||
order: AdminOrder
|
||||
preview: AdminOrderPreview
|
||||
}
|
||||
|
||||
export const OrderEditCreateForm = ({
|
||||
order,
|
||||
preview,
|
||||
}: ReturnCreateFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
/**
|
||||
* MUTATIONS
|
||||
*/
|
||||
|
||||
const { mutateAsync: cancelOrderEditRequest, isPending: isCanceling } =
|
||||
useCancelOrderEdit(order.id)
|
||||
|
||||
const { mutateAsync: requestOrderEdit, isPending: isRequesting } =
|
||||
useRequestOrderEdit(order.id)
|
||||
|
||||
const isRequestRunning = isCanceling || isRequesting
|
||||
|
||||
/**
|
||||
* FORM
|
||||
*/
|
||||
const form = useForm<CreateOrderEditSchemaType>({
|
||||
defaultValues: () => {
|
||||
return Promise.resolve({
|
||||
note: "", // TODO: add note when update edit route is added
|
||||
send_notification: false, // TODO: not supported in the API ATM
|
||||
})
|
||||
},
|
||||
resolver: zodResolver(OrderEditCreateSchema),
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
try {
|
||||
await requestOrderEdit()
|
||||
|
||||
toast.success(t("orders.edits.createSuccessToast"))
|
||||
handleSuccess()
|
||||
} catch (e) {
|
||||
toast.error(t("general.error"), {
|
||||
description: e.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form
|
||||
form={form}
|
||||
onClose={(isSubmitSuccessful) => {
|
||||
if (!isSubmitSuccessful) {
|
||||
cancelOrderEditRequest(undefined, {
|
||||
onSuccess: () => {
|
||||
toast.success(t("orders.edits.cancelSuccessToast"))
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<RouteFocusModal.Header />
|
||||
|
||||
<RouteFocusModal.Body className="flex size-full justify-center overflow-y-auto">
|
||||
<div className="mt-16 w-[720px] max-w-[100%] px-4 md:p-0">
|
||||
<Heading level="h1">{t("orders.edits.create")}</Heading>
|
||||
|
||||
<OrderEditItemsSection preview={preview} order={order} />
|
||||
|
||||
{/*TOTALS SECTION*/}
|
||||
<div className="mt-8 border-y border-dotted py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.edits.currentTotal")}
|
||||
</span>
|
||||
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{getStylizedAmount(order.total, order.currency_code)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.edits.newTotal")}
|
||||
</span>
|
||||
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{getStylizedAmount(preview.total, order.currency_code)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between border-t border-dotted pt-4">
|
||||
<span className="txt-small font-medium">
|
||||
{t("orders.exchanges.refundAmount")}
|
||||
</span>
|
||||
<span className="txt-small font-medium">
|
||||
{getStylizedAmount(
|
||||
preview.summary.pending_difference,
|
||||
order.currency_code
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*NOTE*/}
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="note"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="mt-8 flex">
|
||||
<div className="block flex-1">
|
||||
<Form.Label>{t("fields.note")}</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.edits.noteHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
<div className="w-full flex-1 flex-grow">
|
||||
<Form.Control>
|
||||
<Input {...field} placeholder={t("fields.note")} />
|
||||
</Form.Control>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/*SEND NOTIFICATION*/}
|
||||
<div className="bg-ui-bg-field mt-8 rounded-lg border py-2 pl-2 pr-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="send_notification"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center">
|
||||
<Form.Control className="mr-4 self-start">
|
||||
<Switch
|
||||
className="mt-[2px]"
|
||||
checked={!!value}
|
||||
onCheckedChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
<div className="block">
|
||||
<Form.Label>
|
||||
{t("orders.returns.sendNotification")}
|
||||
</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.returns.sendNotificationHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
<RouteFocusModal.Footer>
|
||||
<div className="flex w-full items-center justify-end gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button type="button" variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
key="submit-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
isLoading={isRequestRunning}
|
||||
>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Footer>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
import { ArrowUturnLeft, DocumentSeries, XCircle } from "@medusajs/icons"
|
||||
import { AdminOrderLineItem } from "@medusajs/types"
|
||||
import { Badge, Input, Text, toast } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
||||
import { useMemo } from "react"
|
||||
import {
|
||||
useAddOrderEditItems,
|
||||
useRemoveOrderEditItem,
|
||||
useUpdateOrderEditAddedItem,
|
||||
useUpdateOrderEditOriginalItem,
|
||||
} from "../../../../../hooks/api/order-edits"
|
||||
|
||||
type OrderEditItemProps = {
|
||||
item: AdminOrderLineItem
|
||||
currencyCode: string
|
||||
orderId: string
|
||||
}
|
||||
|
||||
function OrderEditItem({ item, currencyCode, orderId }: OrderEditItemProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { mutateAsync: addItems } = useAddOrderEditItems(orderId)
|
||||
const { mutateAsync: updateAddedItem } = useUpdateOrderEditAddedItem(orderId)
|
||||
const { mutateAsync: updateOriginalItem } =
|
||||
useUpdateOrderEditOriginalItem(orderId)
|
||||
const { mutateAsync: undoAction } = useRemoveOrderEditItem(orderId)
|
||||
|
||||
const isAddedItem = useMemo(
|
||||
() => !!item.actions?.find((a) => a.action === "ITEM_ADD"),
|
||||
[item]
|
||||
)
|
||||
|
||||
const isItemUpdated = useMemo(
|
||||
() => !!item.actions?.find((a) => a.action === "ITEM_UPDATE"),
|
||||
[item]
|
||||
)
|
||||
|
||||
const isItemRemoved = useMemo(() => {
|
||||
// To be removed item needs to have updated quantity
|
||||
const updateAction = item.actions?.find((a) => a.action === "ITEM_UPDATE")
|
||||
return !!updateAction && item.quantity === item.detail.fulfilled_quantity
|
||||
}, [item])
|
||||
|
||||
/**
|
||||
* HANDLERS
|
||||
*/
|
||||
|
||||
const onUpdate = async (quantity: number) => {
|
||||
if (quantity <= item.detail.fulfilled_quantity) {
|
||||
toast.error(t("orders.edits.validation.quantityLowerThanFulfillment"))
|
||||
return
|
||||
}
|
||||
|
||||
if (quantity === item.quantity) {
|
||||
return
|
||||
}
|
||||
|
||||
const addItemAction = item.actions?.find((a) => a.action === "ITEM_ADD")
|
||||
|
||||
try {
|
||||
if (addItemAction) {
|
||||
await updateAddedItem({ quantity, actionId: addItemAction.id })
|
||||
} else {
|
||||
await updateOriginalItem({ quantity, itemId: item.id })
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const onRemove = async () => {
|
||||
const addItemAction = item.actions?.find((a) => a.action === "ITEM_ADD")
|
||||
|
||||
try {
|
||||
if (addItemAction) {
|
||||
await undoAction(addItemAction.id)
|
||||
} else {
|
||||
await updateOriginalItem({
|
||||
quantity: item.detail.fulfilled_quantity, //
|
||||
itemId: item.id,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const onRemoveUndo = async () => {
|
||||
const updateItemAction = item.actions?.find(
|
||||
(a) => a.action === "ITEM_UPDATE"
|
||||
)
|
||||
|
||||
try {
|
||||
if (updateItemAction) {
|
||||
await undoAction(updateItemAction.id) // Remove action that updated items quantity to fulfilled quantity which makes it "removed"
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const onDuplicate = async () => {
|
||||
try {
|
||||
await addItems({
|
||||
items: [
|
||||
{
|
||||
variant_id: item.variant_id,
|
||||
quantity: item.quantity,
|
||||
},
|
||||
],
|
||||
})
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.quantity}
|
||||
className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl "
|
||||
>
|
||||
<div className="flex flex-col items-center gap-x-2 gap-y-2 p-3 text-sm md:flex-row">
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-3">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
<Text className="txt-small" as="span" weight="plus">
|
||||
{item.title}{" "}
|
||||
</Text>
|
||||
|
||||
{item.variant_sku && <span>({item.variant_sku})</span>}
|
||||
</div>
|
||||
<Text as="div" className="text-ui-fg-subtle txt-small">
|
||||
{item.product_title}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAddedItem && (
|
||||
<Badge size="2xsmall" rounded="full" color="blue" className="mr-1">
|
||||
{t("general.new")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{isItemRemoved ? (
|
||||
<Badge size="2xsmall" rounded="full" color="red" className="mr-1">
|
||||
{t("general.removed")}
|
||||
</Badge>
|
||||
) : (
|
||||
isItemUpdated && (
|
||||
<Badge
|
||||
size="2xsmall"
|
||||
rounded="full"
|
||||
color="orange"
|
||||
className="mr-1"
|
||||
>
|
||||
{t("general.modified")}
|
||||
</Badge>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 justify-between">
|
||||
<div className="flex flex-grow items-center gap-2">
|
||||
<Input
|
||||
className="bg-ui-bg-base txt-small w-[67px] rounded-lg [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
type="number"
|
||||
disabled={item.detail.fulfilled_quantity === item.quantity}
|
||||
min={item.detail.fulfilled_quantity}
|
||||
defaultValue={item.quantity}
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value
|
||||
const payload = val === "" ? null : Number(val)
|
||||
|
||||
if (payload) {
|
||||
onUpdate(payload)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Text className="txt-small text-ui-fg-subtle">
|
||||
{t("fields.qty")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-subtle txt-small mr-2 flex flex-shrink-0">
|
||||
<MoneyAmountCell currencyCode={currencyCode} amount={item.total} />
|
||||
</div>
|
||||
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.duplicate"),
|
||||
onClick: onDuplicate,
|
||||
icon: <DocumentSeries />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
!isItemRemoved
|
||||
? {
|
||||
label: t("actions.remove"),
|
||||
onClick: onRemove,
|
||||
icon: <XCircle />,
|
||||
disabled:
|
||||
item.detail.fulfilled_quantity === item.quantity,
|
||||
}
|
||||
: {
|
||||
label: t("actions.undo"),
|
||||
onClick: onRemoveUndo,
|
||||
icon: <ArrowUturnLeft />,
|
||||
},
|
||||
].filter(Boolean),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { OrderEditItem }
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { AdminOrder, AdminOrderPreview } from "@medusajs/types"
|
||||
import { Button, Heading, Input, toast } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
StackedFocusModal,
|
||||
useStackedModal,
|
||||
} from "../../../../../components/modals"
|
||||
import { AddOrderEditItemsTable } from "../add-order-edit-items-table"
|
||||
import { OrderEditItem } from "./order-edit-item"
|
||||
import { useAddOrderEditItems } from "../../../../../hooks/api/order-edits"
|
||||
|
||||
type ExchangeInboundSectionProps = {
|
||||
order: AdminOrder
|
||||
preview: AdminOrderPreview
|
||||
}
|
||||
|
||||
let addedVariants: string[] = []
|
||||
|
||||
export const OrderEditItemsSection = ({
|
||||
order,
|
||||
preview,
|
||||
}: ExchangeInboundSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* STATE
|
||||
*/
|
||||
const { setIsOpen } = useStackedModal()
|
||||
const [filterTerm, setFilterTerm] = useState("")
|
||||
|
||||
/*
|
||||
* MUTATIONS
|
||||
*/
|
||||
const { mutateAsync: addItems, isPending } = useAddOrderEditItems(order.id)
|
||||
|
||||
/**
|
||||
* CALLBACKS
|
||||
*/
|
||||
const onItemsSelected = async () => {
|
||||
try {
|
||||
await addItems({
|
||||
items: addedVariants.map((i) => ({
|
||||
variant_id: i,
|
||||
quantity: 1,
|
||||
})),
|
||||
})
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
|
||||
setIsOpen("inbound-items", false)
|
||||
}
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
return preview.items.filter(
|
||||
(i) =>
|
||||
i.title.toLowerCase().includes(filterTerm) ||
|
||||
i.product_title.toLowerCase().includes(filterTerm)
|
||||
)
|
||||
}, [preview, filterTerm])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 mt-8 flex items-center justify-between">
|
||||
<Heading level="h2">{t("fields.items")}</Heading>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={filterTerm}
|
||||
onChange={(e) => setFilterTerm(e.target.value)}
|
||||
placeholder={t("fields.search")}
|
||||
autoComplete="off"
|
||||
type="search"
|
||||
/>
|
||||
|
||||
<StackedFocusModal id="inbound-items">
|
||||
<StackedFocusModal.Trigger asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.addItems")}
|
||||
</Button>
|
||||
</StackedFocusModal.Trigger>
|
||||
|
||||
<StackedFocusModal.Content>
|
||||
<StackedFocusModal.Header />
|
||||
|
||||
<AddOrderEditItemsTable
|
||||
currencyCode={order.currency_code}
|
||||
onSelectionChange={(finalSelection) => {
|
||||
addedVariants = finalSelection
|
||||
}}
|
||||
/>
|
||||
|
||||
<StackedFocusModal.Footer>
|
||||
<div className="flex w-full items-center justify-end gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button type="button" variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button
|
||||
key="submit-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="small"
|
||||
role="button"
|
||||
disabled={isPending}
|
||||
onClick={async () => await onItemsSelected()}
|
||||
>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StackedFocusModal.Footer>
|
||||
</StackedFocusModal.Content>
|
||||
</StackedFocusModal>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredItems.map((item) => (
|
||||
<OrderEditItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
orderId={order.id}
|
||||
currencyCode={order.currency_code}
|
||||
/>
|
||||
))}
|
||||
|
||||
{filterTerm && !filteredItems.length && (
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(-45deg, rgb(212, 212, 216, 0.15), rgb(212, 212, 216,.15) 10px, transparent 10px, transparent 20px)",
|
||||
}}
|
||||
className="bg-ui-bg-field mt-4 block h-[56px] w-full rounded-lg border border-dashed"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const OrderEditCreateSchema = z.object({
|
||||
note: z.string().optional(),
|
||||
send_notification: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type CreateOrderEditSchemaType = z.infer<typeof OrderEditCreateSchema>
|
||||
@@ -0,0 +1 @@
|
||||
export { OrderEditCreate as Component } from "./order-edit-create.tsx"
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { toast } from "@medusajs/ui"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate, useParams } from "react-router-dom"
|
||||
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import { useOrder, useOrderPreview } from "../../../hooks/api/orders"
|
||||
import { DEFAULT_FIELDS } from "../order-detail/constants"
|
||||
import { OrderEditCreateForm } from "./components/order-edit-create-form"
|
||||
import { useCreateOrderEdit } from "../../../hooks/api/order-edits"
|
||||
|
||||
let IS_REQUEST_RUNNING = false
|
||||
|
||||
export const OrderEditCreate = () => {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { order } = useOrder(id!, {
|
||||
fields: DEFAULT_FIELDS,
|
||||
})
|
||||
|
||||
const { order: preview } = useOrderPreview(id!)
|
||||
const { mutateAsync: createOrderEdit } = useCreateOrderEdit(order.id)
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
if (IS_REQUEST_RUNNING || !preview) {
|
||||
return
|
||||
}
|
||||
|
||||
if (preview.order_change) {
|
||||
if (preview.order_change.change_type !== "edit") {
|
||||
navigate(`/orders/${preview.id}`, { replace: true })
|
||||
toast.error(t("orders.edits.activeChangeError"))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
IS_REQUEST_RUNNING = true
|
||||
|
||||
try {
|
||||
const { order } = await createOrderEdit({
|
||||
order_id: preview.id,
|
||||
})
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
navigate(`/orders/${preview.id}`, { replace: true })
|
||||
} finally {
|
||||
IS_REQUEST_RUNNING = false
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
}, [preview])
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{preview && order && (
|
||||
<OrderEditCreateForm order={order} preview={preview} />
|
||||
)}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -41,7 +41,7 @@ function ExchangeInboundItem({
|
||||
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl ">
|
||||
<div className="flex flex-col items-center gap-x-2 gap-y-2 border-b p-3 text-sm md:flex-row">
|
||||
<div className="flex flex-col items-center gap-x-2 gap-y-2 p-3 text-sm md:flex-row">
|
||||
<div className="flex flex-1 items-center gap-x-3">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ function ExchangeOutboundItem({
|
||||
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl ">
|
||||
<div className="flex flex-col items-center gap-x-2 gap-y-2 border-b p-3 text-sm md:flex-row">
|
||||
<div className="flex flex-col items-center gap-x-2 gap-y-2 p-3 text-sm md:flex-row">
|
||||
<div className="flex flex-1 items-center gap-x-3">
|
||||
<Thumbnail src={previewItem.thumbnail} />
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./order-active-edit-section"
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
import { Button, Container, Copy, Heading, toast } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ExclamationCircleSolid } from "@medusajs/icons"
|
||||
|
||||
import { useOrderPreview } from "../../../../../hooks/api"
|
||||
import {
|
||||
useCancelOrderEdit,
|
||||
useConfirmOrderEdit,
|
||||
} from "../../../../../hooks/api/order-edits"
|
||||
import { useMemo } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
|
||||
type OrderActiveEditSectionProps = {
|
||||
order: HttpTypes.AdminOrder
|
||||
quantity: number
|
||||
}
|
||||
|
||||
function EditItem({
|
||||
item,
|
||||
quantity,
|
||||
}: {
|
||||
item: HttpTypes.AdminOrderLineItem
|
||||
quantity: number
|
||||
}) {
|
||||
return (
|
||||
<div key={item.id} className="text-ui-fg-subtle items-center gap-x-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="w-fit min-w-[27px]">
|
||||
<span className="txt-small tabular-nums">{quantity}</span>x
|
||||
</div>
|
||||
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
|
||||
<span className="txt-small txt-subtile font-medium">{item.title}</span>
|
||||
|
||||
{item.variant_sku && " · "}
|
||||
|
||||
{item.variant_sku && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="txt-small">{item.variant_sku}</span>
|
||||
<Copy content={item.variant_sku} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const OrderActiveEditSection = ({
|
||||
order,
|
||||
}: OrderActiveEditSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { order: orderPreview } = useOrderPreview(order.id)
|
||||
|
||||
const { mutateAsync: cancelOrderEdit } = useCancelOrderEdit(order.id)
|
||||
const { mutateAsync: confirmOrderEdit } = useConfirmOrderEdit(order.id)
|
||||
|
||||
const [addedItems, removedItems] = useMemo(() => {
|
||||
const added = []
|
||||
const removed = []
|
||||
|
||||
const orderLookupMap = new Map(order.items!.map((i) => [i.id, i]))
|
||||
|
||||
;(orderPreview?.items || []).forEach((currentItem) => {
|
||||
const originalItem = orderLookupMap.get(currentItem.id)
|
||||
|
||||
if (!originalItem) {
|
||||
added.push({ item: currentItem, quantity: currentItem.quantity })
|
||||
return
|
||||
}
|
||||
|
||||
if (originalItem.quantity > currentItem.quantity) {
|
||||
removed.push({
|
||||
item: currentItem,
|
||||
quantity: originalItem.quantity - currentItem.quantity,
|
||||
})
|
||||
}
|
||||
|
||||
if (originalItem.quantity < currentItem.quantity) {
|
||||
added.push({
|
||||
item: currentItem,
|
||||
quantity: currentItem.quantity - originalItem.quantity,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return [added, removed]
|
||||
}, [orderPreview])
|
||||
|
||||
const onConfirmOrderEdit = async () => {
|
||||
try {
|
||||
await confirmOrderEdit()
|
||||
|
||||
toast.success(t("orders.edits.toast.confirmedSuccessfully"))
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancelOrderEdit = async () => {
|
||||
try {
|
||||
await cancelOrderEdit()
|
||||
|
||||
toast.success(t("orders.edits.toast.canceledSuccessfully"))
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
if (!orderPreview || orderPreview.order_change?.change_type !== "edit") {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(-45deg, rgb(212, 212, 216, 0.15), rgb(212, 212, 216,.15) 10px, transparent 10px, transparent 20px)",
|
||||
}}
|
||||
className="-m-4 mb-1 border-b p-4"
|
||||
>
|
||||
<Container className="flex items-center justify-between p-0">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/*ADDED ITEMS*/}
|
||||
{!!addedItems.length && (
|
||||
<div className="txt-small text-ui-fg-subtle flex flex-row px-6 py-4">
|
||||
<span className="flex-1 font-medium">{t("labels.added")}</span>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-y-2">
|
||||
{addedItems.map(({ item, quantity }) => (
|
||||
<EditItem key={item.id} item={item} quantity={quantity} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*REMOVED ITEMS*/}
|
||||
{!!removedItems.length && (
|
||||
<div className="txt-small text-ui-fg-subtle flex flex-row px-6 py-4">
|
||||
<span className="flex-1 font-medium">{t("labels.removed")}</span>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-y-2">
|
||||
{removedItems.map(({ item, quantity }) => (
|
||||
<EditItem key={item.id} item={item} quantity={quantity} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={onCancelOrderEdit}
|
||||
>
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+15
-5
@@ -9,6 +9,7 @@ import {
|
||||
ArrowUturnLeft,
|
||||
DocumentText,
|
||||
ExclamationCircle,
|
||||
PencilSquare,
|
||||
} from "@medusajs/icons"
|
||||
import {
|
||||
AdminClaim,
|
||||
@@ -255,11 +256,20 @@ const Header = ({
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
// {
|
||||
// label: t("orders.summary.editItems"),
|
||||
// to: `/orders/${order.id}/edit`,
|
||||
// icon: <PencilSquare />,
|
||||
// },
|
||||
{
|
||||
label: t("orders.summary.editOrder"),
|
||||
to: `/orders/${order.id}/edits`,
|
||||
icon: <PencilSquare />,
|
||||
disabled:
|
||||
(orderPreview?.order_change &&
|
||||
orderPreview?.order_change?.change_type !== "edit") ||
|
||||
(orderPreview?.order_change?.change_type === "edit" &&
|
||||
orderPreview?.order_change?.status === "requested"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
// {
|
||||
// label: t("orders.summary.allocateItems"),
|
||||
// to: "#", // TODO: Open modal to allocate items
|
||||
|
||||
@@ -15,6 +15,7 @@ import after from "virtual:medusa/widgets/order/details/after"
|
||||
import before from "virtual:medusa/widgets/order/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/order/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/order/details/side/before"
|
||||
import { OrderActiveEditSection } from "./components/order-active-edit-section"
|
||||
|
||||
export const OrderDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<ReturnType<typeof orderLoader>>
|
||||
@@ -50,6 +51,7 @@ export const OrderDetail = () => {
|
||||
})}
|
||||
<div className="flex flex-col gap-x-4 lg:flex-row xl:items-start">
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
<OrderActiveEditSection order={order} />
|
||||
<OrderGeneralSection order={order} />
|
||||
<OrderSummarySection order={order} />
|
||||
<OrderPaymentSection order={order} />
|
||||
|
||||
@@ -13,7 +13,7 @@ import { throwIfOrderIsCancelled } from "../../utils/order-validation"
|
||||
/**
|
||||
* This step validates that an order-edit can be requested for an order.
|
||||
*/
|
||||
export const beginorderEditValidationStep = createStep(
|
||||
export const beginOrderEditValidationStep = createStep(
|
||||
"begin-order-edit-validation",
|
||||
async function ({ order }: { order: OrderDTO }) {
|
||||
throwIfOrderIsCancelled({ order })
|
||||
@@ -37,7 +37,7 @@ export const beginOrderEditOrderWorkflow = createWorkflow(
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
|
||||
beginorderEditValidationStep({ order })
|
||||
beginOrderEditValidationStep({ order })
|
||||
|
||||
const orderChangeInput = transform({ input }, ({ input }) => {
|
||||
return {
|
||||
|
||||
@@ -33,6 +33,7 @@ import { TaxRate } from "./tax-rate"
|
||||
import { TaxRegion } from "./tax-region"
|
||||
import { Upload } from "./upload"
|
||||
import { User } from "./user"
|
||||
import { OrderEdit } from "./order-edit"
|
||||
|
||||
export class Admin {
|
||||
public invite: Invite
|
||||
@@ -56,6 +57,7 @@ export class Admin {
|
||||
public inventoryItem: InventoryItem
|
||||
public notification: Notification
|
||||
public order: Order
|
||||
public orderEdit: OrderEdit
|
||||
public return: Return
|
||||
public claim: Claim
|
||||
public exchange: Exchange
|
||||
@@ -92,6 +94,7 @@ export class Admin {
|
||||
this.inventoryItem = new InventoryItem(client)
|
||||
this.notification = new Notification(client)
|
||||
this.order = new Order(client)
|
||||
this.orderEdit = new OrderEdit(client)
|
||||
this.return = new Return(client)
|
||||
this.claim = new Claim(client)
|
||||
this.taxRate = new TaxRate(client)
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Client } from "../client"
|
||||
import { ClientHeaders } from "../types"
|
||||
|
||||
export class OrderEdit {
|
||||
private client: Client
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async initiateRequest(
|
||||
body: HttpTypes.AdminInitiateOrderEditRequest,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminOrderEditPreviewResponse>(
|
||||
`/admin/order-edits`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async request(
|
||||
id: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminOrderEditPreviewResponse>(
|
||||
`/admin/order-edits/${id}/request`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async confirm(
|
||||
id: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminOrderEditPreviewResponse>(
|
||||
`/admin/order-edits/${id}/confirm`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async cancelRequest(
|
||||
id: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminOrderEditDeleteResponse>(
|
||||
`/admin/order-edits/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async addItems(
|
||||
id: string,
|
||||
body: HttpTypes.AdminAddOrderEditItems,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminOrderEditPreviewResponse>(
|
||||
`/admin/order-edits/${id}/items`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async updateOriginalItem(
|
||||
id: string,
|
||||
itemId: string,
|
||||
body: HttpTypes.AdminUpdateOrderEditItem,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminOrderEditPreviewResponse>(
|
||||
`/admin/order-edits/${id}/items/item/${itemId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async updateAddedItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
body: HttpTypes.AdminUpdateOrderEditItem,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminOrderEditPreviewResponse>(
|
||||
`/admin/order-edits/${id}/items/${actionId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async removeAddedItem(
|
||||
id: string,
|
||||
actionId: string,
|
||||
query?: HttpTypes.SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminOrderEditPreviewResponse>(
|
||||
`/admin/order-edits/${id}/items/${actionId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,11 @@ export class Order {
|
||||
)
|
||||
}
|
||||
|
||||
async retrievePreview(id: string, headers?: ClientHeaders) {
|
||||
async retrievePreview(
|
||||
id: string,
|
||||
query?: SelectParams,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
return await this.client.fetch<HttpTypes.AdminOrderPreviewResponse>(
|
||||
`/admin/orders/${id}/preview`,
|
||||
{
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./responses"
|
||||
export * from "./payloads"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export interface AdminInitiateOrderEditRequest {
|
||||
order_id: string
|
||||
description?: string
|
||||
internal_note?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminAddOrderEditItems {
|
||||
items: {
|
||||
variant_id: string
|
||||
quantity: number
|
||||
unit_price?: number
|
||||
internal_note?: string
|
||||
allow_backorder?: boolean
|
||||
metadata?: Record<string, unknown>
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface AdminUpdateOrderEditItem {
|
||||
quantity?: number
|
||||
internal_note?: string | null
|
||||
}
|
||||
@@ -7,3 +7,9 @@ export interface AdminOrderEditPreviewResponse {
|
||||
export interface AdminOrderEditResponse {
|
||||
order_change: OrderChangeDTO
|
||||
}
|
||||
|
||||
export interface AdminOrderEditDeleteResponse {
|
||||
id: string
|
||||
object: "order-edit"
|
||||
deleted: true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user