Merged in bug-fixing-16-jan-24 (pull request #26)

Report PDF improvements / bugs

Approved-by: Tiago Ribeiro
This commit is contained in:
João Ramos
2024-01-16 23:20:00 +00:00
committed by Tiago Ribeiro
6 changed files with 368 additions and 353 deletions

View File

@@ -33,6 +33,7 @@ interface Props {
summaryPNG: string;
summaryScore: string;
groupScoreSummary: any[];
passportId: string;
}
const customStyles = StyleSheet.create({
@@ -81,6 +82,7 @@ const GroupTestReport = ({
summaryPNG,
summaryScore,
groupScoreSummary,
passportId,
}: Props) => {
const defaultTextStyle = [styles.textFont, { fontSize: 8 }];
return (
@@ -114,6 +116,7 @@ const GroupTestReport = ({
<Text style={defaultTextStyle}>ID: {id}</Text>
<Text style={defaultTextStyle}>Email: {email}</Text>
<Text style={defaultTextStyle}>Gender: {gender}</Text>
<Text style={defaultTextStyle}>Passport ID: {passportId}</Text>
<Text style={defaultTextStyle}>
Total Number of Students: {numberOfStudents}
</Text>
@@ -203,7 +206,7 @@ const GroupTestReport = ({
percentage={percent}
/>
</View>
<Text style={[customStyles.tableCell, { maxWidth: "24px" }]}>
<Text style={[customStyles.tableCell, { maxWidth: "48px" }]}>
{percent}%
</Text>
<Text style={customStyles.tableCell}>{description}</Text>

View File

@@ -28,6 +28,9 @@ export const styles = StyleSheet.create({
fontFamily: "Helvetica-Bold",
fontWeight: "bold",
},
textNormal: {
fontWeight: "normal",
},
textColor: {
color: "#4e4969",
},

View File

@@ -18,18 +18,18 @@ const TestReportFooter = () => (
>
<View style={[styles.spacedRow, styles.textMargin]}>
<View>
<Text>Validity</Text>
<Text style={styles.textBold}>Validity</Text>
<Text>
This report remains valid for a duration of three months from the test
date.
</Text>
</View>
<View>
<Text>Confidential circulated for concern people</Text>
<Text style={styles.textBold}>Confidential <Text style={[styles.textFont, styles.textNormal]}>circulated for concern people</Text></Text>
</View>
</View>
<View style={{ paddingTop: 10 }}>
<Text>Declaration</Text>
<Text style={styles.textBold}>Declaration</Text>
<Text style={{ paddingTop: 5 }}>
We hereby declare that exam results on our platform, assessed by AI, are
not the sole determinants of candidates&apos; English proficiency

View File

@@ -27,6 +27,7 @@ interface Props {
title: string;
summaryPNG: string;
summaryScore: string;
passportId: string;
}
const TestReport = ({
@@ -43,6 +44,7 @@ const TestReport = ({
renderDetails,
summaryPNG,
summaryScore,
passportId,
}: Props) => {
const defaultTextStyle = [styles.textFont, { fontSize: 8 }];
const defaultSkillsTextStyle = [styles.textFont, { fontSize: 8 }];
@@ -83,6 +85,7 @@ const TestReport = ({
<Text style={defaultTextStyle}>ID: {id}</Text>
<Text style={defaultTextStyle}>Email: {email}</Text>
<Text style={defaultTextStyle}>Gender: {gender}</Text>
<Text style={defaultTextStyle}>Passport ID: {passportId}</Text>
</View>
<View style={{ height: "120px" }}>
<Text

View File

@@ -1,430 +1,434 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { app, storage } from "@/firebase";
import {
getFirestore,
doc,
getDoc,
updateDoc,
getDocs,
query,
collection,
where,
documentId,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import type {NextApiRequest, NextApiResponse} from "next";
import {app, storage} from "@/firebase";
import {getFirestore, doc, getDoc, updateDoc, getDocs, query, collection, where, documentId} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
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 { Stat } from "@/interfaces/user";
import { User } from "@/interfaces/user";
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 {ref, uploadBytes, getDownloadURL} from "firebase/storage";
import {Stat, CorporateUser} from "@/interfaces/user";
import {User, DemographicInformation} from "@/interfaces/user";
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";
interface GroupScoreSummaryHelper {
score: [number, number];
label: string;
sessions: string[];
score: [number, number];
label: string;
sessions: string[];
}
const db = getFirestore(app);
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 post(req, res);
if (req.method === "GET") return get(req, res);
if (req.method === "POST") return post(req, res);
}
const getExamSummary = (score: number) => {
if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam collectively demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading for this group of students. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. The group is encouraged to continue challenging themselves with advanced material in writing, speaking, listening, and reading to further refine their impressive command of the English language.";
}
if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam collectively demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading for this group of students. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. The group is encouraged to continue challenging themselves with advanced material in writing, speaking, listening, and reading to further refine their impressive command of the English language.";
}
if (score > 0.6) {
return "The group's average scores between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflect a commendable level of proficiency. There's evidence of a solid grasp of key concepts collectively, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for the entire group to further their mastery.";
}
if (score > 0.6) {
return "The group's average scores between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflect a commendable level of proficiency. There's evidence of a solid grasp of key concepts collectively, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for the entire group to further their mastery.";
}
if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading indicates a moderate level of understanding for the group. While there's a commendable grasp of key concepts collectively, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. The group is encouraged to work together with consistent effort and targeted focus on weaker areas.";
}
if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading indicates a moderate level of understanding for the group. While there's a commendable grasp of key concepts collectively, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. The group is encouraged to work together with consistent effort and targeted focus on weaker areas.";
}
if (score > 0.2) {
return "The group's average scores between 21% and 40% on the English exam, encompassing writing, speaking, listening, and reading, show some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills for the entire group. Strengthening writing, speaking, listening, and reading abilities collectively through consistent effort and focused group study will contribute to overall proficiency.";
}
if (score > 0.2) {
return "The group's average scores between 21% and 40% on the English exam, encompassing writing, speaking, listening, and reading, show some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills for the entire group. Strengthening writing, speaking, listening, and reading abilities collectively through consistent effort and focused group study will contribute to overall proficiency.";
}
return "The average performance of this group of students in English, covering writing, speaking, listening, and reading, indicates a collective need for improvement, with scores falling between 0% and 20%. Across all language domains, there's a significant gap in understanding key concepts. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial for the entire group. Implementing a shared, consistent study routine and seeking group support in each area can contribute to substantial progress.";
return "The average performance of this group of students in English, covering writing, speaking, listening, and reading, indicates a collective need for improvement, with scores falling between 0% and 20%. Across all language domains, there's a significant gap in understanding key concepts. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial for the entire group. Implementing a shared, consistent study routine and seeking group support in each area can contribute to substantial progress.";
};
const getLevelSummary = (score: number) => {
if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam collectively demonstrates an outstanding level of proficiency for this group of students, showcasing a mastery of key concepts related to vocabulary and grammar. There's evidence of not only a high level of skill but also a dedication to excellence. The group is encouraged to continue challenging themselves with advanced material in vocabulary and grammar to further refine their impressive command of the English language.";
}
if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam collectively demonstrates an outstanding level of proficiency for this group of students, showcasing a mastery of key concepts related to vocabulary and grammar. There's evidence of not only a high level of skill but also a dedication to excellence. The group is encouraged to continue challenging themselves with advanced material in vocabulary and grammar to further refine their impressive command of the English language.";
}
if (score > 0.6) {
return "The group's average scores between 61% and 80% on the English exam reflect a commendable level of proficiency with solid grasp of key concepts related to vocabulary and grammar. Room for refinement and deeper exploration in these language skills remains, presenting an opportunity for the entire group to further their mastery. Consistent effort in honing nuanced aspects of vocabulary and grammar will contribute to even greater proficiency.";
}
if (score > 0.6) {
return "The group's average scores between 61% and 80% on the English exam reflect a commendable level of proficiency with solid grasp of key concepts related to vocabulary and grammar. Room for refinement and deeper exploration in these language skills remains, presenting an opportunity for the entire group to further their mastery. Consistent effort in honing nuanced aspects of vocabulary and grammar will contribute to even greater proficiency.";
}
if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam indicates a moderate level of understanding for the group, with commendable grasp of key concepts related to vocabulary and grammar. Refining these fundamental language skills can lead to notable improvement. The group is encouraged to work together with consistent effort and targeted focus on enhancing their vocabulary and grammar.";
}
if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam indicates a moderate level of understanding for the group, with commendable grasp of key concepts related to vocabulary and grammar. Refining these fundamental language skills can lead to notable improvement. The group is encouraged to work together with consistent effort and targeted focus on enhancing their vocabulary and grammar.";
}
if (score > 0.2) {
return "The group's average scores between 21% and 40% on the English exam show some understanding of key concepts in vocabulary and grammar. However, there's room for improvement in these fundamental language skills for the entire group. Strengthening vocabulary and grammar collectively through consistent effort and focused group study will contribute to overall proficiency.";
}
if (score > 0.2) {
return "The group's average scores between 21% and 40% on the English exam show some understanding of key concepts in vocabulary and grammar. However, there's room for improvement in these fundamental language skills for the entire group. Strengthening vocabulary and grammar collectively through consistent effort and focused group study will contribute to overall proficiency.";
}
return "The average performance of this group of students in English suggests a collective need for improvement, with scores falling between 0% and 20%. There's a significant gap in understanding key concepts related to vocabulary and grammar. Strengthening fundamental language skills, such as vocabulary and grammar, is crucial for the entire group. Implementing a shared, consistent study routine and seeking group support in these areas can contribute to substantial progress.";
return "The average performance of this group of students in English suggests a collective need for improvement, with scores falling between 0% and 20%. There's a significant gap in understanding key concepts related to vocabulary and grammar. Strengthening fundamental language skills, such as vocabulary and grammar, is crucial for the entire group. Implementing a shared, consistent study routine and seeking group support in these areas can contribute to substantial progress.";
};
const getPerformanceSummary = (module: Module, score: number) => {
if (module === "level") return getLevelSummary(score);
return getExamSummary(score);
if (module === "level") return getLevelSummary(score);
return getExamSummary(score);
};
const getScoreAndTotal = (stats: Stat[]) => {
return stats.reduce(
(acc, { score }) => {
return {
...acc,
correct: acc.correct + score.correct,
total: acc.total + score.total,
};
},
{ correct: 0, total: 0 }
);
return stats.reduce(
(acc, {score}) => {
return {
...acc,
correct: acc.correct + score.correct,
total: acc.total + score.total,
};
},
{correct: 0, total: 0},
);
};
const getLevelScoreForUserExams = (bandScore: number) => {
const [level] = getLevelScore(bandScore);
return level;
const [level] = getLevelScore(bandScore);
return level;
};
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 };
// 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 {
assigner: string;
assignees: string[];
results: any;
exams: { module: Module }[];
startDate: string;
pdf?: string;
};
if (!data) {
res.status(400).end();
return;
}
const docSnap = await getDoc(doc(db, "assignments", id));
const data = docSnap.data() as {
assigner: string;
assignees: string[];
results: any;
exams: {module: Module}[];
startDate: string;
pdf?: string;
};
if (!data) {
res.status(400).end();
return;
}
if (data.assigner !== req.session.user.id) {
res.status(401).json({ ok: false });
return;
}
if (data.pdf) {
// if it does, return the pdf url
const fileRef = ref(storage, data.pdf);
const url = await getDownloadURL(fileRef);
if (data.assigner !== req.session.user.id) {
res.status(401).json({ok: false});
return;
}
if (data.pdf) {
// if it does, return the pdf url
const fileRef = ref(storage, data.pdf);
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
res.status(200).end(url);
return;
}
try {
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 User;
try {
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 User;
// generate the QR code for the report
const qrcode = await generateQRCode(
(req.headers.origin || "") + req.url
);
// generate the QR code for the report
const qrcode = await generateQRCode((req.headers.origin || "") + req.url);
if (!qrcode) {
res.status(500).json({ ok: false });
return;
}
if (!qrcode) {
res.status(500).json({ok: false});
return;
}
const flattenResults = data.results.reduce(
(accm: Stat[], entry: any) => {
const stats = entry.stats as Stat[];
return [...accm, ...stats];
},
[]
) as Stat[];
const flattenResults = data.results.reduce((accm: Stat[], entry: any) => {
const stats = entry.stats as Stat[];
return [...accm, ...stats];
}, []) as Stat[];
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 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 flattenResultsWithGrade = flattenResults.map((e) => {
const focus = users.find((u) => u.id === e.user)?.focus || "academic";
const bandScore = calculateBandScore(
e.score.correct,
e.score.total,
e.module,
focus
);
const flattenResultsWithGrade = flattenResults.map((e) => {
const focus = users.find((u) => u.id === e.user)?.focus || "academic";
const bandScore = calculateBandScore(e.score.correct, e.score.total, e.module, focus);
return { ...e, bandScore };
});
return {...e, bandScore};
});
const moduleResults = data.exams.map(({ module }) => {
const moduleResults = flattenResultsWithGrade.filter(
(e) => e.module === module
);
const moduleResults = data.exams.map(({module}) => {
const moduleResults = flattenResultsWithGrade.filter((e) => e.module === module);
const baseBandScore =
moduleResults.reduce((accm, curr) => accm + curr.bandScore, 0) /
moduleResults.length;
const bandScore = isNaN(baseBandScore) ? 0 : baseBandScore;
const { correct, total } = getScoreAndTotal(moduleResults);
const png = getRadialProgressPNG("azul", correct, total);
const baseBandScore = moduleResults.reduce((accm, curr) => accm + curr.bandScore, 0) / moduleResults.length;
const bandScore = isNaN(baseBandScore) ? 0 : baseBandScore;
const {correct, total} = getScoreAndTotal(moduleResults);
const png = getRadialProgressPNG("azul", correct, total);
return {
bandScore,
png,
module: module[0].toUpperCase() + module.substring(1),
score: bandScore,
total,
code: module,
};
}) as ModuleScore[];
return {
bandScore,
png,
module: module[0].toUpperCase() + module.substring(1),
score: bandScore,
total,
code: module,
};
}) as ModuleScore[];
const { correct: overallCorrect, total: overallTotal } =
getScoreAndTotal(flattenResults);
const baseOverallResult = overallCorrect / overallTotal;
const overallResult = isNaN(baseOverallResult) ? 0 : baseOverallResult;
const {correct: overallCorrect, total: overallTotal} = getScoreAndTotal(flattenResults);
const baseOverallResult = overallCorrect / overallTotal;
const overallResult = isNaN(baseOverallResult) ? 0 : baseOverallResult;
const overallPNG = getRadialProgressPNG(
"laranja",
overallCorrect,
overallTotal
);
// generate the overall detail report
const overallDetail = {
module: "Overall",
score: overallCorrect,
total: overallTotal,
png: overallPNG,
} as ModuleScore;
const overallPNG = getRadialProgressPNG("laranja", overallCorrect, overallTotal);
// generate the overall detail report
const overallDetail = {
module: "Overall",
score: overallCorrect,
total: overallTotal,
png: overallPNG,
} as ModuleScore;
const testDetails = [overallDetail, ...moduleResults];
// generate the performance summary based on the overall result
const baseStat = data.exams[0];
const performanceSummary = getPerformanceSummary(
// from what I noticed, exams is either an array with the level module
// or X modules, either way
// as long as I verify the first entry I should be fine
baseStat.module,
overallResult
);
const testDetails = [overallDetail, ...moduleResults];
// generate the performance summary based on the overall result
const baseStat = data.exams[0];
const performanceSummary = getPerformanceSummary(
// from what I noticed, exams is either an array with the level module
// or X modules, either way
// as long as I verify the first entry I should be fine
baseStat.module,
overallResult,
);
const showLevel = baseStat.module === "level";
const showLevel = baseStat.module === "level";
// level exams have a different report structure than the skill exams
const getCustomData = () => {
if (showLevel) {
return {
title: "GROUP ENGLISH LEVEL TEST RESULT REPORT ",
details: (
<LevelExamDetails
detail={overallDetail}
title="Group Average CEFR"
/>
),
};
}
// level exams have a different report structure than the skill exams
const getCustomData = () => {
if (showLevel) {
return {
title: "GROUP ENGLISH LEVEL TEST RESULT REPORT ",
details: <LevelExamDetails detail={overallDetail} title="Group Average CEFR" />,
};
}
return {
title: "GROUP ENGLISH SKILLS TEST RESULT REPORT",
details: <SkillExamDetails testDetails={testDetails} />,
};
};
return {
title: "GROUP ENGLISH SKILLS TEST RESULT REPORT",
details: <SkillExamDetails testDetails={testDetails} />,
};
};
const { title, details } = getCustomData();
const {title, details} = getCustomData();
const numberOfStudents = data.assignees.length;
const numberOfStudents = data.assignees.length;
const getStudentsData = async (): Promise<StudentData[]> => {
return data.assignees.map((id) => {
const user = users.find((u) => u.id === id);
const exams = flattenResultsWithGrade.filter((e) => e.user === id);
const date =
exams.length === 0
? "N/A"
: new Date(exams[0].date).toLocaleDateString(undefined, {
year: "numeric",
month: "numeric",
day: "numeric",
});
const getStudentsData = async (): Promise<StudentData[]> => {
return data.assignees.map((id) => {
const user = users.find((u) => u.id === id);
const exams = flattenResultsWithGrade.filter((e) => e.user === id);
const date =
exams.length === 0
? "N/A"
: new Date(exams[0].date).toLocaleDateString(undefined, {
year: "numeric",
month: "numeric",
day: "numeric",
});
const bandScore =
exams.length === 0
? 0
: exams.reduce((accm, curr) => accm + curr.bandScore, 0) /
exams.length;
const { correct, total } = getScoreAndTotal(exams);
const bandScore = exams.length === 0 ? 0 : exams.reduce((accm, curr) => accm + curr.bandScore, 0) / exams.length;
const {correct, total} = getScoreAndTotal(exams);
const result = exams.length === 0 ? "N/A" : `${correct}/${total}`;
const result = exams.length === 0 ? "N/A" : `${correct}/${total}`;
return {
id,
name: user?.name || "N/A",
email: user?.email || "N/A",
gender: user?.demographicInformation?.gender || "N/A",
date,
result,
level: showLevel
? getLevelScoreForUserExams(bandScore)
: undefined,
bandScore,
};
});
};
return {
id,
name: user?.name || "N/A",
email: user?.email || "N/A",
gender: user?.demographicInformation?.gender || "N/A",
date,
result,
level: showLevel ? getLevelScoreForUserExams(bandScore) : undefined,
bandScore,
};
});
};
const studentsData = await getStudentsData();
const studentsData = await getStudentsData();
const getGroupScoreSummary = () => {
const resultHelper = studentsData.reduce(
(accm: GroupScoreSummaryHelper[], curr) => {
const { bandScore, id } = curr;
const getGroupScoreSummary = () => {
const resultHelper = studentsData.reduce((accm: GroupScoreSummaryHelper[], curr) => {
const {bandScore, id} = curr;
const flooredScore = Math.floor(bandScore);
const flooredScore = Math.floor(bandScore);
const hasMatch = accm.find((a) => a.score.includes(flooredScore));
if (hasMatch) {
return accm.map((a) => {
if (a.score.includes(flooredScore)) {
return {
...a,
sessions: [...a.sessions, id],
};
}
const hasMatch = accm.find((a) => a.score.includes(flooredScore));
if (hasMatch) {
return accm.map((a) => {
if (a.score.includes(flooredScore)) {
return {
...a,
sessions: [...a.sessions, id],
};
}
return a;
});
}
return a;
});
}
return [
...accm,
{
score: [flooredScore, flooredScore + 0.5],
label: `${flooredScore} - ${flooredScore + 0.5}`,
sessions: [id],
},
];
},
[]
) as GroupScoreSummaryHelper[];
return [
...accm,
{
score: [flooredScore, flooredScore + 0.5],
label: `${flooredScore} - ${flooredScore + 0.5}`,
sessions: [id],
},
];
}, []) as GroupScoreSummaryHelper[];
const result = resultHelper.map(({ score, label, sessions }) => {
const finalLabel = showLevel ? getLevelScore(score[0])[1] : label;
return {
label: finalLabel,
percent: Math.floor((sessions.length / numberOfStudents) * 100),
description: `No. Candidates ${sessions.length} of ${numberOfStudents}`,
};
});
return result;
};
const result = resultHelper.map(({score, label, sessions}) => {
const finalLabel = showLevel ? getLevelScore(score[0])[1] : label;
return {
label: finalLabel,
percent: Math.floor((sessions.length / numberOfStudents) * 100),
description: `No. Candidates ${sessions.length} of ${numberOfStudents}`,
};
});
return result;
};
const groupScoreSummary = getGroupScoreSummary();
const getInstitution = async () => {
try {
// due to database inconsistencies, I'll be overprotective here
const assignerUserSnap = await getDoc(doc(db, "users", data.assigner));
if (assignerUserSnap.exists()) {
// we'll need the user in order to get the user data (name, email, focus, etc);
const assignerUser = assignerUserSnap.data() as User;
const pdfStream = await ReactPDF.renderToStream(
<GroupTestReport
title={title}
date={moment(data.startDate).tz(user.demographicInformation?.timezone || 'UTC').format('ll HH:mm:ss')}
name={user.name}
email={user.email}
id={user.id}
gender={user.demographicInformation?.gender}
summary={performanceSummary}
renderDetails={details}
logo={"public/logo_title.png"}
qrcode={qrcode}
numberOfStudents={numberOfStudents}
institution="TODO: PLACEHOLDER"
studentsData={studentsData}
showLevel={showLevel}
summaryPNG={overallPNG}
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
groupScoreSummary={groupScoreSummary}
/>
);
if (assignerUser.type === "teacher") {
// also search for groups where this user belongs
const queryGroups = query(collection(db, "groups"), where("participants", "array-contains", assignerUser.id));
const groupSnapshot = await getDocs(queryGroups);
// generate the file ref for storage
const fileName = `${Date.now().toString()}.pdf`;
const refName = `assignment_report/${fileName}`;
const fileRef = ref(storage, refName);
const groups = groupSnapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Group[];
// upload the pdf to storage
const pdfBuffer = await streamToBuffer(pdfStream);
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
contentType: "application/pdf",
});
if (groups.length > 0) {
const adminQuery = query(
collection(db, "users"),
where(
documentId(),
"in",
groups.map((g) => g.admin),
),
);
const adminUsersSnap = await getDocs(adminQuery);
// update the stats entries with the pdf url to prevent duplication
await updateDoc(docSnap.ref, {
pdf: refName,
});
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
const admins = adminUsersSnap.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as CorporateUser[];
res.status(401).json({ ok: false });
return;
} catch (err) {
console.error(err);
res.status(500).json({ ok: false });
return;
}
}
const adminData = admins.find((a) => a.corporateInformation?.companyInformation?.name);
if (adminData) {
return adminData.corporateInformation.companyInformation.name;
}
}
}
if (assignerUser.type === "corporate" && assignerUser.corporateInformation?.companyInformation?.name) {
return assignerUser.corporateInformation.companyInformation.name;
}
}
} catch (err) {
console.error(err);
}
return "";
};
const institution = await getInstitution();
const groupScoreSummary = getGroupScoreSummary();
const demographicInformation = user.demographicInformation as DemographicInformation;
const pdfStream = await ReactPDF.renderToStream(
<GroupTestReport
title={title}
date={moment(data.startDate)
.tz(user.demographicInformation?.timezone || "UTC")
.format("ll HH:mm:ss")}
name={user.name}
email={user.email}
id={user.id}
gender={demographicInformation?.gender}
summary={performanceSummary}
renderDetails={details}
logo={"public/logo_title.png"}
qrcode={qrcode}
numberOfStudents={numberOfStudents}
institution={institution}
studentsData={studentsData}
showLevel={showLevel}
summaryPNG={overallPNG}
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
groupScoreSummary={groupScoreSummary}
passportId={demographicInformation?.passport_id || ""}
/>,
);
// generate the file ref for storage
const fileName = `${Date.now().toString()}.pdf`;
const refName = `assignment_report/${fileName}`;
const fileRef = ref(storage, refName);
// upload the pdf to storage
const pdfBuffer = await streamToBuffer(pdfStream);
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
contentType: "application/pdf",
});
// update the stats entries with the pdf url to prevent duplication
await updateDoc(docSnap.ref, {
pdf: refName,
});
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
res.status(401).json({ok: false});
return;
} catch (err) {
console.error(err);
res.status(500).json({ok: false});
return;
}
}
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (req.session.user) {
const { id } = req.query as { id: string };
if (req.session.user) {
const {id} = req.query as {id: string};
const docSnap = await getDoc(doc(db, "assignments", id));
const data = docSnap.data();
if (!data) {
res.status(400).end();
return;
}
const docSnap = await getDoc(doc(db, "assignments", id));
const data = docSnap.data();
if (!data) {
res.status(400).end();
return;
}
if (data.assigner !== req.session.user.id) {
res.status(401).json({ ok: false });
return;
}
if (data.assigner !== req.session.user.id) {
res.status(401).json({ok: false});
return;
}
if (data.pdf) {
const fileRef = ref(storage, data.pdf);
const url = await getDownloadURL(fileRef);
return res.redirect(url);
}
if (data.pdf) {
const fileRef = ref(storage, data.pdf);
const url = await getDownloadURL(fileRef);
return res.redirect(url);
}
res.status(404).end();
return;
}
res.status(404).end();
return;
}
res.status(401).json({ ok: false });
return;
res.status(401).json({ok: false});
return;
}

View File

@@ -15,7 +15,7 @@ import { sessionOptions } from "@/lib/session";
import ReactPDF from "@react-pdf/renderer";
import TestReport from "@/exams/pdf/test.report";
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { User } from "@/interfaces/user";
import { DemographicInformation, User } from "@/interfaces/user";
import { Module } from "@/interfaces";
import { ModuleScore } from "@/interfaces/module.scores";
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
@@ -305,6 +305,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const { title, details } = getCustomData();
const demographicInformation = user.demographicInformation as DemographicInformation;
const pdfStream = await ReactPDF.renderToStream(
<TestReport
title={title}
@@ -312,7 +313,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
name={user.name}
email={user.email}
id={user.id}
gender={user.demographicInformation?.gender}
gender={demographicInformation?.gender}
summary={performanceSummary}
testDetails={testDetails}
renderDetails={details}
@@ -320,6 +321,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
qrcode={qrcode}
summaryPNG={overallPNG}
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
passportId={demographicInformation?.passport_id || ""}
/>
);