feat(dashboard,types): added inbound shipping placeholder + shipping summary details (#9150)

what:

- shipping summary

<img width="1018" alt="Screenshot 2024-09-16 at 18 05 37" src="https://github.com/user-attachments/assets/d7e3d73c-bf11-42e5-90fb-03069437e96c">

- No Shipping options placeholder

<img width="781" alt="Screenshot 2024-09-16 at 18 06 11" src="https://github.com/user-attachments/assets/05ed682a-97cd-4e66-9af1-f1e77d3d47cb">
This commit is contained in:
Riqwan Thamir
2024-09-16 20:50:41 +00:00
committed by GitHub
parent d6ff526820
commit aa0928d437
11 changed files with 202 additions and 29 deletions
@@ -20,6 +20,7 @@ import {
ComponentPropsWithoutRef, ComponentPropsWithoutRef,
ForwardedRef, ForwardedRef,
Fragment, Fragment,
ReactNode,
useCallback, useCallback,
useDeferredValue, useDeferredValue,
useImperativeHandle, useImperativeHandle,
@@ -50,6 +51,7 @@ interface ComboboxProps<T extends Value = Value>
fetchNextPage?: () => void fetchNextPage?: () => void
isFetchingNextPage?: boolean isFetchingNextPage?: boolean
onCreateOption?: (value: string) => void onCreateOption?: (value: string) => void
noResultsPlaceholder?: ReactNode
} }
const ComboboxImpl = <T extends Value = string>( const ComboboxImpl = <T extends Value = string>(
@@ -64,6 +66,7 @@ const ComboboxImpl = <T extends Value = string>(
fetchNextPage, fetchNextPage,
isFetchingNextPage, isFetchingNextPage,
onCreateOption, onCreateOption,
noResultsPlaceholder,
...inputProps ...inputProps
}: ComboboxProps<T>, }: ComboboxProps<T>,
ref: ForwardedRef<HTMLInputElement> ref: ForwardedRef<HTMLInputElement>
@@ -182,7 +185,7 @@ const ComboboxImpl = <T extends Value = string>(
setOpen(open) setOpen(open)
} }
const hasValue = selectedValues.length > 0 const hasValue = selectedValues?.length > 0
const showTag = hasValue && isArrayValue const showTag = hasValue && isArrayValue
const showSelected = showTag && !searchValue && !open const showSelected = showTag && !searchValue && !open
@@ -321,13 +324,20 @@ const ComboboxImpl = <T extends Value = string>(
<div className="bg-ui-bg-component size-full h-5 w-full animate-pulse rounded-[4px]" /> <div className="bg-ui-bg-component size-full h-5 w-full animate-pulse rounded-[4px]" />
</div> </div>
)} )}
{!results.length && ( {!results.length &&
<div className="flex items-center gap-x-2 rounded-[4px] px-2 py-1.5"> (noResultsPlaceholder && !searchValue?.length ? (
<Text size="small" leading="compact" className="text-ui-fg-subtle"> noResultsPlaceholder
{t("general.noResultsTitle")} ) : (
</Text> <div className="flex items-center gap-x-2 rounded-[4px] px-2 py-1.5">
</div> <Text
)} size="small"
leading="compact"
className="text-ui-fg-subtle"
>
{t("general.noResultsTitle")}
</Text>
</div>
))}
{!results.length && onCreateOption && ( {!results.length && onCreateOption && (
<Fragment> <Fragment>
<PrimitiveSeparator className="bg-ui-border-base -mx-1" /> <PrimitiveSeparator className="bg-ui-border-base -mx-1" />
@@ -835,6 +835,9 @@
}, },
"orders": { "orders": {
"domain": "Orders", "domain": "Orders",
"claim": "Claim",
"exchange": "Exchange",
"return": "Return",
"cancelWarning": "You are about to cancel the order {{id}}. This action cannot be undone.", "cancelWarning": "You are about to cancel the order {{id}}. This action cannot be undone.",
"onDateFromSalesChannel": "{{date}} from {{salesChannel}}", "onDateFromSalesChannel": "{{date}} from {{salesChannel}}",
"list": { "list": {
@@ -929,7 +932,7 @@
"noteHint": "You can type freely if you want to specify something.", "noteHint": "You can type freely if you want to specify something.",
"location": "Location", "location": "Location",
"locationHint": "Choose which location you want to return the items to.", "locationHint": "Choose which location you want to return the items to.",
"inboundShipping": "Inbound shipping", "inboundShipping": "Return shipping",
"inboundShippingHint": "Choose which method you want to use.", "inboundShippingHint": "Choose which method you want to use.",
"returnableQuantityLabel": "Returnable quantity", "returnableQuantityLabel": "Returnable quantity",
"refundableAmountLabel": "Refundable amount", "refundableAmountLabel": "Refundable amount",
@@ -944,6 +947,16 @@
"title": "Cancel Return", "title": "Cancel Return",
"description": "Are you sure you want to cancel the return request?" "description": "Are you sure you want to cancel the return request?"
}, },
"placeholders": {
"noReturnShippingOptions": {
"title": "No return shipping options found",
"hint": "No return shipping options were created for the location. You can create one at <LinkComponent>Location & Shipping</LinkComponent>."
},
"outboundShippingOptions": {
"title": "No outbound shipping options found",
"hint": "No outbound shipping options were created for the location. You can create one at <LinkComponent>Location & Shipping</LinkComponent>."
}
},
"receive": { "receive": {
"action": "Receive items", "action": "Receive items",
"receiveItems": "{{ returnType }} {{ id }}", "receiveItems": "{{ returnType }} {{ id }}",
@@ -2561,6 +2574,8 @@
"totalExclTax": "Total excl. tax", "totalExclTax": "Total excl. tax",
"subtotal": "Subtotal", "subtotal": "Subtotal",
"shipping": "Shipping", "shipping": "Shipping",
"outboundShipping": "Outbound Shipping",
"returnShipping": "Return Shipping",
"tax": "Tax", "tax": "Tax",
"created": "Created", "created": "Created",
"key": "Key", "key": "Key",
@@ -0,0 +1,48 @@
import { Trans, useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
export const ReturnShippingPlaceholder = () => {
const { t } = useTranslation()
return (
<div className="flex h-[120px] flex-col items-center justify-center gap-2 p-2 text-center">
<span className="txt-small text-ui-fg-subtle font-medium">
{t("orders.returns.placeholders.noReturnShippingOptions.title")}
</span>
<span className="txt-small text-ui-fg-muted">
<Trans
i18nKey="orders.returns.placeholders.noReturnShippingOptions.hint"
components={{
LinkComponent: (
<Link to={`/settings/locations`} className="text-blue-500" />
),
}}
/>
</span>
</div>
)
}
export const OutboundShippingPlaceholder = () => {
const { t } = useTranslation()
return (
<div className="flex h-[120px] flex-col items-center justify-center gap-2 p-2 text-center">
<span className="txt-small text-ui-fg-subtle font-medium">
{t("orders.returns.placeholders.outboundShippingOptions.title")}
</span>
<span className="txt-small text-ui-fg-muted">
<Trans
i18nKey="orders.returns.placeholders.outboundShippingOptions.hint"
components={{
LinkComponent: (
<Link to={`/settings/locations`} className="text-blue-500" />
),
}}
/>
</span>
</div>
)
}
@@ -52,6 +52,7 @@ import {
import { useUpdateReturn } from "../../../../../hooks/api/returns.tsx" import { useUpdateReturn } from "../../../../../hooks/api/returns.tsx"
import { sdk } from "../../../../../lib/client" import { sdk } from "../../../../../lib/client"
import { currencies } from "../../../../../lib/data/currencies" import { currencies } from "../../../../../lib/data/currencies"
import { ReturnShippingPlaceholder } from "../../../common/placeholders.tsx"
import { ClaimOutboundSection } from "./claim-outbound-section" import { ClaimOutboundSection } from "./claim-outbound-section"
import { ItemPlaceholder } from "./item-placeholder" import { ItemPlaceholder } from "./item-placeholder"
@@ -715,12 +716,15 @@ export const ClaimCreateForm = ({
{/*INBOUND SHIPPING*/} {/*INBOUND SHIPPING*/}
<div className="grid grid-cols-1 gap-2 md:grid-cols-2"> <div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<div> <div>
<Form.Label <Form.Label>
tooltip={t(
"orders.claims.tooltips.onlyReturnShippingOptions"
)}
>
{t("orders.returns.inboundShipping")} {t("orders.returns.inboundShipping")}
<Text
size="small"
leading="compact"
className="text-ui-fg-muted inline ml-1"
>
({t("fields.optional")})
</Text>
</Form.Label> </Form.Label>
<Form.Hint className="!mt-1"> <Form.Hint className="!mt-1">
@@ -748,6 +752,9 @@ export const ClaimCreateForm = ({
value: so.id, value: so.id,
}))} }))}
disabled={!locationId} disabled={!locationId}
noResultsPlaceholder={
<ReturnShippingPlaceholder />
}
/> />
</Form.Control> </Form.Control>
</Form.Item> </Form.Item>
@@ -26,6 +26,7 @@ import {
} from "../../../../../hooks/api/claims" } from "../../../../../hooks/api/claims"
import { useShippingOptions } from "../../../../../hooks/api/shipping-options" import { useShippingOptions } from "../../../../../hooks/api/shipping-options"
import { sdk } from "../../../../../lib/client" import { sdk } from "../../../../../lib/client"
import { OutboundShippingPlaceholder } from "../../../common/placeholders"
import { AddClaimOutboundItemsTable } from "../add-claim-outbound-items-table" import { AddClaimOutboundItemsTable } from "../add-claim-outbound-items-table"
import { ClaimOutboundItem } from "./claim-outbound-item" import { ClaimOutboundItem } from "./claim-outbound-item"
import { ItemPlaceholder } from "./item-placeholder" import { ItemPlaceholder } from "./item-placeholder"
@@ -405,6 +406,7 @@ export const ClaimOutboundSection = ({
value: so.id, value: so.id,
}))} }))}
disabled={!shipping_options.length} disabled={!shipping_options.length}
noResultsPlaceholder={<OutboundShippingPlaceholder />}
/> />
</Form.Control> </Form.Control>
</Form.Item> </Form.Item>
@@ -28,6 +28,7 @@ import {
} from "../../../../../hooks/api/exchanges" } from "../../../../../hooks/api/exchanges"
import { useUpdateReturn } from "../../../../../hooks/api/returns" import { useUpdateReturn } from "../../../../../hooks/api/returns"
import { sdk } from "../../../../../lib/client" import { sdk } from "../../../../../lib/client"
import { ReturnShippingPlaceholder } from "../../../common/placeholders"
import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder" import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder"
import { AddExchangeInboundItemsTable } from "../add-exchange-inbound-items-table" import { AddExchangeInboundItemsTable } from "../add-exchange-inbound-items-table"
import { ExchangeInboundItem } from "./exchange-inbound-item" import { ExchangeInboundItem } from "./exchange-inbound-item"
@@ -480,12 +481,15 @@ export const ExchangeInboundSection = ({
{/*INBOUND SHIPPING*/} {/*INBOUND SHIPPING*/}
<div className="grid grid-cols-1 gap-2 md:grid-cols-2"> <div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<div> <div>
<Form.Label <Form.Label>
tooltip={t(
"orders.exchanges.tooltips.onlyReturnShippingOptions"
)}
>
{t("orders.returns.inboundShipping")} {t("orders.returns.inboundShipping")}
<Text
size="small"
leading="compact"
className="text-ui-fg-muted inline ml-1"
>
({t("fields.optional")})
</Text>
</Form.Label> </Form.Label>
<Form.Hint className="!mt-1"> <Form.Hint className="!mt-1">
@@ -512,6 +516,7 @@ export const ExchangeInboundSection = ({
value: so.id, value: so.id,
}))} }))}
disabled={!locationId} disabled={!locationId}
noResultsPlaceholder={<ReturnShippingPlaceholder />}
/> />
</Form.Control> </Form.Control>
</Form.Item> </Form.Item>
@@ -25,6 +25,7 @@ import {
} from "../../../../../hooks/api/exchanges" } from "../../../../../hooks/api/exchanges"
import { useShippingOptions } from "../../../../../hooks/api/shipping-options" import { useShippingOptions } from "../../../../../hooks/api/shipping-options"
import { sdk } from "../../../../../lib/client" import { sdk } from "../../../../../lib/client"
import { OutboundShippingPlaceholder } from "../../../common/placeholders"
import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder" import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder"
import { AddExchangeOutboundItemsTable } from "../add-exchange-outbound-items-table" import { AddExchangeOutboundItemsTable } from "../add-exchange-outbound-items-table"
import { ExchangeOutboundItem } from "./exchange-outbound-item" import { ExchangeOutboundItem } from "./exchange-outbound-item"
@@ -402,6 +403,7 @@ export const ExchangeOutboundSection = ({
<Form.Item> <Form.Item>
<Form.Control> <Form.Control>
<Combobox <Combobox
noResultsPlaceholder={<OutboundShippingPlaceholder />}
value={value ?? undefined} value={value ?? undefined}
onChange={(val) => { onChange={(val) => {
onChange(val) onChange(val)
@@ -46,6 +46,7 @@ import { useStockLocations } from "../../../../../hooks/api/stock-locations"
import { sdk } from "../../../../../lib/client" import { sdk } from "../../../../../lib/client"
import { currencies } from "../../../../../lib/data/currencies" import { currencies } from "../../../../../lib/data/currencies"
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
import { ReturnShippingPlaceholder } from "../../../common/placeholders"
import { AddReturnItemsTable } from "../add-return-items-table" import { AddReturnItemsTable } from "../add-return-items-table"
import { ReturnItem } from "./return-item" import { ReturnItem } from "./return-item"
import { ReturnCreateSchema, ReturnCreateSchemaType } from "./schema" import { ReturnCreateSchema, ReturnCreateSchemaType } from "./schema"
@@ -549,7 +550,15 @@ export const ReturnCreateForm = ({
<div> <div>
<Form.Label> <Form.Label>
{t("orders.returns.inboundShipping")} {t("orders.returns.inboundShipping")}
<Text
size="small"
leading="compact"
className="text-ui-fg-muted inline ml-1"
>
({t("fields.optional")})
</Text>
</Form.Label> </Form.Label>
<Form.Hint className="!mt-1"> <Form.Hint className="!mt-1">
{t("orders.returns.inboundShippingHint")} {t("orders.returns.inboundShippingHint")}
</Form.Hint> </Form.Hint>
@@ -588,6 +597,9 @@ export const ReturnCreateForm = ({
value: so.id, value: so.id,
}))} }))}
disabled={!locationId} disabled={!locationId}
noResultsPlaceholder={
<ReturnShippingPlaceholder />
}
/> />
</Form.Control> </Form.Control>
</Form.Item> </Form.Item>
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react" import { ReactNode, useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
@@ -54,6 +54,7 @@ import { getTotalCaptured } from "../../../../../lib/payment"
import { getReturnableQuantity } from "../../../../../lib/rma" import { getReturnableQuantity } from "../../../../../lib/rma"
import { CopyPaymentLink } from "../copy-payment-link/copy-payment-link" import { CopyPaymentLink } from "../copy-payment-link/copy-payment-link"
import ReturnInfoPopover from "./return-info-popover" import ReturnInfoPopover from "./return-info-popover"
import ShippingInfoPopover from "./shipping-info-popover"
type OrderSummarySectionProps = { type OrderSummarySectionProps = {
order: AdminOrder order: AdminOrder
@@ -523,14 +524,16 @@ const Cost = ({
label, label,
value, value,
secondaryValue, secondaryValue,
tooltip,
}: { }: {
label: string label: string
value: string | number value: string | number
secondaryValue: string secondaryValue: string
tooltip?: ReactNode
}) => ( }) => (
<div className="grid grid-cols-3 items-center"> <div className="grid grid-cols-3 items-center">
<Text size="small" leading="compact"> <Text size="small" leading="compact">
{label} {label} {tooltip}
</Text> </Text>
<div className="text-right"> <div className="text-right">
<Text size="small" leading="compact"> <Text size="small" leading="compact">
@@ -568,14 +571,23 @@ const CostBreakdown = ({ order }: { order: AdminOrder }) => {
.sort((m1, m2) => .sort((m1, m2) =>
(m1.created_at as string).localeCompare(m2.created_at as string) (m1.created_at as string).localeCompare(m2.created_at as string)
) )
.map((sm, i) => ( .map((sm, i) => {
<Cost return (
key={sm.id} <div>
label={t("fields.shipping") + (i ? ` ${i + 1}` : "")} <Cost
secondaryValue={sm.name} key={sm.id}
value={getLocaleAmount(sm.total, order.currency_code)} label={
/> sm.detail.return_id
))} ? t("fields.returnShipping")
: t("fields.outboundShipping")
}
secondaryValue={sm.name}
value={getLocaleAmount(sm.total, order.currency_code)}
tooltip={<ShippingInfoPopover key={i} shippingMethod={sm} />}
/>
</div>
)
})}
</div> </div>
) )
} }
@@ -0,0 +1,48 @@
import { InformationCircleSolid } from "@medusajs/icons"
import { AdminOrderShippingMethod } from "@medusajs/types"
import { Badge, Tooltip } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
type ShippingInfoPopoverProps = {
shippingMethod: AdminOrderShippingMethod
}
function ShippingInfoPopover({ shippingMethod }: ShippingInfoPopoverProps) {
const { t } = useTranslation()
const shippingDetail = shippingMethod?.detail
if (!shippingDetail) {
return
}
let rmaType = t("orders.return")
let rmaId = shippingDetail.return_id
if (shippingDetail.claim_id) {
rmaType = t("orders.claim")
rmaId = shippingDetail.claim_id
}
if (shippingDetail.exchange_id) {
rmaType = t("orders.exchange")
rmaId = shippingDetail.exchange_id
}
if (!rmaId) {
return
}
return (
<Tooltip
content={
<Badge size="2xsmall" rounded="full">
{rmaType}: #{rmaId.slice(-7)}
</Badge>
}
>
<InformationCircleSolid className="inline-block text-ui-fg-muted ml-1" />
</Tooltip>
)
}
export default ShippingInfoPopover
@@ -105,6 +105,7 @@ export interface BaseOrderShippingMethod {
original_subtotal: BigNumberValue original_subtotal: BigNumberValue
original_tax_total: BigNumberValue original_tax_total: BigNumberValue
total: BigNumberValue total: BigNumberValue
detail?: BaseOrderShippingDetail
subtotal: BigNumberValue subtotal: BigNumberValue
tax_total: BigNumberValue tax_total: BigNumberValue
discount_total: BigNumberValue discount_total: BigNumberValue
@@ -176,6 +177,17 @@ export interface BaseOrderItemDetail {
updated_at: Date updated_at: Date
} }
export interface BaseOrderShippingDetail {
id: string
shipping_method_id: string
shipping_method: BaseOrderShippingMethod
claim_id: string
exchange_id: string
return_id: string
created_at: Date
updated_at: Date
}
export interface BaseOrderChange { export interface BaseOrderChange {
id: string id: string
order_id: string order_id: string