Prashikshak / API /src /controller /event.controller.ts
Abhisingh-18's picture
Initial commit: Prashikshak - disaster management training platform
9a92a42
Raw
History Blame Contribute Delete
26.7 kB
import { Request, Response } from 'express';
import Event, { IEvent } from '../model/event.model';
import EventDay from '../model/eventDay.model';
import User from '../model/user.model';
import crypto from 'crypto';
import sendMail from '../util/mailer.util';
import mongoose from 'mongoose';
import { deleteEventDays } from '../util/eventDay.util';
interface IEventRequest extends Request {
userId?: string;
}
// Helper: Generate unique event code
const generateEventCode = async (orgName: string, theme: string): Promise<string> => {
const orgPrefix = orgName.substring(0, 4).toUpperCase().replace(/[^A-Z]/g, '');
const themePrefix = theme.substring(0, 4).toUpperCase().replace(/[^A-Z]/g, '');
const random = Math.floor(1000 + Math.random() * 9000);
let code = `${orgPrefix}-${themePrefix}-${random}`;
let exists = await Event.findOne({ code });
while (exists) {
const newRandom = Math.floor(1000 + Math.random() * 9000);
code = `${orgPrefix}-${themePrefix}-${newRandom}`;
exists = await Event.findOne({ code });
}
return code;
};
// Helper: Calculate distance between two coordinates (Haversine formula)
const calculateDistance = (
lat1: number, lon1: number,
lat2: number, lon2: number
): number => {
const R = 6371e3; // Earth radius in meters
const φ1 = lat1 * Math.PI / 180;
const φ2 = lat2 * Math.PI / 180;
const Δφ = (lat2 - lat1) * Math.PI / 180;
const Δλ = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos1) * Math.cos2) *
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in meters
};
// ==================== CREATE EVENT ====================
export const createEvent = async (req: IEventRequest, res: Response) => {
try {
const userId = req.userId;
const user = await User.findById(userId).populate('organization');
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
// Allow trainers, organizations, and admins to create events
const allowedRoles = ['trainer', 'organization', 'admin'];
if (!allowedRoles.includes(user.role)) {
return res.status(403).json({
message: 'Only trainers, organizations, and admins can create events'
});
}
// Verification checks based on role
if (user.role === 'trainer') {
// Check if trainer is verified
if (user.organizationVerificationStatus !== 'verified') {
return res.status(403).json({
message: 'Only verified trainers can create events',
verificationStatus: user.organizationVerificationStatus
});
}
} else if (user.role === 'organization') {
// Check if organization is verified
if (user.verificationStatus !== 'approved') {
return res.status(403).json({
message: 'Only verified organizations can create events',
verificationStatus: user.verificationStatus
});
}
}
// Admins don't need verification checks
const {
title, theme, description, capacity,
startDate, endDate, type, venue,
longitude, latitude, geofenceRadius,
onlineLink, allowSelfJoin = true,
defaultStartTime = "09:00",
defaultEndTime = "17:00"
} = req.body;
// Validation
if (!title || !theme || !capacity || !startDate || !endDate || !type) {
return res.status(400).json({ message: 'Missing required fields' });
}
if (new Date(endDate) <= new Date(startDate)) {
return res.status(400).json({ message: 'End date must be after start date' });
}
// Validate time format (HH:MM)
const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
if (!timeRegex.test(defaultStartTime) || !timeRegex.test(defaultEndTime)) {
return res.status(400).json({ message: 'Invalid time format. Use HH:MM (e.g., 16:00)' });
}
// Validate end time is after start time
const [startHour, startMin] = defaultStartTime.split(':').map(Number);
const [endHour, endMin] = defaultEndTime.split(':').map(Number);
if (endHour < startHour || (endHour === startHour && endMin <= startMin)) {
return res.status(400).json({ message: 'End time must be after start time' });
}
if ((type === 'in-person' || type === 'hybrid') && (!longitude || !latitude || !venue)) {
return res.status(400).json({
message: 'Location (longitude, latitude) and venue required for in-person/hybrid events'
});
}
if ((type === 'online' || type === 'hybrid') && !onlineLink) {
return res.status(400).json({ message: 'Online link required for online/hybrid events' });
}
// Generate unique code and join token
// Determine organization based on user role
let eventOrganization: any;
let orgName: string;
if (user.role === 'trainer') {
// Trainers: use their organization
eventOrganization = user.organization;
orgName = (user.organization as any)?.username || 'ORG';
} else if (user.role === 'organization') {
// Organizations: use themselves as the organization
eventOrganization = userId;
orgName = user.username || 'ORG';
} else {
// Admins: use their organization if they have one, otherwise use their own ID
eventOrganization = user.organization || userId;
orgName = user.organization ? (user.organization as any)?.username : user.username || 'ORG';
}
const code = await generateEventCode(orgName, theme);
const joinToken = crypto.randomBytes(32).toString('hex');
const eventData: any = {
title,
code,
theme,
description,
organization: eventOrganization,
createdBy: userId,
capacity,
startDate: new Date(startDate),
endDate: new Date(endDate),
defaultStartTime,
defaultEndTime,
type,
joinToken,
allowSelfJoin,
status: 'published'
};
if (type === 'in-person' || type === 'hybrid') {
eventData.venue = venue;
eventData.location = {
type: 'Point',
coordinates: [parseFloat(longitude), parseFloat(latitude)]
};
eventData.geofenceRadius = geofenceRadius || 100;
}
if (type === 'online' || type === 'hybrid') {
eventData.onlineLink = onlineLink;
}
const event = new Event(eventData);
await event.save();
return res.status(201).json({
message: 'Event created successfully',
event: {
_id: event._id,
title: event.title,
code: event.code,
theme: event.theme,
startDate: event.startDate,
endDate: event.endDate,
type: event.type,
capacity: event.capacity,
joinToken: event.joinToken
}
});
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== SEARCH EVENTS ====================
export const searchEvents = async (req: IEventRequest, res: Response) => {
try {
const {
query, // text search
theme,
organization,
type,
status = 'published',
startDate,
endDate,
page = 1,
limit = 20
} = req.query;
const filter: any = { status };
// Text search on title, theme, description
if (query) {
filter.$text = { $search: query as string };
}
if (theme) filter.theme = theme;
if (organization) filter.organization = organization;
if (type) filter.type = type;
// Date range filter
if (startDate || endDate) {
filter.startDate = {};
if (startDate) filter.startDate.$gte = new Date(startDate as string);
if (endDate) filter.startDate.$lte = new Date(endDate as string);
}
const skip = (Number(page) - 1) * Number(limit);
const events = await Event.find(filter)
.populate('organization', 'username organizationType')
.populate('createdBy', 'username profilePhoto')
.select('-joinToken -participants.attendance')
.sort({ startDate: 1 })
.skip(skip)
.limit(Number(limit));
const total = await Event.countDocuments(filter);
// Add participant count to each event
const eventsWithCount = events.map(event => ({
...event.toObject(),
participantCount: event.participants.length,
isCapacityFull: event.participants.length >= event.capacity
}));
return res.json({
events: eventsWithCount,
pagination: {
total,
page: Number(page),
limit: Number(limit),
totalPages: Math.ceil(total / Number(limit))
}
});
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET NEARBY EVENTS ====================
export const getNearbyEvents = async (req: IEventRequest, res: Response) => {
try {
const { longitude, latitude, maxDistance = 50000 } = req.query; // default 50km
if (!longitude || !latitude) {
return res.status(400).json({ message: 'Location (longitude, latitude) required' });
}
const events = await Event.find({
status: 'published',
location: {
$near: {
$geometry: {
type: 'Point',
coordinates: [parseFloat(longitude as string), parseFloat(latitude as string)]
},
$maxDistance: Number(maxDistance)
}
},
startDate: { $gte: new Date() } // Only upcoming events
})
.populate('organization', 'username organizationType')
.populate('createdBy', 'username profilePhoto')
.select('-joinToken -participants.attendance')
.limit(20);
const eventsWithDistance = events.map(event => {
const distance = event.location ? calculateDistance(
parseFloat(latitude as string),
parseFloat(longitude as string),
event.location.coordinates[1],
event.location.coordinates[0]
) : null;
return {
...event.toObject(),
distance: distance ? Math.round(distance) : null,
participantCount: event.participants.length,
isCapacityFull: event.participants.length >= event.capacity
};
});
return res.json({ events: eventsWithDistance });
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET EVENT DASHBOARD ====================
export const getEventDashboard = async (req: IEventRequest, res: Response) => {
try {
const userId = req.userId;
const now = new Date();
// User's upcoming events
const upcomingEvents = await Event.find({
'participants.user': userId,
startDate: { $gt: now },
status: 'published'
})
.populate('organization', 'username')
.sort({ startDate: 1 })
.limit(10);
// User's past events
const pastEvents = await Event.find({
'participants.user': userId,
endDate: { $lt: now },
status: { $in: ['completed', 'published'] }
})
.populate('organization', 'username')
.sort({ endDate: -1 })
.limit(10);
// Recommended events (same themes as user's past events)
const userThemes = [...new Set(pastEvents.map(e => e.theme))];
const recommended = await Event.find({
status: 'published',
startDate: { $gt: now },
theme: { $in: userThemes },
'participants.user': { $ne: userId }
})
.populate('organization', 'username')
.limit(5);
return res.json({
upcoming: upcomingEvents.map(e => ({
...e.toObject(),
participantCount: e.participants.length
})),
past: pastEvents.map(e => {
const participantCount = e.participants.length;
return {
...e.toObject(),
participantCount
};
}),
recommended: recommended.map(e => ({
...e.toObject(),
participantCount: e.participants.length
}))
});
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== JOIN EVENT BY CODE ====================
export const joinEventByCode = async (req: IEventRequest, res: Response) => {
try {
const userId = req.userId;
const { code } = req.body;
if (!code) {
return res.status(400).json({ message: 'Event code required' });
}
const event = await Event.findOne({ code: code.toUpperCase(), status: 'published' });
if (!event) {
return res.status(404).json({ message: 'Event not found or not published' });
}
// Check if already joined
if (event.participants.find(p => p.user.toString() === userId)) {
return res.status(400).json({ message: 'Already joined this event' });
}
// Check capacity
if (event.participants.length >= event.capacity) {
// Add to waitlist
if (!event.waitlist.includes(userId as any)) {
event.waitlist.push(userId as any);
await event.save();
}
return res.status(400).json({
message: 'Event is full. You have been added to the waitlist.',
waitlisted: true
});
}
// Add participant
event.participants.push({
user: userId as any,
joinedAt: new Date(),
joinMethod: 'code'
} as any);
await event.save();
// Send confirmation email
const user = await User.findById(userId);
if (user) {
await sendMail({
to: user.email,
subject: `Registered for ${event.title}`,
text: `You have successfully registered for ${event.title} (${event.code})`,
html: `<p>You have successfully registered for <strong>${event.title}</strong></p>
<p>Code: ${event.code}</p>
<p>Start: ${event.startDate.toLocaleString()}</p>`
});
}
return res.json({
message: 'Successfully joined event',
event: {
_id: event._id,
title: event.title,
code: event.code,
startDate: event.startDate,
endDate: event.endDate,
type: event.type,
venue: event.venue,
onlineLink: event.onlineLink
}
});
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== JOIN EVENT BY LINK ====================
export const joinEventByLink = async (req: IEventRequest, res: Response) => {
try {
const userId = req.userId;
const { eventId, token } = req.body;
if (!eventId || !token) {
return res.status(400).json({ message: 'Event ID and token required' });
}
const event = await Event.findOne({
_id: eventId,
joinToken: token,
status: 'published'
});
if (!event) {
return res.status(404).json({ message: 'Invalid event link' });
}
// Check if already joined
if (event.participants.find(p => p.user.toString() === userId)) {
return res.status(400).json({ message: 'Already joined this event' });
}
// Check capacity
if (event.participants.length >= event.capacity) {
if (!event.waitlist.includes(userId as any)) {
event.waitlist.push(userId as any);
await event.save();
}
return res.status(400).json({
message: 'Event is full. You have been added to the waitlist.',
waitlisted: true
});
}
// Add participant
event.participants.push({
user: userId as any,
joinedAt: new Date(),
joinMethod: 'link'
} as any);
await event.save();
return res.json({
message: 'Successfully joined event',
event: {
_id: event._id,
title: event.title,
code: event.code,
startDate: event.startDate,
endDate: event.endDate
}
});
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET EVENT DETAILS ====================
export const getEventDetails = async (req: IEventRequest, res: Response) => {
try {
const { eventId } = req.params;
const userId = req.userId;
const event = await Event.findById(eventId)
.populate('organization', 'username email organizationType')
.populate('createdBy', 'username email profilePhoto workDesignation')
.populate('participants.user', 'username profilePhoto');
if (!event) {
return res.status(404).json({ message: 'Event not found' });
}
const isParticipant = event.participants.find(p => p.user._id.toString() === userId);
const isCreator = event.createdBy._id.toString() === userId;
const response: any = {
...event.toObject(),
participantCount: event.participants.length,
isCapacityFull: event.participants.length >= event.capacity,
isJoined: !!isParticipant,
isCreator
};
// Only show join token to creator
if (!isCreator) {
delete response.joinToken;
}
// Only show participant details if joined or creator
if (!isParticipant && !isCreator) {
response.participants = undefined;
}
return res.json(response);
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET CREATOR'S EVENTS ====================
export const getTrainerEvents = async (req: IEventRequest, res: Response) => {
try {
const creatorId = req.userId;
const { status } = req.query;
const filter: any = { createdBy: creatorId };
if (status) filter.status = status;
const events = await Event.find(filter)
.populate('organization', 'username')
.sort({ startDate: -1 });
// Get day-level statistics for each event
const eventsWithStats = await Promise.all(events.map(async event => {
const eventDays = await EventDay.find({ event: event._id });
const totalDays = eventDays.length;
const completedDays = eventDays.filter(day => day.isCompleted).length;
// Calculate total attendance across all days
const totalAttendance = eventDays.reduce((sum, day) => sum + day.attendance.length, 0);
const averageAttendancePerDay = totalDays > 0 ? totalAttendance / totalDays : 0;
return {
...event.toObject(),
participantCount: event.participants.length,
totalDays,
completedDays,
averageAttendancePerDay: Math.round(averageAttendancePerDay * 100) / 100,
waitlistCount: event.waitlist.length
};
}));
return res.json({ events: eventsWithStats });
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== UPDATE EVENT ====================
export const updateEvent = async (req: IEventRequest, res: Response) => {
try {
const creatorId = req.userId;
const { eventId } = req.params;
const updates = req.body;
const event = await Event.findOne({ _id: eventId, createdBy: creatorId });
if (!event) {
return res.status(404).json({ message: 'Event not found or unauthorized' });
}
// Don't allow changing certain fields
delete updates.code;
delete updates.joinToken;
delete updates.organization;
delete updates.createdBy;
delete updates.participants;
Object.assign(event, updates);
await event.save();
return res.json({
message: 'Event updated successfully',
event
});
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== DELETE EVENT ====================
export const deleteEvent = async (req: IEventRequest, res: Response) => {
try {
const creatorId = req.userId;
const { eventId } = req.params;
const event = await Event.findOne({
_id: eventId,
createdBy: creatorId
});
if (!event) {
return res.status(404).json({ message: 'Event not found or unauthorized' });
}
// Delete all associated EventDay records and photos
await deleteEventDays(eventId);
// Delete the event
await Event.findByIdAndDelete(eventId);
return res.json({ message: 'Event deleted successfully' });
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET ORGANIZATION EVENT STATS ====================
export const getOrganizationEventStats = async (req: IEventRequest, res: Response) => {
try {
const { organizationId } = req.params;
// Calculate date range (last 365 days)
const endDate = new Date();
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 1);
const events = await Event.aggregate([
{
$match: {
organization: new mongoose.Types.ObjectId(organizationId),
startDate: { $gte: startDate, $lte: endDate },
status: { $in: ['published', 'ongoing', 'completed'] }
}
},
{
$group: {
_id: { $dateToString: { format: "%Y-%m-%d", date: "$startDate" } },
count: { $sum: 1 }
}
},
{
$project: {
date: "$_id",
count: 1,
_id: 0
}
}
]);
return res.json({ stats: events });
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET LIVE EVENTS ====================
// Returns events that are currently ongoing (within date range) but don't have started training sessions
export const getLiveEvents = async (req: IEventRequest, res: Response) => {
try {
const now = new Date();
// Find all events that have started training sessions
const startedTrainingDays = await EventDay.find({
hasStarted: true,
isCompleted: false
}).distinct('event');
// Find events that are within their date range but NOT in the started training list
const ongoingEvents = await Event.find({
status: { $in: ['published', 'ongoing'] },
startDate: { $lte: now },
endDate: { $gte: now },
_id: { $nin: startedTrainingDays }, // Exclude events with started training
location: { $exists: true, $ne: null } // Only events with location
})
.populate('organization', 'username organizationType')
.populate('createdBy', 'username profilePhoto')
.select('-joinToken');
const eventsWithCount = ongoingEvents.map(event => ({
...event.toObject(),
participantCount: event.participants.length,
isCapacityFull: event.participants.length >= event.capacity
}));
return res.json({ events: eventsWithCount });
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET LIVE TRAINING EVENTS ====================
export const getLiveTrainingEvents = async (req: IEventRequest, res: Response) => {
try {
const now = new Date();
// Find all event days that have started but not completed
const liveTrainingDays = await EventDay.find({
hasStarted: true,
isCompleted: false
}).populate({
path: 'event',
populate: [
{ path: 'organization', select: 'username organizationType' },
{ path: 'createdBy', select: 'username profilePhoto' }
]
});
// Filter and map to events with location data
const liveTrainingEvents = liveTrainingDays
.filter(eventDay => {
const event = eventDay.event as any;
// Only include events with location data
return event && event.location && event.location.coordinates;
})
.map(eventDay => {
const event = eventDay.event as any;
return {
...event.toObject(),
currentEventDay: {
_id: eventDay._id,
date: eventDay.date,
dayNumber: eventDay.dayNumber,
startTime: eventDay.startTime,
endTime: eventDay.endTime,
hasStarted: eventDay.hasStarted,
isCompleted: eventDay.isCompleted
},
participantCount: event.participants.length,
isCapacityFull: event.participants.length >= event.capacity,
isLiveTraining: true
};
});
// Remove duplicates (same event can have multiple live days)
const uniqueEvents = Array.from(
new Map(liveTrainingEvents.map(event => [event._id.toString(), event])).values()
);
return res.json({ events: uniqueEvents });
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};
// ==================== GET ALL EVENTS (ADMIN) ====================
export const getAllEventsForAdmin = async (req: IEventRequest, res: Response) => {
try {
const { status, page = 1, limit = 20 } = req.query;
const filter: any = {};
if (status) filter.status = status;
const skip = (Number(page) - 1) * Number(limit);
const events = await Event.find(filter)
.populate('organization', 'username organizationType')
.populate('createdBy', 'username profilePhoto')
.sort({ createdAt: -1 })
.skip(skip)
.limit(Number(limit));
const total = await Event.countDocuments(filter);
// Get day-level statistics for each event
const eventsWithStats = await Promise.all(events.map(async event => {
const eventDays = await EventDay.find({ event: event._id });
const totalDays = eventDays.length;
const completedDays = eventDays.filter(day => day.isCompleted).length;
const liveDays = eventDays.filter(day => {
const now = new Date();
return day.hasStarted && !day.isCompleted &&
now >= day.startTime && now <= day.endTime;
}).length;
return {
...event.toObject(),
participantCount: event.participants.length,
totalDays,
completedDays,
liveDays,
waitlistCount: event.waitlist.length
};
}));
return res.json({
events: eventsWithStats,
pagination: {
total,
page: Number(page),
limit: Number(limit),
totalPages: Math.ceil(total / Number(limit))
}
});
} catch (err: any) {
console.error(err);
return res.status(500).json({ message: 'Server error', error: err.message });
}
};