diff --git a/src/components/Diagnostic.tsx b/src/components/Diagnostic.tsx
index 943d90e7..a221f1d2 100644
--- a/src/components/Diagnostic.tsx
+++ b/src/components/Diagnostic.tsx
@@ -22,8 +22,8 @@ interface Props {
export default function Diagnostic({onFinish}: Props) {
const [focus, setFocus] = useState<"academic" | "general">();
- const [levels, setLevels] = useState({reading: -1, listening: -1, writing: -1, speaking: -1});
- const [desiredLevels, setDesiredLevels] = useState({reading: 9, listening: 9, writing: 9, speaking: 9});
+ const [levels, setLevels] = useState({reading: -1, listening: -1, writing: -1, speaking: -1, level: 0});
+ const [desiredLevels, setDesiredLevels] = useState({reading: 9, listening: 9, writing: 9, speaking: 9, level: 9});
const router = useRouter();
@@ -51,7 +51,7 @@ export default function Diagnostic({onFinish}: Props) {
axios
.patch("/api/users/update", {
focus,
- levels: Object.values(levels).includes(-1) ? {reading: 0, listening: 0, writing: 0, speaking: 0} : levels,
+ levels: Object.values(levels).includes(-1) ? {reading: 0, listening: 0, writing: 0, speaking: 0, level: 0} : levels,
desiredLevels,
isFirstLogin: false,
})
diff --git a/src/components/Exercises/TrueFalse.tsx b/src/components/Exercises/TrueFalse.tsx
index 19330684..4c7ffae9 100644
--- a/src/components/Exercises/TrueFalse.tsx
+++ b/src/components/Exercises/TrueFalse.tsx
@@ -17,7 +17,7 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
const calculateScore = () => {
const total = questions.length || 0;
const correct = answers.filter(
- (x) => questions.find((y) => x.id.toString() === y.id.toString())?.solution === x.solution.toLowerCase() || false,
+ (x) => questions.find((y) => x.id.toString() === y.id.toString())?.solution?.toLowerCase() === x.solution.toLowerCase() || false,
).length;
const missing = total - answers.filter((x) => questions.find((y) => x.id.toString() === y.id.toString())).length;
@@ -62,41 +62,37 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
You can click a selected option again to deselect it.
- {questions.map((question, index) => (
-
-
- {index + 1}. {question.prompt}
-
-
-
-
-
+ {questions.map((question, index) => {
+ const id = question.id.toString();
+
+ return (
+
+
+ {index + 1}. {question.prompt}
+
+
+
+
+
+
-
- ))}
+ );
+ })}
diff --git a/src/components/Exercises/Writing.tsx b/src/components/Exercises/Writing.tsx
index 0edf3689..5560c2b7 100644
--- a/src/components/Exercises/Writing.tsx
+++ b/src/components/Exercises/Writing.tsx
@@ -26,6 +26,8 @@ export default function Writing({
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
useEffect(() => {
+ if (localStorage.getItem("enable_paste")) return;
+
const listener = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
e.preventDefault();
@@ -93,8 +95,8 @@ export default function Writing({
)}
-
{prefix}
-
{prompt}
+
{prefix.replaceAll("\\n", "\n")}
+
{prompt.replaceAll("\\n", "\n")}
{attachment && (
![]()
setIsModalOpen(true)}
diff --git a/src/components/Low/Button.tsx b/src/components/Low/Button.tsx
index 29d529e2..9bd207f0 100644
--- a/src/components/Low/Button.tsx
+++ b/src/components/Low/Button.tsx
@@ -4,7 +4,7 @@ import {BsArrowRepeat} from "react-icons/bs";
interface Props {
children: ReactNode;
- color?: "rose" | "purple" | "red" | "green";
+ color?: "rose" | "purple" | "red" | "green" | "gray";
variant?: "outline" | "solid";
className?: string;
disabled?: boolean;
@@ -39,6 +39,11 @@ export default function Button({
outline:
"bg-transparent text-mti-red-light border border-mti-red-light hover:bg-mti-red-light disabled:text-mti-red disabled:bg-mti-red-ultralight disabled:border-none selection:bg-mti-red-dark hover:text-white selection:text-white",
},
+ gray: {
+ solid: "bg-mti-gray-davy text-white border border-mti-gray-davy hover:bg-mti-gray-davy disabled:text-mti-gray-davy disabled:bg-mti-gray-davy selection:bg-mti-gray-davy",
+ outline:
+ "bg-transparent text-mti-gray-davy border border-mti-gray-davy hover:bg-mti-gray-davy disabled:text-mti-gray-davy disabled:bg-mti-gray-davy disabled:border-none selection:bg-mti-gray-davy hover:text-white selection:text-white",
+ },
rose: {
solid: "bg-mti-rose-light text-white border border-mti-rose-light hover:bg-mti-rose disabled:text-mti-rose disabled:bg-mti-rose-ultralight selection:bg-mti-rose-dark",
outline:
diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx
index 0e042287..dd31e85e 100644
--- a/src/components/MobileMenu.tsx
+++ b/src/components/MobileMenu.tsx
@@ -103,7 +103,7 @@ export default function MobileMenu({isOpen, onClose, path, user}: Props) {
)}>
Record
- {["admin", "developer", "agent"].includes(user.type) && (
+ {["admin", "developer", "agent", "corporate"].includes(user.type) && (
void;
+ paymentId: string;
}) => {
- const { asset, permissions, type, paymentId } = props;
+ const {asset, permissions, type, paymentId} = props;
- const fileInputRef = React.useRef
(null);
- const fileInputReplaceRef = React.useRef(null);
+ const fileInputRef = React.useRef(null);
+ const fileInputReplaceRef = React.useRef(null);
- const [managingAsset, setManagingAsset] = React.useState({
- file: asset || "",
- complete: asset ? true : false,
- });
+ const [managingAsset, setManagingAsset] = React.useState({
+ file: asset || "",
+ complete: asset ? true : false,
+ });
- const { file, complete } = managingAsset;
+ const {file, complete} = managingAsset;
- const deleteAsset = () => {
- if (confirm("Are you sure you want to delete this document?")) {
- axios
- .delete(`/api/payments/files/${type}/${paymentId}`)
- .then((response) => {
- if (response.status === 200) {
- console.log("File deleted successfully!");
- setManagingAsset({
- file: "",
- complete: false,
- });
- return;
- }
+ const deleteAsset = () => {
+ if (confirm("Are you sure you want to delete this document?")) {
+ axios
+ .delete(`/api/payments/files/${type}/${paymentId}`)
+ .then((response) => {
+ if (response.status === 200) {
+ console.log("File deleted successfully!");
+ setManagingAsset({
+ file: "",
+ complete: false,
+ });
+ return;
+ }
- console.error("File deletion failed");
- })
- .catch((error) => {
- console.error("Error occurred during file deletion:", error);
- });
- }
- };
+ console.error("File deletion failed");
+ })
+ .catch((error) => {
+ console.error("Error occurred during file deletion:", error);
+ })
+ .finally(props.reload);
+ }
+ };
- const renderFileInput = (
- onChange: any,
- ref: React.RefObject
- ) => (
-
- );
+ const renderFileInput = (onChange: any, ref: React.RefObject) => (
+
+ );
- const handleFileChange = async (e: Event, method: "post" | "patch") => {
- const newFile = (e.target as HTMLInputElement).files?.[0];
- if (newFile) {
- setManagingAsset({
- file: newFile,
- complete: false,
- });
+ const handleFileChange = async (e: Event, method: "post" | "patch") => {
+ const newFile = (e.target as HTMLInputElement).files?.[0];
+ if (newFile) {
+ setManagingAsset({
+ file: newFile,
+ complete: false,
+ });
- const formData = new FormData();
- formData.append("file", newFile);
+ const formData = new FormData();
+ formData.append("file", newFile);
- axios[method](`/api/payments/files/${type}/${paymentId}`, formData, {
- headers: {
- "Content-Type": "multipart/form-data",
- },
- })
- .then((response) => {
- if (response.status === 200) {
- console.log("File uploaded successfully!");
- console.log("Uploaded File URL:", response.data.ref);
- // Further actions upon successful upload
- setManagingAsset({
- file: response.data.ref,
- complete: true,
- });
- return;
- }
+ axios[method](`/api/payments/files/${type}/${paymentId}`, formData, {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ })
+ .then((response) => {
+ if (response.status === 200) {
+ console.log("File uploaded successfully!");
+ console.log("Uploaded File URL:", response.data.ref);
+ // Further actions upon successful upload
+ setManagingAsset({
+ file: response.data.ref,
+ complete: true,
+ });
+ return;
+ }
- console.error("File upload failed");
- })
- .catch((error) => {
- console.error("Error occurred during file upload:", error);
- });
- }
- };
+ console.error("File upload failed");
+ })
+ .catch((error) => {
+ console.error("Error occurred during file upload:", error);
+ })
+ .finally(props.reload);
+ }
+ };
- const downloadAsset = () => {
- axios
- .get(`/api/payments/files/${type}/${paymentId}`)
- .then((response) => {
- if (response.status === 200) {
- console.log("Uploaded File URL:", response.data.url);
- const link = document.createElement("a");
- link.download = response.data.filename;
- link.href = response.data.url;
- link.click();
- return;
- }
+ const downloadAsset = () => {
+ axios
+ .get(`/api/payments/files/${type}/${paymentId}`)
+ .then((response) => {
+ if (response.status === 200) {
+ console.log("Uploaded File URL:", response.data.url);
+ const link = document.createElement("a");
+ link.download = response.data.filename;
+ link.href = response.data.url;
+ link.click();
+ return;
+ }
- console.error("Failed to download file");
- })
- .catch((error) => {
- console.error("Error occurred during file upload:", error);
- });
- };
+ console.error("Failed to download file");
+ })
+ .catch((error) => {
+ console.error("Error occurred during file upload:", error);
+ });
+ };
- if (permissions === "read") {
- if (file) return ;
- return null;
- }
+ if (permissions === "read") {
+ if (file) return ;
+ return null;
+ }
- if (file) {
- if (complete) {
- return (
- <>
-
- fileInputReplaceRef.current?.click()} />
-
- {renderFileInput(
- (e: Event) => handleFileChange(e, "patch"),
- fileInputReplaceRef
- )}
- {renderFileInput(
- (e: Event) => handleFileChange(e, "post"),
- fileInputRef
- )}
- >
- );
- }
+ if (file) {
+ if (complete) {
+ return (
+ <>
+
+ fileInputReplaceRef.current?.click()} />
+
+ {renderFileInput((e: Event) => handleFileChange(e, "patch"), fileInputReplaceRef)}
+ {renderFileInput((e: Event) => handleFileChange(e, "post"), fileInputRef)}
+ >
+ );
+ }
- return ;
- }
+ return ;
+ }
- return (
- <>
- fileInputRef.current?.click()} />
- {renderFileInput((e: Event) => handleFileChange(e, "post"), fileInputRef)}
- >
- );
+ return permissions === "write" ? (
+ <>
+ fileInputRef.current?.click()} />
+ {renderFileInput((e: Event) => handleFileChange(e, "post"), fileInputRef)}
+ >
+ ) : (
+
+ );
};
export default PaymentAssetManager;
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index c445cf4b..b9da2b6c 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -99,7 +99,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
)}
- {["admin", "developer", "agent"].includes(userType || "") && (
+ {["admin", "developer", "agent", "corporate"].includes(userType || "") && (
diff --git a/src/components/Solutions/MatchSentences.tsx b/src/components/Solutions/MatchSentences.tsx
index c2537202..27b7dfcc 100644
--- a/src/components/Solutions/MatchSentences.tsx
+++ b/src/components/Solutions/MatchSentences.tsx
@@ -50,7 +50,7 @@ export default function MatchSentencesSolutions({
className={clsx(
"w-8 h-8 rounded-full z-10 text-white",
"transition duration-300 ease-in-out",
- !userSolutions.find((x) => x.question.toString() === id.toString()) && "!bg-mti-red",
+ !userSolutions.find((x) => x.question.toString() === id.toString()) && "!bg-mti-gray-davy",
userSolutions.find((x) => x.question.toString() === id.toString())?.option === solution && "bg-mti-purple",
userSolutions.find((x) => x.question.toString() === id.toString())?.option !== solution && "bg-mti-rose",
)}>
@@ -96,7 +96,7 @@ export default function MatchSentencesSolutions({
Correct
-
Unanswered
+
Unanswered
Wrong
diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx
index 7a81ad16..0dfe1785 100644
--- a/src/components/Solutions/MultipleChoice.tsx
+++ b/src/components/Solutions/MultipleChoice.tsx
@@ -14,7 +14,7 @@ function Question({
}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) {
const optionColor = (option: string) => {
if (option === solution && !userSolution) {
- return "!border-mti-red-light !text-mti-red-light";
+ return "!border-mti-gray-davy !text-mti-gray-davy";
}
if (option === solution) {
@@ -114,7 +114,7 @@ export default function MultipleChoice({
Correct
diff --git a/src/components/Solutions/Speaking.tsx b/src/components/Solutions/Speaking.tsx
index ae5febaa..8447b8e3 100644
--- a/src/components/Solutions/Speaking.tsx
+++ b/src/components/Solutions/Speaking.tsx
@@ -80,7 +80,8 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
))}
- {userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? (
+ {userSolutions[0].evaluation &&
+ (userSolutions[0].evaluation.perfect_answer || userSolutions[0].evaluation.perfect_answer_1) ? (
- {userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
+ {userSolutions[0].evaluation!.perfect_answer &&
+ userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
+ {userSolutions[0].evaluation!.perfect_answer_1 &&
+ userSolutions[0].evaluation!.perfect_answer_1.replaceAll(/\s{2,}/g, "\n\n")}
diff --git a/src/components/Solutions/TrueFalse.tsx b/src/components/Solutions/TrueFalse.tsx
index a80df152..44699bfb 100644
--- a/src/components/Solutions/TrueFalse.tsx
+++ b/src/components/Solutions/TrueFalse.tsx
@@ -33,7 +33,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
return "rose";
}
- return "red";
+ return "gray";
};
return (
@@ -67,6 +67,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
{userSolutions &&
questions.map((question, index) => {
const userSolution = userSolutions.find((x) => x.id === question.id.toString());
+ const solution = question.solution.toString().toLowerCase() as Solution;
return (
@@ -75,23 +76,23 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
@@ -105,7 +106,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
Correct
diff --git a/src/components/Solutions/WriteBlanks.tsx b/src/components/Solutions/WriteBlanks.tsx
index a7ef4f9b..6740a4c5 100644
--- a/src/components/Solutions/WriteBlanks.tsx
+++ b/src/components/Solutions/WriteBlanks.tsx
@@ -38,7 +38,7 @@ function Blank({
const getSolutionStyling = () => {
if (!userSolution) {
- return "bg-mti-red-ultralight text-mti-red-light";
+ return "bg-mti-gray-davy text-mti-gray-davy";
}
return "bg-mti-purple-ultralight text-mti-purple-light";
@@ -131,7 +131,7 @@ export default function WriteBlanksSolutions({
Correct
diff --git a/src/components/Solutions/Writing.tsx b/src/components/Solutions/Writing.tsx
index c6557825..804df4fa 100644
--- a/src/components/Solutions/Writing.tsx
+++ b/src/components/Solutions/Writing.tsx
@@ -6,10 +6,31 @@ import Button from "../Low/Button";
import {Dialog, Tab, Transition} from "@headlessui/react";
import {writingReverseMarking} from "@/utils/score";
import clsx from "clsx";
+import reactStringReplace from "react-string-replace";
export default function Writing({id, type, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
+ const formatSolution = (solution: string, errors: {correction: string | null; misspelled: string}[]) => {
+ const errorRegex = new RegExp(errors.map((x) => `(${x.misspelled})`).join("|"));
+
+ return (
+ <>
+ {reactStringReplace(solution, errorRegex, (match) => {
+ const correction = errors.find((x) => x.misspelled === match)?.correction;
+
+ return (
+
+ {match}
+
+ );
+ })}
+ >
+ );
+ };
+
return (
<>
{attachment && (
@@ -67,12 +88,14 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
{userSolutions && (
Your answer:
-
+
+ {userSolutions[0]!.evaluation && userSolutions[0]!.evaluation.misspelled_pairs
+ ? formatSolution(
+ userSolutions[0]!.solution.replaceAll("\\n", "\n"),
+ userSolutions[0]!.evaluation.misspelled_pairs,
+ )
+ : userSolutions[0]!.solution.replaceAll("\\n", "\n")}
+
)}
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
@@ -116,7 +139,7 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
- {userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
+ {userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n").replaceAll("\\n", "\n")}
diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx
index ec67cff7..369249f7 100644
--- a/src/dashboards/Student.tsx
+++ b/src/dashboards/Student.tsx
@@ -191,11 +191,12 @@ export default function StudentDashboard({user}: Props) {
{module === "listening" &&
}
{module === "writing" &&
}
{module === "speaking" &&
}
+ {module === "level" &&
}
{capitalize(module)}
- Level {user.levels[module]} / Level {user.desiredLevels[module]}
+ Level {user.levels[module] || 0} / Level {user.desiredLevels[module] || 9}
diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx
index 47bc559f..d3c76218 100644
--- a/src/exams/Finish.tsx
+++ b/src/exams/Finish.tsx
@@ -10,8 +10,8 @@ import Link from "next/link";
import {useRouter} from "next/router";
import {Fragment, useEffect, useState} from "react";
import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs";
-import { LevelScore } from "@/constants/ielts";
-import { getLevelScore } from "@/utils/score";
+import {LevelScore} from "@/constants/ielts";
+import {getLevelScore} from "@/utils/score";
interface Score {
module: Module;
@@ -71,20 +71,18 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
const bandScore: number = calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus);
const showLevel = (level: number) => {
- if(selectedModule === "level") {
+ if (selectedModule === "level") {
const [levelStr, grade] = getLevelScore(level);
return (
{levelStr}
{grade}
- )
- }
-
-
- return {level};
+ );
+ }
- }
+ return {level};
+ };
return (
<>
@@ -156,14 +154,16 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
{isLoading && (
- Evaluating your answers...
+
+ Evaluating your answers, please be patient...
+
+ You can also check it later on your records page!
+
)}
{!isLoading && (
-
- {moduleResultText(selectedModule, bandScore)}
-
+
{moduleResultText(selectedModule, bandScore)}
{
+ if(fields.length === 0) return data;
+ const [key, ...otherFields] = fields;
+
+ if(data[key]) return getFieldValue(otherFields, data[key]);
+ return data;
+}
+
+export const useListSearch = (fields: string[][], rows: any[]) => {
+ const [text, setText] = useState('');
+
+ const renderSearch = () => (
+
+ )
+
+ const updatedRows = useMemo(() => {
+ const searchText = text.toLowerCase();
+ return rows.filter((row) => {
+ return fields.some((fieldsKeys) => {
+ const value = getFieldValue(fieldsKeys, row);
+ if(typeof value === 'string') {
+ return value.toLowerCase().includes(searchText);
+ }
+ })
+ })
+ }, [fields, rows, text])
+
+ return {
+ rows: updatedRows,
+ renderSearch,
+ }
+}
\ No newline at end of file
diff --git a/src/hooks/useStats.tsx b/src/hooks/useStats.tsx
index 46681763..e076f737 100644
--- a/src/hooks/useStats.tsx
+++ b/src/hooks/useStats.tsx
@@ -10,7 +10,7 @@ export default function useStats(id?: string) {
useEffect(() => {
setIsLoading(true);
axios
- .get
(!id ? "/api/stats" : `/api/stats/${id}`)
+ .get(!id ? "/api/stats" : `/api/stats/user/${id}`)
.then((response) => setStats(response.data))
.finally(() => setIsLoading(false));
}, [id]);
diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts
index f25cdfbb..99185964 100644
--- a/src/interfaces/exam.ts
+++ b/src/interfaces/exam.ts
@@ -44,6 +44,7 @@ export interface ListeningPart {
}
export interface UserSolution {
+ id?: string;
solutions: any[];
module?: Module;
exam?: string;
@@ -91,6 +92,7 @@ export interface Evaluation {
comment: string;
overall: number;
task_response: {[key: string]: number};
+ misspelled_pairs?: {correction: string | null; misspelled: string}[];
}
interface InteractiveSpeakingEvaluation extends Evaluation {
@@ -101,6 +103,7 @@ interface InteractiveSpeakingEvaluation extends Evaluation {
interface CommonEvaluation extends Evaluation {
perfect_answer?: string;
+ perfect_answer_1?: string;
}
export interface WritingExercise {
diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts
index cd337ac5..c7b0efff 100644
--- a/src/interfaces/user.ts
+++ b/src/interfaces/user.ts
@@ -98,6 +98,7 @@ export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
];
export interface Stat {
+ id: string;
user: string;
exam: string;
exercise: string;
diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx
index 5d83a17c..89b9656a 100644
--- a/src/pages/(admin)/Lists/UserList.tsx
+++ b/src/pages/(admin)/Lists/UserList.tsx
@@ -2,7 +2,7 @@ import Button from "@/components/Low/Button";
import {PERMISSIONS} from "@/constants/userPermissions";
import useGroups from "@/hooks/useGroups";
import useUsers from "@/hooks/useUsers";
-import {Type, User, userTypes} from "@/interfaces/user";
+import {Type, User, userTypes, CorporateUser} from "@/interfaces/user";
import {Popover, Transition} from "@headlessui/react";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import axios from "axios";
@@ -19,9 +19,15 @@ import UserCard from "@/components/UserCard";
import {USER_TYPE_LABELS} from "@/resources/user";
import useFilterStore from "@/stores/listFilterStore";
import {useRouter} from "next/router";
+import {isCorporateUser} from '@/resources/user';
+import { useListSearch } from "@/hooks/useListSearch";
const columnHelper = createColumnHelper();
-
+const searchFields = [
+ ['name'],
+ ['email'],
+ ['corporateInformation', 'companyInformation', 'name'],
+];
export default function UserList({user, filters = []}: {user: User; filters?: ((user: User) => boolean)[]}) {
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
const [sorter, setSorter] = useState();
@@ -325,6 +331,15 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
) as any,
cell: (info) => USER_TYPE_LABELS[info.getValue()],
}),
+ columnHelper.accessor('corporateInformation.companyInformation.name', {
+ header: (
+
+ ) as any,
+ cell: (info) => getCorporateName(info.row.original),
+ }),
columnHelper.accessor("subscriptionExpirationDate", {
header: (
);
}
diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx
index f092ffcb..67cc275a 100644
--- a/src/pages/(exam)/ExamPage.tsx
+++ b/src/pages/(exam)/ExamPage.tsx
@@ -37,6 +37,7 @@ export default function ExamPage({page}: Props) {
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
const [avoidRepeated, setAvoidRepeated] = useState(false);
const [timeSpent, setTimeSpent] = useState(0);
+ const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState
([]);
const [exams, setExams] = useExamStore((state) => [state.exams, state.setExams]);
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
@@ -94,6 +95,7 @@ export default function ExamPage({page}: Props) {
if (selectedModules.length > 0 && exams.length !== 0 && moduleIndex >= selectedModules.length && !hasBeenUploaded && !showSolutions) {
const newStats: Stat[] = userSolutions.map((solution) => ({
...solution,
+ id: solution.id || uuidv4(),
timeSpent,
session: sessionId,
exam: solution.exam!,
@@ -111,6 +113,41 @@ export default function ExamPage({page}: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModules, moduleIndex, hasBeenUploaded]);
+ useEffect(() => {
+ if (statsAwaitingEvaluation.length === 0) return setIsEvaluationLoading(false);
+ return setIsEvaluationLoading(true);
+ }, [statsAwaitingEvaluation]);
+
+ useEffect(() => {
+ if (statsAwaitingEvaluation.length > 0) {
+ statsAwaitingEvaluation.forEach(checkIfStatHasBeenEvaluated);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [statsAwaitingEvaluation]);
+
+ const checkIfStatHasBeenEvaluated = (id: string) => {
+ setTimeout(async () => {
+ const statRequest = await axios.get(`/api/stats/${id}`);
+ const stat = statRequest.data;
+ if (stat.solutions.every((x) => x.evaluation !== null)) {
+ const userSolution: UserSolution = {
+ id,
+ exercise: stat.exercise,
+ score: stat.score,
+ solutions: stat.solutions,
+ type: stat.type,
+ exam: stat.exam,
+ module: stat.module,
+ };
+
+ setUserSolutions(userSolutions.map((x) => (x.exercise === userSolution.exercise ? userSolution : x)));
+ return setStatsAwaitingEvaluation((prev) => prev.filter((x) => x !== id));
+ }
+
+ return checkIfStatHasBeenEvaluated(id);
+ }, 5 * 1000);
+ };
+
const updateExamWithUserSolutions = (exam: Exam): Exam => {
if (exam.module === "reading" || exam.module === "listening") {
const parts = exam.parts.map((p) =>
@@ -137,20 +174,19 @@ export default function ExamPage({page}: Props) {
Promise.all(
exam.exercises.map(async (exercise) => {
- if (exercise.type === "writing") {
- return await evaluateWritingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!);
- }
+ const evaluationID = uuidv4();
+ if (exercise.type === "writing")
+ return await evaluateWritingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID);
- if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking") {
- return await evaluateSpeakingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!);
- }
+ if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking")
+ return await evaluateSpeakingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID);
}),
)
.then((responses) => {
+ setStatsAwaitingEvaluation((prev) => [...prev, ...responses.filter((x) => !!x).map((r) => (r as any).id)]);
setUserSolutions([...userSolutions, ...responses.filter((x) => !!x)] as any);
})
.finally(() => {
- setIsEvaluationLoading(false);
setHasBeenUploaded(false);
});
}
diff --git a/src/pages/(register)/RegisterIndividual.tsx b/src/pages/(register)/RegisterIndividual.tsx
index 9fbd7bf4..3a2b2195 100644
--- a/src/pages/(register)/RegisterIndividual.tsx
+++ b/src/pages/(register)/RegisterIndividual.tsx
@@ -4,21 +4,22 @@ import Input from "@/components/Low/Input";
import {User} from "@/interfaces/user";
import {sendEmailVerification} from "@/utils/email";
import axios from "axios";
-import {useState} from "react";
+import {useEffect, useState} from "react";
import {toast} from "react-toastify";
import {KeyedMutator} from "swr";
interface Props {
queryCode?: string;
+ defaultEmail?: string;
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
mutateUser: KeyedMutator;
sendEmailVerification: typeof sendEmailVerification;
}
-export default function RegisterIndividual({queryCode, isLoading, setIsLoading, mutateUser, sendEmailVerification}: Props) {
+export default function RegisterIndividual({queryCode, defaultEmail, isLoading, setIsLoading, mutateUser, sendEmailVerification}: Props) {
const [name, setName] = useState("");
- const [email, setEmail] = useState("");
+ const [email, setEmail] = useState(defaultEmail || "");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [code, setCode] = useState(queryCode || "");
@@ -73,7 +74,15 @@ export default function RegisterIndividual({queryCode, isLoading, setIsLoading,
return (