From 80feb972cb0b56c984eee17538fc20e0cef2fb49 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:25:59 +0100 Subject: [PATCH] feat(dashboard): Customer groups domain (#6098) **What** - Adds list, detail, edit, create, add customers views to the CG domain. **Note** - Missing metadata in forms (coming in separate PR for entire admin) - Missing table filters (separate PR) CLOSES CORE-1648 --- .../public/locales/en/translation.json | 11 +- .../empty-table-content.tsx | 8 +- .../common/table-row-actions/index.ts | 1 + .../table-row-actions/table-row-actions.tsx | 84 ++++ .../router-provider/router-provider.tsx | 38 +- .../add-customers-form/add-customers-form.tsx | 347 ++++++++++++++++ .../components/add-customers-form/index.ts | 1 + .../customer-group-add-customers.tsx | 18 + .../customer-group-add-customers/index.ts | 1 + .../create-customer-group-form.tsx | 105 +++++ .../create-customer-group-form/index.ts | 1 + .../customer-group-create.tsx | 15 + .../customer-group-create/index.ts | 1 + .../customer-group-customer-section.tsx | 387 ++++++++++++++++++ .../customer-group-customer-section/index.ts | 1 + .../customer-group-general-section.tsx | 56 +++ .../customer-group-general-section/index.ts | 1 + .../customer-group-detail.tsx | 40 ++ .../customer-group-detail/index.ts | 2 + .../customer-group-detail/loader.ts | 22 + .../edit-customer-group-form.tsx | 91 ++++ .../edit-customer-group-form/index.ts | 1 + .../customer-group-edit.tsx | 42 ++ .../customer-group-edit/index.ts | 1 + .../customer-group-list-table.tsx | 251 ++++++++++++ .../customer-group-list-table/index.ts | 1 + .../customer-group-list.tsx | 11 + .../customer-group-list/index.ts | 1 + .../customer-groups/details/details.tsx | 11 - .../routes/customer-groups/details/index.ts | 1 - .../src/routes/customer-groups/list/index.ts | 1 - .../src/routes/customer-groups/list/list.tsx | 11 - .../customer-list-table.tsx | 25 +- 33 files changed, 1540 insertions(+), 48 deletions(-) create mode 100644 packages/admin-next/dashboard/src/components/common/table-row-actions/index.ts create mode 100644 packages/admin-next/dashboard/src/components/common/table-row-actions/table-row-actions.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/customer-group-add-customers.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/customer-group-create.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/customer-group-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/loader.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/customer-group-edit.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/customer-group-list.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/details/details.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/list/list.tsx 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 && ( - + )} 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 ( +
+ + +
+ {form.formState.errors.customer_ids && ( + + {form.formState.errors.customer_ids.message} + + )} + + + + +
+
+ + {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) => ( + cg.id) + .includes(customerGroupId), + } + )} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ ) : ( +
+ +
+ )} +
+ +
+ )} +
+
+ + ) +} + +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 ( +
+ + +
+ + + + +
+
+ +
+
+ {t("customerGroups.createCustomerGroup")} + + {t("customerGroups.createCustomerGroupHint")} + +
+
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> +
+
+
+
+ + ) +} 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")} + + + +
+
+ {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 ( +
+ + + { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> + + +
+ + + + +
+
+
+ + ) +} 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")} + + + +
+
+ {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 }) => {