diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 695a979..c4f1b20 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -19,6 +19,8 @@ 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'; +import { EventException } from "./entity/event-exception.entity"; +import { EventExceptionsModule } from "./event-exception/event-exceptions.module"; const moduleTypeOrm = TypeOrmModule.forRootAsync({ imports: [ConfigModule], @@ -39,7 +41,8 @@ const moduleTypeOrm = TypeOrmModule.forRootAsync({ Product, Event, RecurrenceRule, - ], + EventException + ], logging: true, // synchronize: true, }; @@ -59,7 +62,8 @@ const moduleTypeOrm = TypeOrmModule.forRootAsync({ UserGroupsModule, UserRolesModule, RecurrenceRulesModule, - ], + EventExceptionsModule + ], controllers: [AppController], providers: [AppService], }) diff --git a/server/src/entity/event-exception.entity.ts b/server/src/entity/event-exception.entity.ts new file mode 100644 index 0000000..5a45ea3 --- /dev/null +++ b/server/src/entity/event-exception.entity.ts @@ -0,0 +1,52 @@ +// dvbooking-cli/src/templates/nestjs/entity.ts.tpl + +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { + IsString, + IsNumber, + IsBoolean, + IsDate, + IsOptional, +} from 'class-validator'; + +@Entity({ name: 'event_exceptions' }) +export class EventException { + @PrimaryGeneratedColumn() + id: number; + + @Column() + @IsNumber() + event_id: number; + + @Column() + @IsDate() + original_start_time: Date; + + @Column({ default: false }) + @IsBoolean() + is_cancelled: boolean = false; + + @Column({ type: 'timestamp with time zone', nullable: true }) + @IsOptional() + @IsDate() + new_start_time: Date | null; + + @Column({ type: 'timestamp with time zone', nullable: true }) + @IsOptional() + @IsDate() + new_end_time: Date | null; + + @Column({ type: 'character varying', nullable: true }) + @IsOptional() + @IsString() + title: string | null; + + @Column({ type: 'text', nullable: true }) + @IsOptional() + @IsString() + description: string | null; + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + @IsDate() + created_at: Date; +} diff --git a/server/src/entity/event-type.entity.ts b/server/src/entity/event-type.entity.ts index 6e2e2e5..ad419b0 100644 --- a/server/src/entity/event-type.entity.ts +++ b/server/src/entity/event-type.entity.ts @@ -1,11 +1,13 @@ // dvbooking-cli/src/templates/nestjs/entity.ts.tpl import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; -import { IsString, IsNumber, IsBoolean, IsDate, IsOptional } from 'class-validator'; +import { + IsString, + IsOptional, +} from 'class-validator'; @Entity({ name: 'event_type' }) export class EventType { - @PrimaryGeneratedColumn() id: number; @@ -22,5 +24,4 @@ export class EventType { @IsOptional() @IsString() color: string | null; - -} \ No newline at end of file +} diff --git a/server/src/entity/product.entity.ts b/server/src/entity/product.entity.ts index eb5870c..18bbd78 100644 --- a/server/src/entity/product.entity.ts +++ b/server/src/entity/product.entity.ts @@ -1,11 +1,10 @@ // dvbooking-cli/src/templates/nestjs/entity.ts.tpl import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; -import { IsString, IsNumber, IsBoolean, IsDate, IsOptional } from 'class-validator'; +import { IsString, IsNumber, IsBoolean, IsOptional } from 'class-validator'; @Entity({ name: 'products' }) export class Product { - @PrimaryGeneratedColumn() id: number; @@ -22,5 +21,4 @@ export class Product { @IsOptional() @IsBoolean() is_available: boolean | null = true; - -} \ No newline at end of file +} diff --git a/server/src/event-exception/dto/create-event-exception.dto.ts b/server/src/event-exception/dto/create-event-exception.dto.ts new file mode 100644 index 0000000..d3f57fe --- /dev/null +++ b/server/src/event-exception/dto/create-event-exception.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/mapped-types'; +import { EventException } from '../../entity/event-exception.entity'; + +export class CreateEventExceptionDto extends OmitType(EventException, ['id']) {} \ No newline at end of file diff --git a/server/src/event-exception/dto/query-event-exception.dto.ts b/server/src/event-exception/dto/query-event-exception.dto.ts new file mode 100644 index 0000000..09a6dc6 --- /dev/null +++ b/server/src/event-exception/dto/query-event-exception.dto.ts @@ -0,0 +1,9 @@ +import { IsOptional, IsString, IsNumber, IsIn, IsBoolean } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class QueryEventExceptionDto { + @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/event-exception/dto/update-event-exception.dto.ts b/server/src/event-exception/dto/update-event-exception.dto.ts new file mode 100644 index 0000000..d480149 --- /dev/null +++ b/server/src/event-exception/dto/update-event-exception.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateEventExceptionDto } from './create-event-exception.dto'; + +export class UpdateEventExceptionDto extends PartialType( + CreateEventExceptionDto, +) {} diff --git a/server/src/event-exception/event-exceptions.controller.ts b/server/src/event-exception/event-exceptions.controller.ts new file mode 100644 index 0000000..71360e9 --- /dev/null +++ b/server/src/event-exception/event-exceptions.controller.ts @@ -0,0 +1,68 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + ParseIntPipe, + DefaultValuePipe, + UseGuards, +} from '@nestjs/common'; +import { EventExceptionsService } from './event-exceptions.service'; +import { CreateEventExceptionDto } from './dto/create-event-exception.dto'; +import { UpdateEventExceptionDto } from './dto/update-event-exception.dto'; +import { QueryEventExceptionDto } from './dto/query-event-exception.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('event-exceptions') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles(Role.Admin) +export class EventExceptionsController { + constructor( + private readonly eventExceptionsService: EventExceptionsService, + ) {} + + @Post() + create(@Body() createEventExceptionDto: CreateEventExceptionDto) { + return this.eventExceptionsService.create(createEventExceptionDto); + } + + @Get() + findAll(@Query() queryParams: QueryEventExceptionDto) { + return this.eventExceptionsService.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.eventExceptionsService.search(term, { page, limit }); + } + + @Get(':id') + findOne(@Param('id', ParseIntPipe) id: number) { + return this.eventExceptionsService.findOne(id); + } + + @Patch(':id') + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateEventExceptionDto: UpdateEventExceptionDto, + ) { + return this.eventExceptionsService.update(id, updateEventExceptionDto); + } + + @Delete(':id') + remove(@Param('id', ParseIntPipe) id: number) { + return this.eventExceptionsService.remove(id); + } +} diff --git a/server/src/event-exception/event-exceptions.module.ts b/server/src/event-exception/event-exceptions.module.ts new file mode 100644 index 0000000..d0fe79c --- /dev/null +++ b/server/src/event-exception/event-exceptions.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EventExceptionsService } from './event-exceptions.service'; +import { EventExceptionsController } from './event-exceptions.controller'; +import { EventException } from '../entity/event-exception.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([EventException])], + controllers: [EventExceptionsController], + providers: [EventExceptionsService], +}) +export class EventExceptionsModule {} \ No newline at end of file diff --git a/server/src/event-exception/event-exceptions.service.ts b/server/src/event-exception/event-exceptions.service.ts new file mode 100644 index 0000000..4bdc21a --- /dev/null +++ b/server/src/event-exception/event-exceptions.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 { CreateEventExceptionDto } from './dto/create-event-exception.dto'; +import { UpdateEventExceptionDto } from './dto/update-event-exception.dto'; +import { QueryEventExceptionDto } from './dto/query-event-exception.dto'; +import { EventException } from '../entity/event-exception.entity'; + +type QueryConfigItem = { + param: keyof Omit< + QueryEventExceptionDto, + 'page' | 'limit' | 'sortBy' | 'order' + >; + dbField: keyof EventException; + operator: 'equals' | 'like'; +}; + +@Injectable() +export class EventExceptionsService { + constructor( + @InjectRepository(EventException) + private readonly eventExceptionRepository: Repository, + ) {} + + private readonly searchableFields: (keyof EventException)[] = [ + 'title', + 'description', + ]; + + create(createEventExceptionDto: CreateEventExceptionDto) { + const newRecord = this.eventExceptionRepository.create( + createEventExceptionDto, + ); + return this.eventExceptionRepository.save(newRecord); + } + + async findAll(queryParams: QueryEventExceptionDto) { + 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.eventExceptionRepository.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.eventExceptionRepository.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.eventExceptionRepository.findOneBy({ + id: id, + }); + if (!record) { + throw new NotFoundException(`EventException with ID ${id} not found`); + } + return record; + } + + async update(id: number, updateEventExceptionDto: UpdateEventExceptionDto) { + const record = await this.findOne(id); + Object.assign(record, updateEventExceptionDto); + return this.eventExceptionRepository.save(record); + } + + async remove(id: number) { + const result = await this.eventExceptionRepository.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`EventException with ID ${id} not found`); + } + return { deleted: true, id }; + } +}