Final improvements for Groups PDF's

This commit is contained in:
Joao Ramos
2024-01-09 20:17:21 +00:00
parent 4e378f0c71
commit 1aadc4647c
8 changed files with 213 additions and 82 deletions

View File

@@ -5,6 +5,7 @@ import { styles } from "../styles";
import { RadialResult } from "./radial.result";
interface Props {
detail: ModuleScore;
title: string;
}
const thresholds = [
@@ -63,7 +64,7 @@ const customStyles = StyleSheet.create({
},
});
export const LevelExamDetails = ({ detail }: Props) => {
export const LevelExamDetails = ({ detail, title }: Props) => {
const updatedThresholds = thresholds.map((t) => ({
...t,
match: detail.score >= t.minValue && detail.score <= t.maxValue,
@@ -86,7 +87,7 @@ export const LevelExamDetails = ({ detail }: Props) => {
<Text
style={[styles.textBold, styles.textColor, { fontSize: "10px" }]}
>
Level as per CEFR Levels
{title}
</Text>
</View>
<View style={customStyles.tableBody}>

View File

@@ -1,39 +1,22 @@
/* eslint-disable jsx-a11y/alt-text */
import React from "react";
import { View, Text, Image, StyleSheet } from "@react-pdf/renderer";
import { View, Text, Image } from "@react-pdf/renderer";
import { styles } from "../styles";
import { ModuleScore } from "@/interfaces/module.scores";
const customStyles = StyleSheet.create({
container: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 4,
position: "relative",
},
resultContainer: {
display: "flex",
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
fontSize: 10,
gap: 8,
},
});
export const RadialResult = ({ module, score, total, png }: ModuleScore) => (
<View key="module" style={[styles.textFont, customStyles.container]}>
export const RadialResult = ({
module,
score,
total,
png,
}: ModuleScore) => (
<View style={[styles.textFont, styles.radialContainer]}>
<Text style={[styles.textColor, styles.textBold, { fontSize: 10 }]}>
{module}
</Text>
<Image src={png} style={styles.image64}></Image>
<View style={[styles.textColor, customStyles.resultContainer]}>
<View style={[styles.textColor, styles.radialResultContainer]}>
<Text style={styles.textBold}>{score}</Text>
<Text style={{ fontSize: 8 }}>out of {total}</Text>
</View>

View File

@@ -13,7 +13,6 @@ import { styles } from "./styles";
import TestReportFooter from "./test.report.footer";
import { ModuleScore, StudentData } from "@/interfaces/module.scores";
import ProgressBar from "./progress.bar";
Font.registerHyphenationCallback((word) => [word]);
interface Props {
@@ -32,6 +31,9 @@ interface Props {
institution: string;
studentsData: StudentData[];
showLevel: boolean;
summaryPNG: string;
summaryScore: string;
groupScoreSummary: any[];
}
const customStyles = StyleSheet.create({
@@ -44,13 +46,11 @@ const customStyles = StyleSheet.create({
flexDirection: "column",
// maxWidth: "600px",
// margin: "0 auto",
border: "1px solid #ccc",
// borderCollapse: 'collapse',
},
tableRow: {
display: "flex",
flexDirection: "row",
borderBottom: "1px solid #ccc",
},
tableHeader: {
fontWeight: "bold",
@@ -80,6 +80,9 @@ const GroupTestReport = ({
institution,
studentsData,
showLevel,
summaryPNG,
summaryScore,
groupScoreSummary,
}: Props) => {
const defaultTextStyle = [styles.textFont, { fontSize: 8 }];
const defaultSkillsTextStyle = [styles.textFont, { fontSize: 8 }];
@@ -150,9 +153,17 @@ const GroupTestReport = ({
>
Group Overall Performance Summary
</Text>
<View>
<View style={{ display: "flex", flexDirection: "row", gap: 16 }}>
<View style={{ flex: 1 }}>
<Text style={[styles.textFont, { fontSize: 8 }]}>{summary}</Text>
</View>
<View style={[styles.textFont, styles.radialContainer]}>
<Image src={summaryPNG} style={styles.image64}></Image>
<View style={[styles.textColor, styles.radialResultContainer]}>
<Text style={styles.textBold}>{summaryScore}</Text>
</View>
</View>
</View>
</View>
<View style={[{ paddingTop: 30 }, styles.separator]}></View>
<View>
@@ -173,14 +184,45 @@ const GroupTestReport = ({
gap: 8,
}}
>
{testDetails
.filter(
({ suggestions, evaluation }) => suggestions || evaluation
)
.map(({ module, suggestions, evaluation }) => (
<Text key={module}>TODO</Text>
<View
style={[
customStyles.table,
styles.textFont,
{ width: "100%", fontSize: "8px" },
]}
>
{groupScoreSummary.map(({ label, percent, description }) => (
<View
style={[
customStyles.tableRow,
{
width: "100%",
alignItems: "center",
justifyContent: "center",
},
]}
key={label}
>
<Text style={[customStyles.tableCell, { maxWidth: "48px" }]}>
{label}
</Text>
<View style={customStyles.tableCell}>
<ProgressBar
width={200}
height={18}
backgroundColor="#fab7b0"
progressColor="#cc5b55"
percentage={percent}
/>
</View>
<Text style={[customStyles.tableCell, { maxWidth: "24px" }]}>
{percent}
</Text>
<Text style={customStyles.tableCell}>{description}</Text>
</View>
))}
</View>
</View>
<View style={styles.alignRightRow}>
<Image src={qrcode} style={styles.qrcode} />
</View>
@@ -205,7 +247,7 @@ const GroupTestReport = ({
style={[
customStyles.table,
styles.textFont,
{ width: "100%", fontSize: "8px" },
{ border: "1px solid #ccc", width: "100%", fontSize: "8px" },
]}
>
<View
@@ -213,6 +255,7 @@ const GroupTestReport = ({
customStyles.tableRow,
customStyles.tableHeader,
customStyles.tableCellHighlight,
{ borderBottom: "1px solid #ccc" },
]}
>
<Text style={[customStyles.tableCell, { maxWidth: "24px" }]}>
@@ -229,11 +272,17 @@ const GroupTestReport = ({
<Text style={[customStyles.tableCell, { maxWidth: "48px" }]}>
Result
</Text>
<Text style={customStyles.tableCell}>Level</Text>
{showLevel && <Text style={customStyles.tableCell}>Level</Text>}
</View>
{studentsData.map(
({ id, name, email, gender, date, result, level }, index) => (
<View style={customStyles.tableRow} key={id}>
<View
style={[
customStyles.tableRow,
{ borderBottom: "1px solid #ccc" },
]}
key={id}
>
<Text
style={[
customStyles.tableCell,
@@ -254,7 +303,9 @@ const GroupTestReport = ({
<Text style={[customStyles.tableCell, { maxWidth: "48px" }]}>
{result}
</Text>
{showLevel && (
<Text style={customStyles.tableCell}>{level}</Text>
)}
</View>
)
)}

View File

@@ -40,7 +40,7 @@ const ProgressBar = ({
>
<View
style={[
{ width: `${percentage}px`, backgroundColor: progressColor },
{ width: `${percentage}%`, backgroundColor: progressColor },
styles.progressBarPerc,
]}
></View>

View File

@@ -51,4 +51,23 @@ export const styles = StyleSheet.create({
width: "80px",
height: "80px",
},
radialContainer: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 4,
position: "relative",
},
radialResultContainer: {
display: "flex",
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
fontSize: 10,
gap: 8,
},
});

View File

@@ -18,4 +18,5 @@ export interface ModuleScore {
date: string;
result: string;
level?: string;
bandScore: number;
}

View File

@@ -32,6 +32,11 @@ import {
streamToBuffer,
} from "@/utils/pdf";
interface GroupScoreSummaryHelper {
score: [number, number];
label: string;
sessions: string[];
}
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
@@ -99,13 +104,7 @@ const getScoreAndTotal = (stats: Stat[]) => {
);
};
const getLevelScoreForUserExams = (
correct: number,
total: number,
module: Module,
focus: "academic" | "general"
) => {
const bandScore = calculateBandScore(correct, total, module, focus);
const getLevelScoreForUserExams = (bandScore: number) => {
const [level] = getLevelScore(bandScore);
return level;
};
@@ -158,20 +157,45 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
[]
) 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 = flattenResults.filter(
const moduleResults = flattenResultsWithGrade.filter(
(e) => e.module === module
);
const bandScore =
moduleResults.reduce((accm, curr) => accm + curr.bandScore, 0) /
moduleResults.length;
const { correct, total } = getScoreAndTotal(moduleResults);
const score = calculateBandScore(correct, total, module, "academic");
const png = getRadialProgressPNG("azul", score, total);
const png = getRadialProgressPNG("azul", correct, total);
return {
bandScore: score,
bandScore,
png,
module: module[0].toUpperCase() + module.substring(1),
score,
score: bandScore,
total,
code: module,
};
@@ -181,12 +205,17 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
getScoreAndTotal(flattenResults);
const overallResult = overallCorrect / overallTotal;
const overallPNG = getRadialProgressPNG(
"laranja",
overallCorrect,
overallTotal
);
// generate the overall detail report
const overallDetail = {
module: "Overall",
score: overallCorrect,
total: overallTotal,
png: getRadialProgressPNG("laranja", overallCorrect, overallTotal),
png: overallPNG,
} as ModuleScore;
const testDetails = [overallDetail, ...moduleResults];
@@ -207,7 +236,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
if (showLevel) {
return {
title: "GROUP ENGLISH LEVEL TEST RESULT REPORT ",
details: <LevelExamDetails detail={overallDetail} />,
details: (
<LevelExamDetails
detail={overallDetail}
title="Group Average CEFR"
/>
),
};
}
@@ -222,21 +256,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const numberOfStudents = data.assignees.length;
const getStudentsData = async (): Promise<StudentData[]> => {
// const usersCol = collection(db, "users");
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[];
return data.assignees.map((id) => {
const user = users.find((u) => u.id === id);
const exams = flattenResults.filter((e) => e.user === id);
const exams = flattenResultsWithGrade.filter((e) => e.user === id);
const date =
exams.length === 0
? "N/A"
@@ -246,6 +268,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
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}`;
@@ -258,19 +285,60 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
date,
result,
level: showLevel
? getLevelScoreForUserExams(
correct,
total,
baseStat.module,
user?.focus || "academic"
)
: "",
? 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(({ label, sessions }) => {
return {
label,
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}
@@ -288,6 +356,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
institution="TODO: PLACEHOLDER"
studentsData={studentsData}
showLevel={showLevel}
summaryPNG={overallPNG}
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
groupScoreSummary={groupScoreSummary}
/>
);

View File

@@ -284,7 +284,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
if (stat.module === "level") {
return {
title: "ENGLISH LEVEL TEST RESULT REPORT ",
details: <LevelExamDetails detail={overallDetail} />,
details: (
<LevelExamDetails
detail={overallDetail}
title="Level as per CEFR Levels"
/>
),
};
}