feat(admin-ui): Add location names to fulfilment rows and timeline events (#3481)

Adds location information to fulfilment rows and timeline events

![image](https://user-images.githubusercontent.com/948623/225306827-ebd08517-41c5-426e-88f2-43192b337995.png)
![image](https://user-images.githubusercontent.com/948623/225306876-7f77cbb8-6583-4082-b141-21b84fc2c79e.png)

Resolves CORE-1234
This commit is contained in:
Rares Stefan authored and GitHub committed 2023-03-16 14:41:39 +00:00
1 parent 02c77d7059
commit 4213326fe8
8 files changed
+98 -19

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/admin-ui": patch
---
feat(admin-ui): Add location names to fulfilment rows and timeline events
@@ -25,6 +25,7 @@ export type EventContainerProps = {
isFirst?: boolean isFirst?: boolean
expandable?: boolean expandable?: boolean
children: ReactNode children: ReactNode
detail?: string | React.ReactNode
} }
const EventContainer: React.FC<EventContainerProps> = ({ const EventContainer: React.FC<EventContainerProps> = ({
@@ -38,6 +39,7 @@ const EventContainer: React.FC<EventContainerProps> = ({
isFirst = false, isFirst = false,
expandable = false, expandable = false,
children, children,
detail,
}) => { }) => {
const [isExpanded, setIsExpanded] = useState<boolean>(!expandable) const [isExpanded, setIsExpanded] = useState<boolean>(!expandable)
@@ -92,6 +94,7 @@ const EventContainer: React.FC<EventContainerProps> = ({
{children && isExpanded && ( {children && isExpanded && (
<div className="mt-small pb-base w-full">{children}</div> <div className="mt-small pb-base w-full">{children}</div>
)} )}
<div className="text-grey-50">{detail}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -9,30 +9,38 @@ type EventItemContainerProps = {
title: string title: string
} }
} }
detail?: string
} }
const EventItemContainer: React.FC<EventItemContainerProps> = ({ item }) => { const EventItemContainer: React.FC<EventItemContainerProps> = ({
item,
detail,
}) => {
if (!item) { if (!item) {
return null return null
} }
return ( return (
<div className="gap-x-small mb-base flex items-center last:mb-0"> <div className="mb-base flex flex-col last:mb-0 ">
{item.thumbnail && ( <div className="gap-x-small flex items-center">
<div className="rounded-base h-10 w-[30px] overflow-hidden"> {item.thumbnail && (
<img <div className="rounded-base h-10 w-[30px] overflow-hidden">
src={item.thumbnail} <img
alt={`Thumbnail for ${item.title}`} src={item.thumbnail}
className="h-full w-full object-cover" alt={`Thumbnail for ${item.title}`}
/> className="h-full w-full object-cover"
/>
</div>
)}
<div className="inter-small-regular flex w-full flex-col">
<div className="flex w-full items-center justify-between">
<p>{item.title}</p>
<span className="inter-small-semibold text-violet-60">{`x${item.quantity}`}</span>
</div>
<p className="text-grey-50">{item.variant.title}</p>
</div> </div>
)}
<div className="inter-small-regular flex w-full flex-col">
<div className="flex w-full items-center justify-between">
<p>{item.title}</p>
<span className="inter-small-semibold text-violet-60">{`x${item.quantity}`}</span>
</div>
<p className="text-grey-50">{item.variant.title}</p>
</div> </div>
{detail && <div className="text-grey-50">{detail}</div>}
</div> </div>
) )
} }
@@ -16,6 +16,10 @@ const ItemsFulfilled: React.FC<ItemsFulfilledProps> = ({ event }) => {
? "Exchange Items Fulfilled" ? "Exchange Items Fulfilled"
: "Items Fulfilled" : "Items Fulfilled"
const detail = event.locationName
? `Shipping from ${event.locationName}`
: undefined
const args = { const args = {
icon: <PackageIcon size={20} />, icon: <PackageIcon size={20} />,
time: event.time, time: event.time,
@@ -25,7 +29,9 @@ const ItemsFulfilled: React.FC<ItemsFulfilledProps> = ({ event }) => {
)), )),
noNotification: event.noNotification, noNotification: event.noNotification,
isFirst: event.first, isFirst: event.first,
detail,
} }
return <EventContainer {...args} /> return <EventContainer {...args} />
} }
@@ -16,6 +16,10 @@ const ItemsShipped: React.FC<ItemsShippedProps> = ({ event }) => {
? "Exchange Items Shipped" ? "Exchange Items Shipped"
: "Items Shipped" : "Items Shipped"
const detail = event.locationName
? `Shipped from ${event.locationName}`
: undefined
const args = { const args = {
icon: <TruckIcon size={20} />, icon: <TruckIcon size={20} />,
time: event.time, time: event.time,
@@ -25,6 +29,7 @@ const ItemsShipped: React.FC<ItemsShippedProps> = ({ event }) => {
)), )),
noNotification: event.noNotification, noNotification: event.noNotification,
isFirst: event.first, isFirst: event.first,
detail,
} }
return <EventContainer {...args} /> return <EventContainer {...args} />
} }
@@ -4,11 +4,14 @@ import {
useAdminCancelFulfillment, useAdminCancelFulfillment,
useAdminCancelSwapFulfillment, useAdminCancelSwapFulfillment,
} from "medusa-react" } from "medusa-react"
import IconBadge from "../../../../components/fundamentals/icon-badge"
import BuildingsIcon from "../../../../components/fundamentals/icons/buildings-icon"
import CancelIcon from "../../../../components/fundamentals/icons/cancel-icon" import CancelIcon from "../../../../components/fundamentals/icons/cancel-icon"
import PackageIcon from "../../../../components/fundamentals/icons/package-icon" import PackageIcon from "../../../../components/fundamentals/icons/package-icon"
import Actionables from "../../../../components/molecules/actionables" import Actionables from "../../../../components/molecules/actionables"
import useImperativeDialog from "../../../../hooks/use-imperative-dialog" import useImperativeDialog from "../../../../hooks/use-imperative-dialog"
import useNotification from "../../../../hooks/use-notification" import useNotification from "../../../../hooks/use-notification"
import useStockLocations from "../../../../hooks/use-stock-locations"
import { getErrorMessage } from "../../../../utils/error-messages" import { getErrorMessage } from "../../../../utils/error-messages"
import { TrackingLink } from "./tracking-link" import { TrackingLink } from "./tracking-link"
@@ -23,6 +26,7 @@ export const FormattedFulfillment = ({
const cancelFulfillment = useAdminCancelFulfillment(order.id) const cancelFulfillment = useAdminCancelFulfillment(order.id)
const cancelSwapFulfillment = useAdminCancelSwapFulfillment(order.id) const cancelSwapFulfillment = useAdminCancelSwapFulfillment(order.id)
const cancelClaimFulfillment = useAdminCancelClaimFulfillment(order.id) const cancelClaimFulfillment = useAdminCancelClaimFulfillment(order.id)
const { getLocationNameById } = useStockLocations()
const { fulfillment } = fulfillmentObj const { fulfillment } = fulfillmentObj
const hasLinks = !!fulfillment.tracking_links?.length const hasLinks = !!fulfillment.tracking_links?.length
@@ -89,7 +93,7 @@ export const FormattedFulfillment = ({
return ( return (
<div className="flex w-full justify-between"> <div className="flex w-full justify-between">
<div className="flex flex-col space-y-1 py-2"> <div className="flex flex-col space-y-1 py-4">
<div className="text-grey-90"> <div className="text-grey-90">
{fulfillment.canceled_at {fulfillment.canceled_at
? "Fulfillment has been canceled" ? "Fulfillment has been canceled"
@@ -104,6 +108,19 @@ export const FormattedFulfillment = ({
<TrackingLink key={j} trackingLink={tl} /> <TrackingLink key={j} trackingLink={tl} />
))} ))}
</div> </div>
{!fulfillment.canceled_at && fulfillment.location_id && (
<div className="flex flex-col">
<div className="text-grey-50 font-semibold">
{fulfillment.shipped_at ? "Shipped" : "Shipping"} from{" "}
</div>
<div className="flex items-center pt-2">
<IconBadge className="mr-2">
<BuildingsIcon />
</IconBadge>
{getLocationNameById(fulfillment.location_id)}
</div>
</div>
)}
</div> </div>
{!fulfillment.canceled_at && !fulfillment.shipped_at && ( {!fulfillment.canceled_at && !fulfillment.shipped_at && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@@ -15,6 +15,7 @@ import {
import { useMemo } from "react" import { useMemo } from "react"
import useOrdersExpandParam from "../domain/orders/details/utils/use-admin-expand-paramter" import useOrdersExpandParam from "../domain/orders/details/utils/use-admin-expand-paramter"
import { useFeatureFlag } from "../providers/feature-flag-provider" import { useFeatureFlag } from "../providers/feature-flag-provider"
import useStockLocations from "./use-stock-locations"
export interface TimelineEvent { export interface TimelineEvent {
id: string id: string
@@ -92,10 +93,12 @@ interface FulfillmentEvent extends TimelineEvent {
export interface ItemsFulfilledEvent extends FulfillmentEvent { export interface ItemsFulfilledEvent extends FulfillmentEvent {
items: OrderItem[] items: OrderItem[]
locationName?: string
} }
export interface ItemsShippedEvent extends FulfillmentEvent { export interface ItemsShippedEvent extends FulfillmentEvent {
items: OrderItem[] items: OrderItem[]
locationName?: string
} }
export interface RefundEvent extends TimelineEvent { export interface RefundEvent extends TimelineEvent {
@@ -175,6 +178,8 @@ export const useBuildTimeline = (orderId: string) => {
const { notifications } = useAdminNotifications({ resource_id: orderId }) const { notifications } = useAdminNotifications({ resource_id: orderId })
const { getLocationNameById } = useStockLocations()
const events: TimelineEvent[] | undefined = useMemo(() => { const events: TimelineEvent[] | undefined = useMemo(() => {
if (!order) { if (!order) {
return undefined return undefined
@@ -318,9 +323,10 @@ export const useBuildTimeline = (orderId: string) => {
id: event.id, id: event.id,
time: event.created_at, time: event.created_at,
type: "fulfilled", type: "fulfilled",
items: event.items.map((item) => getLineItem(allItems, item.item_id)), items: event.items.map((item) => getFulfilmentItem(allItems, item)),
noNotification: event.no_notification, noNotification: event.no_notification,
orderId: order.id, orderId: order.id,
locationName: getLocationNameById(event.location_id),
} as ItemsFulfilledEvent) } as ItemsFulfilledEvent)
if (event.shipped_at) { if (event.shipped_at) {
@@ -328,9 +334,10 @@ export const useBuildTimeline = (orderId: string) => {
id: event.id, id: event.id,
time: event.shipped_at, time: event.shipped_at,
type: "shipped", type: "shipped",
items: event.items.map((item) => getLineItem(allItems, item.item_id)), items: event.items.map((item) => getFulfilmentItem(allItems, item)),
noNotification: event.no_notification, noNotification: event.no_notification,
orderId: order.id, orderId: order.id,
locationName: getLocationNameById(event.location_id),
} as ItemsShippedEvent) } as ItemsShippedEvent)
} }
} }
@@ -592,3 +599,18 @@ function getWasRefundClaim(claimId, order) {
return claim.type === "refund" return claim.type === "refund"
} }
function getFulfilmentItem(allItems, item) {
const line = allItems.find((line) => line.id === item.item_id)
if (!line) {
return
}
return {
title: line.title,
quantity: item.quantity,
thumbnail: line.thumbnail,
variant: { title: line?.variant?.title || "-" },
}
}
@@ -0,0 +1,13 @@
import { useAdminStockLocations } from "medusa-react"
const useStockLocations = () => {
const { stock_locations } = useAdminStockLocations()
const getLocationNameById = (locationId: string | null) =>
stock_locations?.find((stock_location) => stock_location.id === locationId)
?.name
return { stock_locations, getLocationNameById }
}
export default useStockLocations