From eb515dbe68392bf50a5bcb735370c7dc027734f0 Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Tue, 17 Jun 2025 18:31:20 +0800 Subject: [PATCH] feat: enhance party user schema with company, status, role and verification fields, update seeding and frontend form --- 03_source/cms_backend/prisma/schema.prisma | 4 + .../cms_backend/prisma/seeds/partyUser.ts | 86 ++++++++++++++++--- 03_source/cms_backend/scripts/00_dev.sh | 3 +- .../src/app/api/party-user/update/test.http | 7 +- 03_source/frontend/src/actions/party-user.ts | 31 ++++--- .../party-user/party-user-new-edit-form.tsx | 70 ++++++++------- 6 files changed, 143 insertions(+), 58 deletions(-) diff --git a/03_source/cms_backend/prisma/schema.prisma b/03_source/cms_backend/prisma/schema.prisma index 33ae79d..48dcc14 100644 --- a/03_source/cms_backend/prisma/schema.prisma +++ b/03_source/cms_backend/prisma/schema.prisma @@ -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) } diff --git a/03_source/cms_backend/prisma/seeds/partyUser.ts b/03_source/cms_backend/prisma/seeds/partyUser.ts index be13518..e5fe238 100644 --- a/03_source/cms_backend/prisma/seeds/partyUser.ts +++ b/03_source/cms_backend/prisma/seeds/partyUser.ts @@ -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, }, }); } diff --git a/03_source/cms_backend/scripts/00_dev.sh b/03_source/cms_backend/scripts/00_dev.sh index 74fc351..f14d8d8 100755 --- a/03_source/cms_backend/scripts/00_dev.sh +++ b/03_source/cms_backend/scripts/00_dev.sh @@ -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 diff --git a/03_source/cms_backend/src/app/api/party-user/update/test.http b/03_source/cms_backend/src/app/api/party-user/update/test.http index 5b59a25..904ea85 100644 --- a/03_source/cms_backend/src/app/api/party-user/update/test.http +++ b/03_source/cms_backend/src/app/api/party-user/update/test.http @@ -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" } } diff --git a/03_source/frontend/src/actions/party-user.ts b/03_source/frontend/src/actions/party-user.ts index 0a53956..7190309 100644 --- a/03_source/frontend/src/actions/party-user.ts +++ b/03_source/frontend/src/actions/party-user.ts @@ -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) { + /** + * 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) { diff --git a/03_source/frontend/src/sections/party-user/party-user-new-edit-form.tsx b/03_source/frontend/src/sections/party-user/party-user-new-edit-form.tsx index 2390a0e..792b2fe 100644 --- a/03_source/frontend/src/sections/party-user/party-user-new-edit-form.tsx +++ b/03_source/frontend/src/sections/party-user/party-user-new-edit-form.tsx @@ -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; 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)' }, }} > + + + @@ -274,19 +283,20 @@ export function PartyUserNewEditForm({ currentUser }: Props) { - + + <>{JSON.stringify({ errors })}