From 549da39473acc4f941247df4d8e3af0efa0739a3 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 22 Jul 2026 11:52:10 +0330 Subject: [PATCH] add group to user --- d-menu.code-workspace | 11 +++++ .../users/controllers/users.controller.ts | 11 +++++ src/modules/users/dto/create-user.dto.ts | 13 +++++- src/modules/users/dto/update-user.dto.ts | 13 +++++- .../users/providers/user-group.service.ts | 38 ++++++++++++++++++ src/modules/users/providers/user.service.ts | 40 +++++++++++++++---- 6 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 d-menu.code-workspace diff --git a/d-menu.code-workspace b/d-menu.code-workspace new file mode 100644 index 0000000..f1e1fc0 --- /dev/null +++ b/d-menu.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../dmenu-admin" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/modules/users/controllers/users.controller.ts b/src/modules/users/controllers/users.controller.ts index c14b509..e8b81b8 100644 --- a/src/modules/users/controllers/users.controller.ts +++ b/src/modules/users/controllers/users.controller.ts @@ -215,6 +215,17 @@ export class UsersController { return this.userService.adminUpdateUser(userId, restId, dto); } + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @ApiHeader(API_HEADER_SLUG) + @ApiOperation({ summary: 'Get user groups for a customer (admin)' }) + @ApiParam({ name: 'userId', description: 'User ID' }) + @Get('admin/users/:userId/groups') + async adminGetUserGroups(@Param('userId') userId: string, @RestId() restId: string) { + return this.userService.getUserGroups(userId, restId); + } + @UseGuards(AdminAuthGuard) @ApiBearerAuth() @Permissions(Permission.MANAGE_USERS) diff --git a/src/modules/users/dto/create-user.dto.ts b/src/modules/users/dto/create-user.dto.ts index 273f1f6..0276b85 100644 --- a/src/modules/users/dto/create-user.dto.ts +++ b/src/modules/users/dto/create-user.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsBoolean, IsMobilePhone, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsArray, IsBoolean, IsMobilePhone, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateUserDto { @ApiProperty({ description: "User's phone number", example: '09121234567' }) @@ -37,4 +37,15 @@ export class CreateUserDto { @IsOptional() @IsString() avatarUrl?: string; + + @ApiPropertyOptional({ + description: 'User group IDs to assign the customer to', + type: [String], + example: ['01HXABCDEFGHIJKLMNOPQRSTUV'], + }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + groupIds?: string[]; } diff --git a/src/modules/users/dto/update-user.dto.ts b/src/modules/users/dto/update-user.dto.ts index 533545c..a779a11 100644 --- a/src/modules/users/dto/update-user.dto.ts +++ b/src/modules/users/dto/update-user.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsOptional, IsBoolean } from 'class-validator'; +import { IsString, IsOptional, IsBoolean, IsArray, IsNotEmpty } from 'class-validator'; import { ApiPropertyOptional } from '@nestjs/swagger'; export class UpdateUserDto { @@ -32,4 +32,15 @@ export class UpdateUserDto { @IsOptional() @IsString() avatarUrl?: string; + + @ApiPropertyOptional({ + description: 'User group IDs to assign the customer to (replaces current groups)', + type: [String], + example: ['01HXABCDEFGHIJKLMNOPQRSTUV'], + }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + groupIds?: string[]; } diff --git a/src/modules/users/providers/user-group.service.ts b/src/modules/users/providers/user-group.service.ts index 3734d5e..906f981 100644 --- a/src/modules/users/providers/user-group.service.ts +++ b/src/modules/users/providers/user-group.service.ts @@ -138,4 +138,42 @@ export class UserGroupService { group.count = Math.max(0, group.count - 1); return group; } + + async assignUserToGroups(restId: string, userId: string, groupIds: string[]): Promise { + const uniqueGroupIds = [...new Set(groupIds)]; + for (const groupId of uniqueGroupIds) { + await this.addUsers(restId, groupId, { userIds: [userId] }); + } + } + + async getGroupsForUser(restId: string, userId: string): Promise { + const links = await this.userGroupUserRepository.find( + { + user: { id: userId }, + userGroup: { restaurant: { id: restId }, deletedAt: null }, + }, + { populate: ['userGroup'], orderBy: { createdAt: 'asc' } }, + ); + + return links.map(link => link.userGroup); + } + + async syncUserGroups(restId: string, userId: string, groupIds: string[]): Promise { + const targetGroupIds = [...new Set(groupIds)]; + const currentGroups = await this.getGroupsForUser(restId, userId); + const currentGroupIds = new Set(currentGroups.map(group => group.id)); + const targetGroupIdsSet = new Set(targetGroupIds); + + for (const group of currentGroups) { + if (!targetGroupIdsSet.has(group.id)) { + await this.removeUser(restId, group.id, userId); + } + } + + for (const groupId of targetGroupIds) { + if (!currentGroupIds.has(groupId)) { + await this.addUsers(restId, groupId, { userIds: [userId] }); + } + } + } } diff --git a/src/modules/users/providers/user.service.ts b/src/modules/users/providers/user.service.ts index fe6019a..4eca791 100644 --- a/src/modules/users/providers/user.service.ts +++ b/src/modules/users/providers/user.service.ts @@ -25,6 +25,7 @@ import { UserRestaurant } from '../entities/user-restuarant.entity'; import { ImportUsersResult } from '../interfaces/import-users.interface'; import { assertExcelMimeType, parseUsersExcel } from '../utils/import-users-excel.util'; import { AuthMessage, UserImportMessage } from 'src/common/enums/message.enum'; +import { UserGroupService } from './user-group.service'; @Injectable() export class UserService { @@ -35,6 +36,7 @@ export class UserService { private readonly walletService: WalletService, private readonly em: EntityManager, private readonly userRestaurantRepository: UserRestaurantRepository, + private readonly userGroupService: UserGroupService, ) { } async findOrCreateByPhone(phone: string): Promise { @@ -189,6 +191,10 @@ export class UserService { this.em.persist(userRestaurant); await this.em.flush(); + if (dto.groupIds?.length) { + await this.userGroupService.assignUserToGroups(restId, user.id, dto.groupIds); + } + await this.em.populate(userRestaurant, ['user']); return userRestaurant; } @@ -202,7 +208,26 @@ export class UserService { throw new NotFoundException(`User with ID ${userId} not found.`); } - return this.updateUser(userId, restId, dto); + const { groupIds, ...userDto } = dto; + const user = await this.updateUser(userId, restId, userDto); + + if (groupIds !== undefined) { + await this.userGroupService.syncUserGroups(restId, userId, groupIds); + } + + return user; + } + + async getUserGroups(userId: string, restId: string) { + const userRestaurant = await this.userRestaurantRepository.findOne({ + user: { id: userId }, + restaurant: { id: restId }, + }); + if (!userRestaurant) { + throw new NotFoundException(`User with ID ${userId} not found.`); + } + + return this.userGroupService.getGroupsForUser(restId, userId); } async updateUser(userId: string, restId: string, dto: UpdateUserDto): Promise { @@ -211,15 +236,16 @@ export class UserService { throw new NotFoundException(`User with ID ${userId} not found.`); } + const { groupIds: _groupIds, ...userFields } = dto; + // Normalize date strings into Date objects if provided - const assignData: Partial = { ...dto } as Partial; - if (dto.birthDate) { - assignData.birthDate = new Date(dto.birthDate); + const assignData: Partial = { ...userFields } as Partial; + if (userFields.birthDate) { + assignData.birthDate = new Date(userFields.birthDate); } - if (dto.marriageDate) { - assignData.marriageDate = new Date(dto.marriageDate); + if (userFields.marriageDate) { + assignData.marriageDate = new Date(userFields.marriageDate); } - // get restuarant this.em.assign(user, assignData); await this.em.flush();