feat(admin-ui): Implement allocations on draft orders (#3753)
This commit is contained in:
1 parent
2cc2acb6e0
commit
ba45a316a7
7 files changed
+420
-238
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/admin-ui": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(admin): Implement allocations on draft orders
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { LineItem } from "@medusajs/medusa"
|
||||||
|
import { sum } from "lodash"
|
||||||
|
import { ReservationItemDTO } from "@medusajs/types"
|
||||||
|
import { useAdminStockLocations, useAdminVariantsInventory } from "medusa-react"
|
||||||
|
import Tooltip from "../../../../components/atoms/tooltip"
|
||||||
|
import CircleQuarterSolid from "../../../../components/fundamentals/icons/circle-quarter-solid"
|
||||||
|
import WarningCircleIcon from "../../../../components/fundamentals/icons/warning-circle"
|
||||||
|
import CheckCircleFillIcon from "../../../../components/fundamentals/icons/check-circle-fill-icon"
|
||||||
|
import EditAllocationDrawer from "../../details/allocations/edit-allocation-modal"
|
||||||
|
import Button from "../../../../components/fundamentals/button"
|
||||||
|
|
||||||
|
const ReservationIndicator = ({
|
||||||
|
reservations,
|
||||||
|
lineItem,
|
||||||
|
}: {
|
||||||
|
reservations?: ReservationItemDTO[]
|
||||||
|
lineItem: LineItem
|
||||||
|
}) => {
|
||||||
|
const { variant, isLoading, isFetching } = useAdminVariantsInventory(
|
||||||
|
lineItem.variant_id!,
|
||||||
|
{
|
||||||
|
enabled: !!lineItem?.variant_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { stock_locations } = useAdminStockLocations({
|
||||||
|
id: reservations?.map((r) => r.location_id) || [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const [reservation, setReservation] =
|
||||||
|
React.useState<ReservationItemDTO | null>(null)
|
||||||
|
|
||||||
|
const locationMap = new Map(stock_locations?.map((l) => [l.id, l.name]) || [])
|
||||||
|
|
||||||
|
const reservationsSum = sum(reservations?.map((r) => r.quantity) || [])
|
||||||
|
|
||||||
|
const allocatableSum = lineItem.quantity - (lineItem?.fulfilled_quantity || 0)
|
||||||
|
|
||||||
|
const awaitingAllocation = allocatableSum - reservationsSum
|
||||||
|
|
||||||
|
if (
|
||||||
|
isLoading ||
|
||||||
|
isFetching ||
|
||||||
|
!lineItem.variant_id ||
|
||||||
|
!variant?.inventory.length
|
||||||
|
) {
|
||||||
|
return <div className="w-[20px]" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={awaitingAllocation ? "text-rose-50" : "text-grey-40"}>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<div className="inter-small-regular flex flex-col items-center px-1 pt-1 pb-2">
|
||||||
|
{reservationsSum || awaitingAllocation ? (
|
||||||
|
<div className="gap-y-base grid grid-cols-1 divide-y">
|
||||||
|
{!!awaitingAllocation && (
|
||||||
|
<span className="flex w-full items-center">
|
||||||
|
{awaitingAllocation} items await allocation
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{reservations?.map((reservation) => (
|
||||||
|
<EditAllocationButton
|
||||||
|
key={reservation.id}
|
||||||
|
locationName={locationMap.get(reservation.location_id)}
|
||||||
|
totalReservedQuantity={reservationsSum}
|
||||||
|
reservation={reservation}
|
||||||
|
lineItem={lineItem}
|
||||||
|
onClick={() => setReservation(reservation)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="flex w-full items-center">
|
||||||
|
This item has been fulfilled.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
side="bottom"
|
||||||
|
>
|
||||||
|
{awaitingAllocation ? (
|
||||||
|
reservationsSum ? (
|
||||||
|
<CircleQuarterSolid size={20} />
|
||||||
|
) : (
|
||||||
|
<WarningCircleIcon fillType="solid" size={20} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<CheckCircleFillIcon size={20} />
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{reservation && (
|
||||||
|
<EditAllocationDrawer
|
||||||
|
totalReservedQuantity={reservationsSum}
|
||||||
|
close={() => setReservation(null)}
|
||||||
|
reservation={reservation}
|
||||||
|
item={lineItem}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditAllocationButton = ({
|
||||||
|
reservation,
|
||||||
|
locationName,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
reservation: ReservationItemDTO
|
||||||
|
totalReservedQuantity: number
|
||||||
|
locationName?: string
|
||||||
|
lineItem: LineItem
|
||||||
|
onClick: () => void
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="pt-base first:pt-0">
|
||||||
|
{`${reservation.quantity} item: ${locationName}`}
|
||||||
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
className="mt-2 w-full border"
|
||||||
|
>
|
||||||
|
Edit Allocation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReservationIndicator
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Controller, useForm, useWatch } from "react-hook-form"
|
import { Controller, useForm, useWatch } from "react-hook-form"
|
||||||
import { LineItem, Order, ReservationItemDTO } from "@medusajs/medusa"
|
import { LineItem } from "@medusajs/medusa"
|
||||||
import { NestedForm, nestedForm } from "../../../../utils/nested-form"
|
import { NestedForm, nestedForm } from "../../../../utils/nested-form"
|
||||||
import React, { useEffect, useMemo } from "react"
|
import React, { useEffect, useMemo } from "react"
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +20,7 @@ import { getErrorMessage } from "../../../../utils/error-messages"
|
|||||||
import { getFulfillableQuantity } from "../create-fulfillment/item-table"
|
import { getFulfillableQuantity } from "../create-fulfillment/item-table"
|
||||||
import { sum } from "lodash"
|
import { sum } from "lodash"
|
||||||
import useNotification from "../../../../hooks/use-notification"
|
import useNotification from "../../../../hooks/use-notification"
|
||||||
|
import { ReservationItemDTO } from "@medusajs/types"
|
||||||
|
|
||||||
type AllocationModalFormData = {
|
type AllocationModalFormData = {
|
||||||
location?: { label: string; value: string }
|
location?: { label: string; value: string }
|
||||||
@@ -27,13 +28,13 @@ type AllocationModalFormData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AllocateItemsModalProps = {
|
type AllocateItemsModalProps = {
|
||||||
order: Order
|
items: LineItem[]
|
||||||
reservationItemsMap: Record<string, ReservationItemDTO[]>
|
reservationItemsMap: Record<string, ReservationItemDTO[]>
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AllocateItemsModal: React.FC<AllocateItemsModalProps> = ({
|
const AllocateItemsModal: React.FC<AllocateItemsModalProps> = ({
|
||||||
order,
|
items,
|
||||||
close,
|
close,
|
||||||
reservationItemsMap,
|
reservationItemsMap,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -175,7 +176,7 @@ const AllocateItemsModal: React.FC<AllocateItemsModalProps> = ({
|
|||||||
<p className="inter-base-regular">
|
<p className="inter-base-regular">
|
||||||
Select the number of items that you wish to allocate.
|
Select the number of items that you wish to allocate.
|
||||||
</p>
|
</p>
|
||||||
{order.items?.map((item, i) => {
|
{items?.map((item, i) => {
|
||||||
return (
|
return (
|
||||||
<AllocationLineItem
|
<AllocationLineItem
|
||||||
form={nestedForm(form, `items.${i}` as "items.0")}
|
form={nestedForm(form, `items.${i}` as "items.0")}
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import React, { useMemo } from "react"
|
||||||
|
import { sum } from "lodash"
|
||||||
|
import {
|
||||||
|
AdminGetVariantsVariantInventoryRes,
|
||||||
|
DraftOrder,
|
||||||
|
VariantInventory,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import Badge from "../../../../components/fundamentals/badge"
|
||||||
|
import ImagePlaceholder from "../../../../components/fundamentals/image-placeholder"
|
||||||
|
import BodyCard from "../../../../components/organisms/body-card"
|
||||||
|
import { formatAmountWithSymbol } from "../../../../utils/prices"
|
||||||
|
import { DisplayTotal } from "../templates"
|
||||||
|
import { ActionType } from "../../../../components/molecules/actionables"
|
||||||
|
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
|
||||||
|
import useToggleState from "../../../../hooks/use-toggle-state"
|
||||||
|
import { useAdminReservations, useMedusa } from "medusa-react"
|
||||||
|
import { ReservationItemDTO } from "@medusajs/types"
|
||||||
|
import ReservationIndicator from "../../components/reservation-indicator/reservation-indicator"
|
||||||
|
import AllocateItemsModal from "../allocations/allocate-items-modal"
|
||||||
|
import { Response } from "@medusajs/medusa-js"
|
||||||
|
import StatusIndicator from "../../../../components/fundamentals/status-indicator"
|
||||||
|
|
||||||
|
type DraftSummaryCardProps = {
|
||||||
|
order: DraftOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
const DraftSummaryCard: React.FC<DraftSummaryCardProps> = ({ order }) => {
|
||||||
|
const { client } = useMedusa()
|
||||||
|
const { isFeatureEnabled } = useFeatureFlag()
|
||||||
|
const inventoryEnabled = useMemo(() => {
|
||||||
|
return isFeatureEnabled("inventoryService")
|
||||||
|
}, [isFeatureEnabled])
|
||||||
|
|
||||||
|
const { cart } = order
|
||||||
|
const { region } = cart
|
||||||
|
|
||||||
|
const [variantInventoryMap, setVariantInventoryMap] = React.useState<
|
||||||
|
Map<string, VariantInventory>
|
||||||
|
>(new Map())
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!inventoryEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchInventory = async () => {
|
||||||
|
const inventory = await Promise.all(
|
||||||
|
cart.items.map(async (item) => {
|
||||||
|
if (!item.variant_id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return await client.admin.variants.getInventory(item.variant_id)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
setVariantInventoryMap(
|
||||||
|
new Map(
|
||||||
|
inventory
|
||||||
|
.filter(
|
||||||
|
(
|
||||||
|
inventoryItem
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
): inventoryItem is Response<AdminGetVariantsVariantInventoryRes> =>
|
||||||
|
!!inventoryItem
|
||||||
|
)
|
||||||
|
.map((i) => {
|
||||||
|
return [i.variant.id, i.variant]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchInventory()
|
||||||
|
}, [cart.items, inventoryEnabled, client.admin.variants])
|
||||||
|
|
||||||
|
const { reservations } = useAdminReservations(
|
||||||
|
{
|
||||||
|
line_item_id: cart?.items.map((item) => item.id),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: inventoryEnabled,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const reservationItemsMap = useMemo(() => {
|
||||||
|
if (!reservations?.length || !inventoryEnabled) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reservations.reduce(
|
||||||
|
(acc: Record<string, ReservationItemDTO[]>, item: ReservationItemDTO) => {
|
||||||
|
if (!item.line_item_id) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
acc[item.line_item_id] = acc[item.line_item_id]
|
||||||
|
? [...acc[item.line_item_id], item]
|
||||||
|
: [item]
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}, [reservations, inventoryEnabled])
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: allocationModalIsOpen,
|
||||||
|
open: showAllocationModal,
|
||||||
|
close: closeAllocationModal,
|
||||||
|
} = useToggleState()
|
||||||
|
|
||||||
|
const allItemsReserved = useMemo(() => {
|
||||||
|
return cart.items.every((item) => {
|
||||||
|
if (
|
||||||
|
!item.variant_id ||
|
||||||
|
!variantInventoryMap.get(item.variant_id)?.inventory.length
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const reservations = reservationItemsMap[item.id]
|
||||||
|
|
||||||
|
return (
|
||||||
|
item.quantity === item.fulfilled_quantity ||
|
||||||
|
(reservations &&
|
||||||
|
sum(reservations.map((r) => r.quantity)) ===
|
||||||
|
item.quantity - (item.fulfilled_quantity || 0))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [cart.items, variantInventoryMap, reservationItemsMap])
|
||||||
|
|
||||||
|
const actionables = useMemo(() => {
|
||||||
|
const actionables: ActionType[] = []
|
||||||
|
if (inventoryEnabled && !allItemsReserved) {
|
||||||
|
actionables.push({
|
||||||
|
label: "Allocate",
|
||||||
|
onClick: showAllocationModal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return actionables
|
||||||
|
}, [inventoryEnabled, showAllocationModal, allItemsReserved])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BodyCard
|
||||||
|
className={"mb-4 h-auto min-h-0 w-full"}
|
||||||
|
title="Summary"
|
||||||
|
status={
|
||||||
|
isFeatureEnabled("inventoryService") &&
|
||||||
|
Array.isArray(reservations) && (
|
||||||
|
<StatusIndicator
|
||||||
|
onClick={allItemsReserved ? undefined : showAllocationModal}
|
||||||
|
variant={allItemsReserved ? "success" : "danger"}
|
||||||
|
title={allItemsReserved ? "Allocated" : "Awaits allocation"}
|
||||||
|
className="rounded-rounded border px-3 py-1.5"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
actionables={actionables}
|
||||||
|
>
|
||||||
|
<div className="mt-6">
|
||||||
|
{cart?.items?.map((item, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="hover:bg-grey-5 rounded-rounded mx-[-5px] mb-1 flex h-[64px] justify-between py-2 px-[5px]"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center space-x-4">
|
||||||
|
<div className="rounded-rounded flex h-[48px] w-[36px] items-center justify-center">
|
||||||
|
{item?.thumbnail ? (
|
||||||
|
<img
|
||||||
|
src={item.thumbnail}
|
||||||
|
className="rounded-rounded object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ImagePlaceholder />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<span className="inter-small-regular text-grey-90 max-w-[225px] truncate">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
{item?.variant && (
|
||||||
|
<span className="inter-small-regular text-grey-50">
|
||||||
|
{item.variant.sku}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="small:space-x-2 medium:space-x-4 large:space-x-6 mr-3 flex">
|
||||||
|
<div className="inter-small-regular text-grey-50">
|
||||||
|
{formatAmountWithSymbol({
|
||||||
|
amount: (item?.total ?? 0) / item.quantity,
|
||||||
|
currency: region?.currency_code ?? "",
|
||||||
|
digits: 2,
|
||||||
|
tax: [],
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="inter-small-regular text-grey-50">
|
||||||
|
x {item.quantity}
|
||||||
|
</div>
|
||||||
|
{inventoryEnabled && (
|
||||||
|
<ReservationIndicator
|
||||||
|
reservations={reservationItemsMap[item.id]}
|
||||||
|
lineItem={item}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="inter-small-regular text-grey-90">
|
||||||
|
{formatAmountWithSymbol({
|
||||||
|
amount: item.total ?? 0,
|
||||||
|
currency: region?.currency_code ?? "",
|
||||||
|
digits: 2,
|
||||||
|
tax: [],
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="inter-small-regular text-grey-50">
|
||||||
|
{region?.currency_code.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<DisplayTotal
|
||||||
|
currency={region?.currency_code}
|
||||||
|
totalAmount={order?.cart?.subtotal}
|
||||||
|
totalTitle={"Subtotal"}
|
||||||
|
/>
|
||||||
|
{cart?.discounts?.map((discount, index) => (
|
||||||
|
<div key={index} className="mt-4 flex items-center justify-between">
|
||||||
|
<div className="inter-small-regular text-grey-90 flex items-center">
|
||||||
|
Discount:{" "}
|
||||||
|
<Badge className="ml-3" variant="default">
|
||||||
|
{discount.code}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="inter-small-regular text-grey-90">
|
||||||
|
-
|
||||||
|
{formatAmountWithSymbol({
|
||||||
|
amount: cart?.discount_total,
|
||||||
|
currency: region?.currency_code || "",
|
||||||
|
digits: 2,
|
||||||
|
tax: region?.tax_rate,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<DisplayTotal
|
||||||
|
currency={region?.currency_code}
|
||||||
|
totalAmount={cart?.shipping_total}
|
||||||
|
totalTitle={"Shipping"}
|
||||||
|
/>
|
||||||
|
<DisplayTotal
|
||||||
|
currency={region?.currency_code}
|
||||||
|
totalAmount={cart?.tax_total}
|
||||||
|
totalTitle={`Tax`}
|
||||||
|
/>
|
||||||
|
<DisplayTotal
|
||||||
|
currency={region?.currency_code}
|
||||||
|
variant="large"
|
||||||
|
totalAmount={cart?.total}
|
||||||
|
totalTitle={`Total`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{allocationModalIsOpen && (
|
||||||
|
<AllocateItemsModal
|
||||||
|
reservationItemsMap={reservationItemsMap}
|
||||||
|
items={cart.items}
|
||||||
|
close={closeAllocationModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</BodyCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DraftSummaryCard
|
||||||
@@ -261,7 +261,7 @@ const SummaryCard: React.FC<SummaryCardProps> = ({ order, reservations }) => {
|
|||||||
{allocationModalIsOpen && (
|
{allocationModalIsOpen && (
|
||||||
<AllocateItemsModal
|
<AllocateItemsModal
|
||||||
reservationItemsMap={reservationItemsMap}
|
reservationItemsMap={reservationItemsMap}
|
||||||
order={order}
|
items={order.items}
|
||||||
close={closeAllocationModal}
|
close={closeAllocationModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { LineItem, ReservationItemDTO } from "@medusajs/medusa"
|
import { LineItem } from "@medusajs/medusa"
|
||||||
import { useAdminStockLocations, useAdminVariantsInventory } from "medusa-react"
|
import { ReservationItemDTO } from "@medusajs/types"
|
||||||
|
|
||||||
import Button from "../../../../components/fundamentals/button"
|
|
||||||
import CheckCircleFillIcon from "../../../../components/fundamentals/icons/check-circle-fill-icon"
|
|
||||||
import CircleQuarterSolid from "../../../../components/fundamentals/icons/circle-quarter-solid"
|
|
||||||
import EditAllocationDrawer from "../allocations/edit-allocation-modal"
|
|
||||||
import ImagePlaceholder from "../../../../components/fundamentals/image-placeholder"
|
import ImagePlaceholder from "../../../../components/fundamentals/image-placeholder"
|
||||||
import React from "react"
|
|
||||||
import Tooltip from "../../../../components/atoms/tooltip"
|
|
||||||
import WarningCircleIcon from "../../../../components/fundamentals/icons/warning-circle"
|
|
||||||
import { formatAmountWithSymbol } from "../../../../utils/prices"
|
import { formatAmountWithSymbol } from "../../../../utils/prices"
|
||||||
import { sum } from "lodash"
|
|
||||||
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
|
import { useFeatureFlag } from "../../../../providers/feature-flag-provider"
|
||||||
|
import ReservationIndicator from "../../components/reservation-indicator/reservation-indicator"
|
||||||
|
|
||||||
type OrderLineProps = {
|
type OrderLineProps = {
|
||||||
item: LineItem
|
item: LineItem
|
||||||
@@ -77,123 +70,4 @@ const OrderLine = ({ item, currencyCode, reservations }: OrderLineProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReservationIndicator = ({
|
|
||||||
reservations,
|
|
||||||
lineItem,
|
|
||||||
}: {
|
|
||||||
reservations?: ReservationItemDTO[]
|
|
||||||
lineItem: LineItem
|
|
||||||
}) => {
|
|
||||||
const { variant, isLoading, isFetching } = useAdminVariantsInventory(
|
|
||||||
lineItem.variant_id!,
|
|
||||||
{
|
|
||||||
enabled: !!lineItem?.variant_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const { stock_locations } = useAdminStockLocations({
|
|
||||||
id: reservations?.map((r) => r.location_id) || [],
|
|
||||||
})
|
|
||||||
|
|
||||||
const [reservation, setReservation] =
|
|
||||||
React.useState<ReservationItemDTO | null>(null)
|
|
||||||
|
|
||||||
const locationMap = new Map(stock_locations?.map((l) => [l.id, l.name]) || [])
|
|
||||||
|
|
||||||
const reservationsSum = sum(reservations?.map((r) => r.quantity) || [])
|
|
||||||
|
|
||||||
const allocatableSum = lineItem.quantity - (lineItem?.fulfilled_quantity || 0)
|
|
||||||
|
|
||||||
const awaitingAllocation = allocatableSum - reservationsSum
|
|
||||||
|
|
||||||
if (
|
|
||||||
isLoading ||
|
|
||||||
isFetching ||
|
|
||||||
!lineItem.variant_id ||
|
|
||||||
!variant?.inventory.length
|
|
||||||
) {
|
|
||||||
return <div className="w-[20px]" />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={awaitingAllocation ? "text-rose-50" : "text-grey-40"}>
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<div className="inter-small-regular flex flex-col items-center px-1 pt-1 pb-2">
|
|
||||||
{reservationsSum || awaitingAllocation ? (
|
|
||||||
<div className="gap-y-base grid grid-cols-1 divide-y">
|
|
||||||
{!!awaitingAllocation && (
|
|
||||||
<span className="flex w-full items-center">
|
|
||||||
{awaitingAllocation} items await allocation
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{reservations?.map((reservation) => (
|
|
||||||
<EditAllocationButton
|
|
||||||
key={reservation.id}
|
|
||||||
locationName={locationMap.get(reservation.location_id)}
|
|
||||||
totalReservedQuantity={reservationsSum}
|
|
||||||
reservation={reservation}
|
|
||||||
lineItem={lineItem}
|
|
||||||
onClick={() => setReservation(reservation)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="flex w-full items-center">
|
|
||||||
This item has been fulfilled.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
side="bottom"
|
|
||||||
>
|
|
||||||
{awaitingAllocation ? (
|
|
||||||
reservationsSum ? (
|
|
||||||
<CircleQuarterSolid size={20} />
|
|
||||||
) : (
|
|
||||||
<WarningCircleIcon fillType="solid" size={20} />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<CheckCircleFillIcon size={20} />
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{reservation && (
|
|
||||||
<EditAllocationDrawer
|
|
||||||
totalReservedQuantity={reservationsSum}
|
|
||||||
close={() => setReservation(null)}
|
|
||||||
reservation={reservation}
|
|
||||||
item={lineItem}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditAllocationButton = ({
|
|
||||||
reservation,
|
|
||||||
locationName,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
reservation: ReservationItemDTO
|
|
||||||
totalReservedQuantity: number
|
|
||||||
locationName?: string
|
|
||||||
lineItem: LineItem
|
|
||||||
onClick: () => void
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="pt-base first:pt-0">
|
|
||||||
{`${reservation.quantity} item: ${locationName}`}
|
|
||||||
<Button
|
|
||||||
onClick={onClick}
|
|
||||||
variant="ghost"
|
|
||||||
size="small"
|
|
||||||
className="mt-2 w-full border"
|
|
||||||
>
|
|
||||||
Edit Allocation
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OrderLine
|
export default OrderLine
|
||||||
@@ -13,12 +13,10 @@ import Avatar from "../../../components/atoms/avatar"
|
|||||||
import BackButton from "../../../components/atoms/back-button"
|
import BackButton from "../../../components/atoms/back-button"
|
||||||
import CopyToClipboard from "../../../components/atoms/copy-to-clipboard"
|
import CopyToClipboard from "../../../components/atoms/copy-to-clipboard"
|
||||||
import Spinner from "../../../components/atoms/spinner"
|
import Spinner from "../../../components/atoms/spinner"
|
||||||
import Badge from "../../../components/fundamentals/badge"
|
|
||||||
import Button from "../../../components/fundamentals/button"
|
import Button from "../../../components/fundamentals/button"
|
||||||
import DetailsIcon from "../../../components/fundamentals/details-icon"
|
import DetailsIcon from "../../../components/fundamentals/details-icon"
|
||||||
import DollarSignIcon from "../../../components/fundamentals/icons/dollar-sign-icon"
|
import DollarSignIcon from "../../../components/fundamentals/icons/dollar-sign-icon"
|
||||||
import TruckIcon from "../../../components/fundamentals/icons/truck-icon"
|
import TruckIcon from "../../../components/fundamentals/icons/truck-icon"
|
||||||
import ImagePlaceholder from "../../../components/fundamentals/image-placeholder"
|
|
||||||
import StatusDot from "../../../components/fundamentals/status-indicator"
|
import StatusDot from "../../../components/fundamentals/status-indicator"
|
||||||
import JSONView from "../../../components/molecules/json-view"
|
import JSONView from "../../../components/molecules/json-view"
|
||||||
import BodyCard from "../../../components/organisms/body-card"
|
import BodyCard from "../../../components/organisms/body-card"
|
||||||
@@ -31,6 +29,7 @@ import { getErrorMessage } from "../../../utils/error-messages"
|
|||||||
import extractCustomerName from "../../../utils/extract-customer-name"
|
import extractCustomerName from "../../../utils/extract-customer-name"
|
||||||
import { formatAmountWithSymbol } from "../../../utils/prices"
|
import { formatAmountWithSymbol } from "../../../utils/prices"
|
||||||
import AddressModal from "../details/address-modal"
|
import AddressModal from "../details/address-modal"
|
||||||
|
import DraftSummaryCard from "../details/detail-cards/draft-summary"
|
||||||
import { DisplayTotal, FormattedAddress } from "../details/templates"
|
import { DisplayTotal, FormattedAddress } from "../details/templates"
|
||||||
|
|
||||||
type DeletePromptData = {
|
type DeletePromptData = {
|
||||||
@@ -211,108 +210,7 @@ const DraftOrderDetails = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BodyCard>
|
</BodyCard>
|
||||||
<BodyCard className={"mb-4 h-auto min-h-0 w-full"} title="Summary">
|
<DraftSummaryCard order={draft_order} />
|
||||||
<div className="mt-6">
|
|
||||||
{cart?.items?.map((item, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="hover:bg-grey-5 rounded-rounded mx-[-5px] mb-1 flex h-[64px] justify-between py-2 px-[5px]"
|
|
||||||
>
|
|
||||||
<div className="flex justify-center space-x-4">
|
|
||||||
<div className="rounded-rounded flex h-[48px] w-[36px] items-center justify-center">
|
|
||||||
{item?.thumbnail ? (
|
|
||||||
<img
|
|
||||||
src={item.thumbnail}
|
|
||||||
className="rounded-rounded object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ImagePlaceholder />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-center">
|
|
||||||
<span className="inter-small-regular text-grey-90 max-w-[225px] truncate">
|
|
||||||
{item.title}
|
|
||||||
</span>
|
|
||||||
{item?.variant && (
|
|
||||||
<span className="inter-small-regular text-grey-50">
|
|
||||||
{item.variant.sku}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="small:space-x-2 medium:space-x-4 large:space-x-6 mr-3 flex">
|
|
||||||
<div className="inter-small-regular text-grey-50">
|
|
||||||
{formatAmountWithSymbol({
|
|
||||||
amount: (item?.total ?? 0) / item.quantity,
|
|
||||||
currency: region?.currency_code ?? "",
|
|
||||||
digits: 2,
|
|
||||||
tax: [],
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="inter-small-regular text-grey-50">
|
|
||||||
x {item.quantity}
|
|
||||||
</div>
|
|
||||||
<div className="inter-small-regular text-grey-90">
|
|
||||||
{formatAmountWithSymbol({
|
|
||||||
amount: item.total ?? 0,
|
|
||||||
currency: region?.currency_code ?? "",
|
|
||||||
digits: 2,
|
|
||||||
tax: [],
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="inter-small-regular text-grey-50">
|
|
||||||
{region?.currency_code.toUpperCase()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<DisplayTotal
|
|
||||||
currency={region?.currency_code}
|
|
||||||
totalAmount={draft_order?.cart?.subtotal}
|
|
||||||
totalTitle={"Subtotal"}
|
|
||||||
/>
|
|
||||||
{cart?.discounts?.map((discount, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="mt-4 flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<div className="inter-small-regular text-grey-90 flex items-center">
|
|
||||||
Discount:{" "}
|
|
||||||
<Badge className="ml-3" variant="default">
|
|
||||||
{discount.code}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="inter-small-regular text-grey-90">
|
|
||||||
-
|
|
||||||
{formatAmountWithSymbol({
|
|
||||||
amount: cart?.discount_total,
|
|
||||||
currency: region?.currency_code || "",
|
|
||||||
digits: 2,
|
|
||||||
tax: region?.tax_rate,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<DisplayTotal
|
|
||||||
currency={region?.currency_code}
|
|
||||||
totalAmount={cart?.shipping_total}
|
|
||||||
totalTitle={"Shipping"}
|
|
||||||
/>
|
|
||||||
<DisplayTotal
|
|
||||||
currency={region?.currency_code}
|
|
||||||
totalAmount={cart?.tax_total}
|
|
||||||
totalTitle={`Tax`}
|
|
||||||
/>
|
|
||||||
<DisplayTotal
|
|
||||||
currency={region?.currency_code}
|
|
||||||
variant="large"
|
|
||||||
totalAmount={cart?.total}
|
|
||||||
totalTitle={`Total`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</BodyCard>
|
|
||||||
<BodyCard
|
<BodyCard
|
||||||
className={"mb-4 h-auto min-h-0 w-full"}
|
className={"mb-4 h-auto min-h-0 w-full"}
|
||||||
title="Payment"
|
title="Payment"
|
||||||
|
|||||||
Reference in new issue
Block a user