diff --git a/components.json b/components.json new file mode 100644 index 00000000..8b9622e5 --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b7c22f81..920b7381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@beam-australia/react-env": "^3.1.1", "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@firebase/util": "^1.9.7", "@headlessui/react": "^1.7.13", "@mdi/js": "^7.1.96", @@ -453,6 +454,19 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@dnd-kit/sortable": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz", + "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.1.0", + "react": ">=16.8.0" + } + }, "node_modules/@dnd-kit/utilities": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", @@ -10430,6 +10444,15 @@ "tslib": "^2.0.0" } }, + "@dnd-kit/sortable": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz", + "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", + "requires": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + } + }, "@dnd-kit/utilities": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", diff --git a/package.json b/package.json index d7536158..d0373ee9 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,15 @@ "dependencies": { "@beam-australia/react-env": "^3.1.1", "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@firebase/util": "^1.9.7", - "@headlessui/react": "^1.7.13", + "@headlessui/react": "^2.1.2", "@mdi/js": "^7.1.96", "@mdi/react": "^1.6.1", - "@next/font": "13.1.6", "@paypal/paypal-js": "^7.1.0", "@paypal/react-paypal-js": "^8.1.3", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-popover": "^1.1.1", "@react-pdf/renderer": "^3.1.14", "@react-spring/web": "^9.7.4", "@tanstack/react-table": "^8.10.1", @@ -29,7 +31,8 @@ "axios": "^1.3.5", "bcrypt": "^5.1.1", "chart.js": "^4.2.1", - "clsx": "^1.2.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", "countries-list": "^3.0.1", "country-codes-list": "^1.6.11", "currency-symbol-map": "^5.1.0", @@ -48,7 +51,7 @@ "lodash": "^4.17.21", "moment": "^2.29.4", "moment-timezone": "^0.5.44", - "next": "13.1.6", + "next": "^14.2.5", "nodemailer": "^6.9.5", "nodemailer-express-handlebars": "^6.1.0", "primeicons": "^6.0.1", @@ -77,7 +80,9 @@ "short-unique-id": "5.0.2", "stripe": "^13.10.0", "swr": "^2.1.3", + "tailwind-merge": "^2.5.2", "tailwind-scrollbar-hide": "^1.1.7", + "tailwindcss-animate": "^1.0.7", "typescript": "4.9.5", "use-file-picker": "^2.1.0", "uuid": "^9.0.0", diff --git a/src/components/Exercises/FillBlanks.tsx b/src/components/Exercises/FillBlanks.tsx deleted file mode 100644 index d208687b..00000000 --- a/src/components/Exercises/FillBlanks.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import {FillBlanksExercise} from "@/interfaces/exam"; -import useExamStore from "@/stores/examStore"; -import clsx from "clsx"; -import {Fragment, useEffect, useState} from "react"; -import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; -import Button from "../Low/Button"; - -interface WordsDrawerProps { - words: {word: string; isDisabled: boolean}[]; - isOpen: boolean; - blankId?: string; - previouslySelectedWord?: string; - onCancel: () => void; - onAnswer: (answer: string) => void; -} - -function WordsDrawer({words, isOpen, blankId, previouslySelectedWord, onCancel, onAnswer}: WordsDrawerProps) { - const [selectedWord, setSelectedWord] = useState(previouslySelectedWord); - - return ( - <> -
-
-
-
{blankId}
- Choose the correct word: -
-
- {words.map(({word, isDisabled}) => ( - - ))} -
-
- - -
-
- - ); -} - -export default function FillBlanks({ - id, - allowRepetition, - type, - prompt, - solutions, - text, - words, - userSolutions, - onNext, - onBack, -}: FillBlanksExercise & CommonProps) { - const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions); - - const hasExamEnded = useExamStore((state) => state.hasExamEnded); - - useEffect(() => { - if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type}); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasExamEnded]); - - const calculateScore = () => { - const total = text.match(/({{\d+}})/g)?.length || 0; - const correct = answers.filter((x) => { - const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution.toLowerCase(); - if (!solution) return false; - - const option = words.find((w) => - typeof w === "string" ? w.toLowerCase() === x.solution.toLowerCase() : w.letter.toLowerCase() === x.solution.toLowerCase(), - ); - if (!option) return false; - - return solution === (typeof option === "string" ? option.toLowerCase() : option.word.toLowerCase()); - }).length; - const missing = total - answers.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; - - return {total, correct, missing}; - }; - - const renderLines = (line: string) => { - return ( -
- {reactStringReplace(line, /({{\d+}})/g, (match) => { - const id = match.replaceAll(/[\{\}]/g, ""); - const userSolution = answers.find((x) => x.id === id); - - return ( - setAnswers((prev) => [...prev.filter((x) => x.id !== id), {id, solution: e.target.value}])} - value={userSolution?.solution}> - ); - })} -
- ); - }; - - return ( - <> -
- - {prompt.split("\\n").map((line, index) => ( - - {line} -
-
- ))} -
- - {text.split("\\n").map((line, index) => ( -

- {renderLines(line)} -
-

- ))} -
-
- Options -
- {words.map((v) => { - const text = typeof v === "string" ? v : `${v.letter} - ${v.word}`; - - return ( - x.solution.toLowerCase() === (typeof v === "string" ? v : v.letter).toLowerCase()) && - "bg-mti-purple-dark text-white", - )} - key={text}> - {text} - - ); - })} -
-
-
- -
- - - -
- - ); -} diff --git a/src/components/Exercises/FillBlanks/WordsDrawer.tsx b/src/components/Exercises/FillBlanks/WordsDrawer.tsx new file mode 100644 index 00000000..2553f0a2 --- /dev/null +++ b/src/components/Exercises/FillBlanks/WordsDrawer.tsx @@ -0,0 +1,64 @@ +import Button from "@/components/Low/Button"; +import clsx from "clsx"; +import { useState } from "react"; + +interface WordsDrawerProps { + words: {word: string; isDisabled: boolean}[]; + isOpen: boolean; + blankId?: string; + previouslySelectedWord?: string; + onCancel: () => void; + onAnswer: (answer: string) => void; +} + + +const WordsDrawer: React.FC = ({words, isOpen, blankId, previouslySelectedWord, onCancel, onAnswer}) => { + const [selectedWord, setSelectedWord] = useState(previouslySelectedWord); + + return ( + <> +
+
+
+
{blankId}
+ Choose the correct word: +
+
+ {words.map(({word, isDisabled}) => ( + + ))} +
+
+ + +
+
+ + ); +} + +export default WordsDrawer; diff --git a/src/components/Exercises/FillBlanks/index.tsx b/src/components/Exercises/FillBlanks/index.tsx new file mode 100644 index 00000000..44f8d693 --- /dev/null +++ b/src/components/Exercises/FillBlanks/index.tsx @@ -0,0 +1,232 @@ +import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam"; +import useExamStore from "@/stores/examStore"; +import clsx from "clsx"; +import { Fragment, useEffect, useState } from "react"; +import reactStringReplace from "react-string-replace"; +import { CommonProps } from ".."; +import Button from "../../Low/Button"; +import { v4 } from "uuid"; + + +const FillBlanks: React.FC = ({ + id, + type, + prompt, + solutions, + text, + words, + userSolutions, + variant, + onNext, + onBack, +}) => { + //const { shuffleMaps } = useExamStore((state) => state); + const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions); + const hasExamEnded = useExamStore((state) => state.hasExamEnded); + + const [currentMCSelection, setCurrentMCSelection] = useState<{ id: string, selection: FillBlanksMCOption }>(); + + const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => { + return Array.isArray(words) && words.every( + word => word && typeof word === 'object' && 'id' in word && 'options' in word + ); + } + + const excludeWordMCType = (x: any) => { + return typeof x === "string" ? x : x as { letter: string; word: string }; + } + + useEffect(() => { + if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasExamEnded]); + + const calculateScore = () => { + const total = text.match(/({{\d+}})/g)?.length || 0; + const correct = userSolutions.filter((x) => { + const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; + if (!solution) return false; + + const option = words.find((w) => { + if (typeof w === "string") { + return w.toLowerCase() === x.solution.toLowerCase(); + } else if ('letter' in w) { + return w.word.toLowerCase() === x.solution.toLowerCase(); + } else { + return w.id === x.id; + } + }); + if (!option) return false; + + if (typeof option === "string") { + return solution.toLowerCase() === option.toLowerCase(); + } else if ('letter' in option) { + return solution.toLowerCase() === option.word.toLowerCase(); + } else if ('options' in option) { + /* + if (shuffleMaps.length !== 0) { + const shuffleMap = shuffleMaps.find((map) => map.id == x.id) + if (!shuffleMap) { + return false; + } + const original = shuffleMap[x.solution as keyof typeof shuffleMap]; + return solution.toLowerCase() === (option.options[original as keyof typeof option.options] || '').toLowerCase(); + }*/ + + return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase(); + } + return false; + }).length; + + const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; + + return { total, correct, missing }; + }; + + const renderLines = (line: string) => { + return ( +
+ {reactStringReplace(line, /({{\d+}})/g, (match) => { + const id = match.replaceAll(/[\{\}]/g, ""); + const userSolution = answers.find((x) => x.id === id); + const styles = clsx( + "rounded-full hover:text-white focus:ring-0 focus:outline-none focus:!text-white focus:bg-mti-purple transition duration-300 ease-in-out my-1 px-5 py-2 text-center", + !userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight", + userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight", + ) + return ( + variant === "mc" ? ( + + ) : ( + setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id, solution: e.target.value }])} + value={userSolution?.solution} /> + ) + ); + })} +
+ ); + }; + + const onSelection = (id: string, value: string) => { + setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id: id, solution: value }]); + } + + /*const getShuffles = () => { + let shuffle = {}; + if (shuffleMaps.length !== 0) { + shuffle = { + shuffleMaps: shuffleMaps.filter((map) => + answers.some(answer => answer.id === map.id) + ) + } + } + return shuffle; + }*/ + + return ( + <> +
+ + {prompt.split("\\n").map((line, index) => ( + + {line} +
+
+ ))} +
+ + {text.split("\\n").map((line, index) => ( +

+ {renderLines(line)} +
+

+ ))} +
+ {variant === "mc" && typeCheckWordsMC(words) ? ( + <> + {currentMCSelection && ( +
+ Options +
+ {currentMCSelection.selection?.options && Object.entries(currentMCSelection.selection.options).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => { + return ; + })} +
+
+ )} + + ) : ( +
+ Options +
+ {words.map((v) => { + v = excludeWordMCType(v); + const text = typeof v === "string" ? v : `${v.letter} - ${v.word}`; + + return ( + x.solution.toLowerCase() === (typeof v === "string" ? v : ("letter" in v ? v.letter : "")).toLowerCase()) && + "bg-mti-purple-dark text-white", + )} + key={v4()} + > + {text} + + ) + })} +
+
+ )} +
+
+ + + +
+ + ); +} + +export default FillBlanks; diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx index aed907e2..70d8a3ba 100644 --- a/src/components/Exercises/MultipleChoice.tsx +++ b/src/components/Exercises/MultipleChoice.tsx @@ -1,10 +1,10 @@ /* eslint-disable @next/next/no-img-element */ -import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam"; +import { MultipleChoiceExercise, MultipleChoiceQuestion } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import clsx from "clsx"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; +import { CommonProps } from "."; import Button from "../Low/Button"; function Question({ @@ -14,22 +14,30 @@ function Question({ options, userSolution, onSelectOption, -}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) { +}: MultipleChoiceQuestion & { + userSolution: string | undefined; + onSelectOption?: (option: string) => void; + showSolution?: boolean, +}) { + + /* const renderPrompt = (prompt: string) => { return reactStringReplace(prompt, /(()[\w\s']+(<\/u>))/g, (match) => { const word = match.replaceAll("", "").replaceAll("", ""); return word.length > 0 ? {word} : null; }); }; + */ return ( + // {renderPrompt(prompt).filter((x) => x?.toString() !== "")}
{isNaN(Number(id)) ? ( - {renderPrompt(prompt).filter((x) => x?.toString() !== "")} + ) : ( <> - {id} - {renderPrompt(prompt).filter((x) => x?.toString() !== "")} + {id} - )} @@ -65,53 +73,79 @@ function Question({ ); } -export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) { - const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions); +export default function MultipleChoice({ id, prompt, type, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) { + const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions); - const {questionIndex, setQuestionIndex} = useExamStore((state) => state); - const {userSolutions: storeUserSolutions, setUserSolutions} = useExamStore((state) => state); + //const { shuffleMaps } = useExamStore((state) => state); + const { questionIndex, setQuestionIndex } = useExamStore((state) => state); + const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state); const hasExamEnded = useExamStore((state) => state.hasExamEnded); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); useEffect(() => { - setUserSolutions([...storeUserSolutions.filter((x) => x.exercise !== id), {exercise: id, solutions: answers, score: calculateScore(), type}]); + setUserSolutions( + [...storeUserSolutions.filter((x) => x.exercise !== id), { + exercise: id, solutions: answers, score: calculateScore(), type + }]); // eslint-disable-next-line react-hooks/exhaustive-deps }, [answers]); useEffect(() => { - if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type}); + if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasExamEnded]); const onSelectOption = (option: string) => { const question = questions[questionIndex]; - setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), {option, question: question.id}]); + setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]); }; const calculateScore = () => { const total = questions.length; - const correct = answers.filter( - (x) => questions.find((y) => y.id.toString() === x.question.toString())?.solution === x.option || false, - ).length; - const missing = total - answers.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length; + const correct = answers.filter((x) => { + const matchingQuestion = questions.find((y) => { + return y.id.toString() === x.question.toString(); + }); - return {total, correct, missing}; + let isSolutionCorrect; + //if (shuffleMaps.length == 0) { + isSolutionCorrect = matchingQuestion?.solution === x.option; + //} else { + // const shuffleMap = shuffleMaps.find((map) => map.id == x.question) + // isSolutionCorrect = shuffleMap?.map[x.option] == matchingQuestion?.solution; + //} + return isSolutionCorrect || false; + }).length; + const missing = total - correct; + + return { total, correct, missing }; }; + /*const getShuffles = () => { + let shuffle = {}; + if (shuffleMaps.length !== 0) { + shuffle = { + shuffleMaps: shuffleMaps.filter((map) => + answers.some(answer => answer.question === map.id) + ) + } + } + return shuffle; + }*/ + const next = () => { if (questionIndex === questions.length - 1) { - onNext({exercise: id, solutions: answers, score: calculateScore(), type}); + onNext({ exercise: id, solutions: answers, score: calculateScore(), type, });//...getShuffles() }); } else { setQuestionIndex(questionIndex + 1); } - scrollToTop(); }; const back = () => { if (questionIndex === 0) { - onBack({exercise: id, solutions: answers, score: calculateScore(), type}); + onBack({ exercise: id, solutions: answers, score: calculateScore(), type, });// ...getShuffles() }); } else { setQuestionIndex(questionIndex - 1); } diff --git a/src/components/Generation/fill.blanks.edit.tsx b/src/components/Generation/fill.blanks.edit.tsx index 4c82c68f..507f3e5d 100644 --- a/src/components/Generation/fill.blanks.edit.tsx +++ b/src/components/Generation/fill.blanks.edit.tsx @@ -63,7 +63,7 @@ const FillBlanksEdit = (props: Props) => { label={`Word ${index + 1}`} name="word" required - value={typeof word === "string" ? word : word.word} + value={typeof word === "string" ? word : ("word" in word ? word.word : "")} onChange={(value) => updateExercise({ words: exercise.words.map((sol, idx) => diff --git a/src/components/HighlightContent.tsx b/src/components/HighlightContent.tsx new file mode 100644 index 00000000..32401523 --- /dev/null +++ b/src/components/HighlightContent.tsx @@ -0,0 +1,39 @@ +import { useCallback } from "react"; + +const HighlightContent: React.FC<{ + html: string; + highlightPhrases: string[], + firstOccurence?: boolean +}> = ({ + html, + highlightPhrases, + firstOccurence = false +}) => { + + const createHighlightedContent = useCallback(() => { + if (highlightPhrases.length === 0) { + return { __html: html }; + } + + const escapeRegExp = (string: string) => { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }; + + const regex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'i'); + const globalRegex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'gi'); + + let highlightedHtml = html; + + if (firstOccurence) { + highlightedHtml = html.replace(regex, (match) => `${match}`); + } else { + highlightedHtml = html.replace(globalRegex, (match) => `${match}`); + } + + return { __html: highlightedHtml }; + }, [html, highlightPhrases, firstOccurence]); + + return
; +}; + +export default HighlightContent; diff --git a/src/components/List.tsx b/src/components/List.tsx new file mode 100644 index 00000000..0e76b663 --- /dev/null +++ b/src/components/List.tsx @@ -0,0 +1,51 @@ +import {Column, flexRender, getCoreRowModel, getSortedRowModel, useReactTable} from "@tanstack/react-table"; + +export default function List({data, columns}: {data: T[]; columns: any[]}) { + const table = useReactTable({ + data, + columns: columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: " 🔼", + desc: " 🔽", + }[header.column.getIsSorted() as string] ?? null} +
+ + )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d69feebf..cf1bdeee 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -13,6 +13,7 @@ import { BsCurrencyDollar, BsClipboardData, BsFileLock, + BsPeople, } from "react-icons/bs"; import {CiDumbbell} from "react-icons/ci"; import {RiLogoutBoxFill} from "react-icons/ri"; @@ -109,6 +110,9 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u {checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
diff --git a/src/components/Solutions/FillBlanks.tsx b/src/components/Solutions/FillBlanks.tsx index a08f5330..807536f9 100644 --- a/src/components/Solutions/FillBlanks.tsx +++ b/src/components/Solutions/FillBlanks.tsx @@ -1,8 +1,8 @@ -import {FillBlanksExercise} from "@/interfaces/exam"; +import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam"; import clsx from "clsx"; import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; -import {Fragment} from "react"; +import { CommonProps } from "."; +import { Fragment } from "react"; import Button from "../Low/Button"; export default function FillBlanksSolutions({ @@ -19,21 +19,42 @@ export default function FillBlanksSolutions({ const calculateScore = () => { const total = text.match(/({{\d+}})/g)?.length || 0; const correct = userSolutions.filter((x) => { - const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution.toLowerCase(); + const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; if (!solution) return false; - const option = words.find((w) => - typeof w === "string" ? w.toLowerCase() === x.solution.toLowerCase() : w.letter.toLowerCase() === x.solution.toLowerCase(), - ); + const option = words.find((w) => { + if (typeof w === "string") { + return w.toLowerCase() === x.solution.toLowerCase(); + } else if ('letter' in w) { + return w.word.toLowerCase() === x.solution.toLowerCase(); + } else { + return w.id === x.id; + } + }); if (!option) return false; - return solution === (typeof option === "string" ? option.toLowerCase() : option.word.toLowerCase()); + if (typeof option === "string") { + return solution.toLowerCase() === option.toLowerCase(); + } else if ('letter' in option) { + return solution.toLowerCase() === option.word.toLowerCase(); + } else if ('options' in option) { + return option.options[solution as keyof typeof option.options] == x.solution; + } + return false; }).length; + const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; + + const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => { + return Array.isArray(words) && words.every( + word => word && typeof word === 'object' && 'id' in word && 'options' in word + ); + } + const renderLines = (line: string) => { return ( @@ -56,23 +77,53 @@ export default function FillBlanksSolutions({ const userSolutionWord = words.find((w) => typeof w === "string" ? w.toLowerCase() === userSolution.solution.toLowerCase() - : w.letter.toLowerCase() === userSolution.solution.toLowerCase(), + : 'letter' in w + ? w.letter.toLowerCase() === userSolution.solution.toLowerCase() + : 'options' in w + ? w.id === userSolution.id + : false ); - const userSolutionText = typeof userSolutionWord === "string" ? userSolutionWord : userSolutionWord?.word; - if (userSolutionText === solution.solution) { + const userSolutionText = + typeof userSolutionWord === "string" + ? userSolutionWord + : userSolutionWord && 'letter' in userSolutionWord + ? userSolutionWord.word + : userSolutionWord && 'options' in userSolutionWord + ? userSolution.solution + : userSolution.solution; + + let correct; + let solutionText; + if (typeCheckWordsMC(words)) { + const options = words.find((x) => x.id === id); + if (options) { + const correctKey = Object.keys(options.options).find(key => + key.toLowerCase() === solution.solution.toLowerCase() + ); + correct = userSolution.solution == options.options[correctKey as keyof typeof options.options]; + solutionText = options.options[correctKey as keyof typeof options.options] || solution.solution; + } else { + correct = false; + solutionText = solution?.solution; + } + + } else { + correct = userSolutionText === solution.solution; + solutionText = solution.solution; + } + + if (correct) { return ( ); - } - - if (userSolutionText !== solution.solution) { + } else { return ( <> ); @@ -138,14 +189,14 @@ export default function FillBlanksSolutions({ diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx index 7f1ba0fd..d3ecfabc 100644 --- a/src/components/Solutions/MultipleChoice.tsx +++ b/src/components/Solutions/MultipleChoice.tsx @@ -1,10 +1,10 @@ /* eslint-disable @next/next/no-img-element */ -import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam"; +import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import clsx from "clsx"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; +import { CommonProps } from "."; import Button from "../Low/Button"; function Question({ @@ -14,7 +14,40 @@ function Question({ solution, options, userSolution, -}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) { +}: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean }) { + const { userSolutions } = useExamStore((state) => state); + + /* + const getShuffledOptions = (options: {id: string, text: string}[], questionShuffleMap: ShuffleMap) => { + const shuffledOptions = ['A', 'B', 'C', 'D'].map(newId => { + const originalId = questionShuffleMap.map[newId]; + const originalOption = options.find(option => option.id === originalId); + return { + id: newId, + text: originalOption!.text + }; + }); + return shuffledOptions; + } + + const getShuffledSolution = (originalSolution: string, questionShuffleMap: ShuffleMap) => { + for (const [newPosition, originalPosition] of Object.entries(questionShuffleMap.map)) { + if (originalPosition === originalSolution) { + return newPosition; + } + } + return originalSolution; + } + + const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => { + if (foundMap) return foundMap; + return userSolution.shuffleMaps?.find(map => map.id === id) || null; + }, null as ShuffleMap | null); + */ + + const questionOptions = options; // questionShuffleMap ? getShuffledOptions(options as {id: string, text: string}[], questionShuffleMap) : options; + const newSolution = solution; //questionShuffleMap ? getShuffledSolution(solution, questionShuffleMap) : solution; + const renderPrompt = (prompt: string) => { return reactStringReplace(prompt, /(()[\w\s']+(<\/u>))/g, (match) => { const word = match.replaceAll("", "").replaceAll("", ""); @@ -23,11 +56,11 @@ function Question({ }; const optionColor = (option: string) => { - if (option === solution && !userSolution) { + if (option === newSolution && !userSolution) { return "!border-mti-gray-davy !text-mti-gray-davy"; } - if (option === solution) { + if (option === newSolution) { return "!border-mti-purple-light !text-mti-purple-light"; } @@ -47,24 +80,24 @@ function Question({ )}
{variant === "image" && - options.map((option) => ( + questionOptions.map((option) => (
- {option.id} - {`Option + {option?.id} + {"src" in option && {`Option}
))} {variant === "text" && - options.map((option) => ( + questionOptions.map((option) => (
- {option.id}. - {option.text} + key={option?.id} + className={clsx("flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-sm", optionColor(option!.id))}> + {option?.id}. + {option?.text}
))}
@@ -72,8 +105,8 @@ function Question({ ); } -export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) { - const {questionIndex, setQuestionIndex} = useExamStore((state) => state); +export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) { + const { questionIndex, setQuestionIndex } = useExamStore((state) => state); const calculateScore = () => { const total = questions.length; @@ -82,12 +115,12 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio ).length; const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; const next = () => { if (questionIndex === questions.length - 1) { - onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type}); + onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type }); } else { setQuestionIndex(questionIndex + 1); } @@ -95,7 +128,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio const back = () => { if (questionIndex === 0) { - onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type}); + onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type }); } else { setQuestionIndex(questionIndex - 1); } diff --git a/src/components/TrainingContent/AnimatedHighlight.tsx b/src/components/TrainingContent/AnimatedHighlight.tsx deleted file mode 100644 index 493907c2..00000000 --- a/src/components/TrainingContent/AnimatedHighlight.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useCallback } from "react"; - -const HighlightedContent: React.FC<{ html: string; highlightPhrases: string[] }> = ({ html, highlightPhrases }) => { - - const createHighlightedContent = useCallback(() => { - if (highlightPhrases.length === 0) { - return { __html: html }; - } - - const escapeRegExp = (string: string) => { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - }; - - const regex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'gi'); - const highlightedHtml = html.replace(regex, (match) => `${match}`); - - return { __html: highlightedHtml }; - }, [html, highlightPhrases]); - - return
; -}; - -export default HighlightedContent; diff --git a/src/components/TrainingContent/ExerciseWalkthrough.tsx b/src/components/TrainingContent/ExerciseWalkthrough.tsx index 07ea16ff..74c7219f 100644 --- a/src/components/TrainingContent/ExerciseWalkthrough.tsx +++ b/src/components/TrainingContent/ExerciseWalkthrough.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { animated } from '@react-spring/web'; import { FaRegCirclePlay, FaRegCircleStop } from "react-icons/fa6"; -import HighlightedContent from './AnimatedHighlight'; +import HighlightContent from '../HighlightContent'; import { ITrainingTip, SegmentRef, TimelineEvent } from './TrainingInterfaces'; @@ -267,7 +267,7 @@ const ExerciseWalkthrough: React.FC = (tip: ITrainingTip) => {
{/*

Question

*/}
- +
diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 23a79fea..ba02b079 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -525,7 +525,9 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, setExpiryDate(checked ? user.subscriptionExpirationDate || new Date() : null)} - disabled={disabled}> + disabled={ + disabled || (!["admin", "developer"].includes(loggedInUser.type) && !!loggedInUser.subscriptionExpirationDate) + }> Enabled
@@ -564,7 +566,12 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
- {checkAccess(loggedInUser, ["developer", "admin"]) && ( + {checkAccess( + loggedInUser, + ["developer", "admin", "corporate", "mastercorporate"], + permissions, + user.type === "teacher" ? "editTeacher" : user.type === "student" ? "editStudent" : undefined, + ) && ( <>
@@ -572,7 +579,10 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, { + if (x.value === "student") + return checkAccess( + loggedInUser, + ["developer", "admin", "corporate", "mastercorporate"], + permissions, + "editStudent", + ); + + if (x.value === "teacher") + return checkAccess( + loggedInUser, + ["developer", "admin", "corporate", "mastercorporate"], + permissions, + "editTeacher", + ); + + if (x.value === "corporate") + return checkAccess(loggedInUser, ["developer", "admin", "mastercorporate"], permissions, "editCorporate"); + + return checkAccess(loggedInUser, ["developer", "admin"]); + })} menuPortalTarget={document?.body} value={USER_TYPE_OPTIONS.find((o) => o.value === type)} onChange={(value) => setType(value?.value as typeof user.type)} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 00000000..7bc4061e --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/dashboards/Admin.tsx b/src/dashboards/Admin.tsx index 85939403..65f15ed1 100644 --- a/src/dashboards/Admin.tsx +++ b/src/dashboards/Admin.tsx @@ -2,714 +2,613 @@ import Modal from "@/components/Modal"; import useStats from "@/hooks/useStats"; import useUsers from "@/hooks/useUsers"; -import { User } from "@/interfaces/user"; +import {User} from "@/interfaces/user"; import UserList from "@/pages/(admin)/Lists/UserList"; -import { dateSorter } from "@/utils"; +import {dateSorter} from "@/utils"; import moment from "moment"; -import { useEffect, useState } from "react"; +import {useEffect, useState} from "react"; import { - BsArrowLeft, - BsBriefcaseFill, - BsGlobeCentralSouthAsia, - BsPerson, - BsPersonFill, - BsPencilSquare, - BsBank, - BsCurrencyDollar, - BsLayoutWtf, - BsLayoutSidebar, + BsArrowLeft, + BsBriefcaseFill, + BsGlobeCentralSouthAsia, + BsPerson, + BsPersonFill, + BsPencilSquare, + BsBank, + BsCurrencyDollar, + BsLayoutWtf, + BsLayoutSidebar, } from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; import IconCard from "./IconCard"; import useFilterStore from "@/stores/listFilterStore"; -import { useRouter } from "next/router"; +import {useRouter} from "next/router"; import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers"; import CorporateStudentsLevels from "./CorporateStudentsLevels"; interface Props { - user: User; + user: User; } -export default function AdminDashboard({ user }: Props) { - const [page, setPage] = useState(""); - const [selectedUser, setSelectedUser] = useState(); - const [showModal, setShowModal] = useState(false); +export default function AdminDashboard({user}: Props) { + const [page, setPage] = useState(""); + const [selectedUser, setSelectedUser] = useState(); + const [showModal, setShowModal] = useState(false); - const { stats } = useStats(user.id); - const { users, reload } = useUsers(); - const { groups } = useGroups(); - const { pending, done } = usePaymentStatusUsers(); + const {stats} = useStats(user.id); + const {users, reload} = useUsers(); + const {groups} = useGroups({}); + const {pending, done} = usePaymentStatusUsers(); - const appendUserFilters = useFilterStore((state) => state.appendUserFilter); - const router = useRouter(); + const appendUserFilters = useFilterStore((state) => state.appendUserFilter); + const router = useRouter(); - useEffect(() => { - setShowModal(!!selectedUser && page === ""); - }, [selectedUser, page]); + useEffect(() => { + setShowModal(!!selectedUser && page === ""); + }, [selectedUser, page]); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(reload, [page]); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(reload, [page]); - const inactiveCountryManagerFilter = (x: User) => - x.type === "agent" && - (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); + const inactiveCountryManagerFilter = (x: User) => + x.type === "agent" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); - const UserDisplay = (displayUser: User) => ( -
setSelectedUser(displayUser)} - className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300" - > - {displayUser.name} -
- - {displayUser.type === "corporate" - ? displayUser.corporateInformation?.companyInformation?.name || - displayUser.name - : displayUser.name} - - {displayUser.email} -
-
- ); + const UserDisplay = (displayUser: User) => ( +
setSelectedUser(displayUser)} + className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"> + {displayUser.name} +
+ + {displayUser.type === "corporate" + ? displayUser.corporateInformation?.companyInformation?.name || displayUser.name + : displayUser.name} + + {displayUser.email} +
+
+ ); - const StudentsList = () => { - const filter = (x: User) => - x.type === "student" && - (!!selectedUser - ? groups - .filter( - (g) => - g.admin === selectedUser.id || - g.participants.includes(selectedUser.id) - ) - .flatMap((g) => g.participants) - .includes(x.id) - : true); + const StudentsList = () => { + const filter = (x: User) => + x.type === "student" && + (!!selectedUser + ? groups + .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) + .flatMap((g) => g.participants) + .includes(x.id) + : true); - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

Students ({total})

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Students ({total})

+
+ )} + /> + ); + }; - const TeachersList = () => { - const filter = (x: User) => - x.type === "teacher" && - (!!selectedUser - ? groups - .filter( - (g) => - g.admin === selectedUser.id || - g.participants.includes(selectedUser.id) - ) - .flatMap((g) => g.participants) - .includes(x.id) || false - : true); + const TeachersList = () => { + const filter = (x: User) => + x.type === "teacher" && + (!!selectedUser + ? groups + .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) + .flatMap((g) => g.participants) + .includes(x.id) || false + : true); - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

Teachers ({total})

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Teachers ({total})

+
+ )} + /> + ); + }; - const AgentsList = () => { - const filter = (x: User) => x.type === "agent"; + const AgentsList = () => { + const filter = (x: User) => x.type === "agent"; - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- Country Managers ({total}) -

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Country Managers ({total})

+
+ )} + /> + ); + }; - const CorporateList = () => ( - x.type === "corporate"]} - renderHeader={(total) => ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

Corporate ({total})

-
- )} - /> - ); + const CorporateList = () => ( + x.type === "corporate"]} + renderHeader={(total) => ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Corporate ({total})

+
+ )} + /> + ); - const CorporatePaidStatusList = ({ paid }: { paid: Boolean }) => { - const list = paid ? done : pending; - const filter = (x: User) => x.type === "corporate" && list.includes(x.id); + const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => { + const list = paid ? done : pending; + const filter = (x: User) => x.type === "corporate" && list.includes(x.id); - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- {paid ? "Payment Done" : "Pending Payment"} ({total}) -

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

+ {paid ? "Payment Done" : "Pending Payment"} ({total}) +

+
+ )} + /> + ); + }; - const InactiveCountryManagerList = () => { - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- Inactive Country Managers ({total}) -

-
- )} - /> - ); - }; + const InactiveCountryManagerList = () => { + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Inactive Country Managers ({total})

+
+ )} + /> + ); + }; - const InactiveStudentsList = () => { - const filter = (x: User) => - x.type === "student" && - (x.status === "disabled" || - moment().isAfter(x.subscriptionExpirationDate)); + const InactiveStudentsList = () => { + const filter = (x: User) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- Inactive Students ({total}) -

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Inactive Students ({total})

+
+ )} + /> + ); + }; - const InactiveCorporateList = () => { - const filter = (x: User) => - x.type === "corporate" && - (x.status === "disabled" || - moment().isAfter(x.subscriptionExpirationDate)); + const InactiveCorporateList = () => { + const filter = (x: User) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- Inactive Corporate ({total}) -

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Inactive Corporate ({total})

+
+ )} + /> + ); + }; - const CorporateStudentsLevelsHelper = () => { - return ( - <> -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- Corporate Students Levels -

-
- - - ); - }; + const CorporateStudentsLevelsHelper = () => { + return ( + <> +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Corporate Students Levels

+
+ + + ); + }; - const DefaultDashboard = () => ( - <> -
- x.type === "student").length} - onClick={() => setPage("students")} - color="purple" - /> - x.type === "teacher").length} - onClick={() => setPage("teachers")} - color="purple" - /> - x.type === "corporate").length} - onClick={() => setPage("corporate")} - color="purple" - /> - x.type === "agent").length} - onClick={() => setPage("agents")} - color="purple" - /> - x.demographicInformation) - .map((x) => x.demographicInformation?.country) - ), - ].length - } - color="purple" - /> - setPage("inactiveStudents")} - Icon={BsPersonFill} - label="Inactive Students" - value={ - users.filter( - (x) => - x.type === "student" && - (x.status === "disabled" || - moment().isAfter(x.subscriptionExpirationDate)) - ).length - } - color="rose" - /> - setPage("inactiveCountryManagers")} - Icon={BsBriefcaseFill} - label="Inactive Country Managers" - value={users.filter(inactiveCountryManagerFilter).length} - color="rose" - /> - setPage("inactiveCorporate")} - Icon={BsBank} - label="Inactive Corporate" - value={ - users.filter( - (x) => - x.type === "corporate" && - (x.status === "disabled" || - moment().isAfter(x.subscriptionExpirationDate)) - ).length - } - color="rose" - /> - setPage("paymentdone")} - Icon={BsCurrencyDollar} - label="Payment Done" - value={done.length} - color="purple" - /> - setPage("paymentpending")} - Icon={BsCurrencyDollar} - label="Pending Payment" - value={pending.length} - color="rose" - /> - router.push("https://cms.encoach.com/admin")} - Icon={BsLayoutSidebar} - label="Content Management System (CMS)" - color="green" - /> - setPage("corporatestudentslevels")} - Icon={BsPersonFill} - label="Corporate Students Levels" - color="purple" - /> -
+ const DefaultDashboard = () => ( + <> +
+ x.type === "student").length} + onClick={() => setPage("students")} + color="purple" + /> + x.type === "teacher").length} + onClick={() => setPage("teachers")} + color="purple" + /> + x.type === "corporate").length} + onClick={() => setPage("corporate")} + color="purple" + /> + x.type === "agent").length} + onClick={() => setPage("agents")} + color="purple" + /> + x.demographicInformation).map((x) => x.demographicInformation?.country))].length} + color="purple" + /> + setPage("inactiveStudents")} + Icon={BsPersonFill} + label="Inactive Students" + value={ + users.filter((x) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate))) + .length + } + color="rose" + /> + setPage("inactiveCountryManagers")} + Icon={BsBriefcaseFill} + label="Inactive Country Managers" + value={users.filter(inactiveCountryManagerFilter).length} + color="rose" + /> + setPage("inactiveCorporate")} + Icon={BsBank} + label="Inactive Corporate" + value={ + users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate))) + .length + } + color="rose" + /> + setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" /> + setPage("paymentpending")} + Icon={BsCurrencyDollar} + label="Pending Payment" + value={pending.length} + color="rose" + /> + router.push("https://cms.encoach.com/admin")} + Icon={BsLayoutSidebar} + label="Content Management System (CMS)" + color="green" + /> + setPage("corporatestudentslevels")} Icon={BsPersonFill} label="Corporate Students Levels" color="purple" /> +
-
-
- Latest students -
- {users - .filter((x) => x.type === "student") - .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) - .map((x) => ( - - ))} -
-
-
- Latest teachers -
- {users - .filter((x) => x.type === "teacher") - .sort((a, b) => { - return dateSorter(a, b, "desc", "registrationDate"); - }) - .map((x) => ( - - ))} -
-
-
- Latest corporate -
- {users - .filter((x) => x.type === "corporate") - .sort((a, b) => { - return dateSorter(a, b, "desc", "registrationDate"); - }) - .map((x) => ( - - ))} -
-
-
- Unpaid Corporate -
- {users - .filter( - (x) => x.type === "corporate" && x.status === "paymentDue" - ) - .map((x) => ( - - ))} -
-
-
- Students expiring in 1 month -
- {users - .filter( - (x) => - x.type === "student" && - x.subscriptionExpirationDate && - moment().isAfter( - moment(x.subscriptionExpirationDate).subtract(30, "days") - ) && - moment().isBefore(moment(x.subscriptionExpirationDate)) - ) - .map((x) => ( - - ))} -
-
-
- Teachers expiring in 1 month -
- {users - .filter( - (x) => - x.type === "teacher" && - x.subscriptionExpirationDate && - moment().isAfter( - moment(x.subscriptionExpirationDate).subtract(30, "days") - ) && - moment().isBefore(moment(x.subscriptionExpirationDate)) - ) - .map((x) => ( - - ))} -
-
-
- Country Manager expiring in 1 month -
- {users - .filter( - (x) => - x.type === "agent" && - x.subscriptionExpirationDate && - moment().isAfter( - moment(x.subscriptionExpirationDate).subtract(30, "days") - ) && - moment().isBefore(moment(x.subscriptionExpirationDate)) - ) - .map((x) => ( - - ))} -
-
-
- Corporate expiring in 1 month -
- {users - .filter( - (x) => - x.type === "corporate" && - x.subscriptionExpirationDate && - moment().isAfter( - moment(x.subscriptionExpirationDate).subtract(30, "days") - ) && - moment().isBefore(moment(x.subscriptionExpirationDate)) - ) - .map((x) => ( - - ))} -
-
-
- Expired Students -
- {users - .filter( - (x) => - x.type === "student" && - x.subscriptionExpirationDate && - moment().isAfter(moment(x.subscriptionExpirationDate)) - ) - .map((x) => ( - - ))} -
-
-
- Expired Teachers -
- {users - .filter( - (x) => - x.type === "teacher" && - x.subscriptionExpirationDate && - moment().isAfter(moment(x.subscriptionExpirationDate)) - ) - .map((x) => ( - - ))} -
-
-
- Expired Country Manager -
- {users - .filter( - (x) => - x.type === "agent" && - x.subscriptionExpirationDate && - moment().isAfter(moment(x.subscriptionExpirationDate)) - ) - .map((x) => ( - - ))} -
-
-
- Expired Corporate -
- {users - .filter( - (x) => - x.type === "corporate" && - x.subscriptionExpirationDate && - moment().isAfter(moment(x.subscriptionExpirationDate)) - ) - .map((x) => ( - - ))} -
-
-
- - ); +
+
+ Latest students +
+ {users + .filter((x) => x.type === "student") + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest teachers +
+ {users + .filter((x) => x.type === "teacher") + .sort((a, b) => { + return dateSorter(a, b, "desc", "registrationDate"); + }) + .map((x) => ( + + ))} +
+
+
+ Latest corporate +
+ {users + .filter((x) => x.type === "corporate") + .sort((a, b) => { + return dateSorter(a, b, "desc", "registrationDate"); + }) + .map((x) => ( + + ))} +
+
+
+ Unpaid Corporate +
+ {users + .filter((x) => x.type === "corporate" && x.status === "paymentDue") + .map((x) => ( + + ))} +
+
+
+ Students expiring in 1 month +
+ {users + .filter( + (x) => + x.type === "student" && + x.subscriptionExpirationDate && + moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) && + moment().isBefore(moment(x.subscriptionExpirationDate)), + ) + .map((x) => ( + + ))} +
+
+
+ Teachers expiring in 1 month +
+ {users + .filter( + (x) => + x.type === "teacher" && + x.subscriptionExpirationDate && + moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) && + moment().isBefore(moment(x.subscriptionExpirationDate)), + ) + .map((x) => ( + + ))} +
+
+
+ Country Manager expiring in 1 month +
+ {users + .filter( + (x) => + x.type === "agent" && + x.subscriptionExpirationDate && + moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) && + moment().isBefore(moment(x.subscriptionExpirationDate)), + ) + .map((x) => ( + + ))} +
+
+
+ Corporate expiring in 1 month +
+ {users + .filter( + (x) => + x.type === "corporate" && + x.subscriptionExpirationDate && + moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) && + moment().isBefore(moment(x.subscriptionExpirationDate)), + ) + .map((x) => ( + + ))} +
+
+
+ Expired Students +
+ {users + .filter( + (x) => x.type === "student" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)), + ) + .map((x) => ( + + ))} +
+
+
+ Expired Teachers +
+ {users + .filter( + (x) => x.type === "teacher" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)), + ) + .map((x) => ( + + ))} +
+
+
+ Expired Country Manager +
+ {users + .filter( + (x) => x.type === "agent" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)), + ) + .map((x) => ( + + ))} +
+
+
+ Expired Corporate +
+ {users + .filter( + (x) => + x.type === "corporate" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)), + ) + .map((x) => ( + + ))} +
+
+
+ + ); - return ( - <> - setSelectedUser(undefined)}> - <> - {selectedUser && ( -
- { - setSelectedUser(undefined); - if (shouldReload) reload(); - }} - onViewStudents={ - selectedUser.type === "corporate" || - selectedUser.type === "teacher" - ? () => { - appendUserFilters({ - id: "view-students", - filter: (x: User) => x.type === "student", - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: (x: User) => - groups - .filter( - (g) => - g.admin === selectedUser.id || - g.participants.includes(selectedUser.id) - ) - .flatMap((g) => g.participants) - .includes(x.id), - }); + return ( + <> + setSelectedUser(undefined)}> + <> + {selectedUser && ( +
+ { + setSelectedUser(undefined); + if (shouldReload) reload(); + }} + onViewStudents={ + selectedUser.type === "corporate" || selectedUser.type === "teacher" + ? () => { + appendUserFilters({ + id: "view-students", + filter: (x: User) => x.type === "student", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) + .flatMap((g) => g.participants) + .includes(x.id), + }); - router.push("/list/users"); - } - : undefined - } - onViewTeachers={ - selectedUser.type === "corporate" || - selectedUser.type === "student" - ? () => { - appendUserFilters({ - id: "view-teachers", - filter: (x: User) => x.type === "teacher", - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: (x: User) => - groups - .filter( - (g) => - g.admin === selectedUser.id || - g.participants.includes(selectedUser.id) - ) - .flatMap((g) => g.participants) - .includes(x.id), - }); + router.push("/list/users"); + } + : undefined + } + onViewTeachers={ + selectedUser.type === "corporate" || selectedUser.type === "student" + ? () => { + appendUserFilters({ + id: "view-teachers", + filter: (x: User) => x.type === "teacher", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) + .flatMap((g) => g.participants) + .includes(x.id), + }); - router.push("/list/users"); - } - : undefined - } - onViewCorporate={ - selectedUser.type === "teacher" || - selectedUser.type === "student" - ? () => { - appendUserFilters({ - id: "view-corporate", - filter: (x: User) => x.type === "corporate", - }); - appendUserFilters({ - id: "belongs-to-admin", - filter: (x: User) => - groups - .filter((g) => - g.participants.includes(selectedUser.id) - ) - .flatMap((g) => [g.admin, ...g.participants]) - .includes(x.id), - }); + router.push("/list/users"); + } + : undefined + } + onViewCorporate={ + selectedUser.type === "teacher" || selectedUser.type === "student" + ? () => { + appendUserFilters({ + id: "view-corporate", + filter: (x: User) => x.type === "corporate", + }); + appendUserFilters({ + id: "belongs-to-admin", + filter: (x: User) => + groups + .filter((g) => g.participants.includes(selectedUser.id)) + .flatMap((g) => [g.admin, ...g.participants]) + .includes(x.id), + }); - router.push("/list/users"); - } - : undefined - } - user={selectedUser} - /> -
- )} - -
- {page === "students" && } - {page === "teachers" && } - {page === "corporate" && } - {page === "agents" && } - {page === "inactiveStudents" && } - {page === "inactiveCorporate" && } - {page === "inactiveCountryManagers" && } - {page === "paymentdone" && } - {page === "paymentpending" && } - {page === "corporatestudentslevels" && } - {page === "" && } - - ); + router.push("/list/users"); + } + : undefined + } + user={selectedUser} + /> +
+ )} + +
+ {page === "students" && } + {page === "teachers" && } + {page === "corporate" && } + {page === "agents" && } + {page === "inactiveStudents" && } + {page === "inactiveCorporate" && } + {page === "inactiveCountryManagers" && } + {page === "paymentdone" && } + {page === "paymentpending" && } + {page === "corporatestudentslevels" && } + {page === "" && } + + ); } diff --git a/src/dashboards/Agent.tsx b/src/dashboards/Agent.tsx index bff2c46d..11028894 100644 --- a/src/dashboards/Agent.tsx +++ b/src/dashboards/Agent.tsx @@ -2,17 +2,12 @@ import Modal from "@/components/Modal"; import useStats from "@/hooks/useStats"; import useUsers from "@/hooks/useUsers"; -import { User } from "@/interfaces/user"; +import {User} from "@/interfaces/user"; import UserList from "@/pages/(admin)/Lists/UserList"; -import { dateSorter } from "@/utils"; +import {dateSorter} from "@/utils"; import moment from "moment"; -import { useEffect, useState } from "react"; -import { - BsArrowLeft, - BsPersonFill, - BsBank, - BsCurrencyDollar, -} from "react-icons/bs"; +import {useEffect, useState} from "react"; +import {BsArrowLeft, BsPersonFill, BsBank, BsCurrencyDollar} from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; @@ -20,276 +15,235 @@ import IconCard from "./IconCard"; import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers"; interface Props { - user: User; + user: User; } -export default function AgentDashboard({ user }: Props) { - const [page, setPage] = useState(""); - const [selectedUser, setSelectedUser] = useState(); - const [showModal, setShowModal] = useState(false); +export default function AgentDashboard({user}: Props) { + const [page, setPage] = useState(""); + const [selectedUser, setSelectedUser] = useState(); + const [showModal, setShowModal] = useState(false); - const { stats } = useStats(); - const { users, reload } = useUsers(); - const { groups } = useGroups(user.id); - const { pending, done } = usePaymentStatusUsers(); + const {stats} = useStats(); + const {users, reload} = useUsers(); + const {pending, done} = usePaymentStatusUsers(); - useEffect(() => { - setShowModal(!!selectedUser && page === ""); - }, [selectedUser, page]); + useEffect(() => { + setShowModal(!!selectedUser && page === ""); + }, [selectedUser, page]); - const corporateFilter = (user: User) => user.type === "corporate"; - const referredCorporateFilter = (x: User) => - x.type === "corporate" && - !!x.corporateInformation && - x.corporateInformation.referralAgent === user.id; - const inactiveReferredCorporateFilter = (x: User) => - referredCorporateFilter(x) && - (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); + const corporateFilter = (user: User) => user.type === "corporate"; + const referredCorporateFilter = (x: User) => + x.type === "corporate" && !!x.corporateInformation && x.corporateInformation.referralAgent === user.id; + const inactiveReferredCorporateFilter = (x: User) => + referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); - const UserDisplay = ({ - displayUser, - allowClick = true, - }: { - displayUser: User; - allowClick?: boolean; - }) => ( -
allowClick && setSelectedUser(displayUser)} - className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300" - > - {displayUser.name} -
- - {displayUser.type === "corporate" - ? displayUser.corporateInformation?.companyInformation?.name || - displayUser.name - : displayUser.name} - - {displayUser.email} -
-
- ); + const UserDisplay = ({displayUser, allowClick = true}: {displayUser: User; allowClick?: boolean}) => ( +
allowClick && setSelectedUser(displayUser)} + className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"> + {displayUser.name} +
+ + {displayUser.type === "corporate" + ? displayUser.corporateInformation?.companyInformation?.name || displayUser.name + : displayUser.name} + + {displayUser.email} +
+
+ ); - const ReferredCorporateList = () => { - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- Referred Corporate ({total}) -

-
- )} - /> - ); - }; + const ReferredCorporateList = () => { + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Referred Corporate ({total})

+
+ )} + /> + ); + }; - const InactiveReferredCorporateList = () => { - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- Inactive Referred Corporate ({total}) -

-
- )} - /> - ); - }; + const InactiveReferredCorporateList = () => { + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Inactive Referred Corporate ({total})

+
+ )} + /> + ); + }; - const CorporateList = () => { - const filter = (x: User) => x.type === "corporate"; + const CorporateList = () => { + const filter = (x: User) => x.type === "corporate"; - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

Corporate ({total})

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

Corporate ({total})

+
+ )} + /> + ); + }; - const CorporatePaidStatusList = ({ paid }: { paid: Boolean }) => { - const list = paid ? done : pending; - const filter = (x: User) => x.type === "corporate" && list.includes(x.id); + const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => { + const list = paid ? done : pending; + const filter = (x: User) => x.type === "corporate" && list.includes(x.id); - return ( - ( -
-
setPage("")} - className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" - > - - Back -
-

- {paid ? "Payment Done" : "Pending Payment"} ({total}) -

-
- )} - /> - ); - }; + return ( + ( +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

+ {paid ? "Payment Done" : "Pending Payment"} ({total}) +

+
+ )} + /> + ); + }; - const DefaultDashboard = () => ( - <> -
- setPage("referredCorporate")} - Icon={BsBank} - label="Referred Corporate" - value={users.filter(referredCorporateFilter).length} - color="purple" - /> - setPage("inactiveReferredCorporate")} - Icon={BsBank} - label="Inactive Referred Corporate" - value={users.filter(inactiveReferredCorporateFilter).length} - color="rose" - /> - setPage("corporate")} - Icon={BsBank} - label="Corporate" - value={users.filter(corporateFilter).length} - color="purple" - /> - setPage("paymentdone")} - Icon={BsCurrencyDollar} - label="Payment Done" - value={done.length} - color="purple" - /> - setPage("paymentpending")} - Icon={BsCurrencyDollar} - label="Pending Payment" - value={pending.length} - color="rose" - /> -
+ const DefaultDashboard = () => ( + <> +
+ setPage("referredCorporate")} + Icon={BsBank} + label="Referred Corporate" + value={users.filter(referredCorporateFilter).length} + color="purple" + /> + setPage("inactiveReferredCorporate")} + Icon={BsBank} + label="Inactive Referred Corporate" + value={users.filter(inactiveReferredCorporateFilter).length} + color="rose" + /> + setPage("corporate")} + Icon={BsBank} + label="Corporate" + value={users.filter(corporateFilter).length} + color="purple" + /> + setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" /> + setPage("paymentpending")} + Icon={BsCurrencyDollar} + label="Pending Payment" + value={pending.length} + color="rose" + /> +
-
-
- Latest Referred Corporate -
- {users - .filter(referredCorporateFilter) - .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) - .map((x) => ( - - ))} -
-
-
- Latest corporate -
- {users - .filter(corporateFilter) - .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) - .map((x) => ( - - ))} -
-
-
- Referenced corporate expiring in 1 month -
- {users - .filter( - (x) => - referredCorporateFilter(x) && - moment().isAfter( - moment(x.subscriptionExpirationDate).subtract(30, "days") - ) && - moment().isBefore(moment(x.subscriptionExpirationDate)) - ) - .map((x) => ( - - ))} -
-
-
- - ); +
+
+ Latest Referred Corporate +
+ {users + .filter(referredCorporateFilter) + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest corporate +
+ {users + .filter(corporateFilter) + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Referenced corporate expiring in 1 month +
+ {users + .filter( + (x) => + referredCorporateFilter(x) && + moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) && + moment().isBefore(moment(x.subscriptionExpirationDate)), + ) + .map((x) => ( + + ))} +
+
+
+ + ); - return ( - <> - setSelectedUser(undefined)}> - <> - {selectedUser && ( -
- { - setSelectedUser(undefined); - if (shouldReload) reload(); - }} - onViewStudents={ - selectedUser.type === "corporate" || - selectedUser.type === "teacher" - ? () => setPage("students") - : undefined - } - onViewTeachers={ - selectedUser.type === "corporate" - ? () => setPage("teachers") - : undefined - } - user={selectedUser} - /> -
- )} - -
- {page === "referredCorporate" && } - {page === "corporate" && } - {page === "inactiveReferredCorporate" && ( - - )} - {page === "paymentdone" && } - {page === "paymentpending" && } - {page === "" && } - - ); + return ( + <> + setSelectedUser(undefined)}> + <> + {selectedUser && ( +
+ { + setSelectedUser(undefined); + if (shouldReload) reload(); + }} + onViewStudents={ + selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined + } + onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined} + user={selectedUser} + /> +
+ )} + +
+ {page === "referredCorporate" && } + {page === "corporate" && } + {page === "inactiveReferredCorporate" && } + {page === "paymentdone" && } + {page === "paymentpending" && } + {page === "" && } + + ); } diff --git a/src/dashboards/AssignmentCard.tsx b/src/dashboards/AssignmentCard.tsx index 6d1c608f..06c68e82 100644 --- a/src/dashboards/AssignmentCard.tsx +++ b/src/dashboards/AssignmentCard.tsx @@ -11,8 +11,10 @@ import {useAssignmentArchive} from "@/hooks/useAssignmentArchive"; import {uniqBy} from "lodash"; import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive"; import {getUserName} from "@/utils/users"; +import {User} from "@/interfaces/user"; interface Props { + users: User[]; onClick?: () => void; allowDownload?: boolean; reload?: Function; @@ -37,9 +39,8 @@ export default function AssignmentCard({ allowArchive, allowUnarchive, allowExcelDownload, + users, }: Assignment & Props) { - const {users} = useUsers(); - const renderPdfIcon = usePDFDownload("assignments"); const renderExcelIcon = usePDFDownload("assignments", "excel"); const renderArchiveIcon = useAssignmentArchive(id, reload); diff --git a/src/dashboards/AssignmentCreator.tsx b/src/dashboards/AssignmentCreator.tsx index b62bd856..aa4cc716 100644 --- a/src/dashboards/AssignmentCreator.tsx +++ b/src/dashboards/AssignmentCreator.tsx @@ -371,7 +371,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro !startDate || !endDate || assignees.length === 0 || - (!!examIDs && examIDs.length < selectedModules.length) + (!useRandomExams && examIDs.length < selectedModules.length) } className="w-full max-w-[200px]" onClick={createAssignment} diff --git a/src/dashboards/Corporate.tsx b/src/dashboards/Corporate.tsx index d9e8e52e..1f50539a 100644 --- a/src/dashboards/Corporate.tsx +++ b/src/dashboards/Corporate.tsx @@ -29,7 +29,7 @@ import { } from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; -import {calculateAverageLevel, calculateBandScore} from "@/utils/score"; +import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score"; import {MODULE_ARRAY} from "@/utils/moduleUtils"; import {Module} from "@/interfaces"; import {groupByExam} from "@/utils/stats"; @@ -45,11 +45,113 @@ import AssignmentView from "./AssignmentView"; import AssignmentCreator from "./AssignmentCreator"; import clsx from "clsx"; import AssignmentCard from "./AssignmentCard"; +import {createColumnHelper} from "@tanstack/react-table"; +import Checkbox from "@/components/Low/Checkbox"; +import List from "@/components/List"; +import {getUserCompanyName} from "@/resources/user"; interface Props { user: CorporateUser; } +type StudentPerformanceItem = User & {corporateName: string; group: string}; +const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]}) => { + const [isShowingAmount, setIsShowingAmount] = useState(false); + + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.accessor("name", { + header: "Student Name", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("email", { + header: "E-mail", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("demographicInformation.passport_id", { + header: "ID", + cell: (info) => info.getValue() || "N/A", + }), + columnHelper.accessor("group", { + header: "Group", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("corporateName", { + header: "Corporate", + cell: (info) => info.getValue() || "N/A", + }), + columnHelper.accessor("levels.reading", { + header: "Reading", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`, + }), + columnHelper.accessor("levels.listening", { + header: "Listening", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`, + }), + columnHelper.accessor("levels.writing", { + header: "Writing", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`, + }), + columnHelper.accessor("levels.speaking", { + header: "Speaking", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`, + }), + columnHelper.accessor("levels.level", { + header: "Level", + cell: (info) => + !isShowingAmount + ? info.getValue() || 0 + : `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`, + }), + columnHelper.accessor("levels", { + id: "overall_level", + header: "Overall", + cell: (info) => + !isShowingAmount + ? averageLevelCalculator( + users, + stats.filter((x) => x.user === info.row.original.id), + ).toFixed(1) + : `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`, + }), + ]; + + return ( +
+ + Show Utilization + + + data={items.sort( + (a, b) => + averageLevelCalculator( + users, + stats.filter((x) => x.user === b.id), + ) - + averageLevelCalculator( + users, + stats.filter((x) => x.user === a.id), + ), + )} + columns={columns} + /> +
+ ); +}; + export default function CorporateDashboard({user}: Props) { const [page, setPage] = useState(""); const [selectedUser, setSelectedUser] = useState(); @@ -57,11 +159,12 @@ export default function CorporateDashboard({user}: Props) { const [corporateUserToShow, setCorporateUserToShow] = useState(); const [selectedAssignment, setSelectedAssignment] = useState(); const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); + const [userBalance, setUserBalance] = useState(0); const {stats} = useStats(); - const {users, reload} = useUsers(); + const {users, reload, isLoading} = useUsers(); const {codes} = useCodes(user.id); - const {groups} = useGroups(user.id); + const {groups} = useGroups({admin: user.id}); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); @@ -71,6 +174,14 @@ export default function CorporateDashboard({user}: Props) { setShowModal(!!selectedUser && page === ""); }, [selectedUser, page]); + useEffect(() => { + const relatedGroups = groups.filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate"); + const usersInGroups = relatedGroups.map((x) => x.participants).flat(); + const filteredCodes = codes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)); + + setUserBalance(usersInGroups.length + filteredCodes.length); + }, [codes, groups]); + useEffect(() => { // in this case it fetches the master corporate account getUserCorporate(user.id).then(setCorporateUserToShow); @@ -228,7 +339,7 @@ export default function CorporateDashboard({user}: Props) {

Active Assignments ({assignments.filter(activeFilter).length})

{assignments.filter(activeFilter).map((a) => ( - setSelectedAssignment(a)} key={a.id} /> + setSelectedAssignment(a)} key={a.id} /> ))}
@@ -244,6 +355,7 @@ export default function CorporateDashboard({user}: Props) { {assignments.filter(futureFilter).map((a) => ( { setSelectedAssignment(a); setIsCreatingAssignment(true); @@ -259,6 +371,7 @@ export default function CorporateDashboard({user}: Props) { {assignments.filter(pastFilter).map((a) => ( setSelectedAssignment(a)} key={a.id} allowDownload @@ -275,6 +388,7 @@ export default function CorporateDashboard({user}: Props) { {assignments.filter(archivedFilter).map((a) => ( setSelectedAssignment(a)} key={a.id} allowDownload @@ -289,6 +403,36 @@ export default function CorporateDashboard({user}: Props) { ); }; + const StudentPerformancePage = () => { + const students = users + .filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id)) + .map((u) => ({ + ...u, + group: groups.find((x) => x.participants.includes(u.id))?.name || "N/A", + corporateName: getUserCompanyName(u, users, groups), + })); + + return ( + <> +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+
+ Reload + +
+
+ + + ); + }; + const averageLevelCalculator = (studentStats: Stat[]) => { const formattedStats = studentStats .map((s) => ({ @@ -352,7 +496,7 @@ export default function CorporateDashboard({user}: Props) { + setPage("studentsPerformance")} + /> - + +
@@ -619,23 +860,24 @@ export default function MasterCorporateDashboard({ user }: Props) { .includes(x.id), }); - router.push("/list/users"); - } - : undefined - } - user={selectedUser} - /> -
- )} - - - {page === "students" && } - {page === "teachers" && } - {page === "groups" && } - {page === "corporate" && } - {page === "assignments" && } + router.push("/list/users"); + } + : undefined + } + user={selectedUser} + /> +
+ )} + + + {page === "students" && } + {page === "teachers" && } + {page === "groups" && } + {page === "corporate" && } + {page === "assignments" && } + {page === "studentsPerformance" && } {page === "statistical" && } - {page === "" && } - - ); -} + {page === "" && } + + ); +} \ No newline at end of file diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index d4238b67..243df376 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -63,7 +63,7 @@ export default function TeacherDashboard({user}: Props) { const {stats} = useStats(); const {users, reload} = useUsers(); - const {groups} = useGroups(user.id); + const {groups} = useGroups({adminAdmins: user.id}); const {permissions} = usePermissions(user.id); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id}); @@ -193,7 +193,7 @@ export default function TeacherDashboard({user}: Props) { ? groups .filter((g) => g.admin === selectedUser.id) .flatMap((g) => g.participants) - .includes(x.id) || false + .includes(x.id) : groups.flatMap((g) => g.participants).includes(x.id)), )} assigner={user.id} @@ -222,7 +222,7 @@ export default function TeacherDashboard({user}: Props) {

Active Assignments ({assignments.filter(activeFilter).length})

{assignments.filter(activeFilter).map((a) => ( - setSelectedAssignment(a)} key={a.id} /> + setSelectedAssignment(a)} key={a.id} /> ))}
@@ -238,6 +238,7 @@ export default function TeacherDashboard({user}: Props) { {assignments.filter(futureFilter).map((a) => ( { setSelectedAssignment(a); setIsCreatingAssignment(true); @@ -253,6 +254,7 @@ export default function TeacherDashboard({user}: Props) { {assignments.filter(pastFilter).map((a) => ( setSelectedAssignment(a)} key={a.id} allowDownload @@ -268,6 +270,7 @@ export default function TeacherDashboard({user}: Props) { {assignments.filter(archivedFilter).map((a) => ( setSelectedAssignment(a)} key={a.id} allowDownload diff --git a/src/exams/Level.tsx b/src/exams/Level.tsx index 8f1043e2..ea360929 100644 --- a/src/exams/Level.tsx +++ b/src/exams/Level.tsx @@ -1,55 +1,191 @@ import BlankQuestionsModal from "@/components/BlankQuestionsModal"; -import {renderExercise} from "@/components/Exercises"; +import { renderExercise } from "@/components/Exercises"; +import HighlightContent from "@/components/HighlightContent"; import Button from "@/components/Low/Button"; import ModuleTitle from "@/components/Medium/ModuleTitle"; -import {renderSolution} from "@/components/Solutions"; -import {infoButtonStyle} from "@/constants/buttonStyles"; -import {LevelExam, LevelPart, UserSolution, WritingExam} from "@/interfaces/exam"; +import { renderSolution } from "@/components/Solutions"; +import { infoButtonStyle } from "@/constants/buttonStyles"; +import { Module } from "@/interfaces"; +import { Exercise, FillBlanksExercise, FillBlanksMCOption, LevelExam, LevelPart, MultipleChoiceExercise, ShuffleMap, UserSolution, WritingExam } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; -import {defaultUserSolutions} from "@/utils/exams"; -import {countExercises} from "@/utils/moduleUtils"; -import {mdiArrowRight} from "@mdi/js"; +import { defaultUserSolutions } from "@/utils/exams"; +import { countExercises } from "@/utils/moduleUtils"; +import { mdiArrowRight } from "@mdi/js"; import Icon from "@mdi/react"; import clsx from "clsx"; -import {Fragment, useEffect, useState} from "react"; -import {BsChevronDown, BsChevronUp} from "react-icons/bs"; -import {toast} from "react-toastify"; +import { Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState } from "react"; +import { BsChevronDown, BsChevronUp } from "react-icons/bs"; +import { toast } from "react-toastify"; interface Props { exam: LevelExam; showSolutions?: boolean; onFinish: (userSolutions: UserSolution[]) => void; + editing?: boolean; } -function TextComponent({part}: {part: LevelPart}) { +function TextComponent({ + part, highlightPhrases, contextWord, setContextWordLine +}: { + part: LevelPart, highlightPhrases: string[], contextWord: string | undefined, setContextWordLine: React.Dispatch> +}) { + const textRef = useRef(null); + const [lineNumbers, setLineNumbers] = useState([]); + const [lineHeight, setLineHeight] = useState(0); + part.showContextLines = true; + + const calculateLineNumbers = () => { + if (textRef.current) { + const computedStyle = window.getComputedStyle(textRef.current); + const lineHeightValue = parseFloat(computedStyle.lineHeight); + const containerWidth = textRef.current.clientWidth; + setLineHeight(lineHeightValue); + + const offscreenElement = document.createElement('div'); + offscreenElement.style.position = 'absolute'; + offscreenElement.style.top = '-9999px'; + offscreenElement.style.left = '-9999px'; + offscreenElement.style.whiteSpace = 'pre-wrap'; + offscreenElement.style.width = `${containerWidth}px`; + offscreenElement.style.font = computedStyle.font; + offscreenElement.style.lineHeight = computedStyle.lineHeight; + offscreenElement.style.wordWrap = 'break-word'; + offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign; + + const textContent = textRef.current.textContent || ''; + textContent.split(/(\s+)/).forEach((word: string) => { + const span = document.createElement('span'); + span.textContent = word; + offscreenElement.appendChild(span); + }); + + document.body.appendChild(offscreenElement); + + const lines: string[][] = [[]]; + let currentLine = 1; + let currentLineTop: number | undefined; + let contextWordLine: number | null = null; + + const firstChild = offscreenElement.firstChild as HTMLElement; + if (firstChild) { + currentLineTop = firstChild.getBoundingClientRect().top; + } + + const spans = offscreenElement.querySelectorAll('span'); + + spans.forEach(span => { + const rect = span.getBoundingClientRect(); + const top = rect.top; + + if (currentLineTop !== undefined && top > currentLineTop) { + currentLine++; + currentLineTop = top; + lines.push([]); + } + + lines[lines.length - 1].push(span.textContent?.trim() || ''); + + + if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) { + contextWordLine = currentLine; + } + }); + + setLineNumbers(lines.map((_, index) => index + 1)); + if (contextWordLine) { + setContextWordLine(contextWordLine); + } + + document.body.removeChild(offscreenElement); + } + }; + + useEffect(() => { + calculateLineNumbers(); + + const resizeObserver = new ResizeObserver(() => { + calculateLineNumbers(); + }); + + if (textRef.current) { + resizeObserver.observe(textRef.current); + } + + return () => { + if (textRef.current) { + resizeObserver.unobserve(textRef.current); + } + }; + }, [part.context, part.showContextLines, contextWord]); + + if (typeof part.showContextLines === "undefined") { + return ( +
+
+ {!!part.context && + part.context + .split(/\n|(\\n)/g) + .filter((x) => x && x.length > 0 && x !== "\\n") + .map((line, index) => ( + +

{line}

+
+ ))} +
+ ); + } + return (
- {!!part.context && - part.context - .split(/\n|(\\n)/g) - .filter((x) => x && x.length > 0 && x !== "\\n") - .map((line, index) => ( - -

{line}

-
+
+
+ {lineNumbers.map(num => ( +
+ {num} +
))} +
+
+ +
+
); } -export default function Level({exam, showSolutions = false, onFinish}: Props) { - const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]); + +const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => { + return Array.isArray(words) && words.every( + word => word && typeof word === 'object' && 'id' in word && 'options' in word + ); +} + + +export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) { + const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]); const [showBlankModal, setShowBlankModal] = useState(false); - const {userSolutions, setUserSolutions} = useExamStore((state) => state); - const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state); - const {partIndex, setPartIndex} = useExamStore((state) => state); - const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state); + const { userSolutions, setUserSolutions } = useExamStore((state) => state); + const { hasExamEnded, setHasExamEnded } = useExamStore((state) => state); + const { partIndex, setPartIndex } = useExamStore((state) => state); + const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state); const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]); + //const [shuffleMaps, setShuffleMaps] = useExamStore((state) => [state.shuffleMaps, state.setShuffleMaps]) + const [currentExercise, setCurrentExercise] = useState(); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); + const [highlightPhrases, setContextHighlight] = useState([]); + const [contextWord, setContextWord] = useState(undefined); + const [contextWordLine, setContextWordLine] = useState(undefined); + + /*useEffect(() => { + if (showSolutions && userSolutions[exerciseIndex].shuffleMaps) { + setShuffleMaps(userSolutions[exerciseIndex].shuffleMaps as ShuffleMap[]) + } + }, [showSolutions])*/ + useEffect(() => { if (hasExamEnded && exerciseIndex === -1) { setExerciseIndex(exerciseIndex + 1); @@ -65,15 +201,136 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { onFinish(userSolutions); }; + + const getExercise = () => { + if (exerciseIndex === -1) { + return undefined; + } + let exercise = exam.parts[partIndex]?.exercises[exerciseIndex]; + if (!exercise) return undefined; + + exercise = { + ...exercise, + userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], + }; + + /*if (exam.shuffle && exercise.type === "multipleChoice") { + if (shuffleMaps.length == 0 && !showSolutions) { + console.log("Shuffling answers"); + const newShuffleMaps: ShuffleMap[] = []; + + exercise.questions = exercise.questions.map(question => { + const options = [...question.options]; + let shuffledOptions = [...options].sort(() => Math.random() - 0.5); + + const newOptions = options.map((option, index) => ({ + id: option.id, + text: shuffledOptions[index].text + })); + + const optionMapping = options.reduce<{ [key: string]: string }>((acc, originalOption) => { + const shuffledPosition = newOptions.find(newOpt => newOpt.text === originalOption.text)?.id; + if (shuffledPosition) { + acc[shuffledPosition] = originalOption.id; + } + return acc; + }, {}); + + newShuffleMaps.push({ id: question.id, map: optionMapping }); + + return { ...question, options: newOptions }; + }); + + setShuffleMaps(newShuffleMaps); + } else { + console.log("Retrieving shuffles"); + exercise.questions = exercise.questions.map(question => { + const questionShuffleMap = shuffleMaps.find(map => map.id === question.id); + if (questionShuffleMap) { + const newOptions = question.options.map(option => ({ + id: option.id, + text: question.options.find(o => questionShuffleMap.map[o.id] === option.id)?.text || option.text + })); + return { ...question, options: newOptions }; + } + return question; + }); + } + } else if (exam.shuffle && exercise.type === "fillBlanks" && typeCheckWordsMC(exercise.words)) { + if (shuffleMaps.length === 0 && !showSolutions) { + const newShuffleMaps: ShuffleMap[] = []; + + exercise.words = exercise.words.map(word => { + if ('options' in word) { + const options = { ...word.options }; + const originalKeys = Object.keys(options); + const shuffledKeys = [...originalKeys].sort(() => Math.random() - 0.5); + + const newOptions = shuffledKeys.reduce((acc, key, index) => { + acc[key as keyof typeof options] = options[originalKeys[index] as keyof typeof options]; + return acc; + }, {} as { [key in keyof typeof options]: string }); + + const optionMapping = originalKeys.reduce((acc, key, index) => { + acc[key as keyof typeof options] = shuffledKeys[index]; + return acc; + }, {} as { [key in keyof typeof options]: string }); + + newShuffleMaps.push({ id: word.id, map: optionMapping }); + + return { ...word, options: newOptions }; + } + return word; + }); + + setShuffleMaps(newShuffleMaps); + } + } + */ + return exercise; + }; + + useEffect(() => { + //console.log("Getting another exercise"); + //setShuffleMaps([]); + setCurrentExercise(getExercise()); + }, [partIndex, exerciseIndex]); + + + useEffect(() => { + const regex = /.*?['"](.*?)['"] in line (\d+)\?$/; + if (currentExercise && currentExercise.type === "multipleChoice") { + const match = currentExercise.questions[storeQuestionIndex].prompt.match(regex); + if (match) { + const word = match[1]; + const originalLineNumber = match[2]; + setContextHighlight([word]); + + if (word !== contextWord) { + setContextWord(word); + } + + const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace( + `in line ${originalLineNumber}`, + `in line ${contextWordLine || originalLineNumber}` + ); + + currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt; + } else { + setContextHighlight([]); + setContextWord(undefined); + } + } + }, [storeQuestionIndex, contextWordLine, exerciseIndex, partIndex]); //, shuffleMaps]); + const nextExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]); } if (storeQuestionIndex > 0) { - const exercise = getExercise(); - setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]); + setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]); } setStoreQuestionIndex(0); @@ -94,6 +351,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { (x) => x === 0, ) && !showSolutions && + !editing && !hasExamEnded ) { setShowBlankModal(true); @@ -103,7 +361,11 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { setHasExamEnded(false); if (solution) { - onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]); + let stat = { ...solution, module: "level" as Module, exam: exam.id } + /*if (exam.shuffle) { + stat.shuffleMaps = shuffleMaps + }*/ + onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...stat }]); } else { onFinish(userSolutions); } @@ -112,26 +374,17 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]); } if (storeQuestionIndex > 0) { - const exercise = getExercise(); - setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]); + setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]); } setStoreQuestionIndex(0); setExerciseIndex(exerciseIndex - 1); }; - const getExercise = () => { - const exercise = exam.parts[partIndex].exercises[exerciseIndex]; - return { - ...exercise, - userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], - }; - }; - const calculateExerciseIndex = () => { if (partIndex === 0) return ( @@ -157,7 +410,12 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { You will be allowed to read the text while doing the exercises
- +
); @@ -171,7 +429,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { exerciseIndex={calculateExerciseIndex()} module="level" totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))} - disableTimer={showSolutions} + disableTimer={showSolutions || editing} />
-1 && exerciseIndex < exam.parts[partIndex].exercises.length && !showSolutions && - renderExercise(getExercise(), exam.id, nextExercise, previousExercise)} + !editing && + currentExercise && + renderExercise(currentExercise, exam.id, nextExercise, previousExercise)} {exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && - showSolutions && + (showSolutions || editing) && renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
{exerciseIndex === -1 && partIndex > 0 && ( diff --git a/src/exams/Selection.tsx b/src/exams/Selection.tsx index b33ea752..95169e27 100644 --- a/src/exams/Selection.tsx +++ b/src/exams/Selection.tsx @@ -1,442 +1,310 @@ /* eslint-disable @next/next/no-img-element */ -import { useState } from "react"; -import { Module } from "@/interfaces"; +import {useState} from "react"; +import {Module} from "@/interfaces"; import clsx from "clsx"; -import { User } from "@/interfaces/user"; +import {User} from "@/interfaces/user"; import ProgressBar from "@/components/Low/ProgressBar"; -import { - BsArrowRepeat, - BsBook, - BsCheck, - BsCheckCircle, - BsClipboard, - BsHeadphones, - BsMegaphone, - BsPen, - BsXCircle, -} from "react-icons/bs"; -import { totalExamsByModule } from "@/utils/stats"; +import {BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs"; +import {totalExamsByModule} from "@/utils/stats"; import useStats from "@/hooks/useStats"; import Button from "@/components/Low/Button"; -import { calculateAverageLevel } from "@/utils/score"; -import { sortByModuleName } from "@/utils/moduleUtils"; -import { capitalize } from "lodash"; +import {calculateAverageLevel} from "@/utils/score"; +import {sortByModuleName} from "@/utils/moduleUtils"; +import {capitalize} from "lodash"; import ProfileSummary from "@/components/ProfileSummary"; -import { Variant } from "@/interfaces/exam"; -import useSessions, { Session } from "@/hooks/useSessions"; +import {Variant} from "@/interfaces/exam"; +import useSessions, {Session} from "@/hooks/useSessions"; import SessionCard from "@/components/Medium/SessionCard"; import useExamStore from "@/stores/examStore"; import moment from "moment"; interface Props { - user: User; - page: "exercises" | "exams"; - onStart: ( - modules: Module[], - avoidRepeated: boolean, - variant: Variant, - ) => void; - disableSelection?: boolean; + user: User; + page: "exercises" | "exams"; + onStart: (modules: Module[], avoidRepeated: boolean, variant: Variant) => void; + disableSelection?: boolean; } -export default function Selection({ - user, - page, - onStart, - disableSelection = false, -}: Props) { - const [selectedModules, setSelectedModules] = useState([]); - const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true); - const [variant, setVariant] = useState("full"); +export default function Selection({user, page, onStart, disableSelection = false}: Props) { + const [selectedModules, setSelectedModules] = useState([]); + const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true); + const [variant, setVariant] = useState("full"); - const { stats } = useStats(user?.id); - const { sessions, isLoading, reload } = useSessions(user.id); + const {stats} = useStats(user?.id); + const {sessions, isLoading, reload} = useSessions(user.id); - const state = useExamStore((state) => state); + const state = useExamStore((state) => state); - const toggleModule = (module: Module) => { - const modules = selectedModules.filter((x) => x !== module); - setSelectedModules((prev) => - prev.includes(module) ? modules : [...modules, module], - ); - }; + const toggleModule = (module: Module) => { + const modules = selectedModules.filter((x) => x !== module); + setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module])); + }; - const loadSession = async (session: Session) => { - state.setSelectedModules(session.selectedModules); - state.setExam(session.exam); - state.setExams(session.exams); - state.setSessionId(session.sessionId); - state.setAssignment(session.assignment); - state.setExerciseIndex(session.exerciseIndex); - state.setPartIndex(session.partIndex); - state.setModuleIndex(session.moduleIndex); - state.setTimeSpent(session.timeSpent); - state.setUserSolutions(session.userSolutions); - state.setShowSolutions(false); - state.setQuestionIndex(session.questionIndex); - }; + const loadSession = async (session: Session) => { + state.setSelectedModules(session.selectedModules); + state.setExam(session.exam); + state.setExams(session.exams); + state.setSessionId(session.sessionId); + state.setAssignment(session.assignment); + state.setExerciseIndex(session.exerciseIndex); + state.setPartIndex(session.partIndex); + state.setModuleIndex(session.moduleIndex); + state.setTimeSpent(session.timeSpent); + state.setUserSolutions(session.userSolutions); + state.setShowSolutions(false); + state.setQuestionIndex(session.questionIndex); + }; - return ( - <> -
- {user && ( - - ), - label: "Reading", - value: totalExamsByModule(stats, "reading"), - tooltip: "The amount of reading exams performed.", - }, - { - icon: ( - - ), - label: "Listening", - value: totalExamsByModule(stats, "listening"), - tooltip: "The amount of listening exams performed.", - }, - { - icon: ( - - ), - label: "Writing", - value: totalExamsByModule(stats, "writing"), - tooltip: "The amount of writing exams performed.", - }, - { - icon: ( - - ), - label: "Speaking", - value: totalExamsByModule(stats, "speaking"), - tooltip: "The amount of speaking exams performed.", - }, - { - icon: ( - - ), - label: "Level", - value: totalExamsByModule(stats, "level"), - tooltip: "The amount of level exams performed.", - }, - ]} - /> - )} + return ( + <> +
+ {user && ( + , + label: "Reading", + value: totalExamsByModule(stats, "reading"), + tooltip: "The amount of reading exams performed.", + }, + { + icon: , + label: "Listening", + value: totalExamsByModule(stats, "listening"), + tooltip: "The amount of listening exams performed.", + }, + { + icon: , + label: "Writing", + value: totalExamsByModule(stats, "writing"), + tooltip: "The amount of writing exams performed.", + }, + { + icon: , + label: "Speaking", + value: totalExamsByModule(stats, "speaking"), + tooltip: "The amount of speaking exams performed.", + }, + { + icon: , + label: "Level", + value: totalExamsByModule(stats, "level"), + tooltip: "The amount of level exams performed.", + }, + ]} + /> + )} -
- About {capitalize(page)} - - {page === "exercises" && ( - <> - In the realm of language acquisition, practice makes perfect, - and our exercises are the key to unlocking your full potential. - Dive into a world of interactive and engaging exercises that - cater to diverse learning styles. From grammar drills that build - a strong foundation to vocabulary challenges that broaden your - lexicon, our exercises are carefully designed to make learning - English both enjoyable and effective. Whether you're - looking to reinforce specific skills or embark on a holistic - language journey, our exercises are your companions in the - pursuit of excellence. Embrace the joy of learning as you - navigate through a variety of activities that cater to every - facet of language acquisition. Your linguistic adventure starts - here! - - )} - {page === "exams" && ( - <> - Welcome to the heart of success on your English language - journey! Our exams are crafted with precision to assess and - enhance your language skills. Each test is a passport to your - linguistic prowess, designed to challenge and elevate your - abilities. Whether you're a beginner or a seasoned learner, - our exams cater to all levels, providing a comprehensive - evaluation of your reading, writing, speaking, and listening - skills. Prepare to embark on a journey of self-discovery and - language mastery as you navigate through our thoughtfully - curated exams. Your success is not just a destination; it's - a testament to your dedication and our commitment to empowering - you with the English language. - - )} - -
+
+ About {capitalize(page)} + + {page === "exercises" && ( + <> + In the realm of language acquisition, practice makes perfect, and our exercises are the key to unlocking your full + potential. Dive into a world of interactive and engaging exercises that cater to diverse learning styles. From grammar + drills that build a strong foundation to vocabulary challenges that broaden your lexicon, our exercises are carefully + designed to make learning English both enjoyable and effective. Whether you're looking to reinforce specific + skills or embark on a holistic language journey, our exercises are your companions in the pursuit of excellence. + Embrace the joy of learning as you navigate through a variety of activities that cater to every facet of language + acquisition. Your linguistic adventure starts here! + + )} + {page === "exams" && ( + <> + Welcome to the heart of success on your English language journey! Our exams are crafted with precision to assess and + enhance your language skills. Each test is a passport to your linguistic prowess, designed to challenge and elevate + your abilities. Whether you're a beginner or a seasoned learner, our exams cater to all levels, providing a + comprehensive evaluation of your reading, writing, speaking, and listening skills. Prepare to embark on a journey of + self-discovery and language mastery as you navigate through our thoughtfully curated exams. Your success is not just a + destination; it's a testament to your dedication and our commitment to empowering you with the English language. + + )} + +
- {sessions.length > 0 && ( -
-
-
- - Unfinished Sessions - - -
-
- - {sessions - .sort((a, b) => moment(b.date).diff(moment(a.date))) - .map((session) => ( - - ))} - -
- )} + {sessions.length > 0 && ( +
+
+
+ Unfinished Sessions + +
+
+ + {sessions + .sort((a, b) => moment(b.date).diff(moment(a.date))) + .map((session) => ( + + ))} + +
+ )} -
-
toggleModule("reading") - : undefined - } - className={clsx( - "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", - selectedModules.includes("reading") || disableSelection - ? "border-mti-purple-light" - : "border-mti-gray-platinum", - )} - > -
- -
- Reading: -

- Expand your vocabulary, improve your reading comprehension and - improve your ability to interpret texts in English. -

- {!selectedModules.includes("reading") && - !selectedModules.includes("level") && - !disableSelection && ( -
- )} - {(selectedModules.includes("reading") || disableSelection) && ( - - )} - {selectedModules.includes("level") && ( - - )} -
-
toggleModule("listening") - : undefined - } - className={clsx( - "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", - selectedModules.includes("listening") || disableSelection - ? "border-mti-purple-light" - : "border-mti-gray-platinum", - )} - > -
- -
- Listening: -

- Improve your ability to follow conversations in English and your - ability to understand different accents and intonations. -

- {!selectedModules.includes("listening") && - !selectedModules.includes("level") && - !disableSelection && ( -
- )} - {(selectedModules.includes("listening") || disableSelection) && ( - - )} - {selectedModules.includes("level") && ( - - )} -
-
toggleModule("writing") - : undefined - } - className={clsx( - "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", - selectedModules.includes("writing") || disableSelection - ? "border-mti-purple-light" - : "border-mti-gray-platinum", - )} - > -
- -
- Writing: -

- Allow you to practice writing in a variety of formats, from simple - paragraphs to complex essays. -

- {!selectedModules.includes("writing") && - !selectedModules.includes("level") && - !disableSelection && ( -
- )} - {(selectedModules.includes("writing") || disableSelection) && ( - - )} - {selectedModules.includes("level") && ( - - )} -
-
toggleModule("speaking") - : undefined - } - className={clsx( - "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", - selectedModules.includes("speaking") || disableSelection - ? "border-mti-purple-light" - : "border-mti-gray-platinum", - )} - > -
- -
- Speaking: -

- You'll have access to interactive dialogs, pronunciation - exercises and speech recordings. -

- {!selectedModules.includes("speaking") && - !selectedModules.includes("level") && - !disableSelection && ( -
- )} - {(selectedModules.includes("speaking") || disableSelection) && ( - - )} - {selectedModules.includes("level") && ( - - )} -
- {!disableSelection && ( -
toggleModule("level") - : undefined - } - className={clsx( - "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", - selectedModules.includes("level") || disableSelection - ? "border-mti-purple-light" - : "border-mti-gray-platinum", - )} - > -
- -
- Level: -

- You'll be able to test your english level with multiple - choice questions. -

- {!selectedModules.includes("level") && - selectedModules.length === 0 && - !disableSelection && ( -
- )} - {(selectedModules.includes("level") || disableSelection) && ( - - )} - {!selectedModules.includes("level") && - selectedModules.length > 0 && ( - - )} -
- )} -
-
-
-
setAvoidRepeatedExams((prev) => !prev)} - > - -
- -
- - Avoid Repeated Questions - -
-
setVariant((prev) => (prev === "full" ? "partial" : "full"))}> - > - -
- -
- Full length exams -
-
-
- -
- -
-
- - ); +
+
toggleModule("reading") : undefined} + className={clsx( + "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", + selectedModules.includes("reading") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum", + )}> +
+ +
+ Reading: +

+ Expand your vocabulary, improve your reading comprehension and improve your ability to interpret texts in English. +

+ {!selectedModules.includes("reading") && !selectedModules.includes("level") && !disableSelection && ( +
+ )} + {(selectedModules.includes("reading") || disableSelection) && ( + + )} + {selectedModules.includes("level") && } +
+
toggleModule("listening") : undefined} + className={clsx( + "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", + selectedModules.includes("listening") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum", + )}> +
+ +
+ Listening: +

+ Improve your ability to follow conversations in English and your ability to understand different accents and intonations. +

+ {!selectedModules.includes("listening") && !selectedModules.includes("level") && !disableSelection && ( +
+ )} + {(selectedModules.includes("listening") || disableSelection) && ( + + )} + {selectedModules.includes("level") && } +
+
toggleModule("writing") : undefined} + className={clsx( + "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", + selectedModules.includes("writing") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum", + )}> +
+ +
+ Writing: +

+ Allow you to practice writing in a variety of formats, from simple paragraphs to complex essays. +

+ {!selectedModules.includes("writing") && !selectedModules.includes("level") && !disableSelection && ( +
+ )} + {(selectedModules.includes("writing") || disableSelection) && ( + + )} + {selectedModules.includes("level") && } +
+
toggleModule("speaking") : undefined} + className={clsx( + "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", + selectedModules.includes("speaking") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum", + )}> +
+ +
+ Speaking: +

+ You'll have access to interactive dialogs, pronunciation exercises and speech recordings. +

+ {!selectedModules.includes("speaking") && !selectedModules.includes("level") && !disableSelection && ( +
+ )} + {(selectedModules.includes("speaking") || disableSelection) && ( + + )} + {selectedModules.includes("level") && } +
+ {!disableSelection && ( +
toggleModule("level") : undefined} + className={clsx( + "bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out", + selectedModules.includes("level") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum", + )}> +
+ +
+ Level: +

You'll be able to test your english level with multiple choice questions.

+ {!selectedModules.includes("level") && selectedModules.length === 0 && !disableSelection && ( +
+ )} + {(selectedModules.includes("level") || disableSelection) && ( + + )} + {!selectedModules.includes("level") && selectedModules.length > 0 && ( + + )} +
+ )} +
+
+
+
setAvoidRepeatedExams((prev) => !prev)}> + +
+ +
+ + Avoid Repeated Questions + +
+
setVariant((prev) => (prev === "full" ? "partial" : "full"))}> + +
+ +
+ Full length exams +
+
+
+ +
+ +
+
+ + ); } diff --git a/src/hooks/useGroups.tsx b/src/hooks/useGroups.tsx index a4d39dc7..f49ab5e5 100644 --- a/src/hooks/useGroups.tsx +++ b/src/hooks/useGroups.tsx @@ -2,32 +2,40 @@ import {Group, User} from "@/interfaces/user"; import axios from "axios"; import {useEffect, useState} from "react"; -export default function useGroups(admin?: string, userType?: string) { +interface Props { + admin?: string; + userType?: string; + adminAdmins?: string; +} + +export default function useGroups({admin, userType, adminAdmins}: Props) { const [groups, setGroups] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); - const isMasterType = userType?.startsWith('master'); + const isMasterType = userType?.startsWith("master"); const getData = () => { setIsLoading(true); - const url = admin ? `/api/groups?admin=${admin}` : "/api/groups"; + const url = admin && !adminAdmins ? `/api/groups?admin=${admin}` : "/api/groups"; axios .get(url) .then((response) => { - if(isMasterType) { - return setGroups(response.data); - } - const filter = (g: Group) => g.admin === admin || g.participants.includes(admin || ""); + if (isMasterType) return setGroups(response.data); - const filteredGroups = admin ? response.data.filter(filter) : response.data; + const filterByAdmins = !!adminAdmins + ? [adminAdmins, ...response.data.filter((g) => g.participants.includes(adminAdmins)).flatMap((g) => g.admin)] + : [admin]; + const adminFilter = (g: Group) => filterByAdmins.includes(g.admin) || g.participants.includes(admin || ""); + + const filteredGroups = !!admin || !!adminAdmins ? response.data.filter(adminFilter) : response.data; return setGroups(admin ? filteredGroups.map((g) => ({...g, disableEditing: g.disableEditing || g.admin !== admin})) : filteredGroups); }) .finally(() => setIsLoading(false)); }; - useEffect(getData, [admin, isMasterType]); + useEffect(getData, [admin, adminAdmins, isMasterType]); return {groups, isLoading, isError, reload: getData}; } diff --git a/src/hooks/usePermissions.tsx b/src/hooks/usePermissions.tsx index 67d49394..5fdf962e 100644 --- a/src/hooks/usePermissions.tsx +++ b/src/hooks/usePermissions.tsx @@ -17,7 +17,6 @@ export default function usePermissions(user: string) { const permissionTypes = response.data .filter((x) => !x.users.includes(user)) .reduce((acc, curr) => [...acc, curr.type], [] as PermissionType[]); - console.log(response.data, permissionTypes); setPermissions(permissionTypes); }) .finally(() => setIsLoading(false)); diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 18c222b2..85a1bc2c 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -12,6 +12,7 @@ interface ExamBase { isDiagnostic: boolean; variant?: Variant; difficulty?: Difficulty; + shuffle?: boolean; createdBy?: string; // option as it has been added later createdAt?: string; // option as it has been added later } @@ -36,6 +37,7 @@ export interface LevelExam extends ExamBase { export interface LevelPart { context?: string; + showContextLines?: boolean; exercises: Exercise[]; } @@ -65,6 +67,7 @@ export interface UserSolution { }; exercise: string; isDisabled?: boolean; + shuffleMaps?: ShuffleMap[] } export interface WritingExam extends ExamBase { @@ -77,7 +80,7 @@ interface WordCounter { limit: number; } -export interface SpeakingExam extends ExamBase { +export interface SpeakingExam extends ExamBase { module: "speaking"; exercises: (SpeakingExercise | InteractiveSpeakingExercise)[]; instructorGender: InstructorGender; @@ -96,8 +99,8 @@ export type Exercise = export interface Evaluation { comment: string; overall: number; - task_response: {[key: string]: number | {grade: number; comment: string}}; - misspelled_pairs?: {correction: string | null; misspelled: string}[]; + task_response: { [key: string]: number | { grade: number; comment: string } }; + misspelled_pairs?: { correction: string | null; misspelled: string }[]; } @@ -110,10 +113,9 @@ type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string }; type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string }; interface InteractiveSpeakingEvaluation extends Evaluation, -InteractivePerfectAnswerType, -InteractiveTranscriptType, -InteractiveFixedTextType -{} + InteractivePerfectAnswerType, + InteractiveTranscriptType, + InteractiveFixedTextType { } interface SpeakingEvaluation extends CommonEvaluation { @@ -187,10 +189,10 @@ export interface InteractiveSpeakingExercise { first_title?: string; second_title?: string; text: string; - prompts: {text: string; video_url: string}[]; + prompts: { text: string; video_url: string }[]; userSolutions: { id: string; - solution: {questionIndex: number; question: string; answer: string}[]; + solution: { questionIndex: number; question: string; answer: string }[]; evaluation?: InteractiveSpeakingEvaluation; }[]; topic?: string; @@ -199,13 +201,23 @@ export interface InteractiveSpeakingExercise { variant?: "initial" | "final"; } +export interface FillBlanksMCOption { + id: string; + options: { + A: string; + B: string; + C: string; + D: string; + } +} + export interface FillBlanksExercise { prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it." type: "fillBlanks"; id: string; - words: (string | {letter: string; word: string})[]; // *EXAMPLE: ["preserve", "unaware"] + words: (string | { letter: string; word: string } | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"] text: string; // *EXAMPLE: "They tried to {{1}} burning" - allowRepetition: boolean; + allowRepetition?: boolean; solutions: { id: string; // *EXAMPLE: "1" solution: string; // *EXAMPLE: "preserve" @@ -214,6 +226,7 @@ export interface FillBlanksExercise { id: string; // *EXAMPLE: "1" solution: string; // *EXAMPLE: "preserve" }[]; + variant?: string; } export interface TrueFalseExercise { @@ -221,7 +234,7 @@ export interface TrueFalseExercise { id: string; prompt: string; // *EXAMPLE: "Select the appropriate option." questions: TrueFalseQuestion[]; - userSolutions: {id: string; solution: "true" | "false" | "not_given"}[]; + userSolutions: { id: string; solution: "true" | "false" | "not_given" }[]; } export interface TrueFalseQuestion { @@ -250,7 +263,7 @@ export interface MatchSentencesExercise { type: "matchSentences"; id: string; prompt: string; - userSolutions: {question: string; option: string}[]; + userSolutions: { question: string; option: string }[]; sentences: MatchSentenceExerciseSentence[]; allowRepetition: boolean; options: MatchSentenceExerciseOption[]; @@ -273,7 +286,7 @@ export interface MultipleChoiceExercise { id: string; prompt: string; // *EXAMPLE: "Select the appropriate option." questions: MultipleChoiceQuestion[]; - userSolutions: {question: string; option: string}[]; + userSolutions: { question: string; option: string }[]; } export interface MultipleChoiceQuestion { @@ -286,4 +299,12 @@ export interface MultipleChoiceQuestion { src?: string; // *EXAMPLE: "https://i.imgur.com/rEbrSqA.png" (only used if the variant is "image") text?: string; // *EXAMPLE: "wallet, pens and novel" (only used if the variant is "text") }[]; + shuffleMap?: Record; +} + +export interface ShuffleMap { + id: string; + map: { + [key: string]: string; + } } diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index ddda1ccb..bd1cad94 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -1,189 +1,162 @@ -import { Module } from "."; -import { InstructorGender } from "./exam"; -import { PermissionType } from "./permissions"; +import {Module} from "."; +import {InstructorGender, ShuffleMap} from "./exam"; +import {PermissionType} from "./permissions"; -export type User = - | StudentUser - | TeacherUser - | CorporateUser - | AgentUser - | AdminUser - | DeveloperUser - | MasterCorporateUser; +export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser; export type UserStatus = "active" | "disabled" | "paymentDue"; export interface BasicUser { - email: string; - name: string; - profilePicture: string; - id: string; - isFirstLogin: boolean; - focus: "academic" | "general"; - levels: { [key in Module]: number }; - desiredLevels: { [key in Module]: number }; - type: Type; - bio: string; - isVerified: boolean; - subscriptionExpirationDate?: null | Date; - registrationDate?: Date; - status: UserStatus; - permissions: PermissionType[], + email: string; + name: string; + profilePicture: string; + id: string; + isFirstLogin: boolean; + focus: "academic" | "general"; + levels: {[key in Module]: number}; + desiredLevels: {[key in Module]: number}; + type: Type; + bio: string; + isVerified: boolean; + subscriptionExpirationDate?: null | Date; + registrationDate?: Date; + status: UserStatus; + permissions: PermissionType[]; + lastLogin?: Date; } export interface StudentUser extends BasicUser { - type: "student"; - preferredGender?: InstructorGender; - demographicInformation?: DemographicInformation; - preferredTopics?: string[]; + type: "student"; + preferredGender?: InstructorGender; + demographicInformation?: DemographicInformation; + preferredTopics?: string[]; } export interface TeacherUser extends BasicUser { - type: "teacher"; - demographicInformation?: DemographicInformation; + type: "teacher"; + demographicInformation?: DemographicInformation; } export interface CorporateUser extends BasicUser { - type: "corporate"; - corporateInformation: CorporateInformation; - demographicInformation?: DemographicCorporateInformation; + type: "corporate"; + corporateInformation: CorporateInformation; + demographicInformation?: DemographicCorporateInformation; } export interface MasterCorporateUser extends BasicUser { - type: "mastercorporate"; - corporateInformation: CorporateInformation; - demographicInformation?: DemographicCorporateInformation; + type: "mastercorporate"; + corporateInformation: CorporateInformation; + demographicInformation?: DemographicCorporateInformation; } export interface AgentUser extends BasicUser { - type: "agent"; - agentInformation: AgentInformation; - demographicInformation?: DemographicInformation; + type: "agent"; + agentInformation: AgentInformation; + demographicInformation?: DemographicInformation; } export interface AdminUser extends BasicUser { - type: "admin"; - demographicInformation?: DemographicInformation; + type: "admin"; + demographicInformation?: DemographicInformation; } export interface DeveloperUser extends BasicUser { - type: "developer"; - preferredGender?: InstructorGender; - demographicInformation?: DemographicInformation; - preferredTopics?: string[]; + type: "developer"; + preferredGender?: InstructorGender; + demographicInformation?: DemographicInformation; + preferredTopics?: string[]; } export interface CorporateInformation { - companyInformation: CompanyInformation; - monthlyDuration: number; - payment?: { - value: number; - currency: string; - commission: number; - }; - referralAgent?: string; + companyInformation: CompanyInformation; + monthlyDuration: number; + payment?: { + value: number; + currency: string; + commission: number; + }; + referralAgent?: string; } export interface AgentInformation { - companyName: string; - commercialRegistration: string; - companyArabName?: string; + companyName: string; + commercialRegistration: string; + companyArabName?: string; } export interface CompanyInformation { - name: string; - userAmount: number; + name: string; + userAmount: number; } export interface DemographicInformation { - country: string; - phone: string; - gender: Gender; - employment: EmploymentStatus; - passport_id?: string; - timezone?: string; + country: string; + phone: string; + gender: Gender; + employment: EmploymentStatus; + passport_id?: string; + timezone?: string; } export interface DemographicCorporateInformation { - country: string; - phone: string; - gender: Gender; - position: string; - timezone?: string; + country: string; + phone: string; + gender: Gender; + position: string; + timezone?: string; } export type Gender = "male" | "female" | "other"; -export type EmploymentStatus = - | "employed" - | "student" - | "self-employed" - | "unemployed" - | "retired" - | "other"; -export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] = - [ - { status: "student", label: "Student" }, - { status: "employed", label: "Employed" }, - { status: "unemployed", label: "Unemployed" }, - { status: "self-employed", label: "Self-employed" }, - { status: "retired", label: "Retired" }, - { status: "other", label: "Other" }, - ]; +export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other"; +export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [ + {status: "student", label: "Student"}, + {status: "employed", label: "Employed"}, + {status: "unemployed", label: "Unemployed"}, + {status: "self-employed", label: "Self-employed"}, + {status: "retired", label: "Retired"}, + {status: "other", label: "Other"}, +]; export interface Stat { - id: string; - user: string; - exam: string; - exercise: string; - session: string; - date: number; - module: Module; - solutions: any[]; - type: string; - timeSpent?: number; - inactivity?: number; - assignment?: string; - score: { - correct: number; - total: number; - missing: number; - }; - isDisabled?: boolean; + id: string; + user: string; + exam: string; + exercise: string; + session: string; + date: number; + module: Module; + solutions: any[]; + type: string; + timeSpent?: number; + inactivity?: number; + assignment?: string; + score: { + correct: number; + total: number; + missing: number; + }; + isDisabled?: boolean; + shuffleMaps?: ShuffleMap[]; } export interface Group { - admin: string; - name: string; - participants: string[]; - id: string; - disableEditing?: boolean; + admin: string; + name: string; + participants: string[]; + id: string; + disableEditing?: boolean; } export interface Code { - code: string; - creator: string; - expiryDate: Date; - type: Type; - creationDate?: string; - userId?: string; - email?: string; - name?: string; - passport_id?: string; + code: string; + creator: string; + expiryDate: Date; + type: Type; + creationDate?: string; + userId?: string; + email?: string; + name?: string; + passport_id?: string; } -export type Type = - | "student" - | "teacher" - | "corporate" - | "admin" - | "developer" - | "agent" - | "mastercorporate"; -export const userTypes: Type[] = [ - "student", - "teacher", - "corporate", - "admin", - "developer", - "agent", - "mastercorporate", -]; +export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate"; +export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"]; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 00000000..d084ccad --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/pages/(admin)/BatchCreateUser.tsx b/src/pages/(admin)/BatchCreateUser.tsx index 43bd8ec8..d0d50c57 100644 --- a/src/pages/(admin)/BatchCreateUser.tsx +++ b/src/pages/(admin)/BatchCreateUser.tsx @@ -111,7 +111,6 @@ export default function BatchCreateUser({user}: {user: User}) { return clear(); } - console.log(information); setInfos(information); } catch { toast.error( diff --git a/src/pages/(admin)/Lists/GroupList.tsx b/src/pages/(admin)/Lists/GroupList.tsx index 4db19d47..47197b36 100644 --- a/src/pages/(admin)/Lists/GroupList.tsx +++ b/src/pages/(admin)/Lists/GroupList.tsx @@ -154,7 +154,13 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => { label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`, }))} options={users - .filter((x) => (user.type === "teacher" ? x.type === "student" : x.type === "student" || x.type === "teacher")) + .filter((x) => + user.type === "teacher" + ? x.type === "student" + : user.type === "corporate" + ? x.type === "student" || x.type === "teacher" + : x.type === "student" || x.type === "teacher" || x.type === "corporate", + ) .map((x) => ({value: x.id, label: `${x.email} - ${x.name}`}))} onChange={(value) => setParticipants(value.map((x) => x.value))} isMulti @@ -201,7 +207,11 @@ export default function GroupList({user}: {user: User}) { const {permissions} = usePermissions(user?.id || ""); const {users} = useUsers(); - const {groups, reload} = useGroups(user && filterTypes.includes(user?.type) ? user.id : undefined, user?.type); + const {groups, reload} = useGroups({ + admin: user && filterTypes.includes(user?.type) ? user.id : undefined, + userType: user?.type, + adminAdmins: user?.type === "teacher" ? user?.id : undefined, + }); useEffect(() => { if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) { diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index cd939522..220022a4 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -58,7 +58,7 @@ export default function UserList({ const {users, reload} = useUsers(); const {permissions} = usePermissions(user?.id || ""); - const {groups} = useGroups(user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined); + const {groups} = useGroups({admin: user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined}); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); @@ -262,10 +262,12 @@ export default function UserList({ cell: ({row, getValue}) => (
(PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}> + onClick={() => + checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) ? setSelectedUser(row.original) : null + }> {getValue()}
), @@ -279,10 +281,10 @@ export default function UserList({ ) as any, cell: (info) => info.getValue() - ? `${countryCodes.findOne("countryCode" as any, info.getValue()).flag} ${ - countries[info.getValue() as unknown as keyof TCountries].name - } (+${countryCodes.findOne("countryCode" as any, info.getValue()).countryCallingCode})` - : "Not available", + ? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${ + countries[info.getValue() as unknown as keyof TCountries]?.name + } (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})` + : "N/A", }), columnHelper.accessor("demographicInformation.phone", { header: ( @@ -291,7 +293,7 @@ export default function UserList({ ) as any, - cell: (info) => info.getValue() || "Not available", + cell: (info) => info.getValue() || "N/A", enableSorting: true, }), columnHelper.accessor( @@ -301,14 +303,23 @@ export default function UserList({ id: "employment", header: ( ) as any, - cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "Not available", + cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A", enableSorting: true, }, ), + columnHelper.accessor("lastLogin", { + header: ( + + ) as any, + cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"), + }), columnHelper.accessor("demographicInformation.gender", { header: ( ) as any, - cell: (info) => capitalize(info.getValue()) || "Not available", + cell: (info) => capitalize(info.getValue()) || "N/A", enableSorting: true, }), { @@ -341,11 +352,13 @@ export default function UserList({ cell: ({row, getValue}) => (
(PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}> - {row.original.type === "corporate" ? row.original.corporateInformation?.companyInformation?.name || getValue() : getValue()} + onClick={() => + checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) ? setSelectedUser(row.original) : null + }> + {getValue()}
), }), @@ -379,7 +392,7 @@ export default function UserList({ columnHelper.accessor("corporateInformation.companyInformation.name", { header: ( ) as any, @@ -388,7 +401,7 @@ export default function UserList({ columnHelper.accessor("subscriptionExpirationDate", { header: ( ) as any, @@ -401,7 +414,7 @@ export default function UserList({ columnHelper.accessor("isVerified", { header: ( ) as any, @@ -464,6 +477,15 @@ export default function UserList({ return 0; } + if (sorter === "lastLogin" || sorter === reverseString("lastLogin")) { + if (!a.lastLogin && b.lastLogin) return sorter === "lastLogin" ? -1 : 1; + if (a.lastLogin && !b.lastLogin) return sorter === "lastLogin" ? 1 : -1; + if (!a.lastLogin && !b.lastLogin) return 0; + if (moment(a.lastLogin).isAfter(b.lastLogin)) return sorter === "lastLogin" ? -1 : 1; + if (moment(b.lastLogin).isAfter(a.lastLogin)) return sorter === "lastLogin" ? 1 : -1; + return 0; + } + if (sorter === "country" || sorter === reverseString("country")) { if (!a.demographicInformation?.country && b.demographicInformation?.country) return sorter === "country" ? -1 : 1; if (a.demographicInformation?.country && !b.demographicInformation?.country) return sorter === "country" ? 1 : -1; diff --git a/src/pages/(admin)/Lists/index.tsx b/src/pages/(admin)/Lists/index.tsx index b69d3892..c397ba74 100644 --- a/src/pages/(admin)/Lists/index.tsx +++ b/src/pages/(admin)/Lists/index.tsx @@ -27,7 +27,7 @@ export default function Lists({user}: {user: User}) { }> User List - {checkAccess(user, ["developer"]) && ( + {checkAccess(user, ["developer", "admin", "corporate", "mastercorporate", "teacher"]) && ( clsx( diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 671712af..57b38e4b 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -12,7 +12,7 @@ import Selection from "@/exams/Selection"; import Speaking from "@/exams/Speaking"; import Writing from "@/exams/Writing"; import useUser from "@/hooks/useUser"; -import {Exam, UserSolution, Variant} from "@/interfaces/exam"; +import {Exam, LevelExam, UserSolution, Variant} from "@/interfaces/exam"; import {Stat} from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; @@ -257,6 +257,7 @@ export default function ExamPage({page}: Props) { user: user?.id || "", date: new Date().getTime(), isDisabled: solution.isDisabled, + shuffleMaps: solution.shuffleMaps, ...(assignment ? {assignment: assignment.id} : {}), })); @@ -459,6 +460,19 @@ export default function ExamPage({page}: Props) { inactivity: totalInactivity, }} onViewResults={(index?: number) => { + if (exams[0].module === "level") { + const levelExam = exams[0] as LevelExam; + const allExercises = levelExam.parts.flatMap(part => part.exercises); + const exerciseOrderMap = new Map(allExercises.map((ex, index) => [ex.id, index])); + const orderedSolutions = userSolutions.slice().sort((a, b) => { + const indexA = exerciseOrderMap.get(a.exercise) ?? Infinity; + const indexB = exerciseOrderMap.get(b.exercise) ?? Infinity; + return indexA - indexB; + }); + setUserSolutions(orderedSolutions); + } else { + setUserSolutions(userSolutions); + } setShowSolutions(true); setModuleIndex(index || 0); setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0); diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index cf04baa2..b081db1d 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -31,7 +31,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) { const {packages} = usePackages(); const {discounts} = useDiscounts(); const {users} = useUsers(); - const {groups} = useGroups(); + const {groups} = useGroups({}); const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id}); useEffect(() => { diff --git a/src/pages/api/assignments/corporate/[id].ts b/src/pages/api/assignments/corporate/[id].ts index 21d1767f..a70f3830 100644 --- a/src/pages/api/assignments/corporate/[id].ts +++ b/src/pages/api/assignments/corporate/[id].ts @@ -36,5 +36,5 @@ async function GET(req: NextApiRequest, res: NextApiResponse) { const assigners = await getAllAssignersByCorporate(id); const assignments = await getAssignmentsByAssigners([...assigners, id]); - res.status(200).json(assignments); + res.status(200).json(uniqBy(assignments, "id")); } diff --git a/src/pages/api/code/index.ts b/src/pages/api/code/index.ts index 937803ee..2d829e64 100644 --- a/src/pages/api/code/index.ts +++ b/src/pages/api/code/index.ts @@ -1,174 +1,160 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from "next"; -import { app } from "@/firebase"; -import { - getFirestore, - setDoc, - doc, - query, - collection, - where, - getDocs, - getDoc, - deleteDoc, -} from "firebase/firestore"; -import { withIronSessionApiRoute } from "iron-session/next"; -import { sessionOptions } from "@/lib/session"; -import { Code, Type } from "@/interfaces/user"; -import { PERMISSIONS } from "@/constants/userPermissions"; -import { uuidv4 } from "@firebase/util"; -import { prepareMailer, prepareMailOptions } from "@/email"; +import type {NextApiRequest, NextApiResponse} from "next"; +import {app} from "@/firebase"; +import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, deleteDoc} from "firebase/firestore"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {Code, Group, Type} from "@/interfaces/user"; +import {PERMISSIONS} from "@/constants/userPermissions"; +import {uuidv4} from "@firebase/util"; +import {prepareMailer, prepareMailOptions} from "@/email"; const db = getFirestore(app); export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === "GET") return get(req, res); - if (req.method === "POST") return post(req, res); - if (req.method === "DELETE") return del(req, res); + if (req.method === "GET") return get(req, res); + if (req.method === "POST") return post(req, res); + if (req.method === "DELETE") return del(req, res); - return res.status(404).json({ ok: false }); + return res.status(404).json({ok: false}); } async function get(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res - .status(401) - .json({ ok: false, reason: "You must be logged in to generate a code!" }); - return; - } + if (!req.session.user) { + res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"}); + return; + } - const { creator } = req.query as { creator?: string }; - const q = query( - collection(db, "codes"), - where("creator", "==", creator || ""), - ); - const snapshot = await getDocs(creator ? q : collection(db, "codes")); + const {creator} = req.query as {creator?: string}; + const q = query(collection(db, "codes"), where("creator", "==", creator || "")); + const snapshot = await getDocs(creator ? q : collection(db, "codes")); - res.status(200).json(snapshot.docs.map((doc) => doc.data())); + res.status(200).json(snapshot.docs.map((doc) => doc.data())); } async function post(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res - .status(401) - .json({ ok: false, reason: "You must be logged in to generate a code!" }); - return; - } + if (!req.session.user) { + res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"}); + return; + } - const { type, codes, infos, expiryDate } = req.body as { - type: Type; - codes: string[]; - infos?: { email: string; name: string; passport_id?: string }[]; - expiryDate: null | Date; - }; - const permission = PERMISSIONS.generateCode[type]; + const {type, codes, infos, expiryDate} = req.body as { + type: Type; + codes: string[]; + infos?: {email: string; name: string; passport_id?: string}[]; + expiryDate: null | Date; + }; + const permission = PERMISSIONS.generateCode[type]; - if (!permission.includes(req.session.user.type)) { - res.status(403).json({ - ok: false, - reason: - "Your account type does not have permissions to generate a code for that type of user!", - }); - return; - } + if (!permission.includes(req.session.user.type)) { + res.status(403).json({ + ok: false, + reason: "Your account type does not have permissions to generate a code for that type of user!", + }); + return; + } - const codesGeneratedByUserSnapshot = await getDocs( - query(collection(db, "codes"), where("creator", "==", req.session.user.id)), - ); - const userCodes = codesGeneratedByUserSnapshot.docs.map((x) => ({ - ...x.data(), - })); + const codesGeneratedByUserSnapshot = await getDocs(query(collection(db, "codes"), where("creator", "==", req.session.user.id))); + const creatorGroupsSnapshot = await getDocs(query(collection(db, "groups"), where("admin", "==", req.session.user.id))); - if (req.session.user.type === "corporate") { - const totalCodes = codesGeneratedByUserSnapshot.docs.length + codes.length; - const allowedCodes = - req.session.user.corporateInformation?.companyInformation.userAmount || 0; + const creatorGroups = ( + creatorGroupsSnapshot.docs.map((x) => ({ + ...x.data(), + })) as Group[] + ).filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate"); - if (totalCodes > allowedCodes) { - res.status(403).json({ - ok: false, - reason: `You have or would have exceeded your amount of allowed codes, you currently are allowed to generate ${ - allowedCodes - codesGeneratedByUserSnapshot.docs.length - } codes.`, - }); - return; - } - } + const usersInGroups = creatorGroups.flatMap((x) => x.participants); + const userCodes = codesGeneratedByUserSnapshot.docs.map((x) => ({ + ...x.data(), + })) as Code[]; - const codePromises = codes.map(async (code, index) => { - const codeRef = doc(db, "codes", code); - let codeInformation = { - type, - code, - creator: req.session.user!.id, - creationDate: new Date().toISOString(), - expiryDate, - }; + if (req.session.user.type === "corporate") { + const totalCodes = userCodes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)).length + usersInGroups.length + codes.length; + const allowedCodes = req.session.user.corporateInformation?.companyInformation.userAmount || 0; - if (infos && infos.length > index) { - const { email, name, passport_id } = infos[index]; - const previousCode = userCodes.find((x) => x.email === email) as Code; + if (totalCodes > allowedCodes) { + res.status(403).json({ + ok: false, + reason: `You have or would have exceeded your amount of allowed codes, you currently are allowed to generate ${ + allowedCodes - codesGeneratedByUserSnapshot.docs.length + } codes.`, + }); + return; + } + } - const transport = prepareMailer(); - const mailOptions = prepareMailOptions( - { - type, - code: previousCode ? previousCode.code : code, - environment: process.env.ENVIRONMENT, - }, - [email.toLowerCase().trim()], - "EnCoach Registration", - "main", - ); + const codePromises = codes.map(async (code, index) => { + const codeRef = doc(db, "codes", code); + let codeInformation = { + type, + code, + creator: req.session.user!.id, + creationDate: new Date().toISOString(), + expiryDate, + }; - try { - await transport.sendMail(mailOptions); + if (infos && infos.length > index) { + const {email, name, passport_id} = infos[index]; + const previousCode = userCodes.find((x) => x.email === email) as Code; - if (!previousCode) { - await setDoc( - codeRef, - { - ...codeInformation, - email: email.trim().toLowerCase(), - name: name.trim(), - ...(passport_id ? { passport_id: passport_id.trim() } : {}), - }, - { merge: true }, - ); - } + const transport = prepareMailer(); + const mailOptions = prepareMailOptions( + { + type, + code: previousCode ? previousCode.code : code, + environment: process.env.ENVIRONMENT, + }, + [email.toLowerCase().trim()], + "EnCoach Registration", + "main", + ); - return true; - } catch (e) { - return false; - } - } else { - await setDoc(codeRef, codeInformation); - } - }); + try { + await transport.sendMail(mailOptions); - Promise.all(codePromises).then((results) => { - res.status(200).json({ ok: true, valid: results.filter((x) => x).length }); - }); + if (!previousCode) { + await setDoc( + codeRef, + { + ...codeInformation, + email: email.trim().toLowerCase(), + 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((results) => { + res.status(200).json({ok: true, valid: results.filter((x) => x).length}); + }); } async function del(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res - .status(401) - .json({ ok: false, reason: "You must be logged in to generate a code!" }); - return; - } + if (!req.session.user) { + res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"}); + return; + } - const codes = req.query.code as string[]; + const codes = req.query.code as string[]; - for (const code of codes) { - const snapshot = await getDoc(doc(db, "codes", code as string)); - if (!snapshot.exists()) continue; + for (const code of codes) { + const snapshot = await getDoc(doc(db, "codes", code as string)); + if (!snapshot.exists()) continue; - await deleteDoc(snapshot.ref); - } + await deleteDoc(snapshot.ref); + } - res.status(200).json({ codes }); + res.status(200).json({codes}); } diff --git a/src/pages/api/user.ts b/src/pages/api/user.ts index 6d9c176b..6240aa18 100644 --- a/src/pages/api/user.ts +++ b/src/pages/api/user.ts @@ -107,10 +107,12 @@ async function get(req: NextApiRequest, res: NextApiResponse) { } const user = docUser.data() as User; + await setDoc(docUser.ref, {lastLogin: new Date().toISOString()}, {merge: true}); req.session.user = { ...user, id: req.session.user.id, + lastLogin: new Date(), }; await req.session.save(); diff --git a/src/pages/groups.tsx b/src/pages/groups.tsx new file mode 100644 index 00000000..6e037316 --- /dev/null +++ b/src/pages/groups.tsx @@ -0,0 +1,119 @@ +/* eslint-disable @next/next/no-img-element */ +import Head from "next/head"; +import Navbar from "@/components/Navbar"; +import {BsFileEarmarkText, BsPencil, BsStar, BsBook, BsHeadphones, BsPen, BsMegaphone} from "react-icons/bs"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {useEffect, useState} from "react"; +import useStats from "@/hooks/useStats"; +import {averageScore, groupBySession, totalExams} from "@/utils/stats"; +import useUser from "@/hooks/useUser"; +import Diagnostic from "@/components/Diagnostic"; +import {ToastContainer} from "react-toastify"; +import {capitalize} from "lodash"; +import {Module} from "@/interfaces"; +import ProgressBar from "@/components/Low/ProgressBar"; +import Layout from "@/components/High/Layout"; +import {calculateAverageLevel} from "@/utils/score"; +import axios from "axios"; +import DemographicInformationInput from "@/components/DemographicInformationInput"; +import moment from "moment"; +import Link from "next/link"; +import {MODULE_ARRAY} from "@/utils/moduleUtils"; +import ProfileSummary from "@/components/ProfileSummary"; +import StudentDashboard from "@/dashboards/Student"; +import AdminDashboard from "@/dashboards/Admin"; +import CorporateDashboard from "@/dashboards/Corporate"; +import TeacherDashboard from "@/dashboards/Teacher"; +import AgentDashboard from "@/dashboards/Agent"; +import MasterCorporateDashboard from "@/dashboards/MasterCorporate"; +import PaymentDue from "./(status)/PaymentDue"; +import {useRouter} from "next/router"; +import {PayPalScriptProvider} from "@paypal/react-paypal-js"; +import {CorporateUser, MasterCorporateUser, Type, User, userTypes} from "@/interfaces/user"; +import Select from "react-select"; +import {USER_TYPE_LABELS} from "@/resources/user"; +import {checkAccess, getTypesOfUser} from "@/utils/permissions"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import useGroups from "@/hooks/useGroups"; +import useUsers from "@/hooks/useUsers"; +import {getUserName} from "@/utils/users"; + +export const getServerSideProps = withIronSessionSsr(({req, res}) => { + const user = req.session.user; + + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + return { + props: {user: req.session.user}, + }; +}, sessionOptions); + +interface Props { + user: User; + envVariables: {[key: string]: string}; +} +export default function Home(props: Props) { + const {user, mutateUser} = useUser({redirectTo: "/login"}); + const {groups} = useGroups({}); + const {users} = useUsers(); + + const router = useRouter(); + + useEffect(() => { + console.log(groups); + }, [groups]); + + return ( + <> + + EnCoach + + + + + + {user && ( + +
+ {groups + .filter((x) => x.participants.includes(user.id)) + .map((group) => ( +
+ + Group: + {group.name} + + + Admin: + {getUserName(users.find((x) => x.id === group.admin))} + + Participants: + {group.participants.map((x) => getUserName(users.find((u) => u.id === x))).join(", ")} +
+ ))} +
+
+ )} + + ); +} diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 5e1fcae9..f4d78455 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -1,818 +1,659 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import { withIronSessionSsr } from "iron-session/next"; -import { sessionOptions } from "@/lib/session"; -import { - ChangeEvent, - Dispatch, - ReactNode, - SetStateAction, - useEffect, - useRef, - useState, -} from "react"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState} from "react"; import useUser from "@/hooks/useUser"; -import { toast, ToastContainer } from "react-toastify"; +import {toast, ToastContainer} from "react-toastify"; import Layout from "@/components/High/Layout"; import Input from "@/components/Low/Input"; import Button from "@/components/Low/Button"; import Link from "next/link"; import axios from "axios"; -import { ErrorMessage } from "@/constants/errors"; +import {ErrorMessage} from "@/constants/errors"; import clsx from "clsx"; -import { - CorporateUser, - EmploymentStatus, - EMPLOYMENT_STATUS, - Gender, - User, - DemographicInformation, -} from "@/interfaces/user"; +import {CorporateUser, EmploymentStatus, EMPLOYMENT_STATUS, Gender, User, DemographicInformation} from "@/interfaces/user"; import CountrySelect from "@/components/Low/CountrySelect"; -import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; import moment from "moment"; -import { BsCamera, BsQuestionCircleFill } from "react-icons/bs"; -import { USER_TYPE_LABELS } from "@/resources/user"; +import {BsCamera, BsQuestionCircleFill} from "react-icons/bs"; +import {USER_TYPE_LABELS} from "@/resources/user"; import useGroups from "@/hooks/useGroups"; import useUsers from "@/hooks/useUsers"; -import { convertBase64 } from "@/utils"; -import { Divider } from "primereact/divider"; +import {convertBase64} from "@/utils"; +import {Divider} from "primereact/divider"; import GenderInput from "@/components/High/GenderInput"; import EmploymentStatusInput from "@/components/High/EmploymentStatusInput"; import TimezoneSelect from "@/components/Low/TImezoneSelect"; import Modal from "@/components/Modal"; -import { Module } from "@/interfaces"; +import {Module} from "@/interfaces"; import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector"; import Select from "@/components/Low/Select"; -import { InstructorGender } from "@/interfaces/exam"; -import { capitalize } from "lodash"; +import {InstructorGender} from "@/interfaces/exam"; +import {capitalize} from "lodash"; import TopicModal from "@/components/Medium/TopicModal"; -import { v4 } from "uuid"; -import { checkAccess, getTypesOfUser } from "@/utils/permissions"; +import {v4} from "uuid"; +import {checkAccess, getTypesOfUser} from "@/utils/permissions"; -export const getServerSideProps = withIronSessionSsr(({ req, res }) => { - const user = req.session.user; +export const getServerSideProps = withIronSessionSsr(({req, res}) => { + const user = req.session.user; - if (!user || !user.isVerified) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } - if (shouldRedirectHome(user)) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } - return { - props: { user: req.session.user }, - }; + return { + props: {user: req.session.user}, + }; }, sessionOptions); interface Props { - user: User; - mutateUser: Function; + user: User; + mutateUser: Function; } -const DoubleColumnRow = ({ children }: { children: ReactNode }) => ( -
{children}
-); +const DoubleColumnRow = ({children}: {children: ReactNode}) =>
{children}
; -function UserProfile({ user, mutateUser }: Props) { - const [bio, setBio] = useState(user.bio || ""); - const [name, setName] = useState(user.name || ""); - const [email, setEmail] = useState(user.email || ""); - const [password, setPassword] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [profilePicture, setProfilePicture] = useState(user.profilePicture); +function UserProfile({user, mutateUser}: Props) { + const [bio, setBio] = useState(user.bio || ""); + const [name, setName] = useState(user.name || ""); + const [email, setEmail] = useState(user.email || ""); + const [password, setPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [profilePicture, setProfilePicture] = useState(user.profilePicture); - const [desiredLevels, setDesiredLevels] = useState< - { [key in Module]: number } | undefined - >( - checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined - ); - const [focus, setFocus] = useState<"academic" | "general">(user.focus); + const [desiredLevels, setDesiredLevels] = useState<{[key in Module]: number} | undefined>( + checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined, + ); + const [focus, setFocus] = useState<"academic" | "general">(user.focus); - const [country, setCountry] = useState( - user.demographicInformation?.country || "" - ); - const [phone, setPhone] = useState( - user.demographicInformation?.phone || "" - ); - const [gender, setGender] = useState( - user.demographicInformation?.gender || undefined - ); - const [employment, setEmployment] = useState( - checkAccess(user, ["corporate", "mastercorporate"]) - ? undefined - : (user.demographicInformation as DemographicInformation)?.employment - ); - const [passport_id, setPassportID] = useState( - checkAccess(user, ["student"]) - ? (user.demographicInformation as DemographicInformation)?.passport_id - : undefined - ); + const [country, setCountry] = useState(user.demographicInformation?.country || ""); + const [phone, setPhone] = useState(user.demographicInformation?.phone || ""); + const [gender, setGender] = useState(user.demographicInformation?.gender || undefined); + const [employment, setEmployment] = useState( + checkAccess(user, ["corporate", "mastercorporate"]) ? undefined : (user.demographicInformation as DemographicInformation)?.employment, + ); + const [passport_id, setPassportID] = useState( + checkAccess(user, ["student"]) ? (user.demographicInformation as DemographicInformation)?.passport_id : undefined, + ); - const [preferredGender, setPreferredGender] = useState< - InstructorGender | undefined - >( - user.type === "student" || user.type === "developer" - ? user.preferredGender || "varied" - : undefined - ); - const [preferredTopics, setPreferredTopics] = useState( - user.type === "student" || user.type === "developer" - ? user.preferredTopics - : undefined - ); + const [preferredGender, setPreferredGender] = useState( + user.type === "student" || user.type === "developer" ? user.preferredGender || "varied" : undefined, + ); + const [preferredTopics, setPreferredTopics] = useState( + user.type === "student" || user.type === "developer" ? user.preferredTopics : undefined, + ); - const [position, setPosition] = useState( - user.type === "corporate" - ? user.demographicInformation?.position - : undefined - ); - const [corporateInformation, setCorporateInformation] = useState( - user.type === "corporate" ? user.corporateInformation : undefined - ); - const [companyName, setCompanyName] = useState( - user.type === "agent" ? user.agentInformation?.companyName : undefined - ); - const [commercialRegistration, setCommercialRegistration] = useState< - string | undefined - >( - user.type === "agent" - ? user.agentInformation?.commercialRegistration - : undefined - ); - const [arabName, setArabName] = useState( - user.type === "agent" ? user.agentInformation?.companyArabName : undefined - ); + const [position, setPosition] = useState(user.type === "corporate" ? user.demographicInformation?.position : undefined); + const [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : undefined); + const [companyName, setCompanyName] = useState(user.type === "agent" ? user.agentInformation?.companyName : undefined); + const [commercialRegistration, setCommercialRegistration] = useState( + user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, + ); + const [arabName, setArabName] = useState(user.type === "agent" ? user.agentInformation?.companyArabName : undefined); - const [timezone, setTimezone] = useState( - user.demographicInformation?.timezone || moment.tz.guess() - ); + const [timezone, setTimezone] = useState(user.demographicInformation?.timezone || moment.tz.guess()); - const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false); + const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false); - const { groups } = useGroups(); - const { users } = useUsers(); + const {groups} = useGroups({}); + const {users} = useUsers(); - const profilePictureInput = useRef(null); - const expirationDateColor = (date: Date) => { - const momentDate = moment(date); - const today = moment(new Date()); + const profilePictureInput = useRef(null); + const expirationDateColor = (date: Date) => { + const momentDate = moment(date); + const today = moment(new Date()); - if (today.add(1, "days").isAfter(momentDate)) - return "!bg-mti-red-ultralight border-mti-red-light"; - if (today.add(3, "days").isAfter(momentDate)) - return "!bg-mti-rose-ultralight border-mti-rose-light"; - if (today.add(7, "days").isAfter(momentDate)) - return "!bg-mti-orange-ultralight border-mti-orange-light"; - }; + if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light"; + if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light"; + if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light"; + }; - const uploadProfilePicture = async (event: ChangeEvent) => { - if (event.target.files && event.target.files[0]) { - const picture = event.target.files[0]; - const base64 = await convertBase64(picture); - setProfilePicture(base64 as string); - } - }; + const uploadProfilePicture = async (event: ChangeEvent) => { + if (event.target.files && event.target.files[0]) { + const picture = event.target.files[0]; + const base64 = await convertBase64(picture); + setProfilePicture(base64 as string); + } + }; - const updateUser = async () => { - setIsLoading(true); - if (email !== user?.email && !password) { - toast.error("To update your e-mail you need to input your password!"); - setIsLoading(false); - return; - } + const updateUser = async () => { + setIsLoading(true); + if (email !== user?.email && !password) { + toast.error("To update your e-mail you need to input your password!"); + setIsLoading(false); + return; + } - if (newPassword && !password) { - toast.error( - "To update your password you need to input your current one!" - ); - setIsLoading(false); - return; - } + if (newPassword && !password) { + toast.error("To update your password you need to input your current one!"); + setIsLoading(false); + return; + } - if (email !== user?.email) { - const userAdmins = groups - .filter((x) => x.participants.includes(user.id)) - .map((x) => x.admin); - const message = - users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate") - .length > 0 - ? "If you change your e-mail address, you will lose all benefits from your university/institute. Are you sure you want to continue?" - : "Are you sure you want to update your e-mail address?"; + if (email !== user?.email) { + const userAdmins = groups.filter((x) => x.participants.includes(user.id)).map((x) => x.admin); + const message = + users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate").length > 0 + ? "If you change your e-mail address, you will lose all benefits from your university/institute. Are you sure you want to continue?" + : "Are you sure you want to update your e-mail address?"; - if (!confirm(message)) { - setIsLoading(false); - return; - } - } + if (!confirm(message)) { + setIsLoading(false); + return; + } + } - axios - .post("/api/users/update", { - bio, - name, - email, - password, - newPassword, - profilePicture, - desiredLevels, - preferredGender, - preferredTopics, - focus, - demographicInformation: { - phone, - country, - employment: user?.type === "corporate" ? undefined : employment, - position: user?.type === "corporate" ? position : undefined, - gender, - passport_id, - timezone, - }, - ...(user.type === "corporate" ? { corporateInformation } : {}), - ...(user.type === "agent" - ? { - agentInformation: { - companyName, - commercialRegistration, - arabName, - }, - } - : {}), - }) - .then((response) => { - if (response.status === 200) { - toast.success("Your profile has been updated!"); - mutateUser((response.data as { user: User }).user); - setIsLoading(false); - return; - } - }) - .catch((error) => { - console.log(error); - toast.error((error.response.data as ErrorMessage).message); - }) - .finally(() => { - setIsLoading(false); - }); - }; + axios + .post("/api/users/update", { + bio, + name, + email, + password, + newPassword, + profilePicture, + desiredLevels, + preferredGender, + preferredTopics, + focus, + demographicInformation: { + phone, + country, + employment: user?.type === "corporate" ? undefined : employment, + position: user?.type === "corporate" ? position : undefined, + gender, + passport_id, + timezone, + }, + ...(user.type === "corporate" ? {corporateInformation} : {}), + ...(user.type === "agent" + ? { + agentInformation: { + companyName, + commercialRegistration, + arabName, + }, + } + : {}), + }) + .then((response) => { + if (response.status === 200) { + toast.success("Your profile has been updated!"); + mutateUser((response.data as {user: User}).user); + setIsLoading(false); + return; + } + }) + .catch((error) => { + console.log(error); + toast.error((error.response.data as ErrorMessage).message); + }) + .finally(() => { + setIsLoading(false); + }); + }; - const ExpirationDate = () => ( -
- - - {!user.subscriptionExpirationDate && "Unlimited"} - {user.subscriptionExpirationDate && - moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")} - -
- ); + const ExpirationDate = () => ( +
+ + + {!user.subscriptionExpirationDate && "Unlimited"} + {user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")} + +
+ ); - const TimezoneInput = () => ( -
- - -
- ); + const TimezoneInput = () => ( +
+ + +
+ ); - const manualDownloadLink = ["student", "teacher", "corporate"].includes( - user.type - ) - ? `/manuals/${user.type}.pdf` - : ""; + const manualDownloadLink = ["student", "teacher", "corporate"].includes(user.type) ? `/manuals/${user.type}.pdf` : ""; - return ( - -
-

Edit Profile

-
-
-

Edit Profile

-
e.preventDefault()} - > - - {user.type !== "corporate" ? ( - setName(e)} - placeholder="Enter your name" - defaultValue={name} - required - /> - ) : ( - - setCorporateInformation((prev) => ({ - ...prev!, - companyInformation: { - ...prev!.companyInformation, - name: e, - }, - })) - } - placeholder="Enter your company's name" - defaultValue={corporateInformation?.companyInformation.name} - required - /> - )} + return ( + +
+

Edit Profile

+
+
+

Edit Profile

+ e.preventDefault()}> + + {user.type !== "corporate" ? ( + setName(e)} + placeholder="Enter your name" + defaultValue={name} + required + /> + ) : ( + + setCorporateInformation((prev) => ({ + ...prev!, + companyInformation: { + ...prev!.companyInformation, + name: e, + }, + })) + } + placeholder="Enter your company's name" + defaultValue={corporateInformation?.companyInformation.name} + required + /> + )} - {user.type === "agent" && ( - setArabName(e)} - placeholder="Enter your arab name" - defaultValue={arabName} - required - /> - )} + {user.type === "agent" && ( + setArabName(e)} + placeholder="Enter your arab name" + defaultValue={arabName} + required + /> + )} - setEmail(e)} - placeholder="Enter email address" - defaultValue={email} - required - /> - - - setPassword(e)} - placeholder="Enter your password" - required - /> - setNewPassword(e)} - placeholder="Enter your new password (optional)" - /> - - {user.type === "agent" && ( -
- null} - placeholder="Enter your company's name" - defaultValue={companyName} - disabled - /> - null} - placeholder="Enter commercial registration" - defaultValue={commercialRegistration} - disabled - /> -
- )} + setEmail(e)} + placeholder="Enter email address" + defaultValue={email} + required + /> + + + setPassword(e)} + placeholder="Enter your password" + required + /> + setNewPassword(e)} + placeholder="Enter your new password (optional)" + /> + + {user.type === "agent" && ( +
+ null} + placeholder="Enter your company's name" + defaultValue={companyName} + disabled + /> + null} + placeholder="Enter commercial registration" + defaultValue={commercialRegistration} + disabled + /> +
+ )} - -
- - -
- setPhone(e)} - placeholder="Enter phone number" - defaultValue={phone} - required - /> -
+ +
+ + +
+ setPhone(e)} + placeholder="Enter phone number" + defaultValue={phone} + required + /> +
- {user.type === "student" ? ( - - setPassportID(e)} - placeholder="Enter National ID or Passport number" - value={passport_id} - required - /> - - - ) : ( - - )} + {user.type === "student" ? ( + + setPassportID(e)} + placeholder="Enter National ID or Passport number" + value={passport_id} + required + /> + + + ) : ( + + )} - + - {desiredLevels && - ["developer", "student"].includes(user.type) && ( - <> -
- - - > - } - /> -
-
- -
- - -
-
- - )} + {desiredLevels && ["developer", "student"].includes(user.type) && ( + <> +
+ + >} + /> +
+
+ +
+ + +
+
+ + )} - {preferredGender && - ["developer", "student"].includes(user.type) && ( - <> - - -
- - (value ? setPreferredGender(value.value as InstructorGender) : null)} + options={[ + {value: "male", label: "Male"}, + {value: "female", label: "Female"}, + {value: "varied", label: "Varied"}, + ]} + /> +
+
+ + +
+
- setIsPreferredTopicsOpen(false)} - selectTopics={setPreferredTopics} - initialTopics={preferredTopics || []} - /> + setIsPreferredTopicsOpen(false)} + selectTopics={setPreferredTopics} + initialTopics={preferredTopics || []} + /> - - - )} + + + )} - {user.type === "corporate" && ( - <> - - null} - label="Number of users" - defaultValue={ - user.corporateInformation.companyInformation.userAmount - } - disabled - required - /> - null} - label="Pricing" - defaultValue={`${user.corporateInformation.payment?.value} ${user.corporateInformation.payment?.currency}`} - disabled - required - /> - - - - )} + {user.type === "corporate" && ( + <> + + null} + label="Number of users" + defaultValue={user.corporateInformation.companyInformation.userAmount} + disabled + required + /> + null} + label="Pricing" + defaultValue={`${user.corporateInformation.payment?.value} ${user.corporateInformation.payment?.currency}`} + disabled + required + /> + + + + )} - {user.type === "corporate" && ( - <> - - - setName(e)} - placeholder="Enter your name" - defaultValue={name} - required - /> - - - - )} + {user.type === "corporate" && ( + <> + + + setName(e)} + placeholder="Enter your name" + defaultValue={name} + required + /> + + + + )} - {user.type === "corporate" && - user.corporateInformation.referralAgent && ( - <> - - - null} - defaultValue={ - users.find( - (x) => - x.id === user.corporateInformation.referralAgent - )?.name - } - type="text" - label="Country Manager's Name" - placeholder="Not available" - required - disabled - /> - null} - defaultValue={ - users.find( - (x) => - x.id === user.corporateInformation.referralAgent - )?.email - } - type="text" - label="Country Manager's E-mail" - placeholder="Not available" - required - disabled - /> - - -
- - - x.id === user.corporateInformation.referralAgent - )?.demographicInformation?.country - } - onChange={() => null} - disabled - /> -
+ {user.type === "corporate" && user.corporateInformation.referralAgent && ( + <> + + + null} + defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.name} + type="text" + label="Country Manager's Name" + placeholder="Not available" + required + disabled + /> + null} + defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.email} + type="text" + label="Country Manager's E-mail" + placeholder="Not available" + required + disabled + /> + + +
+ + x.id === user.corporateInformation.referralAgent)?.demographicInformation + ?.country + } + onChange={() => null} + disabled + /> +
- null} - placeholder="Not available" - defaultValue={ - users.find( - (x) => - x.id === user.corporateInformation.referralAgent - )?.demographicInformation?.phone - } - disabled - required - /> -
- - )} + null} + placeholder="Not available" + defaultValue={ + users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation?.phone + } + disabled + required + /> +
+ + )} - {user.type !== "corporate" && ( - - + {user.type !== "corporate" && ( + + -
- - -
-
- )} - -
-
-
(profilePictureInput.current as any)?.click()} - > -
-
- -
- {user.name} -
- - (profilePictureInput.current as any)?.click()} - className="cursor-pointer text-mti-purple-light text-sm" - > - Change picture - -
- {USER_TYPE_LABELS[user.type]} -
-
- {user.type === "agent" && ( -
- { -
- )} - {manualDownloadLink && ( - - - - )} -
-
-
- Bio -