import type { NextApiRequest, NextApiResponse } from "next"; import { app, storage } from "@/firebase"; import client from "@/lib/mongodb"; import { ObjectId } from 'mongodb'; import { withIronSessionApiRoute } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; import { CorporateUser, MasterCorporateUser } from "@/interfaces/user"; import { User } from "@/interfaces/user"; import { Module } from "@/interfaces"; import moment from "moment-timezone"; import ExcelJS from "exceljs"; import { getStudentGroupsForUsersWithoutAdmin } from "@/utils/groups.be"; import { getSpecificUsers, getUser } from "@/utils/users.be"; import { getUserName } from "@/utils/users"; interface GroupScoreSummaryHelper { score: [number, number]; label: string; sessions: string[]; } interface AssignmentData { _id: ObjectId; assigner: string; assignees: string[]; results: any; exams: { module: Module }[]; startDate: string; excel: { path: string; version: string; }; name: string; } const db = client.db(process.env.MONGODB_DB); export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { // if (req.method === "GET") return get(req, res); if (req.method === "POST") return await post(req, res); } function logWorksheetData(worksheet: any) { worksheet.eachRow((row: any, rowNumber: number) => { console.log(`Row ${rowNumber}:`); row.eachCell((cell: any, colNumber: number) => { console.log(` Cell ${colNumber}: ${cell.value}`); }); }); } function commonExcel({ data, userName, users, sectionName, customTable, customTableHeaders, renderCustomTableData, }: { data: AssignmentData; userName: string; users: User[]; sectionName: string; customTable: string[][]; customTableHeaders: string[]; renderCustomTableData: (data: any) => string[]; }) { const allStats = data.results.flatMap((r: any) => r.stats); const uniqueExercises = [...new Set(allStats.map((s: any) => s.exercise))]; const assigneesData = data.assignees .map((assignee: string) => { const userStats = allStats.filter((s: any) => s.user === assignee); const dates = userStats.map((s: any) => moment(s.date)); const user = users.find((u) => u.id === assignee); return { userId: assignee, // added some default values in case the user is not found // could it be possible to have an assigned user deleted from the database? user: user || { name: "Unknown", email: "Unknown", demographicInformation: { passportId: "Unknown", gender: "Unknown" }, }, ...userStats.reduce( (acc: any, curr: any) => { return { ...acc, correct: acc.correct + curr.score.correct, missing: acc.missing + curr.score.missing, total: acc.total + curr.score.total, }; }, { correct: 0, missing: 0, total: 0 } ), firstDate: moment.min(...dates), lastDate: moment.max(...dates), stats: userStats, }; }) .sort((a, b) => b.correct - a.correct); const results = assigneesData.map((r: any) => r.correct); const highestScore = Math.max(...results); const lowestScore = Math.min(...results); const averageScore = results.reduce((a, b) => a + b, 0) / results.length; const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate)); const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate)); const firstSectionData = [ { label: sectionName, value: userName, }, { label: "Report Download date :", value: moment().format("DD/MM/YYYY"), }, { label: "Test Information :", value: data.name }, { label: "Date of Test :", value: moment(data.startDate).format("DD/MM/YYYY"), }, { label: "Number of Candidates :", value: data.assignees.length }, { label: "Highest score :", value: highestScore }, { label: "Lowest score :", value: lowestScore }, { label: "Average score :", value: averageScore }, { label: "", value: "" }, { label: "Date and time of First submission :", value: firstDate.format("DD/MM/YYYY"), }, { label: "Date and time of Last submission :", value: lastDate.format("DD/MM/YYYY"), }, ]; // Create a new workbook and add a worksheet const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet("Report Data"); // Populate the worksheet with the data firstSectionData.forEach(({ label, value }, index) => { worksheet.getCell(`A${index + 1}`).value = label; // First column (labels) worksheet.getCell(`B${index + 1}`).value = value; // Second column (values) }); // added empty arrays to force row spacings const customTableAndLine = [[], ...customTable, []]; customTableAndLine.forEach((row: string[], index) => { worksheet.addRow(row); }); // Define the static part of the headers (before "Test Sections") const staticHeaders = [ "Sr N", "Candidate ID", "First and Last Name", "Passport/ID", "Email ID", "Gender", ...customTableHeaders, ]; // Define additional headers after "Test Sections" const additionalHeaders = ["Time Spent", "Date", "Score"]; // Calculate the dynamic columns based on the testSectionsArray const testSectionHeaders = uniqueExercises.map( (section, index) => `Part ${index + 1}` ); const tableColumnHeadersFirstPart = [ ...staticHeaders, ...uniqueExercises.map((a) => "Test Sections"), ]; // Add the main header row, merging static columns and "Test Sections" const tableColumnHeaders = [ ...tableColumnHeadersFirstPart, ...additionalHeaders, ]; worksheet.addRow(tableColumnHeaders); // 1 headers rows const startIndexTable = firstSectionData.length + customTableAndLine.length + 1; // // Merge "Test Sections" over dynamic number of columns // const tableColumns = staticHeaders.length + numberOfTestSections; // K10:M12 = 10,11,12,13 // horizontally group Test Sections // if there are test section headers to even merge: if (testSectionHeaders.length > 1) { worksheet.mergeCells( startIndexTable, staticHeaders.length + 1, startIndexTable, tableColumnHeadersFirstPart.length ); } // Add the dynamic second and third header rows for test sections and sub-columns worksheet.addRow([ ...Array(staticHeaders.length).fill(""), ...testSectionHeaders, "", "", "", ]); worksheet.addRow([ ...Array(staticHeaders.length).fill(""), ...uniqueExercises.map(() => "Grammar & Vocabulary"), "", "", "", ]); worksheet.addRow([ ...Array(staticHeaders.length).fill(""), ...uniqueExercises.map( (exercise) => allStats.find((s: any) => s.exercise === exercise).type ), "", "", "", ]); // vertically group based on the part, exercise and type staticHeaders.forEach((header, index) => { worksheet.mergeCells( startIndexTable, index + 1, startIndexTable + 3, index + 1 ); }); assigneesData.forEach((data, index) => { worksheet.addRow([ index + 1, data.userId, data.user.name, data.user.demographicInformation?.passportId, data.user.email, data.user.demographicInformation?.gender, ...renderCustomTableData(data), ...uniqueExercises.map((exercise) => { const score = data.stats.find( (s: any) => s.exercise === exercise && s.user === data.userId ).score; return `${score.correct}/${score.total}`; }), `${Math.ceil( data.stats.reduce((acc: number, curr: any) => acc + curr.timeSpent, 0) / 60 )} minutes`, data.lastDate.format("DD/MM/YYYY HH:mm"), data.correct, ]); }); worksheet.addRow([""]); worksheet.addRow([""]); for (let i = 0; i < tableColumnHeaders.length; i++) { worksheet.getColumn(i + 1).width = 30; } // Apply styles to the headers [startIndexTable].forEach((rowNumber) => { worksheet.getRow(rowNumber).eachCell((cell) => { if (cell.value) { cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFBFBFBF" }, // Grey color for headers }; cell.font = { bold: true }; cell.alignment = { vertical: "middle", horizontal: "center" }; } }); }); worksheet.addRow(["Printed by: Confidential Information"]); worksheet.addRow(["info@encoach.com"]); // Convert workbook to Buffer (Node.js) or Blob (Browser) return workbook.xlsx.writeBuffer(); } function corporateAssignment( user: CorporateUser, data: AssignmentData, users: User[] ) { return commonExcel({ data, userName: user.corporateInformation?.companyInformation?.name || "", users, sectionName: "Corporate Name :", customTable: [], customTableHeaders: [], renderCustomTableData: () => [], }); } async function mastercorporateAssignment( user: MasterCorporateUser, data: AssignmentData, users: User[] ) { const userGroups = await getStudentGroupsForUsersWithoutAdmin( user.id, data.assignees ); const adminUsers = [...new Set(userGroups.map((g) => g.admin))]; const userGroupsParticipants = userGroups.flatMap((g) => g.participants); const adminsData = await getSpecificUsers(adminUsers); const companiesData = adminsData.map((user) => { const name = getUserName(user); const users = userGroupsParticipants.filter((p) => data.assignees.includes(p) ); const stats = data.results .flatMap((r: any) => r.stats) .filter((s: any) => users.includes(s.user)); const correct = stats.reduce( (acc: number, s: any) => acc + s.score.correct, 0 ); const total = stats.reduce( (acc: number, curr: any) => acc + curr.score.total, 0 ); return { name, correct, total, }; }); const customTable = [ ...companiesData, { name: "Total", correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0), total: companiesData.reduce((acc, curr) => acc + curr.total, 0), }, ].map((c) => [c.name, `${c.correct}/${c.total}`]); const customTableHeaders = [ { name: "Corporate", helper: (data: any) => data.user.corporateName }, ]; return commonExcel({ data, userName: user.corporateInformation?.companyInformation?.name || "", users: users.map((u) => { const userGroup = userGroups.find((g) => g.participants.includes(u.id)); const admin = adminsData.find((a) => a.id === userGroup?.admin); return { ...u, corporateName: getUserName(admin), }; }), sectionName: "Master Corporate Name :", customTable: [["Corporate Summary"], ...customTable], customTableHeaders: customTableHeaders.map((h) => h.name), renderCustomTableData: (data) => customTableHeaders.map((h) => h.helper(data)), }); } async function post(req: NextApiRequest, res: NextApiResponse) { // verify if it's a logged user that is trying to export if (req.session.user) { const { id } = req.query as { id: string }; const assignment = await db.collection("assignments").findOne({ _id: new ObjectId(id) }); if (!assignment) { res.status(400).end(); return; } // if ( // data.excel && // data.excel.path && // data.excel.version === process.env.EXCEL_VERSION // ) { // // if it does, return the excel url // const fileRef = ref(storage, data.excel.path); // const url = await getDownloadURL(fileRef); // res.status(200).end(url); // return; // } const objectIds = assignment.assignees.map(id => new ObjectId(id)); const users = await db.collection("users").find({ _id: { $in: objectIds } }).toArray() as User[] | null; const user = await db.collection("users").findOne({ _id: new ObjectId(assignment.assigner) }); // we'll need the user in order to get the user data (name, email, focus, etc); if (user && users) { // generate the file ref for storage const fileName = `${Date.now().toString()}.xlsx`; const refName = `assignment_report/${fileName}`; const fileRef = ref(storage, refName); const getExcelFn = () => { switch (user.type) { case "teacher": case "corporate": return corporateAssignment(user as CorporateUser, assignment, users); case "mastercorporate": return mastercorporateAssignment( user as MasterCorporateUser, assignment, users ); default: throw new Error("Invalid user type"); } }; const buffer = await getExcelFn(); // upload the pdf to storage await uploadBytes(fileRef, buffer, { contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); // update the stats entries with the pdf url to prevent duplication await db.collection("assignments").updateOne( { _id: assignment._id }, { $set: { excel: { path: refName, version: process.env.EXCEL_VERSION, } } } ); const url = await getDownloadURL(fileRef); res.status(200).end(url); return; } } res.status(401).json({ message: "Unauthorized" }); }