diff --git a/packages/admin-next/dashboard/src/components/common/button-menu/button-menu.tsx b/packages/admin-next/dashboard/src/components/common/button-menu/button-menu.tsx new file mode 100644 index 0000000000..54ba5a476e --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/button-menu/button-menu.tsx @@ -0,0 +1,96 @@ +import { DropdownMenu, clx } from "@medusajs/ui" + +import { PropsWithChildren, ReactNode } from "react" +import { Link } from "react-router-dom" + +type Action = { + icon: ReactNode + label: string + disabled?: boolean +} & ( + | { + to: string + onClick?: never + } + | { + onClick: () => void + to?: never + } +) + +type ActionGroup = { + actions: Action[] +} + +type ActionMenuProps = { + groups: ActionGroup[] +} + +export const ButtonMenu = ({ + groups, + children, +}: PropsWithChildren) => { + return ( + + {children} + + {groups.map((group, index) => { + if (!group.actions.length) { + return null + } + + const isLast = index === groups.length - 1 + + return ( + + {group.actions.map((action, index) => { + if (action.onClick) { + return ( + { + e.stopPropagation() + action.onClick() + }} + className={clx( + "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", + { + "[&_svg]:text-ui-fg-disabled": action.disabled, + } + )} + > + {action.icon} + {action.label} + + ) + } + + return ( +
+ + e.stopPropagation()}> + {action.icon} + {action.label} + + +
+ ) + })} + {!isLast && } +
+ ) + })} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/common/button-menu/index.ts b/packages/admin-next/dashboard/src/components/common/button-menu/index.ts new file mode 100644 index 0000000000..a3a827d617 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/button-menu/index.ts @@ -0,0 +1 @@ +export * from "./action-menu" diff --git a/packages/admin-next/dashboard/src/components/modals/route-modal-form/route-modal-form.tsx b/packages/admin-next/dashboard/src/components/modals/route-modal-form/route-modal-form.tsx index 8fbc01fa46..2135f6c696 100644 --- a/packages/admin-next/dashboard/src/components/modals/route-modal-form/route-modal-form.tsx +++ b/packages/admin-next/dashboard/src/components/modals/route-modal-form/route-modal-form.tsx @@ -8,12 +8,14 @@ import { Form } from "../../common/form" type RouteModalFormProps = PropsWithChildren<{ form: UseFormReturn blockSearch?: boolean + onClose?: (isSubmitSuccessful: boolean) => void }> export const RouteModalForm = ({ form, blockSearch = false, children, + onClose, }: RouteModalFormProps) => { const { t } = useTranslation() @@ -25,6 +27,7 @@ export const RouteModalForm = ({ const { isSubmitSuccessful } = nextLocation.state || {} if (isSubmitSuccessful) { + onClose?.(true) return false } @@ -32,10 +35,22 @@ export const RouteModalForm = ({ const isSearchChanged = currentLocation.search !== nextLocation.search if (blockSearch) { - return isDirty && (isPathChanged || isSearchChanged) + const ret = isDirty && (isPathChanged || isSearchChanged) + + if (!ret) { + onClose?.(isSubmitSuccessful) + } + + return ret } - return isDirty && isPathChanged + const ret = isDirty && isPathChanged + + if (!ret) { + onClose?.(isSubmitSuccessful) + } + + return ret }) const handleCancel = () => { @@ -44,6 +59,7 @@ export const RouteModalForm = ({ const handleContinue = () => { blocker?.proceed?.() + onClose?.(false) } return ( diff --git a/packages/admin-next/dashboard/src/hooks/api/returns.tsx b/packages/admin-next/dashboard/src/hooks/api/returns.tsx index b700209995..a0d39fc832 100644 --- a/packages/admin-next/dashboard/src/hooks/api/returns.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/returns.tsx @@ -53,6 +53,10 @@ export const useReturns = ( return { ...data, ...rest } } +/** + * REQUEST RETURN + */ + export const useInitiateReturn = ( orderId: string, options?: UseMutationOptions< @@ -81,6 +85,74 @@ export const useInitiateReturn = ( }) } +export const useConfirmReturnRequest = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminReturnResponse, + Error, + HttpTypes.AdminConfirmReturnRequest + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminConfirmReturnRequest) => + sdk.admin.return.confirmRequest(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.lists(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + + queryClient.invalidateQueries({ + queryKey: returnsQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: returnsQueryKeys.lists(), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useCancelReturnRequest = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: () => sdk.admin.return.cancelRequest(id), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.lists(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + refetchType: "all", + }) + + queryClient.invalidateQueries({ + queryKey: returnsQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: returnsQueryKeys.lists(), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + export const useAddReturnItem = ( id: string, orderId: string, @@ -235,18 +307,185 @@ export const useDeleteReturnShipping = ( }) } -export const useConfirmReturnRequest = ( +/** + * RECEIVE RETURN + */ + +export const useInitiateReceiveReturn = ( id: string, orderId: string, options?: UseMutationOptions< HttpTypes.AdminReturnResponse, Error, - HttpTypes.AdminConfirmReturnRequest + HttpTypes.AdminInitiateReceiveReturn > ) => { return useMutation({ - mutationFn: (payload: HttpTypes.AdminConfirmReturnRequest) => - sdk.admin.return.confirmRequest(id, payload), + mutationFn: (payload: HttpTypes.AdminInitiateReceiveReturn) => + sdk.admin.return.initiateReceive(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.lists(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useAddReceiveItems = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminReturnResponse, + Error, + HttpTypes.AdminReceiveItems + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminReceiveItems) => + sdk.admin.return.receiveItems(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateReceiveItem = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminReturnResponse, + Error, + HttpTypes.AdminUpdateReceiveItems & { actionId: string } + > +) => { + return useMutation({ + mutationFn: ({ + actionId, + ...payload + }: HttpTypes.AdminUpdateReceiveItems & { actionId: string }) => { + return sdk.admin.return.updateReceiveItem(id, actionId, payload) + }, + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useRemoveReceiveItems = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: (actionId: string) => { + return sdk.admin.return.removeReceiveItem(id, actionId) + }, + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useAddDismissItems = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminReturnResponse, + Error, + HttpTypes.AdminDismissItems + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminDismissItems) => + sdk.admin.return.dismissItems(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateDismissItem = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminReturnResponse, + Error, + HttpTypes.AdminUpdateDismissItems & { actionId: string } + > +) => { + return useMutation({ + mutationFn: ({ + actionId, + ...payload + }: HttpTypes.AdminUpdateReceiveItems & { actionId: string }) => { + return sdk.admin.return.updateDismissItem(id, actionId, payload) + }, + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useRemoveDismissItem = ( + id: string, + orderId: string, + options?: UseMutationOptions +) => { + return useMutation({ + mutationFn: (actionId: string) => { + return sdk.admin.return.removeDismissItem(id, actionId) + }, + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useConfirmReturnReceive = ( + id: string, + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminReturnResponse, + Error, + HttpTypes.AdminConfirmReceiveReturn + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminConfirmReceiveReturn) => + sdk.admin.return.confirmReceive(id, payload), onSuccess: (data: any, variables: any, context: any) => { queryClient.invalidateQueries({ queryKey: ordersQueryKeys.details(), @@ -271,13 +510,13 @@ export const useConfirmReturnRequest = ( }) } -export const useCancelReturnRequest = ( +export const useCancelReceiveReturn = ( id: string, orderId: string, options?: UseMutationOptions ) => { return useMutation({ - mutationFn: () => sdk.admin.return.cancelRequest(id), + mutationFn: () => sdk.admin.return.cancelReceive(id), onSuccess: (data: any, variables: any, context: any) => { queryClient.invalidateQueries({ queryKey: ordersQueryKeys.details(), @@ -288,6 +527,7 @@ export const useCancelReturnRequest = ( queryClient.invalidateQueries({ queryKey: ordersQueryKeys.preview(orderId), + refetchType: "all", // For some reason RQ will refetch this but will return stale record from the cache }) queryClient.invalidateQueries({ diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index 8fa322ca52..c5a71d05c0 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -849,6 +849,7 @@ "sendNotificationHint": "Notify customer about return.", "returnTotal": "Return total", "refundAmount": "Refund amount", + "outstandingAmount": "Outstanding amount", "reason": "Reason", "reasonHint": "Choose why the customer want to return items.", "note": "Note", @@ -861,7 +862,25 @@ "inboundShippingHint": "Choose which method you want to use.", "returnableQuantityLabel": "Returnable quantity", "refundableAmountLabel": "Refundable amount", - "returnRequestedInfo": "{{requestedItemsCount}}x item return requested" + "returnRequestedInfo": "{{requestedItemsCount}}x item return requested", + "returnReceivedInfo": "{{requestedItemsCount}}x item return received", + "activeChangeError": "There is an active order change in progress on this order. Please finish or discard the change first.", + "receive": { + "action": "Receive items", + "receive": "Return {{label}}", + "restockAll": "Restock all items", + "itemsLabel": "Items received", + "title": "Receive items for #{{returnId}}", + "sendNotificationHint": "Notify customer about received return.", + "inventoryWarning": "Please note that we will automatically adjust the inventory levels based on your input above.", + "writeOffInputLabel": "How many of the items are damaged?", + "toast": { + "success": "Return received successfully.", + "errorLargeValue": "Quantity is greater than requested item quantity.", + "errorNegativeValue": "Quantity cannot be a negative value.", + "errorLargeDamagedValue": "Damaged items quantity + non damaged received quantity exceeds total item quantity on the return. Please decrease quantity of non damaged items." + } + } }, "reservations": { "allocatedLabel": "Allocated", @@ -981,7 +1000,8 @@ "items_other": "{{count}} items" }, "return": { - "created": "Return created", + "created": "Return #{{returnId}} created", + "received": "Return #{{returnId}} received", "items_one": "{{count}} item returned", "items_other": "{{count}} items returned" }, diff --git a/packages/admin-next/dashboard/src/lib/rma.ts b/packages/admin-next/dashboard/src/lib/rma.ts index 9108fd73fa..890a491c1f 100644 --- a/packages/admin-next/dashboard/src/lib/rma.ts +++ b/packages/admin-next/dashboard/src/lib/rma.ts @@ -2,14 +2,16 @@ import { AdminOrderLineItem } from "@medusajs/types" export function getReturnableQuantity(item: AdminOrderLineItem): number { const { - // TODO: this should be `fulfilled_quantity`? now there is check on the BD that we can't return more quantity than we shipped but some items don't require shipping shipped_quantity, return_received_quantity, - return_dismissed_quantity, // TODO: check this + return_dismissed_quantity, return_requested_quantity, } = item.detail return ( - shipped_quantity - (return_received_quantity + return_requested_quantity) + shipped_quantity - + (return_received_quantity + + return_requested_quantity + + return_dismissed_quantity) ) } diff --git a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx index b2aa98f2b7..bc368605ad 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx @@ -223,6 +223,11 @@ export const RouteMap: RouteObject[] = [ lazy: () => import("../../routes/orders/order-create-fulfillment"), }, + { + path: "returns/:return_id/receive", + lazy: () => + import("../../routes/orders/order-receive-return"), + }, { path: "allocate-items", lazy: () => diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-return/components/return-create-form/return-create-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-return/components/return-create-form/return-create-form.tsx index e5cbef46e5..ee89fcd6ed 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-create-return/components/return-create-form/return-create-form.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-return/components/return-create-form/return-create-form.tsx @@ -52,8 +52,6 @@ type ReturnCreateFormProps = { let selectedItems: string[] = [] -let IS_CANCELING = false - export const ReturnCreateForm = ({ order, preview, @@ -62,6 +60,27 @@ export const ReturnCreateForm = ({ const { t } = useTranslation() const { handleSuccess } = useRouteModal() + const itemsMap = useMemo( + () => new Map(order.items.map((i) => [i.id, i])), + [order.items] + ) + + /** + * Only consider items that belong to this return. + */ + const previewItems = useMemo( + () => + preview.items.filter( + (i) => !!i.actions?.find((a) => a.return_id === activeReturn.id) + ), + [preview.items] + ) + + const previewItemsMap = useMemo( + () => new Map(previewItems.map((i) => [i.id, i])), + [previewItems] + ) + /** * STATE */ @@ -142,16 +161,14 @@ export const ReturnCreateForm = ({ ) return Promise.resolve({ - items: preview.items - .filter((i) => !!i.detail.return_requested_quantity) - .map((i) => ({ - item_id: i.id, - quantity: i.detail.return_requested_quantity, - note: i.actions?.find((a) => a.action === "RETURN_ITEM") - ?.internal_note, - reason_id: i.actions?.find((a) => a.action === "RETURN_ITEM") - ?.details?.reason_id, - })), + items: previewItems.map((i) => ({ + item_id: i.id, + quantity: i.detail.return_requested_quantity, + note: i.actions?.find((a) => a.action === "RETURN_ITEM") + ?.internal_note, + reason_id: i.actions?.find((a) => a.action === "RETURN_ITEM")?.details + ?.reason_id, + })), option_id: method ? method.shipping_option_id : "", location_id: "", send_notification: false, @@ -160,16 +177,6 @@ export const ReturnCreateForm = ({ resolver: zodResolver(ReturnCreateSchema), }) - const itemsMap = useMemo( - () => new Map(order.items.map((i) => [i.id, i])), - [order.items] - ) - - const previewItemsMap = useMemo( - () => new Map(preview.items.map((i) => [i.id, i])), - [preview.items] - ) - const { fields: items, append, @@ -183,7 +190,7 @@ export const ReturnCreateForm = ({ useEffect(() => { const existingItemsMap = {} - preview.items.forEach((i) => { + previewItems.forEach((i) => { const ind = items.findIndex((field) => field.item_id === i.id) /** @@ -218,7 +225,7 @@ export const ReturnCreateForm = ({ remove(ind) } }) - }, [preview.items]) + }, [previewItems]) useEffect(() => { const method = preview.shipping_methods.find( @@ -348,19 +355,6 @@ export const ReturnCreateForm = ({ }) }, [items]) - useEffect(() => { - /** - * Unmount hook - */ - return () => { - if (IS_CANCELING) { - cancelReturnRequest() - // TODO: add this on ESC press - IS_CANCELING = false - } - } - }, []) - const returnTotal = preview.return_requested_total const shippingTotal = useMemo(() => { @@ -374,7 +368,14 @@ export const ReturnCreateForm = ({ const refundAmount = returnTotal - shippingTotal return ( - + { + if (!isSubmitSuccessful) { + cancelReturnRequest() + } + }} + >
@@ -443,7 +444,7 @@ export const ReturnCreateForm = ({ currencyCode={order.currency_code} form={form} onRemove={() => { - const actionId = preview.items + const actionId = previewItems .find((i) => i.id === item.item_id) ?.actions?.find((a) => a.action === "RETURN_ITEM")?.id @@ -452,7 +453,7 @@ export const ReturnCreateForm = ({ } }} onUpdate={(payload) => { - const actionId = preview.items + const actionId = previewItems .find((i) => i.id === item.item_id) ?.actions?.find((a) => a.action === "RETURN_ITEM")?.id @@ -691,12 +692,7 @@ export const ReturnCreateForm = ({
- diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-return/return-create.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-return/return-create.tsx index c92e304324..4b20e8663a 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-create-return/return-create.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-return/return-create.tsx @@ -1,5 +1,8 @@ -import { useParams } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" + +import { toast } from "@medusajs/ui" import { RouteFocusModal } from "../../../components/modals" import { ReturnCreateForm } from "./components/return-create-form" @@ -13,11 +16,14 @@ let IS_REQUEST_RUNNING = false export const ReturnCreate = () => { const { id } = useParams() + const navigate = useNavigate() + const { t } = useTranslation() + const { order } = useOrder(id!, { fields: DEFAULT_FIELDS, }) - const { order: preview } = useOrderPreview(id!) + const { order: preview } = useOrderPreview(id!, undefined, {}) const [activeReturnId, setActiveReturnId] = useState() @@ -29,28 +35,36 @@ export const ReturnCreate = () => { useEffect(() => { async function run() { - if (IS_REQUEST_RUNNING || !order || !preview) { + if (IS_REQUEST_RUNNING || !preview) { return } - /** - * Active return already exists - */ - if (preview.order_change?.change_type === "return") { - setActiveReturnId(preview.order_change.return.id) + if (preview.order_change) { + if (preview.order_change.change_type === "return_request") { + setActiveReturnId(preview.order_change.return_id) + } else { + navigate(`/orders/${order.id}`, { replace: true }) + toast.error(t("orders.returns.activeChangeError")) + } + return } IS_REQUEST_RUNNING = true - const orderReturn = await initiateReturn({ order_id: order.id }) - setActiveReturnId(orderReturn.id) - - IS_REQUEST_RUNNING = false + try { + const orderReturn = await initiateReturn({ order_id: order.id }) + setActiveReturnId(orderReturn.id) + } catch (e) { + navigate(`/orders/${order.id}`, { replace: true }) + toast.error(e.message) + } finally { + IS_REQUEST_RUNNING = false + } } run() - }, [order, preview]) + }, [preview]) return ( diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx index 2959237204..c85b5b8789 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx @@ -81,7 +81,10 @@ type Activity = { const useActivityItems = (order: AdminOrder) => { const { t } = useTranslation() - const { returns = [] } = useReturns({ order_id: order.id, fields: "*items" }) + const { returns = [] } = useReturns({ + order_id: order.id, + fields: "+received_at,*items", + }) const notes = [] const isLoading = false @@ -169,11 +172,24 @@ const useActivityItems = (order: AdminOrder) => { } for (const ret of returns) { + // Always display created action items.push({ - title: t("orders.activity.events.return.created"), + title: t("orders.activity.events.return.created", { + returnId: ret.id.slice(-7), + }), timestamp: ret.created_at, - children: , + children: , }) + + if (ret.status === "received" || ret.status === "partially_received") { + items.push({ + title: t("orders.activity.events.return.received", { + returnId: ret.id.slice(-7), + }), + timestamp: ret.received_at, + children: , + }) + } } // for (const note of notes || []) { @@ -242,13 +258,19 @@ const OrderActivityItem = ({ {title} - - - {getRelativeDate(timestamp)} - - + {timestamp && ( + + + {getRelativeDate(timestamp)} + + + )}
{children}
@@ -392,7 +414,7 @@ const FulfillmentCreatedBody = ({ ) } -const ReturnCreatedBody = ({ orderReturn }: { orderReturn: AdminReturn }) => { +const ReturnBody = ({ orderReturn }: { orderReturn: AdminReturn }) => { const { t } = useTranslation() const numberOfItems = orderReturn.items.reduce((acc, item) => { diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx index 9fdcb0c277..6cbbbcc796 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx @@ -4,15 +4,21 @@ import { useMemo } from "react" import { AdminOrder, + AdminReturn, OrderLineItemDTO, ReservationItemDTO, } from "@medusajs/types" -import { ArrowDownRightMini, ArrowUturnLeft } from "@medusajs/icons" +import { + ArrowDownRightMini, + ArrowLongRight, + ArrowUturnLeft, +} from "@medusajs/icons" import { Button, Container, Copy, Heading, + IconButton, StatusBadge, Text, } from "@medusajs/ui" @@ -26,6 +32,7 @@ import { import { useReservationItems } from "../../../../../hooks/api/reservations" import { useReturns } from "../../../../../hooks/api/returns" import { useDate } from "../../../../../hooks/use-date" +import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx" type OrderSummarySectionProps = { order: AdminOrder @@ -39,6 +46,14 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { line_item_id: order.items.map((i) => i.id), }) + const { returns = [] } = useReturns({ + status: "requested", + order_id: order.id, + fields: "+received_at", + }) + + const showReturns = !!returns.length + /** * Show Allocation button only if there are unfulfilled items that don't have reservations */ @@ -71,20 +86,41 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
- - {showAllocateButton && ( -
- -
- )} + {showAllocateButton || + (showReturns && ( +
+ {showReturns && ( + ({ + label: t("orders.returns.receive.receive", { + label: `#${r.id.slice(-7)}`, + }), + icon: , + to: `/orders/${order.id}/returns/${r.id}/receive`, + })), + }, + ]} + > + + + )} + {showAllocateButton && ( + + )} +
+ ))} ) } @@ -126,73 +162,80 @@ const Item = ({ item, currencyCode, reservation, + returns, }: { item: OrderLineItemDTO currencyCode: string reservation?: ReservationItemDTO | null + returns: AdminReturn[] }) => { const { t } = useTranslation() const isInventoryManaged = item.variant.manage_inventory return ( -
-
- -
- - {item.title} - - {item.variant_sku && ( -
- {item.variant_sku} - -
- )} - - {item.variant?.options.map((o) => o.value).join(" · ")} - -
-
-
-
- - {getLocaleAmount(item.unit_price, currencyCode)} - -
-
-
+ <> +
+
+ +
+ + {item.title} + + {item.variant_sku && ( +
+ {item.variant_sku} + +
+ )} - {item.quantity}x + {item.variant?.options.map((o) => o.value).join(" · ")}
-
- {isInventoryManaged && ( - - {reservation - ? t("orders.reservations.allocatedLabel") - : t("orders.reservations.notAllocatedLabel")} - - )} +
+
+
+ + {getLocaleAmount(item.unit_price, currencyCode)} + +
+
+
+ + {item.quantity}x + +
+
+ {isInventoryManaged && ( + + {reservation + ? t("orders.reservations.allocatedLabel") + : t("orders.reservations.notAllocatedLabel")} + + )} +
+
+
+ + {getLocaleAmount(item.subtotal || 0, currencyCode)} +
-
- - {getLocaleAmount(item.subtotal || 0, currencyCode)} - -
-
+ {returns.map((r) => ( + + ))} + ) } @@ -201,6 +244,33 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => { line_item_id: order.items.map((i) => i.id), }) + const { returns } = useReturns({ + order_id: order.id, + fields: "*items", + }) + + const itemsReturnsMap = useMemo(() => { + if (!returns) { + return {} + } + + const ret = {} + + order.items?.forEach((i) => { + returns.forEach((r) => { + if (r.items.some((ri) => ri.item_id === i.id)) { + if (ret[i.id]) { + ret[i.id].push(r) + } else { + ret[i.id] = [r] + } + } + }) + }) + + return ret + }, [returns]) + return (
{order.items.map((item) => { @@ -214,6 +284,7 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => { item={item} currencyCode={order.currency_code} reservation={reservation} + returns={itemsReturnsMap[item.id] || []} /> ) })} @@ -275,39 +346,56 @@ const CostBreakdown = ({ order }: { order: AdminOrder }) => { ) } -const ReturnBreakdown = ({ order }: { order: AdminOrder }) => { +const ReturnBreakdown = ({ + orderReturn, + itemId, +}: { + orderReturn: AdminReturn + itemId: string +}) => { const { t } = useTranslation() const { getRelativeDate } = useDate() - const { returns = [] } = useReturns({ - order_id: order.id, - status: "requested", - fields: "*items", - }) - - if (!returns.length) { + if ( + !["requested", "received", "partially_received"].includes( + orderReturn.status + ) + ) { return null } - return returns.map((activeReturn) => ( + const isRequested = orderReturn.status === "requested" + + return (
- {t("orders.returns.returnRequestedInfo", { - requestedItemsCount: activeReturn.items.length, - })} + {t( + `orders.returns.${ + isRequested ? "returnRequestedInfo" : "returnReceivedInfo" + }`, + { + requestedItemsCount: orderReturn.items.find( + (ri) => ri.item_id === itemId + )[isRequested ? "quantity" : "received_quantity"], + } + )}
- - {getRelativeDate(activeReturn.created_at)} - + {isRequested && ( + + {getRelativeDate( + isRequested ? orderReturn.created_at : orderReturn.received_at + )} + + )}
- )) + ) } const Total = ({ order }: { order: AdminOrder }) => { diff --git a/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/constants.ts b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/constants.ts new file mode 100644 index 0000000000..0619423763 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/constants.ts @@ -0,0 +1,12 @@ +import { z } from "zod" + +export const ReceiveReturnSchema = z.object({ + items: z.array( + z.object({ + quantity: z.number().nullable(), + written_off_quantity: z.number().nullable(), + item_id: z.string(), + }) + ), + send_notification: z.boolean().optional(), +}) diff --git a/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/index.ts new file mode 100644 index 0000000000..93d1d658b3 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/index.ts @@ -0,0 +1 @@ +export * from "./order-receive-return-form.tsx" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/order-receive-return-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/order-receive-return-form.tsx new file mode 100644 index 0000000000..de2bf8e937 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/order-receive-return-form.tsx @@ -0,0 +1,379 @@ +import { useTranslation } from "react-i18next" +import { AdminOrder, AdminReturn } from "@medusajs/types" +import { Alert, Button, Input, Switch, Text, toast } from "@medusajs/ui" +import React, { useEffect, useMemo } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { ArrrowRight } from "@medusajs/icons" +import * as zod from "zod" + +import { Thumbnail } from "../../../../../components/common/thumbnail" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { useStockLocation } from "../../../../../hooks/api" +import { ReceiveReturnSchema } from "./constants" +import { Form } from "../../../../../components/common/form" +import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" +import { + useAddReceiveItems, + useCancelReceiveReturn, + useConfirmReturnReceive, + useRemoveReceiveItems, + useUpdateReceiveItem, +} from "../../../../../hooks/api/returns" +import WrittenOffQuantity from "./written-off-quantity" + +type OrderAllocateItemsFormProps = { + order: AdminOrder + preview: AdminOrder + orderReturn: AdminReturn +} + +export function OrderReceiveReturnForm({ + order, + preview, + orderReturn, +}: OrderAllocateItemsFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + /** + * Items on the preview order that are part of the return we are receiving currently. + */ + const previewItems = useMemo(() => { + const idsMap = {} + + orderReturn.items.forEach((i) => (idsMap[i.item_id] = true)) + + return preview.items.filter((i) => idsMap[i.id]) + }, [preview.items, orderReturn]) + + const { mutateAsync: confirmReturnReceive } = useConfirmReturnReceive( + orderReturn.id, + order.id + ) + + const { mutateAsync: cancelReceiveReturn } = useCancelReceiveReturn( + orderReturn.id, + order.id + ) + + const { mutateAsync: addReceiveItems } = useAddReceiveItems( + orderReturn.id, + order.id + ) + const { mutateAsync: updateReceiveItem } = useUpdateReceiveItem( + orderReturn.id, + order.id + ) + const { mutateAsync: removeReceiveItem } = useRemoveReceiveItems( + orderReturn.id, + order.id + ) + + const { stock_location } = useStockLocation( + orderReturn.location_id, + undefined, + { + enabled: !!orderReturn.location_id, + } + ) + + const itemsMap = useMemo(() => { + const ret = {} + order.items.forEach((i) => (ret[i.id] = i)) + return ret + }, [order.items]) + + const form = useForm>({ + defaultValues: { + items: previewItems + ?.sort((i1, i2) => i1.id.localeCompare(i2.id)) + .map((i) => ({ + item_id: i.id, + quantity: i.detail.return_received_quantity, + written_off_quantity: i.detail.written_off_quantity, + })), + send_notification: false, + }, + resolver: zodResolver(ReceiveReturnSchema), + }) + + useEffect(() => { + previewItems + ?.sort((i1, i2) => i1.id.localeCompare(i2.id)) + .forEach((item, index) => { + form.setValue( + `items.${index}.quantity`, + item.detail.return_received_quantity, + { shouldTouch: true, shouldDirty: true } + ) + form.setValue( + `items.${index}.written_off_quantity`, + item.detail.written_off_quantity, + { shouldTouch: true, shouldDirty: true } + ) + }) + }, [previewItems]) + + /** + * HANDLERS + */ + + const handleSubmit = form.handleSubmit(async (data) => { + try { + await confirmReturnReceive({ no_notification: !data.send_notification }) + + handleSuccess(`/orders/${order.id}`) + + toast.success(t("general.success"), { + description: t("orders.returns.receive.toast.success"), + dismissLabel: t("actions.close"), + }) + } catch (e) { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("actions.close"), + }) + } + }) + + const handleQuantityChange = async ( + itemId: string, + value: number | null, + index: number + ) => { + const item = previewItems?.find((i) => i.id === itemId) + const action = item?.actions?.find( + (a) => a.action === "RECEIVE_RETURN_ITEM" + ) + + if (typeof value === "number" && value < 0) { + form.setValue( + `items.${index}.quantity`, + item.detail.return_received_quantity, + { shouldTouch: true, shouldDirty: true } + ) + + toast.error(t("orders.returns.receive.toast.errorNegativeValue")) + + return + } + + if (typeof value === "number" && value > item.quantity) { + // reset value in the form and notify the user to be aware that we didn't chang anything + + form.setValue( + `items.${index}.quantity`, + item.detail.return_received_quantity, + { shouldTouch: true, shouldDirty: true } + ) + + toast.error(t("orders.returns.receive.toast.errorLargeValue")) + + return + } + + try { + if (action) { + if (value === null || value === 0) { + await removeReceiveItem(action.id) + + return + } + + await updateReceiveItem({ actionId: action.id, quantity: value }) + } else { + if (typeof value === "number" && value > 0 && value <= item.quantity) { + await addReceiveItems({ items: [{ id: item.id, quantity: value }] }) + } + } + } catch (e) { + toast.error(e.message) + } + } + + const onFormClose = async (isSubmitSuccessful: boolean) => { + try { + if (!isSubmitSuccessful) { + await cancelReceiveReturn() + } + } catch (e) { + toast.error(e.message) + } + } + + return ( + + + +
+
+ {stock_location && ( +
+ {" "} + + {stock_location.name} + +
+ )} +
+ + {t("orders.returns.receive.itemsLabel")} + +
+ {previewItems.map((item, ind) => { + const originalItem = itemsMap[item.id] + + return ( +
+
+
+ + {item.quantity}x + + + +
+
+ + {item.title}{" "} + + {originalItem.variant.sku && ( + ({originalItem.variant.sku}) + )} +
+ + {originalItem.variant.product.title} + +
+
+ +
+ + { + return ( + + + { + const value = + e.target.value === "" + ? null + : parseFloat(e.target.value) + + onChange(value) + }} + {...field} + onBlur={() => { + field.onBlur() + handleQuantityChange(item.id, value, ind) + }} + /> + + + ) + }} + /> +
+
+
+ ) + })} + + {/*TOTALS*/} + +
+
+ + {t("fields.total")} + + + {getStylizedAmount(preview.total, order.currency_code)} + +
+ +
+ + {t("orders.returns.outstandingAmount")} + + + {getStylizedAmount( + preview.summary.difference_sum || 0, + order.currency_code + )} + +
+
+ + + {t("orders.returns.receive.inventoryWarning")} + + +
+ { + return ( + +
+ + + +
+ + {t("orders.returns.sendNotification")} + + + {t("orders.returns.receive.sendNotificationHint")} + +
+
+ +
+ ) + }} + /> +
+
+ +
+ + + + +
+
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/written-off-quantity.tsx b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/written-off-quantity.tsx new file mode 100644 index 0000000000..e9364e6c78 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/components/order-receive-return-form/written-off-quantity.tsx @@ -0,0 +1,159 @@ +import React, { useState } from "react" +import { HeartBroken } from "@medusajs/icons" +import { UseFormReturn } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { AdminOrderLineItem } from "@medusajs/types" +import { Button, Input, Popover, toast } from "@medusajs/ui" + +import { ReceiveReturnSchema } from "./constants" +import { Form } from "../../../../../components/common/form" +import { + useAddDismissItems, + useRemoveDismissItem, + useUpdateDismissItem, +} from "../../../../../hooks/api/returns" + +type WriteOffQuantityProps = { + returnId: string + orderId: string + index: number + item: AdminOrderLineItem + form: UseFormReturn +} + +function WrittenOffQuantity({ + form, + item, + index, + returnId, + orderId, +}: WriteOffQuantityProps) { + const { t } = useTranslation() + const [isOpen, setIsOpen] = useState(false) + + const { mutateAsync: addDismissedItems } = useAddDismissItems( + returnId, + orderId + ) + + const { mutateAsync: updateDismissedItems } = useUpdateDismissItem( + returnId, + orderId + ) + + const { mutateAsync: removeDismissedItems } = useRemoveDismissItem( + returnId, + orderId + ) + + const onDismissedQuantityChanged = async (value: number | null) => { + // TODO: if out of bounds prevent sending and notify user + + const action = item.actions?.find( + (a) => a.action === "RECEIVE_DAMAGED_RETURN_ITEM" + ) + + if (typeof value === "number" && value < 0) { + form.setValue( + `items.${index}.written_off_quantity`, + item.detail.written_off_quantity, + { shouldTouch: true, shouldDirty: true } + ) + + toast.error(t("orders.returns.receive.toast.errorNegativeValue")) + + return + } + + if ( + typeof value === "number" && + value > item.quantity - item.detail.return_received_quantity + ) { + form.setValue( + `items.${index}.written_off_quantity`, + item.detail.written_off_quantity, + { shouldTouch: true, shouldDirty: true } + ) + + toast.error(t("orders.returns.receive.toast.errorLargeDamagedValue")) + + return + } + + try { + if (value) { + if (!action) { + await addDismissedItems({ + items: [{ id: item.id, quantity: value }], + }) + } else { + await updateDismissedItems({ actionId: action.id, quantity: value }) + } + } else { + if (action) { + // remove damaged item if value is unset and it was added before + await removeDismissedItems(action.id) + } + } + } catch (e) { + toast.error(e.message) + } + } + + return ( + + + + + +
+ + {t("orders.returns.receive.writeOffInputLabel")} + + { + return ( + + + { + const value = + e.target.value === "" + ? null + : parseFloat(e.target.value) + + onChange(value) + }} + {...field} + onBlur={() => { + field.onBlur() + onDismissedQuantityChanged(value) + }} + /> + + + ) + }} + /> +
+
+
+ ) +} + +export default WrittenOffQuantity diff --git a/packages/admin-next/dashboard/src/routes/orders/order-receive-return/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/index.ts new file mode 100644 index 0000000000..0b8ea2aff8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/index.ts @@ -0,0 +1 @@ +export { OrderReceiveReturn as Component } from "./order-receive-return" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-receive-return/order-receive-return.tsx b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/order-receive-return.tsx new file mode 100644 index 0000000000..8c7421dbbf --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-receive-return/order-receive-return.tsx @@ -0,0 +1,96 @@ +import { useNavigate, useParams } from "react-router-dom" +import { useTranslation } from "react-i18next" +import { Heading, toast } from "@medusajs/ui" +import { useEffect } from "react" + +import { useOrder, useOrderPreview } from "../../../hooks/api/orders" +import { RouteDrawer } from "../../../components/modals" +import { OrderReceiveReturnForm } from "./components/order-receive-return-form" +import { + useAddReceiveItems, + useInitiateReceiveReturn, + useReturn, +} from "../../../hooks/api/returns" + +let IS_REQUEST_RUNNING = false + +export function OrderReceiveReturn() { + const { id, return_id } = useParams() + const { t } = useTranslation() + const navigate = useNavigate() + + /** + * HOOKS + */ + + const { order } = useOrder(id!, { fields: "+currency_code,*items" }) + const { order: preview } = useOrderPreview(id!) + const { return: orderReturn } = useReturn(return_id, { + fields: "*items.item,*items.item.variant,*items.item.variant.product", + }) // TODO: fix API needs to return 404 if return not exists and not an empty object + + /** + * MUTATIONS + */ + + const { mutateAsync: initiateReceiveReturn } = useInitiateReceiveReturn( + return_id, + id + ) + + const { mutateAsync: addReceiveItems } = useAddReceiveItems(return_id, id) + + useEffect(() => { + ;(async function () { + if (IS_REQUEST_RUNNING || !preview) { + return + } + + if (preview.order_change) { + if (preview.order_change.change_type !== "return_receive") { + navigate(`/orders/${order.id}`, { replace: true }) + toast.error(t("orders.returns.activeChangeError")) + } + return + } + + IS_REQUEST_RUNNING = true + + try { + const { return: _return } = await initiateReceiveReturn({}) + + await addReceiveItems({ + items: _return.items.map((i) => ({ + id: i.item_id, + quantity: i.quantity, + })), + }) + } catch (e) { + toast.error(e.message) + } finally { + IS_REQUEST_RUNNING = false + } + })() + }, [preview]) + + const ready = order && orderReturn && preview + + return ( + + + + {t("orders.returns.receive.title", { + returnId: return_id?.slice(-7), + })} + + + {ready && ( + + )} + + ) +} diff --git a/packages/core/js-sdk/src/admin/return.ts b/packages/core/js-sdk/src/admin/return.ts index 11e876e39c..c2a39a3695 100644 --- a/packages/core/js-sdk/src/admin/return.ts +++ b/packages/core/js-sdk/src/admin/return.ts @@ -162,6 +162,23 @@ export class Return { ) } + async updateRequest( + id: string, + body: HttpTypes.AdminUpdateReturnRequest, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/returns/${id}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + async confirmRequest( id: string, body: HttpTypes.AdminConfirmReturnRequest, @@ -179,14 +196,14 @@ export class Return { ) } - async updateRequest( + async initiateReceive( id: string, - body: HttpTypes.AdminUpdateReturnRequest, + body: HttpTypes.AdminInitiateReceiveReturn, query?: HttpTypes.SelectParams, headers?: ClientHeaders ) { return await this.client.fetch( - `/admin/returns/${id}`, + `/admin/returns/${id}/receive`, { method: "POST", headers, @@ -195,4 +212,138 @@ export class Return { } ) } + + async receiveItems( + id: string, + body: HttpTypes.AdminReceiveItems, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/returns/${id}/receive-items`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async updateReceiveItem( + id: string, + actionId: string, + body: HttpTypes.AdminUpdateReceiveItems, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/returns/${id}/receive-items/${actionId}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async removeReceiveItem( + id: string, + actionId: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/returns/${id}/receive-items/${actionId}`, + { + method: "DELETE", + headers, + query, + } + ) + } + + async dismissItems( + id: string, + body: HttpTypes.AdminDismissItems, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/returns/${id}/dismiss-items`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async updateDismissItem( + id: string, + actionId: string, + body: HttpTypes.AdminUpdateDismissItems, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/returns/${id}/dismiss-items/${actionId}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async removeDismissItem( + id: string, + actionId: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/returns/${id}/dismiss-items/${actionId}`, + { + method: "DELETE", + headers, + query, + } + ) + } + + async confirmReceive( + id: string, + body: HttpTypes.AdminConfirmReceiveReturn, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/returns/${id}/receive/confirm`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async cancelReceive( + id: string, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/returns/${id}/receive`, + { + method: "DELETE", + headers, + query, + } + ) + } } diff --git a/packages/core/types/src/http/return/admin.ts b/packages/core/types/src/http/return/admin.ts index 61e1c00f80..b7d89e36dd 100644 --- a/packages/core/types/src/http/return/admin.ts +++ b/packages/core/types/src/http/return/admin.ts @@ -84,6 +84,38 @@ export interface AdminUpdateReturnRequest { metadata?: Record | null } +export interface AdminConfirmReceiveReturn { + no_notification?: boolean +} + +export interface AdminInitiateReceiveReturn { + internal_note?: string + description?: string + metadata?: Record +} + +export interface AdminReceiveItems { + items: { id: string; quantity: number; internal_note?: string }[] +} + +export interface AdminDismissItems { + items: { id: string; quantity: number; internal_note?: string }[] +} + +export interface AdminUpdateReceiveItems { + quantity?: number + internal_note?: string + reason_id?: string + metadata?: Record +} + +export interface AdminUpdateDismissItems { + quantity?: number + internal_note?: string + reason_id?: string + metadata?: Record +} + export interface AdminReturnFilters extends FindParams { id?: string[] | string | OperatorMap order_id?: string[] | string | OperatorMap diff --git a/packages/design-system/icons/src/components/heart-broken.tsx b/packages/design-system/icons/src/components/heart-broken.tsx new file mode 100644 index 0000000000..6329533726 --- /dev/null +++ b/packages/design-system/icons/src/components/heart-broken.tsx @@ -0,0 +1,37 @@ +import * as React from "react" +import type { IconProps } from "../types" + +const HeartBroken = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + ) + } +) + +HeartBroken.displayName = "HeartBroken" +export default HeartBroken diff --git a/packages/design-system/icons/src/components/index.ts b/packages/design-system/icons/src/components/index.ts index 7f90cb6698..e7c8028f52 100644 --- a/packages/design-system/icons/src/components/index.ts +++ b/packages/design-system/icons/src/components/index.ts @@ -164,6 +164,7 @@ export { default as GridList } from "./grid-list" export { default as HandTruck } from "./hand-truck" export { default as Hashtag } from "./hashtag" export { default as Heart } from "./heart" +export { default as HeartBroken } from "./heart-broken" export { default as History } from "./history" export { default as InboxSolid } from "./inbox-solid" export { default as InformationCircle } from "./information-circle"