Merge remote-tracking branch 'origin/develop' into feature/training-content

This commit is contained in:
Carlos Mesquita
2024-08-28 13:32:21 +01:00
21 changed files with 331 additions and 139 deletions

View File

@@ -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"
onClick={() => {
if (disabled) return;
onChange(!isChecked); 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 ",
)}> )}>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,4 +33,4 @@ export interface Assignment {
start?: boolean; start?: boolean;
} }
export type AssignmentWithCorporateId = Assignment & { corporateId: string }; export type AssignmentWithCorporateId = Assignment & {corporateId: string};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
// if there are test section headers to even merge:
if (testSectionHeaders.length > 1) {
worksheet.mergeCells( worksheet.mergeCells(
startIndexTable, startIndexTable,
staticHeaders.length + 1, staticHeaders.length + 1,
startIndexTable, startIndexTable,
tableColumnHeadersFirstPart.length 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");
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,30 @@
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
): Promise<CorporateUser | undefined> => {
const userRequest = await axios.get<User>(`/api/users/${userID}`); const userRequest = await axios.get<User>(`/api/users/${userID}`);
if (userRequest.status === 200) { if (userRequest.status === 200) {
const user = userRequest.data; const user = userRequest.data;