Merge branch 'develop' into feature/62/upload-users-with-excel
This commit is contained in:
429
src/pages/api/assignments/[id]/export.tsx
Normal file
429
src/pages/api/assignments/[id]/export.tsx
Normal file
@@ -0,0 +1,429 @@
|
||||
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";
|
||||
|
||||
interface GroupScoreSummaryHelper {
|
||||
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);
|
||||
}
|
||||
|
||||
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.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.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.";
|
||||
};
|
||||
|
||||
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.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.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.";
|
||||
};
|
||||
|
||||
const getPerformanceSummary = (module: Module, score: number) => {
|
||||
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 }
|
||||
);
|
||||
};
|
||||
|
||||
const getLevelScoreForUserExams = (bandScore: number) => {
|
||||
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 };
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 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 };
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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 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 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"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: "GROUP ENGLISH SKILLS TEST RESULT REPORT",
|
||||
details: <SkillExamDetails testDetails={testDetails} />,
|
||||
};
|
||||
};
|
||||
|
||||
const { title, details } = getCustomData();
|
||||
|
||||
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 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}`;
|
||||
|
||||
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 getGroupScoreSummary = () => {
|
||||
const resultHelper = studentsData.reduce(
|
||||
(accm: GroupScoreSummaryHelper[], curr) => {
|
||||
const { bandScore, id } = curr;
|
||||
|
||||
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],
|
||||
};
|
||||
}
|
||||
|
||||
return a;
|
||||
});
|
||||
}
|
||||
|
||||
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 groupScoreSummary = getGroupScoreSummary();
|
||||
|
||||
const pdfStream = await ReactPDF.renderToStream(
|
||||
<GroupTestReport
|
||||
title={title}
|
||||
date={new Date(data.startDate).toLocaleString()}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
// 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 };
|
||||
|
||||
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.pdf) {
|
||||
const fileRef = ref(storage, data.pdf);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
return res.redirect(url);
|
||||
}
|
||||
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
376
src/pages/api/stats/[id]/export.tsx
Normal file
376
src/pages/api/stats/[id]/export.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { app, storage } from "@/firebase";
|
||||
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 { ref, uploadBytes, getDownloadURL } from "firebase/storage";
|
||||
import { 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";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const getExamSummary = (score: number) => {
|
||||
if (score > 0.8) {
|
||||
return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language.";
|
||||
}
|
||||
|
||||
if (score > 0.6) {
|
||||
return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery.";
|
||||
}
|
||||
|
||||
if (score > 0.4) {
|
||||
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended.";
|
||||
}
|
||||
|
||||
if (score > 0.2) {
|
||||
return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency.";
|
||||
}
|
||||
|
||||
return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional 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 showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability.";
|
||||
}
|
||||
|
||||
if (score > 0.6) {
|
||||
return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies.";
|
||||
}
|
||||
|
||||
if (score > 0.4) {
|
||||
return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies.";
|
||||
}
|
||||
|
||||
if (score > 0.2) {
|
||||
return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency.";
|
||||
}
|
||||
|
||||
return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments.";
|
||||
};
|
||||
|
||||
const getPerformanceSummary = (module: Module, score: number) => {
|
||||
if (module === "level") return getLevelSummary(score);
|
||||
return getExamSummary(score);
|
||||
};
|
||||
interface SkillsFeedbackRequest {
|
||||
code: Module;
|
||||
name: string;
|
||||
grade: number;
|
||||
}
|
||||
|
||||
interface SkillsFeedbackResponse extends SkillsFeedbackRequest {
|
||||
evaluation: string;
|
||||
suggestions: string;
|
||||
}
|
||||
|
||||
const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => {
|
||||
const backendRequest = await axios.post(
|
||||
`${process.env.BACKEND_URL}/grading_summary`,
|
||||
{ sections },
|
||||
{
|
||||
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> => {
|
||||
let i = 0;
|
||||
try {
|
||||
const data = await getSkillsFeedback(sections);
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (i < 3) {
|
||||
i++;
|
||||
return handleSkillsFeedbackRequest(sections);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
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),
|
||||
where("user", "==", req.session.user.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);
|
||||
|
||||
if (hasPDF) {
|
||||
// if it does, return the pdf url
|
||||
const fileRef = ref(storage, hasPDF.pdf);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// generate the pdf report
|
||||
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
|
||||
);
|
||||
|
||||
if (!qrcode) {
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// stats may contain multiple exams of the same type so we need to aggregate them
|
||||
const results = (
|
||||
stats.reduce((accm: ModuleScore[], { module, score }) => {
|
||||
const fixedModuleStr =
|
||||
module[0].toUpperCase() + module.substring(1);
|
||||
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
|
||||
return accm.map((e: ModuleScore) => {
|
||||
if (e.module === fixedModuleStr) {
|
||||
return {
|
||||
...e,
|
||||
score: e.score + score.correct,
|
||||
total: e.total + score.total,
|
||||
};
|
||||
}
|
||||
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
...accm,
|
||||
{
|
||||
module: fixedModuleStr,
|
||||
score: score.correct,
|
||||
total: score.total,
|
||||
code: module,
|
||||
},
|
||||
];
|
||||
}, []) as ModuleScore[]
|
||||
).map((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
|
||||
);
|
||||
|
||||
return {
|
||||
...moduleScore,
|
||||
// generate the closest radial progress png for the score
|
||||
png: getRadialProgressPNG("azul", score, total),
|
||||
bandScore,
|
||||
};
|
||||
});
|
||||
|
||||
// get the skills feedback from the backend based on the module grade
|
||||
const skillsFeedback = (await handleSkillsFeedbackRequest(
|
||||
results.map(({ code, bandScore }) => ({
|
||||
code,
|
||||
name: moduleLabels[code],
|
||||
grade: bandScore,
|
||||
}))
|
||||
)) as SkillsFeedbackResponse[];
|
||||
|
||||
if (!skillsFeedback) {
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// assign the feedback to the results
|
||||
const finalResults = results.map((result) => {
|
||||
const feedback = skillsFeedback.find(
|
||||
(f: SkillsFeedbackResponse) => f.code === result.code
|
||||
);
|
||||
|
||||
if (feedback) {
|
||||
return {
|
||||
...result,
|
||||
evaluation: feedback?.evaluation,
|
||||
suggestions: feedback?.suggestions,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// calculate the overall score out of all the aggregated results
|
||||
const overallScore = results.reduce(
|
||||
(accm, { score }) => accm + score,
|
||||
0
|
||||
);
|
||||
const overallTotal = results.reduce(
|
||||
(accm, { total }) => accm + total,
|
||||
0
|
||||
);
|
||||
const overallResult = overallScore / overallTotal;
|
||||
|
||||
// generate the overall detail report
|
||||
const overallDetail = {
|
||||
module: "Overall",
|
||||
score: overallScore,
|
||||
total: overallTotal,
|
||||
png: getRadialProgressPNG("laranja", overallScore, overallTotal),
|
||||
} 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"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: "ENGLISH SKILLS TEST RESULT REPORT",
|
||||
details: <SkillExamDetails testDetails={testDetails} />,
|
||||
};
|
||||
};
|
||||
|
||||
const { title, details } = getCustomData();
|
||||
const pdfStream = await ReactPDF.renderToStream(
|
||||
<TestReport
|
||||
title={title}
|
||||
date={new Date(stat.date).toLocaleString()}
|
||||
name={user.name}
|
||||
email={user.email}
|
||||
id={user.id}
|
||||
gender={user.demographicInformation?.gender}
|
||||
summary={performanceSummary}
|
||||
testDetails={testDetails}
|
||||
renderDetails={details}
|
||||
logo={"public/logo_title.png"}
|
||||
qrcode={qrcode}
|
||||
/>
|
||||
);
|
||||
|
||||
// generate the file ref for storage
|
||||
const fileName = `${Date.now().toString()}.pdf`;
|
||||
const refName = `exam_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
|
||||
docsSnap.docs.forEach(async (doc) => {
|
||||
await updateDoc(doc.ref, {
|
||||
pdf: refName,
|
||||
});
|
||||
});
|
||||
const url = await getDownloadURL(fileRef);
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
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))
|
||||
);
|
||||
|
||||
if (docsSnap.empty) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = docsSnap.docs.map((d) => d.data());
|
||||
|
||||
const hasPDF = stats.find((s) => s.pdf);
|
||||
|
||||
if (hasPDF) {
|
||||
const fileRef = ref(storage, hasPDF.pdf);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
return res.redirect(url);
|
||||
}
|
||||
|
||||
res.status(500).end();
|
||||
}
|
||||
23
src/pages/api/stats/[id]/index.ts
Normal file
23
src/pages/api/stats/[id]/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {app} from "@/firebase";
|
||||
import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc, deleteDoc} from "firebase/firestore";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {uuidv4} from "@firebase/util";
|
||||
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return GET(req, res);
|
||||
|
||||
res.status(404).json({ok: false});
|
||||
}
|
||||
|
||||
async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||
const {id} = req.query;
|
||||
|
||||
const snapshot = await getDoc(doc(db, "stats", id as string));
|
||||
|
||||
res.status(200).json({...snapshot.data(), id: snapshot.id});
|
||||
}
|
||||
@@ -26,4 +26,4 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
...doc.data(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user