Final improvements for Groups PDF's
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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]}>
|
||||
<Text style={[styles.textColor, styles.textBold, { fontSize: 10 }]}>
|
||||
{module}
|
||||
</Text>
|
||||
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>
|
||||
|
||||
@@ -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,8 +153,16 @@ const GroupTestReport = ({
|
||||
>
|
||||
Group Overall Performance Summary
|
||||
</Text>
|
||||
<View>
|
||||
<Text style={[styles.textFont, { fontSize: 8 }]}>{summary}</Text>
|
||||
<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>
|
||||
@@ -173,13 +184,44 @@ 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} />
|
||||
@@ -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>
|
||||
<Text style={customStyles.tableCell}>{level}</Text>
|
||||
{showLevel && (
|
||||
<Text style={customStyles.tableCell}>{level}</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -40,7 +40,7 @@ const ProgressBar = ({
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
{ width: `${percentage}px`, backgroundColor: progressColor },
|
||||
{ width: `${percentage}%`, backgroundColor: progressColor },
|
||||
styles.progressBarPerc,
|
||||
]}
|
||||
></View>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,4 +18,5 @@ export interface ModuleScore {
|
||||
date: string;
|
||||
result: string;
|
||||
level?: string;
|
||||
bandScore: number;
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user