Solved an issue where, for developers, because of the amount of permissions, the cookie was too big, so I separated the permissions logic into a hook
This commit is contained in:
@@ -1,255 +1,179 @@
|
|||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {
|
import {Ticket, TicketStatus, TicketStatusLabel, TicketType, TicketTypeLabel} from "@/interfaces/ticket";
|
||||||
Ticket,
|
import {User} from "@/interfaces/user";
|
||||||
TicketStatus,
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
TicketStatusLabel,
|
|
||||||
TicketType,
|
|
||||||
TicketTypeLabel,
|
|
||||||
} from "@/interfaces/ticket";
|
|
||||||
import { User } from "@/interfaces/user";
|
|
||||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useState } from "react";
|
import {useState} from "react";
|
||||||
import { toast } from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import Input from "../Low/Input";
|
import Input from "../Low/Input";
|
||||||
import Select from "../Low/Select";
|
import Select from "../Low/Select";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import {checkAccess} from "@/utils/permissions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
ticket: Ticket;
|
ticket: Ticket;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TicketDisplay({ user, ticket, onClose }: Props) {
|
export default function TicketDisplay({user, ticket, onClose}: Props) {
|
||||||
const [subject] = useState(ticket.subject);
|
const [subject] = useState(ticket.subject);
|
||||||
const [type, setType] = useState<TicketType>(ticket.type);
|
const [type, setType] = useState<TicketType>(ticket.type);
|
||||||
const [description] = useState(ticket.description);
|
const [description] = useState(ticket.description);
|
||||||
const [reporter] = useState(ticket.reporter);
|
const [reporter] = useState(ticket.reporter);
|
||||||
const [reportedFrom] = useState(ticket.reportedFrom);
|
const [reportedFrom] = useState(ticket.reportedFrom);
|
||||||
const [status, setStatus] = useState(ticket.status);
|
const [status, setStatus] = useState(ticket.status);
|
||||||
const [assignedTo, setAssignedTo] = useState<string | null>(
|
const [assignedTo, setAssignedTo] = useState<string | null>(ticket.assignedTo || null);
|
||||||
ticket.assignedTo || null,
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const { users } = useUsers();
|
const {users} = useUsers();
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (!type)
|
if (!type) return toast.error("Please choose a type!", {toastId: "missing-type"});
|
||||||
return toast.error("Please choose a type!", { toastId: "missing-type" });
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.patch(`/api/tickets/${ticket.id}`, {
|
.patch(`/api/tickets/${ticket.id}`, {
|
||||||
subject,
|
subject,
|
||||||
type,
|
type,
|
||||||
description,
|
description,
|
||||||
reporter,
|
reporter,
|
||||||
reportedFrom,
|
reportedFrom,
|
||||||
status,
|
status,
|
||||||
assignedTo,
|
assignedTo,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The ticket has been updated!`, { toastId: "submitted" });
|
toast.success(`The ticket has been updated!`, {toastId: "submitted"});
|
||||||
onClose();
|
onClose();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error("Something went wrong, please try again later!", {
|
toast.error("Something went wrong, please try again later!", {
|
||||||
toastId: "error",
|
toastId: "error",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const del = () => {
|
const del = () => {
|
||||||
if (!confirm("Are you sure you want to delete this ticket?")) return;
|
if (!confirm("Are you sure you want to delete this ticket?")) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.delete(`/api/tickets/${ticket.id}`)
|
.delete(`/api/tickets/${ticket.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The ticket has been deleted!`, { toastId: "submitted" });
|
toast.success(`The ticket has been deleted!`, {toastId: "submitted"});
|
||||||
onClose();
|
onClose();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error("Something went wrong, please try again later!", {
|
toast.error("Something went wrong, please try again later!", {
|
||||||
toastId: "error",
|
toastId: "error",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="flex flex-col gap-4 pt-8">
|
<form className="flex flex-col gap-4 pt-8">
|
||||||
<Input
|
<Input label="Subject" type="text" name="subject" placeholder="Subject..." value={subject} onChange={(e) => null} disabled />
|
||||||
label="Subject"
|
|
||||||
type="text"
|
|
||||||
name="subject"
|
|
||||||
placeholder="Subject..."
|
|
||||||
value={subject}
|
|
||||||
onChange={(e) => null}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="-md:flex-col flex w-full items-center gap-4">
|
<div className="-md:flex-col flex w-full items-center gap-4">
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
<label className="text-mti-gray-dim text-base font-normal">Status</label>
|
||||||
Status
|
<Select
|
||||||
</label>
|
options={Object.keys(TicketStatusLabel).map((x) => ({
|
||||||
<Select
|
value: x,
|
||||||
options={Object.keys(TicketStatusLabel).map((x) => ({
|
label: TicketStatusLabel[x as keyof typeof TicketStatusLabel],
|
||||||
value: x,
|
}))}
|
||||||
label: TicketStatusLabel[x as keyof typeof TicketStatusLabel],
|
value={{value: status, label: TicketStatusLabel[status]}}
|
||||||
}))}
|
onChange={(value) => setStatus((value?.value as TicketStatus) ?? undefined)}
|
||||||
value={{ value: status, label: TicketStatusLabel[status] }}
|
placeholder="Status..."
|
||||||
onChange={(value) =>
|
/>
|
||||||
setStatus((value?.value as TicketStatus) ?? undefined)
|
</div>
|
||||||
}
|
<div className="flex w-full flex-col gap-3">
|
||||||
placeholder="Status..."
|
<label className="text-mti-gray-dim text-base font-normal">Type</label>
|
||||||
/>
|
<Select
|
||||||
</div>
|
options={Object.keys(TicketTypeLabel).map((x) => ({
|
||||||
<div className="flex w-full flex-col gap-3">
|
value: x,
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
label: TicketTypeLabel[x as keyof typeof TicketTypeLabel],
|
||||||
Type
|
}))}
|
||||||
</label>
|
value={{value: type, label: TicketTypeLabel[type]}}
|
||||||
<Select
|
onChange={(value) => setType(value!.value as TicketType)}
|
||||||
options={Object.keys(TicketTypeLabel).map((x) => ({
|
placeholder="Type..."
|
||||||
value: x,
|
/>
|
||||||
label: TicketTypeLabel[x as keyof typeof TicketTypeLabel],
|
</div>
|
||||||
}))}
|
</div>
|
||||||
value={{ value: type, label: TicketTypeLabel[type] }}
|
|
||||||
onChange={(value) => setType(value!.value as TicketType)}
|
|
||||||
placeholder="Type..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
<label className="text-mti-gray-dim text-base font-normal">Assignee</label>
|
||||||
Assignee
|
<Select
|
||||||
</label>
|
options={[
|
||||||
<Select
|
{value: "me", label: "Assign to me"},
|
||||||
options={[
|
...users
|
||||||
{ value: "me", label: "Assign to me" },
|
.filter((x) => checkAccess(x, ["admin", "developer", "agent"]))
|
||||||
...users
|
.map((u) => ({
|
||||||
.filter((x) => checkAccess(x, ["admin", "developer", "agent"]))
|
value: u.id,
|
||||||
.map((u) => ({
|
label: `${u.name} - ${u.email}`,
|
||||||
value: u.id,
|
})),
|
||||||
label: `${u.name} - ${u.email}`,
|
]}
|
||||||
})),
|
disabled={checkAccess(user, ["agent"])}
|
||||||
]}
|
value={
|
||||||
disabled={checkAccess(user, ["agent"])}
|
assignedTo
|
||||||
value={
|
? {
|
||||||
assignedTo
|
value: assignedTo,
|
||||||
? {
|
label: `${users.find((u) => u.id === assignedTo)?.name} - ${users.find((u) => u.id === assignedTo)?.email}`,
|
||||||
value: assignedTo,
|
}
|
||||||
label: `${users.find((u) => u.id === assignedTo)?.name} - ${users.find((u) => u.id === assignedTo)?.email}`,
|
: null
|
||||||
}
|
}
|
||||||
: null
|
onChange={(value) => (value ? setAssignedTo(value.value === "me" ? user.id : value.value) : setAssignedTo(null))}
|
||||||
}
|
placeholder="Assignee..."
|
||||||
onChange={(value) =>
|
isClearable
|
||||||
value
|
/>
|
||||||
? setAssignedTo(value.value === "me" ? user.id : value.value)
|
</div>
|
||||||
: setAssignedTo(null)
|
|
||||||
}
|
|
||||||
placeholder="Assignee..."
|
|
||||||
isClearable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="-md:flex-col flex w-full items-center gap-4">
|
<div className="-md:flex-col flex w-full items-center gap-4">
|
||||||
<Input
|
<Input label="Reported From" type="text" name="reportedFrom" onChange={() => null} value={reportedFrom} disabled />
|
||||||
label="Reported From"
|
<Input label="Date" type="text" name="date" onChange={() => null} value={moment(ticket.date).format("DD/MM/YYYY - HH:mm")} disabled />
|
||||||
type="text"
|
</div>
|
||||||
name="reportedFrom"
|
|
||||||
onChange={() => null}
|
|
||||||
value={reportedFrom}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Date"
|
|
||||||
type="text"
|
|
||||||
name="date"
|
|
||||||
onChange={() => null}
|
|
||||||
value={moment(ticket.date).format("DD/MM/YYYY - HH:mm")}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="-md:flex-col flex w-full items-center gap-4">
|
<div className="-md:flex-col flex w-full items-center gap-4">
|
||||||
<Input
|
<Input label="Reporter's Name" type="text" name="reporter" onChange={() => null} value={reporter.name} disabled />
|
||||||
label="Reporter's Name"
|
<Input label="Reporter's E-mail" type="text" name="reporter" onChange={() => null} value={reporter.email} disabled />
|
||||||
type="text"
|
<Input
|
||||||
name="reporter"
|
label="Reporter's Type"
|
||||||
onChange={() => null}
|
type="text"
|
||||||
value={reporter.name}
|
name="reporterType"
|
||||||
disabled
|
onChange={() => null}
|
||||||
/>
|
value={USER_TYPE_LABELS[reporter.type]}
|
||||||
<Input
|
disabled
|
||||||
label="Reporter's E-mail"
|
/>
|
||||||
type="text"
|
</div>
|
||||||
name="reporter"
|
|
||||||
onChange={() => null}
|
|
||||||
value={reporter.email}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Reporter's Type"
|
|
||||||
type="text"
|
|
||||||
name="reporterType"
|
|
||||||
onChange={() => null}
|
|
||||||
value={USER_TYPE_LABELS[reporter.type]}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className="input border-mti-gray-platinum h-full min-h-[300px] w-full cursor-text rounded-3xl border bg-white px-7 py-8"
|
className="input border-mti-gray-platinum h-full min-h-[300px] w-full cursor-text rounded-3xl border bg-white px-7 py-8"
|
||||||
placeholder="Write your ticket's description here..."
|
placeholder="Write your ticket's description here..."
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
value={description}
|
value={description}
|
||||||
spellCheck
|
spellCheck
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="-md:flex-col-reverse mt-2 flex w-full items-center justify-between gap-4">
|
<div className="-md:flex-col-reverse mt-2 flex w-full items-center justify-between gap-4">
|
||||||
<Button
|
<Button type="button" color="red" className="w-full md:max-w-[200px]" variant="outline" onClick={del} isLoading={isLoading}>
|
||||||
type="button"
|
Delete
|
||||||
color="red"
|
</Button>
|
||||||
className="w-full md:max-w-[200px]"
|
|
||||||
variant="outline"
|
|
||||||
onClick={del}
|
|
||||||
isLoading={isLoading}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="-md:flex-col-reverse flex w-full items-center justify-end gap-4">
|
<div className="-md:flex-col-reverse flex w-full items-center justify-end gap-4">
|
||||||
<Button
|
<Button type="button" color="red" className="w-full md:max-w-[200px]" variant="outline" onClick={onClose} isLoading={isLoading}>
|
||||||
type="button"
|
Cancel
|
||||||
color="red"
|
</Button>
|
||||||
className="w-full md:max-w-[200px]"
|
<Button type="button" className="w-full md:max-w-[200px]" isLoading={isLoading} onClick={submit}>
|
||||||
variant="outline"
|
Update
|
||||||
onClick={onClose}
|
</Button>
|
||||||
isLoading={isLoading}
|
</div>
|
||||||
>
|
</div>
|
||||||
Cancel
|
</form>
|
||||||
</Button>
|
);
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="w-full md:max-w-[200px]"
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClick={submit}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
BsClipboardData,
|
BsClipboardData,
|
||||||
BsFileLock,
|
BsFileLock,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import { CiDumbbell } from "react-icons/ci";
|
import {CiDumbbell} from "react-icons/ci";
|
||||||
import {RiLogoutBoxFill} from "react-icons/ri";
|
import {RiLogoutBoxFill} from "react-icons/ri";
|
||||||
import {SlPencil} from "react-icons/sl";
|
import {SlPencil} from "react-icons/sl";
|
||||||
import {FaAward} from "react-icons/fa";
|
import {FaAward} from "react-icons/fa";
|
||||||
@@ -28,6 +28,7 @@ import usePreferencesStore from "@/stores/preferencesStore";
|
|||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import useTicketsListener from "@/hooks/useTicketsListener";
|
import useTicketsListener from "@/hooks/useTicketsListener";
|
||||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
interface Props {
|
interface Props {
|
||||||
path: string;
|
path: string;
|
||||||
navDisabled?: boolean;
|
navDisabled?: boolean;
|
||||||
@@ -80,6 +81,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]);
|
const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]);
|
||||||
|
|
||||||
const {totalAssignedTickets} = useTicketsListener(user.id);
|
const {totalAssignedTickets} = useTicketsListener(user.id);
|
||||||
|
const {permissions} = usePermissions(user.id);
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
axios.post("/api/logout").finally(() => {
|
axios.post("/api/logout").finally(() => {
|
||||||
@@ -98,22 +100,22 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
)}>
|
)}>
|
||||||
<div className="-xl:hidden flex-col gap-3 xl:flex">
|
<div className="-xl:hidden flex-col gap-3 xl:flex">
|
||||||
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={isMinimized} />
|
||||||
{checkAccess(user, ["student", "teacher", "developer"], "viewExams") && (
|
{checkAccess(user, ["student", "teacher", "developer"], permissions, "viewExams") && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized={isMinimized} />
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["student", "teacher", "developer"], "viewExercises") && (
|
{checkAccess(user, ["student", "teacher", "developer"], permissions, "viewExercises") && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={isMinimized} />
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), "viewStats") && (
|
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
|
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
|
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
|
||||||
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized={isMinimized} />
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["admin", "developer", "agent", "corporate", "mastercorporate"], "viewPaymentRecords") && (
|
{checkAccess(user, ["admin", "developer", "agent", "corporate", "mastercorporate"], permissions, "viewPaymentRecords") && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsCurrencyDollar}
|
Icon={BsCurrencyDollar}
|
||||||
@@ -133,7 +135,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
isMinimized={isMinimized}
|
isMinimized={isMinimized}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["admin", "developer", "agent"], "viewTickets") && (
|
{checkAccess(user, ["admin", "developer", "agent"], permissions, "viewTickets") && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsClipboardData}
|
Icon={BsClipboardData}
|
||||||
@@ -169,13 +171,13 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={true} />
|
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={true} />
|
||||||
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized={true} />
|
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized={true} />
|
||||||
<Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={true} />
|
<Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={true} />
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), "viewStats") && (
|
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={true} />
|
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={true} />
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
|
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={true} />
|
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={true} />
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
|
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
|
||||||
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized={true} />
|
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized={true} />
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["student"])) && (
|
{checkAccess(user, getTypesOfUser(["student"])) && (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,480 +2,397 @@
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
|
import {CorporateUser, Group, Stat, User} from "@/interfaces/user";
|
||||||
import UserList from "@/pages/(admin)/Lists/UserList";
|
import UserList from "@/pages/(admin)/Lists/UserList";
|
||||||
import { dateSorter } from "@/utils";
|
import {dateSorter} from "@/utils";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {
|
import {
|
||||||
BsArrowLeft,
|
BsArrowLeft,
|
||||||
BsArrowRepeat,
|
BsArrowRepeat,
|
||||||
BsClipboard2Data,
|
BsClipboard2Data,
|
||||||
BsClipboard2DataFill,
|
BsClipboard2DataFill,
|
||||||
BsClipboard2Heart,
|
BsClipboard2Heart,
|
||||||
BsClipboard2X,
|
BsClipboard2X,
|
||||||
BsClipboardPulse,
|
BsClipboardPulse,
|
||||||
BsClock,
|
BsClock,
|
||||||
BsEnvelopePaper,
|
BsEnvelopePaper,
|
||||||
BsGlobeCentralSouthAsia,
|
BsGlobeCentralSouthAsia,
|
||||||
BsPaperclip,
|
BsPaperclip,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
BsPerson,
|
BsPerson,
|
||||||
BsPersonAdd,
|
BsPersonAdd,
|
||||||
BsPersonFill,
|
BsPersonFill,
|
||||||
BsPersonFillGear,
|
BsPersonFillGear,
|
||||||
BsPersonGear,
|
BsPersonGear,
|
||||||
BsPlus,
|
BsPlus,
|
||||||
BsRepeat,
|
BsRepeat,
|
||||||
BsRepeat1,
|
BsRepeat1,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
||||||
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||||
import { Module } from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import { groupByExam } from "@/utils/stats";
|
import {groupByExam} from "@/utils/stats";
|
||||||
import IconCard from "./IconCard";
|
import IconCard from "./IconCard";
|
||||||
import GroupList from "@/pages/(admin)/Lists/GroupList";
|
import GroupList from "@/pages/(admin)/Lists/GroupList";
|
||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import {Assignment} from "@/interfaces/results";
|
||||||
import AssignmentCard from "./AssignmentCard";
|
import AssignmentCard from "./AssignmentCard";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import AssignmentCreator from "./AssignmentCreator";
|
import AssignmentCreator from "./AssignmentCreator";
|
||||||
import AssignmentView from "./AssignmentView";
|
import AssignmentView from "./AssignmentView";
|
||||||
import { getUserCorporate } from "@/utils/groups";
|
import {getUserCorporate} from "@/utils/groups";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import {checkAccess} from "@/utils/permissions";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeacherDashboard({ user }: Props) {
|
export default function TeacherDashboard({user}: Props) {
|
||||||
const [page, setPage] = useState("");
|
const [page, setPage] = useState("");
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
||||||
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
const [corporateUserToShow, setCorporateUserToShow] =
|
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
|
||||||
useState<CorporateUser>();
|
|
||||||
|
|
||||||
const { stats } = useStats();
|
const {stats} = useStats();
|
||||||
const { users, reload } = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const { groups } = useGroups(user.id);
|
const {groups} = useGroups(user.id);
|
||||||
const {
|
const {permissions} = usePermissions(user.id);
|
||||||
assignments,
|
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
|
||||||
isLoading: isAssignmentsLoading,
|
|
||||||
reload: reloadAssignments,
|
|
||||||
} = useAssignments({ assigner: user.id });
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
}, [selectedUser, page]);
|
}, [selectedUser, page]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserCorporate(user.id).then(setCorporateUserToShow);
|
getUserCorporate(user.id).then(setCorporateUserToShow);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const studentFilter = (user: User) =>
|
const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id);
|
||||||
user.type === "student" &&
|
|
||||||
groups.flatMap((g) => g.participants).includes(user.id);
|
|
||||||
|
|
||||||
const getStatsByStudent = (user: User) =>
|
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
|
||||||
stats.filter((s) => s.user === user.id);
|
|
||||||
|
|
||||||
const UserDisplay = (displayUser: User) => (
|
const UserDisplay = (displayUser: User) => (
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedUser(displayUser)}
|
onClick={() => setSelectedUser(displayUser)}
|
||||||
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
|
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" />
|
||||||
<img
|
<div className="flex flex-col gap-1 items-start">
|
||||||
src={displayUser.profilePicture}
|
<span>{displayUser.name}</span>
|
||||||
alt={displayUser.name}
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
className="rounded-full w-10 h-10"
|
</div>
|
||||||
/>
|
</div>
|
||||||
<div className="flex flex-col gap-1 items-start">
|
);
|
||||||
<span>{displayUser.name}</span>
|
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const StudentsList = () => {
|
const StudentsList = () => {
|
||||||
const filter = (x: User) =>
|
const filter = (x: User) =>
|
||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
(!!selectedUser
|
(!!selectedUser
|
||||||
? groups
|
? groups
|
||||||
.filter((g) => g.admin === selectedUser.id)
|
.filter((g) => g.admin === selectedUser.id)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id) || false
|
.includes(x.id) || false
|
||||||
: groups.flatMap((g) => g.participants).includes(x.id));
|
: groups.flatMap((g) => g.participants).includes(x.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
user={user}
|
user={user}
|
||||||
filters={[filter]}
|
filters={[filter]}
|
||||||
renderHeader={(total) => (
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
onClick={() => setPage("")}
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
|
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" />
|
||||||
<BsArrowLeft className="text-xl" />
|
<span>Back</span>
|
||||||
<span>Back</span>
|
</div>
|
||||||
</div>
|
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
||||||
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const GroupsList = () => {
|
const GroupsList = () => {
|
||||||
const filter = (x: Group) =>
|
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id);
|
||||||
x.admin === user.id || x.participants.includes(user.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
onClick={() => setPage("")}
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
|
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" />
|
||||||
<BsArrowLeft className="text-xl" />
|
<span>Back</span>
|
||||||
<span>Back</span>
|
</div>
|
||||||
</div>
|
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
|
||||||
<h2 className="text-2xl font-semibold">
|
</div>
|
||||||
Groups ({groups.filter(filter).length})
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GroupList user={user} />
|
<GroupList user={user} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
const formattedStats = studentStats
|
const formattedStats = studentStats
|
||||||
.map((s) => ({
|
.map((s) => ({
|
||||||
focus: users.find((u) => u.id === s.user)?.focus,
|
focus: users.find((u) => u.id === s.user)?.focus,
|
||||||
score: s.score,
|
score: s.score,
|
||||||
module: s.module,
|
module: s.module,
|
||||||
}))
|
}))
|
||||||
.filter((f) => !!f.focus);
|
.filter((f) => !!f.focus);
|
||||||
const bandScores = formattedStats.map((s) => ({
|
const bandScores = formattedStats.map((s) => ({
|
||||||
module: s.module,
|
module: s.module,
|
||||||
level: calculateBandScore(
|
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||||
s.score.correct,
|
}));
|
||||||
s.score.total,
|
|
||||||
s.module,
|
|
||||||
s.focus!
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const levels: { [key in Module]: number } = {
|
const levels: {[key in Module]: number} = {
|
||||||
reading: 0,
|
reading: 0,
|
||||||
listening: 0,
|
listening: 0,
|
||||||
writing: 0,
|
writing: 0,
|
||||||
speaking: 0,
|
speaking: 0,
|
||||||
level: 0,
|
level: 0,
|
||||||
};
|
};
|
||||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||||
|
|
||||||
return calculateAverageLevel(levels);
|
return calculateAverageLevel(levels);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AssignmentsPage = () => {
|
const AssignmentsPage = () => {
|
||||||
const activeFilter = (a: Assignment) =>
|
const activeFilter = (a: Assignment) =>
|
||||||
moment(a.endDate).isAfter(moment()) &&
|
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
||||||
moment(a.startDate).isBefore(moment()) &&
|
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
|
||||||
a.assignees.length > a.results.length;
|
const archivedFilter = (a: Assignment) => a.archived;
|
||||||
const pastFilter = (a: Assignment) =>
|
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
||||||
(moment(a.endDate).isBefore(moment()) ||
|
|
||||||
a.assignees.length === a.results.length) &&
|
|
||||||
!a.archived;
|
|
||||||
const archivedFilter = (a: Assignment) => a.archived;
|
|
||||||
const futureFilter = (a: Assignment) =>
|
|
||||||
moment(a.startDate).isAfter(moment());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AssignmentView
|
<AssignmentView
|
||||||
isOpen={!!selectedAssignment && !isCreatingAssignment}
|
isOpen={!!selectedAssignment && !isCreatingAssignment}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setSelectedAssignment(undefined);
|
setSelectedAssignment(undefined);
|
||||||
setIsCreatingAssignment(false);
|
setIsCreatingAssignment(false);
|
||||||
reloadAssignments();
|
reloadAssignments();
|
||||||
}}
|
}}
|
||||||
assignment={selectedAssignment}
|
assignment={selectedAssignment}
|
||||||
/>
|
/>
|
||||||
<AssignmentCreator
|
<AssignmentCreator
|
||||||
assignment={selectedAssignment}
|
assignment={selectedAssignment}
|
||||||
groups={groups.filter(
|
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
|
||||||
(x) => x.admin === user.id || x.participants.includes(user.id)
|
users={users.filter(
|
||||||
)}
|
(x) =>
|
||||||
users={users.filter(
|
x.type === "student" &&
|
||||||
(x) =>
|
(!!selectedUser
|
||||||
x.type === "student" &&
|
? groups
|
||||||
(!!selectedUser
|
.filter((g) => g.admin === selectedUser.id)
|
||||||
? groups
|
.flatMap((g) => g.participants)
|
||||||
.filter((g) => g.admin === selectedUser.id)
|
.includes(x.id) || false
|
||||||
.flatMap((g) => g.participants)
|
: groups.flatMap((g) => g.participants).includes(x.id)),
|
||||||
.includes(x.id) || false
|
)}
|
||||||
: groups.flatMap((g) => g.participants).includes(x.id))
|
assigner={user.id}
|
||||||
)}
|
isCreating={isCreatingAssignment}
|
||||||
assigner={user.id}
|
cancelCreation={() => {
|
||||||
isCreating={isCreatingAssignment}
|
setIsCreatingAssignment(false);
|
||||||
cancelCreation={() => {
|
setSelectedAssignment(undefined);
|
||||||
setIsCreatingAssignment(false);
|
reloadAssignments();
|
||||||
setSelectedAssignment(undefined);
|
}}
|
||||||
reloadAssignments();
|
/>
|
||||||
}}
|
<div className="w-full flex justify-between items-center">
|
||||||
/>
|
<div
|
||||||
<div className="w-full flex justify-between items-center">
|
onClick={() => setPage("")}
|
||||||
<div
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
onClick={() => setPage("")}
|
<BsArrowLeft className="text-xl" />
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
|
<span>Back</span>
|
||||||
>
|
</div>
|
||||||
<BsArrowLeft className="text-xl" />
|
<div
|
||||||
<span>Back</span>
|
onClick={reloadAssignments}
|
||||||
</div>
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
<div
|
<span>Reload</span>
|
||||||
onClick={reloadAssignments}
|
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
|
</div>
|
||||||
>
|
</div>
|
||||||
<span>Reload</span>
|
<section className="flex flex-col gap-4">
|
||||||
<BsArrowRepeat
|
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
||||||
className={clsx(
|
<div className="flex flex-wrap gap-2">
|
||||||
"text-xl",
|
{assignments.filter(activeFilter).map((a) => (
|
||||||
isAssignmentsLoading && "animate-spin"
|
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
||||||
)}
|
))}
|
||||||
/>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
<section className="flex flex-col gap-4">
|
||||||
<section className="flex flex-col gap-4">
|
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
|
||||||
<h2 className="text-2xl font-semibold">
|
<div className="flex flex-wrap gap-2">
|
||||||
Active Assignments ({assignments.filter(activeFilter).length})
|
<div
|
||||||
</h2>
|
onClick={() => setIsCreatingAssignment(true)}
|
||||||
<div className="flex flex-wrap gap-2">
|
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300">
|
||||||
{assignments.filter(activeFilter).map((a) => (
|
<BsPlus className="text-6xl" />
|
||||||
<AssignmentCard
|
<span className="text-lg">New Assignment</span>
|
||||||
{...a}
|
</div>
|
||||||
onClick={() => setSelectedAssignment(a)}
|
{assignments.filter(futureFilter).map((a) => (
|
||||||
key={a.id}
|
<AssignmentCard
|
||||||
/>
|
{...a}
|
||||||
))}
|
onClick={() => {
|
||||||
</div>
|
setSelectedAssignment(a);
|
||||||
</section>
|
setIsCreatingAssignment(true);
|
||||||
<section className="flex flex-col gap-4">
|
}}
|
||||||
<h2 className="text-2xl font-semibold">
|
key={a.id}
|
||||||
Planned Assignments ({assignments.filter(futureFilter).length})
|
/>
|
||||||
</h2>
|
))}
|
||||||
<div className="flex flex-wrap gap-2">
|
</div>
|
||||||
<div
|
</section>
|
||||||
onClick={() => setIsCreatingAssignment(true)}
|
<section className="flex flex-col gap-4">
|
||||||
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"
|
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
||||||
>
|
<div className="flex flex-wrap gap-2">
|
||||||
<BsPlus className="text-6xl" />
|
{assignments.filter(pastFilter).map((a) => (
|
||||||
<span className="text-lg">New Assignment</span>
|
<AssignmentCard
|
||||||
</div>
|
{...a}
|
||||||
{assignments.filter(futureFilter).map((a) => (
|
onClick={() => setSelectedAssignment(a)}
|
||||||
<AssignmentCard
|
key={a.id}
|
||||||
{...a}
|
allowDownload
|
||||||
onClick={() => {
|
reload={reloadAssignments}
|
||||||
setSelectedAssignment(a);
|
allowArchive
|
||||||
setIsCreatingAssignment(true);
|
/>
|
||||||
}}
|
))}
|
||||||
key={a.id}
|
</div>
|
||||||
/>
|
</section>
|
||||||
))}
|
<section className="flex flex-col gap-4">
|
||||||
</div>
|
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedFilter).length})</h2>
|
||||||
</section>
|
<div className="flex flex-wrap gap-2">
|
||||||
<section className="flex flex-col gap-4">
|
{assignments.filter(archivedFilter).map((a) => (
|
||||||
<h2 className="text-2xl font-semibold">
|
<AssignmentCard
|
||||||
Past Assignments ({assignments.filter(pastFilter).length})
|
{...a}
|
||||||
</h2>
|
onClick={() => setSelectedAssignment(a)}
|
||||||
<div className="flex flex-wrap gap-2">
|
key={a.id}
|
||||||
{assignments.filter(pastFilter).map((a) => (
|
allowDownload
|
||||||
<AssignmentCard
|
reload={reloadAssignments}
|
||||||
{...a}
|
allowUnarchive
|
||||||
onClick={() => setSelectedAssignment(a)}
|
/>
|
||||||
key={a.id}
|
))}
|
||||||
allowDownload
|
</div>
|
||||||
reload={reloadAssignments}
|
</section>
|
||||||
allowArchive
|
</>
|
||||||
/>
|
);
|
||||||
))}
|
};
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="flex flex-col gap-4">
|
|
||||||
<h2 className="text-2xl font-semibold">
|
|
||||||
Archived Assignments ({assignments.filter(archivedFilter).length})
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{assignments.filter(archivedFilter).map((a) => (
|
|
||||||
<AssignmentCard
|
|
||||||
{...a}
|
|
||||||
onClick={() => setSelectedAssignment(a)}
|
|
||||||
key={a.id}
|
|
||||||
allowDownload
|
|
||||||
reload={reloadAssignments}
|
|
||||||
allowUnarchive
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DefaultDashboard = () => (
|
const DefaultDashboard = () => (
|
||||||
<>
|
<>
|
||||||
{corporateUserToShow && (
|
{corporateUserToShow && (
|
||||||
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
|
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
|
||||||
Linked to:{" "}
|
Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
|
||||||
<b>
|
</div>
|
||||||
{corporateUserToShow?.corporateInformation?.companyInformation
|
)}
|
||||||
.name || corporateUserToShow.name}
|
<section
|
||||||
</b>
|
className={clsx(
|
||||||
</div>
|
"flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center",
|
||||||
)}
|
!!corporateUserToShow && "mt-12 xl:mt-6",
|
||||||
<section
|
)}>
|
||||||
className={clsx(
|
<IconCard
|
||||||
"flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center",
|
onClick={() => setPage("students")}
|
||||||
!!corporateUserToShow && "mt-12 xl:mt-6"
|
Icon={BsPersonFill}
|
||||||
)}
|
label="Students"
|
||||||
>
|
value={users.filter(studentFilter).length}
|
||||||
<IconCard
|
color="purple"
|
||||||
onClick={() => setPage("students")}
|
/>
|
||||||
Icon={BsPersonFill}
|
<IconCard
|
||||||
label="Students"
|
Icon={BsClipboard2Data}
|
||||||
value={users.filter(studentFilter).length}
|
label="Exams Performed"
|
||||||
color="purple"
|
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
||||||
/>
|
color="purple"
|
||||||
<IconCard
|
/>
|
||||||
Icon={BsClipboard2Data}
|
<IconCard
|
||||||
label="Exams Performed"
|
Icon={BsPaperclip}
|
||||||
value={
|
label="Average Level"
|
||||||
stats.filter((s) =>
|
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
|
||||||
groups.flatMap((g) => g.participants).includes(s.user)
|
color="purple"
|
||||||
).length
|
/>
|
||||||
}
|
{checkAccess(user, ["teacher", "developer"], permissions, "viewGroup") && (
|
||||||
color="purple"
|
<IconCard Icon={BsPeople} label="Groups" value={groups.length} color="purple" onClick={() => setPage("groups")} />
|
||||||
/>
|
)}
|
||||||
<IconCard
|
<div
|
||||||
Icon={BsPaperclip}
|
onClick={() => setPage("assignments")}
|
||||||
label="Average Level"
|
className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300">
|
||||||
value={averageLevelCalculator(
|
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
|
||||||
stats.filter((s) =>
|
<span className="flex flex-col gap-1 items-center text-xl">
|
||||||
groups.flatMap((g) => g.participants).includes(s.user)
|
<span className="text-lg">Assignments</span>
|
||||||
)
|
<span className="font-semibold text-mti-purple-light">{assignments.filter((a) => !a.archived).length}</span>
|
||||||
).toFixed(1)}
|
</span>
|
||||||
color="purple"
|
</div>
|
||||||
/>
|
</section>
|
||||||
{checkAccess(user, ["teacher", "developer"], "viewGroup") && (
|
|
||||||
<IconCard
|
|
||||||
Icon={BsPeople}
|
|
||||||
label="Groups"
|
|
||||||
value={groups.length}
|
|
||||||
color="purple"
|
|
||||||
onClick={() => setPage("groups")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
onClick={() => setPage("assignments")}
|
|
||||||
className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"
|
|
||||||
>
|
|
||||||
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
|
|
||||||
<span className="flex flex-col gap-1 items-center text-xl">
|
|
||||||
<span className="text-lg">Assignments</span>
|
|
||||||
<span className="font-semibold text-mti-purple-light">
|
|
||||||
{assignments.filter((a) => !a.archived).length}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full justify-between">
|
||||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
<span className="p-4">Latest students</span>
|
<span className="p-4">Latest students</span>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(studentFilter)
|
.filter(studentFilter)
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
<span className="p-4">Highest level students</span>
|
<span className="p-4">Highest level students</span>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(studentFilter)
|
.filter(studentFilter)
|
||||||
.sort(
|
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
||||||
(a, b) =>
|
.map((x) => (
|
||||||
calculateAverageLevel(b.levels) -
|
<UserDisplay key={x.id} {...x} />
|
||||||
calculateAverageLevel(a.levels)
|
))}
|
||||||
)
|
</div>
|
||||||
.map((x) => (
|
</div>
|
||||||
<UserDisplay key={x.id} {...x} />
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
))}
|
<span className="p-4">Highest exam count students</span>
|
||||||
</div>
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
</div>
|
{users
|
||||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
.filter(studentFilter)
|
||||||
<span className="p-4">Highest exam count students</span>
|
.sort(
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
(a, b) =>
|
||||||
{users
|
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
|
||||||
.filter(studentFilter)
|
)
|
||||||
.sort(
|
.map((x) => (
|
||||||
(a, b) =>
|
<UserDisplay key={x.id} {...x} />
|
||||||
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
))}
|
||||||
Object.keys(groupByExam(getStatsByStudent(a))).length
|
</div>
|
||||||
)
|
</div>
|
||||||
.map((x) => (
|
</section>
|
||||||
<UserDisplay key={x.id} {...x} />
|
</>
|
||||||
))}
|
);
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
|
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
|
||||||
<>
|
<>
|
||||||
{selectedUser && (
|
{selectedUser && (
|
||||||
<div className="w-full flex flex-col gap-8">
|
<div className="w-full flex flex-col gap-8">
|
||||||
<UserCard
|
<UserCard
|
||||||
loggedInUser={user}
|
loggedInUser={user}
|
||||||
onClose={(shouldReload) => {
|
onClose={(shouldReload) => {
|
||||||
setSelectedUser(undefined);
|
setSelectedUser(undefined);
|
||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" ||
|
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
|
||||||
selectedUser.type === "teacher"
|
}
|
||||||
? () => setPage("students")
|
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
|
||||||
: undefined
|
user={selectedUser}
|
||||||
}
|
/>
|
||||||
onViewTeachers={
|
</div>
|
||||||
selectedUser.type === "corporate"
|
)}
|
||||||
? () => setPage("teachers")
|
</>
|
||||||
: undefined
|
</Modal>
|
||||||
}
|
{page === "students" && <StudentsList />}
|
||||||
user={selectedUser}
|
{page === "groups" && <GroupsList />}
|
||||||
/>
|
{page === "assignments" && <AssignmentsPage />}
|
||||||
</div>
|
{page === "" && <DefaultDashboard />}
|
||||||
)}
|
</>
|
||||||
</>
|
);
|
||||||
</Modal>
|
|
||||||
{page === "students" && <StudentsList />}
|
|
||||||
{page === "groups" && <GroupsList />}
|
|
||||||
{page === "assignments" && <AssignmentsPage />}
|
|
||||||
{page === "" && <DefaultDashboard />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/hooks/usePermissions.tsx
Normal file
29
src/hooks/usePermissions.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {Exam} from "@/interfaces/exam";
|
||||||
|
import {Permission, PermissionType} from "@/interfaces/permissions";
|
||||||
|
import {ExamState} from "@/stores/examStore";
|
||||||
|
import axios from "axios";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
|
export default function usePermissions(user: string) {
|
||||||
|
const [permissions, setPermissions] = useState<PermissionType[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
axios
|
||||||
|
.get<Permission[]>(`/api/permissions`)
|
||||||
|
.then((response) => {
|
||||||
|
const permissionTypes = response.data
|
||||||
|
.filter((x) => !x.users.includes(user))
|
||||||
|
.reduce((acc, curr) => [...acc, curr.type], [] as PermissionType[]);
|
||||||
|
console.log(response.data, permissionTypes);
|
||||||
|
setPermissions(permissionTypes);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(getData, [user]);
|
||||||
|
|
||||||
|
return {permissions, isLoading, isError, reload: getData};
|
||||||
|
}
|
||||||
@@ -1,57 +1,58 @@
|
|||||||
export const markets = ["au", "br", "de"] as const;
|
export const markets = ["au", "br", "de"] as const;
|
||||||
|
|
||||||
export const permissions = [
|
export const permissions = [
|
||||||
// generate codes are basicly invites
|
// generate codes are basicly invites
|
||||||
"createCodeStudent",
|
"createCodeStudent",
|
||||||
"createCodeTeacher",
|
"createCodeTeacher",
|
||||||
"createCodeCorporate",
|
"createCodeCorporate",
|
||||||
"createCodeCountryManager",
|
"createCodeCountryManager",
|
||||||
"createCodeAdmin",
|
"createCodeAdmin",
|
||||||
// exams
|
// exams
|
||||||
"createReadingExam",
|
"createReadingExam",
|
||||||
"createListeningExam",
|
"createListeningExam",
|
||||||
"createWritingExam",
|
"createWritingExam",
|
||||||
"createSpeakingExam",
|
"createSpeakingExam",
|
||||||
"createLevelExam",
|
"createLevelExam",
|
||||||
// view pages
|
// view pages
|
||||||
"viewExams",
|
"viewExams",
|
||||||
"viewExercises",
|
"viewExercises",
|
||||||
"viewRecords",
|
"viewRecords",
|
||||||
"viewStats",
|
"viewStats",
|
||||||
"viewTickets",
|
"viewTickets",
|
||||||
"viewPaymentRecords",
|
"viewPaymentRecords",
|
||||||
// view data
|
// view data
|
||||||
"viewStudent",
|
"viewStudent",
|
||||||
"viewTeacher",
|
"viewTeacher",
|
||||||
"viewCorporate",
|
"viewCorporate",
|
||||||
"viewCountryManager",
|
"viewCountryManager",
|
||||||
"viewAdmin",
|
"viewAdmin",
|
||||||
"viewGroup",
|
"viewGroup",
|
||||||
"viewCodes",
|
"viewCodes",
|
||||||
// edit data
|
// edit data
|
||||||
"editStudent",
|
"editStudent",
|
||||||
"editTeacher",
|
"editTeacher",
|
||||||
"editCorporate",
|
"editCorporate",
|
||||||
"editCountryManager",
|
"editCountryManager",
|
||||||
"editAdmin",
|
"editAdmin",
|
||||||
"editGroup",
|
"editGroup",
|
||||||
// delete data
|
// delete data
|
||||||
"deleteStudent",
|
"deleteStudent",
|
||||||
"deleteTeacher",
|
"deleteTeacher",
|
||||||
"deleteCorporate",
|
"deleteCorporate",
|
||||||
"deleteCountryManager",
|
"deleteCountryManager",
|
||||||
"deleteAdmin",
|
"deleteAdmin",
|
||||||
"deleteGroup",
|
"deleteGroup",
|
||||||
"deleteCodes",
|
"deleteCodes",
|
||||||
// create options
|
// create options
|
||||||
"createGroup",
|
"createGroup",
|
||||||
"createCodes"
|
"createCodes",
|
||||||
|
"all",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type PermissionType = (typeof permissions)[keyof typeof permissions];
|
export type PermissionType = (typeof permissions)[keyof typeof permissions];
|
||||||
|
|
||||||
export interface Permission {
|
export interface Permission {
|
||||||
id: string;
|
id: string;
|
||||||
type: PermissionType;
|
type: PermissionType;
|
||||||
users: string[];
|
users: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,377 +1,280 @@
|
|||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { Type, User } from "@/interfaces/user";
|
import {Type, User} from "@/interfaces/user";
|
||||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { capitalize, uniqBy } from "lodash";
|
import {capitalize, uniqBy} from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import { toast } from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import { useFilePicker } from "use-file-picker";
|
import {useFilePicker} from "use-file-picker";
|
||||||
import readXlsxFile from "read-excel-file";
|
import readXlsxFile from "read-excel-file";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import { BsFileEarmarkEaselFill, BsQuestionCircleFill } from "react-icons/bs";
|
import {BsFileEarmarkEaselFill, BsQuestionCircleFill} from "react-icons/bs";
|
||||||
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||||
import { PermissionType } from "@/interfaces/permissions";
|
import {PermissionType} from "@/interfaces/permissions";
|
||||||
const EMAIL_REGEX = new RegExp(
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/
|
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
||||||
);
|
|
||||||
|
|
||||||
const USER_TYPE_PERMISSIONS: {
|
const USER_TYPE_PERMISSIONS: {
|
||||||
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
|
[key in Type]: {perm: PermissionType | undefined; list: Type[]};
|
||||||
} = {
|
} = {
|
||||||
student: {
|
student: {
|
||||||
perm: "createCodeStudent",
|
perm: "createCodeStudent",
|
||||||
list: [],
|
list: [],
|
||||||
},
|
},
|
||||||
teacher: {
|
teacher: {
|
||||||
perm: "createCodeTeacher",
|
perm: "createCodeTeacher",
|
||||||
list: [],
|
list: [],
|
||||||
},
|
},
|
||||||
agent: {
|
agent: {
|
||||||
perm: "createCodeCountryManager",
|
perm: "createCodeCountryManager",
|
||||||
list: [],
|
list: [],
|
||||||
},
|
},
|
||||||
corporate: {
|
corporate: {
|
||||||
perm: "createCodeCorporate",
|
perm: "createCodeCorporate",
|
||||||
list: ["student", "teacher"],
|
list: ["student", "teacher"],
|
||||||
},
|
},
|
||||||
mastercorporate: {
|
mastercorporate: {
|
||||||
perm: undefined,
|
perm: undefined,
|
||||||
list: ["student", "teacher", "corporate"],
|
list: ["student", "teacher", "corporate"],
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
perm: "createCodeAdmin",
|
perm: "createCodeAdmin",
|
||||||
list: [
|
list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
|
||||||
"student",
|
},
|
||||||
"teacher",
|
developer: {
|
||||||
"agent",
|
perm: undefined,
|
||||||
"corporate",
|
list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
|
||||||
"admin",
|
},
|
||||||
"mastercorporate",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
developer: {
|
|
||||||
perm: undefined,
|
|
||||||
list: [
|
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"agent",
|
|
||||||
"corporate",
|
|
||||||
"admin",
|
|
||||||
"developer",
|
|
||||||
"mastercorporate",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BatchCodeGenerator({ user }: { user: User }) {
|
export default function BatchCodeGenerator({user}: {user: User}) {
|
||||||
const [infos, setInfos] = useState<
|
const [infos, setInfos] = useState<{email: string; name: string; passport_id: string}[]>([]);
|
||||||
{ email: string; name: string; passport_id: string }[]
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
>([]);
|
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null,
|
||||||
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
);
|
||||||
user?.subscriptionExpirationDate
|
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
||||||
? moment(user.subscriptionExpirationDate).toDate()
|
const [type, setType] = useState<Type>("student");
|
||||||
: null
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
);
|
|
||||||
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
|
||||||
const [type, setType] = useState<Type>("student");
|
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
|
||||||
|
|
||||||
const { users } = useUsers();
|
const {users} = useUsers();
|
||||||
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
|
|
||||||
const { openFilePicker, filesContent, clear } = useFilePicker({
|
const {openFilePicker, filesContent, clear} = useFilePicker({
|
||||||
accept: ".xlsx",
|
accept: ".xlsx",
|
||||||
multiple: false,
|
multiple: false,
|
||||||
readAs: "ArrayBuffer",
|
readAs: "ArrayBuffer",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isExpiryDateEnabled) setExpiryDate(null);
|
if (!isExpiryDateEnabled) setExpiryDate(null);
|
||||||
}, [isExpiryDateEnabled]);
|
}, [isExpiryDateEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filesContent.length > 0) {
|
if (filesContent.length > 0) {
|
||||||
const file = filesContent[0];
|
const file = filesContent[0];
|
||||||
readXlsxFile(file.content).then((rows) => {
|
readXlsxFile(file.content).then((rows) => {
|
||||||
try {
|
try {
|
||||||
const information = uniqBy(
|
const information = uniqBy(
|
||||||
rows
|
rows
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
const [
|
const [firstName, lastName, country, passport_id, email, ...phone] = row as string[];
|
||||||
firstName,
|
return EMAIL_REGEX.test(email.toString().trim())
|
||||||
lastName,
|
? {
|
||||||
country,
|
email: email.toString().trim().toLowerCase(),
|
||||||
passport_id,
|
name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
|
||||||
email,
|
passport_id: passport_id?.toString().trim() || undefined,
|
||||||
...phone
|
}
|
||||||
] = row as string[];
|
: undefined;
|
||||||
return EMAIL_REGEX.test(email.toString().trim())
|
})
|
||||||
? {
|
.filter((x) => !!x) as typeof infos,
|
||||||
email: email.toString().trim().toLowerCase(),
|
(x) => x.email,
|
||||||
name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
|
);
|
||||||
passport_id: passport_id?.toString().trim() || undefined,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
})
|
|
||||||
.filter((x) => !!x) as typeof infos,
|
|
||||||
(x) => x.email
|
|
||||||
);
|
|
||||||
|
|
||||||
if (information.length === 0) {
|
if (information.length === 0) {
|
||||||
toast.error(
|
toast.error(
|
||||||
"Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!"
|
"Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!",
|
||||||
);
|
);
|
||||||
return clear();
|
return clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
setInfos(information);
|
setInfos(information);
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(
|
toast.error(
|
||||||
"Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!"
|
"Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!",
|
||||||
);
|
);
|
||||||
return clear();
|
return clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [filesContent]);
|
}, [filesContent]);
|
||||||
|
|
||||||
const generateAndInvite = async () => {
|
const generateAndInvite = async () => {
|
||||||
const newUsers = infos.filter(
|
const newUsers = infos.filter((x) => !users.map((u) => u.email).includes(x.email));
|
||||||
(x) => !users.map((u) => u.email).includes(x.email)
|
const existingUsers = infos
|
||||||
);
|
.filter((x) => users.map((u) => u.email).includes(x.email))
|
||||||
const existingUsers = infos
|
.map((i) => users.find((u) => u.email === i.email))
|
||||||
.filter((x) => users.map((u) => u.email).includes(x.email))
|
.filter((x) => !!x && x.type === "student") as User[];
|
||||||
.map((i) => users.find((u) => u.email === i.email))
|
|
||||||
.filter((x) => !!x && x.type === "student") as User[];
|
|
||||||
|
|
||||||
const newUsersSentence =
|
const newUsersSentence = newUsers.length > 0 ? `generate ${newUsers.length} code(s)` : undefined;
|
||||||
newUsers.length > 0 ? `generate ${newUsers.length} code(s)` : undefined;
|
const existingUsersSentence = existingUsers.length > 0 ? `invite ${existingUsers.length} registered student(s)` : undefined;
|
||||||
const existingUsersSentence =
|
if (
|
||||||
existingUsers.length > 0
|
!confirm(
|
||||||
? `invite ${existingUsers.length} registered student(s)`
|
`You are about to ${[newUsersSentence, existingUsersSentence].filter((x) => !!x).join(" and ")}, are you sure you want to continue?`,
|
||||||
: undefined;
|
)
|
||||||
if (
|
)
|
||||||
!confirm(
|
return;
|
||||||
`You are about to ${[newUsersSentence, existingUsersSentence]
|
|
||||||
.filter((x) => !!x)
|
|
||||||
.join(" and ")}, are you sure you want to continue?`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
Promise.all(
|
Promise.all(existingUsers.map(async (u) => await axios.post(`/api/invites`, {to: u.id, from: user.id})))
|
||||||
existingUsers.map(
|
.then(() => toast.success(`Successfully invited ${existingUsers.length} registered student(s)!`))
|
||||||
async (u) =>
|
.finally(() => {
|
||||||
await axios.post(`/api/invites`, { to: u.id, from: user.id })
|
if (newUsers.length === 0) setIsLoading(false);
|
||||||
)
|
});
|
||||||
)
|
|
||||||
.then(() =>
|
|
||||||
toast.success(
|
|
||||||
`Successfully invited ${existingUsers.length} registered student(s)!`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
if (newUsers.length === 0) setIsLoading(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (newUsers.length > 0) generateCode(type, newUsers);
|
if (newUsers.length > 0) generateCode(type, newUsers);
|
||||||
setInfos([]);
|
setInfos([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateCode = (type: Type, informations: typeof infos) => {
|
const generateCode = (type: Type, informations: typeof infos) => {
|
||||||
const uid = new ShortUniqueId();
|
const uid = new ShortUniqueId();
|
||||||
const codes = informations.map(() => uid.randomUUID(6));
|
const codes = informations.map(() => uid.randomUUID(6));
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.post<{ ok: boolean; valid?: number; reason?: string }>("/api/code", {
|
.post<{ok: boolean; valid?: number; reason?: string}>("/api/code", {
|
||||||
type,
|
type,
|
||||||
codes,
|
codes,
|
||||||
infos: informations,
|
infos: informations,
|
||||||
expiryDate,
|
expiryDate,
|
||||||
})
|
})
|
||||||
.then(({ data, status }) => {
|
.then(({data, status}) => {
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
toast.success(
|
toast.success(
|
||||||
`Successfully generated${
|
`Successfully generated${data.valid ? ` ${data.valid}/${informations.length}` : ""} ${capitalize(
|
||||||
data.valid ? ` ${data.valid}/${informations.length}` : ""
|
type,
|
||||||
} ${capitalize(type)} codes and they have been notified by e-mail!`,
|
)} codes and they have been notified by e-mail!`,
|
||||||
{ toastId: "success" }
|
{toastId: "success"},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
toast.error(data.reason, { toastId: "forbidden" });
|
toast.error(data.reason, {toastId: "forbidden"});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(({ response: { status, data } }) => {
|
.catch(({response: {status, data}}) => {
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
toast.error(data.reason, { toastId: "forbidden" });
|
toast.error(data.reason, {toastId: "forbidden"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error(`Something went wrong, please try again later!`, {
|
toast.error(`Something went wrong, please try again later!`, {
|
||||||
toastId: "error",
|
toastId: "error",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return clear();
|
return clear();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)} title="Excel File Format">
|
||||||
isOpen={showHelp}
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
onClose={() => setShowHelp(false)}
|
<span>Please upload an Excel file with the following format:</span>
|
||||||
title="Excel File Format"
|
<table className="w-full">
|
||||||
>
|
<thead>
|
||||||
<div className="mt-4 flex flex-col gap-2">
|
<tr>
|
||||||
<span>Please upload an Excel file with the following format:</span>
|
<th className="border border-neutral-200 px-2 py-1">First Name</th>
|
||||||
<table className="w-full">
|
<th className="border border-neutral-200 px-2 py-1">Last Name</th>
|
||||||
<thead>
|
<th className="border border-neutral-200 px-2 py-1">Country</th>
|
||||||
<tr>
|
<th className="border border-neutral-200 px-2 py-1">Passport/National ID</th>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
<th className="border border-neutral-200 px-2 py-1">E-mail</th>
|
||||||
First Name
|
<th className="border border-neutral-200 px-2 py-1">Phone Number</th>
|
||||||
</th>
|
</tr>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
</thead>
|
||||||
Last Name
|
</table>
|
||||||
</th>
|
<span className="mt-4">
|
||||||
<th className="border border-neutral-200 px-2 py-1">Country</th>
|
<b>Notes:</b>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
<ul>
|
||||||
Passport/National ID
|
<li>- All incorrect e-mails will be ignored;</li>
|
||||||
</th>
|
<li>- All already registered e-mails will be ignored;</li>
|
||||||
<th className="border border-neutral-200 px-2 py-1">E-mail</th>
|
<li>- You may have a header row with the format above, however, it is not necessary;</li>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
<li>- All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.</li>
|
||||||
Phone Number
|
</ul>
|
||||||
</th>
|
</span>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</Modal>
|
||||||
</table>
|
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
|
||||||
<span className="mt-4">
|
<div className="flex items-end justify-between">
|
||||||
<b>Notes:</b>
|
<label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label>
|
||||||
<ul>
|
<div className="tooltip cursor-pointer" data-tip="Excel File Format" onClick={() => setShowHelp(true)}>
|
||||||
<li>- All incorrect e-mails will be ignored;</li>
|
<BsQuestionCircleFill />
|
||||||
<li>- All already registered e-mails will be ignored;</li>
|
</div>
|
||||||
<li>
|
</div>
|
||||||
- You may have a header row with the format above, however, it
|
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
|
||||||
is not necessary;
|
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
||||||
</li>
|
</Button>
|
||||||
<li>
|
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||||
- All of the e-mails in the file will receive an e-mail to join
|
<>
|
||||||
EnCoach with the role selected below.
|
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
||||||
</li>
|
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
||||||
</ul>
|
<Checkbox isChecked={isExpiryDateEnabled} onChange={setIsExpiryDateEnabled} disabled={!!user.subscriptionExpirationDate}>
|
||||||
</span>
|
Enabled
|
||||||
</div>
|
</Checkbox>
|
||||||
</Modal>
|
</div>
|
||||||
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
|
{isExpiryDateEnabled && (
|
||||||
<div className="flex items-end justify-between">
|
<ReactDatePicker
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
className={clsx(
|
||||||
Choose an Excel file
|
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
|
||||||
</label>
|
"hover:border-mti-purple tooltip",
|
||||||
<div
|
"transition duration-300 ease-in-out",
|
||||||
className="tooltip cursor-pointer"
|
)}
|
||||||
data-tip="Excel File Format"
|
filterDate={(date) =>
|
||||||
onClick={() => setShowHelp(true)}
|
moment(date).isAfter(new Date()) &&
|
||||||
>
|
(user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true)
|
||||||
<BsQuestionCircleFill />
|
}
|
||||||
</div>
|
dateFormat="dd/MM/yyyy"
|
||||||
</div>
|
selected={expiryDate}
|
||||||
<Button
|
onChange={(date) => setExpiryDate(date)}
|
||||||
onClick={openFilePicker}
|
/>
|
||||||
isLoading={isLoading}
|
)}
|
||||||
disabled={isLoading}
|
</>
|
||||||
>
|
)}
|
||||||
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
<label className="text-mti-gray-dim text-base font-normal">Select the type of user they should be</label>
|
||||||
</Button>
|
{user && (
|
||||||
{user &&
|
<select
|
||||||
checkAccess(user, [
|
defaultValue="student"
|
||||||
"developer",
|
onChange={(e) => setType(e.target.value as typeof user.type)}
|
||||||
"admin",
|
className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none">
|
||||||
"corporate",
|
{Object.keys(USER_TYPE_LABELS)
|
||||||
"mastercorporate",
|
.filter((x) => {
|
||||||
]) && (
|
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
|
||||||
<>
|
return checkAccess(user, getTypesOfUser(list), permissions, perm);
|
||||||
<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">
|
.map((type) => (
|
||||||
Expiry Date
|
<option key={type} value={type}>
|
||||||
</label>
|
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
|
||||||
<Checkbox
|
</option>
|
||||||
isChecked={isExpiryDateEnabled}
|
))}
|
||||||
onChange={setIsExpiryDateEnabled}
|
</select>
|
||||||
disabled={!!user.subscriptionExpirationDate}
|
)}
|
||||||
>
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "createCodes") && (
|
||||||
Enabled
|
<Button onClick={generateAndInvite} disabled={infos.length === 0 || (isExpiryDateEnabled ? !expiryDate : false)}>
|
||||||
</Checkbox>
|
Generate & Send
|
||||||
</div>
|
</Button>
|
||||||
{isExpiryDateEnabled && (
|
)}
|
||||||
<ReactDatePicker
|
</div>
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
|
||||||
Select the type of user they should be
|
|
||||||
</label>
|
|
||||||
{user && (
|
|
||||||
<select
|
|
||||||
defaultValue="student"
|
|
||||||
onChange={(e) => setType(e.target.value as typeof user.type)}
|
|
||||||
className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none"
|
|
||||||
>
|
|
||||||
{Object.keys(USER_TYPE_LABELS)
|
|
||||||
.filter((x) => {
|
|
||||||
const { list, perm } = USER_TYPE_PERMISSIONS[x as Type];
|
|
||||||
return checkAccess(user, getTypesOfUser(list), perm);
|
|
||||||
})
|
|
||||||
.map((type) => (
|
|
||||||
<option key={type} value={type}>
|
|
||||||
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
{checkAccess(
|
|
||||||
user,
|
|
||||||
["developer", "admin", "corporate", "mastercorporate"],
|
|
||||||
"createCodes"
|
|
||||||
) && (
|
|
||||||
<Button
|
|
||||||
onClick={generateAndInvite}
|
|
||||||
disabled={
|
|
||||||
infos.length === 0 || (isExpiryDateEnabled ? !expiryDate : false)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Generate & Send
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,198 +1,162 @@
|
|||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
import { Type, User } from "@/interfaces/user";
|
import {Type, User} from "@/interfaces/user";
|
||||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { capitalize } from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import { toast } from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import {checkAccess} from "@/utils/permissions";
|
||||||
import { PermissionType } from "@/interfaces/permissions";
|
import {PermissionType} from "@/interfaces/permissions";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
|
||||||
const USER_TYPE_PERMISSIONS: {
|
const USER_TYPE_PERMISSIONS: {
|
||||||
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
|
[key in Type]: {perm: PermissionType | undefined; list: Type[]};
|
||||||
} = {
|
} = {
|
||||||
student: {
|
student: {
|
||||||
perm: "createCodeStudent",
|
perm: "createCodeStudent",
|
||||||
list: [],
|
list: [],
|
||||||
},
|
},
|
||||||
teacher: {
|
teacher: {
|
||||||
perm: "createCodeTeacher",
|
perm: "createCodeTeacher",
|
||||||
list: [],
|
list: [],
|
||||||
},
|
},
|
||||||
agent: {
|
agent: {
|
||||||
perm: "createCodeCountryManager",
|
perm: "createCodeCountryManager",
|
||||||
list: [],
|
list: [],
|
||||||
},
|
},
|
||||||
corporate: {
|
corporate: {
|
||||||
perm: "createCodeCorporate",
|
perm: "createCodeCorporate",
|
||||||
list: ["student", "teacher"],
|
list: ["student", "teacher"],
|
||||||
},
|
},
|
||||||
mastercorporate: {
|
mastercorporate: {
|
||||||
perm: undefined,
|
perm: undefined,
|
||||||
list: ["student", "teacher", "corporate"],
|
list: ["student", "teacher", "corporate"],
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
perm: "createCodeAdmin",
|
perm: "createCodeAdmin",
|
||||||
list: [
|
list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
|
||||||
"student",
|
},
|
||||||
"teacher",
|
developer: {
|
||||||
"agent",
|
perm: undefined,
|
||||||
"corporate",
|
list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
|
||||||
"admin",
|
},
|
||||||
"mastercorporate",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
developer: {
|
|
||||||
perm: undefined,
|
|
||||||
list: [
|
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"agent",
|
|
||||||
"corporate",
|
|
||||||
"admin",
|
|
||||||
"developer",
|
|
||||||
"mastercorporate",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CodeGenerator({ user }: { user: User }) {
|
export default function CodeGenerator({user}: {user: User}) {
|
||||||
const [generatedCode, setGeneratedCode] = useState<string>();
|
const [generatedCode, setGeneratedCode] = useState<string>();
|
||||||
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
||||||
user?.subscriptionExpirationDate
|
user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null,
|
||||||
? moment(user.subscriptionExpirationDate).toDate()
|
);
|
||||||
: null
|
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
||||||
);
|
const [type, setType] = useState<Type>("student");
|
||||||
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
const [type, setType] = useState<Type>("student");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isExpiryDateEnabled) setExpiryDate(null);
|
if (!isExpiryDateEnabled) setExpiryDate(null);
|
||||||
}, [isExpiryDateEnabled]);
|
}, [isExpiryDateEnabled]);
|
||||||
|
|
||||||
const generateCode = (type: Type) => {
|
const generateCode = (type: Type) => {
|
||||||
const uid = new ShortUniqueId();
|
const uid = new ShortUniqueId();
|
||||||
const code = uid.randomUUID(6);
|
const code = uid.randomUUID(6);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post("/api/code", { type, codes: [code], expiryDate })
|
.post("/api/code", {type, codes: [code], expiryDate})
|
||||||
.then(({ data, status }) => {
|
.then(({data, status}) => {
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
toast.success(`Successfully generated a ${capitalize(type)} code!`, {
|
toast.success(`Successfully generated a ${capitalize(type)} code!`, {
|
||||||
toastId: "success",
|
toastId: "success",
|
||||||
});
|
});
|
||||||
setGeneratedCode(code);
|
setGeneratedCode(code);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
toast.error(data.reason, { toastId: "forbidden" });
|
toast.error(data.reason, {toastId: "forbidden"});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(({ response: { status, data } }) => {
|
.catch(({response: {status, data}}) => {
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
toast.error(data.reason, { toastId: "forbidden" });
|
toast.error(data.reason, {toastId: "forbidden"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error(`Something went wrong, please try again later!`, {
|
toast.error(`Something went wrong, please try again later!`, {
|
||||||
toastId: "error",
|
toastId: "error",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 border p-4 border-mti-gray-platinum rounded-xl">
|
<div className="flex flex-col gap-4 border p-4 border-mti-gray-platinum rounded-xl">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">User Code Generator</label>
|
||||||
User Code Generator
|
{user && (
|
||||||
</label>
|
<select
|
||||||
{user && (
|
defaultValue="student"
|
||||||
<select
|
onChange={(e) => setType(e.target.value as typeof user.type)}
|
||||||
defaultValue="student"
|
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">
|
||||||
onChange={(e) => setType(e.target.value as typeof user.type)}
|
{Object.keys(USER_TYPE_LABELS)
|
||||||
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"
|
.filter((x) => {
|
||||||
>
|
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
|
||||||
{Object.keys(USER_TYPE_LABELS)
|
return checkAccess(user, list, permissions, perm);
|
||||||
.filter((x) => {
|
})
|
||||||
const { list, perm } = USER_TYPE_PERMISSIONS[x as Type];
|
.map((type) => (
|
||||||
return checkAccess(user, list, perm);
|
<option key={type} value={type}>
|
||||||
})
|
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
|
||||||
.map((type) => (
|
</option>
|
||||||
<option key={type} value={type}>
|
))}
|
||||||
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
|
</select>
|
||||||
</option>
|
)}
|
||||||
))}
|
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||||
</select>
|
<>
|
||||||
)}
|
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
||||||
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
||||||
<>
|
<Checkbox isChecked={isExpiryDateEnabled} onChange={setIsExpiryDateEnabled} disabled={!!user.subscriptionExpirationDate}>
|
||||||
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
Enabled
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
</Checkbox>
|
||||||
Expiry Date
|
</div>
|
||||||
</label>
|
{isExpiryDateEnabled && (
|
||||||
<Checkbox
|
<ReactDatePicker
|
||||||
isChecked={isExpiryDateEnabled}
|
className={clsx(
|
||||||
onChange={setIsExpiryDateEnabled}
|
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
|
||||||
disabled={!!user.subscriptionExpirationDate}
|
"hover:border-mti-purple tooltip",
|
||||||
>
|
"transition duration-300 ease-in-out",
|
||||||
Enabled
|
)}
|
||||||
</Checkbox>
|
filterDate={(date) =>
|
||||||
</div>
|
moment(date).isAfter(new Date()) &&
|
||||||
{isExpiryDateEnabled && (
|
(user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true)
|
||||||
<ReactDatePicker
|
}
|
||||||
className={clsx(
|
dateFormat="dd/MM/yyyy"
|
||||||
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
|
selected={expiryDate}
|
||||||
"hover:border-mti-purple tooltip",
|
onChange={(date) => setExpiryDate(date)}
|
||||||
"transition duration-300 ease-in-out"
|
/>
|
||||||
)}
|
)}
|
||||||
filterDate={(date) =>
|
</>
|
||||||
moment(date).isAfter(new Date()) &&
|
)}
|
||||||
(user.subscriptionExpirationDate
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "createCodes") && (
|
||||||
? moment(date).isBefore(user.subscriptionExpirationDate)
|
<Button onClick={() => generateCode(type)} disabled={isExpiryDateEnabled ? !expiryDate : false}>
|
||||||
: true)
|
Generate
|
||||||
}
|
</Button>
|
||||||
dateFormat="dd/MM/yyyy"
|
)}
|
||||||
selected={expiryDate}
|
<label className="font-normal text-base text-mti-gray-dim">Generated Code:</label>
|
||||||
onChange={(date) => setExpiryDate(date)}
|
<div
|
||||||
/>
|
className={clsx(
|
||||||
)}
|
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
</>
|
"hover:border-mti-purple tooltip",
|
||||||
)}
|
"transition duration-300 ease-in-out",
|
||||||
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], 'createCodes') && (
|
)}
|
||||||
<Button
|
data-tip="Click to copy"
|
||||||
onClick={() => generateCode(type)}
|
onClick={() => {
|
||||||
disabled={isExpiryDateEnabled ? !expiryDate : false}
|
if (generatedCode) navigator.clipboard.writeText(generatedCode);
|
||||||
>
|
}}>
|
||||||
Generate
|
{generatedCode}
|
||||||
</Button>
|
</div>
|
||||||
)}
|
{generatedCode && <span className="text-sm text-mti-gray-dim font-light">Give this code to the user to complete their registration</span>}
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
</div>
|
||||||
Generated Code:
|
);
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
|
||||||
"hover:border-mti-purple tooltip",
|
|
||||||
"transition duration-300 ease-in-out"
|
|
||||||
)}
|
|
||||||
data-tip="Click to copy"
|
|
||||||
onClick={() => {
|
|
||||||
if (generatedCode) navigator.clipboard.writeText(generatedCode);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{generatedCode}
|
|
||||||
</div>
|
|
||||||
{generatedCode && (
|
|
||||||
<span className="text-sm text-mti-gray-dim font-light">
|
|
||||||
Give this code to the user to complete their registration
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,364 +4,306 @@ import Select from "@/components/Low/Select";
|
|||||||
import useCodes from "@/hooks/useCodes";
|
import useCodes from "@/hooks/useCodes";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { Code, User } from "@/interfaces/user";
|
import {Code, User} from "@/interfaces/user";
|
||||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
import {
|
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||||
createColumnHelper,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import {useEffect, useState, useMemo} from "react";
|
||||||
import { BsTrash } from "react-icons/bs";
|
import {BsTrash} from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import {checkAccess} from "@/utils/permissions";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Code>();
|
const columnHelper = createColumnHelper<Code>();
|
||||||
|
|
||||||
const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
|
const CreatorCell = ({id, users}: {id: string; users: User[]}) => {
|
||||||
const [creatorUser, setCreatorUser] = useState<User>();
|
const [creatorUser, setCreatorUser] = useState<User>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCreatorUser(users.find((x) => x.id === id));
|
setCreatorUser(users.find((x) => x.id === id));
|
||||||
}, [id, users]);
|
}, [id, users]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(creatorUser?.type === "corporate"
|
{(creatorUser?.type === "corporate" ? creatorUser?.corporateInformation?.companyInformation?.name : creatorUser?.name || "N/A") || "N/A"}{" "}
|
||||||
? creatorUser?.corporateInformation?.companyInformation?.name
|
{creatorUser && `(${USER_TYPE_LABELS[creatorUser.type]})`}
|
||||||
: creatorUser?.name || "N/A") || "N/A"}{" "}
|
</>
|
||||||
{creatorUser && `(${USER_TYPE_LABELS[creatorUser.type]})`}
|
);
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CodeList({ user }: { user: User }) {
|
export default function CodeList({user}: {user: User}) {
|
||||||
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
||||||
|
|
||||||
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(
|
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(user?.type === "corporate" ? user : undefined);
|
||||||
user?.type === "corporate" ? user : undefined
|
const [filterAvailability, setFilterAvailability] = useState<"in-use" | "unused">();
|
||||||
);
|
|
||||||
const [filterAvailability, setFilterAvailability] = useState<
|
|
||||||
"in-use" | "unused"
|
|
||||||
>();
|
|
||||||
|
|
||||||
// const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
|
|
||||||
const { users } = useUsers();
|
// const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
|
||||||
const { codes, reload } = useCodes(
|
|
||||||
user?.type === "corporate" ? user?.id : undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
const {users} = useUsers();
|
||||||
|
const {codes, reload} = useCodes(user?.type === "corporate" ? user?.id : undefined);
|
||||||
|
|
||||||
|
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
||||||
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
|
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
|
||||||
const filteredCodes = useMemo(() => {
|
const filteredCodes = useMemo(() => {
|
||||||
return codes.filter((x) => {
|
return codes.filter((x) => {
|
||||||
// TODO: if the expiry date is missing, it does not make sense to filter by date
|
// TODO: if the expiry date is missing, it does not make sense to filter by date
|
||||||
// so we need to find a way to handle this edge case
|
// so we need to find a way to handle this edge case
|
||||||
if(startDate && endDate && x.expiryDate) {
|
if (startDate && endDate && x.expiryDate) {
|
||||||
const date = moment(x.expiryDate);
|
const date = moment(x.expiryDate);
|
||||||
if(date.isBefore(startDate) || date.isAfter(endDate)) {
|
if (date.isBefore(startDate) || date.isAfter(endDate)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filteredCorporate && x.creator !== filteredCorporate.id) return false;
|
if (filteredCorporate && x.creator !== filteredCorporate.id) return false;
|
||||||
if (filterAvailability) {
|
if (filterAvailability) {
|
||||||
if (filterAvailability === "in-use" && !x.userId) return false;
|
if (filterAvailability === "in-use" && !x.userId) return false;
|
||||||
if (filterAvailability === "unused" && x.userId) return false;
|
if (filterAvailability === "unused" && x.userId) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [codes, startDate, endDate, filteredCorporate, filterAvailability]);
|
}, [codes, startDate, endDate, filteredCorporate, filterAvailability]);
|
||||||
|
|
||||||
const toggleCode = (id: string) => {
|
const toggleCode = (id: string) => {
|
||||||
setSelectedCodes((prev) =>
|
setSelectedCodes((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
||||||
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
};
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleAllCodes = (checked: boolean) => {
|
const toggleAllCodes = (checked: boolean) => {
|
||||||
if (checked)
|
if (checked) return setSelectedCodes(filteredCodes.filter((x) => !x.userId).map((x) => x.code));
|
||||||
return setSelectedCodes(
|
|
||||||
filteredCodes.filter((x) => !x.userId).map((x) => x.code)
|
|
||||||
);
|
|
||||||
|
|
||||||
return setSelectedCodes([]);
|
return setSelectedCodes([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCodes = async (codes: string[]) => {
|
const deleteCodes = async (codes: string[]) => {
|
||||||
if (
|
if (!confirm(`Are you sure you want to delete these ${codes.length} code(s)?`)) return;
|
||||||
!confirm(`Are you sure you want to delete these ${codes.length} code(s)?`)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
codes.forEach((code) => params.append("code", code));
|
codes.forEach((code) => params.append("code", code));
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/code?${params.toString()}`)
|
.delete(`/api/code?${params.toString()}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`Deleted the codes!`);
|
toast.success(`Deleted the codes!`);
|
||||||
setSelectedCodes([]);
|
setSelectedCodes([]);
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
if (reason.response.status === 404) {
|
if (reason.response.status === 404) {
|
||||||
toast.error("Code not found!");
|
toast.error("Code not found!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reason.response.status === 403) {
|
if (reason.response.status === 403) {
|
||||||
toast.error("You do not have permission to delete this code!");
|
toast.error("You do not have permission to delete this code!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error("Something went wrong, please try again later.");
|
toast.error("Something went wrong, please try again later.");
|
||||||
})
|
})
|
||||||
.finally(reload);
|
.finally(reload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCode = async (code: Code) => {
|
const deleteCode = async (code: Code) => {
|
||||||
if (!confirm(`Are you sure you want to delete this "${code.code}" code?`))
|
if (!confirm(`Are you sure you want to delete this "${code.code}" code?`)) return;
|
||||||
return;
|
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/code/${code.code}`)
|
.delete(`/api/code/${code.code}`)
|
||||||
.then(() => toast.success(`Deleted the "${code.code}" exam`))
|
.then(() => toast.success(`Deleted the "${code.code}" exam`))
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
if (reason.response.status === 404) {
|
if (reason.response.status === 404) {
|
||||||
toast.error("Code not found!");
|
toast.error("Code not found!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reason.response.status === 403) {
|
if (reason.response.status === 403) {
|
||||||
toast.error("You do not have permission to delete this code!");
|
toast.error("You do not have permission to delete this code!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error("Something went wrong, please try again later.");
|
toast.error("Something went wrong, please try again later.");
|
||||||
})
|
})
|
||||||
.finally(reload);
|
.finally(reload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const allowedToDelete = checkAccess(
|
const allowedToDelete = checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "deleteCodes");
|
||||||
user,
|
|
||||||
["developer", "admin", "corporate", "mastercorporate"],
|
|
||||||
"deleteCodes"
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
columnHelper.accessor("code", {
|
columnHelper.accessor("code", {
|
||||||
id: "codeCheckbox",
|
id: "codeCheckbox",
|
||||||
header: () => (
|
header: () => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={filteredCodes.filter((x) => !x.userId).length === 0}
|
disabled={filteredCodes.filter((x) => !x.userId).length === 0}
|
||||||
isChecked={
|
isChecked={
|
||||||
selectedCodes.length ===
|
selectedCodes.length === filteredCodes.filter((x) => !x.userId).length && filteredCodes.filter((x) => !x.userId).length > 0
|
||||||
filteredCodes.filter((x) => !x.userId).length &&
|
}
|
||||||
filteredCodes.filter((x) => !x.userId).length > 0
|
onChange={(checked) => toggleAllCodes(checked)}>
|
||||||
}
|
{""}
|
||||||
onChange={(checked) => toggleAllCodes(checked)}
|
</Checkbox>
|
||||||
>
|
),
|
||||||
{""}
|
cell: (info) =>
|
||||||
</Checkbox>
|
!info.row.original.userId ? (
|
||||||
),
|
<Checkbox isChecked={selectedCodes.includes(info.getValue())} onChange={() => toggleCode(info.getValue())}>
|
||||||
cell: (info) =>
|
{""}
|
||||||
!info.row.original.userId ? (
|
</Checkbox>
|
||||||
<Checkbox
|
) : null,
|
||||||
isChecked={selectedCodes.includes(info.getValue())}
|
}),
|
||||||
onChange={() => toggleCode(info.getValue())}
|
columnHelper.accessor("code", {
|
||||||
>
|
header: "Code",
|
||||||
{""}
|
cell: (info) => info.getValue(),
|
||||||
</Checkbox>
|
}),
|
||||||
) : null,
|
columnHelper.accessor("creationDate", {
|
||||||
}),
|
header: "Creation Date",
|
||||||
columnHelper.accessor("code", {
|
cell: (info) => (info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A"),
|
||||||
header: "Code",
|
}),
|
||||||
cell: (info) => info.getValue(),
|
columnHelper.accessor("email", {
|
||||||
}),
|
header: "Invited E-mail",
|
||||||
columnHelper.accessor("creationDate", {
|
cell: (info) => info.getValue() || "N/A",
|
||||||
header: "Creation Date",
|
}),
|
||||||
cell: (info) =>
|
columnHelper.accessor("creator", {
|
||||||
info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A",
|
header: "Creator",
|
||||||
}),
|
cell: (info) => <CreatorCell id={info.getValue()} users={users} />,
|
||||||
columnHelper.accessor("email", {
|
}),
|
||||||
header: "Invited E-mail",
|
columnHelper.accessor("userId", {
|
||||||
cell: (info) => info.getValue() || "N/A",
|
header: "Availability",
|
||||||
}),
|
cell: (info) =>
|
||||||
columnHelper.accessor("creator", {
|
info.getValue() ? (
|
||||||
header: "Creator",
|
<span className="flex gap-1 items-center text-mti-green">
|
||||||
cell: (info) => <CreatorCell id={info.getValue()} users={users} />,
|
<div className="w-2 h-2 rounded-full bg-mti-green" /> In Use
|
||||||
}),
|
</span>
|
||||||
columnHelper.accessor("userId", {
|
) : (
|
||||||
header: "Availability",
|
<span className="flex gap-1 items-center text-mti-red">
|
||||||
cell: (info) =>
|
<div className="w-2 h-2 rounded-full bg-mti-red" /> Unused
|
||||||
info.getValue() ? (
|
</span>
|
||||||
<span className="flex gap-1 items-center text-mti-green">
|
),
|
||||||
<div className="w-2 h-2 rounded-full bg-mti-green" /> In Use
|
}),
|
||||||
</span>
|
{
|
||||||
) : (
|
header: "",
|
||||||
<span className="flex gap-1 items-center text-mti-red">
|
id: "actions",
|
||||||
<div className="w-2 h-2 rounded-full bg-mti-red" /> Unused
|
cell: ({row}: {row: {original: Code}}) => {
|
||||||
</span>
|
return (
|
||||||
),
|
<div className="flex gap-4">
|
||||||
}),
|
{allowedToDelete && !row.original.userId && (
|
||||||
{
|
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteCode(row.original)}>
|
||||||
header: "",
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
id: "actions",
|
</div>
|
||||||
cell: ({ row }: { row: { original: Code } }) => {
|
)}
|
||||||
return (
|
</div>
|
||||||
<div className="flex gap-4">
|
);
|
||||||
{allowedToDelete && !row.original.userId && (
|
},
|
||||||
<div
|
},
|
||||||
data-tip="Delete"
|
];
|
||||||
className="cursor-pointer tooltip"
|
|
||||||
onClick={() => deleteCode(row.original)}
|
|
||||||
>
|
|
||||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredCodes,
|
data: filteredCodes,
|
||||||
columns: defaultColumns,
|
columns: defaultColumns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between pb-4 pt-1">
|
<div className="flex items-center justify-between pb-4 pt-1">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Select
|
<Select
|
||||||
className="!w-96 !py-1"
|
className="!w-96 !py-1"
|
||||||
disabled={user?.type === "corporate"}
|
disabled={user?.type === "corporate"}
|
||||||
isClearable
|
isClearable
|
||||||
placeholder="Corporate"
|
placeholder="Corporate"
|
||||||
value={
|
value={
|
||||||
filteredCorporate
|
filteredCorporate
|
||||||
? {
|
? {
|
||||||
label: `${
|
label: `${
|
||||||
filteredCorporate.type === "corporate"
|
filteredCorporate.type === "corporate"
|
||||||
? filteredCorporate.corporateInformation
|
? filteredCorporate.corporateInformation?.companyInformation?.name || filteredCorporate.name
|
||||||
?.companyInformation?.name || filteredCorporate.name
|
: filteredCorporate.name
|
||||||
: filteredCorporate.name
|
} (${USER_TYPE_LABELS[filteredCorporate.type]})`,
|
||||||
} (${USER_TYPE_LABELS[filteredCorporate.type]})`,
|
value: filteredCorporate.id,
|
||||||
value: filteredCorporate.id,
|
}
|
||||||
}
|
: null
|
||||||
: null
|
}
|
||||||
}
|
options={users
|
||||||
options={users
|
.filter((x) => ["admin", "developer", "corporate"].includes(x.type))
|
||||||
.filter((x) =>
|
.map((x) => ({
|
||||||
["admin", "developer", "corporate"].includes(x.type)
|
label: `${x.type === "corporate" ? x.corporateInformation?.companyInformation?.name || x.name : x.name} (${
|
||||||
)
|
USER_TYPE_LABELS[x.type]
|
||||||
.map((x) => ({
|
})`,
|
||||||
label: `${
|
value: x.id,
|
||||||
x.type === "corporate"
|
user: x,
|
||||||
? x.corporateInformation?.companyInformation?.name || x.name
|
}))}
|
||||||
: x.name
|
onChange={(value) => setFilteredCorporate(value ? users.find((x) => x.id === value?.value) : undefined)}
|
||||||
} (${USER_TYPE_LABELS[x.type]})`,
|
/>
|
||||||
value: x.id,
|
<Select
|
||||||
user: x,
|
className="!w-96 !py-1"
|
||||||
}))}
|
placeholder="Availability"
|
||||||
onChange={(value) =>
|
isClearable
|
||||||
setFilteredCorporate(
|
options={[
|
||||||
value ? users.find((x) => x.id === value?.value) : undefined
|
{label: "In Use", value: "in-use"},
|
||||||
)
|
{label: "Unused", value: "unused"},
|
||||||
}
|
]}
|
||||||
/>
|
onChange={(value) => setFilterAvailability(value ? (value.value as typeof filterAvailability) : undefined)}
|
||||||
<Select
|
/>
|
||||||
className="!w-96 !py-1"
|
<ReactDatePicker
|
||||||
placeholder="Availability"
|
dateFormat="dd/MM/yyyy"
|
||||||
isClearable
|
className="px-4 py-6 w-full text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||||
options={[
|
selected={startDate}
|
||||||
{ label: "In Use", value: "in-use" },
|
startDate={startDate}
|
||||||
{ label: "Unused", value: "unused" },
|
endDate={endDate}
|
||||||
]}
|
selectsRange
|
||||||
onChange={(value) =>
|
showMonthDropdown
|
||||||
setFilterAvailability(
|
filterDate={(date: Date) => moment(date).isSameOrBefore(moment(new Date()))}
|
||||||
value ? (value.value as typeof filterAvailability) : undefined
|
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
||||||
)
|
setStartDate(initialDate ?? moment("01/01/2023").toDate());
|
||||||
}
|
if (finalDate) {
|
||||||
/>
|
// basicly selecting a final day works as if I'm selecting the first
|
||||||
<ReactDatePicker
|
// minute of that day. this way it covers the whole day
|
||||||
dateFormat="dd/MM/yyyy"
|
setEndDate(moment(finalDate).endOf("day").toDate());
|
||||||
className="px-4 py-6 w-full text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
|
return;
|
||||||
selected={startDate}
|
}
|
||||||
startDate={startDate}
|
setEndDate(null);
|
||||||
endDate={endDate}
|
}}
|
||||||
selectsRange
|
/>
|
||||||
showMonthDropdown
|
</div>
|
||||||
filterDate={(date: Date) =>
|
{allowedToDelete && (
|
||||||
moment(date).isSameOrBefore(moment(new Date()))
|
<div className="flex gap-4 items-center">
|
||||||
}
|
<span>{selectedCodes.length} code(s) selected</span>
|
||||||
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
<Button
|
||||||
setStartDate(initialDate ?? moment("01/01/2023").toDate());
|
disabled={selectedCodes.length === 0}
|
||||||
if (finalDate) {
|
variant="outline"
|
||||||
// basicly selecting a final day works as if I'm selecting the first
|
color="red"
|
||||||
// minute of that day. this way it covers the whole day
|
className="!py-1 px-10"
|
||||||
setEndDate(moment(finalDate).endOf("day").toDate());
|
onClick={() => deleteCodes(selectedCodes)}>
|
||||||
return;
|
Delete
|
||||||
}
|
</Button>
|
||||||
setEndDate(null);
|
</div>
|
||||||
}}
|
)}
|
||||||
/>
|
</div>
|
||||||
</div>
|
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||||
{allowedToDelete && (
|
<thead>
|
||||||
<div className="flex gap-4 items-center">
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<span>{selectedCodes.length} code(s) selected</span>
|
<tr key={headerGroup.id}>
|
||||||
<Button
|
{headerGroup.headers.map((header) => (
|
||||||
disabled={selectedCodes.length === 0}
|
<th className="p-4 text-left" key={header.id}>
|
||||||
variant="outline"
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
color="red"
|
</th>
|
||||||
className="!py-1 px-10"
|
))}
|
||||||
onClick={() => deleteCodes(selectedCodes)}
|
</tr>
|
||||||
>
|
))}
|
||||||
Delete
|
</thead>
|
||||||
</Button>
|
<tbody className="px-2">
|
||||||
</div>
|
{table.getRowModel().rows.map((row) => (
|
||||||
)}
|
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||||
</div>
|
{row.getVisibleCells().map((cell) => (
|
||||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
<td className="px-4 py-2" key={cell.id}>
|
||||||
<thead>
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
</td>
|
||||||
<tr key={headerGroup.id}>
|
))}
|
||||||
{headerGroup.headers.map((header) => (
|
</tr>
|
||||||
<th className="p-4 text-left" key={header.id}>
|
))}
|
||||||
{header.isPlaceholder
|
</tbody>
|
||||||
? null
|
</table>
|
||||||
: flexRender(
|
</>
|
||||||
header.column.columnDef.header,
|
);
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
<tbody className="px-2">
|
|
||||||
{table.getRowModel().rows.map((row) => (
|
|
||||||
<tr
|
|
||||||
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
|
||||||
key={row.id}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<td className="px-4 py-2" key={cell.id}>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,474 +3,341 @@ import Input from "@/components/Low/Input";
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { CorporateUser, Group, User } from "@/interfaces/user";
|
import {CorporateUser, Group, User} from "@/interfaces/user";
|
||||||
import {
|
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||||
createColumnHelper,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { capitalize, uniq } from "lodash";
|
import {capitalize, uniq} from "lodash";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import { BsPencil, BsQuestionCircleFill, BsTrash } from "react-icons/bs";
|
import {BsPencil, BsQuestionCircleFill, BsTrash} from "react-icons/bs";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import { toast } from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import readXlsxFile from "read-excel-file";
|
import readXlsxFile from "read-excel-file";
|
||||||
import { useFilePicker } from "use-file-picker";
|
import {useFilePicker} from "use-file-picker";
|
||||||
import { getUserCorporate } from "@/utils/groups";
|
import {getUserCorporate} from "@/utils/groups";
|
||||||
import { isAgentUser, isCorporateUser } from "@/resources/user";
|
import {isAgentUser, isCorporateUser} from "@/resources/user";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import {checkAccess} from "@/utils/permissions";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Group>();
|
const columnHelper = createColumnHelper<Group>();
|
||||||
const EMAIL_REGEX = new RegExp(
|
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
||||||
/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/
|
|
||||||
);
|
|
||||||
|
|
||||||
const LinkedCorporate = ({
|
const LinkedCorporate = ({userId, users, groups}: {userId: string; users: User[]; groups: Group[]}) => {
|
||||||
userId,
|
const [companyName, setCompanyName] = useState("");
|
||||||
users,
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
groups,
|
|
||||||
}: {
|
|
||||||
userId: string;
|
|
||||||
users: User[];
|
|
||||||
groups: Group[];
|
|
||||||
}) => {
|
|
||||||
const [companyName, setCompanyName] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const user = users.find((u) => u.id === userId);
|
const user = users.find((u) => u.id === userId);
|
||||||
if (!user) return setCompanyName("");
|
if (!user) return setCompanyName("");
|
||||||
|
|
||||||
if (isCorporateUser(user))
|
if (isCorporateUser(user)) return setCompanyName(user.corporateInformation?.companyInformation?.name || user.name);
|
||||||
return setCompanyName(
|
if (isAgentUser(user)) return setCompanyName(user.agentInformation?.companyName || user.name);
|
||||||
user.corporateInformation?.companyInformation?.name || user.name
|
|
||||||
);
|
|
||||||
if (isAgentUser(user))
|
|
||||||
return setCompanyName(user.agentInformation?.companyName || user.name);
|
|
||||||
|
|
||||||
const belongingGroups = groups.filter((x) =>
|
const belongingGroups = groups.filter((x) => x.participants.includes(userId));
|
||||||
x.participants.includes(userId)
|
const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x));
|
||||||
);
|
|
||||||
const belongingGroupsAdmins = belongingGroups
|
|
||||||
.map((x) => users.find((u) => u.id === x.admin))
|
|
||||||
.filter((x) => !!x && isCorporateUser(x));
|
|
||||||
|
|
||||||
if (belongingGroupsAdmins.length === 0) return setCompanyName("");
|
if (belongingGroupsAdmins.length === 0) return setCompanyName("");
|
||||||
|
|
||||||
const admin = belongingGroupsAdmins[0] as CorporateUser;
|
const admin = belongingGroupsAdmins[0] as CorporateUser;
|
||||||
setCompanyName(
|
setCompanyName(admin.corporateInformation?.companyInformation.name || admin.name);
|
||||||
admin.corporateInformation?.companyInformation.name || admin.name
|
}, [userId, users, groups]);
|
||||||
);
|
|
||||||
}, [userId, users, groups]);
|
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>;
|
||||||
<span className="animate-pulse">Loading...</span>
|
|
||||||
) : (
|
|
||||||
<>{companyName}</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CreateDialogProps {
|
interface CreateDialogProps {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
group?: Group;
|
group?: Group;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
|
const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
|
||||||
const [name, setName] = useState<string | undefined>(
|
const [name, setName] = useState<string | undefined>(group?.name || undefined);
|
||||||
group?.name || undefined
|
const [admin, setAdmin] = useState<string>(group?.admin || user.id);
|
||||||
);
|
const [participants, setParticipants] = useState<string[]>(group?.participants || []);
|
||||||
const [admin, setAdmin] = useState<string>(group?.admin || user.id);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [participants, setParticipants] = useState<string[]>(
|
|
||||||
group?.participants || []
|
|
||||||
);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const { openFilePicker, filesContent, clear } = useFilePicker({
|
const {openFilePicker, filesContent, clear} = useFilePicker({
|
||||||
accept: ".xlsx",
|
accept: ".xlsx",
|
||||||
multiple: false,
|
multiple: false,
|
||||||
readAs: "ArrayBuffer",
|
readAs: "ArrayBuffer",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filesContent.length > 0) {
|
if (filesContent.length > 0) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const file = filesContent[0];
|
const file = filesContent[0];
|
||||||
readXlsxFile(file.content).then((rows) => {
|
readXlsxFile(file.content).then((rows) => {
|
||||||
const emails = uniq(
|
const emails = uniq(
|
||||||
rows
|
rows
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
const [email] = row as string[];
|
const [email] = row as string[];
|
||||||
return EMAIL_REGEX.test(email) &&
|
return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email) ? email.toString().trim() : undefined;
|
||||||
!users.map((u) => u.email).includes(email)
|
})
|
||||||
? email.toString().trim()
|
.filter((x) => !!x),
|
||||||
: undefined;
|
);
|
||||||
})
|
|
||||||
.filter((x) => !!x)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (emails.length === 0) {
|
if (emails.length === 0) {
|
||||||
toast.error("Please upload an Excel file containing e-mails!");
|
toast.error("Please upload an Excel file containing e-mails!");
|
||||||
clear();
|
clear();
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailUsers = [...new Set(emails)]
|
const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined);
|
||||||
.map((x) => users.find((y) => y.email.toLowerCase() === x))
|
const filteredUsers = emailUsers.filter(
|
||||||
.filter((x) => x !== undefined);
|
(x) =>
|
||||||
const filteredUsers = emailUsers.filter(
|
((user.type === "developer" || user.type === "admin" || user.type === "corporate" || user.type === "mastercorporate") &&
|
||||||
(x) =>
|
(x?.type === "student" || x?.type === "teacher")) ||
|
||||||
((user.type === "developer" ||
|
(user.type === "teacher" && x?.type === "student"),
|
||||||
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));
|
setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id));
|
||||||
toast.success(
|
toast.success(
|
||||||
user.type !== "teacher"
|
user.type !== "teacher"
|
||||||
? "Added all teachers and students found in the file you've provided!"
|
? "Added all teachers and students found in the file you've provided!"
|
||||||
: "Added all students found in the file you've provided!",
|
: "Added all students found in the file you've provided!",
|
||||||
{ toastId: "upload-success" }
|
{toastId: "upload-success"},
|
||||||
);
|
);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [filesContent, user.type, users]);
|
}, [filesContent, user.type, users]);
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
if (name !== group?.name && (name === "Students" || name === "Teachers")) {
|
if (name !== group?.name && (name === "Students" || name === "Teachers")) {
|
||||||
toast.error(
|
toast.error("That group name is reserved and cannot be used, please enter another one.");
|
||||||
"That group name is reserved and cannot be used, please enter another one."
|
setIsLoading(false);
|
||||||
);
|
return;
|
||||||
setIsLoading(false);
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(group ? axios.patch : axios.post)(
|
(group ? axios.patch : axios.post)(group ? `/api/groups/${group.id}` : "/api/groups", {name, admin, participants})
|
||||||
group ? `/api/groups/${group.id}` : "/api/groups",
|
.then(() => {
|
||||||
{ name, admin, participants }
|
toast.success(`Group "${name}" ${group ? "edited" : "created"} successfully`);
|
||||||
)
|
return true;
|
||||||
.then(() => {
|
})
|
||||||
toast.success(
|
.catch(() => {
|
||||||
`Group "${name}" ${group ? "edited" : "created"} successfully`
|
toast.error("Something went wrong, please try again later!");
|
||||||
);
|
return false;
|
||||||
return true;
|
})
|
||||||
})
|
.finally(() => {
|
||||||
.catch(() => {
|
setIsLoading(false);
|
||||||
toast.error("Something went wrong, please try again later!");
|
onClose();
|
||||||
return false;
|
});
|
||||||
})
|
};
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
onClose();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex w-full flex-col gap-12 px-4 py-2">
|
<div className="mt-4 flex w-full flex-col gap-12 px-4 py-2">
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<Input
|
<Input name="name" type="text" label="Name" defaultValue={name} onChange={setName} required disabled={group?.disableEditing} />
|
||||||
name="name"
|
<div className="flex w-full flex-col gap-3">
|
||||||
type="text"
|
<div className="flex items-center gap-2">
|
||||||
label="Name"
|
<label className="text-mti-gray-dim text-base font-normal">Participants</label>
|
||||||
defaultValue={name}
|
<div className="tooltip" data-tip="The Excel file should only include a column with the desired e-mails.">
|
||||||
onChange={setName}
|
<BsQuestionCircleFill />
|
||||||
required
|
</div>
|
||||||
disabled={group?.disableEditing}
|
</div>
|
||||||
/>
|
<div className="flex w-full gap-8">
|
||||||
<div className="flex w-full flex-col gap-3">
|
<Select
|
||||||
<div className="flex items-center gap-2">
|
className="w-full"
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
value={participants.map((x) => ({
|
||||||
Participants
|
value: x,
|
||||||
</label>
|
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
|
||||||
<div
|
}))}
|
||||||
className="tooltip"
|
placeholder="Participants..."
|
||||||
data-tip="The Excel file should only include a column with the desired e-mails."
|
defaultValue={participants.map((x) => ({
|
||||||
>
|
value: x,
|
||||||
<BsQuestionCircleFill />
|
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
|
||||||
</div>
|
}))}
|
||||||
</div>
|
options={users
|
||||||
<div className="flex w-full gap-8">
|
.filter((x) => (user.type === "teacher" ? x.type === "student" : x.type === "student" || x.type === "teacher"))
|
||||||
<Select
|
.map((x) => ({value: x.id, label: `${x.email} - ${x.name}`}))}
|
||||||
className="w-full"
|
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
||||||
value={participants.map((x) => ({
|
isMulti
|
||||||
value: x,
|
isSearchable
|
||||||
label: `${users.find((y) => y.id === x)?.email} - ${
|
menuPortalTarget={document?.body}
|
||||||
users.find((y) => y.id === x)?.name
|
styles={{
|
||||||
}`,
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
}))}
|
control: (styles) => ({
|
||||||
placeholder="Participants..."
|
...styles,
|
||||||
defaultValue={participants.map((x) => ({
|
backgroundColor: "white",
|
||||||
value: x,
|
borderRadius: "999px",
|
||||||
label: `${users.find((y) => y.id === x)?.email} - ${
|
padding: "1rem 1.5rem",
|
||||||
users.find((y) => y.id === x)?.name
|
zIndex: "40",
|
||||||
}`,
|
}),
|
||||||
}))}
|
}}
|
||||||
options={users
|
/>
|
||||||
.filter((x) =>
|
{user.type !== "teacher" && (
|
||||||
user.type === "teacher"
|
<Button className="w-full max-w-[300px]" onClick={openFilePicker} isLoading={isLoading} variant="outline">
|
||||||
? x.type === "student"
|
{filesContent.length === 0 ? "Upload participants Excel file" : filesContent[0].name}
|
||||||
: x.type === "student" || x.type === "teacher"
|
</Button>
|
||||||
)
|
)}
|
||||||
.map((x) => ({ value: x.id, label: `${x.email} - ${x.name}` }))}
|
</div>
|
||||||
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
</div>
|
||||||
isMulti
|
</div>
|
||||||
isSearchable
|
<div className="mt-8 flex w-full items-center justify-end gap-8">
|
||||||
menuPortalTarget={document?.body}
|
<Button variant="outline" color="red" className="w-full max-w-[200px]" isLoading={isLoading} onClick={onClose}>
|
||||||
styles={{
|
Cancel
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
</Button>
|
||||||
control: (styles) => ({
|
<Button className="w-full max-w-[200px]" onClick={submit} isLoading={isLoading} disabled={!name}>
|
||||||
...styles,
|
Submit
|
||||||
backgroundColor: "white",
|
</Button>
|
||||||
borderRadius: "999px",
|
</div>
|
||||||
padding: "1rem 1.5rem",
|
</div>
|
||||||
zIndex: "40",
|
);
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{user.type !== "teacher" && (
|
|
||||||
<Button
|
|
||||||
className="w-full max-w-[300px]"
|
|
||||||
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"];
|
const filterTypes = ["corporate", "teacher", "mastercorporate"];
|
||||||
|
|
||||||
export default function GroupList({ user }: { user: User }) {
|
export default function GroupList({user}: {user: User}) {
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [editingGroup, setEditingGroup] = useState<Group>();
|
const [editingGroup, setEditingGroup] = useState<Group>();
|
||||||
const [filterByUser, setFilterByUser] = useState(false);
|
const [filterByUser, setFilterByUser] = useState(false);
|
||||||
|
|
||||||
const { users } = useUsers();
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
const { groups, reload } = useGroups(
|
|
||||||
user && filterTypes.includes(user?.type) ? user.id : undefined,
|
|
||||||
user?.type
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const {users} = useUsers();
|
||||||
if (
|
const {groups, reload} = useGroups(user && filterTypes.includes(user?.type) ? user.id : undefined, user?.type);
|
||||||
user &&
|
|
||||||
["corporate", "teacher", "mastercorporate"].includes(user.type)
|
|
||||||
) {
|
|
||||||
setFilterByUser(true);
|
|
||||||
}
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const deleteGroup = (group: Group) => {
|
useEffect(() => {
|
||||||
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
|
if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) {
|
||||||
|
setFilterByUser(true);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
axios
|
const deleteGroup = (group: Group) => {
|
||||||
.delete<{ ok: boolean }>(`/api/groups/${group.id}`)
|
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
|
||||||
.then(() => toast.success(`Group "${group.name}" deleted successfully`))
|
|
||||||
.catch(() => toast.error("Something went wrong, please try again later!"))
|
|
||||||
.finally(reload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultColumns = [
|
axios
|
||||||
columnHelper.accessor("id", {
|
.delete<{ok: boolean}>(`/api/groups/${group.id}`)
|
||||||
header: "ID",
|
.then(() => toast.success(`Group "${group.name}" deleted successfully`))
|
||||||
cell: (info) => info.getValue(),
|
.catch(() => toast.error("Something went wrong, please try again later!"))
|
||||||
}),
|
.finally(reload);
|
||||||
columnHelper.accessor("name", {
|
};
|
||||||
header: "Name",
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("admin", {
|
|
||||||
header: "Admin",
|
|
||||||
cell: (info) => (
|
|
||||||
<div
|
|
||||||
className="tooltip"
|
|
||||||
data-tip={capitalize(
|
|
||||||
users.find((x) => x.id === info.getValue())?.type
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{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) =>
|
|
||||||
info
|
|
||||||
.getValue()
|
|
||||||
.map((x) => users.find((y) => y.id === x)?.name)
|
|
||||||
.join(", "),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
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({
|
const defaultColumns = [
|
||||||
data: groups,
|
columnHelper.accessor("id", {
|
||||||
columns: defaultColumns,
|
header: "ID",
|
||||||
getCoreRowModel: getCoreRowModel(),
|
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={capitalize(users.find((x) => x.id === info.getValue())?.type)}>
|
||||||
|
{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) =>
|
||||||
|
info
|
||||||
|
.getValue()
|
||||||
|
.map((x) => users.find((y) => y.id === x)?.name)
|
||||||
|
.join(", "),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
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 closeModal = () => {
|
const table = useReactTable({
|
||||||
setIsCreating(false);
|
data: groups,
|
||||||
setEditingGroup(undefined);
|
columns: defaultColumns,
|
||||||
reload();
|
getCoreRowModel: getCoreRowModel(),
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
const closeModal = () => {
|
||||||
<div className="h-full w-full rounded-xl">
|
setIsCreating(false);
|
||||||
<Modal
|
setEditingGroup(undefined);
|
||||||
isOpen={isCreating || !!editingGroup}
|
reload();
|
||||||
onClose={closeModal}
|
};
|
||||||
title={editingGroup ? `Editing ${editingGroup.name}` : "New Group"}
|
|
||||||
>
|
|
||||||
<CreatePanel
|
|
||||||
group={editingGroup}
|
|
||||||
user={user}
|
|
||||||
onClose={closeModal}
|
|
||||||
users={
|
|
||||||
user?.type === "corporate" || user?.type === "teacher"
|
|
||||||
? users.filter(
|
|
||||||
(u) =>
|
|
||||||
groups
|
|
||||||
.filter((g) => g.admin === user.id)
|
|
||||||
.flatMap((g) => g.participants)
|
|
||||||
.includes(u.id) ||
|
|
||||||
groups.flatMap((g) => g.participants).includes(u.id)
|
|
||||||
)
|
|
||||||
: users
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
<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(
|
return (
|
||||||
user,
|
<div className="h-full w-full rounded-xl">
|
||||||
["teacher", "corporate", "mastercorporate", "admin", "developer"],
|
<Modal isOpen={isCreating || !!editingGroup} onClose={closeModal} title={editingGroup ? `Editing ${editingGroup.name}` : "New Group"}>
|
||||||
"createGroup"
|
<CreatePanel
|
||||||
) && (
|
group={editingGroup}
|
||||||
<button
|
user={user}
|
||||||
onClick={() => setIsCreating(true)}
|
onClose={closeModal}
|
||||||
className="bg-mti-purple-light hover:bg-mti-purple w-full py-2 text-white transition duration-300 ease-in-out"
|
users={
|
||||||
>
|
user?.type === "corporate" || user?.type === "teacher"
|
||||||
New Group
|
? users.filter(
|
||||||
</button>
|
(u) =>
|
||||||
)}
|
groups
|
||||||
</div>
|
.filter((g) => g.admin === user.id)
|
||||||
);
|
.flatMap((g) => g.participants)
|
||||||
|
.includes(u.id) || groups.flatMap((g) => g.participants).includes(u.id),
|
||||||
|
)
|
||||||
|
: users
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import { User } from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import { Tab } from "@headlessui/react";
|
import {Tab} from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import CodeList from "./CodeList";
|
import CodeList from "./CodeList";
|
||||||
import DiscountList from "./DiscountList";
|
import DiscountList from "./DiscountList";
|
||||||
@@ -7,137 +7,118 @@ import ExamList from "./ExamList";
|
|||||||
import GroupList from "./GroupList";
|
import GroupList from "./GroupList";
|
||||||
import PackageList from "./PackageList";
|
import PackageList from "./PackageList";
|
||||||
import UserList from "./UserList";
|
import UserList from "./UserList";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import {checkAccess} from "@/utils/permissions";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
|
||||||
export default function Lists({ user }: { user: User }) {
|
export default function Lists({user}: {user: User}) {
|
||||||
return (
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
<Tab.Group>
|
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
|
return (
|
||||||
<Tab
|
<Tab.Group>
|
||||||
className={({ selected }) =>
|
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
|
||||||
clsx(
|
<Tab
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
className={({selected}) =>
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
clsx(
|
||||||
"transition duration-300 ease-in-out",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
||||||
selected
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
||||||
? "bg-white shadow"
|
"transition duration-300 ease-in-out",
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
>
|
User List
|
||||||
User List
|
</Tab>
|
||||||
</Tab>
|
{checkAccess(user, ["developer"]) && (
|
||||||
{checkAccess(user, ["developer"]) && (
|
<Tab
|
||||||
<Tab
|
className={({selected}) =>
|
||||||
className={({ selected }) =>
|
clsx(
|
||||||
clsx(
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
"transition duration-300 ease-in-out",
|
||||||
"transition duration-300 ease-in-out",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
selected
|
)
|
||||||
? "bg-white shadow"
|
}>
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
Exam List
|
||||||
)
|
</Tab>
|
||||||
}
|
)}
|
||||||
>
|
<Tab
|
||||||
Exam List
|
className={({selected}) =>
|
||||||
</Tab>
|
clsx(
|
||||||
)}
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
||||||
<Tab
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
||||||
className={({ selected }) =>
|
"transition duration-300 ease-in-out",
|
||||||
clsx(
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
)
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
}>
|
||||||
"transition duration-300 ease-in-out",
|
Group List
|
||||||
selected
|
</Tab>
|
||||||
? "bg-white shadow"
|
{checkAccess(user, ["developer", "admin", "corporate"]) && (
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
<Tab
|
||||||
)
|
className={({selected}) =>
|
||||||
}
|
clsx(
|
||||||
>
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
||||||
Group List
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
||||||
</Tab>
|
"transition duration-300 ease-in-out",
|
||||||
{checkAccess(user, ["developer", "admin", "corporate"]) && (
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
<Tab
|
)
|
||||||
className={({ selected }) =>
|
}>
|
||||||
clsx(
|
Code List
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
</Tab>
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
)}
|
||||||
"transition duration-300 ease-in-out",
|
{checkAccess(user, ["developer", "admin"]) && (
|
||||||
selected
|
<Tab
|
||||||
? "bg-white shadow"
|
className={({selected}) =>
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
clsx(
|
||||||
)
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
||||||
}
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
||||||
>
|
"transition duration-300 ease-in-out",
|
||||||
Code List
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
</Tab>
|
)
|
||||||
)}
|
}>
|
||||||
{checkAccess(user, ["developer", "admin"]) && (
|
Package List
|
||||||
<Tab
|
</Tab>
|
||||||
className={({ selected }) =>
|
)}
|
||||||
clsx(
|
{checkAccess(user, ["developer", "admin"]) && (
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
<Tab
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
className={({selected}) =>
|
||||||
"transition duration-300 ease-in-out",
|
clsx(
|
||||||
selected
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
||||||
? "bg-white shadow"
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
"transition duration-300 ease-in-out",
|
||||||
)
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
}
|
)
|
||||||
>
|
}>
|
||||||
Package List
|
Discount List
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["developer", "admin"]) && (
|
</Tab.List>
|
||||||
<Tab
|
<Tab.Panels className="mt-2">
|
||||||
className={({ selected }) =>
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
clsx(
|
<UserList user={user} />
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
|
</Tab.Panel>
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
|
{checkAccess(user, ["developer"]) && (
|
||||||
"transition duration-300 ease-in-out",
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
selected
|
<ExamList user={user} />
|
||||||
? "bg-white shadow"
|
</Tab.Panel>
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
)}
|
||||||
)
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
}
|
<GroupList user={user} />
|
||||||
>
|
</Tab.Panel>
|
||||||
Discount List
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "viewCodes") && (
|
||||||
</Tab>
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
)}
|
<CodeList user={user} />
|
||||||
</Tab.List>
|
</Tab.Panel>
|
||||||
<Tab.Panels className="mt-2">
|
)}
|
||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
{checkAccess(user, ["developer", "admin"]) && (
|
||||||
<UserList user={user} />
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
</Tab.Panel>
|
<PackageList user={user} />
|
||||||
{checkAccess(user, ["developer"]) && (
|
</Tab.Panel>
|
||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
)}
|
||||||
<ExamList user={user} />
|
{checkAccess(user, ["developer", "admin"]) && (
|
||||||
</Tab.Panel>
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
)}
|
<DiscountList user={user} />
|
||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
</Tab.Panel>
|
||||||
<GroupList user={user} />
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panels>
|
||||||
{checkAccess(
|
</Tab.Group>
|
||||||
user,
|
);
|
||||||
["developer", "admin", "corporate", "mastercorporate"],
|
|
||||||
"viewCodes"
|
|
||||||
) && (
|
|
||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
|
||||||
<CodeList user={user} />
|
|
||||||
</Tab.Panel>
|
|
||||||
)}
|
|
||||||
{checkAccess(user, ["developer", "admin"]) && (
|
|
||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
|
||||||
<PackageList user={user} />
|
|
||||||
</Tab.Panel>
|
|
||||||
)}
|
|
||||||
{checkAccess(user, ["developer", "admin"]) && (
|
|
||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
|
||||||
<DiscountList user={user} />
|
|
||||||
</Tab.Panel>
|
|
||||||
)}
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,46 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import { app } from "@/firebase";
|
import {app} from "@/firebase";
|
||||||
import { getFirestore, doc, setDoc } from "firebase/firestore";
|
import {getFirestore, doc, setDoc, getDoc} from "firebase/firestore";
|
||||||
import { withIronSessionApiRoute } from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
import {getPermissionDoc} from "@/utils/permissions.be";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method === "PATCH") return patch(req, res);
|
if (req.method === "PATCH") return patch(req, res);
|
||||||
|
if (req.method === "GET") return get(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!req.session.user) {
|
||||||
|
res.status(401).json({ok: false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {id} = req.query as {id: string};
|
||||||
|
|
||||||
|
const permissionDoc = await getPermissionDoc(id);
|
||||||
|
return res.status(200).json({allowed: permissionDoc.users.includes(req.session.user.id)});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function patch(req: NextApiRequest, res: NextApiResponse) {
|
async function patch(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res.status(401).json({ ok: false });
|
res.status(401).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { id } = req.query as { id: string };
|
|
||||||
const { users } = req.body;
|
const {id} = req.query as {id: string};
|
||||||
try {
|
const {users} = req.body;
|
||||||
await setDoc(doc(db, "permissions", id), { users }, { merge: true });
|
|
||||||
return res.status(200).json({ ok: true });
|
try {
|
||||||
} catch (err) {
|
await setDoc(doc(db, "permissions", id), {users}, {merge: true});
|
||||||
console.error(err);
|
return res.status(200).json({ok: true});
|
||||||
return res.status(500).json({ ok: false });
|
} catch (err) {
|
||||||
}
|
console.error(err);
|
||||||
|
return res.status(500).json({ok: false});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
// based on the admin of each group, verify if it exists and it's of type corporate
|
// based on the admin of each group, verify if it exists and it's of type corporate
|
||||||
const groupsAdmins = [...new Set(groups.map((g) => g.admin).filter((id) => id))];
|
const groupsAdmins = [...new Set(groups.map((g) => g.admin).filter((id) => id))];
|
||||||
const adminsSnapshot = await getDocs(query(collection(db, "users"), where("id", "in", groupsAdmins), where("type", "==", "corporate")));
|
const adminsSnapshot =
|
||||||
|
groupsAdmins.length > 0
|
||||||
|
? await getDocs(query(collection(db, "users"), where("id", "in", groupsAdmins), where("type", "==", "corporate")))
|
||||||
|
: {docs: []};
|
||||||
const admins = adminsSnapshot.docs.map((doc) => doc.data());
|
const admins = adminsSnapshot.docs.map((doc) => doc.data());
|
||||||
|
|
||||||
const docsWithAdmins = docs.map((d) => {
|
const docsWithAdmins = docs.map((d) => {
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
import { app, adminApp } from "@/firebase";
|
import {app, adminApp} from "@/firebase";
|
||||||
import { Group, User } from "@/interfaces/user";
|
import {Group, User} from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {
|
import {collection, deleteDoc, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore";
|
||||||
collection,
|
import {getAuth} from "firebase-admin/auth";
|
||||||
deleteDoc,
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
doc,
|
import {NextApiRequest, NextApiResponse} from "next";
|
||||||
getDoc,
|
import {getPermissions, getPermissionDocs} from "@/utils/permissions.be";
|
||||||
getDocs,
|
|
||||||
getFirestore,
|
|
||||||
query,
|
|
||||||
setDoc,
|
|
||||||
where,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import { getAuth } from "firebase-admin/auth";
|
|
||||||
import { withIronSessionApiRoute } from "iron-session/next";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { getPermissions, getPermissionDocs } from "@/utils/permissions.be";
|
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
const auth = getAuth(adminApp);
|
const auth = getAuth(adminApp);
|
||||||
@@ -24,132 +14,108 @@ const auth = getAuth(adminApp);
|
|||||||
export default withIronSessionApiRoute(user, sessionOptions);
|
export default withIronSessionApiRoute(user, sessionOptions);
|
||||||
|
|
||||||
async function user(req: NextApiRequest, res: NextApiResponse) {
|
async function user(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method === "GET") return get(req, res);
|
if (req.method === "GET") return get(req, res);
|
||||||
if (req.method === "DELETE") return del(req, res);
|
if (req.method === "DELETE") return del(req, res);
|
||||||
|
|
||||||
res.status(404).json(undefined);
|
res.status(404).json(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del(req: NextApiRequest, res: NextApiResponse) {
|
async function del(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res.status(401).json({ ok: false });
|
res.status(401).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = req.query as { id: string };
|
const {id} = req.query as {id: string};
|
||||||
|
|
||||||
const docUser = await getDoc(doc(db, "users", req.session.user.id));
|
const docUser = await getDoc(doc(db, "users", req.session.user.id));
|
||||||
if (!docUser.exists()) {
|
if (!docUser.exists()) {
|
||||||
res.status(401).json({ ok: false });
|
res.status(401).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = docUser.data() as User;
|
const user = docUser.data() as User;
|
||||||
|
|
||||||
const docTargetUser = await getDoc(doc(db, "users", id));
|
const docTargetUser = await getDoc(doc(db, "users", id));
|
||||||
if (!docTargetUser.exists()) {
|
if (!docTargetUser.exists()) {
|
||||||
res.status(404).json({ ok: false });
|
res.status(404).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetUser = { ...docTargetUser.data(), id: docTargetUser.id } as User;
|
const targetUser = {...docTargetUser.data(), id: docTargetUser.id} as User;
|
||||||
|
|
||||||
if (
|
if (user.type === "corporate" && (targetUser.type === "student" || targetUser.type === "teacher")) {
|
||||||
user.type === "corporate" &&
|
res.json({ok: true});
|
||||||
(targetUser.type === "student" || targetUser.type === "teacher")
|
|
||||||
) {
|
|
||||||
res.json({ ok: true });
|
|
||||||
|
|
||||||
const userParticipantGroup = await getDocs(
|
const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id)));
|
||||||
query(
|
await Promise.all([
|
||||||
collection(db, "groups"),
|
...userParticipantGroup.docs
|
||||||
where("participants", "array-contains", id)
|
.filter((x) => (x.data() as Group).admin === user.id)
|
||||||
)
|
.map(
|
||||||
);
|
async (x) =>
|
||||||
await Promise.all([
|
await setDoc(
|
||||||
...userParticipantGroup.docs
|
x.ref,
|
||||||
.filter((x) => (x.data() as Group).admin === user.id)
|
{
|
||||||
.map(
|
participants: x.data().participants.filter((y: string) => y !== id),
|
||||||
async (x) =>
|
},
|
||||||
await setDoc(
|
{merge: true},
|
||||||
x.ref,
|
),
|
||||||
{
|
),
|
||||||
participants: x
|
]);
|
||||||
.data()
|
|
||||||
.participants.filter((y: string) => y !== id),
|
|
||||||
},
|
|
||||||
{ merge: true }
|
|
||||||
)
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permission = PERMISSIONS.deleteUser[targetUser.type];
|
const permission = PERMISSIONS.deleteUser[targetUser.type];
|
||||||
if (!permission.list.includes(user.type)) {
|
if (!permission.list.includes(user.type)) {
|
||||||
res.status(403).json({ ok: false });
|
res.status(403).json({ok: false});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ok: true});
|
||||||
|
|
||||||
await auth.deleteUser(id);
|
await auth.deleteUser(id);
|
||||||
await deleteDoc(doc(db, "users", id));
|
await deleteDoc(doc(db, "users", id));
|
||||||
const userCodeDocs = await getDocs(
|
const userCodeDocs = await getDocs(query(collection(db, "codes"), where("userId", "==", id)));
|
||||||
query(collection(db, "codes"), where("userId", "==", id))
|
const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id)));
|
||||||
);
|
const userGroupAdminDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id)));
|
||||||
const userParticipantGroup = await getDocs(
|
const userStatsDocs = await getDocs(query(collection(db, "stats"), where("user", "==", id)));
|
||||||
query(collection(db, "groups"), where("participants", "array-contains", id))
|
|
||||||
);
|
|
||||||
const userGroupAdminDocs = await getDocs(
|
|
||||||
query(collection(db, "groups"), where("admin", "==", id))
|
|
||||||
);
|
|
||||||
const userStatsDocs = await getDocs(
|
|
||||||
query(collection(db, "stats"), where("user", "==", id))
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...userCodeDocs.docs.map(async (x) => await deleteDoc(x.ref)),
|
...userCodeDocs.docs.map(async (x) => await deleteDoc(x.ref)),
|
||||||
...userGroupAdminDocs.docs.map(async (x) => await deleteDoc(x.ref)),
|
...userGroupAdminDocs.docs.map(async (x) => await deleteDoc(x.ref)),
|
||||||
...userStatsDocs.docs.map(async (x) => await deleteDoc(x.ref)),
|
...userStatsDocs.docs.map(async (x) => await deleteDoc(x.ref)),
|
||||||
...userParticipantGroup.docs.map(
|
...userParticipantGroup.docs.map(
|
||||||
async (x) =>
|
async (x) =>
|
||||||
await setDoc(
|
await setDoc(
|
||||||
x.ref,
|
x.ref,
|
||||||
{
|
{
|
||||||
participants: x.data().participants.filter((y: string) => y !== id),
|
participants: x.data().participants.filter((y: string) => y !== id),
|
||||||
},
|
},
|
||||||
{ merge: true }
|
{merge: true},
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.session.user) {
|
if (req.session.user) {
|
||||||
const docUser = await getDoc(doc(db, "users", req.session.user.id));
|
const docUser = await getDoc(doc(db, "users", req.session.user.id));
|
||||||
if (!docUser.exists()) {
|
if (!docUser.exists()) {
|
||||||
res.status(401).json(undefined);
|
res.status(401).json(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = docUser.data() as User;
|
const user = docUser.data() as User;
|
||||||
|
|
||||||
const permissionDocs = await getPermissionDocs();
|
|
||||||
|
|
||||||
const userWithPermissions = {
|
req.session.user = {
|
||||||
...user,
|
...user,
|
||||||
permissions: getPermissions(req.session.user.id, permissionDocs),
|
id: req.session.user.id,
|
||||||
};
|
};
|
||||||
req.session.user = {
|
await req.session.save();
|
||||||
...userWithPermissions,
|
|
||||||
id: req.session.user.id,
|
|
||||||
};
|
|
||||||
await req.session.save();
|
|
||||||
|
|
||||||
res.json({ ...userWithPermissions, id: req.session.user.id });
|
res.json({...user, id: req.session.user.id});
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json(undefined);
|
res.status(401).json(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import BatchCodeGenerator from "./(admin)/BatchCodeGenerator";
|
|||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import ExamGenerator from "./(admin)/ExamGenerator";
|
import ExamGenerator from "./(admin)/ExamGenerator";
|
||||||
import BatchCreateUser from "./(admin)/BatchCreateUser";
|
import BatchCreateUser from "./(admin)/BatchCreateUser";
|
||||||
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
@@ -43,6 +44,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
|
|
||||||
export default function Admin() {
|
export default function Admin() {
|
||||||
const {user} = useUser({redirectTo: "/login"});
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -60,11 +62,11 @@ export default function Admin() {
|
|||||||
<Layout user={user} className="gap-6">
|
<Layout user={user} className="gap-6">
|
||||||
<section className="w-full flex -md:flex-col -xl:gap-2 gap-8 justify-between">
|
<section className="w-full flex -md:flex-col -xl:gap-2 gap-8 justify-between">
|
||||||
<ExamLoader />
|
<ExamLoader />
|
||||||
<BatchCreateUser user={user} />
|
<BatchCreateUser user={user} />
|
||||||
{checkAccess(user, getTypesOfUser(["teacher"]), 'viewCodes') && (
|
{checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && (
|
||||||
<>
|
<>
|
||||||
<CodeGenerator user={user} />
|
<CodeGenerator user={user} />
|
||||||
<BatchCodeGenerator user={user} />
|
<BatchCodeGenerator user={user} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,45 +1,42 @@
|
|||||||
import { PermissionType } from "@/interfaces/permissions";
|
import {PermissionType} from "@/interfaces/permissions";
|
||||||
import { User, Type, userTypes } from "@/interfaces/user";
|
import {User, Type, userTypes} from "@/interfaces/user";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
export function checkAccess(
|
export function checkAccess(user: User, types: Type[], permissions?: PermissionType[], permission?: PermissionType) {
|
||||||
user: User,
|
if (!user) {
|
||||||
types: Type[],
|
return false;
|
||||||
permission?: PermissionType
|
}
|
||||||
) {
|
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if(user.type === '') {
|
// if(user.type === '') {
|
||||||
if (!user.type) {
|
if (!user.type) {
|
||||||
console.warn("User type is empty");
|
console.warn("User type is empty");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (types.length === 0) {
|
if (types.length === 0) {
|
||||||
console.warn("No types provided");
|
console.warn("No types provided");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!types.includes(user.type)) {
|
if (!types.includes(user.type)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we may not want a permission check as most screens dont even havr a specific permission
|
// we may not want a permission check as most screens dont even havr a specific permission
|
||||||
if (permission) {
|
if (permission) {
|
||||||
// this works more like a blacklist
|
// this works more like a blacklist
|
||||||
// therefore if we don't find the permission here, he can't do it
|
// therefore if we don't find the permission here, he can't do it
|
||||||
if (!(user.permissions || []).includes(permission)) {
|
if (!(permissions || []).includes(permission)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTypesOfUser(types: Type[]) {
|
export function getTypesOfUser(types: Type[]) {
|
||||||
// basicly generate a list of all types except the excluded ones
|
// basicly generate a list of all types except the excluded ones
|
||||||
return userTypes.filter((userType) => {
|
return userTypes.filter((userType) => {
|
||||||
return !types.includes(userType);
|
return !types.includes(userType);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user