feat: add REQ0188 frontend party-user CRUD functionality with backend API endpoints and database schema

This commit is contained in:
louiscklaw
2025-06-16 02:26:41 +08:00
parent 47660be0cd
commit 17aaf97722
22 changed files with 661 additions and 13 deletions

View File

@@ -0,0 +1,20 @@
---
tags: frontend, party-user
---
# REQ0188 frontend party-user
frontend page to handle party-user (CRUD)
edit page T.B.A.
## TODO
## sources
T.B.A.
## branch
develop/requirements/REQ0188
develop/frontend/party-user/trunk

View File

@@ -31,17 +31,21 @@ model Account {
oauth_token_secret String?
oauth_token String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
PartyUser PartyUser? @relation(fields: [partyUserId], references: [id])
partyUserId String?
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique @map("session_token")
userId String @map("user_id")
id String @id @default(cuid())
sessionToken String @unique @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
PartyUser PartyUser? @relation(fields: [partyUserId], references: [id])
partyUserId String?
}
model User {
@@ -1257,3 +1261,21 @@ model PartyOrderItem {
// OrderPayment OrderPayment[]
// OrderShippingAddress OrderShippingAddress[]
}
model PartyUser {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
name String?
username String? @unique
email String @unique
emailVerified DateTime?
password String?
image String?
bucketImage String?
admin Boolean @default(false)
accounts Account[]
sessions Session[]
info Json?
}

View File

@@ -31,7 +31,9 @@ import { EventReviewSeed } from './seeds/eventReview';
import { appLogSeed } from './seeds/AppLog';
import { accessLogSeed } from './seeds/AccessLog';
import { userMetaSeed } from './seeds/userMeta';
//
import { partyOrderItemSeed } from './seeds/partyOrderItem';
import { partyUserSeed } from './seeds/partyUser';
//
// import { Blog } from './seeds/blog';
@@ -60,8 +62,9 @@ import { partyOrderItemSeed } from './seeds/partyOrderItem';
//
await appLogSeed;
await accessLogSeed;
//
await partyOrderItemSeed;
await partyUserSeed;
// await Blog;
// await Mail;

View File

@@ -0,0 +1,50 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function partyUser() {
const alice = await prisma.partyUser.upsert({
where: { email: 'alice@prisma.io' },
update: {},
create: {
email: 'alice@prisma.io',
name: 'Alice',
password: 'Aa12345678',
emailVerified: new Date(),
},
});
await prisma.partyUser.upsert({
where: { email: 'demo@minimals.cc' },
update: {},
create: {
email: 'demo@minimals.cc',
name: 'Demo',
password: '@2Minimal',
emailVerified: new Date(),
},
});
await prisma.partyUser.upsert({
where: { email: 'bob@prisma.io' },
update: {},
create: {
email: 'bob@prisma.io',
name: 'Bob',
password: 'Aa12345678',
emailVerified: new Date(),
},
});
console.log('seed partyUser done');
}
const partyUserSeed = partyUser()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
export { partyUserSeed };

View File

@@ -0,0 +1,60 @@
<!-- NOTES to AI: please maintain same format and level of detail when edit this file -->
# GUIDELINE
- Party-user Management API endpoint for managing party-user accounts
- Handles party-user CRUD operations and profile updates
- Follows Next.js API route conventions
## Endpoints
### `create/route.ts`
Creates new party-user account with required details
### `delete/route.ts`
Deletes party-user account by ID
### `details/route.ts`
Gets detailed party-user information
### `helloworld/route.ts`
Simple test endpoint returning "Hello World"
### `list/route.ts`
Lists all party-users with pagination support
### `search/route.ts`
Searches party-users by name or email
### `update/route.ts`
Updates party-user profile information
## Testing
Test files are available per endpoint in their respective directories:
- `create/test.http`
- `delete/test.http`
- `details/test.http`
- `helloworld/test.http`
- `list/test.http`
- `update/test.http`
## Related Services
`../../services/party-user.service.ts` (assumed) would handle:
`createUser` - Register new party-user account
`updateUser` - Update party-user profile
`deleteUser` - Remove party-user account
`listUsers` - Get paginated party-user list
`searchUsers` - Search party-users by criteria
`changeUserRole` - Modify party-user permissions
`uploadUserImage` - Handle profile picture uploads

View File

@@ -0,0 +1,34 @@
// src/app/api/party-user/create/route.ts
//
// PURPOSE:
// create new party user in db
//
// RULES:
// 1. Validates input data shape
// 2. Returns created party user data
//
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { createPartyUser } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/**
***************************************
* POST - create PartyUser
***************************************
*/
export async function POST(req: NextRequest) {
const { partyUserData } = await req.json();
try {
const partyUser = await createPartyUser(partyUserData);
return response({ partyUser }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Create', error);
}
}

View File

@@ -0,0 +1,20 @@
###
POST http://localhost:7272/api/party-user/create
Content-Type: application/json
{
"partyUserData": {
"createdAt": "2025-06-15T17:47:24.547Z",
"updatedAt": "2025-06-15T17:47:24.547Z",
"name": "Alice",
"username": null,
"email": "alice@prisma.io",
"emailVerified": "2025-06-15T17:47:23.919Z",
"password": "Aa12345678",
"image": null,
"bucketImage": null,
"admin": false,
"info": null
}
}

View File

@@ -0,0 +1,35 @@
// src/app/api/party-user/delete/route.ts
//
// PURPOSE:
// delete party user from db by id
//
// RULES:
// 1. Requires valid party user ID
// 2. Returns deleted party user ID
// 3. Handles errors appropriately
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { deletePartyUser } from 'src/app/services/party-user.service';
/**
**************************************
* PATCH - Delete party user
***************************************
*/
export async function PATCH(req: NextRequest) {
try {
const { partyUserId } = await req.json();
if (!partyUserId) throw new Error('partyUserId cannot be null');
await deletePartyUser(partyUserId);
return response({ partyUserId }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Delete', error);
}
}

View File

@@ -0,0 +1,8 @@
###
PATCH http://localhost:7272/api/party-user/delete
Content-Type: application/json
{
"partyUserId": "cmbxz8t2b000oiigxib3o4jla"
}

View File

@@ -0,0 +1,43 @@
// src/app/api/party-user/details/route.ts
//
// PURPOSE:
// get party user details from db by id
//
// RULES:
// 1. Requires valid partyUserId parameter
// 2. Returns party user details if found
// 3. Handles not found case appropriately
// 4. Logs the operation
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { getPartyUser } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/** **************************************
* GET PartyUser detail
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: userId must exist
const partyUserId = searchParams.get('partyUserId');
if (!partyUserId) return response({ message: 'partyUserId is required!' }, STATUS.BAD_REQUEST);
// NOTE: userId confirmed exist, run below
const partyUser = await getPartyUser(partyUserId);
if (!partyUser) return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
logger('[User] details', partyUser.id);
return response({ partyUser }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Get details', error);
}
}

View File

@@ -0,0 +1,4 @@
###
GET http://localhost:7272/api/party-user/details?partyUserId=cmbxziat6000b715hmhrnwlx2

View File

@@ -0,0 +1,11 @@
import type { NextRequest, NextResponse } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
export async function GET(req: NextRequest, res: NextResponse) {
try {
return response({ helloworld: 'party-user' }, STATUS.OK);
} catch (error) {
return handleError('GET - helloworld', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/party-user/helloworld

View File

@@ -0,0 +1,35 @@
// src/app/api/party-user/update/route.ts
//
// PURPOSE:
// update existing party user in db
//
// RULES:
// 1. Requires valid party user ID
// 2. Validates input data shape
//
//
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { listPartyUsers } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/**
***************************************
* GET - Products
***************************************
*/
export async function GET() {
try {
const partyUsers = await listPartyUsers();
logger('[User] list', partyUsers.length);
return response({ users: partyUsers }, STATUS.OK);
} catch (error) {
return handleError('Product - Get list', error);
}
}

View File

@@ -0,0 +1,2 @@
###
GET http://localhost:7272/api/party-user/list

View File

@@ -0,0 +1,35 @@
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { _products } from 'src/_mock/_product';
// ----------------------------------------------------------------------
export const runtime = 'edge';
/** **************************************
* GET - Search products
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
const query = searchParams.get('query')?.trim().toLowerCase();
if (!query) {
return response({ results: [] }, STATUS.OK);
}
const products = _products();
// Accept search by name or sku
const results = products.filter(({ name, sku }) => name.toLowerCase().includes(query) || sku?.toLowerCase().includes(query));
logger('[Product] search-results', results.length);
return response({ results }, STATUS.OK);
} catch (error) {
return handleError('Product - Get search', error);
}
}

View File

@@ -0,0 +1,88 @@
// src/app/api/party-user/update/route.ts
//
// PURPOSE:
// update existing party user in db
//
// RULES:
// 1. Requires valid party user ID
// 2. Validates input data shape
// 3. Returns updated party user data
// 4. Handles errors appropriately
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { updatePartyUser } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/** **************************************
* PUT - Update PartyUser
*************************************** */
export async function PUT(req: NextRequest) {
// logger('[Product] list', products.length);
const { partyUserData } = await req.json();
try {
await updatePartyUser(partyUserData.id, partyUserData);
return response({ partyUserData }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Update', error);
}
}
export type IProductItem = {
id: string;
sku: string;
name: string;
code: string;
price: number;
taxes: number;
tags: string[];
sizes: string[];
publish: string;
gender: string[];
coverUrl: string;
images: string[];
colors: string[];
quantity: number;
category: string;
available: number;
totalSold: number;
description: string;
totalRatings: number;
totalReviews: number;
// createdAt: IDateValue;
inventoryType: string;
subDescription: string;
priceSale: number | null;
// reviews: IProductReview[];
newLabel: {
content: string;
enabled: boolean;
};
saleLabel: {
content: string;
enabled: boolean;
};
ratings: {
name: string;
starCount: number;
reviewCount: number;
}[];
};
export type IDateValue = string | number | null;
export type IProductReview = {
id: string;
name: string;
rating: number;
comment: string;
helpful: number;
avatarUrl: string;
postedAt: IDateValue;
isPurchased: boolean;
attachments?: string[];
};

View File

@@ -0,0 +1,21 @@
###
PUT http://localhost:7272/api/party-user/update
Content-Type: application/json
{
"partyUserData": {
"id": "cmbxzds7q0009xyn5367ifizp",
"createdAt": "2025-06-15T17:47:24.547Z",
"updatedAt": "2025-06-15T17:47:24.547Z",
"name": "Alice",
"username": null,
"email": "alice@prisma.io",
"emailVerified": "2025-06-15T17:47:23.919Z",
"password": "Aa12345678",
"image": null,
"bucketImage": null,
"admin": false,
"info": null
}
}

View File

@@ -13,10 +13,7 @@ import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { isDev } from 'src/constants';
import prisma from '../../../lib/prisma';
import { createProduct } from 'src/app/services/product.service';
// import { createProduct } from 'src/app/services/product.service';
// ----------------------------------------------------------------------

View File

@@ -0,0 +1,81 @@
<!-- AI: please maintain same format and level of detail when edit this file -->
# GUIDELINE
- User Management API endpoint for managing user accounts
- Handles user CRUD operations, role management and profile updates
- Follows Next.js API route conventions
## `route.ts`
Main user route handler with HTTP methods:
- `GET` - Get current user details
- `POST` - Create new user account
- `PUT` - Update user profile
- `DELETE` - Delete user account
## Sub-routes
### `changeToAdmin/route.ts`
Changes user role to admin
### `changeToUser/route.ts`
Changes user role to regular user
### `checkAdmin/route.ts`
Verifies if user has admin privileges
### `createUser/route.ts`
Creates new user account with required details
### `deleteUser/route.ts`
Deletes user account by ID
### `details/route.ts`
Gets detailed user information
### `list/route.ts`
Lists all users with pagination support
### `saveUser/route.ts`
Saves user profile changes
### `search/route.ts`
Searches users by name or email
### `image/upload/route.ts`
Handles user profile picture uploads
## `test.http`
Contains test requests for:
- User authentication
- Role changes (admin/user)
- Profile updates
- Account creation/deletion
- User listing/searching
- Image uploads
## Related Services
`../../services/user.service.ts` (assumed) would handle:
`createUser` - Register new user account
`updateUser` - Update user profile
`deleteUser` - Remove user account
`listUsers` - Get paginated user list
`searchUsers` - Search users by criteria
`changeUserRole` - Modify user permissions
`uploadUserImage` - Handle profile picture uploads

View File

@@ -1,13 +1,13 @@
Hi,
i copied from
`03_source/cms_backend/src/app/services/party-event.service.ts`
`03_source/cms_backend/src/app/services/user.service.ts`
to
`03_source/cms_backend/src/app/services/party-order.service.ts`
`03_source/cms_backend/src/app/services/party-user.service.ts`
with knowledge in `schema.prisma` file, and reference to the sibling files in same folder
i want you to update `party-order.service.ts` content to handle party order (the purchase order of the party)
please use the model `PartyOrderItem` to handle it.
i want you to update `party-user.service.ts` content to handle `party user` (the user joining party)
please use the model `PartyUser` to handle it.
thanks.

View File

@@ -0,0 +1,76 @@
// src/app/services/user.service.ts
//
// PURPOSE:
// - Handle User Record CRUD operations
//
// RULES:
// - Follow Prisma best practices for database operations
// - Validate input data before processing
//
import type { User, PartyUser } from '@prisma/client';
import prisma from '../lib/prisma';
type CreateUser = {
email: string;
// name?: string;
// password: string;
// role?: Role;
// isEmailVerified?: boolean;
// admin?: boolean;
};
type UpdateUser = {
email?: string;
// name?: string;
// password?: string;
// role?: Role;
// isEmailVerified?: boolean;
isAdmin?: boolean;
};
async function listPartyUsers(): Promise<PartyUser[]> {
return prisma.partyUser.findMany({});
}
async function getPartyUser(partyUserId: string): Promise<PartyUser | null> {
return prisma.partyUser.findUnique({
where: { id: partyUserId },
// include: { reviews: true },
});
}
async function getUserById(id: string): Promise<User | null> {
return prisma.user.findFirst({ where: { id } });
}
async function createPartyUser(partyUserData: any): Promise<PartyUser> {
return prisma.partyUser.create({ data: partyUserData });
}
async function updatePartyUser(partyUserId: string, partyUserData: any): Promise<PartyUser | null> {
return prisma.partyUser.update({
where: { id: partyUserId },
data: partyUserData,
});
}
async function deletePartyUser(partyUserId: string): Promise<PartyUser | null> {
return prisma.partyUser.delete({
where: { id: partyUserId },
});
}
export {
getUserById,
getPartyUser,
listPartyUsers,
createPartyUser,
updatePartyUser,
deletePartyUser,
//
type CreateUser,
type UpdateUser,
//
};