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) {
|
||||
return (
|
||||
<div className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer" onClick={() => {
|
||||
if(disabled) return;
|
||||
<div
|
||||
className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer"
|
||||
onClick={() => {
|
||||
if (disabled) return;
|
||||
onChange(!isChecked);
|
||||
}}>
|
||||
<input type="checkbox" className="hidden" />
|
||||
<div
|
||||
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",
|
||||
isChecked && "!bg-mti-purple-light ",
|
||||
)}>
|
||||
|
||||
@@ -4,17 +4,17 @@ import clsx from "clsx";
|
||||
import {Stat, User} from "@/interfaces/user";
|
||||
import {Module} from "@/interfaces";
|
||||
import ai_usage from "@/utils/ai.detection";
|
||||
import { calculateBandScore } from "@/utils/score";
|
||||
import moment from 'moment';
|
||||
import { Assignment } from '@/interfaces/results';
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import { useRouter } from "next/router";
|
||||
import { uniqBy } from "lodash";
|
||||
import { sortByModule } from "@/utils/moduleUtils";
|
||||
import { convertToUserSolutions } from "@/utils/stats";
|
||||
import { getExamById } from "@/utils/exams";
|
||||
import { Exam, UserSolution } from '@/interfaces/exam';
|
||||
import ModuleBadge from '../ModuleBadge';
|
||||
import {calculateBandScore} from "@/utils/score";
|
||||
import moment from "moment";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
import {uuidv4} from "@firebase/util";
|
||||
import {useRouter} from "next/router";
|
||||
import {uniqBy} from "lodash";
|
||||
import {sortByModule} from "@/utils/moduleUtils";
|
||||
import {convertToUserSolutions} from "@/utils/stats";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {Exam, UserSolution} from "@/interfaces/exam";
|
||||
import ModuleBadge from "../ModuleBadge";
|
||||
|
||||
const formatTimestamp = (timestamp: string | number) => {
|
||||
const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;
|
||||
@@ -262,7 +262,7 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
||||
key={uuidv4()}
|
||||
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",
|
||||
isDisabled && "grayscale tooltip",
|
||||
(isDisabled || (!!assignment && !assignment.released)) && "grayscale tooltip",
|
||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
||||
correct / total < 0.3 && "hover:border-mti-rose",
|
||||
@@ -276,7 +276,7 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
||||
...(width !== undefined && {width}),
|
||||
...(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">
|
||||
{content}
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,30 @@ export default function AssignmentCard({
|
||||
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 (
|
||||
<div
|
||||
onClick={onClick}
|
||||
@@ -70,8 +94,8 @@ export default function AssignmentCard({
|
||||
<div className="flex flex-row justify-between">
|
||||
<h3 className="text-xl font-semibold">{name}</h3>
|
||||
<div className="flex gap-2">
|
||||
{allowDownload && released && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
||||
{allowExcelDownload && released && renderExcelIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
||||
{shouldRenderPDF() && renderPdfIcon(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")}
|
||||
{allowUnarchive && archived && renderUnarchiveIcon("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>
|
||||
</div>
|
||||
<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
|
||||
key={module}
|
||||
className={clsx(
|
||||
|
||||
@@ -44,6 +44,20 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
|
||||
.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 date = moment(parseInt(timestamp));
|
||||
const formatter = "YYYY/MM/DD - HH:mm";
|
||||
@@ -301,6 +315,11 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
|
||||
Delete
|
||||
</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]">
|
||||
Close
|
||||
</Button>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {useRouter} from "next/router";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {
|
||||
BsArrowCounterclockwise,
|
||||
BsBan,
|
||||
BsBook,
|
||||
BsClipboard,
|
||||
BsClipboardFill,
|
||||
@@ -27,6 +28,7 @@ import Modal from "@/components/Modal";
|
||||
import {UserSolution} from "@/interfaces/exam";
|
||||
import ai_usage from "@/utils/ai.detection";
|
||||
import useGradingSystem from "@/hooks/useGrading";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
|
||||
interface Score {
|
||||
module: Module;
|
||||
@@ -45,10 +47,11 @@ interface Props {
|
||||
};
|
||||
solutions: UserSolution[];
|
||||
isLoading: boolean;
|
||||
assignment?: Assignment;
|
||||
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 [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === modules[0])!);
|
||||
const [isExtraInformationOpen, setIsExtraInformationOpen] = useState(false);
|
||||
@@ -209,7 +212,18 @@ export default function Finish({user, scores, modules, information, solutions, i
|
||||
</span>
|
||||
</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">
|
||||
<span className="max-w-3xl">{moduleResultText(selectedModule, bandScore)}</span>
|
||||
<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 Variant = "full" | "partial";
|
||||
@@ -15,6 +15,7 @@ interface ExamBase {
|
||||
shuffle?: boolean;
|
||||
createdBy?: string; // option as it has been added later
|
||||
createdAt?: string; // option as it has been added later
|
||||
private?: boolean;
|
||||
}
|
||||
export interface ReadingExam extends ExamBase {
|
||||
module: "reading";
|
||||
@@ -67,7 +68,7 @@ export interface UserSolution {
|
||||
};
|
||||
exercise: string;
|
||||
isDisabled?: boolean;
|
||||
shuffleMaps?: ShuffleMap[]
|
||||
shuffleMaps?: ShuffleMap[];
|
||||
}
|
||||
|
||||
export interface WritingExam extends ExamBase {
|
||||
@@ -99,24 +100,19 @@ export type Exercise =
|
||||
export interface Evaluation {
|
||||
comment: string;
|
||||
overall: number;
|
||||
task_response: { [key: string]: number | { grade: number; comment: string } };
|
||||
misspelled_pairs?: { correction: string | null; misspelled: string }[];
|
||||
task_response: {[key: string]: number | {grade: number; comment: string}};
|
||||
misspelled_pairs?: {correction: string | null; misspelled: string}[];
|
||||
}
|
||||
|
||||
|
||||
type InteractivePerfectAnswerKey = `perfect_answer_${number}`;
|
||||
type InteractiveTranscriptKey = `transcript_${number}`;
|
||||
type InteractiveFixedTextKey = `fixed_text_${number}`;
|
||||
|
||||
type InteractivePerfectAnswerType = { [key in InteractivePerfectAnswerKey]: { answer: string } };
|
||||
type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string };
|
||||
type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
|
||||
|
||||
interface InteractiveSpeakingEvaluation extends Evaluation,
|
||||
InteractivePerfectAnswerType,
|
||||
InteractiveTranscriptType,
|
||||
InteractiveFixedTextType { }
|
||||
type InteractivePerfectAnswerType = {[key in InteractivePerfectAnswerKey]: {answer: string}};
|
||||
type InteractiveTranscriptType = {[key in InteractiveTranscriptKey]?: string};
|
||||
type InteractiveFixedTextType = {[key in InteractiveFixedTextKey]?: string};
|
||||
|
||||
interface InteractiveSpeakingEvaluation extends Evaluation, InteractivePerfectAnswerType, InteractiveTranscriptType, InteractiveFixedTextType {}
|
||||
|
||||
interface SpeakingEvaluation extends CommonEvaluation {
|
||||
perfect_answer_1?: string;
|
||||
@@ -189,10 +185,10 @@ export interface InteractiveSpeakingExercise {
|
||||
first_title?: string;
|
||||
second_title?: string;
|
||||
text: string;
|
||||
prompts: { text: string; video_url: string }[];
|
||||
prompts: {text: string; video_url: string}[];
|
||||
userSolutions: {
|
||||
id: string;
|
||||
solution: { questionIndex: number; question: string; answer: string }[];
|
||||
solution: {questionIndex: number; question: string; answer: string}[];
|
||||
evaluation?: InteractiveSpeakingEvaluation;
|
||||
}[];
|
||||
topic?: string;
|
||||
@@ -208,14 +204,14 @@ export interface FillBlanksMCOption {
|
||||
B: string;
|
||||
C: string;
|
||||
D: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface FillBlanksExercise {
|
||||
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
||||
type: "fillBlanks";
|
||||
id: string;
|
||||
words: (string | { letter: string; word: string } | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"]
|
||||
words: (string | {letter: string; word: string} | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"]
|
||||
text: string; // *EXAMPLE: "They tried to {{1}} burning"
|
||||
allowRepetition?: boolean;
|
||||
solutions: {
|
||||
@@ -234,7 +230,7 @@ export interface TrueFalseExercise {
|
||||
id: string;
|
||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||
questions: TrueFalseQuestion[];
|
||||
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
|
||||
userSolutions: {id: string; solution: "true" | "false" | "not_given"}[];
|
||||
}
|
||||
|
||||
export interface TrueFalseQuestion {
|
||||
@@ -263,7 +259,7 @@ export interface MatchSentencesExercise {
|
||||
type: "matchSentences";
|
||||
id: string;
|
||||
prompt: string;
|
||||
userSolutions: { question: string; option: string }[];
|
||||
userSolutions: {question: string; option: string}[];
|
||||
sentences: MatchSentenceExerciseSentence[];
|
||||
allowRepetition: boolean;
|
||||
options: MatchSentenceExerciseOption[];
|
||||
@@ -286,7 +282,7 @@ export interface MultipleChoiceExercise {
|
||||
id: string;
|
||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||
questions: MultipleChoiceQuestion[];
|
||||
userSolutions: { question: string; option: string }[];
|
||||
userSolutions: {question: string; option: string}[];
|
||||
}
|
||||
|
||||
export interface MultipleChoiceQuestion {
|
||||
@@ -306,10 +302,10 @@ export interface ShuffleMap {
|
||||
questionID: string;
|
||||
map: {
|
||||
[key: string]: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface Shuffles {
|
||||
exerciseID: string;
|
||||
shuffles: ShuffleMap[]
|
||||
shuffles: ShuffleMap[];
|
||||
}
|
||||
|
||||
@@ -33,4 +33,4 @@ export interface Assignment {
|
||||
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 {capitalize} from "lodash";
|
||||
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 {useListSearch} from "@/hooks/useListSearch";
|
||||
|
||||
@@ -72,6 +72,28 @@ export default function ExamList({user}: {user: User}) {
|
||||
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) => {
|
||||
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",
|
||||
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", {
|
||||
header: "Created At",
|
||||
cell: (info) => {
|
||||
@@ -140,12 +166,18 @@ export default function ExamList({user}: {user: User}) {
|
||||
cell: ({row}: {row: {original: Exam}}) => {
|
||||
return (
|
||||
<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"
|
||||
className="cursor-pointer tooltip"
|
||||
onClick={async () => await loadExam(row.original.module, row.original.id)}>
|
||||
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
</button>
|
||||
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
||||
<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" />
|
||||
|
||||
@@ -459,6 +459,7 @@ export default function ExamPage({page}: Props) {
|
||||
user={user!}
|
||||
modules={selectedModules}
|
||||
solutions={userSolutions}
|
||||
assignment={assignment}
|
||||
information={{
|
||||
timeSpent,
|
||||
inactivity: totalInactivity,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import FillBlanksEdit from "@/components/Generation/fill.blanks.edit";
|
||||
import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit";
|
||||
import WriteBlankEdits from "@/components/Generation/write.blanks.edit";
|
||||
import Checkbox from "@/components/Low/Checkbox";
|
||||
import Input from "@/components/Low/Input";
|
||||
import Select from "@/components/Low/Select";
|
||||
import {
|
||||
@@ -234,7 +235,7 @@ interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const LevelGeneration = ({ id } : Props) => {
|
||||
const LevelGeneration = ({id}: Props) => {
|
||||
const [generatedExam, setGeneratedExam] = useState<LevelExam>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [resultingExam, setResultingExam] = useState<LevelExam>();
|
||||
@@ -242,6 +243,7 @@ const LevelGeneration = ({ id } : Props) => {
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||
const [numberOfParts, setNumberOfParts] = useState(1);
|
||||
const [parts, setParts] = useState<LevelSection[]>([{quantity: 10, type: "multiple_choice_4"}]);
|
||||
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
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,
|
||||
variant: "full",
|
||||
isDiagnostic: false,
|
||||
private: isPrivate,
|
||||
parts: parts
|
||||
.map((part, index) => {
|
||||
const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any;
|
||||
@@ -424,7 +427,7 @@ const LevelGeneration = ({ id } : Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!id) {
|
||||
if (!id) {
|
||||
toast.error("Please insert a title before submitting");
|
||||
return;
|
||||
}
|
||||
@@ -456,8 +459,8 @@ const LevelGeneration = ({ id } : Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-4 w-full">
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<div className="flex gap-4 w-full items-center">
|
||||
<div className="flex flex-col gap-3 w-1/2">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Difficulty</label>
|
||||
<Select
|
||||
options={DIFFICULTIES.map((x) => ({
|
||||
@@ -468,14 +471,20 @@ const LevelGeneration = ({ id } : Props) => {
|
||||
value={{value: difficulty, label: capitalize(difficulty)}}
|
||||
/>
|
||||
</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>
|
||||
<Input type="number" name="Number of Parts" onChange={(v) => setNumberOfParts(parseInt(v))} value={numberOfParts} />
|
||||
</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>
|
||||
<Input type="number" name="Timer (in minutes)" onChange={(v) => setTimer(parseInt(v))} value={timer} />
|
||||
</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>
|
||||
<Tab.Group>
|
||||
<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 WriteBlanksEdit from "@/components/Generation/write.blanks.edit";
|
||||
import {generate} from "random-words";
|
||||
import Checkbox from "@/components/Low/Checkbox";
|
||||
|
||||
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
||||
|
||||
@@ -232,7 +233,7 @@ interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ListeningGeneration = ({ id } : Props) => {
|
||||
const ListeningGeneration = ({id}: Props) => {
|
||||
const [part1, setPart1] = useState<ListeningPart>();
|
||||
const [part2, setPart2] = useState<ListeningPart>();
|
||||
const [part3, setPart3] = useState<ListeningPart>();
|
||||
@@ -241,6 +242,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [resultingExam, setResultingExam] = useState<ListeningExam>();
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const part1Timer = part1 ? 5 : 0;
|
||||
@@ -262,7 +264,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
||||
console.log({parts});
|
||||
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");
|
||||
return;
|
||||
}
|
||||
@@ -275,6 +277,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
||||
parts,
|
||||
minTimer,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
})
|
||||
.then((result) => {
|
||||
playSound("sent");
|
||||
@@ -313,7 +316,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
||||
|
||||
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">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||
<Input
|
||||
@@ -324,7 +327,7 @@ const ListeningGeneration = ({ id } : Props) => {
|
||||
className="max-w-[300px]"
|
||||
/>
|
||||
</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>
|
||||
<Select
|
||||
options={DIFFICULTIES.map((x) => ({
|
||||
@@ -336,6 +339,12 @@ const ListeningGeneration = ({ id } : Props) => {
|
||||
disabled={!!part1 || !!part2 || !!part3 || !!part4}
|
||||
/>
|
||||
</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>
|
||||
<Tab.Group>
|
||||
<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 MatchSentencesEdit from "@/components/Generation/match.sentences.edit";
|
||||
import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit";
|
||||
import Checkbox from "@/components/Low/Checkbox";
|
||||
|
||||
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
||||
|
||||
@@ -262,7 +263,7 @@ interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReadingGeneration = ({ id } : Props) => {
|
||||
const ReadingGeneration = ({id}: Props) => {
|
||||
const [part1, setPart1] = useState<ReadingPart>();
|
||||
const [part2, setPart2] = useState<ReadingPart>();
|
||||
const [part3, setPart3] = useState<ReadingPart>();
|
||||
@@ -270,6 +271,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [resultingExam, setResultingExam] = useState<ReadingExam>();
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const parts = [part1, part2, part3].filter((x) => !!x);
|
||||
@@ -304,7 +306,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!id) {
|
||||
if (!id) {
|
||||
toast.error("Please insert a title before submitting");
|
||||
return;
|
||||
}
|
||||
@@ -319,6 +321,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
||||
type: "academic",
|
||||
variant: parts.length === 3 ? "full" : "partial",
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
};
|
||||
|
||||
axios
|
||||
@@ -344,7 +347,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
||||
|
||||
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">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||
<Input
|
||||
@@ -355,7 +358,7 @@ const ReadingGeneration = ({ id } : Props) => {
|
||||
className="max-w-[300px]"
|
||||
/>
|
||||
</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>
|
||||
<Select
|
||||
options={DIFFICULTIES.map((x) => ({
|
||||
@@ -367,6 +370,12 @@ const ReadingGeneration = ({ id } : Props) => {
|
||||
disabled={!!part1 || !!part2 || !!part3}
|
||||
/>
|
||||
</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>
|
||||
<Tab.Group>
|
||||
<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 Select from "@/components/Low/Select";
|
||||
import {Difficulty, Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam";
|
||||
@@ -225,7 +226,7 @@ interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const SpeakingGeneration = ({ id } : Props) => {
|
||||
const SpeakingGeneration = ({id}: Props) => {
|
||||
const [part1, setPart1] = useState<SpeakingPart>();
|
||||
const [part2, setPart2] = useState<SpeakingPart>();
|
||||
const [part3, setPart3] = useState<SpeakingPart>();
|
||||
@@ -233,6 +234,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [resultingExam, setResultingExam] = useState<SpeakingExam>();
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const parts = [part1, part2, part3].filter((x) => !!x);
|
||||
@@ -247,7 +249,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
||||
const submitExam = () => {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
@@ -272,6 +274,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
||||
variant: minTimer >= 14 ? "full" : "partial",
|
||||
module: "speaking",
|
||||
instructorGender: genders.every((x) => x === "male") ? "male" : genders.every((x) => x === "female") ? "female" : "varied",
|
||||
private: isPrivate,
|
||||
};
|
||||
|
||||
axios
|
||||
@@ -313,7 +316,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
||||
|
||||
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">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||
<Input
|
||||
@@ -324,7 +327,7 @@ const SpeakingGeneration = ({ id } : Props) => {
|
||||
className="max-w-[300px]"
|
||||
/>
|
||||
</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>
|
||||
<Select
|
||||
options={DIFFICULTIES.map((x) => ({
|
||||
@@ -336,6 +339,13 @@ const SpeakingGeneration = ({ id } : Props) => {
|
||||
disabled={!!part1 || !!part2 || !!part3}
|
||||
/>
|
||||
</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>
|
||||
|
||||
<Tab.Group>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Checkbox from "@/components/Low/Checkbox";
|
||||
import Input from "@/components/Low/Input";
|
||||
import Select from "@/components/Low/Select";
|
||||
import {Difficulty, WritingExam, WritingExercise} from "@/interfaces/exam";
|
||||
@@ -79,13 +80,14 @@ interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const WritingGeneration = ({ id } : Props) => {
|
||||
const WritingGeneration = ({id}: Props) => {
|
||||
const [task1, setTask1] = useState<string>();
|
||||
const [task2, setTask2] = useState<string>();
|
||||
const [minTimer, setMinTimer] = useState(60);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [resultingExam, setResultingExam] = useState<WritingExam>();
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
|
||||
const [isPrivate, setPrivate] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const task1Timer = task1 ? 20 : 0;
|
||||
@@ -120,7 +122,7 @@ const WritingGeneration = ({ id } : Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!id) {
|
||||
if (!id) {
|
||||
toast.error("Please insert a title before submitting");
|
||||
return;
|
||||
}
|
||||
@@ -164,6 +166,7 @@ const WritingGeneration = ({ id } : Props) => {
|
||||
id,
|
||||
variant: exercise1 && exercise2 ? "full" : "partial",
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
};
|
||||
|
||||
axios
|
||||
@@ -188,7 +191,7 @@ const WritingGeneration = ({ id } : Props) => {
|
||||
|
||||
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">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||
<Input
|
||||
@@ -199,7 +202,7 @@ const WritingGeneration = ({ id } : Props) => {
|
||||
className="max-w-[300px]"
|
||||
/>
|
||||
</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>
|
||||
<Select
|
||||
options={DIFFICULTIES.map((x) => ({value: x, label: capitalize(x)}))}
|
||||
@@ -208,6 +211,13 @@ const WritingGeneration = ({ id } : Props) => {
|
||||
disabled={!!task1 || !!task2}
|
||||
/>
|
||||
</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>
|
||||
|
||||
<Tab.Group>
|
||||
|
||||
@@ -84,9 +84,16 @@ function commonExcel({
|
||||
.map((assignee: string) => {
|
||||
const userStats = allStats.filter((s: any) => s.user === assignee);
|
||||
const dates = userStats.map((s: any) => moment(s.date));
|
||||
const user = users.find((u) => u.id === assignee);
|
||||
return {
|
||||
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(
|
||||
(acc: any, curr: any) => {
|
||||
return {
|
||||
@@ -152,7 +159,7 @@ function commonExcel({
|
||||
});
|
||||
|
||||
// added empty arrays to force row spacings
|
||||
const customTableAndLine = [[],...customTable, []];
|
||||
const customTableAndLine = [[], ...customTable, []];
|
||||
customTableAndLine.forEach((row: string[], index) => {
|
||||
worksheet.addRow(row);
|
||||
});
|
||||
@@ -188,19 +195,24 @@ function commonExcel({
|
||||
worksheet.addRow(tableColumnHeaders);
|
||||
|
||||
// 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
|
||||
// const tableColumns = staticHeaders.length + numberOfTestSections;
|
||||
|
||||
// K10:M12 = 10,11,12,13
|
||||
// horizontally group Test Sections
|
||||
|
||||
// if there are test section headers to even merge:
|
||||
if (testSectionHeaders.length > 1) {
|
||||
worksheet.mergeCells(
|
||||
startIndexTable,
|
||||
staticHeaders.length + 1,
|
||||
startIndexTable,
|
||||
tableColumnHeadersFirstPart.length
|
||||
);
|
||||
}
|
||||
|
||||
// Add the dynamic second and third header rows for test sections and sub-columns
|
||||
worksheet.addRow([
|
||||
@@ -229,7 +241,12 @@ function commonExcel({
|
||||
|
||||
// vertically group based on the part, exercise and type
|
||||
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) => {
|
||||
@@ -316,13 +333,17 @@ async function mastercorporateAssignment(
|
||||
const adminsData = await getSpecificUsers(adminUsers);
|
||||
const companiesData = adminsData.map((user) => {
|
||||
const name = getUserName(user);
|
||||
const users = userGroupsParticipants
|
||||
.filter((p) => data.assignees.includes(p));
|
||||
const users = userGroupsParticipants.filter((p) =>
|
||||
data.assignees.includes(p)
|
||||
);
|
||||
|
||||
const stats = data.results
|
||||
.flatMap((r: any) => r.stats)
|
||||
.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(
|
||||
(acc: number, curr: any) => acc + curr.score.total,
|
||||
0
|
||||
@@ -342,9 +363,11 @@ async function mastercorporateAssignment(
|
||||
correct: companiesData.reduce((acc, curr) => acc + curr.correct, 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({
|
||||
data,
|
||||
userName: user.corporateInformation?.companyInformation?.name || "",
|
||||
@@ -354,12 +377,13 @@ async function mastercorporateAssignment(
|
||||
return {
|
||||
...u,
|
||||
corporateName: getUserName(admin),
|
||||
}
|
||||
};
|
||||
}),
|
||||
sectionName: "Master Corporate Name :",
|
||||
customTable: [['Corporate Summary'], ...customTable],
|
||||
customTable: [["Corporate Summary"], ...customTable],
|
||||
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":
|
||||
return corporateAssignment(user as CorporateUser, data, users);
|
||||
case "mastercorporate":
|
||||
return mastercorporateAssignment(user as MasterCorporateUser, data, users);
|
||||
return mastercorporateAssignment(
|
||||
user as MasterCorporateUser,
|
||||
data,
|
||||
users
|
||||
);
|
||||
default:
|
||||
throw new Error("Invalid user type");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
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 {sessionOptions} from "@/lib/session";
|
||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||
@@ -12,6 +12,7 @@ export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return get(req, res);
|
||||
if (req.method === "PATCH") return patch(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) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
|
||||
@@ -10,7 +10,7 @@ import {v4} from "uuid";
|
||||
import {checkAccess} from "@/utils/permissions";
|
||||
import {CEFR_STEPS} from "@/resources/grading";
|
||||
import {getCorporateUser} from "@/resources/user";
|
||||
import {getUserCorporate} from "@/utils/groups";
|
||||
import {getUserCorporate} from "@/utils/groups.be";
|
||||
import {Grading} from "@/interfaces";
|
||||
import {getGroupsForUser} from "@/utils/groups.be";
|
||||
import {uniq} from "lodash";
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import moment from "moment";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
|
||||
export const futureAssignmentFilter = (a: Assignment) =>
|
||||
moment(a.startDate).isAfter(moment()) && !a.archived && !a.start;
|
||||
export const futureAssignmentFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()) && !a.archived;
|
||||
|
||||
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 activeAssignmentFilter = (a: Assignment) =>
|
||||
moment(a.endDate).isAfter(moment()) &&
|
||||
// && moment(a.startDate).isBefore(moment())
|
||||
a.start &&
|
||||
a.assignees.length > a.results.length;
|
||||
export const activeAssignmentFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment());
|
||||
|
||||
@@ -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 {Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam} from "@/interfaces/exam";
|
||||
import {DeveloperUser, Stat, StudentUser, User} from "@/interfaces/user";
|
||||
@@ -17,7 +17,7 @@ export const getExams = async (
|
||||
): Promise<Exam[]> => {
|
||||
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 allExams = shuffle(
|
||||
|
||||
@@ -33,11 +33,34 @@ export const updateExpiryDateOnGroup = async (participantID: string, corporateID
|
||||
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 () => {
|
||||
const groupDocs = await getDocs(collection(db, "groups"));
|
||||
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[]> => {
|
||||
const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id)));
|
||||
return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[];
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
import { CorporateUser, Group, User, Type } from "@/interfaces/user";
|
||||
import {CorporateUser, Group, User, Type} from "@/interfaces/user";
|
||||
import axios from "axios";
|
||||
|
||||
export const isUserFromCorporate = async (userID: string) => {
|
||||
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`))
|
||||
.data;
|
||||
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`)).data;
|
||||
const users = (await axios.get<User[]>("/api/users/list")).data;
|
||||
|
||||
const adminTypes = groups.map(
|
||||
(g) => users.find((u) => u.id === g.admin)?.type
|
||||
);
|
||||
const adminTypes = groups.map((g) => users.find((u) => u.id === g.admin)?.type);
|
||||
return adminTypes.includes("corporate");
|
||||
};
|
||||
|
||||
const getAdminForGroup = async (userID: string, role: Type) => {
|
||||
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`))
|
||||
.data;
|
||||
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`)).data;
|
||||
|
||||
const adminRequests = await Promise.all(
|
||||
groups.map(async (g) => {
|
||||
const userRequest = await axios.get<User>(`/api/users/${g.admin}`);
|
||||
if (userRequest.status === 200) return userRequest.data;
|
||||
return undefined;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const admins = adminRequests.filter((x) => x?.type === role);
|
||||
return admins.length > 0 ? (admins[0] as CorporateUser) : undefined;
|
||||
};
|
||||
|
||||
export const getUserCorporate = async (
|
||||
userID: string
|
||||
): Promise<CorporateUser | undefined> => {
|
||||
export const getUserCorporate = async (userID: string): Promise<CorporateUser | undefined> => {
|
||||
const userRequest = await axios.get<User>(`/api/users/${userID}`);
|
||||
if (userRequest.status === 200) {
|
||||
const user = userRequest.data;
|
||||
|
||||
Reference in New Issue
Block a user