feat(adshboard,types,medusa): enable adding notes/return reason to inbound claims (#8488)

* wip: setup UI

* wip: rendering modal, adding claim items, create checks

* fix: make form work after merge

* fix: continuation of claim edit

* chore: ability to add and remove items to claim inbound

* chore: minor fixes

* chore: add toast messages on actions

* feat(adshboard,types,medusa): enable adding notes/return reason to inbound items

* chore: fix types in a bunch of places

* chore: add conditional for actions

* Update packages/admin-next/dashboard/src/routes/orders/order-create-claim/components/claim-create-form/claim-create-form.tsx

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>

---------

Co-authored-by: fPolic <mainacc.polic@gmail.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Riqwan Thamir
2024-08-07 19:32:07 +02:00
committed by GitHub
co-authored by Oli Juhl fPolic
parent c017be2a54
commit d50161fa32
13 changed files with 404 additions and 99 deletions
@@ -6,10 +6,10 @@ import {
UseQueryOptions,
} from "@tanstack/react-query"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { sdk } from "../../lib/client"
import { HttpTypes } from "@medusajs/types"
const ORDERS_QUERY_KEY = "orders" as const
const _orderKeys = queryKeysFactory(ORDERS_QUERY_KEY)
@@ -40,7 +40,12 @@ export const useOrder = (
export const useOrderPreview = (
id: string,
options?: Omit<
UseQueryOptions<any, Error, any, QueryKey>,
UseQueryOptions<
HttpTypes.AdminOrderPreviewResponse,
Error,
HttpTypes.AdminOrderPreviewResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
@@ -886,7 +886,12 @@
"create": "Create Claim",
"outbound": "Outbound",
"refundAmount": "Estimated difference",
"activeChangeError": "There is an active order change on this order. Please finish or discard the previous change."
"activeChangeError": "There is an active order change on this order. Please finish or discard the previous change.",
"actions": {
"cancelClaim": {
"successToast": "Claim was successfully canceled."
}
}
},
"reservations": {
"allocatedLabel": "Allocated",
@@ -1,6 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { PencilSquare } from "@medusajs/icons"
import { AdminClaim, AdminOrder, InventoryLevelDTO } from "@medusajs/types"
import {
AdminClaim,
AdminOrder,
AdminOrderPreview,
InventoryLevelDTO,
} from "@medusajs/types"
import {
Alert,
Button,
@@ -46,7 +51,7 @@ import { currencies } from "../../../../../lib/data/currencies"
type ReturnCreateFormProps = {
order: AdminOrder
claim: AdminClaim
preview: AdminOrder
preview: AdminOrderPreview
}
let itemsToAdd: string[] = []
@@ -134,14 +139,14 @@ export const ClaimCreateForm = ({
*/
const previewItems = useMemo(
() =>
preview.items.filter(
preview?.items?.filter(
(i) => !!i.actions?.find((a) => a.claim_id === claim.id)
),
[preview.items]
)
const itemsMap = useMemo(
() => new Map(order.items.map((i) => [i.id, i])),
() => new Map(order?.items?.map((i) => [i.id, i])),
[order.items]
)
@@ -161,14 +166,18 @@ export const ClaimCreateForm = ({
)
return Promise.resolve({
inbound_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,
})),
inbound_items: previewItems.map((i) => {
const returnAction = i.actions?.find(
(a) => a.action === "RETURN_ITEM"
)
return {
item_id: i.id,
quantity: i.detail.return_requested_quantity,
note: returnAction?.internal_note,
reason_id: returnAction?.details?.reason_id as string | undefined,
}
}),
inbound_option_id: method ? method.shipping_option_id : "",
location_id: "",
send_notification: false,
@@ -188,7 +197,7 @@ export const ClaimCreateForm = ({
})
useEffect(() => {
const existingItemsMap = {}
const existingItemsMap: Record<string, boolean> = {}
previewItems.forEach((i) => {
const ind = items.findIndex((field) => field.item_id === i.id)
@@ -205,7 +214,7 @@ export const ClaimCreateForm = ({
...items[ind],
quantity: i.detail.return_requested_quantity,
note: returnItemAction?.internal_note,
reason_id: returnItemAction?.details?.reason_id,
reason_id: returnItemAction?.details?.reason_id as string,
})
}
} else {
@@ -226,13 +235,13 @@ export const ClaimCreateForm = ({
)
if (method) {
form.setValue("option_id", method.shipping_option_id)
form.setValue("inbound_option_id", method.shipping_option_id)
}
}, [preview.shipping_methods])
const showPlaceholder = !items.length
const locationId = form.watch("location_id")
const shippingOptionId = form.watch("option_id")
const shippingOptionId = form.watch("inbound_option_id")
const handleSubmit = form.handleSubmit(async (data) => {
try {
@@ -242,19 +251,25 @@ export const ClaimCreateForm = ({
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
})
const onItemsSelected = async () => {
itemsToAdd.length &&
(await addInboundItem({
items: itemsToAdd.map((id) => ({
id,
quantity: 1,
})),
}))
(await addInboundItem(
{
items: itemsToAdd.map((id) => ({
id,
quantity: 1,
})),
},
{
onError: (error) => {
toast.error(error.message)
},
}
))
for (const itemToRemove of itemsToRemove) {
const actionId = previewItems
@@ -262,14 +277,18 @@ export const ClaimCreateForm = ({
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
if (actionId) {
await removeInboundItem(actionId)
await removeInboundItem(actionId, {
onError: (error) => {
toast.error(error.message)
},
})
}
}
setIsOpen("items", false)
}
const onLocationChange = async (selectedLocationId: string) => {
const onLocationChange = async (selectedLocationId?: string | null) => {
await updateClaimRequest({ location_id: selectedLocationId })
}
@@ -281,12 +300,19 @@ export const ClaimCreateForm = ({
await Promise.all(promises)
await addInboundShipping({ shipping_option_id: selectedOptionId })
await addInboundShipping(
{ shipping_option_id: selectedOptionId },
{
onError: (error) => {
toast.error(error.message)
},
}
)
}
useEffect(() => {
if (isShippingPriceEdit) {
document.getElementById("js-shipping-input").focus()
document.getElementById("js-shipping-input")?.focus()
}
}, [isShippingPriceEdit])
@@ -298,7 +324,7 @@ export const ClaimCreateForm = ({
const allItemsHaveLocation = items
.map((_i) => {
const item = itemsMap.get(_i.item_id)
if (!item?.variant_id) {
if (!item?.variant_id || !item?.variant) {
return true
}
@@ -326,11 +352,12 @@ export const ClaimCreateForm = ({
;(
await Promise.all(
items.map(async (_i) => {
const item = itemsMap.get(_i.item_id)
const item = itemsMap.get(_i.item_id)!
if (!item.variant_id) {
if (!item.variant_id || !item.variant?.product) {
return undefined
}
return await sdk.admin.product.retrieveVariant(
item.variant.product.id,
item.variant_id,
@@ -339,8 +366,9 @@ export const ClaimCreateForm = ({
})
)
)
.filter((it) => it?.variant)
.filter((it) => !!it?.variant)
.forEach((item) => {
const { variant } = item
const levels = variant.inventory[0]?.location_levels
@@ -365,23 +393,30 @@ export const ClaimCreateForm = ({
*/
return () => {
if (IS_CANCELING) {
cancelClaimRequest()
cancelClaimRequest(undefined, {
onSuccess: () => {
toast.success(t("orders.claims.actions.cancelClaim.successToast"))
},
onError: (error) => {
toast.error(error.message)
},
})
// TODO: add this on ESC press
IS_CANCELING = false
}
}
}, [])
const returnTotal = preview.return_requested_total
const shippingTotal = useMemo(() => {
const method = preview.shipping_methods.find(
(sm) => !!sm.actions?.find((a) => a.action === "SHIPPING_ADD")
)
return method?.total || 0
return (method?.total as number) || 0
}, [preview.shipping_methods])
const returnTotal = preview.return_requested_total
const refundAmount = returnTotal - shippingTotal
return (
@@ -473,7 +508,11 @@ export const ClaimCreateForm = ({
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
if (actionId) {
removeInboundItem(actionId)
removeInboundItem(actionId, {
onError: (error) => {
toast.error(error.message)
},
})
}
}}
onUpdate={(payload) => {
@@ -482,7 +521,14 @@ export const ClaimCreateForm = ({
?.actions?.find((a) => a.action === "RETURN_ITEM")?.id
if (actionId) {
updateInboundItem({ ...payload, actionId })
updateInboundItem(
{ ...payload, actionId },
{
onError: (error) => {
toast.error(error.message)
},
}
)
}
}}
index={index}
@@ -543,16 +589,16 @@ export const ClaimCreateForm = ({
{/*TODO: WHAT IF THE RETURN OPTION HAS COMPUTED PRICE*/}
<Form.Field
control={form.control}
name="option_id"
name="inbound_option_id"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<Form.Control>
<Combobox
value={value}
onChange={(v) => {
onChange(v)
onShippingOptionChange(v)
value={value ?? undefined}
onChange={(val) => {
onChange(val)
val && onShippingOptionChange(val)
}}
{...field}
options={(shipping_options ?? [])
@@ -640,13 +686,20 @@ export const ClaimCreateForm = ({
})
if (actionId) {
updateInboundShipping({
actionId,
custom_price:
typeof customShippingAmount === "string"
? null
: customShippingAmount,
})
updateInboundShipping(
{
actionId,
custom_price:
typeof customShippingAmount === "string"
? null
: customShippingAmount,
},
{
onError: (error) => {
toast.error(error.message)
},
}
)
}
setIsShippingPriceEdit(false)
}}
@@ -656,7 +709,7 @@ export const ClaimCreateForm = ({
}
code={order.currency_code}
onValueChange={(value) =>
setCustomShippingAmount(value ? parseInt(value) : "")
value && setCustomShippingAmount(parseInt(value))
}
value={customShippingAmount}
disabled={showPlaceholder}
@@ -1,16 +1,14 @@
import { useTranslation } from "react-i18next"
import React from "react"
import { ChatBubble, DocumentText, XCircle, XMark } from "@medusajs/icons"
import { AdminOrderLineItem, HttpTypes } from "@medusajs/types"
import { IconButton, Input, Text } from "@medusajs/ui"
import { UseFormReturn } from "react-hook-form"
import { HttpTypes, AdminOrderLineItem } from "@medusajs/types"
import { ChatBubble, DocumentText, XCircle, XMark } from "@medusajs/icons"
import { useTranslation } from "react-i18next"
import { Thumbnail } from "../../../../../components/common/thumbnail"
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
import { Form } from "../../../../../components/common/form"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { Form } from "../../../../../components/common/form"
import { Thumbnail } from "../../../../../components/common/thumbnail"
import { Combobox } from "../../../../../components/inputs/combobox"
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
import { useReturnReasons } from "../../../../../hooks/api/return-reasons"
type OrderEditItemProps = {
@@ -35,11 +33,9 @@ function ClaimInboundItem({
index,
}: OrderEditItemProps) {
const { t } = useTranslation()
const { return_reasons = [] } = useReturnReasons({ fields: "+label" })
const formItem = form.watch(`inbound_items.${index}`)
const showReturnReason = typeof formItem.reason_id === "string"
const showNote = typeof formItem.note === "string"
@@ -48,15 +44,17 @@ function ClaimInboundItem({
<div className="flex flex-col items-center gap-x-2 gap-y-2 border-b p-3 text-sm md:flex-row">
<div className="flex flex-1 items-center gap-x-3">
<Thumbnail src={item.thumbnail} />
<div className="flex flex-col">
<div>
<Text className="txt-small" as="span" weight="plus">
{item.title}{" "}
</Text>
{item.variant.sku && <span>({item.variant.sku})</span>}
{item.variant?.sku && <span>({item.variant.sku})</span>}
</div>
<Text as="div" className="text-ui-fg-subtle txt-small">
{item.variant.product.title}
{item.variant?.product?.title}
</Text>
</div>
</div>
@@ -71,19 +69,18 @@ function ClaimInboundItem({
<Form.Item>
<Form.Control>
<Input
{...field}
className="bg-ui-bg-base txt-small w-[67px] rounded-lg"
min={1}
max={item.quantity}
type="number"
{...field}
onChange={(e) => {
onBlur={(e) => {
const val = e.target.value
const payload = val === "" ? null : Number(val)
field.onChange(payload)
if (payload) {
// todo: move on blur
onUpdate({ quantity: payload })
}
}}
@@ -178,8 +175,9 @@ function ClaimInboundItem({
className="flex-shrink"
variant="transparent"
onClick={() => {
onUpdate({ reason_id: null }) // TODO BE: we should be able to set to unset reason here
form.setValue(`inbound_items.${index}.reason_id`, "")
form.setValue(`inbound_items.${index}.reason_id`, null)
onUpdate({ reason_id: null })
}}
>
<XMark className="text-ui-fg-muted" />
@@ -202,17 +200,17 @@ function ClaimInboundItem({
<div className="flex-grow">
<Form.Field
control={form.control}
name={`items.${index}.note`}
render={({ field: { ref, onChange, ...field } }) => {
name={`inbound_items.${index}.note`}
render={({ field: { ref, ...field } }) => {
return (
<Form.Item>
<Form.Control>
<Input
onChange={onChange}
{...field}
onBlur={() =>
onBlur={() => {
field.onChange(field.value)
onUpdate({ internal_note: field.value })
}
}}
className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
/>
</Form.Control>
@@ -222,15 +220,14 @@ function ClaimInboundItem({
}}
/>
</div>
<IconButton
type="button"
className="flex-shrink"
variant="transparent"
onClick={() => {
form.setValue(`items.${index}.note`, {
shouldDirty: true,
shouldTouch: true,
})
form.setValue(`inbound_items.${index}.note`, null)
onUpdate({ internal_note: null })
}}
>
@@ -5,18 +5,19 @@ export const ClaimCreateSchema = z.object({
z.object({
item_id: z.string(),
quantity: z.number(),
reason_id: z.string().optional().nullable(),
note: z.string().optional().nullable(),
})
),
outbound_items: z.array(
z.object({
item_id: z.string(), // TODO: variant id?
quantity: z.number(),
reason_id: z.string().nullish(),
note: z.string().nullish(),
})
),
// TODO: Bring back when introducing outbound items
// outbound_items: z.array(
// z.object({
// item_id: z.string(), // TODO: variant id?
// quantity: z.number(),
// })
// ),
location_id: z.string().optional(),
inbound_option_id: z.string(),
inbound_option_id: z.string().nullish(),
send_notification: z.boolean().optional(),
})