Continued updating the code to work with entities better
This commit is contained in:
@@ -3,373 +3,300 @@ import Input from "@/components/Low/Input";
|
||||
import Modal from "@/components/Modal";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import {CorporateUser, Group, User} from "@/interfaces/user";
|
||||
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||
import { CorporateUser, Group, User } from "@/interfaces/user";
|
||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import {capitalize, uniq} from "lodash";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {BsPencil, BsQuestionCircleFill, BsTrash} from "react-icons/bs";
|
||||
import { capitalize, uniq } from "lodash";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { BsPencil, BsQuestionCircleFill, BsTrash } from "react-icons/bs";
|
||||
import Select from "react-select";
|
||||
import {toast} from "react-toastify";
|
||||
import { toast } from "react-toastify";
|
||||
import readXlsxFile from "read-excel-file";
|
||||
import {useFilePicker} from "use-file-picker";
|
||||
import {getUserCorporate} from "@/utils/groups";
|
||||
import {isAgentUser, isCorporateUser, USER_TYPE_LABELS} from "@/resources/user";
|
||||
import {checkAccess} from "@/utils/permissions";
|
||||
import { useFilePicker } from "use-file-picker";
|
||||
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";
|
||||
import { useListSearch } from "@/hooks/useListSearch";
|
||||
import Table from "@/components/High/Table";
|
||||
import useEntitiesGroups from "@/hooks/useEntitiesGroups";
|
||||
import useEntitiesUsers from "@/hooks/useEntitiesUsers";
|
||||
import { WithEntity } from "@/interfaces/entity";
|
||||
const searchFields = [["name"]];
|
||||
|
||||
const columnHelper = createColumnHelper<Group>();
|
||||
const columnHelper = createColumnHelper<WithEntity<Group>>();
|
||||
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
||||
|
||||
const LinkedCorporate = ({userId, users, groups}: {userId: string; users: User[]; groups: Group[]}) => {
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const user = users.find((u) => u.id === userId);
|
||||
if (!user) return setCompanyName("");
|
||||
|
||||
if (isCorporateUser(user)) return setCompanyName(user.corporateInformation?.companyInformation?.name || user.name);
|
||||
if (isAgentUser(user)) return setCompanyName(user.agentInformation?.companyName || user.name);
|
||||
|
||||
const belongingGroups = groups.filter((x) => x.participants.includes(userId));
|
||||
const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x));
|
||||
|
||||
if (belongingGroupsAdmins.length === 0) return setCompanyName("");
|
||||
|
||||
const admin = belongingGroupsAdmins[0] as CorporateUser;
|
||||
setCompanyName(admin.corporateInformation?.companyInformation.name || admin.name);
|
||||
}, [userId, users, groups]);
|
||||
|
||||
return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>;
|
||||
};
|
||||
|
||||
interface CreateDialogProps {
|
||||
user: User;
|
||||
users: User[];
|
||||
group?: Group;
|
||||
onClose: () => void;
|
||||
user: User;
|
||||
users: User[];
|
||||
group?: Group;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
|
||||
const [name, setName] = useState<string | undefined>(group?.name || undefined);
|
||||
const [admin, setAdmin] = useState<string>(group?.admin || user.id);
|
||||
const [participants, setParticipants] = useState<string[]>(group?.participants || []);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
|
||||
const [name, setName] = useState<string | undefined>(group?.name || undefined);
|
||||
const [admin, setAdmin] = useState<string>(group?.admin || user.id);
|
||||
const [participants, setParticipants] = useState<string[]>(group?.participants || []);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const {openFilePicker, filesContent, clear} = useFilePicker({
|
||||
accept: ".xlsx",
|
||||
multiple: false,
|
||||
readAs: "ArrayBuffer",
|
||||
});
|
||||
const { openFilePicker, filesContent, clear } = useFilePicker({
|
||||
accept: ".xlsx",
|
||||
multiple: false,
|
||||
readAs: "ArrayBuffer",
|
||||
});
|
||||
|
||||
const availableUsers = useMemo(() => {
|
||||
if (user.type === "teacher") return users.filter((x) => ["student"].includes(x.type));
|
||||
if (user.type === "corporate") return users.filter((x) => ["teacher", "student"].includes(x.type));
|
||||
if (user.type === "mastercorporate") return users.filter((x) => ["corporate", "teacher", "student"].includes(x.type));
|
||||
const availableUsers = useMemo(() => {
|
||||
if (user.type === "teacher") return users.filter((x) => ["student"].includes(x.type));
|
||||
if (user.type === "corporate") return users.filter((x) => ["teacher", "student"].includes(x.type));
|
||||
if (user.type === "mastercorporate") return users.filter((x) => ["corporate", "teacher", "student"].includes(x.type));
|
||||
|
||||
return users;
|
||||
}, [user, users]);
|
||||
return users;
|
||||
}, [user, users]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filesContent.length > 0) {
|
||||
setIsLoading(true);
|
||||
useEffect(() => {
|
||||
if (filesContent.length > 0) {
|
||||
setIsLoading(true);
|
||||
|
||||
const file = filesContent[0];
|
||||
readXlsxFile(file.content).then((rows) => {
|
||||
const emails = uniq(
|
||||
rows
|
||||
.map((row) => {
|
||||
const [email] = row as string[];
|
||||
return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email) ? email.toString().trim() : undefined;
|
||||
})
|
||||
.filter((x) => !!x),
|
||||
);
|
||||
const file = filesContent[0];
|
||||
readXlsxFile(file.content).then((rows) => {
|
||||
const emails = uniq(
|
||||
rows
|
||||
.map((row) => {
|
||||
const [email] = row as string[];
|
||||
return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email) ? email.toString().trim() : undefined;
|
||||
})
|
||||
.filter((x) => !!x),
|
||||
);
|
||||
|
||||
if (emails.length === 0) {
|
||||
toast.error("Please upload an Excel file containing e-mails!");
|
||||
clear();
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (emails.length === 0) {
|
||||
toast.error("Please upload an Excel file containing e-mails!");
|
||||
clear();
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined);
|
||||
const filteredUsers = emailUsers.filter(
|
||||
(x) =>
|
||||
((user.type === "developer" || user.type === "admin" || user.type === "corporate" || user.type === "mastercorporate") &&
|
||||
(x?.type === "student" || x?.type === "teacher")) ||
|
||||
(user.type === "teacher" && x?.type === "student"),
|
||||
);
|
||||
const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined);
|
||||
const filteredUsers = emailUsers.filter(
|
||||
(x) =>
|
||||
((user.type === "developer" || user.type === "admin" || user.type === "corporate" || user.type === "mastercorporate") &&
|
||||
(x?.type === "student" || x?.type === "teacher")) ||
|
||||
(user.type === "teacher" && x?.type === "student"),
|
||||
);
|
||||
|
||||
setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id));
|
||||
toast.success(
|
||||
user.type !== "teacher"
|
||||
? "Added all teachers and students found in the file you've provided!"
|
||||
: "Added all students found in the file you've provided!",
|
||||
{toastId: "upload-success"},
|
||||
);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filesContent, user.type, users]);
|
||||
setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id));
|
||||
toast.success(
|
||||
user.type !== "teacher"
|
||||
? "Added all teachers and students found in the file you've provided!"
|
||||
: "Added all students found in the file you've provided!",
|
||||
{ toastId: "upload-success" },
|
||||
);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filesContent, user.type, users]);
|
||||
|
||||
const submit = () => {
|
||||
setIsLoading(true);
|
||||
const submit = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (name !== group?.name && (name?.trim() === "Students" || name?.trim() === "Teachers" || name?.trim() === "Corporate")) {
|
||||
toast.error("That group name is reserved and cannot be used, please enter another one.");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (name !== group?.name && (name?.trim() === "Students" || name?.trim() === "Teachers" || name?.trim() === "Corporate")) {
|
||||
toast.error("That group name is reserved and cannot be used, please enter another one.");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
(group ? axios.patch : axios.post)(group ? `/api/groups/${group.id}` : "/api/groups", {name, admin, participants})
|
||||
.then(() => {
|
||||
toast.success(`Group "${name}" ${group ? "edited" : "created"} successfully`);
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Something went wrong, please try again later!");
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
onClose();
|
||||
});
|
||||
};
|
||||
(group ? axios.patch : axios.post)(group ? `/api/groups/${group.id}` : "/api/groups", { name, admin, participants })
|
||||
.then(() => {
|
||||
toast.success(`Group "${name}" ${group ? "edited" : "created"} successfully`);
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Something went wrong, please try again later!");
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
onClose();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex w-full flex-col gap-12 px-4 py-2">
|
||||
<div className="flex flex-col gap-8">
|
||||
<Input name="name" type="text" label="Name" defaultValue={name} onChange={setName} required disabled={group?.disableEditing} />
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-mti-gray-dim text-base font-normal">Participants</label>
|
||||
<div className="tooltip" data-tip="The Excel file should only include a column with the desired e-mails.">
|
||||
<BsQuestionCircleFill />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full gap-8">
|
||||
<Select
|
||||
className="w-full"
|
||||
value={participants.map((x) => ({
|
||||
value: x,
|
||||
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
|
||||
}))}
|
||||
placeholder="Participants..."
|
||||
defaultValue={participants.map((x) => ({
|
||||
value: x,
|
||||
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
|
||||
}))}
|
||||
options={availableUsers.map((x) => ({value: x.id, label: `${x.email} - ${x.name}`}))}
|
||||
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
||||
isMulti
|
||||
isSearchable
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
backgroundColor: "white",
|
||||
padding: "1rem 1.5rem",
|
||||
zIndex: "40",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{user.type !== "teacher" && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex w-full items-center justify-end gap-8">
|
||||
<Button variant="outline" color="red" className="w-full max-w-[200px]" isLoading={isLoading} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="w-full max-w-[200px]" onClick={submit} isLoading={isLoading} disabled={!name}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="mt-4 flex w-full flex-col gap-12 px-4 py-2">
|
||||
<div className="flex flex-col gap-8">
|
||||
<Input name="name" type="text" label="Name" defaultValue={name} onChange={setName} required disabled={group?.disableEditing} />
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-mti-gray-dim text-base font-normal">Participants</label>
|
||||
<div className="tooltip" data-tip="The Excel file should only include a column with the desired e-mails.">
|
||||
<BsQuestionCircleFill />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full gap-8">
|
||||
<Select
|
||||
className="w-full"
|
||||
value={participants.map((x) => ({
|
||||
value: x,
|
||||
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
|
||||
}))}
|
||||
placeholder="Participants..."
|
||||
defaultValue={participants.map((x) => ({
|
||||
value: x,
|
||||
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
|
||||
}))}
|
||||
options={availableUsers.map((x) => ({ value: x.id, label: `${x.email} - ${x.name}` }))}
|
||||
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
||||
isMulti
|
||||
isSearchable
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
backgroundColor: "white",
|
||||
padding: "1rem 1.5rem",
|
||||
zIndex: "40",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{user.type !== "teacher" && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex w-full items-center justify-end gap-8">
|
||||
<Button variant="outline" color="red" className="w-full max-w-[200px]" isLoading={isLoading} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="w-full max-w-[200px]" onClick={submit} isLoading={isLoading} disabled={!name}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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>();
|
||||
|
||||
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 || "");
|
||||
|
||||
const {permissions} = usePermissions(user?.id || "");
|
||||
const { users } = useEntitiesUsers();
|
||||
const { groups, reload } = useEntitiesGroups();
|
||||
|
||||
const {users} = useUsers();
|
||||
const {groups, reload} = useGroups({
|
||||
admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
|
||||
userType: user?.type,
|
||||
});
|
||||
const deleteGroup = (group: Group) => {
|
||||
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
|
||||
|
||||
const {groups: corporateGroups} = useGroups({
|
||||
admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
|
||||
userType: user?.type,
|
||||
adminAdmins: user?.id,
|
||||
});
|
||||
axios
|
||||
.delete<{ ok: boolean }>(`/api/groups/${group.id}`)
|
||||
.then(() => toast.success(`Group "${group.name}" deleted successfully`))
|
||||
.catch(() => toast.error("Something went wrong, please try again later!"))
|
||||
.finally(reload);
|
||||
};
|
||||
|
||||
const {rows: filteredRows, renderSearch} = useListSearch<Group>(searchFields, groups);
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("id", {
|
||||
header: "ID",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: "Name",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("admin", {
|
||||
header: "Admin",
|
||||
cell: (info) => (
|
||||
<div className="tooltip" data-tip={USER_TYPE_LABELS[users.find((x) => x.id === info.getValue())?.type || "student"]}>
|
||||
{users.find((x) => x.id === info.getValue())?.name}
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("entity.label", {
|
||||
header: "Entity",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("participants", {
|
||||
header: "Participants",
|
||||
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: "",
|
||||
id: "actions",
|
||||
cell: ({ row }: { row: { original: Group } }) => {
|
||||
return (
|
||||
<>
|
||||
{user && (checkAccess(user, ["developer", "admin"]) || user.id === row.original.admin) && (
|
||||
<div className="flex gap-2">
|
||||
{(!row.original.disableEditing || checkAccess(user, ["developer", "admin"]), "editGroup") && (
|
||||
<div data-tip="Edit" className="tooltip cursor-pointer" onClick={() => setEditingGroup(row.original)}>
|
||||
<BsPencil className="hover:text-mti-purple-light transition duration-300 ease-in-out" />
|
||||
</div>
|
||||
)}
|
||||
{(!row.original.disableEditing || checkAccess(user, ["developer", "admin"]), "deleteGroup") && (
|
||||
<div data-tip="Delete" className="tooltip cursor-pointer" onClick={() => deleteGroup(row.original)}>
|
||||
<BsTrash className="hover:text-mti-purple-light transition duration-300 ease-in-out" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const deleteGroup = (group: Group) => {
|
||||
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
|
||||
const closeModal = () => {
|
||||
setIsCreating(false);
|
||||
setEditingGroup(undefined);
|
||||
reload();
|
||||
};
|
||||
|
||||
axios
|
||||
.delete<{ok: boolean}>(`/api/groups/${group.id}`)
|
||||
.then(() => toast.success(`Group "${group.name}" deleted successfully`))
|
||||
.catch(() => toast.error("Something went wrong, please try again later!"))
|
||||
.finally(reload);
|
||||
};
|
||||
return (
|
||||
<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}
|
||||
user={user}
|
||||
onClose={closeModal}
|
||||
users={users}
|
||||
/>
|
||||
</Modal>
|
||||
<Table data={groups} columns={defaultColumns} searchFields={searchFields} />
|
||||
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("id", {
|
||||
header: "ID",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: "Name",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("admin", {
|
||||
header: "Admin",
|
||||
cell: (info) => (
|
||||
<div className="tooltip" data-tip={USER_TYPE_LABELS[users.find((x) => x.id === info.getValue())?.type || "student"]}>
|
||||
{users.find((x) => x.id === info.getValue())?.name}
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("admin", {
|
||||
header: "Linked Corporate",
|
||||
cell: (info) => <LinkedCorporate userId={info.getValue()} users={users} groups={groups} />,
|
||||
}),
|
||||
columnHelper.accessor("participants", {
|
||||
header: "Participants",
|
||||
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: "",
|
||||
id: "actions",
|
||||
cell: ({row}: {row: {original: Group}}) => {
|
||||
return (
|
||||
<>
|
||||
{user && (checkAccess(user, ["developer", "admin"]) || user.id === row.original.admin) && (
|
||||
<div className="flex gap-2">
|
||||
{(!row.original.disableEditing || checkAccess(user, ["developer", "admin"]), "editGroup") && (
|
||||
<div data-tip="Edit" className="tooltip cursor-pointer" onClick={() => setEditingGroup(row.original)}>
|
||||
<BsPencil className="hover:text-mti-purple-light transition duration-300 ease-in-out" />
|
||||
</div>
|
||||
)}
|
||||
{(!row.original.disableEditing || checkAccess(user, ["developer", "admin"]), "deleteGroup") && (
|
||||
<div data-tip="Delete" className="tooltip cursor-pointer" onClick={() => deleteGroup(row.original)}>
|
||||
<BsTrash className="hover:text-mti-purple-light transition duration-300 ease-in-out" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredRows,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
setIsCreating(false);
|
||||
setEditingGroup(undefined);
|
||||
reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<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}
|
||||
user={user}
|
||||
onClose={closeModal}
|
||||
users={
|
||||
checkAccess(user, ["corporate", "teacher", "mastercorporate"])
|
||||
? users.filter(
|
||||
(u) =>
|
||||
groups
|
||||
.filter((g) => g.admin === user.id)
|
||||
.flatMap((g) => g.participants)
|
||||
.includes(u.id) ||
|
||||
(user?.type === "teacher" ? corporateGroups : groups).flatMap((g) => g.participants).includes(u.id),
|
||||
)
|
||||
: users
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
{renderSearch()}
|
||||
<table className="bg-mti-purple-ultralight/40 w-full rounded-xl">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th className="py-4" 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="even:bg-mti-purple-ultralight/40 rounded-lg py-2 odd:bg-white" 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>
|
||||
|
||||
{checkAccess(user, ["teacher", "corporate", "mastercorporate", "admin", "developer"], permissions, "createGroup") && (
|
||||
<button
|
||||
onClick={() => setIsCreating(true)}
|
||||
className="bg-mti-purple-light hover:bg-mti-purple w-full py-2 text-white transition duration-300 ease-in-out">
|
||||
New Group
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{checkAccess(user, ["teacher", "corporate", "mastercorporate", "admin", "developer"], permissions, "createGroup") && (
|
||||
<button
|
||||
onClick={() => setIsCreating(true)}
|
||||
className="bg-mti-purple-light hover:bg-mti-purple w-full py-2 text-white transition duration-300 ease-in-out">
|
||||
New Group
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,288 +1,273 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import Checkbox from "@/components/Low/Checkbox";
|
||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||
import {CorporateUser, TeacherUser, Type, User} from "@/interfaces/user";
|
||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
||||
import { CorporateUser, TeacherUser, Type, User } from "@/interfaces/user";
|
||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import {capitalize, uniqBy} from "lodash";
|
||||
import { capitalize, uniqBy } from "lodash";
|
||||
import moment from "moment";
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import {toast} from "react-toastify";
|
||||
import { toast } from "react-toastify";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||
import {PermissionType} from "@/interfaces/permissions";
|
||||
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
||||
import { PermissionType } from "@/interfaces/permissions";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import Input from "@/components/Low/Input";
|
||||
import CountrySelect from "@/components/Low/CountrySelect";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import {getUserName} from "@/utils/users";
|
||||
import { getUserName } from "@/utils/users";
|
||||
import Select from "@/components/Low/Select";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import useEntitiesGroups from "@/hooks/useEntitiesGroups";
|
||||
|
||||
const USER_TYPE_PERMISSIONS: {
|
||||
[key in Type]: {perm: PermissionType | undefined; list: Type[]};
|
||||
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
|
||||
} = {
|
||||
student: {
|
||||
perm: "createCodeStudent",
|
||||
list: [],
|
||||
},
|
||||
teacher: {
|
||||
perm: "createCodeTeacher",
|
||||
list: [],
|
||||
},
|
||||
agent: {
|
||||
perm: "createCodeCountryManager",
|
||||
list: ["student", "teacher", "corporate", "mastercorporate"],
|
||||
},
|
||||
corporate: {
|
||||
perm: "createCodeCorporate",
|
||||
list: ["student", "teacher"],
|
||||
},
|
||||
mastercorporate: {
|
||||
perm: undefined,
|
||||
list: ["student", "teacher", "corporate"],
|
||||
},
|
||||
admin: {
|
||||
perm: "createCodeAdmin",
|
||||
list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
|
||||
},
|
||||
developer: {
|
||||
perm: undefined,
|
||||
list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
|
||||
},
|
||||
student: {
|
||||
perm: "createCodeStudent",
|
||||
list: [],
|
||||
},
|
||||
teacher: {
|
||||
perm: "createCodeTeacher",
|
||||
list: [],
|
||||
},
|
||||
agent: {
|
||||
perm: "createCodeCountryManager",
|
||||
list: ["student", "teacher", "corporate", "mastercorporate"],
|
||||
},
|
||||
corporate: {
|
||||
perm: "createCodeCorporate",
|
||||
list: ["student", "teacher"],
|
||||
},
|
||||
mastercorporate: {
|
||||
perm: undefined,
|
||||
list: ["student", "teacher", "corporate"],
|
||||
},
|
||||
admin: {
|
||||
perm: "createCodeAdmin",
|
||||
list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
|
||||
},
|
||||
developer: {
|
||||
perm: undefined,
|
||||
list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
|
||||
},
|
||||
};
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
permissions: PermissionType[];
|
||||
onFinish: () => void;
|
||||
user: User;
|
||||
users: User[];
|
||||
entities: EntityWithRoles[]
|
||||
permissions: PermissionType[];
|
||||
onFinish: () => void;
|
||||
}
|
||||
|
||||
export default function UserCreator({user, users, permissions, onFinish}: Props) {
|
||||
const [name, setName] = useState<string>();
|
||||
const [email, setEmail] = useState<string>();
|
||||
const [phone, setPhone] = useState<string>();
|
||||
const [passportID, setPassportID] = useState<string>();
|
||||
const [studentID, setStudentID] = useState<string>();
|
||||
const [country, setCountry] = useState(user?.demographicInformation?.country);
|
||||
const [group, setGroup] = useState<string | null>();
|
||||
const [availableCorporates, setAvailableCorporates] = useState<User[]>([]);
|
||||
const [selectedCorporate, setSelectedCorporate] = useState<string | null>();
|
||||
const [password, setPassword] = useState<string>();
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>();
|
||||
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
||||
user?.subscriptionExpirationDate ? moment(user?.subscriptionExpirationDate).toDate() : null,
|
||||
);
|
||||
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [type, setType] = useState<Type>("student");
|
||||
const [position, setPosition] = useState<string>();
|
||||
export default function UserCreator({ user, users, entities = [], permissions, onFinish }: Props) {
|
||||
const [name, setName] = useState<string>();
|
||||
const [email, setEmail] = useState<string>();
|
||||
const [phone, setPhone] = useState<string>();
|
||||
const [passportID, setPassportID] = useState<string>();
|
||||
const [studentID, setStudentID] = useState<string>();
|
||||
const [country, setCountry] = useState(user?.demographicInformation?.country);
|
||||
const [group, setGroup] = useState<string | null>();
|
||||
const [password, setPassword] = useState<string>();
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>();
|
||||
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
||||
user?.subscriptionExpirationDate ? moment(user?.subscriptionExpirationDate).toDate() : null,
|
||||
);
|
||||
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [type, setType] = useState<Type>("student");
|
||||
const [position, setPosition] = useState<string>();
|
||||
const [entity, setEntity] = useState((entities || [])[0]?.id || undefined)
|
||||
|
||||
const {groups} = useGroups({admin: ["developer", "admin"].includes(user?.type) ? undefined : user?.id, userType: user?.type});
|
||||
const { groups } = useEntitiesGroups();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpiryDateEnabled) setExpiryDate(null);
|
||||
}, [isExpiryDateEnabled]);
|
||||
useEffect(() => {
|
||||
if (!isExpiryDateEnabled) setExpiryDate(null);
|
||||
}, [isExpiryDateEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
setAvailableCorporates(
|
||||
uniqBy(
|
||||
users.filter((u) => u.type === "corporate" && groups.flatMap((g) => g.participants).includes(u.id)),
|
||||
"id",
|
||||
),
|
||||
);
|
||||
}, [users, groups]);
|
||||
const createUser = () => {
|
||||
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 < 6) return toast.error("Please enter a valid password!");
|
||||
if (password !== confirmPassword) return toast.error("The passwords do not match!");
|
||||
|
||||
const createUser = () => {
|
||||
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 < 6) return toast.error("Please enter a valid password!");
|
||||
if (password !== confirmPassword) return toast.error("The passwords do not match!");
|
||||
setIsLoading(true);
|
||||
|
||||
setIsLoading(true);
|
||||
const body = {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
groupID: group,
|
||||
entity,
|
||||
type,
|
||||
studentID: type === "student" ? studentID : undefined,
|
||||
expiryDate,
|
||||
demographicInformation: {
|
||||
passport_id: type === "student" ? passportID : undefined,
|
||||
phone,
|
||||
country,
|
||||
position,
|
||||
},
|
||||
};
|
||||
|
||||
const body = {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
groupID: group,
|
||||
corporate: selectedCorporate || user.id,
|
||||
type,
|
||||
studentID: type === "student" ? studentID : undefined,
|
||||
expiryDate,
|
||||
demographicInformation: {
|
||||
passport_id: type === "student" ? passportID : undefined,
|
||||
phone,
|
||||
country,
|
||||
position,
|
||||
},
|
||||
};
|
||||
axios
|
||||
.post("/api/make_user", body)
|
||||
.then(() => {
|
||||
toast.success("That user has been created!");
|
||||
onFinish();
|
||||
|
||||
axios
|
||||
.post("/api/make_user", body)
|
||||
.then(() => {
|
||||
toast.success("That user has been created!");
|
||||
onFinish();
|
||||
setName("");
|
||||
setEmail("");
|
||||
setPhone("");
|
||||
setPassportID("");
|
||||
setStudentID("");
|
||||
setCountry(user?.demographicInformation?.country);
|
||||
setGroup(null);
|
||||
setEntity((entities || [])[0]?.id || undefined)
|
||||
setExpiryDate(user?.subscriptionExpirationDate ? moment(user?.subscriptionExpirationDate).toDate() : null);
|
||||
setIsExpiryDateEnabled(true);
|
||||
setType("student");
|
||||
setPosition(undefined);
|
||||
})
|
||||
.catch((error) => {
|
||||
const data = error?.response?.data;
|
||||
if (!!data?.message) return toast.error(data.message);
|
||||
toast.error("Something went wrong! Please try again later!");
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
setName("");
|
||||
setEmail("");
|
||||
setPhone("");
|
||||
setPassportID("");
|
||||
setStudentID("");
|
||||
setCountry(user?.demographicInformation?.country);
|
||||
setGroup(null);
|
||||
setSelectedCorporate(null);
|
||||
setExpiryDate(user?.subscriptionExpirationDate ? moment(user?.subscriptionExpirationDate).toDate() : null);
|
||||
setIsExpiryDateEnabled(true);
|
||||
setType("student");
|
||||
setPosition(undefined);
|
||||
})
|
||||
.catch((error) => {
|
||||
const data = error?.response?.data;
|
||||
if (!!data?.message) return toast.error(data.message);
|
||||
toast.error("Something went wrong! Please try again later!");
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-4 border p-4 border-mti-gray-platinum rounded-xl">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input required label="Name" value={name} onChange={setName} type="text" name="name" placeholder="Name" />
|
||||
<Input label="E-mail" required value={email} onChange={setEmail} type="email" name="email" placeholder="E-mail" />
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 border p-4 border-mti-gray-platinum rounded-xl">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input required label="Name" value={name} onChange={setName} type="text" name="name" placeholder="Name" />
|
||||
<Input label="E-mail" required value={email} onChange={setEmail} type="email" name="email" placeholder="E-mail" />
|
||||
<Input type="password" name="password" label="Password" value={password} onChange={setPassword} placeholder="Password" required />
|
||||
<Input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
label="Confirm Password"
|
||||
value={confirmPassword}
|
||||
onChange={setConfirmPassword}
|
||||
placeholder="ConfirmPassword"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input type="password" name="password" label="Password" value={password} onChange={setPassword} placeholder="Password" required />
|
||||
<Input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
label="Confirm Password"
|
||||
value={confirmPassword}
|
||||
onChange={setConfirmPassword}
|
||||
placeholder="ConfirmPassword"
|
||||
required
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Country *</label>
|
||||
<CountrySelect value={country} onChange={setCountry} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Country *</label>
|
||||
<CountrySelect value={country} onChange={setCountry} />
|
||||
</div>
|
||||
<Input type="tel" name="phone" label="Phone number" value={phone} onChange={setPhone} placeholder="Phone number" required />
|
||||
|
||||
<Input type="tel" name="phone" label="Phone number" value={phone} onChange={setPhone} placeholder="Phone number" required />
|
||||
{type === "student" && (
|
||||
<>
|
||||
<Input
|
||||
type="text"
|
||||
name="passport_id"
|
||||
label="Passport/National ID"
|
||||
onChange={setPassportID}
|
||||
value={passportID}
|
||||
placeholder="National ID or Passport number"
|
||||
required
|
||||
/>
|
||||
<Input type="text" name="studentID" label="Student ID" onChange={setStudentID} value={studentID} placeholder="Student ID" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "student" && (
|
||||
<>
|
||||
<Input
|
||||
type="text"
|
||||
name="passport_id"
|
||||
label="Passport/National ID"
|
||||
onChange={setPassportID}
|
||||
value={passportID}
|
||||
placeholder="National ID or Passport number"
|
||||
required
|
||||
/>
|
||||
<Input type="text" name="studentID" label="Student ID" onChange={setStudentID} value={studentID} placeholder="Student ID" />
|
||||
</>
|
||||
)}
|
||||
<div className={clsx("flex flex-col gap-4")}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
||||
<Select
|
||||
defaultValue={{ value: (entities || [])[0]?.id, label: (entities || [])[0]?.label }}
|
||||
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
||||
onChange={(e) => setEntity(e?.value || undefined)}
|
||||
isClearable={checkAccess(user, ["admin", "developer"])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{["student", "teacher"].includes(type) && !["corporate", "teacher"].includes(user?.type) && (
|
||||
<div className={clsx("flex flex-col gap-4")}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
|
||||
<Select
|
||||
options={availableCorporates.map((u) => ({value: u.id, label: getUserName(u)}))}
|
||||
isClearable
|
||||
onChange={(e) => setSelectedCorporate(e?.value || undefined)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{["corporate", "mastercorporate"].includes(type) && (
|
||||
<Input type="text" name="department" label="Department" onChange={setPosition} value={position} placeholder="Department" />
|
||||
)}
|
||||
|
||||
{["corporate", "mastercorporate"].includes(type) && (
|
||||
<Input type="text" name="department" label="Department" onChange={setPosition} value={position} placeholder="Department" />
|
||||
)}
|
||||
<div className={clsx("flex flex-col gap-4")}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Group</label>
|
||||
<Select
|
||||
options={groups
|
||||
.filter((x) => x.entity?.id === entity)
|
||||
.map((g) => ({ value: g.id, label: g.name }))}
|
||||
onChange={(e) => setGroup(e?.value || undefined)}
|
||||
isClearable
|
||||
/>
|
||||
</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(
|
||||
"flex flex-col gap-4",
|
||||
!checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && "col-span-2",
|
||||
)}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Type</label>
|
||||
{user && (
|
||||
<select
|
||||
defaultValue="student"
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as Type)}
|
||||
className="p-6 w-full min-w-[350px] min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
|
||||
{Object.keys(USER_TYPE_LABELS)
|
||||
.filter((x) => {
|
||||
const { list, perm } = USER_TYPE_PERMISSIONS[x as Type];
|
||||
return checkAccess(user, getTypesOfUser(list), permissions, perm);
|
||||
})
|
||||
.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"flex flex-col gap-4",
|
||||
!checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && "col-span-2",
|
||||
)}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Type</label>
|
||||
{user && (
|
||||
<select
|
||||
defaultValue="student"
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as Type)}
|
||||
className="p-6 w-full min-w-[350px] min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
|
||||
{Object.keys(USER_TYPE_LABELS)
|
||||
.filter((x) => {
|
||||
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
|
||||
return checkAccess(user, getTypesOfUser(list), permissions, perm);
|
||||
})
|
||||
.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||
<>
|
||||
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
||||
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
||||
<Checkbox
|
||||
isChecked={isExpiryDateEnabled}
|
||||
onChange={setIsExpiryDateEnabled}
|
||||
disabled={!!user?.subscriptionExpirationDate}>
|
||||
Enabled
|
||||
</Checkbox>
|
||||
</div>
|
||||
{isExpiryDateEnabled && (
|
||||
<ReactDatePicker
|
||||
className={clsx(
|
||||
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
|
||||
"hover:border-mti-purple tooltip",
|
||||
"transition duration-300 ease-in-out",
|
||||
)}
|
||||
filterDate={(date) =>
|
||||
moment(date).isAfter(new Date()) &&
|
||||
(user?.subscriptionExpirationDate ? moment(date).isBefore(user?.subscriptionExpirationDate) : true)
|
||||
}
|
||||
dateFormat="dd/MM/yyyy"
|
||||
selected={expiryDate}
|
||||
onChange={(date) => setExpiryDate(date)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||
<>
|
||||
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
||||
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
||||
<Checkbox
|
||||
isChecked={isExpiryDateEnabled}
|
||||
onChange={setIsExpiryDateEnabled}
|
||||
disabled={!!user?.subscriptionExpirationDate}>
|
||||
Enabled
|
||||
</Checkbox>
|
||||
</div>
|
||||
{isExpiryDateEnabled && (
|
||||
<ReactDatePicker
|
||||
className={clsx(
|
||||
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
|
||||
"hover:border-mti-purple tooltip",
|
||||
"transition duration-300 ease-in-out",
|
||||
)}
|
||||
filterDate={(date) =>
|
||||
moment(date).isAfter(new Date()) &&
|
||||
(user?.subscriptionExpirationDate ? moment(date).isBefore(user?.subscriptionExpirationDate) : true)
|
||||
}
|
||||
dateFormat="dd/MM/yyyy"
|
||||
selected={expiryDate}
|
||||
onChange={(date) => setExpiryDate(date)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={createUser} isLoading={isLoading} disabled={(isExpiryDateEnabled ? !expiryDate : false) || isLoading}>
|
||||
Create User
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
<Button onClick={createUser} isLoading={isLoading} disabled={(isExpiryDateEnabled ? !expiryDate : false) || isLoading}>
|
||||
Create User
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
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';
|
||||
@@ -9,53 +9,58 @@ import axios from "axios";
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "POST") return post(req, res);
|
||||
if (req.method === "POST") return post(req, res);
|
||||
|
||||
return res.status(404).json({ok: false});
|
||||
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 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 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 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;
|
||||
};
|
||||
entity?: string
|
||||
entities: { id: string, role: 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}`,
|
||||
},
|
||||
});
|
||||
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);
|
||||
|
||||
return res.status(backendRequest.status).json(backendRequest.data)
|
||||
currentUser.entities = [{ id: currentUser.entity!, role: "90ce8f08-08c8-41e4-9848-f1500ddc3930" }]
|
||||
delete currentUser.entity
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
32
src/pages/api/entities/groups.ts
Normal file
32
src/pages/api/entities/groups.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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 { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { Entity, WithEntities, WithEntity, WithLabeledEntities } from "@/interfaces/entity";
|
||||
import { v4 } from "uuid";
|
||||
import { mapBy } from "@/utils";
|
||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||
import { Group, User } from "@/interfaces/user";
|
||||
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return await get(req, res);
|
||||
}
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = req.session.user;
|
||||
|
||||
const groups: WithEntity<Group>[] = ["admin", "developer"].includes(user.type)
|
||||
? await getGroups()
|
||||
: await getGroupsByEntities(mapBy(user.entities || [], 'id'))
|
||||
|
||||
res.status(200).json(groups);
|
||||
}
|
||||
47
src/pages/api/entities/users.ts
Normal file
47
src/pages/api/entities/users.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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 { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { Entity, WithEntities, WithLabeledEntities } from "@/interfaces/entity";
|
||||
import { v4 } from "uuid";
|
||||
import { mapBy } from "@/utils";
|
||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||
import { User } from "@/interfaces/user";
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return await get(req, res);
|
||||
}
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = req.session.user;
|
||||
|
||||
const { type } = req.query as { type: string }
|
||||
const entities = await getEntitiesWithRoles(mapBy(user.entities || [], 'id'))
|
||||
|
||||
const filter = !type ? undefined : { type }
|
||||
const users = ["admin", "developer"].includes(user.type)
|
||||
? await getUsers(filter)
|
||||
: await getEntitiesUsers(mapBy(entities, 'id') as string[], filter)
|
||||
|
||||
const usersWithEntities: WithLabeledEntities<User>[] = users.map((u) => {
|
||||
return {
|
||||
...u, entities: (u.entities || []).map((e) => {
|
||||
const entity = entities.find((x) => x.id === e.id)
|
||||
if (!entity) return e
|
||||
|
||||
const role = entity.roles.find((x) => x.id === e.role)
|
||||
return { id: e.id, label: entity.label, role: e.role, roleLabel: role?.label }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
res.status(200).json(usersWithEntities);
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {app} from "@/firebase";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {v4} from "uuid";
|
||||
import {CorporateUser, Group, Type, User} from "@/interfaces/user";
|
||||
import {createUserWithEmailAndPassword, getAuth} from "firebase/auth";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { app } from "@/firebase";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { v4 } from "uuid";
|
||||
import { CorporateUser, Group, Type, User } from "@/interfaces/user";
|
||||
import { createUserWithEmailAndPassword, getAuth } from "firebase/auth";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import {getGroup, getGroups, getUserCorporate, getUserGroups, getUserNamedGroup} from "@/utils/groups.be";
|
||||
import {uniq} from "lodash";
|
||||
import {getSpecificUsers, getUser} from "@/utils/users.be";
|
||||
import { getGroup, getGroups, getUserCorporate, getUserGroups, getUserNamedGroup } from "@/utils/groups.be";
|
||||
import { uniq } from "lodash";
|
||||
import { getSpecificUsers, getUser } from "@/utils/users.be";
|
||||
import client from "@/lib/mongodb";
|
||||
|
||||
const DEFAULT_DESIRED_LEVELS = {
|
||||
reading: 9,
|
||||
listening: 9,
|
||||
writing: 9,
|
||||
speaking: 9,
|
||||
reading: 9,
|
||||
listening: 9,
|
||||
writing: 9,
|
||||
speaking: 9,
|
||||
};
|
||||
|
||||
const DEFAULT_LEVELS = {
|
||||
reading: 0,
|
||||
listening: 0,
|
||||
writing: 0,
|
||||
speaking: 0,
|
||||
reading: 0,
|
||||
listening: 0,
|
||||
writing: 0,
|
||||
speaking: 0,
|
||||
};
|
||||
|
||||
const auth = getAuth(app);
|
||||
@@ -30,198 +30,97 @@ const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
const getUsersOfType = async (admin: string, type: Type) => {
|
||||
const groups = await getUserGroups(admin);
|
||||
const participants = groups.flatMap((x) => x.participants);
|
||||
const users = await getSpecificUsers(participants);
|
||||
|
||||
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);
|
||||
if (req.method === "POST") return post(req, res);
|
||||
|
||||
return res.status(404).json({ok: false});
|
||||
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 maker = req.session.user;
|
||||
if (!maker) {
|
||||
return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" });
|
||||
}
|
||||
|
||||
const corporateCorporate = await getUserCorporate(maker.id);
|
||||
const { email, passport_id, password, type, groupID, entity, expiryDate, corporate } = req.body as {
|
||||
email: string;
|
||||
password?: string;
|
||||
passport_id: string;
|
||||
type: string;
|
||||
entity: string;
|
||||
groupID?: string;
|
||||
corporate?: string;
|
||||
expiryDate: null | Date;
|
||||
};
|
||||
|
||||
const {email, passport_id, password, type, groupID, expiryDate, corporate} = req.body as {
|
||||
email: string;
|
||||
password?: string;
|
||||
passport_id: string;
|
||||
type: string;
|
||||
groupID?: string;
|
||||
corporate?: string;
|
||||
expiryDate: null | Date;
|
||||
};
|
||||
// cleaning data
|
||||
delete req.body.passport_id;
|
||||
delete req.body.groupID;
|
||||
delete req.body.expiryDate;
|
||||
delete req.body.password;
|
||||
delete req.body.corporate;
|
||||
// cleaning data
|
||||
delete req.body.passport_id;
|
||||
delete req.body.groupID;
|
||||
delete req.body.expiryDate;
|
||||
delete req.body.password;
|
||||
delete req.body.corporate;
|
||||
delete req.body.entity
|
||||
|
||||
await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id)
|
||||
.then(async (userCredentials) => {
|
||||
const userId = userCredentials.user.uid;
|
||||
await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id)
|
||||
.then(async (userCredentials) => {
|
||||
const userId = userCredentials.user.uid;
|
||||
|
||||
const profilePicture = !corporateCorporate ? "/defaultAvatar.png" : corporateCorporate.profilePicture;
|
||||
const user = {
|
||||
...req.body,
|
||||
bio: "",
|
||||
id: userId,
|
||||
type: type,
|
||||
focus: "academic",
|
||||
status: "active",
|
||||
desiredLevels: DEFAULT_DESIRED_LEVELS,
|
||||
profilePicture: "/defaultAvatar.png",
|
||||
levels: DEFAULT_LEVELS,
|
||||
isFirstLogin: false,
|
||||
isVerified: true,
|
||||
registrationDate: new Date(),
|
||||
entities: [{ id: entity, role: "90ce8f08-08c8-41e4-9848-f1500ddc3930" }],
|
||||
subscriptionExpirationDate: expiryDate || null,
|
||||
...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate"
|
||||
? {
|
||||
corporateInformation: {
|
||||
companyInformation: {
|
||||
name: maker.corporateInformation?.companyInformation?.name || "N/A",
|
||||
userAmount: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const user = {
|
||||
...req.body,
|
||||
bio: "",
|
||||
id: userId,
|
||||
type: type,
|
||||
focus: "academic",
|
||||
status: "active",
|
||||
desiredLevels: DEFAULT_DESIRED_LEVELS,
|
||||
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();
|
||||
const code = uid.randomUUID(6);
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const code = uid.randomUUID(6);
|
||||
await db.collection("users").insertOne(user);
|
||||
await db.collection("codes").insertOne({
|
||||
code,
|
||||
creator: maker.id,
|
||||
expiryDate,
|
||||
type,
|
||||
creationDate: new Date(),
|
||||
userId,
|
||||
email: email.toLowerCase(),
|
||||
name: req.body.name,
|
||||
...(!!passport_id ? { passport_id } : {}),
|
||||
});
|
||||
|
||||
await db.collection("users").insertOne(user);
|
||||
await db.collection("codes").insertOne({
|
||||
code,
|
||||
creator: maker.id,
|
||||
expiryDate,
|
||||
type,
|
||||
creationDate: new Date(),
|
||||
userId,
|
||||
email: email.toLowerCase(),
|
||||
name: req.body.name,
|
||||
...(!!passport_id ? {passport_id} : {}),
|
||||
});
|
||||
if (!!groupID) {
|
||||
const group = await getGroup(groupID);
|
||||
if (!!group) await db.collection("groups").updateOne({ id: group.id }, { $set: { participants: [...group.participants, userId] } });
|
||||
}
|
||||
|
||||
if (type === "corporate") {
|
||||
const students = maker.type === "corporate" ? await getUsersOfType(maker.id, "student") : [];
|
||||
const teachers = maker.type === "corporate" ? await getUsersOfType(maker.id, "teacher") : [];
|
||||
console.log(`Returning - ${email}`);
|
||||
return res.status(200).json({ ok: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code.includes("email-already-in-use")) return res.status(403).json({ error, message: "E-mail is already in the platform." });
|
||||
|
||||
const defaultTeachersGroup: Group = {
|
||||
admin: userId,
|
||||
id: v4(),
|
||||
name: "Teachers",
|
||||
participants: teachers,
|
||||
disableEditing: true,
|
||||
};
|
||||
|
||||
const defaultStudentsGroup: Group = {
|
||||
admin: userId,
|
||||
id: v4(),
|
||||
name: "Students",
|
||||
participants: students,
|
||||
disableEditing: true,
|
||||
};
|
||||
|
||||
await db.collection("groups").insertMany([defaultStudentsGroup, defaultTeachersGroup]);
|
||||
}
|
||||
|
||||
if (!!corporate) {
|
||||
const corporateUser = await db.collection("users").findOne<CorporateUser>({email: corporate.trim().toLowerCase()});
|
||||
|
||||
if (!!corporateUser) {
|
||||
await db.collection("codes").updateOne({code}, {$set: {creator: corporateUser.id}});
|
||||
const typeGroup = await db
|
||||
.collection("groups")
|
||||
.findOne<Group>({creator: corporateUser.id, name: type === "student" ? "Students" : "Teachers"});
|
||||
|
||||
if (!!typeGroup) {
|
||||
if (!typeGroup.participants.includes(userId)) {
|
||||
await db.collection("groups").updateOne({id: typeGroup.id}, {$set: {participants: [...typeGroup.participants, userId]}});
|
||||
}
|
||||
} else {
|
||||
const defaultGroup: Group = {
|
||||
admin: corporateUser.id,
|
||||
id: v4(),
|
||||
name: type === "student" ? "Students" : "Teachers",
|
||||
participants: [userId],
|
||||
disableEditing: true,
|
||||
};
|
||||
|
||||
await db.collection("groups").insertOne(defaultGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maker.type === "corporate") {
|
||||
await db.collection("codes").updateOne({code}, {$set: {creator: maker.id}});
|
||||
const typeGroup = await getUserNamedGroup(maker.id, type === "student" ? "Students" : "Teachers");
|
||||
|
||||
if (!!typeGroup) {
|
||||
if (!typeGroup.participants.includes(userId)) {
|
||||
await db.collection("groups").updateOne({id: typeGroup.id}, {$set: {participants: [...typeGroup.participants, userId]}});
|
||||
}
|
||||
} else {
|
||||
const defaultGroup: Group = {
|
||||
admin: maker.id,
|
||||
id: v4(),
|
||||
name: type === "student" ? "Students" : "Teachers",
|
||||
participants: [userId],
|
||||
disableEditing: true,
|
||||
};
|
||||
|
||||
await db.collection("groups").insertOne(defaultGroup);
|
||||
}
|
||||
}
|
||||
|
||||
if (!!corporateCorporate && corporateCorporate.type === "mastercorporate" && type === "corporate") {
|
||||
const corporateGroup = await getUserNamedGroup(corporateCorporate.id, "Corporate");
|
||||
|
||||
if (!!corporateGroup) {
|
||||
if (!corporateGroup.participants.includes(userId)) {
|
||||
await db
|
||||
.collection("groups")
|
||||
.updateOne({id: corporateGroup.id}, {$set: {participants: [...corporateGroup.participants, userId]}});
|
||||
}
|
||||
} else {
|
||||
const defaultGroup: Group = {
|
||||
admin: corporateCorporate.id,
|
||||
id: v4(),
|
||||
name: "Corporate",
|
||||
participants: [userId],
|
||||
disableEditing: true,
|
||||
};
|
||||
|
||||
await db.collection("groups").insertOne(defaultGroup);
|
||||
}
|
||||
}
|
||||
|
||||
if (!!groupID) {
|
||||
const group = await getGroup(groupID);
|
||||
if (!!group) await db.collection("groups").updateOne({id: group.id}, {$set: {participants: [...group.participants, userId]}});
|
||||
}
|
||||
|
||||
console.log(`Returning - ${email}`);
|
||||
return res.status(200).json({ok: true});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code.includes("email-already-in-use")) return res.status(403).json({error, message: "E-mail is already in the platform."});
|
||||
|
||||
console.log(`Failing - ${email}`);
|
||||
console.log(error);
|
||||
return res.status(401).json({error});
|
||||
});
|
||||
console.log(`Failing - ${email}`);
|
||||
console.log(error);
|
||||
return res.status(401).json({ error });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {NextApiRequest, NextApiResponse} from "next";
|
||||
import {getPermissions, getPermissionDocs} from "@/utils/permissions.be";
|
||||
import client from "@/lib/mongodb";
|
||||
import {getGroupsForUser, getParticipantGroups} from "@/utils/groups.be";
|
||||
import {getGroupsForUser, getParticipantGroups, removeParticipantFromGroup} from "@/utils/groups.be";
|
||||
import { mapBy } from "@/utils";
|
||||
|
||||
const auth = getAuth(adminApp);
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
@@ -41,20 +42,6 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.type === "corporate" && (targetUser.type === "student" || targetUser.type === "teacher")) {
|
||||
const groups = await getGroupsForUser(user.id, targetUser.id);
|
||||
await Promise.all([
|
||||
...groups
|
||||
.filter((x) => x.admin === user.id)
|
||||
.map(
|
||||
async (x) =>
|
||||
await db.collection("groups").updateOne({id: x.id}, {$set: {participants: x.participants.filter((y: string) => y !== id)}}),
|
||||
),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await auth.deleteUser(id);
|
||||
await db.collection("users").deleteOne({id: targetUser.id});
|
||||
await db.collection("codes").deleteMany({userId: targetUser.id});
|
||||
@@ -62,11 +49,10 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
||||
await db.collection("stats").deleteMany({user: targetUser.id});
|
||||
|
||||
const groups = await getParticipantGroups(targetUser.id);
|
||||
await Promise.all(
|
||||
groups.map(
|
||||
async (x) => await db.collection("groups").updateOne({id: x.id}, {$set: {participants: x.participants.filter((y: string) => y !== id)}}),
|
||||
),
|
||||
);
|
||||
await Promise.all(
|
||||
groups
|
||||
.map(async (g) => await removeParticipantFromGroup(g.id, targetUser.id)),
|
||||
);
|
||||
|
||||
res.json({ok: true});
|
||||
}
|
||||
|
||||
195
src/pages/dashboard/admin.tsx
Normal file
195
src/pages/dashboard/admin.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Layout from "@/components/High/Layout";
|
||||
import UserDisplayList from "@/components/UserDisplayList";
|
||||
import IconCard from "@/dashboards/IconCard";
|
||||
import { Module } from "@/interfaces";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
import { Group, Stat, User } from "@/interfaces/user";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { dateSorter, filterBy, mapBy, serialize } from "@/utils";
|
||||
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||
import { groupByExam } from "@/utils/stats";
|
||||
import { getStatsByUsers } from "@/utils/stats.be";
|
||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||
import { withIronSessionSsr } from "iron-session/next";
|
||||
import { uniqBy } from "lodash";
|
||||
import moment from "moment";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
BsBank,
|
||||
BsClipboard2Data,
|
||||
BsClock,
|
||||
BsEnvelopePaper,
|
||||
BsPaperclip,
|
||||
BsPencilSquare,
|
||||
BsPeople,
|
||||
BsPeopleFill,
|
||||
BsPersonFill,
|
||||
BsPersonFillGear,
|
||||
} from "react-icons/bs";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
entities: EntityWithRoles[];
|
||||
assignments: Assignment[];
|
||||
stats: Stat[];
|
||||
groups: Group[];
|
||||
}
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
const user = req.session.user as User | undefined;
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!checkAccess(user, ["admin", "developer"]))
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const users = await getUsers();
|
||||
const entities = await getEntitiesWithRoles();
|
||||
const assignments = await getAssignments();
|
||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||
const groups = await getGroups();
|
||||
|
||||
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
||||
const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]);
|
||||
const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||
const formattedStats = studentStats
|
||||
.map((s) => ({
|
||||
focus: students.find((u) => u.id === s.user)?.focus,
|
||||
score: s.score,
|
||||
module: s.module,
|
||||
}))
|
||||
.filter((f) => !!f.focus);
|
||||
const bandScores = formattedStats.map((s) => ({
|
||||
module: s.module,
|
||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||
}));
|
||||
|
||||
const levels: { [key in Module]: number } = {
|
||||
reading: 0,
|
||||
listening: 0,
|
||||
writing: 0,
|
||||
speaking: 0,
|
||||
level: 0,
|
||||
};
|
||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||
|
||||
return calculateAverageLevel(levels);
|
||||
};
|
||||
|
||||
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 />
|
||||
<Layout user={user}>
|
||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||
<IconCard
|
||||
onClick={() => router.push("/list/users?type=student")}
|
||||
Icon={BsPersonFill}
|
||||
label="Students"
|
||||
value={students.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
onClick={() => router.push("/list/users?type=teacher")}
|
||||
Icon={BsPencilSquare}
|
||||
label="Teachers"
|
||||
value={teachers.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsBank}
|
||||
onClick={() => router.push("/list/users?type=corporate")}
|
||||
label="Corporates"
|
||||
value={corporates.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsBank}
|
||||
onClick={() => router.push("/list/users?type=mastercorporate")}
|
||||
label="Master Corporates"
|
||||
value={masterCorporates.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" />
|
||||
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||
<IconCard Icon={BsPersonFillGear} label="Student Performance" value={students.length} color="purple" />
|
||||
<IconCard
|
||||
Icon={BsEnvelopePaper}
|
||||
onClick={() => router.push("/assignments")}
|
||||
label="Assignments"
|
||||
value={assignments.filter((a) => !a.archived).length}
|
||||
color="purple"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
title="Latest Students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
title="Latest Teachers"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||
title="Highest level students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={
|
||||
students
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||
)
|
||||
}
|
||||
title="Highest exam count students"
|
||||
/>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Layout from "@/components/High/Layout";
|
||||
import IconCard from "@/dashboards/IconCard";
|
||||
import {Module} from "@/interfaces";
|
||||
import {EntityWithRoles} from "@/interfaces/entity";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
import {Group, Stat, User} from "@/interfaces/user";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {dateSorter, filterBy, mapBy, serialize} from "@/utils";
|
||||
import {getAssignments, getEntitiesAssignments} from "@/utils/assignments.be";
|
||||
import {getEntitiesWithRoles} from "@/utils/entities.be";
|
||||
import {getGroups, getGroupsByEntities} from "@/utils/groups.be";
|
||||
import {checkAccess} from "@/utils/permissions";
|
||||
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
||||
import {groupByExam} from "@/utils/stats";
|
||||
import {getStatsByUsers} from "@/utils/stats.be";
|
||||
import {getEntitiesUsers, getUsers} from "@/utils/users.be";
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import {uniqBy} from "lodash";
|
||||
import moment from "moment";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import {useRouter} from "next/router";
|
||||
import {useMemo} from "react";
|
||||
import {
|
||||
BsBank,
|
||||
BsClipboard2Data,
|
||||
BsClock,
|
||||
BsEnvelopePaper,
|
||||
BsPaperclip,
|
||||
BsPencilSquare,
|
||||
BsPeople,
|
||||
BsPeopleFill,
|
||||
BsPersonFill,
|
||||
BsPersonFillGear,
|
||||
} from "react-icons/bs";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
entities: EntityWithRoles[];
|
||||
assignments: Assignment[];
|
||||
stats: Stat[];
|
||||
groups: Group[];
|
||||
}
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
const user = req.session.user as User | undefined;
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!checkAccess(user, ["admin", "developer"]))
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const users = await getUsers();
|
||||
const entities = await getEntitiesWithRoles();
|
||||
const assignments = await getAssignments();
|
||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||
const groups = await getGroups();
|
||||
|
||||
return {props: serialize({user, users, entities, assignments, stats, groups})};
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Dashboard({user, users, entities, assignments, stats, groups}: Props) {
|
||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
||||
const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]);
|
||||
const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||
const formattedStats = studentStats
|
||||
.map((s) => ({
|
||||
focus: students.find((u) => u.id === s.user)?.focus,
|
||||
score: s.score,
|
||||
module: s.module,
|
||||
}))
|
||||
.filter((f) => !!f.focus);
|
||||
const bandScores = formattedStats.map((s) => ({
|
||||
module: s.module,
|
||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||
}));
|
||||
|
||||
const levels: {[key in Module]: number} = {
|
||||
reading: 0,
|
||||
listening: 0,
|
||||
writing: 0,
|
||||
speaking: 0,
|
||||
level: 0,
|
||||
};
|
||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||
|
||||
return calculateAverageLevel(levels);
|
||||
};
|
||||
|
||||
const UserDisplay = (displayUser: User) => (
|
||||
<div className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
|
||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
||||
<div className="flex flex-col gap-1 items-start">
|
||||
<span>{displayUser.name}</span>
|
||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 />
|
||||
<Layout user={user}>
|
||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||
<IconCard
|
||||
onClick={() => router.push("/lists/users?type=student")}
|
||||
Icon={BsPersonFill}
|
||||
label="Students"
|
||||
value={students.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
onClick={() => router.push("/lists/users?type=teacher")}
|
||||
Icon={BsPencilSquare}
|
||||
label="Teachers"
|
||||
value={teachers.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsBank}
|
||||
onClick={() => router.push("/lists/users?type=corporate")}
|
||||
label="Corporates"
|
||||
value={corporates.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsBank}
|
||||
onClick={() => router.push("/lists/users?type=mastercorporate")}
|
||||
label="Master Corporates"
|
||||
value={masterCorporates.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" />
|
||||
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||
<IconCard Icon={BsPersonFillGear} label="Student Performance" value={students.length} color="purple" />
|
||||
<IconCard
|
||||
Icon={BsEnvelopePaper}
|
||||
onClick={() => router.push("/assignments")}
|
||||
label="Assignments"
|
||||
value={assignments.filter((a) => !a.archived).length}
|
||||
color="purple"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Latest students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Latest teachers</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{teachers
|
||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest level students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest exam count students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Layout from "@/components/High/Layout";
|
||||
import UserDisplayList from "@/components/UserDisplayList";
|
||||
import IconCard from "@/dashboards/IconCard";
|
||||
import { Module } from "@/interfaces";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
@@ -136,14 +137,14 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
||||
)}
|
||||
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
||||
<IconCard
|
||||
onClick={() => router.push("/lists/users?type=student")}
|
||||
onClick={() => router.push("/list/users?type=student")}
|
||||
Icon={BsPersonFill}
|
||||
label="Students"
|
||||
value={students.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
onClick={() => router.push("/lists/users?type=teacher")}
|
||||
onClick={() => router.push("/list/users?type=teacher")}
|
||||
Icon={BsPencilSquare}
|
||||
label="Teachers"
|
||||
value={teachers.length}
|
||||
@@ -178,50 +179,29 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
||||
</div>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Latest students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Latest teachers</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{teachers
|
||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest level students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest exam count students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
title="Latest Students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
title="Latest Teachers"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||
title="Highest level students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={
|
||||
students
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
title="Highest exam count students"
|
||||
/>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
201
src/pages/dashboard/developer.tsx
Normal file
201
src/pages/dashboard/developer.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Layout from "@/components/High/Layout";
|
||||
import UserDisplayList from "@/components/UserDisplayList";
|
||||
import IconCard from "@/dashboards/IconCard";
|
||||
import { Module } from "@/interfaces";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
import { Group, Stat, User } from "@/interfaces/user";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { dateSorter, filterBy, mapBy, serialize } from "@/utils";
|
||||
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||
import { groupByExam } from "@/utils/stats";
|
||||
import { getStatsByUsers } from "@/utils/stats.be";
|
||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||
import { withIronSessionSsr } from "iron-session/next";
|
||||
import { uniqBy } from "lodash";
|
||||
import moment from "moment";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
BsBank,
|
||||
BsClipboard2Data,
|
||||
BsClock,
|
||||
BsEnvelopePaper,
|
||||
BsPaperclip,
|
||||
BsPencilSquare,
|
||||
BsPeople,
|
||||
BsPeopleFill,
|
||||
BsPersonFill,
|
||||
BsPersonFillGear,
|
||||
} from "react-icons/bs";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
entities: EntityWithRoles[];
|
||||
assignments: Assignment[];
|
||||
stats: Stat[];
|
||||
groups: Group[];
|
||||
}
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
const user = req.session.user as User | undefined;
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!checkAccess(user, ["admin", "developer"]))
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const users = await getUsers();
|
||||
const entities = await getEntitiesWithRoles();
|
||||
const assignments = await getAssignments();
|
||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||
const groups = await getGroups();
|
||||
|
||||
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
||||
const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]);
|
||||
const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||
const formattedStats = studentStats
|
||||
.map((s) => ({
|
||||
focus: students.find((u) => u.id === s.user)?.focus,
|
||||
score: s.score,
|
||||
module: s.module,
|
||||
}))
|
||||
.filter((f) => !!f.focus);
|
||||
const bandScores = formattedStats.map((s) => ({
|
||||
module: s.module,
|
||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||
}));
|
||||
|
||||
const levels: { [key in Module]: number } = {
|
||||
reading: 0,
|
||||
listening: 0,
|
||||
writing: 0,
|
||||
speaking: 0,
|
||||
level: 0,
|
||||
};
|
||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||
|
||||
return calculateAverageLevel(levels);
|
||||
};
|
||||
|
||||
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 />
|
||||
<Layout user={user}>
|
||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||
<IconCard
|
||||
onClick={() => router.push("/list/users?type=student")}
|
||||
Icon={BsPersonFill}
|
||||
label="Students"
|
||||
value={students.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
onClick={() => router.push("/list/users?type=teacher")}
|
||||
Icon={BsPencilSquare}
|
||||
label="Teachers"
|
||||
value={teachers.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsBank}
|
||||
onClick={() => router.push("/list/users?type=corporate")}
|
||||
label="Corporates"
|
||||
value={corporates.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsBank}
|
||||
onClick={() => router.push("/list/users?type=mastercorporate")}
|
||||
label="Master Corporates"
|
||||
value={masterCorporates.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsPeople}
|
||||
onClick={() => router.push("/entities")}
|
||||
label="Classrooms"
|
||||
value={groups.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||
<IconCard Icon={BsPersonFillGear} label="Student Performance" value={students.length} color="purple" />
|
||||
<IconCard
|
||||
Icon={BsEnvelopePaper}
|
||||
onClick={() => router.push("/assignments")}
|
||||
label="Assignments"
|
||||
value={assignments.filter((a) => !a.archived).length}
|
||||
color="purple"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
title="Latest Students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
title="Latest Teachers"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||
title="Highest level students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={
|
||||
students
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||
)
|
||||
}
|
||||
title="Highest exam count students"
|
||||
/>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Layout from "@/components/High/Layout";
|
||||
import IconCard from "@/dashboards/IconCard";
|
||||
import {Module} from "@/interfaces";
|
||||
import {EntityWithRoles} from "@/interfaces/entity";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
import {Group, Stat, User} from "@/interfaces/user";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {dateSorter, filterBy, mapBy, serialize} from "@/utils";
|
||||
import {getAssignments, getEntitiesAssignments} from "@/utils/assignments.be";
|
||||
import {getEntitiesWithRoles} from "@/utils/entities.be";
|
||||
import {getGroups, getGroupsByEntities} from "@/utils/groups.be";
|
||||
import {checkAccess} from "@/utils/permissions";
|
||||
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
||||
import {groupByExam} from "@/utils/stats";
|
||||
import {getStatsByUsers} from "@/utils/stats.be";
|
||||
import {getEntitiesUsers, getUsers} from "@/utils/users.be";
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import {uniqBy} from "lodash";
|
||||
import moment from "moment";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import {useRouter} from "next/router";
|
||||
import {useMemo} from "react";
|
||||
import {
|
||||
BsBank,
|
||||
BsClipboard2Data,
|
||||
BsClock,
|
||||
BsEnvelopePaper,
|
||||
BsPaperclip,
|
||||
BsPencilSquare,
|
||||
BsPeople,
|
||||
BsPeopleFill,
|
||||
BsPersonFill,
|
||||
BsPersonFillGear,
|
||||
} from "react-icons/bs";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
entities: EntityWithRoles[];
|
||||
assignments: Assignment[];
|
||||
stats: Stat[];
|
||||
groups: Group[];
|
||||
}
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
const user = req.session.user as User | undefined;
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!checkAccess(user, ["admin", "developer"]))
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const users = await getUsers();
|
||||
const entities = await getEntitiesWithRoles();
|
||||
const assignments = await getAssignments();
|
||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||
const groups = await getGroups();
|
||||
|
||||
return {props: serialize({user, users, entities, assignments, stats, groups})};
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Dashboard({user, users, entities, assignments, stats, groups}: Props) {
|
||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
||||
const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]);
|
||||
const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||
const formattedStats = studentStats
|
||||
.map((s) => ({
|
||||
focus: students.find((u) => u.id === s.user)?.focus,
|
||||
score: s.score,
|
||||
module: s.module,
|
||||
}))
|
||||
.filter((f) => !!f.focus);
|
||||
const bandScores = formattedStats.map((s) => ({
|
||||
module: s.module,
|
||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||
}));
|
||||
|
||||
const levels: {[key in Module]: number} = {
|
||||
reading: 0,
|
||||
listening: 0,
|
||||
writing: 0,
|
||||
speaking: 0,
|
||||
level: 0,
|
||||
};
|
||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||
|
||||
return calculateAverageLevel(levels);
|
||||
};
|
||||
|
||||
const UserDisplay = (displayUser: User) => (
|
||||
<div className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
|
||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
||||
<div className="flex flex-col gap-1 items-start">
|
||||
<span>{displayUser.name}</span>
|
||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 />
|
||||
<Layout user={user}>
|
||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||
<IconCard
|
||||
onClick={() => router.push("/lists/users?type=student")}
|
||||
Icon={BsPersonFill}
|
||||
label="Students"
|
||||
value={students.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
onClick={() => router.push("/lists/users?type=teacher")}
|
||||
Icon={BsPencilSquare}
|
||||
label="Teachers"
|
||||
value={teachers.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsBank}
|
||||
onClick={() => router.push("/lists/users?type=corporate")}
|
||||
label="Corporates"
|
||||
value={corporates.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsBank}
|
||||
onClick={() => router.push("/lists/users?type=mastercorporate")}
|
||||
label="Master Corporates"
|
||||
value={masterCorporates.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" />
|
||||
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||
<IconCard Icon={BsPersonFillGear} label="Student Performance" value={students.length} color="purple" />
|
||||
<IconCard
|
||||
Icon={BsEnvelopePaper}
|
||||
onClick={() => router.push("/assignments")}
|
||||
label="Assignments"
|
||||
value={assignments.filter((a) => !a.archived).length}
|
||||
color="purple"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Latest students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Latest teachers</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{teachers
|
||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest level students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest exam count students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Layout from "@/components/High/Layout";
|
||||
import UserDisplayList from "@/components/UserDisplayList";
|
||||
import IconCard from "@/dashboards/IconCard";
|
||||
import { Module } from "@/interfaces";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
@@ -133,21 +134,21 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
||||
<Layout user={user}>
|
||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||
<IconCard
|
||||
onClick={() => router.push("/lists/users?type=student")}
|
||||
onClick={() => router.push("/list/users?type=student")}
|
||||
Icon={BsPersonFill}
|
||||
label="Students"
|
||||
value={students.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
onClick={() => router.push("/lists/users?type=teacher")}
|
||||
onClick={() => router.push("/list/users?type=teacher")}
|
||||
Icon={BsPencilSquare}
|
||||
label="Teachers"
|
||||
value={teachers.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
onClick={() => router.push("/lists/users?type=corporate")} Icon={BsBank} label="Corporate Accounts" value={corporates.length} color="purple" />
|
||||
onClick={() => router.push("/list/users?type=corporate")} Icon={BsBank} label="Corporate Accounts" value={corporates.length} color="purple" />
|
||||
<IconCard onClick={() => router.push("/classrooms")} Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" />
|
||||
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
@@ -169,50 +170,29 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Latest students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Latest teachers</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{teachers
|
||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest level students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest exam count students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
title="Latest Students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
title="Latest Teachers"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||
title="Highest level students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={
|
||||
students
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
title="Highest exam count students"
|
||||
/>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
170
src/pages/dashboard/teacher.tsx
Normal file
170
src/pages/dashboard/teacher.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Layout from "@/components/High/Layout";
|
||||
import UserDisplayList from "@/components/UserDisplayList";
|
||||
import IconCard from "@/dashboards/IconCard";
|
||||
import { Module } from "@/interfaces";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
import { Group, Stat, User } from "@/interfaces/user";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { dateSorter, filterBy, mapBy, serialize } from "@/utils";
|
||||
import { getEntitiesAssignments } from "@/utils/assignments.be";
|
||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { getGroupsByEntities } from "@/utils/groups.be";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||
import { groupByExam } from "@/utils/stats";
|
||||
import { getStatsByUsers } from "@/utils/stats.be";
|
||||
import { getEntitiesUsers } from "@/utils/users.be";
|
||||
import { withIronSessionSsr } from "iron-session/next";
|
||||
import { uniqBy } from "lodash";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo } from "react";
|
||||
import { BsClipboard2Data, BsEnvelopePaper, BsPaperclip, BsPeople, BsPersonFill } from "react-icons/bs";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
entities: EntityWithRoles[];
|
||||
assignments: Assignment[];
|
||||
stats: Stat[];
|
||||
groups: Group[];
|
||||
}
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
const user = req.session.user as User | undefined;
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!checkAccess(user, ["admin", "developer", "teacher"]))
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const entityIDS = mapBy(user.entities, "id") || [];
|
||||
|
||||
const users = await getEntitiesUsers(entityIDS);
|
||||
const entities = await getEntitiesWithRoles(entityIDS);
|
||||
const assignments = await getEntitiesAssignments(entityIDS);
|
||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||
const groups = await getGroupsByEntities(entityIDS);
|
||||
|
||||
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||
const router = useRouter();
|
||||
|
||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||
const formattedStats = studentStats
|
||||
.map((s) => ({
|
||||
focus: students.find((u) => u.id === s.user)?.focus,
|
||||
score: s.score,
|
||||
module: s.module,
|
||||
}))
|
||||
.filter((f) => !!f.focus);
|
||||
const bandScores = formattedStats.map((s) => ({
|
||||
module: s.module,
|
||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||
}));
|
||||
|
||||
const levels: { [key in Module]: number } = {
|
||||
reading: 0,
|
||||
listening: 0,
|
||||
writing: 0,
|
||||
speaking: 0,
|
||||
level: 0,
|
||||
};
|
||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||
|
||||
return calculateAverageLevel(levels);
|
||||
};
|
||||
|
||||
const UserDisplay = (displayUser: User) => (
|
||||
<div className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
|
||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
||||
<div className="flex flex-col gap-1 items-start">
|
||||
<span>{displayUser.name}</span>
|
||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 />
|
||||
<Layout user={user}>
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
{entities.length > 0 && (
|
||||
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
||||
</div>
|
||||
)}
|
||||
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
||||
<IconCard Icon={BsPersonFill} label="Students" value={students.length} color="purple" />
|
||||
<IconCard
|
||||
onClick={() => router.push("/classrooms")}
|
||||
Icon={BsPeople}
|
||||
label="Classrooms"
|
||||
value={groups.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||
<IconCard
|
||||
Icon={BsEnvelopePaper}
|
||||
onClick={() => router.push("/assignments")}
|
||||
label="Assignments"
|
||||
value={assignments.filter((a) => !a.archived).length}
|
||||
color="purple"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
title="Latest Students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||
title="Highest level students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={
|
||||
students
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||
)
|
||||
}
|
||||
title="Highest exam count students"
|
||||
/>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Layout from "@/components/High/Layout";
|
||||
import IconCard from "@/dashboards/IconCard";
|
||||
import {Module} from "@/interfaces";
|
||||
import {EntityWithRoles} from "@/interfaces/entity";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
import {Group, Stat, User} from "@/interfaces/user";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {dateSorter, filterBy, mapBy, serialize} from "@/utils";
|
||||
import {getEntitiesAssignments} from "@/utils/assignments.be";
|
||||
import {getEntitiesWithRoles} from "@/utils/entities.be";
|
||||
import {getGroupsByEntities} from "@/utils/groups.be";
|
||||
import {checkAccess} from "@/utils/permissions";
|
||||
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
||||
import {groupByExam} from "@/utils/stats";
|
||||
import {getStatsByUsers} from "@/utils/stats.be";
|
||||
import {getEntitiesUsers} from "@/utils/users.be";
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import {uniqBy} from "lodash";
|
||||
import Head from "next/head";
|
||||
import {useRouter} from "next/router";
|
||||
import {useMemo} from "react";
|
||||
import {BsClipboard2Data, BsEnvelopePaper, BsPaperclip, BsPeople, BsPersonFill} from "react-icons/bs";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
entities: EntityWithRoles[];
|
||||
assignments: Assignment[];
|
||||
stats: Stat[];
|
||||
groups: Group[];
|
||||
}
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
const user = req.session.user as User | undefined;
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!checkAccess(user, ["admin", "developer", "teacher"]))
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const entityIDS = mapBy(user.entities, "id") || [];
|
||||
|
||||
const users = await getEntitiesUsers(entityIDS);
|
||||
const entities = await getEntitiesWithRoles(entityIDS);
|
||||
const assignments = await getEntitiesAssignments(entityIDS);
|
||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||
const groups = await getGroupsByEntities(entityIDS);
|
||||
|
||||
return {props: serialize({user, users, entities, assignments, stats, groups})};
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Dashboard({user, users, entities, assignments, stats, groups}: Props) {
|
||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||
const router = useRouter();
|
||||
|
||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||
const formattedStats = studentStats
|
||||
.map((s) => ({
|
||||
focus: students.find((u) => u.id === s.user)?.focus,
|
||||
score: s.score,
|
||||
module: s.module,
|
||||
}))
|
||||
.filter((f) => !!f.focus);
|
||||
const bandScores = formattedStats.map((s) => ({
|
||||
module: s.module,
|
||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||
}));
|
||||
|
||||
const levels: {[key in Module]: number} = {
|
||||
reading: 0,
|
||||
listening: 0,
|
||||
writing: 0,
|
||||
speaking: 0,
|
||||
level: 0,
|
||||
};
|
||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||
|
||||
return calculateAverageLevel(levels);
|
||||
};
|
||||
|
||||
const UserDisplay = (displayUser: User) => (
|
||||
<div className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
|
||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
||||
<div className="flex flex-col gap-1 items-start">
|
||||
<span>{displayUser.name}</span>
|
||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 />
|
||||
<Layout user={user}>
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
{entities.length > 0 && (
|
||||
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
||||
</div>
|
||||
)}
|
||||
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
||||
<IconCard Icon={BsPersonFill} label="Students" value={students.length} color="purple" />
|
||||
<IconCard
|
||||
onClick={() => router.push("/classrooms")}
|
||||
Icon={BsPeople}
|
||||
label="Classrooms"
|
||||
value={groups.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||
<IconCard
|
||||
Icon={BsEnvelopePaper}
|
||||
onClick={() => router.push("/assignments")}
|
||||
label="Assignments"
|
||||
value={assignments.filter((a) => !a.archived).length}
|
||||
color="purple"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Latest students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest level students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||
<span className="p-4">Highest exam count students</span>
|
||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||
{students
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||
)
|
||||
.map((x) => (
|
||||
<UserDisplay key={x.id} {...x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,21 @@
|
||||
import Layout from "@/components/High/Layout";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import { Type, User } from "@/interfaces/user";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import useFilterStore from "@/stores/listFilterStore";
|
||||
import { serialize } from "@/utils";
|
||||
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 {BsArrowLeft, BsChevronLeft} from "react-icons/bs";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
import UserList from "../(admin)/Lists/UserList";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res, query}) => {
|
||||
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]!;
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -30,17 +25,22 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
};
|
||||
}
|
||||
|
||||
const {type} = query as {type?: Type}
|
||||
|
||||
return {
|
||||
props: {user: req.session.user, envVariables},
|
||||
props: serialize({user: req.session.user, type}),
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
export default function UsersListPage() {
|
||||
const {user} = useUser();
|
||||
interface Props {
|
||||
user: User
|
||||
type?: Type
|
||||
}
|
||||
|
||||
export default function UsersListPage({ user, type }: Props) {
|
||||
const [filters, clearFilters] = useFilterStore((state) => [state.userFilters, state.clearUserFilters]);
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -59,19 +59,19 @@ export default function UsersListPage() {
|
||||
<Layout user={user}>
|
||||
<UserList
|
||||
user={user}
|
||||
type={type}
|
||||
filters={filters.map((f) => f.filter)}
|
||||
renderHeader={(total) => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
clearFilters();
|
||||
router.back();
|
||||
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>
|
||||
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
||||
<BsChevronLeft />
|
||||
</button>
|
||||
<h2 className="font-bold text-2xl">Users ({ total })</h2>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
import Layout from "@/components/High/Layout";
|
||||
import List from "@/components/List";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import {Type, User} from "@/interfaces/user";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import useFilterStore from "@/stores/listFilterStore";
|
||||
import {mapBy, serialize} from "@/utils";
|
||||
import {getEntitiesUsers, getEntityUsers} from "@/utils/users.be";
|
||||
import {createColumnHelper} from "@tanstack/react-table";
|
||||
import clsx from "clsx";
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import Head from "next/head";
|
||||
import {useRouter} from "next/router";
|
||||
import {useEffect, useState} from "react";
|
||||
import {BsArrowLeft, BsCheck, BsChevronLeft} from "react-icons/bs";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
import UserList from "../(admin)/Lists/UserList";
|
||||
import {countries, TCountries} from "countries-list";
|
||||
import countryCodes from "country-codes-list";
|
||||
import {capitalize} from "lodash";
|
||||
import moment from "moment";
|
||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||
import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be";
|
||||
import {EntityWithRoles} from "@/interfaces/entity";
|
||||
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
|
||||
const expirationDateColor = (date: Date) => {
|
||||
const momentDate = moment(date);
|
||||
const today = moment(new Date());
|
||||
|
||||
if (today.isAfter(momentDate)) return "!text-mti-red-light font-bold line-through";
|
||||
if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light";
|
||||
if (today.add(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-light";
|
||||
if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light";
|
||||
};
|
||||
|
||||
const SEARCH_FIELDS = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]];
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => {
|
||||
const user = req.session.user as User | undefined;
|
||||
|
||||
if (!user)
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const {type} = query as {type?: Type};
|
||||
const users = await getEntitiesUsers(mapBy(user.entities, "id"));
|
||||
const entities = await getEntitiesWithRoles();
|
||||
|
||||
const filters: ((u: User) => boolean)[] = [];
|
||||
if (type) filters.push((u) => u.type === type);
|
||||
|
||||
return {
|
||||
props: serialize({user, users: filters.reduce((d, f) => d.filter(f), users), entities}),
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
entities: EntityWithRoles[];
|
||||
}
|
||||
|
||||
export default function UsersList({user, users, entities}: Props) {
|
||||
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("name", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Name</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: ({row, getValue}) => <div>{getValue()}</div>,
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>E-mail</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: ({row, getValue}) => <div>{getValue()}</div>,
|
||||
}),
|
||||
columnHelper.accessor("type", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Type</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) => USER_TYPE_LABELS[info.getValue()],
|
||||
}),
|
||||
columnHelper.accessor("studentID", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Student ID</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) => info.getValue() || "N/A",
|
||||
}),
|
||||
columnHelper.accessor("entities", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Entities</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: ({getValue}) =>
|
||||
getValue()
|
||||
.map((e) => entities.find((x) => x.id === e.id)?.label)
|
||||
.join(", "),
|
||||
}),
|
||||
columnHelper.accessor("subscriptionExpirationDate", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Expiration</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) => (
|
||||
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
||||
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("isVerified", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Verified</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) => (
|
||||
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
|
||||
<div
|
||||
className={clsx(
|
||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
||||
"transition duration-300 ease-in-out",
|
||||
info.getValue() && "!bg-mti-purple-light ",
|
||||
)}>
|
||||
<BsCheck color="white" className="w-full h-full" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
{
|
||||
header: (
|
||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
||||
Switch
|
||||
</span>
|
||||
),
|
||||
id: "actions",
|
||||
cell: () => "",
|
||||
},
|
||||
];
|
||||
|
||||
const demographicColumns = [
|
||||
columnHelper.accessor("name", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Name</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: ({row, getValue}) => <div>{getValue()}</div>,
|
||||
}),
|
||||
columnHelper.accessor("demographicInformation.country", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Country</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) =>
|
||||
info.getValue()
|
||||
? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${
|
||||
countries[info.getValue() as unknown as keyof TCountries]?.name
|
||||
} (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})`
|
||||
: "N/A",
|
||||
}),
|
||||
columnHelper.accessor("demographicInformation.phone", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Phone</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) => info.getValue() || "N/A",
|
||||
enableSorting: true,
|
||||
}),
|
||||
columnHelper.accessor(
|
||||
(x) =>
|
||||
x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment,
|
||||
{
|
||||
id: "employment",
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Employment</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
|
||||
enableSorting: true,
|
||||
},
|
||||
),
|
||||
columnHelper.accessor("lastLogin", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Last Login</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
|
||||
}),
|
||||
columnHelper.accessor("demographicInformation.gender", {
|
||||
header: (
|
||||
<button className="flex gap-2 items-center">
|
||||
<span>Gender</span>
|
||||
</button>
|
||||
) as any,
|
||||
cell: (info) => capitalize(info.getValue()) || "N/A",
|
||||
enableSorting: true,
|
||||
}),
|
||||
{
|
||||
header: (
|
||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
||||
Switch
|
||||
</span>
|
||||
),
|
||||
id: "actions",
|
||||
cell: () => "",
|
||||
},
|
||||
];
|
||||
|
||||
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 />
|
||||
|
||||
<Layout user={user} className="!gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
||||
<BsChevronLeft />
|
||||
</button>
|
||||
<h2 className="font-bold text-2xl">User List</h2>
|
||||
</div>
|
||||
<List<User> data={users} searchFields={SEARCH_FIELDS} columns={showDemographicInformation ? demographicColumns : defaultColumns} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import Head from "next/head";
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {Stat, User} from "@/interfaces/user";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {useEffect, useMemo, useRef, useState} from "react";
|
||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||
import {groupByDate} from "@/utils/stats";
|
||||
import moment from "moment";
|
||||
@@ -22,9 +22,18 @@ import RecordFilter from "@/components/Medium/RecordFilter";
|
||||
import {useRouter} from "next/router";
|
||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
import {getUsers} from "@/utils/users.be";
|
||||
import {getAssignments, getAssignmentsByAssigner} from "@/utils/assignments.be";
|
||||
import {getEntitiesUsers, getUsers} from "@/utils/users.be";
|
||||
import {getAssignments, getAssignmentsByAssigner, getEntitiesAssignments} from "@/utils/assignments.be";
|
||||
import useGradingSystem from "@/hooks/useGrading";
|
||||
import { mapBy, serialize } from "@/utils";
|
||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||
import { getGradingSystemByEntity } from "@/utils/grading.be";
|
||||
import { Grading } from "@/interfaces";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { useListSearch } from "@/hooks/useListSearch";
|
||||
import CardList from "@/components/High/CardList";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
const user = req.session.user;
|
||||
@@ -47,11 +56,16 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
};
|
||||
}
|
||||
|
||||
const users = await getUsers();
|
||||
const assignments = await getAssignments();
|
||||
const entityIDs = mapBy(user.entities, 'id')
|
||||
|
||||
const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs)
|
||||
const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id')))
|
||||
const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id')))
|
||||
const assignments = await (checkAccess(user, ["admin", "developer"]) ? getAssignments() : getEntitiesAssignments(mapBy(entities, 'id')))
|
||||
const gradingSystems = await Promise.all(entityIDs.map(getGradingSystemByEntity))
|
||||
|
||||
return {
|
||||
props: {user, users, assignments},
|
||||
props: serialize({user, users, assignments, entities, gradingSystems}),
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
@@ -61,9 +75,13 @@ interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
assignments: Assignment[];
|
||||
gradingSystems: Grading[]
|
||||
entities: EntityWithRoles[]
|
||||
}
|
||||
|
||||
export default function History({user, users, assignments}: Props) {
|
||||
const MAX_TRAINING_EXAMS = 10;
|
||||
|
||||
export default function History({user, users, assignments, entities, gradingSystems}: Props) {
|
||||
const router = useRouter();
|
||||
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
||||
state.selectedUser,
|
||||
@@ -72,8 +90,6 @@ export default function History({user, users, assignments}: Props) {
|
||||
state.setTraining,
|
||||
]);
|
||||
|
||||
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
||||
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
|
||||
const [filter, setFilter] = useState<Filter>();
|
||||
|
||||
const {data: stats, isLoading: isStatsLoading} = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
|
||||
@@ -87,30 +103,32 @@ export default function History({user, users, assignments}: Props) {
|
||||
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
|
||||
const renderPdfIcon = usePDFDownload("stats");
|
||||
|
||||
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
|
||||
const setTrainingStats = useTrainingContentStore((state) => state.setStats);
|
||||
|
||||
const groupedStats = useMemo(() => groupByDate(
|
||||
stats.filter((x) => {
|
||||
if (
|
||||
(x.module === "writing" || x.module === "speaking") &&
|
||||
!x.isDisabled &&
|
||||
!x.solutions.every((y) => Object.keys(y).includes("evaluation"))
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
}),
|
||||
), [stats])
|
||||
|
||||
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (stats && !isStatsLoading) {
|
||||
setGroupedStats(
|
||||
groupByDate(
|
||||
stats.filter((x) => {
|
||||
if (
|
||||
(x.module === "writing" || x.module === "speaking") &&
|
||||
!x.isDisabled &&
|
||||
!x.solutions.every((y) => Object.keys(y).includes("evaluation"))
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [stats, isStatsLoading]);
|
||||
|
||||
// useEffect(() => {
|
||||
// // just set this initially
|
||||
// if (!statsUserId) setStatsUserId(user.id);
|
||||
// }, []);
|
||||
const handleRouteChange = (url: string) => {
|
||||
setTraining(false);
|
||||
};
|
||||
router.events.on("routeChangeStart", handleRouteChange);
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleRouteChange);
|
||||
};
|
||||
}, [router.events, setTraining]);
|
||||
|
||||
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
|
||||
if (filter && filter !== "assignments") {
|
||||
@@ -139,39 +157,29 @@ export default function History({user, users, assignments}: Props) {
|
||||
return stats;
|
||||
};
|
||||
|
||||
const MAX_TRAINING_EXAMS = 10;
|
||||
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
|
||||
const setTrainingStats = useTrainingContentStore((state) => state.setStats);
|
||||
const handleTrainingContentSubmission = () => {
|
||||
if (groupedStats) {
|
||||
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
||||
const allStats = Object.keys(groupedStatsByDate);
|
||||
const selectedStats = selectedTrainingExams.reduce<Record<string, Stat[]>>((accumulator, moduleAndTimestamp) => {
|
||||
const timestamp = moduleAndTimestamp.split("-")[1];
|
||||
if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) {
|
||||
accumulator[timestamp] = groupedStatsByDate[timestamp];
|
||||
}
|
||||
return accumulator;
|
||||
}, {});
|
||||
setTrainingStats(Object.values(selectedStats).flat());
|
||||
router.push("/training");
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrainingContentSubmission = () => {
|
||||
if (groupedStats) {
|
||||
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
||||
const allStats = Object.keys(groupedStatsByDate);
|
||||
const selectedStats = selectedTrainingExams.reduce<Record<string, Stat[]>>((accumulator, moduleAndTimestamp) => {
|
||||
const timestamp = moduleAndTimestamp.split("-")[1];
|
||||
if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) {
|
||||
accumulator[timestamp] = groupedStatsByDate[timestamp];
|
||||
}
|
||||
return accumulator;
|
||||
}, {});
|
||||
setTrainingStats(Object.values(selectedStats).flat());
|
||||
router.push("/training");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChange = (url: string) => {
|
||||
setTraining(false);
|
||||
};
|
||||
router.events.on("routeChangeStart", handleRouteChange);
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleRouteChange);
|
||||
};
|
||||
}, [router.events, setTraining]);
|
||||
const filteredStats = useMemo(() =>
|
||||
Object.keys(filterStatsByDate(groupedStats))
|
||||
.sort((a, b) => parseInt(b) - parseInt(a)),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[groupedStats, filter])
|
||||
|
||||
const customContent = (timestamp: string) => {
|
||||
if (!groupedStats) return <></>;
|
||||
|
||||
const dateStats = groupedStats[timestamp];
|
||||
|
||||
return (
|
||||
@@ -212,7 +220,7 @@ export default function History({user, users, assignments}: Props) {
|
||||
<ToastContainer />
|
||||
{user && (
|
||||
<Layout user={user}>
|
||||
<RecordFilter user={user} filterState={{filter: filter, setFilter: setFilter}}>
|
||||
<RecordFilter user={user} users={users} entities={entities} filterState={{filter: filter, setFilter: setFilter}}>
|
||||
{training && (
|
||||
<div className="flex flex-row">
|
||||
<div className="font-semibold text-2xl mr-4">
|
||||
@@ -231,14 +239,12 @@ export default function History({user, users, assignments}: Props) {
|
||||
</div>
|
||||
)}
|
||||
</RecordFilter>
|
||||
{groupedStats && Object.keys(groupedStats).length > 0 && !isStatsLoading && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
||||
{Object.keys(filterStatsByDate(groupedStats))
|
||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
||||
.map(customContent)}
|
||||
</div>
|
||||
|
||||
|
||||
{filteredStats.length > 0 && !isStatsLoading && (
|
||||
<CardList list={filteredStats} renderCard={customContent} searchFields={[]} pageSize={30} className="lg:!grid-cols-3" />
|
||||
)}
|
||||
{groupedStats && Object.keys(groupedStats).length === 0 && !isStatsLoading && (
|
||||
{filteredStats.length === 0 && !isStatsLoading && (
|
||||
<span className="font-semibold ml-1">No record to display...</span>
|
||||
)}
|
||||
{isStatsLoading && (
|
||||
|
||||
@@ -1,156 +1,161 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Head from "next/head";
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
import { withIronSessionSsr } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import Layout from "@/components/High/Layout";
|
||||
import CodeGenerator from "./(admin)/CodeGenerator";
|
||||
import ExamLoader from "./(admin)/ExamLoader";
|
||||
import {Tab} from "@headlessui/react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import Lists from "./(admin)/Lists";
|
||||
import BatchCodeGenerator from "./(admin)/BatchCodeGenerator";
|
||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||
import ExamGenerator from "./(admin)/ExamGenerator";
|
||||
import BatchCreateUser from "./(admin)/BatchCreateUser";
|
||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import {useState} from "react";
|
||||
import { useState } from "react";
|
||||
import Modal from "@/components/Modal";
|
||||
import IconCard from "@/dashboards/IconCard";
|
||||
import {BsCode, BsCodeSquare, BsGearFill, BsPeopleFill, BsPersonFill} from "react-icons/bs";
|
||||
import { BsCode, BsCodeSquare, BsGearFill, BsPeopleFill, BsPersonFill } from "react-icons/bs";
|
||||
import UserCreator from "./(admin)/UserCreator";
|
||||
import CorporateGradingSystem from "./(admin)/CorporateGradingSystem";
|
||||
import useGradingSystem from "@/hooks/useGrading";
|
||||
import {CEFR_STEPS} from "@/resources/grading";
|
||||
import {User} from "@/interfaces/user";
|
||||
import {getUserPermissions} from "@/utils/permissions.be";
|
||||
import {Permission, PermissionType} from "@/interfaces/permissions";
|
||||
import {getUsers} from "@/utils/users.be";
|
||||
import { CEFR_STEPS } from "@/resources/grading";
|
||||
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";
|
||||
import { getEntitiesWithRoles, getEntityWithRoles } from "@/utils/entities.be";
|
||||
import { mapBy, serialize } from "@/utils";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
const user = req.session.user;
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
const user = req.session.user as User;
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"])) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"])) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const permissions = await getUserPermissions(user.id);
|
||||
const permissions = await getUserPermissions(user.id);
|
||||
const entities = await getEntitiesWithRoles(mapBy(user.entities, 'id')) || []
|
||||
|
||||
return {
|
||||
props: {user, permissions},
|
||||
};
|
||||
return {
|
||||
props: serialize({ user, permissions, entities }),
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
permissions: PermissionType[];
|
||||
user: User;
|
||||
permissions: PermissionType[];
|
||||
entities: EntityWithRoles[]
|
||||
}
|
||||
|
||||
export default function Admin({user, permissions}: Props) {
|
||||
const {gradingSystem, mutate} = useGradingSystem();
|
||||
const {users} = useUsers();
|
||||
export default function Admin({ user, entities, permissions }: Props) {
|
||||
const { gradingSystem, mutate } = useGradingSystem();
|
||||
const { users } = useUsers();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState<string>();
|
||||
const [modalOpen, setModalOpen] = useState<string>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Settings Panel | 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 />
|
||||
<Layout user={user} className="gap-6">
|
||||
<Modal isOpen={modalOpen === "batchCreateUser"} onClose={() => setModalOpen(undefined)}>
|
||||
<BatchCreateUser user={user} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||
</Modal>
|
||||
<Modal isOpen={modalOpen === "batchCreateCode"} onClose={() => setModalOpen(undefined)}>
|
||||
<BatchCodeGenerator user={user} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||
</Modal>
|
||||
<Modal isOpen={modalOpen === "createCode"} onClose={() => setModalOpen(undefined)}>
|
||||
<CodeGenerator user={user} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||
</Modal>
|
||||
<Modal isOpen={modalOpen === "createUser"} onClose={() => setModalOpen(undefined)}>
|
||||
<UserCreator user={user} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||
</Modal>
|
||||
<Modal isOpen={modalOpen === "gradingSystem"} onClose={() => setModalOpen(undefined)}>
|
||||
<CorporateGradingSystem
|
||||
user={user}
|
||||
defaultSteps={gradingSystem?.steps || CEFR_STEPS}
|
||||
mutate={(steps) => {
|
||||
mutate({user: user.id, steps});
|
||||
setModalOpen(undefined);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Settings Panel | 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 />
|
||||
<Layout user={user} className="gap-6">
|
||||
<Modal isOpen={modalOpen === "batchCreateUser"} onClose={() => setModalOpen(undefined)}>
|
||||
<BatchCreateUser user={user} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||
</Modal>
|
||||
<Modal isOpen={modalOpen === "batchCreateCode"} onClose={() => setModalOpen(undefined)}>
|
||||
<BatchCodeGenerator user={user} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||
</Modal>
|
||||
<Modal isOpen={modalOpen === "createCode"} onClose={() => setModalOpen(undefined)}>
|
||||
<CodeGenerator user={user} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||
</Modal>
|
||||
<Modal isOpen={modalOpen === "createUser"} onClose={() => setModalOpen(undefined)}>
|
||||
<UserCreator user={user} entities={entities} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||
</Modal>
|
||||
<Modal isOpen={modalOpen === "gradingSystem"} onClose={() => setModalOpen(undefined)}>
|
||||
<CorporateGradingSystem
|
||||
user={user}
|
||||
defaultSteps={gradingSystem?.steps || CEFR_STEPS}
|
||||
mutate={(steps) => {
|
||||
mutate({ user: user.id, steps });
|
||||
setModalOpen(undefined);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<section className="w-full grid grid-cols-2 -md:grid-cols-1 gap-8">
|
||||
<ExamLoader />
|
||||
{checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && (
|
||||
<div className="w-full grid grid-cols-2 gap-4">
|
||||
<IconCard
|
||||
Icon={BsCode}
|
||||
label="Generate Single Code"
|
||||
color="purple"
|
||||
className="w-full h-full"
|
||||
onClick={() => setModalOpen("createCode")}
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsCodeSquare}
|
||||
label="Generate Codes in Batch"
|
||||
color="purple"
|
||||
className="w-full h-full"
|
||||
onClick={() => setModalOpen("batchCreateCode")}
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsPersonFill}
|
||||
label="Create Single User"
|
||||
color="purple"
|
||||
className="w-full h-full"
|
||||
onClick={() => setModalOpen("createUser")}
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsPeopleFill}
|
||||
label="Create Users in Batch"
|
||||
color="purple"
|
||||
className="w-full h-full"
|
||||
onClick={() => setModalOpen("batchCreateUser")}
|
||||
/>
|
||||
{checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) && (
|
||||
<IconCard
|
||||
Icon={BsGearFill}
|
||||
label="Grading System"
|
||||
color="purple"
|
||||
className="w-full h-full col-span-2"
|
||||
onClick={() => setModalOpen("gradingSystem")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<section className="w-full">
|
||||
<Lists user={user} users={users} permissions={permissions} />
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
<section className="w-full grid grid-cols-2 -md:grid-cols-1 gap-8">
|
||||
<ExamLoader />
|
||||
{checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && (
|
||||
<div className="w-full grid grid-cols-2 gap-4">
|
||||
<IconCard
|
||||
Icon={BsCode}
|
||||
label="Generate Single Code"
|
||||
color="purple"
|
||||
className="w-full h-full"
|
||||
onClick={() => setModalOpen("createCode")}
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsCodeSquare}
|
||||
label="Generate Codes in Batch"
|
||||
color="purple"
|
||||
className="w-full h-full"
|
||||
onClick={() => setModalOpen("batchCreateCode")}
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsPersonFill}
|
||||
label="Create Single User"
|
||||
color="purple"
|
||||
className="w-full h-full"
|
||||
onClick={() => setModalOpen("createUser")}
|
||||
/>
|
||||
<IconCard
|
||||
Icon={BsPeopleFill}
|
||||
label="Create Users in Batch"
|
||||
color="purple"
|
||||
className="w-full h-full"
|
||||
onClick={() => setModalOpen("batchCreateUser")}
|
||||
/>
|
||||
{checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) && (
|
||||
<IconCard
|
||||
Icon={BsGearFill}
|
||||
label="Grading System"
|
||||
color="purple"
|
||||
className="w-full h-full col-span-2"
|
||||
onClick={() => setModalOpen("gradingSystem")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<section className="w-full">
|
||||
<Lists user={user} users={users} permissions={permissions} />
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,22 +17,28 @@ import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
||||
import {countExamModules, countFullExams, MODULE_ARRAY, sortByModule} from "@/utils/moduleUtils";
|
||||
import {Chart} from "react-chartjs-2";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import Select from "react-select";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import DatePicker from "react-datepicker";
|
||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||
import ProfileSummary from "@/components/ProfileSummary";
|
||||
import moment from "moment";
|
||||
import {Stat} from "@/interfaces/user";
|
||||
import {Group, Stat, User} from "@/interfaces/user";
|
||||
import {Divider} from "primereact/divider";
|
||||
import Badge from "@/components/Low/Badge";
|
||||
import { mapBy, serialize } from "@/utils";
|
||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||
import Select from "@/components/Low/Select";
|
||||
|
||||
ChartJS.register(LinearScale, CategoryScale, PointElement, LineElement, LineController, Legend, Tooltip);
|
||||
|
||||
const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8", "#414288"];
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
const user = req.session.user;
|
||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
const user = req.session.user as User;
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
@@ -52,13 +58,25 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
};
|
||||
}
|
||||
|
||||
const entityIDs = mapBy(user.entities, 'id')
|
||||
const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs)
|
||||
const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id')))
|
||||
const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id')))
|
||||
|
||||
return {
|
||||
props: {user: req.session.user},
|
||||
props: serialize({user, entities, users, groups}),
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Stats() {
|
||||
const [statsUserId, setStatsUserId] = useState<string>();
|
||||
interface Props {
|
||||
user: User
|
||||
users: User[]
|
||||
entities: EntityWithRoles[]
|
||||
groups: Group[]
|
||||
}
|
||||
|
||||
export default function Stats({ user, entities, users, groups }: Props) {
|
||||
const [statsUserId, setStatsUserId] = useState<string>(user.id);
|
||||
const [startDate, setStartDate] = useState<Date | null>(moment(new Date()).subtract(1, "weeks").toDate());
|
||||
const [endDate, setEndDate] = useState<Date | null>(new Date());
|
||||
const [initialStatDate, setInitialStatDate] = useState<Date>();
|
||||
@@ -69,15 +87,8 @@ export default function Stats() {
|
||||
const [dailyScoreDate, setDailyScoreDate] = useState<Date | null>(new Date());
|
||||
const [intervalDates, setIntervalDates] = useState<Date[]>([]);
|
||||
|
||||
const {user} = useUser({redirectTo: "/login"});
|
||||
const {users} = useUsers();
|
||||
const {groups} = useGroups({admin: user?.id});
|
||||
const {data: stats} = useFilterRecordsByUser<Stat[]>(statsUserId, !statsUserId);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) setStatsUserId(user.id);
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
setInitialStatDate(
|
||||
stats
|
||||
@@ -190,16 +201,7 @@ export default function Stats() {
|
||||
className="w-full"
|
||||
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
color: state.isFocused ? "black" : styles.color,
|
||||
}),
|
||||
}}
|
||||
onChange={(value) => setStatsUserId(value?.value || user.id)}
|
||||
/>
|
||||
)}
|
||||
{["corporate", "teacher", "mastercorporate"].includes(user.type) && groups.length > 0 && (
|
||||
@@ -209,16 +211,7 @@ export default function Stats() {
|
||||
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
||||
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
color: state.isFocused ? "black" : styles.color,
|
||||
}),
|
||||
}}
|
||||
onChange={(value) => setStatsUserId(value?.value || user.id)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -20,9 +20,15 @@ import TrainingScore from "@/training/TrainingScore";
|
||||
import ModuleBadge from "@/components/ModuleBadge";
|
||||
import RecordFilter from "@/components/Medium/RecordFilter";
|
||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||
import { mapBy, serialize } from "@/utils";
|
||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { getAssignmentsByAssignee } from "@/utils/assignments.be";
|
||||
import { getEntitiesUsers } from "@/utils/users.be";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
const user = req.session.user;
|
||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||
const user = req.session.user as User;
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
@@ -42,12 +48,16 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
};
|
||||
}
|
||||
|
||||
const entityIDs = mapBy(user.entities, 'id')
|
||||
const entities = await getEntitiesWithRoles(entityIDs)
|
||||
const users = await getEntitiesUsers(entityIDs)
|
||||
|
||||
return {
|
||||
props: {user: req.session.user},
|
||||
props: serialize({user, users, entities}),
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
const Training: React.FC<{user: User}> = ({user}) => {
|
||||
const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[] }> = ({user, entities, users}) => {
|
||||
const [recordUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setTraining]);
|
||||
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
||||
|
||||
@@ -193,7 +203,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<RecordFilter user={user} filterState={{filter: filter, setFilter: setFilter}} assignments={false}>
|
||||
<RecordFilter users={users} entities={entities} user={user} filterState={{filter: filter, setFilter: setFilter}} assignments={false}>
|
||||
{user.type === "student" && (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
|
||||
Reference in New Issue
Block a user