diff --git a/.changeset/quick-rice-deny.md b/.changeset/quick-rice-deny.md new file mode 100644 index 0000000000..1abd08b519 --- /dev/null +++ b/.changeset/quick-rice-deny.md @@ -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 diff --git a/packages/admin/admin-shared/src/extensions/widgets/constants.ts b/packages/admin/admin-shared/src/extensions/widgets/constants.ts index 0137c4214f..eab257487e 100644 --- a/packages/admin/admin-shared/src/extensions/widgets/constants.ts +++ b/packages/admin/admin-shared/src/extensions/widgets/constants.ts @@ -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 diff --git a/packages/admin/dashboard/src/components/layout/settings-layout/settings-layout.tsx b/packages/admin/dashboard/src/components/layout/settings-layout/settings-layout.tsx index dfdc4900c5..e3951ea657 100644 --- a/packages/admin/dashboard/src/components/layout/settings-layout/settings-layout.tsx +++ b/packages/admin/dashboard/src/components/layout/settings-layout/settings-layout.tsx @@ -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", diff --git a/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx b/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx index da0c7fb6f1..23c39c6839 100644 --- a/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx +++ b/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx @@ -1780,6 +1780,42 @@ export function getRouteMap({ }, ], }, + { + path: "refund-reasons", + element: , + 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 || []), ], }, diff --git a/packages/admin/dashboard/src/hooks/api/refund-reasons.tsx b/packages/admin/dashboard/src/hooks/api/refund-reasons.tsx index db889056f0..d63c20a4c2 100644 --- a/packages/admin/dashboard/src/hooks/api/refund-reasons.tsx +++ b/packages/admin/dashboard/src/hooks/api/refund-reasons.tsx @@ -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, + }) +} diff --git a/packages/admin/dashboard/src/hooks/table/columns/index.ts b/packages/admin/dashboard/src/hooks/table/columns/index.ts index bbeb3d4d62..0095d324b4 100644 --- a/packages/admin/dashboard/src/hooks/table/columns/index.ts +++ b/packages/admin/dashboard/src/hooks/table/columns/index.ts @@ -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" diff --git a/packages/admin/dashboard/src/hooks/table/columns/use-refund-reason-table-columns.tsx b/packages/admin/dashboard/src/hooks/table/columns/use-refund-reason-table-columns.tsx new file mode 100644 index 0000000000..ba9f27691f --- /dev/null +++ b/packages/admin/dashboard/src/hooks/table/columns/use-refund-reason-table-columns.tsx @@ -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() + +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 }) => , + enableSorting: true, + sortLabel: t("fields.description"), + sortAscLabel: t("filters.sorting.alphabeticallyAsc"), + sortDescLabel: t("filters.sorting.alphabeticallyDesc"), + }), + ], + [t] + ) +} diff --git a/packages/admin/dashboard/src/hooks/table/query/index.ts b/packages/admin/dashboard/src/hooks/table/query/index.ts index fb5f55c912..92461c3106 100644 --- a/packages/admin/dashboard/src/hooks/table/query/index.ts +++ b/packages/admin/dashboard/src/hooks/table/query/index.ts @@ -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" diff --git a/packages/admin/dashboard/src/hooks/table/query/use-refund-reason-table-query.tsx b/packages/admin/dashboard/src/hooks/table/query/use-refund-reason-table-query.tsx new file mode 100644 index 0000000000..578a88b802 --- /dev/null +++ b/packages/admin/dashboard/src/hooks/table/query/use-refund-reason-table-query.tsx @@ -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, + } +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index 8a3b0b2417..41209b6049 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -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 } diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 3088c16fc7..30eb795f18 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -52,6 +52,7 @@ "noSearchResultsFor": "No search results for <0>'{{query}}'", "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", "title": "Welcome to Medusa", diff --git a/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/index.ts b/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/index.ts new file mode 100644 index 0000000000..732708c4de --- /dev/null +++ b/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/index.ts @@ -0,0 +1 @@ +export { RefundReasonCreateForm } from "./refund-reason-create-form" diff --git a/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/refund-reason-create-form.tsx b/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/refund-reason-create-form.tsx new file mode 100644 index 0000000000..f1852fefb7 --- /dev/null +++ b/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/refund-reason-create-form.tsx @@ -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>({ + 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 ( + + + + +
+
+ + {t("refundReasons.create.header")} + + + + {t("refundReasons.create.subtitle")} + + +
+
+ { + return ( + + + {t("refundReasons.fields.label.label")} + + + + + + + ) + }} + /> +
+ { + return ( + + + {t("refundReasons.fields.description.label")} + + +