import { Request, Response } from 'express'; import Event from '../model/event.model'; import User from '../model/user.model'; import { Parser } from 'json2csv'; interface IAnalyticsRequest extends Request { userId?: string; } // ==================== GET EVENT MAP DATA ==================== export const getEventMapData = async (req: IAnalyticsRequest, res: Response) => { try { const { theme, organization, startDate, endDate, status = 'published' } = req.query; const filter: any = { status, location: { $exists: true } // Only events with location }; if (theme) filter.theme = theme; if (organization) filter.organization = organization; 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 events = await Event.find(filter) .populate('organization', 'username organizationType') .populate('createdBy', 'username') .select('title code theme location startDate endDate type participantCount venue'); // Format for map markers const mapData = events.map(event => ({ id: event._id, title: event.title, code: event.code, theme: event.theme, organization: (event.organization as any).username, coordinates: event.location?.coordinates, latitude: event.location?.coordinates[1], longitude: event.location?.coordinates[0], startDate: event.startDate, endDate: event.endDate, type: event.type, venue: event.venue, participantCount: event.participants.length, capacity: event.capacity })); return res.json({ totalEvents: mapData.length, events: mapData }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== GET COVERAGE HEATMAP DATA ==================== export const getHeatmapData = async (req: IAnalyticsRequest, res: Response) => { try { const { theme, startDate, endDate } = req.query; const filter: any = { status: { $in: ['published', 'completed'] }, location: { $exists: true } }; if (theme) filter.theme = theme; 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 events = await Event.find(filter) .select('location participants'); // Create heatmap points (lat, lng, weight) const heatmapPoints = events.map(event => ({ lat: event.location?.coordinates[1], lng: event.location?.coordinates[0], weight: event.participants.length // Weight by attendance })); return res.json({ points: heatmapPoints }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== GET THEME COVERAGE STATISTICS ==================== export const getThemeCoverage = async (req: IAnalyticsRequest, res: Response) => { try { const { startDate, endDate, organization } = req.query; const filter: any = { status: { $in: ['published', 'completed'] } }; if (organization) filter.organization = organization; 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); } // Aggregate by theme const themeCoverage = await Event.aggregate([ { $match: filter }, { $group: { _id: '$theme', eventCount: { $sum: 1 }, totalParticipants: { $sum: { $size: '$participants' } }, totalCapacity: { $sum: '$capacity' }, avgAttendance: { $avg: { $size: '$participants' } } } }, { $sort: { eventCount: -1 } } ]); return res.json({ themeCoverage }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== GET ORGANIZATION STATISTICS ==================== export const getOrganizationStats = async (req: IAnalyticsRequest, res: Response) => { try { const { startDate, endDate } = req.query; const filter: any = { status: { $in: ['published', 'completed'] } }; 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 orgStats = await Event.aggregate([ { $match: filter }, { $group: { _id: '$organization', eventCount: { $sum: 1 }, totalParticipants: { $sum: { $size: '$participants' } }, totalCapacity: { $sum: '$capacity' }, themes: { $addToSet: '$theme' } } }, { $lookup: { from: 'users', localField: '_id', foreignField: '_id', as: 'orgDetails' } }, { $project: { organization: { $arrayElemAt: ['$orgDetails.username', 0] }, organizationType: { $arrayElemAt: ['$orgDetails.organizationType', 0] }, eventCount: 1, totalParticipants: 1, totalCapacity: 1, themeCount: { $size: '$themes' }, utilizationRate: { $multiply: [ { $divide: ['$totalParticipants', '$totalCapacity'] }, 100 ] } } }, { $sort: { eventCount: -1 } } ]); return res.json({ organizationStats: orgStats }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== GET TRAINER ACTIVITY ==================== export const getTrainerActivity = async (req: IAnalyticsRequest, res: Response) => { try { const { organization, startDate, endDate } = req.query; const filter: any = { status: { $in: ['published', 'completed'] } }; if (organization) filter.organization = organization; 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 trainerStats = await Event.aggregate([ { $match: filter }, { $group: { _id: '$createdBy', eventCount: { $sum: 1 }, totalParticipants: { $sum: { $size: '$participants' } }, themes: { $addToSet: '$theme' } } }, { $lookup: { from: 'users', localField: '_id', foreignField: '_id', as: 'trainerDetails' } }, { $project: { trainer: { $arrayElemAt: ['$trainerDetails.username', 0] }, email: { $arrayElemAt: ['$trainerDetails.email', 0] }, workDesignation: { $arrayElemAt: ['$trainerDetails.workDesignation', 0] }, eventCount: 1, totalParticipants: 1, themeCount: { $size: '$themes' }, avgParticipantsPerEvent: { $divide: ['$totalParticipants', '$eventCount'] } } }, { $sort: { eventCount: -1 } } ]); return res.json({ trainerActivity: trainerStats }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== GET DISTRICT COVERAGE ==================== export const getDistrictCoverage = async (req: IAnalyticsRequest, res: Response) => { try { // This would require reverse geocoding to get district from coordinates // For now, returning placeholder structure // You'd integrate with a geocoding service (Google Maps, Mapbox, etc.) const { state } = req.query; // Placeholder - implement reverse geocoding return res.json({ message: 'District coverage requires geocoding integration', placeholder: { state: state || 'Maharashtra', districts: [ { name: 'Mumbai', eventCount: 45, participantCount: 1200 }, { name: 'Pune', eventCount: 32, participantCount: 890 }, { name: 'Nagpur', eventCount: 18, participantCount: 450 } ] } }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== EXPORT ANALYTICS TO CSV ==================== export const exportAnalyticsToCsv = async (req: IAnalyticsRequest, res: Response) => { try { const { startDate, endDate, organization, theme } = req.query; const filter: any = { status: { $in: ['published', 'completed'] } }; if (organization) filter.organization = organization; if (theme) filter.theme = theme; 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 events = await Event.find(filter) .populate('organization', 'username organizationType') .populate('createdBy', 'username email') .lean(); // Format data for CSV const csvData = events.map(event => ({ 'Event Code': event.code, 'Title': event.title, 'Theme': event.theme, 'Organization': (event.organization as any).username, 'Organization Type': (event.organization as any).organizationType, 'Trainer': (event.createdBy as any).username, 'Trainer Email': (event.createdBy as any).email, 'Type': event.type, 'Venue': event.venue || 'N/A', 'Start Date': new Date(event.startDate).toLocaleDateString(), 'End Date': new Date(event.endDate).toLocaleDateString(), 'Capacity': event.capacity, 'Registered': event.participants.length, 'Attendance': event.participants.filter((p: any) => p.attendance?.checkInTime).length, 'Utilization %': ((event.participants.length / event.capacity) * 100).toFixed(2), 'Status': event.status })); const parser = new Parser(); const csv = parser.parse(csvData); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', 'attachment; filename=training_analytics.csv'); return res.send(csv); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } }; // ==================== GET DASHBOARD OVERVIEW ==================== export const getDashboardOverview = async (req: IAnalyticsRequest, res: Response) => { try { const { startDate, endDate, organization } = req.query; const filter: any = {}; if (organization) filter.organization = organization; 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 now = new Date(); // Total events const totalEvents = await Event.countDocuments(filter); // Upcoming events const upcomingEvents = await Event.countDocuments({ ...filter, startDate: { $gt: now }, status: 'published' }); // Completed events const completedEvents = await Event.countDocuments({ ...filter, endDate: { $lt: now }, status: { $in: ['completed', 'published'] } }); // Pending Verifications (Trainers + Organizations) // Note: This is global unless filtered by organization (for trainers) const verificationFilter: any = { role: 'organization', verificationStatus: 'pending' }; const pendingOrgVerifications = await User.countDocuments(verificationFilter); const pendingTrainerFilter: any = { role: 'trainer', organizationVerificationStatus: 'pending' }; if (organization) pendingTrainerFilter.organization = organization; const pendingTrainerVerifications = await User.countDocuments(pendingTrainerFilter); const totalPendingVerifications = pendingOrgVerifications + pendingTrainerVerifications; // Active Trainers const trainerFilter: any = { role: 'trainer', isActive: true }; if (organization) trainerFilter.organization = organization; const activeTrainers = await User.countDocuments(trainerFilter); // Active Organizations const activeOrganizations = await User.countDocuments({ role: 'organization', verificationStatus: 'approved' }); // Total participants across all events const participantStats = await Event.aggregate([ { $match: filter }, { $group: { _id: null, totalParticipants: { $sum: { $size: '$participants' } }, totalCapacity: { $sum: '$capacity' }, totalAttended: { $sum: { $size: { $filter: { input: '$participants', as: 'p', cond: { $ne: ['$$p.attendance.checkInTime', null] } } } } } } } ]); const stats = participantStats[0] || { totalParticipants: 0, totalCapacity: 0, totalAttended: 0 }; // Unique themes const themes = await Event.distinct('theme', filter); return res.json({ overview: { totalEvents, upcomingEvents, completedEvents, totalParticipants: stats.totalParticipants, totalCapacity: stats.totalCapacity, totalAttended: stats.totalAttended, utilizationRate: stats.totalCapacity > 0 ? ((stats.totalParticipants / stats.totalCapacity) * 100).toFixed(2) : 0, attendanceRate: stats.totalParticipants > 0 ? ((stats.totalAttended / stats.totalParticipants) * 100).toFixed(2) : 0, uniqueThemes: themes.length, activeOrganizations, activeTrainers, pendingVerifications: totalPendingVerifications, pendingOrgVerifications, pendingTrainerVerifications } }); } catch (err: any) { console.error(err); return res.status(500).json({ message: 'Server error', error: err.message }); } };