feat(admin-shared,dashboard,js-sdk,types): refund reasons in dashboard (#13575)
CLOSES CORE-1209 This PR just adds the stuff necessary to support refund reasons in the dashboard. It adds the option in the settings tab and allows viewing, creating, editing and deleting refund reasons. I hate to open such a big PR but most of it is copy pasted from the return reasons. Major difference is only the fact that refund reasons don't have a `value` field
This commit is contained in:
@@ -43,6 +43,10 @@ const useSettingRoutes = (): INavItem[] => {
|
||||
label: t("returnReasons.domain"),
|
||||
to: "/settings/return-reasons",
|
||||
},
|
||||
{
|
||||
label: t("refundReasons.domain"),
|
||||
to: "/settings/refund-reasons",
|
||||
},
|
||||
{
|
||||
label: t("salesChannels.domain"),
|
||||
to: "/settings/sales-channels",
|
||||
|
||||
@@ -1780,6 +1780,42 @@ export function getRouteMap({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "refund-reasons",
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
breadcrumb: () => t("refundReasons.domain"),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () =>
|
||||
import("../../routes/refund-reasons/refund-reason-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/refund-reasons/refund-reason-create"
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
path: ":id",
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/refund-reasons/refund-reason-edit"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...(settingsRoutes?.[0]?.children || []),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,29 +1,124 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { FetchError } from "@medusajs/js-sdk"
|
||||
import {
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
} from "@tanstack/react-query"
|
||||
|
||||
const REFUND_REASON_QUERY_KEY = "refund-reason" as const
|
||||
export const refundReasonQueryKeys = queryKeysFactory(REFUND_REASON_QUERY_KEY)
|
||||
import { FetchError } from "@medusajs/js-sdk"
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const REFUND_REASONS_QUERY_KEY = "refund_reasons" as const
|
||||
export const refundReasonsQueryKeys = queryKeysFactory(REFUND_REASONS_QUERY_KEY)
|
||||
|
||||
export const useRefundReasons = (
|
||||
query?: HttpTypes.RefundReasonFilters,
|
||||
query?: HttpTypes.AdminRefundReasonListParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.RefundReasonsResponse,
|
||||
HttpTypes.AdminRefundReasonListResponse,
|
||||
FetchError,
|
||||
HttpTypes.RefundReasonsResponse,
|
||||
QueryKey
|
||||
HttpTypes.AdminRefundReasonListResponse
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => sdk.admin.refundReason.list(query),
|
||||
queryKey: [],
|
||||
queryKey: refundReasonsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useRefundReason = (
|
||||
id: string,
|
||||
query?: HttpTypes.AdminRefundReasonParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminRefundReasonResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminRefundReasonResponse
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => sdk.admin.refundReason.retrieve(id, query),
|
||||
queryKey: refundReasonsQueryKeys.detail(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateRefundReason = (
|
||||
query?: HttpTypes.AdminRefundReasonParams,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.RefundReasonResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminCreateRefundReason
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: async (data) => sdk.admin.refundReason.create(data, query),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: refundReasonsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateRefundReason = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminRefundReasonResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminUpdateRefundReason
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: async (data) => sdk.admin.refundReason.update(id, data),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: refundReasonsQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: refundReasonsQueryKeys.detail(data.refund_reason.id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteRefundReasonLazy = (
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminRefundReasonDeleteResponse,
|
||||
FetchError,
|
||||
string
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => sdk.admin.refundReason.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: refundReasonsQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: refundReasonsQueryKeys.details(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from "./use-order-table-columns"
|
||||
export * from "./use-product-table-columns"
|
||||
export * from "./use-product-tag-table-columns"
|
||||
export * from "./use-product-type-table-columns"
|
||||
export * from "./use-refund-reason-table-columns"
|
||||
export * from "./use-region-table-columns"
|
||||
export * from "./use-return-reason-table-columns"
|
||||
export * from "./use-tax-rates-table-columns"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { createDataTableColumnHelper } from "@medusajs/ui"
|
||||
import { DescriptionCell } from "../../../components/table/table-cells/sales-channel/description-cell"
|
||||
|
||||
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminRefundReason>()
|
||||
|
||||
export const useRefundReasonTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("label", {
|
||||
header: () => t("fields.label"),
|
||||
enableSorting: true,
|
||||
sortLabel: t("fields.label"),
|
||||
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
|
||||
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
|
||||
}),
|
||||
columnHelper.accessor("description", {
|
||||
header: () => t("fields.description"),
|
||||
cell: ({ getValue }) => <DescriptionCell description={getValue()} />,
|
||||
enableSorting: true,
|
||||
sortLabel: t("fields.description"),
|
||||
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
|
||||
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from "./use-order-table-query"
|
||||
export * from "./use-product-table-query"
|
||||
export * from "./use-product-tag-table-query"
|
||||
export * from "./use-product-type-table-query"
|
||||
export * from "./use-refund-reason-table-query"
|
||||
export * from "./use-region-table-query"
|
||||
export * from "./use-return-reason-table-query"
|
||||
export * from "./use-shipping-option-table-query"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useQueryParams } from "../../use-query-params"
|
||||
|
||||
type UseRefundReasonTableQueryProps = {
|
||||
prefix?: string
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const useRefundReasonTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 20,
|
||||
}: UseRefundReasonTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
["offset", "q", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, q, order, created_at, updated_at } = queryObject
|
||||
const searchParams: HttpTypes.AdminRefundReasonListParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
order,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
q,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw: queryObject,
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,9 @@
|
||||
"noRecordsMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"noRecordsMessageFiltered": {
|
||||
"type": "string"
|
||||
},
|
||||
"unsavedChangesTitle": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -229,6 +232,7 @@
|
||||
"noSearchResultsFor",
|
||||
"noRecordsTitle",
|
||||
"noRecordsMessage",
|
||||
"noRecordsMessageFiltered",
|
||||
"unsavedChangesTitle",
|
||||
"unsavedChangesDescription",
|
||||
"includesTaxTooltip",
|
||||
@@ -10202,6 +10206,115 @@
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"refundReasons": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"calloutHint": {
|
||||
"type": "string"
|
||||
},
|
||||
"editReason": {
|
||||
"type": "string"
|
||||
},
|
||||
"create": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"type": "string"
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"hint": {
|
||||
"type": "string"
|
||||
},
|
||||
"successToast": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["header", "subtitle", "hint", "successToast"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"edit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"type": "string"
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"successToast": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["header", "subtitle", "successToast"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"delete": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirmation": {
|
||||
"type": "string"
|
||||
},
|
||||
"successToast": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["confirmation", "successToast"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["label", "placeholder"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["label", "placeholder"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["label", "description"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"domain",
|
||||
"subtitle",
|
||||
"calloutHint",
|
||||
"editReason",
|
||||
"create",
|
||||
"edit",
|
||||
"delete",
|
||||
"fields"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"login": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11554,70 +11667,6 @@
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"views": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"save": {
|
||||
"type": "string"
|
||||
},
|
||||
"saveAsNew": {
|
||||
"type": "string"
|
||||
},
|
||||
"updateDefaultForEveryone": {
|
||||
"type": "string"
|
||||
},
|
||||
"updateViewName": {
|
||||
"type": "string"
|
||||
},
|
||||
"prompts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"updateDefault": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"confirmText": {
|
||||
"type": "string"
|
||||
},
|
||||
"cancelText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description", "confirmText", "cancelText"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"updateView": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"confirmText": {
|
||||
"type": "string"
|
||||
},
|
||||
"cancelText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description", "confirmText", "cancelText"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["updateDefault", "updateView"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["save", "saveAsNew", "updateDefaultForEveryone", "updateViewName", "prompts"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"dateTime": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11681,6 +11730,76 @@
|
||||
"seconds_other"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"views": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"save": {
|
||||
"type": "string"
|
||||
},
|
||||
"saveAsNew": {
|
||||
"type": "string"
|
||||
},
|
||||
"updateDefaultForEveryone": {
|
||||
"type": "string"
|
||||
},
|
||||
"updateViewName": {
|
||||
"type": "string"
|
||||
},
|
||||
"prompts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"updateDefault": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"confirmText": {
|
||||
"type": "string"
|
||||
},
|
||||
"cancelText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description", "confirmText", "cancelText"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"updateView": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"confirmText": {
|
||||
"type": "string"
|
||||
},
|
||||
"cancelText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description", "confirmText", "cancelText"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["updateDefault", "updateView"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"save",
|
||||
"saveAsNew",
|
||||
"updateDefaultForEveryone",
|
||||
"updateViewName",
|
||||
"prompts"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11724,6 +11843,7 @@
|
||||
"salesChannels",
|
||||
"apiKeyManagement",
|
||||
"returnReasons",
|
||||
"refundReasons",
|
||||
"login",
|
||||
"invite",
|
||||
"resetPassword",
|
||||
@@ -11736,8 +11856,8 @@
|
||||
"statuses",
|
||||
"labels",
|
||||
"fields",
|
||||
"views",
|
||||
"dateTime"
|
||||
"dateTime",
|
||||
"views"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"noSearchResultsFor": "No search results for <0>'{{query}}'</0>",
|
||||
"noRecordsTitle": "No records",
|
||||
"noRecordsMessage": "There are no records to show",
|
||||
"noRecordsMessageFiltered": "There are no records to show matching the filters",
|
||||
"unsavedChangesTitle": "Are you sure you want to leave this form?",
|
||||
"unsavedChangesDescription": "You have unsaved changes that will be lost if you exit this form.",
|
||||
"includesTaxTooltip": "Prices in this column are tax inclusive.",
|
||||
@@ -2740,6 +2741,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"refundReasons": {
|
||||
"domain": "Refund Reasons",
|
||||
"subtitle": "Manage reasons for issuing refunds.",
|
||||
"calloutHint": "Manage the reasons to categorize refunds.",
|
||||
"editReason": "Edit Refund Reason",
|
||||
"create": {
|
||||
"header": "Add Refund Reason",
|
||||
"subtitle": "Specify the most common reasons for refunds.",
|
||||
"hint": "Create a new refund reason to categorize refunds.",
|
||||
"successToast": "Refund reason {{label}} was successfully created."
|
||||
},
|
||||
"edit": {
|
||||
"header": "Edit Refund Reason",
|
||||
"subtitle": "Edit the value of the refund reason.",
|
||||
"successToast": "Refund reason {{label}} was successfully updated."
|
||||
},
|
||||
"delete": {
|
||||
"confirmation": "You are about to delete the refund reason \"{{label}}\". This action cannot be undone.",
|
||||
"successToast": "Refund reason was successfully deleted."
|
||||
},
|
||||
"fields": {
|
||||
"label": {
|
||||
"label": "Label",
|
||||
"placeholder": "Gesture of goodwill"
|
||||
},
|
||||
"description": {
|
||||
"label": "Description",
|
||||
"placeholder": "Customer had a bad shopping experience"
|
||||
}
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"forgotPassword": "Forgot password? - <0>Reset</0>",
|
||||
"title": "Welcome to Medusa",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { RefundReasonCreateForm } from "./refund-reason-create-form"
|
||||
@@ -0,0 +1,131 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, Heading, Input, Text, Textarea, toast } from "@medusajs/ui"
|
||||
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/modals"
|
||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||
import { useCreateRefundReason } from "../../../../../hooks/api"
|
||||
|
||||
const RefundReasonCreateSchema = z.object({
|
||||
label: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export const RefundReasonCreateForm = () => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<z.infer<typeof RefundReasonCreateSchema>>({
|
||||
defaultValues: {
|
||||
label: "",
|
||||
description: "",
|
||||
},
|
||||
resolver: zodResolver(RefundReasonCreateSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useCreateRefundReason()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: ({ refund_reason }) => {
|
||||
toast.success(
|
||||
t("refundReasons.create.successToast", {
|
||||
label: refund_reason.label,
|
||||
})
|
||||
)
|
||||
handleSuccess(`../`)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<KeyboundForm
|
||||
className="flex size-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RouteFocusModal.Header />
|
||||
<RouteFocusModal.Body className="flex flex-1 justify-center overflow-auto px-6 py-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<RouteFocusModal.Title asChild>
|
||||
<Heading>{t("refundReasons.create.header")}</Heading>
|
||||
</RouteFocusModal.Title>
|
||||
<RouteFocusModal.Description asChild>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("refundReasons.create.subtitle")}
|
||||
</Text>
|
||||
</RouteFocusModal.Description>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="label"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("refundReasons.fields.label.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"refundReasons.fields.label.placeholder"
|
||||
)}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("refundReasons.fields.description.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"refundReasons.fields.description.placeholder"
|
||||
)}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
<RouteFocusModal.Footer>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary" type="button">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Footer>
|
||||
</KeyboundForm>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { RefundReasonCreate as Component } from "./refund-reason-create"
|
||||
@@ -0,0 +1,10 @@
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import { RefundReasonCreateForm } from "./components/refund-reason-create-form"
|
||||
|
||||
export const RefundReasonCreate = () => {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<RefundReasonCreateForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { RefundReasonEditForm } from "./refund-reason-edit-form"
|
||||
@@ -0,0 +1,119 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Button, Input, Textarea, toast } from "@medusajs/ui"
|
||||
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/modals"
|
||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||
import { useUpdateRefundReason } from "../../../../../hooks/api/refund-reasons"
|
||||
|
||||
type RefundReasonEditFormProps = {
|
||||
refundReason: HttpTypes.AdminRefundReason
|
||||
}
|
||||
|
||||
const RefundReasonEditSchema = z.object({
|
||||
label: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export const RefundReasonEditForm = ({
|
||||
refundReason,
|
||||
}: RefundReasonEditFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<z.infer<typeof RefundReasonEditSchema>>({
|
||||
defaultValues: {
|
||||
label: refundReason.label,
|
||||
description: refundReason.description ?? undefined,
|
||||
},
|
||||
resolver: zodResolver(RefundReasonEditSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useUpdateRefundReason(refundReason.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: ({ refund_reason }) => {
|
||||
toast.success(
|
||||
t("refundReasons.edit.successToast", {
|
||||
label: refund_reason.label,
|
||||
})
|
||||
)
|
||||
handleSuccess()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<KeyboundForm
|
||||
className="flex size-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-4 overflow-auto">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="label"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("refundReasons.fields.label.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("refundReasons.fields.label.placeholder")}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("refundReasons.fields.description.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"refundReasons.fields.description.placeholder"
|
||||
)}
|
||||
/>
|
||||
</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" type="button">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</KeyboundForm>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { RefundReasonEdit as Component } from "./refund-reason-edit"
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteDrawer } from "../../../components/modals"
|
||||
import { useRefundReason } from "../../../hooks/api/refund-reasons"
|
||||
import { RefundReasonEditForm } from "./components/refund-reason-edit-form"
|
||||
|
||||
export const RefundReasonEdit = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { refund_reason, isPending, isError, error } = useRefundReason(id!)
|
||||
|
||||
const ready = !isPending && !!refund_reason
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<RouteDrawer.Title asChild>
|
||||
<Heading>{t("refundReasons.edit.header")}</Heading>
|
||||
</RouteDrawer.Title>
|
||||
<RouteDrawer.Description className="sr-only">
|
||||
{t("refundReasons.edit.subtitle")}
|
||||
</RouteDrawer.Description>
|
||||
</RouteDrawer.Header>
|
||||
{ready && <RefundReasonEditForm refundReason={refund_reason} />}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { RefundReasonListTable } from "./refund-reason-list-table"
|
||||
@@ -0,0 +1,130 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Container, createDataTableColumnHelper, toast, usePrompt, } from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useCallback, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { DataTable } from "../../../../../components/data-table"
|
||||
import { useDeleteRefundReasonLazy, useRefundReasons, } from "../../../../../hooks/api"
|
||||
import { useRefundReasonTableColumns } from "../../../../../hooks/table/columns"
|
||||
import { useRefundReasonTableQuery } from "../../../../../hooks/table/query"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const RefundReasonListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
const { searchParams } = useRefundReasonTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const { refund_reasons, count, isPending, isError, error } = useRefundReasons(
|
||||
searchParams,
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y px-0 py-0">
|
||||
<DataTable
|
||||
data={refund_reasons}
|
||||
columns={columns}
|
||||
rowCount={count}
|
||||
pageSize={PAGE_SIZE}
|
||||
getRowId={(row) => row.id}
|
||||
heading={t("refundReasons.domain")}
|
||||
subHeading={t("refundReasons.subtitle")}
|
||||
emptyState={{
|
||||
empty: {
|
||||
heading: t("general.noRecordsMessage"),
|
||||
},
|
||||
filtered: {
|
||||
heading: t("general.noRecordsMessage"),
|
||||
description: t("general.noRecordsMessageFiltered"),
|
||||
},
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
label: t("actions.create"),
|
||||
to: "create",
|
||||
},
|
||||
]}
|
||||
isLoading={isPending}
|
||||
enableSearch={true}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminRefundReason>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const navigate = useNavigate()
|
||||
const base = useRefundReasonTableColumns()
|
||||
|
||||
const { mutateAsync } = useDeleteRefundReasonLazy()
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (refundReason: HttpTypes.AdminRefundReason) => {
|
||||
const confirm = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("refundReasons.delete.confirmation", {
|
||||
label: refundReason.label,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(refundReason.id, {
|
||||
onSuccess: () => {
|
||||
toast.success(t("refundReasons.delete.successToast"))
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(e.message)
|
||||
},
|
||||
})
|
||||
},
|
||||
[t, prompt, mutateAsync]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
...base,
|
||||
columnHelper.action({
|
||||
actions: (ctx) => [
|
||||
[
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
onClick: () =>
|
||||
navigate(
|
||||
`/settings/refund-reasons/${ctx.row.original.id}/edit`
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: () => handleDelete(ctx.row.original),
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
],
|
||||
[base, handleDelete, navigate, t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { refundReasonListLoader as loader } from "./loader"
|
||||
export { RefundReasonList as Component } from "./refund-reason-list"
|
||||
@@ -0,0 +1,16 @@
|
||||
import { refundReasonsQueryKeys } from "../../../hooks/api"
|
||||
import { sdk } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/query-client"
|
||||
|
||||
const refundReasonListQuery = () => ({
|
||||
queryKey: refundReasonsQueryKeys.list(),
|
||||
queryFn: async () => sdk.admin.refundReason.list(),
|
||||
})
|
||||
|
||||
export const refundReasonListLoader = async () => {
|
||||
const query = refundReasonListQuery()
|
||||
return (
|
||||
queryClient.getQueryData(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useExtension } from "../../../providers/extension-provider"
|
||||
import { RefundReasonListTable } from "./components/refund-reason-list-table"
|
||||
|
||||
export const RefundReasonList = () => {
|
||||
const { getWidgets } = useExtension()
|
||||
|
||||
return (
|
||||
<SingleColumnPage
|
||||
showMetadata={false}
|
||||
showJSON={false}
|
||||
hasOutlet
|
||||
widgets={{
|
||||
after: getWidgets("refund_reason.list.after"),
|
||||
before: getWidgets("refund_reason.list.before"),
|
||||
}}
|
||||
>
|
||||
<RefundReasonListTable />
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user