feat: add grades & averages
This commit is contained in:
parent
35a8191938
commit
1d3728b48d
6 changed files with 248 additions and 3 deletions
41
app/controllers/grades_controller.ts
Normal file
41
app/controllers/grades_controller.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { GradeService } from '#services/grade_service'
|
||||
import { inject } from '@adonisjs/core'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
const PERIODS = ['MONTH', 'SEMESTER', 'YEAR']
|
||||
|
||||
@inject()
|
||||
export default class GradesController {
|
||||
constructor(private gradeService: GradeService) {}
|
||||
|
||||
// GET /grades
|
||||
async index({ auth, request, response }: HttpContext) {
|
||||
const period = request.input('period', PERIODS[0]).toUpperCase()
|
||||
if (!PERIODS.includes(period)) {
|
||||
return response.badRequest({
|
||||
message: `Invalid period. Allowed values are: ${PERIODS.join(', ')}`,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Choose a start date
|
||||
// const { startDate: rawStartDate } = request.qs()
|
||||
|
||||
// Validate startDate
|
||||
// const startDate = rawStartDate ? DateTime.fromISO(rawStartDate, { zone: 'local' }) : null
|
||||
// if (!rawStartDate || !startDate || !startDate.isValid) {
|
||||
// return response.badRequest({ message: 'Invalid start date format' })
|
||||
// }
|
||||
|
||||
const userId = auth.user!.id
|
||||
if (period === PERIODS[0]) {
|
||||
const startDate = DateTime.now().minus({ month: 1 })
|
||||
return this.gradeService.getMonthGrade(userId, startDate)
|
||||
}
|
||||
const months = period === PERIODS[1] ? 3 : 12
|
||||
const startDate = DateTime.now().minus({ months })
|
||||
return this.gradeService.getPeriodGrade(userId, startDate, months)
|
||||
}
|
||||
|
||||
// TODO: Radar chart for subjects
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ export default class NotificationsController {
|
|||
const device = this.service.getUserSignature(ua)
|
||||
|
||||
const data = await request.validateUsing(subscribeValidator)
|
||||
await Subscription.create({
|
||||
return Subscription.create({
|
||||
userId: auth.user!.id,
|
||||
device,
|
||||
events: 0, // Default to no events
|
||||
|
|
|
|||
174
app/services/grade_service.ts
Normal file
174
app/services/grade_service.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import Colle from '#models/colle'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
export class GradeService {
|
||||
private calculateAverage(colles: Colle[]) {
|
||||
const total = colles.reduce((sum, colle) => sum + colle.grade, 0)
|
||||
return parseFloat((total / colles.length).toFixed(2))
|
||||
}
|
||||
|
||||
private calculateSubjectAverage(colles: Colle[], subject: string) {
|
||||
const subjectColles = colles.filter((colle) => colle.subject.name === subject)
|
||||
const total = subjectColles.reduce((sum, colle) => sum + colle.grade, 0)
|
||||
return parseFloat((total / subjectColles.length).toFixed(2))
|
||||
}
|
||||
|
||||
private getSubjects(colles: Colle[]) {
|
||||
return Array.from(new Set(colles.map((colle) => colle.subject.name)))
|
||||
}
|
||||
|
||||
private getPeriodColles(colles: Colle[], startDate: DateTime, endDate: DateTime) {
|
||||
return colles.filter((colle) => colle.date >= startDate && colle.date < endDate)
|
||||
}
|
||||
|
||||
private getColles(userId: number, startDate: DateTime, months: number = 0) {
|
||||
const endDate = startDate.plus({ months })
|
||||
return Colle.query()
|
||||
.where('studentId', userId)
|
||||
.where('date', '>=', startDate.toJSDate())
|
||||
.where('date', '<', endDate.toJSDate())
|
||||
.preload('subject')
|
||||
.orderBy('date', 'asc')
|
||||
}
|
||||
|
||||
private async getGlobalAverage(userId: number, subjectId: number, beforeDate: DateTime) {
|
||||
const colles = await Colle.query()
|
||||
.where('studentId', userId)
|
||||
.where('subjectId', subjectId)
|
||||
.where('date', '<', beforeDate.toJSDate())
|
||||
return colles.length ? this.calculateAverage(colles) : 0
|
||||
}
|
||||
|
||||
public async getMonthGrade(userId: number, startDate: DateTime) {
|
||||
const colles = await this.getColles(userId, startDate, 1)
|
||||
const subjects = this.getSubjects(colles)
|
||||
|
||||
const results: PeriodResult[] = []
|
||||
let periodStartDate = startDate
|
||||
|
||||
for (let week = 1; week <= 4; week++) {
|
||||
const periodEndDate = periodStartDate.plus({ weeks: 1 })
|
||||
const periodColles = this.getPeriodColles(colles, periodStartDate, periodEndDate)
|
||||
|
||||
const periodAverage = this.calculateAverage(periodColles)
|
||||
const subjectAverages = await this.getSubjectAverages(
|
||||
subjects,
|
||||
periodColles,
|
||||
colles,
|
||||
results,
|
||||
week,
|
||||
userId,
|
||||
periodStartDate
|
||||
)
|
||||
|
||||
results.push({
|
||||
period: `Week ${week}`,
|
||||
average: periodAverage,
|
||||
...this.reduce(subjectAverages),
|
||||
})
|
||||
|
||||
periodStartDate = periodEndDate
|
||||
}
|
||||
|
||||
return { grades: results, subjects }
|
||||
}
|
||||
|
||||
public async getPeriodGrade(userId: number, startDate: DateTime, months: number = 0) {
|
||||
const colles = await this.getColles(userId, startDate, months)
|
||||
const subjects = this.getSubjects(colles)
|
||||
|
||||
const results: PeriodResult[] = []
|
||||
let periodStartDate = startDate
|
||||
|
||||
let index = 1
|
||||
|
||||
while (periodStartDate < startDate.plus({ months })) {
|
||||
const periodEndDate = periodStartDate.endOf('month')
|
||||
const periodColles = this.getPeriodColles(colles, periodStartDate, periodEndDate)
|
||||
|
||||
const periodAverage = this.calculateAverage(periodColles)
|
||||
const subjectAverages = await this.getSubjectAverages(
|
||||
subjects,
|
||||
periodColles,
|
||||
colles,
|
||||
results,
|
||||
index,
|
||||
userId,
|
||||
periodStartDate
|
||||
)
|
||||
|
||||
console.log(this.reduce(subjectAverages), subjectAverages)
|
||||
results.push({
|
||||
period: periodStartDate.toFormat('MMM'),
|
||||
average: periodAverage,
|
||||
...this.reduce(subjectAverages),
|
||||
})
|
||||
|
||||
periodStartDate = periodEndDate.plus({ days: 1 })
|
||||
index++
|
||||
}
|
||||
|
||||
return { grades: results, subjects }
|
||||
}
|
||||
|
||||
private reduce(subjectAverages: SubjectPerformance[]) {
|
||||
return subjectAverages.reduce((acc: any, { subject, average }) => {
|
||||
acc[subject] = average
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
private async getSubjectAverages(
|
||||
subjects: string[],
|
||||
periodColles: Colle[],
|
||||
allColles: Colle[],
|
||||
previousResults: PeriodResult[],
|
||||
unitIndex: number,
|
||||
userId: number,
|
||||
periodStart: DateTime
|
||||
) {
|
||||
const results = await Promise.all(
|
||||
subjects.map(async (subject) => {
|
||||
if (periodColles.length > 0) {
|
||||
return {
|
||||
subject,
|
||||
average: this.calculateSubjectAverage(periodColles, subject),
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use the previous unit's average
|
||||
if (unitIndex > 1) {
|
||||
const prev = previousResults[unitIndex - 2].subjectAverages?.find(
|
||||
(s) => s.subject === subject
|
||||
)
|
||||
if (prev) {
|
||||
return { subject, average: prev.average }
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise fall back to global average before this period
|
||||
const subjectId = allColles.find((colle) => colle.subject.name === subject)!.subjectId
|
||||
|
||||
const beforeAverage = await this.getGlobalAverage(userId, subjectId, periodStart)
|
||||
if (beforeAverage) {
|
||||
return { subject, average: beforeAverage }
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
|
||||
return results.filter((s): s is SubjectPerformance => Boolean(s))
|
||||
}
|
||||
}
|
||||
|
||||
interface SubjectPerformance {
|
||||
subject: string
|
||||
average: number
|
||||
}
|
||||
|
||||
interface PeriodResult {
|
||||
period: string
|
||||
average: number
|
||||
subjectAverages: SubjectPerformance[]
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ export class NotificationService {
|
|||
? device.type
|
||||
: 'Desktop'
|
||||
|
||||
return [deviceStr, osStr, browserStr].filter(Boolean).join(' | ')
|
||||
return [deviceStr, osStr, browserStr].filter(Boolean).join(' - ')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
export default class extends BaseSchema {
|
||||
protected tableName = 'subscriptions'
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id')
|
||||
table
|
||||
.integer('user_id')
|
||||
.notNullable()
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('users')
|
||||
.onDelete('CASCADE')
|
||||
table.string('device').notNullable()
|
||||
table.integer('events').defaultTo(0).notNullable()
|
||||
table.jsonb('data').notNullable()
|
||||
table.integer('failed_attempts').defaultTo(0).notNullable()
|
||||
table.boolean('enabled').defaultTo(true).notNullable()
|
||||
})
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
||||
|
|
@ -52,10 +52,13 @@ const NotificationsController = () => import('#controllers/notifications_control
|
|||
router
|
||||
.group(() => {
|
||||
router.get('/', [NotificationsController, 'index'])
|
||||
router.post('/subscribe', [NotificationsController, 'subscribe'])
|
||||
router.post('/', [NotificationsController, 'subscribe'])
|
||||
router.post('/:id/unsubscribe', [NotificationsController, 'unsubscribe'])
|
||||
router.post('/:id', [NotificationsController, 'update'])
|
||||
router.post('/:id/test', [NotificationsController, 'test']).use(middleware.auth())
|
||||
})
|
||||
.prefix('/notifications')
|
||||
.use(middleware.auth())
|
||||
|
||||
const GradesController = () => import('#controllers/grades_controller')
|
||||
router.get('/grades', [GradesController, 'index']).use(middleware.auth())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue