fix(dashboard,medusa): Fixes to Customer and Customer Groups domains (#7081)
**What** - Cleanup of domains - Adds toasts - Adds delete customer hook - Fixes validation of create and update customer endpoints.
This commit is contained in:
1 parent
122b3ea76b
commit
7e66dd0dd0
29 files changed
+636
-390
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(medusa): Fix validation of V2 POST /customers and POST /customers/:id
|
||||||
@@ -43,7 +43,9 @@
|
|||||||
"unsavedChangesTitle": "Are you sure you want to leave this page?",
|
"unsavedChangesTitle": "Are you sure you want to leave this page?",
|
||||||
"unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page.",
|
"unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page.",
|
||||||
"includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved.",
|
"includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved.",
|
||||||
"timeline": "Timeline"
|
"timeline": "Timeline",
|
||||||
|
"success": "Success",
|
||||||
|
"error": "Error"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"mustBeInt": "The value must be a whole number.",
|
"mustBeInt": "The value must be a whole number.",
|
||||||
@@ -353,29 +355,56 @@
|
|||||||
},
|
},
|
||||||
"customers": {
|
"customers": {
|
||||||
"domain": "Customers",
|
"domain": "Customers",
|
||||||
"editCustomer": "Edit Customer",
|
"create": {
|
||||||
"createCustomer": "Create Customer",
|
"header": "Create Customer",
|
||||||
"createCustomerHint": "Create a new customer to manage their details.",
|
"hint": "Create a new customer to manage their details.",
|
||||||
"passwordHint": "Create a password for the customer to use when logging in to the storefront. Make sure that you communicate the password to the customer.",
|
"successToast": "Customer {{email}} was successfully created."
|
||||||
"changePassword": "Change password",
|
},
|
||||||
"changePasswordPromptTitle": "Change password",
|
"edit": {
|
||||||
"changePasswordPromptDescription": "You are about to change the password for {{email}}. Make sure that you have communicated the new password to the customer before proceeding.",
|
"header": "Edit Customer",
|
||||||
"guest": "Guest",
|
"emailDisabledTooltip": "The email address cannot be changed for registered customers.",
|
||||||
"registered": "Registered",
|
"successToast": "Customer {{email}} was successfully updated."
|
||||||
"firstSeen": "First seen",
|
},
|
||||||
"viewOrder": "View order",
|
"delete": {
|
||||||
"groups": "Groups"
|
"title": "Delete Customer",
|
||||||
|
"description": "You are about to delete the customer {{email}}. This action cannot be undone.",
|
||||||
|
"successToast": "Customer {{email}} was successfully deleted."
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"guest": "Guest",
|
||||||
|
"registered": "Registered",
|
||||||
|
"groups": "Groups"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"customerGroups": {
|
"customerGroups": {
|
||||||
"domain": "Customer Groups",
|
"domain": "Customer Groups",
|
||||||
"createGroup": "Create group",
|
"create": {
|
||||||
"createCustomerGroup": "Create Customer Group",
|
"header": "Create Customer Group",
|
||||||
"createCustomerGroupHint": "Create a new customer group to segment your customers.",
|
"hint": "Create a new customer group to segment your customers.",
|
||||||
"customerAlreadyAdded": "The customer has already been added to the group.",
|
"successToast": "Customer group {{name}} was successfully created."
|
||||||
"editCustomerGroup": "Edit Customer Group",
|
},
|
||||||
"removeCustomersWarning_one": "You are about to remove {{count}} customer from the customer group. This action cannot be undone.",
|
"edit": {
|
||||||
"removeCustomersWarning_other": "You are about to remove {{count}} customers from the customer group. This action cannot be undone.",
|
"header": "Edit Customer Group",
|
||||||
"deleteCustomerGroupWarning": "You are about to delete the customer group {{name}}. This action cannot be undone."
|
"successToast": "Customer group {{name}} was successfully updated."
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"title": "Delete Customer Group",
|
||||||
|
"description": "You are about to delete the customer group {{name}}. This action cannot be undone.",
|
||||||
|
"successToast": "Customer group {{name}} was successfully deleted."
|
||||||
|
},
|
||||||
|
"customers": {
|
||||||
|
"alreadyAddedTooltip": "The customer has already been added to the group.",
|
||||||
|
"add": {
|
||||||
|
"successToast_one": "Customer was successfully added to the group.",
|
||||||
|
"successToast_other": "Customers were successfully added to the group."
|
||||||
|
},
|
||||||
|
"remove": {
|
||||||
|
"title_one": "Remove customer",
|
||||||
|
"title_other": "Remove customers",
|
||||||
|
"description_one": "You are about to remove {{count}} customer from the customer group. This action cannot be undone.",
|
||||||
|
"description_other": "You are about to remove {{count}} customers from the customer group. This action cannot be undone."
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"orders": {
|
"orders": {
|
||||||
"domain": "Orders",
|
"domain": "Orders",
|
||||||
@@ -575,7 +604,7 @@
|
|||||||
"addZone": "Add shipping zone",
|
"addZone": "Add shipping zone",
|
||||||
"enablePickup": "Enable pickup",
|
"enablePickup": "Enable pickup",
|
||||||
"enableDelivery": "Enable delivery",
|
"enableDelivery": "Enable delivery",
|
||||||
"noRecords" : {
|
"noRecords": {
|
||||||
"action": "Add Location",
|
"action": "Add Location",
|
||||||
"title": "No inventory locations",
|
"title": "No inventory locations",
|
||||||
"message": "Please create an invnetory location first."
|
"message": "Please create an invnetory location first."
|
||||||
|
|||||||
+8
-2
@@ -1,3 +1,5 @@
|
|||||||
|
import { PlaceholderCell } from "../placeholder-cell"
|
||||||
|
|
||||||
type CellProps = {
|
type CellProps = {
|
||||||
text?: string | number
|
text?: string | number
|
||||||
}
|
}
|
||||||
@@ -7,6 +9,10 @@ type HeaderProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TextCell = ({ text }: CellProps) => {
|
export const TextCell = ({ text }: CellProps) => {
|
||||||
|
if (!text) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
|
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
|
||||||
<span className="truncate">{text}</span>
|
<span className="truncate">{text}</span>
|
||||||
@@ -16,8 +22,8 @@ export const TextCell = ({ text }: CellProps) => {
|
|||||||
|
|
||||||
export const TextHeader = ({ text }: HeaderProps) => {
|
export const TextHeader = ({ text }: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<div className=" flex h-full w-full items-center">
|
<div className="flex h-full w-full items-center">
|
||||||
<span>{text}</span>
|
<span className="truncate">{text}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+3
-1
@@ -9,7 +9,9 @@ export const AccountCell = ({ hasAccount }: AccountCellProps) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const color = hasAccount ? "green" : ("orange" as const)
|
const color = hasAccount ? "green" : ("orange" as const)
|
||||||
const text = hasAccount ? t("customers.registered") : t("customers.guest")
|
const text = hasAccount
|
||||||
|
? t("customers.fields.registered")
|
||||||
|
: t("customers.fields.guest")
|
||||||
|
|
||||||
return <StatusCell color={color}>{text}</StatusCell>
|
return <StatusCell color={color}>{text}</StatusCell>
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -1,11 +1,16 @@
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { DateCell } from "../../common/date-cell"
|
import { DateCell } from "../../common/date-cell"
|
||||||
|
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||||
|
|
||||||
type FirstSeenCellProps = {
|
type FirstSeenCellProps = {
|
||||||
createdAt: Date
|
createdAt?: Date | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FirstSeenCell = ({ createdAt }: FirstSeenCellProps) => {
|
export const FirstSeenCell = ({ createdAt }: FirstSeenCellProps) => {
|
||||||
|
if (!createdAt) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
return <DateCell date={createdAt} />
|
return <DateCell date={createdAt} />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,7 +19,7 @@ export const FirstSeenHeader = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center">
|
<div className="flex h-full w-full items-center">
|
||||||
<span className="truncate">{t("customers.firstSeen")}</span>
|
<span className="truncate">{t("fields.createdAt")}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
AdminCustomerListResponse,
|
||||||
|
AdminCustomerResponse,
|
||||||
|
DeleteResponse,
|
||||||
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
QueryKey,
|
QueryKey,
|
||||||
UseMutationOptions,
|
UseMutationOptions,
|
||||||
@@ -9,10 +14,6 @@ import { client } from "../../lib/client"
|
|||||||
import { queryClient } from "../../lib/medusa"
|
import { queryClient } from "../../lib/medusa"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads"
|
import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads"
|
||||||
import {
|
|
||||||
AdminCustomerResponse,
|
|
||||||
AdminCustomerListResponse,
|
|
||||||
} from "@medusajs/types"
|
|
||||||
|
|
||||||
const CUSTOMERS_QUERY_KEY = "customers" as const
|
const CUSTOMERS_QUERY_KEY = "customers" as const
|
||||||
export const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
|
export const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
|
||||||
@@ -88,3 +89,21 @@ export const useUpdateCustomer = (
|
|||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useDeleteCustomer = (
|
||||||
|
id: string,
|
||||||
|
options?: UseMutationOptions<DeleteResponse, Error, void>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => client.customers.delete(id),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: customersQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: customersQueryKeys.detail(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
+16
-17
@@ -1,35 +1,34 @@
|
|||||||
import { Text } from "@medusajs/ui"
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
|
|
||||||
import { AdminCustomerGroupResponse } from "@medusajs/types"
|
import { AdminCustomerGroupResponse } from "@medusajs/types"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import {
|
import {
|
||||||
CreatedAtCell,
|
TextCell,
|
||||||
CreatedAtHeader,
|
TextHeader,
|
||||||
} from "../../../components/table/table-cells/common/created-at-cell"
|
} from "../../../components/table/table-cells/common/text-cell"
|
||||||
import { NameHeader } from "../../../components/table/table-cells/common/name-cell"
|
|
||||||
|
|
||||||
const columnHelper =
|
const columnHelper =
|
||||||
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
|
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
|
||||||
|
|
||||||
export const useCustomerGroupTableColumns = () => {
|
export const useCustomerGroupTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
columnHelper.display({
|
columnHelper.accessor("name", {
|
||||||
id: "name",
|
header: () => <TextHeader text={t("fields.name")} />,
|
||||||
header: () => <NameHeader />,
|
cell: ({ getValue }) => <TextCell text={getValue() || "-"} />,
|
||||||
cell: ({
|
|
||||||
row: {
|
|
||||||
original: { name },
|
|
||||||
},
|
|
||||||
}) => <Text size="small">{name}</Text>,
|
|
||||||
}),
|
}),
|
||||||
|
columnHelper.accessor("customers", {
|
||||||
|
header: () => <TextHeader text={t("customers.domain")} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const count = getValue()?.length ?? 0
|
||||||
|
|
||||||
columnHelper.accessor("created_at", {
|
return <TextCell text={count} />
|
||||||
header: () => <CreatedAtHeader />,
|
},
|
||||||
cell: ({ getValue }) => <CreatedAtCell date={getValue()} />,
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[]
|
[t]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { Customer } from "@medusajs/medusa"
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
import { AdminCustomerResponse } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
EmailCell,
|
EmailCell,
|
||||||
EmailHeader,
|
EmailHeader,
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
FirstSeenHeader,
|
FirstSeenHeader,
|
||||||
} from "../../../components/table/table-cells/customer/first-seen-cell"
|
} from "../../../components/table/table-cells/customer/first-seen-cell"
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Customer>()
|
const columnHelper = createColumnHelper<AdminCustomerResponse["customer"]>()
|
||||||
|
|
||||||
export const useCustomerTableColumns = () => {
|
export const useCustomerTableColumns = () => {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Filter } from "../../../../../components/table/data-table"
|
import { Filter } from "../../../components/table/data-table"
|
||||||
|
|
||||||
export const useCustomerGroupTableFilters = () => {
|
export const useCustomerGroupTableFilters = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
AdminCustomerListResponse,
|
AdminCustomerListResponse,
|
||||||
AdminCustomerResponse,
|
AdminCustomerResponse,
|
||||||
|
DeleteResponse,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads"
|
import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads"
|
||||||
import { getRequest, postRequest } from "./common"
|
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||||
|
|
||||||
async function retrieveCustomer(id: string, query?: Record<string, any>) {
|
async function retrieveCustomer(id: string, query?: Record<string, any>) {
|
||||||
return getRequest<AdminCustomerResponse>(`/admin/customers/${id}`, query)
|
return getRequest<AdminCustomerResponse>(`/admin/customers/${id}`, query)
|
||||||
@@ -21,9 +22,14 @@ async function updateCustomer(id: string, payload: UpdateCustomerReq) {
|
|||||||
return postRequest<AdminCustomerResponse>(`/admin/customers/${id}`, payload)
|
return postRequest<AdminCustomerResponse>(`/admin/customers/${id}`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteCustomer(id: string) {
|
||||||
|
return deleteRequest<DeleteResponse>(`/admin/customers/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
export const customers = {
|
export const customers = {
|
||||||
retrieve: retrieveCustomer,
|
retrieve: retrieveCustomer,
|
||||||
list: listCustomers,
|
list: listCustomers,
|
||||||
create: createCustomer,
|
create: createCustomer,
|
||||||
update: updateCustomer,
|
update: updateCustomer,
|
||||||
|
delete: deleteCustomer,
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ export type UpdateApiKeyReq = UpdateApiKeyDTO
|
|||||||
|
|
||||||
// Customers
|
// Customers
|
||||||
export type CreateCustomerReq = CreateCustomerDTO
|
export type CreateCustomerReq = CreateCustomerDTO
|
||||||
export type UpdateCustomerReq = UpdateCustomerDTO
|
export type UpdateCustomerReq = Omit<UpdateCustomerDTO, "id">
|
||||||
|
|
||||||
// Sales Channels
|
// Sales Channels
|
||||||
export type CreateSalesChannelReq = CreateSalesChannelDTO
|
export type CreateSalesChannelReq = CreateSalesChannelDTO
|
||||||
|
|||||||
+22
-58
@@ -1,44 +1,27 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Customer } from "@medusajs/medusa"
|
import { Button, Checkbox, Hint, Tooltip, toast } from "@medusajs/ui"
|
||||||
import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
|
|
||||||
import {
|
import {
|
||||||
OnChangeFn,
|
OnChangeFn,
|
||||||
PaginationState,
|
|
||||||
RowSelectionState,
|
RowSelectionState,
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
import {
|
|
||||||
adminCustomerKeys,
|
|
||||||
useAdminAddCustomersToCustomerGroup,
|
|
||||||
useAdminCustomers,
|
|
||||||
} from "medusa-react"
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import {
|
import { AdminCustomerResponse } from "@medusajs/types"
|
||||||
NoRecords,
|
|
||||||
NoResults,
|
|
||||||
} from "../../../../../components/common/empty-table-content"
|
|
||||||
import { Query } from "../../../../../components/filtering/query"
|
|
||||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { queryClient } from "../../../../../lib/medusa"
|
|
||||||
import { useCustomers } from "../../../../../hooks/api/customers"
|
|
||||||
import { useAddCustomersToGroup } from "../../../../../hooks/api/customer-groups"
|
import { useAddCustomersToGroup } from "../../../../../hooks/api/customer-groups"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useCustomers } from "../../../../../hooks/api/customers"
|
||||||
|
import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
|
||||||
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
|
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
|
||||||
import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query"
|
import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { AdminCustomerResponse } from "@medusajs/types"
|
|
||||||
|
|
||||||
type AddCustomersFormProps = {
|
type AddCustomersFormProps = {
|
||||||
customerGroupId: string
|
customerGroupId: string
|
||||||
@@ -65,19 +48,6 @@ export const AddCustomersForm = ({
|
|||||||
|
|
||||||
const { setValue } = form
|
const { setValue } = form
|
||||||
|
|
||||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: PAGE_SIZE,
|
|
||||||
})
|
|
||||||
|
|
||||||
const pagination = useMemo(
|
|
||||||
() => ({
|
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
}),
|
|
||||||
[pageIndex, pageSize]
|
|
||||||
)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -130,8 +100,7 @@ export const AddCustomersForm = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading: isMutating } =
|
const { mutateAsync, isPending } = useAddCustomersToGroup(customerGroupId)
|
||||||
useAddCustomersToGroup(customerGroupId)
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
@@ -140,6 +109,13 @@ export const AddCustomersForm = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("customerGroups.customers.add.successToast", {
|
||||||
|
count: data.customer_ids.length,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
|
||||||
handleSuccess(`/customer-groups/${customerGroupId}`)
|
handleSuccess(`/customer-groups/${customerGroupId}`)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -172,13 +148,13 @@ export const AddCustomersForm = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
isLoading={isMutating}
|
isLoading={isPending}
|
||||||
>
|
>
|
||||||
{t("general.add")}
|
{t("actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Header>
|
||||||
<RouteFocusModal.Body>
|
<RouteFocusModal.Body className="size-full overflow-hidden">
|
||||||
<DataTable
|
<DataTable
|
||||||
table={table}
|
table={table}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -194,6 +170,7 @@ export const AddCustomersForm = ({
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
]}
|
]}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
layout="fill"
|
||||||
search
|
search
|
||||||
queryObject={raw}
|
queryObject={raw}
|
||||||
/>
|
/>
|
||||||
@@ -207,6 +184,7 @@ const columnHelper = createColumnHelper<AdminCustomerResponse["customer"]>()
|
|||||||
|
|
||||||
const useColumns = () => {
|
const useColumns = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const base = useCustomerTableColumns()
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -244,7 +222,7 @@ const useColumns = () => {
|
|||||||
if (isPreSelected) {
|
if (isPreSelected) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={t("customerGroups.customerAlreadyAdded")}
|
content={t("customerGroups.customers.alreadyAddedTooltip")}
|
||||||
side="right"
|
side="right"
|
||||||
>
|
>
|
||||||
{Component}
|
{Component}
|
||||||
@@ -255,23 +233,9 @@ const useColumns = () => {
|
|||||||
return Component
|
return Component
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
...base,
|
||||||
header: t("fields.email"),
|
|
||||||
cell: ({ getValue }) => getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.display({
|
|
||||||
id: "name",
|
|
||||||
header: t("fields.name"),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const name = [row.original.first_name, row.original.last_name]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" ")
|
|
||||||
|
|
||||||
return name || "-"
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
[t]
|
[t, base]
|
||||||
)
|
)
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
|
|||||||
+18
-6
@@ -1,6 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Button, Heading, Input, Text } from "@medusajs/ui"
|
import { Button, Heading, Input, Text, toast } from "@medusajs/ui"
|
||||||
import { useAdminCreateCustomerGroup } from "medusa-react"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
@@ -27,7 +26,7 @@ export const CreateCustomerGroupForm = () => {
|
|||||||
resolver: zodResolver(CreateCustomerGroupSchema),
|
resolver: zodResolver(CreateCustomerGroupSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useCreateCustomerGroup()
|
const { mutateAsync, isPending } = useCreateCustomerGroup()
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
@@ -36,8 +35,21 @@ export const CreateCustomerGroupForm = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: ({ customer_group }) => {
|
onSuccess: ({ customer_group }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("customerGroups.create.successToast", {
|
||||||
|
name: customer_group.name,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
|
||||||
handleSuccess(`/customer-groups/${customer_group.id}`)
|
handleSuccess(`/customer-groups/${customer_group.id}`)
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: error.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -56,7 +68,7 @@ export const CreateCustomerGroupForm = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
>
|
>
|
||||||
{t("actions.create")}
|
{t("actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -65,9 +77,9 @@ export const CreateCustomerGroupForm = () => {
|
|||||||
<RouteFocusModal.Body className="flex flex-col items-center pt-[72px]">
|
<RouteFocusModal.Body className="flex flex-col items-center pt-[72px]">
|
||||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
<div>
|
<div>
|
||||||
<Heading>{t("customerGroups.createCustomerGroup")}</Heading>
|
<Heading>{t("customerGroups.create.header")}</Heading>
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
{t("customerGroups.createCustomerGroupHint")}
|
{t("customerGroups.create.hint")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
+19
-20
@@ -1,27 +1,22 @@
|
|||||||
import { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
|
|
||||||
import { Customer } from "@medusajs/medusa"
|
|
||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import {
|
import {
|
||||||
adminCustomerGroupKeys,
|
AdminCustomerGroupResponse,
|
||||||
useAdminCustomPost,
|
AdminCustomerResponse,
|
||||||
useAdminCustomerGroupCustomers,
|
} from "@medusajs/types"
|
||||||
} from "medusa-react"
|
import { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
|
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { Link } from "react-router-dom"
|
import { useRemoveCustomersFromGroup } from "../../../../../hooks/api/customer-groups"
|
||||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
import { useCustomers } from "../../../../../hooks/api/customers"
|
||||||
import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
|
import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
|
||||||
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
|
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
|
||||||
import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query"
|
import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useMemo, useState } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { AdminCustomerGroupResponse, AdminCustomerResponse } from "@medusajs/types"
|
|
||||||
import { useCustomers } from "../../../../../hooks/api/customers"
|
|
||||||
import {
|
|
||||||
useRemoveCustomersFromGroup,
|
|
||||||
useUpdateCustomerGroup,
|
|
||||||
} from "../../../../../hooks/api/customer-groups"
|
|
||||||
|
|
||||||
type CustomerGroupCustomerSectionProps = {
|
type CustomerGroupCustomerSectionProps = {
|
||||||
group: AdminCustomerGroupResponse["customer_group"]
|
group: AdminCustomerGroupResponse["customer_group"]
|
||||||
@@ -72,8 +67,10 @@ export const CustomerGroupCustomerSection = ({
|
|||||||
const keys = Object.keys(rowSelection)
|
const keys = Object.keys(rowSelection)
|
||||||
|
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("customerGroups.customers.remove.title", {
|
||||||
description: t("customerGroups.removeCustomersWarning", {
|
count: keys.length,
|
||||||
|
}),
|
||||||
|
description: t("customerGroups.customers.remove.description", {
|
||||||
count: keys.length,
|
count: keys.length,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.continue"),
|
confirmText: t("actions.continue"),
|
||||||
@@ -151,8 +148,10 @@ const CustomerActions = ({
|
|||||||
|
|
||||||
const handleRemove = async () => {
|
const handleRemove = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
title: t("general.areYouSure"),
|
title: t("customerGroups.customers.remove.title", {
|
||||||
description: t("customerGroups.removeCustomersWarning", {
|
count: 1,
|
||||||
|
}),
|
||||||
|
description: t("customerGroups.customers.remove.description", {
|
||||||
count: 1,
|
count: 1,
|
||||||
}),
|
}),
|
||||||
confirmText: t("actions.continue"),
|
confirmText: t("actions.continue"),
|
||||||
|
|||||||
+63
-26
@@ -1,9 +1,9 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { Container, Heading } from "@medusajs/ui"
|
import { AdminCustomerGroupResponse } from "@medusajs/types"
|
||||||
|
import { Container, Heading, Text, toast, usePrompt } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { AdminCustomerGroupResponse } from "@medusajs/types"
|
|
||||||
import { useDeleteCustomerGroup } from "../../../../../hooks/api/customer-groups"
|
import { useDeleteCustomerGroup } from "../../../../../hooks/api/customer-groups"
|
||||||
|
|
||||||
type CustomerGroupGeneralSectionProps = {
|
type CustomerGroupGeneralSectionProps = {
|
||||||
@@ -14,43 +14,80 @@ export const CustomerGroupGeneralSection = ({
|
|||||||
group,
|
group,
|
||||||
}: CustomerGroupGeneralSectionProps) => {
|
}: CustomerGroupGeneralSectionProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { mutateAsync } = useDeleteCustomerGroup(group.id)
|
const { mutateAsync } = useDeleteCustomerGroup(group.id)
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("customerGroups.delete.title"),
|
||||||
|
description: t("customerGroups.delete.description", {
|
||||||
|
name: group.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await mutateAsync(undefined, {
|
await mutateAsync(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("customerGroups.delete.successToast", {
|
||||||
|
name: group.name,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
|
||||||
navigate("/customer-groups", { replace: true })
|
navigate("/customer-groups", { replace: true })
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: error.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="flex items-center justify-between px-6 py-4">
|
<Container className="divide-y p-0">
|
||||||
<Heading>{group.name}</Heading>
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<ActionMenu
|
<Heading>{group.name}</Heading>
|
||||||
groups={[
|
<ActionMenu
|
||||||
{
|
groups={[
|
||||||
actions: [
|
{
|
||||||
{
|
actions: [
|
||||||
icon: <PencilSquare />,
|
{
|
||||||
label: t("actions.edit"),
|
icon: <PencilSquare />,
|
||||||
to: `/customer-groups/${group.id}/edit`,
|
label: t("actions.edit"),
|
||||||
},
|
to: `/customer-groups/${group.id}/edit`,
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
actions: [
|
{
|
||||||
{
|
actions: [
|
||||||
icon: <Trash />,
|
{
|
||||||
label: t("actions.delete"),
|
icon: <Trash />,
|
||||||
onClick: handleDelete,
|
label: t("actions.delete"),
|
||||||
},
|
onClick: handleDelete,
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
]}
|
},
|
||||||
/>
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("customers.domain")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{group.customers?.length || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+8
-10
@@ -1,9 +1,9 @@
|
|||||||
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { useCustomerGroup } from "../../../hooks/api/customer-groups"
|
||||||
import { CustomerGroupCustomerSection } from "./components/customer-group-customer-section"
|
import { CustomerGroupCustomerSection } from "./components/customer-group-customer-section"
|
||||||
import { CustomerGroupGeneralSection } from "./components/customer-group-general-section"
|
import { CustomerGroupGeneralSection } from "./components/customer-group-general-section"
|
||||||
import { customerGroupLoader } from "./loader"
|
import { customerGroupLoader } from "./loader"
|
||||||
import { useCustomerGroup } from "../../../hooks/api/customer-groups"
|
|
||||||
|
|
||||||
export const CustomerGroupDetail = () => {
|
export const CustomerGroupDetail = () => {
|
||||||
const initialData = useLoaderData() as Awaited<
|
const initialData = useLoaderData() as Awaited<
|
||||||
@@ -13,20 +13,18 @@ export const CustomerGroupDetail = () => {
|
|||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { customer_group, isLoading, isError, error } = useCustomerGroup(
|
const { customer_group, isLoading, isError, error } = useCustomerGroup(
|
||||||
id!,
|
id!,
|
||||||
undefined,
|
{
|
||||||
|
fields: "+customers.id",
|
||||||
|
},
|
||||||
{ initialData }
|
{ initialData }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || !customer_group) {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !customer_group) {
|
if (isError) {
|
||||||
if (error) {
|
throw error
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
throw json("An unknown error occurred", 500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+4
-1
@@ -7,7 +7,10 @@ import { medusa, queryClient } from "../../../lib/medusa"
|
|||||||
|
|
||||||
const customerGroupDetailQuery = (id: string) => ({
|
const customerGroupDetailQuery = (id: string) => ({
|
||||||
queryKey: adminProductKeys.detail(id),
|
queryKey: adminProductKeys.detail(id),
|
||||||
queryFn: async () => medusa.admin.customerGroups.retrieve(id),
|
queryFn: async () =>
|
||||||
|
medusa.admin.customerGroups.retrieve(id, {
|
||||||
|
fields: "+customers.id",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const customerGroupLoader = async ({ params }: LoaderFunctionArgs) => {
|
export const customerGroupLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
|||||||
+12
-6
@@ -1,6 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Button, Input } from "@medusajs/ui"
|
import { AdminCustomerGroupResponse } from "@medusajs/types"
|
||||||
import { useAdminUpdateCustomerGroup } from "medusa-react"
|
import { Button, Input, toast } from "@medusajs/ui"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as z from "zod"
|
import * as z from "zod"
|
||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { AdminCustomerGroupResponse } from "@medusajs/types"
|
|
||||||
import { useUpdateCustomerGroup } from "../../../../../hooks/api/customer-groups"
|
import { useUpdateCustomerGroup } from "../../../../../hooks/api/customer-groups"
|
||||||
|
|
||||||
type EditCustomerGroupFormProps = {
|
type EditCustomerGroupFormProps = {
|
||||||
@@ -33,11 +32,18 @@ export const EditCustomerGroupForm = ({
|
|||||||
resolver: zodResolver(EditCustomerGroupSchema),
|
resolver: zodResolver(EditCustomerGroupSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useUpdateCustomerGroup(group.id)
|
const { mutateAsync, isPending } = useUpdateCustomerGroup(group.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(data, {
|
await mutateAsync(data, {
|
||||||
onSuccess: () => {
|
onSuccess: ({ customer_group }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("customerGroups.edit.successToast", {
|
||||||
|
name: customer_group.name,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -73,7 +79,7 @@ export const EditCustomerGroupForm = ({
|
|||||||
{t("actions.cancel")}
|
{t("actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</RouteDrawer.Close>
|
</RouteDrawer.Close>
|
||||||
<Button size="small" type="submit" isLoading={isLoading}>
|
<Button size="small" type="submit" isLoading={isPending}>
|
||||||
{t("actions.save")}
|
{t("actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@ import { Heading } from "@medusajs/ui"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { RouteDrawer } from "../../../components/route-modal"
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
import { EditCustomerGroupForm } from "./components/edit-customer-group-form"
|
|
||||||
import { useCustomerGroup } from "../../../hooks/api/customer-groups"
|
import { useCustomerGroup } from "../../../hooks/api/customer-groups"
|
||||||
|
import { EditCustomerGroupForm } from "./components/edit-customer-group-form"
|
||||||
|
|
||||||
export const CustomerGroupEdit = () => {
|
export const CustomerGroupEdit = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -18,7 +18,7 @@ export const CustomerGroupEdit = () => {
|
|||||||
return (
|
return (
|
||||||
<RouteDrawer>
|
<RouteDrawer>
|
||||||
<RouteDrawer.Header>
|
<RouteDrawer.Header>
|
||||||
<Heading>{t("customerGroups.editCustomerGroup")}</Heading>
|
<Heading>{t("customerGroups.edit.header")}</Heading>
|
||||||
</RouteDrawer.Header>
|
</RouteDrawer.Header>
|
||||||
{!isLoading && customer_group && (
|
{!isLoading && customer_group && (
|
||||||
<EditCustomerGroupForm group={customer_group} />
|
<EditCustomerGroupForm group={customer_group} />
|
||||||
|
|||||||
+101
-6
@@ -1,12 +1,21 @@
|
|||||||
import { Button, Container, Heading } from "@medusajs/ui"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { AdminCustomerGroupResponse } from "@medusajs/types"
|
||||||
|
import { Button, Container, Heading, toast, usePrompt } from "@medusajs/ui"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import {
|
||||||
|
useCustomerGroups,
|
||||||
|
useDeleteCustomerGroup,
|
||||||
|
} from "../../../../../hooks/api/customer-groups"
|
||||||
|
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
|
||||||
|
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
|
||||||
|
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useCustomerGroupTableColumns } from "./use-customer-group-table-columns"
|
|
||||||
import { useCustomerGroupTableFilters } from "./use-customer-group-table-filters"
|
|
||||||
import { useCustomerGroupTableQuery } from "./use-customer-group-table-query"
|
|
||||||
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
|
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
@@ -23,7 +32,7 @@ export const CustomerGroupListTable = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const filters = useCustomerGroupTableFilters()
|
const filters = useCustomerGroupTableFilters()
|
||||||
const columns = useCustomerGroupTableColumns()
|
const columns = useColumns()
|
||||||
|
|
||||||
const { table } = useDataTable({
|
const { table } = useDataTable({
|
||||||
data: customer_groups ?? [],
|
data: customer_groups ?? [],
|
||||||
@@ -64,3 +73,89 @@ export const CustomerGroupListTable = () => {
|
|||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CustomerGroupRowActions = ({
|
||||||
|
group,
|
||||||
|
}: {
|
||||||
|
group: AdminCustomerGroupResponse["customer_group"]
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const { mutateAsync } = useDeleteCustomerGroup(group.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("customerGroups.delete.title"),
|
||||||
|
description: t("customerGroups.delete.description", {
|
||||||
|
name: group.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("customerGroups.delete.successToast", {
|
||||||
|
name: group.name,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: error.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: `/customer-groups/${group.id}/edit`,
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
icon: <Trash />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper =
|
||||||
|
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const columns = useCustomerGroupTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
...columns,
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => <CustomerGroupRowActions group={row.original} />,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[columns]
|
||||||
|
)
|
||||||
|
}
|
||||||
-90
@@ -1,90 +0,0 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
|
||||||
import { usePrompt } from "@medusajs/ui"
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
|
||||||
import { AdminCustomerGroupResponse } from "@medusajs/types"
|
|
||||||
import { useDeleteCustomerGroup } from "../../../../../hooks/api/customer-groups"
|
|
||||||
|
|
||||||
const columnHelper =
|
|
||||||
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
|
|
||||||
|
|
||||||
export const useCustomerGroupTableColumns = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => [
|
|
||||||
columnHelper.accessor("name", {
|
|
||||||
header: t("fields.name"),
|
|
||||||
cell: ({ getValue }) => getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("customers", {
|
|
||||||
header: t("customers.domain"),
|
|
||||||
cell: ({ getValue }) => {
|
|
||||||
const count = getValue()?.length ?? 0
|
|
||||||
|
|
||||||
return count
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.display({
|
|
||||||
id: "actions",
|
|
||||||
cell: ({ row }) => <CustomerGroupActions group={row.original} />,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomerGroupActions = ({
|
|
||||||
group,
|
|
||||||
}: {
|
|
||||||
group: AdminCustomerGroupResponse["customer_group"]
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const prompt = usePrompt()
|
|
||||||
|
|
||||||
const { mutateAsync } = useDeleteCustomerGroup(group.id)
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
const res = await prompt({
|
|
||||||
title: t("general.areYouSure"),
|
|
||||||
description: t("customerGroups.deleteCustomerGroupWarning", {
|
|
||||||
name: group.name,
|
|
||||||
}),
|
|
||||||
confirmText: t("actions.delete"),
|
|
||||||
cancelText: t("actions.cancel"),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutateAsync()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: t("actions.edit"),
|
|
||||||
to: `/customer-groups/${group.id}/edit`,
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: t("actions.delete"),
|
|
||||||
onClick: handleDelete,
|
|
||||||
icon: <Trash />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
|
||||||
|
|
||||||
type UseCustomerGroupTableQueryProps = {
|
|
||||||
prefix?: string
|
|
||||||
pageSize?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCustomerGroupTableQuery = ({
|
|
||||||
prefix,
|
|
||||||
pageSize = 20,
|
|
||||||
}: UseCustomerGroupTableQueryProps) => {
|
|
||||||
const queryObject = useQueryParams(
|
|
||||||
["offset", "q", "order", "created_at", "updated_at"],
|
|
||||||
prefix
|
|
||||||
)
|
|
||||||
|
|
||||||
const { offset, created_at, updated_at, q, order } = queryObject
|
|
||||||
|
|
||||||
const searchParams = {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+51
-19
@@ -1,33 +1,37 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Button, Heading, Input, Text, toast } from "@medusajs/ui"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { Button, Heading, Input, Text } from "@medusajs/ui"
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { Form } from "../../../../../components/common/form"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { useCreateCustomer } from "../../../../../hooks/api/customers"
|
import { useCreateCustomer } from "../../../../../hooks/api/customers"
|
||||||
|
|
||||||
const CreateCustomerSchema = zod.object({
|
const CreateCustomerSchema = zod.object({
|
||||||
email: zod.string().email(),
|
email: zod.string().email(),
|
||||||
first_name: zod.string().min(1),
|
first_name: zod.string().optional(),
|
||||||
last_name: zod.string().min(1),
|
last_name: zod.string().optional(),
|
||||||
phone: zod.string().min(1).optional(),
|
company_name: zod.string().optional(),
|
||||||
|
phone: zod.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const CreateCustomerForm = () => {
|
export const CreateCustomerForm = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useCreateCustomer()
|
const { mutateAsync, isPending } = useCreateCustomer()
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateCustomerSchema>>({
|
const form = useForm<zod.infer<typeof CreateCustomerSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
first_name: "",
|
first_name: "",
|
||||||
last_name: "",
|
last_name: "",
|
||||||
|
phone: "",
|
||||||
|
company_name: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateCustomerSchema),
|
resolver: zodResolver(CreateCustomerSchema),
|
||||||
})
|
})
|
||||||
@@ -35,15 +39,28 @@ export const CreateCustomerForm = () => {
|
|||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
email: data.email,
|
email: data.email || undefined,
|
||||||
first_name: data.first_name,
|
first_name: data.first_name || undefined,
|
||||||
last_name: data.last_name,
|
last_name: data.last_name || undefined,
|
||||||
phone: data.phone,
|
company_name: data.company_name || undefined,
|
||||||
|
phone: data.phone || undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: ({ customer }) => {
|
onSuccess: ({ customer }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("customers.create.successToast", {
|
||||||
|
email: customer.email,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
handleSuccess(`/customers/${customer.id}`)
|
handleSuccess(`/customers/${customer.id}`)
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: error.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -62,7 +79,7 @@ export const CreateCustomerForm = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
>
|
>
|
||||||
{t("actions.create")}
|
{t("actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -71,19 +88,19 @@ export const CreateCustomerForm = () => {
|
|||||||
<RouteFocusModal.Body className="flex flex-col items-center py-16">
|
<RouteFocusModal.Body className="flex flex-col items-center py-16">
|
||||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
<div>
|
<div>
|
||||||
<Heading>{t("customers.createCustomer")}</Heading>
|
<Heading>{t("customers.create.header")}</Heading>
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
{t("customers.createCustomerHint")}
|
{t("customers.create.hint")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="first_name"
|
name="first_name"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("fields.firstName")}</Form.Label>
|
<Form.Label optional>{t("fields.firstName")}</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input autoComplete="off" {...field} />
|
<Input autoComplete="off" {...field} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -98,7 +115,7 @@ export const CreateCustomerForm = () => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("fields.lastName")}</Form.Label>
|
<Form.Label optional>{t("fields.lastName")}</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input autoComplete="off" {...field} />
|
<Input autoComplete="off" {...field} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -122,6 +139,21 @@ export const CreateCustomerForm = () => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="company_name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label optional>{t("fields.company")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input autoComplete="off" {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="phone"
|
name="phone"
|
||||||
|
|||||||
+74
-8
@@ -1,8 +1,17 @@
|
|||||||
import { PencilSquare } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { Container, Heading, StatusBadge, Text } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
|
||||||
import { AdminCustomerResponse } from "@medusajs/types"
|
import { AdminCustomerResponse } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
StatusBadge,
|
||||||
|
Text,
|
||||||
|
toast,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useDeleteCustomer } from "../../../../../hooks/api/customers"
|
||||||
|
|
||||||
type CustomerGeneralSectionProps = {
|
type CustomerGeneralSectionProps = {
|
||||||
customer: AdminCustomerResponse["customer"]
|
customer: AdminCustomerResponse["customer"]
|
||||||
@@ -12,6 +21,10 @@ export const CustomerGeneralSection = ({
|
|||||||
customer,
|
customer,
|
||||||
}: CustomerGeneralSectionProps) => {
|
}: CustomerGeneralSectionProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { mutateAsync } = useDeleteCustomer(customer.id)
|
||||||
|
|
||||||
const name = [customer.first_name, customer.last_name]
|
const name = [customer.first_name, customer.last_name]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -19,8 +32,44 @@ export const CustomerGeneralSection = ({
|
|||||||
|
|
||||||
const statusColor = customer.has_account ? "green" : "orange"
|
const statusColor = customer.has_account ? "green" : "orange"
|
||||||
const statusText = customer.has_account
|
const statusText = customer.has_account
|
||||||
? t("customers.registered")
|
? t("customers.fields.registered")
|
||||||
: t("customers.guest")
|
: t("customers.fields.guest")
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("customers.delete.title"),
|
||||||
|
description: t("customers.delete.description", {
|
||||||
|
email: customer.email,
|
||||||
|
}),
|
||||||
|
verificationInstruction: t("general.typeToConfirm"),
|
||||||
|
verificationText: customer.email,
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("customers.delete.successToast", {
|
||||||
|
email: customer.email,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
|
||||||
|
navigate("/customers", { replace: true })
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: error.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="divide-y p-0">
|
<Container className="divide-y p-0">
|
||||||
@@ -39,6 +88,15 @@ export const CustomerGeneralSection = ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
icon: <Trash />,
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +106,15 @@ export const CustomerGeneralSection = ({
|
|||||||
{t("fields.name")}
|
{t("fields.name")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
{name ?? "-"}
|
{name || "-"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("fields.company")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{customer.company_name || "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||||
@@ -56,7 +122,7 @@ export const CustomerGeneralSection = ({
|
|||||||
{t("fields.phone")}
|
{t("fields.phone")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
{customer.phone ?? "-"}
|
{customer.phone || "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
+58
-19
@@ -1,17 +1,22 @@
|
|||||||
import { Customer, CustomerGroup } from "@medusajs/medusa"
|
|
||||||
import { Container, Heading } from "@medusajs/ui"
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
|
|
||||||
import {
|
import {
|
||||||
AdminCustomerGroupResponse,
|
AdminCustomerGroupResponse,
|
||||||
AdminCustomerResponse,
|
AdminCustomerResponse,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useCustomerGroupTableFilters } from "../../../../customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-filters"
|
|
||||||
import { t } from "i18next"
|
import { t } from "i18next"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
|
||||||
|
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
|
||||||
|
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
|
||||||
|
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
|
||||||
type CustomerGroupSectionProps = {
|
type CustomerGroupSectionProps = {
|
||||||
customer: AdminCustomerResponse["customer"]
|
customer: AdminCustomerResponse["customer"]
|
||||||
@@ -22,10 +27,20 @@ const PAGE_SIZE = 10
|
|||||||
export const CustomerGroupSection = ({
|
export const CustomerGroupSection = ({
|
||||||
customer,
|
customer,
|
||||||
}: CustomerGroupSectionProps) => {
|
}: CustomerGroupSectionProps) => {
|
||||||
|
const { raw, searchParams } = useCustomerGroupTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
const { customer_groups, count, isLoading, isError, error } =
|
const { customer_groups, count, isLoading, isError, error } =
|
||||||
useCustomerGroups({
|
useCustomerGroups(
|
||||||
customers: { id: customer.id },
|
{
|
||||||
})
|
...searchParams,
|
||||||
|
fields: "+customers.id",
|
||||||
|
customers: { id: customer.id },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const filters = useCustomerGroupTableFilters()
|
const filters = useCustomerGroupTableFilters()
|
||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
@@ -64,27 +79,51 @@ export const CustomerGroupSection = ({
|
|||||||
search
|
search
|
||||||
pagination
|
pagination
|
||||||
orderBy={["name", "created_at", "updated_at"]}
|
orderBy={["name", "created_at", "updated_at"]}
|
||||||
|
queryObject={raw}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add remove association when /customer-groups/:id/batch has been created.
|
||||||
|
const CustomerGroupRowActions = ({
|
||||||
|
group,
|
||||||
|
}: {
|
||||||
|
group: AdminCustomerGroupResponse["customer_group"]
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
to: `/customer-groups/${group.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const columnHelper =
|
const columnHelper =
|
||||||
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
|
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
|
||||||
|
|
||||||
const useColumns = () => {
|
const useColumns = () => {
|
||||||
const { t } = useTranslation()
|
const columns = useCustomerGroupTableColumns()
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
...columns,
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "select",
|
id: "actions",
|
||||||
}),
|
cell: ({ row }) => <CustomerGroupRowActions group={row.original} />,
|
||||||
columnHelper.accessor("name", {
|
|
||||||
header: t("fields.name"),
|
|
||||||
cell: ({ getValue }) => getValue(),
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[t]
|
[columns]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+6
-10
@@ -1,9 +1,9 @@
|
|||||||
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
import { CustomerGeneralSection } from "./components/customer-general-section"
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
import { customerLoader } from "./loader"
|
|
||||||
import { useCustomer } from "../../../hooks/api/customers"
|
import { useCustomer } from "../../../hooks/api/customers"
|
||||||
|
import { CustomerGeneralSection } from "./components/customer-general-section"
|
||||||
import { CustomerGroupSection } from "./components/customer-group-section"
|
import { CustomerGroupSection } from "./components/customer-group-section"
|
||||||
|
import { customerLoader } from "./loader"
|
||||||
|
|
||||||
export const CustomerDetail = () => {
|
export const CustomerDetail = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -15,16 +15,12 @@ export const CustomerDetail = () => {
|
|||||||
initialData,
|
initialData,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || !customer) {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !customer) {
|
if (isError) {
|
||||||
if (error) {
|
throw error
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
throw json("An unknown error occurred", 500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+52
-15
@@ -1,14 +1,15 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { AdminCustomerResponse } from "@medusajs/types"
|
||||||
|
import { Button, Input, toast } from "@medusajs/ui"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { Button, Input } from "@medusajs/ui"
|
import { ConditionalTooltip } from "../../../../../components/common/conditional-tooltip"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
import {
|
import {
|
||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { Form } from "../../../../../components/common/form"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { AdminCustomerResponse } from "@medusajs/types"
|
|
||||||
import { useUpdateCustomer } from "../../../../../hooks/api/customers"
|
import { useUpdateCustomer } from "../../../../../hooks/api/customers"
|
||||||
|
|
||||||
type EditCustomerFormProps = {
|
type EditCustomerFormProps = {
|
||||||
@@ -17,8 +18,9 @@ type EditCustomerFormProps = {
|
|||||||
|
|
||||||
const EditCustomerSchema = zod.object({
|
const EditCustomerSchema = zod.object({
|
||||||
email: zod.string().email(),
|
email: zod.string().email(),
|
||||||
first_name: zod.string().min(1).optional(),
|
first_name: zod.string().optional(),
|
||||||
last_name: zod.string().min(1).optional(),
|
last_name: zod.string().optional(),
|
||||||
|
company_name: zod.string().optional(),
|
||||||
phone: zod.string().optional(),
|
phone: zod.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -31,25 +33,40 @@ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
|
|||||||
email: customer.email || "",
|
email: customer.email || "",
|
||||||
first_name: customer.first_name || "",
|
first_name: customer.first_name || "",
|
||||||
last_name: customer.last_name || "",
|
last_name: customer.last_name || "",
|
||||||
|
company_name: customer.company_name || "",
|
||||||
phone: customer.phone || "",
|
phone: customer.phone || "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(EditCustomerSchema),
|
resolver: zodResolver(EditCustomerSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useUpdateCustomer(customer.id)
|
const { mutateAsync, isPending } = useUpdateCustomer(customer.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
email: customer.has_account ? undefined : data.email,
|
email: customer.has_account ? undefined : data.email,
|
||||||
first_name: data.first_name,
|
first_name: data.first_name || null,
|
||||||
last_name: data.last_name,
|
last_name: data.last_name || null,
|
||||||
phone: data.phone,
|
phone: data.phone || null,
|
||||||
|
company_name: data.company_name || null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: ({ customer }) => {
|
||||||
|
toast.success(t("general.success"), {
|
||||||
|
description: t("customers.edit.successToast", {
|
||||||
|
email: customer.email,
|
||||||
|
}),
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: error.message,
|
||||||
|
dismissLabel: t("actions.close"),
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -67,7 +84,12 @@ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
|
|||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("fields.email")}</Form.Label>
|
<Form.Label>{t("fields.email")}</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input {...field} disabled={customer.has_account} />
|
<ConditionalTooltip
|
||||||
|
showTooltip={customer.has_account}
|
||||||
|
content={t("customers.edit.emailDisabledTooltip")}
|
||||||
|
>
|
||||||
|
<Input {...field} disabled={customer.has_account} />
|
||||||
|
</ConditionalTooltip>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -104,6 +126,21 @@ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="company_name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.company")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="phone"
|
name="phone"
|
||||||
@@ -129,7 +166,7 @@ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</RouteDrawer.Close>
|
</RouteDrawer.Close>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
+14
-7
@@ -1,17 +1,19 @@
|
|||||||
import { PencilSquare } from "@medusajs/icons"
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
import { Button, Container, Heading } from "@medusajs/ui"
|
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
|
||||||
import { AdminCustomerResponse } from "@medusajs/types"
|
import { AdminCustomerResponse } from "@medusajs/types"
|
||||||
|
import { Button, Container, Heading } from "@medusajs/ui"
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useCustomers } from "../../../../../hooks/api/customers"
|
||||||
import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
|
import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
|
||||||
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
|
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
|
||||||
import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query"
|
import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useCustomers } from "../../../../../hooks/api/customers"
|
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
@@ -19,9 +21,14 @@ export const CustomerListTable = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE })
|
const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE })
|
||||||
const { customers, count, isLoading, isError, error } = useCustomers({
|
const { customers, count, isLoading, isError, error } = useCustomers(
|
||||||
...searchParams,
|
{
|
||||||
})
|
...searchParams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const filters = useCustomerTableFilters()
|
const filters = useCustomerTableFilters()
|
||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
@@ -110,5 +117,5 @@ const useColumns = () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[columns]
|
[columns]
|
||||||
) as ColumnDef<AdminCustomerResponse["customer"]>[]
|
)
|
||||||
}
|
}
|
||||||
@@ -44,14 +44,20 @@ export const AdminCustomersParams = createFindParams({
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const AdminCreateCustomer = z.object({
|
export const AdminCreateCustomer = z.object({
|
||||||
|
email: z.string().email().optional(),
|
||||||
company_name: z.string().optional(),
|
company_name: z.string().optional(),
|
||||||
first_name: z.string().optional(),
|
first_name: z.string().optional(),
|
||||||
last_name: z.string().optional(),
|
last_name: z.string().optional(),
|
||||||
email: z.string().optional(),
|
|
||||||
phone: z.string().optional(),
|
phone: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AdminUpdateCustomer = AdminCreateCustomer
|
export const AdminUpdateCustomer = z.object({
|
||||||
|
email: z.string().email().nullable().optional(),
|
||||||
|
company_name: z.string().nullable().optional(),
|
||||||
|
first_name: z.string().nullable().optional(),
|
||||||
|
last_name: z.string().nullable().optional(),
|
||||||
|
phone: z.string().nullable().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export const AdminCreateCustomerAddress = z.object({
|
export const AdminCreateCustomerAddress = z.object({
|
||||||
address_name: z.string().optional(),
|
address_name: z.string().optional(),
|
||||||
|
|||||||
Reference in new issue
Block a user