feat: auto-register walk-in attendees during check-in

- 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
This commit is contained in:
2025-11-20 19:10:29 +00:00
parent 2102f053da
commit 57b7cd2308
4 changed files with 66 additions and 17 deletions

View File

@@ -582,20 +582,12 @@ export const eventController = {
// Find or create the attendee user // Find or create the attendee user
const attendeeUserId = await findOrCreateUser(clerkUserId); const attendeeUserId = await findOrCreateUser(clerkUserId);
// Check in the attendee // Check in the attendee (will auto-register if not registered)
const result = await eventService.checkInAttendee(id, attendeeUserId); const result = await eventService.checkInAttendee(id, attendeeUserId);
res.json(result); res.json(result);
} catch (error: any) { } catch (error: any) {
console.error('Error checking in attendee:', error); 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')) { if (error.message.includes('already checked in')) {
return res.status(409).json({ return res.status(409).json({

View File

@@ -495,7 +495,7 @@ export const eventService = {
async checkInAttendee(eventId: string, userId: string) { async checkInAttendee(eventId: string, userId: string) {
// Check if user is registered // Check if user is registered
const registration = await prisma.eventRegistration.findUnique({ let registration = await prisma.eventRegistration.findUnique({
where: { where: {
userId_eventId: { userId_eventId: {
userId, userId,
@@ -515,15 +515,49 @@ export const eventService = {
}, },
}); });
// If not registered, register them now (walk-in)
if (!registration) { if (!registration) {
throw new Error('User is not registered for this event'); registration = await prisma.eventRegistration.create({
data: {
userId,
eventId,
attended: true, // Check them in immediately
},
include: {
user: {
select: {
id: true,
username: true,
firstName: true,
lastName: true,
avatarUrl: true,
},
},
},
});
return {
success: true,
username: registration.user.username,
firstName: registration.user.firstName,
lastName: registration.user.lastName,
wasWalkIn: true, // Flag to indicate this was a walk-in registration
attendee: {
id: registration.id,
userId: registration.userId,
eventId: registration.eventId,
registeredAt: registration.registeredAt || new Date(),
attended: registration.attended || false,
user: registration.user,
},
};
} }
if (registration.attended) { if (registration.attended) {
throw new Error('User is already checked in'); throw new Error('User is already checked in');
} }
// Update attendance // Update attendance for pre-registered user
const updated = await prisma.eventRegistration.update({ const updated = await prisma.eventRegistration.update({
where: { where: {
userId_eventId: { userId_eventId: {
@@ -552,6 +586,7 @@ export const eventService = {
username: updated.user.username, username: updated.user.username,
firstName: updated.user.firstName, firstName: updated.user.firstName,
lastName: updated.user.lastName, lastName: updated.user.lastName,
wasWalkIn: false,
attendee: { attendee: {
id: updated.id, id: updated.id,
userId: updated.userId, userId: updated.userId,

View File

@@ -55,7 +55,13 @@ export function EventCheckInPage() {
try { try {
// Send the Clerk ID to backend and let it handle the lookup // Send the Clerk ID to backend and let it handle the lookup
const response = await apiClient.post<{ success: boolean; username?: string; firstName?: string; lastName?: string }>( const response = await apiClient.post<{
success: boolean;
username?: string;
firstName?: string;
lastName?: string;
wasWalkIn?: boolean;
}>(
`/events/${eventId}/checkin`, `/events/${eventId}/checkin`,
{ clerkUserId } { clerkUserId }
); );
@@ -64,7 +70,11 @@ export function EventCheckInPage() {
? `${response.firstName} ${response.lastName}` ? `${response.firstName} ${response.lastName}`
: response.username || 'user'; : response.username || 'user';
setSuccessMessage(`Successfully checked in ${displayName}`); if (response.wasWalkIn) {
setSuccessMessage(`✨ ${displayName} registered and checked in as walk-in!`);
} else {
setSuccessMessage(`✓ Successfully checked in ${displayName}`);
}
// Reload attendees to get updated status // Reload attendees to get updated status
setTimeout(() => { setTimeout(() => {
@@ -255,7 +265,13 @@ export function EventCheckInPage() {
<button <button
onClick={async () => { onClick={async () => {
try { try {
const response = await apiClient.post<{ success: boolean; username?: string; firstName?: string; lastName?: string }>( const response = await apiClient.post<{
success: boolean;
username?: string;
firstName?: string;
lastName?: string;
wasWalkIn?: boolean;
}>(
`/events/${eventId}/checkin`, `/events/${eventId}/checkin`,
{ clerkUserId: attendee.user.clerkId } { clerkUserId: attendee.user.clerkId }
); );
@@ -264,7 +280,11 @@ export function EventCheckInPage() {
? `${response.firstName} ${response.lastName}` ? `${response.firstName} ${response.lastName}`
: response.username || attendee.user.username; : response.username || attendee.user.username;
setSuccessMessage(`Successfully checked in ${displayName}`); if (response.wasWalkIn) {
setSuccessMessage(`✨ ${displayName} registered and checked in as walk-in!`);
} else {
setSuccessMessage(`✓ Successfully checked in ${displayName}`);
}
// Reload attendees to get updated status // Reload attendees to get updated status
setTimeout(() => { setTimeout(() => {

View File

@@ -98,7 +98,9 @@ export function UserProfilePage() {
areasOfInterest: profileData.areasOfInterest || [], areasOfInterest: profileData.areasOfInterest || [],
}); });
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load profile'); console.error('Error loading profile data:', err);
const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
setError(errorMessage);
} finally { } finally {
setLoading(false); setLoading(false);
} }