Compare commits
28 Commits
faeture/pa
...
improvemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ecc5be05 | ||
|
|
77af0b3495 | ||
|
|
e2e38284a7 | ||
|
|
ccd2560451 | ||
|
|
390658f2b0 | ||
|
|
450a4e9fe3 | ||
|
|
dfbbf0456d | ||
|
|
d46f92edb2 | ||
|
|
26c4368f31 | ||
|
|
ec56a5426b | ||
|
|
fe32584ff9 | ||
|
|
db7762c6e2 | ||
|
|
e70e26f84c | ||
|
|
7dc9d568d1 | ||
|
|
0049ab272b | ||
|
|
f48885bba6 | ||
|
|
5eaa0ac269 | ||
|
|
f7af21878e | ||
|
|
9d4071d4cd | ||
|
|
6f5dd86cd1 | ||
|
|
8b9537b272 | ||
|
|
a526e76c70 | ||
|
|
62b2f477f4 | ||
|
|
f36384fdb4 | ||
|
|
9c8d7988c5 | ||
|
|
18f163768c | ||
|
|
72083439af | ||
|
|
523149327b |
28
.vscode/launch.json
vendored
Normal file
28
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Next.js: debug server-side",
|
||||||
|
"type": "node-terminal",
|
||||||
|
"request": "launch",
|
||||||
|
"command": "npm run dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Next.js: debug client-side",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:3000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Next.js: debug full stack",
|
||||||
|
"type": "node-terminal",
|
||||||
|
"request": "launch",
|
||||||
|
"command": "npm run dev",
|
||||||
|
"serverReadyAction": {
|
||||||
|
"pattern": "- Local:.+(https?://.+)",
|
||||||
|
"uriFormat": "%s",
|
||||||
|
"action": "debugWithChrome"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ export default function DemographicInformationInput({user, mutateUser}: Props) {
|
|||||||
<form className="flex flex-col items-center justify-items-center gap-6 w-full h-full -md:px-4 lg:w-1/2 mb-32" onSubmit={save}>
|
<form className="flex flex-col items-center justify-items-center gap-6 w-full h-full -md:px-4 lg:w-1/2 mb-32" onSubmit={save}>
|
||||||
{user.type === "agent" && (
|
{user.type === "agent" && (
|
||||||
<div className="w-full flex gap-8">
|
<div className="w-full flex gap-8">
|
||||||
<Input type="text" onChange={setCompanyName} name="companyName" label="Company Name" required />
|
<Input type="text" onChange={setCompanyName} name="companyName" label="Corporate Name" required />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
onChange={setCommercialRegistration}
|
onChange={setCommercialRegistration}
|
||||||
|
|||||||
@@ -93,22 +93,8 @@ export default function Writing({
|
|||||||
)}
|
)}
|
||||||
<div className="flex flex-col h-full w-full gap-9 mb-20">
|
<div className="flex flex-col h-full w-full gap-9 mb-20">
|
||||||
<div className="flex flex-col w-full gap-7 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
<div className="flex flex-col w-full gap-7 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
||||||
<span>
|
<span className="whitespace-pre-wrap">{prefix}</span>
|
||||||
{prefix.split("\\n").map((line, index) => (
|
<span className="font-semibold whitespace-pre-wrap">{prompt}</span>
|
||||||
<React.Fragment key={index}>
|
|
||||||
{line}
|
|
||||||
<br />
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{prompt.split("\\n").map((line, index) => (
|
|
||||||
<Fragment key={index}>
|
|
||||||
<p>{line}</p>
|
|
||||||
<br />
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
{attachment && (
|
{attachment && (
|
||||||
<img
|
<img
|
||||||
onClick={() => setIsModalOpen(true)}
|
onClick={() => setIsModalOpen(true)}
|
||||||
@@ -120,14 +106,7 @@ export default function Writing({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full h-full flex flex-col gap-4">
|
<div className="w-full h-full flex flex-col gap-4">
|
||||||
<span>
|
<span className="whitespace-pre-wrap">{suffix}</span>
|
||||||
{suffix.split("\\n").map((line, index) => (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
{line}
|
|
||||||
<br />
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
<textarea
|
<textarea
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
className="w-full h-full min-h-[300px] cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl"
|
className="w-full h-full min-h-[300px] cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import Button from "../Low/Button";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {speakingReverseMarking} from "@/utils/score";
|
import {speakingReverseMarking} from "@/utils/score";
|
||||||
|
import {Tab} from "@headlessui/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ export default function InteractiveSpeaking({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<span className="font-bold">You should talk about the following things:</span>
|
<span className="font-bold">You should talk about the following things:</span>
|
||||||
<div className="grid grid-cols-3 gap-6 text-center">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 text-center">
|
||||||
{prompts.map((x, index) => (
|
{prompts.map((x, index) => (
|
||||||
<div className="italic flex flex-col gap-2 text-sm" key={index}>
|
<div className="italic flex flex-col gap-2 text-sm" key={index}>
|
||||||
<video key={index} controls className="">
|
<video key={index} controls className="">
|
||||||
@@ -61,11 +63,11 @@ export default function InteractiveSpeaking({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full h-full flex flex-col gap-8">
|
<div className="w-full h-full flex flex-col gap-8">
|
||||||
<div className="flex items-center gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{solutionsURL.map((x, index) => (
|
{solutionsURL.map((x, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="w-full min-w-[460px] p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
||||||
<div className="flex gap-8 items-center justify-center py-8">
|
<div className="flex gap-8 items-center justify-center py-8">
|
||||||
<Waveform audio={x} waveColor="#FCDDEC" progressColor="#EF5DA8" />
|
<Waveform audio={x} waveColor="#FCDDEC" progressColor="#EF5DA8" />
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +75,7 @@ export default function InteractiveSpeaking({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{userSolutions && userSolutions.length > 0 && (
|
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<div className="flex gap-4 px-1">
|
<div className="flex gap-4 px-1">
|
||||||
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
||||||
@@ -82,9 +84,81 @@ export default function InteractiveSpeaking({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full min-h-fit cursor-text px-7 py-8 bg-mti-gray-smoke rounded-3xl">
|
{userSolutions[0].evaluation &&
|
||||||
|
Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).length === 3 ? (
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Evaluation
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Recommended Answer (Prompt 1)
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Recommended Answer (Prompt 2)
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Recommended Answer (Prompt 3)
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
|
{userSolutions[0].evaluation!.perfect_answer_1!.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
|
</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
|
{userSolutions[0].evaluation!.perfect_answer_2!.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
|
</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
|
{userSolutions[0].evaluation!.perfect_answer_3!.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
|
</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full min-h-fit cursor-text px-7 py-8 bg-ielts-speaking/10 rounded-3xl">
|
||||||
{userSolutions[0].evaluation!.comment}
|
{userSolutions[0].evaluation!.comment}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import Button from "../Low/Button";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {speakingReverseMarking} from "@/utils/score";
|
import {speakingReverseMarking} from "@/utils/score";
|
||||||
|
import {Tab} from "@headlessui/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
|||||||
{solutionURL && <Waveform audio={solutionURL} waveColor="#FCDDEC" progressColor="#EF5DA8" />}
|
{solutionURL && <Waveform audio={solutionURL} waveColor="#FCDDEC" progressColor="#EF5DA8" />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{userSolutions && userSolutions.length > 0 && (
|
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<div className="flex gap-4 px-1">
|
<div className="flex gap-4 px-1">
|
||||||
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
||||||
@@ -78,9 +80,48 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full min-h-fit cursor-text px-7 py-8 bg-mti-gray-smoke rounded-3xl">
|
{userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? (
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Evaluation
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Recommended Answer
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
|
{userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
|
</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full min-h-fit cursor-text px-7 py-8 bg-ielts-speaking/10 rounded-3xl">
|
||||||
{userSolutions[0].evaluation!.comment}
|
{userSolutions[0].evaluation!.comment}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
|
||||||
import {WritingExercise} from "@/interfaces/exam";
|
import {WritingExercise} from "@/interfaces/exam";
|
||||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
|
||||||
import Icon from "@mdi/react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import {CommonProps} from ".";
|
import {CommonProps} from ".";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import {Fragment, useState} from "react";
|
||||||
import {toast} from "react-toastify";
|
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import {Dialog, Transition} from "@headlessui/react";
|
import {Dialog, Tab, Transition} from "@headlessui/react";
|
||||||
import {writingReverseMarking} from "@/utils/score";
|
import {writingReverseMarking} from "@/utils/score";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
export default function Writing({id, type, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
|
export default function Writing({id, type, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
@@ -79,7 +75,7 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{userSolutions && userSolutions.length > 0 && (
|
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<div className="flex gap-4 px-1">
|
<div className="flex gap-4 px-1">
|
||||||
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
|
||||||
@@ -88,9 +84,48 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full min-h-fit cursor-text px-7 py-8 bg-mti-gray-smoke rounded-3xl">
|
{userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? (
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-writing/20 p-1">
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-writing",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Evaluation
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-writing",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Recommended Answer
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
|
{userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
|
</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full min-h-fit cursor-text px-7 py-8 bg-ielts-writing/10 rounded-3xl">
|
||||||
{userSolutions[0].evaluation!.comment}
|
{userSolutions[0].evaluation!.comment}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
);
|
);
|
||||||
const [userAmount, setUserAmount] = useState(user.type === "corporate" ? user.corporateInformation?.companyInformation.userAmount : undefined);
|
const [userAmount, setUserAmount] = useState(user.type === "corporate" ? user.corporateInformation?.companyInformation.userAmount : undefined);
|
||||||
const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined);
|
const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined);
|
||||||
const [paymentCurrency, setPaymentCurrency] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.currency : undefined);
|
const [paymentCurrency, setPaymentCurrency] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.currency : "EUR");
|
||||||
const [monthlyDuration, setMonthlyDuration] = useState(user.type === "corporate" ? user.corporateInformation?.monthlyDuration : undefined);
|
const [monthlyDuration, setMonthlyDuration] = useState(user.type === "corporate" ? user.corporateInformation?.monthlyDuration : undefined);
|
||||||
|
|
||||||
const {stats} = useStats(user.id);
|
const {stats} = useStats(user.id);
|
||||||
@@ -146,11 +146,11 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
||||||
<Input
|
<Input
|
||||||
label="Company Name"
|
label="Corporate Name"
|
||||||
type="text"
|
type="text"
|
||||||
name="companyName"
|
name="companyName"
|
||||||
onChange={setCompanyName}
|
onChange={setCompanyName}
|
||||||
placeholder="Enter company name"
|
placeholder="Enter corporate name"
|
||||||
defaultValue={companyName}
|
defaultValue={companyName}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -159,7 +159,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
type="text"
|
type="text"
|
||||||
name="commercialRegistration"
|
name="commercialRegistration"
|
||||||
onChange={setCommercialRegistration}
|
onChange={setCommercialRegistration}
|
||||||
placeholder="Enter company name"
|
placeholder="Enter commercial registration"
|
||||||
defaultValue={commercialRegistration}
|
defaultValue={commercialRegistration}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -171,11 +171,11 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||||
<Input
|
<Input
|
||||||
label="Company Name"
|
label="Corporate Name"
|
||||||
type="text"
|
type="text"
|
||||||
name="companyName"
|
name="companyName"
|
||||||
onChange={setCompanyName}
|
onChange={setCompanyName}
|
||||||
placeholder="Enter company name"
|
placeholder="Enter corporate name"
|
||||||
defaultValue={companyName}
|
defaultValue={companyName}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ import {
|
|||||||
BsGlobeCentralSouthAsia,
|
BsGlobeCentralSouthAsia,
|
||||||
BsPerson,
|
BsPerson,
|
||||||
BsPersonFill,
|
BsPersonFill,
|
||||||
BsPersonFillGear,
|
BsPencilSquare,
|
||||||
BsPersonGear,
|
BsBank,
|
||||||
BsPersonLinesFill,
|
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
@@ -43,6 +42,8 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
}, [selectedUser, page]);
|
}, [selectedUser, page]);
|
||||||
|
|
||||||
|
const inactiveCountryManagerFilter = (x: User) => x.type === "agent" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
||||||
|
|
||||||
const UserDisplay = (displayUser: User) => (
|
const UserDisplay = (displayUser: User) => (
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedUser(displayUser)}
|
onClick={() => setSelectedUser(displayUser)}
|
||||||
@@ -149,6 +150,24 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const InactiveCountryManagerList = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
onClick={() => setPage("")}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Inactive Country Managers ({users.filter(inactiveCountryManagerFilter).length})</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UserList user={user} filters={[inactiveCountryManagerFilter]} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const InactiveStudentsList = () => {
|
const InactiveStudentsList = () => {
|
||||||
const filter = (x: User) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
const filter = (x: User) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
||||||
|
|
||||||
@@ -200,14 +219,14 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPersonLinesFill}
|
Icon={BsPencilSquare}
|
||||||
label="Teachers"
|
label="Teachers"
|
||||||
value={users.filter((x) => x.type === "teacher").length}
|
value={users.filter((x) => x.type === "teacher").length}
|
||||||
onClick={() => setPage("teachers")}
|
onClick={() => setPage("teachers")}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPersonLinesFill}
|
Icon={BsBank}
|
||||||
label="Corporate"
|
label="Corporate"
|
||||||
value={users.filter((x) => x.type === "corporate").length}
|
value={users.filter((x) => x.type === "corporate").length}
|
||||||
onClick={() => setPage("corporate")}
|
onClick={() => setPage("corporate")}
|
||||||
@@ -236,6 +255,13 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
}
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
|
<IconCard
|
||||||
|
onClick={() => setPage("inactiveCountryManagers")}
|
||||||
|
Icon={BsPerson}
|
||||||
|
label="Inactive Country Managers"
|
||||||
|
value={users.filter(inactiveCountryManagerFilter).length}
|
||||||
|
color="rose"
|
||||||
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("inactiveCorporate")}
|
onClick={() => setPage("inactiveCorporate")}
|
||||||
Icon={BsPerson}
|
Icon={BsPerson}
|
||||||
@@ -298,7 +324,7 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
<span className="p-4">Teachers expiring in 1 month</span>
|
<span className="p-4">Country Manager expiring in 1 month</span>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(
|
.filter(
|
||||||
@@ -342,7 +368,7 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
<span className="p-4">Expired Teachers</span>
|
<span className="p-4">Expired Country Manager</span>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(
|
.filter(
|
||||||
@@ -454,7 +480,9 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
{page === "agents" && <AgentsList />}
|
{page === "agents" && <AgentsList />}
|
||||||
{page === "inactiveStudents" && <InactiveStudentsList />}
|
{page === "inactiveStudents" && <InactiveStudentsList />}
|
||||||
{page === "inactiveCorporate" && <InactiveCorporateList />}
|
{page === "inactiveCorporate" && <InactiveCorporateList />}
|
||||||
|
{page === "inactiveCountryManagers" && <InactiveCountryManagerList />}
|
||||||
{page === "" && <DefaultDashboard />}
|
{page === "" && <DefaultDashboard />}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,8 @@ import moment from "moment";
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {
|
import {
|
||||||
BsArrowLeft,
|
BsArrowLeft,
|
||||||
BsClipboard2Data,
|
|
||||||
BsClipboard2DataFill,
|
|
||||||
BsClock,
|
|
||||||
BsGlobeCentralSouthAsia,
|
|
||||||
BsPaperclip,
|
|
||||||
BsPerson,
|
|
||||||
BsPersonAdd,
|
|
||||||
BsPersonFill,
|
BsPersonFill,
|
||||||
BsPersonFillGear,
|
BsBank
|
||||||
BsPersonGear,
|
|
||||||
BsPersonLinesFill,
|
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
@@ -50,6 +41,7 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
const corporateFilter = (user: User) => user.type === "corporate";
|
const corporateFilter = (user: User) => user.type === "corporate";
|
||||||
const referredCorporateFilter = (x: User) =>
|
const referredCorporateFilter = (x: User) =>
|
||||||
x.type === "corporate" && !!x.corporateInformation && x.corporateInformation.referralAgent === user.id;
|
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: User) => (
|
const UserDisplay = (displayUser: User) => (
|
||||||
<div
|
<div
|
||||||
@@ -68,8 +60,6 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ReferredCorporateList = () => {
|
const ReferredCorporateList = () => {
|
||||||
const filter = (x: User) => x.type === "corporate" && !!x.corporateInformation && x.corporateInformation.referralAgent === user.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -79,10 +69,28 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
<BsArrowLeft className="text-xl" />
|
<BsArrowLeft className="text-xl" />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">Referred Corporate ({users.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">Referred Corporate ({users.filter(referredCorporateFilter).length})</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UserList user={user} filters={[filter]} />
|
<UserList user={user} filters={[referredCorporateFilter]} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InactiveReferredCorporateList = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
onClick={() => setPage("")}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Inactive Referred Corporate ({users.filter(inactiveReferredCorporateFilter).length})</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UserList user={user} filters={[inactiveReferredCorporateFilter]} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -118,8 +126,15 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("corporate")}
|
onClick={() => setPage("inactiveReferredCorporate")}
|
||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
|
label="Inactive Referred Corporate"
|
||||||
|
value={users.filter(inactiveReferredCorporateFilter).length}
|
||||||
|
color="rose"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
onClick={() => setPage("corporate")}
|
||||||
|
Icon={BsBank}
|
||||||
label="Corporate"
|
label="Corporate"
|
||||||
value={users.filter(corporateFilter).length}
|
value={users.filter(corporateFilter).length}
|
||||||
color="purple"
|
color="purple"
|
||||||
@@ -149,6 +164,21 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
|
<span className="p-4">Referenced corporate expiring in 1 month</span>
|
||||||
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
|
{users
|
||||||
|
.filter(
|
||||||
|
(x) =>
|
||||||
|
referredCorporateFilter(x) &&
|
||||||
|
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||||
|
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||||
|
)
|
||||||
|
.map((x) => (
|
||||||
|
<UserDisplay key={x.id} {...x} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -177,6 +207,7 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
</Modal>
|
</Modal>
|
||||||
{page === "referredCorporate" && <ReferredCorporateList />}
|
{page === "referredCorporate" && <ReferredCorporateList />}
|
||||||
{page === "corporate" && <CorporateList />}
|
{page === "corporate" && <CorporateList />}
|
||||||
|
{page === "inactiveReferredCorporate" && <InactiveReferredCorporateList />}
|
||||||
{page === "" && <DefaultDashboard />}
|
{page === "" && <DefaultDashboard />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {getExam} from "@/utils/exams";
|
|||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import {Assignment} from "@/interfaces/results";
|
||||||
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isCreating: boolean;
|
isCreating: boolean;
|
||||||
@@ -35,6 +36,8 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "day").toDate());
|
const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "day").toDate());
|
||||||
const [endDate, setEndDate] = useState<Date | null>(assignment ? moment(assignment.endDate).toDate() : moment().add(8, "day").toDate());
|
const [endDate, setEndDate] = useState<Date | null>(assignment ? moment(assignment.endDate).toDate() : moment().add(8, "day").toDate());
|
||||||
|
// creates a new exam for each assignee or just one exam for all assignees
|
||||||
|
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
||||||
|
|
||||||
const toggleModule = (module: Module) => {
|
const toggleModule = (module: Module) => {
|
||||||
const modules = selectedModules.filter((x) => x !== module);
|
const modules = selectedModules.filter((x) => x !== module);
|
||||||
@@ -48,20 +51,23 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
const createAssignment = () => {
|
const createAssignment = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const examPromises = selectedModules.map(async (module) => getExam(module, false));
|
(assignment ? axios.patch : axios.post)(
|
||||||
Promise.all(examPromises)
|
`/api/assignments${assignment ? `/${assignment.id}` : ""}`,
|
||||||
.then((exams) => {
|
{
|
||||||
(assignment ? axios.patch : axios.post)(`/api/assignments${assignment ? `/${assignment.id}` : ""}`, {
|
|
||||||
assigner,
|
|
||||||
assignees,
|
assignees,
|
||||||
name,
|
name,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
results: [],
|
selectedModules,
|
||||||
exams: exams.map((e) => ({module: e?.module, id: e?.id})),
|
generateMultiple,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`);
|
toast.success(
|
||||||
|
`The assignment "${name}" has been ${
|
||||||
|
assignment ? "updated" : "created"
|
||||||
|
} successfully!`
|
||||||
|
);
|
||||||
cancelCreation();
|
cancelCreation();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -69,12 +75,6 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
toast.error("Something went wrong, please try again later!");
|
toast.error("Something went wrong, please try again later!");
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
toast.error("Something went wrong, please try again later!");
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteAssignment = () => {
|
const deleteAssignment = () => {
|
||||||
@@ -284,6 +284,11 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<div className="flex gap-4 w-full justify-end">
|
||||||
|
<Checkbox isChecked={generateMultiple} onChange={() => setGenerateMultiple(d => !d)}>
|
||||||
|
Generate different exams
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
<div className="flex gap-4 w-full justify-end">
|
<div className="flex gap-4 w-full justify-end">
|
||||||
<Button className="w-full max-w-[200px]" variant="outline" onClick={cancelCreation} disabled={isLoading} isLoading={isLoading}>
|
<Button className="w-full max-w-[200px]" variant="outline" onClick={cancelCreation} disabled={isLoading} isLoading={isLoading}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
BsPersonFill,
|
BsPersonFill,
|
||||||
BsPersonFillGear,
|
BsPersonFillGear,
|
||||||
BsPersonGear,
|
BsPersonGear,
|
||||||
BsPersonLinesFill,
|
BsPencilSquare,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
@@ -170,7 +170,7 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("teachers")}
|
onClick={() => setPage("teachers")}
|
||||||
Icon={BsPersonLinesFill}
|
Icon={BsPencilSquare}
|
||||||
label="Teachers"
|
label="Teachers"
|
||||||
value={users.filter(teacherFilter).length}
|
value={users.filter(teacherFilter).length}
|
||||||
color="purple"
|
color="purple"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function StudentDashboard({user}: Props) {
|
|||||||
const setAssignment = useExamStore((state) => state.setAssignment);
|
const setAssignment = useExamStore((state) => state.setAssignment);
|
||||||
|
|
||||||
const startAssignment = (assignment: Assignment) => {
|
const startAssignment = (assignment: Assignment) => {
|
||||||
const examPromises = assignment.exams.map((e) => getExamById(e.module, e.id));
|
const examPromises = assignment.exams.filter((e) => e.assignee === user.id).map((e) => getExamById(e.module, e.id));
|
||||||
|
|
||||||
Promise.all(examPromises).then((exams) => {
|
Promise.all(examPromises).then((exams) => {
|
||||||
if (exams.every((x) => !!x)) {
|
if (exams.every((x) => !!x)) {
|
||||||
@@ -121,6 +121,7 @@ export default function StudentDashboard({user}: Props) {
|
|||||||
<div className="flex justify-between w-full items-center">
|
<div className="flex justify-between w-full items-center">
|
||||||
<div className="grid grid-cols-2 gap-2 place-items-center justify-center w-fit min-w-[104px] -md:mt-2">
|
<div className="grid grid-cols-2 gap-2 place-items-center justify-center w-fit min-w-[104px] -md:mt-2">
|
||||||
{assignment.exams
|
{assignment.exams
|
||||||
|
.filter((e) => e.assignee === user.id)
|
||||||
.map((e) => e.module)
|
.map((e) => e.module)
|
||||||
.sort(sortByModuleName)
|
.sort(sortByModuleName)
|
||||||
.map((module) => (
|
.map((module) => (
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
BsPersonFill,
|
BsPersonFill,
|
||||||
BsPersonFillGear,
|
BsPersonFillGear,
|
||||||
BsPersonGear,
|
BsPersonGear,
|
||||||
BsPersonLinesFill,
|
|
||||||
BsPlus,
|
BsPlus,
|
||||||
BsRepeat,
|
BsRepeat,
|
||||||
BsRepeat1,
|
BsRepeat1,
|
||||||
|
|||||||
@@ -92,6 +92,17 @@ export interface Evaluation {
|
|||||||
overall: number;
|
overall: number;
|
||||||
task_response: {[key: string]: number};
|
task_response: {[key: string]: number};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InteractiveSpeakingEvaluation extends Evaluation {
|
||||||
|
perfect_answer_1?: string;
|
||||||
|
perfect_answer_2?: string;
|
||||||
|
perfect_answer_3?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommonEvaluation extends Evaluation {
|
||||||
|
perfect_answer?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WritingExercise {
|
export interface WritingExercise {
|
||||||
id: string;
|
id: string;
|
||||||
type: "writing";
|
type: "writing";
|
||||||
@@ -106,7 +117,7 @@ export interface WritingExercise {
|
|||||||
userSolutions: {
|
userSolutions: {
|
||||||
id: string;
|
id: string;
|
||||||
solution: string;
|
solution: string;
|
||||||
evaluation?: Evaluation;
|
evaluation?: CommonEvaluation;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +131,7 @@ export interface SpeakingExercise {
|
|||||||
userSolutions: {
|
userSolutions: {
|
||||||
id: string;
|
id: string;
|
||||||
solution: string;
|
solution: string;
|
||||||
evaluation?: Evaluation;
|
evaluation?: CommonEvaluation;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +144,7 @@ export interface InteractiveSpeakingExercise {
|
|||||||
userSolutions: {
|
userSolutions: {
|
||||||
id: string;
|
id: string;
|
||||||
solution: {question: string; answer: string}[];
|
solution: {question: string; answer: string}[];
|
||||||
evaluation?: Evaluation;
|
evaluation?: InteractiveSpeakingEvaluation;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export interface Assignment {
|
|||||||
type: "academic" | "general";
|
type: "academic" | "general";
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
}[];
|
}[];
|
||||||
exams: {id: string; module: Module}[];
|
exams: {id: string; module: Module, assignee: string}[];
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import {getFirestore, collection, getDocs, query, where, setDoc, doc} from "fire
|
|||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
|
import { Module } from "@/interfaces";
|
||||||
|
import { getExams } from "@/utils/exams.be";
|
||||||
|
import { Exam } from "@/interfaces/exam";
|
||||||
|
import { flatten } from "lodash";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -34,8 +38,107 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
res.status(200).json(docs);
|
res.status(200).json(docs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ExamWithUser {
|
||||||
|
module: Module;
|
||||||
|
id: string;
|
||||||
|
assignee: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomIndex(arr: any[]): number {
|
||||||
|
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||||
|
return randomIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateExams = async (
|
||||||
|
generateMultiple: Boolean,
|
||||||
|
selectedModules: Module[],
|
||||||
|
assignees: string[]
|
||||||
|
): Promise<ExamWithUser[]> => {
|
||||||
|
if (generateMultiple) {
|
||||||
|
// for optimization purposes, it would be better to create a new endpoint that returned the answers for all users at once
|
||||||
|
const allExams = await assignees.map(async (assignee) => {
|
||||||
|
const selectedModulePromises = await selectedModules.map(
|
||||||
|
async (module: Module) => {
|
||||||
|
try {
|
||||||
|
const exams: Exam[] = await getExams(db, module, "true", assignee);
|
||||||
|
|
||||||
|
const exam = exams[getRandomIndex(exams)];
|
||||||
|
if (exam) {
|
||||||
|
return { module: exam.module, id: exam.id, assignee };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const newModules = await Promise.all(selectedModulePromises);
|
||||||
|
|
||||||
|
return newModules;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exams = flatten(await Promise.all(allExams)).filter(
|
||||||
|
(x) => x !== null
|
||||||
|
) as ExamWithUser[];
|
||||||
|
return exams;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedModulePromises = await selectedModules.map(
|
||||||
|
async (module: Module) => {
|
||||||
|
const exams: Exam[] = await getExams(db, module, "false", undefined);
|
||||||
|
const exam = exams[getRandomIndex(exams)];
|
||||||
|
|
||||||
|
if (exam) {
|
||||||
|
return { module: exam.module, id: exam.id };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const exams = await Promise.all(selectedModulePromises);
|
||||||
|
const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[];
|
||||||
|
return flatten(
|
||||||
|
assignees.map((assignee) =>
|
||||||
|
examesFiltered.map((exam) => ({ ...exam, assignee }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
async function POST(req: NextApiRequest, res: NextApiResponse) {
|
async function POST(req: NextApiRequest, res: NextApiResponse) {
|
||||||
await setDoc(doc(db, "assignments", uuidv4()), {assigner: req.session.user?.id, ...req.body});
|
const {
|
||||||
|
selectedModules,
|
||||||
|
assignees,
|
||||||
|
// Generarte multiple true would generate an unique exam for eacah user
|
||||||
|
// false would generate the same exam for all usersa
|
||||||
|
generateMultiple = false,
|
||||||
|
...body
|
||||||
|
} = req.body as {
|
||||||
|
selectedModules: Module[];
|
||||||
|
assignees: string[];
|
||||||
|
generateMultiple: Boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exams: ExamWithUser[] = await generateExams(
|
||||||
|
generateMultiple,
|
||||||
|
selectedModules,
|
||||||
|
assignees
|
||||||
|
);
|
||||||
|
if (exams.length === 0) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ ok: false, error: "No exams found for the selected modules" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await setDoc(doc(db, "assignments", uuidv4()), {
|
||||||
|
assigner: req.session.user?.id,
|
||||||
|
assignees,
|
||||||
|
results: [],
|
||||||
|
exams,
|
||||||
|
...body,
|
||||||
|
});
|
||||||
|
|
||||||
res.status(200).json({ ok: true });
|
res.status(200).json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {app} from "@/firebase";
|
import {app} from "@/firebase";
|
||||||
import {getFirestore, collection, getDocs, query, where, setDoc, doc} from "firebase/firestore";
|
import {getFirestore, setDoc, doc} from "firebase/firestore";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {shuffle} from "lodash";
|
|
||||||
import {Exam} from "@/interfaces/exam";
|
import {Exam} from "@/interfaces/exam";
|
||||||
import {Stat} from "@/interfaces/user";
|
import { getExams } from "@/utils/exams.be";
|
||||||
import {v4} from "uuid";
|
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
@@ -26,31 +23,12 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {module, avoidRepeated} = req.query as {module: string; avoidRepeated: string};
|
const {
|
||||||
const moduleRef = collection(db, module);
|
|
||||||
|
|
||||||
const q = query(moduleRef, where("isDiagnostic", "==", false));
|
|
||||||
const snapshot = await getDocs(q);
|
|
||||||
|
|
||||||
const exams: Exam[] = shuffle(
|
|
||||||
snapshot.docs.map((doc) => ({
|
|
||||||
id: doc.id,
|
|
||||||
...doc.data(),
|
|
||||||
module,
|
module,
|
||||||
})),
|
avoidRepeated,
|
||||||
) as Exam[];
|
} = req.query as {module: string; avoidRepeated: string};
|
||||||
|
|
||||||
if (avoidRepeated === "true") {
|
|
||||||
const statsQ = query(collection(db, "stats"), where("user", "==", req.session.user.id));
|
|
||||||
const statsSnapshot = await getDocs(statsQ);
|
|
||||||
|
|
||||||
const stats: Stat[] = statsSnapshot.docs.map((doc) => ({id: doc.id, ...doc.data()})) as unknown as Stat[];
|
|
||||||
const filteredExams = exams.filter((x) => !stats.map((s) => s.exam).includes(x.id));
|
|
||||||
|
|
||||||
res.status(200).json(filteredExams.length > 0 ? filteredExams : exams);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const exams: Exam[] = await getExams(db, module, avoidRepeated, req.session.user.id);
|
||||||
res.status(200).json(exams);
|
res.status(200).json(exams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ async function registerIndividual(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
bio: "",
|
bio: "",
|
||||||
isFirstLogin: codeData ? codeData.type === "student" : true,
|
isFirstLogin: codeData ? codeData.type === "student" : true,
|
||||||
focus: "academic",
|
focus: "academic",
|
||||||
type: codeData ? codeData.type : "student",
|
type: email.endsWith("@ecrop.dev") ? "developer" : codeData ? codeData.type : "student",
|
||||||
subscriptionExpirationDate: codeData ? codeData.expiryDate : moment().subtract(1, "days").toISOString(),
|
subscriptionExpirationDate: codeData ? codeData.expiryDate : moment().subtract(1, "days").toISOString(),
|
||||||
registrationDate: new Date(),
|
registrationDate: new Date(),
|
||||||
status: code ? "active" : "paymentDue",
|
status: code ? "active" : "paymentDue",
|
||||||
|
|||||||
@@ -75,6 +75,13 @@ const PaymentCreator = ({onClose, reload}: {onClose: () => void; reload: () => v
|
|||||||
setReferralAgent(referralAgent as AgentUser | undefined);
|
setReferralAgent(referralAgent as AgentUser | undefined);
|
||||||
}, [corporate, users]);
|
}, [corporate, users]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const payment = corporate?.corporateInformation?.payment;
|
||||||
|
|
||||||
|
setPrice(payment?.value || 0);
|
||||||
|
setCurrency(payment?.currency || "EUR");
|
||||||
|
}, [corporate]);
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
axios
|
axios
|
||||||
.post(`/api/payments`, {
|
.post(`/api/payments`, {
|
||||||
@@ -138,14 +145,15 @@ const PaymentCreator = ({onClose, reload}: {onClose: () => void; reload: () => v
|
|||||||
name="paymentValue"
|
name="paymentValue"
|
||||||
onChange={(e) => setPrice(e ? parseInt(e) : 0)}
|
onChange={(e) => setPrice(e ? parseInt(e) : 0)}
|
||||||
type="number"
|
type="number"
|
||||||
defaultValue={0}
|
value={price}
|
||||||
className="col-span-3"
|
className="col-span-3"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
className="px-4 col-span-2 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
className="px-4 col-span-2 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||||
options={CURRENCIES.map(({label, currency}) => ({value: currency, label}))}
|
options={CURRENCIES.map(({label, currency}) => ({value: currency, label}))}
|
||||||
defaultValue={{value: "EUR", label: "Euro"}}
|
defaultValue={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||||
onChange={(value) => setCurrency(value?.value || "EUR")}
|
onChange={(value) => setCurrency(value?.value || "EUR")}
|
||||||
|
value={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
@@ -227,11 +235,47 @@ const PaymentCreator = ({onClose, reload}: {onClose: () => void; reload: () => v
|
|||||||
export default function PaymentRecord() {
|
export default function PaymentRecord() {
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
const [isCreatingPayment, setIsCreatingPayment] = useState(false);
|
const [isCreatingPayment, setIsCreatingPayment] = useState(false);
|
||||||
|
const [filters, setFilters] = useState<{filter: (p: Payment) => boolean; id: string}[]>([]);
|
||||||
|
const [displayPayments, setDisplayPayments] = useState<Payment[]>([]);
|
||||||
|
|
||||||
|
const [corporate, setCorporate] = useState<User>();
|
||||||
|
const [agent, setAgent] = useState<User>();
|
||||||
|
|
||||||
const {user} = useUser({redirectTo: "/login"});
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {payments, reload} = usePayments();
|
const {payments, reload} = usePayments();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayPayments(
|
||||||
|
filters
|
||||||
|
.map((f) => f.filter)
|
||||||
|
.reduce((d, f) => d.filter(f), payments)
|
||||||
|
.sort((a, b) => moment(b.date).diff(moment(a.date))),
|
||||||
|
);
|
||||||
|
}, [payments, filters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && user.type === "agent") {
|
||||||
|
setAgent(user);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilters((prev) => [
|
||||||
|
...prev.filter((x) => x.id !== "agent-filter"),
|
||||||
|
...(!agent ? [] : [{id: "agent-filter", filter: (p: Payment) => p.agent === agent.id}]),
|
||||||
|
]);
|
||||||
|
}, [agent]);
|
||||||
|
|
||||||
|
useEffect(() => console.log(filters), [filters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilters((prev) => [
|
||||||
|
...prev.filter((x) => x.id !== "corporate-filter"),
|
||||||
|
...(!corporate ? [] : [{id: "corporate-filter", filter: (p: Payment) => p.corporate === corporate.id}]),
|
||||||
|
]);
|
||||||
|
}, [corporate]);
|
||||||
|
|
||||||
const updatePayment = (payment: Payment, key: string, value: any) => {
|
const updatePayment = (payment: Payment, key: string, value: any) => {
|
||||||
axios
|
axios
|
||||||
.patch(`api/payments/${payment.id}`, {...payment, [key]: value})
|
.patch(`api/payments/${payment.id}`, {...payment, [key]: value})
|
||||||
@@ -272,7 +316,8 @@ export default function PaymentRecord() {
|
|||||||
<div
|
<div
|
||||||
className={clsx("underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer")}
|
className={clsx("underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer")}
|
||||||
onClick={() => setSelectedUser(users.find((x) => x.id === info.row.original.corporate))}>
|
onClick={() => setSelectedUser(users.find((x) => x.id === info.row.original.corporate))}>
|
||||||
{(users.find((x) => x.id === info.row.original.corporate) as CorporateUser)?.corporateInformation.companyInformation.name}
|
{(users.find((x) => x.id === info.row.original.corporate) as CorporateUser)?.corporateInformation.companyInformation.name ||
|
||||||
|
(users.find((x) => x.id === info.row.original.corporate) as CorporateUser)?.name}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
@@ -338,7 +383,7 @@ export default function PaymentRecord() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: (user?.type === "agent" ? payments.filter((p) => p.agent === user.id) : payments).sort((a, b) => moment(b.date).diff(moment(a.date))),
|
data: displayPayments,
|
||||||
columns: defaultColumns,
|
columns: defaultColumns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
@@ -386,6 +431,71 @@ export default function PaymentRecord() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-8 w-full">
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Corporate account *</label>
|
||||||
|
<Select
|
||||||
|
isClearable
|
||||||
|
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||||
|
options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
|
||||||
|
value: user.id,
|
||||||
|
meta: user,
|
||||||
|
label: `${user.corporateInformation.companyInformation.name || user.name} - ${user.email}`,
|
||||||
|
}))}
|
||||||
|
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
||||||
|
styles={{
|
||||||
|
control: (styles) => ({
|
||||||
|
...styles,
|
||||||
|
paddingLeft: "4px",
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
":focus": {
|
||||||
|
outline: "none",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
option: (styles, state) => ({
|
||||||
|
...styles,
|
||||||
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
|
color: state.isFocused ? "black" : styles.color,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Country manager *</label>
|
||||||
|
<Select
|
||||||
|
isClearable
|
||||||
|
isDisabled={user.type === "agent"}
|
||||||
|
className={clsx(
|
||||||
|
"px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none",
|
||||||
|
user.type === "agent" ? "bg-mti-gray-platinum/40" : "bg-white",
|
||||||
|
)}
|
||||||
|
options={(users.filter((u) => u.type === "agent") as AgentUser[]).map((user) => ({
|
||||||
|
value: user.id,
|
||||||
|
meta: user,
|
||||||
|
label: `${user.name} - ${user.email}`,
|
||||||
|
}))}
|
||||||
|
value={agent ? {value: agent?.id, label: `${agent.name} - ${agent.email}`} : undefined}
|
||||||
|
onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)}
|
||||||
|
styles={{
|
||||||
|
control: (styles) => ({
|
||||||
|
...styles,
|
||||||
|
paddingLeft: "4px",
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
":focus": {
|
||||||
|
outline: "none",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
option: (styles, state) => ({
|
||||||
|
...styles,
|
||||||
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
|
color: state.isFocused ? "black" : styles.color,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@@ -214,11 +214,11 @@ export default function Home() {
|
|||||||
{user.type === "agent" && (
|
{user.type === "agent" && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
||||||
<Input
|
<Input
|
||||||
label="Company Name"
|
label="Corporate Name"
|
||||||
type="text"
|
type="text"
|
||||||
name="companyName"
|
name="companyName"
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
placeholder="Enter company name"
|
placeholder="Enter corporate name"
|
||||||
defaultValue={user.agentInformation.companyName}
|
defaultValue={user.agentInformation.companyName}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
@@ -227,7 +227,7 @@ export default function Home() {
|
|||||||
type="text"
|
type="text"
|
||||||
name="commercialRegistration"
|
name="commercialRegistration"
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
placeholder="Enter company name"
|
placeholder="Enter commercial registration"
|
||||||
defaultValue={user.agentInformation.commercialRegistration}
|
defaultValue={user.agentInformation.commercialRegistration}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
|
|||||||
52
src/utils/exams.be.ts
Normal file
52
src/utils/exams.be.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
collection,
|
||||||
|
getDocs,
|
||||||
|
query,
|
||||||
|
where,
|
||||||
|
setDoc,
|
||||||
|
doc,
|
||||||
|
Firestore,
|
||||||
|
} from "firebase/firestore";
|
||||||
|
import { shuffle } from "lodash";
|
||||||
|
import { Exam } from "@/interfaces/exam";
|
||||||
|
import { Stat } from "@/interfaces/user";
|
||||||
|
|
||||||
|
export const getExams = async (
|
||||||
|
db: Firestore,
|
||||||
|
module: string,
|
||||||
|
avoidRepeated: string,
|
||||||
|
// added userId as due to assignments being set from the teacher to the student
|
||||||
|
// we need to make sure we are serving exams not executed by the user and not
|
||||||
|
// by the teacher that performed the request
|
||||||
|
userId: string | undefined
|
||||||
|
): Promise<Exam[]> => {
|
||||||
|
const moduleRef = collection(db, module);
|
||||||
|
|
||||||
|
const q = query(moduleRef, where("isDiagnostic", "==", false));
|
||||||
|
const snapshot = await getDocs(q);
|
||||||
|
|
||||||
|
const exams: Exam[] = shuffle(
|
||||||
|
snapshot.docs.map((doc) => ({
|
||||||
|
id: doc.id,
|
||||||
|
...doc.data(),
|
||||||
|
module,
|
||||||
|
}))
|
||||||
|
) as Exam[];
|
||||||
|
|
||||||
|
if (avoidRepeated === "true") {
|
||||||
|
const statsQ = query(collection(db, "stats"), where("user", "==", userId));
|
||||||
|
const statsSnapshot = await getDocs(statsQ);
|
||||||
|
|
||||||
|
const stats: Stat[] = statsSnapshot.docs.map((doc) => ({
|
||||||
|
id: doc.id,
|
||||||
|
...doc.data(),
|
||||||
|
})) as unknown as Stat[];
|
||||||
|
const filteredExams = exams.filter(
|
||||||
|
(x) => !stats.map((s) => s.exam).includes(x.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredExams.length > 0 ? filteredExams : exams;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exams;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user