Compare commits

...

12 Commits

Author SHA1 Message Date
louiscklaw
5bebc1f40e ```
add COL_BILLING_ADDRESS constant and update exports
```
2025-05-13 13:28:21 +08:00
louiscklaw
f4e5f94e17 ``update .gitignore to modify exclusion pattern for _del files from '**/_del' to '**/*del'`` 2025-05-13 13:28:13 +08:00
louiscklaw
2d022cb613 ```
remove Docker Compose configuration files for CMS, Doc, Ionic Mobile, API_TS, and PocketBase services
```
2025-05-13 13:27:56 +08:00
louiscklaw
3560ea79fc "``update teacher and student seed scripts to dynamically loop through row_array and um_row_array instead of fixed iterations``" 2025-05-13 13:27:48 +08:00
louiscklaw
a441e3e52d ```
refactor teacher and user meta management UI by updating form components, replacing COL_TEACHERS with COL_USER_METAS where applicable, adding development environment checks, improving type definitions for user meta including billing address, and fixing parameter naming inconsistencies in form handlers
```
2025-05-13 13:27:41 +08:00
louiscklaw
09ded06dd2 ``update student database operations to use COL_USER_METAS instead of COL_STUDENTS, refactor getStudentById to include expanded billing address data, and add/update related types and functions for student and user meta management`` 2025-05-13 13:27:27 +08:00
louiscklaw
7ecacd0692 ```
refactor student management UI by updating create form component name and adding new edit page with translation support and form integration
```
2025-05-13 13:27:17 +08:00
louiscklaw
8a094afdd2 "``refactor student create and edit forms with translation support, update schema validation, and use new database operations for student management``" 2025-05-13 13:26:58 +08:00
louiscklaw
64ca29cf60 ``add new database operations for billing address module including create, delete, get, update functions and related type definitions`` 2025-05-13 13:26:41 +08:00
louiscklaw
1aa0502edc ``fix inconsistent quotes in code and update schema with additional fields and rules`` 2025-05-13 13:26:22 +08:00
louiscklaw
3e1f2e1057 ```
add billing address seed data and update user seed scripts with teacher and student roles
```
2025-05-13 12:35:05 +08:00
louiscklaw
9be33f641f ```
add 'visible', 'state', 'company', 'taxId', 'timezone', 'language', 'currency' fields and update 'billingAddress' and 't1' collection rules in PocketBase seed schema
```
2025-05-13 12:34:51 +08:00
43 changed files with 962 additions and 447 deletions

2
.gitignore vendored
View File

@@ -7,7 +7,7 @@ _del
*.bak
*.log
*.del
**/_del
**/*del
**/volumes/**
006_lab

View File

@@ -1,11 +1,11 @@
import { faker } from "@faker-js/faker";
import { faker } from '@faker-js/faker';
const getId = (id) => id.padStart(15, "0");
const getId = (id) => id.padStart(15, '0');
const row_array = Array.from({ length: 10 }, (_, i) => [
getId(String(i + 1)),
faker.person.firstName(),
"",
'',
faker.internet.email(),
faker.phone.number(),
faker.company.name(),
@@ -19,13 +19,11 @@ const row_array = Array.from({ length: 10 }, (_, i) => [
},
Math.floor(Math.random() * (100 - 0 + 1)) + 0,
faker.location.timeZone(),
["en", "de", "es", "fr", "ja", "ko", "zh-CN"].sort(
() => Math.random() - 0.5
)[0],
['en', 'de', 'es', 'fr', 'ja', 'ko', 'zh-CN'].sort(() => Math.random() - 0.5)[0],
faker.finance.currencyCode(),
]);
import fs from "fs";
const filePath = "output.json";
import fs from 'fs';
const filePath = 'output.json';
fs.writeFileSync(filePath, JSON.stringify(row_array, null, 2));
console.log(`Wrote ${row_array.length} records to ${filePath}`);

View File

@@ -1,4 +1,4 @@
// Generated at: 2025-05-12T06:02:53.613Z
// Generated at: 2025-05-13T05:24:33.962Z
//
@@ -206,9 +206,9 @@ Table QuizLPCategories {
cat_image file
pos integer
init_answer text
visible text
created datetime
updated datetime
visible text
slug text
remarks text
description text
@@ -223,8 +223,6 @@ Table QuizLPQuestions {
word text
sound file
cat_id integer [ref: > QuizLPCategories.id] // relation3870140739
created datetime
updated datetime
cat_name text
cat_image file
pos integer
@@ -233,6 +231,8 @@ Table QuizLPQuestions {
slug text
remarks text
description text
created datetime
updated datetime
}
//
@@ -258,9 +258,9 @@ Table QuizMFCategories {
cat_image file
pos integer
init_answer text
visible text
created datetime
updated datetime
visible text
}
//
@@ -341,17 +341,23 @@ Table Teachers {
// collection type: base
Table UserMetas {
id text [pk, not null]
helloworld text
address text
meta text
user_id integer [ref: > users.id] // relation2809058197
state text
created datetime
updated datetime
status text
avatar file
role text
name text
email text
phone text
company text
taxId text
timezone text
language text
currency text
billingAddress integer [ref: > billingAddress.id] // relation2115670734
}
//

View File

@@ -1750,6 +1750,20 @@
"system": false,
"type": "json"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
@@ -1770,20 +1784,6 @@
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
@@ -1892,26 +1892,6 @@
"system": false,
"type": "relation"
},
{
"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"
},
{
"autogeneratePattern": "",
"hidden": false,
@@ -2014,6 +1994,26 @@
"required": false,
"system": false,
"type": "editor"
},
{
"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": [],
@@ -2182,6 +2182,20 @@
"system": false,
"type": "json"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
@@ -2201,20 +2215,6 @@
"presentable": false,
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"indexes": [],
@@ -2870,7 +2870,7 @@
"id": "text4192936109",
"max": 0,
"min": 0,
"name": "helloworld",
"name": "address",
"pattern": "",
"presentable": false,
"primaryKey": false,
@@ -2901,6 +2901,20 @@
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2744374011",
"max": 0,
"min": 0,
"name": "state",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
@@ -2921,20 +2935,6 @@
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2744374011",
"max": 0,
"min": 0,
"name": "status",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "file376926767",
@@ -3001,6 +3001,89 @@
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1337919823",
"max": 0,
"min": 0,
"name": "company",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2020362641",
"max": 0,
"min": 0,
"name": "taxId",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text922858135",
"max": 0,
"min": 0,
"name": "timezone",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3571151285",
"max": 0,
"min": 0,
"name": "language",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1767278655",
"max": 0,
"min": 0,
"name": "currency",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1509025625",
"hidden": false,
"id": "relation2115670734",
"maxSelect": 999,
"minSelect": 0,
"name": "billingAddress",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}
],
"indexes": [],
@@ -3604,11 +3687,11 @@
},
{
"id": "pbc_1509025625",
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",
"name": "billingAddress",
"type": "base",
"fields": [
@@ -3798,11 +3881,11 @@
},
{
"id": "pbc_2109205374",
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",
"name": "t1",
"type": "base",
"fields": [

View File

@@ -9,7 +9,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { config } from '@/config';
import { paths } from '@/paths';
import { CustomerCreateForm } 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;
@@ -41,7 +41,7 @@ export default function Page(): React.JSX.Element {
<Typography variant="h4">Create customer</Typography>
</div>
</Stack>
<CustomerCreateForm />
<StudentCreateForm />
</Stack>
</Box>
);

View File

@@ -1 +0,0 @@
this `tsx` file is clone from elsewhere, please understand, modify and update the content of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/students/edit/[customerId]/page.tsx.draft` to handle `Student` record thanks, modify comments/variables/paths/functions name please

View File

@@ -0,0 +1,6 @@
this `tsx` file is clone from elsewhere,
please understand, modify and update the content of
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/students/edit/[customerId]/page.tsx.draft`
to handle `Student` record thanks,
modify comments/variables/paths/functions name please

View File

@@ -1,6 +1,6 @@
'use client';
// src/app/dashboard/students/edit/[customerId]/page.tsx
// src/app/dashboard/students/edit/[customerId]/page.tsx
import * as React from 'react';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
@@ -11,7 +11,8 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
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 { StudentEditForm } from '@/components/dashboard/student/student-edit-form';
export default function Page(): React.JSX.Element {

View File

@@ -1,9 +1,17 @@
'use client';
// src/components/dashboard/student/student-create-form.tsx
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
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 { LoadingButton } from '@mui/lab';
//
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
@@ -16,41 +24,38 @@ import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
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 isDevelopment from '@/lib/check-is-development';
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 { toast } from '@/components/core/toaster';
import { createCustomer } from '@/db/Customers/Create';
import isDevelopment from '@/lib/check-is-development';
import FormLoading from '@/components/loading';
function fileToBase64(file: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(new Error('Error converting file to base64'));
};
});
}
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import { CreateFormProps, Student } from './type.d';
// TODO: review schema
const schema = zod.object({
avatar: zod.string().optional(),
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),
phone: zod.string().min(1, 'Phone is required').max(25),
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),
@@ -63,12 +68,12 @@ const schema = zod.object({
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<typeof schema>;
const defaultValues = {
avatar: '',
name: 'new name',
email: '123@123.com',
phone: '91234567',
@@ -85,10 +90,18 @@ const defaultValues = {
timezone: 'new_york',
language: 'en',
currency: 'USD',
avatar: '',
} satisfies Values;
export function CustomerCreateForm(): React.JSX.Element {
export function StudentCreateForm(): React.JSX.Element {
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 {
control,
@@ -100,14 +113,35 @@ export function CustomerCreateForm(): React.JSX.Element {
const onSubmit = React.useCallback(
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 {
// Use standard create method from db/Customers/Create
const record = await createCustomer(values);
toast.success('Customer created');
router.push(paths.dashboard.students.view(record.id));
// if (billingAddressId) {
// await UpdateBillingAddressById(billingAddressId, values.billingAddress);
// }
const record = await createStudent(tempCreate);
toast.success('Student created');
// router.push(paths.dashboard.students.view(record.id));
} catch (err) {
logger.error(err);
toast.error('Failed to create customer');
toast.error('Failed to create Student');
} finally {
setIsUpdating(false);
}
},
[router]
@@ -137,7 +171,7 @@ export function CustomerCreateForm(): React.JSX.Element {
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">Account information</Typography>
<Typography variant="h6">{t('create.basic-info')}</Typography>
<Grid
container
spacing={3}
@@ -151,12 +185,13 @@ export function CustomerCreateForm(): React.JSX.Element {
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '50%',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
variant="rounded"
src={avatar}
sx={{
'--Avatar-size': '100px',
@@ -175,8 +210,8 @@ export function CustomerCreateForm(): React.JSX.Element {
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">Avatar</Typography>
<Typography variant="caption">Min 400x400px, PNG or JPEG</Typography>
<Typography variant="subtitle1">{t('create.avatar')}</Typography>
<Typography variant="caption">{t('create.avatarRequirements')}</Typography>
<Button
color="secondary"
onClick={() => {
@@ -184,7 +219,7 @@ export function CustomerCreateForm(): React.JSX.Element {
}}
variant="outlined"
>
Select
{t('create.avatar_select')}
</Button>
<input
hidden
@@ -226,7 +261,7 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email address</InputLabel>
<InputLabel required>{t('create.email-address')}</InputLabel>
<OutlinedInput
{...field}
type="email"
@@ -248,7 +283,7 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone number</InputLabel>
<InputLabel required>{t('create.phone-number')}</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
@@ -268,7 +303,10 @@ export function CustomerCreateForm(): React.JSX.Element {
fullWidth
>
<InputLabel>Company</InputLabel>
<OutlinedInput {...field} />
<OutlinedInput
{...field}
placeholder="no company name"
/>
{errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null}
</FormControl>
)}
@@ -276,8 +314,9 @@ export function CustomerCreateForm(): React.JSX.Element {
</Grid>
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">Billing information</Typography>
<Typography variant="h6">{t('create.billing-information')}</Typography>
<Grid
container
spacing={3}
@@ -296,10 +335,12 @@ export function CustomerCreateForm(): React.JSX.Element {
>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<Option value="">Choose a country</Option>
<Option value="us">United States</Option>
<Option value="de">Germany</Option>
<Option value="es">Spain</Option>
<MenuItem value="">Choose a country</MenuItem>
<MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress?.country?.message}</FormHelperText>
@@ -362,7 +403,7 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)}
fullWidth
>
<InputLabel required>Zip code</InputLabel>
<InputLabel required>{t('create.zip-code')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress?.zipCode?.message}</FormHelperText>
@@ -383,7 +424,7 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)}
fullWidth
>
<InputLabel required>Address</InputLabel>
<InputLabel required>{t('create.address-line-1')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress?.line1?.message}</FormHelperText>
@@ -424,7 +465,7 @@ export function CustomerCreateForm(): React.JSX.Element {
/>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional information</Typography>
<Typography variant="h6">{t('create.additional-information')}</Typography>
<Grid
container
spacing={3}
@@ -443,10 +484,14 @@ export function CustomerCreateForm(): React.JSX.Element {
>
<InputLabel required>Timezone</InputLabel>
<Select {...field}>
<Option value="">Select a timezone</Option>
<Option value="new_york">US - New York</Option>
<Option value="california">US - California</Option>
<Option value="london">UK - London</Option>
<MenuItem value="">Select a timezone</MenuItem>
<MenuItem value="Europe/London">London</MenuItem>
<MenuItem value="Asia/Tokyo">Tokyo</MenuItem>
<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>
{errors.timezone ? <FormHelperText>{errors.timezone.message}</FormHelperText> : null}
</FormControl>
@@ -467,10 +512,11 @@ export function CustomerCreateForm(): React.JSX.Element {
>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<Option value="">Select a language</Option>
<Option value="en">English</Option>
<Option value="es">Spanish</Option>
<Option value="de">German</Option>
<MenuItem value="">Select a language</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem>
<MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
</FormControl>
@@ -489,12 +535,12 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.currency)}
fullWidth
>
<InputLabel>Currency</InputLabel>
<InputLabel required>{t('create.currency')}</InputLabel>
<Select {...field}>
<Option value="">Select a currency</Option>
<Option value="USD">USD</Option>
<Option value="EUR">EUR</Option>
<Option value="RON">RON</Option>
<MenuItem value="">no currency selected</MenuItem>
<MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem>
</Select>
{errors.currency ? <FormHelperText>{errors.currency.message}</FormHelperText> : null}
</FormControl>
@@ -511,14 +557,17 @@ export function CustomerCreateForm(): React.JSX.Element {
component={RouterLink}
href={paths.dashboard.students.list}
>
Cancel
{t('create.cancelButton')}
</Button>
<Button
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit"
variant="contained"
>
Create customer
</Button>
{t('create.updateButton')}
</LoadingButton>
</CardActions>
</Card>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>

View File

@@ -1,10 +1,15 @@
'use client';
// src/components/dashboard/student/student-edit-form.tsx
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_CUSTOMERS } from '@/constants';
import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { getStudentById } from '@/db/Students/GetById';
import { UpdateStudentById } from '@/db/Students/UpdateById';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
@@ -87,14 +92,15 @@ const defaultValues = {
export function StudentEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { t } = useTranslation(['students']);
const { customerId } = useParams<{ customerId: string }>();
const { id: studentId } = useParams<{ id: string }>();
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const [billingAddressId, setBillingAddressId] = React.useState<string | null>(null);
const {
control,
@@ -110,30 +116,38 @@ export function StudentEditForm(): React.JSX.Element {
setIsUpdating(true);
const updateData = {
avatar: values.avatar ? await base64ToFile(values.avatar) : null,
//
name: values.name,
email: values.email,
phone: values.phone,
company: values.company,
billingAddress: values.billingAddress,
taxId: values.taxId,
//
// billingAddress: values.billingAddress,
//
timezone: values.timezone,
language: values.language,
currency: values.currency,
avatar: values.avatar ? await base64ToFile(values.avatar) : null,
taxId: values.taxId,
};
try {
await pb.collection(COL_CUSTOMERS).update(customerId, updateData);
toast.success('Customer updated successfully');
// await pb.collection(COL_USER_METAS).update(studentId, updateData);
await UpdateStudentById(studentId, updateData);
toast.success('Student updated successfully');
router.push(paths.dashboard.students.list);
if (billingAddressId) {
await UpdateBillingAddressById(billingAddressId, values.billingAddress);
}
} catch (error) {
logger.error(error);
toast.error('Failed to update customer');
toast.error('Failed to update student');
} finally {
setIsUpdating(false);
}
},
[customerId, router]
[studentId, router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
@@ -162,13 +176,14 @@ export function StudentEditForm(): React.JSX.Element {
setShowLoading(true);
try {
const result = await pb.collection(COL_CUSTOMERS).getOne(id);
const result = await getStudentById(id);
reset({ ...defaultValues, ...result });
console.log({ result });
if (result.avatar_file) {
setBillingAddressId(result.billingAddress.id);
if (result.avatar) {
const fetchResult = await fetch(
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar_file}`
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar}`
);
const blob = await fetchResult.blob();
const url = await fileToBase64(blob);
@@ -176,7 +191,7 @@ export function StudentEditForm(): React.JSX.Element {
}
} catch (error) {
logger.error(error);
toast.error('Failed to load customer data');
toast.error('Failed to load student data');
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally {
setShowLoading(false);
@@ -186,9 +201,9 @@ export function StudentEditForm(): React.JSX.Element {
);
React.useEffect(() => {
void loadExistingData(customerId);
void loadExistingData(studentId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [customerId]);
}, [studentId]);
if (showLoading) return <FormLoading />;
if (showError.show)
@@ -299,7 +314,7 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email</InputLabel>
<InputLabel required>{t('edit.email-address')}</InputLabel>
<OutlinedInput
{...field}
type="email"
@@ -321,7 +336,7 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone</InputLabel>
<InputLabel required>{t('edit.phone-number')}</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
@@ -354,7 +369,7 @@ export function StudentEditForm(): React.JSX.Element {
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">Billing Information</Typography>
<Typography variant="h6">{t('edit.billing-information')}</Typography>
<Grid
container
spacing={3}
@@ -373,9 +388,12 @@ export function StudentEditForm(): React.JSX.Element {
>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<MenuItem value="">No Country selected</MenuItem>
<MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress.country.message}</FormHelperText>
@@ -438,7 +456,7 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)}
fullWidth
>
<InputLabel required>Zip Code</InputLabel>
<InputLabel required>{t('edit.zip-code')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress.zipCode.message}</FormHelperText>
@@ -459,7 +477,7 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)}
fullWidth
>
<InputLabel required>Address Line 1</InputLabel>
<InputLabel required>{t('edit.address-line-1')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress.line1.message}</FormHelperText>
@@ -494,7 +512,7 @@ export function StudentEditForm(): React.JSX.Element {
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional Information</Typography>
<Typography variant="h6">{t('edit.additional-information')}</Typography>
<Grid
container
spacing={3}
@@ -541,8 +559,10 @@ export function StudentEditForm(): React.JSX.Element {
>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<MenuItem value="">no language selected</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem>
<MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
@@ -562,8 +582,9 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.currency)}
fullWidth
>
<InputLabel required>Currency</InputLabel>
<InputLabel required>{t('edit.currency')}</InputLabel>
<Select {...field}>
<MenuItem value="">no currency selected</MenuItem>
<MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem>

View File

@@ -23,21 +23,23 @@ export interface CreateFormProps {
email: string;
phone?: string;
company?: string;
billingAddress?: {
country: string;
state: string;
city: string;
zipCode: string;
line1: string;
line2?: string;
};
// handle seperately
// billingAddress?: {
// country: string;
// state: string;
// city: string;
// zipCode: string;
// line1: string;
// line2?: string;
// };
taxId?: string;
timezone: string;
language: string;
currency: string;
avatar?: string;
avatar?: File | null;
// quota?: number;
// status?: 'pending' | 'active' | 'blocked';
state?: 'pending' | 'active' | 'blocked';
meta: Record<string, null>;
}
// RULES: form data structure for editing existing student

View File

@@ -1,10 +1,12 @@
'use client';
// src/components/dashboard/teacher/teacher-edit-form.tsx
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_TEACHERS, COL_USER_METAS } from '@/constants';
import { COL_USER_METAS } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
@@ -32,6 +34,7 @@ import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
@@ -40,7 +43,6 @@ import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import isDevelopment from '@/lib/check-is-development';
// TODO: review this
const schema = zod.object({

View File

@@ -32,6 +32,7 @@ import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
@@ -40,7 +41,6 @@ import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import isDevelopment from '@/lib/check-is-development';
// TODO: review this
const schema = zod.object({

View File

@@ -1,8 +1,55 @@
'use client';
import type { BillingAddress } from '@/db/billingAddress/type';
// RULES: sorting direction for teacher lists
export type SortDir = 'asc' | 'desc';
// obsoleted
// export interface BillingAddress {
// city: string;
// country: string;
// line1: string;
// line2: string;
// state: string;
// zipCode: string;
// //
// id: string;
// collectionId: string;
// collectionName: string;
// updated: string;
// created: string;
// }
export interface DBUserMeta {
name: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
avatar?: string;
avatar_file?: string;
//
email: string;
phone: string;
quota: number;
company: string;
//
// billingAddress: BillingAddress[] | [];
expand: { billingAddress?: BillingAddress[] };
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
timezone: string;
language: string;
currency: string;
//
id: string;
created: string;
updated?: string;
collectionId: string;
}
// RULES: core teacher data structure
export interface UserMeta {
name: string;
@@ -14,10 +61,18 @@ export interface UserMeta {
email: string;
phone?: string;
quota: number;
company?: string;
//
billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
timezone: string;
language: string;
currency: string;
//
id: string;
createdAt: Date;

View File

@@ -1,10 +1,15 @@
'use client';
// src/components/dashboard/user_meta/user-meta-edit-form.tsx
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_TEACHERS, COL_USER_METAS } from '@/constants';
import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { getUserMetaById } from '@/db/UserMetas/GetById';
import { UpdateUserMetaById } from '@/db/UserMetas/UpdateById';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
@@ -32,6 +37,7 @@ import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
@@ -40,7 +46,6 @@ import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import isDevelopment from '@/lib/check-is-development';
// TODO: review this
const schema = zod.object({
@@ -89,7 +94,7 @@ export function UserMetaEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { id: teacherId } = useParams<{ id: string }>();
const { id: userMetaId } = useParams<{ id: string }>();
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
@@ -123,7 +128,7 @@ export function UserMetaEditForm(): React.JSX.Element {
};
try {
await pb.collection(COL_USER_METAS).update(teacherId, updateData);
await pb.collection(COL_USER_METAS).update(userMetaId, updateData);
toast.success('Teacher updated successfully');
router.push(paths.dashboard.teachers.list);
} catch (error) {
@@ -133,7 +138,7 @@ export function UserMetaEditForm(): React.JSX.Element {
setIsUpdating(false);
}
},
[teacherId, router]
[userMetaId, router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
@@ -186,9 +191,9 @@ export function UserMetaEditForm(): React.JSX.Element {
);
React.useEffect(() => {
void loadExistingData(teacherId);
void loadExistingData(userMetaId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [teacherId]);
}, [userMetaId]);
if (showLoading) return <FormLoading />;
if (showError.show)

View File

@@ -5,6 +5,7 @@ const COL_LESSON_TYPES = 'LessonsTypes';
const COL_LESSON_CATEGORIES = 'LessonsCategories';
const COL_USERS = 'users';
const COL_USER_METAS = 'UserMetas';
const COL_BILLING_ADDRESS = 'billingAddress';
// RULES:
// do not use LP_CATEGORIES anymore
@@ -56,4 +57,5 @@ export {
COL_VOCABULARIES,
NS_VOCABULARY,
//
COL_BILLING_ADDRESS,
};

View File

@@ -1,11 +1,12 @@
// api method for crate student record
// RULES:
// TBA
import { pb } from '@/lib/pb';
import { COL_STUDENTS } from '@/constants';
import type { CreateFormProps } from '@/components/dashboard/student/type.d';
import { COL_STUDENTS, COL_USER_METAS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { CreateFormProps } from '@/components/dashboard/student/type.d';
export async function createStudent(data: CreateFormProps): Promise<RecordModel> {
return pb.collection(COL_STUDENTS).create(data);
return pb.collection(COL_USER_METAS).create(data);
}

View File

@@ -1,7 +1,28 @@
import { pb } from '@/lib/pb';
import { COL_STUDENTS } from '@/constants';
import { RecordModel } from 'pocketbase';
import { COL_USER_METAS } from '@/constants';
export async function getStudentById(id: string): Promise<RecordModel> {
return pb.collection(COL_STUDENTS).getOne(id);
import { pb } from '@/lib/pb';
import type { DBUserMeta, UserMeta } from '@/components/dashboard/user_meta/type.d';
export async function getStudentById(id: string): Promise<UserMeta> {
const record = await pb.collection(COL_USER_METAS).getOne<DBUserMeta>(id, { expand: 'billingAddress, helloworld' });
const temp: UserMeta = {
id: record.id,
name: record.name,
email: record.email,
quota: record.quota,
billingAddress: record.expand.billingAddress ? record.expand.billingAddress[0] : {},
status: record.status,
state: record.state,
createdAt: new Date(record.created),
collectionId: record.collectionId,
avatar: record.avatar,
phone: record.phone,
company: record.company,
timezone: record.timezone,
language: record.language,
currency: record.currency,
};
return temp;
}

View File

@@ -0,0 +1,10 @@
import { COL_USER_METAS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { UpdateStudent } from './type';
export async function UpdateStudentById(id: string, data: Partial<UpdateStudent>): Promise<RecordModel> {
return pb.collection(COL_USER_METAS).update(id, data);
}

View File

@@ -1,3 +1,5 @@
import type { BillingAddress } from '@/components/dashboard/user_meta/type.d';
// Student type definitions
export interface Student {
id: string;
@@ -9,3 +11,29 @@ export interface Student {
status: 'active' | 'blocked' | 'pending';
createdAt: Date;
}
export interface UpdateStudent {
name?: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
// avatar_file?: string;
avatar: File | null;
//
email?: string;
phone?: string;
quota?: number;
company?: string;
//
// relation handle seperately
// billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
// status: 'pending' | 'active' | 'blocked';
state?: 'pending' | 'active' | 'blocked';
//
timezone?: string;
language?: string;
currency?: string;
//
taxId?: string;
}

View File

@@ -0,0 +1,10 @@
import { COL_USER_METAS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { UpdateUserMeta } from './type';
export async function UpdateUserMetaById(id: string, data: Partial<UpdateUserMeta>): Promise<RecordModel> {
return pb.collection(COL_USER_METAS).update(id, data);
}

View File

@@ -0,0 +1,39 @@
import type { BillingAddress } from '@/components/dashboard/user_meta/type.d';
// UserMeta type definitions
export interface UserMeta {
id: string;
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'active' | 'blocked' | 'pending';
createdAt: Date;
}
export interface UpdateUserMeta {
name?: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
// avatar_file?: string;
avatar: File | null;
//
email?: string;
phone?: string;
quota?: number;
company?: string;
//
// relation handle seperately
// billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
// status: 'pending' | 'active' | 'blocked';
state?: 'pending' | 'active' | 'blocked';
//
timezone?: string;
language?: string;
currency?: string;
//
taxId?: string;
}

View File

@@ -0,0 +1,11 @@
// api method for crate student record
// RULES:
// TBA
import { pb } from '@/lib/pb';
import { COL_STUDENTS } from '@/constants';
import type { CreateFormProps } from '@/components/dashboard/student/type.d';
import type { RecordModel } from 'pocketbase';
export async function createStudent(data: CreateFormProps): Promise<RecordModel> {
return pb.collection(COL_STUDENTS).create(data);
}

View File

@@ -0,0 +1,6 @@
import { pb } from '@/lib/pb';
import { COL_STUDENTS, COL_USER_METAS } from '@/constants';
export async function deleteStudent(id: string): Promise<boolean> {
return pb.collection(COL_USER_METAS).delete(id);
}

View File

@@ -0,0 +1,9 @@
import { COL_STUDENTS, COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetActiveCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, {
filter: 'status = "active" && role = "student"',
});
return count;
}

View File

@@ -0,0 +1,7 @@
import { pb } from '@/lib/pb';
import { COL_STUDENTS } from '@/constants';
import { RecordModel } from 'pocketbase';
export async function getAllStudents(options = {}): Promise<RecordModel[]> {
return pb.collection(COL_STUDENTS).getFullList(options);
}

View File

@@ -0,0 +1,10 @@
import { pb } from '@/lib/pb';
import { COL_USER_METAS } from '@/constants';
export async function getAllStudentsCount(): Promise<number> {
const result = await pb.collection(COL_USER_METAS).getList(1, 1, {
filter: `role = "student"`,
//
});
return result.totalItems;
}

View File

@@ -0,0 +1,9 @@
import { COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetBlockedCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, {
filter: 'status = "blocked" && role = "student"',
});
return count;
}

View File

@@ -0,0 +1,34 @@
import { COL_BILLING_ADDRESS, COL_STUDENTS, COL_USER_METAS } from '@/constants';
import { RecordModel } from 'pocketbase';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import type { DBUserMeta, UserMeta } from '@/components/dashboard/user_meta/type.d';
export async function getBillingAddressById(id: string): Promise<UserMeta> {
const record = await pb
.collection(COL_BILLING_ADDRESS)
.getOne<DBUserMeta>(id, { expand: 'billingAddress, helloworld' });
console.log({ record });
const temp: UserMeta = {
id: record.id,
name: record.name,
email: record.email,
quota: record.quota,
billingAddress: record.expand.billingAddress ? record.expand.billingAddress[0] : {},
status: record.status,
state: record.state,
createdAt: new Date(record.created),
collectionId: record.collectionId,
avatar: record.avatar,
phone: record.phone,
company: record.company,
timezone: record.timezone,
language: record.language,
currency: record.currency,
};
return temp;
}

View File

@@ -0,0 +1,9 @@
import { COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetPendingCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, {
filter: 'status = "pending" && role = "student"',
});
return count;
}

View File

@@ -0,0 +1,3 @@
export function helloCustomer() {
return 'Hello from Customers module!';
}

View File

@@ -0,0 +1,8 @@
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import type { EditFormProps } from '@/components/dashboard/customer/type.d';
export async function updateCustomer(id: string, data: Partial<EditFormProps>): Promise<RecordModel> {
return pb.collection(COL_CUSTOMERS).update(id, data);
}

View File

@@ -0,0 +1,10 @@
import { COL_BILLING_ADDRESS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { UpdateBillingAddress } from './type';
export async function UpdateBillingAddressById(id: string, data: Partial<UpdateBillingAddress>): Promise<RecordModel> {
return pb.collection(COL_BILLING_ADDRESS).update(id, data);
}

View File

@@ -0,0 +1,31 @@
# GUIDELINES
This folder contains drivers for `Customer`/`Customers` records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)
- write (Update.tsx)
- count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx)
- misc (Helloworld.tsx)
- delete (Delete.tsx)
- list (GetAll.tsx)
the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src`
## Assumption and Requirements
- assume `pb` is located in `@/lib/pb`
- no need to handle error in this function, i'll handle it in the caller
- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx`
simple template:
```typescript
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
export async function createCustomer(data: CreateFormProps) {
// ...content
// use direct return of pb.collection (e.g. return pb.collection(xxx))
}
```

View File

@@ -0,0 +1,23 @@
export interface BillingAddress {
city: string;
country: string;
line1: string;
line2: string;
state: string;
zipCode: string;
//
id: string;
collectionId: string;
collectionName: string;
updated: string;
created: string;
}
export interface UpdateBillingAddress {
city?: string;
country?: string;
line1?: string;
line2?: string;
state?: string;
zipCode?: string;
}

View File

@@ -1,149 +0,0 @@
volumes:
shared:
dist:
services:
cms:
image: 192.168.10.61:5000/cms_ubuntu
# build: ./cms
env_file:
- .env
volumes:
- ./cms:/app
ports:
- 3000:3000
working_dir: /app
command: ./scripts/docker/entrypoint.sh
depends_on:
pocketbase:
condition: service_healthy
healthcheck:
#optional (recommended) since v0.10.0
test: wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
interval: 5s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
reservations:
cpus: "0.01"
doc:
build: ./doc
env_file:
- .env
volumes:
- ./doc:/app
ports:
- 3001:3000
working_dir: /app
command: ./scripts/docker/entrypoint.sh
healthcheck:
#optional (recommended) since v0.10.0
test: wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
interval: 5s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
reservations:
cpus: "0.01"
ionic_mobile:
# image: node:20-bullseye-slim
# build: ./ionic_mobile
image: 192.168.10.61:5000/ionic_mobile_ubuntu
# user: 1000:1000
env_file:
- .env
volumes:
- ./ionic_mobile:/app
ports:
- 5173:5173
working_dir: /app
command: ./scripts/docker/entrypoint.sh
depends_on:
pocketbase:
condition: service_healthy
healthcheck:
#optional (recommended) since v0.10.0
test: wget --no-verbose --tries=1 --spider http://localhost:5173 || exit 1
interval: 5s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
reservations:
cpus: "0.01"
api_ts:
image: 192.168.10.61:5000/api_ts_ubuntu
# build: ./api_ts
volumes:
- ./api_ts:/app
working_dir: /app
# env_file:
# - .env
environment:
- NODE_ENV=production
- PB_HOSTNAME=pocketbase
- PB_USERNAME=admin@123.com
- PB_PASSWORD=Aa12345678
ports:
- 8080:3000
# command: sleep infinity
command: ./entrypoint.sh
# depends_on:
# pocketbase:
# condition: service_healthy
# healthcheck:
# #optional (recommended) since v0.10.0
# test: wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
# interval: 5s
# timeout: 5s
# retries: 5
deploy:
resources:
limits:
cpus: 0.5
reservations:
cpus: 0.01
pocketbase:
# image: ghcr.io/muchobien/pocketbase:latest
build:
context: ./pocketbase/docker
args:
- VERSION=0.26.6 # Specify the PocketBase version here
# hostname: pocketbase
restart: always
# environment:
# ENCRYPTION: example #optional
ports:
- 8090:8090
volumes:
- ./pocketbase/volumes/pb_data:/pb_data
- ./pocketbase/pb_hooks:/pb_hooks
- ./pocketbase/pb_migrations:/pb_migrations
# healthcheck:
# #optional (recommended) since v0.10.0
# test: wget --no-verbose --tries=1 --spider http://localhost:8090/api/health || exit 1
# interval: 5s
# timeout: 5s
# retries: 5
deploy:
resources:
limits:
cpus: 0.5
reservations:
cpus: 0.01

View File

@@ -16,8 +16,7 @@ $app.rootCmd.addCommand(
require(`${__hooks}/seed/002_LessonsCategories.js`)($app);
require(`${__hooks}/seed/003_Categories.js`)($app);
require(`${__hooks}/seed/004_clean_users.js`)($app);
require(`${__hooks}/seed/005_Users_teacher.js`)($app);
require(`${__hooks}/seed/006_Users_student.js`)($app);
require(`${__hooks}/seed/007_Users_admin.js`)($app);
require(`${__hooks}/seed/010_Vocabularies.js`)($app);
//
@@ -35,6 +34,10 @@ $app.rootCmd.addCommand(
require(`${__hooks}/seed/052_Students.js`)($app);
//
require(`${__hooks}/seed/060_Notifications.js`)($app);
require(`${__hooks}/seed/062_billingAddress.js`)($app);
//
require(`${__hooks}/seed/063_Users_teacher.js`)($app);
require(`${__hooks}/seed/064_Users_student.js`)($app);
$app.reloadCachedCollections();
$app.reloadSettings();

View File

@@ -0,0 +1,54 @@
//
// RULES:
// this is not a normal nodejs engine,
// it is a nodejs provided by golang,
// so fakerjs cannot be used here
// use vscode extensions 'Gruntfuggly.align-mode' to align comma
//
const config = require("/pb_hooks/seed/config.js");
const utils = require("/pb_hooks/seed/utils.js");
//
module.exports = ($app) => {
const { CR_cat_id_news, CR_cat_id_technology, user_id_admin, user_id_test_teacher_1 } = config;
const { getId, getAsset, dirtyTruncateTable } = utils;
// generate from `./project/001_documentation/Requirements/REQ0006/gen_customer/gen_customer.mjs`
const SAMPLE_BILLING_ADDRESS_CSV = `
Central African Republic , Arizona , Winfieldburgh , 92017-8004, 1838 Willa Freeway , Suite 307
Iraq , Nevada , Casa Grande , 83831-3843, 6984 Alberto Radial , Suite 154
Grenada , Georgia , New Brodyfort , 18887-7075, 493 Pfannerstill Meadow, Apt. 358
Australia , North Carolina, Fort Jerrell , 14211 , 1763 West Street , Suite 699
Reunion , New York , Kayton , 82048-0645, 636 Angel Junction , Apt. 361
Heard Island and McDonald Islands, Wisconsin , Jalenbury , 75732-7013, 669 Sven Trail , Suite 409
Israel , Maryland , East Allenmouth, 21779 , 6070 W Grand Avenue , Suite 448
Canada , Michigan , Lafayette , 90430-8775, 430 Orland Place , Suite 891
South Georgia , Colorado , Lake Isaias , 26025-5909, 143 Kautzer Unions , Apt. 752
Mali , Illinois , Stammburgh , 92318 , 7669 Jude Drive , Apt. 594
`;
const SAMPLE_BILLING_ADDRESS_AA = SAMPLE_BILLING_ADDRESS_CSV.trim()
.split("\n")
.map((r) => r.split(",").map((c) => c.trim()));
let row_array = SAMPLE_BILLING_ADDRESS_AA;
dirtyTruncateTable("billingAddress");
let ba_collection = $app.findCollectionByNameOrId("billingAddress");
for (let i = 0; i < row_array.length; i++) {
let ba = row_array[i];
let record = new Record(ba_collection);
record.set("id", getId(i.toString()));
record.set("country", ba[0]);
record.set("state", ba[1]);
record.set("city", ba[2]);
record.set("zipCode", ba[3]);
record.set("line1", ba[4]);
record.set("line2", ba[5]);
$app.save(record);
}
console.log(`062_billingAddress done`);
};

View File

@@ -1,9 +1,16 @@
//
// RULES:
// this is not a normal nodejs engine,
// it is a nodejs provided by golang,
// so fakerjs cannot be used here
// use vscode extensions 'Gruntfuggly.align-mode' to align comma
//
const config = require("/pb_hooks/seed/config.js");
const utils = require("/pb_hooks/seed/utils.js");
module.exports = ($app) => {
const { CR_cat_id_news, CR_cat_id_technology } = config;
const { getId, getAsset } = utils;
const { getId, getAsset, randomId } = utils;
let row_array = [
[getId("11"), "teacher1@123.com", "teacher1@123.com", "teacher1@123.com", true, true, "test_teacher_1"],
@@ -52,15 +59,10 @@ module.exports = ($app) => {
um_record.set("email", user[3]);
um_record.set("phone", "9123456" + i.toString());
um_record.set("billingAddress", randomId(10));
$app.save(um_record);
}
console.log("005 add teacher user done");
console.log("063 add teacher user done");
};
// TODO: delete this ?
// const dirtyTruncateTable = (COLLECTION_NAME) => {
// console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
// const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
// cmd_to_exec.output();
// };

View File

@@ -1,9 +1,16 @@
//
// RULES:
// this is not a normal nodejs engine,
// it is a nodejs provided by golang,
// so fakerjs cannot be used here
// use vscode extensions 'Gruntfuggly.align-mode' to align comma
//
const config = require("/pb_hooks/seed/config.js");
const utils = require("/pb_hooks/seed/utils.js");
module.exports = ($app) => {
const { CR_cat_id_news, CR_cat_id_technology } = config;
const { getId, getAsset } = utils;
const { getId, getAsset, randomId } = utils;
let row_array = [
[getId("1"), "user1@123.com", "user1@123.com", "user1@123.com", true, true, "test_student_1"],
@@ -51,20 +58,20 @@ module.exports = ($app) => {
um_record.set("avatar_file", um[5]);
//
um_record.set("role", um[6]);
um_record.set("name", um[7]);
um_record.set("email", user[3]);
um_record.set("phone", "9123456" + i.toString());
um_record.set("company", "company_" + i.toString());
um_record.set("taxId", "taxId_" + i.toString());
um_record.set("timezone", "America/New_York");
um_record.set("language", "en");
um_record.set("currency", "EUR");
um_record.set("billingAddress", randomId(10));
um_record.set("role", um[6]);
$app.save(um_record);
}
console.log("006 add student user done");
console.log("064 add student user done");
};
// TODO: delete this ?
// const dirtyTruncateTable = (COLLECTION_NAME) => {
// console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
// const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
// cmd_to_exec.output();
// };

View File

@@ -0,0 +1,3 @@
{
"hello": "json"
}

View File

@@ -1750,6 +1750,20 @@
"system": false,
"type": "json"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
@@ -1770,20 +1784,6 @@
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
@@ -1892,26 +1892,6 @@
"system": false,
"type": "relation"
},
{
"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"
},
{
"autogeneratePattern": "",
"hidden": false,
@@ -2014,6 +1994,26 @@
"required": false,
"system": false,
"type": "editor"
},
{
"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": [],
@@ -2182,6 +2182,20 @@
"system": false,
"type": "json"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
@@ -2201,20 +2215,6 @@
"presentable": false,
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"indexes": [],
@@ -2870,7 +2870,7 @@
"id": "text4192936109",
"max": 0,
"min": 0,
"name": "helloworld",
"name": "address",
"pattern": "",
"presentable": false,
"primaryKey": false,
@@ -2901,6 +2901,20 @@
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2744374011",
"max": 0,
"min": 0,
"name": "state",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
@@ -2921,20 +2935,6 @@
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2744374011",
"max": 0,
"min": 0,
"name": "status",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "file376926767",
@@ -3001,6 +3001,89 @@
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1337919823",
"max": 0,
"min": 0,
"name": "company",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2020362641",
"max": 0,
"min": 0,
"name": "taxId",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text922858135",
"max": 0,
"min": 0,
"name": "timezone",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3571151285",
"max": 0,
"min": 0,
"name": "language",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1767278655",
"max": 0,
"min": 0,
"name": "currency",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1509025625",
"hidden": false,
"id": "relation2115670734",
"maxSelect": 999,
"minSelect": 0,
"name": "billingAddress",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}
],
"indexes": [],
@@ -3604,11 +3687,11 @@
},
{
"id": "pbc_1509025625",
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",
"name": "billingAddress",
"type": "base",
"fields": [
@@ -3798,11 +3881,11 @@
},
{
"id": "pbc_2109205374",
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",
"name": "t1",
"type": "base",
"fields": [

View File

@@ -10,6 +10,10 @@ module.exports = {
}
},
getId: (id) => id.padStart(15, 0),
randomId: (max) =>
Math.floor(Math.random() * max)
.toString()
.padStart(15, 0),
dirtyTruncateTable: (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);