diff --git a/packages/admin-next/dashboard/public/locales/en/translation.json b/packages/admin-next/dashboard/public/locales/en/translation.json
index c1642e35c4..ed7e017c13 100644
--- a/packages/admin-next/dashboard/public/locales/en/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en/translation.json
@@ -35,6 +35,7 @@
"areYouSure": "Are you sure?",
"noRecordsFound": "No records found",
"typeToConfirm": "Please type {val} to confirm:",
+ "noResultsTitle": "No results",
"noResultsMessage": "Try changing the filters or search query",
"noRecordsTitle": "No records",
"noRecordsMessage": "There are no records to show",
@@ -80,7 +81,15 @@
"firstSeen": "First seen"
},
"customerGroups": {
- "domain": "Customer Groups"
+ "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."
},
"orders": {
"domain": "Orders"
diff --git a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx
index 062c47913c..858d344311 100644
--- a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx
+++ b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
type NoResultsProps = {
- title: string
+ title?: string
message?: string
}
@@ -16,7 +16,7 @@ export const NoResults = ({ title, message }: NoResultsProps) => {
- {title}
+ {title ?? t("general.noResultsTitle")}
{message ?? t("general.noResultsMessage")}
@@ -51,7 +51,9 @@ export const NoRecords = ({ title, message, action }: NoRecordsProps) => {
{action && (
- {action.label}
+
+ {action.label}
+
)}
diff --git a/packages/admin-next/dashboard/src/components/common/table-row-actions/index.ts b/packages/admin-next/dashboard/src/components/common/table-row-actions/index.ts
new file mode 100644
index 0000000000..155d370c4a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/common/table-row-actions/index.ts
@@ -0,0 +1 @@
+export * from "./table-row-actions"
diff --git a/packages/admin-next/dashboard/src/components/common/table-row-actions/table-row-actions.tsx b/packages/admin-next/dashboard/src/components/common/table-row-actions/table-row-actions.tsx
new file mode 100644
index 0000000000..99e5d6adfa
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/common/table-row-actions/table-row-actions.tsx
@@ -0,0 +1,84 @@
+import { EllipsisHorizontal } from "@medusajs/icons"
+import { DropdownMenu, IconButton } from "@medusajs/ui"
+import { ReactNode } from "react"
+import { Link } from "react-router-dom"
+
+type TableRowAction = {
+ icon: ReactNode
+ label: string
+} & (
+ | {
+ to: string
+ onClick?: never
+ }
+ | {
+ onClick: () => void
+ to?: never
+ }
+)
+
+type TableRowActionGroup = {
+ actions: TableRowAction[]
+}
+
+type TableRowActionsProps = {
+ groups: TableRowActionGroup[]
+}
+
+export const TableRowActions = ({ groups }: TableRowActionsProps) => {
+ return (
+
+
+
+
+
+
+
+ {groups.map((group, index) => {
+ if (!group.actions.length) {
+ return null
+ }
+
+ const isLast = index === groups.length - 1
+
+ return (
+
+ {group.actions.map((action, index) => {
+ if (action.onClick) {
+ return (
+ {
+ e.stopPropagation()
+ action.onClick()
+ }}
+ className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2"
+ >
+ {action.icon}
+ {action.label}
+
+ )
+ }
+
+ return (
+
+ {
+ e.stopPropagation()
+ }}
+ className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2"
+ >
+ {action.icon}
+ {action.label}
+
+
+ )
+ })}
+ {!isLast && }
+
+ )
+ })}
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
index 4c30503aaf..3249db7847 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx
@@ -1,4 +1,5 @@
import type {
+ AdminCustomerGroupsRes,
AdminCustomersRes,
AdminProductsRes,
AdminRegionsRes,
@@ -183,12 +184,43 @@ const router = createBrowserRouter([
},
children: [
{
- index: true,
- lazy: () => import("../../routes/customer-groups/list"),
+ path: "",
+ lazy: () =>
+ import("../../routes/customer-groups/customer-group-list"),
+ children: [
+ {
+ path: "create",
+ lazy: () =>
+ import(
+ "../../routes/customer-groups/customer-group-create"
+ ),
+ },
+ ],
},
{
path: ":id",
- lazy: () => import("../../routes/customer-groups/details"),
+ lazy: () =>
+ import("../../routes/customer-groups/customer-group-detail"),
+ handle: {
+ crumb: (data: AdminCustomerGroupsRes) =>
+ data.customer_group.name,
+ },
+ children: [
+ {
+ path: "add-customers",
+ lazy: () =>
+ import(
+ "../../routes/customer-groups/customer-group-add-customers"
+ ),
+ },
+ {
+ path: "edit",
+ lazy: () =>
+ import(
+ "../../routes/customer-groups/customer-group-edit"
+ ),
+ },
+ ],
},
],
},
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx
new file mode 100644
index 0000000000..ac47e264f5
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx
@@ -0,0 +1,347 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Customer } from "@medusajs/medusa"
+import {
+ Button,
+ Checkbox,
+ FocusModal,
+ Hint,
+ Table,
+ Tooltip,
+ clx,
+} from "@medusajs/ui"
+import {
+ 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 { useNavigate } from "react-router-dom"
+import * as zod from "zod"
+import {
+ NoRecords,
+ NoResults,
+} from "../../../../../components/common/empty-table-content"
+import { Form } from "../../../../../components/common/form"
+import { Query } from "../../../../../components/filtering/query"
+import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
+import { useQueryParams } from "../../../../../hooks/use-query-params"
+import { queryClient } from "../../../../../lib/medusa"
+
+type AddCustomersFormProps = {
+ customerGroupId: string
+ subscribe: (state: boolean) => void
+}
+
+const AddCustomersSchema = zod.object({
+ customer_ids: zod.array(zod.string()).min(1),
+})
+
+const PAGE_SIZE = 10
+
+export const AddCustomersForm = ({
+ customerGroupId,
+ subscribe,
+}: AddCustomersFormProps) => {
+ const navigate = useNavigate()
+ const { t } = useTranslation()
+
+ const form = useForm>({
+ defaultValues: {
+ customer_ids: [],
+ },
+ resolver: zodResolver(AddCustomersSchema),
+ })
+
+ const {
+ formState: { isDirty },
+ } = form
+
+ useEffect(() => {
+ subscribe(isDirty)
+ }, [isDirty])
+
+ const [{ pageIndex, pageSize }, setPagination] = useState({
+ pageIndex: 0,
+ pageSize: PAGE_SIZE,
+ })
+
+ const pagination = useMemo(
+ () => ({
+ pageIndex,
+ pageSize,
+ }),
+ [pageIndex, pageSize]
+ )
+
+ const [rowSelection, setRowSelection] = useState({})
+
+ useEffect(() => {
+ form.setValue(
+ "customer_ids",
+ Object.keys(rowSelection).filter((k) => rowSelection[k])
+ )
+ }, [rowSelection])
+
+ const params = useQueryParams(["q"])
+ const { customers, count, isLoading, isError, error } = useAdminCustomers({
+ expand: "groups",
+ limit: PAGE_SIZE,
+ offset: pageIndex * PAGE_SIZE,
+ ...params,
+ })
+
+ const columns = useColumns()
+
+ const table = useReactTable({
+ data: customers ?? [],
+ columns,
+ pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
+ state: {
+ pagination,
+ rowSelection,
+ },
+ getRowId: (row) => row.id,
+ onPaginationChange: setPagination,
+ onRowSelectionChange: setRowSelection,
+ getCoreRowModel: getCoreRowModel(),
+ manualPagination: true,
+ meta: {
+ customerGroupId,
+ },
+ })
+
+ const { mutateAsync, isLoading: isMutating } =
+ useAdminAddCustomersToCustomerGroup(customerGroupId)
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ await mutateAsync(
+ {
+ customer_ids: data.customer_ids.map((id) => ({ id })),
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(adminCustomerKeys.lists())
+ navigate(`/customer-groups/${customerGroupId}`)
+ },
+ }
+ )
+ })
+
+ const noRecords =
+ !isLoading &&
+ !customers?.length &&
+ !Object.values(params).filter(Boolean).length
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+ )
+}
+
+const columnHelper = createColumnHelper()
+
+const useColumns = () => {
+ const { t } = useTranslation()
+
+ const columns = useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row, table }) => {
+ const { customerGroupId } = table.options.meta as {
+ customerGroupId: string
+ }
+
+ const isAdded = row.original.groups
+ ?.map((gc) => gc.id)
+ .includes(customerGroupId)
+
+ const isSelected = row.getIsSelected() || isAdded
+
+ const Component = (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+
+ if (isAdded) {
+ return (
+
+ {Component}
+
+ )
+ }
+
+ 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 || "-"
+ },
+ }),
+ ],
+ [t]
+ )
+
+ return columns
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/index.ts
new file mode 100644
index 0000000000..512d1edf6f
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/index.ts
@@ -0,0 +1 @@
+export * from "./add-customers-form"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/customer-group-add-customers.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/customer-group-add-customers.tsx
new file mode 100644
index 0000000000..874d277800
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/customer-group-add-customers.tsx
@@ -0,0 +1,18 @@
+import { FocusModal } from "@medusajs/ui"
+import { useParams } from "react-router-dom"
+import { useRouteModalState } from "../../../hooks/use-route-modal-state"
+import { AddCustomersForm } from "./components/add-customers-form"
+
+export const CustomerGroupAddCustomers = () => {
+ const [open, onOpenChange, subscribe] = useRouteModalState()
+
+ const { id } = useParams()
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/index.ts
new file mode 100644
index 0000000000..3b39c9e782
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/index.ts
@@ -0,0 +1 @@
+export { CustomerGroupAddCustomers as Component } from "./customer-group-add-customers"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx
new file mode 100644
index 0000000000..76ff4075f0
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx
@@ -0,0 +1,105 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui"
+import { useAdminCreateCustomerGroup } from "medusa-react"
+import { useEffect } from "react"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import { useNavigate } from "react-router-dom"
+import * as zod from "zod"
+import { Form } from "../../../../../components/common/form"
+
+type CreateCustomerGroupFormProps = {
+ subscribe: (state: boolean) => void
+}
+
+const CreateCustomerGroupSchema = zod.object({
+ name: zod.string().min(1),
+})
+
+export const CreateCustomerGroupForm = ({
+ subscribe,
+}: CreateCustomerGroupFormProps) => {
+ const { t } = useTranslation()
+ const navigate = useNavigate()
+
+ const form = useForm>({
+ defaultValues: {
+ name: "",
+ },
+ resolver: zodResolver(CreateCustomerGroupSchema),
+ })
+
+ const {
+ formState: { isDirty },
+ } = form
+
+ useEffect(() => {
+ subscribe(isDirty)
+ }, [isDirty])
+
+ const { mutateAsync, isLoading } = useAdminCreateCustomerGroup()
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ await mutateAsync(
+ {
+ name: data.name,
+ },
+ {
+ onSuccess: ({ customer_group }) => {
+ navigate(`/customer-groups/${customer_group.id}`)
+ },
+ }
+ )
+ })
+
+ return (
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/index.ts
new file mode 100644
index 0000000000..960232c9c2
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/index.ts
@@ -0,0 +1 @@
+export * from "./create-customer-group-form"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/customer-group-create.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/customer-group-create.tsx
new file mode 100644
index 0000000000..6fc39b5067
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/customer-group-create.tsx
@@ -0,0 +1,15 @@
+import { FocusModal } from "@medusajs/ui"
+import { useRouteModalState } from "../../../hooks/use-route-modal-state"
+import { CreateCustomerGroupForm } from "./components/create-customer-group-form"
+
+export const CustomerGroupCreate = () => {
+ const [open, onOpenChange, subscribe] = useRouteModalState()
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/index.ts
new file mode 100644
index 0000000000..9bab0611a8
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/index.ts
@@ -0,0 +1 @@
+export { CustomerGroupCreate as Component } from "./customer-group-create"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx
new file mode 100644
index 0000000000..04eb0e4bcb
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx
@@ -0,0 +1,387 @@
+import { PencilSquare, Trash } from "@medusajs/icons"
+import { Customer, CustomerGroup } from "@medusajs/medusa"
+import {
+ Button,
+ Checkbox,
+ CommandBar,
+ Container,
+ Heading,
+ StatusBadge,
+ Table,
+ clx,
+ usePrompt,
+} from "@medusajs/ui"
+import {
+ PaginationState,
+ RowSelectionState,
+ createColumnHelper,
+ flexRender,
+ getCoreRowModel,
+ useReactTable,
+} from "@tanstack/react-table"
+import {
+ useAdminCustomerGroupCustomers,
+ useAdminRemoveCustomersFromCustomerGroup,
+} from "medusa-react"
+import { useMemo, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { Link, useNavigate } from "react-router-dom"
+import {
+ NoRecords,
+ NoResults,
+} from "../../../../../components/common/empty-table-content"
+import { TableRowActions } from "../../../../../components/common/table-row-actions"
+import { Query } from "../../../../../components/filtering/query"
+import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
+import { useQueryParams } from "../../../../../hooks/use-query-params"
+
+type CustomerGroupCustomerSectionProps = {
+ group: CustomerGroup
+}
+
+const PAGE_SIZE = 10
+
+export const CustomerGroupCustomerSection = ({
+ group,
+}: CustomerGroupCustomerSectionProps) => {
+ const navigate = useNavigate()
+ const { t } = useTranslation()
+
+ const [{ pageIndex, pageSize }, setPagination] = useState({
+ pageIndex: 0,
+ pageSize: PAGE_SIZE,
+ })
+
+ const pagination = useMemo(
+ () => ({
+ pageIndex,
+ pageSize,
+ }),
+ [pageIndex, pageSize]
+ )
+
+ const [rowSelection, setRowSelection] = useState({})
+
+ const params = useQueryParams(["q"])
+ const { customers, count, isLoading, isError, error } =
+ useAdminCustomerGroupCustomers(
+ group.id,
+ {
+ limit: PAGE_SIZE,
+ offset: pageIndex * PAGE_SIZE,
+ ...params,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const columns = useColumns()
+
+ const table = useReactTable({
+ data: customers ?? [],
+ columns,
+ pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
+ state: {
+ pagination,
+ rowSelection,
+ },
+ getRowId: (row) => row.id,
+ onPaginationChange: setPagination,
+ onRowSelectionChange: setRowSelection,
+ getCoreRowModel: getCoreRowModel(),
+ manualPagination: true,
+ meta: {
+ customerGroupId: group.id,
+ },
+ })
+
+ const { mutateAsync } = useAdminRemoveCustomersFromCustomerGroup(group.id)
+ const prompt = usePrompt()
+
+ const handleRemoveCustomers = async () => {
+ const selected = Object.keys(rowSelection).filter((k) => rowSelection[k])
+
+ const res = await prompt({
+ title: t("general.areYouSure"),
+ description: t("customerGroups.removeCustomersWarning", {
+ count: selected.length,
+ }),
+ confirmText: t("general.continue"),
+ cancelText: t("general.cancel"),
+ })
+
+ if (!res) {
+ return
+ }
+
+ await mutateAsync(
+ {
+ customer_ids: selected.map((s) => ({ id: s })),
+ },
+ {
+ onSuccess: () => {
+ setRowSelection({})
+ },
+ }
+ )
+ }
+
+ const noRecords =
+ !isLoading &&
+ !customers?.length &&
+ !Object.values(params).filter(Boolean).length
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+ {t("customers.domain")}
+
+
+ {t("general.add")}
+
+
+
+
+ {noRecords ? (
+
+ ) : (
+
+
+
+ {(customers?.length || 0) > 0 ? (
+
+
+ {table.getHeaderGroups().map((headerGroup) => {
+ return (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ )
+ })}
+
+ )
+ })}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ navigate(`/customers/${row.original.id}`)
+ }
+ >
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ ))}
+
+ ))}
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {t("general.countSelected", {
+ count: Object.keys(rowSelection).length,
+ })}
+
+
+
+
+
+
+
+ )}
+
+
+ )
+}
+
+const CustomerActions = ({
+ customer,
+ customerGroupId,
+}: {
+ customer: Customer
+ customerGroupId: string
+}) => {
+ const { t } = useTranslation()
+ const { mutateAsync } =
+ useAdminRemoveCustomersFromCustomerGroup(customerGroupId)
+
+ const prompt = usePrompt()
+
+ const handleRemove = async () => {
+ const res = await prompt({
+ title: t("general.areYouSure"),
+ description: t("customerGroups.removeCustomersWarning", {
+ count: 1,
+ }),
+ confirmText: t("general.continue"),
+ cancelText: t("general.cancel"),
+ })
+
+ if (!res) {
+ return
+ }
+
+ await mutateAsync({
+ customer_ids: [{ id: customer.id }],
+ })
+ }
+
+ return (
+ ,
+ label: t("general.edit"),
+ to: `/customers/${customer.id}/edit`,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ icon: ,
+ label: t("general.remove"),
+ onClick: handleRemove,
+ },
+ ],
+ },
+ ]}
+ />
+ )
+}
+
+const columnHelper = createColumnHelper()
+
+const useColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ 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 || "-"
+ },
+ }),
+ columnHelper.accessor("has_account", {
+ header: t("fields.account"),
+ cell: ({ getValue }) => {
+ const hasAccount = getValue()
+
+ return (
+
+ {hasAccount ? t("customers.registered") : t("customers.guest")}
+
+ )
+ },
+ }),
+ columnHelper.display({
+ id: "actions",
+ cell: ({ row, table }) => {
+ const { customerGroupId } = table.options.meta as {
+ customerGroupId: string
+ }
+
+ return (
+
+ )
+ },
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/index.ts
new file mode 100644
index 0000000000..3098ff7aca
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/index.ts
@@ -0,0 +1 @@
+export * from "./customer-group-customer-section"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx
new file mode 100644
index 0000000000..e11dd5464e
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx
@@ -0,0 +1,56 @@
+import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
+import type { CustomerGroup } from "@medusajs/medusa"
+import { Container, DropdownMenu, Heading, IconButton } from "@medusajs/ui"
+import { useAdminDeleteCustomerGroup } from "medusa-react"
+import { useTranslation } from "react-i18next"
+import { Link, useNavigate } from "react-router-dom"
+
+type CustomerGroupGeneralSectionProps = {
+ group: CustomerGroup
+}
+
+export const CustomerGroupGeneralSection = ({
+ group,
+}: CustomerGroupGeneralSectionProps) => {
+ const { t } = useTranslation()
+ const navigate = useNavigate()
+
+ const { mutateAsync } = useAdminDeleteCustomerGroup(group.id)
+
+ const handleDelete = async () => {
+ await mutateAsync(undefined, {
+ onSuccess: () => {
+ navigate("/customer-groups", { replace: true })
+ },
+ })
+ }
+
+ return (
+
+ {group.name}
+
+
+
+
+
+
+
+
+
+
+ {t("general.edit")}
+
+
+
+
+
+ {t("general.delete")}
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/index.ts
new file mode 100644
index 0000000000..dfe7099024
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/index.ts
@@ -0,0 +1 @@
+export * from "./customer-group-general-section"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/customer-group-detail.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/customer-group-detail.tsx
new file mode 100644
index 0000000000..0d93790b5a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/customer-group-detail.tsx
@@ -0,0 +1,40 @@
+import { useAdminCustomerGroup } from "medusa-react"
+import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
+import { JsonViewSection } from "../../../components/common/json-view-section"
+import { CustomerGroupCustomerSection } from "./components/customer-group-customer-section"
+import { CustomerGroupGeneralSection } from "./components/customer-group-general-section"
+import { customerGroupLoader } from "./loader"
+
+export const CustomerGroupDetail = () => {
+ const initialData = useLoaderData() as Awaited<
+ ReturnType
+ >
+
+ const { id } = useParams()
+ const { customer_group, isLoading, isError, error } = useAdminCustomerGroup(
+ id!,
+ undefined,
+ { initialData }
+ )
+
+ if (isLoading) {
+ return Loading...
+ }
+
+ if (isError || !customer_group) {
+ if (error) {
+ throw error
+ }
+
+ throw json("An unknown error occurred", 500)
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/index.ts
new file mode 100644
index 0000000000..c1f4e53399
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/index.ts
@@ -0,0 +1,2 @@
+export { CustomerGroupDetail as Component } from "./customer-group-detail"
+export { customerGroupLoader as loader } from "./loader"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/loader.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/loader.ts
new file mode 100644
index 0000000000..1c8c2beb6b
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/loader.ts
@@ -0,0 +1,22 @@
+import { AdminCustomerGroupsRes } from "@medusajs/medusa"
+import { Response } from "@medusajs/medusa-js"
+import { adminProductKeys } from "medusa-react"
+import { LoaderFunctionArgs } from "react-router-dom"
+
+import { medusa, queryClient } from "../../../lib/medusa"
+
+const customerGroupDetailQuery = (id: string) => ({
+ queryKey: adminProductKeys.detail(id),
+ queryFn: async () => medusa.admin.customerGroups.retrieve(id),
+})
+
+export const customerGroupLoader = async ({ params }: LoaderFunctionArgs) => {
+ const id = params.id
+ const query = customerGroupDetailQuery(id!)
+
+ return (
+ queryClient.getQueryData>(
+ query.queryKey
+ ) ?? (await queryClient.fetchQuery(query))
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx
new file mode 100644
index 0000000000..0e034ba603
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx
@@ -0,0 +1,91 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { CustomerGroup } from "@medusajs/medusa"
+import { Button, Drawer, Input } from "@medusajs/ui"
+import { useAdminUpdateCustomerGroup } from "medusa-react"
+import { useEffect } from "react"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import * as z from "zod"
+import { Form } from "../../../../../components/common/form"
+
+type EditCustomerGroupFormProps = {
+ group: CustomerGroup
+ onSuccessfulSubmit: () => void
+ subscribe: (state: boolean) => void
+}
+
+const EditCustomerGroupSchema = z.object({
+ name: z.string().min(1),
+})
+
+export const EditCustomerGroupForm = ({
+ group,
+ onSuccessfulSubmit,
+ subscribe,
+}: EditCustomerGroupFormProps) => {
+ const { t } = useTranslation()
+
+ const form = useForm>({
+ defaultValues: {
+ name: group.name || "",
+ },
+ resolver: zodResolver(EditCustomerGroupSchema),
+ })
+
+ const {
+ formState: { isDirty },
+ } = form
+
+ useEffect(() => {
+ subscribe(isDirty)
+ }, [isDirty])
+
+ const { mutateAsync, isLoading } = useAdminUpdateCustomerGroup(group.id)
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ await mutateAsync(data, {
+ onSuccess: () => {
+ onSuccessfulSubmit()
+ },
+ })
+ })
+
+ return (
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/index.ts
new file mode 100644
index 0000000000..3062389f75
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-customer-group-form"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/customer-group-edit.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/customer-group-edit.tsx
new file mode 100644
index 0000000000..0e43bdd314
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/customer-group-edit.tsx
@@ -0,0 +1,42 @@
+import { Drawer, Heading } from "@medusajs/ui"
+import { useAdminCustomerGroup } from "medusa-react"
+import { useTranslation } from "react-i18next"
+import { useParams } from "react-router-dom"
+import { useRouteModalState } from "../../../hooks/use-route-modal-state"
+import { EditCustomerGroupForm } from "./components/edit-customer-group-form"
+
+export const CustomerGroupEdit = () => {
+ const [open, onOpenChange, subscribe] = useRouteModalState()
+
+ const { id } = useParams()
+ const { customer_group, isLoading, isError, error } = useAdminCustomerGroup(
+ id!
+ )
+
+ const { t } = useTranslation()
+
+ const handleSuccessfulSubmit = () => {
+ onOpenChange(false, true)
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+ {t("customerGroups.editCustomerGroup")}
+
+ {!isLoading && customer_group && (
+
+ )}
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/index.ts
new file mode 100644
index 0000000000..edb805c0ad
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/index.ts
@@ -0,0 +1 @@
+export { CustomerGroupEdit as Component } from "./customer-group-edit"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx
new file mode 100644
index 0000000000..b090df3679
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx
@@ -0,0 +1,251 @@
+import { PencilSquare, Trash } from "@medusajs/icons"
+import { CustomerGroup } from "@medusajs/medusa"
+import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui"
+import {
+ PaginationState,
+ RowSelectionState,
+ createColumnHelper,
+ flexRender,
+ getCoreRowModel,
+ useReactTable,
+} from "@tanstack/react-table"
+import {
+ useAdminCustomerGroups,
+ useAdminDeleteCustomerGroup,
+} from "medusa-react"
+import { useMemo, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { Link, useNavigate } from "react-router-dom"
+import {
+ NoRecords,
+ NoResults,
+} from "../../../../../components/common/empty-table-content"
+import { TableRowActions } from "../../../../../components/common/table-row-actions"
+import { OrderBy } from "../../../../../components/filtering/order-by"
+import { Query } from "../../../../../components/filtering/query"
+import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
+import { useQueryParams } from "../../../../../hooks/use-query-params"
+
+const PAGE_SIZE = 50
+
+export const CustomerGroupListTable = () => {
+ const navigate = useNavigate()
+ const { t } = useTranslation()
+
+ const [{ pageIndex, pageSize }, setPagination] = useState({
+ pageIndex: 0,
+ pageSize: PAGE_SIZE,
+ })
+
+ const pagination = useMemo(
+ () => ({
+ pageIndex,
+ pageSize,
+ }),
+ [pageIndex, pageSize]
+ )
+
+ const [rowSelection, setRowSelection] = useState({})
+
+ const params = useQueryParams(["q", "order"])
+ const { customer_groups, count, isLoading, isError, error } =
+ useAdminCustomerGroups({
+ limit: PAGE_SIZE,
+ offset: pageIndex * PAGE_SIZE,
+ ...params,
+ })
+
+ const columns = useColumns()
+
+ const table = useReactTable({
+ data: customer_groups ?? [],
+ columns,
+ pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
+ state: {
+ pagination,
+ rowSelection,
+ },
+ onPaginationChange: setPagination,
+ onRowSelectionChange: setRowSelection,
+ getCoreRowModel: getCoreRowModel(),
+ manualPagination: true,
+ })
+
+ const noRecords =
+ !isLoading &&
+ !customer_groups?.length &&
+ !Object.values(params).filter(Boolean).length
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+ {t("customerGroups.domain")}
+
+
+ {t("general.create")}
+
+
+
+
+ {noRecords ? (
+
+ ) : (
+
+
+
+ {(customer_groups?.length || 0) > 0 ? (
+
+
+ {table.getHeaderGroups().map((headerGroup) => {
+ return (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ )
+ })}
+
+ )
+ })}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ navigate(`/customer-groups/${row.original.id}`)
+ }
+ >
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ ))}
+
+ ))}
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ )}
+
+
+ )
+}
+
+const CustomerGroupActions = ({ group }: { group: CustomerGroup }) => {
+ const { t } = useTranslation()
+ const prompt = usePrompt()
+
+ const { mutateAsync } = useAdminDeleteCustomerGroup(group.id)
+
+ const handleDelete = async () => {
+ const res = await prompt({
+ title: t("general.areYouSure"),
+ description: t("customerGroups.deleteCustomerGroupWarning", {
+ name: group.name,
+ }),
+ confirmText: t("general.delete"),
+ cancelText: t("general.cancel"),
+ })
+
+ if (!res) {
+ return
+ }
+
+ await mutateAsync(undefined)
+ }
+
+ return (
+ ,
+ label: t("general.edit"),
+ to: `/customer-groups/${group.id}/edit`,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ icon: ,
+ label: t("general.delete"),
+ onClick: handleDelete,
+ },
+ ],
+ },
+ ]}
+ />
+ )
+}
+
+const columnHelper = createColumnHelper()
+
+const useColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.accessor("name", {
+ header: t("fields.name"),
+ cell: ({ getValue }) => getValue(),
+ }),
+ columnHelper.display({
+ id: "actions",
+ cell: ({ row }) => ,
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/index.ts
new file mode 100644
index 0000000000..ead4d74743
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/index.ts
@@ -0,0 +1 @@
+export * from "./customer-group-list-table"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/customer-group-list.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/customer-group-list.tsx
new file mode 100644
index 0000000000..3e3786160a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/customer-group-list.tsx
@@ -0,0 +1,11 @@
+import { Outlet } from "react-router-dom"
+import { CustomerGroupListTable } from "./components/customer-group-list-table"
+
+export const CustomerGroupsList = () => {
+ return (
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/index.ts
new file mode 100644
index 0000000000..f04a72f6dd
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/index.ts
@@ -0,0 +1 @@
+export { CustomerGroupsList as Component } from "./customer-group-list"
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/details/details.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/details/details.tsx
deleted file mode 100644
index cc59b9ecdc..0000000000
--- a/packages/admin-next/dashboard/src/routes/customer-groups/details/details.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Container, Heading } from "@medusajs/ui";
-
-export const CustomerGroupDetails = () => {
- return (
-
-
- Customers
-
-
- );
-};
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/details/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/details/index.ts
deleted file mode 100644
index bb5e10a796..0000000000
--- a/packages/admin-next/dashboard/src/routes/customer-groups/details/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { CustomerGroupDetails as Component } from "./details";
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/list/index.ts b/packages/admin-next/dashboard/src/routes/customer-groups/list/index.ts
deleted file mode 100644
index c9929eedb7..0000000000
--- a/packages/admin-next/dashboard/src/routes/customer-groups/list/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { CustomerGroupsList as Component } from "./list";
diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/list/list.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/list/list.tsx
deleted file mode 100644
index 6d5f9e292b..0000000000
--- a/packages/admin-next/dashboard/src/routes/customer-groups/list/list.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Container, Heading } from "@medusajs/ui";
-
-export const CustomerGroupsList = () => {
- return (
-
-
- Customer Groups
-
-
- );
-};
diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx
index 1d0a8137ad..f43a9c5aa4 100644
--- a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx
+++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx
@@ -182,30 +182,21 @@ const useColumns = () => {
return useMemo(
() => [
+ columnHelper.accessor("email", {
+ header: t("fields.email"),
+ cell: ({ getValue }) => {getValue()} ,
+ }),
columnHelper.display({
id: "name",
header: t("fields.name"),
cell: ({ row }) => {
- const firstName = row.original.first_name
- const lastName = row.original.last_name
+ const name = [row.original.first_name, row.original.last_name]
+ .filter(Boolean)
+ .join(" ")
- let value = "-"
-
- if (firstName && lastName) {
- value = `${firstName} ${lastName}`
- } else if (firstName) {
- value = firstName
- } else if (lastName) {
- value = lastName
- }
-
- return {value}
+ return name || "-"
},
}),
- columnHelper.accessor("email", {
- header: t("fields.email"),
- cell: ({ getValue }) => {getValue()} ,
- }),
columnHelper.accessor("has_account", {
header: t("fields.account"),
cell: ({ getValue }) => {