ENCOA-114: In Exam List and Group List provide Fuzzy Search Filter
This commit is contained in:
@@ -1,27 +1,25 @@
|
|||||||
import { useMemo } from "react";
|
import {useMemo} from "react";
|
||||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
import useExams from "@/hooks/useExams";
|
import useExams from "@/hooks/useExams";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { Module } from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import { Exam } from "@/interfaces/exam";
|
import {Exam} from "@/interfaces/exam";
|
||||||
import { Type, User } from "@/interfaces/user";
|
import {Type, User} from "@/interfaces/user";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import { getExamById } from "@/utils/exams";
|
import {getExamById} from "@/utils/exams";
|
||||||
import { countExercises } from "@/utils/moduleUtils";
|
import {countExercises} from "@/utils/moduleUtils";
|
||||||
import {
|
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||||
createColumnHelper,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { capitalize } from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import { useRouter } from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import { BsCheck, BsTrash, BsUpload } from "react-icons/bs";
|
import {BsCheck, BsTrash, BsUpload} from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
|
import {useListSearch} from "@/hooks/useListSearch";
|
||||||
|
|
||||||
const CLASSES: { [key in Module]: string } = {
|
const searchFields = [["module"], ["id"], ["createdBy"]];
|
||||||
|
|
||||||
|
const CLASSES: {[key in Module]: string} = {
|
||||||
reading: "text-ielts-reading",
|
reading: "text-ielts-reading",
|
||||||
listening: "text-ielts-listening",
|
listening: "text-ielts-listening",
|
||||||
speaking: "text-ielts-speaking",
|
speaking: "text-ielts-speaking",
|
||||||
@@ -31,9 +29,9 @@ const CLASSES: { [key in Module]: string } = {
|
|||||||
|
|
||||||
const columnHelper = createColumnHelper<Exam>();
|
const columnHelper = createColumnHelper<Exam>();
|
||||||
|
|
||||||
export default function ExamList({ user }: { user: User }) {
|
export default function ExamList({user}: {user: User}) {
|
||||||
const { exams, reload } = useExams();
|
const {exams, reload} = useExams();
|
||||||
const { users } = useUsers();
|
const {users} = useUsers();
|
||||||
|
|
||||||
const parsedExams = useMemo(() => {
|
const parsedExams = useMemo(() => {
|
||||||
return exams.map((exam) => {
|
return exams.map((exam) => {
|
||||||
@@ -51,6 +49,8 @@ export default function ExamList({ user }: { user: User }) {
|
|||||||
});
|
});
|
||||||
}, [exams, users]);
|
}, [exams, users]);
|
||||||
|
|
||||||
|
const {rows: filteredRows, renderSearch} = useListSearch<Exam>(searchFields, parsedExams);
|
||||||
|
|
||||||
const setExams = useExamStore((state) => state.setExams);
|
const setExams = useExamStore((state) => state.setExams);
|
||||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||||
|
|
||||||
@@ -59,12 +59,9 @@ export default function ExamList({ user }: { user: User }) {
|
|||||||
const loadExam = async (module: Module, examId: string) => {
|
const loadExam = async (module: Module, examId: string) => {
|
||||||
const exam = await getExamById(module, examId.trim());
|
const exam = await getExamById(module, examId.trim());
|
||||||
if (!exam) {
|
if (!exam) {
|
||||||
toast.error(
|
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", {
|
||||||
"Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID",
|
|
||||||
{
|
|
||||||
toastId: "invalid-exam-id",
|
toastId: "invalid-exam-id",
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -76,12 +73,7 @@ export default function ExamList({ user }: { user: User }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteExam = async (exam: Exam) => {
|
const deleteExam = async (exam: Exam) => {
|
||||||
if (
|
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
||||||
!confirm(
|
|
||||||
`Are you sure you want to delete this ${capitalize(exam.module)} exam?`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/exam/${exam.module}/${exam.id}`)
|
.delete(`/api/exam/${exam.module}/${exam.id}`)
|
||||||
@@ -103,11 +95,7 @@ export default function ExamList({ user }: { user: User }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTotalExercises = (exam: Exam) => {
|
const getTotalExercises = (exam: Exam) => {
|
||||||
if (
|
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") {
|
||||||
exam.module === "reading" ||
|
|
||||||
exam.module === "listening" ||
|
|
||||||
exam.module === "level"
|
|
||||||
) {
|
|
||||||
return countExercises(exam.parts.flatMap((x) => x.exercises));
|
return countExercises(exam.parts.flatMap((x) => x.exercises));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,11 +109,7 @@ export default function ExamList({ user }: { user: User }) {
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("module", {
|
columnHelper.accessor("module", {
|
||||||
header: "Module",
|
header: "Module",
|
||||||
cell: (info) => (
|
cell: (info) => <span className={CLASSES[info.getValue()]}>{capitalize(info.getValue())}</span>,
|
||||||
<span className={CLASSES[info.getValue()]}>
|
|
||||||
{capitalize(info.getValue())}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor((x) => getTotalExercises(x), {
|
columnHelper.accessor((x) => getTotalExercises(x), {
|
||||||
header: "Exercises",
|
header: "Exercises",
|
||||||
@@ -153,24 +137,17 @@ export default function ExamList({ user }: { user: User }) {
|
|||||||
{
|
{
|
||||||
header: "",
|
header: "",
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }: { row: { original: Exam } }) => {
|
cell: ({row}: {row: {original: Exam}}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div
|
<div
|
||||||
data-tip="Load exam"
|
data-tip="Load exam"
|
||||||
className="cursor-pointer tooltip"
|
className="cursor-pointer tooltip"
|
||||||
onClick={async () =>
|
onClick={async () => await loadExam(row.original.module, row.original.id)}>
|
||||||
await loadExam(row.original.module, row.original.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
||||||
<div
|
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteExam(row.original)}>
|
||||||
data-tip="Delete"
|
|
||||||
className="cursor-pointer tooltip"
|
|
||||||
onClick={() => deleteExam(row.original)}
|
|
||||||
>
|
|
||||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -181,24 +158,21 @@ export default function ExamList({ user }: { user: User }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: parsedExams,
|
data: filteredRows,
|
||||||
columns: defaultColumns,
|
columns: defaultColumns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full h-full">
|
||||||
|
{renderSearch()}
|
||||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th className="p-4 text-left" key={header.id}>
|
<th className="p-4 text-left" key={header.id}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -206,10 +180,7 @@ export default function ExamList({ user }: { user: User }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="px-2">
|
<tbody className="px-2">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<tr
|
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||||
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
|
||||||
key={row.id}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td className="px-4 py-2" key={cell.id}>
|
<td className="px-4 py-2" key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
@@ -219,5 +190,6 @@ export default function ExamList({ user }: { user: User }) {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {getUserCorporate} from "@/utils/groups";
|
|||||||
import {isAgentUser, isCorporateUser, USER_TYPE_LABELS} from "@/resources/user";
|
import {isAgentUser, isCorporateUser, USER_TYPE_LABELS} from "@/resources/user";
|
||||||
import {checkAccess} from "@/utils/permissions";
|
import {checkAccess} from "@/utils/permissions";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
import {useListSearch} from "@/hooks/useListSearch";
|
||||||
|
const searchFields = [["name"]];
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Group>();
|
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]+)*$/);
|
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,
|
adminAdmins: user?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {rows: filteredRows, renderSearch} = useListSearch<Group>(searchFields, groups);
|
||||||
|
|
||||||
const deleteGroup = (group: Group) => {
|
const deleteGroup = (group: Group) => {
|
||||||
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
|
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({
|
const table = useReactTable({
|
||||||
data: groups,
|
data: filteredRows,
|
||||||
columns: defaultColumns,
|
columns: defaultColumns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
@@ -295,7 +299,7 @@ export default function GroupList({user}: {user: User}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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"}>
|
<Modal isOpen={isCreating || !!editingGroup} onClose={closeModal} title={editingGroup ? `Editing ${editingGroup.name}` : "New Group"}>
|
||||||
<CreatePanel
|
<CreatePanel
|
||||||
group={editingGroup}
|
group={editingGroup}
|
||||||
@@ -315,6 +319,7 @@ export default function GroupList({user}: {user: User}) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
{renderSearch()}
|
||||||
<table className="bg-mti-purple-ultralight/40 w-full rounded-xl">
|
<table className="bg-mti-purple-ultralight/40 w-full rounded-xl">
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@@ -109,23 +109,6 @@ export default function UserList({
|
|||||||
.finally(reload);
|
.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) => {
|
const verifyAccount = (user: User) => {
|
||||||
axios
|
axios
|
||||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
||||||
|
|||||||
Reference in New Issue
Block a user