feat: implement QR code check-in system for event attendees
- Add UserQRCode component to display unique QR code on user profile - Add QRCodeScanner component for mobile camera-based scanning - Create EventCheckInPage for organizers to scan attendee QR codes - Add backend endpoint POST /api/events/:id/checkin for check-in - Add check-in button to EventDetailModal for organizers - Install qrcode.react and html5-qrcode dependencies - Implement real-time attendee list updates - Add organizer permission validation - Prevent duplicate check-ins - Mobile-optimized with back camera support
This commit is contained in:
@@ -536,4 +536,81 @@ export const eventController = {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 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
|
||||
const result = await eventService.checkInAttendee(id, attendeeUserId);
|
||||
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
console.error('Error checking in attendee:', error);
|
||||
|
||||
if (error.message.includes('not registered')) {
|
||||
return res.status(404).json({
|
||||
code: 'NOT_REGISTERED',
|
||||
message: error.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,5 +20,6 @@ router.get('/:id/registration-status', requireAuth, eventController.getRegistrat
|
||||
router.post('/:id/complete', requireAuth, eventController.completeEvent);
|
||||
router.patch('/:id/attendees/:attendeeId', requireAuth, eventController.updateAttendeeStatus);
|
||||
router.post('/:id/attendees/bulk', requireAuth, eventController.bulkUpdateAttendees);
|
||||
router.post('/:id/checkin', requireAuth, eventController.checkInAttendee);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -491,4 +491,74 @@ export const eventService = {
|
||||
|
||||
return updates;
|
||||
},
|
||||
|
||||
async checkInAttendee(eventId: string, userId: string) {
|
||||
// Check if user is registered
|
||||
const registration = await prisma.eventRegistration.findUnique({
|
||||
where: {
|
||||
userId_eventId: {
|
||||
userId,
|
||||
eventId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
avatarUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!registration) {
|
||||
throw new Error('User is not registered for this event');
|
||||
}
|
||||
|
||||
if (registration.attended) {
|
||||
throw new Error('User is already checked in');
|
||||
}
|
||||
|
||||
// Update attendance
|
||||
const updated = await prisma.eventRegistration.update({
|
||||
where: {
|
||||
userId_eventId: {
|
||||
userId,
|
||||
eventId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
attended: true,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
avatarUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
username: updated.user.username,
|
||||
firstName: updated.user.firstName,
|
||||
lastName: updated.user.lastName,
|
||||
attendee: {
|
||||
id: updated.id,
|
||||
userId: updated.userId,
|
||||
eventId: updated.eventId,
|
||||
registeredAt: updated.registeredAt || new Date(),
|
||||
attended: updated.attended || false,
|
||||
user: updated.user,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user