feat(dashboard): Return Reasons domain (#6640)
**What** - Adds Return Reason domain.
This commit is contained in:
committed by
GitHub
parent
fb25471e92
commit
b8bedb84cf
@@ -288,9 +288,9 @@
|
||||
"editConditions": "Edit conditions",
|
||||
"conditionsHint": "Create conditions to apply on the discount",
|
||||
"isTemplateDiscount": "Is this a template discount?",
|
||||
"percentageDescription" : "Discount applied in %",
|
||||
"fixedDescription" : "Amount discount",
|
||||
"shippingDescription" : "Override delivery amount",
|
||||
"percentageDescription": "Discount applied in %",
|
||||
"fixedDescription": "Amount discount",
|
||||
"shippingDescription": "Override delivery amount",
|
||||
"selectRegionFirst": "Select region first",
|
||||
"templateHint": "Template discounts allow you to define a set of rules that can be used across a group of discounts. This is useful in campaigns that should generate unique codes for each user, but where the rules for all unique codes should be the same.",
|
||||
"conditions": {
|
||||
@@ -505,6 +505,15 @@
|
||||
"createdBy": "Created by",
|
||||
"revokedBy": "Revoked by"
|
||||
},
|
||||
"returnReasons": {
|
||||
"domain": "Return Reasons",
|
||||
"calloutHint": "Manage the reasons to categorize returns.",
|
||||
"deleteReasonWarning": "You are about to delete the return reason {{label}}. This action cannot be undone.",
|
||||
"createReason": "Create Return Reason",
|
||||
"createReasonHint": "Create a new return reason to categorize returns.",
|
||||
"editReason": "Edit Return Reason",
|
||||
"valueTooltip": "The value should be a unique identifier for the return reason."
|
||||
},
|
||||
"login": {
|
||||
"forgotPassword": "Forgot password? - <0>Reset</0>",
|
||||
"title": "Log in",
|
||||
@@ -696,6 +705,7 @@
|
||||
"maxSubtotal": "Max. Subtotal",
|
||||
"shippingProfile": "Shipping Profile",
|
||||
"summary": "Summary",
|
||||
"label": "Label",
|
||||
"rate": "Rate",
|
||||
"requiresShipping": "Requires shipping"
|
||||
},
|
||||
|
||||
@@ -37,6 +37,10 @@ const useSettingRoutes = (): NavItemProps[] => {
|
||||
label: t("regions.domain"),
|
||||
to: "/settings/regions",
|
||||
},
|
||||
{
|
||||
label: t("returnReasons.domain"),
|
||||
to: "/settings/return-reasons",
|
||||
},
|
||||
{
|
||||
label: "Taxes",
|
||||
to: "/settings/taxes",
|
||||
|
||||
@@ -494,6 +494,34 @@ const router = createBrowserRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "return-reasons",
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Return Reasons",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () =>
|
||||
import("../../routes/return-reasons/return-reason-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/return-reasons/return-reason-create"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ":id/edit",
|
||||
lazy: () =>
|
||||
import("../../routes/return-reasons/return-reason-edit"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "regions",
|
||||
element: <Outlet />,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, Heading, Input, Text, Textarea, clx } from "@medusajs/ui"
|
||||
import { useAdminCreateReturnReason } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
|
||||
const CreateReturnReasonSchema = z.object({
|
||||
value: z.string().min(1, "Value is required"),
|
||||
label: z.string().min(1, "Label is required"),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export const CreateReturnReasonForm = () => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<z.infer<typeof CreateReturnReasonSchema>>({
|
||||
defaultValues: {
|
||||
value: "",
|
||||
label: "",
|
||||
description: "",
|
||||
},
|
||||
resolver: zodResolver(CreateReturnReasonSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreateReturnReason()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: () => {
|
||||
handleSuccess()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button type="submit" size="small" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-hidden">
|
||||
<div
|
||||
className={clx(
|
||||
"flex h-full w-full flex-col items-center overflow-y-auto p-16"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading>{t("returnReasons.createReason")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("returnReasons.createReasonHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="label"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.label")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label tooltip={t("returnReasons.valueTooltip")}>
|
||||
{t("fields.value")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("fields.description")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./create-return-reason-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { ReturnReasonCreate as Component } from "./return-reason-create"
|
||||
@@ -0,0 +1,10 @@
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { CreateReturnReasonForm } from "./components/create-return-reason-form"
|
||||
|
||||
export const ReturnReasonCreate = () => {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateReturnReasonForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { ReturnReason } from "@medusajs/medusa"
|
||||
import { Button, Input, Textarea } from "@medusajs/ui"
|
||||
import { useAdminUpdateReturnReason } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
|
||||
type EditReturnReasonFormProps = {
|
||||
reason: ReturnReason
|
||||
}
|
||||
|
||||
const EditReturnReasonSchema = z.object({
|
||||
value: z.string().min(1, "Value is required"),
|
||||
label: z.string().min(1, "Label is required"),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export const EditReturnReasonForm = ({ reason }: EditReturnReasonFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<z.infer<typeof EditReturnReasonSchema>>({
|
||||
defaultValues: {
|
||||
value: reason.value,
|
||||
label: reason.label,
|
||||
description: reason.description || "",
|
||||
},
|
||||
resolver: zodResolver(EditReturnReasonSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminUpdateReturnReason(reason.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: () => {
|
||||
handleSuccess()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<RouteDrawer.Body className="flex h-full flex-col gap-y-8 overflow-auto">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="label"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.label")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label tooltip={t("returnReasons.valueTooltip")}>
|
||||
{t("fields.value")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.description")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button type="submit" size="small" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./edit-return-reason-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { ReturnReasonEdit as Component } from "./return-reason-edit"
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useAdminReturnReason } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteDrawer } from "../../../components/route-modal"
|
||||
import { EditReturnReasonForm } from "./components/edit-return-reason-form"
|
||||
|
||||
export const ReturnReasonEdit = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
|
||||
const { return_reason, isLoading, isError, error } = useAdminReturnReason(id!)
|
||||
|
||||
const ready = !isLoading && return_reason
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("returnReasons.editReason")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{ready && <EditReturnReasonForm reason={return_reason} />}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./return-reason-callout"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
export const ReturnReasonCallout = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="flex items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
<Heading>Return Reasons</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle text-pretty">
|
||||
Manage reasons for returned items.
|
||||
</Text>
|
||||
</div>
|
||||
<Button variant="secondary" size="small" asChild>
|
||||
<Link to="create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./return-reason-overview"
|
||||
@@ -0,0 +1,121 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { ReturnReason } from "@medusajs/medusa"
|
||||
import { Badge, Container, Text, usePrompt } from "@medusajs/ui"
|
||||
import { useAdminDeleteReturnReason, useAdminReturnReasons } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||
|
||||
export const ReturnReasonOverview = () => {
|
||||
const { return_reasons, isLoading, isError, error } = useAdminReturnReasons()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<ItemSkeleton key={i} />
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (!return_reasons || !return_reasons.length) {
|
||||
return (
|
||||
<Container className="p-0">
|
||||
<NoRecords />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
{return_reasons.map((reason) => (
|
||||
<Item key={reason.id} reason={reason} />
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Item = ({ reason }: { reason: ReturnReason }) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteReturnReason(reason.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("returnReasons.deleteReasonWarning", {
|
||||
label: reason.label,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 items-start px-6 py-4">
|
||||
<Badge size="2xsmall" className="w-fit">
|
||||
{reason.value}
|
||||
</Badge>
|
||||
<div className="grid grid-cols-[1fr_28px] items-start gap-x-3">
|
||||
<div>
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{reason.label}
|
||||
</Text>
|
||||
<Text size="small" className="text-ui-fg-subtle text-pretty">
|
||||
{reason.description}
|
||||
</Text>
|
||||
</div>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: `${reason.id}/edit`,
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ItemSkeleton = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-2 items-start px-6 py-4">
|
||||
<Skeleton className="h-5 w-[90px]" />
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-4 w-[120px]" />
|
||||
<Skeleton className="mt-2 w-3/4" />
|
||||
</div>
|
||||
<Skeleton className="h-7 w-7" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ReturnReasonList as Component } from "./return-reason-list"
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { ReturnReasonCallout } from "./components/return-reason-callout"
|
||||
import { ReturnReasonOverview } from "./components/return-reason-overview"
|
||||
|
||||
export const ReturnReasonList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ReturnReasonCallout />
|
||||
<ReturnReasonOverview />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user