feat: implement backend API for event management (task 13)
- Create eventController with CRUD operations and registration endpoints - Implement eventService with Prisma database operations - Add event routes with authentication middleware - Support event creation, listing, filtering, registration/unregistration - Include spatial data handling for event locations - Add participant count tracking and registration status checks - Requirements: 4.1, 4.2, 4.3, 4.4
This commit is contained in:
327
server/src/services/eventService.ts
Normal file
327
server/src/services/eventService.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface EventFilters {
|
||||
status?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
interface CreateEventData {
|
||||
organizerId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
locationName: string;
|
||||
scheduledDate: Date;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface UpdateEventData {
|
||||
title?: string;
|
||||
description?: string;
|
||||
scheduledDate?: Date;
|
||||
duration?: number;
|
||||
status?: 'upcoming' | 'completed' | 'cancelled';
|
||||
litterCollected?: number;
|
||||
photos?: string[];
|
||||
}
|
||||
|
||||
export const eventService = {
|
||||
async getEvents(filters: EventFilters = {}) {
|
||||
const where: any = {};
|
||||
|
||||
// Filter by status
|
||||
if (filters.status && filters.status !== 'all') {
|
||||
where.status = filters.status;
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if (filters.startDate || filters.endDate) {
|
||||
where.scheduledDate = {};
|
||||
if (filters.startDate) {
|
||||
where.scheduledDate.gte = filters.startDate;
|
||||
}
|
||||
if (filters.endDate) {
|
||||
where.scheduledDate.lte = filters.endDate;
|
||||
}
|
||||
}
|
||||
|
||||
const events = await prisma.event.findMany({
|
||||
where,
|
||||
orderBy: {
|
||||
scheduledDate: 'asc',
|
||||
},
|
||||
include: {
|
||||
organizer: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
avatarUrl: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
registrations: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Map to frontend format
|
||||
return events.map((event) => ({
|
||||
id: event.id,
|
||||
organizerId: event.organizerId,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
latitude: parseFloat(event.latitude.toString()),
|
||||
longitude: parseFloat(event.longitude.toString()),
|
||||
locationName: event.locationName,
|
||||
scheduledDate: event.scheduledDate,
|
||||
duration: event.duration,
|
||||
status: event.status || 'upcoming',
|
||||
participantCount: event._count.registrations,
|
||||
litterCollected: event.litterCollected ? parseFloat(event.litterCollected.toString()) : undefined,
|
||||
photos: event.photos ? (Array.isArray(event.photos) ? event.photos : []) : undefined,
|
||||
createdAt: event.createdAt,
|
||||
updatedAt: event.updatedAt,
|
||||
}));
|
||||
},
|
||||
|
||||
async getEventById(id: string) {
|
||||
const event = await prisma.event.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
organizer: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
avatarUrl: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
registrations: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!event) return null;
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
organizerId: event.organizerId,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
latitude: parseFloat(event.latitude.toString()),
|
||||
longitude: parseFloat(event.longitude.toString()),
|
||||
locationName: event.locationName,
|
||||
scheduledDate: event.scheduledDate,
|
||||
duration: event.duration,
|
||||
status: event.status || 'upcoming',
|
||||
participantCount: event._count.registrations,
|
||||
litterCollected: event.litterCollected ? parseFloat(event.litterCollected.toString()) : undefined,
|
||||
photos: event.photos ? (Array.isArray(event.photos) ? event.photos : []) : undefined,
|
||||
createdAt: event.createdAt,
|
||||
updatedAt: event.updatedAt,
|
||||
};
|
||||
},
|
||||
|
||||
async createEvent(data: CreateEventData) {
|
||||
// Create point geometry for spatial queries
|
||||
const locationPoint = `POINT(${data.longitude} ${data.latitude})`;
|
||||
|
||||
const event = await prisma.$executeRaw`
|
||||
INSERT INTO events (
|
||||
id, organizer_id, title, description, location, latitude, longitude,
|
||||
location_name, scheduled_date, duration, status, participant_count
|
||||
) VALUES (
|
||||
UUID(), ${data.organizerId}, ${data.title}, ${data.description},
|
||||
ST_GeomFromText(${locationPoint}, 4326),
|
||||
${data.latitude}, ${data.longitude}, ${data.locationName},
|
||||
${data.scheduledDate}, ${data.duration}, 'upcoming', 0
|
||||
)
|
||||
`;
|
||||
|
||||
// Fetch the created event
|
||||
const createdEvent = await prisma.event.findFirst({
|
||||
where: {
|
||||
organizerId: data.organizerId,
|
||||
title: data.title,
|
||||
scheduledDate: data.scheduledDate,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
registrations: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdEvent) {
|
||||
throw new Error('Failed to retrieve created event');
|
||||
}
|
||||
|
||||
return {
|
||||
id: createdEvent.id,
|
||||
organizerId: createdEvent.organizerId,
|
||||
title: createdEvent.title,
|
||||
description: createdEvent.description,
|
||||
latitude: parseFloat(createdEvent.latitude.toString()),
|
||||
longitude: parseFloat(createdEvent.longitude.toString()),
|
||||
locationName: createdEvent.locationName,
|
||||
scheduledDate: createdEvent.scheduledDate,
|
||||
duration: createdEvent.duration,
|
||||
status: createdEvent.status || 'upcoming',
|
||||
participantCount: createdEvent._count.registrations,
|
||||
litterCollected: createdEvent.litterCollected ? parseFloat(createdEvent.litterCollected.toString()) : undefined,
|
||||
photos: createdEvent.photos ? (Array.isArray(createdEvent.photos) ? createdEvent.photos : []) : undefined,
|
||||
createdAt: createdEvent.createdAt,
|
||||
updatedAt: createdEvent.updatedAt,
|
||||
};
|
||||
},
|
||||
|
||||
async updateEvent(id: string, data: UpdateEventData) {
|
||||
const updateData: any = {};
|
||||
|
||||
if (data.title !== undefined) updateData.title = data.title;
|
||||
if (data.description !== undefined) updateData.description = data.description;
|
||||
if (data.scheduledDate !== undefined) updateData.scheduledDate = data.scheduledDate;
|
||||
if (data.duration !== undefined) updateData.duration = data.duration;
|
||||
if (data.status !== undefined) updateData.status = data.status;
|
||||
if (data.litterCollected !== undefined) updateData.litterCollected = data.litterCollected;
|
||||
if (data.photos !== undefined) updateData.photos = data.photos;
|
||||
|
||||
const event = await prisma.event.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
registrations: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
organizerId: event.organizerId,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
latitude: parseFloat(event.latitude.toString()),
|
||||
longitude: parseFloat(event.longitude.toString()),
|
||||
locationName: event.locationName,
|
||||
scheduledDate: event.scheduledDate,
|
||||
duration: event.duration,
|
||||
status: event.status || 'upcoming',
|
||||
participantCount: event._count.registrations,
|
||||
litterCollected: event.litterCollected ? parseFloat(event.litterCollected.toString()) : undefined,
|
||||
photos: event.photos ? (Array.isArray(event.photos) ? event.photos : []) : undefined,
|
||||
createdAt: event.createdAt,
|
||||
updatedAt: event.updatedAt,
|
||||
};
|
||||
},
|
||||
|
||||
async deleteEvent(id: string) {
|
||||
await prisma.event.delete({
|
||||
where: { id },
|
||||
});
|
||||
},
|
||||
|
||||
async registerForEvent(eventId: string, userId: string) {
|
||||
// Check if already registered
|
||||
const existing = await prisma.eventRegistration.findUnique({
|
||||
where: {
|
||||
userId_eventId: {
|
||||
userId,
|
||||
eventId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new Error('User is already registered for this event');
|
||||
}
|
||||
|
||||
// Create registration
|
||||
const registration = await prisma.eventRegistration.create({
|
||||
data: {
|
||||
userId,
|
||||
eventId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: registration.id,
|
||||
userId: registration.userId,
|
||||
eventId: registration.eventId,
|
||||
registeredAt: registration.registeredAt || new Date(),
|
||||
attended: registration.attended || false,
|
||||
};
|
||||
},
|
||||
|
||||
async unregisterFromEvent(eventId: string, userId: string) {
|
||||
await prisma.eventRegistration.delete({
|
||||
where: {
|
||||
userId_eventId: {
|
||||
userId,
|
||||
eventId,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async getEventRegistrations(eventId: string) {
|
||||
const registrations = await prisma.eventRegistration.findMany({
|
||||
where: { eventId },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
avatarUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return registrations.map((reg) => ({
|
||||
id: reg.id,
|
||||
userId: reg.userId,
|
||||
eventId: reg.eventId,
|
||||
registeredAt: reg.registeredAt || new Date(),
|
||||
attended: reg.attended || false,
|
||||
user: reg.user,
|
||||
}));
|
||||
},
|
||||
|
||||
async isUserRegistered(eventId: string, userId: string): Promise<boolean> {
|
||||
const registration = await prisma.eventRegistration.findUnique({
|
||||
where: {
|
||||
userId_eventId: {
|
||||
userId,
|
||||
eventId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return !!registration;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user