| 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; |
| } |
|
|
| |
| 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 } |
| }; |
|
|
| 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'); |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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'); |
|
|
| |
| const heatmapPoints = events.map(event => ({ |
| lat: event.location?.coordinates[1], |
| lng: event.location?.coordinates[0], |
| weight: event.participants.length |
| })); |
|
|
| return res.json({ points: heatmapPoints }); |
| } catch (err: any) { |
| console.error(err); |
| return res.status(500).json({ message: 'Server error', error: err.message }); |
| } |
| }; |
|
|
| |
| 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); |
| } |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| export const getDistrictCoverage = async (req: IAnalyticsRequest, res: Response) => { |
| try { |
| |
| |
| |
|
|
| const { state } = req.query; |
|
|
| |
| 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 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(); |
|
|
| |
| 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 }); |
| } |
| }; |
|
|
| |
| 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(); |
|
|
| |
| const totalEvents = await Event.countDocuments(filter); |
|
|
| |
| const upcomingEvents = await Event.countDocuments({ |
| ...filter, |
| startDate: { $gt: now }, |
| status: 'published' |
| }); |
|
|
| |
| const completedEvents = await Event.countDocuments({ |
| ...filter, |
| endDate: { $lt: now }, |
| status: { $in: ['completed', 'published'] } |
| }); |
|
|
| |
| |
| 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; |
|
|
| |
| const trainerFilter: any = { role: 'trainer', isActive: true }; |
| if (organization) trainerFilter.organization = organization; |
| const activeTrainers = await User.countDocuments(trainerFilter); |
|
|
| |
| const activeOrganizations = await User.countDocuments({ role: 'organization', verificationStatus: 'approved' }); |
|
|
| |
| 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 |
| }; |
|
|
| |
| 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 }); |
| } |
| }; |