refactor Student and Teacher create/edit forms to implement i18n support, update UI components, and standardize API calls
```
This commit is contained in:
louiscklaw
2025-05-15 11:35:29 +08:00
parent 097918340c
commit 7e2844dd74
19 changed files with 289 additions and 158 deletions

View File

@@ -1,19 +1,25 @@
'use client';
// src/app/dashboard/students/create/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react'; import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { useTranslation } from 'react-i18next';
import { config } from '@/config'; import { config } from '@/config';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { StudentCreateForm } from '@/components/dashboard/student/student-create-form'; import { StudentCreateForm } from '@/components/dashboard/student/student-create-form';
export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
export default function Page(): React.JSX.Element { export default function Page(): React.JSX.Element {
const { t } = useTranslation(['students']);
return ( return (
<Box <Box
sx={{ sx={{
@@ -29,16 +35,19 @@ export default function Page(): React.JSX.Element {
<Link <Link
color="text.primary" color="text.primary"
component={RouterLink} component={RouterLink}
href={paths.dashboard.customers.list} href={paths.dashboard.students.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }} sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2" variant="subtitle2"
> >
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" /> <ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Customers {t('students')}
</Link> </Link>
</div> </div>
<div> <div>
<Typography variant="h4">Create customer</Typography> <Typography variant="h4">
{t('create-student')}
{/* */}
</Typography>
</div> </div>
</Stack> </Stack>
<StudentCreateForm /> <StudentCreateForm />

View File

@@ -1,6 +1,9 @@
'use client'; 'use client';
// src/app/dashboard/students/edit/[customerId]/page.tsx // src/app/dashboard/students/edit/[id]/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';

View File

@@ -1,4 +1,9 @@
'use client'; 'use client';
// src/app/dashboard/teachers/create/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
@@ -6,12 +11,15 @@ import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { useTranslation } from 'react-i18next';
import { config } from '@/config'; import { config } from '@/config';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { TeacherCreateForm } from '@/components/dashboard/teacher/teacher-create-form'; import { TeacherCreateForm } from '@/components/dashboard/teacher/teacher-create-form';
export default function Page(): React.JSX.Element { export default function Page(): React.JSX.Element {
const { t } = useTranslation(['teachers']);
return ( return (
<Box <Box
sx={{ sx={{
@@ -32,11 +40,14 @@ export default function Page(): React.JSX.Element {
variant="subtitle2" variant="subtitle2"
> >
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" /> <ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Teachers {t('teachers')}
</Link> </Link>
</div> </div>
<div> <div>
<Typography variant="h4">Create teacher</Typography> <Typography variant="h4">
{t('create-teacher')}
{/* */}
</Typography>
</div> </div>
</Stack> </Stack>
<TeacherCreateForm /> <TeacherCreateForm />

View File

@@ -1,5 +1,9 @@
'use client'; 'use client';
// src/app/dashboard/teachers/edit/[id]/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
@@ -10,7 +14,8 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form'; // TODO: remove me
// import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
import { TeacherEditForm } from '@/components/dashboard/teacher/teacher-edit-form'; import { TeacherEditForm } from '@/components/dashboard/teacher/teacher-edit-form';
export default function Page(): React.JSX.Element { export default function Page(): React.JSX.Element {

View File

@@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next';
import { PropertyItem } from '@/components/core/property-item'; import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list'; import { PropertyList } from '@/components/core/property-list';
// import { CrCategory } from '@/components/dashboard/cr/categories/type'; // import { CrCategory } from '@/components/dashboard/cr/categories/type';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d'; import type { UserMeta } from '@/components/dashboard/user_meta/type_move.d';
export default function BasicDetailCard({ export default function BasicDetailCard({
userMeta, userMeta,

View File

@@ -1,5 +1,9 @@
'use client'; 'use client';
// src/app/dashboard/user_metas/view/[id]/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -7,7 +11,7 @@ import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import { COL_USER_METAS } from '@/constants';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
@@ -21,16 +25,14 @@ import { paths } from '@/paths';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error'; import ErrorDisplay from '@/components/dashboard/error';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
import { Notifications } from '@/components/dashboard/user_meta/notifications'; import { Notifications } from '@/components/dashboard/user_meta/notifications';
import type { UserMeta } from '@/components/dashboard/user_meta/type_move.d';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard'; import BasicDetailCard from './BasicDetailCard';
import TitleCard from './TitleCard'; import TitleCard from './TitleCard';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d';
import { COL_USER_METAS } from '@/constants';
export default function Page(): React.JSX.Element { export default function Page(): React.JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -1,3 +1,6 @@
// PURPOSE
// T.B.A.
//
const helloworld = 'helloworld'; const helloworld = 'helloworld';
export { helloworld }; export { helloworld };

View File

@@ -1,14 +1,13 @@
'use client'; 'use client';
// src/components/dashboard/student/student-create-form.tsx // src/components/dashboard/student/student-create-form.tsx
// PURPOSE
// T.B.A.
// //
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { createStudent } from '@/db/Students/Create'; import { createStudent } from '@/db/Students/Create';
import { getStudentById } from '@/db/Students/GetById';
import { UpdateStudentById } from '@/db/Students/UpdateById';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
// //
@@ -41,14 +40,10 @@ import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development'; import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { Option } from '@/components/core/option';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error'; // import ErrorDisplay from '../../error';
import ErrorDisplay from '../error'; import { CreateFormProps } from './type.d';
import { CreateFormProps, Student } from './type.d';
// TODO: review schema // TODO: review schema
const schema = zod.object({ const schema = zod.object({
@@ -135,11 +130,11 @@ export function StudentCreateForm(): React.JSX.Element {
// } // }
const record = await createStudent(tempCreate); const record = await createStudent(tempCreate);
toast.success('Student created'); toast.success('student-created');
// router.push(paths.dashboard.students.view(record.id)); router.push(paths.dashboard.students.view(record.id));
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
toast.error('Failed to create Student'); toast.error('failed-to-create-student');
} finally { } finally {
setIsUpdating(false); setIsUpdating(false);
} }

View File

@@ -1,12 +1,13 @@
'use client'; 'use client';
// src/components/dashboard/student/student-edit-form.tsx // src/components/dashboard/student/student-edit-form.tsx
// PURPOSE:
// handle change details for student collection
// //
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
// //
import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById'; import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { getStudentById } from '@/db/Students/GetById'; import { getStudentById } from '@/db/Students/GetById';
import { UpdateStudentById } from '@/db/Students/UpdateById'; import { UpdateStudentById } from '@/db/Students/UpdateById';
@@ -40,12 +41,13 @@ import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development'; import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb'; import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error'; // import ErrorDisplay from '../../error';
import ErrorDisplay from '../error'; import ErrorDisplay from '../error';
import type { Student } from './type.d';
// TODO: review schema // TODO: review schema
const schema = zod.object({ const schema = zod.object({
@@ -116,8 +118,6 @@ export function StudentEditForm(): React.JSX.Element {
setIsUpdating(true); setIsUpdating(true);
const updateData = { const updateData = {
avatar: values.avatar ? await base64ToFile(values.avatar) : null,
//
name: values.name, name: values.name,
email: values.email, email: values.email,
phone: values.phone, phone: values.phone,
@@ -125,16 +125,17 @@ export function StudentEditForm(): React.JSX.Element {
// //
// billingAddress: values.billingAddress, // billingAddress: values.billingAddress,
// //
taxId: values.taxId,
timezone: values.timezone, timezone: values.timezone,
language: values.language, language: values.language,
currency: values.currency, currency: values.currency,
taxId: values.taxId, avatar: values.avatar ? await base64ToFile(values.avatar) : null,
}; };
try { try {
// await pb.collection(COL_USER_METAS).update(studentId, updateData);
await UpdateStudentById(studentId, updateData); await UpdateStudentById(studentId, updateData);
toast.success('Student updated successfully'); //
toast.success(t('student-updated-successfully'));
router.push(paths.dashboard.students.list); router.push(paths.dashboard.students.list);
if (billingAddressId) { if (billingAddressId) {
@@ -142,7 +143,7 @@ export function StudentEditForm(): React.JSX.Element {
} }
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
toast.error('Failed to update student'); toast.error(t('failed-to-update-student'));
} finally { } finally {
setIsUpdating(false); setIsUpdating(false);
} }
@@ -176,22 +177,21 @@ export function StudentEditForm(): React.JSX.Element {
setShowLoading(true); setShowLoading(true);
try { try {
const result = await getStudentById(id); const result = (await getStudentById(id)) as unknown as Student;
//
reset({ ...defaultValues, ...result }); reset({ ...defaultValues, ...result });
setBillingAddressId(result.billingAddress.id); setBillingAddressId(result.billingAddress.id);
if (result.avatar) { if (result.avatar) {
const fetchResult = await fetch( const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar));
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar}`
);
const blob = await fetchResult.blob(); const blob = await fetchResult.blob();
const url = await fileToBase64(blob); const url = await fileToBase64(blob);
setValue('avatar', url); setValue('avatar', url);
} }
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
toast.error('Failed to load student data'); toast.error(t('failed-to-load-student-data'));
setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally { } finally {
setShowLoading(false); setShowLoading(false);
@@ -365,6 +365,7 @@ export function StudentEditForm(): React.JSX.Element {
)} )}
/> />
</Grid> </Grid>
{/* */}
</Grid> </Grid>
</Stack> </Stack>
{/* */} {/* */}

View File

@@ -1,9 +1,19 @@
'use client'; 'use client';
// src/components/dashboard/teacher/teacher-create-form.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { createTeacher } from '@/db/Teachers/Create';
import { getTeacherById } from '@/db/Teachers/GetById';
import { UpdateTeacherById } from '@/db/Teachers/UpdateById';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@@ -16,41 +26,38 @@ import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import FormHelperText from '@mui/material/FormHelperText'; import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel'; import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import OutlinedInput from '@mui/material/OutlinedInput'; import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2'; import Grid from '@mui/material/Unstable_Grid2';
//
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
//
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod'; import { z as zod } from 'zod';
import { paths } from '@/paths'; import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { Option } from '@/components/core/option'; import { Option } from '@/components/core/option';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import { createTeacher } from '@/db/Teachers/Create'; import FormLoading from '@/components/loading';
import isDevelopment from '@/lib/check-is-development';
function fileToBase64(file: Blob): Promise<string> { // import ErrorDisplay from '../../error';
return new Promise((resolve, reject) => { import ErrorDisplay from '../error';
const reader = new FileReader(); import { CreateFormProps } from './type.d';
reader.readAsDataURL(file);
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(new Error('Error converting file to base64'));
};
});
}
// TODO: review schema
const schema = zod.object({ const schema = zod.object({
avatar: zod.string().optional(),
name: zod.string().min(1, 'Name is required').max(255), 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), 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), phone: zod.string().min(1, 'Phone is required').max(25),
company: zod.string().max(255), company: zod.string().max(255).optional(),
billingAddress: zod.object({ billingAddress: zod.object({
country: zod.string().min(1, 'Country is required').max(255), country: zod.string().min(1, 'Country is required').max(255),
state: zod.string().min(1, 'State is required').max(255), state: zod.string().min(1, 'State is required').max(255),
@@ -63,12 +70,12 @@ const schema = zod.object({
timezone: zod.string().min(1, 'Timezone is required').max(255), timezone: zod.string().min(1, 'Timezone is required').max(255),
language: zod.string().min(1, 'Language is required').max(255), language: zod.string().min(1, 'Language is required').max(255),
currency: zod.string().min(1, 'Currency is required').max(255), currency: zod.string().min(1, 'Currency is required').max(255),
avatar: zod.string().optional(),
}); });
type Values = zod.infer<typeof schema>; type Values = zod.infer<typeof schema>;
const defaultValues = { const defaultValues = {
avatar: '',
name: 'new name', name: 'new name',
email: '123@123.com', email: '123@123.com',
phone: '91234567', phone: '91234567',
@@ -85,10 +92,18 @@ const defaultValues = {
timezone: 'new_york', timezone: 'new_york',
language: 'en', language: 'en',
currency: 'USD', currency: 'USD',
avatar: '',
} satisfies Values; } satisfies Values;
export function TeacherCreateForm(): React.JSX.Element { export function TeacherCreateForm(): React.JSX.Element {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(['students']);
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const { const {
control, control,
@@ -100,14 +115,31 @@ export function TeacherCreateForm(): React.JSX.Element {
const onSubmit = React.useCallback( const onSubmit = React.useCallback(
async (values: Values): Promise<void> => { async (values: Values): Promise<void> => {
// Use standard create method from db/Customers/Create
const tempCreate: CreateFormProps = {
avatar: values.avatar ? await base64ToFile(values.avatar) : null,
//
name: values.name,
email: values.email,
phone: values.phone,
company: values.company,
timezone: values.timezone,
language: values.language,
currency: values.currency,
taxId: values.taxId,
state: 'pending',
meta: {},
};
try { try {
// Use standard create method from db/Customers/Create const record = await createTeacher(tempCreate);
const record = await createTeacher(values); toast.success('teacher-created');
toast.success('Customer created');
router.push(paths.dashboard.teachers.details(record.id)); router.push(paths.dashboard.teachers.details(record.id));
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
toast.error('Failed to create customer'); toast.error('failed-to-create-teacher');
} finally {
setIsUpdating(false);
} }
}, },
[router] [router]
@@ -137,7 +169,7 @@ export function TeacherCreateForm(): React.JSX.Element {
spacing={4} spacing={4}
> >
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">Account information</Typography> <Typography variant="h6">{t('create.basic-info')}</Typography>
<Grid <Grid
container container
spacing={3} spacing={3}
@@ -151,12 +183,13 @@ export function TeacherCreateForm(): React.JSX.Element {
<Box <Box
sx={{ sx={{
border: '1px dashed var(--mui-palette-divider)', border: '1px dashed var(--mui-palette-divider)',
borderRadius: '50%', borderRadius: '5%',
display: 'inline-flex', display: 'inline-flex',
p: '4px', p: '4px',
}} }}
> >
<Avatar <Avatar
variant="rounded"
src={avatar} src={avatar}
sx={{ sx={{
'--Avatar-size': '100px', '--Avatar-size': '100px',
@@ -175,8 +208,8 @@ export function TeacherCreateForm(): React.JSX.Element {
spacing={1} spacing={1}
sx={{ alignItems: 'flex-start' }} sx={{ alignItems: 'flex-start' }}
> >
<Typography variant="subtitle1">Avatar</Typography> <Typography variant="subtitle1">{t('create.avatar')}</Typography>
<Typography variant="caption">Min 400x400px, PNG or JPEG</Typography> <Typography variant="caption">{t('create.avatarRequirements')}</Typography>
<Button <Button
color="secondary" color="secondary"
onClick={() => { onClick={() => {
@@ -184,7 +217,7 @@ export function TeacherCreateForm(): React.JSX.Element {
}} }}
variant="outlined" variant="outlined"
> >
Select {t('create.avatar_select')}
</Button> </Button>
<input <input
hidden hidden
@@ -226,7 +259,7 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.email)} error={Boolean(errors.email)}
fullWidth fullWidth
> >
<InputLabel required>Email address</InputLabel> <InputLabel required>{t('create.email-address')}</InputLabel>
<OutlinedInput <OutlinedInput
{...field} {...field}
type="email" type="email"
@@ -248,7 +281,7 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.phone)} error={Boolean(errors.phone)}
fullWidth fullWidth
> >
<InputLabel required>Phone number</InputLabel> <InputLabel required>{t('create.phone-number')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null} {errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl> </FormControl>
@@ -268,7 +301,10 @@ export function TeacherCreateForm(): React.JSX.Element {
fullWidth fullWidth
> >
<InputLabel>Company</InputLabel> <InputLabel>Company</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput
{...field}
placeholder="no company name"
/>
{errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null} {errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null}
</FormControl> </FormControl>
)} )}
@@ -276,8 +312,9 @@ export function TeacherCreateForm(): React.JSX.Element {
</Grid> </Grid>
</Grid> </Grid>
</Stack> </Stack>
{/* */}
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">Billing information</Typography> <Typography variant="h6">{t('create.billing-information')}</Typography>
<Grid <Grid
container container
spacing={3} spacing={3}
@@ -296,10 +333,12 @@ export function TeacherCreateForm(): React.JSX.Element {
> >
<InputLabel required>Country</InputLabel> <InputLabel required>Country</InputLabel>
<Select {...field}> <Select {...field}>
<Option value="">Choose a country</Option> <MenuItem value="">Choose a country</MenuItem>
<Option value="us">United States</Option> <MenuItem value="US">United States</MenuItem>
<Option value="de">Germany</Option> <MenuItem value="UK">United Kingdom</MenuItem>
<Option value="es">Spain</Option> <MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select> </Select>
{errors.billingAddress?.country ? ( {errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress?.country?.message}</FormHelperText> <FormHelperText>{errors.billingAddress?.country?.message}</FormHelperText>
@@ -362,7 +401,7 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)} error={Boolean(errors.billingAddress?.zipCode)}
fullWidth fullWidth
> >
<InputLabel required>Zip code</InputLabel> <InputLabel required>{t('create.zip-code')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? ( {errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress?.zipCode?.message}</FormHelperText> <FormHelperText>{errors.billingAddress?.zipCode?.message}</FormHelperText>
@@ -383,7 +422,7 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)} error={Boolean(errors.billingAddress?.line1)}
fullWidth fullWidth
> >
<InputLabel required>Address</InputLabel> <InputLabel required>{t('create.address-line-1')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.billingAddress?.line1 ? ( {errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress?.line1?.message}</FormHelperText> <FormHelperText>{errors.billingAddress?.line1?.message}</FormHelperText>
@@ -424,7 +463,7 @@ export function TeacherCreateForm(): React.JSX.Element {
/> />
</Stack> </Stack>
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">Additional information</Typography> <Typography variant="h6">{t('create.additional-information')}</Typography>
<Grid <Grid
container container
spacing={3} spacing={3}
@@ -443,10 +482,14 @@ export function TeacherCreateForm(): React.JSX.Element {
> >
<InputLabel required>Timezone</InputLabel> <InputLabel required>Timezone</InputLabel>
<Select {...field}> <Select {...field}>
<Option value="">Select a timezone</Option> <MenuItem value="">Select a timezone</MenuItem>
<Option value="new_york">US - New York</Option> <MenuItem value="Europe/London">London</MenuItem>
<Option value="california">US - California</Option> <MenuItem value="Asia/Tokyo">Tokyo</MenuItem>
<Option value="london">UK - London</Option> <MenuItem value="America/Boa_Vista">Boa Vista</MenuItem>
<MenuItem value="America/Grand_Turk">Grand Turk</MenuItem>
<MenuItem value="Asia/Manila">Manila</MenuItem>
<MenuItem value="Asia/Urumqi">Urumqi</MenuItem>
<MenuItem value="Africa/Tunis">Tunis</MenuItem>
</Select> </Select>
{errors.timezone ? <FormHelperText>{errors.timezone.message}</FormHelperText> : null} {errors.timezone ? <FormHelperText>{errors.timezone.message}</FormHelperText> : null}
</FormControl> </FormControl>
@@ -467,10 +510,11 @@ export function TeacherCreateForm(): React.JSX.Element {
> >
<InputLabel required>Language</InputLabel> <InputLabel required>Language</InputLabel>
<Select {...field}> <Select {...field}>
<Option value="">Select a language</Option> <MenuItem value="">Select a language</MenuItem>
<Option value="en">English</Option> <MenuItem value="en">English</MenuItem>
<Option value="es">Spanish</Option> <MenuItem value="es">Spanish</MenuItem>
<Option value="de">German</Option> <MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem>
</Select> </Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null} {errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
</FormControl> </FormControl>
@@ -489,12 +533,12 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.currency)} error={Boolean(errors.currency)}
fullWidth fullWidth
> >
<InputLabel>Currency</InputLabel> <InputLabel required>{t('create.currency')}</InputLabel>
<Select {...field}> <Select {...field}>
<Option value="">Select a currency</Option> <MenuItem value="">no currency selected</MenuItem>
<Option value="USD">USD</Option> <MenuItem value="USD">USD</MenuItem>
<Option value="EUR">EUR</Option> <MenuItem value="EUR">EUR</MenuItem>
<Option value="RON">RON</Option> <MenuItem value="GBP">GBP</MenuItem>
</Select> </Select>
{errors.currency ? <FormHelperText>{errors.currency.message}</FormHelperText> : null} {errors.currency ? <FormHelperText>{errors.currency.message}</FormHelperText> : null}
</FormControl> </FormControl>
@@ -511,14 +555,17 @@ export function TeacherCreateForm(): React.JSX.Element {
component={RouterLink} component={RouterLink}
href={paths.dashboard.teachers.list} href={paths.dashboard.teachers.list}
> >
Cancel {t('create.cancelButton')}
</Button> </Button>
<Button
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit" type="submit"
variant="contained" variant="contained"
> >
Create teacher {t('create.updateButton')}
</Button> </LoadingButton>
</CardActions> </CardActions>
</Card> </Card>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}> <Box sx={{ display: isDevelopment ? 'block' : 'none' }}>

View File

@@ -1,12 +1,16 @@
'use client'; 'use client';
// src/components/dashboard/teacher/teacher-edit-form.tsx // src/components/dashboard/teacher/teacher-edit-form.tsx
// PURPOSE:
// handle change details for teachers collection
// //
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
// //
import { COL_USER_METAS } from '@/constants'; import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { getTeacherById } from '@/db/Teachers/GetById';
import { UpdateTeacherById } from '@/db/Teachers/UpdateById';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
// //
@@ -37,14 +41,15 @@ import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development'; import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb'; import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error'; // import ErrorDisplay from '../../error';
import ErrorDisplay from '../error'; import ErrorDisplay from '../error';
import type { Teacher } from './type.d';
// TODO: review this // TODO: review schema
const schema = zod.object({ const schema = zod.object({
name: zod.string().min(1, 'Name is required').max(255), 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), email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
@@ -89,7 +94,7 @@ const defaultValues = {
export function TeacherEditForm(): React.JSX.Element { export function TeacherEditForm(): React.JSX.Element {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(['lp_categories']); const { t } = useTranslation(['teachers']);
const { id: teacherId } = useParams<{ id: string }>(); const { id: teacherId } = useParams<{ id: string }>();
// //
@@ -97,6 +102,7 @@ export function TeacherEditForm(): React.JSX.Element {
const [showLoading, setShowLoading] = React.useState<boolean>(false); const [showLoading, setShowLoading] = React.useState<boolean>(false);
// //
const [showError, setShowError] = React.useState({ show: false, detail: '' }); const [showError, setShowError] = React.useState({ show: false, detail: '' });
const [billingAddressId, setBillingAddressId] = React.useState<string | null>(null);
const { const {
control, control,
@@ -116,7 +122,9 @@ export function TeacherEditForm(): React.JSX.Element {
email: values.email, email: values.email,
phone: values.phone, phone: values.phone,
company: values.company, company: values.company,
billingAddress: values.billingAddress, //
// billingAddress: values.billingAddress,
//
taxId: values.taxId, taxId: values.taxId,
timezone: values.timezone, timezone: values.timezone,
language: values.language, language: values.language,
@@ -125,12 +133,17 @@ export function TeacherEditForm(): React.JSX.Element {
}; };
try { try {
await pb.collection(COL_USER_METAS).update(teacherId, updateData); await UpdateTeacherById(teacherId, updateData);
toast.success('Teacher updated successfully'); //
toast.success(t('teacher-updated-successfully'));
router.push(paths.dashboard.teachers.list); router.push(paths.dashboard.teachers.list);
if (billingAddressId) {
await UpdateBillingAddressById(billingAddressId, values.billingAddress);
}
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
toast.error('Failed to update teacher'); toast.error(t('failed-to-update-teacher'));
} finally { } finally {
setIsUpdating(false); setIsUpdating(false);
} }
@@ -164,21 +177,21 @@ export function TeacherEditForm(): React.JSX.Element {
setShowLoading(true); setShowLoading(true);
try { try {
const result = await pb.collection(COL_USER_METAS).getOne(id); const result = (await getTeacherById(id)) as unknown as Teacher;
//
reset({ ...defaultValues, ...result }); reset({ ...defaultValues, ...result });
console.log({ result });
setBillingAddressId(result.billingAddress.id);
if (result.avatar) { if (result.avatar) {
const fetchResult = await fetch( const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar));
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar}`
);
const blob = await fetchResult.blob(); const blob = await fetchResult.blob();
const url = await fileToBase64(blob); const url = await fileToBase64(blob);
setValue('avatar', url); setValue('avatar', url);
} }
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
toast.error('Failed to load teacher data'); toast.error(t('failed-to-load-teacher-data'));
setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally { } finally {
setShowLoading(false); setShowLoading(false);
@@ -301,7 +314,7 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.email)} error={Boolean(errors.email)}
fullWidth fullWidth
> >
<InputLabel required>Email</InputLabel> <InputLabel required>{t('edit.email-address')}</InputLabel>
<OutlinedInput <OutlinedInput
{...field} {...field}
type="email" type="email"
@@ -323,7 +336,7 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.phone)} error={Boolean(errors.phone)}
fullWidth fullWidth
> >
<InputLabel required>Phone</InputLabel> <InputLabel required>{t('edit.phone-number')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null} {errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl> </FormControl>
@@ -352,11 +365,12 @@ export function TeacherEditForm(): React.JSX.Element {
)} )}
/> />
</Grid> </Grid>
{/* */}
</Grid> </Grid>
</Stack> </Stack>
{/* */} {/* */}
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">Billing Information</Typography> <Typography variant="h6">{t('edit.billing-information')}</Typography>
<Grid <Grid
container container
spacing={3} spacing={3}
@@ -375,9 +389,12 @@ export function TeacherEditForm(): React.JSX.Element {
> >
<InputLabel required>Country</InputLabel> <InputLabel required>Country</InputLabel>
<Select {...field}> <Select {...field}>
<MenuItem value="">No Country selected</MenuItem>
<MenuItem value="US">United States</MenuItem> <MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem> <MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem> <MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select> </Select>
{errors.billingAddress?.country ? ( {errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress.country.message}</FormHelperText> <FormHelperText>{errors.billingAddress.country.message}</FormHelperText>
@@ -440,7 +457,7 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)} error={Boolean(errors.billingAddress?.zipCode)}
fullWidth fullWidth
> >
<InputLabel required>Zip Code</InputLabel> <InputLabel required>{t('edit.zip-code')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? ( {errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress.zipCode.message}</FormHelperText> <FormHelperText>{errors.billingAddress.zipCode.message}</FormHelperText>
@@ -461,7 +478,7 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)} error={Boolean(errors.billingAddress?.line1)}
fullWidth fullWidth
> >
<InputLabel required>Address Line 1</InputLabel> <InputLabel required>{t('edit.address-line-1')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.billingAddress?.line1 ? ( {errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress.line1.message}</FormHelperText> <FormHelperText>{errors.billingAddress.line1.message}</FormHelperText>
@@ -496,7 +513,7 @@ export function TeacherEditForm(): React.JSX.Element {
</Stack> </Stack>
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">Additional Information</Typography> <Typography variant="h6">{t('edit.additional-information')}</Typography>
<Grid <Grid
container container
spacing={3} spacing={3}
@@ -543,8 +560,10 @@ export function TeacherEditForm(): React.JSX.Element {
> >
<InputLabel required>Language</InputLabel> <InputLabel required>Language</InputLabel>
<Select {...field}> <Select {...field}>
<MenuItem value="">no language selected</MenuItem>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem> <MenuItem value="es">Spanish</MenuItem>
<MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem> <MenuItem value="fr">French</MenuItem>
</Select> </Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null} {errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
@@ -564,8 +583,9 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.currency)} error={Boolean(errors.currency)}
fullWidth fullWidth
> >
<InputLabel required>Currency</InputLabel> <InputLabel required>{t('edit.currency')}</InputLabel>
<Select {...field}> <Select {...field}>
<MenuItem value="">no currency selected</MenuItem>
<MenuItem value="USD">USD</MenuItem> <MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem> <MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem> <MenuItem value="GBP">GBP</MenuItem>

View File

@@ -1,5 +1,9 @@
'use client'; 'use client';
// src/components/dashboard/teacher/teachers-table.tsx
// PURPOSE:
// handle change details for teachers collection
//
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
@@ -213,7 +217,6 @@ export function TeachersTable({ rows, reloadRows }: TeachersTableProps): React.J
sx={{ textAlign: 'center' }} sx={{ textAlign: 'center' }}
variant="body2" variant="body2"
> >
{/* TODO: update this */}
{t('no-teachers-found')} {t('no-teachers-found')}
</Typography> </Typography>
</Box> </Box>

View File

@@ -31,21 +31,23 @@ export interface CreateFormProps {
email: string; email: string;
phone?: string; phone?: string;
company?: string; company?: string;
billingAddress?: { // handle seperately
country: string; // billingAddress?: {
state: string; // country: string;
city: string; // state: string;
zipCode: string; // city: string;
line1: string; // zipCode: string;
line2?: string; // line1: string;
}; // line2?: string;
// };
taxId?: string; taxId?: string;
timezone: string; timezone: string;
language: string; language: string;
currency: string; currency: string;
avatar?: string; avatar?: File | null;
// quota?: number; // quota?: number;
// status?: 'pending' | 'active' | 'blocked'; state?: 'pending' | 'active' | 'blocked';
meta: Record<string, null>;
} }
// RULES: form data structure for editing existing teacher // RULES: form data structure for editing existing teacher
@@ -77,6 +79,8 @@ export interface TeachersFiltersProps {
sortDir?: SortDir; sortDir?: SortDir;
fullData: Teacher[]; fullData: Teacher[];
} }
// RULES: available filter options for student data
export interface Filters { export interface Filters {
email?: string; email?: string;
phone?: string; phone?: string;

View File

@@ -3,6 +3,7 @@
// empty valur for customer // empty valur for customer
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import type { UserMeta } from './type.d'; import type { UserMeta } from './type.d';
export const defaultUserMeta: UserMeta = { export const defaultUserMeta: UserMeta = {

View File

@@ -1,12 +1,13 @@
'use client'; 'use client';
// src/components/dashboard/user_meta/user-meta-edit-form.tsx // src/components/dashboard/user_meta/user-meta-edit-form.tsx
// PURPOSE:
// handle change details for user meta collection
// //
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
// //
import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById'; import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { getUserMetaById } from '@/db/UserMetas/GetById'; import { getUserMetaById } from '@/db/UserMetas/GetById';
import { UpdateUserMetaById } from '@/db/UserMetas/UpdateById'; import { UpdateUserMetaById } from '@/db/UserMetas/UpdateById';
@@ -41,15 +42,14 @@ import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error'; // import ErrorDisplay from '../../error';
import ErrorDisplay from '../error'; import ErrorDisplay from '../error';
import { UserMeta } from './type.d'; import type { UserMeta } from './type.d';
// TODO: review this // TODO: review schema
const schema = zod.object({ const schema = zod.object({
name: zod.string().min(1, 'Name is required').max(255), 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), email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
@@ -102,6 +102,7 @@ export function UserMetaEditForm(): React.JSX.Element {
const [showLoading, setShowLoading] = React.useState<boolean>(false); const [showLoading, setShowLoading] = React.useState<boolean>(false);
// //
const [showError, setShowError] = React.useState({ show: false, detail: '' }); const [showError, setShowError] = React.useState({ show: false, detail: '' });
const [billingAddressId, setBillingAddressId] = React.useState<string | null>(null);
const { const {
control, control,
@@ -121,7 +122,9 @@ export function UserMetaEditForm(): React.JSX.Element {
email: values.email, email: values.email,
phone: values.phone, phone: values.phone,
company: values.company, company: values.company,
billingAddress: values.billingAddress, //
// billingAddress: values.billingAddress,
//
taxId: values.taxId, taxId: values.taxId,
timezone: values.timezone, timezone: values.timezone,
language: values.language, language: values.language,
@@ -130,10 +133,14 @@ export function UserMetaEditForm(): React.JSX.Element {
}; };
try { try {
await pb.collection(COL_USER_METAS).update(userMetaId, updateData); await UpdateUserMetaById(userMetaId, updateData);
//
toast.success(t('user-updated-successfully')); toast.success(t('user-updated-successfully'));
router.push(paths.dashboard.user_metas.list); router.push(paths.dashboard.user_metas.list);
if (billingAddressId) {
await UpdateBillingAddressById(billingAddressId, values.billingAddress);
}
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
toast.error(t('failed-to-update-user-meta')); toast.error(t('failed-to-update-user-meta'));
@@ -164,20 +171,18 @@ export function UserMetaEditForm(): React.JSX.Element {
const [textDescription, setTextDescription] = React.useState<string>(''); const [textDescription, setTextDescription] = React.useState<string>('');
const [textRemarks, setTextRemarks] = React.useState<string>(''); const [textRemarks, setTextRemarks] = React.useState<string>('');
const [userId, setUserId] = React.useState<string>('');
// load existing data when user arrive // load existing data when user arrive
const loadExistingData = React.useCallback( const loadExistingData = React.useCallback(
async (id: string) => { async (id: string) => {
setShowLoading(true); setShowLoading(true);
try { try {
// const result = await pb.collection(COL_USER_METAS).getOne(id);
const result = (await getUserMetaById(id)) as unknown as UserMeta; const result = (await getUserMetaById(id)) as unknown as UserMeta;
setUserId(result.user_id); //
reset({ ...defaultValues, ...result }); reset({ ...defaultValues, ...result });
setBillingAddressId(result.billingAddress.id);
if (result.avatar) { if (result.avatar) {
const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar)); const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar));
const blob = await fetchResult.blob(); const blob = await fetchResult.blob();
@@ -186,7 +191,7 @@ export function UserMetaEditForm(): React.JSX.Element {
} }
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
toast.error('failed-to-load-user-meta-data'); toast.error(t('failed-to-load-user-meta-data'));
setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally { } finally {
setShowLoading(false); setShowLoading(false);
@@ -309,7 +314,7 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.email)} error={Boolean(errors.email)}
fullWidth fullWidth
> >
<InputLabel required>Email</InputLabel> <InputLabel required>{t('edit.email-address')}</InputLabel>
<OutlinedInput <OutlinedInput
{...field} {...field}
type="email" type="email"
@@ -331,7 +336,7 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.phone)} error={Boolean(errors.phone)}
fullWidth fullWidth
> >
<InputLabel required>Phone</InputLabel> <InputLabel required>{t('edit.phone-number')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null} {errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl> </FormControl>
@@ -365,7 +370,7 @@ export function UserMetaEditForm(): React.JSX.Element {
</Stack> </Stack>
{/* */} {/* */}
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">Billing Information</Typography> <Typography variant="h6">{t('edit.billing-information')}</Typography>
<Grid <Grid
container container
spacing={3} spacing={3}
@@ -384,9 +389,12 @@ export function UserMetaEditForm(): React.JSX.Element {
> >
<InputLabel required>Country</InputLabel> <InputLabel required>Country</InputLabel>
<Select {...field}> <Select {...field}>
<MenuItem value="">No Country selected</MenuItem>
<MenuItem value="US">United States</MenuItem> <MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem> <MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem> <MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select> </Select>
{errors.billingAddress?.country ? ( {errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress.country.message}</FormHelperText> <FormHelperText>{errors.billingAddress.country.message}</FormHelperText>
@@ -449,7 +457,7 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)} error={Boolean(errors.billingAddress?.zipCode)}
fullWidth fullWidth
> >
<InputLabel required>Zip Code</InputLabel> <InputLabel required>{t('edit.zip-code')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? ( {errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress.zipCode.message}</FormHelperText> <FormHelperText>{errors.billingAddress.zipCode.message}</FormHelperText>
@@ -470,7 +478,7 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)} error={Boolean(errors.billingAddress?.line1)}
fullWidth fullWidth
> >
<InputLabel required>Address Line 1</InputLabel> <InputLabel required>{t('edit.address-line-1')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.billingAddress?.line1 ? ( {errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress.line1.message}</FormHelperText> <FormHelperText>{errors.billingAddress.line1.message}</FormHelperText>
@@ -505,7 +513,7 @@ export function UserMetaEditForm(): React.JSX.Element {
</Stack> </Stack>
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">{t('additional-information')}</Typography> <Typography variant="h6">{t('edit.additional-information')}</Typography>
<Grid <Grid
container container
spacing={3} spacing={3}
@@ -552,8 +560,10 @@ export function UserMetaEditForm(): React.JSX.Element {
> >
<InputLabel required>Language</InputLabel> <InputLabel required>Language</InputLabel>
<Select {...field}> <Select {...field}>
<MenuItem value="">no language selected</MenuItem>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem> <MenuItem value="es">Spanish</MenuItem>
<MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem> <MenuItem value="fr">French</MenuItem>
</Select> </Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null} {errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
@@ -573,8 +583,9 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.currency)} error={Boolean(errors.currency)}
fullWidth fullWidth
> >
<InputLabel required>Currency</InputLabel> <InputLabel required>{t('edit.currency')}</InputLabel>
<Select {...field}> <Select {...field}>
<MenuItem value="">no currency selected</MenuItem>
<MenuItem value="USD">USD</MenuItem> <MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem> <MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem> <MenuItem value="GBP">GBP</MenuItem>

View File

@@ -1,11 +1,15 @@
'use client'; 'use client';
// src/components/dashboard/user_meta/user-metas-filters.tsx
// RULES: // RULES:
// T.B.A. // T.B.A.
// //
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import GetActiveCount from '@/db/UserMetas/GetActiveCount';
import getAllUserMetasCount from '@/db/UserMetas/GetAllCount'; import getAllUserMetasCount from '@/db/UserMetas/GetAllCount';
import GetBlockedCount from '@/db/UserMetas/GetBlockedCount';
import GetPendingCount from '@/db/UserMetas/GetPendingCount';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
@@ -18,17 +22,14 @@ import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { FilterButton } from '@/components/core/filter-button'; import { FilterButton } from '@/components/core/filter-button';
import { Option } from '@/components/core/option'; import { Option } from '@/components/core/option';
import { useUserMetasSelection } from './user-metas-selection-context';
import GetBlockedCount from '@/db/UserMetas/GetBlockedCount';
import GetPendingCount from '@/db/UserMetas/GetPendingCount';
import GetActiveCount from '@/db/UserMetas/GetActiveCount';
import PhoneFilterPopover from './phone-filter-popover';
import EmailFilterPopover from './email-filter-popover'; import EmailFilterPopover from './email-filter-popover';
import type { UserMetasFiltersProps, Filters, SortDir } from './type.d'; import PhoneFilterPopover from './phone-filter-popover';
import { logger } from '@/lib/default-logger'; import type { Filters, SortDir, UserMetasFiltersProps } from './type.d';
import { useUserMetasSelection } from './user-metas-selection-context';
export function UserMetasFilters({ export function UserMetasFilters({
filters = {}, filters = {},

View File

@@ -1,5 +1,9 @@
'use client'; 'use client';
// src/components/dashboard/user_meta/user-metas-table.tsx
// RULES:
// T.B.A.
//
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
@@ -18,14 +22,16 @@ import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images';
import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table'; import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal'; import ConfirmDeleteModal from './confirm-delete-modal';
import { useUserMetasSelection } from './user-metas-selection-context';
import type { UserMeta } from './type.d'; import type { UserMeta } from './type.d';
import { useUserMetasSelection } from './user-metas-selection-context';
function columns(handleDeleteClick: (userMetaId: string) => void): ColumnDef<UserMeta>[] { function columns(handleDeleteClick: (userMetaId: string) => void): ColumnDef<UserMeta>[] {
return [ return [
@@ -168,6 +174,7 @@ export interface UserMetasTableProps {
} }
export function UserMetasTable({ rows, reloadRows }: UserMetasTableProps): React.JSX.Element { export function UserMetasTable({ rows, reloadRows }: UserMetasTableProps): React.JSX.Element {
const { t } = useTranslation(['user_metas']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useUserMetasSelection(); const { deselectAll, deselectOne, selectAll, selectOne, selected } = useUserMetasSelection();
const [idToDelete, setIdToDelete] = React.useState(''); const [idToDelete, setIdToDelete] = React.useState('');
@@ -207,7 +214,7 @@ export function UserMetasTable({ rows, reloadRows }: UserMetasTableProps): React
sx={{ textAlign: 'center' }} sx={{ textAlign: 'center' }}
variant="body2" variant="body2"
> >
No user metadata found {t('no-user-meta-found')}
</Typography> </Typography>
</Box> </Box>
) : null} ) : null}

View File

@@ -1,3 +1,7 @@
// src/db/Students/Helloworld.tsx
// RULES:
// T.B.A.
//
export function helloCustomer() { export function helloCustomer() {
return 'Hello from Customers module!'; return 'Hello from Customers module!';
} }

View File

@@ -1,3 +1,7 @@
// src/lib/helloworld.ts
// RULES:
// T.B.A.
//
export function helloworld(): string { export function helloworld(): string {
return 'Helloworld'; return 'Helloworld';
} }