diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9a15ef..b804623 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -142,7 +142,7 @@ model CaseStudies { @@map("case_studies") @@schema("cnt") } - + model KlcArchive { id Int @id @default(autoincrement()) title String @map("title") @@ -153,5 +153,5 @@ model KlcArchive { updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") @@map("klc_archive") - @@schema("cnt") + @@schema("cnt") } diff --git a/src/app.module.ts b/src/app.module.ts index 5d9007f..0c02afd 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,12 @@ import { ThrottlerModule } from '@nestjs/throttler'; import { PrismaModule } from './common/database/prisma.module'; import { AuthModule } from './modules/auth/auth.module'; import { BlogsModule } from './modules/blog/blogs.module'; +import { FaqModule } from './modules/faq/faq.module'; +import { PodcastsModule } from './modules/podcasts/podcasts.module'; +import { CaseStudiesModule } from './modules/case-studies/case-studies.module'; +import { KlcArchiveModule } from './modules/klc-archive/klc-archive.module'; +import { ReadingMaterialsModule } from './modules/reading-materials/reading-materials.module'; +import { TrainingMaterialsModule } from './modules/training-materials/training-materials.module'; import { WebcastsModule } from './modules/webcast/webcasts.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @@ -30,6 +36,12 @@ import { AppService } from './app.service'; // Feature modules AuthModule, BlogsModule, + FaqModule, + PodcastsModule, + CaseStudiesModule, + KlcArchiveModule, + ReadingMaterialsModule, + TrainingMaterialsModule, WebcastsModule, ], controllers: [AppController], diff --git a/src/modules/case-studies/case-studies.module.ts b/src/modules/case-studies/case-studies.module.ts new file mode 100644 index 0000000..9ca95a5 --- /dev/null +++ b/src/modules/case-studies/case-studies.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { CaseStudiesController } from './controllers/case-studies.controller'; +import { CaseStudiesService } from './services/case-studies.service'; +import { PrismaModule } from '../../common/database/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [CaseStudiesController], + providers: [CaseStudiesService], + exports: [CaseStudiesService], +}) +export class CaseStudiesModule {} diff --git a/src/modules/case-studies/controllers/case-studies.controller.ts b/src/modules/case-studies/controllers/case-studies.controller.ts new file mode 100644 index 0000000..d8d2663 --- /dev/null +++ b/src/modules/case-studies/controllers/case-studies.controller.ts @@ -0,0 +1,110 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { CaseStudiesService } from '../services/case-studies.service'; +import { CreateCaseStudyDto } from '../dto/create-case-study.dto'; +import { UpdateCaseStudyDto } from '../dto/update-case-study.dto'; +import { CaseStudyResponseDto } from '../dto/case-study-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; + +@ApiTags('case-studies') +@Controller('case-studies') +export class CaseStudiesController { + constructor(private readonly caseStudiesService: CaseStudiesService) {} + + @Post() + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new case study' }) + @ApiResponse({ status: 201, description: 'Case study created successfully', type: CaseStudyResponseDto }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async create(@Body() createCaseStudyDto: CreateCaseStudyDto): Promise { + return this.caseStudiesService.create(createCaseStudyDto); + } + + @Get() + @ApiOperation({ summary: 'Get all case studies with pagination' }) + @ApiResponse({ status: 200, description: 'Case studies retrieved successfully' }) + async findAll(@Query() paginationDto: PaginationDto) { + return this.caseStudiesService.findAll(paginationDto); + } + + @Get('search/title') + @ApiOperation({ summary: 'Search case studies by title' }) + @ApiResponse({ status: 200, description: 'Case studies retrieved successfully' }) + async searchByTitle( + @Query('q') title: string, + @Query() paginationDto: PaginationDto, + ) { + return this.caseStudiesService.searchByTitle(title, paginationDto); + } + + @Get('search/description') + @ApiOperation({ summary: 'Search case studies by description' }) + @ApiResponse({ status: 200, description: 'Case studies retrieved successfully' }) + async searchByDescription( + @Query('q') description: string, + @Query() paginationDto: PaginationDto, + ) { + return this.caseStudiesService.searchByDescription(description, paginationDto); + } + + @Get('tag/:tag') + @ApiOperation({ summary: 'Get case studies by tag' }) + @ApiResponse({ status: 200, description: 'Case studies retrieved successfully' }) + async findByTag( + @Param('tag') tag: string, + @Query() paginationDto: PaginationDto, + ) { + return this.caseStudiesService.findByTag(tag, paginationDto); + } + + @Get(':id') + @ApiOperation({ summary: 'Get case study by ID' }) + @ApiResponse({ status: 200, description: 'Case study retrieved successfully', type: CaseStudyResponseDto }) + @ApiResponse({ status: 404, description: 'Case study not found' }) + async findOne(@Param('id', ParseIntPipe) id: number): Promise { + return this.caseStudiesService.findOne(id); + } + + @Patch(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Update case study' }) + @ApiResponse({ status: 200, description: 'Case study updated successfully', type: CaseStudyResponseDto }) + @ApiResponse({ status: 404, description: 'Case study not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async update( + @Param('id', ParseIntPipe) id: number, + @Body() updateCaseStudyDto: UpdateCaseStudyDto, + ): Promise { + return this.caseStudiesService.update(id, updateCaseStudyDto); + } + + @Delete(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Delete case study (soft delete)' }) + @ApiResponse({ status: 200, description: 'Case study deleted successfully' }) + @ApiResponse({ status: 404, description: 'Case study not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> { + await this.caseStudiesService.remove(id); + return { message: 'Case study deleted successfully' }; + } +} diff --git a/src/modules/case-studies/dto/case-study-response.dto.ts b/src/modules/case-studies/dto/case-study-response.dto.ts new file mode 100644 index 0000000..410e734 --- /dev/null +++ b/src/modules/case-studies/dto/case-study-response.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CaseStudyResponseDto { + @ApiProperty({ description: 'Case study ID' }) + id: number; + + @ApiProperty({ description: 'Case study title' }) + title: string; + + @ApiProperty({ description: 'Case study description' }) + description: string; + + @ApiProperty({ description: 'Case study file URL' }) + fileUrl: string; + + @ApiProperty({ description: 'Case study tags', type: [String] }) + tags: string[]; + + @ApiProperty({ description: 'Creation date' }) + createdAt: Date; + + @ApiProperty({ description: 'Last update date' }) + updatedAt: Date; +} diff --git a/src/modules/case-studies/dto/create-case-study.dto.ts b/src/modules/case-studies/dto/create-case-study.dto.ts new file mode 100644 index 0000000..5d83f56 --- /dev/null +++ b/src/modules/case-studies/dto/create-case-study.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsOptional, IsArray, IsUrl } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateCaseStudyDto { + @ApiProperty({ description: 'Case study title' }) + @IsString() + title: string; + + @ApiProperty({ description: 'Case study description' }) + @IsString() + description: string; + + @ApiProperty({ description: 'Case study file URL' }) + @IsUrl() + fileUrl: string; + + @ApiPropertyOptional({ description: 'Case study tags', type: [String] }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tags?: string[]; +} diff --git a/src/modules/case-studies/dto/update-case-study.dto.ts b/src/modules/case-studies/dto/update-case-study.dto.ts new file mode 100644 index 0000000..e9f15d5 --- /dev/null +++ b/src/modules/case-studies/dto/update-case-study.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateCaseStudyDto } from './create-case-study.dto'; + +export class UpdateCaseStudyDto extends PartialType(CreateCaseStudyDto) {} diff --git a/src/modules/case-studies/services/case-studies.service.ts b/src/modules/case-studies/services/case-studies.service.ts new file mode 100644 index 0000000..fb3e92b --- /dev/null +++ b/src/modules/case-studies/services/case-studies.service.ts @@ -0,0 +1,223 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { CreateCaseStudyDto } from '../dto/create-case-study.dto'; +import { UpdateCaseStudyDto } from '../dto/update-case-study.dto'; +import { CaseStudyResponseDto } from '../dto/case-study-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; +import { paginate } from '../../../common/utils/pagination.util'; + +@Injectable() +export class CaseStudiesService { + constructor(private readonly prisma: PrismaService) {} + + async create(createCaseStudyDto: CreateCaseStudyDto): Promise { + const caseStudy = await this.prisma.caseStudies.create({ + data: { + ...createCaseStudyDto, + tags: createCaseStudyDto.tags || [], + }, + }); + + return this.mapToResponseDto(caseStudy); + } + + async findAll(paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [caseStudies, total] = await Promise.all([ + this.prisma.caseStudies.findMany({ + where: { + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.caseStudies.count({ + where: { + deletedAt: null, + }, + }), + ]); + + const mappedCaseStudies = caseStudies.map(caseStudy => this.mapToResponseDto(caseStudy)); + + return paginate(mappedCaseStudies, { page, limit }, total); + } + + async findOne(id: number): Promise { + const caseStudy = await this.prisma.caseStudies.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!caseStudy) { + throw new NotFoundException(`Case study with ID ${id} not found`); + } + + return this.mapToResponseDto(caseStudy); + } + + async update(id: number, updateCaseStudyDto: UpdateCaseStudyDto): Promise { + const existingCaseStudy = await this.prisma.caseStudies.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingCaseStudy) { + throw new NotFoundException(`Case study with ID ${id} not found`); + } + + const caseStudy = await this.prisma.caseStudies.update({ + where: { id }, + data: { + ...updateCaseStudyDto, + updatedAt: new Date(), + }, + }); + + return this.mapToResponseDto(caseStudy); + } + + async remove(id: number): Promise { + const existingCaseStudy = await this.prisma.caseStudies.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingCaseStudy) { + throw new NotFoundException(`Case study with ID ${id} not found`); + } + + await this.prisma.caseStudies.update({ + where: { id }, + data: { + deletedAt: new Date(), + }, + }); + } + + async findByTag(tag: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [caseStudies, total] = await Promise.all([ + this.prisma.caseStudies.findMany({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.caseStudies.count({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + }), + ]); + + const mappedCaseStudies = caseStudies.map(caseStudy => this.mapToResponseDto(caseStudy)); + + return paginate(mappedCaseStudies, { page, limit }, total); + } + + async searchByTitle(title: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [caseStudies, total] = await Promise.all([ + this.prisma.caseStudies.findMany({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.caseStudies.count({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedCaseStudies = caseStudies.map(caseStudy => this.mapToResponseDto(caseStudy)); + + return paginate(mappedCaseStudies, { page, limit }, total); + } + + async searchByDescription(description: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [caseStudies, total] = await Promise.all([ + this.prisma.caseStudies.findMany({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.caseStudies.count({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedCaseStudies = caseStudies.map(caseStudy => this.mapToResponseDto(caseStudy)); + + return paginate(mappedCaseStudies, { page, limit }, total); + } + + private mapToResponseDto(caseStudy: any): CaseStudyResponseDto { + return { + id: caseStudy.id, + title: caseStudy.title, + description: caseStudy.description, + fileUrl: caseStudy.fileUrl, + tags: caseStudy.tags, + createdAt: caseStudy.createdAt, + updatedAt: caseStudy.updatedAt, + }; + } +} diff --git a/src/modules/faq/controllers/faq.controller.ts b/src/modules/faq/controllers/faq.controller.ts new file mode 100644 index 0000000..7a70bd0 --- /dev/null +++ b/src/modules/faq/controllers/faq.controller.ts @@ -0,0 +1,114 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { FaqService } from '../services/faq.service'; +import { CreateFaqDto } from '../dto/create-faq.dto'; +import { UpdateFaqDto } from '../dto/update-faq.dto'; +import { FaqResponseDto } from '../dto/faq-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; +import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; +import { RolesGuard } from '../../../common/guards/roles.guard'; +import { Roles } from '../../../common/decorators/roles.decorator'; + +@ApiTags('faq') +@Controller('faq') +export class FaqController { + constructor(private readonly faqService: FaqService) {} + + @Post() + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new FAQ' }) + @ApiResponse({ status: 201, description: 'FAQ created successfully', type: FaqResponseDto }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async create(@Body() createFaqDto: CreateFaqDto): Promise { + return this.faqService.create(createFaqDto); + } + + @Get() + @ApiOperation({ summary: 'Get all FAQs with pagination' }) + @ApiResponse({ status: 200, description: 'FAQs retrieved successfully' }) + async findAll(@Query() paginationDto: PaginationDto) { + return this.faqService.findAll(paginationDto); + } + + @Get('category/:category') + @ApiOperation({ summary: 'Get FAQs by category' }) + @ApiResponse({ status: 200, description: 'FAQs retrieved successfully' }) + async findByCategory( + @Param('category') category: string, + @Query() paginationDto: PaginationDto, + ) { + return this.faqService.findByCategory(category, paginationDto); + } + + @Get('tag/:tag') + @ApiOperation({ summary: 'Get FAQs by tag' }) + @ApiResponse({ status: 200, description: 'FAQs retrieved successfully' }) + async findByTag( + @Param('tag') tag: string, + @Query() paginationDto: PaginationDto, + ) { + return this.faqService.findByTag(tag, paginationDto); + } + + @Get('global-tag/:globalTag') + @ApiOperation({ summary: 'Get FAQs by global tag' }) + @ApiResponse({ status: 200, description: 'FAQs retrieved successfully' }) + async findByGlobalTag( + @Param('globalTag') globalTag: string, + @Query() paginationDto: PaginationDto, + ) { + return this.faqService.findByGlobalTag(globalTag, paginationDto); + } + + @Get(':id') + @ApiOperation({ summary: 'Get FAQ by ID' }) + @ApiResponse({ status: 200, description: 'FAQ retrieved successfully', type: FaqResponseDto }) + @ApiResponse({ status: 404, description: 'FAQ not found' }) + async findOne(@Param('id', ParseIntPipe) id: number): Promise { + return this.faqService.findOne(id); + } + + @Patch(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Update FAQ' }) + @ApiResponse({ status: 200, description: 'FAQ updated successfully', type: FaqResponseDto }) + @ApiResponse({ status: 404, description: 'FAQ not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async update( + @Param('id', ParseIntPipe) id: number, + @Body() updateFaqDto: UpdateFaqDto, + ): Promise { + return this.faqService.update(id, updateFaqDto); + } + + @Delete(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Delete FAQ (soft delete)' }) + @ApiResponse({ status: 200, description: 'FAQ deleted successfully' }) + @ApiResponse({ status: 404, description: 'FAQ not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> { + await this.faqService.remove(id); + return { message: 'FAQ deleted successfully' }; + } +} diff --git a/src/modules/faq/dto/create-faq.dto.ts b/src/modules/faq/dto/create-faq.dto.ts new file mode 100644 index 0000000..553dab9 --- /dev/null +++ b/src/modules/faq/dto/create-faq.dto.ts @@ -0,0 +1,29 @@ +import { IsString, IsOptional, IsArray } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateFaqDto { + @ApiProperty({ description: 'FAQ question' }) + @IsString() + question: string; + + @ApiPropertyOptional({ description: 'FAQ category' }) + @IsString() + @IsOptional() + category?: string; + + @ApiProperty({ description: 'FAQ answer' }) + @IsString() + answer: string; + + @ApiPropertyOptional({ description: 'FAQ tags', type: [String] }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tags?: string[]; + + @ApiPropertyOptional({ description: 'Global tags', type: [String] }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + globalTag?: string[]; +} diff --git a/src/modules/faq/dto/faq-response.dto.ts b/src/modules/faq/dto/faq-response.dto.ts new file mode 100644 index 0000000..b306314 --- /dev/null +++ b/src/modules/faq/dto/faq-response.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class FaqResponseDto { + @ApiProperty({ description: 'FAQ ID' }) + id: number; + + @ApiProperty({ description: 'FAQ question' }) + question: string; + + @ApiPropertyOptional({ description: 'FAQ category' }) + category?: string; + + @ApiProperty({ description: 'FAQ answer' }) + answer: string; + + @ApiProperty({ description: 'FAQ tags', type: [String] }) + tags: string[]; + + @ApiProperty({ description: 'Global tags', type: [String] }) + globalTag: string[]; + + @ApiProperty({ description: 'Creation date' }) + createdAt: Date; + + @ApiProperty({ description: 'Last update date' }) + updatedAt: Date; +} diff --git a/src/modules/faq/dto/update-faq.dto.ts b/src/modules/faq/dto/update-faq.dto.ts new file mode 100644 index 0000000..86afe2d --- /dev/null +++ b/src/modules/faq/dto/update-faq.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateFaqDto } from './create-faq.dto'; + +export class UpdateFaqDto extends PartialType(CreateFaqDto) {} diff --git a/src/modules/faq/faq.module.ts b/src/modules/faq/faq.module.ts new file mode 100644 index 0000000..543bb1c --- /dev/null +++ b/src/modules/faq/faq.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { FaqController } from './controllers/faq.controller'; +import { FaqService } from './services/faq.service'; +import { PrismaModule } from '../../common/database/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [FaqController], + providers: [FaqService], + exports: [FaqService], +}) +export class FaqModule {} diff --git a/src/modules/faq/services/faq.service.ts b/src/modules/faq/services/faq.service.ts new file mode 100644 index 0000000..7c3cef7 --- /dev/null +++ b/src/modules/faq/services/faq.service.ts @@ -0,0 +1,217 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { CreateFaqDto } from '../dto/create-faq.dto'; +import { UpdateFaqDto } from '../dto/update-faq.dto'; +import { FaqResponseDto } from '../dto/faq-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; +import { paginate } from '../../../common/utils/pagination.util'; + +@Injectable() +export class FaqService { + constructor(private readonly prisma: PrismaService) {} + + async create(createFaqDto: CreateFaqDto): Promise { + const faq = await this.prisma.fAQ.create({ + data: { + ...createFaqDto, + tags: createFaqDto.tags || [], + globalTag: createFaqDto.globalTag || [], + }, + }); + + return this.mapToResponseDto(faq); + } + + async findAll(paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [faqs, total] = await Promise.all([ + this.prisma.fAQ.findMany({ + where: { + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.fAQ.count({ + where: { + deletedAt: null, + }, + }), + ]); + + const mappedFaqs = faqs.map(faq => this.mapToResponseDto(faq)); + + return paginate(mappedFaqs, { page, limit }, total); + } + + async findOne(id: number): Promise { + const faq = await this.prisma.fAQ.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!faq) { + throw new NotFoundException(`FAQ with ID ${id} not found`); + } + + return this.mapToResponseDto(faq); + } + + async update(id: number, updateFaqDto: UpdateFaqDto): Promise { + const existingFaq = await this.prisma.fAQ.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingFaq) { + throw new NotFoundException(`FAQ with ID ${id} not found`); + } + + const faq = await this.prisma.fAQ.update({ + where: { id }, + data: { + ...updateFaqDto, + updatedAt: new Date(), + }, + }); + + return this.mapToResponseDto(faq); + } + + async remove(id: number): Promise { + const existingFaq = await this.prisma.fAQ.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingFaq) { + throw new NotFoundException(`FAQ with ID ${id} not found`); + } + + await this.prisma.fAQ.update({ + where: { id }, + data: { + deletedAt: new Date(), + }, + }); + } + + async findByCategory(category: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [faqs, total] = await Promise.all([ + this.prisma.fAQ.findMany({ + where: { + category, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.fAQ.count({ + where: { + category, + deletedAt: null, + }, + }), + ]); + + const mappedFaqs = faqs.map(faq => this.mapToResponseDto(faq)); + + return paginate(mappedFaqs, { page, limit }, total); + } + + async findByTag(tag: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [faqs, total] = await Promise.all([ + this.prisma.fAQ.findMany({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.fAQ.count({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + }), + ]); + + const mappedFaqs = faqs.map(faq => this.mapToResponseDto(faq)); + + return paginate(mappedFaqs, { page, limit }, total); + } + + async findByGlobalTag(globalTag: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [faqs, total] = await Promise.all([ + this.prisma.fAQ.findMany({ + where: { + globalTag: { + has: globalTag, + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.fAQ.count({ + where: { + globalTag: { + has: globalTag, + }, + deletedAt: null, + }, + }), + ]); + + const mappedFaqs = faqs.map(faq => this.mapToResponseDto(faq)); + + return paginate(mappedFaqs, { page, limit }, total); + } + + private mapToResponseDto(faq: any): FaqResponseDto { + return { + id: faq.id, + question: faq.question, + category: faq.category, + answer: faq.answer, + tags: faq.tags, + globalTag: faq.globalTag, + createdAt: faq.createdAt, + updatedAt: faq.updatedAt, + }; + } +} diff --git a/src/modules/klc-archive/controllers/klc-archive.controller.ts b/src/modules/klc-archive/controllers/klc-archive.controller.ts new file mode 100644 index 0000000..1ef0758 --- /dev/null +++ b/src/modules/klc-archive/controllers/klc-archive.controller.ts @@ -0,0 +1,110 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { KlcArchiveService } from '../services/klc-archive.service'; +import { CreateKlcArchiveDto } from '../dto/create-klc-archive.dto'; +import { UpdateKlcArchiveDto } from '../dto/update-klc-archive.dto'; +import { KlcArchiveResponseDto } from '../dto/klc-archive-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; + +@ApiTags('klc-archive') +@Controller('klc-archive') +export class KlcArchiveController { + constructor(private readonly klcArchiveService: KlcArchiveService) {} + + @Post() + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new KLC Archive entry' }) + @ApiResponse({ status: 201, description: 'KLC Archive created successfully', type: KlcArchiveResponseDto }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async create(@Body() createKlcArchiveDto: CreateKlcArchiveDto): Promise { + return this.klcArchiveService.create(createKlcArchiveDto); + } + + @Get() + @ApiOperation({ summary: 'Get all KLC Archive entries with pagination' }) + @ApiResponse({ status: 200, description: 'KLC Archive entries retrieved successfully' }) + async findAll(@Query() paginationDto: PaginationDto) { + return this.klcArchiveService.findAll(paginationDto); + } + + @Get('search/title') + @ApiOperation({ summary: 'Search KLC Archive entries by title' }) + @ApiResponse({ status: 200, description: 'KLC Archive entries retrieved successfully' }) + async searchByTitle( + @Query('q') title: string, + @Query() paginationDto: PaginationDto, + ) { + return this.klcArchiveService.searchByTitle(title, paginationDto); + } + + @Get('search/description') + @ApiOperation({ summary: 'Search KLC Archive entries by description' }) + @ApiResponse({ status: 200, description: 'KLC Archive entries retrieved successfully' }) + async searchByDescription( + @Query('q') description: string, + @Query() paginationDto: PaginationDto, + ) { + return this.klcArchiveService.searchByDescription(description, paginationDto); + } + + @Get('tag/:tag') + @ApiOperation({ summary: 'Get KLC Archive entries by tag' }) + @ApiResponse({ status: 200, description: 'KLC Archive entries retrieved successfully' }) + async findByTag( + @Param('tag') tag: string, + @Query() paginationDto: PaginationDto, + ) { + return this.klcArchiveService.findByTag(tag, paginationDto); + } + + @Get(':id') + @ApiOperation({ summary: 'Get KLC Archive entry by ID' }) + @ApiResponse({ status: 200, description: 'KLC Archive entry retrieved successfully', type: KlcArchiveResponseDto }) + @ApiResponse({ status: 404, description: 'KLC Archive entry not found' }) + async findOne(@Param('id', ParseIntPipe) id: number): Promise { + return this.klcArchiveService.findOne(id); + } + + @Patch(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Update KLC Archive entry' }) + @ApiResponse({ status: 200, description: 'KLC Archive entry updated successfully', type: KlcArchiveResponseDto }) + @ApiResponse({ status: 404, description: 'KLC Archive entry not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async update( + @Param('id', ParseIntPipe) id: number, + @Body() updateKlcArchiveDto: UpdateKlcArchiveDto, + ): Promise { + return this.klcArchiveService.update(id, updateKlcArchiveDto); + } + + @Delete(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Delete KLC Archive entry (soft delete)' }) + @ApiResponse({ status: 200, description: 'KLC Archive entry deleted successfully' }) + @ApiResponse({ status: 404, description: 'KLC Archive entry not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> { + await this.klcArchiveService.remove(id); + return { message: 'KLC Archive entry deleted successfully' }; + } +} diff --git a/src/modules/klc-archive/dto/create-klc-archive.dto.ts b/src/modules/klc-archive/dto/create-klc-archive.dto.ts new file mode 100644 index 0000000..9b102b7 --- /dev/null +++ b/src/modules/klc-archive/dto/create-klc-archive.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsOptional, IsArray, IsUrl } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateKlcArchiveDto { + @ApiProperty({ description: 'KLC Archive title' }) + @IsString() + title: string; + + @ApiProperty({ description: 'KLC Archive description' }) + @IsString() + description: string; + + @ApiProperty({ description: 'KLC Archive file URL' }) + @IsUrl() + fileUrl: string; + + @ApiPropertyOptional({ description: 'KLC Archive tags', type: [String] }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tags?: string[]; +} diff --git a/src/modules/klc-archive/dto/klc-archive-response.dto.ts b/src/modules/klc-archive/dto/klc-archive-response.dto.ts new file mode 100644 index 0000000..8d936a9 --- /dev/null +++ b/src/modules/klc-archive/dto/klc-archive-response.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class KlcArchiveResponseDto { + @ApiProperty({ description: 'KLC Archive ID' }) + id: number; + + @ApiProperty({ description: 'KLC Archive title' }) + title: string; + + @ApiProperty({ description: 'KLC Archive description' }) + description: string; + + @ApiProperty({ description: 'KLC Archive file URL' }) + fileUrl: string; + + @ApiProperty({ description: 'KLC Archive tags', type: [String] }) + tags: string[]; + + @ApiProperty({ description: 'Creation date' }) + createdAt: Date; + + @ApiProperty({ description: 'Last update date' }) + updatedAt: Date; +} diff --git a/src/modules/klc-archive/dto/update-klc-archive.dto.ts b/src/modules/klc-archive/dto/update-klc-archive.dto.ts new file mode 100644 index 0000000..b11bb1e --- /dev/null +++ b/src/modules/klc-archive/dto/update-klc-archive.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateKlcArchiveDto } from './create-klc-archive.dto'; + +export class UpdateKlcArchiveDto extends PartialType(CreateKlcArchiveDto) {} diff --git a/src/modules/klc-archive/klc-archive.module.ts b/src/modules/klc-archive/klc-archive.module.ts new file mode 100644 index 0000000..51d1347 --- /dev/null +++ b/src/modules/klc-archive/klc-archive.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { KlcArchiveController } from './controllers/klc-archive.controller'; +import { KlcArchiveService } from './services/klc-archive.service'; +import { PrismaModule } from '../../common/database/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [KlcArchiveController], + providers: [KlcArchiveService], + exports: [KlcArchiveService], +}) +export class KlcArchiveModule {} diff --git a/src/modules/klc-archive/services/klc-archive.service.ts b/src/modules/klc-archive/services/klc-archive.service.ts new file mode 100644 index 0000000..f05fbf7 --- /dev/null +++ b/src/modules/klc-archive/services/klc-archive.service.ts @@ -0,0 +1,223 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { CreateKlcArchiveDto } from '../dto/create-klc-archive.dto'; +import { UpdateKlcArchiveDto } from '../dto/update-klc-archive.dto'; +import { KlcArchiveResponseDto } from '../dto/klc-archive-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; +import { paginate } from '../../../common/utils/pagination.util'; + +@Injectable() +export class KlcArchiveService { + constructor(private readonly prisma: PrismaService) {} + + async create(createKlcArchiveDto: CreateKlcArchiveDto): Promise { + const klcArchive = await this.prisma.klcArchive.create({ + data: { + ...createKlcArchiveDto, + tags: createKlcArchiveDto.tags || [], + }, + }); + + return this.mapToResponseDto(klcArchive); + } + + async findAll(paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [klcArchives, total] = await Promise.all([ + this.prisma.klcArchive.findMany({ + where: { + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.klcArchive.count({ + where: { + deletedAt: null, + }, + }), + ]); + + const mappedKlcArchives = klcArchives.map(klcArchive => this.mapToResponseDto(klcArchive)); + + return paginate(mappedKlcArchives, { page, limit }, total); + } + + async findOne(id: number): Promise { + const klcArchive = await this.prisma.klcArchive.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!klcArchive) { + throw new NotFoundException(`KLC Archive with ID ${id} not found`); + } + + return this.mapToResponseDto(klcArchive); + } + + async update(id: number, updateKlcArchiveDto: UpdateKlcArchiveDto): Promise { + const existingKlcArchive = await this.prisma.klcArchive.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingKlcArchive) { + throw new NotFoundException(`KLC Archive with ID ${id} not found`); + } + + const klcArchive = await this.prisma.klcArchive.update({ + where: { id }, + data: { + ...updateKlcArchiveDto, + updatedAt: new Date(), + }, + }); + + return this.mapToResponseDto(klcArchive); + } + + async remove(id: number): Promise { + const existingKlcArchive = await this.prisma.klcArchive.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingKlcArchive) { + throw new NotFoundException(`KLC Archive with ID ${id} not found`); + } + + await this.prisma.klcArchive.update({ + where: { id }, + data: { + deletedAt: new Date(), + }, + }); + } + + async findByTag(tag: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [klcArchives, total] = await Promise.all([ + this.prisma.klcArchive.findMany({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.klcArchive.count({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + }), + ]); + + const mappedKlcArchives = klcArchives.map(klcArchive => this.mapToResponseDto(klcArchive)); + + return paginate(mappedKlcArchives, { page, limit }, total); + } + + async searchByTitle(title: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [klcArchives, total] = await Promise.all([ + this.prisma.klcArchive.findMany({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.klcArchive.count({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedKlcArchives = klcArchives.map(klcArchive => this.mapToResponseDto(klcArchive)); + + return paginate(mappedKlcArchives, { page, limit }, total); + } + + async searchByDescription(description: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [klcArchives, total] = await Promise.all([ + this.prisma.klcArchive.findMany({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.klcArchive.count({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedKlcArchives = klcArchives.map(klcArchive => this.mapToResponseDto(klcArchive)); + + return paginate(mappedKlcArchives, { page, limit }, total); + } + + private mapToResponseDto(klcArchive: any): KlcArchiveResponseDto { + return { + id: klcArchive.id, + title: klcArchive.title, + description: klcArchive.description, + fileUrl: klcArchive.fileUrl, + tags: klcArchive.tags, + createdAt: klcArchive.createdAt, + updatedAt: klcArchive.updatedAt, + }; + } +} diff --git a/src/modules/podcasts/controllers/podcasts.controller.ts b/src/modules/podcasts/controllers/podcasts.controller.ts new file mode 100644 index 0000000..1e76139 --- /dev/null +++ b/src/modules/podcasts/controllers/podcasts.controller.ts @@ -0,0 +1,114 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { PodcastsService } from '../services/podcasts.service'; +import { CreatePodcastDto } from '../dto/create-podcast.dto'; +import { UpdatePodcastDto } from '../dto/update-podcast.dto'; +import { PodcastResponseDto } from '../dto/podcast-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; +import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; +import { RolesGuard } from '../../../common/guards/roles.guard'; +import { Roles } from '../../../common/decorators/roles.decorator'; + +@ApiTags('podcasts') +@Controller('podcasts') +export class PodcastsController { + constructor(private readonly podcastsService: PodcastsService) {} + + @Post() + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new podcast' }) + @ApiResponse({ status: 201, description: 'Podcast created successfully', type: PodcastResponseDto }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async create(@Body() createPodcastDto: CreatePodcastDto): Promise { + return this.podcastsService.create(createPodcastDto); + } + + @Get() + @ApiOperation({ summary: 'Get all podcasts with pagination' }) + @ApiResponse({ status: 200, description: 'Podcasts retrieved successfully' }) + async findAll(@Query() paginationDto: PaginationDto) { + return this.podcastsService.findAll(paginationDto); + } + + @Get('search/title') + @ApiOperation({ summary: 'Search podcasts by title' }) + @ApiResponse({ status: 200, description: 'Podcasts retrieved successfully' }) + async searchByTitle( + @Query('q') title: string, + @Query() paginationDto: PaginationDto, + ) { + return this.podcastsService.searchByTitle(title, paginationDto); + } + + @Get('search/description') + @ApiOperation({ summary: 'Search podcasts by description' }) + @ApiResponse({ status: 200, description: 'Podcasts retrieved successfully' }) + async searchByDescription( + @Query('q') description: string, + @Query() paginationDto: PaginationDto, + ) { + return this.podcastsService.searchByDescription(description, paginationDto); + } + + @Get('tag/:tag') + @ApiOperation({ summary: 'Get podcasts by tag' }) + @ApiResponse({ status: 200, description: 'Podcasts retrieved successfully' }) + async findByTag( + @Param('tag') tag: string, + @Query() paginationDto: PaginationDto, + ) { + return this.podcastsService.findByTag(tag, paginationDto); + } + + @Get(':id') + @ApiOperation({ summary: 'Get podcast by ID' }) + @ApiResponse({ status: 200, description: 'Podcast retrieved successfully', type: PodcastResponseDto }) + @ApiResponse({ status: 404, description: 'Podcast not found' }) + async findOne(@Param('id', ParseIntPipe) id: number): Promise { + return this.podcastsService.findOne(id); + } + + @Patch(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Update podcast' }) + @ApiResponse({ status: 200, description: 'Podcast updated successfully', type: PodcastResponseDto }) + @ApiResponse({ status: 404, description: 'Podcast not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async update( + @Param('id', ParseIntPipe) id: number, + @Body() updatePodcastDto: UpdatePodcastDto, + ): Promise { + return this.podcastsService.update(id, updatePodcastDto); + } + + @Delete(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Delete podcast (soft delete)' }) + @ApiResponse({ status: 200, description: 'Podcast deleted successfully' }) + @ApiResponse({ status: 404, description: 'Podcast not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> { + await this.podcastsService.remove(id); + return { message: 'Podcast deleted successfully' }; + } +} diff --git a/src/modules/podcasts/dto/create-podcast.dto.ts b/src/modules/podcasts/dto/create-podcast.dto.ts new file mode 100644 index 0000000..94a359f --- /dev/null +++ b/src/modules/podcasts/dto/create-podcast.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsOptional, IsArray, IsUrl } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreatePodcastDto { + @ApiProperty({ description: 'Podcast title' }) + @IsString() + title: string; + + @ApiProperty({ description: 'Podcast description' }) + @IsString() + description: string; + + @ApiProperty({ description: 'Podcast file URL' }) + @IsUrl() + fileUrl: string; + + @ApiPropertyOptional({ description: 'Podcast tags', type: [String] }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tags?: string[]; +} diff --git a/src/modules/podcasts/dto/podcast-response.dto.ts b/src/modules/podcasts/dto/podcast-response.dto.ts new file mode 100644 index 0000000..2c213fb --- /dev/null +++ b/src/modules/podcasts/dto/podcast-response.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PodcastResponseDto { + @ApiProperty({ description: 'Podcast ID' }) + id: number; + + @ApiProperty({ description: 'Podcast title' }) + title: string; + + @ApiProperty({ description: 'Podcast description' }) + description: string; + + @ApiProperty({ description: 'Podcast file URL' }) + fileUrl: string; + + @ApiProperty({ description: 'Podcast tags', type: [String] }) + tags: string[]; + + @ApiProperty({ description: 'Creation date' }) + createdAt: Date; + + @ApiProperty({ description: 'Last update date' }) + updatedAt: Date; +} diff --git a/src/modules/podcasts/dto/update-podcast.dto.ts b/src/modules/podcasts/dto/update-podcast.dto.ts new file mode 100644 index 0000000..ffc4465 --- /dev/null +++ b/src/modules/podcasts/dto/update-podcast.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreatePodcastDto } from './create-podcast.dto'; + +export class UpdatePodcastDto extends PartialType(CreatePodcastDto) {} diff --git a/src/modules/podcasts/podcasts.module.ts b/src/modules/podcasts/podcasts.module.ts new file mode 100644 index 0000000..0689233 --- /dev/null +++ b/src/modules/podcasts/podcasts.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PodcastsController } from './controllers/podcasts.controller'; +import { PodcastsService } from './services/podcasts.service'; +import { PrismaModule } from '../../common/database/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [PodcastsController], + providers: [PodcastsService], + exports: [PodcastsService], +}) +export class PodcastsModule {} diff --git a/src/modules/podcasts/services/podcasts.service.ts b/src/modules/podcasts/services/podcasts.service.ts new file mode 100644 index 0000000..2f57c82 --- /dev/null +++ b/src/modules/podcasts/services/podcasts.service.ts @@ -0,0 +1,223 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { CreatePodcastDto } from '../dto/create-podcast.dto'; +import { UpdatePodcastDto } from '../dto/update-podcast.dto'; +import { PodcastResponseDto } from '../dto/podcast-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; +import { paginate } from '../../../common/utils/pagination.util'; + +@Injectable() +export class PodcastsService { + constructor(private readonly prisma: PrismaService) {} + + async create(createPodcastDto: CreatePodcastDto): Promise { + const podcast = await this.prisma.podcasts.create({ + data: { + ...createPodcastDto, + tags: createPodcastDto.tags || [], + }, + }); + + return this.mapToResponseDto(podcast); + } + + async findAll(paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [podcasts, total] = await Promise.all([ + this.prisma.podcasts.findMany({ + where: { + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.podcasts.count({ + where: { + deletedAt: null, + }, + }), + ]); + + const mappedPodcasts = podcasts.map(podcast => this.mapToResponseDto(podcast)); + + return paginate(mappedPodcasts, { page, limit }, total); + } + + async findOne(id: number): Promise { + const podcast = await this.prisma.podcasts.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!podcast) { + throw new NotFoundException(`Podcast with ID ${id} not found`); + } + + return this.mapToResponseDto(podcast); + } + + async update(id: number, updatePodcastDto: UpdatePodcastDto): Promise { + const existingPodcast = await this.prisma.podcasts.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingPodcast) { + throw new NotFoundException(`Podcast with ID ${id} not found`); + } + + const podcast = await this.prisma.podcasts.update({ + where: { id }, + data: { + ...updatePodcastDto, + updatedAt: new Date(), + }, + }); + + return this.mapToResponseDto(podcast); + } + + async remove(id: number): Promise { + const existingPodcast = await this.prisma.podcasts.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingPodcast) { + throw new NotFoundException(`Podcast with ID ${id} not found`); + } + + await this.prisma.podcasts.update({ + where: { id }, + data: { + deletedAt: new Date(), + }, + }); + } + + async findByTag(tag: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [podcasts, total] = await Promise.all([ + this.prisma.podcasts.findMany({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.podcasts.count({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + }), + ]); + + const mappedPodcasts = podcasts.map(podcast => this.mapToResponseDto(podcast)); + + return paginate(mappedPodcasts, { page, limit }, total); + } + + async searchByTitle(title: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [podcasts, total] = await Promise.all([ + this.prisma.podcasts.findMany({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.podcasts.count({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedPodcasts = podcasts.map(podcast => this.mapToResponseDto(podcast)); + + return paginate(mappedPodcasts, { page, limit }, total); + } + + async searchByDescription(description: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [podcasts, total] = await Promise.all([ + this.prisma.podcasts.findMany({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.podcasts.count({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedPodcasts = podcasts.map(podcast => this.mapToResponseDto(podcast)); + + return paginate(mappedPodcasts, { page, limit }, total); + } + + private mapToResponseDto(podcast: any): PodcastResponseDto { + return { + id: podcast.id, + title: podcast.title, + description: podcast.description, + fileUrl: podcast.fileUrl, + tags: podcast.tags, + createdAt: podcast.createdAt, + updatedAt: podcast.updatedAt, + }; + } +} diff --git a/src/modules/reading-materials/controllers/reading-materials.controller.ts b/src/modules/reading-materials/controllers/reading-materials.controller.ts new file mode 100644 index 0000000..e90189c --- /dev/null +++ b/src/modules/reading-materials/controllers/reading-materials.controller.ts @@ -0,0 +1,110 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { ReadingMaterialsService } from '../services/reading-materials.service'; +import { CreateReadingMaterialDto } from '../dto/create-reading-material.dto'; +import { UpdateReadingMaterialDto } from '../dto/update-reading-material.dto'; +import { ReadingMaterialResponseDto } from '../dto/reading-material-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; + +@ApiTags('reading-materials') +@Controller('reading-materials') +export class ReadingMaterialsController { + constructor(private readonly readingMaterialsService: ReadingMaterialsService) {} + + @Post() + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new reading material' }) + @ApiResponse({ status: 201, description: 'Reading material created successfully', type: ReadingMaterialResponseDto }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async create(@Body() createReadingMaterialDto: CreateReadingMaterialDto): Promise { + return this.readingMaterialsService.create(createReadingMaterialDto); + } + + @Get() + @ApiOperation({ summary: 'Get all reading materials with pagination' }) + @ApiResponse({ status: 200, description: 'Reading materials retrieved successfully' }) + async findAll(@Query() paginationDto: PaginationDto) { + return this.readingMaterialsService.findAll(paginationDto); + } + + @Get('search/title') + @ApiOperation({ summary: 'Search reading materials by title' }) + @ApiResponse({ status: 200, description: 'Reading materials retrieved successfully' }) + async searchByTitle( + @Query('q') title: string, + @Query() paginationDto: PaginationDto, + ) { + return this.readingMaterialsService.searchByTitle(title, paginationDto); + } + + @Get('search/description') + @ApiOperation({ summary: 'Search reading materials by description' }) + @ApiResponse({ status: 200, description: 'Reading materials retrieved successfully' }) + async searchByDescription( + @Query('q') description: string, + @Query() paginationDto: PaginationDto, + ) { + return this.readingMaterialsService.searchByDescription(description, paginationDto); + } + + @Get('tag/:tag') + @ApiOperation({ summary: 'Get reading materials by tag' }) + @ApiResponse({ status: 200, description: 'Reading materials retrieved successfully' }) + async findByTag( + @Param('tag') tag: string, + @Query() paginationDto: PaginationDto, + ) { + return this.readingMaterialsService.findByTag(tag, paginationDto); + } + + @Get(':id') + @ApiOperation({ summary: 'Get reading material by ID' }) + @ApiResponse({ status: 200, description: 'Reading material retrieved successfully', type: ReadingMaterialResponseDto }) + @ApiResponse({ status: 404, description: 'Reading material not found' }) + async findOne(@Param('id', ParseIntPipe) id: number): Promise { + return this.readingMaterialsService.findOne(id); + } + + @Patch(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Update reading material' }) + @ApiResponse({ status: 200, description: 'Reading material updated successfully', type: ReadingMaterialResponseDto }) + @ApiResponse({ status: 404, description: 'Reading material not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async update( + @Param('id', ParseIntPipe) id: number, + @Body() updateReadingMaterialDto: UpdateReadingMaterialDto, + ): Promise { + return this.readingMaterialsService.update(id, updateReadingMaterialDto); + } + + @Delete(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Delete reading material (soft delete)' }) + @ApiResponse({ status: 200, description: 'Reading material deleted successfully' }) + @ApiResponse({ status: 404, description: 'Reading material not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> { + await this.readingMaterialsService.remove(id); + return { message: 'Reading material deleted successfully' }; + } +} diff --git a/src/modules/reading-materials/dto/create-reading-material.dto.ts b/src/modules/reading-materials/dto/create-reading-material.dto.ts new file mode 100644 index 0000000..1cf4085 --- /dev/null +++ b/src/modules/reading-materials/dto/create-reading-material.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsOptional, IsArray, IsUrl } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateReadingMaterialDto { + @ApiProperty({ description: 'Reading material title' }) + @IsString() + title: string; + + @ApiProperty({ description: 'Reading material description' }) + @IsString() + description: string; + + @ApiProperty({ description: 'Reading material file URL' }) + @IsUrl() + fileUrl: string; + + @ApiPropertyOptional({ description: 'Reading material tags', type: [String] }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tags?: string[]; +} diff --git a/src/modules/reading-materials/dto/reading-material-response.dto.ts b/src/modules/reading-materials/dto/reading-material-response.dto.ts new file mode 100644 index 0000000..12b9369 --- /dev/null +++ b/src/modules/reading-materials/dto/reading-material-response.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ReadingMaterialResponseDto { + @ApiProperty({ description: 'Reading material ID' }) + id: number; + + @ApiProperty({ description: 'Reading material title' }) + title: string; + + @ApiProperty({ description: 'Reading material description' }) + description: string; + + @ApiProperty({ description: 'Reading material file URL' }) + fileUrl: string; + + @ApiProperty({ description: 'Reading material tags', type: [String] }) + tags: string[]; + + @ApiProperty({ description: 'Creation date' }) + createdAt: Date; + + @ApiProperty({ description: 'Last update date' }) + updatedAt: Date; +} diff --git a/src/modules/reading-materials/dto/update-reading-material.dto.ts b/src/modules/reading-materials/dto/update-reading-material.dto.ts new file mode 100644 index 0000000..a66eede --- /dev/null +++ b/src/modules/reading-materials/dto/update-reading-material.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateReadingMaterialDto } from './create-reading-material.dto'; + +export class UpdateReadingMaterialDto extends PartialType(CreateReadingMaterialDto) {} diff --git a/src/modules/reading-materials/reading-materials.module.ts b/src/modules/reading-materials/reading-materials.module.ts new file mode 100644 index 0000000..1778d38 --- /dev/null +++ b/src/modules/reading-materials/reading-materials.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ReadingMaterialsController } from './controllers/reading-materials.controller'; +import { ReadingMaterialsService } from './services/reading-materials.service'; +import { PrismaModule } from '../../common/database/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [ReadingMaterialsController], + providers: [ReadingMaterialsService], + exports: [ReadingMaterialsService], +}) +export class ReadingMaterialsModule {} diff --git a/src/modules/reading-materials/services/reading-materials.service.ts b/src/modules/reading-materials/services/reading-materials.service.ts new file mode 100644 index 0000000..0abcb76 --- /dev/null +++ b/src/modules/reading-materials/services/reading-materials.service.ts @@ -0,0 +1,223 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { CreateReadingMaterialDto } from '../dto/create-reading-material.dto'; +import { UpdateReadingMaterialDto } from '../dto/update-reading-material.dto'; +import { ReadingMaterialResponseDto } from '../dto/reading-material-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; +import { paginate } from '../../../common/utils/pagination.util'; + +@Injectable() +export class ReadingMaterialsService { + constructor(private readonly prisma: PrismaService) {} + + async create(createReadingMaterialDto: CreateReadingMaterialDto): Promise { + const readingMaterial = await this.prisma.readingMaterials.create({ + data: { + ...createReadingMaterialDto, + tags: createReadingMaterialDto.tags || [], + }, + }); + + return this.mapToResponseDto(readingMaterial); + } + + async findAll(paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [readingMaterials, total] = await Promise.all([ + this.prisma.readingMaterials.findMany({ + where: { + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.readingMaterials.count({ + where: { + deletedAt: null, + }, + }), + ]); + + const mappedReadingMaterials = readingMaterials.map(readingMaterial => this.mapToResponseDto(readingMaterial)); + + return paginate(mappedReadingMaterials, { page, limit }, total); + } + + async findOne(id: number): Promise { + const readingMaterial = await this.prisma.readingMaterials.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!readingMaterial) { + throw new NotFoundException(`Reading material with ID ${id} not found`); + } + + return this.mapToResponseDto(readingMaterial); + } + + async update(id: number, updateReadingMaterialDto: UpdateReadingMaterialDto): Promise { + const existingReadingMaterial = await this.prisma.readingMaterials.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingReadingMaterial) { + throw new NotFoundException(`Reading material with ID ${id} not found`); + } + + const readingMaterial = await this.prisma.readingMaterials.update({ + where: { id }, + data: { + ...updateReadingMaterialDto, + updatedAt: new Date(), + }, + }); + + return this.mapToResponseDto(readingMaterial); + } + + async remove(id: number): Promise { + const existingReadingMaterial = await this.prisma.readingMaterials.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingReadingMaterial) { + throw new NotFoundException(`Reading material with ID ${id} not found`); + } + + await this.prisma.readingMaterials.update({ + where: { id }, + data: { + deletedAt: new Date(), + }, + }); + } + + async findByTag(tag: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [readingMaterials, total] = await Promise.all([ + this.prisma.readingMaterials.findMany({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.readingMaterials.count({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + }), + ]); + + const mappedReadingMaterials = readingMaterials.map(readingMaterial => this.mapToResponseDto(readingMaterial)); + + return paginate(mappedReadingMaterials, { page, limit }, total); + } + + async searchByTitle(title: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [readingMaterials, total] = await Promise.all([ + this.prisma.readingMaterials.findMany({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.readingMaterials.count({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedReadingMaterials = readingMaterials.map(readingMaterial => this.mapToResponseDto(readingMaterial)); + + return paginate(mappedReadingMaterials, { page, limit }, total); + } + + async searchByDescription(description: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [readingMaterials, total] = await Promise.all([ + this.prisma.readingMaterials.findMany({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.readingMaterials.count({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedReadingMaterials = readingMaterials.map(readingMaterial => this.mapToResponseDto(readingMaterial)); + + return paginate(mappedReadingMaterials, { page, limit }, total); + } + + private mapToResponseDto(readingMaterial: any): ReadingMaterialResponseDto { + return { + id: readingMaterial.id, + title: readingMaterial.title, + description: readingMaterial.description, + fileUrl: readingMaterial.fileUrl, + tags: readingMaterial.tags, + createdAt: readingMaterial.createdAt, + updatedAt: readingMaterial.updatedAt, + }; + } +} diff --git a/src/modules/training-materials/controllers/training-materials.controller.ts b/src/modules/training-materials/controllers/training-materials.controller.ts new file mode 100644 index 0000000..8a3dcf8 --- /dev/null +++ b/src/modules/training-materials/controllers/training-materials.controller.ts @@ -0,0 +1,110 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { TrainingMaterialsService } from '../services/training-materials.service'; +import { CreateTrainingMaterialDto } from '../dto/create-training-material.dto'; +import { UpdateTrainingMaterialDto } from '../dto/update-training-material.dto'; +import { TrainingMaterialResponseDto } from '../dto/training-material-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; + +@ApiTags('training-materials') +@Controller('training-materials') +export class TrainingMaterialsController { + constructor(private readonly trainingMaterialsService: TrainingMaterialsService) {} + + @Post() + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new training material' }) + @ApiResponse({ status: 201, description: 'Training material created successfully', type: TrainingMaterialResponseDto }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async create(@Body() createTrainingMaterialDto: CreateTrainingMaterialDto): Promise { + return this.trainingMaterialsService.create(createTrainingMaterialDto); + } + + @Get() + @ApiOperation({ summary: 'Get all training materials with pagination' }) + @ApiResponse({ status: 200, description: 'Training materials retrieved successfully' }) + async findAll(@Query() paginationDto: PaginationDto) { + return this.trainingMaterialsService.findAll(paginationDto); + } + + @Get('search/title') + @ApiOperation({ summary: 'Search training materials by title' }) + @ApiResponse({ status: 200, description: 'Training materials retrieved successfully' }) + async searchByTitle( + @Query('q') title: string, + @Query() paginationDto: PaginationDto, + ) { + return this.trainingMaterialsService.searchByTitle(title, paginationDto); + } + + @Get('search/description') + @ApiOperation({ summary: 'Search training materials by description' }) + @ApiResponse({ status: 200, description: 'Training materials retrieved successfully' }) + async searchByDescription( + @Query('q') description: string, + @Query() paginationDto: PaginationDto, + ) { + return this.trainingMaterialsService.searchByDescription(description, paginationDto); + } + + @Get('tag/:tag') + @ApiOperation({ summary: 'Get training materials by tag' }) + @ApiResponse({ status: 200, description: 'Training materials retrieved successfully' }) + async findByTag( + @Param('tag') tag: string, + @Query() paginationDto: PaginationDto, + ) { + return this.trainingMaterialsService.findByTag(tag, paginationDto); + } + + @Get(':id') + @ApiOperation({ summary: 'Get training material by ID' }) + @ApiResponse({ status: 200, description: 'Training material retrieved successfully', type: TrainingMaterialResponseDto }) + @ApiResponse({ status: 404, description: 'Training material not found' }) + async findOne(@Param('id', ParseIntPipe) id: number): Promise { + return this.trainingMaterialsService.findOne(id); + } + + @Patch(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Update training material' }) + @ApiResponse({ status: 200, description: 'Training material updated successfully', type: TrainingMaterialResponseDto }) + @ApiResponse({ status: 404, description: 'Training material not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async update( + @Param('id', ParseIntPipe) id: number, + @Body() updateTrainingMaterialDto: UpdateTrainingMaterialDto, + ): Promise { + return this.trainingMaterialsService.update(id, updateTrainingMaterialDto); + } + + @Delete(':id') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ADMIN', 'HR') + // @ApiBearerAuth() + @ApiOperation({ summary: 'Delete training material (soft delete)' }) + @ApiResponse({ status: 200, description: 'Training material deleted successfully' }) + @ApiResponse({ status: 404, description: 'Training material not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Forbidden' }) + async remove(@Param('id', ParseIntPipe) id: number): Promise<{ message: string }> { + await this.trainingMaterialsService.remove(id); + return { message: 'Training material deleted successfully' }; + } +} diff --git a/src/modules/training-materials/dto/create-training-material.dto.ts b/src/modules/training-materials/dto/create-training-material.dto.ts new file mode 100644 index 0000000..7357e95 --- /dev/null +++ b/src/modules/training-materials/dto/create-training-material.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsOptional, IsArray, IsUrl } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateTrainingMaterialDto { + @ApiProperty({ description: 'Training material title' }) + @IsString() + title: string; + + @ApiProperty({ description: 'Training material description' }) + @IsString() + description: string; + + @ApiProperty({ description: 'Training material file URL' }) + @IsUrl() + fileUrl: string; + + @ApiPropertyOptional({ description: 'Training material tags', type: [String] }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tags?: string[]; +} diff --git a/src/modules/training-materials/dto/training-material-response.dto.ts b/src/modules/training-materials/dto/training-material-response.dto.ts new file mode 100644 index 0000000..5d8f8d0 --- /dev/null +++ b/src/modules/training-materials/dto/training-material-response.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class TrainingMaterialResponseDto { + @ApiProperty({ description: 'Training material ID' }) + id: number; + + @ApiProperty({ description: 'Training material title' }) + title: string; + + @ApiProperty({ description: 'Training material description' }) + description: string; + + @ApiProperty({ description: 'Training material file URL' }) + fileUrl: string; + + @ApiProperty({ description: 'Training material tags', type: [String] }) + tags: string[]; + + @ApiProperty({ description: 'Creation date' }) + createdAt: Date; + + @ApiProperty({ description: 'Last update date' }) + updatedAt: Date; +} diff --git a/src/modules/training-materials/dto/update-training-material.dto.ts b/src/modules/training-materials/dto/update-training-material.dto.ts new file mode 100644 index 0000000..f5ffd3c --- /dev/null +++ b/src/modules/training-materials/dto/update-training-material.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateTrainingMaterialDto } from './create-training-material.dto'; + +export class UpdateTrainingMaterialDto extends PartialType(CreateTrainingMaterialDto) {} diff --git a/src/modules/training-materials/services/training-materials.service.ts b/src/modules/training-materials/services/training-materials.service.ts new file mode 100644 index 0000000..a94ede7 --- /dev/null +++ b/src/modules/training-materials/services/training-materials.service.ts @@ -0,0 +1,223 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { CreateTrainingMaterialDto } from '../dto/create-training-material.dto'; +import { UpdateTrainingMaterialDto } from '../dto/update-training-material.dto'; +import { TrainingMaterialResponseDto } from '../dto/training-material-response.dto'; +import { PaginationDto } from '../../../common/dto/pagination.dto'; +import { paginate } from '../../../common/utils/pagination.util'; + +@Injectable() +export class TrainingMaterialsService { + constructor(private readonly prisma: PrismaService) {} + + async create(createTrainingMaterialDto: CreateTrainingMaterialDto): Promise { + const trainingMaterial = await this.prisma.trainingMaterials.create({ + data: { + ...createTrainingMaterialDto, + tags: createTrainingMaterialDto.tags || [], + }, + }); + + return this.mapToResponseDto(trainingMaterial); + } + + async findAll(paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [trainingMaterials, total] = await Promise.all([ + this.prisma.trainingMaterials.findMany({ + where: { + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.trainingMaterials.count({ + where: { + deletedAt: null, + }, + }), + ]); + + const mappedTrainingMaterials = trainingMaterials.map(trainingMaterial => this.mapToResponseDto(trainingMaterial)); + + return paginate(mappedTrainingMaterials, { page, limit }, total); + } + + async findOne(id: number): Promise { + const trainingMaterial = await this.prisma.trainingMaterials.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!trainingMaterial) { + throw new NotFoundException(`Training material with ID ${id} not found`); + } + + return this.mapToResponseDto(trainingMaterial); + } + + async update(id: number, updateTrainingMaterialDto: UpdateTrainingMaterialDto): Promise { + const existingTrainingMaterial = await this.prisma.trainingMaterials.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingTrainingMaterial) { + throw new NotFoundException(`Training material with ID ${id} not found`); + } + + const trainingMaterial = await this.prisma.trainingMaterials.update({ + where: { id }, + data: { + ...updateTrainingMaterialDto, + updatedAt: new Date(), + }, + }); + + return this.mapToResponseDto(trainingMaterial); + } + + async remove(id: number): Promise { + const existingTrainingMaterial = await this.prisma.trainingMaterials.findFirst({ + where: { + id, + deletedAt: null, + }, + }); + + if (!existingTrainingMaterial) { + throw new NotFoundException(`Training material with ID ${id} not found`); + } + + await this.prisma.trainingMaterials.update({ + where: { id }, + data: { + deletedAt: new Date(), + }, + }); + } + + async findByTag(tag: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [trainingMaterials, total] = await Promise.all([ + this.prisma.trainingMaterials.findMany({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.trainingMaterials.count({ + where: { + tags: { + has: tag, + }, + deletedAt: null, + }, + }), + ]); + + const mappedTrainingMaterials = trainingMaterials.map(trainingMaterial => this.mapToResponseDto(trainingMaterial)); + + return paginate(mappedTrainingMaterials, { page, limit }, total); + } + + async searchByTitle(title: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [trainingMaterials, total] = await Promise.all([ + this.prisma.trainingMaterials.findMany({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.trainingMaterials.count({ + where: { + title: { + contains: title, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedTrainingMaterials = trainingMaterials.map(trainingMaterial => this.mapToResponseDto(trainingMaterial)); + + return paginate(mappedTrainingMaterials, { page, limit }, total); + } + + async searchByDescription(description: string, paginationDto: PaginationDto) { + const { page, limit } = paginationDto; + const skip = (page - 1) * limit; + + const [trainingMaterials, total] = await Promise.all([ + this.prisma.trainingMaterials.findMany({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip, + take: limit, + }), + this.prisma.trainingMaterials.count({ + where: { + description: { + contains: description, + mode: 'insensitive', + }, + deletedAt: null, + }, + }), + ]); + + const mappedTrainingMaterials = trainingMaterials.map(trainingMaterial => this.mapToResponseDto(trainingMaterial)); + + return paginate(mappedTrainingMaterials, { page, limit }, total); + } + + private mapToResponseDto(trainingMaterial: any): TrainingMaterialResponseDto { + return { + id: trainingMaterial.id, + title: trainingMaterial.title, + description: trainingMaterial.description, + fileUrl: trainingMaterial.fileUrl, + tags: trainingMaterial.tags, + createdAt: trainingMaterial.createdAt, + updatedAt: trainingMaterial.updatedAt, + }; + } +} diff --git a/src/modules/training-materials/training-materials.module.ts b/src/modules/training-materials/training-materials.module.ts new file mode 100644 index 0000000..370dc98 --- /dev/null +++ b/src/modules/training-materials/training-materials.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TrainingMaterialsController } from './controllers/training-materials.controller'; +import { TrainingMaterialsService } from './services/training-materials.service'; +import { PrismaModule } from '../../common/database/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [TrainingMaterialsController], + providers: [TrainingMaterialsService], + exports: [TrainingMaterialsService], +}) +export class TrainingMaterialsModule {}