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:
William Bouchard
2025-09-23 11:51:40 -04:00
committed by GitHub
parent 543c9f7d0f
commit 5e827ec95d
39 changed files with 1190 additions and 131 deletions

View File

@@ -0,0 +1,9 @@
---
"@medusajs/admin-shared": patch
"@medusajs/dashboard": patch
"@medusajs/js-sdk": patch
"@medusajs/types": patch
"@medusajs/payment": patch
---
feat(admin-shared,dashboard,js-sdk,types,payment): refund reasons in dashboard

View File

@@ -189,6 +189,11 @@ const RETURN_REASON_INJECTION_ZONES = [
"return_reason.list.after",
] as const
const REFUND_REASON_INJECTION_ZONES = [
"refund_reason.list.before",
"refund_reason.list.after",
] as const
const INVENTORY_ITEM_INJECTION_ZONES = [
"inventory_item.details.before",
"inventory_item.details.after",
@@ -229,5 +234,6 @@ export const INJECTION_ZONES = [
...CAMPAIGN_INJECTION_ZONES,
...TAX_INJECTION_ZONES,
...RETURN_REASON_INJECTION_ZONES,
...REFUND_REASON_INJECTION_ZONES,
...INVENTORY_ITEM_INJECTION_ZONES,
] as const

View File

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

View File

@@ -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 || []),
],
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
export { RefundReasonCreateForm } from "./refund-reason-create-form"

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
import { RouteFocusModal } from "../../../components/modals"
import { RefundReasonCreateForm } from "./components/refund-reason-create-form"
export const RefundReasonCreate = () => {
return (
<RouteFocusModal>
<RefundReasonCreateForm />
</RouteFocusModal>
)
}

View File

@@ -0,0 +1 @@
export { RefundReasonEditForm } from "./refund-reason-edit-form"

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
export { RefundReasonListTable } from "./refund-reason-list-table"

View File

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

View File

@@ -0,0 +1,2 @@
export { refundReasonListLoader as loader } from "./loader"
export { RefundReasonList as Component } from "./refund-reason-list"

View File

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

View File

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

View File

@@ -15,28 +15,28 @@ export class RefundReason {
}
/**
* This method retrieves a list of refund reasons. It sends a request to the
* This method retrieves a list of refund reasons. It sends a request to the
* [List Refund Reasons](https://docs.medusajs.com/api/admin#refund-reasons_getrefundreasons)
* API route.
*
*
* @param query - Filters and pagination configurations.
* @param headers - Headers to pass in the request.
* @returns The paginated list of refund reasons.
*
*
* @example
* To retrieve the list of refund reasons:
*
*
* ```ts
* sdk.admin.refundReason.list()
* .then(({ refund_reasons, count, limit, offset }) => {
* console.log(refund_reasons)
* })
* ```
*
*
* To configure the pagination, pass the `limit` and `offset` query parameters.
*
*
* For example, to retrieve only 10 items and skip 10 items:
*
*
* ```ts
* sdk.admin.refundReason.list({
* limit: 10,
@@ -46,23 +46,26 @@ export class RefundReason {
* console.log(refund_reasons)
* })
* ```
*
*
* Using the `fields` query parameter, you can specify the fields and relations to retrieve
* in each refund reason:
*
*
* ```ts
* sdk.admin.refundReason.list({
* fields: "id,name"
* fields: "id,label"
* })
* .then(({ refund_reasons, count, limit, offset }) => {
* console.log(refund_reasons)
* })
* ```
*
*
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/admin#select-fields-and-relations).
*
*
*/
async list(query?: HttpTypes.RefundReasonFilters, headers?: ClientHeaders) {
async list(
query?: HttpTypes.AdminRefundReasonListParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.RefundReasonsResponse>(
`/admin/refund-reasons`,
{
@@ -71,4 +74,154 @@ export class RefundReason {
}
)
}
/**
* This method retrieves a refund reason by ID. It sends a request to the
* [Get Refund Reason](https://docs.medusajs.com/api/admin#refund-reasons_getrefundreasonsid)
* API route.
*
* @param id - The refund reason's ID.
* @param query - Configure the fields and relations to retrieve in the refund reason.
* @param headers - Headers to pass in the request.
* @returns The refund reason's details.
*
* @example
* To retrieve a refund reason by its ID:
*
* ```ts
* sdk.admin.refundReason.retrieve("refr_123")
* .then(({ refund_reason }) => {
* console.log(refund_reason)
* })
* ```
*
* To specify the fields and relations to retrieve:
*
* ```ts
* sdk.admin.refundReason.retrieve("refr_123", {
* fields: "id,value"
* })
* .then(({ refund_reason }) => {
* console.log(refund_reason)
* })
* ```
*
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/admin#select-fields-and-relations).
*/
async retrieve(
id: string,
query?: HttpTypes.AdminRefundReasonParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminRefundReasonResponse>(
`/admin/refund-reasons/${id}`,
{
query,
headers,
}
)
}
/**
* This method creates a refund reason. It sends a request to the
* [Create Refund Reason](https://docs.medusajs.com/api/admin#refund-reasons_postrefundreasons)
* API route.
*
* @param body - The details of the refund reason to create.
* @param query - Configure the fields and relations to retrieve in the refund reason.
* @param headers - Headers to pass in the request.
* @returns The refund reason's details.
*
* @example
* sdk.admin.refundReason.create({
* value: "refund",
* label: "Refund",
* })
* .then(({ refund_reason }) => {
* console.log(refund_reason)
* })
*/
async create(
body: HttpTypes.AdminCreateRefundReason,
query?: HttpTypes.AdminRefundReasonParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminRefundReasonResponse>(
`/admin/refund-reasons`,
{
method: "POST",
headers,
body,
query,
}
)
}
/**
* This method updates a refund reason. It sends a request to the
* [Update Refund Reason](https://docs.medusajs.com/api/admin#refund-reasons_postrefundreasonsid)
* API route.
*
* @param id - The refund reason's ID.
* @param body - The details of the refund reason to update.
* @param query - Configure the fields and relations to retrieve in the refund reason.
* @param headers - Headers to pass in the request.
* @returns The refund reason's details.
*
* @example
* sdk.admin.refundReason.update("ret_123", {
* value: "refund",
* label: "Refund",
* })
* .then(({ refund_reason }) => {
* console.log(refund_reason)
* })
*/
async update(
id: string,
body: HttpTypes.AdminUpdateRefundReason,
query?: HttpTypes.AdminRefundReasonParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminRefundReasonResponse>(
`/admin/refund-reasons/${id}`,
{
method: "POST",
headers,
body,
query,
}
)
}
/**
* This method deletes a refund reason. It sends a request to the
* [Delete Refund Reason](https://docs.medusajs.com/api/admin#refund-reasons_deleterefundreasonsid)
* API route.
*
* @param id - The refund reason's ID.
* @param query - Query parameters to pass to the request.
* @param headers - Headers to pass in the request.
* @returns The deletion's details.
*
* @example
* sdk.admin.refundReason.delete("ret_123")
* .then(({ deleted }) => {
* console.log(deleted)
* })
*/
async delete(
id: string,
query?: HttpTypes.AdminRefundReasonParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminRefundReasonDeleteResponse>(
`/admin/refund-reasons/${id}`,
{
method: "DELETE",
headers,
query,
}
)
}
}

View File

@@ -30,6 +30,7 @@ export * from "./product-category"
export * from "./product-tag"
export * from "./product-type"
export * from "./promotion"
export * from "./refund-reason"
export * from "./region"
export * from "./reservation"
export * from "./return"

View File

@@ -4,7 +4,6 @@ import {
BasePaymentProvider,
BasePaymentSession,
BaseRefund,
RefundReason,
} from "../common"
export interface AdminPaymentProvider extends BasePaymentProvider {
@@ -49,4 +48,3 @@ export interface AdminPaymentSession extends BasePaymentSession {
payment_collection?: AdminPaymentCollection
}
export interface AdminRefund extends BaseRefund {}
export interface AdminRefundReason extends RefundReason {}

View File

@@ -22,11 +22,6 @@ export interface AdminRefundPayment {
note?: string | null
}
export interface AdminCreateRefundReason {
label: string
description?: string
}
export interface AdminCreatePaymentCollection {
/**
* The ID of the order this payment collection belongs to.

View File

@@ -5,7 +5,7 @@ import {
BasePaymentFilters,
BasePaymentSessionFilters,
} from "../common"
import { AdminRefundReason } from "./entities"
import { AdminRefundReason } from "../../refund-reason"
export interface AdminPaymentProviderFilters
extends FindParams,

View File

@@ -4,8 +4,8 @@ import {
AdminPaymentCollection,
AdminPaymentProvider,
AdminRefund,
AdminRefundReason,
} from "./entities"
import { AdminRefundReason } from "../../refund-reason"
export interface AdminPaymentCollectionResponse {
/**
@@ -60,5 +60,3 @@ export type AdminPaymentProviderListResponse = PaginatedResponse<{
*/
payment_providers: AdminPaymentProvider[]
}>
export type AdminRefundReasonDeleteResponse = DeleteResponse<"refund_reason">

View File

@@ -1,4 +1,5 @@
import { BaseFilterable, OperatorMap } from "../../dal"
import { BaseRefundReason } from "../refund-reason/common"
/**
* The payment collection's status.
@@ -254,7 +255,7 @@ export interface BaseRefund {
/**
* The id of the refund_reason that is associated with the refund
*/
refund_reason?: RefundReason | null
refund_reason?: BaseRefundReason | null
/**
* A field to add some additional information about the refund
@@ -337,33 +338,6 @@ export interface BasePaymentSession {
payment?: BasePayment
}
export interface RefundReason {
/**
* The ID of the refund reason
*/
id: string
/**
* The label of the refund reason
*/
label: string
/**
* The description of the refund reason
*/
description?: string | null
/**
* The metadata of the refund reason
*/
metadata: Record<string, unknown> | null
/**
* When the refund reason was created
*/
created_at: Date | string
/**
* When the refund reason was updated
*/
updated_at: Date | string
}
/**
* The filters to apply on the retrieved payment collection.
*/

View File

@@ -0,0 +1,3 @@
import { BaseRefundReason } from "../common"
export interface AdminRefundReason extends BaseRefundReason {}

View File

@@ -0,0 +1,4 @@
export * from "./entities"
export * from "./payloads"
export * from "./queries"
export * from "./responses"

View File

@@ -0,0 +1,21 @@
type AdminBaseRefundReasonPayload = {
/**
* The refund reason's label.
*
* @example
* "Refund"
*/
label: string
/**
* The refund reason's description.
*/
description?: string
/**
* Custom key-value pairs that can be added to the refund reason.
*/
metadata?: Record<string, unknown> | null
}
export interface AdminCreateRefundReason extends AdminBaseRefundReasonPayload {}
export interface AdminUpdateRefundReason extends AdminBaseRefundReasonPayload {}

View File

@@ -0,0 +1,14 @@
import { BaseFilterable, OperatorMap } from "../../../dal"
import { SelectParams } from "../../common"
import { BaseRefundReasonListParams } from "../common"
export interface AdminRefundReasonListParams
extends BaseRefundReasonListParams,
BaseFilterable<AdminRefundReasonListParams> {
/**
* Apply filters on the refund reason's deletion date.
*/
deleted_at?: OperatorMap<string>
}
export interface AdminRefundReasonParams extends SelectParams {}

View File

@@ -0,0 +1,20 @@
import { DeleteResponse, PaginatedResponse } from "../../common"
import { AdminRefundReason } from "./entities"
export interface AdminRefundReasonResponse {
/**
* The refund reason's details.
*/
refund_reason: AdminRefundReason
}
export interface AdminRefundReasonListResponse
extends PaginatedResponse<{
/**
* The list of refund reasons.
*/
refund_reasons: AdminRefundReason[]
}> {}
export interface AdminRefundReasonDeleteResponse
extends DeleteResponse<"refund_reason"> {}

View File

@@ -0,0 +1,42 @@
import { OperatorMap } from "../../dal"
import { FindParams } from "../common"
export interface BaseRefundReason {
/**
* The refund reason's ID.
*/
id: string
/**
* The refund reason's label.
*
* @example
* "Refund"
*/
label: string
/**
* The refund reason's description.
*/
description?: string | null
/**
* Custom key-value pairs that can be added to the refund reason.
*/
metadata?: Record<string, any> | null
/**
* The date that the refund reason was created.
*/
created_at: string
/**
* The date that the refund reason was updated.
*/
updated_at: string
}
export interface BaseRefundReasonListParams extends FindParams {
q?: string
id?: string | string[]
label?: string | OperatorMap<string>
description?: string | OperatorMap<string>
parent_refund_reason_id?: string | OperatorMap<string | string[]>
created_at?: OperatorMap<string>
updated_at?: OperatorMap<string>
}

View File

@@ -0,0 +1 @@
export * from "./admin"

View File

@@ -4,7 +4,7 @@ import Refund from "./refund"
const RefundReason = model.define("RefundReason", {
id: model.id({ prefix: "refr" }).primaryKey(),
label: model.text().searchable(),
description: model.text().nullable(),
description: model.text().searchable().nullable(),
metadata: model.json().nullable(),
refunds: model.hasMany(() => Refund, {
mappedBy: "refund_reason",