// 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', }, }, };