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[]
info Json?
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';
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() {
const alice = await prisma.partyUser.upsert({
where: { email: 'alice@prisma.io' },
@@ -8,8 +43,14 @@ async function partyUser() {
create: {
email: 'alice@prisma.io',
name: 'Alice',
username: 'pualice',
password: 'Aa12345678',
emailVerified: new Date(),
phoneNumber: '+85291234567',
company: 'helloworld company',
status: STATUS[0],
role: ROLE[0],
isVerified: true,
},
});
@@ -19,31 +60,48 @@ async function partyUser() {
create: {
email: 'demo@minimals.cc',
name: 'Demo',
username: 'pudemo',
password: '@2Minimal',
emailVerified: new Date(),
phoneNumber: '+85291234568',
company: 'helloworld company',
status: STATUS[1],
role: ROLE[1],
isVerified: true,
},
});
await prisma.partyUser.upsert({
where: { email: 'bob@prisma.io' },
update: {},
create: {
email: 'bob@prisma.io',
name: 'Bob',
password: 'Aa12345678',
emailVerified: new Date(),
},
});
for (let i = 0; i < 5; i++) {
const CJK_LOCALES = {
en: enFaker,
zh: zhFaker,
ja: jaFaker,
ko: koFaker,
tw: twFaker,
};
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({
where: { email: `bob${i}@prisma.io` },
where: { email: `party_user${i}@prisma.io` },
update: {},
create: {
email: `bob${i}@prisma.io`,
name: 'Bob',
email: `party_user${i}@prisma.io`,
name: `Party Dummy ${i}`,
username: `pu${i.toString()}`,
password: 'Aa12345678',
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
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
killall node

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
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 { Label } from 'src/components/label';
import { toast } from 'src/components/snackbar';
@@ -27,27 +27,30 @@ import { z as zod } from 'zod';
export type NewUserSchemaType = zod.infer<typeof NewUserSchema>;
export const NewUserSchema = zod.object({
name: zod.string().min(1, { message: 'Name is required!' }),
city: zod.string().min(1, { message: 'City is required!' }),
name: zod.string().min(1, { message: 'Name is required!' }).optional().or(zod.literal('')),
city: zod.string().min(1, { message: 'City is required!' }).optional().or(zod.literal('')),
role: zod.string().min(1, { message: 'Role is required!' }),
email: zod
.string()
.min(1, { message: 'Email is required!' })
.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(),
address: zod.string().min(1, { message: 'Address is required!' }),
country: schemaHelper.nullableInput(zod.string().min(1, { 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!' }),
avatarUrl: schemaHelper.file({ message: 'Avatar is required!' }),
phoneNumber: schemaHelper.phoneNumber({ isValid: isValidPhoneNumber }),
isVerified: zod.boolean(),
username: zod.string(),
password: zod.string(),
address: zod.string().min(1, { message: 'Address is required!' }).optional().or(zod.literal('')),
country: schemaHelper
.nullableInput(zod.string().min(1, { message: 'Country is required!' }), {
// message for null value
message: 'Country is required!',
})
.optional()
.or(zod.literal('')),
zipCode: zod.string().min(1, { message: 'Zip code is required!' }).optional().or(zod.literal('')),
company: zod.string().min(1, { message: 'Company is required!' }).optional().or(zod.literal('')),
avatarUrl: zod.string().optional().or(zod.literal('')),
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 = {
status: '',
avatarUrl: null,
isVerified: true,
avatarUrl: '',
name: '新用戶名字',
email: 'user@123.com',
phoneNumber: '+85291234567',
country: 'Hong Kong',
state: 'HK',
city: 'hong kong',
address: 'Kwun Tong, Sau Mau Ping',
zipCode: '00000',
company: 'test company',
phoneNumber: '',
country: '',
state: '',
city: '',
address: '',
zipCode: '',
company: '',
role: 'user',
// Email is verified
isVerified: true,
//
username: '',
password: '',
@@ -123,12 +127,14 @@ export function PartyUserNewEditForm({ currentUser }: Props) {
data.avatarUrl = await fileToBase64(temp);
}
const sanitizedValues: IPartyUserItem = values as unknown as IPartyUserItem;
if (currentUser) {
// perform save
await saveUser(currentUser.id, data);
// perform
await updatePartyUser(sanitizedValues);
} else {
// perform create
await createUser(data);
await createUser(sanitizedValues);
}
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)' },
}}
>
<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="email" label={t('Email address')} />
<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="city" label={t('City')} />
<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="role" label={t('Role')} />
</Box>
<Stack sx={{ mt: 3, alignItems: 'flex-end' }}>
<>{JSON.stringify({ errors })}</>
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
variant="contained"
>
{!currentUser ? t('Create user') : t('Save changes')}
{!currentUser ? t('create-user') : t('save-changes')}
</Button>
</Stack>
</Card>