From 7dc7716f189274843d7ee96ca553a9cbd5e5ea65 Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Tue, 22 Apr 2025 22:28:40 +0800 Subject: [PATCH] update for customers, --- .../src/app/dashboard/customers/customers.tsx | 157 +++++++++ .../cms/src/app/dashboard/customers/page.tsx | 328 ++++++++---------- .../dashboard/customer/_constants.ts | 17 + .../customer/confirm-delete-modal.tsx | 125 +++++++ .../dashboard/customer/customers-filters.tsx | 211 +++++------ .../customer/customers-pagination.tsx | 27 +- .../customer/customers-selection-context.tsx | 2 +- .../dashboard/customer/customers-table.tsx | 242 ++++++++----- .../customer/email-filter-popover.tsx | 50 +++ .../customer/phone-filter-popover.tsx | 50 +++ .../components/dashboard/customer/type.d.tsx | 46 +++ 11 files changed, 893 insertions(+), 362 deletions(-) create mode 100644 002_source/cms/src/app/dashboard/customers/customers.tsx create mode 100644 002_source/cms/src/components/dashboard/customer/_constants.ts create mode 100644 002_source/cms/src/components/dashboard/customer/confirm-delete-modal.tsx create mode 100644 002_source/cms/src/components/dashboard/customer/email-filter-popover.tsx create mode 100644 002_source/cms/src/components/dashboard/customer/phone-filter-popover.tsx create mode 100644 002_source/cms/src/components/dashboard/customer/type.d.tsx diff --git a/002_source/cms/src/app/dashboard/customers/customers.tsx b/002_source/cms/src/app/dashboard/customers/customers.tsx new file mode 100644 index 0000000..b991279 --- /dev/null +++ b/002_source/cms/src/app/dashboard/customers/customers.tsx @@ -0,0 +1,157 @@ +// src/app/dashboard/customers/page.tsx +'use client'; +import type { Customer } from '@/components/dashboard/customer/type.d'; +import { dayjs } from '@/lib/dayjs'; + +export const SampleCustomers = [ + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, +] satisfies Customer[]; diff --git a/002_source/cms/src/app/dashboard/customers/page.tsx b/002_source/cms/src/app/dashboard/customers/page.tsx index efb61b4..a55faf8 100644 --- a/002_source/cms/src/app/dashboard/customers/page.tsx +++ b/002_source/cms/src/app/dashboard/customers/page.tsx @@ -1,7 +1,14 @@ +// src/app/dashboard/customers/page.tsx 'use client'; +// RULES: +// contains list page for customers (Customers) +// contain definition to collection only +// import * as React from 'react'; -// import type { Metadata } from 'next'; +import { useRouter } from 'next/navigation'; +import { COL_CUSTOMERS } from '@/constants'; +import { LoadingButton } from '@mui/lab'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; @@ -9,184 +16,138 @@ import Divider from '@mui/material/Divider'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import type { ListResult, RecordModel } from 'pocketbase'; import { config } from '@/config'; -import { dayjs } from '@/lib/dayjs'; import { CustomersFilters } from '@/components/dashboard/customer/customers-filters'; -import type { Filters } from '@/components/dashboard/customer/customers-filters'; +// import type { Filters } from '@/components/dashboard/customer/customers-filters'; import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination'; import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context'; import { CustomersTable } from '@/components/dashboard/customer/customers-table'; -import type { Customer } from '@/components/dashboard/customer/customers-table'; +import type { Customer, Filters } from '@/components/dashboard/customer/type.d'; +import { SampleCustomers } from './customers'; +import { useTranslation } from 'react-i18next'; -// export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; - -const customers = [ - { - id: 'USR-005', - name: 'Fran Perez', - avatar: '/assets/avatar-5.png', - email: 'fran.perez@domain.com', - phone: '(815) 704-0045', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(1, 'hour').toDate(), - }, - { - id: 'USR-004', - name: 'Penjani Inyene', - avatar: '/assets/avatar-4.png', - email: 'penjani.inyene@domain.com', - phone: '(803) 937-8925', - quota: 100, - status: 'active', - createdAt: dayjs().subtract(3, 'hour').toDate(), - }, - { - id: 'USR-003', - name: 'Carson Darrin', - avatar: '/assets/avatar-3.png', - email: 'carson.darrin@domain.com', - phone: '(715) 278-5041', - quota: 10, - status: 'blocked', - createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-002', - name: 'Siegbert Gottfried', - avatar: '/assets/avatar-2.png', - email: 'siegbert.gottfried@domain.com', - phone: '(603) 766-0431', - quota: 0, - status: 'pending', - createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-001', - name: 'Miron Vitold', - avatar: '/assets/avatar-1.png', - email: 'miron.vitold@domain.com', - phone: '(425) 434-5535', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), - }, - { - id: 'USR-005', - name: 'Fran Perez', - avatar: '/assets/avatar-5.png', - email: 'fran.perez@domain.com', - phone: '(815) 704-0045', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(1, 'hour').toDate(), - }, - { - id: 'USR-004', - name: 'Penjani Inyene', - avatar: '/assets/avatar-4.png', - email: 'penjani.inyene@domain.com', - phone: '(803) 937-8925', - quota: 100, - status: 'active', - createdAt: dayjs().subtract(3, 'hour').toDate(), - }, - { - id: 'USR-003', - name: 'Carson Darrin', - avatar: '/assets/avatar-3.png', - email: 'carson.darrin@domain.com', - phone: '(715) 278-5041', - quota: 10, - status: 'blocked', - createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-002', - name: 'Siegbert Gottfried', - avatar: '/assets/avatar-2.png', - email: 'siegbert.gottfried@domain.com', - phone: '(603) 766-0431', - quota: 0, - status: 'pending', - createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-001', - name: 'Miron Vitold', - avatar: '/assets/avatar-1.png', - email: 'miron.vitold@domain.com', - phone: '(425) 434-5535', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), - }, - { - id: 'USR-005', - name: 'Fran Perez', - avatar: '/assets/avatar-5.png', - email: 'fran.perez@domain.com', - phone: '(815) 704-0045', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(1, 'hour').toDate(), - }, - { - id: 'USR-004', - name: 'Penjani Inyene', - avatar: '/assets/avatar-4.png', - email: 'penjani.inyene@domain.com', - phone: '(803) 937-8925', - quota: 100, - status: 'active', - createdAt: dayjs().subtract(3, 'hour').toDate(), - }, - { - id: 'USR-003', - name: 'Carson Darrin', - avatar: '/assets/avatar-3.png', - email: 'carson.darrin@domain.com', - phone: '(715) 278-5041', - quota: 10, - status: 'blocked', - createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-002', - name: 'Siegbert Gottfried', - avatar: '/assets/avatar-2.png', - email: 'siegbert.gottfried@domain.com', - phone: '(603) 766-0431', - quota: 0, - status: 'pending', - createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-001', - name: 'Miron Vitold', - avatar: '/assets/avatar-1.png', - email: 'miron.vitold@domain.com', - phone: '(425) 434-5535', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), - }, -] satisfies Customer[]; - -interface PageProps { - searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; -} +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { pb } from '@/lib/pb'; +import { toast } from '@/components/core/toaster'; +import ErrorDisplay from '@/components/dashboard/error'; +import { defaultCustomer } from '@/components/dashboard/customer/_constants'; +import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['customers']); + const router = useRouter(); + const { email, phone, sortDir, status } = searchParams; - const sortedCustomers = applySort(customers, sortDir); - const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status }); + const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); + // + + const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + // + const [rowsPerPage, setRowsPerPage] = React.useState(5); + // + const [f, setF] = React.useState([]); + const [currentPage, setCurrentPage] = React.useState(0); + // + const [recordCount, setRecordCount] = React.useState(0); + const [listOption, setListOption] = React.useState({}); + const [listSort, setListSort] = React.useState({}); + + // + // const sortedCustomers = applySort(SampleCustomers, sortDir); + // const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status }); + + // + const reloadRows = async (): Promise => { + try { + const models: ListResult = await pb + .collection(COL_CUSTOMERS) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: Customer[] = items.map((lt) => { + return { ...defaultCustomer, ...lt }; + }); + + setLessonCategoriesData(tempLessonTypes); + setRecordCount(totalItems); + setF(tempLessonTypes); + // console.log({ currentPage, f }); + } catch (error) { + // + logger.error(error); + setShowError({ + // + show: true, + detail: JSON.stringify(error, null, 2), + }); + } finally { + setShowLoading(false); + } + }; + + const [lastListOption, setLastListOption] = React.useState({}); + const isFirstRun = React.useRef(false); + React.useEffect(() => { + if (!isFirstRun.current) { + isFirstRun.current = true; + } else { + if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + // reset page number as tab changes + setLastListOption(listOption); + setCurrentPage(0); + void reloadRows(); + } else { + void reloadRows(); + } + } + }, [currentPage, rowsPerPage, listOption]); React.useEffect(() => { - console.log('helloworld'); - }, []); + let tempFilter = [], + tempSortDir = ''; + + if (sortDir) { + tempSortDir = `-created`; + } + + if (email) { + tempFilter.push(`email ~ "%${email}%"`); + } + if (phone) { + tempFilter.push(`phone ~ "%${phone}%"`); + } + + let preFinalListOption = {}; + if (tempFilter.length > 0) { + preFinalListOption = { filter: tempFilter.join(' && ') }; + } + if (tempSortDir.length > 0) { + preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; + } + setListOption(preFinalListOption); + // setListOption({ + // filter: tempFilter.join(' && '), + // sort: tempSortDir, + // // + // }); + }, [sortDir, email, phone]); + + if (showLoading) return ; + + if (showError.show) + return ( + + ); return ( - Customers + {t('list.title')} - + {t('list.add')} + - + - + + +
{JSON.stringify(f, null, 2)}
+
); } @@ -272,3 +248,7 @@ function applyFilters(row: Customer[], { email, phone, status }: Filters): Custo return true; }); } + +interface PageProps { + searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; +} diff --git a/002_source/cms/src/components/dashboard/customer/_constants.ts b/002_source/cms/src/components/dashboard/customer/_constants.ts new file mode 100644 index 0000000..2e8381b --- /dev/null +++ b/002_source/cms/src/components/dashboard/customer/_constants.ts @@ -0,0 +1,17 @@ +import { dayjs } from '@/lib/dayjs'; +import type { Customer } from './type.d'; + +export const defaultCustomer: Customer = { + id: '', + name: '', + avatar: undefined, + email: '', + phone: undefined, + quota: 0, + status: 'pending', + createdAt: dayjs().toDate(), +}; + +export const emptyLpCategory: Customer = { + ...defaultCustomer, +}; diff --git a/002_source/cms/src/components/dashboard/customer/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/customer/confirm-delete-modal.tsx new file mode 100644 index 0000000..4d5b267 --- /dev/null +++ b/002_source/cms/src/components/dashboard/customer/confirm-delete-modal.tsx @@ -0,0 +1,125 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_TYPES } from '@/constants'; +import deleteQuizLPCategories from '@/db/QuizListenings/Delete'; +import { LoadingButton } from '@mui/lab'; +import { Button, Container, Modal, Paper } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; +import PocketBase from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; + +export default function ConfirmDeleteModal({ + open, + setOpen, + idToDelete, + reloadRows, +}: { + open: boolean; + setOpen: (b: boolean) => void; + idToDelete: string; + reloadRows: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + // const handleClose = () => setOpen(false); + function handleClose(): void { + setOpen(false); + } + + const [isDeleteing, setIsDeleteing] = React.useState(false); + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }; + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + deleteQuizLPCategories(idToDelete) + .then(() => { + reloadRows(); + handleClose(); + toast(t('delete.success')); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('delete.error')); + }) + .finally(() => { + setIsDeleteing(false); + }); + } + } + + return ( +
+ + + + + + + + + + + {t('Delete Lesson Type ?')} + + {t('Are you sure you want to delete lesson type ?')} + + + + + { + handleUserConfirmDelete(); + }} + loading={isDeleteing} + > + {t('Delete')} + + + + + + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/customer/customers-filters.tsx b/002_source/cms/src/components/dashboard/customer/customers-filters.tsx index 1567e3b..c847641 100644 --- a/002_source/cms/src/components/dashboard/customer/customers-filters.tsx +++ b/002_source/cms/src/components/dashboard/customer/customers-filters.tsx @@ -1,53 +1,72 @@ 'use client'; - +// RULES: +// T.B.A. +// import * as React from 'react'; import { useRouter } from 'next/navigation'; +import { getAllCustomersCount } from '@/db/Customers/GetAllCount'; + import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import OutlinedInput from '@mui/material/OutlinedInput'; import Select from '@mui/material/Select'; import type { SelectChangeEvent } from '@mui/material/Select'; import Stack from '@mui/material/Stack'; import Tab from '@mui/material/Tab'; import Tabs from '@mui/material/Tabs'; import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; -import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { FilterButton } from '@/components/core/filter-button'; import { Option } from '@/components/core/option'; import { useCustomersSelection } from './customers-selection-context'; +import GetBlockedCount from '@/db/Customers/GetBlockedCount'; +import GetPendingCount from '@/db/Customers/GetPendingCount'; +import GetActiveCount from '@/db/Customers/GetActiveCount'; +import PhoneFilterPopover from './phone-filter-popover'; +import EmailFilterPopover from './email-filter-popover'; +import type { CustomersFiltersProps, Filters, SortDir } from './type.d'; -// The tabs should be generated using API data. -const tabs = [ - { label: 'All', value: '', count: 5 }, - { label: 'Active', value: 'active', count: 3 }, - { label: 'Pending', value: 'pending', count: 1 }, - { label: 'Blocked', value: 'blocked', count: 1 }, -] as const; +export function CustomersFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: CustomersFiltersProps): React.JSX.Element { + const { t } = useTranslation(); -export interface Filters { - email?: string; - phone?: string; - status?: string; -} - -export type SortDir = 'asc' | 'desc'; - -export interface CustomersFiltersProps { - filters?: Filters; - sortDir?: SortDir; -} - -export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element { const { email, phone, status } = filters; + const [totalCount, setTotalCount] = React.useState(0); + const [activeCount, setActiveCount] = React.useState(0); + const [pendingCount, setPendingCount] = React.useState(0); + const [blockedCount, setBlockedCount] = React.useState(0); + const router = useRouter(); const selection = useCustomersSelection(); + function getVisible(): number { + return fullData.reduce((count, item: CrQuestion) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: CrQuestion) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + + // The tabs should be generated using API data. + const tabs = [ + { label: 'All', value: '', count: totalCount }, + { label: 'Active', value: 'active', count: activeCount }, + { label: 'Pending', value: 'pending', count: pendingCount }, + { label: 'Blocked', value: 'blocked', count: blockedCount }, + ] as const; + const updateSearchParams = React.useCallback( (newFilters: Filters, newSortDir: SortDir): void => { const searchParams = new URLSearchParams(); @@ -107,12 +126,43 @@ export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFi const hasFilters = status || email || phone; + React.useEffect(() => { + const fetchCount = async (): Promise => { + try { + const tc = await getAllCustomersCount(); + setTotalCount(tc); + + const bc = await GetBlockedCount(); + setBlockedCount(bc); + const pc = await GetPendingCount(); + setPendingCount(pc); + const ac = await GetActiveCount(); + setActiveCount(ac); + } catch (error) { + // + } + }; + void fetchCount(); + }, []); + return (
- + {tabs.map((tab) => ( } + icon={ + + } iconPosition="end" key={tab.value} label={tab.label} @@ -123,8 +173,16 @@ export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFi ))} - - + + } value={email} /> + } value={phone} /> + {hasFilters ? : null} {selection.selectedAny ? ( - - + + {selection.selected.size} selected - ) : null} - @@ -169,73 +244,3 @@ export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFi
); } - -function EmailFilterPopover(): React.JSX.Element { - const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); - const [value, setValue] = React.useState(''); - - React.useEffect(() => { - setValue((initialValue as string | undefined) ?? ''); - }, [initialValue]); - - return ( - - - { - setValue(event.target.value); - }} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onApply(value); - } - }} - value={value} - /> - - - - ); -} - -function PhoneFilterPopover(): React.JSX.Element { - const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); - const [value, setValue] = React.useState(''); - - React.useEffect(() => { - setValue((initialValue as string | undefined) ?? ''); - }, [initialValue]); - - return ( - - - { - setValue(event.target.value); - }} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onApply(value); - } - }} - value={value} - /> - - - - ); -} diff --git a/002_source/cms/src/components/dashboard/customer/customers-pagination.tsx b/002_source/cms/src/components/dashboard/customer/customers-pagination.tsx index ab01272..a7a89b6 100644 --- a/002_source/cms/src/components/dashboard/customer/customers-pagination.tsx +++ b/002_source/cms/src/components/dashboard/customer/customers-pagination.tsx @@ -10,20 +10,39 @@ function noop(): void { interface CustomersPaginationProps { count: number; page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; } -export function CustomersPagination({ count, page }: CustomersPaginationProps): React.JSX.Element { +export function CustomersPagination({ + count, + page, + // + setPage, + setRowsPerPage, + rowsPerPage, +}: CustomersPaginationProps): React.JSX.Element { // You should implement the pagination using a similar logic as the filters. // Note that when page change, you should keep the filter search params. + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value)); + // console.log(parseInt(event.target.value)); + }; return ( diff --git a/002_source/cms/src/components/dashboard/customer/customers-selection-context.tsx b/002_source/cms/src/components/dashboard/customer/customers-selection-context.tsx index 023dbc0..72cd8ba 100644 --- a/002_source/cms/src/components/dashboard/customer/customers-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/customer/customers-selection-context.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import type { Customer } from './customers-table'; +import type { Customer } from './type.d'; function noop(): void { return undefined; diff --git a/002_source/cms/src/components/dashboard/customer/customers-table.tsx b/002_source/cms/src/components/dashboard/customer/customers-table.tsx index bf9b01a..720d9a8 100644 --- a/002_source/cms/src/components/dashboard/customer/customers-table.tsx +++ b/002_source/cms/src/components/dashboard/customer/customers-table.tsx @@ -2,8 +2,10 @@ import * as React from 'react'; import RouterLink from 'next/link'; +import { LoadingButton } from '@mui/lab'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import IconButton from '@mui/material/IconButton'; import LinearProgress from '@mui/material/LinearProgress'; @@ -12,109 +14,184 @@ import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; import { paths } from '@/paths'; import { dayjs } from '@/lib/dayjs'; import { DataTable } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table'; +import ConfirmDeleteModal from './confirm-delete-modal'; import { useCustomersSelection } from './customers-selection-context'; +import type { Customer } from './type.d'; -export interface Customer { - id: string; - name: string; - avatar?: string; - email: string; - phone?: string; - quota: number; - status: 'pending' | 'active' | 'blocked'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {' '} -
- void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + {' '} +
+ + {row.name} + + + {row.email} + +
+
+ ), + name: 'Name', + width: '250px', + }, + { + formatter: (row): React.JSX.Element => ( + + + - {row.name} - - - {row.email} + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} -
-
- ), - name: 'Name', - width: '250px', - }, - { - formatter: (row): React.JSX.Element => ( - - - - {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} - - - ), - name: 'Quota', - width: '250px', - }, - { field: 'phone', name: 'Phone number', width: '150px' }, - { - formatter(row) { - return dayjs(row.createdAt).format('MMM D, YYYY h:mm A'); + + ), + name: 'Quota', + width: '150px', }, - name: 'Created at', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - active: { label: 'Active', icon: }, - blocked: { label: 'Blocked', icon: }, - pending: { label: 'Pending', icon: }, - } as const; - const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + { field: 'phone', name: 'Phone number', width: '150px' }, - return ; + { + formatter: (row): React.JSX.Element => { + // eslint-disable-next-line react-hooks/rules-of-hooks + + const mapping = { + active: { + label: 'Active', + icon: ( + + ), + }, + blocked: { label: 'Blocked', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ( + + ); + }, + name: 'Status', + width: '150px', }, - name: 'Status', - width: '150px', - }, - { - formatter: (): React.JSX.Element => ( - - - - ), - name: 'Actions', - hideName: true, - width: '100px', - align: 'right', - }, -] satisfies ColumnDef[]; + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY'); + }, + name: 'Created at', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + align: 'right', + }, + ]; +} export interface CustomersTableProps { rows: Customer[]; + reloadRows: () => void; } -export function CustomersTable({ rows }: CustomersTableProps): React.JSX.Element { +export function CustomersTable({ rows, reloadRows }: CustomersTableProps): React.JSX.Element { + const { t } = useTranslation(['customers']); const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection(); + const [idToDelete, setIdToDelete] = React.useState(''); + const [open, setOpen] = React.useState(false); + + function handleDeleteClick(testId: string): void { + setOpen(true); + setIdToDelete(testId); + } + return ( + - columns={columns} + columns={columns(handleDeleteClick)} onDeselectAll={deselectAll} onDeselectOne={(_, row) => { deselectOne(row.id); @@ -129,8 +206,13 @@ export function CustomersTable({ rows }: CustomersTableProps): React.JSX.Element /> {!rows.length ? ( - - No customers found + + {/* TODO: update this */} + {t('no-record-found')} ) : null} diff --git a/002_source/cms/src/components/dashboard/customer/email-filter-popover.tsx b/002_source/cms/src/components/dashboard/customer/email-filter-popover.tsx new file mode 100644 index 0000000..2636af0 --- /dev/null +++ b/002_source/cms/src/components/dashboard/customer/email-filter-popover.tsx @@ -0,0 +1,50 @@ +'use client'; + +import * as React from 'react'; + +import Button from '@mui/material/Button'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; + +import { FilterPopover, useFilterContext } from '@/components/core/filter-button'; + +// EmailFilterPopover -> email-filter-popover.tsx +export default function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/customer/phone-filter-popover.tsx b/002_source/cms/src/components/dashboard/customer/phone-filter-popover.tsx new file mode 100644 index 0000000..08cbad7 --- /dev/null +++ b/002_source/cms/src/components/dashboard/customer/phone-filter-popover.tsx @@ -0,0 +1,50 @@ +'use client'; + +import * as React from 'react'; + +import Button from '@mui/material/Button'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; + +import { FilterPopover, useFilterContext } from '@/components/core/filter-button'; + +// phone-filter-popover.tsx +export default function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/customer/type.d.tsx b/002_source/cms/src/components/dashboard/customer/type.d.tsx new file mode 100644 index 0000000..6f8d61e --- /dev/null +++ b/002_source/cms/src/components/dashboard/customer/type.d.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { SortDir } from './phone-filter-popover'; +import { Customer } from './type.d'; + +export interface Customer { + id: string; + name: string; + avatar?: string; + email: string; + phone?: string; + quota: number; + status: 'pending' | 'active' | 'blocked'; + createdAt: Date; + updatedAt?: Date; +} + +export interface CreateFormProps { + name: string; + avatar?: string; + email: string; + phone?: string; + quota: number; + status: 'pending' | 'active' | 'blocked'; +} + +export interface EditFormProps { + name: string; + avatar?: string; + email: string; + phone?: string; + quota: number; + status: 'pending' | 'active' | 'blocked'; +} +export interface CustomersFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: Customer[]; +} +export interface Filters { + email?: string; + phone?: string; + status?: string; +} + +export type SortDir = 'asc' | 'desc';