Compare commits

...

28 Commits

Author SHA1 Message Date
Tiago Ribeiro
e0ecc5be05 Merge branch 'develop' into improvement-37/writing-evaluation-perfect-answer 2023-12-09 14:47:06 +00:00
João Ramos
77af0b3495 Merged in feature-multiplerandomexams (pull request #1)
Dynamic tests generation of assignment + Minor changes

Approved-by: Tiago Ribeiro
2023-12-09 14:43:48 +00:00
Tiago Ribeiro
e2e38284a7 Uncommented a section 2023-12-09 14:39:03 +00:00
Tiago Ribeiro
ccd2560451 Merged develop into feature-multiplerandomexams 2023-12-09 14:37:34 +00:00
João Ramos
390658f2b0 Merged in feature-removeCompanyReferences (pull request #2)
Changed Comercial labels to Corporate

Approved-by: Tiago Ribeiro
2023-12-09 14:28:30 +00:00
Joao Ramos
450a4e9fe3 Changed Comercial labels to Corporate 2023-12-08 15:43:19 +00:00
Joao Ramos
dfbbf0456d Revert "Changed Comercial labels to Corporate"
This reverts commit 9c8d7988c5.
2023-12-08 14:55:16 +00:00
Joao Ramos
d46f92edb2 Added Referenced corporate expiring in 1 month 2023-12-07 23:42:04 +00:00
Joao Ramos
26c4368f31 Minor improvement on reusability of filter function 2023-12-07 23:34:31 +00:00
Joao Ramos
ec56a5426b Added Inactive Referred corporate 2023-12-07 23:31:16 +00:00
Joao Ramos
fe32584ff9 Add Inactive Country manager 2023-12-07 23:23:39 +00:00
Joao Ramos
db7762c6e2 Replaced Teacher labels 2023-12-07 23:20:19 +00:00
Joao Ramos
e70e26f84c Updated checkbox string 2023-12-07 23:17:23 +00:00
Joao Ramos
7dc9d568d1 Replaced Teachers Icon 2023-12-07 23:13:42 +00:00
Joao Ramos
0049ab272b Added dynamic generation of exams as an option 2023-12-07 23:07:35 +00:00
Joao Ramos
f48885bba6 Updatd UI to display the unique tests for each user in an assignment 2023-12-07 18:23:44 +00:00
Joao Ramos
5eaa0ac269 Assignments now generate unique list of exams for each user 2023-12-07 18:23:00 +00:00
Joao Ramos
f7af21878e Separate get exam bussiness logic into a backend asset 2023-12-07 18:20:11 +00:00
Joao Ramos
9d4071d4cd Added debug settings for vscoe 2023-12-07 18:19:01 +00:00
Tiago Ribeiro
6f5dd86cd1 Updated so the new payment prefills with all of the corporate's payment information 2023-12-07 16:36:57 +00:00
Tiago Ribeiro
8b9537b272 Merge branch 'develop' into improvement-37/writing-evaluation-perfect-answer 2023-12-06 16:43:14 +00:00
Tiago Ribeiro
a526e76c70 Added a feature to allow a user to filter the payment record 2023-12-06 16:41:11 +00:00
Joao Ramos
62b2f477f4 Replaced Corporate Icon on Agent Dashboard 2023-12-06 15:54:49 +00:00
Joao Ramos
f36384fdb4 Replaced Corporate Icon on Admin dashboard 2023-12-06 15:43:44 +00:00
Joao Ramos
9c8d7988c5 Changed Comercial labels to Corporate 2023-12-06 15:16:48 +00:00
Tiago Ribeiro
18f163768c Made it so, when a user registers with an eCrop e-mail, they get the role of a developer 2023-12-06 15:15:50 +00:00
Tiago Ribeiro
72083439af Updated Writing and Speaking to have a tab system for the evaluation vs the "perfect answer" 2023-12-06 14:48:54 +00:00
Tiago Ribeiro
523149327b Turned the name into a fallback when there is no corporate name 2023-12-06 11:31:56 +00:00
21 changed files with 628 additions and 153 deletions

28
.vscode/launch.json vendored Normal file
View 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"
}
}
]
}

View File

@@ -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}

View File

@@ -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"

View File

@@ -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 &&
{userSolutions[0].evaluation!.comment} Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).length === 3 ? (
</div> <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}
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -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 ? (
{userSolutions[0].evaluation!.comment} <Tab.Group>
</div> <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}
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -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 ? (
{userSolutions[0].evaluation!.comment} <Tab.Group>
</div> <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}
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -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

View File

@@ -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 />}
</> </>
); );
} }

View File

@@ -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 />}
</> </>
); );

View File

@@ -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);
@@ -46,36 +49,33 @@ 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}` : ""}`, { assignees,
assigner, name,
assignees, startDate,
name, endDate,
startDate, selectedModules,
endDate, generateMultiple,
results: [], }
exams: exams.map((e) => ({module: e?.module, id: e?.id})), )
}) .then(() => {
.then(() => { toast.success(
toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`); `The assignment "${name}" has been ${
cancelCreation(); assignment ? "updated" : "created"
}) } successfully!`
.catch((e) => { );
console.log(e); cancelCreation();
toast.error("Something went wrong, please try again later!"); })
}) .catch((e) => {
.finally(() => setIsLoading(false)); console.log(e);
}) toast.error("Something went wrong, please try again later!");
.catch((e) => { })
console.log(e); .finally(() => setIsLoading(false));
toast.error("Something went wrong, please try again later!"); };
setIsLoading(false);
});
};
const deleteAssignment = () => { const deleteAssignment = () => {
if (assignment) { if (assignment) {
@@ -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

View File

@@ -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"

View File

@@ -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) => (

View File

@@ -24,7 +24,6 @@ import {
BsPersonFill, BsPersonFill,
BsPersonFillGear, BsPersonFillGear,
BsPersonGear, BsPersonGear,
BsPersonLinesFill,
BsPlus, BsPlus,
BsRepeat, BsRepeat,
BsRepeat1, BsRepeat1,

View File

@@ -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;
}[]; }[];
} }

View File

@@ -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;
} }

View File

@@ -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);
} }
async function POST(req: NextApiRequest, res: NextApiResponse) { interface ExamWithUser {
await setDoc(doc(db, "assignments", uuidv4()), {assigner: req.session.user?.id, ...req.body}); module: Module;
id: string;
res.status(200).json({ok: true}); 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) {
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 });
} }

View File

@@ -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); module,
avoidRepeated,
const q = query(moduleRef, where("isDiagnostic", "==", false)); } = req.query as {module: string; avoidRepeated: string};
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", "==", 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);
} }

View File

@@ -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",

View File

@@ -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) => (

View File

@@ -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
View 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;
};