| 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; |
| } |
|
|
| |
| export const getEventDays = async (req: IEventDayRequest, res: Response) => { |
| try { |
| const { eventId } = req.params; |
| const userId = req.userId; |
|
|
| |
| const event = await Event.findById(eventId); |
| if (!event) { |
| return res.status(404).json({ message: 'Event not found' }); |
| } |
|
|
| |
| 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.' }); |
| } |
|
|
| |
| let eventDays = await EventDay.find({ event: eventId }) |
| .sort({ dayNumber: 1 }) |
| .populate('attendance.user', 'username profilePhoto') |
| .populate('photos.uploadedBy', 'username'); |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| const stats = await calculateAttendanceStats(eventId); |
|
|
| return res.json({ |
| eventDays: eventDays.map(day => { |
| const now = new Date(); |
| const eventEndTime = new Date(day.endTime); |
|
|
| |
| 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, |
| |
| ...(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 }); |
| } |
| }; |
|
|
| |
| 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 { |
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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; |
|
|
| 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' }); |
| } |
|
|
| |
| 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' |
| }); |
| } |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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' }); |
| } |
|
|
| |
| 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' }); |
| } |
|
|
| |
| try { |
| await deleteEventPhoto(photo.publicId); |
| } catch (cloudinaryErr) { |
| console.error('Failed to delete from Cloudinary:', cloudinaryErr); |
| } |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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' }); |
| } |
|
|
| |
| 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 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' }); |
| } |
|
|
| |
| 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' }); |
| } |
|
|
| |
| const attendance: any = { |
| user: userId, |
| checkInTime: new Date(), |
| checkInMethod, |
| verifiedBy: checkInMethod === 'manual' ? currentUserId : undefined |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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' }); |
| } |
|
|
| |
| 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) { |
| |
| const isParticipant = event.participants.some((p: any) => p.user.toString() === userId); |
| if (!isParticipant) { |
| results.notParticipant.push(userId); |
| continue; |
| } |
|
|
| |
| const existingAttendance = eventDay.attendance.find((a: any) => a.user.toString() === userId); |
| if (existingAttendance) { |
| results.alreadyMarked.push(userId); |
| continue; |
| } |
|
|
| |
| const attendance: any = { |
| user: userId, |
| checkInTime: new Date(), |
| checkInMethod |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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' }); |
| } |
|
|
| |
| 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' |
| }); |
| } |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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' }); |
| } |
|
|
| |
| 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'); |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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' }); |
| } |
|
|
| |
| 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' |
| }); |
| } |
|
|
| |
| 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.' |
| }); |
| } |
|
|
| |
| if (eventDay.hasStarted) { |
| return res.status(400).json({ message: 'Event day already started' }); |
| } |
|
|
| |
| if (eventDay.isCompleted) { |
| return res.status(400).json({ message: 'Event day already completed' }); |
| } |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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' }); |
| } |
|
|
| |
| 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' |
| }); |
| } |
|
|
| |
| if (!eventDay.hasStarted) { |
| return res.status(400).json({ message: 'Event day has not been started yet' }); |
| } |
|
|
| |
| if (eventDay.isCompleted) { |
| return res.status(400).json({ message: 'Event day already completed' }); |
| } |
|
|
| |
| if (eventDay.photos.length === 0) { |
| return res.status(400).json({ |
| message: 'Cannot end event day: No photos uploaded', |
| requirement: 'photos' |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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; |
|
|
| |
| if (eventDay.isCompleted && eventDay.photos.length > 0 && eventDay.attendance.length > 0) { |
| status = 'green'; |
| message = 'Event completed successfully'; |
| } |
| |
| 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'; |
| } |
| } |
| |
| 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 }); |
| } |
| }; |
|
|
|
|