Files
ocean-cleanup/server/src/controllers/eventController.ts
Paul Bailey c1f362a604 Implement event completion workflow (Task 14)
- Add completeEvent endpoint to backend API
- Create EventCompletionForm component with litter quantity input and photo upload
- Update EventDetailModal to show completion form for organizers
- Add isOrganizer check in EventsPage
- Update event service to support completion
- Display completion data (litter collected, photos) for completed events
2025-11-16 20:21:47 +00:00

409 lines
12 KiB
TypeScript

import { Response } from 'express';
import { AuthRequest } from '../middleware/auth';
import { eventService } from '../services/eventService';
import prisma from '../config/database';
import { clerkClient } from '@clerk/clerk-sdk-node';
// Helper to find or create user from Clerk ID
async function findOrCreateUser(clerkId: string): Promise<string> {
// Check if user exists
let user = await prisma.user.findUnique({
where: { clerkId },
select: { id: true },
});
if (user) {
return user.id;
}
// User doesn't exist, fetch from Clerk and create
try {
const clerkUser = await clerkClient.users.getUser(clerkId);
user = await prisma.user.create({
data: {
clerkId,
email: clerkUser.emailAddresses[0]?.emailAddress || `${clerkId}@temp.com`,
username: clerkUser.username || `user_${clerkId.substring(0, 8)}`,
firstName: clerkUser.firstName,
lastName: clerkUser.lastName,
avatarUrl: clerkUser.imageUrl,
},
select: { id: true },
});
return user.id;
} catch (error) {
console.error('Error creating user from Clerk:', error);
throw new Error('Failed to create user');
}
}
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 clerkId = req.userId;
if (!clerkId) {
return res.status(401).json({
code: 'UNAUTHORIZED',
message: 'User not authenticated',
timestamp: new Date().toISOString(),
});
}
// Find or create user in database
const userId = await findOrCreateUser(clerkId);
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 clerkId = req.userId;
if (!clerkId) {
return res.status(401).json({
code: 'UNAUTHORIZED',
message: 'User not authenticated',
timestamp: new Date().toISOString(),
});
}
const userId = await findOrCreateUser(clerkId);
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 clerkId = req.userId;
if (!clerkId) {
return res.status(401).json({
code: 'UNAUTHORIZED',
message: 'User not authenticated',
timestamp: new Date().toISOString(),
});
}
const userId = await findOrCreateUser(clerkId);
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 clerkId = req.userId;
if (!clerkId) {
return res.status(401).json({
code: 'UNAUTHORIZED',
message: 'User not authenticated',
timestamp: new Date().toISOString(),
});
}
const userId = await findOrCreateUser(clerkId);
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 clerkId = req.userId;
if (!clerkId) {
return res.status(401).json({
code: 'UNAUTHORIZED',
message: 'User not authenticated',
timestamp: new Date().toISOString(),
});
}
const userId = await findOrCreateUser(clerkId);
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 clerkId = req.userId;
if (!clerkId) {
return res.json({ registered: false });
}
const userId = await findOrCreateUser(clerkId);
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(),
});
}
},
// Complete event
async completeEvent(req: AuthRequest, res: Response) {
try {
const clerkId = req.userId;
if (!clerkId) {
return res.status(401).json({
code: 'UNAUTHORIZED',
message: 'User not authenticated',
timestamp: new Date().toISOString(),
});
}
const userId = await findOrCreateUser(clerkId);
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 complete this event',
timestamp: new Date().toISOString(),
});
}
const { litterCollected, photos } = req.body;
// Validation
if (litterCollected === undefined || litterCollected === null) {
return res.status(400).json({
code: 'VALIDATION_ERROR',
message: 'litterCollected is required',
timestamp: new Date().toISOString(),
});
}
const completedEvent = await eventService.completeEvent(id, {
litterCollected: parseFloat(litterCollected),
photos: photos || [],
});
res.json(completedEvent);
} catch (error: any) {
console.error('Error completing event:', error);
res.status(500).json({
code: 'COMPLETE_EVENT_ERROR',
message: 'Failed to complete event',
details: error.message,
timestamp: new Date().toISOString(),
});
}
},
};