added access variable to exams soo we can distinguish private, public and confidential exams and also bugfixes and improvements
This commit is contained in:
@@ -265,7 +265,7 @@ export default function CorporateGradingSystem({
|
||||
<>
|
||||
<Separator />
|
||||
<label className="font-normal text-base text-mti-gray-dim">
|
||||
Apply this grading system to other entities
|
||||
Copy this grading system to other entities
|
||||
</label>
|
||||
<Select
|
||||
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
||||
@@ -282,7 +282,7 @@ export default function CorporateGradingSystem({
|
||||
disabled={isLoading || otherEntities.length === 0}
|
||||
variant="outline"
|
||||
>
|
||||
Apply to {otherEntities.length} other entities
|
||||
Copy to {otherEntities.length} other entities
|
||||
</Button>
|
||||
<Separator />
|
||||
</>
|
||||
@@ -326,7 +326,7 @@ export default function CorporateGradingSystem({
|
||||
disabled={isLoading}
|
||||
className="mt-8"
|
||||
>
|
||||
Save Grading System
|
||||
Save Changes to entities
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,170 +15,210 @@ import { findBy, mapBy } from "@/utils";
|
||||
import useEntitiesCodes from "@/hooks/useEntitiesCodes";
|
||||
import Table from "@/components/High/Table";
|
||||
|
||||
type TableData = Code & { entity?: EntityWithRoles, creator?: User }
|
||||
type TableData = Code & { entity?: EntityWithRoles; creator?: User };
|
||||
const columnHelper = createColumnHelper<TableData>();
|
||||
|
||||
export default function CodeList({ user, entities, canDeleteCodes }
|
||||
: { user: User, entities: EntityWithRoles[], canDeleteCodes?: boolean }) {
|
||||
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
||||
export default function CodeList({
|
||||
user,
|
||||
entities,
|
||||
canDeleteCodes,
|
||||
}: {
|
||||
user: User;
|
||||
entities: EntityWithRoles[];
|
||||
canDeleteCodes?: boolean;
|
||||
}) {
|
||||
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
||||
|
||||
const entityIDs = useMemo(() => mapBy(entities, 'id'), [entities])
|
||||
const entityIDs = useMemo(() => mapBy(entities, "id"), [entities]);
|
||||
|
||||
const { users } = useUsers();
|
||||
const { codes, reload } = useEntitiesCodes(isAdmin(user) ? undefined : entityIDs)
|
||||
const { users } = useUsers();
|
||||
const { codes, reload, isLoading } = useEntitiesCodes(
|
||||
isAdmin(user) ? undefined : entityIDs
|
||||
);
|
||||
|
||||
const data: TableData[] = useMemo(() => codes.map((code) => ({
|
||||
...code,
|
||||
entity: findBy(entities, 'id', code.entity),
|
||||
creator: findBy(users, 'id', code.creator)
|
||||
})) as TableData[], [codes, entities, users])
|
||||
const data: TableData[] = useMemo(
|
||||
() =>
|
||||
codes.map((code) => ({
|
||||
...code,
|
||||
entity: findBy(entities, "id", code.entity),
|
||||
creator: findBy(users, "id", code.creator),
|
||||
})) as TableData[],
|
||||
[codes, entities, users]
|
||||
);
|
||||
|
||||
const toggleCode = (id: string) => {
|
||||
setSelectedCodes((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
||||
};
|
||||
const toggleCode = (id: string) => {
|
||||
setSelectedCodes((prev) =>
|
||||
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
// const toggleAllCodes = (checked: boolean) => {
|
||||
// if (checked) return setSelectedCodes(visibleRows.filter((x) => !x.userId).map((x) => x.code));
|
||||
// const toggleAllCodes = (checked: boolean) => {
|
||||
// if (checked) return setSelectedCodes(visibleRows.filter((x) => !x.userId).map((x) => x.code));
|
||||
|
||||
// return setSelectedCodes([]);
|
||||
// };
|
||||
// return setSelectedCodes([]);
|
||||
// };
|
||||
|
||||
const deleteCodes = async (codes: string[]) => {
|
||||
if (!canDeleteCodes) return
|
||||
if (!confirm(`Are you sure you want to delete these ${codes.length} code(s)?`)) return;
|
||||
const deleteCodes = async (codes: string[]) => {
|
||||
if (!canDeleteCodes) return;
|
||||
if (
|
||||
!confirm(`Are you sure you want to delete these ${codes.length} code(s)?`)
|
||||
)
|
||||
return;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
codes.forEach((code) => params.append("code", code));
|
||||
const params = new URLSearchParams();
|
||||
codes.forEach((code) => params.append("code", code));
|
||||
|
||||
axios
|
||||
.delete(`/api/code?${params.toString()}`)
|
||||
.then(() => {
|
||||
toast.success(`Deleted the codes!`);
|
||||
setSelectedCodes([]);
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Code not found!");
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.delete(`/api/code?${params.toString()}`)
|
||||
.then(() => {
|
||||
toast.success(`Deleted the codes!`);
|
||||
setSelectedCodes([]);
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Code not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this code!");
|
||||
return;
|
||||
}
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this code!");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
const deleteCode = async (code: Code) => {
|
||||
if (!canDeleteCodes) return
|
||||
if (!confirm(`Are you sure you want to delete this "${code.code}" code?`)) return;
|
||||
const deleteCode = async (code: Code) => {
|
||||
if (!canDeleteCodes) return;
|
||||
if (!confirm(`Are you sure you want to delete this "${code.code}" code?`))
|
||||
return;
|
||||
|
||||
axios
|
||||
.delete(`/api/code/${code.code}`)
|
||||
.then(() => toast.success(`Deleted the "${code.code}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Code not found!");
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.delete(`/api/code/${code.code}`)
|
||||
.then(() => toast.success(`Deleted the "${code.code}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Code not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this code!");
|
||||
return;
|
||||
}
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this code!");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("code", {
|
||||
id: "codeCheckbox",
|
||||
enableSorting: false,
|
||||
header: () => (""),
|
||||
cell: (info) =>
|
||||
!info.row.original.userId ? (
|
||||
<Checkbox isChecked={selectedCodes.includes(info.getValue())} onChange={() => toggleCode(info.getValue())}>
|
||||
{""}
|
||||
</Checkbox>
|
||||
) : null,
|
||||
}),
|
||||
columnHelper.accessor("code", {
|
||||
header: "Code",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("creationDate", {
|
||||
header: "Creation Date",
|
||||
cell: (info) => (info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A"),
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: "E-mail",
|
||||
cell: (info) => info.getValue() || "N/A",
|
||||
}),
|
||||
columnHelper.accessor("creator", {
|
||||
header: "Creator",
|
||||
cell: (info) => info.getValue() ? `${info.getValue().name} (${USER_TYPE_LABELS[info.getValue().type]})` : "N/A",
|
||||
}),
|
||||
columnHelper.accessor("entity", {
|
||||
header: "Entity",
|
||||
cell: (info) => info.getValue()?.label || "N/A",
|
||||
}),
|
||||
columnHelper.accessor("userId", {
|
||||
header: "Availability",
|
||||
cell: (info) =>
|
||||
info.getValue() ? (
|
||||
<span className="flex gap-1 items-center text-mti-green">
|
||||
<div className="w-2 h-2 rounded-full bg-mti-green" /> In Use
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex gap-1 items-center text-mti-red">
|
||||
<div className="w-2 h-2 rounded-full bg-mti-red" /> Unused
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({ row }: { row: { original: Code } }) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{canDeleteCodes && !row.original.userId && (
|
||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteCode(row.original)}>
|
||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("code", {
|
||||
id: "codeCheckbox",
|
||||
enableSorting: false,
|
||||
header: () => "",
|
||||
cell: (info) =>
|
||||
!info.row.original.userId ? (
|
||||
<Checkbox
|
||||
isChecked={selectedCodes.includes(info.getValue())}
|
||||
onChange={() => toggleCode(info.getValue())}
|
||||
>
|
||||
{""}
|
||||
</Checkbox>
|
||||
) : null,
|
||||
}),
|
||||
columnHelper.accessor("code", {
|
||||
header: "Code",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("creationDate", {
|
||||
header: "Creation Date",
|
||||
cell: (info) =>
|
||||
info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A",
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: "E-mail",
|
||||
cell: (info) => info.getValue() || "N/A",
|
||||
}),
|
||||
columnHelper.accessor("creator", {
|
||||
header: "Creator",
|
||||
cell: (info) =>
|
||||
info.getValue()
|
||||
? `${info.getValue().name} (${
|
||||
USER_TYPE_LABELS[info.getValue().type]
|
||||
})`
|
||||
: "N/A",
|
||||
}),
|
||||
columnHelper.accessor("entity", {
|
||||
header: "Entity",
|
||||
cell: (info) => info.getValue()?.label || "N/A",
|
||||
}),
|
||||
columnHelper.accessor("userId", {
|
||||
header: "Availability",
|
||||
cell: (info) =>
|
||||
info.getValue() ? (
|
||||
<span className="flex gap-1 items-center text-mti-green">
|
||||
<div className="w-2 h-2 rounded-full bg-mti-green" /> In Use
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex gap-1 items-center text-mti-red">
|
||||
<div className="w-2 h-2 rounded-full bg-mti-red" /> Unused
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({ row }: { row: { original: Code } }) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{canDeleteCodes && !row.original.userId && (
|
||||
<div
|
||||
data-tip="Delete"
|
||||
className="cursor-pointer tooltip"
|
||||
onClick={() => deleteCode(row.original)}
|
||||
>
|
||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between pb-4 pt-1">
|
||||
{canDeleteCodes && (
|
||||
<div className="flex gap-4 items-center w-full justify-end">
|
||||
<span>{selectedCodes.length} code(s) selected</span>
|
||||
<Button
|
||||
disabled={selectedCodes.length === 0}
|
||||
variant="outline"
|
||||
color="red"
|
||||
className="!py-1 px-10"
|
||||
onClick={() => deleteCodes(selectedCodes)}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Table<TableData>
|
||||
data={data}
|
||||
columns={defaultColumns}
|
||||
searchFields={[["code"], ["email"], ["entity", "label"], ["creator", "name"], ['creator', 'type']]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between pb-4 pt-1">
|
||||
{canDeleteCodes && (
|
||||
<div className="flex gap-4 items-center w-full justify-end">
|
||||
<span>{selectedCodes.length} code(s) selected</span>
|
||||
<Button
|
||||
disabled={selectedCodes.length === 0}
|
||||
variant="outline"
|
||||
color="red"
|
||||
className="!py-1 px-10"
|
||||
onClick={() => deleteCodes(selectedCodes)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Table<TableData>
|
||||
data={data}
|
||||
columns={defaultColumns}
|
||||
isLoading={isLoading}
|
||||
searchFields={[
|
||||
["code"],
|
||||
["email"],
|
||||
["entity", "label"],
|
||||
["creator", "name"],
|
||||
["creator", "type"],
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,287 +1,394 @@
|
||||
import {useMemo, useState} from "react";
|
||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||
import { useMemo, useState } 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 {User} from "@/interfaces/user";
|
||||
import { Module } from "@/interfaces";
|
||||
import { Exam } from "@/interfaces/exam";
|
||||
import { User } from "@/interfaces/user";
|
||||
import useExamStore from "@/stores/exam";
|
||||
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 {capitalize, uniq} from "lodash";
|
||||
import {useRouter} from "next/router";
|
||||
import {BsBan, BsCheck, BsCircle, BsPencil, BsTrash, BsUpload, BsX} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
import {useListSearch} from "@/hooks/useListSearch";
|
||||
import { capitalize } from "lodash";
|
||||
import { useRouter } from "next/router";
|
||||
import { BsCheck, BsPencil, 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 { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
||||
import Button from "@/components/Low/Button";
|
||||
import {EntityWithRoles} from "@/interfaces/entity";
|
||||
import {BiEdit} from "react-icons/bi";
|
||||
import {findBy, mapBy} from "@/utils";
|
||||
import {getUserName} from "@/utils/users";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { BiEdit } from "react-icons/bi";
|
||||
import { findBy, mapBy } from "@/utils";
|
||||
|
||||
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 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, entities}: {user: User; entities: EntityWithRoles[]}) {
|
||||
const [selectedExam, setSelectedExam] = useState<Exam>();
|
||||
export default function ExamList({
|
||||
user,
|
||||
entities,
|
||||
}: {
|
||||
user: User;
|
||||
entities: EntityWithRoles[];
|
||||
}) {
|
||||
const [selectedExam, setSelectedExam] = useState<Exam>();
|
||||
|
||||
const {exams, reload} = useExams();
|
||||
const {users} = useUsers();
|
||||
const canViewConfidentialEntities = useMemo(
|
||||
() =>
|
||||
mapBy(
|
||||
findAllowedEntities(user, entities, "view_confidential_exams"),
|
||||
"id"
|
||||
),
|
||||
[user, entities]
|
||||
);
|
||||
|
||||
const filteredExams = useMemo(
|
||||
() =>
|
||||
exams.filter((e) => {
|
||||
if (!e.private) return true;
|
||||
return (e.entities || []).some((ent) => mapBy(user.entities, "id").includes(ent));
|
||||
}),
|
||||
[exams, user?.entities],
|
||||
);
|
||||
const { exams, reload, isLoading } = useExams();
|
||||
const { users } = useUsers();
|
||||
// Pass this permission filter to the backend later
|
||||
const filteredExams = useMemo(
|
||||
() =>
|
||||
["admin", "developer"].includes(user.type)
|
||||
? exams
|
||||
: exams.filter((item) => {
|
||||
if (
|
||||
item.access === "confidential" &&
|
||||
!canViewConfidentialEntities.find((x) =>
|
||||
(item.entities ?? []).includes(x)
|
||||
)
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
}),
|
||||
[canViewConfidentialEntities, exams, user.type]
|
||||
);
|
||||
|
||||
const parsedExams = useMemo(() => {
|
||||
return filteredExams.map((exam) => {
|
||||
if (exam.createdBy) {
|
||||
const user = users.find((u) => u.id === exam.createdBy);
|
||||
if (!user) return exam;
|
||||
const parsedExams = useMemo(() => {
|
||||
return filteredExams.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;
|
||||
});
|
||||
}, [filteredExams, users]);
|
||||
return exam;
|
||||
});
|
||||
}, [filteredExams, users]);
|
||||
|
||||
const {rows: filteredRows, renderSearch} = useListSearch<Exam>(searchFields, parsedExams);
|
||||
const { rows: filteredRows, renderSearch } = useListSearch<Exam>(
|
||||
searchFields,
|
||||
parsedExams
|
||||
);
|
||||
|
||||
const dispatch = useExamStore((state) => state.dispatch);
|
||||
const dispatch = useExamStore((state) => state.dispatch);
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
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 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",
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
dispatch({type: "INIT_EXAM", payload: {exams: [exam], modules: [module]}});
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
type: "INIT_EXAM",
|
||||
payload: { exams: [exam], modules: [module] },
|
||||
});
|
||||
|
||||
router.push("/exam");
|
||||
};
|
||||
router.push("/exam");
|
||||
};
|
||||
|
||||
const privatizeExam = async (exam: Exam) => {
|
||||
if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return;
|
||||
/*
|
||||
const privatizeExam = async (exam: Exam) => {
|
||||
if (
|
||||
!confirm(
|
||||
`Are you sure you want to make this ${capitalize(exam.module)} exam ${
|
||||
exam.access
|
||||
}?`
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
axios
|
||||
.patch(`/api/exam/${exam.module}/${exam.id}`, {private: !exam.private})
|
||||
.then(() => toast.success(`Updated the "${exam.id}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Exam not found!");
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.patch(`/api/exam/${exam.module}/${exam.id}`, { private: !exam.private })
|
||||
.then(() => toast.success(`Updated the "${exam.id}" exam`))
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
toast.error("Exam not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to update this exam!");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
};
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
*/
|
||||
|
||||
const deleteExam = async (exam: Exam) => {
|
||||
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
||||
const deleteExam = async (exam: Exam) => {
|
||||
if (
|
||||
!confirm(
|
||||
`Are you sure you want to delete this ${capitalize(exam.module)} 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this exam!");
|
||||
return;
|
||||
}
|
||||
if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to delete this exam!");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
})
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
const getTotalExercises = (exam: Exam) => {
|
||||
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") {
|
||||
return countExercises(exam.parts.flatMap((x) => x.exercises));
|
||||
}
|
||||
const getTotalExercises = (exam: Exam) => {
|
||||
if (
|
||||
exam.module === "reading" ||
|
||||
exam.module === "listening" ||
|
||||
exam.module === "level"
|
||||
) {
|
||||
return countExercises((exam.parts ?? []).flatMap((x) => x.exercises));
|
||||
}
|
||||
|
||||
return countExercises(exam.exercises);
|
||||
};
|
||||
return countExercises(exam.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("private", {
|
||||
header: "Private",
|
||||
cell: (info) => <span className="w-full flex items-center justify-center">{!info.getValue() ? <BsX /> : <BsCheck />}</span>,
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: "Created At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
if (value) {
|
||||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
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("access", {
|
||||
header: "Access",
|
||||
cell: (info) => <span>{capitalize(info.getValue())}</span>,
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
header: "Created At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
if (value) {
|
||||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("createdBy", {
|
||||
header: "Created By",
|
||||
cell: (info) => (!info.getValue() ? "System" : findBy(users, "id", info.getValue())?.name || "N/A"),
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({row}: {row: {original: Exam}}) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{(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 exam" onClick={() => setSelectedExam(row.original)} className="cursor-pointer tooltip">
|
||||
<BsPencil />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
{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 null;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("createdBy", {
|
||||
header: "Created By",
|
||||
cell: (info) =>
|
||||
!info.getValue()
|
||||
? "System"
|
||||
: findBy(users, "id", info.getValue())?.name || "N/A",
|
||||
}),
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({ row }: { row: { original: Exam } }) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{(row.original.owners?.includes(user.id) ||
|
||||
checkAccess(user, ["admin", "developer"])) && (
|
||||
<>
|
||||
{checkAccess(user, [
|
||||
"admin",
|
||||
"developer",
|
||||
"mastercorporate",
|
||||
]) && (
|
||||
<button
|
||||
data-tip="Edit exam"
|
||||
onClick={() => setSelectedExam(row.original)}
|
||||
className="cursor-pointer tooltip"
|
||||
>
|
||||
<BsPencil />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
{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 table = useReactTable({
|
||||
data: filteredRows,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
const table = useReactTable({
|
||||
data: filteredRows,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleExamEdit = () => {
|
||||
router.push(`/generation?id=${selectedExam!.id}&module=${selectedExam!.module}`);
|
||||
};
|
||||
const handleExamEdit = () => {
|
||||
router.push(
|
||||
`/generation?id=${selectedExam!.id}&module=${selectedExam!.module}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full h-full">
|
||||
{renderSearch()}
|
||||
<Modal isOpen={!!selectedExam} onClose={() => setSelectedExam(undefined)} maxWidth="max-w-xl">
|
||||
{!!selectedExam ? (
|
||||
<>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<BiEdit className="w-5 h-5 text-gray-600" />
|
||||
<span className="text-gray-600 font-medium">Ready to Edit</span>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full h-full">
|
||||
{renderSearch()}
|
||||
<Modal
|
||||
isOpen={!!selectedExam}
|
||||
onClose={() => setSelectedExam(undefined)}
|
||||
maxWidth="max-w-xl"
|
||||
>
|
||||
{!!selectedExam ? (
|
||||
<>
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<BiEdit className="w-5 h-5 text-gray-600" />
|
||||
<span className="text-gray-600 font-medium">
|
||||
Ready to Edit
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-3">
|
||||
<p className="font-medium mb-1">Exam ID: {selectedExam.id}</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-3">
|
||||
<p className="font-medium mb-1">Exam ID: {selectedExam.id}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-500 text-sm">Click 'Next' to proceed to the exam editor.</p>
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Click 'Next' to proceed to the exam editor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-4 mt-8">
|
||||
<Button color="purple" variant="outline" onClick={() => setSelectedExam(undefined)} className="w-32">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="purple" onClick={handleExamEdit} className="w-32 text-white flex items-center justify-center gap-2">
|
||||
Proceed
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/*<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) => (
|
||||
<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>
|
||||
);
|
||||
<div className="flex justify-between gap-4 mt-8">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={() => setSelectedExam(undefined)}
|
||||
className="w-32"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="purple"
|
||||
onClick={handleExamEdit}
|
||||
className="w-32 text-white flex items-center justify-center gap-2"
|
||||
>
|
||||
Proceed
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/*<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) => (
|
||||
<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>
|
||||
{isLoading ? (
|
||||
<div className="min-h-screen flex justify-center items-start">
|
||||
<span className="loading loading-infinity w-32" />
|
||||
</div>
|
||||
) : (
|
||||
filteredRows.length === 0 && (
|
||||
<div className="w-full flex justify-center items-start">
|
||||
<span className="text-xl text-gray-500">No data found...</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ import useFilterStore from "@/stores/listFilterStore";
|
||||
import { useRouter } from "next/router";
|
||||
import { mapBy } from "@/utils";
|
||||
import { exportListToExcel } from "@/utils/users";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import useUserBalance from "@/hooks/useUserBalance";
|
||||
import useEntitiesUsers from "@/hooks/useEntitiesUsers";
|
||||
import { WithLabeledEntities } from "@/interfaces/entity";
|
||||
import Table from "@/components/High/Table";
|
||||
@@ -494,21 +492,19 @@ export default function UserList({
|
||||
},
|
||||
];
|
||||
|
||||
const downloadExcel = (rows: WithLabeledEntities<User>[]) => {
|
||||
const downloadExcel = async (rows: WithLabeledEntities<User>[]) => {
|
||||
if (entitiesDownloadUsers.length === 0)
|
||||
return toast.error("You are not allowed to download the user list.");
|
||||
|
||||
const allowedRows = rows.filter((r) =>
|
||||
mapBy(r.entities, "id").some((e) =>
|
||||
mapBy(entitiesDownloadUsers, "id").includes(e)
|
||||
)
|
||||
);
|
||||
const csv = exportListToExcel(allowedRows);
|
||||
const allowedRows = rows;
|
||||
const csv = await exportListToExcel(allowedRows);
|
||||
|
||||
const element = document.createElement("a");
|
||||
const file = new Blob([csv], { type: "text/csv" });
|
||||
const file = new Blob([csv], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = "users.csv";
|
||||
element.download = "users.xlsx";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
|
||||
@@ -4,7 +4,6 @@ import clsx from "clsx";
|
||||
import CodeList from "./CodeList";
|
||||
import DiscountList from "./DiscountList";
|
||||
import ExamList from "./ExamList";
|
||||
import GroupList from "./GroupList";
|
||||
import PackageList from "./PackageList";
|
||||
import UserList from "./UserList";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
|
||||
@@ -3,16 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import client from "@/lib/mongodb";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { Code, Group, Type } from "@/interfaces/user";
|
||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
||||
import { prepareMailer, prepareMailOptions } from "@/email";
|
||||
import { isAdmin } from "@/utils/users";
|
||||
import { Code, } from "@/interfaces/user";
|
||||
import { requestUser } from "@/utils/api";
|
||||
import { doesEntityAllow } from "@/utils/permissions";
|
||||
import { getEntity, getEntityWithRoles } from "@/utils/entities.be";
|
||||
import { findBy } from "@/utils";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
@@ -30,7 +22,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
const { entities } = req.query as { entities?: string[] };
|
||||
if (entities)
|
||||
return res.status(200).json(await db.collection("codes").find<Code>({ entity: { $in: entities } }).toArray());
|
||||
return res.status(200).json(await db.collection("codes").find<Code>({ entity: { $in: Array.isArray(entities) ? entities : [entities] } }).toArray());
|
||||
|
||||
return res.status(200).json(await db.collection("codes").find<Code>({}).toArray());
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
const { entity } = req.query as { entity?: string };
|
||||
|
||||
const snapshot = await db.collection("codes").find(entity ? { entity } : {}).toArray();
|
||||
|
||||
res.status(200).json(snapshot);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getApprovalWorkflowsByExamId, updateApprovalWorkflows } from "@/utils/a
|
||||
import { generateExamDifferences } from "@/utils/exam.differences";
|
||||
import { getExams } from "@/utils/exams.be";
|
||||
import { isAdmin } from "@/utils/users";
|
||||
import { access } from "fs";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
@@ -52,6 +53,7 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
try {
|
||||
const exam = {
|
||||
access: "public", // default access is public
|
||||
...req.body,
|
||||
module: module,
|
||||
entities,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import client from "@/lib/mongodb";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {flatten} from "lodash";
|
||||
import {Exam} from "@/interfaces/exam";
|
||||
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { flatten, map } from "lodash";
|
||||
import { AccessType, Exam } from "@/interfaces/exam";
|
||||
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
||||
import { requestUser } from "../../../utils/api";
|
||||
import { mapBy } from "../../../utils";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
@@ -14,17 +16,37 @@ export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
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) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
const user = await requestUser(req, res)
|
||||
if (!user)
|
||||
return res.status(401).json({ ok: false, reason: "You must be logged in!" })
|
||||
const isAdmin = ["admin", "developer"].includes(user.type)
|
||||
const { entities = [] } = req.query as { access?: AccessType, entities?: string[] | string };
|
||||
let entitiesToFetch = Array.isArray(entities) ? entities : entities ? [entities] : []
|
||||
|
||||
if (!isAdmin) {
|
||||
const userEntitiesIDs = mapBy(user.entities || [], 'id')
|
||||
entitiesToFetch = entities ? entitiesToFetch.filter((entity): entity is string => entity ? userEntitiesIDs.includes(entity) : false) : userEntitiesIDs
|
||||
if ((entitiesToFetch.length ?? 0) === 0) {
|
||||
res.status(200).json([])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const moduleExamsPromises = MODULE_ARRAY.map(async (module) => {
|
||||
const snapshot = await db.collection(module).find<Exam>({ isDiagnostic: false }).toArray();
|
||||
const snapshot = await db.collection(module).find<Exam>({
|
||||
isDiagnostic: false, ...(isAdmin && (entitiesToFetch.length ?? 0) === 0 ? {
|
||||
} : {
|
||||
entity: { $in: entitiesToFetch }
|
||||
})
|
||||
}).toArray();
|
||||
|
||||
return snapshot.map((doc) => ({
|
||||
...doc,
|
||||
|
||||
@@ -33,7 +33,6 @@ import moment from "moment";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { generate } from "random-words";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import {
|
||||
|
||||
@@ -159,6 +159,21 @@ export default function Home({ user, group, users, entity }: Props) {
|
||||
prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id]
|
||||
);
|
||||
|
||||
const toggleAllUsersInList = () =>
|
||||
setSelectedUsers((prev) =>
|
||||
prev.length === rows.length
|
||||
? []
|
||||
: [
|
||||
...prev,
|
||||
...items.reduce((acc, i) => {
|
||||
if (!prev.find((item) => item === i.id)) {
|
||||
(acc as string[]).push(i.id);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]),
|
||||
]
|
||||
);
|
||||
|
||||
const removeParticipants = () => {
|
||||
if (selectedUsers.length === 0) return;
|
||||
if (!canRemoveParticipants) return;
|
||||
@@ -428,6 +443,25 @@ export default function Home({ user, group, users, entity }: Props) {
|
||||
{capitalize(type)}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => {
|
||||
toggleAllUsersInList();
|
||||
}}
|
||||
disabled={rows.length === 0}
|
||||
className={clsx(
|
||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||
"transition duration-300 ease-in-out",
|
||||
"disabled:grayscale disabled:hover:bg-mti-purple-ultralight disabled:hover:text-mti-purple disabled:cursor-not-allowed",
|
||||
(isAdding ? nonParticipantUsers : group.participants)
|
||||
.length === selectedUsers.length &&
|
||||
"!bg-mti-purple-light !text-white"
|
||||
)}
|
||||
>
|
||||
{"De/Select All"}
|
||||
</button>
|
||||
<span className="opacity-80">
|
||||
{selectedUsers.length} selected
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import BatchCodeGenerator from "./(admin)/BatchCodeGenerator";
|
||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||
import BatchCreateUser from "./(admin)/Lists/BatchCreateUser";
|
||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||
import {useEffect, useState} from "react";
|
||||
import { useState} from "react";
|
||||
import Modal from "@/components/Modal";
|
||||
import IconCard from "@/components/IconCard";
|
||||
import {BsCode, BsCodeSquare, BsGearFill, BsPeopleFill, BsPersonFill} from "react-icons/bs";
|
||||
|
||||
Reference in New Issue
Block a user