Skip to content
Merged
86 changes: 45 additions & 41 deletions backend/src/events/events.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import {
UseGuards,
HttpCode,
HttpStatus,
UseInterceptors, // Added
UploadedFile, // Added
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express'; // Added
import { diskStorage } from 'multer'; // Added
import { extname } from 'path'; // Added
import { EventsService } from './events.service';
import { CreateEventDto } from './dto/create-event.dto';
import { UpdateEventDto } from './dto/update-event.dto';
Expand All @@ -33,32 +38,54 @@ export class EventsController {
@Query('date_from') dateFrom?: string,
@Query('date_to') dateTo?: string,
) {
return this.eventsService.findAllPublished({
search,
location,
dateFrom,
dateTo,
});
return this.eventsService.findAllPublished({ search, location, dateFrom, dateTo });
}

@Get('my/events')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ORG)
findMyEvents(@GetUser() user: JwtPayload) {
return this.eventsService.findMyEvents(user.sub);
}

@Get('my/user-events')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.USER)
findUserEvents(@GetUser() user: JwtPayload) {
return this.eventsService.findUserEvents(user.sub);
}

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.eventsService.findOne(id);
}

// Organizer
@Post()
// NEW: FILE UPLOAD ENDPOINT
@Post(':id/upload')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ORG)
create(@Body() dto: CreateEventDto, @GetUser() user: JwtPayload) {
return this.eventsService.create(dto, user.sub);
@UseInterceptors(FileInterceptor('file', {
storage: diskStorage({
destination: './uploads/documents',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, `${file.fieldname}-${uniqueSuffix}${extname(file.originalname)}`);
},
}),
}))
uploadFile(
@Param('id', ParseIntPipe) id: number,
@UploadedFile() file: Express.Multer.File,
@GetUser() user: JwtPayload,
) {
return this.eventsService.addDocument(id, user.sub, file);
}

@Get('my/events')
@Post()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ORG)
findMyEvents(@GetUser() user: JwtPayload) {
return this.eventsService.findMyEvents(user.sub);
create(@Body() dto: CreateEventDto, @GetUser() user: JwtPayload) {
return this.eventsService.create(dto, user.sub);
}

@Patch(':id')
Expand All @@ -82,51 +109,28 @@ export class EventsController {
@Get(':id/participants')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ORG)
getParticipants(
@Param('id', ParseIntPipe) id: number,
@GetUser() user: JwtPayload,
) {
getParticipants(@Param('id', ParseIntPipe) id: number, @GetUser() user: JwtPayload) {
return this.eventsService.getParticipants(id, user.sub);
}

// Participant

@Post(':id/register')
@UseGuards(JwtAuthGuard)
registerForEvent(
@Param('id', ParseIntPipe) id: number,
@GetUser() user: JwtPayload,
) {
registerForEvent(@Param('id', ParseIntPipe) id: number, @GetUser() user: JwtPayload) {
return this.eventsService.registerParticipant(id, user.sub);
}

@Delete(':id/cancel-registration')
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
cancelRegistration(
@Param('id', ParseIntPipe) id: number,
@GetUser() user: JwtPayload,
) {
cancelRegistration(@Param('id', ParseIntPipe) id: number, @GetUser() user: JwtPayload) {
return this.eventsService.cancelRegistration(id, user.sub);
}

@Get('my/user-events')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.USER)
findUserEvents(@GetUser() user: JwtPayload) {
return this.eventsService.findUserEvents(user.sub);
}

// Organizer OR Admin

@Delete(':id')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ORG, Role.ADMIN)
@HttpCode(HttpStatus.OK)
cancelEvent(
@Param('id', ParseIntPipe) id: number,
@GetUser() user: JwtPayload,
) {
cancelEvent(@Param('id', ParseIntPipe) id: number, @GetUser() user: JwtPayload) {
return this.eventsService.cancel(id, user.sub, user.role);
}
}
}
3 changes: 2 additions & 1 deletion backend/src/events/events.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { EventsService } from './events.service';
import { EventsController } from './events.controller';
import { PrismaModule } from '../prisma/prisma.module';
import { AuthModule } from '../auth/auth.module';
import { MailModule } from '../mail/mail.module';

@Module({
imports: [PrismaModule, AuthModule],
imports: [PrismaModule, AuthModule,MailModule],
providers: [EventsService],
controllers: [EventsController],
})
Expand Down
116 changes: 73 additions & 43 deletions backend/src/events/events.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import {
ConflictException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { MailService } from '../mail/mail.service';
import { CreateEventDto } from './dto/create-event.dto';
import { UpdateEventDto } from './dto/update-event.dto';

@Injectable()
export class EventsService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
private mailService: MailService,
) {}

// R11 — Create event (Organizer only)
// R11 — Create event
async create(dto: CreateEventDto, organizerId: number) {
return this.prisma.event.create({
data: {
Expand All @@ -27,7 +31,7 @@ export class EventsService {
});
}

// R13 + R33 — List all published events with optional search, location, and date range filters
// R13 + R33 — List all published events
async findAllPublished(filters?: {
search?: string;
location?: string;
Expand Down Expand Up @@ -68,18 +72,19 @@ export class EventsService {
});
}

// List organizer's own events (any status)
async findMyEvents(organizerId: number) {
return this.prisma.event.findMany({
where: { organizer_id: organizerId },
where: {
organizer_id: organizerId,
is_cancelled: false
},
include: {
_count: { select: { registrations: true } },
},
orderBy: { created_at: 'desc' },
});
}

// List events the user is registered for
async findUserEvents(userId: number) {
return this.prisma.event.findMany({
where: {
Expand All @@ -103,12 +108,14 @@ export class EventsService {
orderBy: { created_at: 'desc' },
});
}
// Get a single event by ID

// UPDATED: Added documents to include
async findOne(eventId: number) {
const event = await this.prisma.event.findUnique({
where: { event_id: eventId },
include: {
organizer: { select: { user_id: true, username: true } },
documents: true, // <--- ADD THIS LINE
_count: {
select: { registrations: { where: { status: 'CONFIRMED' } } },
},
Expand All @@ -118,15 +125,32 @@ export class EventsService {
return event;
}

// R30 — Update event details (Organizer, own event only)
// NEW METHOD: Handle the database record for the file
async addDocument(eventId: number, userId: number, file: Express.Multer.File) {
const event = await this.findOne(eventId);
if (event.organizer_id !== userId)
throw new ForbiddenException('You can only upload files to your own events');

return this.prisma.document.create({
data: {
event_id: eventId,
uploaded_by: userId,
file_name: file.originalname,
file_path: `/uploads/documents/${file.filename}`,
file_size_kb: Math.round(file.size / 1024),
},
});
}

// R30 — Update event details
async update(eventId: number, dto: UpdateEventDto, userId: number) {
const event = await this.findOne(eventId);
if (event.organizer_id !== userId)
throw new ForbiddenException('You can only edit your own events');
if (event.is_cancelled)
throw new BadRequestException('Cannot edit a cancelled event');

return this.prisma.event.update({
const updatedEvent = await this.prisma.event.update({
where: { event_id: eventId },
data: {
...(dto.title !== undefined && { title: dto.title }),
Expand All @@ -138,23 +162,12 @@ export class EventsService {
...(dto.capacity !== undefined && { capacity: dto.capacity }),
},
});
}

// Publish event (Organizer, own event)
async publish(eventId: number, userId: number) {
const event = await this.findOne(eventId);
if (event.organizer_id !== userId)
throw new ForbiddenException('You can only publish your own events');
if (event.is_cancelled)
throw new BadRequestException('Cannot publish a cancelled event');

return this.prisma.event.update({
where: { event_id: eventId },
data: { is_published: true },
});
await this.notifyParticipants(eventId, event.title, 'update');
return updatedEvent;
}

// R21 — Cancel event (Organizer cancels own, Admin cancels any)
// R21 — Cancel event
async cancel(eventId: number, userId: number, userRole: string) {
const event = await this.findOne(eventId);
if (event.is_cancelled)
Expand All @@ -165,19 +178,47 @@ export class EventsService {
if (!isAdmin && !isOwner)
throw new ForbiddenException('Not authorised to cancel this event');

return this.prisma.event.update({
const cancelledEvent = await this.prisma.event.update({
where: { event_id: eventId },
data: { is_cancelled: true, is_published: false },
});

await this.notifyParticipants(eventId, event.title, 'cancel');
return cancelledEvent;
}

private async notifyParticipants(eventId: number, title: string, type: 'update' | 'cancel') {
const registrations = await this.prisma.registration.findMany({
where: { event_id: eventId, status: 'CONFIRMED' },
include: { user: { select: { email: true } } },
});

for (const reg of registrations) {
if (type === 'update') {
this.mailService.sendEventUpdateEmail(reg.user.email, title);
} else {
this.mailService.sendEventCancellationEmail(reg.user.email, title);
}
}
}

async publish(eventId: number, userId: number) {
const event = await this.findOne(eventId);
if (event.organizer_id !== userId)
throw new ForbiddenException('You can only publish your own events');
if (event.is_cancelled)
throw new BadRequestException('Cannot publish a cancelled event');

return this.prisma.event.update({
where: { event_id: eventId },
data: { is_published: true },
});
}

// R24 — Participant list for an event (Organizer of that event only)
async getParticipants(eventId: number, userId: number) {
const event = await this.findOne(eventId);
if (event.organizer_id !== userId)
throw new ForbiddenException(
'Only the organizer can view the participant list',
);
throw new ForbiddenException('Only the organizer can view the participant list');

return this.prisma.registration.findMany({
where: { event_id: eventId, status: 'CONFIRMED' },
Expand All @@ -188,29 +229,23 @@ export class EventsService {
});
}

// R14 — Register participant to event (checks R15 capacity)
async registerParticipant(eventId: number, userId: number) {
const event = await this.findOne(eventId);

if (!event.is_published)
throw new BadRequestException('Event is not published');
if (!event.is_published) throw new BadRequestException('Event is not published');
if (event.is_cancelled) throw new BadRequestException('Event is cancelled');

// R15 — capacity check
const count = await this.prisma.registration.count({
where: { event_id: eventId, status: 'CONFIRMED' },
});
if (count >= event.capacity)
throw new BadRequestException('Event is fully booked');
if (count >= event.capacity) throw new BadRequestException('Event is fully booked');

// guard against duplicate registration
const existing = await this.prisma.registration.findUnique({
where: { user_id_event_id: { user_id: userId, event_id: eventId } },
});

if (existing) {
if (existing.status === 'CONFIRMED')
throw new ConflictException('Already registered for this event');
// re-register after cancellation
return this.prisma.registration.update({
where: { user_id_event_id: { user_id: userId, event_id: eventId } },
data: { status: 'CONFIRMED' },
Expand All @@ -222,21 +257,16 @@ export class EventsService {
});
}

// R12 — Cancel own registration
async cancelRegistration(eventId: number, userId: number) {
const existing = await this.prisma.registration.findUnique({
where: { user_id_event_id: { user_id: userId, event_id: eventId } },
});

// If it doesn't exist at all, that's a real 404
if (!existing) throw new NotFoundException('Registration not found');

// If it's already cancelled, just return the record instead of throwing error
if (existing.status === 'CANCELLED') return existing;

return this.prisma.registration.update({
where: { user_id_event_id: { user_id: userId, event_id: eventId } },
data: { status: 'CANCELLED' },
});
}
}
}
Loading