diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 7a65adf..695a979 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -17,6 +17,8 @@ import { EventsModule } from './event/events.module'; import { User } from './entity/user'; import { UserGroupsModule } from './user-group/user-group.module'; import { UserRolesModule } from './user-role/user-role.module'; +import { RecurrenceRule } from './entity/recurrence-rule.entity'; +import { RecurrenceRulesModule } from './recurrence-rule/recurrence-rules.module'; const moduleTypeOrm = TypeOrmModule.forRootAsync({ imports: [ConfigModule], @@ -29,7 +31,15 @@ const moduleTypeOrm = TypeOrmModule.forRootAsync({ username: configService.get('DATABASE_USER'), password: configService.get('DATABASE_PASS'), database: configService.get('DATABASE_NAME'), - entities: [User, UserGroup, UserRole, EventType, Product, Event], + entities: [ + User, + UserGroup, + UserRole, + EventType, + Product, + Event, + RecurrenceRule, + ], logging: true, // synchronize: true, }; @@ -48,6 +58,7 @@ const moduleTypeOrm = TypeOrmModule.forRootAsync({ EventsModule, UserGroupsModule, UserRolesModule, + RecurrenceRulesModule, ], controllers: [AppController], providers: [AppService], diff --git a/server/src/entity/recurrence-rule.entity.ts b/server/src/entity/recurrence-rule.entity.ts new file mode 100644 index 0000000..7ecbb57 --- /dev/null +++ b/server/src/entity/recurrence-rule.entity.ts @@ -0,0 +1,47 @@ +// dvbooking-cli/src/templates/nestjs/entity.ts.tpl + +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { IsString, IsNumber, IsDate, IsOptional } from 'class-validator'; + +@Entity({ name: 'recurrence_rules' }) +export class RecurrenceRule { + @PrimaryGeneratedColumn() + id: number; + + @Column() + @IsNumber() + event_id: number; + + @Column() + @IsString() + frequency: string; + + @Column({ default: '1' }) + @IsNumber() + interval: number = 1; + + @Column({ type: 'date', nullable: true }) + @IsOptional() + @IsDate() + end_date: Date | null; + + @Column({ type: 'integer', nullable: true }) + @IsOptional() + @IsNumber() + count: number | null; + + @Column({ type: 'character varying', nullable: true }) + @IsOptional() + @IsString() + by_day: string | null; + + @Column({ type: 'integer', nullable: true }) + @IsOptional() + @IsNumber() + by_month_day: number | null; + + @Column({ type: 'integer', nullable: true }) + @IsOptional() + @IsNumber() + by_month: number | null; +} diff --git a/server/src/recurrence-rule/dto/create-recurrence-rule.dto.ts b/server/src/recurrence-rule/dto/create-recurrence-rule.dto.ts new file mode 100644 index 0000000..1d86f35 --- /dev/null +++ b/server/src/recurrence-rule/dto/create-recurrence-rule.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/mapped-types'; +import { RecurrenceRule } from '../../entity/recurrence-rule.entity'; + +export class CreateRecurrenceRuleDto extends OmitType(RecurrenceRule, ['id']) {} \ No newline at end of file diff --git a/server/src/recurrence-rule/dto/query-recurrence-rule.dto.ts b/server/src/recurrence-rule/dto/query-recurrence-rule.dto.ts new file mode 100644 index 0000000..306dba5 --- /dev/null +++ b/server/src/recurrence-rule/dto/query-recurrence-rule.dto.ts @@ -0,0 +1,9 @@ +import { IsOptional, IsString, IsNumber, IsIn, IsBoolean } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class QueryRecurrenceRuleDto { + @IsOptional() @Type(() => Number) @IsNumber() page?: number; + @IsOptional() @Type(() => Number) @IsNumber() limit?: number; + @IsOptional() @IsString() sortBy?: string; + @IsOptional() @IsIn(['ASC', 'DESC']) order?: 'ASC' | 'DESC'; +} \ No newline at end of file diff --git a/server/src/recurrence-rule/dto/update-recurrence-rule.dto.ts b/server/src/recurrence-rule/dto/update-recurrence-rule.dto.ts new file mode 100644 index 0000000..d97df23 --- /dev/null +++ b/server/src/recurrence-rule/dto/update-recurrence-rule.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateRecurrenceRuleDto } from './create-recurrence-rule.dto'; + +export class UpdateRecurrenceRuleDto extends PartialType(CreateRecurrenceRuleDto) {} \ No newline at end of file diff --git a/server/src/recurrence-rule/recurrence-rules.controller.ts b/server/src/recurrence-rule/recurrence-rules.controller.ts new file mode 100644 index 0000000..42b90f6 --- /dev/null +++ b/server/src/recurrence-rule/recurrence-rules.controller.ts @@ -0,0 +1,63 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, + DefaultValuePipe, + UseGuards, +} from '@nestjs/common'; +import { RecurrenceRulesService } from './recurrence-rules.service'; +import { CreateRecurrenceRuleDto } from './dto/create-recurrence-rule.dto'; +import { UpdateRecurrenceRuleDto } from './dto/update-recurrence-rule.dto'; +import { QueryRecurrenceRuleDto } from './dto/query-recurrence-rule.dto'; + +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { Roles } from '../auth/roles.decorator'; +import { Role } from '../auth/role.enum'; +import { RolesGuard } from '../auth/roles.guard'; + +@Controller('recurrence-rules') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles(Role.Admin) +export class RecurrenceRulesController { + constructor(private readonly recurrenceRulesService: RecurrenceRulesService) {} + + @Post() + create(@Body() createRecurrenceRuleDto: CreateRecurrenceRuleDto) { + return this.recurrenceRulesService.create(createRecurrenceRuleDto); + } + + @Get() + findAll(@Query() queryParams: QueryRecurrenceRuleDto) { + return this.recurrenceRulesService.findAll(queryParams); + } + + @Get('search') + search( + @Query('q') term: string, + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, + @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number, + ) { + return this.recurrenceRulesService.search(term, { page, limit }); + } + + @Get(':id') + findOne(@Param('id', ParseIntPipe) id: number) { + return this.recurrenceRulesService.findOne(id); + } + + @Patch(':id') + update(@Param('id', ParseIntPipe) id: number, @Body() updateRecurrenceRuleDto: UpdateRecurrenceRuleDto) { + return this.recurrenceRulesService.update(id, updateRecurrenceRuleDto); + } + + @Delete(':id') + remove(@Param('id', ParseIntPipe) id: number) { + return this.recurrenceRulesService.remove(id); + } +} \ No newline at end of file diff --git a/server/src/recurrence-rule/recurrence-rules.module.ts b/server/src/recurrence-rule/recurrence-rules.module.ts new file mode 100644 index 0000000..be99b1b --- /dev/null +++ b/server/src/recurrence-rule/recurrence-rules.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RecurrenceRulesService } from './recurrence-rules.service'; +import { RecurrenceRulesController } from './recurrence-rules.controller'; +import { RecurrenceRule } from '../entity/recurrence-rule.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([RecurrenceRule])], + controllers: [RecurrenceRulesController], + providers: [RecurrenceRulesService], +}) +export class RecurrenceRulesModule {} \ No newline at end of file diff --git a/server/src/recurrence-rule/recurrence-rules.service.ts b/server/src/recurrence-rule/recurrence-rules.service.ts new file mode 100644 index 0000000..17b6f2b --- /dev/null +++ b/server/src/recurrence-rule/recurrence-rules.service.ts @@ -0,0 +1,137 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, FindManyOptions, FindOptionsWhere, ILike } from 'typeorm'; +import { CreateRecurrenceRuleDto } from './dto/create-recurrence-rule.dto'; +import { UpdateRecurrenceRuleDto } from './dto/update-recurrence-rule.dto'; +import { QueryRecurrenceRuleDto } from './dto/query-recurrence-rule.dto'; +import { RecurrenceRule } from '../entity/recurrence-rule.entity'; + +type QueryConfigItem = { + param: keyof Omit< + QueryRecurrenceRuleDto, + 'page' | 'limit' | 'sortBy' | 'order' + >; + dbField: keyof RecurrenceRule; + operator: 'equals' | 'like'; +}; + +@Injectable() +export class RecurrenceRulesService { + constructor( + @InjectRepository(RecurrenceRule) + private readonly recurrenceRuleRepository: Repository, + ) {} + + private readonly searchableFields: (keyof RecurrenceRule)[] = [ + 'frequency', + 'by_day', + ]; + + create(createRecurrenceRuleDto: CreateRecurrenceRuleDto) { + const newRecord = this.recurrenceRuleRepository.create( + createRecurrenceRuleDto, + ); + return this.recurrenceRuleRepository.save(newRecord); + } + + async findAll(queryParams: QueryRecurrenceRuleDto) { + const { page = 1, limit = 0, sortBy, order, ...filters } = queryParams; + const queryConfig: QueryConfigItem[] = []; + const whereClause: { [key: string]: any } = {}; + for (const config of queryConfig) { + if (filters[config.param] !== undefined) { + if (config.operator === 'like') { + whereClause[config.dbField] = ILike(`%${filters[config.param]}%`); + } else { + whereClause[config.dbField] = filters[config.param]; + } + } + } + const findOptions: FindManyOptions = { + where: whereClause as FindOptionsWhere, + }; + const paginated = limit > 0; + if (paginated) { + findOptions.skip = (page - 1) * limit; + findOptions.take = limit; + } + if (sortBy && order) { + findOptions.order = { [sortBy]: order }; + } + const [data, totalItems] = + await this.recurrenceRuleRepository.findAndCount(findOptions); + if (!paginated) { + return { data, total: data.length }; + } + return { + data, + meta: { + totalItems, + itemCount: data.length, + itemsPerPage: limit, + totalPages: Math.ceil(totalItems / limit), + currentPage: page, + }, + }; + } + + async search(term: string, options: { page: number; limit: number }) { + if (this.searchableFields.length === 0) { + console.warn('Search is not configured for this entity.'); + return { + data: [], + meta: { + totalItems: 0, + itemCount: 0, + itemsPerPage: options.limit, + totalPages: 0, + currentPage: options.page, + }, + }; + } + const whereConditions = this.searchableFields.map((field) => ({ + [field]: ILike(`%${term}%`), + })); + const [data, totalItems] = await this.recurrenceRuleRepository.findAndCount( + { + where: whereConditions, + skip: (options.page - 1) * options.limit, + take: options.limit, + }, + ); + return { + data, + meta: { + totalItems, + itemCount: data.length, + itemsPerPage: options.limit, + totalPages: Math.ceil(totalItems / options.limit), + currentPage: options.page, + }, + }; + } + + async findOne(id: number) { + const record = await this.recurrenceRuleRepository.findOneBy({ + id: id, + }); + if (!record) { + throw new NotFoundException(`RecurrenceRule with ID ${id} not found`); + } + return record; + } + + async update(id: number, updateRecurrenceRuleDto: UpdateRecurrenceRuleDto) { + const record = await this.findOne(id); + Object.assign(record, updateRecurrenceRuleDto); + return this.recurrenceRuleRepository.save(record); + } + + async remove(id: number) { + const result = await this.recurrenceRuleRepository.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`RecurrenceRule with ID ${id} not found`); + } + return { deleted: true, id }; + } +}