- Automatically register users who scan QR code but aren't pre-registered - Mark walk-in registrations with wasWalkIn flag - Show different success messages for walk-ins vs pre-registered attendees - Check in walk-ins immediately upon registration - Remove 'not registered' error - now handles walk-ins gracefully - Update both QR scan and manual check-in to support walk-ins
609 lines
18 KiB
TypeScript
609 lines
18 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 || null,
|
|
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, equipmentProvided, requiredItems } = 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),
|
|
equipmentProvided: equipmentProvided !== undefined ? Boolean(equipmentProvided) : false,
|
|
requiredItems: Array.isArray(requiredItems) ? requiredItems : [],
|
|
};
|
|
|
|
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(),
|
|
});
|
|
}
|
|
},
|
|
|
|
// Get event attendees
|
|
async getEventAttendees(req: AuthRequest, res: Response) {
|
|
try {
|
|
const { id } = req.params;
|
|
const attendees = await eventService.getEventAttendees(id);
|
|
res.json(attendees);
|
|
} catch (error: any) {
|
|
console.error('Error fetching event attendees:', error);
|
|
res.status(500).json({
|
|
code: 'FETCH_ATTENDEES_ERROR',
|
|
message: 'Failed to fetch event attendees',
|
|
details: error.message,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
},
|
|
|
|
// Update attendee status
|
|
async updateAttendeeStatus(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, attendeeId } = 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 attendee status',
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
const { attended, litterCollected, contributionNote } = req.body;
|
|
|
|
const updatedAttendee = await eventService.updateAttendeeStatus(id, attendeeId, {
|
|
attended,
|
|
litterCollected: litterCollected !== undefined ? parseFloat(litterCollected) : undefined,
|
|
contributionNote,
|
|
});
|
|
|
|
res.json(updatedAttendee);
|
|
} catch (error: any) {
|
|
console.error('Error updating attendee status:', error);
|
|
res.status(500).json({
|
|
code: 'UPDATE_ATTENDEE_ERROR',
|
|
message: 'Failed to update attendee status',
|
|
details: error.message,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
},
|
|
|
|
// Bulk update attendees
|
|
async bulkUpdateAttendees(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 attendees',
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
const { attendees } = req.body;
|
|
|
|
if (!Array.isArray(attendees)) {
|
|
return res.status(400).json({
|
|
code: 'VALIDATION_ERROR',
|
|
message: 'attendees must be an array',
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
await eventService.bulkUpdateAttendees(id, attendees);
|
|
|
|
// Fetch updated attendees
|
|
const updatedAttendees = await eventService.getEventAttendees(id);
|
|
res.json(updatedAttendees);
|
|
} catch (error: any) {
|
|
console.error('Error bulk updating attendees:', error);
|
|
res.status(500).json({
|
|
code: 'BULK_UPDATE_ATTENDEES_ERROR',
|
|
message: 'Failed to bulk update attendees',
|
|
details: error.message,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
},
|
|
|
|
// QR Code Check-in
|
|
async checkInAttendee(req: AuthRequest, res: Response) {
|
|
try {
|
|
const organizerClerkId = req.userId;
|
|
if (!organizerClerkId) {
|
|
return res.status(401).json({
|
|
code: 'UNAUTHORIZED',
|
|
message: 'User not authenticated',
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
const organizerId = await findOrCreateUser(organizerClerkId);
|
|
const { id } = req.params;
|
|
const { clerkUserId } = req.body;
|
|
|
|
if (!clerkUserId) {
|
|
return res.status(400).json({
|
|
code: 'VALIDATION_ERROR',
|
|
message: 'clerkUserId is required',
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
// Verify event exists and user is organizer
|
|
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(),
|
|
});
|
|
}
|
|
|
|
if (event.organizerId !== organizerId) {
|
|
return res.status(403).json({
|
|
code: 'FORBIDDEN',
|
|
message: 'Only the event organizer can check in attendees',
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
// Find or create the attendee user
|
|
const attendeeUserId = await findOrCreateUser(clerkUserId);
|
|
|
|
// Check in the attendee (will auto-register if not registered)
|
|
const result = await eventService.checkInAttendee(id, attendeeUserId);
|
|
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error('Error checking in attendee:', error);
|
|
|
|
if (error.message.includes('already checked in')) {
|
|
return res.status(409).json({
|
|
code: 'ALREADY_CHECKED_IN',
|
|
message: error.message,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
res.status(500).json({
|
|
code: 'CHECKIN_ERROR',
|
|
message: 'Failed to check in attendee',
|
|
details: error.message,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
},
|
|
};
|