Merge branch 'develop' into bug-fixing-16-jan-24
This commit is contained in:
@@ -42,6 +42,7 @@
|
|||||||
"iron-session": "^6.3.1",
|
"iron-session": "^6.3.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"moment-timezone": "^0.5.44",
|
||||||
"next": "13.1.6",
|
"next": "13.1.6",
|
||||||
"nodemailer": "^6.9.5",
|
"nodemailer": "^6.9.5",
|
||||||
"nodemailer-express-handlebars": "^6.1.0",
|
"nodemailer-express-handlebars": "^6.1.0",
|
||||||
|
|||||||
64
src/components/Low/TImezoneSelect.tsx
Normal file
64
src/components/Low/TImezoneSelect.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Fragment, useState } from "react";
|
||||||
|
import { Combobox, Transition } from "@headlessui/react";
|
||||||
|
import { BsChevronExpand } from "react-icons/bs";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TimezoneSelect({
|
||||||
|
value,
|
||||||
|
disabled = false,
|
||||||
|
onChange,
|
||||||
|
}: Props) {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const timezones = moment.tz.names();
|
||||||
|
|
||||||
|
const filteredTimezones = query === "" ? timezones : timezones.filter((x) => x.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Combobox value={value} onChange={onChange} disabled={disabled}>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<div className="relative w-full cursor-default overflow-hidden ">
|
||||||
|
<Combobox.Input
|
||||||
|
className="py-6 w-full px-8 text-sm font-normal placeholder:text-mti-gray-cool bg-white disabled:bg-mti-gray-platinum/40 rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-8">
|
||||||
|
<BsChevronExpand />
|
||||||
|
</Combobox.Button>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
afterLeave={() => setQuery("")}
|
||||||
|
>
|
||||||
|
<Combobox.Options className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-xl bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||||
|
{filteredTimezones.map((timezone: string) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={timezone}
|
||||||
|
value={timezone}
|
||||||
|
className={({ active }) =>
|
||||||
|
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
||||||
|
active
|
||||||
|
? "bg-mti-purple-light text-white"
|
||||||
|
: "text-gray-900"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{timezone}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,19 +12,26 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
const formatSolution = (solution: string, errors: {correction: string | null; misspelled: string}[]) => {
|
const formatSolution = (solution: string, errors: {correction: string | null; misspelled: string}[]) => {
|
||||||
const errorRegex = new RegExp(errors.map((x) => `(${x.misspelled})`).join("|"));
|
const misspelled = errors.map((x) => x.misspelled);
|
||||||
|
console.log({misspelled});
|
||||||
|
const errorRegex = new RegExp(errors.map((x) => `(${x.misspelled})`).join("|"), "g");
|
||||||
|
console.log(errorRegex.global);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{reactStringReplace(solution, errorRegex, (match) => {
|
{solution.split(" ").map((word) => {
|
||||||
const correction = errors.find((x) => x.misspelled === match)?.correction;
|
if (!misspelled.includes(word)) return <>{word} </>;
|
||||||
|
|
||||||
|
const correction = errors.find((x) => x.misspelled === word)?.correction;
|
||||||
return (
|
return (
|
||||||
<span
|
<>
|
||||||
data-tip={correction ? correction : undefined}
|
<span
|
||||||
className={clsx("text-mti-red-light font-medium underline underline-offset-2", correction && "tooltip")}>
|
key={word}
|
||||||
{match}
|
data-tip={correction ? correction : undefined}
|
||||||
</span>
|
className={clsx("text-mti-red-light font-medium underline underline-offset-2", correction && "tooltip")}>
|
||||||
|
{word}
|
||||||
|
</span>{" "}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export interface DemographicInformation {
|
|||||||
gender: Gender;
|
gender: Gender;
|
||||||
employment: EmploymentStatus;
|
employment: EmploymentStatus;
|
||||||
passport_id?: string;
|
passport_id?: string;
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DemographicCorporateInformation {
|
export interface DemographicCorporateInformation {
|
||||||
@@ -85,6 +86,7 @@ export interface DemographicCorporateInformation {
|
|||||||
phone: string;
|
phone: string;
|
||||||
gender: Gender;
|
gender: Gender;
|
||||||
position: string;
|
position: string;
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Gender = "male" | "female" | "other";
|
export type Gender = "male" | "female" | "other";
|
||||||
|
|||||||
@@ -1,496 +1,434 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import { app, storage } from "@/firebase";
|
import {app, storage} from "@/firebase";
|
||||||
import {
|
import {getFirestore, doc, getDoc, updateDoc, getDocs, query, collection, where, documentId} from "firebase/firestore";
|
||||||
getFirestore,
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
doc,
|
import {sessionOptions} from "@/lib/session";
|
||||||
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 ReactPDF from "@react-pdf/renderer";
|
||||||
import GroupTestReport from "@/exams/pdf/group.test.report";
|
import GroupTestReport from "@/exams/pdf/group.test.report";
|
||||||
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
|
import {ref, uploadBytes, getDownloadURL} from "firebase/storage";
|
||||||
import { Stat, CorporateUser } from "@/interfaces/user";
|
import {Stat, CorporateUser} from "@/interfaces/user";
|
||||||
import { User, DemographicInformation } from "@/interfaces/user";
|
import {User, DemographicInformation} from "@/interfaces/user";
|
||||||
import { Module } from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import { ModuleScore, StudentData } from "@/interfaces/module.scores";
|
import {ModuleScore, StudentData} from "@/interfaces/module.scores";
|
||||||
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
|
import {SkillExamDetails} from "@/exams/pdf/details/skill.exam";
|
||||||
import { LevelExamDetails } from "@/exams/pdf/details/level.exam";
|
import {LevelExamDetails} from "@/exams/pdf/details/level.exam";
|
||||||
import { calculateBandScore, getLevelScore } from "@/utils/score";
|
import {calculateBandScore, getLevelScore} from "@/utils/score";
|
||||||
import {
|
import {generateQRCode, getRadialProgressPNG, streamToBuffer} from "@/utils/pdf";
|
||||||
generateQRCode,
|
import {Group} from "@/interfaces/user";
|
||||||
getRadialProgressPNG,
|
import moment from "moment-timezone";
|
||||||
streamToBuffer,
|
|
||||||
} from "@/utils/pdf";
|
|
||||||
import { Group } from "@/interfaces/user";
|
|
||||||
|
|
||||||
interface GroupScoreSummaryHelper {
|
interface GroupScoreSummaryHelper {
|
||||||
score: [number, number];
|
score: [number, number];
|
||||||
label: string;
|
label: string;
|
||||||
sessions: string[];
|
sessions: string[];
|
||||||
}
|
}
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method === "GET") return get(req, res);
|
if (req.method === "GET") return get(req, res);
|
||||||
if (req.method === "POST") return post(req, res);
|
if (req.method === "POST") return post(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getExamSummary = (score: number) => {
|
const getExamSummary = (score: number) => {
|
||||||
if (score > 0.8) {
|
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.";
|
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) {
|
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.";
|
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) {
|
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.";
|
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) {
|
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 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) => {
|
const getLevelSummary = (score: number) => {
|
||||||
if (score > 0.8) {
|
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.";
|
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) {
|
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.";
|
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) {
|
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.";
|
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) {
|
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 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) => {
|
const getPerformanceSummary = (module: Module, score: number) => {
|
||||||
if (module === "level") return getLevelSummary(score);
|
if (module === "level") return getLevelSummary(score);
|
||||||
return getExamSummary(score);
|
return getExamSummary(score);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getScoreAndTotal = (stats: Stat[]) => {
|
const getScoreAndTotal = (stats: Stat[]) => {
|
||||||
return stats.reduce(
|
return stats.reduce(
|
||||||
(acc, { score }) => {
|
(acc, {score}) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
correct: acc.correct + score.correct,
|
correct: acc.correct + score.correct,
|
||||||
total: acc.total + score.total,
|
total: acc.total + score.total,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ correct: 0, total: 0 }
|
{correct: 0, total: 0},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLevelScoreForUserExams = (bandScore: number) => {
|
const getLevelScoreForUserExams = (bandScore: number) => {
|
||||||
const [level] = getLevelScore(bandScore);
|
const [level] = getLevelScore(bandScore);
|
||||||
return level;
|
return level;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// verify if it's a logged user that is trying to export
|
// verify if it's a logged user that is trying to export
|
||||||
if (req.session.user) {
|
if (req.session.user) {
|
||||||
const { id } = req.query as { id: string };
|
const {id} = req.query as {id: string};
|
||||||
|
|
||||||
const docSnap = await getDoc(doc(db, "assignments", id));
|
const docSnap = await getDoc(doc(db, "assignments", id));
|
||||||
const data = docSnap.data() as {
|
const data = docSnap.data() as {
|
||||||
assigner: string;
|
assigner: string;
|
||||||
assignees: string[];
|
assignees: string[];
|
||||||
results: any;
|
results: any;
|
||||||
exams: { module: Module }[];
|
exams: {module: Module}[];
|
||||||
startDate: string;
|
startDate: string;
|
||||||
pdf?: string;
|
pdf?: string;
|
||||||
};
|
};
|
||||||
if (!data) {
|
if (!data) {
|
||||||
res.status(400).end();
|
res.status(400).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.assigner !== req.session.user.id) {
|
if (data.assigner !== req.session.user.id) {
|
||||||
res.status(401).json({ ok: false });
|
res.status(401).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.pdf) {
|
if (data.pdf) {
|
||||||
// if it does, return the pdf url
|
// if it does, return the pdf url
|
||||||
const fileRef = ref(storage, data.pdf);
|
const fileRef = ref(storage, data.pdf);
|
||||||
const url = await getDownloadURL(fileRef);
|
const url = await getDownloadURL(fileRef);
|
||||||
|
|
||||||
res.status(200).end(url);
|
res.status(200).end(url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const docUser = await getDoc(doc(db, "users", req.session.user.id));
|
const docUser = await getDoc(doc(db, "users", req.session.user.id));
|
||||||
if (docUser.exists()) {
|
if (docUser.exists()) {
|
||||||
// we'll need the user in order to get the user data (name, email, focus, etc);
|
// we'll need the user in order to get the user data (name, email, focus, etc);
|
||||||
const user = docUser.data() as User;
|
const user = docUser.data() as User;
|
||||||
|
|
||||||
// generate the QR code for the report
|
// generate the QR code for the report
|
||||||
const qrcode = await generateQRCode(
|
const qrcode = await generateQRCode((req.headers.origin || "") + req.url);
|
||||||
(req.headers.origin || "") + req.url
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!qrcode) {
|
if (!qrcode) {
|
||||||
res.status(500).json({ ok: false });
|
res.status(500).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const flattenResults = data.results.reduce(
|
const flattenResults = data.results.reduce((accm: Stat[], entry: any) => {
|
||||||
(accm: Stat[], entry: any) => {
|
const stats = entry.stats as Stat[];
|
||||||
const stats = entry.stats as Stat[];
|
return [...accm, ...stats];
|
||||||
return [...accm, ...stats];
|
}, []) as Stat[];
|
||||||
},
|
|
||||||
[]
|
|
||||||
) as Stat[];
|
|
||||||
|
|
||||||
const docsSnap = await getDocs(
|
const docsSnap = await getDocs(query(collection(db, "users"), where(documentId(), "in", data.assignees)));
|
||||||
query(
|
const users = docsSnap.docs.map((d) => ({
|
||||||
collection(db, "users"),
|
...d.data(),
|
||||||
where(documentId(), "in", data.assignees)
|
id: d.id,
|
||||||
)
|
})) as User[];
|
||||||
);
|
|
||||||
const users = docsSnap.docs.map((d) => ({
|
|
||||||
...d.data(),
|
|
||||||
id: d.id,
|
|
||||||
})) as User[];
|
|
||||||
|
|
||||||
const flattenResultsWithGrade = flattenResults.map((e) => {
|
const flattenResultsWithGrade = flattenResults.map((e) => {
|
||||||
const focus = users.find((u) => u.id === e.user)?.focus || "academic";
|
const focus = users.find((u) => u.id === e.user)?.focus || "academic";
|
||||||
const bandScore = calculateBandScore(
|
const bandScore = calculateBandScore(e.score.correct, e.score.total, e.module, focus);
|
||||||
e.score.correct,
|
|
||||||
e.score.total,
|
|
||||||
e.module,
|
|
||||||
focus
|
|
||||||
);
|
|
||||||
|
|
||||||
return { ...e, bandScore };
|
return {...e, bandScore};
|
||||||
});
|
});
|
||||||
|
|
||||||
const moduleResults = data.exams.map(({ module }) => {
|
const moduleResults = data.exams.map(({module}) => {
|
||||||
const moduleResults = flattenResultsWithGrade.filter(
|
const moduleResults = flattenResultsWithGrade.filter((e) => e.module === module);
|
||||||
(e) => e.module === module
|
|
||||||
);
|
|
||||||
|
|
||||||
const baseBandScore =
|
const baseBandScore = moduleResults.reduce((accm, curr) => accm + curr.bandScore, 0) / moduleResults.length;
|
||||||
moduleResults.reduce((accm, curr) => accm + curr.bandScore, 0) /
|
const bandScore = isNaN(baseBandScore) ? 0 : baseBandScore;
|
||||||
moduleResults.length;
|
const {correct, total} = getScoreAndTotal(moduleResults);
|
||||||
const bandScore = isNaN(baseBandScore) ? 0 : baseBandScore;
|
const png = getRadialProgressPNG("azul", correct, total);
|
||||||
const { correct, total } = getScoreAndTotal(moduleResults);
|
|
||||||
const png = getRadialProgressPNG("azul", correct, total);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bandScore,
|
bandScore,
|
||||||
png,
|
png,
|
||||||
module: module[0].toUpperCase() + module.substring(1),
|
module: module[0].toUpperCase() + module.substring(1),
|
||||||
score: bandScore,
|
score: bandScore,
|
||||||
total,
|
total,
|
||||||
code: module,
|
code: module,
|
||||||
};
|
};
|
||||||
}) as ModuleScore[];
|
}) as ModuleScore[];
|
||||||
|
|
||||||
const { correct: overallCorrect, total: overallTotal } =
|
const {correct: overallCorrect, total: overallTotal} = getScoreAndTotal(flattenResults);
|
||||||
getScoreAndTotal(flattenResults);
|
const baseOverallResult = overallCorrect / overallTotal;
|
||||||
const baseOverallResult = overallCorrect / overallTotal;
|
const overallResult = isNaN(baseOverallResult) ? 0 : baseOverallResult;
|
||||||
const overallResult = isNaN(baseOverallResult) ? 0 : baseOverallResult;
|
|
||||||
|
|
||||||
const overallPNG = getRadialProgressPNG(
|
const overallPNG = getRadialProgressPNG("laranja", overallCorrect, overallTotal);
|
||||||
"laranja",
|
// generate the overall detail report
|
||||||
overallCorrect,
|
const overallDetail = {
|
||||||
overallTotal
|
module: "Overall",
|
||||||
);
|
score: overallCorrect,
|
||||||
// generate the overall detail report
|
total: overallTotal,
|
||||||
const overallDetail = {
|
png: overallPNG,
|
||||||
module: "Overall",
|
} as ModuleScore;
|
||||||
score: overallCorrect,
|
|
||||||
total: overallTotal,
|
|
||||||
png: overallPNG,
|
|
||||||
} as ModuleScore;
|
|
||||||
|
|
||||||
const testDetails = [overallDetail, ...moduleResults];
|
const testDetails = [overallDetail, ...moduleResults];
|
||||||
// generate the performance summary based on the overall result
|
// generate the performance summary based on the overall result
|
||||||
const baseStat = data.exams[0];
|
const baseStat = data.exams[0];
|
||||||
const performanceSummary = getPerformanceSummary(
|
const performanceSummary = getPerformanceSummary(
|
||||||
// from what I noticed, exams is either an array with the level module
|
// from what I noticed, exams is either an array with the level module
|
||||||
// or X modules, either way
|
// or X modules, either way
|
||||||
// as long as I verify the first entry I should be fine
|
// as long as I verify the first entry I should be fine
|
||||||
baseStat.module,
|
baseStat.module,
|
||||||
overallResult
|
overallResult,
|
||||||
);
|
);
|
||||||
|
|
||||||
const showLevel = baseStat.module === "level";
|
const showLevel = baseStat.module === "level";
|
||||||
|
|
||||||
// level exams have a different report structure than the skill exams
|
// level exams have a different report structure than the skill exams
|
||||||
const getCustomData = () => {
|
const getCustomData = () => {
|
||||||
if (showLevel) {
|
if (showLevel) {
|
||||||
return {
|
return {
|
||||||
title: "GROUP ENGLISH LEVEL TEST RESULT REPORT ",
|
title: "GROUP ENGLISH LEVEL TEST RESULT REPORT ",
|
||||||
details: (
|
details: <LevelExamDetails detail={overallDetail} title="Group Average CEFR" />,
|
||||||
<LevelExamDetails
|
};
|
||||||
detail={overallDetail}
|
}
|
||||||
title="Group Average CEFR"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: "GROUP ENGLISH SKILLS TEST RESULT REPORT",
|
title: "GROUP ENGLISH SKILLS TEST RESULT REPORT",
|
||||||
details: <SkillExamDetails testDetails={testDetails} />,
|
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[]> => {
|
const getStudentsData = async (): Promise<StudentData[]> => {
|
||||||
return data.assignees.map((id) => {
|
return data.assignees.map((id) => {
|
||||||
const user = users.find((u) => u.id === id);
|
const user = users.find((u) => u.id === id);
|
||||||
const exams = flattenResultsWithGrade.filter((e) => e.user === id);
|
const exams = flattenResultsWithGrade.filter((e) => e.user === id);
|
||||||
const date =
|
const date =
|
||||||
exams.length === 0
|
exams.length === 0
|
||||||
? "N/A"
|
? "N/A"
|
||||||
: new Date(exams[0].date).toLocaleDateString(undefined, {
|
: new Date(exams[0].date).toLocaleDateString(undefined, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "numeric",
|
month: "numeric",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
});
|
});
|
||||||
|
|
||||||
const bandScore =
|
const bandScore = exams.length === 0 ? 0 : exams.reduce((accm, curr) => accm + curr.bandScore, 0) / exams.length;
|
||||||
exams.length === 0
|
const {correct, total} = getScoreAndTotal(exams);
|
||||||
? 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 {
|
return {
|
||||||
id,
|
id,
|
||||||
name: user?.name || "N/A",
|
name: user?.name || "N/A",
|
||||||
email: user?.email || "N/A",
|
email: user?.email || "N/A",
|
||||||
gender: user?.demographicInformation?.gender || "N/A",
|
gender: user?.demographicInformation?.gender || "N/A",
|
||||||
date,
|
date,
|
||||||
result,
|
result,
|
||||||
level: showLevel
|
level: showLevel ? getLevelScoreForUserExams(bandScore) : undefined,
|
||||||
? getLevelScoreForUserExams(bandScore)
|
bandScore,
|
||||||
: undefined,
|
};
|
||||||
bandScore,
|
});
|
||||||
};
|
};
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const studentsData = await getStudentsData();
|
const studentsData = await getStudentsData();
|
||||||
|
|
||||||
const getGroupScoreSummary = () => {
|
const getGroupScoreSummary = () => {
|
||||||
const resultHelper = studentsData.reduce(
|
const resultHelper = studentsData.reduce((accm: GroupScoreSummaryHelper[], curr) => {
|
||||||
(accm: GroupScoreSummaryHelper[], curr) => {
|
const {bandScore, id} = curr;
|
||||||
const { bandScore, id } = curr;
|
|
||||||
|
|
||||||
const flooredScore = Math.floor(bandScore);
|
const flooredScore = Math.floor(bandScore);
|
||||||
|
|
||||||
const hasMatch = accm.find((a) => a.score.includes(flooredScore));
|
const hasMatch = accm.find((a) => a.score.includes(flooredScore));
|
||||||
if (hasMatch) {
|
if (hasMatch) {
|
||||||
return accm.map((a) => {
|
return accm.map((a) => {
|
||||||
if (a.score.includes(flooredScore)) {
|
if (a.score.includes(flooredScore)) {
|
||||||
return {
|
return {
|
||||||
...a,
|
...a,
|
||||||
sessions: [...a.sessions, id],
|
sessions: [...a.sessions, id],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...accm,
|
...accm,
|
||||||
{
|
{
|
||||||
score: [flooredScore, flooredScore + 0.5],
|
score: [flooredScore, flooredScore + 0.5],
|
||||||
label: `${flooredScore} - ${flooredScore + 0.5}`,
|
label: `${flooredScore} - ${flooredScore + 0.5}`,
|
||||||
sessions: [id],
|
sessions: [id],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
}, []) as GroupScoreSummaryHelper[];
|
||||||
[]
|
|
||||||
) as GroupScoreSummaryHelper[];
|
|
||||||
|
|
||||||
const result = resultHelper.map(({ score, label, sessions }) => {
|
const result = resultHelper.map(({score, label, sessions}) => {
|
||||||
const finalLabel = showLevel ? getLevelScore(score[0])[1] : label;
|
const finalLabel = showLevel ? getLevelScore(score[0])[1] : label;
|
||||||
return {
|
return {
|
||||||
label: finalLabel,
|
label: finalLabel,
|
||||||
percent: Math.floor((sessions.length / numberOfStudents) * 100),
|
percent: Math.floor((sessions.length / numberOfStudents) * 100),
|
||||||
description: `No. Candidates ${sessions.length} of ${numberOfStudents}`,
|
description: `No. Candidates ${sessions.length} of ${numberOfStudents}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInstitution = async () => {
|
const getInstitution = async () => {
|
||||||
try {
|
try {
|
||||||
// due to database inconsistencies, I'll be overprotective here
|
// due to database inconsistencies, I'll be overprotective here
|
||||||
const assignerUserSnap = await getDoc(
|
const assignerUserSnap = await getDoc(doc(db, "users", data.assigner));
|
||||||
doc(db, "users", data.assigner)
|
if (assignerUserSnap.exists()) {
|
||||||
);
|
// we'll need the user in order to get the user data (name, email, focus, etc);
|
||||||
if (assignerUserSnap.exists()) {
|
const assignerUser = assignerUserSnap.data() as User;
|
||||||
// we'll need the user in order to get the user data (name, email, focus, etc);
|
|
||||||
const assignerUser = assignerUserSnap.data() as User;
|
|
||||||
|
|
||||||
if (assignerUser.type === "teacher") {
|
if (assignerUser.type === "teacher") {
|
||||||
// also search for groups where this user belongs
|
// also search for groups where this user belongs
|
||||||
const queryGroups = query(
|
const queryGroups = query(collection(db, "groups"), where("participants", "array-contains", assignerUser.id));
|
||||||
collection(db, "groups"),
|
const groupSnapshot = await getDocs(queryGroups);
|
||||||
where("participants", "array-contains", assignerUser.id)
|
|
||||||
);
|
|
||||||
const groupSnapshot = await getDocs(queryGroups);
|
|
||||||
|
|
||||||
const groups = groupSnapshot.docs.map((doc) => ({
|
const groups = groupSnapshot.docs.map((doc) => ({
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
...doc.data(),
|
...doc.data(),
|
||||||
})) as Group[];
|
})) as Group[];
|
||||||
|
|
||||||
if (groups.length > 0) {
|
if (groups.length > 0) {
|
||||||
const adminQuery = query(
|
const adminQuery = query(
|
||||||
collection(db, "users"),
|
collection(db, "users"),
|
||||||
where(
|
where(
|
||||||
documentId(),
|
documentId(),
|
||||||
"in",
|
"in",
|
||||||
groups.map((g) => g.admin)
|
groups.map((g) => g.admin),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const adminUsersSnap = await getDocs(adminQuery);
|
const adminUsersSnap = await getDocs(adminQuery);
|
||||||
|
|
||||||
const admins = adminUsersSnap.docs.map((doc) => ({
|
const admins = adminUsersSnap.docs.map((doc) => ({
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
...doc.data(),
|
...doc.data(),
|
||||||
})) as CorporateUser[];
|
})) as CorporateUser[];
|
||||||
|
|
||||||
const adminData = admins.find(
|
const adminData = admins.find((a) => a.corporateInformation?.companyInformation?.name);
|
||||||
(a) => a.corporateInformation?.companyInformation?.name
|
if (adminData) {
|
||||||
);
|
return adminData.corporateInformation.companyInformation.name;
|
||||||
if (adminData) {
|
}
|
||||||
return adminData.corporateInformation.companyInformation
|
}
|
||||||
.name;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (assignerUser.type === "corporate" && assignerUser.corporateInformation?.companyInformation?.name) {
|
||||||
assignerUser.type === "corporate" &&
|
return assignerUser.corporateInformation.companyInformation.name;
|
||||||
assignerUser.corporateInformation?.companyInformation?.name
|
}
|
||||||
) {
|
}
|
||||||
return assignerUser.corporateInformation.companyInformation
|
} catch (err) {
|
||||||
.name;
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
return "";
|
||||||
} catch (err) {
|
};
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const institution = await getInstitution();
|
const institution = await getInstitution();
|
||||||
const groupScoreSummary = getGroupScoreSummary();
|
const groupScoreSummary = getGroupScoreSummary();
|
||||||
const demographicInformation =
|
const demographicInformation = user.demographicInformation as DemographicInformation;
|
||||||
user.demographicInformation as DemographicInformation;
|
const pdfStream = await ReactPDF.renderToStream(
|
||||||
const pdfStream = await ReactPDF.renderToStream(
|
<GroupTestReport
|
||||||
<GroupTestReport
|
title={title}
|
||||||
title={title}
|
date={moment(data.startDate)
|
||||||
date={new Date(data.startDate).toLocaleString()}
|
.tz(user.demographicInformation?.timezone || "UTC")
|
||||||
name={user.name}
|
.format("ll HH:mm:ss")}
|
||||||
email={user.email}
|
name={user.name}
|
||||||
id={user.id}
|
email={user.email}
|
||||||
gender={demographicInformation?.gender}
|
id={user.id}
|
||||||
summary={performanceSummary}
|
gender={demographicInformation?.gender}
|
||||||
renderDetails={details}
|
summary={performanceSummary}
|
||||||
logo={"public/logo_title.png"}
|
renderDetails={details}
|
||||||
qrcode={qrcode}
|
logo={"public/logo_title.png"}
|
||||||
numberOfStudents={numberOfStudents}
|
qrcode={qrcode}
|
||||||
institution={institution}
|
numberOfStudents={numberOfStudents}
|
||||||
studentsData={studentsData}
|
institution={institution}
|
||||||
showLevel={showLevel}
|
studentsData={studentsData}
|
||||||
summaryPNG={overallPNG}
|
showLevel={showLevel}
|
||||||
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
|
summaryPNG={overallPNG}
|
||||||
groupScoreSummary={groupScoreSummary}
|
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
|
||||||
passportId={demographicInformation?.passport_id || ""}
|
groupScoreSummary={groupScoreSummary}
|
||||||
/>
|
passportId={demographicInformation?.passport_id || ""}
|
||||||
);
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
// generate the file ref for storage
|
// generate the file ref for storage
|
||||||
const fileName = `${Date.now().toString()}.pdf`;
|
const fileName = `${Date.now().toString()}.pdf`;
|
||||||
const refName = `assignment_report/${fileName}`;
|
const refName = `assignment_report/${fileName}`;
|
||||||
const fileRef = ref(storage, refName);
|
const fileRef = ref(storage, refName);
|
||||||
|
|
||||||
// upload the pdf to storage
|
// upload the pdf to storage
|
||||||
const pdfBuffer = await streamToBuffer(pdfStream);
|
const pdfBuffer = await streamToBuffer(pdfStream);
|
||||||
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
|
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
|
||||||
contentType: "application/pdf",
|
contentType: "application/pdf",
|
||||||
});
|
});
|
||||||
|
|
||||||
// update the stats entries with the pdf url to prevent duplication
|
// update the stats entries with the pdf url to prevent duplication
|
||||||
await updateDoc(docSnap.ref, {
|
await updateDoc(docSnap.ref, {
|
||||||
pdf: refName,
|
pdf: refName,
|
||||||
});
|
});
|
||||||
const url = await getDownloadURL(fileRef);
|
const url = await getDownloadURL(fileRef);
|
||||||
res.status(200).end(url);
|
res.status(200).end(url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(401).json({ ok: false });
|
res.status(401).json({ok: false});
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ ok: false });
|
res.status(500).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.session.user) {
|
if (req.session.user) {
|
||||||
const { id } = req.query as { id: string };
|
const {id} = req.query as {id: string};
|
||||||
|
|
||||||
const docSnap = await getDoc(doc(db, "assignments", id));
|
const docSnap = await getDoc(doc(db, "assignments", id));
|
||||||
const data = docSnap.data();
|
const data = docSnap.data();
|
||||||
if (!data) {
|
if (!data) {
|
||||||
res.status(400).end();
|
res.status(400).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.assigner !== req.session.user.id) {
|
if (data.assigner !== req.session.user.id) {
|
||||||
res.status(401).json({ ok: false });
|
res.status(401).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.pdf) {
|
if (data.pdf) {
|
||||||
const fileRef = ref(storage, data.pdf);
|
const fileRef = ref(storage, data.pdf);
|
||||||
const url = await getDownloadURL(fileRef);
|
const url = await getDownloadURL(fileRef);
|
||||||
return res.redirect(url);
|
return res.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(404).end();
|
res.status(404).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(401).json({ ok: false });
|
res.status(401).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
getRadialProgressPNG,
|
getRadialProgressPNG,
|
||||||
streamToBuffer,
|
streamToBuffer,
|
||||||
} from "@/utils/pdf";
|
} from "@/utils/pdf";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -308,7 +309,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const pdfStream = await ReactPDF.renderToStream(
|
const pdfStream = await ReactPDF.renderToStream(
|
||||||
<TestReport
|
<TestReport
|
||||||
title={title}
|
title={title}
|
||||||
date={new Date(stat.date).toLocaleString()}
|
date={moment(stat.date).tz(user.demographicInformation?.timezone || 'UTC').format('ll HH:mm:ss')}
|
||||||
name={user.name}
|
name={user.name}
|
||||||
email={user.email}
|
email={user.email}
|
||||||
id={user.id}
|
id={user.id}
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ import Button from "@/components/Low/Button";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {ErrorMessage} from "@/constants/errors";
|
import {ErrorMessage} from "@/constants/errors";
|
||||||
import {RadioGroup} from "@headlessui/react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {CorporateUser, EmploymentStatus, EMPLOYMENT_STATUS, Gender, User} from "@/interfaces/user";
|
import {CorporateUser, EmploymentStatus, EMPLOYMENT_STATUS, Gender, User} from "@/interfaces/user";
|
||||||
import CountrySelect from "@/components/Low/CountrySelect";
|
import CountrySelect from "@/components/Low/CountrySelect";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {BsCamera, BsCameraFill} from "react-icons/bs";
|
import {BsCamera} from "react-icons/bs";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
@@ -25,7 +24,7 @@ import {convertBase64} from "@/utils";
|
|||||||
import {Divider} from "primereact/divider";
|
import {Divider} from "primereact/divider";
|
||||||
import GenderInput from "@/components/High/GenderInput";
|
import GenderInput from "@/components/High/GenderInput";
|
||||||
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
|
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
|
||||||
|
import TimezoneSelect from "@/components/Low/TImezoneSelect";
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
const [commercialRegistration, setCommercialRegistration] = useState<string | undefined>(
|
const [commercialRegistration, setCommercialRegistration] = useState<string | undefined>(
|
||||||
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
|
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
|
||||||
);
|
);
|
||||||
|
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || "UTC");
|
||||||
const {groups} = useGroups();
|
const {groups} = useGroups();
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
|
|
||||||
@@ -146,6 +145,7 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
position: user?.type === "corporate" ? position : undefined,
|
position: user?.type === "corporate" ? position : undefined,
|
||||||
gender,
|
gender,
|
||||||
passport_id,
|
passport_id,
|
||||||
|
timezone,
|
||||||
},
|
},
|
||||||
...(user.type === "corporate" ? {corporateInformation} : {}),
|
...(user.type === "corporate" ? {corporateInformation} : {}),
|
||||||
});
|
});
|
||||||
@@ -247,6 +247,13 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const TimezoneInput = () => (
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Timezone</label>
|
||||||
|
<TimezoneSelect value={timezone} onChange={setTimezone} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<section className="w-full flex flex-col gap-4 md:gap-8 px-4 py-8">
|
<section className="w-full flex flex-col gap-4 md:gap-8 px-4 py-8">
|
||||||
@@ -286,18 +293,6 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
/>
|
/>
|
||||||
</DoubleColumnRow>
|
</DoubleColumnRow>
|
||||||
<PasswordInput />
|
<PasswordInput />
|
||||||
|
|
||||||
{user.type === "student" && (
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
name="passport_id"
|
|
||||||
label="Passport/National ID"
|
|
||||||
onChange={(e) => setPassportID(e)}
|
|
||||||
placeholder="Enter National ID or Passport number"
|
|
||||||
value={passport_id}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{user.type === "agent" && <AgentInformationInput />}
|
{user.type === "agent" && <AgentInformationInput />}
|
||||||
|
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
@@ -305,6 +300,23 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
<PhoneInput />
|
<PhoneInput />
|
||||||
</DoubleColumnRow>
|
</DoubleColumnRow>
|
||||||
|
|
||||||
|
{user.type === "student" ? (
|
||||||
|
<DoubleColumnRow>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="passport_id"
|
||||||
|
label="Passport/National ID"
|
||||||
|
onChange={(e) => setPassportID(e)}
|
||||||
|
placeholder="Enter National ID or Passport number"
|
||||||
|
value={passport_id}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TimezoneInput />
|
||||||
|
</DoubleColumnRow>
|
||||||
|
) : (
|
||||||
|
<TimezoneInput />
|
||||||
|
)}
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{user.type === "corporate" && (
|
{user.type === "corporate" && (
|
||||||
|
|||||||
@@ -4360,6 +4360,13 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||||
|
|
||||||
|
moment-timezone@^0.5.44:
|
||||||
|
version "0.5.44"
|
||||||
|
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.44.tgz#a64a4e47b68a43deeab5ae4eb4f82da77cdf595f"
|
||||||
|
integrity sha512-nv3YpzI/8lkQn0U6RkLd+f0W/zy/JnoR5/EyPz/dNkPTBjA2jNLCVxaiQ8QpeLymhSZvX0wCL5s27NQWdOPwAw==
|
||||||
|
dependencies:
|
||||||
|
moment "^2.29.4"
|
||||||
|
|
||||||
moment@^2.29.4:
|
moment@^2.29.4:
|
||||||
version "2.29.4"
|
version "2.29.4"
|
||||||
resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz"
|
resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user