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