diff --git a/database/migrations/Migration20260701150000_addUserGroupsTable.ts b/database/migrations/Migration20260701150000_addUserGroupsTable.ts new file mode 100644 index 0000000..f54de5c --- /dev/null +++ b/database/migrations/Migration20260701150000_addUserGroupsTable.ts @@ -0,0 +1,34 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260701150000_addUserGroupsTable extends Migration { + + override async up(): Promise { + this.addSql(` + create table "user_groups" ( + "id" char(26) not null, + "created_at" timestamptz not null default now(), + "updated_at" timestamptz not null default now(), + "deleted_at" timestamptz null, + "restaurant_id" char(26) not null, + "name" varchar(255) not null, + "count" int not null default 0, + constraint "user_groups_pkey" primary key ("id") + ); + `); + this.addSql(`create index "user_groups_created_at_index" on "user_groups" ("created_at");`); + this.addSql(`create index "user_groups_deleted_at_index" on "user_groups" ("deleted_at");`); + this.addSql(`create index "user_groups_restaurant_id_index" on "user_groups" ("restaurant_id");`); + this.addSql(`create unique index "user_groups_restaurant_id_name_unique" on "user_groups" ("restaurant_id", "name") where "deleted_at" is null;`); + this.addSql(` + alter table "user_groups" + add constraint "user_groups_restaurant_id_foreign" + foreign key ("restaurant_id") references "restaurants" ("id") on update cascade; + `); + } + + override async down(): Promise { + this.addSql(`alter table "user_groups" drop constraint if exists "user_groups_restaurant_id_foreign";`); + this.addSql(`drop table if exists "user_groups" cascade;`); + } + +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 32bb649..6a78f8c 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -767,6 +767,10 @@ export const enum SliderMessage { NOT_FOUND = 'اسلایدر یافت نشد', } +export const enum UserGroupMessage { + NOT_FOUND = 'گروه کاربری یافت نشد', +} + export const enum InventoryMessage { AVAILABLE_STOCK_EXCEEDS_TOTAL = 'موجودی موجود نمی‌تواند از موجودی کل بیشتر باشد', FOOD_NOT_FOUND = 'غذا یافت نشد', diff --git a/src/modules/users/controllers/user-groups.controller.ts b/src/modules/users/controllers/user-groups.controller.ts new file mode 100644 index 0000000..c6d3a7f --- /dev/null +++ b/src/modules/users/controllers/user-groups.controller.ts @@ -0,0 +1,75 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiParam, + ApiBody, + ApiBearerAuth, +} from '@nestjs/swagger'; +import { UserGroupService } from '../providers/user-group.service'; +import { CreateUserGroupDto } from '../dto/create-user-group.dto'; +import { UpdateUserGroupDto } from '../dto/update-user-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'; +import { Permissions } from 'src/common/decorators/permissions.decorator'; + +@ApiTags('User Groups') +@Controller() +export class UserGroupsController { + constructor(private readonly userGroupService: UserGroupService) {} + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @Post('admin/user-groups') + @ApiOperation({ summary: 'Create a user group for the restaurant' }) + @ApiBody({ type: CreateUserGroupDto }) + create(@Body() dto: CreateUserGroupDto, @RestId() restId: string) { + return this.userGroupService.create(restId, dto); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @Get('admin/user-groups') + @ApiOperation({ summary: 'Get all user groups for the restaurant' }) + findAll(@RestId() restId: string) { + return this.userGroupService.findByRestaurantId(restId); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @Get('admin/user-groups/:groupId') + @ApiOperation({ summary: 'Get a user group by ID' }) + @ApiParam({ name: 'groupId', description: 'User group ID' }) + findOne(@Param('groupId') groupId: string, @RestId() restId: string) { + return this.userGroupService.findOneOrFail(restId, groupId); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @Patch('admin/user-groups/:groupId') + @ApiOperation({ summary: 'Update a user group' }) + @ApiParam({ name: 'groupId', description: 'User group ID' }) + @ApiBody({ type: UpdateUserGroupDto }) + update( + @Param('groupId') groupId: string, + @Body() dto: UpdateUserGroupDto, + @RestId() restId: string, + ) { + return this.userGroupService.update(restId, groupId, dto); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @Delete('admin/user-groups/:groupId') + @ApiOperation({ summary: 'Delete a user group' }) + @ApiParam({ name: 'groupId', description: 'User group ID' }) + remove(@Param('groupId') groupId: string, @RestId() restId: string) { + return this.userGroupService.remove(restId, groupId); + } +} diff --git a/src/modules/users/dto/create-user-group.dto.ts b/src/modules/users/dto/create-user-group.dto.ts new file mode 100644 index 0000000..18e5196 --- /dev/null +++ b/src/modules/users/dto/create-user-group.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateUserGroupDto { + @ApiProperty({ example: 'مشتریان VIP', description: 'Group name' }) + @IsNotEmpty() + @IsString() + name!: string; +} diff --git a/src/modules/users/dto/update-user-group.dto.ts b/src/modules/users/dto/update-user-group.dto.ts new file mode 100644 index 0000000..b434e16 --- /dev/null +++ b/src/modules/users/dto/update-user-group.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateUserGroupDto } from './create-user-group.dto'; + +export class UpdateUserGroupDto extends PartialType(CreateUserGroupDto) {} diff --git a/src/modules/users/entities/user-group.entity.ts b/src/modules/users/entities/user-group.entity.ts new file mode 100644 index 0000000..3b649a9 --- /dev/null +++ b/src/modules/users/entities/user-group.entity.ts @@ -0,0 +1,17 @@ +import { Entity, Index, ManyToOne, Property, Unique } from '@mikro-orm/core'; +import { BaseEntity } from '../../../common/entities/base.entity'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; + +@Entity({ tableName: 'user_groups' }) +@Index({ properties: ['restaurant'] }) +@Unique({ properties: ['restaurant', 'name'] }) +export class UserGroup extends BaseEntity { + @ManyToOne(() => Restaurant) + restaurant!: Restaurant; + + @Property() + name!: string; + + @Property({ type: 'integer', default: 0 }) + count: number = 0; +} diff --git a/src/modules/users/providers/user-group.service.ts b/src/modules/users/providers/user-group.service.ts new file mode 100644 index 0000000..f357405 --- /dev/null +++ b/src/modules/users/providers/user-group.service.ts @@ -0,0 +1,64 @@ +import { 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 { CreateUserGroupDto } from '../dto/create-user-group.dto'; +import { UpdateUserGroupDto } from '../dto/update-user-group.dto'; +import { UserGroupMessage } from 'src/common/enums/message.enum'; +import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service'; + +@Injectable() +export class UserGroupService { + constructor( + private readonly userGroupRepository: UserGroupRepository, + private readonly restService: RestaurantsService, + private readonly em: EntityManager, + ) {} + + async create(restId: string, dto: CreateUserGroupDto): Promise { + const restaurant = await this.restService.findOneOrFail(restId); + + const data: RequiredEntityData = { + restaurant, + name: dto.name, + count: 0, + }; + + const group = this.userGroupRepository.create(data); + await this.em.persistAndFlush(group); + return group; + } + + async findByRestaurantId(restId: string): Promise { + return this.userGroupRepository.find( + { restaurant: { id: restId } }, + { orderBy: { createdAt: 'asc' } }, + ); + } + + async findOneOrFail(restId: string, groupId: string): Promise { + const group = await this.userGroupRepository.findOne({ + id: groupId, + restaurant: { id: restId }, + }); + + if (!group) { + throw new NotFoundException(UserGroupMessage.NOT_FOUND); + } + + return group; + } + + async update(restId: string, groupId: string, dto: UpdateUserGroupDto): Promise { + const group = await this.findOneOrFail(restId, groupId); + this.em.assign(group, dto); + await this.em.persistAndFlush(group); + return group; + } + + async remove(restId: string, groupId: string): Promise { + const group = await this.findOneOrFail(restId, groupId); + group.deletedAt = new Date(); + await this.em.persistAndFlush(group); + } +} diff --git a/src/modules/users/repositories/user-group.repository.ts b/src/modules/users/repositories/user-group.repository.ts new file mode 100644 index 0000000..a9f5495 --- /dev/null +++ b/src/modules/users/repositories/user-group.repository.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { UserGroup } from '../entities/user-group.entity'; + +@Injectable() +export class UserGroupRepository extends EntityRepository { + constructor(readonly em: EntityManager) { + super(em, UserGroup); + } +} diff --git a/src/modules/users/user.module.ts b/src/modules/users/user.module.ts index d002b18..31cad81 100644 --- a/src/modules/users/user.module.ts +++ b/src/modules/users/user.module.ts @@ -15,17 +15,21 @@ import { PointTransactionRepository } from './repositories/point-transaction.rep 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 { UserGroupRepository } from './repositories/user-group.repository'; +import { UserGroupService } from './providers/user-group.service'; +import { UserGroupsController } from './controllers/user-groups.controller'; @Module({ - providers: [UserService, WalletService, + providers: [UserService, WalletService, UserGroupService, UserRepository, WalletTransactionRepository, - PointTransactionRepository, UserRestaurantRepository, UserRestaurantCrone], - controllers: [UsersController], + PointTransactionRepository, UserRestaurantRepository, UserGroupRepository, UserRestaurantCrone], + controllers: [UsersController, UserGroupsController], imports: [ - MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction, UserRestaurant]), + MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction, UserRestaurant, UserGroup]), JwtModule, RestaurantsModule, ], - exports: [UserService, WalletService, UserRepository, WalletTransactionRepository, PointTransactionRepository], + exports: [UserService, WalletService, UserGroupService, UserRepository, WalletTransactionRepository, PointTransactionRepository, UserGroupRepository], }) export class UserModule { }