Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard) admin 3.0 return creation #6713

Merged
merged 24 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d9758fa
feat: initial work
fPolic Mar 14, 2024
fe3f96f
wip: shipping location sections
fPolic Mar 15, 2024
589ee6b
feat: display inventory levels check
fPolic Mar 15, 2024
6ee0699
feat: refundable amount, custom refund and shipping form elements, ad…
fPolic Mar 15, 2024
9990e93
feat: shipping price, translations
fPolic Mar 15, 2024
d1c9863
Merge branch 'develop' into feat/admin-3.0-returns
fPolic Mar 18, 2024
3f91253
fix: status logic, refactor rename
fPolic Mar 18, 2024
dde8ff9
fix: disallow going back to items select
fPolic Mar 18, 2024
7cd6c36
feat: handle return reason
fPolic Mar 18, 2024
d2f6039
fix: fields/defaults and flow
fPolic Mar 18, 2024
caf2135
fix: layout issues, custom shipping price calculations
fPolic Mar 18, 2024
7200fa9
feat: send data
fPolic Mar 18, 2024
01e62f9
feat: open return from details
fPolic Mar 18, 2024
b425241
Merge branch 'develop' into feat/admin-3.0-returns
fPolic Mar 18, 2024
ffb3bd8
Merge branch 'develop' into feat/admin-3.0-returns
fPolic Mar 19, 2024
aef52eb
fix: address comments
fPolic Mar 19, 2024
fa5e3e8
fix: correct tooltip message
fPolic Mar 19, 2024
e900f03
feat: disable shipping select
fPolic Mar 19, 2024
f57ac50
Merge branch 'develop' into feat/admin-3.0-returns
fPolic Mar 20, 2024
b0bb815
Merge branch 'develop' into feat/admin-3.0-returns
fPolic Mar 20, 2024
43c43c7
fix: get returnable items helper
fPolic Mar 21, 2024
0ccf1d2
fix: expands
fPolic Mar 21, 2024
8cd5443
cleanup PR, add table filters, and fix type errors
kasperkristensen Mar 22, 2024
1dc549f
Merge branch 'develop' into feat/admin-3.0-returns
kasperkristensen Mar 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"revoked": "Revoked",
"admin": "Admin",
"store": "Store",
"details": "Details",
"items_one": "{{count}} item",
"items_other": "{{count}} items",
"countSelected": "{{count}} selected",
Expand Down Expand Up @@ -54,6 +55,8 @@
"revoke": "Revoke",
"cancel": "Cancel",
"save": "Save",
"next": "Next",
"back": "Back",
"continue": "Continue",
"edit": "Edit",
"download": "Download",
Expand Down Expand Up @@ -236,6 +239,7 @@
"cancelWarning": "You are about to cancel the order {{id}}. This action cannot be undone.",
"onDateFromSalesChannel": "{{date}} from {{salesChannel}}",
"summary": {
"requestReturn": "Request return",
"allocateItems": "Allocate items",
"editItems": "Edit items"
},
Expand All @@ -257,6 +261,21 @@
"requiresAction": "Requires action"
}
},
"returns": {
"chooseItems": "Choose items",
"refundAmount": "Refund amount",
"locationDescription": "Choose which location you want to return the items to.",
"shippingDescription": "Choose which method you want to use for this return.",
"noInventoryLevel": "No inventory level",
"sendNotification": "Send notification",
"sendNotificationHint": "Notify customer of created return.",
"customRefund": "Custom refund",
"shippingPriceTooltip": "Shipping needs to be selected",
"customRefundHint": "If you want to refund something else instead of the total refund.",
"customShippingPrice": "Custom shipping",
"customShippingPriceHint": "Custom shipping cost.",
"noInventoryLevelDesc": "The selected location does not have an inventory level for the selected items. The return can be requested but can’t be received until an inventory level is created for the selected location."
},
"reservations": {
"allocatedLabel": "Allocated",
"notAllocatedLabel": "Not allocated"
Expand Down Expand Up @@ -708,6 +727,8 @@
"limit": "Limit",
"tags": "Tags",
"type": "Type",
"reason" : "Reason",
"note": "Note",
"none": "none",
"all": "all",
"percentage": "Percentage",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ export const v1Routes: RouteObject[] = [
lazy: () =>
import("../../routes/orders/order-transfer-ownership"),
},
{
path: "returns",
lazy: () => import("../../routes/orders/returns-create"),
},
],
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Buildings, PencilSquare } from "@medusajs/icons"
import { Buildings, PencilSquare, ArrowUturnLeft } from "@medusajs/icons"
import { LineItem, Order } from "@medusajs/medusa"
import { ReservationItemDTO } from "@medusajs/types"
import { Container, Copy, Heading, StatusBadge, Text } from "@medusajs/ui"
Expand Down Expand Up @@ -46,6 +46,11 @@ const Header = ({ order }: { order: Order }) => {
to: "#", // TODO: Open modal to allocate items
icon: <Buildings />,
},
{
label: t("orders.summary.requestReturn"),
to: `/orders/${order.id}/returns`,
icon: <ArrowUturnLeft />,
},
],
},
]}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import React, { useEffect, useRef, useState } from "react"
import { useForm } from "react-hook-form"

import { AdminPostOrdersOrderReturnsReq, Order } from "@medusajs/medusa"
import { Button, ProgressStatus, ProgressTabs } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import * as zod from "zod"

import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { ItemsTable } from "../items-table"
import { ReturnsForm } from "./returns-form"
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
import { useAdminRequestReturn, useAdminShippingOptions } from "medusa-react"
import { getErrorMessage } from "@medusajs/admin-ui/ui/src/utils/error-messages.ts"

type CreateReturnsFormProps = {
order: Order
}

enum Tab {
ITEMS = "items",
DETAILS = "details",
}

type StepStatus = {
[key in Tab]: ProgressStatus
}

const CreateReturnSchema = zod.object({
quantity: zod.record(zod.string(), zod.number()),
reason: zod.record(zod.string(), zod.string().optional()),
note: zod.record(zod.string(), zod.string().optional()),
location: zod.string(),
shipping: zod.string(),
send_notification: zod.boolean().optional(),

enable_custom_refund: zod.boolean().optional(),
enable_custom_shipping_price: zod.boolean().optional(),

custom_refund: zod.number().optional(),
custom_shipping_price: zod.number().optional(),
})

export function CreateReturns({ order }: CreateReturnsFormProps) {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()

const [selectedItems, setSelectedItems] = useState([])
const [tab, setTab] = React.useState<Tab>(Tab.ITEMS)
const [isSubmitting, setIsSubmitting] = useState(false)

const { mutateAsync: requestReturnOrder } = useAdminRequestReturn(order.id)

const { shipping_options = [] } = useAdminShippingOptions({
region_id: order.region_id,
is_return: true,
})

const refundableAmount = useRef(0)

// TODO: should we filter fullfilled items only here
const selected = order.items.filter((i) => selectedItems.includes(i.id))

const form = useForm<zod.infer<typeof CreateReturnSchema>>({
defaultValues: {
// Items not selected so we don't know defaults yet
quantity: {},
reason: {},
note: {},

location: "",
shipping: "",
send_notification: !order.no_notification,

enable_custom_refund: false,
enable_custom_shipping_price: false,

custom_refund: 0,
custom_shipping_price: 0,
},
})

const onSubmit = form.handleSubmit(async (data) => {
setIsSubmitting(true)

const items = selected.map((item) => {
const ret = {
item_id: item.id,
quantity: data.quantity[item.id],
}

if (data.reason[item.id]) {
ret["reson_id"] = data.reason[item.id]
}

if (data.note[item.id]) {
ret["note"] = data.note[item.id]
}

return ret
})

let refund = refundableAmount.current

if (data.enable_custom_refund && data.custom_refund) {
const customRefund =
data.custom_refund === "" ? 0 : Number(data.custom_refund)
refund = getDbAmount(customRefund, order.currency_code)
}

const payload: AdminPostOrdersOrderReturnsReq = {
items,
no_notification: !data.send_notification,
refund,
}

if (data.location) {
payload["location_id"] = data.location
}

if (data.shipping) {
const option = shipping_options.find((o) => o.id === data.shipping)!

const taxRate = option?.tax_rates.reduce((acc, curr) => {
return acc + curr.rate / 100
}, 0)

let price = option.price_incl_tax
? Math.round(option.price_incl_tax / (1 + taxRate))
: 0

if (data.enable_custom_shipping_price) {
const customShipping =
data.custom_shipping_price === ""
? 0
: Number(data.custom_shipping_price)
price = getDbAmount(customShipping, order.currency_code)
}

// TODO: do we send shipping if custom refund is set?
payload["return_shipping"] = {
option_id: data.shipping,
price,
}
}

try {
await requestReturnOrder(payload)
handleSuccess(`/orders/${order.id}`)
} finally {
setIsSubmitting(false)
}
})

const [status, setStatus] = React.useState<StepStatus>({
[Tab.ITEMS]: "not-started",
[Tab.DETAILS]: "not-started",
})

const onTabChange = React.useCallback(
async (value: Tab) => {
setTab(value)
},
[tab]
)

// const onBack = React.useCallback(async () => {
// switch (tab) {
// case Tab.ITEMS:
// break
// case Tab.DETAILS:
// setTab(Tab.ITEMS)
// break
// }
// }, [tab])

const onNext = React.useCallback(async () => {
switch (tab) {
case Tab.ITEMS: {
selected.forEach((item) => {
form.setValue(`quantity.${item.id}`, item.quantity)
form.setValue(`reason.${item.id}`, "")
form.setValue(`note.${item.id}`, "")
})
setTab(Tab.DETAILS)
break
}
case Tab.DETAILS:
await onSubmit()
break
}
}, [tab, selected])

const onSelectionChange = (ids: string[]) => {
setSelectedItems(ids)

if (ids.length) {
const state = { ...status }
state[Tab.ITEMS] = "in-progress"
setStatus(state)
}
}

const onRefundableAmountChange = (amount: number) => {
refundableAmount.current = amount
}

useEffect(() => {
if (tab === Tab.DETAILS) {
const state = { ...status }
state[Tab.ITEMS] = "completed"
setStatus({ [Tab.ITEMS]: "completed", [Tab.DETAILS]: "not-started" })
}
}, [tab])

useEffect(() => {
if (form.formState.isDirty) {
const state = { ...status }
state[Tab.ITEMS] = "completed"
setStatus({ [Tab.ITEMS]: "completed", [Tab.DETAILS]: "in-progress" })
}
}, [form.formState.isDirty])

const canMoveToDetails = selectedItems.length

return (
<RouteFocusModal.Form form={form}>
<ProgressTabs
value={tab}
className="h-full"
onValueChange={(tab) => onTabChange(tab as Tab)}
>
<RouteFocusModal.Header className="flex w-full items-center justify-between">
<ProgressTabs.List className="border-ui-border-base -my-2 ml-2 min-w-0 flex-1 border-l">
<ProgressTabs.Trigger
value={Tab.ITEMS}
className="w-full max-w-[200px]"
status={status[Tab.ITEMS]}
disabled={tab === Tab.DETAILS}
>
<span className="w-full cursor-auto overflow-hidden text-ellipsis whitespace-nowrap">
{t("orders.returns.chooseItems")}
</span>
</ProgressTabs.Trigger>
<ProgressTabs.Trigger
value={Tab.DETAILS}
className="w-full max-w-[200px]"
status={status[Tab.DETAILS]}
disabled={!canMoveToDetails}
>
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap">
{t("orders.returns.details")}
</span>
</ProgressTabs.Trigger>
</ProgressTabs.List>
<div className="flex flex-1 items-center justify-end gap-x-2">
<Button
className="whitespace-nowrap"
isLoading={isSubmitting}
onClick={onNext}
disabled={!canMoveToDetails}
type={tab === Tab.DETAILS ? "submit" : "button"}
>
{t(tab === Tab.DETAILS ? "actions.save" : "actions.next")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-[calc(100%-56px)] w-full flex-col items-center overflow-y-auto">
<ProgressTabs.Content value={Tab.ITEMS} className="h-full w-full">
<ItemsTable
items={order.items}
selectedItems={selectedItems}
onSelectionChange={onSelectionChange}
/>
</ProgressTabs.Content>
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
<ReturnsForm
form={form}
items={selected}
order={order}
onRefundableAmountChange={onRefundableAmountChange}
/>
</ProgressTabs.Content>
</RouteFocusModal.Body>
</ProgressTabs>
</RouteFocusModal.Form>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CreateReturns } from "./create-returns"
Loading
Loading