From a09fa468f2ad133e376c199dc0962b0aeb456d7a Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 15 Dec 2025 13:01:49 +0330 Subject: [PATCH] icons --- src/app.module.ts | 2 + src/common/enums/message.enum.ts | 14 +++ .../icons/controllers/icons.controller.ts | 114 ++++++++++++++++++ src/modules/icons/dto/create-group.dto.ts | 16 +++ src/modules/icons/dto/create-icon.dto.ts | 23 ++++ src/modules/icons/dto/update-group.dto.ts | 5 + src/modules/icons/dto/update-icon.dto.ts | 4 + src/modules/icons/entities/group.entity.ts | 16 +++ src/modules/icons/entities/icon.entity.ts | 20 +++ src/modules/icons/icons.module.ts | 18 +++ src/modules/icons/providers/group.service.ts | 59 +++++++++ src/modules/icons/providers/icon.service.ts | 81 +++++++++++++ .../icons/repositories/group.repository.ts | 10 ++ .../icons/repositories/icon.repository.ts | 10 ++ 14 files changed, 392 insertions(+) create mode 100644 src/modules/icons/controllers/icons.controller.ts create mode 100644 src/modules/icons/dto/create-group.dto.ts create mode 100644 src/modules/icons/dto/create-icon.dto.ts create mode 100644 src/modules/icons/dto/update-group.dto.ts create mode 100644 src/modules/icons/dto/update-icon.dto.ts create mode 100644 src/modules/icons/entities/group.entity.ts create mode 100644 src/modules/icons/entities/icon.entity.ts create mode 100644 src/modules/icons/icons.module.ts create mode 100644 src/modules/icons/providers/group.service.ts create mode 100644 src/modules/icons/providers/icon.service.ts create mode 100644 src/modules/icons/repositories/group.repository.ts create mode 100644 src/modules/icons/repositories/icon.repository.ts diff --git a/src/app.module.ts b/src/app.module.ts index 7c23489..a8c183a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,6 +22,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul import { EventEmitterModule } from '@nestjs/event-emitter'; import { PagerModule } from './modules/pager/pager.module'; import { ContactModule } from './modules/contact/contact.module'; +import { IconsModule } from './modules/icons/icons.module'; @Module({ imports: [ @@ -52,6 +53,7 @@ import { ContactModule } from './modules/contact/contact.module'; EventEmitterModule.forRoot(), PagerModule, ContactModule, + IconsModule, ], controllers: [], providers: [], diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index c9dd793..4134e85 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -649,3 +649,17 @@ export const enum ReviewMessage { COMMENT_UPDATED = 'نظر با موفقیت به‌روزرسانی شد', COMMENT_DELETED = 'نظر با موفقیت حذف شد', } + +export const enum IconMessage { + NOT_FOUND = 'آیکون یافت نشد', + NOT_CREATED = 'ایجاد آیکون با خطا مواجه شد', + NOT_UPDATED = 'به‌روزرسانی آیکون با خطا مواجه شد', + NOT_DELETED = 'حذف آیکون با خطا مواجه شد', +} + +export const enum GroupMessage { + NOT_FOUND = 'گروه آیکون یافت نشد', + NOT_CREATED = 'ایجاد گروه آیکون با خطا مواجه شد', + NOT_UPDATED = 'به‌روزرسانی گروه آیکون با خطا مواجه شد', + NOT_DELETED = 'حذف گروه آیکون با خطا مواجه شد', +} diff --git a/src/modules/icons/controllers/icons.controller.ts b/src/modules/icons/controllers/icons.controller.ts new file mode 100644 index 0000000..6135bf7 --- /dev/null +++ b/src/modules/icons/controllers/icons.controller.ts @@ -0,0 +1,114 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { IconService } from '../providers/icon.service'; +import { GroupService } from '../providers/group.service'; +import { CreateIconDto } from '../dto/create-icon.dto'; +import { UpdateIconDto } from '../dto/update-icon.dto'; +import { CreateGroupDto } from '../dto/create-group.dto'; +import { UpdateGroupDto } from '../dto/update-group.dto'; +import { + ApiTags, + ApiOperation, + ApiCreatedResponse, + ApiOkResponse, + ApiNotFoundResponse, + ApiParam, + ApiBody, + ApiBearerAuth, +} from '@nestjs/swagger'; +import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; +import { UseGuards } from '@nestjs/common'; + +@ApiTags('icons') +@Controller('admin/icons') +@UseGuards(AdminAuthGuard) +@ApiBearerAuth() +export class IconsController { + constructor( + private readonly iconService: IconService, + private readonly groupService: GroupService, + ) {} + + // Icon endpoints + @Post() + @ApiOperation({ summary: 'Create icon' }) + @ApiCreatedResponse({ description: 'Icon created', type: CreateIconDto }) + @ApiBody({ type: CreateIconDto }) + createIcon(@Body() dto: CreateIconDto) { + return this.iconService.create(dto); + } + + @Get() + @ApiOperation({ summary: 'Get all icons' }) + @ApiOkResponse({ description: 'List of all icons' }) + findAllIcons() { + return this.iconService.findAll(); + } + + @Get(':id') + @ApiOperation({ summary: 'Get icon by id' }) + @ApiOkResponse({ description: 'Icon detail', type: CreateIconDto }) + @ApiNotFoundResponse({ description: 'Icon not found' }) + @ApiParam({ name: 'id', required: true, type: String }) + findOneIcon(@Param('id') id: string) { + return this.iconService.findOne(id); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update icon' }) + @ApiParam({ name: 'id' }) + @ApiBody({ type: UpdateIconDto }) + @ApiOkResponse({ description: 'Updated icon', type: UpdateIconDto }) + updateIcon(@Param('id') id: string, @Body() dto: UpdateIconDto) { + return this.iconService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete icon' }) + @ApiParam({ name: 'id' }) + @ApiOkResponse({ description: 'Deleted' }) + removeIcon(@Param('id') id: string) { + return this.iconService.remove(id); + } + + // Group endpoints (must come before :id routes to avoid route conflicts) + @Post('groups') + @ApiOperation({ summary: 'Create icon group' }) + @ApiCreatedResponse({ description: 'Group created', type: CreateGroupDto }) + @ApiBody({ type: CreateGroupDto }) + createGroup(@Body() dto: CreateGroupDto) { + return this.groupService.create(dto); + } + + @Get('groups') + @ApiOperation({ summary: 'Get all icon groups' }) + @ApiOkResponse({ description: 'List of all icon groups' }) + findAllGroups() { + return this.groupService.findAll(); + } + + @Get('groups/:id') + @ApiOperation({ summary: 'Get icon group by id' }) + @ApiOkResponse({ description: 'Group detail', type: CreateGroupDto }) + @ApiNotFoundResponse({ description: 'Group not found' }) + @ApiParam({ name: 'id', required: true, type: String }) + findOneGroup(@Param('id') id: string) { + return this.groupService.findOne(id); + } + + @Patch('groups/:id') + @ApiOperation({ summary: 'Update icon group' }) + @ApiParam({ name: 'id' }) + @ApiBody({ type: UpdateGroupDto }) + @ApiOkResponse({ description: 'Updated group', type: UpdateGroupDto }) + updateGroup(@Param('id') id: string, @Body() dto: UpdateGroupDto) { + return this.groupService.update(id, dto); + } + + @Delete('groups/:id') + @ApiOperation({ summary: 'Delete icon group' }) + @ApiParam({ name: 'id' }) + @ApiOkResponse({ description: 'Deleted' }) + removeGroup(@Param('id') id: string) { + return this.groupService.remove(id); + } +} diff --git a/src/modules/icons/dto/create-group.dto.ts b/src/modules/icons/dto/create-group.dto.ts new file mode 100644 index 0000000..f42df6e --- /dev/null +++ b/src/modules/icons/dto/create-group.dto.ts @@ -0,0 +1,16 @@ +import { IsString, IsOptional, IsBoolean } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class CreateGroupDto { + @IsString() + @ApiProperty({ example: 'Social Media' }) + name!: string; + + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + @ApiPropertyOptional({ example: true }) + isActive?: boolean; +} + diff --git a/src/modules/icons/dto/create-icon.dto.ts b/src/modules/icons/dto/create-icon.dto.ts new file mode 100644 index 0000000..40ca410 --- /dev/null +++ b/src/modules/icons/dto/create-icon.dto.ts @@ -0,0 +1,23 @@ +import { IsString, IsOptional, IsBoolean } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class CreateIconDto { + @IsString() + @ApiProperty({ example: 'Facebook Icon' }) + name!: string; + + @IsString() + @ApiProperty({ example: 'https://example.com/icons/facebook.svg' }) + url!: string; + + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + @ApiPropertyOptional({ example: true }) + isActive?: boolean; + + @IsString() + @ApiProperty({ example: 'group-id-here' }) + groupId!: string; +} diff --git a/src/modules/icons/dto/update-group.dto.ts b/src/modules/icons/dto/update-group.dto.ts new file mode 100644 index 0000000..8174604 --- /dev/null +++ b/src/modules/icons/dto/update-group.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateGroupDto } from './create-group.dto'; + +export class UpdateGroupDto extends PartialType(CreateGroupDto) {} + diff --git a/src/modules/icons/dto/update-icon.dto.ts b/src/modules/icons/dto/update-icon.dto.ts new file mode 100644 index 0000000..6ad1b53 --- /dev/null +++ b/src/modules/icons/dto/update-icon.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateIconDto } from './create-icon.dto'; + +export class UpdateIconDto extends PartialType(CreateIconDto) {} diff --git a/src/modules/icons/entities/group.entity.ts b/src/modules/icons/entities/group.entity.ts new file mode 100644 index 0000000..0cdcf3f --- /dev/null +++ b/src/modules/icons/entities/group.entity.ts @@ -0,0 +1,16 @@ +import { Entity, Index, Property, Collection, OneToMany } from '@mikro-orm/core'; +import { BaseEntity } from '../../../common/entities/base.entity'; +import { Icon } from './icon.entity'; + +@Entity({ tableName: 'icon_groups' }) +@Index({ properties: ['isActive'] }) +export class Group extends BaseEntity { + @Property() + name!: string; + + @Property({ default: true }) + isActive: boolean = true; + + @OneToMany(() => Icon, icon => icon.group) + icons = new Collection(this); +} diff --git a/src/modules/icons/entities/icon.entity.ts b/src/modules/icons/entities/icon.entity.ts new file mode 100644 index 0000000..ea19569 --- /dev/null +++ b/src/modules/icons/entities/icon.entity.ts @@ -0,0 +1,20 @@ +import { Entity, Index, Property, ManyToOne } from '@mikro-orm/core'; +import { BaseEntity } from '../../../common/entities/base.entity'; +import { Group } from './group.entity'; + +@Entity({ tableName: 'icons' }) +@Index({ properties: ['isActive'] }) +@Index({ properties: ['group', 'isActive'] }) +export class Icon extends BaseEntity { + @Property() + name!: string; + + @Property() + url!: string; + + @Property({ default: true }) + isActive: boolean = true; + + @ManyToOne(() => Group) + group!: Group; +} diff --git a/src/modules/icons/icons.module.ts b/src/modules/icons/icons.module.ts new file mode 100644 index 0000000..2e8931a --- /dev/null +++ b/src/modules/icons/icons.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { Icon } from './entities/icon.entity'; +import { Group } from './entities/group.entity'; +import { IconService } from './providers/icon.service'; +import { GroupService } from './providers/group.service'; +import { IconRepository } from './repositories/icon.repository'; +import { GroupRepository } from './repositories/group.repository'; +import { IconsController } from './controllers/icons.controller'; +import { JwtModule } from '@nestjs/jwt'; + +@Module({ + imports: [MikroOrmModule.forFeature([Icon, Group]), JwtModule], + controllers: [IconsController], + providers: [IconService, GroupService, IconRepository, GroupRepository], + exports: [IconRepository, GroupRepository, IconService, GroupService], +}) +export class IconsModule {} diff --git a/src/modules/icons/providers/group.service.ts b/src/modules/icons/providers/group.service.ts new file mode 100644 index 0000000..b9983b6 --- /dev/null +++ b/src/modules/icons/providers/group.service.ts @@ -0,0 +1,59 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CreateGroupDto } from '../dto/create-group.dto'; +import { UpdateGroupDto } from '../dto/update-group.dto'; +import { GroupRepository } from '../repositories/group.repository'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { RequiredEntityData } from '@mikro-orm/core'; +import { Group } from '../entities/group.entity'; +import { GroupMessage } from 'src/common/enums/message.enum'; + +@Injectable() +export class GroupService { + constructor( + private readonly groupRepository: GroupRepository, + private readonly em: EntityManager, + ) {} + + async create(dto: CreateGroupDto): Promise { + const data: RequiredEntityData = { + name: dto.name, + isActive: dto.isActive ?? true, + }; + + const group = this.groupRepository.create(data); + await this.em.persistAndFlush(group); + return group; + } + + async findAll(): Promise { + return this.groupRepository.find({}, { populate: ['icons'] }); + } + + async findOne(id: string): Promise { + const group = await this.groupRepository.findOne({ id }, { populate: ['icons'] }); + if (!group) { + throw new NotFoundException(GroupMessage.NOT_FOUND); + } + return group; + } + + async update(id: string, dto: UpdateGroupDto): Promise { + const group = await this.groupRepository.findOne({ id }); + if (!group) { + throw new NotFoundException(GroupMessage.NOT_FOUND); + } + this.em.assign(group, dto); + await this.em.persistAndFlush(group); + return group; + } + + async remove(id: string): Promise { + const group = await this.groupRepository.findOne({ id }); + if (!group) { + throw new NotFoundException(GroupMessage.NOT_FOUND); + } + group.deletedAt = new Date(); + await this.em.persistAndFlush(group); + } +} + diff --git a/src/modules/icons/providers/icon.service.ts b/src/modules/icons/providers/icon.service.ts new file mode 100644 index 0000000..a2c66d2 --- /dev/null +++ b/src/modules/icons/providers/icon.service.ts @@ -0,0 +1,81 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CreateIconDto } from '../dto/create-icon.dto'; +import { UpdateIconDto } from '../dto/update-icon.dto'; +import { IconRepository } from '../repositories/icon.repository'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { RequiredEntityData } from '@mikro-orm/core'; +import { Icon } from '../entities/icon.entity'; +import { IconMessage, GroupMessage } from 'src/common/enums/message.enum'; +import { Group } from '../entities/group.entity'; + +@Injectable() +export class IconService { + constructor( + private readonly iconRepository: IconRepository, + private readonly em: EntityManager, + ) {} + + async create(dto: CreateIconDto): Promise { + const group = await this.em.findOne(Group, { id: dto.groupId }); + if (!group) { + throw new NotFoundException(GroupMessage.NOT_FOUND); + } + + const data: RequiredEntityData = { + name: dto.name, + url: dto.url, + isActive: dto.isActive ?? true, + group: group, + }; + + const icon = this.iconRepository.create(data); + await this.em.persistAndFlush(icon); + return icon; + } + + async findAll(): Promise { + return this.iconRepository.find({}, { populate: ['group'] }); + } + + async findOne(id: string): Promise { + const icon = await this.iconRepository.findOne({ id }, { populate: ['group'] }); + if (!icon) { + throw new NotFoundException(IconMessage.NOT_FOUND); + } + return icon; + } + + async update(id: string, dto: UpdateIconDto): Promise { + const icon = await this.iconRepository.findOne({ id }); + if (!icon) { + throw new NotFoundException(IconMessage.NOT_FOUND); + } + + if (dto.groupId) { + const group = await this.em.findOne(Group, { id: dto.groupId }); + if (!group) { + throw new NotFoundException(GroupMessage.NOT_FOUND); + } + icon.group = group; + } + + this.em.assign(icon, { + name: dto.name, + url: dto.url, + isActive: dto.isActive, + }); + + await this.em.persistAndFlush(icon); + return icon; + } + + async remove(id: string): Promise { + const icon = await this.iconRepository.findOne({ id }); + if (!icon) { + throw new NotFoundException(IconMessage.NOT_FOUND); + } + icon.deletedAt = new Date(); + await this.em.persistAndFlush(icon); + } +} + diff --git a/src/modules/icons/repositories/group.repository.ts b/src/modules/icons/repositories/group.repository.ts new file mode 100644 index 0000000..fdc6749 --- /dev/null +++ b/src/modules/icons/repositories/group.repository.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { Group } from '../entities/group.entity'; + +@Injectable() +export class GroupRepository extends EntityRepository { + constructor(readonly em: EntityManager) { + super(em, Group); + } +} diff --git a/src/modules/icons/repositories/icon.repository.ts b/src/modules/icons/repositories/icon.repository.ts new file mode 100644 index 0000000..c748b94 --- /dev/null +++ b/src/modules/icons/repositories/icon.repository.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { Icon } from '../entities/icon.entity'; + +@Injectable() +export class IconRepository extends EntityRepository { + constructor(readonly em: EntityManager) { + super(em, Icon); + } +}