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

This commit is contained in:
Carlos Mesquita
2024-09-07 11:38:18 +01:00
92 changed files with 4778 additions and 3063 deletions

View File

@@ -125,7 +125,7 @@ export default function BatchCreateUser({user, users, permissions, onFinish}: Pr
demographicInformation: {
country: countryItem?.countryCode,
passport_id: passport_id?.toString().trim() || undefined,
phone,
phone: phone.toString(),
},
}
: undefined;
@@ -161,7 +161,7 @@ export default function BatchCreateUser({user, users, permissions, onFinish}: Pr
setIsLoading(true);
try {
for (const newUser of newUsers) await axios.post("/api/make_user", {...newUser, type, expiryDate});
await axios.post("/api/batch_users", { users: newUsers.map(user => ({...user, type, expiryDate})) });
toast.success(`Successfully added ${newUsers.length} user(s)!`);
onFinish();
} catch {

View File

@@ -30,7 +30,6 @@ export default function CorporateGradingSystem({user, defaultSteps, mutate}: {us
if (areStepsOverlapped(steps)) return toast.error("There seems to be an overlap in one of your steps.");
if (
steps.reduce((acc, curr) => {
console.log(acc - (curr.max - curr.min + 1));
return acc - (curr.max - curr.min + 1);
}, 100) > 0
)

View File

@@ -1,4 +1,4 @@
import {useMemo} from "react";
import {useMemo, useState} from "react";
import {PERMISSIONS} from "@/constants/userPermissions";
import useExams from "@/hooks/useExams";
import useUsers from "@/hooks/useUsers";
@@ -11,11 +11,15 @@ import {countExercises} from "@/utils/moduleUtils";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import axios from "axios";
import clsx from "clsx";
import {capitalize} from "lodash";
import {capitalize, uniq} from "lodash";
import {useRouter} from "next/router";
import {BsBan, BsBanFill, BsCheck, BsCircle, BsStop, BsTrash, BsUpload, BsX} from "react-icons/bs";
import {BsBan, BsBanFill, BsCheck, BsCircle, BsPencil, BsStop, BsTrash, BsUpload, BsX} from "react-icons/bs";
import {toast} from "react-toastify";
import {useListSearch} from "@/hooks/useListSearch";
import Modal from "@/components/Modal";
import {checkAccess} from "@/utils/permissions";
import useGroups from "@/hooks/useGroups";
import Button from "@/components/Low/Button";
const searchFields = [["module"], ["id"], ["createdBy"]];
@@ -29,9 +33,40 @@ const CLASSES: {[key in Module]: string} = {
const columnHelper = createColumnHelper<Exam>();
const ExamOwnerSelector = ({options, exam, onSave}: {options: User[]; exam: Exam; onSave: (owners: string[]) => void}) => {
const [owners, setOwners] = useState(exam.owners || []);
return (
<div className="w-full flex flex-col gap-4">
<div className="grid grid-cols-4 mt-4">
{options.map((c) => (
<Button
variant={owners.includes(c.id) ? "solid" : "outline"}
onClick={() => setOwners((prev) => (prev.includes(c.id) ? prev.filter((x) => x !== c.id) : [...prev, c.id]))}
className="max-w-[200px] w-full"
key={c.id}>
{c.name}
</Button>
))}
</div>
<Button onClick={() => onSave(owners)} className="w-full max-w-[200px] self-end">
Save
</Button>
</div>
);
};
export default function ExamList({user}: {user: User}) {
const [selectedExam, setSelectedExam] = useState<Exam>();
const {exams, reload} = useExams();
const {users} = useUsers();
const {groups} = useGroups({admin: user?.id, userType: user?.type});
const filteredCorporates = useMemo(() => {
const participantsAndAdmins = uniq(groups.flatMap((x) => [...x.participants, x.admin])).filter((x) => x !== user?.id);
return users.filter((x) => participantsAndAdmins.includes(x.id) && x.type === "corporate");
}, [users, groups, user]);
const parsedExams = useMemo(() => {
return exams.map((exam) => {
@@ -94,6 +129,29 @@ export default function ExamList({user}: {user: User}) {
.finally(reload);
};
const updateExam = async (exam: Exam, body: object) => {
if (!confirm(`Are you sure you want to update this ${capitalize(exam.module)} exam?`)) return;
axios
.patch(`/api/exam/${exam.module}/${exam.id}`, body)
.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)
.finally(() => setSelectedExam(undefined));
};
const deleteExam = async (exam: Exam) => {
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
@@ -166,12 +224,21 @@ export default function ExamList({user}: {user: User}) {
cell: ({row}: {row: {original: Exam}}) => {
return (
<div className="flex gap-4">
<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>
{(row.original.owners?.includes(user.id) || checkAccess(user, ["admin", "developer"])) && (
<>
<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>
{checkAccess(user, ["admin", "developer", "mastercorporate"]) && (
<button data-tip="Edit owners" onClick={() => setSelectedExam(row.original)} className="cursor-pointer tooltip">
<BsPencil />
</button>
)}
</>
)}
<button
data-tip="Load exam"
className="cursor-pointer tooltip"
@@ -198,6 +265,13 @@ export default function ExamList({user}: {user: User}) {
return (
<div className="flex flex-col gap-4 w-full h-full">
{renderSearch()}
<Modal isOpen={!!selectedExam} title={`Edit Exam Owners - ${selectedExam?.id}`} onClose={() => setSelectedExam(undefined)}>
{!!selectedExam ? (
<ExamOwnerSelector options={filteredCorporates} exam={selectedExam} onSave={(owners) => updateExam(selectedExam, {owners})} />
) : (
<div />
)}
</Modal>
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (

View File

@@ -173,14 +173,13 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
control: (styles) => ({
...styles,
backgroundColor: "white",
borderRadius: "999px",
padding: "1rem 1.5rem",
zIndex: "40",
}),
}}
/>
{user.type !== "teacher" && (
<Button className="w-full max-w-[300px]" onClick={openFilePicker} isLoading={isLoading} variant="outline">
<Button className="w-full max-w-[300px] h-fit" onClick={openFilePicker} isLoading={isLoading} variant="outline">
{filesContent.length === 0 ? "Upload participants Excel file" : filesContent[0].name}
</Button>
)}
@@ -204,6 +203,7 @@ const filterTypes = ["corporate", "teacher", "mastercorporate"];
export default function GroupList({user}: {user: User}) {
const [isCreating, setIsCreating] = useState(false);
const [editingGroup, setEditingGroup] = useState<Group>();
const [viewingAllParticipants, setViewingAllParticipants] = useState<string>();
const {permissions} = usePermissions(user?.id || "");
@@ -254,11 +254,29 @@ export default function GroupList({user}: {user: User}) {
}),
columnHelper.accessor("participants", {
header: "Participants",
cell: (info) =>
info
.getValue()
.map((x) => users.find((y) => y.id === x)?.name)
.join(", "),
cell: (info) => (
<span>
{info
.getValue()
.slice(0, viewingAllParticipants === info.row.original.id ? undefined : 5)
.map((x) => users.find((y) => y.id === x)?.name)
.join(", ")}
{info.getValue().length > 5 && viewingAllParticipants !== info.row.original.id && (
<button
className="text-mti-purple-light font-bold hover:text-mti-purple-dark transition ease-in-out duration-300"
onClick={() => setViewingAllParticipants(info.row.original.id)}>
, View More
</button>
)}
{info.getValue().length > 5 && viewingAllParticipants === info.row.original.id && (
<button
className="text-mti-purple-light font-bold hover:text-mti-purple-dark transition ease-in-out duration-300"
onClick={() => setViewingAllParticipants(undefined)}>
, View Less
</button>
)}
</span>
),
}),
{
header: "",

View File

@@ -9,7 +9,7 @@ import axios from "axios";
import clsx from "clsx";
import {capitalize, reverse} from "lodash";
import moment from "moment";
import {Fragment, useEffect, useState} from "react";
import {Fragment, useEffect, useState, useMemo} from "react";
import {BsArrowDown, BsArrowDownUp, BsArrowUp, BsCheck, BsCheckCircle, BsEye, BsFillExclamationOctagonFill, BsPerson, BsTrash} from "react-icons/bs";
import {toast} from "react-toastify";
import {countries, TCountries} from "countries-list";
@@ -46,10 +46,12 @@ const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; grou
export default function UserList({
user,
filters = [],
type,
renderHeader,
}: {
user: User;
filters?: ((user: User) => boolean)[];
type?: Type;
renderHeader?: (total: number) => JSX.Element;
}) {
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
@@ -57,7 +59,12 @@ export default function UserList({
const [displayUsers, setDisplayUsers] = useState<User[]>([]);
const [selectedUser, setSelectedUser] = useState<User>();
const {users, reload} = useUsers();
const userHash = useMemo(() => ({
type,
size: 25,
}), [type])
const {users, page, total, reload, next, previous} = useUsers(userHash);
const {permissions} = usePermissions(user?.id || "");
const {balance} = useUserBalance();
const {groups} = useGroups({
@@ -80,19 +87,15 @@ export default function UserList({
useEffect(() => {
(async () => {
if (user && users) {
const filterUsers = ["corporate", "teacher", "mastercorporate"].includes(user.type)
? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id))
: users;
const filteredUsers = filters.reduce((d, f) => d.filter(f), filterUsers);
if (users && users.length > 0) {
const filteredUsers = filters.reduce((d, f) => d.filter(f), users);
const sortedUsers = await asyncSorter<User>(filteredUsers, sortFunction);
setDisplayUsers([...sortedUsers]);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, users, sorter, groups]);
}, [users, sorter]);
const deleteAccount = (user: User) => {
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
@@ -604,7 +607,7 @@ export default function UserList({
return (
<>
{renderHeader && renderHeader(displayUsers.length)}
{renderHeader && renderHeader(total)}
<div className="w-full">
<Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}>
{selectedUser && renderUserCard(selectedUser)}
@@ -616,6 +619,14 @@ export default function UserList({
Download List
</Button>
</div>
<div className="w-full flex gap-2 justify-between">
<Button className="w-full max-w-[200px]" disabled={page === 0} onClick={previous}>
Previous Page
</Button>
<Button className="w-full max-w-[200px]" disabled={page * 25 >= total} onClick={next}>
Next Page
</Button>
</div>
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (

View File

@@ -100,7 +100,7 @@ export default function UserCreator({user, users, permissions, onFinish}: Props)
if (!name || name.trim().length === 0) return toast.error("Please enter a valid name!");
if (!email || email.trim().length === 0) return toast.error("Please enter a valid e-mail address!");
if (users.map((x) => x.email).includes(email.trim())) return toast.error("That e-mail is already in use!");
if (!password || password.trim().length === 0) return toast.error("Please enter a valid password!");
if (!password || password.trim().length < 6) return toast.error("Please enter a valid password!");
if (password !== confirmPassword) return toast.error("The passwords do not match!");
setIsLoading(true);
@@ -199,21 +199,23 @@ export default function UserCreator({user, users, permissions, onFinish}: Props)
<Input type="text" name="department" label="Department" onChange={setPosition} value={position} placeholder="Department" />
)}
<div
className={clsx(
"flex flex-col gap-4",
(!["student", "teacher"].includes(type) || ["corporate", "teacher"].includes(user?.type)) &&
!["corporate", "mastercorporate"].includes(type) &&
"col-span-2",
)}>
<label className="font-normal text-base text-mti-gray-dim">Group</label>
<Select
options={groups
.filter((x) => (!selectedCorporate ? true : x.admin === selectedCorporate))
.map((g) => ({value: g.id, label: g.name}))}
onChange={(e) => setGroup(e?.value || undefined)}
/>
</div>
{!(type === "corporate" && user.type === "corporate") && (
<div
className={clsx(
"flex flex-col gap-4",
(!["student", "teacher"].includes(type) || ["corporate", "teacher"].includes(user?.type)) &&
!["corporate", "mastercorporate"].includes(type) &&
"col-span-2",
)}>
<label className="font-normal text-base text-mti-gray-dim">Group</label>
<Select
options={groups
.filter((x) => (!selectedCorporate ? true : x.admin === selectedCorporate))
.map((g) => ({value: g.id, label: g.name}))}
onChange={(e) => setGroup(e?.value || undefined)}
/>
</div>
)}
<div
className={clsx(

View File

@@ -214,7 +214,7 @@ export default function ExamPage({page, user}: Props) {
if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) {
const nextExam = exams[moduleIndex];
if (partIndex === -1 && nextExam.module !== "listening") setPartIndex(0);
if (partIndex === -1 && nextExam?.module !== "listening") setPartIndex(0);
if (exerciseIndex === -1 && !["reading", "listening"].includes(nextExam?.module)) setExerciseIndex(0);
setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined);
}
@@ -281,7 +281,7 @@ export default function ExamPage({page, user}: Props) {
}, [statsAwaitingEvaluation]);
useEffect(() => {
if (exam && exam.module === "level" && exam.parts[0].intro && !showSolutions) setBgColor("bg-ielts-level-light");
if (exam && exam.module === "level" && !showSolutions) setBgColor("bg-ielts-level-light");
}, [exam, showSolutions, setBgColor]);
const checkIfStatsHaveBeenEvaluated = (ids: string[]) => {

View File

@@ -15,31 +15,38 @@ import {
Exercise,
} from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import {getExamById} from "@/utils/exams";
import {playSound} from "@/utils/sound";
import {Tab} from "@headlessui/react";
import { getExamById } from "@/utils/exams";
import { playSound } from "@/utils/sound";
import { Tab } from "@headlessui/react";
import axios from "axios";
import clsx from "clsx";
import {capitalize, sample} from "lodash";
import {useRouter} from "next/router";
import {useEffect, useState} from "react";
import {BsArrowRepeat, BsCheck, BsPencilSquare, BsX} from "react-icons/bs";
import { capitalize, sample } from "lodash";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { BsArrowRepeat, BsCheck, BsPencilSquare, BsX } from "react-icons/bs";
import reactStringReplace from "react-string-replace";
import {toast} from "react-toastify";
import {v4} from "uuid";
import { toast } from "react-toastify";
import { v4 } from "uuid";
interface Option {
[key: string]: any;
value: string | null;
label: string;
}
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
const TYPES: {[key: string]: string} = {
const TYPES: { [key: string]: string } = {
multiple_choice_4: "Multiple Choice",
multiple_choice_blank_space: "Multiple Choice - Blank Space",
multiple_choice_underlined: "Multiple Choice - Underlined",
blank_space_text: "Blank Space",
reading_passage_utas: "Reading Passage",
fill_blanks_mc: "Multiple Choice - Fill Blanks",
};
type LevelSection = {type: string; quantity: number; topic?: string; part?: LevelPart};
type LevelSection = { type: string; quantity: number; topic?: string; part?: LevelPart };
const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion; onUpdate: (question: MultipleChoiceQuestion) => void}) => {
const QuestionDisplay = ({ question, onUpdate }: { question: MultipleChoiceQuestion; onUpdate: (question: MultipleChoiceQuestion) => void }) => {
const [isEditing, setIsEditing] = useState(false);
const [options, setOptions] = useState(question.options);
const [answer, setAnswer] = useState(question.solution);
@@ -70,7 +77,7 @@ const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion
<input
defaultValue={option.text}
className="w-60"
onChange={(e) => setOptions((prev) => prev.map((x, idx) => (idx === index ? {...x, text: e.target.value} : x)))}
onChange={(e) => setOptions((prev) => prev.map((x, idx) => (idx === index ? { ...x, text: e.target.value } : x)))}
/>
) : (
<span>{option.text}</span>
@@ -90,7 +97,7 @@ const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion
<>
<button
onClick={() => {
onUpdate({...question, options, solution: answer});
onUpdate({ ...question, options, solution: answer });
setIsEditing(false);
}}
className="p-2 border border-neutral-300 bg-white rounded-xl hover:drop-shadow transition ease-in-out duration-300">
@@ -108,9 +115,16 @@ const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion
);
};
const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (section: LevelSection) => void}) => {
const TaskTab = ({ section, label, index, setSection }: { section: LevelSection; label: string, index: number, setSection: (section: LevelSection) => void }) => {
const [isLoading, setIsLoading] = useState(false);
const [category, setCategory] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [customDescription, setCustomDescription] = useState<string>("");
const [previousOption, setPreviousOption] = useState<Option>({ value: "None", label: "None" });
const [descriptionOption, setDescriptionOption] = useState<Option>({ value: "None", label: "None" });
const [updateIntro, setUpdateIntro] = useState<boolean>(false);
const onUpdate = (question: MultipleChoiceQuestion) => {
if (!section) return;
@@ -124,6 +138,66 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
setSection(updatedExam as any);
};
const defaultPresets: any = {
multiple_choice_4: "Welcome to {part} of the {label}. In this section, you'll be asked to select the correct word or group of words that best completes each sentence.\n\nFor each question, carefully read the sentence and click on the option (A, B, C, or D) that you believe is correct. After making your selection, you can proceed to the next question by clicking \"Next\". If you need to review or change your previous answers, you can go back at any time by clicking \"Back\".",
multiple_choice_blank_space: undefined,
multiple_choice_underlined: "Welcome to {part} of the {label}. In this section, you'll be asked to identify the underlined word or group of words that is not correct in each sentence.\n\nFor each question, carefully review the sentence and click on the option (A, B, C, or D) that you believe contains the incorrect word or group of words. After making your selection, you can proceed to the next question by clicking \"Next\". If needed, you can go back to previous questions by clicking \"Back\".",
blank_space_text: undefined,
reading_passage_utas: "Welcome to {part} of the {label}. In this section, you will read a text and answer the questions that follow.\n\nCarefully read the provided text, then select the correct answer (A, B, C, or D) for each question. After making your selection, you can proceed to the next question by clicking \"Next\". If you need to review or change your answers, you can go back at any time by clicking \"Back\".",
fill_blanks_mc: "Welcome to {part} of the {label}. In this section, you will read a text and choose the correct word to fill in each blank space.\n\nFor each question, carefully read the text and click on the option that you believe best fits the context."
};
const getDefaultPreset = () => {
return defaultPresets[section.type] ? defaultPresets[section.type].replace('{part}', `Part ${index + 1}`).replace('{label}', label) :
"No default preset is yet available for this type of exercise."
}
useEffect(() => {
if (descriptionOption.value === "Default" && section?.type) {
setDescription(getDefaultPreset())
}
if (descriptionOption.value === "Custom" && customDescription !== "") {
setDescription(customDescription);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [descriptionOption, section?.type, label])
useEffect(() => {
if (section?.type) {
const defaultPreset = getDefaultPreset();
if (descriptionOption.value === "Default" && previousOption.value === "Default" && description !== defaultPreset) {
setDescriptionOption({ value: "Custom", label: "Custom" });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [descriptionOption, description, label])
useEffect(() => {
setPreviousOption(descriptionOption);
}, [descriptionOption])
useEffect(() => {
if (section?.part && ((descriptionOption.value === "Custom" || descriptionOption.value === "Default") && !section.part.intro)) {
setUpdateIntro(true);
}
}, [section?.part, descriptionOption, category])
useEffect(() => {
if (updateIntro && section.part) {
setSection({
...section,
part: {
...section.part!,
intro: descriptionOption.value === "Default" ? getDefaultPreset() : (descriptionOption.value === "Custom" ? customDescription : undefined),
category: category === "" ? undefined : category
}
})
setUpdateIntro(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updateIntro, section?.part])
const renderExercise = (exercise: Exercise) => {
if (exercise.type === "multipleChoice")
return (
@@ -138,7 +212,12 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
updateExercise={(data: any) =>
setSection({
...section,
part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))},
part: {
...section.part!,
exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? { ...x, ...data } : x)),
intro: descriptionOption.value === "Default" ? getDefaultPreset() : (descriptionOption.value === "Custom" ? customDescription : undefined),
category: category === "" ? undefined : category
}
})
}
/>
@@ -158,7 +237,12 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
updateExercise={(data: any) =>
setSection({
...section,
part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))},
part: {
...section.part!,
exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? { ...x, ...data } : x)),
intro: descriptionOption.value === "Default" ? getDefaultPreset() : (descriptionOption.value === "Custom" ? customDescription : undefined),
category: category === "" ? undefined : category
}
})
}
/>
@@ -178,7 +262,12 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
updateExercise={(data: any) =>
setSection({
...section,
part: {...section.part!, exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? {...x, ...data} : x))},
part: {
...section.part!,
exercises: section.part!.exercises.map((x) => (x.id === exercise.id ? { ...x, ...data } : x)),
intro: descriptionOption.value === "Default" ? getDefaultPreset() : (descriptionOption.value === "Custom" ? customDescription : undefined),
category: category === "" ? undefined : category
},
})
}
/>
@@ -188,30 +277,61 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
return (
<Tab.Panel className="w-full bg-ielts-level/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-8">
<div className="flex flex-row w-full gap-4">
<div className="flex flex-col gap-3 w-1/2">
<label className="font-normal text-base text-mti-gray-dim">Description</label>
<Select
options={["None", "Default", "Custom"].map((descriptionOption) => ({ value: descriptionOption, label: descriptionOption }))}
onChange={(o) => setDescriptionOption({ value: o!.value, label: o!.label })}
value={descriptionOption}
/>
</div>
<div className="flex flex-col gap-3 w-1/2">
<label className="font-normal text-base text-mti-gray-dim">Category</label>
<Input
type="text"
placeholder="Category"
name="category"
onChange={(e) => setCategory(e)}
roundness="full"
defaultValue={category}
/>
</div>
</div>
{descriptionOption.value !== "None" && (
<Input
type="textarea"
placeholder="Part Description"
name="category"
onChange={(e) => { setDescription(e); setCustomDescription(e); }}
roundness="full"
value={descriptionOption.value === "Default" ? description : customDescription}
/>
)}
<div className="flex gap-4 w-full">
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Exercise Type</label>
<Select
options={Object.keys(TYPES).map((key) => ({value: key, label: TYPES[key]}))}
onChange={(e) => setSection({...section, type: e!.value!})}
value={{value: section?.type || "multiple_choice_4", label: TYPES[section?.type || "multiple_choice_4"]}}
options={Object.keys(TYPES).map((key) => ({ value: key, label: TYPES[key] }))}
onChange={(e) => setSection({ ...section, type: e!.value! })}
value={{ value: section?.type || "multiple_choice_4", label: TYPES[section?.type || "multiple_choice_4"] }}
/>
</div>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Number of Questions</label>
<label className="font-normal text-base text-mti-gray-dim">{section?.type && section.type === "fill_blanks_mc" ? "Number of Words" : "Number of Questions"}</label>
<Input
type="number"
name="Number of Questions"
onChange={(v) => setSection({...section, quantity: parseInt(v)})}
onChange={(v) => setSection({ ...section, quantity: parseInt(v) })}
value={section?.quantity || 10}
/>
</div>
</div>
{section?.type === "reading_passage_utas" && (
{section?.type === "reading_passage_utas" || section?.type === "fill_blanks_mc" && (
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Topic</label>
<Input type="text" name="Topic" onChange={(v) => setSection({...section, topic: v})} value={section?.topic} />
<Input type="text" name="Topic" onChange={(v) => setSection({ ...section, topic: v })} value={section?.topic} />
</div>
)}
</div>
@@ -235,18 +355,19 @@ 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>();
const [timer, setTimer] = useState(10);
const [difficulty, setDifficulty] = useState<Difficulty>(sample(DIFFICULTIES)!);
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);
const [label, setLabel] = useState<string>("Placement Test");
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" })));
}, [numberOfParts]);
const router = useRouter();
@@ -289,7 +410,7 @@ const LevelGeneration = ({id}: Props) => {
let newParts = [...parts];
axios
.post<{exercises: {[key: string]: any}}>("/api/exam/level/generate/level", {nr_exercises: numberOfParts, ...body})
.post<{ exercises: { [key: string]: any } }>("/api/exam/level/generate/level", { nr_exercises: numberOfParts, ...body })
.then((result) => {
console.log(result.data);
@@ -304,6 +425,7 @@ const LevelGeneration = ({id}: Props) => {
variant: "full",
isDiagnostic: false,
private: isPrivate,
label: label,
parts: parts
.map((part, index) => {
const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any;
@@ -317,23 +439,55 @@ const LevelGeneration = ({id}: Props) => {
id: v4(),
prompt:
part.type === "multiple_choice_underlined"
? "Select the wrong part of the sentence."
: "Select the appropriate option.",
questions: currentExercise.questions.map((x: any) => ({...x, variant: "text"})),
? "Choose the underlined word or group of words that is not correct.\nFor each question, select your choice (A, B, C or D)."
: "Choose the correct word or group of words that completes the sentences below.\nFor each question, select the correct letter (A, B, C or D).",
questions: currentExercise.questions.map((x: any) => ({ ...x, variant: "text" })),
type: "multipleChoice",
userSolutions: [],
};
const item = {
exercises: [exercise],
intro: parts[index].part?.intro,
category: parts[index].part?.category
};
newParts = newParts.map((p, i) =>
i === index
? {
...p,
part: item,
}
...p,
part: item,
}
: p,
);
return item;
}
if (part.type === "fill_blanks_mc") {
const exercise: FillBlanksExercise = {
id: v4(),
prompt: "Read the text below and choose the correct word for each space.\nFor each question, select your choice (A, B, C or D). ",
text: currentExercise.text,
words: currentExercise.words,
solutions: currentExercise.solutions,
type: "fillBlanks",
variant: "mc",
userSolutions: [],
};
const item = {
exercises: [exercise],
intro: parts[index].part?.intro,
category: parts[index].part?.category
};
newParts = newParts.map((p, i) =>
i === index
? {
...p,
part: item,
}
: p,
);
@@ -341,28 +495,28 @@ const LevelGeneration = ({id}: Props) => {
}
if (part.type === "blank_space_text") {
console.log({currentExercise});
const exercise: WriteBlanksExercise = {
id: v4(),
prompt: "Complete the text below.",
text: currentExercise.text,
maxWords: 3,
solutions: currentExercise.words.map((x: any) => ({id: x.id, solution: [x.text]})),
solutions: currentExercise.words.map((x: any) => ({ id: x.id, solution: [x.text] })),
type: "writeBlanks",
userSolutions: [],
};
const item = {
exercises: [exercise],
intro: parts[index].part?.intro,
category: parts[index].part?.category
};
newParts = newParts.map((p, i) =>
i === index
? {
...p,
part: item,
}
...p,
part: item,
}
: p,
);
@@ -372,7 +526,7 @@ const LevelGeneration = ({id}: Props) => {
const mcExercise: MultipleChoiceExercise = {
id: v4(),
prompt: "Select the appropriate option.",
questions: currentExercise.exercises.multipleChoice.questions.map((x: any) => ({...x, variant: "text"})),
questions: currentExercise.exercises.multipleChoice.questions.map((x: any) => ({ ...x, variant: "text" })),
type: "multipleChoice",
userSolutions: [],
};
@@ -393,14 +547,16 @@ const LevelGeneration = ({id}: Props) => {
const item = {
context: currentExercise.text.content,
exercises: [mcExercise, wbExercise],
intro: parts[index].part?.intro,
category: parts[index].part?.category
};
newParts = newParts.map((p, i) =>
i === index
? {
...p,
part: item,
}
...p,
part: item,
}
: p,
);
@@ -434,10 +590,27 @@ const LevelGeneration = ({id}: Props) => {
setIsLoading(true);
parts.forEach((part) => {
part.part!.exercises.forEach((exercise, i) => {
switch(exercise.type) {
case 'fillBlanks':
exercise.prompt.replaceAll('\n', '\\n')
break;
case 'multipleChoice':
exercise.prompt.replaceAll('\n', '\\n')
break;
case 'writeBlanks':
exercise.prompt.replaceAll('\n', '\\n')
break;
}
});
})
const exam = {
...generatedExam,
id,
parts: generatedExam.parts.map((p, i) => ({...p, exercises: parts[i].part!.exercises})),
label: label,
parts: generatedExam.parts.map((p, i) => ({ ...p, exercises: parts[i].part!.exercises, category: parts[i].part?.category, intro: parts[i].part?.intro?.replaceAll('\n', '\\n') })),
};
axios
@@ -468,7 +641,7 @@ const LevelGeneration = ({id}: Props) => {
label: capitalize(x),
}))}
onChange={(value) => (value ? setDifficulty(value.value as Difficulty) : null)}
value={{value: difficulty, label: capitalize(difficulty)}}
value={{ value: difficulty, label: capitalize(difficulty) }}
/>
</div>
<div className="flex flex-col gap-3 w-1/3">
@@ -486,12 +659,24 @@ const LevelGeneration = ({id}: Props) => {
</Checkbox>
</div>
</div>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">Exam Label *</label>
<Input
type="text"
placeholder="Label"
name="label"
onChange={(e) => setLabel(e)}
roundness="xl"
defaultValue={label}
required
/>
</div>
<Tab.Group>
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
{Array.from(Array(numberOfParts), (_, index) => index).map((index) => (
<Tab
key={index}
className={({selected}) =>
className={({ selected }) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/70",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-level focus:outline-none focus:ring-2",
@@ -507,6 +692,8 @@ const LevelGeneration = ({id}: Props) => {
{Array.from(Array(numberOfParts), (_, index) => index).map((index) => (
<TaskTab
key={index}
label={label}
index={index}
section={parts[index]}
setSection={(part) => {
console.log(part);

View File

@@ -1,68 +1,37 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { getAllAssignersByCorporate } from "@/utils/groups.be";
import { getAssignmentsByAssigners } from "@/utils/assignments.be";
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {getAssignmentsByAssigner, getAssignmentsForCorporates} from "@/utils/assignments.be";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") return await GET(req, res);
if (req.method === "GET") return await GET(req, res);
res.status(404).json({ ok: false });
res.status(404).json({ok: false});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
const { ids, startDate, endDate } = req.query as {
ids: string;
startDate?: string;
endDate?: string;
};
const {ids, startDate, endDate} = req.query as {
ids: string;
startDate?: string;
endDate?: string;
};
const startDateParsed = startDate ? new Date(startDate) : undefined;
const endDateParsed = endDate ? new Date(endDate) : undefined;
try {
const idsList = ids.split(",");
const startDateParsed = startDate ? new Date(startDate) : undefined;
const endDateParsed = endDate ? new Date(endDate) : undefined;
try {
const idsList = ids.split(",");
const assigners = await Promise.all(
idsList.map(async (id) => {
const assigners = await getAllAssignersByCorporate(id);
return {
corporateId: id,
assigners,
};
})
);
const assignments = await Promise.all(assigners.map(async (data) => {
try {
const assigners = [...new Set([...data.assigners, data.corporateId])];
const assignments = await getAssignmentsByAssigners(
assigners,
startDateParsed,
endDateParsed
);
return assignments.map((assignment) => ({
...assignment,
corporateId: data.corporateId,
}));
} catch (err) {
console.error(err);
return [];
}
}));
console.log(assignments);
// const assignments = await getAssignmentsByAssigners(assignmentList, startDateParsed, endDateParsed);
res.status(200).json(assignments.flat());
} catch (err: any) {
res.status(500).json({ error: err.message });
}
const assignments = await getAssignmentsForCorporates(idsList, startDateParsed, endDateParsed);
res.status(200).json(assignments);
} catch (err: any) {
res.status(500).json({error: err.message});
}
}

View File

@@ -12,7 +12,7 @@ import {capitalize, flatten, uniqBy} from "lodash";
import {User} from "@/interfaces/user";
import moment from "moment";
import {sendEmail} from "@/email";
import { release } from "os";
import {release} from "os";
const db = getFirestore(app);
@@ -57,6 +57,7 @@ const generateExams = async (
generateMultiple: Boolean,
selectedModules: Module[],
assignees: string[],
userId: string,
variant?: Variant,
instructorGender?: InstructorGender,
): Promise<ExamWithUser[]> => {
@@ -87,7 +88,7 @@ const generateExams = async (
}
const selectedModulePromises = selectedModules.map(async (module: Module) => {
const exams: Exam[] = await getExams(db, module, "false", undefined, variant, instructorGender);
const exams: Exam[] = await getExams(db, module, "false", userId, variant, instructorGender);
const exam = exams[getRandomIndex(exams)];
if (exam) {
@@ -122,11 +123,12 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
endDate: string;
variant?: Variant;
instructorGender?: InstructorGender;
released: boolean;
};
const exams: ExamWithUser[] = !!examIDs
? examIDs.flatMap((e) => assignees.map((a) => ({...e, assignee: a})))
: await generateExams(generateMultiple, selectedModules, assignees, variant, instructorGender);
: await generateExams(generateMultiple, selectedModules, assignees, req.session.user!.id, variant, instructorGender);
if (exams.length === 0) {
res.status(400).json({ok: false, error: "No exams found for the selected modules"});
@@ -139,7 +141,6 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
results: [],
exams,
instructorGender,
released: false,
...body,
});

View File

@@ -0,0 +1,243 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { app, storage } from "@/firebase";
import { getFirestore } from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { AssignmentWithCorporateId } from "@/interfaces/results";
import moment from "moment-timezone";
import ExcelJS from "exceljs";
import { getSpecificUsers } from "@/utils/users.be";
import { checkAccess } from "@/utils/permissions";
import { getAssignmentsForCorporates } from "@/utils/assignments.be";
import { search } from "@/utils/search";
import { getGradingSystem } from "@/utils/grading.be";
import { Exam } from "@/interfaces/exam";
import { User } from "@/interfaces/user";
import { calculateBandScore, getGradingLabel } from "@/utils/score";
import { Module } from "@/interfaces";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
interface TableData {
user: string;
email: string;
correct: number;
corporate: string;
submitted: boolean;
date: moment.Moment;
assignment: string;
corporateId: string;
score: number;
level: string;
part1?: string;
part2?: string;
part3?: string;
part4?: string;
part5?: string;
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
// if (req.method === "GET") return get(req, res);
if (req.method === "POST") return await post(req, res);
}
const searchFilters = [["email"], ["user"], ["userId"]];
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to export
if (req.session.user) {
if (
!checkAccess(req.session.user, ["mastercorporate", "developer", "admin"])
) {
return res.status(401).json({ error: "Unauthorized" });
}
const { ids, startDate, endDate, searchText } = req.body as {
ids: string[];
startDate?: string;
endDate?: string;
searchText: string;
};
const startDateParsed = startDate ? new Date(startDate) : undefined;
const endDateParsed = endDate ? new Date(endDate) : undefined;
const assignments = await getAssignmentsForCorporates(
ids,
startDateParsed,
endDateParsed
);
const assignmentUsers = [
...new Set(assignments.flatMap((a) => a.assignees)),
];
const assigners = [...new Set(assignments.map((a) => a.assigner))];
const users = await getSpecificUsers(assignmentUsers);
const assignerUsers = await getSpecificUsers(assigners);
const assignerUsersGradingSystems = await Promise.all(
assignerUsers.map(async (user: User) => {
const data = await getGradingSystem(user);
// in this context I need to override as I'll have to match to the assigner
return { ...data, user: user.id };
})
);
const getGradingSystemHelper = (
exams: {id: string; module: Module; assignee: string}[],
assigner: string,
user: User,
correct: number,
total: number
) => {
if (exams.some((e) => e.module === "level")) {
const gradingSystem = assignerUsersGradingSystems.find(
(gs) => gs.user === assigner
);
if (gradingSystem) {
const bandScore = calculateBandScore(
correct,
total,
"level",
user.focus
);
return { label: getGradingLabel(bandScore, gradingSystem?.steps || []), score: bandScore };
}
}
return { score: -1, label: "N/A" };
};
const tableResults = assignments.reduce(
(accmA: TableData[], a: AssignmentWithCorporateId) => {
const userResults = a.assignees.map((assignee) => {
const userStats =
a.results.find((r) => r.user === assignee)?.stats || [];
const userData = users.find((u) => u.id === assignee);
const corporateUser = users.find((u) => u.id === a.assigner);
const correct = userStats.reduce((n, e) => n + e.score.correct, 0);
const total = userStats.reduce((n, e) => n + e.score.total, 0);
const { label: level, score } = getGradingSystemHelper(
a.exams,
a.assigner,
userData!,
correct,
total
);
console.log("Level", level);
const commonData = {
user: userData?.name || "",
email: userData?.email || "",
userId: assignee,
corporateId: a.corporateId,
corporate: corporateUser?.name || "",
assignment: a.name,
level,
score,
};
if (userStats.length === 0) {
return {
...commonData,
correct: 0,
submitted: false,
// date: moment(),
};
}
const partsData = userStats.every((e) => e.module === "level") ? userStats.reduce((acc, e, index) => {
return {
...acc,
[`part${index}`]: `${e.score.correct}/${e.score.total}`
}
}, {}) : {};
return {
...commonData,
correct,
submitted: true,
date: moment.max(userStats.map((e) => moment(e.date))),
...partsData,
};
}) as TableData[];
return [...accmA, ...userResults];
},
[]
).sort((a,b) => b.score - a.score);
// Create a new workbook and add a worksheet
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Master Statistical");
const headers = [
{
label: "User",
value: (entry: TableData) => entry.user,
},
{
label: "Email",
value: (entry: TableData) => entry.email,
},
{
label: "Corporate",
value: (entry: TableData) => entry.corporate,
},
{
label: "Assignment",
value: (entry: TableData) => entry.assignment,
},
{
label: "Submitted",
value: (entry: TableData) => (entry.submitted ? "Yes" : "No"),
},
{
label: "Correct",
value: (entry: TableData) => entry.correct,
},
{
label: "Date",
value: (entry: TableData) => entry.date?.format("YYYY/MM/DD") || "",
},
{
label: "Level",
value: (entry: TableData) => entry.level,
},
...new Array(5).fill(0).map((_, index) => ({
label: `Part ${index + 1}`,
value: (entry: TableData) => {
const key = `part${index}` as keyof TableData;
return entry[key] || "";
},
})),
];
const filteredSearch = searchText
? search(searchText, searchFilters, tableResults)
: tableResults;
worksheet.addRow(headers.map((h) => h.label));
(filteredSearch as TableData[]).forEach((entry) => {
worksheet.addRow(headers.map((h) => h.value(entry)));
});
// Convert workbook to Buffer (Node.js) or Blob (Browser)
const buffer = await workbook.xlsx.writeBuffer();
// generate the file ref for storage
const fileName = `${Date.now().toString()}.xlsx`;
const refName = `statistical/${fileName}`;
const fileRef = ref(storage, refName);
// upload the pdf to storage
const snapshot = await uploadBytes(fileRef, buffer, {
contentType:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
return res.status(401).json({ error: "Unauthorized" });
}

View File

@@ -0,0 +1,61 @@
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import { FirebaseScrypt } from 'firebase-scrypt';
import { firebaseAuthScryptParams } from "@/firebase";
import crypto from 'crypto';
import axios from "axios";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
return res.status(404).json({ok: false});
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const maker = req.session.user;
if (!maker) {
return res.status(401).json({ok: false, reason: "You must be logged in to make user!"});
}
const scrypt = new FirebaseScrypt(firebaseAuthScryptParams)
const users = req.body.users as {
email: string;
name: string;
type: string;
passport_id: string;
groupName?: string;
corporate?: string;
studentID?: string;
expiryDate?: string;
demographicInformation: {
country?: string;
passport_id?: string;
phone: string;
};
passwordHash: string | undefined;
passwordSalt: string | undefined;
}[];
const usersWithPasswordHashes = await Promise.all(users.map(async (user) => {
const currentUser = { ...user };
const salt = crypto.randomBytes(16).toString('base64');
const hash = await scrypt.hash(user.passport_id, salt);
currentUser.email = currentUser.email.toLowerCase();
currentUser.passwordHash = hash;
currentUser.passwordSalt = salt;
return currentUser;
}));
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/batch_users`, { makerID: maker.id, users: usersWithPasswordHashes }, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
});
return res.status(backendRequest.status).json(backendRequest.data)
}

View File

@@ -1,89 +1,76 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import {
getFirestore,
setDoc,
doc,
runTransaction,
collection,
query,
where,
getDocs,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Exam, InstructorGender, Variant } from "@/interfaces/exam";
import { getExams } from "@/utils/exams.be";
import { Module } from "@/interfaces";
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, setDoc, doc, runTransaction, collection, query, where, getDocs} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Exam, InstructorGender, Variant} from "@/interfaces/exam";
import {getExams} from "@/utils/exams.be";
import {Module} from "@/interfaces";
import {getUserCorporate} from "@/utils/groups.be";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await GET(req, res);
if (req.method === "POST") return await POST(req, res);
if (req.method === "GET") return await GET(req, res);
if (req.method === "POST") return await POST(req, res);
res.status(404).json({ ok: false });
res.status(404).json({ok: false});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const { module, avoidRepeated, variant, instructorGender } = req.query as {
module: Module;
avoidRepeated: string;
variant?: Variant;
instructorGender?: InstructorGender;
};
const {module, avoidRepeated, variant, instructorGender} = req.query as {
module: Module;
avoidRepeated: string;
variant?: Variant;
instructorGender?: InstructorGender;
};
const exams: Exam[] = await getExams(
db,
module,
avoidRepeated,
req.session.user.id,
variant,
instructorGender
);
res.status(200).json(exams);
const exams: Exam[] = await getExams(db, module, avoidRepeated, req.session.user.id, variant, instructorGender);
res.status(200).json(exams);
}
async function POST(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.session.user.type !== "developer") {
res.status(403).json({ ok: false });
return;
}
const { module } = req.query as { module: string };
const {module} = req.query as {module: string};
const corporate = await getUserCorporate(req.session.user.id);
try {
const exam = {
...req.body,
module: module,
createdBy: req.session.user.id,
createdAt: new Date().toISOString(),
};
await runTransaction(db, async (transaction) => {
const docRef = doc(db, module, req.body.id);
const docSnap = await transaction.get(docRef);
try {
const exam = {
...req.body,
module: module,
owners: [
...(["mastercorporate", "corporate"].includes(req.session.user.type) ? [req.session.user.id] : []),
...(!!corporate ? [corporate.id] : []),
],
createdBy: req.session.user.id,
createdAt: new Date().toISOString(),
};
if (docSnap.exists()) {
throw new Error("Name already exists");
}
await runTransaction(db, async (transaction) => {
const docRef = doc(db, module, req.body.id);
const docSnap = await transaction.get(docRef);
const newDocRef = doc(db, module, req.body.id);
transaction.set(newDocRef, exam);
});
res.status(200).json(exam);
} catch (error) {
console.error("Transaction failed: ", error);
res.status(500).json({ ok: false, error: (error as any).message });
}
if (docSnap.exists()) {
throw new Error("Name already exists");
}
const newDocRef = doc(db, module, req.body.id);
transaction.set(newDocRef, exam);
});
res.status(200).json(exam);
} catch (error) {
console.error("Transaction failed: ", error);
res.status(500).json({ok: false, error: (error as any).message});
}
}

View File

@@ -24,7 +24,7 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
return;
}
const moduleExamsPromises = [...MODULE_ARRAY, "level"].map(async (module) => {
const moduleExamsPromises = MODULE_ARRAY.map(async (module) => {
const moduleRef = collection(db, module);
const q = query(moduleRef, where("isDiagnostic", "==", false));

View File

@@ -15,6 +15,7 @@ import {Grading} from "@/interfaces";
import {getGroupsForUser} from "@/utils/groups.be";
import {uniq} from "lodash";
import {getUser} from "@/utils/users.be";
import { getGradingSystem } from "@/utils/grading.be";
const db = getFirestore(app);
@@ -31,19 +32,8 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
return;
}
const snapshot = await getDoc(doc(db, "grading", req.session.user.id));
if (snapshot.exists()) return res.status(200).json(snapshot.data());
if (req.session.user.type !== "teacher" && req.session.user.type !== "student")
return res.status(200).json({steps: CEFR_STEPS, user: req.session.user.id});
const corporate = await getUserCorporate(req.session.user.id);
if (!corporate) return res.status(200).json(CEFR_STEPS);
const corporateSnapshot = await getDoc(doc(db, "grading", corporate.id));
if (corporateSnapshot.exists()) return res.status(200).json(snapshot.data());
return res.status(200).json({steps: CEFR_STEPS, user: req.session.user.id});
const gradingSystem = await getGradingSystem(req.session.user);
return res.status(200).json(gradingSystem);
}
async function post(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -7,7 +7,8 @@ import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {v4} from "uuid";
import {updateExpiryDateOnGroup, getGroupsForUser} from "@/utils/groups.be";
import {uniqBy} from "lodash";
import {uniq, uniqBy} from "lodash";
import {getUser} from "@/utils/users.be";
const db = getFirestore(app);
@@ -29,29 +30,10 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
participant: string;
};
if (req.session?.user?.type === "mastercorporate") {
try {
const masterCorporateGroups = await getGroupsForUser(admin, participant);
const corporatesFromMaster = masterCorporateGroups.filter((g) => g.name.trim() === "Corporate").flatMap((g) => g.participants);
if (corporatesFromMaster.length === 0) return res.status(200).json(masterCorporateGroups);
const groups = await Promise.all(corporatesFromMaster.map((c) => getGroupsForUser(c, participant)));
return res.status(200).json([...masterCorporateGroups, ...uniqBy(groups.flat(), "id")]);
} catch (e) {
console.error(e);
res.status(500).json({ok: false});
return;
}
}
try {
const groups = await getGroupsForUser(admin, participant);
res.status(200).json(groups);
} catch (e) {
console.error(e);
res.status(500).json({ok: false});
}
const adminGroups = await getGroupsForUser(admin, participant);
const participants = uniq(adminGroups.flatMap((g) => g.participants));
const groups = await Promise.all(participants.map(async (c) => await getGroupsForUser(c, participant)));
return res.status(200).json([...adminGroups, ...uniqBy(groups.flat(), "id")]);
}
async function post(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -4,9 +4,12 @@ import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, de
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {v4} from "uuid";
import {CorporateUser, Group} from "@/interfaces/user";
import {CorporateUser, Group, Type} from "@/interfaces/user";
import {createUserWithEmailAndPassword, getAuth} from "firebase/auth";
import ShortUniqueId from "short-unique-id";
import {getUserCorporate, getUserGroups} from "@/utils/groups.be";
import {uniq} from "lodash";
import {getUser} from "@/utils/users.be";
const DEFAULT_DESIRED_LEVELS = {
reading: 9,
@@ -27,6 +30,13 @@ const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
const getUsersOfType = async (admin: string, type: Type) => {
const groups = await getUserGroups(admin);
const users = await Promise.all(uniq(groups.flatMap((x) => x.participants)).map(getUser));
return users.filter((x) => x.type === type).map((x) => x.id);
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
@@ -38,19 +48,20 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
if (!maker) {
return res.status(401).json({ok: false, reason: "You must be logged in to make user!"});
}
const {email, passport_id, password, type, groupName, groupID, expiryDate, corporate} = req.body as {
const corporateCorporate = await getUserCorporate(maker.id);
const {email, passport_id, password, type, groupID, expiryDate, corporate} = req.body as {
email: string;
password?: string;
passport_id: string;
type: string;
groupName?: string;
groupID?: string;
corporate?: string;
expiryDate: null | Date;
};
// cleaning data
delete req.body.passport_id;
delete req.body.groupName;
delete req.body.groupID;
delete req.body.expiryDate;
delete req.body.password;
@@ -60,6 +71,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
.then(async (userCredentials) => {
const userId = userCredentials.user.uid;
const profilePicture = !corporateCorporate ? "/defaultAvatar.png" : corporateCorporate.profilePicture;
const user = {
...req.body,
bio: "",
@@ -67,12 +80,22 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
focus: "academic",
status: "active",
desiredLevels: DEFAULT_DESIRED_LEVELS,
profilePicture: "/defaultAvatar.png",
profilePicture,
levels: DEFAULT_LEVELS,
isFirstLogin: false,
isVerified: true,
registrationDate: new Date(),
subscriptionExpirationDate: expiryDate || null,
...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate"
? {
corporateInformation: {
companyInformation: {
name: maker.corporateInformation?.companyInformation?.name || "N/A",
userAmount: 0,
},
},
}
: {}),
};
const uid = new ShortUniqueId();
@@ -88,15 +111,18 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
userId,
email: email.toLowerCase(),
name: req.body.name,
passport_id,
...(!!passport_id ? {passport_id} : {}),
});
if (type === "corporate") {
const students = maker.type === "corporate" ? await getUsersOfType(maker.id, "student") : [];
const teachers = maker.type === "corporate" ? await getUsersOfType(maker.id, "teacher") : [];
const defaultTeachersGroup: Group = {
admin: userId,
id: v4(),
name: "Teachers",
participants: [],
participants: teachers,
disableEditing: true,
};
@@ -104,29 +130,20 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
admin: userId,
id: v4(),
name: "Students",
participants: [],
disableEditing: true,
};
const defaultCorporateGroup: Group = {
admin: userId,
id: v4(),
name: "Corporate",
participants: [],
participants: students,
disableEditing: true,
};
await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup);
await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup);
await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup);
}
if (!!corporate) {
const corporateQ = query(collection(db, "users"), where("email", "==", corporate));
const corporateQ = query(collection(db, "users"), where("email", "==", corporate.trim().toLowerCase()));
const corporateSnapshot = await getDocs(corporateQ);
if (!corporateSnapshot.empty) {
const corporateUser = corporateSnapshot.docs[0].data() as CorporateUser;
const corporateUser = {...corporateSnapshot.docs[0].data(), id: corporateSnapshot.docs[0].id} as CorporateUser;
await setDoc(doc(db, "codes", code), {creator: corporateUser.id}, {merge: true});
const q = query(
@@ -146,33 +163,76 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
participants: [...participants, userId],
});
}
} else {
const defaultGroup: Group = {
admin: corporateUser.id,
id: v4(),
name: type === "student" ? "Students" : "Teachers",
participants: [userId],
disableEditing: true,
};
await setDoc(doc(db, "groups", defaultGroup.id), defaultGroup);
}
}
}
if (typeof groupName === "string" && groupName.trim().length > 0) {
const q = query(collection(db, "groups"), where("admin", "==", maker.id), where("name", "==", groupName.trim()), limit(1));
if (maker.type === "corporate") {
await setDoc(doc(db, "codes", code), {creator: maker.id}, {merge: true});
const q = query(
collection(db, "groups"),
where("admin", "==", maker.id),
where("name", "==", type === "student" ? "Students" : "Teachers"),
limit(1),
);
const snapshot = await getDocs(q);
if (snapshot.empty) {
const values = {
id: v4(),
admin: maker.id,
name: groupName.trim(),
participants: [userId],
disableEditing: false,
};
await setDoc(doc(db, "groups", values.id), values);
} else {
if (!snapshot.empty) {
const doc = snapshot.docs[0];
const participants: string[] = doc.get("participants");
if (!participants.includes(userId)) {
updateDoc(doc.ref, {
await updateDoc(doc.ref, {
participants: [...participants, userId],
});
}
} else {
const defaultGroup: Group = {
admin: maker.id,
id: v4(),
name: type === "student" ? "Students" : "Teachers",
participants: [userId],
disableEditing: true,
};
await setDoc(doc(db, "groups", defaultGroup.id), defaultGroup);
}
}
if (!!corporateCorporate && corporateCorporate.type === "mastercorporate" && type === "corporate") {
const q = query(collection(db, "groups"), where("admin", "==", corporateCorporate.id), where("name", "==", "corporate"), limit(1));
const snapshot = await getDocs(q);
if (!snapshot.empty) {
const doc = snapshot.docs[0];
const participants: string[] = doc.get("participants");
if (!participants.includes(userId)) {
await updateDoc(doc.ref, {
participants: [...participants, userId],
});
}
} else {
const defaultGroup: Group = {
admin: corporateCorporate.id,
id: v4(),
name: "Corporate",
participants: [userId],
disableEditing: true,
};
await setDoc(doc(db, "groups", defaultGroup.id), defaultGroup);
}
}

View File

@@ -4,6 +4,8 @@ import {app} from "@/firebase";
import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc, getDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Session} from "@/hooks/useSessions";
import moment from "moment";
const db = getFirestore(app);
@@ -24,12 +26,17 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
const q = user ? query(collection(db, "sessions"), where("user", "==", user)) : collection(db, "sessions");
const snapshot = await getDocs(q);
const sessions = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Session[];
res.status(200).json(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})),
sessions.filter((x) => {
if (!x.assignment) return true;
if (x.assignment.results.filter((y) => y.user === user).length > 0) return false;
return !moment().isAfter(moment(x.assignment.endDate));
}),
);
}

View File

@@ -45,8 +45,6 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
const targetUser = {...docTargetUser.data(), id: docTargetUser.id} as User;
if (user.type === "corporate" && (targetUser.type === "student" || targetUser.type === "teacher")) {
res.json({ok: true});
const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id)));
await Promise.all([
...userParticipantGroup.docs
@@ -66,14 +64,6 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
return;
}
const permission = PERMISSIONS.deleteUser[targetUser.type];
if (!permission.list.includes(user.type)) {
res.status(403).json({ok: false});
return;
}
res.json({ok: true});
await auth.deleteUser(id);
await deleteDoc(doc(db, "users", id));
const userCodeDocs = await getDocs(query(collection(db, "codes"), where("userId", "==", id)));
@@ -96,6 +86,8 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
),
),
]);
res.json({ok: true});
}
async function get(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -1,11 +1,9 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
const db = getFirestore(app);
import {getLinkedUsers} from "@/utils/users.be";
import {Type} from "@/interfaces/user";
export default withIronSessionApiRoute(handler, sessionOptions);
@@ -15,12 +13,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return;
}
const snapshot = await getDocs(collection(db, "users"));
const {size, type, latestID, firstID} = req.query as {size?: string; type?: Type; latestID?: string; firstID?: string};
res.status(200).json(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})),
const {users, total} = await getLinkedUsers(
req.session.user?.id,
req.session.user?.type,
type,
firstID,
latestID,
size !== undefined ? parseInt(size) : undefined,
);
res.status(200).json({users, total});
}

View File

@@ -29,11 +29,12 @@ import MasterCorporateDashboard from "@/dashboards/MasterCorporate";
import PaymentDue from "./(status)/PaymentDue";
import {useRouter} from "next/router";
import {PayPalScriptProvider} from "@paypal/react-paypal-js";
import {CorporateUser, MasterCorporateUser, Type, userTypes} from "@/interfaces/user";
import {CorporateUser, MasterCorporateUser, Type, User, userTypes} from "@/interfaces/user";
import Select from "react-select";
import {USER_TYPE_LABELS} from "@/resources/user";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import {getUserCorporate} from "@/utils/groups.be";
import {getUsers} from "@/utils/users.be";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const user = req.session.user;
@@ -62,7 +63,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
}, sessionOptions);
interface Props {
user: any;
user: User;
envVariables: {[key: string]: string};
linkedCorporate?: CorporateUser | MasterCorporateUser;
}

View File

@@ -1,85 +1,82 @@
import Layout from "@/components/High/Layout";
import useUser from "@/hooks/useUser";
import useUsers from "@/hooks/useUsers";
import { sessionOptions } from "@/lib/session";
import {sessionOptions} from "@/lib/session";
import useFilterStore from "@/stores/listFilterStore";
import { withIronSessionSsr } from "iron-session/next";
import {withIronSessionSsr} from "iron-session/next";
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { BsArrowLeft } from "react-icons/bs";
import { ToastContainer } from "react-toastify";
import {useRouter} from "next/router";
import {useEffect} from "react";
import {BsArrowLeft} from "react-icons/bs";
import {ToastContainer} from "react-toastify";
import UserList from "../(admin)/Lists/UserList";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
const user = req.session.user;
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
const envVariables: { [key: string]: string } = {};
Object.keys(process.env)
.filter((x) => x.startsWith("NEXT_PUBLIC"))
.forEach((x: string) => {
envVariables[x] = process.env[x]!;
});
const envVariables: {[key: string]: string} = {};
Object.keys(process.env)
.filter((x) => x.startsWith("NEXT_PUBLIC"))
.forEach((x: string) => {
envVariables[x] = process.env[x]!;
});
if (!user || !user.isVerified) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
if (!user || !user.isVerified) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
return {
props: { user: req.session.user, envVariables },
};
return {
props: {user: req.session.user, envVariables},
};
}, sessionOptions);
export default function UsersListPage() {
const { user } = useUser();
const { users } = useUsers();
const [filters, clearFilters] = useFilterStore((state) => [
state.userFilters,
state.clearUserFilters,
]);
const router = useRouter();
const {user} = useUser();
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
const [filters, clearFilters] = useFilterStore((state) => [state.userFilters, state.clearUserFilters]);
{user && (
<Layout user={user}>
<UserList
user={user}
filters={filters.map((f) => f.filter)}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => {
clearFilters();
router.back();
}}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Users ({total})</h2>
</div>
)}
/>
</Layout>
)}
</>
);
const router = useRouter();
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
{user && (
<Layout user={user}>
<UserList
user={user}
filters={filters.map((f) => f.filter)}
renderHeader={(total) => (
<div className="flex flex-col gap-4">
<div
onClick={() => {
clearFilters();
router.back();
}}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">Users ({total})</h2>
</div>
)}
/>
</Layout>
)}
</>
);
}

View File

@@ -97,9 +97,7 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
const [isLoading, setIsLoading] = useState(false);
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
const [desiredLevels, setDesiredLevels] = useState<{[key in Module]: number} | undefined>(
checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined,
);
const [desiredLevels, setDesiredLevels] = useState(checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined);
const [focus, setFocus] = useState<"academic" | "general">(user.focus);
const [country, setCountry] = useState<string>(user.demographicInformation?.country || "");
@@ -119,16 +117,17 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
user.type === "student" || user.type === "developer" ? user.preferredTopics : undefined,
);
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
const [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : undefined);
const [companyName, setCompanyName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyName : undefined);
const [commercialRegistration, setCommercialRegistration] = useState<string | undefined>(
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
const [position, setPosition] = useState<string | undefined>(
user.type === "corporate" || user.type === "mastercorporate" ? user.demographicInformation?.position : undefined,
);
const [corporateInformation, setCorporateInformation] = useState(
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation : undefined,
);
const [companyName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyName : undefined);
const [commercialRegistration] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined);
const [arabName, setArabName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyArabName : undefined);
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || moment.tz.guess());
const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false);
const profilePictureInput = useRef(null);
@@ -288,7 +287,7 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
}))
}
placeholder="Enter your company's name"
defaultValue={corporateInformation?.companyInformation.name}
defaultValue={corporateInformation?.companyInformation?.name}
required
/>
)}
@@ -481,7 +480,7 @@ function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props)
name="companyUsers"
onChange={() => null}
label="Number of users"
defaultValue={user.corporateInformation.companyInformation.userAmount}
defaultValue={user.corporateInformation?.companyInformation.userAmount}
disabled
required
/>

View File

@@ -24,6 +24,7 @@ import useTrainingContentStore from "@/stores/trainingContentStore";
import {Assignment} from "@/interfaces/results";
import {getUsers} from "@/utils/users.be";
import {getAssignments, getAssignmentsByAssigner} from "@/utils/assignments.be";
import useGradingSystem from "@/hooks/useGrading";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const user = req.session.user;
@@ -76,6 +77,7 @@ export default function History({user, users, assignments}: Props) {
const [filter, setFilter] = useState<Filter>();
const {data: stats, isLoading: isStatsLoading} = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
const {gradingSystem} = useGradingSystem();
const setExams = useExamStore((state) => state.setExams);
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
@@ -185,6 +187,7 @@ export default function History({user, users, assignments}: Props) {
setSelectedTrainingExams={setSelectedTrainingExams}
maxTrainingExams={MAX_TRAINING_EXAMS}
setExams={setExams}
gradingSystem={gradingSystem?.steps}
setShowSolutions={setShowSolutions}
setUserSolutions={setUserSolutions}
setSelectedModules={setSelectedModules}

View File

@@ -2,7 +2,6 @@
import Head from "next/head";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import useUser from "@/hooks/useUser";
import {ToastContainer} from "react-toastify";
import Layout from "@/components/High/Layout";
import CodeGenerator from "./(admin)/CodeGenerator";
@@ -28,6 +27,7 @@ import {User} from "@/interfaces/user";
import {getUserPermissions} from "@/utils/permissions.be";
import {Permission, PermissionType} from "@/interfaces/permissions";
import {getUsers} from "@/utils/users.be";
import useUsers from "@/hooks/useUsers";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const user = req.session.user;
@@ -50,21 +50,20 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
}
const permissions = await getUserPermissions(user.id);
const users = await getUsers();
return {
props: {user, permissions, users},
props: {user, permissions},
};
}, sessionOptions);
interface Props {
user: User;
users: User[];
permissions: PermissionType[];
}
export default function Admin({user, users, permissions}: Props) {
export default function Admin({user, permissions}: Props) {
const {gradingSystem, mutate} = useGradingSystem();
const {users} = useUsers();
const [modalOpen, setModalOpen] = useState<string>();