+
setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
+ Full length exams
+
setGenerateMultiple((d) => !d)}>
Generate different exams
diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts
index b8e12632..2f3e2128 100644
--- a/src/interfaces/exam.ts
+++ b/src/interfaces/exam.ts
@@ -1,7 +1,7 @@
import {Module} from ".";
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
-export type Variant = "full" | "diagnostic" | "partial";
+export type Variant = "full" | "partial";
export interface ReadingExam {
parts: ReadingPart[];
diff --git a/src/pages/(admin)/BatchCodeGenerator.tsx b/src/pages/(admin)/BatchCodeGenerator.tsx
index 4eb1ece0..39bb132c 100644
--- a/src/pages/(admin)/BatchCodeGenerator.tsx
+++ b/src/pages/(admin)/BatchCodeGenerator.tsx
@@ -58,45 +58,59 @@ export default function BatchCodeGenerator({user}: {user: User}) {
if (filesContent.length > 0) {
const file = filesContent[0];
readXlsxFile(file.content).then((rows) => {
- const information = uniqBy(
- rows
- .map((row) => {
- const [firstName, lastName, country, passport_id, email, phone] = row as string[];
- return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email)
- ? {
- email: email.toString(),
- name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
- passport_id: passport_id.toString(),
- }
- : undefined;
- })
- .filter((x) => !!x) as typeof infos,
- (x) => x.email,
- );
+ try {
+ const information = uniqBy(
+ rows
+ .map((row) => {
+ const [firstName, lastName, country, passport_id, email, ...phone] = row as string[];
+ return EMAIL_REGEX.test(email.toString().trim()) && !users.map((u) => u.email).includes(email.toString().trim())
+ ? {
+ email: email.toString().trim(),
+ name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
+ passport_id: passport_id?.toString().trim() || undefined,
+ }
+ : undefined;
+ })
+ .filter((x) => !!x) as typeof infos,
+ (x) => x.email,
+ );
- if (information.length === 0) {
+ if (information.length === 0) {
+ toast.error(
+ "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!",
+ );
+ return clear();
+ }
+
+ setInfos(information);
+ } catch {
toast.error(
"Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!",
);
return clear();
}
-
- setInfos(information);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filesContent]);
const generateCode = (type: Type) => {
+ if (!confirm(`You are about to generate ${infos.length} codes, are you sure you want to continue?`)) return;
+
const uid = new ShortUniqueId();
const codes = infos.map(() => uid.randomUUID(6));
setIsLoading(true);
axios
- .post("/api/code", {type, codes, infos: infos, expiryDate})
+ .post<{ok: boolean; valid?: number; reason?: string}>("/api/code", {type, codes, infos: infos, expiryDate})
.then(({data, status}) => {
if (data.ok) {
- toast.success(`Successfully generated ${capitalize(type)} codes and they have been notified by e-mail!`, {toastId: "success"});
+ toast.success(
+ `Successfully generated${data.valid ? ` ${data.valid}/${infos.length}` : ""} ${capitalize(
+ type,
+ )} codes and they have been notified by e-mail!`,
+ {toastId: "success"},
+ );
return;
}
@@ -112,7 +126,10 @@ export default function BatchCodeGenerator({user}: {user: User}) {
toast.error(`Something went wrong, please try again later!`, {toastId: "error"});
})
- .finally(() => setIsLoading(false));
+ .finally(() => {
+ setIsLoading(false);
+ return clear();
+ });
};
return (
diff --git a/src/pages/api/assignments/index.ts b/src/pages/api/assignments/index.ts
index a34228d1..5d454fe1 100644
--- a/src/pages/api/assignments/index.ts
+++ b/src/pages/api/assignments/index.ts
@@ -7,7 +7,7 @@ import {sessionOptions} from "@/lib/session";
import {uuidv4} from "@firebase/util";
import {Module} from "@/interfaces";
import {getExams} from "@/utils/exams.be";
-import {Exam} from "@/interfaces/exam";
+import {Exam, Variant} from "@/interfaces/exam";
import {capitalize, flatten} from "lodash";
import {User} from "@/interfaces/user";
import moment from "moment";
@@ -52,13 +52,18 @@ function getRandomIndex(arr: any[]): number {
return randomIndex;
}
-const generateExams = async (generateMultiple: Boolean, selectedModules: Module[], assignees: string[]): Promise
=> {
+const generateExams = async (
+ generateMultiple: Boolean,
+ selectedModules: Module[],
+ assignees: string[],
+ variant?: Variant,
+): Promise => {
if (generateMultiple) {
// for optimization purposes, it would be better to create a new endpoint that returned the answers for all users at once
const allExams = await assignees.map(async (assignee) => {
const selectedModulePromises = await selectedModules.map(async (module: Module) => {
try {
- const exams: Exam[] = await getExams(db, module, "true", assignee);
+ const exams: Exam[] = await getExams(db, module, "true", assignee, variant);
const exam = exams[getRandomIndex(exams)];
if (exam) {
@@ -101,6 +106,7 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
// Generate multiple true would generate an unique exam for each user
// false would generate the same exam for all users
generateMultiple = false,
+ variant,
...body
} = req.body as {
selectedModules: Module[];
@@ -109,9 +115,10 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
name: string;
startDate: string;
endDate: string;
+ variant?: Variant;
};
- const exams: ExamWithUser[] = await generateExams(generateMultiple, selectedModules, assignees);
+ const exams: ExamWithUser[] = await generateExams(generateMultiple, selectedModules, assignees, variant);
if (exams.length === 0) {
res.status(400).json({ok: false, error: "No exams found for the selected modules"});
diff --git a/src/pages/api/code/index.ts b/src/pages/api/code/index.ts
index 39787f11..d855e1f8 100644
--- a/src/pages/api/code/index.ts
+++ b/src/pages/api/code/index.ts
@@ -42,7 +42,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const {type, codes, infos, expiryDate} = req.body as {
type: Type;
codes: string[];
- infos?: {email: string; name: string; passport_id: string}[];
+ infos?: {email: string; name: string; passport_id?: string}[];
expiryDate: null | Date;
};
const permission = PERMISSIONS.generateCode[type];
@@ -70,11 +70,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const codePromises = codes.map(async (code, index) => {
const codeRef = doc(db, "codes", code);
- await setDoc(codeRef, {type, code, creator: req.session.user!.id, expiryDate});
+ const codeInformation = {type, code, creator: req.session.user!.id, expiryDate};
if (infos && infos.length > index) {
const {email, name, passport_id} = infos[index];
- await setDoc(codeRef, {email: email.trim(), name: name.trim(), passport_id: passport_id.trim()}, {merge: true});
const transport = prepareMailer();
const mailOptions = prepareMailOptions(
@@ -87,11 +86,24 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
"main",
);
- await transport.sendMail(mailOptions);
+ try {
+ await transport.sendMail(mailOptions);
+ await setDoc(
+ codeRef,
+ {...codeInformation, email: email.trim(), name: name.trim(), ...(passport_id ? {passport_id: passport_id.trim()} : {})},
+ {merge: true},
+ );
+
+ return true;
+ } catch (e) {
+ return false;
+ }
+ } else {
+ await setDoc(codeRef, codeInformation);
}
});
- Promise.all(codePromises).then(() => {
- res.status(200).json({ok: true});
+ Promise.all(codePromises).then((results) => {
+ res.status(200).json({ok: true, valid: results.filter((x) => x).length});
});
}
diff --git a/src/pages/api/stats/[id].ts b/src/pages/api/stats/[id].ts
deleted file mode 100644
index 0597f529..00000000
--- a/src/pages/api/stats/[id].ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
-import type {NextApiRequest, NextApiResponse} from "next";
-import {app} from "@/firebase";
-import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc, deleteDoc} from "firebase/firestore";
-import {withIronSessionApiRoute} from "iron-session/next";
-import {sessionOptions} from "@/lib/session";
-import {uuidv4} from "@firebase/util";
-
-const db = getFirestore(app);
-
-export default async function handler(req: NextApiRequest, res: NextApiResponse) {
- if (req.method === "GET") return GET(req, res);
-
- res.status(404).json({ok: false});
-}
-
-async function GET(req: NextApiRequest, res: NextApiResponse) {
- const {id} = req.query;
-
- const snapshot = await getDoc(doc(db, "stats", id as string));
-
- res.status(200).json({...snapshot.data(), id: snapshot.id});
-}
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 79322a97..02ff0a8e 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -30,6 +30,9 @@ import AgentDashboard from "@/dashboards/Agent";
import PaymentDue from "./(status)/PaymentDue";
import {useRouter} from "next/router";
import {PayPalScriptProvider} from "@paypal/react-paypal-js";
+import {CorporateUser, Type, userTypes} from "@/interfaces/user";
+import Select from "react-select";
+import {USER_TYPE_LABELS} from "@/resources/user";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
@@ -61,8 +64,9 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
export default function Home({envVariables}: {envVariables: {[key: string]: string}}) {
const [showDiagnostics, setShowDiagnostics] = useState(false);
const [showDemographicInput, setShowDemographicInput] = useState(false);
+ const [selectedScreen, setSelectedScreen] = useState("admin");
+
const {user, mutateUser} = useUser({redirectTo: "/login"});
- const {stats} = useStats(user?.id);
const router = useRouter();
useEffect(() => {
@@ -176,7 +180,21 @@ export default function Home({envVariables}: {envVariables: {[key: string]: stri
{user.type === "corporate" && }
{user.type === "agent" && }
{user.type === "admin" && }
- {user.type === "developer" && }
+ {user.type === "developer" && (
+ <>
+