From 93557cd47fca3698c299b5d81f63047dad1cb37e Mon Sep 17 00:00:00 2001 From: Kanishk Agarwal Date: Fri, 5 Dec 2025 20:59:18 -0700 Subject: [PATCH] feat: Add Event Popularity Analytics API endpoints - Created eventPopularityController with metrics calculation functions - Implemented getPopularityMetrics endpoint for event type breakdown - Implemented getEngagementMetrics endpoint for session analytics - Implemented getEventValue endpoint for estimated value calculations - Implemented getFormatComparison endpoint for Virtual vs In-Person comparison - Created eventPopularityRouter with all API routes - Registered router in startup/routes.js - Added proper error handling and logging - All endpoints support date range filtering via query parameters --- package-lock.json | 10 - src/controllers/eventPopularityController.js | 304 +++++++++++++++++++ src/routes/eventPopularityRouter.js | 12 + src/startup/routes.js | 2 + 4 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 src/controllers/eventPopularityController.js create mode 100644 src/routes/eventPopularityRouter.js diff --git a/package-lock.json b/package-lock.json index a67ae903b..6e85390a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1372,7 +1372,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3964,7 +3963,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -5013,7 +5011,6 @@ "node_modules/@types/node-fetch": { "version": "2.6.13", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "form-data": "^4.0.4" @@ -5148,7 +5145,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5940,7 +5936,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -7163,7 +7158,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7309,7 +7303,6 @@ "version": "2.32.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7361,7 +7354,6 @@ "version": "6.10.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -7390,7 +7382,6 @@ "version": "7.37.5", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -7422,7 +7413,6 @@ "version": "4.6.2", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, diff --git a/src/controllers/eventPopularityController.js b/src/controllers/eventPopularityController.js new file mode 100644 index 000000000..1dc3a8a54 --- /dev/null +++ b/src/controllers/eventPopularityController.js @@ -0,0 +1,304 @@ +const Event = require('../models/event'); + +const eventPopularityController = () => { + // Calculate popularity metrics by event type + const getPopularityMetrics = async (req, res) => { + try { + const { startDate, endDate } = req.query; + + // Build date filter if provided + const query = { isActive: true }; + if (startDate || endDate) { + query.date = {}; + if (startDate) query.date.$gte = new Date(startDate); + if (endDate) query.date.$lte = new Date(endDate); + } + + // Get all events with attendance data + const events = await Event.find(query).lean(); + + // Handle empty events + if (!events || events.length === 0) { + return res.json({ metrics: [] }); + } + + // Group by event type + const typeMetrics = {}; + + events.forEach((event) => { + const type = event.type || 'Unknown'; + if (!typeMetrics[type]) { + typeMetrics[type] = { + eventType: type, + totalEvents: 0, + totalAttendees: 0, + totalRegistrations: 0, + averageAttendeesPerEvent: 0, + events: [], + }; + } + + const attendees = event.currentAttendees || 0; + const registrations = event.currentAttendees || 0; // Using currentAttendees as proxy for registrations + + typeMetrics[type].totalEvents += 1; + typeMetrics[type].totalAttendees += attendees; + typeMetrics[type].totalRegistrations += registrations; + typeMetrics[type].events.push({ + eventId: event._id, + title: event.title, + attendees, + registrations, + maxAttendees: event.maxAttendees, + location: event.location, + }); + }); + + // Calculate averages + const result = Object.values(typeMetrics).map((metrics) => ({ + ...metrics, + averageAttendeesPerEvent: + metrics.totalEvents > 0 ? metrics.totalAttendees / metrics.totalEvents : 0, + averageRegistrationsPerEvent: + metrics.totalEvents > 0 ? metrics.totalRegistrations / metrics.totalEvents : 0, + })); + + res.json({ metrics: result }); + } catch (error) { + console.error('Error in getPopularityMetrics:', error); + res.status(500).json({ + error: 'Failed to fetch popularity metrics', + details: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, + }); + } + }; + + // Calculate engagement metrics (session duration, interactions) + const getEngagementMetrics = async (req, res) => { + try { + const { startDate, endDate, format } = req.query; // format: 'Virtual' or 'In person' + + const query = { isActive: true }; + if (startDate || endDate) { + query.date = {}; + if (startDate) query.date.$gte = new Date(startDate); + if (endDate) query.date.$lte = new Date(endDate); + } + + if (format) { + query.location = format; + } + + const events = await Event.find(query).lean(); + + // Handle empty events + if (!events || events.length === 0) { + return res.json({ + engagement: { + totalEvents: 0, + totalAttendees: 0, + averageSessionDuration: 0, + averageInteractionRate: 0, + events: [], + }, + }); + } + + // Calculate session duration based on event start/end times + const engagementData = events.map((event) => { + const attendees = event.currentAttendees || 0; + + // Calculate session duration from startTime and endTime + let averageSessionDuration = 0; + if (event.startTime && event.endTime) { + const start = new Date(event.startTime); + const end = new Date(event.endTime); + averageSessionDuration = Math.round((end - start) / (1000 * 60)); // Duration in minutes + } else { + // Default duration based on event type + const defaultDurations = { + Workshop: 120, + Meeting: 60, + Webinar: 90, + 'Social Gathering': 90, + }; + averageSessionDuration = defaultDurations[event.type] || 60; + } + + const interactionRate = event.maxAttendees > 0 ? (attendees / event.maxAttendees) * 100 : 0; + + return { + eventId: event._id, + title: event.title, + type: event.type, + location: event.location, + attendees, + averageSessionDuration, + interactionRate: Math.round(interactionRate * 100) / 100, + }; + }); + + const totalAttendees = engagementData.reduce((sum, e) => sum + e.attendees, 0); + const averageSessionDuration = + engagementData.length > 0 + ? engagementData.reduce((sum, e) => sum + e.averageSessionDuration, 0) / + engagementData.length + : 0; + const averageInteractionRate = + engagementData.length > 0 + ? engagementData.reduce((sum, e) => sum + e.interactionRate, 0) / engagementData.length + : 0; + + res.json({ + engagement: { + totalEvents: events.length, + totalAttendees, + averageSessionDuration: Math.round(averageSessionDuration), + averageInteractionRate: Math.round(averageInteractionRate * 100) / 100, + events: engagementData, + }, + }); + } catch (error) { + console.error('Error in getEngagementMetrics:', error); + res.status(500).json({ + error: 'Failed to fetch engagement metrics', + details: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, + }); + } + }; + + // Calculate event value (estimated value per event) + const getEventValue = async (req, res) => { + try { + const { startDate, endDate } = req.query; + + const query = { isActive: true }; + if (startDate || endDate) { + query.date = {}; + if (startDate) query.date.$gte = new Date(startDate); + if (endDate) query.date.$lte = new Date(endDate); + } + + const events = await Event.find(query).lean(); + + // Handle empty events + if (!events || events.length === 0) { + return res.json({ + eventValues: { + totalValue: 0, + averageValuePerEvent: 0, + events: [], + }, + }); + } + + // Calculate estimated value per event + // Value calculation: base value per attendee * attendee count + // Different event types have different base values + const baseValues = { + Workshop: 50, + Meeting: 30, + Webinar: 25, + 'Social Gathering': 20, + }; + + const eventValues = events.map((event) => { + const attendees = event.currentAttendees || 0; + const baseValue = baseValues[event.type] || 30; + const estimatedValue = attendees * baseValue; + + return { + eventId: event._id, + title: event.title, + type: event.type, + location: event.location, + attendees, + estimatedValue, + baseValuePerAttendee: baseValue, + }; + }); + + const totalValue = eventValues.reduce((sum, e) => sum + e.estimatedValue, 0); + const averageValuePerEvent = eventValues.length > 0 ? totalValue / eventValues.length : 0; + + res.json({ + eventValues: { + totalValue, + averageValuePerEvent: Math.round(averageValuePerEvent * 100) / 100, + events: eventValues, + }, + }); + } catch (error) { + console.error('Error in getEventValue:', error); + res.status(500).json({ + error: 'Failed to fetch event values', + details: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, + }); + } + }; + + // Get comparison metrics for virtual vs in-person + const getFormatComparison = async (req, res) => { + try { + const { startDate, endDate } = req.query; + + const virtualQuery = { isActive: true, location: 'Virtual' }; + const inPersonQuery = { isActive: true, location: 'In person' }; + + if (startDate || endDate) { + const dateFilter = {}; + if (startDate) dateFilter.$gte = new Date(startDate); + if (endDate) dateFilter.$lte = new Date(endDate); + virtualQuery.date = dateFilter; + inPersonQuery.date = dateFilter; + } + + const virtualEvents = await Event.find(virtualQuery).lean(); + const inPersonEvents = await Event.find(inPersonQuery).lean(); + + const calculateMetrics = (events) => { + const totalAttendees = events.reduce( + (sum, event) => sum + (event.currentAttendees || 0), + 0, + ); + const totalEvents = events.length; + const averageAttendees = totalEvents > 0 ? totalAttendees / totalEvents : 0; + + return { + totalEvents, + totalAttendees, + averageAttendeesPerEvent: Math.round(averageAttendees * 100) / 100, + }; + }; + + const virtualMetrics = calculateMetrics(virtualEvents); + const inPersonMetrics = calculateMetrics(inPersonEvents); + + res.json({ + comparison: { + virtual: virtualMetrics, + inPerson: inPersonMetrics, + }, + }); + } catch (error) { + console.error('Error in getFormatComparison:', error); + res.status(500).json({ + error: 'Failed to fetch format comparison', + details: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, + }); + } + }; + + return { + getPopularityMetrics, + getEngagementMetrics, + getEventValue, + getFormatComparison, + }; +}; + +module.exports = eventPopularityController; diff --git a/src/routes/eventPopularityRouter.js b/src/routes/eventPopularityRouter.js new file mode 100644 index 000000000..c4ac3be89 --- /dev/null +++ b/src/routes/eventPopularityRouter.js @@ -0,0 +1,12 @@ +const express = require('express'); +const eventPopularityController = require('../controllers/eventPopularityController'); + +const eventPopularityRouter = express.Router(); +const controller = eventPopularityController(); + +eventPopularityRouter.get('/events/popularity', controller.getPopularityMetrics); +eventPopularityRouter.get('/events/engagement', controller.getEngagementMetrics); +eventPopularityRouter.get('/events/value', controller.getEventValue); +eventPopularityRouter.get('/events/format-comparison', controller.getFormatComparison); + +module.exports = eventPopularityRouter; diff --git a/src/startup/routes.js b/src/startup/routes.js index b7d83878d..ffd2c7b65 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -465,6 +465,8 @@ module.exports = function (app) { app.use('/api/lb', lbListingsRouter); app.use('/api/bm', bmIssueRouter); app.use('/api', eventRouter); + const eventPopularityRouter = require('../routes/eventPopularityRouter'); + app.use('/api', eventPopularityRouter); app.use('/api/villages', require('../routes/lbdashboard/villages')); app.use('/api/lb', lbMessageRouter); app.use('/api/lb', lbUserPrefRouter);