Merged in ENCOA-83_MasterStatistical (pull request #74)

ENCOA-83 MasterStatistical

Approved-by: Tiago Ribeiro
This commit is contained in:
João Ramos
2024-08-20 10:14:19 +00:00
committed by Tiago Ribeiro
4 changed files with 424 additions and 266 deletions

View File

@@ -13,30 +13,34 @@ import {
} from "firebase/firestore"; } from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next"; import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import ReactPDF from "@react-pdf/renderer";
import GroupTestReport from "@/exams/pdf/group.test.report";
import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { Stat, CorporateUser } from "@/interfaces/user"; import { CorporateUser, MasterCorporateUser } from "@/interfaces/user";
import { User, DemographicInformation } from "@/interfaces/user"; import { User } from "@/interfaces/user";
import { Module } from "@/interfaces"; import { Module } from "@/interfaces";
import { ModuleScore, StudentData } from "@/interfaces/module.scores";
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
import { LevelExamDetails } from "@/exams/pdf/details/level.exam";
import { calculateBandScore, getLevelScore } from "@/utils/score";
import {
generateQRCode,
getRadialProgressPNG,
streamToBuffer,
} from "@/utils/pdf";
import { Group } from "@/interfaces/user";
import moment from "moment-timezone"; import moment from "moment-timezone";
import ExcelJS from "exceljs"; import ExcelJS from "exceljs";
import { getStudentGroupsForUsersWithoutAdmin } from "@/utils/groups.be";
import { getSpecificUsers, getUser } from "@/utils/users.be";
import { getUserName } from "@/utils/users";
interface GroupScoreSummaryHelper { interface GroupScoreSummaryHelper {
score: [number, number]; score: [number, number];
label: string; label: string;
sessions: string[]; sessions: string[];
} }
interface AssignmentData {
assigner: string;
assignees: string[];
results: any;
exams: { module: Module }[];
startDate: string;
excel: {
path: string;
version: string;
};
name: string;
}
const db = getFirestore(app); const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions); export default withIronSessionApiRoute(handler, sessionOptions);
@@ -55,63 +59,31 @@ function logWorksheetData(worksheet: any) {
}); });
} }
async function post(req: NextApiRequest, res: NextApiResponse) { function commonExcel({
// verify if it's a logged user that is trying to export data,
if (req.session.user) { userName,
const { id } = req.query as { id: string }; users,
sectionName,
const docSnap = await getDoc(doc(db, "assignments", id)); customTable,
const data = docSnap.data() as { customTableHeaders,
assigner: string; renderCustomTableData,
assignees: string[]; }: {
results: any; data: AssignmentData;
exams: { module: Module }[]; userName: string;
startDate: string; users: User[];
excel: { sectionName: string;
path: string; customTable: string[][];
version: string; customTableHeaders: string[];
}; renderCustomTableData: (data: any) => string[];
name: string; }) {
};
if (!data) {
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 docsSnap = await getDocs(
query(collection(db, "users"), where(documentId(), "in", data.assignees))
);
const users = docsSnap.docs.map((d) => ({
...d.data(),
id: d.id,
})) as User[];
const docUser = await getDoc(doc(db, "users", req.session.user.id));
if (docUser.exists()) {
// we'll need the user in order to get the user data (name, email, focus, etc);
const user = docUser.data() as CorporateUser;
const allStats = data.results.flatMap((r: any) => r.stats); const allStats = data.results.flatMap((r: any) => r.stats);
const uniqueExercises = [ const uniqueExercises = [...new Set(allStats.map((s: any) => s.exercise))];
...new Set(allStats.map((s: any) => s.exercise)),
];
const assigneesData = data.assignees const assigneesData = data.assignees
.map((assignee: string) => { .map((assignee: string) => {
const userStats = allStats.filter((s: any) => s.user === assignee); const userStats = allStats.filter((s: any) => s.user === assignee);
const dates = userStats.map((s: any) => moment(s.date));
return { return {
userId: assignee, userId: assignee,
user: users.find((u) => u.id === assignee), user: users.find((u) => u.id === assignee),
@@ -126,6 +98,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}, },
{ correct: 0, missing: 0, total: 0 } { correct: 0, missing: 0, total: 0 }
), ),
firstDate: moment.min(...dates),
lastDate: moment.max(...dates),
stats: userStats, stats: userStats,
}; };
}) })
@@ -135,18 +109,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const highestScore = Math.max(...results); const highestScore = Math.max(...results);
const lowestScore = Math.min(...results); const lowestScore = Math.min(...results);
const averageScore = results.reduce((a, b) => a + b, 0) / results.length; const averageScore = results.reduce((a, b) => a + b, 0) / results.length;
const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate));
const dates = assigneesData const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate));
.flatMap((r: any) => r.stats)
.map((r: any) => r.date)
.map((d: number) => moment(d));
const firstDate = moment.min(dates);
const lastDate = moment.max(dates);
const firstSectionData = [ const firstSectionData = [
{ {
label: "Corporate Name :", label: sectionName,
value: user.corporateInformation?.companyInformation?.name || "", value: userName,
}, },
{ {
label: "Report Download date :", label: "Report Download date :",
@@ -182,6 +151,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
worksheet.getCell(`B${index + 1}`).value = value; // Second column (values) 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") // Define the static part of the headers (before "Test Sections")
const staticHeaders = [ const staticHeaders = [
"Sr N", "Sr N",
@@ -190,10 +165,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
"Passport/ID", "Passport/ID",
"Email ID", "Email ID",
"Gender", "Gender",
...customTableHeaders,
]; ];
// Define additional headers after "Test Sections" // Define additional headers after "Test Sections"
const additionalHeaders = ["Time Spent", "Score"]; const additionalHeaders = ["Time Spent", "Date", "Score"];
// Calculate the dynamic columns based on the testSectionsArray // Calculate the dynamic columns based on the testSectionsArray
const testSectionHeaders = uniqueExercises.map( const testSectionHeaders = uniqueExercises.map(
@@ -212,30 +188,34 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
worksheet.addRow(tableColumnHeaders); worksheet.addRow(tableColumnHeaders);
// 1 headers rows // 1 headers rows
const startIndexTable = firstSectionData.length + 1; const startIndexTable = firstSectionData.length + customTableAndLine.length + 1;
// // Merge "Test Sections" over dynamic number of columns // // Merge "Test Sections" over dynamic number of columns
// const tableColumns = staticHeaders.length + numberOfTestSections; // const tableColumns = staticHeaders.length + numberOfTestSections;
// K10:M12 = 10,11,12,13
// horizontally group Test Sections
worksheet.mergeCells( worksheet.mergeCells(
1, startIndexTable,
staticHeaders.length + 1, staticHeaders.length + 1,
1, startIndexTable,
tableColumnHeadersFirstPart.length tableColumnHeadersFirstPart.length
); );
// Add the dynamic second and third header rows for test sections and sub-columns // Add the dynamic second and third header rows for test sections and sub-columns
worksheet.addRow([ worksheet.addRow([
...Array(staticHeaders.length).fill(""), ...Array(staticHeaders.length).fill(""),
...testSectionHeaders, ...testSectionHeaders,
"", "",
"", "",
"",
]); ]);
worksheet.addRow([ worksheet.addRow([
...Array(staticHeaders.length).fill(""), ...Array(staticHeaders.length).fill(""),
...uniqueExercises.map(() => "Grammar & Vocabulary"), ...uniqueExercises.map(() => "Grammar & Vocabulary"),
"", "",
"", "",
"",
]); ]);
worksheet.addRow([ worksheet.addRow([
...Array(staticHeaders.length).fill(""), ...Array(staticHeaders.length).fill(""),
@@ -244,15 +224,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
), ),
"", "",
"", "",
"",
]); ]);
// Merging static headers and "Test Sections" over dynamic columns // vertically group based on the part, exercise and type
worksheet.mergeCells(`A${startIndexTable}:A${startIndexTable + 3}`); // "Sr N" staticHeaders.forEach((header, index) => {
worksheet.mergeCells(`B${startIndexTable}:B${startIndexTable + 3}`); // "Candidate ID" worksheet.mergeCells(startIndexTable, index + 1, startIndexTable + 3, index + 1);
worksheet.mergeCells(`C${startIndexTable}:C${startIndexTable + 3}`); // "First and Last Name" });
worksheet.mergeCells(`D${startIndexTable}:D${startIndexTable + 3}`); // "Passport/ID"
worksheet.mergeCells(`E${startIndexTable}:E${startIndexTable + 3}`); // "Email ID"
worksheet.mergeCells(`F${startIndexTable}:F${startIndexTable + 3}`); // "Gender"
assigneesData.forEach((data, index) => { assigneesData.forEach((data, index) => {
worksheet.addRow([ worksheet.addRow([
@@ -262,18 +240,18 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
data.user.demographicInformation?.passportId, data.user.demographicInformation?.passportId,
data.user.email, data.user.email,
data.user.demographicInformation?.gender, data.user.demographicInformation?.gender,
...renderCustomTableData(data),
...uniqueExercises.map((exercise) => { ...uniqueExercises.map((exercise) => {
const score = data.stats.find( const score = data.stats.find(
(s: any) => s.exercise === exercise && s.user === data.userId (s: any) => s.exercise === exercise && s.user === data.userId
).score; ).score;
return `${score.correct}/${score.total}`; return `${score.correct}/${score.total}`;
}), }),
`${ `${Math.ceil(
Math.ceil(data.stats.reduce( data.stats.reduce((acc: number, curr: any) => acc + curr.timeSpent, 0) /
(acc: number, curr: any) => acc + curr.timeSpent, 60
0 )} minutes`,
) / 60) data.lastDate.format("DD/MM/YYYY HH:mm"),
} minutes`,
data.correct, data.correct,
]); ]);
}); });
@@ -304,12 +282,142 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
worksheet.addRow(["info@encoach.com"]); worksheet.addRow(["info@encoach.com"]);
// Convert workbook to Buffer (Node.js) or Blob (Browser) // Convert workbook to Buffer (Node.js) or Blob (Browser)
const buffer = await workbook.xlsx.writeBuffer(); 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 docSnap = await getDoc(doc(db, "assignments", id));
const data = docSnap.data() as AssignmentData;
if (!data) {
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 docsSnap = await getDocs(
query(collection(db, "users"), where(documentId(), "in", data.assignees))
);
const users = docsSnap.docs.map((d) => ({
...d.data(),
id: d.id,
})) as User[];
const docUser = await getDoc(doc(db, "users", data.assigner));
if (docUser.exists()) {
// we'll need the user in order to get the user data (name, email, focus, etc);
const user = docUser.data() as User;
// generate the file ref for storage // generate the file ref for storage
const fileName = `${Date.now().toString()}.xlsx`; const fileName = `${Date.now().toString()}.xlsx`;
const refName = `assignment_report/${fileName}`; const refName = `assignment_report/${fileName}`;
const fileRef = ref(storage, refName); const fileRef = ref(storage, refName);
const getExcelFn = () => {
switch (user.type) {
case "teacher":
case "corporate":
return corporateAssignment(user as CorporateUser, data, users);
case "mastercorporate":
return mastercorporateAssignment(user as MasterCorporateUser, data, users);
default:
throw new Error("Invalid user type");
}
};
const buffer = await getExcelFn();
// upload the pdf to storage // upload the pdf to storage
const snapshot = await uploadBytes(fileRef, buffer, { const snapshot = await uploadBytes(fileRef, buffer, {
contentType: contentType:

View File

@@ -14,7 +14,7 @@ import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import { Group } from "@/interfaces/user"; import { Group } from "@/interfaces/user";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { updateExpiryDateOnGroup } from "@/utils/groups.be"; import { updateExpiryDateOnGroup, getGroupsForUser } from "@/utils/groups.be";
const db = getFirestore(app); const db = getFirestore(app);
@@ -30,30 +30,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") await post(req, res); if (req.method === "POST") await post(req, res);
} }
const getGroupsForUser = async (admin: string, participant: string) => {
try {
const queryConstraints = [
...(admin ? [where("admin", "==", admin)] : []),
...(participant
? [where("participants", "array-contains", participant)]
: []),
];
const snapshot = await getDocs(
queryConstraints.length > 0
? query(collection(db, "groups"), ...queryConstraints)
: collection(db, "groups")
);
const groups = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Group[];
return groups;
} catch (e) {
console.error(e);
return [];
}
};
async function get(req: NextApiRequest, res: NextApiResponse) { async function get(req: NextApiRequest, res: NextApiResponse) {
const { admin, participant } = req.query as { const { admin, participant } = req.query as {
admin: string; admin: string;

View File

@@ -54,3 +54,54 @@ export const getAllAssignersByCorporate = async (corporateID: string): Promise<s
return teacherPromises.filter((x) => !!x).flat() as string[]; return teacherPromises.filter((x) => !!x).flat() as string[];
}; };
export const getGroupsForUser = async (admin: string, participant: string) => {
try {
const queryConstraints = [
...(admin ? [where("admin", "==", admin)] : []),
...(participant
? [where("participants", "array-contains", participant)]
: []),
];
const snapshot = await getDocs(
queryConstraints.length > 0
? query(collection(db, "groups"), ...queryConstraints)
: collection(db, "groups")
);
const groups = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Group[];
return groups;
} catch (e) {
console.error(e);
return [];
}
};
export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => {
try {
const queryConstraints = [
...(admin ? [where("admin", "!=", admin)] : []),
...(participants
? [where("participants", "array-contains-any", participants)]
: []),
where("name", "==", "Students"),
];
const snapshot = await getDocs(
queryConstraints.length > 0
? query(collection(db, "groups"), ...queryConstraints)
: collection(db, "groups")
);
const groups = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Group[];
return groups;
} catch (e) {
console.error(e);
return [];
}
};

View File

@@ -1,6 +1,14 @@
import { app } from "@/firebase"; import { app } from "@/firebase";
import {collection, doc, getDoc, getDocs, getFirestore} from "firebase/firestore"; import {
collection,
doc,
getDoc,
getDocs,
getFirestore,
query,
where,
} from "firebase/firestore";
import { User } from "@/interfaces/user"; import { User } from "@/interfaces/user";
const db = getFirestore(app); const db = getFirestore(app);
@@ -18,3 +26,18 @@ export async function getUser(id: string) {
return { ...userDoc.data(), id } as User; return { ...userDoc.data(), id } as User;
} }
export async function getSpecificUsers(ids: string[]) {
if (ids.length === 0) return [];
const snapshot = await getDocs(
query(collection(db, "users"), where("id", "in", ids))
);
const groups = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as User[];
return groups;
}