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
|
<span
|
||||||
|
key={word}
|
||||||
data-tip={correction ? correction : undefined}
|
data-tip={correction ? correction : undefined}
|
||||||
className={clsx("text-mti-red-light font-medium underline underline-offset-2", correction && "tooltip")}>
|
className={clsx("text-mti-red-light font-medium underline underline-offset-2", correction && "tooltip")}>
|
||||||
{match}
|
{word}
|
||||||
</span>
|
</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,16 +1,6 @@
|
|||||||
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,
|
|
||||||
doc,
|
|
||||||
getDoc,
|
|
||||||
updateDoc,
|
|
||||||
getDocs,
|
|
||||||
query,
|
|
||||||
collection,
|
|
||||||
where,
|
|
||||||
documentId,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import ReactPDF from "@react-pdf/renderer";
|
import ReactPDF from "@react-pdf/renderer";
|
||||||
@@ -23,12 +13,9 @@ 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,
|
|
||||||
getRadialProgressPNG,
|
|
||||||
streamToBuffer,
|
|
||||||
} from "@/utils/pdf";
|
|
||||||
import {Group} from "@/interfaces/user";
|
import {Group} from "@/interfaces/user";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
|
||||||
interface GroupScoreSummaryHelper {
|
interface GroupScoreSummaryHelper {
|
||||||
score: [number, number];
|
score: [number, number];
|
||||||
@@ -98,7 +85,7 @@ const getScoreAndTotal = (stats: Stat[]) => {
|
|||||||
total: acc.total + score.total,
|
total: acc.total + score.total,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ correct: 0, total: 0 }
|
{correct: 0, total: 0},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,29 +133,19 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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(
|
|
||||||
collection(db, "users"),
|
|
||||||
where(documentId(), "in", data.assignees)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const users = docsSnap.docs.map((d) => ({
|
const users = docsSnap.docs.map((d) => ({
|
||||||
...d.data(),
|
...d.data(),
|
||||||
id: d.id,
|
id: d.id,
|
||||||
@@ -176,24 +153,15 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
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) /
|
|
||||||
moduleResults.length;
|
|
||||||
const bandScore = isNaN(baseBandScore) ? 0 : baseBandScore;
|
const bandScore = isNaN(baseBandScore) ? 0 : baseBandScore;
|
||||||
const {correct, total} = getScoreAndTotal(moduleResults);
|
const {correct, total} = getScoreAndTotal(moduleResults);
|
||||||
const png = getRadialProgressPNG("azul", correct, total);
|
const png = getRadialProgressPNG("azul", correct, total);
|
||||||
@@ -208,16 +176,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
};
|
};
|
||||||
}) 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",
|
|
||||||
overallCorrect,
|
|
||||||
overallTotal
|
|
||||||
);
|
|
||||||
// generate the overall detail report
|
// generate the overall detail report
|
||||||
const overallDetail = {
|
const overallDetail = {
|
||||||
module: "Overall",
|
module: "Overall",
|
||||||
@@ -234,7 +197,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
// 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";
|
||||||
@@ -244,12 +207,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,11 +234,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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
|
|
||||||
? 0
|
|
||||||
: exams.reduce((accm, curr) => accm + curr.bandScore, 0) /
|
|
||||||
exams.length;
|
|
||||||
const {correct, total} = getScoreAndTotal(exams);
|
const {correct, total} = getScoreAndTotal(exams);
|
||||||
|
|
||||||
const result = exams.length === 0 ? "N/A" : `${correct}/${total}`;
|
const result = exams.length === 0 ? "N/A" : `${correct}/${total}`;
|
||||||
@@ -292,9 +246,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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)
|
|
||||||
: undefined,
|
|
||||||
bandScore,
|
bandScore,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -303,8 +255,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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);
|
||||||
@@ -331,9 +282,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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;
|
||||||
@@ -349,19 +298,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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()) {
|
if (assignerUserSnap.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 assignerUser = assignerUserSnap.data() as User;
|
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"),
|
|
||||||
where("participants", "array-contains", assignerUser.id)
|
|
||||||
);
|
|
||||||
const groupSnapshot = await getDocs(queryGroups);
|
const groupSnapshot = await getDocs(queryGroups);
|
||||||
|
|
||||||
const groups = groupSnapshot.docs.map((doc) => ({
|
const groups = groupSnapshot.docs.map((doc) => ({
|
||||||
@@ -375,8 +319,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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);
|
||||||
|
|
||||||
@@ -385,22 +329,15 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
...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) {
|
if (adminData) {
|
||||||
return adminData.corporateInformation.companyInformation
|
return adminData.corporateInformation.companyInformation.name;
|
||||||
.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
|
|
||||||
.name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -411,12 +348,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
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={new Date(data.startDate).toLocaleString()}
|
date={moment(data.startDate)
|
||||||
|
.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}
|
||||||
@@ -433,7 +371,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
|
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
|
||||||
groupScoreSummary={groupScoreSummary}
|
groupScoreSummary={groupScoreSummary}
|
||||||
passportId={demographicInformation?.passport_id || ""}
|
passportId={demographicInformation?.passport_id || ""}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// generate the file ref for storage
|
// generate the file ref for storage
|
||||||
|
|||||||
@@ -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,8 +293,15 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
/>
|
/>
|
||||||
</DoubleColumnRow>
|
</DoubleColumnRow>
|
||||||
<PasswordInput />
|
<PasswordInput />
|
||||||
|
{user.type === "agent" && <AgentInformationInput />}
|
||||||
|
|
||||||
{user.type === "student" && (
|
<DoubleColumnRow>
|
||||||
|
<CountryInput />
|
||||||
|
<PhoneInput />
|
||||||
|
</DoubleColumnRow>
|
||||||
|
|
||||||
|
{user.type === "student" ? (
|
||||||
|
<DoubleColumnRow>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
name="passport_id"
|
name="passport_id"
|
||||||
@@ -297,13 +311,11 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
value={passport_id}
|
value={passport_id}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
)}
|
<TimezoneInput />
|
||||||
{user.type === "agent" && <AgentInformationInput />}
|
|
||||||
|
|
||||||
<DoubleColumnRow>
|
|
||||||
<CountryInput />
|
|
||||||
<PhoneInput />
|
|
||||||
</DoubleColumnRow>
|
</DoubleColumnRow>
|
||||||
|
) : (
|
||||||
|
<TimezoneInput />
|
||||||
|
)}
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
|||||||
@@ -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