Update furthest course dashboard tables
Adding a second table, setting range to be 60 and 365 days. Updating teacher columns to prioritize student paid status over furthest student course.
This commit is contained in:
parent
dd603a0436
commit
609884eb51
5 changed files with 185 additions and 48 deletions
|
@ -124,26 +124,72 @@ block content
|
|||
h3#enrollments-graph Enrollments Issued and Redeemed 90 days
|
||||
.paid-courses-chart.line-chart-container
|
||||
|
||||
h3#furthest-courses-table Furthest Course
|
||||
.small Teacher: owner of a course instance
|
||||
.small Student: member of a course instance (assigned to course)
|
||||
.small For course instances created in last #{view.furthestCourseDayRange} days, not Single Player, hourOfCode != true
|
||||
.small Counts are not summed. I.e. a student or teacher only contributes to the count of one course.
|
||||
if view.teacherCourseDistribution
|
||||
table.table.table-striped.table-condensed
|
||||
tr
|
||||
th Course
|
||||
th Teachers
|
||||
th Students
|
||||
th Avg students per teacher
|
||||
each count, courseIndex in view.teacherCourseDistribution
|
||||
#furthest-course
|
||||
h3 Furthest Course in last #{view.furthestCourseDayRangeRecent} days
|
||||
.small Restricted to courses instances from last #{view.furthestCourseDayRangeRecent} days
|
||||
.small Teacher: owner of a course instance
|
||||
.small Student: member of a course instance (assigned to course)
|
||||
.small For course instances != Single Player, hourOfCode != true
|
||||
.small Counts are not summed. I.e. a student or teacher only contributes to the count of one course
|
||||
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
|
||||
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
|
||||
.small Free student: not paid, not trial
|
||||
.small Paid teacher: at least one paid student in course instance
|
||||
.small Trial teacher: at least one trial student in course instance, and no paid students
|
||||
.small Free teacher: no paid students, no trial students
|
||||
.small Paid status takes precedent over furthest course, so teacher furthest course is furthest course of highest paid status student
|
||||
if view.courseDistributionsRecent
|
||||
table.table.table-striped.table-condensed
|
||||
tr
|
||||
td= view.courses.models[courseIndex].get('name')
|
||||
td= count
|
||||
td= view.studentCourseDistribution[courseIndex] || 0
|
||||
td= Math.round((view.studentCourseDistribution[courseIndex] || 0) / count)
|
||||
else
|
||||
div Loading ...
|
||||
th Course
|
||||
th Paid Teachers
|
||||
th Trial Teachers
|
||||
th Free Teachers
|
||||
th Total Teachers
|
||||
th Paid Students
|
||||
th Trial Students
|
||||
th Free Students
|
||||
th Total Students
|
||||
each row in view.courseDistributionsRecent
|
||||
tr
|
||||
td= row.courseName
|
||||
td= row.totals['Paid Teachers'] || 0
|
||||
td= row.totals['Trial Teachers'] || 0
|
||||
td= row.totals['Free Teachers'] || 0
|
||||
td= row.totals['Total Teachers'] || 0
|
||||
td= row.totals['Paid Students'] || 0
|
||||
td= row.totals['Trial Students'] || 0
|
||||
td= row.totals['Free Students'] || 0
|
||||
td= row.totals['Total Students'] || 0
|
||||
else
|
||||
div Loading ...
|
||||
|
||||
h3 Furthest Course in last #{view.furthestCourseDayRange} days
|
||||
if view.courseDistributions
|
||||
table.table.table-striped.table-condensed
|
||||
tr
|
||||
th Course
|
||||
th Paid Teachers
|
||||
th Trial Teachers
|
||||
th Free Teachers
|
||||
th Total Teachers
|
||||
th Paid Students
|
||||
th Trial Students
|
||||
th Free Students
|
||||
th Total Students
|
||||
each row in view.courseDistributions
|
||||
tr
|
||||
td= row.courseName
|
||||
td= row.totals['Paid Teachers'] || 0
|
||||
td= row.totals['Trial Teachers'] || 0
|
||||
td= row.totals['Free Teachers'] || 0
|
||||
td= row.totals['Total Teachers'] || 0
|
||||
td= row.totals['Paid Students'] || 0
|
||||
td= row.totals['Trial Students'] || 0
|
||||
td= row.totals['Free Students'] || 0
|
||||
td= row.totals['Total Students'] || 0
|
||||
else
|
||||
div Loading ...
|
||||
|
||||
#school-sales
|
||||
h3 School Sales
|
||||
|
|
|
@ -11,7 +11,8 @@ utils = require 'core/utils'
|
|||
module.exports = class AnalyticsView extends RootView
|
||||
id: 'admin-analytics-view'
|
||||
template: template
|
||||
furthestCourseDayRange: 30
|
||||
furthestCourseDayRangeRecent: 60
|
||||
furthestCourseDayRange: 365
|
||||
lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan']
|
||||
minSchoolCount: 20
|
||||
|
||||
|
@ -238,35 +239,106 @@ module.exports = class AnalyticsView extends RootView
|
|||
options.error = (models, response, options) =>
|
||||
return if @destroyed
|
||||
console.error 'Failed to get recent course instances', response
|
||||
options.success = (models) =>
|
||||
@courseInstances = models ? []
|
||||
@onCourseInstancesSync()
|
||||
@render?()
|
||||
options.success = (data) =>
|
||||
@onCourseInstancesSync(data)
|
||||
@renderSelectors?('#furthest-course')
|
||||
@supermodel.addRequestResource(options, 0).load()
|
||||
|
||||
onCourseInstancesSync: ->
|
||||
return unless @courseInstances
|
||||
onCourseInstancesSync: (data) ->
|
||||
@courseDistributionsRecent = []
|
||||
@courseDistributions = []
|
||||
return unless data.courseInstances and data.students and data.prepaids
|
||||
|
||||
# Find highest course for teachers and students
|
||||
@teacherFurthestCourseMap = {}
|
||||
@studentFurthestCourseMap = {}
|
||||
for courseInstance in @courseInstances
|
||||
courseID = courseInstance.courseID
|
||||
teacherID = courseInstance.ownerID
|
||||
if not @teacherFurthestCourseMap[teacherID] or @teacherFurthestCourseMap[teacherID] < @courseOrderMap[courseID]
|
||||
@teacherFurthestCourseMap[teacherID] = @courseOrderMap[courseID]
|
||||
for studentID in courseInstance.members
|
||||
if not @studentFurthestCourseMap[studentID] or @studentFurthestCourseMap[studentID] < @courseOrderMap[courseID]
|
||||
@studentFurthestCourseMap[studentID] = @courseOrderMap[courseID]
|
||||
createCourseDistributions = (numDays) =>
|
||||
# Find student furthest course
|
||||
startDate = new Date()
|
||||
startDate.setUTCDate(startDate.getUTCDate() - numDays)
|
||||
teacherStudentsMap = {}
|
||||
studentFurthestCourseMap = {}
|
||||
studentPaidStatusMap = {}
|
||||
for courseInstance in data.courseInstances
|
||||
continue if utils.objectIdToDate(courseInstance._id) < startDate
|
||||
courseID = courseInstance.courseID
|
||||
teacherID = courseInstance.ownerID
|
||||
for studentID in courseInstance.members
|
||||
studentPaidStatusMap[studentID] = 'free'
|
||||
if not studentFurthestCourseMap[studentID] or studentFurthestCourseMap[studentID] < @courseOrderMap[courseID]
|
||||
studentFurthestCourseMap[studentID] = @courseOrderMap[courseID]
|
||||
teacherStudentsMap[teacherID] ?= []
|
||||
teacherStudentsMap[teacherID].push(studentID)
|
||||
|
||||
@teacherCourseDistribution = {}
|
||||
for teacherID, courseIndex of @teacherFurthestCourseMap
|
||||
@teacherCourseDistribution[courseIndex] ?= 0
|
||||
@teacherCourseDistribution[courseIndex]++
|
||||
@studentCourseDistribution = {}
|
||||
for studentID, courseIndex of @studentFurthestCourseMap
|
||||
@studentCourseDistribution[courseIndex] ?= 0
|
||||
@studentCourseDistribution[courseIndex]++
|
||||
# Find paid students
|
||||
prepaidUserMap = {}
|
||||
for user in data.students
|
||||
continue unless studentPaidStatusMap[user._id]
|
||||
if prepaidID = user.coursePrepaidID
|
||||
studentPaidStatusMap[user._id] = 'paid'
|
||||
prepaidUserMap[prepaidID] ?= []
|
||||
prepaidUserMap[prepaidID].push(user._id)
|
||||
|
||||
# Find trial students
|
||||
for prepaid in data.prepaids
|
||||
continue unless prepaidUserMap[prepaid._id]
|
||||
if prepaid.properties?.trialRequestID
|
||||
for userID in prepaidUserMap[prepaid._id]
|
||||
studentPaidStatusMap[userID] = 'trial'
|
||||
|
||||
# Find teacher furthest course and paid status based on their students
|
||||
# Paid teacher: at least one paid student
|
||||
# Trial teacher: at least one trial student in course instance, and no paid students
|
||||
# Free teacher: no paid students, no trial students
|
||||
# Teacher furthest course is furthest course of highest paid status student
|
||||
teacherFurthestCourseMap = {}
|
||||
teacherPaidStatusMap = {}
|
||||
for teacher, students of teacherStudentsMap
|
||||
for student in students
|
||||
if not teacherPaidStatusMap[teacher]
|
||||
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
|
||||
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
|
||||
else if teacherPaidStatusMap[teacher] is 'trial' and studentPaidStatusMap[student] is 'paid'
|
||||
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
|
||||
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
|
||||
else if teacherPaidStatusMap[teacher] is 'free' and studentPaidStatusMap[student] in ['paid', 'trial']
|
||||
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
|
||||
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
|
||||
else if teacherFurthestCourseMap[teacher] < studentFurthestCourseMap[student]
|
||||
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
|
||||
|
||||
# Build table of student/teacher paid/trial/free totals
|
||||
updateCourseTotalsMap = (courseTotalsMap, furthestCourseMap, paidStatusMap, columnSuffix) =>
|
||||
for user, courseIndex of furthestCourseMap
|
||||
courseName = @courses.models[courseIndex].get('name')
|
||||
courseTotalsMap[courseName] ?= {}
|
||||
columnName = switch paidStatusMap[user]
|
||||
when 'paid' then 'Paid ' + columnSuffix
|
||||
when 'trial' then 'Trial ' + columnSuffix
|
||||
when 'free' then 'Free ' + columnSuffix
|
||||
courseTotalsMap[courseName][columnName] ?= 0
|
||||
courseTotalsMap[courseName][columnName]++
|
||||
courseTotalsMap[courseName]['Total ' + columnSuffix] ?= 0
|
||||
courseTotalsMap[courseName]['Total ' + columnSuffix]++
|
||||
courseTotalsMap['All Courses']['Total ' + columnSuffix] ?= 0
|
||||
courseTotalsMap['All Courses']['Total ' + columnSuffix]++
|
||||
courseTotalsMap['All Courses'][columnName] ?= 0
|
||||
courseTotalsMap['All Courses'][columnName]++
|
||||
courseTotalsMap = {'All Courses': {}}
|
||||
updateCourseTotalsMap(courseTotalsMap, teacherFurthestCourseMap, teacherPaidStatusMap, 'Teachers')
|
||||
updateCourseTotalsMap(courseTotalsMap, studentFurthestCourseMap, studentPaidStatusMap, 'Students')
|
||||
|
||||
courseDistributions = []
|
||||
for courseName, totals of courseTotalsMap
|
||||
courseDistributions.push({courseName: courseName, totals: totals})
|
||||
courseDistributions.sort (a, b) ->
|
||||
if a.courseName.indexOf('Introduction') >= 0 and b.courseName.indexOf('Introduction') < 0 then return -1
|
||||
else if b.courseName.indexOf('Introduction') >= 0 and a.courseName.indexOf('Introduction') < 0 then return 1
|
||||
else if a.courseName.indexOf('All Courses') >= 0 and b.courseName.indexOf('All Courses') < 0 then return 1
|
||||
else if b.courseName.indexOf('All Courses') >= 0 and a.courseName.indexOf('All Courses') < 0 then return -1
|
||||
a.courseName.localeCompare(b.courseName)
|
||||
|
||||
courseDistributions
|
||||
|
||||
@courseDistributionsRecent = createCourseDistributions(@furthestCourseDayRangeRecent)
|
||||
@courseDistributions = createCourseDistributions(@furthestCourseDayRange)
|
||||
|
||||
createLineChartPoints: (days, data) ->
|
||||
points = []
|
||||
|
|
|
@ -60,6 +60,7 @@ function getRecurringRevenueCounts(startDay) {
|
|||
}
|
||||
|
||||
if (doc.service === 'ios' || doc.service === 'bitcoin') continue;
|
||||
if (!doc.amount || doc.amount <= 0) continue;
|
||||
|
||||
if (doc.prepaidID) {
|
||||
if (prepaidDayAmountMap[doc.prepaidID.valueOf()]) {
|
||||
|
|
|
@ -207,9 +207,27 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
|||
query = {$and: [{name: {$ne: 'Single Player'}}, {hourOfCode: {$ne: true}}]}
|
||||
query["$and"].push(_id: {$gte: objectIdFromTimestamp(req.body.startDay + "T00:00:00.000Z")}) if req.body.startDay?
|
||||
query["$and"].push(_id: {$lt: objectIdFromTimestamp(req.body.endDay + "T00:00:00.000Z")}) if req.body.endDay?
|
||||
CourseInstance.find query, (err, courseInstances) =>
|
||||
CourseInstance.find query, {courseID: 1, members: 1, ownerID: 1}, (err, courseInstances) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, courseInstances)
|
||||
userIDs = []
|
||||
for courseInstance in courseInstances
|
||||
if members = courseInstance.get('members')
|
||||
userIDs.push(userID) for userID in members
|
||||
|
||||
User.find {_id: {$in: userIDs}}, {coursePrepaidID: 1}, (err, users) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
prepaidIDs = []
|
||||
for user in users
|
||||
if prepaidID = user.get('coursePrepaidID')
|
||||
prepaidIDs.push(prepaidID)
|
||||
|
||||
Prepaid.find {_id: {$in: prepaidIDs}}, {properties: 1}, (err, prepaids) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
data =
|
||||
courseInstances: (@formatEntity(req, courseInstance) for courseInstance in courseInstances)
|
||||
students: (@formatEntity(req, user) for user in users)
|
||||
prepaids: (@formatEntity(req, prepaid) for prepaid in prepaids)
|
||||
@sendSuccess(res, data)
|
||||
|
||||
inviteStudents: (req, res, courseInstanceID) ->
|
||||
return @sendUnauthorizedError(res) if not req.user?
|
||||
|
|
|
@ -44,7 +44,7 @@ PaymentHandler = class PaymentHandler extends Handler
|
|||
payment
|
||||
|
||||
getSchoolSalesAPI: (req, res, code) ->
|
||||
return @sendSuccess(res, []) unless req.user?.isAdmin()
|
||||
return @sendUnauthorizedError(res) unless req.user?.isAdmin()
|
||||
userIDs = [];
|
||||
Payment.find({}, {amount: 1, created: 1, description: 1, prepaidID: 1, productID: 1, purchaser: 1, service: 1}).exec (err, payments) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
|
|
Reference in a new issue