Merge remote-tracking branch 'origin/develop' into feature/training-content
This commit is contained in:
@@ -11,14 +11,16 @@ interface Props {
|
|||||||
|
|
||||||
export default function Checkbox({isChecked, onChange, children, disabled}: Props) {
|
export default function Checkbox({isChecked, onChange, children, disabled}: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer" onClick={() => {
|
<div
|
||||||
if(disabled) return;
|
className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer"
|
||||||
onChange(!isChecked);
|
onClick={() => {
|
||||||
}}>
|
if (disabled) return;
|
||||||
|
onChange(!isChecked);
|
||||||
|
}}>
|
||||||
<input type="checkbox" className="hidden" />
|
<input type="checkbox" className="hidden" />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
"w-6 h-6 min-w-6 min-h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
isChecked && "!bg-mti-purple-light ",
|
isChecked && "!bg-mti-purple-light ",
|
||||||
)}>
|
)}>
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import clsx from "clsx";
|
|||||||
import {Stat, User} from "@/interfaces/user";
|
import {Stat, User} from "@/interfaces/user";
|
||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import ai_usage from "@/utils/ai.detection";
|
import ai_usage from "@/utils/ai.detection";
|
||||||
import { calculateBandScore } from "@/utils/score";
|
import {calculateBandScore} from "@/utils/score";
|
||||||
import moment from 'moment';
|
import moment from "moment";
|
||||||
import { Assignment } from '@/interfaces/results';
|
import {Assignment} from "@/interfaces/results";
|
||||||
import { uuidv4 } from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import { useRouter } from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import { uniqBy } from "lodash";
|
import {uniqBy} from "lodash";
|
||||||
import { sortByModule } from "@/utils/moduleUtils";
|
import {sortByModule} from "@/utils/moduleUtils";
|
||||||
import { convertToUserSolutions } from "@/utils/stats";
|
import {convertToUserSolutions} from "@/utils/stats";
|
||||||
import { getExamById } from "@/utils/exams";
|
import {getExamById} from "@/utils/exams";
|
||||||
import { Exam, UserSolution } from '@/interfaces/exam';
|
import {Exam, UserSolution} from "@/interfaces/exam";
|
||||||
import ModuleBadge from '../ModuleBadge';
|
import ModuleBadge from "../ModuleBadge";
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string | number) => {
|
const formatTimestamp = (timestamp: string | number) => {
|
||||||
const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;
|
const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;
|
||||||
@@ -262,7 +262,7 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
key={uuidv4()}
|
key={uuidv4()}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
|
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
|
||||||
isDisabled && "grayscale tooltip",
|
(isDisabled || (!!assignment && !assignment.released)) && "grayscale tooltip",
|
||||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
||||||
correct / total < 0.3 && "hover:border-mti-rose",
|
correct / total < 0.3 && "hover:border-mti-rose",
|
||||||
@@ -276,7 +276,7 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
...(width !== undefined && {width}),
|
...(width !== undefined && {width}),
|
||||||
...(height !== undefined && {height}),
|
...(height !== undefined && {height}),
|
||||||
}}
|
}}
|
||||||
data-tip="This exam is still being evaluated..."
|
data-tip={isDisabled ? "This exam is still being evaluated..." : "This exam is still locked by its assigner..."}
|
||||||
role="button">
|
role="button">
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,6 +62,30 @@ export default function AssignmentCard({
|
|||||||
return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / results.length;
|
return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / results.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uniqModules = uniqBy(exams, (x) => x.module);
|
||||||
|
|
||||||
|
const shouldRenderPDF = () => {
|
||||||
|
if(released && allowDownload) {
|
||||||
|
// in order to be downloadable, the assignment has to be released
|
||||||
|
// the component should have the allowDownload prop
|
||||||
|
// and the assignment should not have the level module
|
||||||
|
return uniqModules.every(({ module }) => module !== 'level');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRenderExcel = () => {
|
||||||
|
if(released && allowExcelDownload) {
|
||||||
|
// in order to be downloadable, the assignment has to be released
|
||||||
|
// the component should have the allowExcelDownload prop
|
||||||
|
// and the assignment should have the level module
|
||||||
|
return uniqModules.some(({ module }) => module === 'level');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@@ -70,8 +94,8 @@ export default function AssignmentCard({
|
|||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<h3 className="text-xl font-semibold">{name}</h3>
|
<h3 className="text-xl font-semibold">{name}</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{allowDownload && released && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
{shouldRenderPDF() && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
{allowExcelDownload && released && renderExcelIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
{shouldRenderExcel() && renderExcelIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
{allowArchive && !archived && renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
{allowArchive && !archived && renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
{allowUnarchive && archived && renderUnarchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
{allowUnarchive && archived && renderUnarchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
{!released && renderReleaseIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
{!released && renderReleaseIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
@@ -94,7 +118,7 @@ export default function AssignmentCard({
|
|||||||
<span>Assigner: {getUserName(users.find((x) => x.id === assigner))}</span>
|
<span>Assigner: {getUserName(users.find((x) => x.id === assigner))}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
|
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
|
||||||
{uniqBy(exams, (x) => x.module).map(({module}) => (
|
{uniqModules.map(({module}) => (
|
||||||
<div
|
<div
|
||||||
key={module}
|
key={module}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|||||||
@@ -44,6 +44,20 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
|
|||||||
.finally(onClose);
|
.finally(onClose);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startAssignment = () => {
|
||||||
|
if (assignment) {
|
||||||
|
axios
|
||||||
|
.post(`/api/assignments/${assignment.id}/start`)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(`The assignment "${assignment.name}" has been started successfully!`);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
toast.error("Something went wrong, please try again later!");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string) => {
|
const formatTimestamp = (timestamp: string) => {
|
||||||
const date = moment(parseInt(timestamp));
|
const date = moment(parseInt(timestamp));
|
||||||
const formatter = "YYYY/MM/DD - HH:mm";
|
const formatter = "YYYY/MM/DD - HH:mm";
|
||||||
@@ -301,6 +315,11 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
|
|||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{assignment && (assignment.results.length === 0 || moment().isAfter(moment(assignment.startDate))) && (
|
||||||
|
<Button variant="outline" color="green" className="w-full max-w-[200px]" onClick={startAssignment}>
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button onClick={onClose} className="w-full max-w-[200px]">
|
<Button onClick={onClose} className="w-full max-w-[200px]">
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {useRouter} from "next/router";
|
|||||||
import {Fragment, useEffect, useState} from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import {
|
import {
|
||||||
BsArrowCounterclockwise,
|
BsArrowCounterclockwise,
|
||||||
|
BsBan,
|
||||||
BsBook,
|
BsBook,
|
||||||
BsClipboard,
|
BsClipboard,
|
||||||
BsClipboardFill,
|
BsClipboardFill,
|
||||||
@@ -27,6 +28,7 @@ import Modal from "@/components/Modal";
|
|||||||
import {UserSolution} from "@/interfaces/exam";
|
import {UserSolution} from "@/interfaces/exam";
|
||||||
import ai_usage from "@/utils/ai.detection";
|
import ai_usage from "@/utils/ai.detection";
|
||||||
import useGradingSystem from "@/hooks/useGrading";
|
import useGradingSystem from "@/hooks/useGrading";
|
||||||
|
import {Assignment} from "@/interfaces/results";
|
||||||
|
|
||||||
interface Score {
|
interface Score {
|
||||||
module: Module;
|
module: Module;
|
||||||
@@ -45,10 +47,11 @@ interface Props {
|
|||||||
};
|
};
|
||||||
solutions: UserSolution[];
|
solutions: UserSolution[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
assignment?: Assignment;
|
||||||
onViewResults: (moduleIndex?: number) => void;
|
onViewResults: (moduleIndex?: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Finish({user, scores, modules, information, solutions, isLoading, onViewResults}: Props) {
|
export default function Finish({user, scores, modules, information, solutions, isLoading, assignment, onViewResults}: Props) {
|
||||||
const [selectedModule, setSelectedModule] = useState(modules[0]);
|
const [selectedModule, setSelectedModule] = useState(modules[0]);
|
||||||
const [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === modules[0])!);
|
const [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === modules[0])!);
|
||||||
const [isExtraInformationOpen, setIsExtraInformationOpen] = useState(false);
|
const [isExtraInformationOpen, setIsExtraInformationOpen] = useState(false);
|
||||||
@@ -209,7 +212,18 @@ export default function Finish({user, scores, modules, information, solutions, i
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isLoading && (
|
{assignment && !assignment.released && (
|
||||||
|
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-12">
|
||||||
|
{/* <span className={clsx("loading loading-infinity w-32", moduleColors[selectedModule].progress)} /> */}
|
||||||
|
<BsBan size={64} className={clsx(moduleColors[selectedModule].progress)} />
|
||||||
|
<span className={clsx("text-center text-2xl font-bold", moduleColors[selectedModule].progress)}>
|
||||||
|
This exam has not yet been released by its assigner.
|
||||||
|
<br />
|
||||||
|
You can check it later on your records page when it is released!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && false && (
|
||||||
<div className="mb-20 mt-32 flex w-full items-center justify-between gap-9">
|
<div className="mb-20 mt-32 flex w-full items-center justify-between gap-9">
|
||||||
<span className="max-w-3xl">{moduleResultText(selectedModule, bandScore)}</span>
|
<span className="max-w-3xl">{moduleResultText(selectedModule, bandScore)}</span>
|
||||||
<div className="flex gap-9 px-16">
|
<div className="flex gap-9 px-16">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Module } from ".";
|
import {Module} from ".";
|
||||||
|
|
||||||
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
||||||
export type Variant = "full" | "partial";
|
export type Variant = "full" | "partial";
|
||||||
@@ -15,6 +15,7 @@ interface ExamBase {
|
|||||||
shuffle?: boolean;
|
shuffle?: boolean;
|
||||||
createdBy?: string; // option as it has been added later
|
createdBy?: string; // option as it has been added later
|
||||||
createdAt?: string; // option as it has been added later
|
createdAt?: string; // option as it has been added later
|
||||||
|
private?: boolean;
|
||||||
}
|
}
|
||||||
export interface ReadingExam extends ExamBase {
|
export interface ReadingExam extends ExamBase {
|
||||||
module: "reading";
|
module: "reading";
|
||||||
@@ -67,7 +68,7 @@ export interface UserSolution {
|
|||||||
};
|
};
|
||||||
exercise: string;
|
exercise: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
shuffleMaps?: ShuffleMap[]
|
shuffleMaps?: ShuffleMap[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WritingExam extends ExamBase {
|
export interface WritingExam extends ExamBase {
|
||||||
@@ -99,24 +100,19 @@ export type Exercise =
|
|||||||
export interface Evaluation {
|
export interface Evaluation {
|
||||||
comment: string;
|
comment: string;
|
||||||
overall: number;
|
overall: number;
|
||||||
task_response: { [key: string]: number | { grade: number; comment: string } };
|
task_response: {[key: string]: number | {grade: number; comment: string}};
|
||||||
misspelled_pairs?: { correction: string | null; misspelled: string }[];
|
misspelled_pairs?: {correction: string | null; misspelled: string}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type InteractivePerfectAnswerKey = `perfect_answer_${number}`;
|
type InteractivePerfectAnswerKey = `perfect_answer_${number}`;
|
||||||
type InteractiveTranscriptKey = `transcript_${number}`;
|
type InteractiveTranscriptKey = `transcript_${number}`;
|
||||||
type InteractiveFixedTextKey = `fixed_text_${number}`;
|
type InteractiveFixedTextKey = `fixed_text_${number}`;
|
||||||
|
|
||||||
type InteractivePerfectAnswerType = { [key in InteractivePerfectAnswerKey]: { answer: string } };
|
type InteractivePerfectAnswerType = {[key in InteractivePerfectAnswerKey]: {answer: string}};
|
||||||
type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string };
|
type InteractiveTranscriptType = {[key in InteractiveTranscriptKey]?: string};
|
||||||
type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
|
type InteractiveFixedTextType = {[key in InteractiveFixedTextKey]?: string};
|
||||||
|
|
||||||
interface InteractiveSpeakingEvaluation extends Evaluation,
|
|
||||||
InteractivePerfectAnswerType,
|
|
||||||
InteractiveTranscriptType,
|
|
||||||
InteractiveFixedTextType { }
|
|
||||||
|
|
||||||
|
interface InteractiveSpeakingEvaluation extends Evaluation, InteractivePerfectAnswerType, InteractiveTranscriptType, InteractiveFixedTextType {}
|
||||||
|
|
||||||
interface SpeakingEvaluation extends CommonEvaluation {
|
interface SpeakingEvaluation extends CommonEvaluation {
|
||||||
perfect_answer_1?: string;
|
perfect_answer_1?: string;
|
||||||
@@ -189,10 +185,10 @@ export interface InteractiveSpeakingExercise {
|
|||||||
first_title?: string;
|
first_title?: string;
|
||||||
second_title?: string;
|
second_title?: string;
|
||||||
text: string;
|
text: string;
|
||||||
prompts: { text: string; video_url: string }[];
|
prompts: {text: string; video_url: string}[];
|
||||||
userSolutions: {
|
userSolutions: {
|
||||||
id: string;
|
id: string;
|
||||||
solution: { questionIndex: number; question: string; answer: string }[];
|
solution: {questionIndex: number; question: string; answer: string}[];
|
||||||
evaluation?: InteractiveSpeakingEvaluation;
|
evaluation?: InteractiveSpeakingEvaluation;
|
||||||
}[];
|
}[];
|
||||||
topic?: string;
|
topic?: string;
|
||||||
@@ -208,14 +204,14 @@ export interface FillBlanksMCOption {
|
|||||||
B: string;
|
B: string;
|
||||||
C: string;
|
C: string;
|
||||||
D: string;
|
D: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FillBlanksExercise {
|
export interface FillBlanksExercise {
|
||||||
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
||||||
type: "fillBlanks";
|
type: "fillBlanks";
|
||||||
id: string;
|
id: string;
|
||||||
words: (string | { letter: string; word: string } | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"]
|
words: (string | {letter: string; word: string} | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"]
|
||||||
text: string; // *EXAMPLE: "They tried to {{1}} burning"
|
text: string; // *EXAMPLE: "They tried to {{1}} burning"
|
||||||
allowRepetition?: boolean;
|
allowRepetition?: boolean;
|
||||||
solutions: {
|
solutions: {
|
||||||
@@ -234,7 +230,7 @@ export interface TrueFalseExercise {
|
|||||||
id: string;
|
id: string;
|
||||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||||
questions: TrueFalseQuestion[];
|
questions: TrueFalseQuestion[];
|
||||||
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
|
userSolutions: {id: string; solution: "true" | "false" | "not_given"}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrueFalseQuestion {
|
export interface TrueFalseQuestion {
|
||||||
@@ -263,7 +259,7 @@ export interface MatchSentencesExercise {
|
|||||||
type: "matchSentences";
|
type: "matchSentences";
|
||||||
id: string;
|
id: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
userSolutions: { question: string; option: string }[];
|
userSolutions: {question: string; option: string}[];
|
||||||
sentences: MatchSentenceExerciseSentence[];
|
sentences: MatchSentenceExerciseSentence[];
|
||||||
allowRepetition: boolean;
|
allowRepetition: boolean;
|
||||||
options: MatchSentenceExerciseOption[];
|
options: MatchSentenceExerciseOption[];
|
||||||
@@ -286,7 +282,7 @@ export interface MultipleChoiceExercise {
|
|||||||
id: string;
|
id: string;
|
||||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||||
questions: MultipleChoiceQuestion[];
|
questions: MultipleChoiceQuestion[];
|
||||||
userSolutions: { question: string; option: string }[];
|
userSolutions: {question: string; option: string}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultipleChoiceQuestion {
|
export interface MultipleChoiceQuestion {
|
||||||
@@ -306,10 +302,10 @@ export interface ShuffleMap {
|
|||||||
questionID: string;
|
questionID: string;
|
||||||
map: {
|
map: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Shuffles {
|
export interface Shuffles {
|
||||||
exerciseID: string;
|
exerciseID: string;
|
||||||
shuffles: ShuffleMap[]
|
shuffles: ShuffleMap[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ export interface Assignment {
|
|||||||
start?: boolean;
|
start?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssignmentWithCorporateId = Assignment & { corporateId: string };
|
export type AssignmentWithCorporateId = Assignment & {corporateId: string};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import axios from "axios";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {capitalize} from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {BsCheck, BsTrash, BsUpload} from "react-icons/bs";
|
import {BsBan, BsBanFill, BsCheck, BsCircle, BsStop, BsTrash, BsUpload, BsX} from "react-icons/bs";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {useListSearch} from "@/hooks/useListSearch";
|
import {useListSearch} from "@/hooks/useListSearch";
|
||||||
|
|
||||||
@@ -72,6 +72,28 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
router.push("/exercises");
|
router.push("/exercises");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const privatizeExam = async (exam: Exam) => {
|
||||||
|
if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return;
|
||||||
|
|
||||||
|
axios
|
||||||
|
.patch(`/api/exam/${exam.module}/${exam.id}`, {private: !exam.private})
|
||||||
|
.then(() => toast.success(`Updated the "${exam.id}" exam`))
|
||||||
|
.catch((reason) => {
|
||||||
|
if (reason.response.status === 404) {
|
||||||
|
toast.error("Exam not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason.response.status === 403) {
|
||||||
|
toast.error("You do not have permission to update this exam!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error("Something went wrong, please try again later.");
|
||||||
|
})
|
||||||
|
.finally(reload);
|
||||||
|
};
|
||||||
|
|
||||||
const deleteExam = async (exam: Exam) => {
|
const deleteExam = async (exam: Exam) => {
|
||||||
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
||||||
|
|
||||||
@@ -119,6 +141,10 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
header: "Timer",
|
header: "Timer",
|
||||||
cell: (info) => <>{info.getValue()} minute(s)</>,
|
cell: (info) => <>{info.getValue()} minute(s)</>,
|
||||||
}),
|
}),
|
||||||
|
columnHelper.accessor("private", {
|
||||||
|
header: "Private",
|
||||||
|
cell: (info) => <span className="w-full flex items-center justify-center">{!info.getValue() ? <BsX /> : <BsCheck />}</span>,
|
||||||
|
}),
|
||||||
columnHelper.accessor("createdAt", {
|
columnHelper.accessor("createdAt", {
|
||||||
header: "Created At",
|
header: "Created At",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
@@ -140,12 +166,18 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
cell: ({row}: {row: {original: Exam}}) => {
|
cell: ({row}: {row: {original: Exam}}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div
|
<button
|
||||||
|
data-tip={row.original.private ? "Set as public" : "Set as private"}
|
||||||
|
onClick={async () => await privatizeExam(row.original)}
|
||||||
|
className="cursor-pointer tooltip">
|
||||||
|
{row.original.private ? <BsCircle /> : <BsBan />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
data-tip="Load exam"
|
data-tip="Load exam"
|
||||||
className="cursor-pointer tooltip"
|
className="cursor-pointer tooltip"
|
||||||
onClick={async () => await loadExam(row.original.module, row.original.id)}>
|
onClick={async () => await loadExam(row.original.module, row.original.id)}>
|
||||||
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</button>
|
||||||
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
||||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteExam(row.original)}>
|
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteExam(row.original)}>
|
||||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
|
|||||||
@@ -459,6 +459,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
user={user!}
|
user={user!}
|
||||||
modules={selectedModules}
|
modules={selectedModules}
|
||||||
solutions={userSolutions}
|
solutions={userSolutions}
|
||||||
|
assignment={assignment}
|
||||||
information={{
|
information={{
|
||||||
timeSpent,
|
timeSpent,
|
||||||
inactivity: totalInactivity,
|
inactivity: totalInactivity,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import FillBlanksEdit from "@/components/Generation/fill.blanks.edit";
|
import FillBlanksEdit from "@/components/Generation/fill.blanks.edit";
|
||||||
import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit";
|
import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit";
|
||||||
import WriteBlankEdits from "@/components/Generation/write.blanks.edit";
|
import WriteBlankEdits from "@/components/Generation/write.blanks.edit";
|
||||||
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import {
|
import {
|
||||||
@@ -234,7 +235,7 @@ interface Props {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LevelGeneration = ({ id } : Props) => {
|
const LevelGeneration = ({id}: Props) => {
|
||||||
const [generatedExam, setGeneratedExam] = useState<LevelExam>();
|
const [generatedExam, setGeneratedExam] = useState<LevelExam>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<LevelExam>();
|
const [resultingExam, setResultingExam] = useState<LevelExam>();
|
||||||
@@ -242,6 +243,7 @@ const LevelGeneration = ({ id } : Props) => {
|
|||||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||||
const [numberOfParts, setNumberOfParts] = useState(1);
|
const [numberOfParts, setNumberOfParts] = useState(1);
|
||||||
const [parts, setParts] = useState<LevelSection[]>([{quantity: 10, type: "multiple_choice_4"}]);
|
const [parts, setParts] = useState<LevelSection[]>([{quantity: 10, type: "multiple_choice_4"}]);
|
||||||
|
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setParts((prev) => Array.from(Array(numberOfParts)).map((_, i) => (!!prev.at(i) ? prev.at(i)! : {quantity: 10, type: "multiple_choice_4"})));
|
setParts((prev) => Array.from(Array(numberOfParts)).map((_, i) => (!!prev.at(i) ? prev.at(i)! : {quantity: 10, type: "multiple_choice_4"})));
|
||||||
@@ -301,6 +303,7 @@ const LevelGeneration = ({ id } : Props) => {
|
|||||||
difficulty,
|
difficulty,
|
||||||
variant: "full",
|
variant: "full",
|
||||||
isDiagnostic: false,
|
isDiagnostic: false,
|
||||||
|
private: isPrivate,
|
||||||
parts: parts
|
parts: parts
|
||||||
.map((part, index) => {
|
.map((part, index) => {
|
||||||
const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any;
|
const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any;
|
||||||
@@ -424,7 +427,7 @@ const LevelGeneration = ({ id } : Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!id) {
|
if (!id) {
|
||||||
toast.error("Please insert a title before submitting");
|
toast.error("Please insert a title before submitting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -456,8 +459,8 @@ const LevelGeneration = ({ id } : Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-4 w-full">
|
<div className="flex gap-4 w-full items-center">
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-1/2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
||||||
<Select
|
<Select
|
||||||
options={DIFFICULTIES.map((x) => ({
|
options={DIFFICULTIES.map((x) => ({
|
||||||
@@ -468,14 +471,20 @@ const LevelGeneration = ({ id } : Props) => {
|
|||||||
value={{value: difficulty, label: capitalize(difficulty)}}
|
value={{value: difficulty, label: capitalize(difficulty)}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-1/3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Number of Parts</label>
|
<label className="font-normal text-base text-mti-gray-dim">Number of Parts</label>
|
||||||
<Input type="number" name="Number of Parts" onChange={(v) => setNumberOfParts(parseInt(v))} value={numberOfParts} />
|
<Input type="number" name="Number of Parts" onChange={(v) => setNumberOfParts(parseInt(v))} value={numberOfParts} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-1/3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Timer (in minutes)</label>
|
<label className="font-normal text-base text-mti-gray-dim">Timer (in minutes)</label>
|
||||||
<Input type="number" name="Timer (in minutes)" onChange={(v) => setTimer(parseInt(v))} value={timer} />
|
<Input type="number" name="Timer (in minutes)" onChange={(v) => setTimer(parseInt(v))} value={timer} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 w-fit h-fit">
|
||||||
|
<div className="h-6" />
|
||||||
|
<Checkbox isChecked={isPrivate} onChange={setPrivate}>
|
||||||
|
Privacy (Only available for Assignments)
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {BsArrowRepeat, BsCheck} from "react-icons/bs";
|
|||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import WriteBlanksEdit from "@/components/Generation/write.blanks.edit";
|
import WriteBlanksEdit from "@/components/Generation/write.blanks.edit";
|
||||||
import {generate} from "random-words";
|
import {generate} from "random-words";
|
||||||
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
|
|
||||||
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
||||||
|
|
||||||
@@ -232,7 +233,7 @@ interface Props {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListeningGeneration = ({ id } : Props) => {
|
const ListeningGeneration = ({id}: Props) => {
|
||||||
const [part1, setPart1] = useState<ListeningPart>();
|
const [part1, setPart1] = useState<ListeningPart>();
|
||||||
const [part2, setPart2] = useState<ListeningPart>();
|
const [part2, setPart2] = useState<ListeningPart>();
|
||||||
const [part3, setPart3] = useState<ListeningPart>();
|
const [part3, setPart3] = useState<ListeningPart>();
|
||||||
@@ -241,6 +242,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<ListeningExam>();
|
const [resultingExam, setResultingExam] = useState<ListeningExam>();
|
||||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||||
|
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const part1Timer = part1 ? 5 : 0;
|
const part1Timer = part1 ? 5 : 0;
|
||||||
@@ -262,7 +264,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
|||||||
console.log({parts});
|
console.log({parts});
|
||||||
if (parts.length === 0) return toast.error("Please generate at least one section!");
|
if (parts.length === 0) return toast.error("Please generate at least one section!");
|
||||||
|
|
||||||
if(!id) {
|
if (!id) {
|
||||||
toast.error("Please insert a title before submitting");
|
toast.error("Please insert a title before submitting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -275,6 +277,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
|||||||
parts,
|
parts,
|
||||||
minTimer,
|
minTimer,
|
||||||
difficulty,
|
difficulty,
|
||||||
|
private: isPrivate,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
playSound("sent");
|
playSound("sent");
|
||||||
@@ -313,7 +316,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-4 w-1/2">
|
<div className="flex gap-4 w-full items-center">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -324,7 +327,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
|||||||
className="max-w-[300px]"
|
className="max-w-[300px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-1/2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
||||||
<Select
|
<Select
|
||||||
options={DIFFICULTIES.map((x) => ({
|
options={DIFFICULTIES.map((x) => ({
|
||||||
@@ -336,6 +339,12 @@ const ListeningGeneration = ({ id } : Props) => {
|
|||||||
disabled={!!part1 || !!part2 || !!part3 || !!part4}
|
disabled={!!part1 || !!part2 || !!part3 || !!part4}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 w-fit h-fit">
|
||||||
|
<div className="h-6" />
|
||||||
|
<Checkbox isChecked={isPrivate} onChange={setPrivate}>
|
||||||
|
Privacy (Only available for Assignments)
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-listening/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-listening/20 p-1">
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import TrueFalseEdit from "@/components/Generation/true.false.edit";
|
|||||||
import WriteBlanksEdit from "@/components/Generation/write.blanks.edit";
|
import WriteBlanksEdit from "@/components/Generation/write.blanks.edit";
|
||||||
import MatchSentencesEdit from "@/components/Generation/match.sentences.edit";
|
import MatchSentencesEdit from "@/components/Generation/match.sentences.edit";
|
||||||
import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit";
|
import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit";
|
||||||
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
|
|
||||||
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
||||||
|
|
||||||
@@ -262,7 +263,7 @@ interface Props {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReadingGeneration = ({ id } : Props) => {
|
const ReadingGeneration = ({id}: Props) => {
|
||||||
const [part1, setPart1] = useState<ReadingPart>();
|
const [part1, setPart1] = useState<ReadingPart>();
|
||||||
const [part2, setPart2] = useState<ReadingPart>();
|
const [part2, setPart2] = useState<ReadingPart>();
|
||||||
const [part3, setPart3] = useState<ReadingPart>();
|
const [part3, setPart3] = useState<ReadingPart>();
|
||||||
@@ -270,6 +271,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<ReadingExam>();
|
const [resultingExam, setResultingExam] = useState<ReadingExam>();
|
||||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||||
|
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parts = [part1, part2, part3].filter((x) => !!x);
|
const parts = [part1, part2, part3].filter((x) => !!x);
|
||||||
@@ -304,7 +306,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!id) {
|
if (!id) {
|
||||||
toast.error("Please insert a title before submitting");
|
toast.error("Please insert a title before submitting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -319,6 +321,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
|||||||
type: "academic",
|
type: "academic",
|
||||||
variant: parts.length === 3 ? "full" : "partial",
|
variant: parts.length === 3 ? "full" : "partial",
|
||||||
difficulty,
|
difficulty,
|
||||||
|
private: isPrivate,
|
||||||
};
|
};
|
||||||
|
|
||||||
axios
|
axios
|
||||||
@@ -344,7 +347,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-4 w-1/2">
|
<div className="flex gap-4 w-full items-center">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -355,7 +358,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
|||||||
className="max-w-[300px]"
|
className="max-w-[300px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-1/2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
||||||
<Select
|
<Select
|
||||||
options={DIFFICULTIES.map((x) => ({
|
options={DIFFICULTIES.map((x) => ({
|
||||||
@@ -367,6 +370,12 @@ const ReadingGeneration = ({ id } : Props) => {
|
|||||||
disabled={!!part1 || !!part2 || !!part3}
|
disabled={!!part1 || !!part2 || !!part3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 w-fit h-fit">
|
||||||
|
<div className="h-6" />
|
||||||
|
<Checkbox isChecked={isPrivate} onChange={setPrivate}>
|
||||||
|
Privacy (Only available for Assignments)
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-reading/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-reading/20 p-1">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import {Difficulty, Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam";
|
import {Difficulty, Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam";
|
||||||
@@ -225,7 +226,7 @@ interface Props {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpeakingGeneration = ({ id } : Props) => {
|
const SpeakingGeneration = ({id}: Props) => {
|
||||||
const [part1, setPart1] = useState<SpeakingPart>();
|
const [part1, setPart1] = useState<SpeakingPart>();
|
||||||
const [part2, setPart2] = useState<SpeakingPart>();
|
const [part2, setPart2] = useState<SpeakingPart>();
|
||||||
const [part3, setPart3] = useState<SpeakingPart>();
|
const [part3, setPart3] = useState<SpeakingPart>();
|
||||||
@@ -233,6 +234,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<SpeakingExam>();
|
const [resultingExam, setResultingExam] = useState<SpeakingExam>();
|
||||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||||
|
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parts = [part1, part2, part3].filter((x) => !!x);
|
const parts = [part1, part2, part3].filter((x) => !!x);
|
||||||
@@ -247,7 +249,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
|||||||
const submitExam = () => {
|
const submitExam = () => {
|
||||||
if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!");
|
if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!");
|
||||||
|
|
||||||
if(!id) {
|
if (!id) {
|
||||||
toast.error("Please insert a title before submitting");
|
toast.error("Please insert a title before submitting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -272,6 +274,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
|||||||
variant: minTimer >= 14 ? "full" : "partial",
|
variant: minTimer >= 14 ? "full" : "partial",
|
||||||
module: "speaking",
|
module: "speaking",
|
||||||
instructorGender: genders.every((x) => x === "male") ? "male" : genders.every((x) => x === "female") ? "female" : "varied",
|
instructorGender: genders.every((x) => x === "male") ? "male" : genders.every((x) => x === "female") ? "female" : "varied",
|
||||||
|
private: isPrivate,
|
||||||
};
|
};
|
||||||
|
|
||||||
axios
|
axios
|
||||||
@@ -313,7 +316,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-4 w-1/2">
|
<div className="flex gap-4 w-full items-center">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -324,7 +327,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
|||||||
className="max-w-[300px]"
|
className="max-w-[300px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-1/2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
||||||
<Select
|
<Select
|
||||||
options={DIFFICULTIES.map((x) => ({
|
options={DIFFICULTIES.map((x) => ({
|
||||||
@@ -336,6 +339,13 @@ const SpeakingGeneration = ({ id } : Props) => {
|
|||||||
disabled={!!part1 || !!part2 || !!part3}
|
disabled={!!part1 || !!part2 || !!part3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 w-fit h-fit">
|
||||||
|
<div className="h-6" />
|
||||||
|
<Checkbox isChecked={isPrivate} onChange={setPrivate}>
|
||||||
|
Privacy (Only available for Assignments)
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import {Difficulty, WritingExam, WritingExercise} from "@/interfaces/exam";
|
import {Difficulty, WritingExam, WritingExercise} from "@/interfaces/exam";
|
||||||
@@ -79,13 +80,14 @@ interface Props {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WritingGeneration = ({ id } : Props) => {
|
const WritingGeneration = ({id}: Props) => {
|
||||||
const [task1, setTask1] = useState<string>();
|
const [task1, setTask1] = useState<string>();
|
||||||
const [task2, setTask2] = useState<string>();
|
const [task2, setTask2] = useState<string>();
|
||||||
const [minTimer, setMinTimer] = useState(60);
|
const [minTimer, setMinTimer] = useState(60);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<WritingExam>();
|
const [resultingExam, setResultingExam] = useState<WritingExam>();
|
||||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||||
|
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const task1Timer = task1 ? 20 : 0;
|
const task1Timer = task1 ? 20 : 0;
|
||||||
@@ -120,7 +122,7 @@ const WritingGeneration = ({ id } : Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!id) {
|
if (!id) {
|
||||||
toast.error("Please insert a title before submitting");
|
toast.error("Please insert a title before submitting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,6 +166,7 @@ const WritingGeneration = ({ id } : Props) => {
|
|||||||
id,
|
id,
|
||||||
variant: exercise1 && exercise2 ? "full" : "partial",
|
variant: exercise1 && exercise2 ? "full" : "partial",
|
||||||
difficulty,
|
difficulty,
|
||||||
|
private: isPrivate,
|
||||||
};
|
};
|
||||||
|
|
||||||
axios
|
axios
|
||||||
@@ -188,7 +191,7 @@ const WritingGeneration = ({ id } : Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-4 w-1/2">
|
<div className="flex gap-4 w-full items-center">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -199,7 +202,7 @@ const WritingGeneration = ({ id } : Props) => {
|
|||||||
className="max-w-[300px]"
|
className="max-w-[300px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-1/2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
||||||
<Select
|
<Select
|
||||||
options={DIFFICULTIES.map((x) => ({value: x, label: capitalize(x)}))}
|
options={DIFFICULTIES.map((x) => ({value: x, label: capitalize(x)}))}
|
||||||
@@ -208,6 +211,13 @@ const WritingGeneration = ({ id } : Props) => {
|
|||||||
disabled={!!task1 || !!task2}
|
disabled={!!task1 || !!task2}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 w-fit h-fit">
|
||||||
|
<div className="h-6" />
|
||||||
|
<Checkbox isChecked={isPrivate} onChange={setPrivate}>
|
||||||
|
Privacy (Only available for Assignments)
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
|
|||||||
@@ -84,9 +84,16 @@ function commonExcel({
|
|||||||
.map((assignee: string) => {
|
.map((assignee: string) => {
|
||||||
const userStats = allStats.filter((s: any) => s.user === assignee);
|
const userStats = allStats.filter((s: any) => s.user === assignee);
|
||||||
const dates = userStats.map((s: any) => moment(s.date));
|
const dates = userStats.map((s: any) => moment(s.date));
|
||||||
|
const user = users.find((u) => u.id === assignee);
|
||||||
return {
|
return {
|
||||||
userId: assignee,
|
userId: assignee,
|
||||||
user: users.find((u) => u.id === assignee),
|
// added some default values in case the user is not found
|
||||||
|
// could it be possible to have an assigned user deleted from the database?
|
||||||
|
user: user || {
|
||||||
|
name: "Unknown",
|
||||||
|
email: "Unknown",
|
||||||
|
demographicInformation: { passportId: "Unknown", gender: "Unknown" },
|
||||||
|
},
|
||||||
...userStats.reduce(
|
...userStats.reduce(
|
||||||
(acc: any, curr: any) => {
|
(acc: any, curr: any) => {
|
||||||
return {
|
return {
|
||||||
@@ -152,7 +159,7 @@ function commonExcel({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// added empty arrays to force row spacings
|
// added empty arrays to force row spacings
|
||||||
const customTableAndLine = [[],...customTable, []];
|
const customTableAndLine = [[], ...customTable, []];
|
||||||
customTableAndLine.forEach((row: string[], index) => {
|
customTableAndLine.forEach((row: string[], index) => {
|
||||||
worksheet.addRow(row);
|
worksheet.addRow(row);
|
||||||
});
|
});
|
||||||
@@ -188,19 +195,24 @@ function commonExcel({
|
|||||||
worksheet.addRow(tableColumnHeaders);
|
worksheet.addRow(tableColumnHeaders);
|
||||||
|
|
||||||
// 1 headers rows
|
// 1 headers rows
|
||||||
const startIndexTable = firstSectionData.length + customTableAndLine.length + 1;
|
const startIndexTable =
|
||||||
|
firstSectionData.length + customTableAndLine.length + 1;
|
||||||
|
|
||||||
// // Merge "Test Sections" over dynamic number of columns
|
// // Merge "Test Sections" over dynamic number of columns
|
||||||
// const tableColumns = staticHeaders.length + numberOfTestSections;
|
// const tableColumns = staticHeaders.length + numberOfTestSections;
|
||||||
|
|
||||||
// K10:M12 = 10,11,12,13
|
// K10:M12 = 10,11,12,13
|
||||||
// horizontally group Test Sections
|
// horizontally group Test Sections
|
||||||
worksheet.mergeCells(
|
|
||||||
startIndexTable,
|
// if there are test section headers to even merge:
|
||||||
staticHeaders.length + 1,
|
if (testSectionHeaders.length > 1) {
|
||||||
startIndexTable,
|
worksheet.mergeCells(
|
||||||
tableColumnHeadersFirstPart.length
|
startIndexTable,
|
||||||
);
|
staticHeaders.length + 1,
|
||||||
|
startIndexTable,
|
||||||
|
tableColumnHeadersFirstPart.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Add the dynamic second and third header rows for test sections and sub-columns
|
// Add the dynamic second and third header rows for test sections and sub-columns
|
||||||
worksheet.addRow([
|
worksheet.addRow([
|
||||||
@@ -229,7 +241,12 @@ function commonExcel({
|
|||||||
|
|
||||||
// vertically group based on the part, exercise and type
|
// vertically group based on the part, exercise and type
|
||||||
staticHeaders.forEach((header, index) => {
|
staticHeaders.forEach((header, index) => {
|
||||||
worksheet.mergeCells(startIndexTable, index + 1, startIndexTable + 3, index + 1);
|
worksheet.mergeCells(
|
||||||
|
startIndexTable,
|
||||||
|
index + 1,
|
||||||
|
startIndexTable + 3,
|
||||||
|
index + 1
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
assigneesData.forEach((data, index) => {
|
assigneesData.forEach((data, index) => {
|
||||||
@@ -316,13 +333,17 @@ async function mastercorporateAssignment(
|
|||||||
const adminsData = await getSpecificUsers(adminUsers);
|
const adminsData = await getSpecificUsers(adminUsers);
|
||||||
const companiesData = adminsData.map((user) => {
|
const companiesData = adminsData.map((user) => {
|
||||||
const name = getUserName(user);
|
const name = getUserName(user);
|
||||||
const users = userGroupsParticipants
|
const users = userGroupsParticipants.filter((p) =>
|
||||||
.filter((p) => data.assignees.includes(p));
|
data.assignees.includes(p)
|
||||||
|
);
|
||||||
|
|
||||||
const stats = data.results
|
const stats = data.results
|
||||||
.flatMap((r: any) => r.stats)
|
.flatMap((r: any) => r.stats)
|
||||||
.filter((s: any) => users.includes(s.user));
|
.filter((s: any) => users.includes(s.user));
|
||||||
const correct = stats.reduce((acc: number, s: any) => acc + s.score.correct, 0);
|
const correct = stats.reduce(
|
||||||
|
(acc: number, s: any) => acc + s.score.correct,
|
||||||
|
0
|
||||||
|
);
|
||||||
const total = stats.reduce(
|
const total = stats.reduce(
|
||||||
(acc: number, curr: any) => acc + curr.score.total,
|
(acc: number, curr: any) => acc + curr.score.total,
|
||||||
0
|
0
|
||||||
@@ -342,9 +363,11 @@ async function mastercorporateAssignment(
|
|||||||
correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0),
|
correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0),
|
||||||
total: companiesData.reduce((acc, curr) => acc + curr.total, 0),
|
total: companiesData.reduce((acc, curr) => acc + curr.total, 0),
|
||||||
},
|
},
|
||||||
].map((c) => [c.name, `${c.correct}/${c.total}`])
|
].map((c) => [c.name, `${c.correct}/${c.total}`]);
|
||||||
|
|
||||||
const customTableHeaders = [{ name: "Corporate", helper: (data: any) => data.user.corporateName}];
|
const customTableHeaders = [
|
||||||
|
{ name: "Corporate", helper: (data: any) => data.user.corporateName },
|
||||||
|
];
|
||||||
return commonExcel({
|
return commonExcel({
|
||||||
data,
|
data,
|
||||||
userName: user.corporateInformation?.companyInformation?.name || "",
|
userName: user.corporateInformation?.companyInformation?.name || "",
|
||||||
@@ -354,12 +377,13 @@ async function mastercorporateAssignment(
|
|||||||
return {
|
return {
|
||||||
...u,
|
...u,
|
||||||
corporateName: getUserName(admin),
|
corporateName: getUserName(admin),
|
||||||
}
|
};
|
||||||
}),
|
}),
|
||||||
sectionName: "Master Corporate Name :",
|
sectionName: "Master Corporate Name :",
|
||||||
customTable: [['Corporate Summary'], ...customTable],
|
customTable: [["Corporate Summary"], ...customTable],
|
||||||
customTableHeaders: customTableHeaders.map((h) => h.name),
|
customTableHeaders: customTableHeaders.map((h) => h.name),
|
||||||
renderCustomTableData: (data) => customTableHeaders.map((h) => h.helper(data)),
|
renderCustomTableData: (data) =>
|
||||||
|
customTableHeaders.map((h) => h.helper(data)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,7 +435,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
case "corporate":
|
case "corporate":
|
||||||
return corporateAssignment(user as CorporateUser, data, users);
|
return corporateAssignment(user as CorporateUser, data, users);
|
||||||
case "mastercorporate":
|
case "mastercorporate":
|
||||||
return mastercorporateAssignment(user as MasterCorporateUser, data, users);
|
return mastercorporateAssignment(
|
||||||
|
user as MasterCorporateUser,
|
||||||
|
data,
|
||||||
|
users
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid user type");
|
throw new Error("Invalid user type");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// 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, doc, getDoc, deleteDoc} from "firebase/firestore";
|
import {getFirestore, doc, getDoc, deleteDoc, setDoc} 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 {PERMISSIONS} from "@/constants/userPermissions";
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
@@ -12,6 +12,7 @@ export default withIronSessionApiRoute(handler, sessionOptions);
|
|||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method === "GET") return get(req, res);
|
if (req.method === "GET") return get(req, res);
|
||||||
|
if (req.method === "PATCH") return patch(req, res);
|
||||||
if (req.method === "DELETE") return del(req, res);
|
if (req.method === "DELETE") return del(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,25 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function patch(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!req.session.user) {
|
||||||
|
res.status(401).json({ok: false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {module, id} = req.query as {module: string; id: string};
|
||||||
|
|
||||||
|
const docRef = doc(db, module, id);
|
||||||
|
const docSnap = await getDoc(docRef);
|
||||||
|
|
||||||
|
if (docSnap.exists()) {
|
||||||
|
await setDoc(docRef, req.body, {merge: true});
|
||||||
|
res.status(200).json({ok: true});
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ok: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function del(req: NextApiRequest, res: NextApiResponse) {
|
async function del(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res.status(401).json({ok: false});
|
res.status(401).json({ok: false});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {v4} from "uuid";
|
|||||||
import {checkAccess} from "@/utils/permissions";
|
import {checkAccess} from "@/utils/permissions";
|
||||||
import {CEFR_STEPS} from "@/resources/grading";
|
import {CEFR_STEPS} from "@/resources/grading";
|
||||||
import {getCorporateUser} from "@/resources/user";
|
import {getCorporateUser} from "@/resources/user";
|
||||||
import {getUserCorporate} from "@/utils/groups";
|
import {getUserCorporate} from "@/utils/groups.be";
|
||||||
import {Grading} from "@/interfaces";
|
import {Grading} from "@/interfaces";
|
||||||
import {getGroupsForUser} from "@/utils/groups.be";
|
import {getGroupsForUser} from "@/utils/groups.be";
|
||||||
import {uniq} from "lodash";
|
import {uniq} from "lodash";
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import {Assignment} from "@/interfaces/results";
|
||||||
|
|
||||||
export const futureAssignmentFilter = (a: Assignment) =>
|
export const futureAssignmentFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()) && !a.archived;
|
||||||
moment(a.startDate).isAfter(moment()) && !a.archived && !a.start;
|
|
||||||
|
export const pastAssignmentFilter = (a: Assignment) => moment(a.endDate).isBefore(moment()) && !a.archived;
|
||||||
|
|
||||||
export const pastAssignmentFilter = (a: Assignment) =>
|
|
||||||
(moment(a.endDate).isBefore(moment()) ||
|
|
||||||
a.assignees.length === a.results.length ||
|
|
||||||
(moment(a.startDate).isBefore(moment()) && !a.start)) &&
|
|
||||||
!a.archived;
|
|
||||||
export const archivedAssignmentFilter = (a: Assignment) => a.archived;
|
export const archivedAssignmentFilter = (a: Assignment) => a.archived;
|
||||||
|
|
||||||
export const activeAssignmentFilter = (a: Assignment) =>
|
export const activeAssignmentFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment());
|
||||||
moment(a.endDate).isAfter(moment()) &&
|
|
||||||
// && moment(a.startDate).isBefore(moment())
|
|
||||||
a.start &&
|
|
||||||
a.assignees.length > a.results.length;
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {collection, getDocs, query, where, setDoc, doc, Firestore, getDoc} from "firebase/firestore";
|
import {collection, getDocs, query, where, setDoc, doc, Firestore, getDoc, and} from "firebase/firestore";
|
||||||
import {shuffle} from "lodash";
|
import {shuffle} from "lodash";
|
||||||
import {Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam} from "@/interfaces/exam";
|
import {Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam} from "@/interfaces/exam";
|
||||||
import {DeveloperUser, Stat, StudentUser, User} from "@/interfaces/user";
|
import {DeveloperUser, Stat, StudentUser, User} from "@/interfaces/user";
|
||||||
@@ -17,7 +17,7 @@ export const getExams = async (
|
|||||||
): Promise<Exam[]> => {
|
): Promise<Exam[]> => {
|
||||||
const moduleRef = collection(db, module);
|
const moduleRef = collection(db, module);
|
||||||
|
|
||||||
const q = query(moduleRef, where("isDiagnostic", "==", false));
|
const q = query(moduleRef, and(where("isDiagnostic", "==", false), where("private", "!=", true)));
|
||||||
const snapshot = await getDocs(q);
|
const snapshot = await getDocs(q);
|
||||||
|
|
||||||
const allExams = shuffle(
|
const allExams = shuffle(
|
||||||
|
|||||||
@@ -33,11 +33,34 @@ export const updateExpiryDateOnGroup = async (participantID: string, corporateID
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserCorporate = async (id: string) => {
|
||||||
|
const user = await getUser(id);
|
||||||
|
if (user.type === "corporate" || user.type === "mastercorporate") return user;
|
||||||
|
|
||||||
|
const groups = await getParticipantGroups(id);
|
||||||
|
const admins = await Promise.all(groups.map((x) => x.admin).map(getUser));
|
||||||
|
const corporates = admins.filter((x) => x.type === "corporate");
|
||||||
|
|
||||||
|
if (corporates.length === 0) return undefined;
|
||||||
|
return corporates.shift() as CorporateUser;
|
||||||
|
};
|
||||||
|
|
||||||
export const getGroups = async () => {
|
export const getGroups = async () => {
|
||||||
const groupDocs = await getDocs(collection(db, "groups"));
|
const groupDocs = await getDocs(collection(db, "groups"));
|
||||||
return groupDocs.docs.map((x) => ({...x.data(), id: x.id})) as Group[];
|
return groupDocs.docs.map((x) => ({...x.data(), id: x.id})) as Group[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getParticipantGroups = async (id: string) => {
|
||||||
|
const snapshot = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id)));
|
||||||
|
|
||||||
|
const groups = snapshot.docs.map((doc) => ({
|
||||||
|
id: doc.id,
|
||||||
|
...doc.data(),
|
||||||
|
})) as Group[];
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
export const getUserGroups = async (id: string): Promise<Group[]> => {
|
export const getUserGroups = async (id: string): Promise<Group[]> => {
|
||||||
const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id)));
|
const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id)));
|
||||||
return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[];
|
return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[];
|
||||||
|
|||||||
@@ -1,43 +1,37 @@
|
|||||||
import { CorporateUser, Group, User, Type } from "@/interfaces/user";
|
import {CorporateUser, Group, User, Type} from "@/interfaces/user";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const isUserFromCorporate = async (userID: string) => {
|
export const isUserFromCorporate = async (userID: string) => {
|
||||||
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`))
|
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`)).data;
|
||||||
.data;
|
const users = (await axios.get<User[]>("/api/users/list")).data;
|
||||||
const users = (await axios.get<User[]>("/api/users/list")).data;
|
|
||||||
|
|
||||||
const adminTypes = groups.map(
|
const adminTypes = groups.map((g) => users.find((u) => u.id === g.admin)?.type);
|
||||||
(g) => users.find((u) => u.id === g.admin)?.type
|
return adminTypes.includes("corporate");
|
||||||
);
|
|
||||||
return adminTypes.includes("corporate");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAdminForGroup = async (userID: string, role: Type) => {
|
const getAdminForGroup = async (userID: string, role: Type) => {
|
||||||
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`))
|
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`)).data;
|
||||||
.data;
|
|
||||||
|
|
||||||
const adminRequests = await Promise.all(
|
const adminRequests = await Promise.all(
|
||||||
groups.map(async (g) => {
|
groups.map(async (g) => {
|
||||||
const userRequest = await axios.get<User>(`/api/users/${g.admin}`);
|
const userRequest = await axios.get<User>(`/api/users/${g.admin}`);
|
||||||
if (userRequest.status === 200) return userRequest.data;
|
if (userRequest.status === 200) return userRequest.data;
|
||||||
return undefined;
|
return undefined;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const admins = adminRequests.filter((x) => x?.type === role);
|
const admins = adminRequests.filter((x) => x?.type === role);
|
||||||
return admins.length > 0 ? (admins[0] as CorporateUser) : undefined;
|
return admins.length > 0 ? (admins[0] as CorporateUser) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserCorporate = async (
|
export const getUserCorporate = async (userID: string): Promise<CorporateUser | undefined> => {
|
||||||
userID: string
|
const userRequest = await axios.get<User>(`/api/users/${userID}`);
|
||||||
): Promise<CorporateUser | undefined> => {
|
if (userRequest.status === 200) {
|
||||||
const userRequest = await axios.get<User>(`/api/users/${userID}`);
|
const user = userRequest.data;
|
||||||
if (userRequest.status === 200) {
|
if (user.type === "corporate") {
|
||||||
const user = userRequest.data;
|
return getAdminForGroup(userID, "mastercorporate");
|
||||||
if (user.type === "corporate") {
|
}
|
||||||
return getAdminForGroup(userID, "mastercorporate");
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAdminForGroup(userID, "corporate");
|
return getAdminForGroup(userID, "corporate");
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user