From 91b67e03779f599618a1d38de80a371105c1213f Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 1 Jul 2026 15:42:53 +0330 Subject: [PATCH] users group --- ...on20260701160000_addUserGroupUsersTable.ts | 40 ++++++++++ src/common/enums/message.enum.ts | 4 + .../controllers/user-groups.controller.ts | 41 ++++++++++ .../users/dto/add-users-to-group.dto.ts | 15 ++++ .../users/entities/user-group-user.entity.ts | 16 ++++ .../users/entities/user-group.entity.ts | 6 +- .../users/providers/user-group.service.ts | 79 ++++++++++++++++++- .../user-group-user.repository.ts | 10 +++ src/modules/users/user.module.ts | 6 +- 9 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 database/migrations/Migration20260701160000_addUserGroupUsersTable.ts create mode 100644 src/modules/users/dto/add-users-to-group.dto.ts create mode 100644 src/modules/users/entities/user-group-user.entity.ts create mode 100644 src/modules/users/repositories/user-group-user.repository.ts diff --git a/database/migrations/Migration20260701160000_addUserGroupUsersTable.ts b/database/migrations/Migration20260701160000_addUserGroupUsersTable.ts new file mode 100644 index 0000000..635341d --- /dev/null +++ b/database/migrations/Migration20260701160000_addUserGroupUsersTable.ts @@ -0,0 +1,40 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260701160000_addUserGroupUsersTable extends Migration { + + override async up(): Promise { + this.addSql(` + create table "user_group_users" ( + "id" char(26) not null, + "created_at" timestamptz not null default now(), + "updated_at" timestamptz not null default now(), + "deleted_at" timestamptz null, + "user_id" char(26) not null, + "user_group_id" char(26) not null, + constraint "user_group_users_pkey" primary key ("id") + ); + `); + this.addSql(`create index "user_group_users_created_at_index" on "user_group_users" ("created_at");`); + this.addSql(`create index "user_group_users_deleted_at_index" on "user_group_users" ("deleted_at");`); + this.addSql(`create index "user_group_users_user_group_id_index" on "user_group_users" ("user_group_id");`); + this.addSql(`create index "user_group_users_user_id_index" on "user_group_users" ("user_id");`); + this.addSql(`alter table "user_group_users" add constraint "user_group_users_user_id_user_group_id_unique" unique ("user_id", "user_group_id");`); + this.addSql(` + alter table "user_group_users" + add constraint "user_group_users_user_id_foreign" + foreign key ("user_id") references "users" ("id") on update cascade; + `); + this.addSql(` + alter table "user_group_users" + add constraint "user_group_users_user_group_id_foreign" + foreign key ("user_group_id") references "user_groups" ("id") on update cascade; + `); + } + + override async down(): Promise { + this.addSql(`alter table "user_group_users" drop constraint if exists "user_group_users_user_group_id_foreign";`); + this.addSql(`alter table "user_group_users" drop constraint if exists "user_group_users_user_id_foreign";`); + this.addSql(`drop table if exists "user_group_users" cascade;`); + } + +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 6a78f8c..913a5d6 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -769,6 +769,10 @@ export const enum SliderMessage { export const enum UserGroupMessage { NOT_FOUND = 'گروه کاربری یافت نشد', + USER_NOT_IN_RESTAURANT = 'کاربر به این رستوران تعلق ندارد', + USER_NOT_IN_GROUP = 'کاربر در این گروه یافت نشد', + USERS_ADDED = 'کاربران با موفقیت به گروه اضافه شدند', + USER_REMOVED = 'کاربر با موفقیت از گروه حذف شد', } export const enum InventoryMessage { diff --git a/src/modules/users/controllers/user-groups.controller.ts b/src/modules/users/controllers/user-groups.controller.ts index c6d3a7f..093bd1a 100644 --- a/src/modules/users/controllers/user-groups.controller.ts +++ b/src/modules/users/controllers/user-groups.controller.ts @@ -9,6 +9,7 @@ import { import { UserGroupService } from '../providers/user-group.service'; import { CreateUserGroupDto } from '../dto/create-user-group.dto'; import { UpdateUserGroupDto } from '../dto/update-user-group.dto'; +import { AddUsersToGroupDto } from '../dto/add-users-to-group.dto'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; import { RestId } from 'src/common/decorators/rest-id.decorator'; import { Permission } from 'src/common/enums/permission.enum'; @@ -72,4 +73,44 @@ export class UserGroupsController { remove(@Param('groupId') groupId: string, @RestId() restId: string) { return this.userGroupService.remove(restId, groupId); } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @Get('admin/user-groups/:groupId/users') + @ApiOperation({ summary: 'Get all users in a user group' }) + @ApiParam({ name: 'groupId', description: 'User group ID' }) + getUsers(@Param('groupId') groupId: string, @RestId() restId: string) { + return this.userGroupService.getUsers(restId, groupId); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @Post('admin/user-groups/:groupId/users') + @ApiOperation({ summary: 'Add users to a user group' }) + @ApiParam({ name: 'groupId', description: 'User group ID' }) + @ApiBody({ type: AddUsersToGroupDto }) + addUsers( + @Param('groupId') groupId: string, + @Body() dto: AddUsersToGroupDto, + @RestId() restId: string, + ) { + return this.userGroupService.addUsers(restId, groupId, dto); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @Delete('admin/user-groups/:groupId/users/:userId') + @ApiOperation({ summary: 'Remove a user from a user group' }) + @ApiParam({ name: 'groupId', description: 'User group ID' }) + @ApiParam({ name: 'userId', description: 'User ID' }) + removeUser( + @Param('groupId') groupId: string, + @Param('userId') userId: string, + @RestId() restId: string, + ) { + return this.userGroupService.removeUser(restId, groupId, userId); + } } diff --git a/src/modules/users/dto/add-users-to-group.dto.ts b/src/modules/users/dto/add-users-to-group.dto.ts new file mode 100644 index 0000000..00c83b5 --- /dev/null +++ b/src/modules/users/dto/add-users-to-group.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ArrayMinSize, IsArray, IsNotEmpty, IsString } from 'class-validator'; + +export class AddUsersToGroupDto { + @ApiProperty({ + example: ['01HXABCDEFGHIJKLMNOPQRSTUV', '01HXABCDEFGHIJKLMNOPQRSTUW'], + description: 'User IDs to add to the group', + type: [String], + }) + @IsArray() + @ArrayMinSize(1) + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + userIds!: string[]; +} diff --git a/src/modules/users/entities/user-group-user.entity.ts b/src/modules/users/entities/user-group-user.entity.ts new file mode 100644 index 0000000..86fb3da --- /dev/null +++ b/src/modules/users/entities/user-group-user.entity.ts @@ -0,0 +1,16 @@ +import { Entity, Index, ManyToOne, Unique } from '@mikro-orm/core'; +import { BaseEntity } from '../../../common/entities/base.entity'; +import { User } from './user.entity'; +import { UserGroup } from './user-group.entity'; + +@Entity({ tableName: 'user_group_users' }) +@Index({ properties: ['userGroup'] }) +@Index({ properties: ['user'] }) +@Unique({ properties: ['user', 'userGroup'] }) +export class UserGroupUser extends BaseEntity { + @ManyToOne(() => User) + user!: User; + + @ManyToOne(() => UserGroup) + userGroup!: UserGroup; +} diff --git a/src/modules/users/entities/user-group.entity.ts b/src/modules/users/entities/user-group.entity.ts index 3b649a9..d835c33 100644 --- a/src/modules/users/entities/user-group.entity.ts +++ b/src/modules/users/entities/user-group.entity.ts @@ -1,6 +1,7 @@ -import { Entity, Index, ManyToOne, Property, Unique } from '@mikro-orm/core'; +import { Collection, Entity, Index, ManyToOne, OneToMany, Property, Unique } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; +import { UserGroupUser } from './user-group-user.entity'; @Entity({ tableName: 'user_groups' }) @Index({ properties: ['restaurant'] }) @@ -14,4 +15,7 @@ export class UserGroup extends BaseEntity { @Property({ type: 'integer', default: 0 }) count: number = 0; + + @OneToMany(() => UserGroupUser, member => member.userGroup) + members = new Collection(this); } diff --git a/src/modules/users/providers/user-group.service.ts b/src/modules/users/providers/user-group.service.ts index f357405..3734d5e 100644 --- a/src/modules/users/providers/user-group.service.ts +++ b/src/modules/users/providers/user-group.service.ts @@ -1,16 +1,22 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { EntityManager, RequiredEntityData } from '@mikro-orm/postgresql'; import { UserGroup } from '../entities/user-group.entity'; import { UserGroupRepository } from '../repositories/user-group.repository'; +import { UserGroupUserRepository } from '../repositories/user-group-user.repository'; +import { UserRestaurantRepository } from '../repositories/user-restuarant.repository'; import { CreateUserGroupDto } from '../dto/create-user-group.dto'; import { UpdateUserGroupDto } from '../dto/update-user-group.dto'; +import { AddUsersToGroupDto } from '../dto/add-users-to-group.dto'; import { UserGroupMessage } from 'src/common/enums/message.enum'; import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service'; +import { User } from '../entities/user.entity'; @Injectable() export class UserGroupService { constructor( private readonly userGroupRepository: UserGroupRepository, + private readonly userGroupUserRepository: UserGroupUserRepository, + private readonly userRestaurantRepository: UserRestaurantRepository, private readonly restService: RestaurantsService, private readonly em: EntityManager, ) {} @@ -61,4 +67,75 @@ export class UserGroupService { group.deletedAt = new Date(); await this.em.persistAndFlush(group); } + + async getUsers(restId: string, groupId: string): Promise { + const group = await this.findOneOrFail(restId, groupId); + const links = await this.userGroupUserRepository.find( + { userGroup: group }, + { populate: ['user'], orderBy: { createdAt: 'asc' } }, + ); + return links.map(link => link.user); + } + + async addUsers(restId: string, groupId: string, dto: AddUsersToGroupDto): Promise { + const group = await this.findOneOrFail(restId, groupId); + const uniqueUserIds = [...new Set(dto.userIds)]; + + const userRestaurants = await this.userRestaurantRepository.find( + { + restaurant: { id: restId }, + user: { id: { $in: uniqueUserIds } }, + }, + { populate: ['user'] }, + ); + + const validUserIds = new Set(userRestaurants.map(link => link.user.id)); + const invalidUserIds = uniqueUserIds.filter(id => !validUserIds.has(id)); + + if (invalidUserIds.length > 0) { + throw new BadRequestException(UserGroupMessage.USER_NOT_IN_RESTAURANT); + } + + let addedCount = 0; + + for (const userId of uniqueUserIds) { + const existing = await this.userGroupUserRepository.findOne({ + userGroup: group, + user: { id: userId }, + }); + + if (existing) { + continue; + } + + this.userGroupUserRepository.create({ + user: this.em.getReference(User, userId), + userGroup: group, + }); + addedCount++; + } + + if (addedCount > 0) { + group.count += addedCount; + } + + await this.em.flush(); + return group; + } + + async removeUser(restId: string, groupId: string, userId: string): Promise { + const group = await this.findOneOrFail(restId, groupId); + const link = await this.userGroupUserRepository.findOne({ + userGroup: group, + user: { id: userId }, + }); + + if (!link) { + throw new NotFoundException(UserGroupMessage.USER_NOT_IN_GROUP); + } + + await this.em.removeAndFlush(link); + group.count = Math.max(0, group.count - 1); + return group; + } } diff --git a/src/modules/users/repositories/user-group-user.repository.ts b/src/modules/users/repositories/user-group-user.repository.ts new file mode 100644 index 0000000..b64bb3c --- /dev/null +++ b/src/modules/users/repositories/user-group-user.repository.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { UserGroupUser } from '../entities/user-group-user.entity'; + +@Injectable() +export class UserGroupUserRepository extends EntityRepository { + constructor(readonly em: EntityManager) { + super(em, UserGroupUser); + } +} diff --git a/src/modules/users/user.module.ts b/src/modules/users/user.module.ts index 31cad81..e50d5c4 100644 --- a/src/modules/users/user.module.ts +++ b/src/modules/users/user.module.ts @@ -16,17 +16,19 @@ import { UserRestaurant } from './entities/user-restuarant.entity'; import { UserRestaurantRepository } from './repositories/user-restuarant.repository'; import { UserRestaurantCrone } from './crone/user-restuarant.crone'; import { UserGroup } from './entities/user-group.entity'; +import { UserGroupUser } from './entities/user-group-user.entity'; import { UserGroupRepository } from './repositories/user-group.repository'; +import { UserGroupUserRepository } from './repositories/user-group-user.repository'; import { UserGroupService } from './providers/user-group.service'; import { UserGroupsController } from './controllers/user-groups.controller'; @Module({ providers: [UserService, WalletService, UserGroupService, UserRepository, WalletTransactionRepository, - PointTransactionRepository, UserRestaurantRepository, UserGroupRepository, UserRestaurantCrone], + PointTransactionRepository, UserRestaurantRepository, UserGroupRepository, UserGroupUserRepository, UserRestaurantCrone], controllers: [UsersController, UserGroupsController], imports: [ - MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction, UserRestaurant, UserGroup]), + MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction, UserRestaurant, UserGroup, UserGroupUser]), JwtModule, RestaurantsModule, ],