Merged develop into feature/training-content
This commit is contained in:
@@ -1,223 +1,195 @@
|
||||
import { useMemo } from "react";
|
||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
||||
import {useMemo} from "react";
|
||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||
import useExams from "@/hooks/useExams";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import { Module } from "@/interfaces";
|
||||
import { Exam } from "@/interfaces/exam";
|
||||
import { Type, User } from "@/interfaces/user";
|
||||
import {Module} from "@/interfaces";
|
||||
import {Exam} from "@/interfaces/exam";
|
||||
import {Type, User} from "@/interfaces/user";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import { getExamById } from "@/utils/exams";
|
||||
import { countExercises } from "@/utils/moduleUtils";
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
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 { useRouter } from "next/router";
|
||||
import { BsCheck, BsTrash, BsUpload } from "react-icons/bs";
|
||||
import { toast } from "react-toastify";
|
||||
import {capitalize} from "lodash";
|
||||
import {useRouter} from "next/router";
|
||||
import {BsCheck, BsTrash, BsUpload} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
import {useListSearch} from "@/hooks/useListSearch";
|
||||
|
||||
const CLASSES: { [key in Module]: string } = {
|
||||
reading: "text-ielts-reading",
|
||||
listening: "text-ielts-listening",
|
||||
speaking: "text-ielts-speaking",
|
||||
writing: "text-ielts-writing",
|
||||
level: "text-ielts-level",
|
||||
const searchFields = [["module"], ["id"], ["createdBy"]];
|
||||
|
||||
const CLASSES: {[key in Module]: string} = {
|
||||
reading: "text-ielts-reading",
|
||||
listening: "text-ielts-listening",
|
||||
speaking: "text-ielts-speaking",
|
||||
writing: "text-ielts-writing",
|
||||
level: "text-ielts-level",
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<Exam>();
|
||||
|
||||
export default function ExamList({ user }: { user: User }) {
|
||||
const { exams, reload } = useExams();
|
||||
const { users } = useUsers();
|
||||
export default function ExamList({user}: {user: User}) {
|
||||
const {exams, reload} = useExams();
|
||||
const {users} = useUsers();
|
||||
|
||||
const parsedExams = useMemo(() => {
|
||||
return exams.map((exam) => {
|
||||
if (exam.createdBy) {
|
||||
const user = users.find((u) => u.id === exam.createdBy);
|
||||
if (!user) return exam;
|
||||
const parsedExams = useMemo(() => {
|
||||
return exams.map((exam) => {
|
||||
if (exam.createdBy) {
|
||||
const user = users.find((u) => u.id === exam.createdBy);
|
||||
if (!user) return exam;
|
||||
|
||||
return {
|
||||
...exam,
|
||||
createdBy: user.type === "developer" ? "system" : user.name,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...exam,
|
||||
createdBy: user.type === "developer" ? "system" : user.name,
|
||||
};
|
||||
}
|
||||
|
||||
return exam;
|
||||
});
|
||||
}, [exams, users]);
|
||||
return exam;
|
||||
});
|
||||
}, [exams, users]);
|
||||
|
||||
const setExams = useExamStore((state) => state.setExams);
|
||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||
const {rows: filteredRows, renderSearch} = useListSearch<Exam>(searchFields, parsedExams);
|
||||
|
||||
const router = useRouter();
|
||||
const setExams = useExamStore((state) => state.setExams);
|
||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||
|
||||
const loadExam = async (module: Module, examId: string) => {
|
||||
const exam = await getExamById(module, examId.trim());
|
||||
if (!exam) {
|
||||
toast.error(
|
||||
"Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID",
|
||||
{
|
||||
toastId: "invalid-exam-id",
|
||||
}
|
||||
);
|
||||
const router = useRouter();
|
||||
|
||||
return;
|
||||
}
|
||||
const loadExam = async (module: Module, examId: string) => {
|
||||
const exam = await getExamById(module, examId.trim());
|
||||
if (!exam) {
|
||||
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", {
|
||||
toastId: "invalid-exam-id",
|
||||
});
|
||||
|
||||
setExams([exam]);
|
||||
setSelectedModules([module]);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push("/exercises");
|
||||
};
|
||||
setExams([exam]);
|
||||
setSelectedModules([module]);
|
||||
|
||||
const deleteExam = async (exam: Exam) => {
|
||||
if (
|
||||
!confirm(
|
||||
`Are you sure you want to delete this ${capitalize(exam.module)} exam?`
|
||||
)
|
||||
)
|
||||
return;
|
||||
router.push("/exercises");
|
||||
};
|
||||
|
||||
axios
|
||||
.delete(`/api/exam/${exam.module}/${exam.id}`)
|
||||
.then(() => toast.success(`Deleted the "${exam.id}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Exam not found!");
|
||||
return;
|
||||
}
|
||||
const deleteExam = async (exam: Exam) => {
|
||||
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
||||
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this exam!");
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.delete(`/api/exam/${exam.module}/${exam.id}`)
|
||||
.then(() => toast.success(`Deleted the "${exam.id}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Exam not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this exam!");
|
||||
return;
|
||||
}
|
||||
|
||||
const getTotalExercises = (exam: Exam) => {
|
||||
if (
|
||||
exam.module === "reading" ||
|
||||
exam.module === "listening" ||
|
||||
exam.module === "level"
|
||||
) {
|
||||
return countExercises(exam.parts.flatMap((x) => x.exercises));
|
||||
}
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
return countExercises(exam.exercises);
|
||||
};
|
||||
const getTotalExercises = (exam: Exam) => {
|
||||
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") {
|
||||
return countExercises(exam.parts.flatMap((x) => x.exercises));
|
||||
}
|
||||
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("id", {
|
||||
header: "ID",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("module", {
|
||||
header: "Module",
|
||||
cell: (info) => (
|
||||
<span className={CLASSES[info.getValue()]}>
|
||||
{capitalize(info.getValue())}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor((x) => getTotalExercises(x), {
|
||||
header: "Exercises",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("minTimer", {
|
||||
header: "Timer",
|
||||
cell: (info) => <>{info.getValue()} minute(s)</>,
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: "Created At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
if (value) {
|
||||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
return countExercises(exam.exercises);
|
||||
};
|
||||
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("createdBy", {
|
||||
header: "Created By",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({ row }: { row: { original: Exam } }) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div
|
||||
data-tip="Load exam"
|
||||
className="cursor-pointer tooltip"
|
||||
onClick={async () =>
|
||||
await loadExam(row.original.module, row.original.id)
|
||||
}
|
||||
>
|
||||
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
||||
<div
|
||||
data-tip="Delete"
|
||||
className="cursor-pointer tooltip"
|
||||
onClick={() => deleteExam(row.original)}
|
||||
>
|
||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("id", {
|
||||
header: "ID",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("module", {
|
||||
header: "Module",
|
||||
cell: (info) => <span className={CLASSES[info.getValue()]}>{capitalize(info.getValue())}</span>,
|
||||
}),
|
||||
columnHelper.accessor((x) => getTotalExercises(x), {
|
||||
header: "Exercises",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("minTimer", {
|
||||
header: "Timer",
|
||||
cell: (info) => <>{info.getValue()} minute(s)</>,
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: "Created At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
if (value) {
|
||||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
|
||||
const table = useReactTable({
|
||||
data: parsedExams,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("createdBy", {
|
||||
header: "Created By",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({row}: {row: {original: Exam}}) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div
|
||||
data-tip="Load exam"
|
||||
className="cursor-pointer tooltip"
|
||||
onClick={async () => await loadExam(row.original.module, row.original.id)}>
|
||||
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteExam(row.original)}>
|
||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th className="p-4 text-left" key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="px-2">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr
|
||||
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
||||
key={row.id}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td className="px-4 py-2" key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
const table = useReactTable({
|
||||
data: filteredRows,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full h-full">
|
||||
{renderSearch()}
|
||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th className="p-4 text-left" key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="px-2">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td className="px-4 py-2" key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import {getUserCorporate} from "@/utils/groups";
|
||||
import {isAgentUser, isCorporateUser, USER_TYPE_LABELS} from "@/resources/user";
|
||||
import {checkAccess} from "@/utils/permissions";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import {useListSearch} from "@/hooks/useListSearch";
|
||||
const searchFields = [["name"]];
|
||||
|
||||
const columnHelper = createColumnHelper<Group>();
|
||||
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
||||
@@ -217,6 +219,8 @@ export default function GroupList({user}: {user: User}) {
|
||||
adminAdmins: user?.id,
|
||||
});
|
||||
|
||||
const {rows: filteredRows, renderSearch} = useListSearch<Group>(searchFields, groups);
|
||||
|
||||
const deleteGroup = (group: Group) => {
|
||||
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
|
||||
|
||||
@@ -283,7 +287,7 @@ export default function GroupList({user}: {user: User}) {
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: groups,
|
||||
data: filteredRows,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
@@ -295,7 +299,7 @@ export default function GroupList({user}: {user: User}) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full rounded-xl">
|
||||
<div className="h-full w-full rounded-xl flex flex-col gap-4">
|
||||
<Modal isOpen={isCreating || !!editingGroup} onClose={closeModal} title={editingGroup ? `Editing ${editingGroup.name}` : "New Group"}>
|
||||
<CreatePanel
|
||||
group={editingGroup}
|
||||
@@ -315,6 +319,7 @@ export default function GroupList({user}: {user: User}) {
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
{renderSearch()}
|
||||
<table className="bg-mti-purple-ultralight/40 w-full rounded-xl">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
|
||||
@@ -109,23 +109,6 @@ export default function UserList({
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
const updateAccountType = (user: User, type: Type) => {
|
||||
if (!confirm(`Are you sure you want to update ${user.name}'s account from ${capitalize(user.type)} to ${capitalize(type)}?`)) return;
|
||||
|
||||
axios
|
||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
||||
...user,
|
||||
type,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("User type updated successfully!");
|
||||
reload();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Something went wrong!", {toastId: "update-error"});
|
||||
});
|
||||
};
|
||||
|
||||
const verifyAccount = (user: User) => {
|
||||
axios
|
||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
||||
|
||||
Reference in New Issue
Block a user