Merge remote-tracking branch 'origin/develop' into feature/training-content
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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[]) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
243
src/pages/api/assignments/statistical/excel.ts
Normal file
243
src/pages/api/assignments/statistical/excel.ts
Normal 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" });
|
||||
}
|
||||
61
src/pages/api/batch_users.ts
Normal file
61
src/pages/api/batch_users.ts
Normal 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)
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user