import { Request, Response } from 'express'; import EventDay from '../model/eventDay.model'; import Event from '../model/event.model'; import User from '../model/user.model'; import { uploadEventPhoto, deleteEventPhoto, calculateAttendanceStats, generateEventDays } from '../util/eventDay.util'; import { canManageEventDay } from '../util/role.util'; interface IEventDayRequest extends Request { userId?: string; } // ==================== GET ALL DAYS FOR AN EVENT ==================== export const getEventDays = async (req: IEventDayRequest, res: Response) => { try { const { eventId } = req.params; const userId = req.userId; // Verify event exists const event = await Event.findById(eventId); if (!event) { return res.status(404).json({ message: 'Event not found' }); } // Check if user is participant or creator const isCreator = event.createdBy.toString() === userId; const isParticipant = event.participants.some(p => p.user.toString() === userId); if (!isCreator && !isParticipant) { return res.status(403).json({ message: 'Access denied. You must be a participant or creator.' }); } // Get all event days let eventDays = await EventDay.find({ event: eventId }) .sort({ dayNumber: 1 }) .populate('attendance.user', 'username profilePhoto') .populate('photos.uploadedBy', 'username'); // Auto-generate event days if they don't exist (for existing events) if (eventDays.length === 0) { try { await generateEventDays(event); eventDays = await EventDay.find({ event: eventId }) .sort({ dayNumber: 1 }) .populate('attendance.user', 'username profilePhoto') .populate('photos.uploadedBy', 'username'); } catch (err) { console.error('Failed to auto-generate event days:', err); } } // Calculate stats const stats = await calculateAttendanceStats(eventId); return res.json({ eventDays: eventDays.map(day => { const now = new Date(); const eventEndTime = new Date(day.endTime); // Determine status let status: 'green' | 'red' | 'blue'; if (day.isCompleted && day.photos.length > 0 && day.attendance.length > 0) { status = 'green'; } else if (eventEndTime < now && (!day.hasStarted || day.photos.length === 0 || day.attendance.length === 0)) { status = 'red'; } else { status = 'blue'; } return { _id: day._id, date: day.date, dayNumber: day.dayNumber, startTime: day.startTime, endTime: day.endTime, photoCount: day.photos.length, attendanceCount: day.attendance.length, hasStarted: day.hasStarted, isCompleted: day.isCompleted, status, notes: day.notes, weatherCondition: day.weatherCondition, // Only show full details to creator ...(isCreator && { photos: day.photos, attendance: day.attendance, activitiesConducted: day.activitiesConducted, incidentsReported: day.incidentsReported, equipmentUsed: day.equipmentUsed }) }; }), stats }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== GET SPECIFIC DAY DETAILS ==================== export const getEventDayDetails = async (req: IEventDayRequest, res: Response) => { try { const { eventDayId } = req.params; const userId = req.userId; const eventDay = await EventDay.findById(eventDayId) .populate('event') .populate('attendance.user', 'username email profilePhoto') .populate('attendance.verifiedBy', 'username') .populate('photos.uploadedBy', 'username profilePhoto'); if (!eventDay) { return res.status(404).json({ message: 'Event day not found' }); } const event = eventDay.event as any; const isCreator = event.createdBy.toString() === userId; const isParticipant = event.participants.some((p: any) => p.user.toString() === userId); if (!isCreator && !isParticipant) { return res.status(403).json({ message: 'Access denied' }); } const now = new Date(); const eventEndTime = new Date(eventDay.endTime); let status: 'green' | 'red' | 'blue'; if (eventDay.isCompleted && eventDay.photos.length > 0 && eventDay.attendance.length > 0) { status = 'green'; } else if (eventEndTime < now && (!eventDay.hasStarted || eventDay.photos.length === 0 || eventDay.attendance.length === 0)) { status = 'red'; } else { status = 'blue'; } const response: any = { _id: eventDay._id, event: { _id: event._id, title: event.title, code: event.code }, date: eventDay.date, dayNumber: eventDay.dayNumber, startTime: eventDay.startTime, endTime: eventDay.endTime, hasStarted: eventDay.hasStarted, isCompleted: eventDay.isCompleted, status, photos: eventDay.photos, notes: eventDay.notes, weatherCondition: eventDay.weatherCondition }; if (isCreator) { response.attendance = eventDay.attendance; response.activitiesConducted = eventDay.activitiesConducted; response.incidentsReported = eventDay.incidentsReported; response.equipmentUsed = eventDay.equipmentUsed; response.completedAt = eventDay.completedAt; } else { // Show only user's own attendance response.myAttendance = eventDay.attendance.find( (a: any) => a.user._id.toString() === userId ); } return res.json(response); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== UPLOAD DAY PHOTOS ==================== export const uploadDayPhotos = async (req: IEventDayRequest, res: Response) => { try { const { eventDayId } = req.params; const userId = req.userId; const files = req.files as Express.Multer.File[]; const { captions } = req.body; // Array of captions (optional) if (!files || files.length === 0) { return res.status(400).json({ message: 'No files uploaded' }); } const eventDay = await EventDay.findById(eventDayId).populate('event'); if (!eventDay) { return res.status(404).json({ message: 'Event day not found' }); } const event = eventDay.event as any; const user = await User.findById(userId); if (!user) { return res.status(404).json({ message: 'User not found' }); } // Check if user can manage this event day (trainer+ level from same org) const canManage = await canManageEventDay(user, event); if (!canManage) { return res.status(403).json({ message: 'Only trainers, organizations, and admins from the same organization can upload photos' }); } // Upload each file to Cloudinary const uploadedPhotos = []; const captionsArray = captions ? (Array.isArray(captions) ? captions : [captions]) : []; for (let i = 0; i < files.length; i++) { const file = files[i]; const caption = captionsArray[i] || undefined; try { const { url, publicId } = await uploadEventPhoto( file, event._id.toString(), eventDay.dayNumber ); const photo = { url, publicId, caption, uploadedBy: userId as any, uploadedAt: new Date() }; eventDay.photos.push(photo as any); uploadedPhotos.push(photo); } catch (uploadErr) { console.error('Failed to upload photo:', uploadErr); } } // Auto-start event day when first photo is uploaded if (!eventDay.hasStarted && uploadedPhotos.length > 0) { eventDay.hasStarted = true; } await eventDay.save(); return res.json({ message: `${uploadedPhotos.length} photo(s) uploaded successfully`, photos: uploadedPhotos }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== DELETE DAY PHOTO ==================== export const deleteDayPhoto = async (req: IEventDayRequest, res: Response) => { try { const { eventDayId, photoId } = req.params; const userId = req.userId; const eventDay = await EventDay.findById(eventDayId).populate('event'); if (!eventDay) { return res.status(404).json({ message: 'Event day not found' }); } const event = eventDay.event as any; const user = await User.findById(userId); if (!user) { return res.status(404).json({ message: 'User not found' }); } // Check if user can manage this event day const canManage = await canManageEventDay(user, event); if (!canManage) { return res.status(403).json({ message: 'Only trainers, organizations, and admins from the same organization can delete photos' }); } const photo = eventDay.photos.find((p: any) => p._id.toString() === photoId); if (!photo) { return res.status(404).json({ message: 'Photo not found' }); } // Delete from Cloudinary try { await deleteEventPhoto(photo.publicId); } catch (cloudinaryErr) { console.error('Failed to delete from Cloudinary:', cloudinaryErr); } // Remove from database eventDay.photos = eventDay.photos.filter((p: any) => p._id.toString() !== photoId) as any; await eventDay.save(); return res.json({ message: 'Photo deleted successfully' }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== MARK ATTENDANCE ==================== export const markAttendance = async (req: IEventDayRequest, res: Response) => { try { const { eventDayId } = req.params; const currentUserId = req.userId; const { userId, checkInMethod, latitude, longitude } = req.body; if (!userId || !checkInMethod) { return res.status(400).json({ message: 'userId and checkInMethod required' }); } const eventDay = await EventDay.findById(eventDayId).populate('event'); if (!eventDay) { return res.status(404).json({ message: 'Event day not found' }); } const event = eventDay.event as any; const user = await User.findById(currentUserId); if (!user) { return res.status(404).json({ message: 'User not found' }); } // Check if user can manage this event day const canManage = await canManageEventDay(user, event); if (!canManage) { return res.status(403).json({ message: 'Only trainers, organizations, and admins from the same organization can mark attendance' }); } // Check if user is a participant const isParticipant = event.participants.some((p: any) => p.user.toString() === userId); if (!isParticipant) { return res.status(400).json({ message: 'User is not a participant of this event' }); } // Check if already marked const existingAttendance = eventDay.attendance.find((a: any) => a.user.toString() === userId); if (existingAttendance) { return res.status(400).json({ message: 'Attendance already marked for this user' }); } // Create attendance record const attendance: any = { user: userId, checkInTime: new Date(), checkInMethod, verifiedBy: checkInMethod === 'manual' ? currentUserId : undefined }; // Add location if provided if (latitude && longitude) { attendance.checkInLocation = { type: 'Point', coordinates: [parseFloat(longitude), parseFloat(latitude)] }; } eventDay.attendance.push(attendance); await eventDay.save(); return res.json({ message: 'Attendance marked successfully', attendance }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== BULK MARK ATTENDANCE (HOTSPOT) ==================== export const bulkMarkAttendance = async (req: IEventDayRequest, res: Response) => { try { const { eventDayId } = req.params; const currentUserId = req.userId; const { userIds, checkInMethod = 'hotspot', latitude, longitude } = req.body; if (!userIds || !Array.isArray(userIds) || userIds.length === 0) { return res.status(400).json({ message: 'userIds array required' }); } const eventDay = await EventDay.findById(eventDayId).populate('event'); if (!eventDay) { return res.status(404).json({ message: 'Event day not found' }); } const event = eventDay.event as any; const user = await User.findById(currentUserId); if (!user) { return res.status(404).json({ message: 'User not found' }); } // Check if user can manage this event day const canManage = await canManageEventDay(user, event); if (!canManage) { return res.status(403).json({ message: 'Only trainers, organizations, and admins from the same organization can mark attendance' }); } const results = { marked: [] as string[], alreadyMarked: [] as string[], notParticipant: [] as string[] }; for (const userId of userIds) { // Check if user is a participant const isParticipant = event.participants.some((p: any) => p.user.toString() === userId); if (!isParticipant) { results.notParticipant.push(userId); continue; } // Check if already marked const existingAttendance = eventDay.attendance.find((a: any) => a.user.toString() === userId); if (existingAttendance) { results.alreadyMarked.push(userId); continue; } // Create attendance record const attendance: any = { user: userId, checkInTime: new Date(), checkInMethod }; // Add location if provided if (latitude && longitude) { attendance.checkInLocation = { type: 'Point', coordinates: [parseFloat(longitude), parseFloat(latitude)] }; } eventDay.attendance.push(attendance); results.marked.push(userId); } await eventDay.save(); return res.json({ message: `Attendance marked for ${results.marked.length} user(s)`, results }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== UPDATE DAY METADATA ==================== export const updateDayMetadata = async (req: IEventDayRequest, res: Response) => { try { const { eventDayId } = req.params; const userId = req.userId; const { notes, weatherCondition, activitiesConducted, incidentsReported, equipmentUsed, isCompleted } = req.body; const eventDay = await EventDay.findById(eventDayId).populate('event'); if (!eventDay) { return res.status(404).json({ message: 'Event day not found' }); } const event = eventDay.event as any; const user = await User.findById(userId); if (!user) { return res.status(404).json({ message: 'User not found' }); } // Check if user can manage this event day const canManage = await canManageEventDay(user, event); if (!canManage) { return res.status(403).json({ message: 'Only trainers, organizations, and admins from the same organization can update day metadata' }); } // Update fields if (notes !== undefined) eventDay.notes = notes; if (weatherCondition !== undefined) eventDay.weatherCondition = weatherCondition; if (activitiesConducted !== undefined) eventDay.activitiesConducted = activitiesConducted; if (incidentsReported !== undefined) eventDay.incidentsReported = incidentsReported; if (equipmentUsed !== undefined) eventDay.equipmentUsed = equipmentUsed; if (isCompleted !== undefined) eventDay.isCompleted = isCompleted; await eventDay.save(); return res.json({ message: 'Event day updated successfully', eventDay }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== GET ATTENDANCE REPORT ==================== export const getAttendanceReport = async (req: IEventDayRequest, res: Response) => { try { const { eventId } = req.params; const userId = req.userId; const event = await Event.findById(eventId).populate('participants.user', 'username email profilePhoto'); if (!event) { return res.status(404).json({ message: 'Event not found' }); } // Only event creator can view full attendance report if (event.createdBy.toString() !== userId) { return res.status(403).json({ message: 'Only event creator can view attendance report' }); } const stats = await calculateAttendanceStats(eventId); const eventDays = await EventDay.find({ event: eventId }) .sort({ dayNumber: 1 }) .populate('attendance.user', 'username email profilePhoto'); // Build participant-wise report const participantReport = event.participants.map(participant => { const user = participant.user as any; const attendanceDays = eventDays.filter(day => day.attendance.some((a: any) => a.user._id.toString() === user._id.toString()) ); return { user: { _id: user._id, username: user.username, email: user.email, profilePhoto: user.profilePhoto }, daysAttended: attendanceDays.length, totalDays: stats.totalDays, attendancePercentage: stats.totalDays > 0 ? Math.round((attendanceDays.length / stats.totalDays) * 100) : 0, attendanceDates: attendanceDays.map(day => ({ date: day.date, dayNumber: day.dayNumber, checkInTime: day.attendance.find((a: any) => a.user._id.toString() === user._id.toString())?.checkInTime })) }; }); return res.json({ event: { _id: event._id, title: event.title, code: event.code, startDate: event.startDate, endDate: event.endDate }, stats, participantReport, dailyBreakdown: stats.dailyStats }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== START EVENT DAY ==================== export const startEventDay = async (req: IEventDayRequest, res: Response) => { try { const { eventDayId } = req.params; const userId = req.userId; const eventDay = await EventDay.findById(eventDayId).populate('event'); if (!eventDay) { return res.status(404).json({ message: 'Event day not found' }); } const event = eventDay.event as any; const user = await User.findById(userId); if (!user) { return res.status(404).json({ message: 'User not found' }); } // Check if user can manage this event day const canManage = await canManageEventDay(user, event); if (!canManage) { return res.status(403).json({ message: 'Only trainers, organizations, and admins from the same organization can start event day' }); } // Check if event day date is today or in the past (can't start future events) const today = new Date(); today.setHours(0, 0, 0, 0); const eventDate = new Date(eventDay.date); eventDate.setHours(0, 0, 0, 0); if (eventDate > today) { return res.status(400).json({ message: 'Cannot start event day in the future. Event day can only be started on or after its scheduled date.' }); } // Check if already started if (eventDay.hasStarted) { return res.status(400).json({ message: 'Event day already started' }); } // Check if already completed if (eventDay.isCompleted) { return res.status(400).json({ message: 'Event day already completed' }); } // Set as started eventDay.hasStarted = true; await eventDay.save(); return res.json({ message: 'Event day started successfully', eventDay: { _id: eventDay._id, date: eventDay.date, dayNumber: eventDay.dayNumber, hasStarted: eventDay.hasStarted, startTime: eventDay.startTime, endTime: eventDay.endTime } }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== END EVENT DAY ==================== export const endEventDay = async (req: IEventDayRequest, res: Response) => { try { const { eventDayId } = req.params; const userId = req.userId; const eventDay = await EventDay.findById(eventDayId).populate('event'); if (!eventDay) { return res.status(404).json({ message: 'Event day not found' }); } const event = eventDay.event as any; const user = await User.findById(userId); if (!user) { return res.status(404).json({ message: 'User not found' }); } // Check if user can manage this event day const canManage = await canManageEventDay(user, event); if (!canManage) { return res.status(403).json({ message: 'Only trainers, organizations, and admins from the same organization can end event day' }); } // Check if started if (!eventDay.hasStarted) { return res.status(400).json({ message: 'Event day has not been started yet' }); } // Check if already completed if (eventDay.isCompleted) { return res.status(400).json({ message: 'Event day already completed' }); } // Validate completion requirements if (eventDay.photos.length === 0) { return res.status(400).json({ message: 'Cannot end event day: No photos uploaded', requirement: 'photos' }); } // if (eventDay.attendance.length === 0) { // return res.status(400).json({ // message: 'Cannot end event day: No attendance recorded', // requirement: 'attendance' // }); // } // Mark as completed eventDay.isCompleted = true; await eventDay.save(); return res.json({ message: 'Event day ended successfully', eventDay: { _id: eventDay._id, date: eventDay.date, dayNumber: eventDay.dayNumber, isCompleted: eventDay.isCompleted, completedAt: eventDay.completedAt, photoCount: eventDay.photos.length, attendanceCount: eventDay.attendance.length } }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== GET EVENT DAY STATUS ==================== export const getEventDayStatus = async (req: IEventDayRequest, res: Response) => { try { const { eventDayId } = req.params; const eventDay = await EventDay.findById(eventDayId); if (!eventDay) { return res.status(404).json({ message: 'Event day not found' }); } const now = new Date(); const eventEndTime = new Date(eventDay.endTime); let status: 'green' | 'red' | 'blue'; let message: string; // Green: Completed with photos and attendance if (eventDay.isCompleted && eventDay.photos.length > 0 && eventDay.attendance.length > 0) { status = 'green'; message = 'Event completed successfully'; } // Red: Event day has passed but not completed properly else if (eventEndTime < now) { if (!eventDay.hasStarted) { status = 'red'; message = 'Event was not conducted'; } else if (eventDay.photos.length === 0) { status = 'red'; message = 'Event missed: No photos uploaded'; } else if (eventDay.attendance.length === 0) { status = 'red'; message = 'Event missed: No attendance recorded'; } else { status = 'red'; message = 'Event not properly completed'; } } // Blue: Future dates or planned but not yet conducted else { status = 'blue'; message = eventDay.hasStarted ? 'Event in progress' : 'Event planned'; } return res.json({ status, message, eventDay: { _id: eventDay._id, date: eventDay.date, dayNumber: eventDay.dayNumber, startTime: eventDay.startTime, endTime: eventDay.endTime, hasStarted: eventDay.hasStarted, isCompleted: eventDay.isCompleted, photoCount: eventDay.photos.length, attendanceCount: eventDay.attendance.length } }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } };