From c8d184465ad5c581a3431f8be14b5dcc4543716d Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Wed, 23 Apr 2025 01:01:31 +0800 Subject: [PATCH] update customer edit form in the middle, --- .../customers/edit/[customerId]/page.tsx | 1 + .../dashboard/customer/customer-edit-form.tsx | 465 +++++++++++------- .../dashboard/customer/customers-table.tsx | 2 +- .../components/dashboard/customer/type.d.tsx | 24 +- 002_source/cms/src/db/schema.json | 147 +++++- 5 files changed, 448 insertions(+), 191 deletions(-) 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 index c499eff..e4e23bc 100644 --- a/002_source/cms/src/app/dashboard/customers/edit/[customerId]/page.tsx +++ b/002_source/cms/src/app/dashboard/customers/edit/[customerId]/page.tsx @@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form'; +import { CustomerEditForm } from '@/components/dashboard/customer/customer-edit-form'; export default function Page(): React.JSX.Element { const { t } = useTranslation(['lp_categories']); 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 index eda3e7d..e60adaf 100644 --- a/002_source/cms/src/components/dashboard/customer/customer-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/customer/customer-edit-form.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import RouterLink from 'next/link'; import { useParams, useRouter } from 'next/navigation'; // -import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { COL_CUSTOMERS } from '@/constants'; import { zodResolver } from '@hookform/resolvers/zod'; import { LoadingButton } from '@mui/lab'; // @@ -35,64 +35,61 @@ 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'; +// import ErrorDisplay from '../../error'; +import ErrorDisplay from '../error'; +import isDevelopment from '@/lib/check-is-development'; // 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 + name: zod.string().min(1, 'Name is required').max(255), + email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), + phone: zod.string().min(1, 'Phone is required').max(15), + company: zod.string().max(255).optional(), + billingAddress: zod.object({ + country: zod.string().min(1, 'Country is required').max(255), + state: zod.string().min(1, 'State is required').max(255), + city: zod.string().min(1, 'City is required').max(255), + zipCode: zod.string().min(1, 'Zip code is required').max(255), + line1: zod.string().min(1, 'Street line 1 is required').max(255), + line2: zod.string().max(255).optional(), + }), + taxId: zod.string().max(255).optional(), + timezone: zod.string().min(1, 'Timezone is required').max(255), + language: zod.string().min(1, 'Language is required').max(255), + currency: zod.string().min(1, 'Currency is required').max(255), 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: '', + name: '', + email: '', + phone: '', + company: '', + billingAddress: { + country: '', + state: '', + city: '', + zipCode: '', + line1: '', + line2: '', + }, + taxId: '', + timezone: '', + language: '', + currency: '', + avatar: '', } satisfies Values; -export function CrQuestionEditForm(): React.JSX.Element { +export function CustomerEditForm(): React.JSX.Element { const router = useRouter(); const { t } = useTranslation(['lp_categories']); - const { cat_id: catId } = useParams<{ cat_id: string }>(); + const { customerId } = useParams<{ customerId: string }>(); // const [isUpdating, setIsUpdating] = React.useState(false); const [showLoading, setShowLoading] = React.useState(false); @@ -112,37 +109,31 @@ export function CrQuestionEditForm(): React.JSX.Element { 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: '', + const updateData = { + name: values.name, + email: values.email, + phone: values.phone, + company: values.company, + billingAddress: values.billingAddress, + taxId: values.taxId, + timezone: values.timezone, + language: values.language, + currency: values.currency, + avatar: values.avatar ? await base64ToFile(values.avatar) : null, }; 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); + await pb.collection(COL_CUSTOMERS).update(customerId, updateData); + toast.success('Customer updated successfully'); + router.push(paths.dashboard.customers.list); } catch (error) { logger.error(error); - toast.error(t('update.failed')); + toast.error('Failed to update customer'); } finally { setIsUpdating(false); } }, - // t is not necessary here - // eslint-disable-next-line react-hooks/exhaustive-deps - [router] + [customerId, router] ); const avatarInputRef = React.useRef(null); @@ -171,41 +162,33 @@ export function CrQuestionEditForm(): React.JSX.Element { setShowLoading(true); try { - const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); + const result = await pb.collection(COL_CUSTOMERS).getOne(id); + reset({ ...defaultValues, ...result }); + console.log({ result }); - reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); - setTextDescription(result.description); - setTextRemarks(result.remarks); - - if (result.cat_image !== '') { + if (result.avatar_file) { const fetchResult = await fetch( - `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar_file}` ); - 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')); - + toast.error('Failed to load customer data'); setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); } finally { setShowLoading(false); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [catId] + [reset, setValue] ); React.useEffect(() => { - void loadExistingData(catId); + void loadExistingData(customerId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [catId]); + }, [customerId]); if (showLoading) return ; if (showError.show) @@ -290,112 +273,79 @@ export function CrQuestionEditForm(): React.JSX.Element { xs={12} > ( - {t('edit.cat_name')} + Name - {errors.cat_name ? {errors.cat_name.message} : null} + {errors.name ? {errors.name.message} : null} )} /> - {/* */} ( - {t('edit.pos')} + Email { - field.onChange(parseInt(e.target.value)); - }} - type="number" + type="email" /> - {errors.pos ? {errors.pos.message} : null} + {errors.email ? {errors.email.message} : null} )} /> - {/* */} ( - {t('edit.slug')} + Phone - {errors.slug ? {errors.slug.message} : null} + {errors.phone ? {errors.phone.message} : null} )} /> - {/* */} ( - {t('edit.visible')} - - - {errors.visible ? {errors.visible.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.init_answer')} - - {errors.init_answer ? {errors.init_answer.message} : null} + Company + + {errors.company ? {errors.company.message} : null} )} /> @@ -404,7 +354,7 @@ export function CrQuestionEditForm(): React.JSX.Element { {/* */} - {t('edit.detail-information')} + Billing Information { - return ( - - - {t('edit.description')} - - - { - field.onChange({ target: { value: editor.getHTML() } }); - }} - placeholder={t('edit.description.default')} - /> - - - ); - }} + name="billingAddress.country" + render={({ field }) => ( + + Country + + {errors.billingAddress?.country ? ( + {errors.billingAddress.country.message} + ) : null} + + )} /> ( - - - {t('edit.remarks')} - - - { - field.onChange({ target: { value: editor.getText() } }); - }} - hideToolbar - placeholder={t('edit.remarks.default')} - /> - - + + State + + {errors.billingAddress?.state ? ( + {errors.billingAddress.state.message} + ) : null} + + )} + /> + + + ( + + City + + {errors.billingAddress?.city ? ( + {errors.billingAddress.city.message} + ) : null} + + )} + /> + + + ( + + Zip Code + + {errors.billingAddress?.zipCode ? ( + {errors.billingAddress.zipCode.message} + ) : null} + + )} + /> + + + ( + + Address Line 1 + + {errors.billingAddress?.line1 ? ( + {errors.billingAddress.line1.message} + ) : null} + + )} + /> + + + ( + + Tax ID + + {errors.taxId ? {errors.taxId.message} : null} + + )} + /> + + + + + + Additional Information + + + ( + + Timezone + + {errors.timezone ? {errors.timezone.message} : null} + + )} + /> + + + ( + + Language + + {errors.language ? {errors.language.message} : null} + + )} + /> + + + ( + + Currency + + {errors.currency ? {errors.currency.message} : null} + )} /> - {/* */} 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 720d9a8..d22e1c9 100644 --- a/002_source/cms/src/components/dashboard/customer/customers-table.tsx +++ b/002_source/cms/src/components/dashboard/customer/customers-table.tsx @@ -143,7 +143,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef diff --git a/002_source/cms/src/components/dashboard/customer/type.d.tsx b/002_source/cms/src/components/dashboard/customer/type.d.tsx index 6f8d61e..85e31b4 100644 --- a/002_source/cms/src/components/dashboard/customer/type.d.tsx +++ b/002_source/cms/src/components/dashboard/customer/type.d.tsx @@ -1,7 +1,6 @@ 'use client'; -import { SortDir } from './phone-filter-popover'; -import { Customer } from './type.d'; +export type SortDir = 'asc' | 'desc'; export interface Customer { id: string; @@ -26,11 +25,24 @@ export interface CreateFormProps { export interface EditFormProps { name: string; - avatar?: string; email: string; phone?: string; - quota: number; - status: 'pending' | 'active' | 'blocked'; + company?: string; + billingAddress?: { + country: string; + state: string; + city: string; + zipCode: string; + line1: string; + line2?: string; + }; + taxId?: string; + timezone: string; + language: string; + currency: string; + avatar?: string; + // quota?: number; + // status?: 'pending' | 'active' | 'blocked'; } export interface CustomersFiltersProps { filters?: Filters; @@ -42,5 +54,3 @@ export interface Filters { phone?: string; status?: string; } - -export type SortDir = 'asc' | 'desc'; diff --git a/002_source/cms/src/db/schema.json b/002_source/cms/src/db/schema.json index dc47784..9c2202f 100644 --- a/002_source/cms/src/db/schema.json +++ b/002_source/cms/src/db/schema.json @@ -626,6 +626,19 @@ "thumbs": [], "type": "file" }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2809058197", + "maxSelect": 1, + "minSelect": 0, + "name": "user_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, { "hidden": false, "id": "autodate2990389176", @@ -2852,6 +2865,138 @@ ], "system": true }, + { + "id": "pbc_1509025625", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "billingAddress", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1400097126", + "max": 0, + "min": 0, + "name": "country", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2744374011", + "max": 0, + "min": 0, + "name": "state", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text760939060", + "max": 0, + "min": 0, + "name": "city", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4114525948", + "max": 0, + "min": 0, + "name": "zipCode", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3620973610", + "max": 0, + "min": 0, + "name": "line1", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1322974608", + "max": 0, + "min": 0, + "name": "line2", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, { "id": "pbc_123408445", "listRule": "", @@ -2990,4 +3135,4 @@ "indexes": [], "system": false } -] \ No newline at end of file +]