206 lines
7.7 KiB
TypeScript
206 lines
7.7 KiB
TypeScript
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
|
import type { NextApiRequest, NextApiResponse } from "next";
|
|
import { withIronSessionApiRoute } from "iron-session/next";
|
|
import { sessionOptions } from "@/lib/session";
|
|
import { getDownloadURL, getStorage, ref } from "firebase/storage";
|
|
import { app, storage } from "@/firebase";
|
|
import axios from "axios";
|
|
import { requestUser } from "@/utils/api";
|
|
import { checkAccess } from "@/utils/permissions";
|
|
import { EntityWithRoles } from "@/interfaces/entity";
|
|
import { Stat, StudentUser } from "@/interfaces/user";
|
|
import { Assignment, AssignmentResult } from "@/interfaces/results";
|
|
import { Exam } from "@/interfaces/exam";
|
|
import { capitalize, groupBy, uniqBy } from "lodash";
|
|
import { findBy, mapBy } from "@/utils";
|
|
import ExcelJS from "exceljs";
|
|
import moment from "moment";
|
|
import { Session } from "@/hooks/useSessions";
|
|
import { getGradingSystemByEntity } from "@/utils/grading.be";
|
|
import { getGradingLabel } from "@/utils/score";
|
|
|
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
|
|
|
interface Item {
|
|
student: StudentUser
|
|
result: AssignmentResult
|
|
assignment: Assignment
|
|
exams: Exam[]
|
|
session?: Session
|
|
}
|
|
|
|
interface Body {
|
|
entities: EntityWithRoles[]
|
|
items: Item[]
|
|
assignments: Assignment[]
|
|
startDate: Date
|
|
endDate: Date
|
|
}
|
|
|
|
interface EntityInformation {
|
|
entity: EntityWithRoles
|
|
exams: Exam[]
|
|
numberOfAssignees: number
|
|
numberOfSubmissions: number
|
|
numberOfAbsentees: number
|
|
assignment: Assignment
|
|
items: Item[]
|
|
}
|
|
|
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
if (req.method !== "POST") return res.status(404).json({ ok: false })
|
|
|
|
const user = await requestUser(req, res)
|
|
if (!user) return res.status(401).json({ ok: false });
|
|
if (!checkAccess(user, ['admin', 'developer', 'mastercorporate', 'corporate'])) return res.status(403).json({ ok: false });
|
|
|
|
const { entities, items, assignments } = req.body as Body
|
|
const entityInformations: EntityInformation[] = []
|
|
|
|
for (const entity of entities) {
|
|
const entityItems = items.filter(i => i.assignment.entity === entity.id)
|
|
const groupedByAssignments = groupBy(entityItems, (a) => a.assignment.id)
|
|
for (const assignmentID of Object.keys(groupedByAssignments)) {
|
|
const assignmentItems = groupedByAssignments[assignmentID]
|
|
const assignment = findBy(assignments, 'id', assignmentID)!
|
|
const assignmentExams =
|
|
uniqBy(assignmentItems.flatMap(a => a.exams.map(e => ({ ...e, moduleID: `${e.id}_${e.module}` }))), 'moduleID')
|
|
|
|
const assignmentEntityInformation: EntityInformation = {
|
|
entity,
|
|
exams: assignmentExams,
|
|
numberOfAssignees: assignmentItems.length,
|
|
numberOfSubmissions: assignmentItems.filter(x => !!x.result).length,
|
|
numberOfAbsentees: assignmentItems.filter(x => !x.result).length,
|
|
assignment,
|
|
items: assignmentItems
|
|
}
|
|
|
|
entityInformations.push(assignmentEntityInformation)
|
|
}
|
|
}
|
|
|
|
const workbook = new ExcelJS.Workbook();
|
|
const worksheet = workbook.addWorksheet("Statistical");
|
|
|
|
for (const e of entityInformations) {
|
|
await addEntityInformationToWorksheet(worksheet, e)
|
|
}
|
|
|
|
const buffer = await workbook.xlsx.writeBuffer()
|
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
|
res.status(200).send(buffer);
|
|
}
|
|
|
|
const addEntityInformationToWorksheet = async (worksheet: ExcelJS.Worksheet, entityInformation: EntityInformation) => {
|
|
const data = [
|
|
['Entity', undefined, undefined, entityInformation.entity.label],
|
|
['Assignment', undefined, undefined, entityInformation.assignment.name],
|
|
['Date of the Assignment', undefined, undefined, moment(entityInformation.assignment.startDate).format("DD/MM/YYYY")],
|
|
['Exams', undefined, undefined, mapBy(entityInformation.exams, 'id').join(', ')],
|
|
['Modules', undefined, undefined, entityInformation.exams.map(e => capitalize(e.module)).join(', ')],
|
|
['Number of Assignees', undefined, undefined, entityInformation.numberOfAssignees],
|
|
['Number of Submissions', undefined, undefined, entityInformation.numberOfSubmissions],
|
|
['Number of Absentees', undefined, undefined, entityInformation.numberOfAbsentees]
|
|
]
|
|
|
|
const dataRows = worksheet.addRows(data);
|
|
dataRows.forEach(row => row.getCell(1).font = { bold: true, color: { argb: "ffffffff" } })
|
|
dataRows.forEach(row => row.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "ff674ea7" } })
|
|
dataRows.forEach(row => worksheet.mergeCells(`${row.getCell(1).address}:${row.getCell(3).address}`))
|
|
dataRows.forEach(row => row.getCell(4).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "FFD9D2E9" } })
|
|
dataRows.forEach(row => worksheet.mergeCells(`${row.getCell(4).address}:${row.getCell(7).address}`))
|
|
|
|
worksheet.addRows([[], []]);
|
|
const gradingSystem = await getGradingSystemByEntity(entityInformation.entity.id)
|
|
|
|
for (const exam of entityInformation.exams) {
|
|
const examRow = worksheet.addRow([`${capitalize(exam.module)} Exam`, undefined, exam.id])
|
|
examRow.getCell(1).font = { bold: true, color: { argb: "ffffffff" } }
|
|
examRow.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "ff674ea7" } }
|
|
examRow.getCell(3).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "FFD9D2E9" } }
|
|
|
|
worksheet.mergeCells(`${examRow.getCell(1).address}:${examRow.getCell(2).address}`)
|
|
worksheet.mergeCells(`${examRow.getCell(3).address}:${examRow.getCell(6).address}`)
|
|
|
|
const parts = exam.module === "level" || exam.module === "listening" || exam.module === "reading" ? exam.parts : []
|
|
|
|
const header = worksheet.addRow([
|
|
"#",
|
|
"Name",
|
|
"E-mail",
|
|
"Student ID",
|
|
"Passport/ID",
|
|
"Gender",
|
|
"Finished at",
|
|
"Score",
|
|
...(exam.module === "level" ? ["Grade"] : []),
|
|
...parts.map((_, i) => `Part ${i + 1}`)
|
|
])
|
|
header.font = { bold: true, color: { argb: "FFFFFFFF" } }
|
|
header.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "FFD9D2E9" } }
|
|
|
|
const examItems =
|
|
entityInformation.items
|
|
.filter(i => !!i.result)
|
|
.map(i => ({
|
|
...i,
|
|
result: { ...i.result, stats: i.result.stats.filter(x => x.exam === exam.id) },
|
|
}))
|
|
|
|
const orderedItems = examItems.sort((a, b) => {
|
|
const aTotalScore = a.result.stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0)
|
|
const bTotalScore = b.result.stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0)
|
|
|
|
return bTotalScore - aTotalScore
|
|
})
|
|
|
|
const itemRows = orderedItems.map((item, index) => {
|
|
const { total, correct } = calculateScore(item.result.stats)
|
|
const score = `${correct} / ${total}`
|
|
|
|
const finishTimestamp = [...item.result.stats].sort((a, b) => b.date - a.date).shift()?.date || -1
|
|
const finishDate = finishTimestamp === -1 ? "N/A" : moment(new Date(finishTimestamp)).format("DD/MM/YYYY HH:mm")
|
|
|
|
const grade = getGradingLabel(correct, gradingSystem.steps)
|
|
|
|
return [
|
|
index + 1,
|
|
item.student.name,
|
|
item.student.email,
|
|
item.student.studentID || "N/A",
|
|
item.student.demographicInformation?.passport_id || "N/A",
|
|
item.student.demographicInformation?.gender || "N/A",
|
|
finishDate,
|
|
score,
|
|
...(exam.module === "level" ? [grade] : []),
|
|
...parts.map((part) => {
|
|
const exerciseIDs = mapBy(part.exercises, 'id')
|
|
const { total, correct } = calculateScore(item.result.stats.filter(s => exerciseIDs.includes(s.exercise)))
|
|
|
|
return `${correct} / ${total}`
|
|
})
|
|
]
|
|
})
|
|
|
|
worksheet.addRows(itemRows)
|
|
worksheet.addRows([[]]);
|
|
}
|
|
worksheet.addRows([[], []]);
|
|
}
|
|
|
|
const calculateScore = (stats: Stat[]) => {
|
|
const total = stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.total, 0)
|
|
const correct = stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0)
|
|
|
|
return { total, correct }
|
|
}
|
|
|
|
export const config = {
|
|
api: {
|
|
bodyParser: {
|
|
sizeLimit: '20mb',
|
|
},
|
|
},
|
|
};
|