feat(dashboard): Return Reasons domain (#6640)

**What**
- Adds Return Reason domain.
This commit is contained in:
Kasper Fabricius Kristensen
2024-03-11 10:10:56 +01:00
committed by GitHub
parent fb25471e92
commit b8bedb84cf
17 changed files with 494 additions and 3 deletions

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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 />,

View File

@@ -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>
)
}

View File

@@ -0,0 +1 @@
export * from "./create-return-reason-form"

View File

@@ -0,0 +1 @@
export { ReturnReasonCreate as Component } from "./return-reason-create"

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1 @@
export * from "./edit-return-reason-form"

View File

@@ -0,0 +1 @@
export { ReturnReasonEdit as Component } from "./return-reason-edit"

View File

@@ -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>
)
}

View File

@@ -0,0 +1 @@
export * from "./return-reason-callout"

View File

@@ -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>
)
}

View File

@@ -0,0 +1 @@
export * from "./return-reason-overview"

View File

@@ -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>
)
}

View File

@@ -0,0 +1 @@
export { ReturnReasonList as Component } from "./return-reason-list"

View File

@@ -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>
)
}