Merged in ENCOA-83_MasterStatistical (pull request #76)
ENCOA-83 MasterStatistical Approved-by: Tiago Ribeiro
This commit is contained in:
235
src/exams/pdf/level.test.report.tsx
Normal file
235
src/exams/pdf/level.test.report.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import React from "react";
|
||||
import { Document, Page, View, Text, Image } from "@react-pdf/renderer";
|
||||
import { ModuleScore } from "@/interfaces/module.scores";
|
||||
import { styles } from "./styles";
|
||||
import TestReportFooter from "./test.report.footer";
|
||||
|
||||
import { StyleSheet } from "@react-pdf/renderer";
|
||||
|
||||
const customStyles = StyleSheet.create({
|
||||
testDetails: {
|
||||
display: "flex",
|
||||
gap: 4,
|
||||
},
|
||||
testDetailsContainer: {
|
||||
display: "flex",
|
||||
gap: 16,
|
||||
},
|
||||
table: {
|
||||
width: "100%",
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: "row",
|
||||
},
|
||||
tableCol70: {
|
||||
width: "70%", // First column width (50%)
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderColor: "#000",
|
||||
// padding: 5,
|
||||
},
|
||||
tableCol25: {
|
||||
width: "16.67%", // Remaining four columns each get 1/6 of the total width (50% / 3 = 16.67%)
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderColor: "#000",
|
||||
padding: 5,
|
||||
},
|
||||
tableCol20: {
|
||||
width: "20%", // Width for each of the 5 sub-columns (50% / 5 = 20%)
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderColor: "#000",
|
||||
padding: 5,
|
||||
},
|
||||
tableCol10: {
|
||||
width: "10%", // Width for each of the 5 sub-columns (50% / 5 = 20%)
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderColor: "#000",
|
||||
padding: 5,
|
||||
},
|
||||
tableCellHeader: {
|
||||
fontSize: 12,
|
||||
textAlign: "center",
|
||||
},
|
||||
tableCellHeaderColor: {
|
||||
backgroundColor: "#d3d3d3",
|
||||
},
|
||||
tableCell: {
|
||||
fontSize: 10,
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
date: string;
|
||||
name: string;
|
||||
email: string;
|
||||
id: string;
|
||||
gender?: string;
|
||||
passportId: string;
|
||||
corporateName: string;
|
||||
downloadDate: string;
|
||||
userId: string;
|
||||
uniqueExercises: { name: string; result: string }[];
|
||||
timeSpent: string;
|
||||
score: string;
|
||||
}
|
||||
|
||||
const LevelTestReport = ({
|
||||
date,
|
||||
name,
|
||||
email,
|
||||
id,
|
||||
gender,
|
||||
passportId,
|
||||
corporateName,
|
||||
downloadDate,
|
||||
userId,
|
||||
uniqueExercises,
|
||||
timeSpent,
|
||||
score,
|
||||
}: Props) => {
|
||||
const defaultTextStyle = [styles.textFont, { fontSize: 8 }];
|
||||
return (
|
||||
<Document>
|
||||
<Page style={styles.body} orientation="landscape">
|
||||
<Text style={[styles.textFont, styles.textBold, { fontSize: 11 }]}>
|
||||
Corporate Name: {corporateName}
|
||||
</Text>
|
||||
<View style={styles.textMargin}>
|
||||
<Text style={defaultTextStyle}>
|
||||
Report Download date: {downloadDate}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.textFont, styles.textBold, { fontSize: 11 }]}>
|
||||
Test Information: {id}
|
||||
</Text>
|
||||
<View style={styles.textMargin}>
|
||||
<Text style={defaultTextStyle}>Date of Test: {date}</Text>
|
||||
<Text style={defaultTextStyle}>Candidates name: {name}</Text>
|
||||
<Text style={defaultTextStyle}>Email: {email}</Text>
|
||||
<Text style={defaultTextStyle}>National ID: {passportId}</Text>
|
||||
|
||||
<Text style={defaultTextStyle}>Gender: {gender}</Text>
|
||||
<Text style={defaultTextStyle}>Candidate ID: {userId}</Text>
|
||||
</View>
|
||||
|
||||
<View style={customStyles.table}>
|
||||
{/* Header Row */}
|
||||
<View style={customStyles.tableRow}>
|
||||
<View
|
||||
style={[
|
||||
customStyles.tableCol70,
|
||||
customStyles.tableCellHeaderColor,
|
||||
]}
|
||||
>
|
||||
<Text style={[customStyles.tableCellHeader, { padding: 5 }]}>
|
||||
Test sections
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
customStyles.tableCol20,
|
||||
customStyles.tableCellHeaderColor,
|
||||
]}
|
||||
>
|
||||
<Text style={customStyles.tableCellHeader}>Time spent</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
customStyles.tableCol10,
|
||||
customStyles.tableCellHeaderColor,
|
||||
]}
|
||||
>
|
||||
<Text style={customStyles.tableCellHeader}>Score</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={customStyles.tableRow}>
|
||||
<View style={customStyles.tableCol70}>
|
||||
<View style={customStyles.tableRow}>
|
||||
{uniqueExercises.map((exercise, index) => (
|
||||
<View
|
||||
style={[
|
||||
customStyles.tableCol20,
|
||||
index !== uniqueExercises.length - 1
|
||||
? { borderWidth: 0, borderRightWidth: 1 }
|
||||
: { borderWidth: 0 },
|
||||
]}
|
||||
key={index}
|
||||
>
|
||||
<Text style={customStyles.tableCell}>Part {index + 1}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View style={customStyles.tableCol20}>
|
||||
<Text style={customStyles.tableCell}></Text>
|
||||
</View>
|
||||
<View style={customStyles.tableCol10}>
|
||||
<Text style={customStyles.tableCell}></Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={customStyles.tableRow}>
|
||||
<View style={customStyles.tableCol70}>
|
||||
<View style={customStyles.tableRow}>
|
||||
{uniqueExercises.map((exercise, index) => (
|
||||
<View
|
||||
style={[
|
||||
customStyles.tableCol20,
|
||||
index !== uniqueExercises.length - 1
|
||||
? { borderWidth: 0, borderRightWidth: 1 }
|
||||
: { borderWidth: 0 },
|
||||
]}
|
||||
key={index}
|
||||
>
|
||||
<Text style={customStyles.tableCell}>{exercise.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View style={customStyles.tableCol20}>
|
||||
<Text style={customStyles.tableCell}></Text>
|
||||
</View>
|
||||
<View style={customStyles.tableCol10}>
|
||||
<Text style={customStyles.tableCell}></Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={customStyles.tableRow}>
|
||||
<View style={customStyles.tableCol70}>
|
||||
<View style={customStyles.tableRow}>
|
||||
{uniqueExercises.map((exercise, index) => (
|
||||
<View
|
||||
style={[
|
||||
customStyles.tableCol20,
|
||||
index !== uniqueExercises.length - 1
|
||||
? { borderWidth: 0, borderRightWidth: 1 }
|
||||
: { borderWidth: 0 },
|
||||
]}
|
||||
key={index}
|
||||
>
|
||||
<Text style={customStyles.tableCell}>
|
||||
{exercise.result}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View style={customStyles.tableCol20}>
|
||||
<Text style={customStyles.tableCell}>{timeSpent}</Text>
|
||||
</View>
|
||||
<View style={customStyles.tableCol10}>
|
||||
<Text style={customStyles.tableCell}>{score}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<TestReportFooter userId={userId} />
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export default LevelTestReport;
|
||||
@@ -2,7 +2,14 @@ import {Module} from ".";
|
||||
import { InstructorGender, ShuffleMap } from "./exam";
|
||||
import { PermissionType } from "./permissions";
|
||||
|
||||
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser;
|
||||
export type User =
|
||||
| StudentUser
|
||||
| TeacherUser
|
||||
| CorporateUser
|
||||
| AgentUser
|
||||
| AdminUser
|
||||
| DeveloperUser
|
||||
| MasterCorporateUser;
|
||||
export type UserStatus = "active" | "disabled" | "paymentDue";
|
||||
|
||||
export interface BasicUser {
|
||||
@@ -106,8 +113,15 @@ export interface DemographicCorporateInformation {
|
||||
}
|
||||
|
||||
export type Gender = "male" | "female" | "other";
|
||||
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
|
||||
export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
|
||||
export type EmploymentStatus =
|
||||
| "employed"
|
||||
| "student"
|
||||
| "self-employed"
|
||||
| "unemployed"
|
||||
| "retired"
|
||||
| "other";
|
||||
export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] =
|
||||
[
|
||||
{ status: "student", label: "Student" },
|
||||
{ status: "employed", label: "Employed" },
|
||||
{ status: "unemployed", label: "Unemployed" },
|
||||
@@ -136,6 +150,10 @@ export interface Stat {
|
||||
};
|
||||
isDisabled?: boolean;
|
||||
shuffleMaps?: ShuffleMap[];
|
||||
pdf?: {
|
||||
path: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
@@ -158,5 +176,20 @@ export interface Code {
|
||||
passport_id?: string;
|
||||
}
|
||||
|
||||
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate";
|
||||
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"];
|
||||
export type Type =
|
||||
| "student"
|
||||
| "teacher"
|
||||
| "corporate"
|
||||
| "admin"
|
||||
| "developer"
|
||||
| "agent"
|
||||
| "mastercorporate";
|
||||
export const userTypes: Type[] = [
|
||||
"student",
|
||||
"teacher",
|
||||
"corporate",
|
||||
"admin",
|
||||
"developer",
|
||||
"agent",
|
||||
"mastercorporate",
|
||||
];
|
||||
|
||||
@@ -1,22 +1,40 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { app, storage } from "@/firebase";
|
||||
import {getFirestore, doc, getDoc, updateDoc, getDocs, query, collection, where} from "firebase/firestore";
|
||||
import {
|
||||
getFirestore,
|
||||
doc,
|
||||
getDoc,
|
||||
updateDoc,
|
||||
getDocs,
|
||||
query,
|
||||
collection,
|
||||
where,
|
||||
} from "firebase/firestore";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import ReactPDF from "@react-pdf/renderer";
|
||||
import TestReport from "@/exams/pdf/test.report";
|
||||
import LevelTestReport from "@/exams/pdf/level.test.report";
|
||||
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
|
||||
import {DemographicInformation, User} from "@/interfaces/user";
|
||||
import {
|
||||
DemographicInformation,
|
||||
Stat,
|
||||
StudentUser,
|
||||
User,
|
||||
} from "@/interfaces/user";
|
||||
import { Module } from "@/interfaces";
|
||||
import { ModuleScore } from "@/interfaces/module.scores";
|
||||
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
|
||||
import {LevelExamDetails} from "@/exams/pdf/details/level.exam";
|
||||
import { calculateBandScore } from "@/utils/score";
|
||||
import axios from "axios";
|
||||
import { moduleLabels } from "@/utils/moduleUtils";
|
||||
import {generateQRCode, getRadialProgressPNG, streamToBuffer} from "@/utils/pdf";
|
||||
import {
|
||||
generateQRCode,
|
||||
getRadialProgressPNG,
|
||||
streamToBuffer,
|
||||
} from "@/utils/pdf";
|
||||
import moment from "moment-timezone";
|
||||
|
||||
import { getCorporateNameForStudent } from "@/utils/groups.be";
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
@@ -90,14 +108,16 @@ const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return backendRequest.data?.sections;
|
||||
};
|
||||
|
||||
// perform the request with several retries if needed
|
||||
const handleSkillsFeedbackRequest = async (sections: SkillsFeedbackRequest[]): Promise<SkillsFeedbackResponse[] | null> => {
|
||||
const handleSkillsFeedbackRequest = async (
|
||||
sections: SkillsFeedbackRequest[]
|
||||
): Promise<SkillsFeedbackResponse[] | null> => {
|
||||
let i = 0;
|
||||
try {
|
||||
const data = await getSkillsFeedback(sections);
|
||||
@@ -112,59 +132,24 @@ const handleSkillsFeedbackRequest = async (sections: SkillsFeedbackRequest[]): P
|
||||
}
|
||||
};
|
||||
|
||||
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};
|
||||
// fetch stats entries for this particular user with the requested exam session
|
||||
const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id)));
|
||||
|
||||
if (docsSnap.empty) {
|
||||
res.status(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = docsSnap.docs.map((d) => d.data());
|
||||
// verify if the stats already have a pdf generated
|
||||
const hasPDF = stats.find((s) => s.pdf?.path && s.pdf?.version === process.env.PDF_VERSION);
|
||||
// find the user that generated the stats
|
||||
const statIndex = stats.findIndex((s) => s.user);
|
||||
|
||||
if(statIndex === -1) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
const userId = stats[statIndex].user;
|
||||
|
||||
|
||||
if (hasPDF) {
|
||||
// if it does, return the pdf url
|
||||
const fileRef = ref(storage, hasPDF.pdf.path);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// generate the pdf report
|
||||
const docUser = await getDoc(doc(db, "users", userId));
|
||||
|
||||
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;
|
||||
|
||||
async function getDefaultPDFStream(
|
||||
stats: Stat[],
|
||||
user: User,
|
||||
qrcodeUrl: string
|
||||
) {
|
||||
const [stat] = stats;
|
||||
// generate the QR code for the report
|
||||
const qrcode = await generateQRCode((req.headers.origin || "") + req.url);
|
||||
const qrcode = await generateQRCode(qrcodeUrl);
|
||||
|
||||
if (!qrcode) {
|
||||
res.status(500).json({ok: false});
|
||||
return;
|
||||
throw new Error("Failed to generate QR code");
|
||||
}
|
||||
|
||||
// stats may contain multiple exams of the same type so we need to aggregate them
|
||||
const results = (
|
||||
stats.reduce((accm: ModuleScore[], {module, score}) => {
|
||||
const results = stats
|
||||
.reduce((accm: ModuleScore[], stat: Stat) => {
|
||||
const { module, score } = stat;
|
||||
|
||||
const fixedModuleStr = module[0].toUpperCase() + module.substring(1);
|
||||
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
|
||||
return accm.map((e: ModuleScore) => {
|
||||
@@ -180,20 +165,24 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
...accm,
|
||||
{
|
||||
const value = {
|
||||
module: fixedModuleStr,
|
||||
score: score.correct,
|
||||
total: score.total,
|
||||
code: module,
|
||||
},
|
||||
];
|
||||
}, []) as ModuleScore[]
|
||||
).map((moduleScore) => {
|
||||
} as ModuleScore;
|
||||
|
||||
return [...accm, value];
|
||||
}, [])
|
||||
.map((moduleScore: ModuleScore) => {
|
||||
const { score, total } = moduleScore;
|
||||
// with all the scores aggreated we can calculate the band score for each module
|
||||
const bandScore = calculateBandScore(score, total, moduleScore.code as Module, user.focus);
|
||||
const bandScore = calculateBandScore(
|
||||
score,
|
||||
total,
|
||||
moduleScore.code as Module,
|
||||
user.focus
|
||||
);
|
||||
|
||||
return {
|
||||
...moduleScore,
|
||||
@@ -209,17 +198,18 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
code,
|
||||
name: moduleLabels[code],
|
||||
grade: bandScore,
|
||||
})),
|
||||
}))
|
||||
)) as SkillsFeedbackResponse[];
|
||||
|
||||
if (!skillsFeedback) {
|
||||
res.status(500).json({ok: false});
|
||||
return;
|
||||
throw new Error("Failed to get skills feedback");
|
||||
}
|
||||
|
||||
// assign the feedback to the results
|
||||
const finalResults = results.map((result) => {
|
||||
const feedback = skillsFeedback.find((f: SkillsFeedbackResponse) => f.code === result.code);
|
||||
const feedback = skillsFeedback.find(
|
||||
(f: SkillsFeedbackResponse) => f.code === result.code
|
||||
);
|
||||
|
||||
if (feedback) {
|
||||
return {
|
||||
@@ -238,7 +228,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
const overallTotal = results.reduce((accm, { total }) => accm + total, 0);
|
||||
const overallResult = overallScore / overallTotal;
|
||||
|
||||
const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal);
|
||||
const overallPNG = getRadialProgressPNG(
|
||||
"laranja",
|
||||
overallScore,
|
||||
overallTotal
|
||||
);
|
||||
|
||||
// generate the overall detail report
|
||||
const overallDetail = {
|
||||
@@ -249,30 +243,15 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
} as ModuleScore;
|
||||
const testDetails = [overallDetail, ...finalResults];
|
||||
|
||||
const [stat] = stats;
|
||||
|
||||
// generate the performance summary based on the overall result
|
||||
const performanceSummary = getPerformanceSummary(stat.module, overallResult);
|
||||
|
||||
// level exams have a different report structure than the skill exams
|
||||
const getCustomData = () => {
|
||||
if (stat.module === "level") {
|
||||
return {
|
||||
title: "ENGLISH LEVEL TEST RESULT REPORT ",
|
||||
details: <LevelExamDetails detail={overallDetail} title="Level as per CEFR Levels" />,
|
||||
};
|
||||
}
|
||||
const title = "ENGLISH SKILLS TEST RESULT REPORT";
|
||||
const details = <SkillExamDetails testDetails={testDetails} />;
|
||||
|
||||
return {
|
||||
title: "ENGLISH SKILLS TEST RESULT REPORT",
|
||||
details: <SkillExamDetails testDetails={testDetails} />,
|
||||
};
|
||||
};
|
||||
|
||||
const {title, details} = getCustomData();
|
||||
|
||||
const demographicInformation = user.demographicInformation as DemographicInformation;
|
||||
const pdfStream = await ReactPDF.renderToStream(
|
||||
const demographicInformation =
|
||||
user.demographicInformation as DemographicInformation;
|
||||
return ReactPDF.renderToStream(
|
||||
<TestReport
|
||||
title={title}
|
||||
date={moment(stat.date)
|
||||
@@ -280,7 +259,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
.format("ll HH:mm:ss")}
|
||||
name={user.name}
|
||||
email={user.email}
|
||||
id={userId}
|
||||
id={user.id}
|
||||
gender={demographicInformation?.gender}
|
||||
summary={performanceSummary}
|
||||
testDetails={testDetails}
|
||||
@@ -290,9 +269,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
summaryPNG={overallPNG}
|
||||
summaryScore={`${Math.floor(overallResult * 100)}%`}
|
||||
passportId={demographicInformation?.passport_id || ""}
|
||||
/>,
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getPdfUrl(pdfStream: any, docsSnap: any) {
|
||||
// generate the file ref for storage
|
||||
const fileName = `${Date.now().toString()}.pdf`;
|
||||
const refName = `exam_report/${fileName}`;
|
||||
@@ -305,7 +286,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
});
|
||||
|
||||
// update the stats entries with the pdf url to prevent duplication
|
||||
docsSnap.docs.forEach(async (doc) => {
|
||||
docsSnap.docs.forEach(async (doc: any) => {
|
||||
await updateDoc(doc.ref, {
|
||||
pdf: {
|
||||
path: refName,
|
||||
@@ -313,14 +294,112 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
});
|
||||
});
|
||||
return getDownloadURL(fileRef);
|
||||
}
|
||||
|
||||
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 };
|
||||
// fetch stats entries for this particular user with the requested exam session
|
||||
const docsSnap = await getDocs(
|
||||
query(collection(db, "stats"), where("session", "==", id))
|
||||
);
|
||||
|
||||
if (docsSnap.empty) {
|
||||
res.status(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = docsSnap.docs.map((d) => d.data()) as Stat[];
|
||||
// verify if the stats already have a pdf generated
|
||||
const hasPDF = stats.find(
|
||||
(s) => s.pdf?.path && s.pdf?.version === process.env.PDF_VERSION
|
||||
);
|
||||
// find the user that generated the stats
|
||||
const statIndex = stats.findIndex((s) => s.user);
|
||||
|
||||
if (statIndex === -1) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
const userId = stats[statIndex].user;
|
||||
|
||||
if (hasPDF) {
|
||||
// if it does, return the pdf url
|
||||
const fileRef = ref(storage, hasPDF.pdf!.path);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// generate the pdf report
|
||||
const docUser = await getDoc(doc(db, "users", userId));
|
||||
|
||||
if (docUser.exists()) {
|
||||
// we'll need the user in order to get the user data (name, email, focus, etc);
|
||||
|
||||
const [stat] = stats;
|
||||
|
||||
if (stat.module === "level") {
|
||||
const user = docUser.data() as StudentUser;
|
||||
|
||||
const uniqueExercises = stats.map((s) => ({
|
||||
name: "Gramar & Vocabulary",
|
||||
result: `${s.score.correct}/${s.score.total}`,
|
||||
}));
|
||||
const dates = stats.map((s) => moment(s.date));
|
||||
const timeSpent = `${
|
||||
stats.reduce((accm, s: Stat) => accm + (s.timeSpent || 0), 0) / 60
|
||||
} minutes`;
|
||||
const score = stats.reduce((accm, s) => accm + s.score.correct, 0);
|
||||
const corporateName = await getCorporateNameForStudent(userId);
|
||||
const pdfStream = await ReactPDF.renderToStream(
|
||||
<LevelTestReport
|
||||
date={moment.max(dates).format("DD/MM/YYYY")}
|
||||
name={user.name}
|
||||
email={user.email}
|
||||
id={stat.exam}
|
||||
gender={user.demographicInformation?.gender || ""}
|
||||
passportId={user.demographicInformation?.passport_id || ""}
|
||||
corporateName={corporateName}
|
||||
downloadDate={moment().format("DD/MM/YYYY")}
|
||||
userId={userId}
|
||||
uniqueExercises={uniqueExercises}
|
||||
timeSpent={timeSpent}
|
||||
score={score.toString()}
|
||||
/>
|
||||
);
|
||||
|
||||
const url = await getPdfUrl(pdfStream, docsSnap);
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
const user = docUser.data() as User;
|
||||
|
||||
try {
|
||||
const pdfStream = await getDefaultPDFStream(
|
||||
stats,
|
||||
user,
|
||||
`${req.headers.origin || ""}${req.url}`
|
||||
);
|
||||
|
||||
const url = await getPdfUrl(pdfStream, docsSnap);
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
@@ -332,7 +411,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { id } = req.query as { id: string };
|
||||
const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id)));
|
||||
const docsSnap = await getDocs(
|
||||
query(collection(db, "stats"), where("session", "==", id))
|
||||
);
|
||||
|
||||
if (docsSnap.empty) {
|
||||
res.status(404).end();
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { app } from "@/firebase";
|
||||
import {CorporateUser, Group, StudentUser, TeacherUser} from "@/interfaces/user";
|
||||
import {collection, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore";
|
||||
import {
|
||||
CorporateUser,
|
||||
Group,
|
||||
StudentUser,
|
||||
TeacherUser,
|
||||
} from "@/interfaces/user";
|
||||
import {
|
||||
collection,
|
||||
doc,
|
||||
getDoc,
|
||||
getDocs,
|
||||
getFirestore,
|
||||
query,
|
||||
setDoc,
|
||||
where,
|
||||
} from "firebase/firestore";
|
||||
import moment from "moment";
|
||||
import { getUser } from "./users.be";
|
||||
|
||||
import { getSpecificUsers } from "./users.be";
|
||||
const db = getFirestore(app);
|
||||
|
||||
export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => {
|
||||
export const updateExpiryDateOnGroup = async (
|
||||
participantID: string,
|
||||
corporateID: string
|
||||
) => {
|
||||
const corporateRef = await getDoc(doc(db, "users", corporateID));
|
||||
const participantRef = await getDoc(doc(db, "users", participantID));
|
||||
|
||||
@@ -16,19 +33,36 @@ export const updateExpiryDateOnGroup = async (participantID: string, corporateID
|
||||
...corporateRef.data(),
|
||||
id: corporateRef.id,
|
||||
} as CorporateUser;
|
||||
const participant = {...participantRef.data(), id: participantRef.id} as StudentUser | TeacherUser;
|
||||
const participant = { ...participantRef.data(), id: participantRef.id } as
|
||||
| StudentUser
|
||||
| TeacherUser;
|
||||
|
||||
if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return;
|
||||
if (
|
||||
corporate.type !== "corporate" ||
|
||||
(participant.type !== "student" && participant.type !== "teacher")
|
||||
)
|
||||
return;
|
||||
|
||||
if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate) {
|
||||
return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: null}, {merge: true});
|
||||
if (
|
||||
!corporate.subscriptionExpirationDate ||
|
||||
!participant.subscriptionExpirationDate
|
||||
) {
|
||||
return await setDoc(
|
||||
doc(db, "users", participant.id),
|
||||
{ subscriptionExpirationDate: null },
|
||||
{ merge: true }
|
||||
);
|
||||
}
|
||||
|
||||
const corporateDate = moment(corporate.subscriptionExpirationDate);
|
||||
const participantDate = moment(participant.subscriptionExpirationDate);
|
||||
|
||||
if (corporateDate.isAfter(participantDate))
|
||||
return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: corporateDate.toISOString()}, {merge: true});
|
||||
return await setDoc(
|
||||
doc(db, "users", participant.id),
|
||||
{ subscriptionExpirationDate: corporateDate.toISOString() },
|
||||
{ merge: true }
|
||||
);
|
||||
|
||||
return;
|
||||
};
|
||||
@@ -39,17 +73,29 @@ export const getGroups = async () => {
|
||||
};
|
||||
|
||||
export const getUserGroups = async (id: string): Promise<Group[]> => {
|
||||
const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id)));
|
||||
const groupDocs = await getDocs(
|
||||
query(collection(db, "groups"), where("admin", "==", id))
|
||||
);
|
||||
return groupDocs.docs.map((x) => ({ ...x.data(), id })) as Group[];
|
||||
};
|
||||
|
||||
export const getAllAssignersByCorporate = async (corporateID: string): Promise<string[]> => {
|
||||
export const getAllAssignersByCorporate = async (
|
||||
corporateID: string
|
||||
): Promise<string[]> => {
|
||||
const groups = await getUserGroups(corporateID);
|
||||
const groupUsers = (await Promise.all(groups.map(async (g) => await Promise.all(g.participants.map(getUser))))).flat();
|
||||
const groupUsers = (
|
||||
await Promise.all(
|
||||
groups.map(async (g) => await Promise.all(g.participants.map(getUser)))
|
||||
)
|
||||
).flat();
|
||||
const teacherPromises = await Promise.all(
|
||||
groupUsers.map(async (u) =>
|
||||
u.type === "teacher" ? u.id : u.type === "corporate" ? [...(await getAllAssignersByCorporate(u.id)), u.id] : undefined,
|
||||
),
|
||||
u.type === "teacher"
|
||||
? u.id
|
||||
: u.type === "corporate"
|
||||
? [...(await getAllAssignersByCorporate(u.id)), u.id]
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
|
||||
return teacherPromises.filter((x) => !!x).flat() as string[];
|
||||
@@ -80,7 +126,10 @@ export const getGroupsForUser = async (admin: string, participant: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => {
|
||||
export const getStudentGroupsForUsersWithoutAdmin = async (
|
||||
admin: string,
|
||||
participants: string[]
|
||||
) => {
|
||||
try {
|
||||
const queryConstraints = [
|
||||
...(admin ? [where("admin", "!=", admin)] : []),
|
||||
@@ -105,3 +154,20 @@ export const getGroupsForUser = async (admin: string, participant: string) => {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getCorporateNameForStudent = async (studentID: string) => {
|
||||
const groups = await getStudentGroupsForUsersWithoutAdmin("", [studentID]);
|
||||
if (groups.length === 0) return '';
|
||||
|
||||
const adminUserIds = [...new Set(groups.map((g) => g.admin))];
|
||||
const adminUsersData = await getSpecificUsers(adminUserIds);
|
||||
|
||||
if(adminUsersData.length === 0) return '';
|
||||
const admins = adminUsersData.filter((x) => x.type === 'corporate');
|
||||
|
||||
if(admins.length > 0) {
|
||||
return (admins[0] as CorporateUser).corporateInformation.companyInformation.name;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user