import { ReactNode, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" import { ArrowDownRightMini, ArrowLongRight, ArrowPath, ArrowUturnLeft, DocumentText, ExclamationCircle, PencilSquare, ReceiptPercent, TriangleDownMini, } from "@medusajs/icons" import { AdminClaim, AdminExchange, AdminOrder, AdminOrderLineItem, AdminOrderPreview, AdminPaymentCollection, AdminPlugin, AdminRegion, AdminReturn, AdminReturnItem, AdminReturnReason, } from "@medusajs/types" import { Badge, Button, clx, Container, Copy, Heading, StatusBadge, Text, toast, Tooltip, usePrompt, } from "@medusajs/ui" import { AdminReservation } from "@medusajs/types/src/http" import { ActionMenu } from "../../../../../components/common/action-menu" import DisplayId from "../../../../../components/common/display-id/display-id" import { Thumbnail } from "../../../../../components/common/thumbnail" import { useClaims } from "../../../../../hooks/api/claims" import { useExchanges } from "../../../../../hooks/api/exchanges" import { useOrderPreview } from "../../../../../hooks/api/orders" import { useMarkPaymentCollectionAsPaid } from "../../../../../hooks/api/payment-collections" import { useReservationItems } from "../../../../../hooks/api/reservations" import { useReturns } from "../../../../../hooks/api/returns" import { useDate } from "../../../../../hooks/use-date" import { getTotalCreditLines } from "../../../../../lib/credit-line" import { formatCurrency } from "../../../../../lib/format-currency" import { getReservationsLimitCount } from "../../../../../lib/orders" import { getLocaleAmount, getStylizedAmount, isAmountLessThenRoundingError, } from "../../../../../lib/money-amount-helpers" import { getTotalCaptured } from "../../../../../lib/payment" import { getLoyaltyPlugin } from "../../../../../lib/plugins" import { getReturnableQuantity } from "../../../../../lib/rma" import { CopyPaymentLink } from "../copy-payment-link/copy-payment-link" import ReturnInfoPopover from "./return-info-popover" import ShippingInfoPopover from "./shipping-info-popover" import { formatPercentage } from "../../../../../lib/percentage-helpers.ts" type OrderSummarySectionProps = { order: AdminOrder plugins: AdminPlugin[] } export const OrderSummarySection = ({ order, plugins, }: OrderSummarySectionProps) => { const { t } = useTranslation() const prompt = usePrompt() const { reservations } = useReservationItems( { line_item_id: order?.items?.map((i) => i.id), limit: getReservationsLimitCount(order), }, { enabled: Array.isArray(order?.items) } ) const { order: orderPreview } = useOrderPreview(order.id!) const { returns = [] } = useReturns({ status: "requested", order_id: order.id, fields: "+received_at", }) const receivableReturns = useMemo( () => returns.filter((r) => !r.canceled_at), [returns] ) const showReturns = !!receivableReturns.length /** * Show Allocation button only if there are unfulfilled items that don't have reservations */ const showAllocateButton = useMemo(() => { if (!reservations) { return false } const reservationsMap = new Map( reservations.map((r) => [r.line_item_id, r.id]) ) for (const item of order.items) { // Inventory is managed if (item.variant?.manage_inventory) { // There are items that are unfulfilled if (item.quantity - item.detail.fulfilled_quantity > 0) { // Reservation for this item doesn't exist if (!reservationsMap.has(item.id)) { return true } } } } return false }, [order.items, reservations]) const unpaidPaymentCollection = order.payment_collections.find( (pc) => pc.status === "not_paid" ) const { mutateAsync: markAsPaid } = useMarkPaymentCollectionAsPaid( order.id, unpaidPaymentCollection?.id! ) const pendingDifference = order.summary?.pending_difference || 0 const isAmountSignificant = !isAmountLessThenRoundingError( pendingDifference, order.currency_code ) const showPayment = unpaidPaymentCollection && pendingDifference > 0 && isAmountSignificant const showRefund = pendingDifference < 0 && isAmountSignificant const handleMarkAsPaid = async ( paymentCollection: AdminPaymentCollection ) => { const res = await prompt({ title: t("orders.payment.markAsPaid"), description: t("orders.payment.markAsPaidPayment", { amount: formatCurrency( paymentCollection.amount as number, order.currency_code ), }), confirmText: t("actions.confirm"), cancelText: t("actions.cancel"), variant: "confirmation", }) if (!res) { return } await markAsPaid( { order_id: order.id }, { onSuccess: () => { toast.success( t("orders.payment.markAsPaidPaymentSuccess", { amount: formatCurrency( paymentCollection.amount as number, order.currency_code ), }) ) }, onError: (error) => { toast.error(error.message) }, } ) } return (
{(showAllocateButton || showReturns || showPayment || showRefund) && (
{showReturns && (receivableReturns.length === 1 ? ( ) : ( { let id = r.id let returnType = "Return" if (r.exchange_id) { id = r.exchange_id returnType = "Exchange" } if (r.claim_id) { id = r.claim_id returnType = "Claim" } return { label: t("orders.returns.receive.receiveItems", { id: `#${id.slice(-7)}`, returnType, }), icon: , to: `/orders/${order.id}/returns/${r.id}/receive`, } }), }, ]} > ))} {showAllocateButton && ( )} {showPayment && ( )} {showPayment && ( )} {showRefund && ( )}
)} ) } const Header = ({ order, orderPreview, }: { order: AdminOrder orderPreview?: AdminOrderPreview }) => { const { t } = useTranslation() // is ture if there is no shipped items ATM const shouldDisableReturn = order.items.every( (i) => !(getReturnableQuantity(i) > 0) ) const isOrderEditActive = orderPreview?.order_change?.change_type === "edit" // State where creation of order edit was interrupted i.e. order edit is drafted but not confirmed const isOrderEditPending = orderPreview?.order_change?.change_type === "edit" && orderPreview?.order_change?.status === "pending" return (
{t("fields.summary")} , disabled: order.status === "canceled" || (orderPreview?.order_change && orderPreview?.order_change?.change_type !== "edit") || (orderPreview?.order_change?.change_type === "edit" && orderPreview?.order_change?.status === "requested"), }, ], }, { actions: [ { label: t("orders.returns.create"), to: `/orders/${order.id}/returns`, icon: , disabled: shouldDisableReturn || isOrderEditActive || !!orderPreview?.order_change?.exchange_id || !!orderPreview?.order_change?.claim_id, }, { label: orderPreview?.order_change?.id && orderPreview?.order_change?.exchange_id ? t("orders.exchanges.manage") : t("orders.exchanges.create"), to: `/orders/${order.id}/exchanges`, icon: , disabled: shouldDisableReturn || isOrderEditActive || (!!orderPreview?.order_change?.return_id && !orderPreview?.order_change?.exchange_id) || !!orderPreview?.order_change?.claim_id, }, { label: orderPreview?.order_change?.id && orderPreview?.order_change?.claim_id ? t("orders.claims.manage") : t("orders.claims.create"), to: `/orders/${order.id}/claims`, icon: , disabled: shouldDisableReturn || isOrderEditActive || (!!orderPreview?.order_change?.return_id && !orderPreview?.order_change?.claim_id) || !!orderPreview?.order_change?.exchange_id, }, ], }, ]} />
) } const Item = ({ item, currencyCode, reservation, returns, claims, exchanges, }: { item: AdminOrderLineItem currencyCode: string reservation?: AdminReservation returns: AdminReturn[] claims: AdminClaim[] exchanges: AdminExchange[] }) => { const { t } = useTranslation() const isInventoryManaged = item.variant?.manage_inventory const hasInventoryKit = isInventoryManaged && ((item.variant?.inventory_items?.length || 0) > 1 || item.variant?.inventory_items?.some( (i) => (i.required_quantity ?? 0) > 1 )) const hasUnfulfilledItems = item.quantity - item.detail.fulfilled_quantity > 0 const appliedPromoCodes = (item.adjustments || []).map((a) => a.code) return ( <>
{item.title} {item.variant_sku && (
{item.variant_sku}
)} {item.variant?.options?.map((o) => o.value).join(" · ")}
{appliedPromoCodes.length > 0 && ( {appliedPromoCodes.map((code) => (
{code}
))} } >
)}
{getLocaleAmount(item.unit_price, currencyCode)}
{item.quantity}x
{isInventoryManaged && hasUnfulfilledItems && ( {reservation ? t("orders.reservations.allocatedLabel") : t("orders.reservations.notAllocatedLabel")} )}
{getLocaleAmount(item.subtotal || 0, currencyCode)}
{hasInventoryKit && } {returns.map((r) => ( ))} {claims.map((claim) => ( ))} {exchanges.map((exchange) => ( ))} ) } type ExtendedReturn = Omit & { items: (AdminReturnItem & { reason?: AdminReturnReason })[] } const ItemBreakdown = ({ order, reservations, }: { order: AdminOrder reservations?: AdminReservation[] }) => { const { claims = [] } = useClaims({ order_id: order.id, fields: "*additional_items", }) const { exchanges = [] } = useExchanges({ order_id: order.id, fields: "*additional_items", }) const { returns = [] } = useReturns({ order_id: order.id, fields: "*items,*items.reason", }) as { returns: ExtendedReturn[] | undefined } const reservationsMap = useMemo( () => new Map((reservations || []).map((r) => [r.line_item_id, r])), [reservations] ) return (
{order.items?.map((item) => { const reservation = reservationsMap.get(item.id) return ( ) })}
) } const Cost = ({ label, value, secondaryValue, tooltip, }: { label: ReactNode value: string | number secondaryValue?: string tooltip?: ReactNode }) => (
{label} {tooltip}
{secondaryValue}
{value}
) const CostBreakdown = ({ order, }: { order: AdminOrder & { region?: AdminRegion | null } }) => { const { t } = useTranslation() const [isTaxOpen, setIsTaxOpen] = useState(false) const [isShippingOpen, setIsShippingOpen] = useState(false) const taxCodes = useMemo(() => { const taxCodeMap: { [key: string]: { total: number; rate: number } } = {} order.items.forEach((item) => { item.tax_lines?.forEach((line) => { const currTotal = line.subtotal || 0 const prevTotal = taxCodeMap[line.code]?.total || 0 taxCodeMap[line.code] = { total: prevTotal + currTotal, rate: line.rate, } }) }) order.shipping_methods.forEach((sm) => { sm.tax_lines?.forEach((line) => { const currTotal = line.subtotal || 0 const prevTotal = taxCodeMap[line.code]?.total || 0 taxCodeMap[line.code] = { total: prevTotal + currTotal, rate: line.rate, } }) }) return taxCodeMap }, [order]) const hasTaxes = !!Object.keys(taxCodes).length return (
setIsShippingOpen((o) => !o)} className="flex cursor-pointer items-center gap-1" > {t("orders.summary.shippingSubtotal")}
} value={getLocaleAmount(order.shipping_subtotal, order.currency_code)} /> {isShippingOpen && (
{(order.shipping_methods || []) .sort((m1, m2) => (m1.created_at as string).localeCompare(m2.created_at as string) ) .map((sm, i) => { return (
{sm.name} {sm.detail?.return_id && ` (${t("fields.returnShipping")})`}{" "}
{getLocaleAmount(sm.subtotal, order.currency_code)}
) })}
)} <>
hasTaxes && setIsTaxOpen((o) => !o)} className={clx("flex items-center gap-1", { "cursor-pointer": hasTaxes, })} > {t("orders.summary.taxTotal")} {hasTaxes && ( )}
{getLocaleAmount(order.original_tax_total, order.currency_code)}
{isTaxOpen && (
{Object.entries(taxCodes).map(([code, { total, rate }]) => { return (
{code} ({formatPercentage(rate)})
{getLocaleAmount(total, order.currency_code)}
) })}
)}
{t("fields.total")} {getLocaleAmount(order.original_total, order.currency_code)}
) } const DiscountAndTotalBreakdown = ({ order, plugins, }: { order: AdminOrder & { region?: AdminRegion | null } plugins: AdminPlugin[] }) => { const { t } = useTranslation() const [isDiscountOpen, setIsDiscountOpen] = useState(false) const [isCreditLinesOpen, setIsCreditLinesOpen] = useState(false) const creditLines = order.credit_lines ?? [] const loyaltyPlugin = getLoyaltyPlugin(plugins) const discounts = useMemo(() => { const discounts: { type: "item" | "shipping" total: number codes: string[] }[] = [] if (order.item_discount_total) { discounts.push({ type: "item", total: order.item_discount_total, codes: Array.from( new Set( order.items .flatMap((item) => item.adjustments || []) .map((adjustment) => adjustment.code!) ) ).sort(), }) } if (order.shipping_discount_total) { discounts.push({ type: "shipping", total: order.shipping_discount_total, codes: Array.from( new Set( order.shipping_methods .flatMap((shippingMethod) => shippingMethod.adjustments || []) .map((adjustment) => adjustment.code!) ) ).sort(), }) } return discounts }, [order]) const hasDiscount = discounts.length > 0 const hasCreditLines = creditLines.length > 0 return (
hasDiscount && setIsDiscountOpen((o) => !o)} className={clx("flex items-center gap-1", { "cursor-pointer": hasDiscount, })} > {t("orders.summary.discountTotal")} {hasDiscount && ( )}
} value={getLocaleAmount(order.discount_total, order.currency_code)} /> {isDiscountOpen && (
{discounts.map(({ type, total, codes }) => { return (
{t(`fields.${type}`)} ({codes.join(", ")})
{getLocaleAmount(total, order.currency_code)}
) })}
)} {hasCreditLines && ( <> setIsCreditLinesOpen((o) => !o)} className="flex cursor-pointer items-center gap-1" > {loyaltyPlugin ? t("orders.giftCardsStoreCreditLines") : t("orders.creditLines.title")}
} value={getLocaleAmount( order.credit_line_total, order.currency_code )} /> {isCreditLinesOpen && (
{creditLines.map((creditLine) => { const prettyReference = creditLine.reference ?.split("_") .join(" ") .split("-") .join(" ") return (
- ({prettyReference})
{getLocaleAmount( creditLine.amount as number, order.currency_code )}
) })}
)} )}
{t("orders.summary.totalAfterDiscount")} {getLocaleAmount(order.total, order.currency_code)}
) } const InventoryKitBreakdown = ({ item }: { item: AdminOrderLineItem }) => { const { t } = useTranslation() const [isOpen, setIsOpen] = useState(false) const inventory = item.variant?.inventory_items || [] return ( <>
setIsOpen((o) => !o)} className="flex cursor-pointer items-center gap-2 border-t border-dashed px-6 py-4" > {t("orders.summary.inventoryKit", { count: inventory.length })}
{isOpen && (
{inventory.map((i) => { return (
{i.inventory?.title} {i.inventory?.sku && ( {" "} ⋅ {i.inventory?.sku} )}
{i.required_quantity}x
) })}
)} ) } const ReturnBreakdownWithDamages = ({ orderReturn, itemId, }: { orderReturn: ExtendedReturn itemId: string }) => { const { t } = useTranslation() const item = orderReturn?.items?.find((ri) => ri.item_id === itemId) const damagedQuantity = item?.damaged_quantity || 0 return ( item && (
{t(`orders.returns.damagedItemsReturned`, { quantity: damagedQuantity, })} {item?.note && ( )} {item?.reason && ( {item?.reason?.label} )}
{t(`orders.returns.damagedItemReceived`)}
) ) } const ReturnBreakdown = ({ orderReturn, itemId, }: { orderReturn: ExtendedReturn itemId: string }) => { const { t } = useTranslation() const { getRelativeDate } = useDate() if ( !["requested", "received", "partially_received"].includes( orderReturn.status || "" ) ) { return null } const isRequested = orderReturn.status === "requested" const item = orderReturn?.items?.find((ri) => ri.item_id === itemId) const damagedQuantity = item?.damaged_quantity || 0 return ( item && ( <> {damagedQuantity > 0 && ( )}
{t( `orders.returns.${ isRequested ? "returnRequestedInfo" : "returnReceivedInfo" }`, { requestedItemsCount: item?.[isRequested ? "quantity" : "received_quantity"], } )} {item?.note && ( )} {item?.reason && ( {item?.reason?.label} )}
{orderReturn && isRequested && ( {getRelativeDate(orderReturn.created_at)} )} {orderReturn && !isRequested && ( {t(`orders.returns.itemReceived`)} )}
) ) } const ClaimBreakdown = ({ claim, itemId, }: { claim: AdminClaim itemId: string }) => { const { t } = useTranslation() const { getRelativeDate } = useDate() const items = claim.additional_items.filter( (item) => item.item?.id === itemId ) return ( !!items.length && (
{t(`orders.claims.outboundItemAdded`, { itemsCount: items.reduce( (acc, item) => (acc = acc + item.quantity), 0 ), })}
{getRelativeDate(claim.created_at)}
) ) } const ExchangeBreakdown = ({ exchange, itemId, }: { exchange: AdminExchange itemId: string }) => { const { t } = useTranslation() const { getRelativeDate } = useDate() const items = exchange.additional_items.filter( (item) => item?.item?.id === itemId ) return ( !!items.length && (
{t(`orders.exchanges.outboundItemAdded`, { itemsCount: items.reduce( (acc, item) => (acc = acc + item.quantity), 0 ), })}
{getRelativeDate(exchange.created_at)}
) ) } const Total = ({ order }: { order: AdminOrder }) => { const { t } = useTranslation() return (
{t("fields.paidTotal")} {getStylizedAmount( getTotalCaptured(order.payment_collections || []), order.currency_code )}
{getTotalCreditLines(order.credit_lines ?? []) > 0 && (
{t("fields.creditTotal")} {getStylizedAmount( getTotalCreditLines(order.credit_lines ?? []), order.currency_code )}
)}
{t("orders.returns.outstandingAmount")} {getStylizedAmount( order.summary.pending_difference || 0, order.currency_code )}
) }