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:
296
server/src/controllers/eventController.ts
Normal file
296
server/src/controllers/eventController.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
|
import { AuthRequest } from '../middleware/auth';
|
||||||
|
import { eventService } from '../services/eventService';
|
||||||
|
|
||||||
|
export const eventController = {
|
||||||
|
// Get all events with optional filters
|
||||||
|
async getEvents(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { status, startDate, endDate, latitude, longitude, radius } = req.query;
|
||||||
|
|
||||||
|
const filters: any = {};
|
||||||
|
if (status) filters.status = status as string;
|
||||||
|
if (startDate) filters.startDate = new Date(startDate as string);
|
||||||
|
if (endDate) filters.endDate = new Date(endDate as string);
|
||||||
|
if (latitude && longitude) {
|
||||||
|
filters.latitude = parseFloat(latitude as string);
|
||||||
|
filters.longitude = parseFloat(longitude as string);
|
||||||
|
if (radius) filters.radius = parseFloat(radius as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await eventService.getEvents(filters);
|
||||||
|
res.json(events);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching events:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
code: 'FETCH_EVENTS_ERROR',
|
||||||
|
message: 'Failed to fetch events',
|
||||||
|
details: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get single event by ID
|
||||||
|
async getEvent(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const event = await eventService.getEventById(id);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return res.status(404).json({
|
||||||
|
code: 'EVENT_NOT_FOUND',
|
||||||
|
message: 'Event not found',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(event);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching event:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
code: 'FETCH_EVENT_ERROR',
|
||||||
|
message: 'Failed to fetch event',
|
||||||
|
details: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create new event
|
||||||
|
async createEvent(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(401).json({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User not authenticated',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, latitude, longitude, locationName, scheduledDate, duration } = req.body;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!title || !description || !latitude || !longitude || !locationName || !scheduledDate || !duration) {
|
||||||
|
return res.status(400).json({
|
||||||
|
code: 'VALIDATION_ERROR',
|
||||||
|
message: 'Missing required fields',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventData = {
|
||||||
|
organizerId: userId,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
latitude: parseFloat(latitude),
|
||||||
|
longitude: parseFloat(longitude),
|
||||||
|
locationName,
|
||||||
|
scheduledDate: new Date(scheduledDate),
|
||||||
|
duration: parseInt(duration),
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = await eventService.createEvent(eventData);
|
||||||
|
res.status(201).json(event);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error creating event:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
code: 'CREATE_EVENT_ERROR',
|
||||||
|
message: 'Failed to create event',
|
||||||
|
details: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update event
|
||||||
|
async updateEvent(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(401).json({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User not authenticated',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
const event = await eventService.getEventById(id);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return res.status(404).json({
|
||||||
|
code: 'EVENT_NOT_FOUND',
|
||||||
|
message: 'Event not found',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is the organizer
|
||||||
|
if (event.organizerId !== userId) {
|
||||||
|
return res.status(403).json({
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
message: 'Only the event organizer can update this event',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedEvent = await eventService.updateEvent(id, req.body);
|
||||||
|
res.json(updatedEvent);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error updating event:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
code: 'UPDATE_EVENT_ERROR',
|
||||||
|
message: 'Failed to update event',
|
||||||
|
details: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete event
|
||||||
|
async deleteEvent(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(401).json({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User not authenticated',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
const event = await eventService.getEventById(id);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return res.status(404).json({
|
||||||
|
code: 'EVENT_NOT_FOUND',
|
||||||
|
message: 'Event not found',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is the organizer
|
||||||
|
if (event.organizerId !== userId) {
|
||||||
|
return res.status(403).json({
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
message: 'Only the event organizer can delete this event',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await eventService.deleteEvent(id);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error deleting event:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
code: 'DELETE_EVENT_ERROR',
|
||||||
|
message: 'Failed to delete event',
|
||||||
|
details: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Register for event
|
||||||
|
async registerForEvent(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(401).json({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User not authenticated',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
const registration = await eventService.registerForEvent(id, userId);
|
||||||
|
res.status(201).json(registration);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error registering for event:', error);
|
||||||
|
|
||||||
|
if (error.message.includes('already registered')) {
|
||||||
|
return res.status(409).json({
|
||||||
|
code: 'ALREADY_REGISTERED',
|
||||||
|
message: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
code: 'REGISTER_EVENT_ERROR',
|
||||||
|
message: 'Failed to register for event',
|
||||||
|
details: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Unregister from event
|
||||||
|
async unregisterFromEvent(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(401).json({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'User not authenticated',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
await eventService.unregisterFromEvent(id, userId);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error unregistering from event:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
code: 'UNREGISTER_EVENT_ERROR',
|
||||||
|
message: 'Failed to unregister from event',
|
||||||
|
details: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get event registrations
|
||||||
|
async getEventRegistrations(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const registrations = await eventService.getEventRegistrations(id);
|
||||||
|
res.json(registrations);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching event registrations:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
code: 'FETCH_REGISTRATIONS_ERROR',
|
||||||
|
message: 'Failed to fetch event registrations',
|
||||||
|
details: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check registration status
|
||||||
|
async getRegistrationStatus(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return res.json({ registered: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
const isRegistered = await eventService.isUserRegistered(id, userId);
|
||||||
|
res.json({ registered: isRegistered });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error checking registration status:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
code: 'CHECK_REGISTRATION_ERROR',
|
||||||
|
message: 'Failed to check registration status',
|
||||||
|
details: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import helmet from 'helmet';
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import reportRoutes from './routes/reportRoutes';
|
import reportRoutes from './routes/reportRoutes';
|
||||||
|
import eventRoutes from './routes/eventRoutes';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@@ -33,6 +34,7 @@ app.get('/health', (req, res) => {
|
|||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
app.use('/api/reports', reportRoutes);
|
app.use('/api/reports', reportRoutes);
|
||||||
|
app.use('/api/events', eventRoutes);
|
||||||
|
|
||||||
// Error handling middleware
|
// Error handling middleware
|
||||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
|||||||
20
server/src/routes/eventRoutes.ts
Normal file
20
server/src/routes/eventRoutes.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { eventController } from '../controllers/eventController';
|
||||||
|
import { requireAuth } from '../middleware/auth';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Public routes
|
||||||
|
router.get('/', eventController.getEvents);
|
||||||
|
router.get('/:id', eventController.getEvent);
|
||||||
|
router.get('/:id/registrations', eventController.getEventRegistrations);
|
||||||
|
|
||||||
|
// Protected routes (require authentication)
|
||||||
|
router.post('/', requireAuth, eventController.createEvent);
|
||||||
|
router.patch('/:id', requireAuth, eventController.updateEvent);
|
||||||
|
router.delete('/:id', requireAuth, eventController.deleteEvent);
|
||||||
|
router.post('/:id/register', requireAuth, eventController.registerForEvent);
|
||||||
|
router.delete('/:id/register', requireAuth, eventController.unregisterFromEvent);
|
||||||
|
router.get('/:id/registration-status', requireAuth, eventController.getRegistrationStatus);
|
||||||
|
|
||||||
|
export default router;
|
||||||
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