diff --git a/backend/src/events/dto/create-event.dto.ts b/backend/src/events/dto/create-event.dto.ts index 80f0fde7..9e2b5da2 100644 --- a/backend/src/events/dto/create-event.dto.ts +++ b/backend/src/events/dto/create-event.dto.ts @@ -1,49 +1,44 @@ import { IsString, - IsNotEmpty, - IsOptional, - IsUrl, IsDateString, IsInt, + IsOptional, Min, - IsEnum, } from 'class-validator'; -import { EventStatus } from '../entities/event.entity'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateEventDto { + @ApiProperty() @IsString() - @IsNotEmpty() title: string; + @ApiProperty() @IsString() - @IsNotEmpty() description: string; - @IsOptional() - @IsUrl() - coverImageUrl?: string; + @ApiProperty() + @IsString() + hostName: string; + @ApiProperty({ example: '2026-07-01T10:00:00Z' }) @IsDateString() - @IsNotEmpty() startDate: string; + @ApiProperty({ example: '2026-07-01T12:00:00Z' }) @IsDateString() - @IsNotEmpty() endDate: string; + @ApiProperty() @IsString() - @IsNotEmpty() - venue: string; + location: string; + @ApiProperty() @IsInt() @Min(1) capacity: number; - @IsInt() - @Min(0) - price: number; - + @ApiPropertyOptional() @IsOptional() - @IsEnum(EventStatus) - status?: EventStatus; -} \ No newline at end of file + @IsString() + imageUrl?: string; +} diff --git a/backend/src/events/dto/update-event.dto.ts b/backend/src/events/dto/update-event.dto.ts index 4f65a865..8f7d4034 100644 --- a/backend/src/events/dto/update-event.dto.ts +++ b/backend/src/events/dto/update-event.dto.ts @@ -1,4 +1,51 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { CreateEventDto } from './create-event.dto'; +import { + IsString, + IsDateString, + IsInt, + IsOptional, + Min, +} from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; -export class UpdateEventDto extends PartialType(CreateEventDto) {} \ No newline at end of file +export class UpdateEventDto { + @ApiPropertyOptional() + @IsOptional() + @IsString() + title?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + hostName?: string; + + @ApiPropertyOptional({ example: '2026-07-01T10:00:00Z' }) + @IsOptional() + @IsDateString() + startDate?: string; + + @ApiPropertyOptional({ example: '2026-07-01T12:00:00Z' }) + @IsOptional() + @IsDateString() + endDate?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + location?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsInt() + @Min(1) + capacity?: number; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + imageUrl?: string; +} diff --git a/backend/src/events/entities/event-rsvp.entity.ts b/backend/src/events/entities/event-rsvp.entity.ts new file mode 100644 index 00000000..7ca2dd23 --- /dev/null +++ b/backend/src/events/entities/event-rsvp.entity.ts @@ -0,0 +1,38 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + CreateDateColumn, + Unique, + JoinColumn, +} from 'typeorm'; +import { Event } from './event.entity'; +import { User } from '../../users/entities/user.entity'; + +@Entity('event_rsvps') +@Unique(['eventId', 'userId']) +export class EventRsvp { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column('uuid') + eventId: string; + + @ManyToOne(() => Event, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'eventId' }) + event: Event; + + @Column('uuid') + userId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; + + @CreateDateColumn() + rsvpedAt: Date; + + @Column({ default: false }) + attended: boolean; +} diff --git a/backend/src/events/entities/event.entity.ts b/backend/src/events/entities/event.entity.ts index 83fdfe71..397db6bc 100644 --- a/backend/src/events/entities/event.entity.ts +++ b/backend/src/events/entities/event.entity.ts @@ -6,13 +6,6 @@ import { UpdateDateColumn, } from 'typeorm'; -export enum EventStatus { - DRAFT = 'draft', - PUBLISHED = 'published', - CANCELLED = 'cancelled', - COMPLETED = 'completed', -} - @Entity('events') export class Event { @PrimaryGeneratedColumn('uuid') @@ -24,8 +17,8 @@ export class Event { @Column({ type: 'text' }) description: string; - @Column({ nullable: true }) - coverImageUrl?: string; + @Column() + hostName: string; @Column({ type: 'timestamptz' }) startDate: Date; @@ -34,19 +27,19 @@ export class Event { endDate: Date; @Column() - venue: string; + location: string; @Column({ type: 'int' }) capacity: number; - @Column({ type: 'int', default: 0 }) - price: number; + @Column({ nullable: true }) + imageUrl: string; - @Column({ type: 'enum', enum: EventStatus, default: EventStatus.DRAFT }) - status: EventStatus; + @Column({ default: true }) + isPublic: boolean; - @Column({ type: 'uuid', nullable: true }) - createdBy: string; + @Column({ default: false }) + isCancelled: boolean; @CreateDateColumn() createdAt: Date; diff --git a/backend/src/events/events.controller.ts b/backend/src/events/events.controller.ts index a2bb1acb..7b091394 100644 --- a/backend/src/events/events.controller.ts +++ b/backend/src/events/events.controller.ts @@ -1,74 +1,124 @@ -import { Controller, Post, Body, UseGuards, Get, Param, Patch, Delete, Query } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Patch, + Delete, + Body, + Param, + Query, + ParseUUIDPipe, + UseGuards, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; import { EventsService } from './events.service'; import { CreateEventDto } from './dto/create-event.dto'; import { UpdateEventDto } from './dto/update-event.dto'; -import { JwtAuthGuard } from '../auth/guard/jwt.auth.guard'; import { RolesGuard } from '../auth/guard/roles.guard'; import { Roles } from '../auth/decorators/roles.decorators'; -import { UserRole } from '../auth/common/enum/user-role-enum'; -import { CurrentUser } from '../auth/decorators/current.user.decorators'; -import { Public } from '../auth/decorators/public.decorator'; +import { UserRole } from '../users/enums/userRoles.enum'; +import { GetCurrentUser } from '../auth/decorators/getCurrentUser.decorator'; +@ApiTags('events') +@ApiBearerAuth() @Controller('events') export class EventsController { constructor(private readonly eventsService: EventsService) {} @Post() - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - create(@Body() createEventDto: CreateEventDto, @CurrentUser() user) { - return this.eventsService.create(createEventDto, user.id); + @UseGuards(RolesGuard) + @Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN) + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: 'Create an event (Admin only)' }) + async create(@Body() dto: CreateEventDto) { + const event = await this.eventsService.create(dto); + return { message: 'Event created successfully', data: event }; } - @Get('my') - @UseGuards(JwtAuthGuard) - getMyRegistrations(@CurrentUser() user) { - return this.eventsService.getMyRegistrations(user.id); - } - - @Get() - @Public() - findAll(@CurrentUser() user) { - const isPublic = !user || user.role !== UserRole.ADMIN; - return this.eventsService.findAll(isPublic); + @Patch(':id') + @UseGuards(RolesGuard) + @Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN) + @ApiOperation({ summary: 'Update an event (Admin only)' }) + async update( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateEventDto, + ) { + const event = await this.eventsService.update(id, dto); + return { message: 'Event updated successfully', data: event }; } - @Get(':id') - @Public() - findOne(@Param('id') id: string) { - return this.eventsService.findOne(id); + @Delete(':id') + @UseGuards(RolesGuard) + @Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN) + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Cancel an event (Admin only, soft delete)' }) + async cancel(@Param('id', ParseUUIDPipe) id: string) { + const event = await this.eventsService.cancel(id); + return { message: 'Event cancelled successfully', data: event }; } - @Patch(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - update(@Param('id') id: string, @Body() updateEventDto: UpdateEventDto) { - return this.eventsService.update(id, updateEventDto); + @Get() + @ApiOperation({ summary: 'List upcoming non-cancelled events' }) + @ApiQuery({ name: 'page', required: false }) + @ApiQuery({ name: 'limit', required: false }) + async findAll( + @Query('page') page?: string, + @Query('limit') limit?: string, + ) { + const result = await this.eventsService.findAll( + page ? Number(page) : undefined, + limit ? Number(limit) : undefined, + ); + return { message: 'Events retrieved successfully', ...result }; } - @Delete(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - remove(@Param('id') id: string) { - return this.eventsService.remove(id); + @Get(':id') + @ApiOperation({ summary: 'Get event details with RSVP count' }) + async findOne(@Param('id', ParseUUIDPipe) id: string) { + const event = await this.eventsService.findById(id); + return { message: 'Event retrieved successfully', data: event }; } - @Post(':id/register') - @UseGuards(JwtAuthGuard) - register(@Param('id') id: string, @CurrentUser() user) { - return this.eventsService.register(id, user.id); + @Post(':id/rsvp') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: 'RSVP to an event' }) + async rsvp( + @Param('id', ParseUUIDPipe) id: string, + @GetCurrentUser('id') userId: string, + ) { + const rsvp = await this.eventsService.rsvp(id, userId); + return { message: 'RSVP confirmed', data: rsvp }; } - @Delete(':id/register') - @UseGuards(JwtAuthGuard) - cancelRegistration(@Param('id') id: string, @CurrentUser() user) { - return this.eventsService.cancelRegistration(id, user.id); + @Delete(':id/rsvp') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Cancel your RSVP' }) + async cancelRsvp( + @Param('id', ParseUUIDPipe) id: string, + @GetCurrentUser('id') userId: string, + ) { + await this.eventsService.cancelRsvp(id, userId); + return { message: 'RSVP cancelled' }; } - @Get(':id/registrations') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - getRegistrations(@Param('id') id: string) { - return this.eventsService.getRegistrations(id); + @Get(':id/attendees') + @UseGuards(RolesGuard) + @Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN) + @ApiOperation({ summary: 'List event attendees (Admin only)' }) + @ApiQuery({ name: 'page', required: false }) + @ApiQuery({ name: 'limit', required: false }) + async findAttendees( + @Param('id', ParseUUIDPipe) id: string, + @Query('page') page?: string, + @Query('limit') limit?: string, + ) { + const result = await this.eventsService.findAttendees( + id, + page ? Number(page) : undefined, + limit ? Number(limit) : undefined, + ); + return { message: 'Attendees retrieved successfully', ...result }; } -} \ No newline at end of file +} diff --git a/backend/src/events/events.module.ts b/backend/src/events/events.module.ts index fbd5ee40..458b3d2e 100644 --- a/backend/src/events/events.module.ts +++ b/backend/src/events/events.module.ts @@ -1,14 +1,34 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { EventsController } from './events.controller'; -import { EventsService } from './events.service'; import { Event } from './entities/event.entity'; -import { EventRegistration } from './entities/event-registration.entity'; -import { AuthModule } from '../auth/auth.module'; +import { EventRsvp } from './entities/event-rsvp.entity'; +import { EventsService } from './events.service'; +import { EventsController } from './events.controller'; +import { CreateEventProvider } from './providers/create-event.provider'; +import { UpdateEventProvider } from './providers/update-event.provider'; +import { CancelEventProvider } from './providers/cancel-event.provider'; +import { FindEventsProvider } from './providers/find-events.provider'; +import { FindEventByIdProvider } from './providers/find-event-by-id.provider'; +import { RsvpToEventProvider } from './providers/rsvp-to-event.provider'; +import { CancelRsvpProvider } from './providers/cancel-rsvp.provider'; +import { FindAttendeesProvider } from './providers/find-attendees.provider'; @Module({ - imports: [TypeOrmModule.forFeature([Event, EventRegistration]), AuthModule], + imports: [ + TypeOrmModule.forFeature([Event, EventRsvp]), + ], controllers: [EventsController], - providers: [EventsService], + providers: [ + EventsService, + CreateEventProvider, + UpdateEventProvider, + CancelEventProvider, + FindEventsProvider, + FindEventByIdProvider, + RsvpToEventProvider, + CancelRsvpProvider, + FindAttendeesProvider, + ], + exports: [EventsService], }) -export class EventsModule {} \ No newline at end of file +export class EventsModule {} diff --git a/backend/src/events/events.service.ts b/backend/src/events/events.service.ts index 8f19c708..1786c02f 100644 --- a/backend/src/events/events.service.ts +++ b/backend/src/events/events.service.ts @@ -1,115 +1,59 @@ -import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Event, EventStatus } from './entities/event.entity'; +import { Injectable } from '@nestjs/common'; import { CreateEventDto } from './dto/create-event.dto'; import { UpdateEventDto } from './dto/update-event.dto'; -import { EventRegistration, EventRegistrationStatus } from './entities/event-registration.entity'; +import { CreateEventProvider } from './providers/create-event.provider'; +import { UpdateEventProvider } from './providers/update-event.provider'; +import { CancelEventProvider } from './providers/cancel-event.provider'; +import { FindEventsProvider, PaginatedEvents } from './providers/find-events.provider'; +import { FindEventByIdProvider } from './providers/find-event-by-id.provider'; +import { RsvpToEventProvider } from './providers/rsvp-to-event.provider'; +import { CancelRsvpProvider } from './providers/cancel-rsvp.provider'; +import { FindAttendeesProvider, PaginatedAttendees } from './providers/find-attendees.provider'; +import { Event } from './entities/event.entity'; +import { EventRsvp } from './entities/event-rsvp.entity'; @Injectable() export class EventsService { constructor( - @InjectRepository(Event) - private readonly eventRepository: Repository, - @InjectRepository(EventRegistration) - private readonly registrationRepository: Repository, + private readonly createEventProvider: CreateEventProvider, + private readonly updateEventProvider: UpdateEventProvider, + private readonly cancelEventProvider: CancelEventProvider, + private readonly findEventsProvider: FindEventsProvider, + private readonly findEventByIdProvider: FindEventByIdProvider, + private readonly rsvpToEventProvider: RsvpToEventProvider, + private readonly cancelRsvpProvider: CancelRsvpProvider, + private readonly findAttendeesProvider: FindAttendeesProvider, ) {} - async create(createEventDto: CreateEventDto, userId: string): Promise { - const event = this.eventRepository.create({ - ...createEventDto, - createdBy: userId, - }); - return this.eventRepository.save(event); + create(dto: CreateEventDto): Promise { + return this.createEventProvider.create(dto); } - async findAll(isPublic = false): Promise { - if (isPublic) { - return this.eventRepository.find({ where: { status: EventStatus.PUBLISHED } }); - } - return this.eventRepository.find(); + update(id: string, dto: UpdateEventDto): Promise { + return this.updateEventProvider.update(id, dto); } - async findOne(id: string): Promise { - const event = await this.eventRepository.findOne({ where: { id } }); - if (!event) { - throw new NotFoundException(`Event with ID "${id}" not found`); - } - return event; + cancel(id: string): Promise { + return this.cancelEventProvider.cancel(id); } - async update(id: string, updateEventDto: UpdateEventDto): Promise { - const event = await this.findOne(id); - Object.assign(event, updateEventDto); - return this.eventRepository.save(event); + findAll(page?: number, limit?: number): Promise { + return this.findEventsProvider.findAll(page, limit); } - async remove(id: string): Promise { - const event = await this.findOne(id); - await this.eventRepository.remove(event); + findById(id: string): Promise { + return this.findEventByIdProvider.findById(id); } - async register(eventId: string, userId: string): Promise { - const event = await this.findOne(eventId); - - if (event.status !== EventStatus.PUBLISHED) { - throw new BadRequestException('Event is not published'); - } - - const existingRegistration = await this.registrationRepository.findOne({ - where: { eventId, userId }, - }); - - if (existingRegistration) { - throw new ConflictException('You are already registered for this event'); - } - - const registrationCount = await this.registrationRepository.count({ - where: { eventId, status: EventRegistrationStatus.REGISTERED }, - }); - - const status = - registrationCount < event.capacity - ? EventRegistrationStatus.REGISTERED - : EventRegistrationStatus.WAITLISTED; - - const registration = this.registrationRepository.create({ - eventId, - userId, - status, - }); - - return this.registrationRepository.save(registration); - } - - async cancelRegistration(eventId: string, userId: string): Promise { - const registration = await this.registrationRepository.findOne({ - where: { eventId, userId }, - }); - - if (!registration) { - throw new NotFoundException('You are not registered for this event'); - } - - await this.registrationRepository.remove(registration); - - const waitlistedUser = await this.registrationRepository.findOne({ - where: { eventId, status: EventRegistrationStatus.WAITLISTED }, - order: { registeredAt: 'ASC' }, - }); - - if (waitlistedUser) { - waitlistedUser.status = EventRegistrationStatus.REGISTERED; - await this.registrationRepository.save(waitlistedUser); - } + rsvp(eventId: string, userId: string): Promise { + return this.rsvpToEventProvider.rsvp(eventId, userId); } - async getRegistrations(eventId: string): Promise { - await this.findOne(eventId); - return this.registrationRepository.find({ where: { eventId } }); + cancelRsvp(eventId: string, userId: string): Promise { + return this.cancelRsvpProvider.cancelRsvp(eventId, userId); } - async getMyRegistrations(userId: string): Promise { - return this.registrationRepository.find({ where: { userId } }); + findAttendees(eventId: string, page?: number, limit?: number): Promise { + return this.findAttendeesProvider.findAttendees(eventId, page, limit); } -} \ No newline at end of file +} diff --git a/backend/src/events/providers/cancel-event.provider.ts b/backend/src/events/providers/cancel-event.provider.ts new file mode 100644 index 00000000..5ed621d9 --- /dev/null +++ b/backend/src/events/providers/cancel-event.provider.ts @@ -0,0 +1,25 @@ +import { + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Event } from '../entities/event.entity'; + +@Injectable() +export class CancelEventProvider { + constructor( + @InjectRepository(Event) + private readonly eventsRepository: Repository, + ) {} + + async cancel(id: string): Promise { + const event = await this.eventsRepository.findOne({ where: { id } }); + if (!event) { + throw new NotFoundException(`Event "${id}" not found`); + } + + event.isCancelled = true; + return this.eventsRepository.save(event); + } +} diff --git a/backend/src/events/providers/cancel-rsvp.provider.ts b/backend/src/events/providers/cancel-rsvp.provider.ts new file mode 100644 index 00000000..81e4af65 --- /dev/null +++ b/backend/src/events/providers/cancel-rsvp.provider.ts @@ -0,0 +1,26 @@ +import { + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { EventRsvp } from '../entities/event-rsvp.entity'; + +@Injectable() +export class CancelRsvpProvider { + constructor( + @InjectRepository(EventRsvp) + private readonly eventRsvpsRepository: Repository, + ) {} + + async cancelRsvp(eventId: string, userId: string): Promise { + const rsvp = await this.eventRsvpsRepository.findOne({ + where: { eventId, userId }, + }); + if (!rsvp) { + throw new NotFoundException('RSVP not found'); + } + + await this.eventRsvpsRepository.remove(rsvp); + } +} diff --git a/backend/src/events/providers/create-event.provider.ts b/backend/src/events/providers/create-event.provider.ts new file mode 100644 index 00000000..734581ec --- /dev/null +++ b/backend/src/events/providers/create-event.provider.ts @@ -0,0 +1,25 @@ +import { + BadRequestException, + Injectable, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Event } from '../entities/event.entity'; +import { CreateEventDto } from '../dto/create-event.dto'; + +@Injectable() +export class CreateEventProvider { + constructor( + @InjectRepository(Event) + private readonly eventsRepository: Repository, + ) {} + + async create(dto: CreateEventDto): Promise { + if (new Date(dto.endDate) <= new Date(dto.startDate)) { + throw new BadRequestException('endDate must be after startDate'); + } + + const event = this.eventsRepository.create(dto); + return this.eventsRepository.save(event); + } +} diff --git a/backend/src/events/providers/find-attendees.provider.ts b/backend/src/events/providers/find-attendees.provider.ts new file mode 100644 index 00000000..dc81613b --- /dev/null +++ b/backend/src/events/providers/find-attendees.provider.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { EventRsvp } from '../entities/event-rsvp.entity'; + +export interface PaginatedAttendees { + data: EventRsvp[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +@Injectable() +export class FindAttendeesProvider { + constructor( + @InjectRepository(EventRsvp) + private readonly eventRsvpsRepository: Repository, + ) {} + + async findAttendees( + eventId: string, + page = 1, + limit = 20, + ): Promise { + const qb = this.eventRsvpsRepository + .createQueryBuilder('rsvp') + .leftJoinAndSelect('rsvp.user', 'user') + .where('rsvp.eventId = :eventId', { eventId }) + .orderBy('rsvp.rsvpedAt', 'ASC'); + + const total = await qb.getCount(); + const data = await qb + .skip((page - 1) * limit) + .take(limit) + .getMany(); + + return { data, total, page, limit, totalPages: Math.ceil(total / limit) }; + } +} diff --git a/backend/src/events/providers/find-event-by-id.provider.ts b/backend/src/events/providers/find-event-by-id.provider.ts new file mode 100644 index 00000000..4a86c435 --- /dev/null +++ b/backend/src/events/providers/find-event-by-id.provider.ts @@ -0,0 +1,31 @@ +import { + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Event } from '../entities/event.entity'; +import { EventRsvp } from '../entities/event-rsvp.entity'; + +@Injectable() +export class FindEventByIdProvider { + constructor( + @InjectRepository(Event) + private readonly eventsRepository: Repository, + @InjectRepository(EventRsvp) + private readonly eventRsvpsRepository: Repository, + ) {} + + async findById(id: string): Promise { + const event = await this.eventsRepository.findOne({ where: { id } }); + if (!event) { + throw new NotFoundException(`Event "${id}" not found`); + } + + const rsvpCount = await this.eventRsvpsRepository.count({ + where: { eventId: id }, + }); + + return { ...event, rsvpCount }; + } +} diff --git a/backend/src/events/providers/find-events.provider.ts b/backend/src/events/providers/find-events.provider.ts new file mode 100644 index 00000000..82a2b47a --- /dev/null +++ b/backend/src/events/providers/find-events.provider.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Event } from '../entities/event.entity'; + +export interface PaginatedEvents { + data: Event[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +@Injectable() +export class FindEventsProvider { + constructor( + @InjectRepository(Event) + private readonly eventsRepository: Repository, + ) {} + + async findAll(page = 1, limit = 20): Promise { + const qb = this.eventsRepository + .createQueryBuilder('event') + .where('event.isCancelled = :isCancelled', { isCancelled: false }) + .andWhere('event.startDate >= :now', { now: new Date() }) + .orderBy('event.startDate', 'ASC'); + + const total = await qb.getCount(); + const data = await qb + .skip((page - 1) * limit) + .take(limit) + .getMany(); + + return { data, total, page, limit, totalPages: Math.ceil(total / limit) }; + } +} diff --git a/backend/src/events/providers/rsvp-to-event.provider.ts b/backend/src/events/providers/rsvp-to-event.provider.ts new file mode 100644 index 00000000..6e49681e --- /dev/null +++ b/backend/src/events/providers/rsvp-to-event.provider.ts @@ -0,0 +1,48 @@ +import { + ConflictException, + Injectable, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Event } from '../entities/event.entity'; +import { EventRsvp } from '../entities/event-rsvp.entity'; + +@Injectable() +export class RsvpToEventProvider { + constructor( + @InjectRepository(Event) + private readonly eventsRepository: Repository, + @InjectRepository(EventRsvp) + private readonly eventRsvpsRepository: Repository, + ) {} + + async rsvp(eventId: string, userId: string): Promise { + const event = await this.eventsRepository.findOne({ where: { id: eventId } }); + if (!event) { + throw new NotFoundException(`Event "${eventId}" not found`); + } + + if (event.isCancelled) { + throw new BadRequestException('Cannot RSVP to a cancelled event'); + } + + const existing = await this.eventRsvpsRepository.findOne({ + where: { eventId, userId }, + }); + if (existing) { + throw new ConflictException('You have already RSVPed to this event'); + } + + const rsvpCount = await this.eventRsvpsRepository.count({ + where: { eventId }, + }); + if (rsvpCount >= event.capacity) { + throw new BadRequestException('Event is at full capacity'); + } + + const rsvp = this.eventRsvpsRepository.create({ eventId, userId }); + return this.eventRsvpsRepository.save(rsvp); + } +} diff --git a/backend/src/events/providers/update-event.provider.ts b/backend/src/events/providers/update-event.provider.ts new file mode 100644 index 00000000..141b21b3 --- /dev/null +++ b/backend/src/events/providers/update-event.provider.ts @@ -0,0 +1,26 @@ +import { + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Event } from '../entities/event.entity'; +import { UpdateEventDto } from '../dto/update-event.dto'; + +@Injectable() +export class UpdateEventProvider { + constructor( + @InjectRepository(Event) + private readonly eventsRepository: Repository, + ) {} + + async update(id: string, dto: UpdateEventDto): Promise { + const event = await this.eventsRepository.findOne({ where: { id } }); + if (!event) { + throw new NotFoundException(`Event "${id}" not found`); + } + + Object.assign(event, dto); + return this.eventsRepository.save(event); + } +}