feat: enhance party user schema with company, status, role and verification fields, update seeding and frontend form

This commit is contained in:
louiscklaw
2025-06-17 18:31:20 +08:00
parent 7a793be610
commit eb515dbe68
6 changed files with 143 additions and 58 deletions

View File

@@ -1279,4 +1279,8 @@ model PartyUser {
sessions Session[] sessions Session[]
info Json? info Json?
phoneNumber String @default("") phoneNumber String @default("")
company String @default("")
status String @default("pending")
role String @default("")
isVerified Boolean @default(false)
} }

View File

@@ -1,6 +1,41 @@
import { faker as enFaker } from '@faker-js/faker/locale/en_US';
import { faker as zhFaker } from '@faker-js/faker/locale/zh_CN';
import { faker as jaFaker } from '@faker-js/faker/locale/ja';
import { faker as koFaker } from '@faker-js/faker/locale/ko';
import { faker as twFaker } from '@faker-js/faker/locale/zh_TW';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
const ROLE = [
`CEO`,
`CTO`,
`Project Coordinator`,
`Team Leader`,
`Software Developer`,
`Marketing Strategist`,
`Data Analyst`,
`Product Owner`,
`Graphic Designer`,
`Operations Manager`,
`Customer Support Specialist`,
`Sales Manager`,
`HR Recruiter`,
`Business Consultant`,
`Financial Planner`,
`Network Engineer`,
`Content Creator`,
`Quality Assurance Tester`,
`Public Relations Officer`,
`IT Administrator`,
`Compliance Officer`,
`Event Planner`,
`Legal Counsel`,
`Training Coordinator`,
];
const STATUS = ['active', 'pending', 'banned'];
async function partyUser() { async function partyUser() {
const alice = await prisma.partyUser.upsert({ const alice = await prisma.partyUser.upsert({
where: { email: 'alice@prisma.io' }, where: { email: 'alice@prisma.io' },
@@ -8,8 +43,14 @@ async function partyUser() {
create: { create: {
email: 'alice@prisma.io', email: 'alice@prisma.io',
name: 'Alice', name: 'Alice',
username: 'pualice',
password: 'Aa12345678', password: 'Aa12345678',
emailVerified: new Date(), emailVerified: new Date(),
phoneNumber: '+85291234567',
company: 'helloworld company',
status: STATUS[0],
role: ROLE[0],
isVerified: true,
}, },
}); });
@@ -19,31 +60,48 @@ async function partyUser() {
create: { create: {
email: 'demo@minimals.cc', email: 'demo@minimals.cc',
name: 'Demo', name: 'Demo',
username: 'pudemo',
password: '@2Minimal', password: '@2Minimal',
emailVerified: new Date(), emailVerified: new Date(),
phoneNumber: '+85291234568',
company: 'helloworld company',
status: STATUS[1],
role: ROLE[1],
isVerified: true,
}, },
}); });
await prisma.partyUser.upsert({ for (let i = 0; i < 5; i++) {
where: { email: 'bob@prisma.io' }, const CJK_LOCALES = {
update: {}, en: enFaker,
create: { zh: zhFaker,
email: 'bob@prisma.io', ja: jaFaker,
name: 'Bob', ko: koFaker,
password: 'Aa12345678', tw: twFaker,
emailVerified: new Date(), };
},
}); function getRandomCJKFaker() {
const locales = Object.keys(CJK_LOCALES);
const randomKey = locales[Math.floor(Math.random() * locales.length)] as keyof typeof CJK_LOCALES;
return CJK_LOCALES[randomKey];
}
const randomFaker = getRandomCJKFaker();
for (let i = 0; i < 10; i++) {
await prisma.partyUser.upsert({ await prisma.partyUser.upsert({
where: { email: `bob${i}@prisma.io` }, where: { email: `party_user${i}@prisma.io` },
update: {}, update: {},
create: { create: {
email: `bob${i}@prisma.io`, email: `party_user${i}@prisma.io`,
name: 'Bob', name: `Party Dummy ${i}`,
username: `pu${i.toString()}`,
password: 'Aa12345678', password: 'Aa12345678',
emailVerified: new Date(), emailVerified: new Date(),
phoneNumber: `+8529123456${i.toString()}`,
company: randomFaker.company.name(),
role: ROLE[Math.floor(Math.random() * ROLE.length)],
status: STATUS[Math.floor(Math.random() * STATUS.length)],
isVerified: true,
}, },
}); });
} }

View File

@@ -7,7 +7,8 @@ clear
while true; do while true; do
yarn db:studio & yarn db:studio &
npx nodemon --ext ts,tsx,prisma --exec "yarn db:push && yarn seed && yarn dev" npx nodemon --ext ts,tsx,prisma --exec "yarn dev"
# npx nodemon --ext ts,tsx,prisma --exec "yarn db:push && yarn seed && yarn dev"
# yarn dev # yarn dev
killall node killall node

View File

@@ -5,10 +5,10 @@ Content-Type: application/json
{ {
"partyUserData": { "partyUserData": {
"id": "cmbxzds7q0009xyn5367ifizp", "id": "cmc0cedkx000boln3viy77598",
"createdAt": "2025-06-15T17:47:24.547Z", "createdAt": "2025-06-15T17:47:24.547Z",
"updatedAt": "2025-06-15T17:47:24.547Z", "updatedAt": "2025-06-15T17:47:24.547Z",
"name": "Alice", "name": "Alice 123321",
"username": null, "username": null,
"email": "alice@prisma.io", "email": "alice@prisma.io",
"emailVerified": "2025-06-15T17:47:23.919Z", "emailVerified": "2025-06-15T17:47:23.919Z",
@@ -16,6 +16,7 @@ Content-Type: application/json
"image": null, "image": null,
"bucketImage": null, "bucketImage": null,
"admin": false, "admin": false,
"info": null "info": null,
"phoneNumber": "+85291234567"
} }
} }

View File

@@ -113,18 +113,29 @@ type SaveUserData = {
password: string; password: string;
}; };
export async function saveUser(userId: string, saveUserData: SaveUserData) { export async function updatePartyUser(partyUserData: Partial<IPartyUserItem>) {
// const url = userId ? [endpoints.user.details, { params: { userId } }] : ''; /**
* Work on server
*/
const data = { partyUserData };
await axiosInstance.put(endpoints.partyUser.update, data);
const res = await axiosInstance.post( /**
// * Work in local
`http://localhost:7272/api/user/saveUser?userId=${userId}`, */
{ mutate(
data: saveUserData, endpoints.partyUser.list,
} (currentData: any) => {
const currentPartyUsers: IPartyUserItem[] = currentData?.partyUsers;
const partyUsers = currentPartyUsers.map((partyUser) =>
partyUser.id === partyUserData.id ? { ...partyUser, ...partyUserData } : partyUser
);
return { ...currentData, partyUsers };
},
false
); );
return res;
} }
export async function uploadUserImage(saveUserData: SaveUserData) { export async function uploadUserImage(saveUserData: SaveUserData) {

View File

@@ -11,7 +11,7 @@ import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { isValidPhoneNumber } from 'react-phone-number-input/input'; import { isValidPhoneNumber } from 'react-phone-number-input/input';
import { createUser, deletePartyUser, saveUser } from 'src/actions/party-user'; import { createUser, deletePartyUser, updatePartyUser } from 'src/actions/party-user';
import { Field, Form, schemaHelper } from 'src/components/hook-form'; import { Field, Form, schemaHelper } from 'src/components/hook-form';
import { Label } from 'src/components/label'; import { Label } from 'src/components/label';
import { toast } from 'src/components/snackbar'; import { toast } from 'src/components/snackbar';
@@ -27,27 +27,30 @@ import { z as zod } from 'zod';
export type NewUserSchemaType = zod.infer<typeof NewUserSchema>; export type NewUserSchemaType = zod.infer<typeof NewUserSchema>;
export const NewUserSchema = zod.object({ export const NewUserSchema = zod.object({
name: zod.string().min(1, { message: 'Name is required!' }), name: zod.string().min(1, { message: 'Name is required!' }).optional().or(zod.literal('')),
city: zod.string().min(1, { message: 'City is required!' }), city: zod.string().min(1, { message: 'City is required!' }).optional().or(zod.literal('')),
role: zod.string().min(1, { message: 'Role is required!' }), role: zod.string().min(1, { message: 'Role is required!' }),
email: zod email: zod
.string() .string()
.min(1, { message: 'Email is required!' }) .min(1, { message: 'Email is required!' })
.email({ message: 'Email must be a valid email address!' }), .email({ message: 'Email must be a valid email address!' }),
state: zod.string().min(1, { message: 'State is required!' }), state: zod.string().min(1, { message: 'State is required!' }).optional().or(zod.literal('')),
status: zod.string(), status: zod.string(),
address: zod.string().min(1, { message: 'Address is required!' }), address: zod.string().min(1, { message: 'Address is required!' }).optional().or(zod.literal('')),
country: schemaHelper.nullableInput(zod.string().min(1, { message: 'Country is required!' }), { country: schemaHelper
// message for null value .nullableInput(zod.string().min(1, { message: 'Country is required!' }), {
message: 'Country is required!', // message for null value
}), message: 'Country is required!',
zipCode: zod.string().min(1, { message: 'Zip code is required!' }), })
company: zod.string().min(1, { message: 'Company is required!' }), .optional()
avatarUrl: schemaHelper.file({ message: 'Avatar is required!' }), .or(zod.literal('')),
phoneNumber: schemaHelper.phoneNumber({ isValid: isValidPhoneNumber }), zipCode: zod.string().min(1, { message: 'Zip code is required!' }).optional().or(zod.literal('')),
isVerified: zod.boolean(), company: zod.string().min(1, { message: 'Company is required!' }).optional().or(zod.literal('')),
username: zod.string(), avatarUrl: zod.string().optional().or(zod.literal('')),
password: zod.string(), phoneNumber: zod.string().optional().or(zod.literal('')),
isVerified: zod.boolean().default(true),
username: zod.string().optional().or(zod.literal('')),
password: zod.string().optional().or(zod.literal('')),
}); });
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -63,18 +66,19 @@ export function PartyUserNewEditForm({ currentUser }: Props) {
const defaultValues: NewUserSchemaType = { const defaultValues: NewUserSchemaType = {
status: '', status: '',
avatarUrl: null, avatarUrl: '',
isVerified: true,
name: '新用戶名字', name: '新用戶名字',
email: 'user@123.com', email: 'user@123.com',
phoneNumber: '+85291234567', phoneNumber: '',
country: 'Hong Kong', country: '',
state: 'HK', state: '',
city: 'hong kong', city: '',
address: 'Kwun Tong, Sau Mau Ping', address: '',
zipCode: '00000', zipCode: '',
company: 'test company', company: '',
role: 'user', role: 'user',
// Email is verified
isVerified: true,
// //
username: '', username: '',
password: '', password: '',
@@ -123,12 +127,14 @@ export function PartyUserNewEditForm({ currentUser }: Props) {
data.avatarUrl = await fileToBase64(temp); data.avatarUrl = await fileToBase64(temp);
} }
const sanitizedValues: IPartyUserItem = values as unknown as IPartyUserItem;
if (currentUser) { if (currentUser) {
// perform save // perform
await saveUser(currentUser.id, data); await updatePartyUser(sanitizedValues);
} else { } else {
// perform create // perform create
await createUser(data); await createUser(sanitizedValues);
} }
toast.success(currentUser ? t('Update success!') : t('Create success!')); toast.success(currentUser ? t('Update success!') : t('Create success!'));
@@ -260,6 +266,9 @@ export function PartyUserNewEditForm({ currentUser }: Props) {
gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(2, 1fr)' }, gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(2, 1fr)' },
}} }}
> >
<Field.Text name="username" label={t('username')} />
<Field.Text name="password" label={t('password')} />
<Field.Text name="name" label={t('Full name')} /> <Field.Text name="name" label={t('Full name')} />
<Field.Text name="email" label={t('Email address')} /> <Field.Text name="email" label={t('Email address')} />
<Field.Phone name="phoneNumber" label={t('Phone number')} country="HK" /> <Field.Phone name="phoneNumber" label={t('Phone number')} country="HK" />
@@ -274,19 +283,20 @@ export function PartyUserNewEditForm({ currentUser }: Props) {
<Field.Text name="state" label={t('State/region')} /> <Field.Text name="state" label={t('State/region')} />
<Field.Text name="city" label={t('City')} /> <Field.Text name="city" label={t('City')} />
<Field.Text name="address" label={t('Address')} /> <Field.Text name="address" label={t('Address')} />
<Field.Text name="zipCode" label={t('Zip/code')} /> <Field.Text name="zipCode" label={t('Zip/code')} required={false} />
<Field.Text name="company" label={t('Company')} /> <Field.Text name="company" label={t('Company')} />
<Field.Text name="role" label={t('Role')} /> <Field.Text name="role" label={t('Role')} />
</Box> </Box>
<Stack sx={{ mt: 3, alignItems: 'flex-end' }}> <Stack sx={{ mt: 3, alignItems: 'flex-end' }}>
<>{JSON.stringify({ errors })}</>
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting} loading={isSubmitting}
type="submit" type="submit"
variant="contained" variant="contained"
> >
{!currentUser ? t('Create user') : t('Save changes')} {!currentUser ? t('create-user') : t('save-changes')}
</Button> </Button>
</Stack> </Stack>
</Card> </Card>