465 lines
13 KiB
TypeScript
465 lines
13 KiB
TypeScript
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<AssignmentData>({ _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<User>({ _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" });
|
|
}
|