diff --git a/client/src/components/home/shared/AskQuestion.tsx b/client/src/components/home/shared/AskQuestion.tsx index b63e5ad..f893c33 100644 --- a/client/src/components/home/shared/AskQuestion.tsx +++ b/client/src/components/home/shared/AskQuestion.tsx @@ -10,7 +10,6 @@ import BaseCard from '../../common/cards/BaseCard'; import HomeService from '../../../services/HomeService'; import {UserDataContext} from '../../../contexts/UserDataContext'; import {QueueDataContext} from '../../../contexts/QueueDataContext'; -import {StudentDataContext} from '../../../contexts/StudentDataContext'; function createData(assignment_id, name) { return {assignment_id, name}; @@ -34,8 +33,6 @@ export default function AskQuestion() { const [askDisabled, setAskDisabled] = useState(false); - const {studentData, setStudentData} = useContext(StudentDataContext); - const locations = useMemo(() => { if (queueData != null) { const day = date.getDay(); diff --git a/client/src/components/home/shared/QueueStats.tsx b/client/src/components/home/shared/QueueStats.tsx index 3e636f0..74af2f5 100644 --- a/client/src/components/home/shared/QueueStats.tsx +++ b/client/src/components/home/shared/QueueStats.tsx @@ -1,4 +1,4 @@ -import React, {useContext, useEffect} from 'react'; +import React, {useContext} from 'react'; import { CardContent, Divider, Stack, Typography, useTheme, } from '@mui/material'; diff --git a/client/src/components/home/student/UpdateQuestionOverlay.tsx b/client/src/components/home/student/UpdateQuestionOverlay.tsx index 4939ba5..eb1181b 100644 --- a/client/src/components/home/student/UpdateQuestionOverlay.tsx +++ b/client/src/components/home/student/UpdateQuestionOverlay.tsx @@ -1,6 +1,6 @@ import React, {useContext, useState} from 'react'; import { - Button, Dialog, DialogContent, FormControl, Input, Link, Stack, Typography, + Button, Dialog, DialogContent, FormControl, Input, Link, Typography, } from '@mui/material'; import HomeService from '../../../services/HomeService'; diff --git a/client/src/components/home/ta/dialogs/FilterOptions.tsx b/client/src/components/home/ta/dialogs/FilterOptions.tsx index 0fc3200..4f234d3 100644 --- a/client/src/components/home/ta/dialogs/FilterOptions.tsx +++ b/client/src/components/home/ta/dialogs/FilterOptions.tsx @@ -1,10 +1,9 @@ -import React, {useState, useEffect, useContext, useMemo} from 'react'; +import React, {useContext, useMemo} from 'react'; import {List, ListSubheader, ListItem, ListItemButton, ListItemIcon, ListItemText, Checkbox, } from '@mui/material'; -import SettingsService from '../../../../services/SettingsService'; import {QueueDataContext} from '../../../../contexts/QueueDataContext'; function createData(assignment_id, name) { diff --git a/client/src/components/metrics/AdminMetrics.tsx b/client/src/components/metrics/AdminMetrics.tsx new file mode 100644 index 0000000..90d8a9a --- /dev/null +++ b/client/src/components/metrics/AdminMetrics.tsx @@ -0,0 +1,182 @@ +import React, {useEffect, useState} from 'react'; +import MetricsService from '../../services/MetricsService'; +import { + Card, Divider, Typography, Stack, Table, TableBody, TableCell, + TableContainer, TableHead, TablePagination, TableRow, +} from '@mui/material'; + +export default function AdminMetrics() { + const [rankedStudents, setRankedStudents] = useState([]); + const [rankedTAs, setRankedTAs] = useState([]); + + // students pagination + const [studentPage, setStudentPage] = useState(0); + const [rowsPerStudentPage, setRowsPerStudentPage] = useState(10); + const handleChangeStudentPage = (event, newPage) => { + setStudentPage(newPage); + }; + const handleChangeRowsPerStudentPage = (event) => { + setRowsPerStudentPage(+event.target.value); + setStudentPage(0); + }; + + // tas pagination + const [taPage, setTAPage] = useState(0); + const [rowsPerTAPage, setRowsPerTAPage] = useState(10); + const handleChangeTAPage = (event, newPage) => { + setTAPage(newPage); + }; + const handleChangeRowsPerTAPage = (event) => { + setRowsPerTAPage(+event.target.value); + setTAPage(0); + }; + + useEffect(() => { + MetricsService.getRankedStudents().then((res) => { + setRankedStudents(res.data.rankedStudents.map((student) => { + return { + ...student, + average: Math.round(student.timeHelped / student.count * 10) / 10, + }; + })); + }); + + MetricsService.getRankedTAs().then((res) => { + setRankedTAs(res.data.rankedTAs.map((ta) => { + return { + ...ta, + average: Math.round(ta.timeHelping / ta.count * 10) / 10, + }; + })); + }); + }, []); + + const studentCols = [ + {id: 'student_andrew', label: 'Andrew ID', width: 25}, + {id: 'student_name', label: 'Name', width: 25}, + {id: 'count', label: 'Num Questions', width: 100}, + {id: 'badCount', label: 'Num Ask to Fix', width: 100}, + {id: 'timeHelped', label: 'Total Helping Time (min)', width: 100}, + {id: 'average', label: 'Average Helping Time (min)', width: 100}, + ]; + + const taCols = [ + {id: 'ta_andrew', label: 'Andrew ID', width: 25}, + {id: 'ta_name', label: 'Name', width: 25}, + {id: 'count', label: 'Num Questions Answered', width: 100}, + {id: 'timeHelping', label: 'Total Time Helping (min)', width: 100}, + {id: 'average', label: 'Average Time Helping (min)', width: 100}, + ]; + + return ( +
+ + Ranked Students and Ranked TAs + + + + } + spacing={2} + sx={{m: 2}} + > + + + + + + {studentCols.map((column) => ( + + {column.label} + + ))} + + + + {rankedStudents + .slice(studentPage * rowsPerStudentPage, studentPage * rowsPerStudentPage + rowsPerStudentPage) + .map((row) => { + return ( + + {studentCols.map((column) => { + const value = row[column.id]; + return ( + + {value} + + ); + })} + + ); + }) + } + +
+
+ +
+ + + + + + {taCols.map((column) => ( + + {column.label} + + ))} + + + + {rankedTAs + .slice(taPage * rowsPerTAPage, taPage * rowsPerTAPage + rowsPerTAPage) + .map((row) => { + return ( + + {taCols.map((column) => { + const value = row[column.id]; + return ( + + {value} + + ); + })} + + ); + }) + } + +
+
+ +
+
+
+
+ ); +} diff --git a/client/src/components/metrics/Graph.tsx b/client/src/components/metrics/Graph.tsx index f8a3454..383b749 100644 --- a/client/src/components/metrics/Graph.tsx +++ b/client/src/components/metrics/Graph.tsx @@ -45,7 +45,7 @@ export default function Graph() { return (
- Number of Students per Day (in the last week) + Number of Questions per Day (in the last week)
@@ -114,7 +114,7 @@ export default function Graph() {
- Number of Students per Day (overall) + Number of Questions per Day (semester)
@@ -183,7 +183,7 @@ export default function Graph() {
- Number of Students per Day of the Week + Number of Questions per Day of the Week
diff --git a/client/src/components/metrics/MetricsMain.tsx b/client/src/components/metrics/MetricsMain.tsx index 0aa3898..247238d 100644 --- a/client/src/components/metrics/MetricsMain.tsx +++ b/client/src/components/metrics/MetricsMain.tsx @@ -1,26 +1,32 @@ -import React from 'react'; +import React, {useContext} from 'react'; import { Typography, } from '@mui/material'; -import DateTimeSelector from './DateTimeSelector'; import PersonalStats from './PersonalStats'; import OverallStats from './OverallStats'; import CumulativeStats from './CumulativeStats'; import Graph from './Graph'; +import {UserDataContext} from '../../contexts/UserDataContext'; +import AdminMetrics from './AdminMetrics'; export default function MetricsMain(props) { + const {userData} = useContext(UserDataContext); + return (
Metrics - {/* */} + + { + userData.isAdmin && + }
); } diff --git a/client/src/components/settings/admin/DayPicker.tsx b/client/src/components/settings/admin/DayPicker.tsx index bbd6d34..fbc8352 100644 --- a/client/src/components/settings/admin/DayPicker.tsx +++ b/client/src/components/settings/admin/DayPicker.tsx @@ -8,7 +8,7 @@ import {Delete as DeleteIcon} from '@mui/icons-material'; import SettingsService from '../../../services/SettingsService'; export default function DayPicker(props) { - const {convertIdxToDays, daysOfWeek, room, roomDictionary, setRoomDictionary} = props; + const {convertIdxToDays, daysOfWeek, room, roomDictionary} = props; const [newDays, setNewDays] = useState(convertIdxToDays(roomDictionary[room])); const convertDaysToIdx = (daysArr) => { diff --git a/client/src/components/settings/admin/Locations.tsx b/client/src/components/settings/admin/Locations.tsx index ae8b009..a820d10 100644 --- a/client/src/components/settings/admin/Locations.tsx +++ b/client/src/components/settings/admin/Locations.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect, useContext, useMemo} from 'react'; +import React, {useState, useContext, useMemo} from 'react'; import { TableCell, Typography, } from '@mui/material'; diff --git a/client/src/components/settings/admin/TASettings.tsx b/client/src/components/settings/admin/TASettings.tsx index f7f52f8..21e1b11 100644 --- a/client/src/components/settings/admin/TASettings.tsx +++ b/client/src/components/settings/admin/TASettings.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect, useContext, useMemo} from 'react'; +import React, {useState, useContext, useMemo} from 'react'; import { Button, Checkbox, FormControlLabel, Grid, TableCell, TableRow, Typography, useTheme, } from '@mui/material'; diff --git a/client/src/services/MetricsService.tsx b/client/src/services/MetricsService.tsx index 3fc323d..07f65e8 100644 --- a/client/src/services/MetricsService.tsx +++ b/client/src/services/MetricsService.tsx @@ -1,6 +1,12 @@ import http from '../http-common'; class MetricsDataService { + getRankedTAs() { + return http.get('/metrics/rankedTAs'); + } + getRankedStudents() { + return http.get('/metrics/rankedStudents'); + } getHelpedStudents() { return http.get('/metrics/helpedStudents'); } diff --git a/server/controllers/metrics.js b/server/controllers/metrics.js index 25b4038..4b242ae 100644 --- a/server/controllers/metrics.js +++ b/server/controllers/metrics.js @@ -1,6 +1,7 @@ const Sequelize = require('sequelize'); const Promise = require("bluebird"); const moment = require("moment-timezone"); +let settings = require('./settings'); const models = require('../models'); const { sequelize } = require('../models'); @@ -32,7 +33,8 @@ exports.get_helped_students = (req, res) => { ta_id: req.user.ta.ta_id, help_time: { [Sequelize.Op.ne]: null - } + }, + sem_id: settings.get_admin_settings().currSem, }, order: [['entry_time', 'DESC']] }).then((questionModels) => { @@ -76,7 +78,8 @@ exports.get_num_questions_answered = (req, res) => { ta_id: req.user.ta.ta_id, help_time: { [Sequelize.Op.ne]: null - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { respond(req, res, "Got number of questions answered", { numQuestions: count }, 200); @@ -94,7 +97,8 @@ exports.get_avg_time_per_question = (req, res) => { ta_id: req.user.ta.ta_id, help_time: { [Sequelize.Op.ne]: null, - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { let averageTime = 0; @@ -120,7 +124,8 @@ exports.get_num_questions_today = (req, res) => { where: { entry_time: { [Sequelize.Op.gte]: new Date(today - 8 * 60 * 60 * 1000), - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count}) => { respond(req, res, "Got number of questions today", { numQuestionsToday: count }, 200); @@ -141,7 +146,8 @@ exports.get_num_bad_questions_today = (req, res) => { }, num_asked_to_fix: { [Sequelize.Op.gt]: 0 - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count}) => { respond(req, res, "Got number of bad questions today", { numBadQuestionsToday: count }, 200); @@ -161,7 +167,8 @@ exports.get_avg_wait_time_today = (req, res) => { }, help_time: { [Sequelize.Op.ne]: null, - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { @@ -188,7 +195,8 @@ exports.get_ta_student_ratio_today = (req, res) => { where: { entry_time: { [Sequelize.Op.gte]: new Date(today - 8 * 60 * 60 * 1000), - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { const taCount = rows.reduce((acc, questionModel) => { @@ -227,6 +235,10 @@ exports.get_total_num_questions = (req, res) => { models.question.findAndCountAll({ where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + }, } }).then(({count}) => { respond(req, res, "Got number of questions answered", { numQuestions: count }, 200); @@ -243,7 +255,8 @@ exports.get_total_avg_time_per_question = (req, res) => { where: { help_time: { [Sequelize.Op.ne]: null, - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { let averageTime = 0; @@ -269,7 +282,8 @@ exports.get_total_avg_wait_time = (req, res) => { where: { help_time: { [Sequelize.Op.ne]: null, - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { @@ -300,7 +314,11 @@ exports.get_num_students_per_day_last_week = (req, res) => { where: { entry_time: { [Sequelize.Op.gte]: new Date(today - 7 * 24 * 60 * 60 * 1000), - } + }, + help_time: { + [Sequelize.Op.ne]: null + }, + sem_id: settings.get_admin_settings().currSem, }, group: [Sequelize.fn('date', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`))], order: [[Sequelize.col('day'), 'ASC']] @@ -328,6 +346,12 @@ exports.get_num_students_per_day = (req, res) => { [Sequelize.fn('date_part', 'dow', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`)), 'day_of_week'], [Sequelize.fn('count', Sequelize.col('question_id')), 'count'] ], + where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + }, + }, group: [Sequelize.fn('date_part', 'dow', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`))], order: [[Sequelize.col('count'), 'DESC']] }).then((data) => { @@ -355,6 +379,12 @@ exports.get_num_students_overall = (req, res) => { [Sequelize.fn('date', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`)), 'day'], [Sequelize.fn('count', Sequelize.col('question_id')), 'count'] ], + where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + }, + }, group: [Sequelize.fn('date', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`))], order: [[Sequelize.col('day'), 'ASC']] }).then((data) => { @@ -368,3 +398,135 @@ exports.get_num_students_overall = (req, res) => { respond(req, res, "Got number of students overall", { numStudentsOverall: numStudentsOverall }, 200); }); } + +exports.get_ranked_students = (req, res) => { + if (!req.user || !req.user.isTA || !req.user.isAdmin) { + respond_error(req, res, "You don't have permission to perform this operation", 403); + return; + } + + let studentMap = {}; + models.question.findAll({ + where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + } + } + }).then((questionModels) => { + + + for (const questionModel of questionModels) { + let question = questionModel.dataValues; + + if (question.student_id in studentMap) { + studentMap[question.student_id].count++; + studentMap[question.student_id].timeHelped += (question.exit_time - question.help_time) / 1000 / 60; + studentMap[question.student_id].badCount += parseInt(question.num_asked_to_fix); + } else { + studentMap[question.student_id] = { + count: 1, + timeHelped: (question.exit_time - question.help_time) / 1000 / 60, + badCount: parseInt(question.num_asked_to_fix) + }; + } + } + + let accountReqs = []; + for (const student_id in studentMap) { + accountReqs.push(models.account.findByPk(student_id)); + } + + return Promise.all(accountReqs); + }).then((accounts) => { + let rankedStudents = []; + + for (const account of accounts) { + let accountData = account.dataValues; + let user_id = accountData.user_id; + + if (user_id in studentMap) { + rankedStudents.push({ + student_name: accountData.preferred_name, + student_andrew: accountData.email.split("@")[0], + count: studentMap[user_id].count, + badCount: studentMap[user_id].badCount, + timeHelped: Math.round(studentMap[user_id].timeHelped * 10) / 10, + }); + } + } + + rankedStudents.sort((a, b) => { + if (a.count != b.count) { + return b.count - a.count; + } else { + return b.timeHelped - a.timeHelped; + } + }); + respond(req, res, "Got ranked students", { rankedStudents: rankedStudents }, 200); + }); +} + +exports.get_ranked_tas = (req, res) => { + if (!req.user || !req.user.isTA || !req.user.isAdmin) { + respond_error(req, res, "You don't have permission to perform this operation", 403); + return; + } + + let taMap = {}; + models.question.findAll({ + where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + } + } + }).then((questionModels) => { + + for (const questionModel of questionModels) { + let question = questionModel.dataValues; + + if (question.ta_id in taMap) { + taMap[question.ta_id].count++; + taMap[question.ta_id].timeHelping += (question.exit_time - question.help_time) / 1000 / 60; + } else { + taMap[question.ta_id] = { + count: 1, + timeHelping: (question.exit_time - question.help_time) / 1000 / 60, + }; + } + } + + let accountReqs = []; + for (const ta_id in taMap) { + accountReqs.push(models.account.findByPk(ta_id)); + } + + return Promise.all(accountReqs); + }).then((accounts) => { + let rankedTAs = []; + + for (const account of accounts) { + let accountData = account.dataValues; + let user_id = accountData.user_id; + + if (user_id in taMap) { + rankedTAs.push({ + ta_name: accountData.preferred_name, + ta_andrew: accountData.email.split("@")[0], + count: taMap[user_id].count, + timeHelping: Math.round(taMap[user_id].timeHelping * 10) / 10, + }); + } + } + + rankedTAs.sort((a, b) => { + if (a.count != b.count) { + return b.count - a.count; + } else { + return b.timeHelped - a.timeHelped; + } + }); + respond(req, res, "Got ranked TAs", { rankedTAs: rankedTAs }, 200); + }); +} diff --git a/server/routes/metrics.js b/server/routes/metrics.js index 3f58280..ecec056 100644 --- a/server/routes/metrics.js +++ b/server/routes/metrics.js @@ -16,4 +16,6 @@ router.get('/totalAvgWaitTime', metrics.get_total_avg_wait_time); router.get('/numStudentsPerDayLastWeek', metrics.get_num_students_per_day_last_week); router.get('/numStudentsPerDay', metrics.get_num_students_per_day); router.get('/numStudentsOverall', metrics.get_num_students_overall); +router.get('/rankedStudents', metrics.get_ranked_students); +router.get('/rankedTAs', metrics.get_ranked_tas); module.exports = router;