From dfd6ecc744d459608dbdd79a8e533fe3ed919349 Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Tue, 22 Apr 2025 22:56:30 +0800 Subject: [PATCH] update customers in the middle, --- .../dashboard/cr/categories/[cat_id]/page.tsx | 3 - .../src/app/dashboard/cr/categories/page.tsx | 1 + .../[customerId]/BasicDetailCard.tsx | 80 +++ .../customers/[customerId]/TitleCard.tsx | 74 +++ .../dashboard/customers/[customerId]/page.tsx | 352 ++++-------- .../customers/edit/[customerId]/_PROMPT.md | 11 + .../customers/edit/[customerId]/page.tsx | 53 ++ .../src/app/dashboard/lp/categories/page.tsx | 3 +- .../cms/src/app/dashboard/students/page.tsx | 23 +- .../cms/src/app/dashboard/teachers/page.tsx | 23 +- .../cr/questions/cr-questions-pagination.tsx | 1 + .../components/dashboard/customer/_NOTES.md | 9 + .../dashboard/customer/customer-edit-form.tsx | 500 ++++++++++++++++++ .../lesson_type/lesson-types-filters.tsx | 72 ++- .../dashboard/teacher/customers-table.tsx | 68 ++- 002_source/cms/src/constants.ts | 6 + 002_source/cms/src/paths.ts | 1 + 17 files changed, 985 insertions(+), 295 deletions(-) create mode 100644 002_source/cms/src/app/dashboard/customers/[customerId]/BasicDetailCard.tsx create mode 100644 002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx create mode 100644 002_source/cms/src/app/dashboard/customers/edit/[customerId]/_PROMPT.md create mode 100644 002_source/cms/src/app/dashboard/customers/edit/[customerId]/page.tsx create mode 100644 002_source/cms/src/components/dashboard/customer/_NOTES.md create mode 100644 002_source/cms/src/components/dashboard/customer/customer-edit-form.tsx diff --git a/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx index d159c40..5fec968 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx @@ -25,7 +25,6 @@ import { defaultCrCategory } from '@/components/dashboard/cr/categories/_constan import { Notifications } from '@/components/dashboard/cr/categories/notifications'; import type { CrCategory } from '@/components/dashboard/cr/categories/type'; import FormLoading from '@/components/loading'; - import BasicDetailCard from './BasicDetailCard'; import TitleCard from './TitleCard'; @@ -37,7 +36,6 @@ export default function Page(): React.JSX.Element { // const [showLoading, setShowLoading] = React.useState(true); const [showError, setShowError] = React.useState({ show: false, detail: '' }); - // const [showLessonCategory, setShowLessonCategory] = React.useState(defaultCrCategory); @@ -55,7 +53,6 @@ export default function Page(): React.JSX.Element { .catch((err) => { logger.error(err); toast(t('list.error')); - setShowError({ show: true, detail: JSON.stringify(err) }); }) .finally(() => { diff --git a/002_source/cms/src/app/dashboard/cr/categories/page.tsx b/002_source/cms/src/app/dashboard/cr/categories/page.tsx index 53cea07..c00990e 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/page.tsx @@ -34,6 +34,7 @@ import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { const { t } = useTranslation(['lp_categories']); + // TODO: align to customers page.tsx const { email, phone, sortDir, status, name, visible, type } = searchParams; const router = useRouter(); const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); diff --git a/002_source/cms/src/app/dashboard/customers/[customerId]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/customers/[customerId]/BasicDetailCard.tsx new file mode 100644 index 0000000..fdb0dac --- /dev/null +++ b/002_source/cms/src/app/dashboard/customers/[customerId]/BasicDetailCard.tsx @@ -0,0 +1,80 @@ +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Card from '@mui/material/Card'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; +import { useTranslation } from 'react-i18next'; + +import { PropertyItem } from '@/components/core/property-item'; +import { PropertyList } from '@/components/core/property-list'; +// import { CrCategory } from '@/components/dashboard/cr/categories/type'; +import type { Customer } from '@/components/dashboard/customer/type.d'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: Customer; + handleEditClick: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + return ( + + { + handleEditClick(); + }} + > + + + } + avatar={ + + + + } + title={t('list.basic-details')} + /> + } + orientation="vertical" + sx={{ '--PropertyItem-padding': '12px 24px' }} + > + {( + [ + { + key: 'Customer ID', + value: ( + + ), + }, + { key: 'Email', value: model.email }, + { key: 'Quota', value: model.quota }, + { key: 'Status', value: model.status }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + ); +} diff --git a/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx b/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx new file mode 100644 index 0000000..9c9f0aa --- /dev/null +++ b/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx @@ -0,0 +1,74 @@ +'use client'; + +import * as React from 'react'; +import { Button } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { useTranslation } from 'react-i18next'; +import { Customer } from '@/components/dashboard/customer/type.d'; + +// import type { CrCategory } from '@/components/dashboard/cr/categories/type'; + +function getImageUrlFrRecord(record: Customer): string { + return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; +} + +export default function SampleTitleCard({ lpModel }: { lpModel: Customer }): React.JSX.Element { + const { t } = useTranslation(); + + return ( + <> + + + {t('empty')} + +
+ + {lpModel.email} + + } + label={lpModel.quota} + size="small" + variant="outlined" + /> + + + {lpModel.status} + +
+
+
+ +
+ + ); +} diff --git a/002_source/cms/src/app/dashboard/customers/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/customers/[customerId]/page.tsx index edf064e..6196a4a 100644 --- a/002_source/cms/src/app/dashboard/customers/[customerId]/page.tsx +++ b/002_source/cms/src/app/dashboard/customers/[customerId]/page.tsx @@ -1,43 +1,83 @@ +'use client'; + import * as React from 'react'; -import type { Metadata } from 'next'; import RouterLink from 'next/link'; -import Avatar from '@mui/material/Avatar'; +import { useParams, useRouter } from 'next/navigation'; +import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; +import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; +import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; +import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; + import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import IconButton from '@mui/material/IconButton'; -import LinearProgress from '@mui/material/LinearProgress'; import Link from '@mui/material/Link'; import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; -import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; +import type { RecordModel } from 'pocketbase'; +import { useTranslation } from 'react-i18next'; import { config } from '@/config'; import { paths } from '@/paths'; -import { dayjs } from '@/lib/dayjs'; -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -import { Notifications } from '@/components/dashboard/customer/notifications'; -import { Payments } from '@/components/dashboard/customer/payments'; -import type { Address } from '@/components/dashboard/customer/shipping-address'; -import { ShippingAddress } from '@/components/dashboard/customer/shipping-address'; +import { logger } from '@/lib/default-logger'; +import { pb } from '@/lib/pb'; +import { toast } from '@/components/core/toaster'; -export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; +import ErrorDisplay from '@/components/dashboard/error'; + +import { Notifications } from '@/components/dashboard/customer/notifications'; +import FormLoading from '@/components/loading'; +import BasicDetailCard from './BasicDetailCard'; +import TitleCard from './TitleCard'; +import { defaultCustomer } from '@/components/dashboard/customer/_constants'; +import type { Customer } from '@/components/dashboard/customer/type.d'; +import { COL_CUSTOMERS } from '@/constants'; export default function Page(): React.JSX.Element { + const { t } = useTranslation(); + const router = useRouter(); + // + const { customerId } = useParams<{ customerId: string }>(); + // + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + // + const [showLessonCategory, setShowLessonCategory] = React.useState(defaultCustomer); + + function handleEditClick(): void { + router.push(paths.dashboard.customers.edit(showLessonCategory.id)); + } + + React.useEffect(() => { + if (customerId) { + pb.collection(COL_CUSTOMERS) + .getOne(customerId) + .then((model: RecordModel) => { + setShowLessonCategory({ ...defaultCustomer, ...model }); + }) + .catch((err) => { + logger.error(err); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(err) }); + }) + .finally(() => { + setShowLoading(false); + }); + } + }, [customerId]); + + // return <>{JSON.stringify({ showError, showLessonCategory }, null, 2)}; + + if (showLoading) return ; + if (showError.show) + return ( + + ); + return ( - - - - MV - -
- - Miron Vitold - } - label="Active" - size="small" - variant="outlined" - /> - - - miron.vitold@domain.com - -
-
-
- -
+ +
- - + + - - - - - } - avatar={ - - - - } - title="Basic details" - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { key: 'Customer ID', value: }, - { key: 'Name', value: 'Miron Vitold' }, - { key: 'Email', value: 'miron.vitold@domain.com' }, - { key: 'Phone', value: '(425) 434-5535' }, - { key: 'Company', value: 'Devias IO' }, - { - key: 'Quota', - value: ( - - - - 50% - - - ), - }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - - - - - } - title="Security" - /> - - -
- -
- - A deleted customer cannot be restored. All data will be permanently removed. - -
-
-
+ +
- + - - - }> - Edit - - } - avatar={ - - - - } - title="Billing details" - /> - - - } sx={{ '--PropertyItem-padding': '16px' }}> - {( - [ - { key: 'Credit card', value: '**** 4142' }, - { key: 'Country', value: 'United States' }, - { key: 'State', value: 'Michigan' }, - { key: 'City', value: 'Southfield' }, - { key: 'Address', value: '1721 Bartlett Avenue, 48034' }, - { key: 'Tax ID', value: 'EU87956621' }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - - - - }> - Add - - } - avatar={ - - - - } - title="Shipping addresses" - /> - - - {( - [ - { - id: 'ADR-001', - country: 'United States', - state: 'Michigan', - city: 'Lansing', - zipCode: '48933', - street: '480 Haven Lane', - primary: true, - }, - { - id: 'ADR-002', - country: 'United States', - state: 'Missouri', - city: 'Springfield', - zipCode: '65804', - street: '4807 Lighthouse Drive', - }, - ] satisfies Address[] - ).map((address) => ( - - - - ))} - - - - + + + diff --git a/002_source/cms/src/app/dashboard/customers/edit/[customerId]/_PROMPT.md b/002_source/cms/src/app/dashboard/customers/edit/[customerId]/_PROMPT.md new file mode 100644 index 0000000..abf4465 --- /dev/null +++ b/002_source/cms/src/app/dashboard/customers/edit/[customerId]/_PROMPT.md @@ -0,0 +1,11 @@ +# task + +## instruction + +with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx` + +with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx` + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx` + +please draft a tsx for showing error to user thanks, diff --git a/002_source/cms/src/app/dashboard/customers/edit/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/customers/edit/[customerId]/page.tsx new file mode 100644 index 0000000..c499eff --- /dev/null +++ b/002_source/cms/src/app/dashboard/customers/edit/[customerId]/page.tsx @@ -0,0 +1,53 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form'; + +export default function Page(): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + + React.useEffect(() => { + // console.log('helloworld'); + }, []); + + return ( + + + +
+ + + {t('edit.title')} + +
+
+ {t('edit.title')} +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/lp/categories/page.tsx b/002_source/cms/src/app/dashboard/lp/categories/page.tsx index 93bb730..82e2355 100644 --- a/002_source/cms/src/app/dashboard/lp/categories/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/categories/page.tsx @@ -34,11 +34,10 @@ import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { const { t } = useTranslation(['lp_categories']); - const { email, phone, sortDir, status, name, visible, type } = searchParams; const router = useRouter(); + const { email, phone, sortDir, status, name, visible, type } = searchParams; 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: '' }); diff --git a/002_source/cms/src/app/dashboard/students/page.tsx b/002_source/cms/src/app/dashboard/students/page.tsx index 552f8dd..851e6ff 100644 --- a/002_source/cms/src/app/dashboard/students/page.tsx +++ b/002_source/cms/src/app/dashboard/students/page.tsx @@ -15,7 +15,7 @@ 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 } from '@/components/dashboard/customer/type.d'; export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; @@ -192,25 +192,38 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { }} > - + Customers - - + - + diff --git a/002_source/cms/src/app/dashboard/teachers/page.tsx b/002_source/cms/src/app/dashboard/teachers/page.tsx index 552f8dd..851e6ff 100644 --- a/002_source/cms/src/app/dashboard/teachers/page.tsx +++ b/002_source/cms/src/app/dashboard/teachers/page.tsx @@ -15,7 +15,7 @@ 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 } from '@/components/dashboard/customer/type.d'; export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; @@ -192,25 +192,38 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { }} > - + Customers - - + - + diff --git a/002_source/cms/src/components/dashboard/cr/questions/cr-questions-pagination.tsx b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-pagination.tsx index 5b91186..96437ac 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/cr-questions-pagination.tsx +++ b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-pagination.tsx @@ -44,6 +44,7 @@ export function CrQuestionsPagination({ page={page} rowsPerPage={rowsPerPage} rowsPerPageOptions={[5, 10, 25]} + // /> ); } diff --git a/002_source/cms/src/components/dashboard/customer/_NOTES.md b/002_source/cms/src/components/dashboard/customer/_NOTES.md new file mode 100644 index 0000000..e183566 --- /dev/null +++ b/002_source/cms/src/components/dashboard/customer/_NOTES.md @@ -0,0 +1,9 @@ +# task + +Create a customer edit form + +## steps + +- read other `tsx` files in same directory, +- draft `customer-edit-form.tsx`. +- the `customer-edit-form.tsx` is already there with content, you can modify it freely thanks. diff --git a/002_source/cms/src/components/dashboard/customer/customer-edit-form.tsx b/002_source/cms/src/components/dashboard/customer/customer-edit-form.tsx new file mode 100644 index 0000000..eda3e7d --- /dev/null +++ b/002_source/cms/src/components/dashboard/customer/customer-edit-form.tsx @@ -0,0 +1,500 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +// +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +// +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; +import FormLoading from '@/components/loading'; + +import ErrorDisplay from '../../error'; +import type { EditFormProps } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // accept file object when user change image + // accept http string when user not changing image + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + + // position + pos: zod.number().min(1, 'position is required').max(99), + + // it should be a valid JSON + init_answer: zod + .string() + .refine( + (value) => { + try { + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }, + { message: 'init_answer must be a valid JSON' } + ) + .optional(), + visible: zod.string(), + slug: zod.string().min(0, 'slug-is-required').max(255).optional(), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function CrQuestionEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_categories']); + + const { cat_id: catId } = useParams<{ cat_id: string }>(); + // + const [isUpdating, setIsUpdating] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(false); + // + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const tempUpdate: EditFormProps = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + init_answer: JSON.parse(values.init_answer || '{}'), + + visible: values.visible, + slug: values.slug || 'not-defined', + remarks: values.remarks, + description: values.description, + // + // TODO: remove below + type: '', + }; + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('update.failed')); + } finally { + setIsUpdating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + // TODO: need to align with save form + // use trycatch + const [textDescription, setTextDescription] = React.useState(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.cat_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + ); + + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + + setValue('avatar', url); + } else { + setValue('avatar', ''); + } + } catch (error) { + logger.error(error); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + void loadExistingData(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + {t('edit.cat_name')} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('edit.detail-information')} + + + { + return ( + + + {t('edit.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('edit.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('edit.updateButton')} + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-filters.tsx b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-filters.tsx index 470441d..f9e8a0f 100644 --- a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-filters.tsx +++ b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-filters.tsx @@ -178,6 +178,7 @@ export function LessonTypesFilters({ void fetchCount(); }, []); + // TODO: align to customers-filters.tsx order, this should be upper const hasFilters = status || email || phone || visible || name || type; return ( @@ -191,7 +192,13 @@ export function LessonTypesFilters({ > {tabs.map((tab) => ( } + icon={ + + } iconPosition="end" key={tab.value} label={tab.label} @@ -202,8 +209,16 @@ export function LessonTypesFilters({ ))} - - + + {t('Clear filters')} : null} {selection.selectedAny ? ( - - + + {selection.selected.size} {t('selected')} - ) : null} - @@ -261,7 +291,12 @@ function TypeFilterPopover(): React.JSX.Element { }, [initialValue]); return ( - + { @@ -297,7 +332,12 @@ function NameFilterPopover(): React.JSX.Element { }, [initialValue]); return ( - + { @@ -332,7 +372,12 @@ function EmailFilterPopover(): React.JSX.Element { }, [initialValue]); return ( - + { @@ -367,7 +412,12 @@ function PhoneFilterPopover(): React.JSX.Element { }, [initialValue]); return ( - + { diff --git a/002_source/cms/src/components/dashboard/teacher/customers-table.tsx b/002_source/cms/src/components/dashboard/teacher/customers-table.tsx index bf9b01a..f10c26d 100644 --- a/002_source/cms/src/components/dashboard/teacher/customers-table.tsx +++ b/002_source/cms/src/components/dashboard/teacher/customers-table.tsx @@ -36,7 +36,11 @@ export interface Customer { const columns = [ { formatter: (row): React.JSX.Element => ( - + {' '}
{row.name} - + {row.email}
@@ -59,9 +66,20 @@ const columns = [ }, { formatter: (row): React.JSX.Element => ( - - - + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} @@ -80,20 +98,46 @@ const columns = [ { formatter: (row): React.JSX.Element => { const mapping = { - active: { label: 'Active', icon: }, + active: { + label: 'Active', + icon: ( + + ), + }, blocked: { label: 'Blocked', icon: }, - pending: { label: 'Pending', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, } as const; const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; - return ; + return ( + + ); }, name: 'Status', width: '150px', }, { formatter: (): React.JSX.Element => ( - + ), @@ -129,7 +173,11 @@ export function CustomersTable({ rows }: CustomersTableProps): React.JSX.Element /> {!rows.length ? ( - + No customers found diff --git a/002_source/cms/src/constants.ts b/002_source/cms/src/constants.ts index be0183f..1041a59 100644 --- a/002_source/cms/src/constants.ts +++ b/002_source/cms/src/constants.ts @@ -9,6 +9,9 @@ const NS_LESSON_CATEGORY = 'lesson_category'; const COL_USERS = 'users'; const COL_USER_METAS = 'UserMetas'; +// +const COL_CUSTOMERS = 'Customers'; + // RULES: // do not use LP_CATEGORIES anymore const COL_QUIZ_LP_CATEGORIES = 'QuizLPCategories'; @@ -20,6 +23,7 @@ const COL_QUIZ_MF_QUESTIONS = 'QuizMFQuestions'; // New CR versions const COL_QUIZ_CR_CATEGORIES = 'QuizCRCategories'; const COL_QUIZ_CR_QUESTIONS = 'QuizCRQuestions'; +// export { COL_LESSON_TYPES, @@ -39,4 +43,6 @@ export { COL_QUIZ_CR_CATEGORIES, COL_QUIZ_CR_QUESTIONS, // + COL_CUSTOMERS, + // }; diff --git a/002_source/cms/src/paths.ts b/002_source/cms/src/paths.ts index 0a0f2c4..7e51347 100644 --- a/002_source/cms/src/paths.ts +++ b/002_source/cms/src/paths.ts @@ -140,6 +140,7 @@ export const paths = { list: '/dashboard/customers', create: '/dashboard/customers/create', details: (id: string) => `/dashboard/customers/${id}`, + edit: (id: string) => `/dashboard/customers/edit/${id}`, }, eCommerce: '/dashboard/e-commerce', fileStorage: '/dashboard/file-storage',