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
This commit is contained in:
2025-11-16 20:21:47 +00:00
parent ec70baa498
commit c1f362a604
9 changed files with 295 additions and 6 deletions

View File

@@ -343,4 +343,66 @@ export const eventController = {
});
}
},
// 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(),
});
}
},
};

View File

@@ -16,5 +16,6 @@ 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);
router.post('/:id/complete', requireAuth, eventController.completeEvent);
export default router;

View File

@@ -324,4 +324,40 @@ export const eventService = {
return !!registration;
},
async completeEvent(eventId: string, data: { litterCollected: number; photos?: string[] }) {
const event = await prisma.event.update({
where: { id: eventId },
data: {
status: 'completed',
litterCollected: data.litterCollected,
photos: data.photos || [],
},
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,
};
},
};