Merge branch 'develop' of https://bitbucket.org/ecropdev/ielts-ui into feature/level-file-upload
This commit is contained in:
@@ -1,11 +1,5 @@
|
|||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {
|
import {Ticket, TicketStatus, TicketStatusLabel, TicketType, TicketTypeLabel} from "@/interfaces/ticket";
|
||||||
Ticket,
|
|
||||||
TicketStatus,
|
|
||||||
TicketStatusLabel,
|
|
||||||
TicketType,
|
|
||||||
TicketTypeLabel,
|
|
||||||
} from "@/interfaces/ticket";
|
|
||||||
import {User} from "@/interfaces/user";
|
import {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";
|
||||||
@@ -31,16 +25,13 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
|
|||||||
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
|
||||||
@@ -87,37 +78,23 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
|
|||||||
|
|
||||||
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
|
|
||||||
</label>
|
|
||||||
<Select
|
<Select
|
||||||
options={Object.keys(TicketStatusLabel).map((x) => ({
|
options={Object.keys(TicketStatusLabel).map((x) => ({
|
||||||
value: x,
|
value: x,
|
||||||
label: TicketStatusLabel[x as keyof typeof TicketStatusLabel],
|
label: TicketStatusLabel[x as keyof typeof TicketStatusLabel],
|
||||||
}))}
|
}))}
|
||||||
value={{value: status, label: TicketStatusLabel[status]}}
|
value={{value: status, label: TicketStatusLabel[status]}}
|
||||||
onChange={(value) =>
|
onChange={(value) => setStatus((value?.value as TicketStatus) ?? undefined)}
|
||||||
setStatus((value?.value as TicketStatus) ?? undefined)
|
|
||||||
}
|
|
||||||
placeholder="Status..."
|
placeholder="Status..."
|
||||||
/>
|
/>
|
||||||
</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">Type</label>
|
||||||
Type
|
|
||||||
</label>
|
|
||||||
<Select
|
<Select
|
||||||
options={Object.keys(TicketTypeLabel).map((x) => ({
|
options={Object.keys(TicketTypeLabel).map((x) => ({
|
||||||
value: x,
|
value: x,
|
||||||
@@ -131,9 +108,7 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
|
|||||||
</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
|
|
||||||
</label>
|
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
{value: "me", label: "Assign to me"},
|
{value: "me", label: "Assign to me"},
|
||||||
@@ -153,52 +128,20 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
|
|||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onChange={(value) =>
|
onChange={(value) => (value ? setAssignedTo(value.value === "me" ? user.id : value.value) : setAssignedTo(null))}
|
||||||
value
|
|
||||||
? setAssignedTo(value.value === "me" ? user.id : value.value)
|
|
||||||
: setAssignedTo(null)
|
|
||||||
}
|
|
||||||
placeholder="Assignee..."
|
placeholder="Assignee..."
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</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"
|
|
||||||
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>
|
||||||
|
|
||||||
<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"
|
|
||||||
name="reporter"
|
|
||||||
onChange={() => null}
|
|
||||||
value={reporter.name}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Reporter's E-mail"
|
|
||||||
type="text"
|
|
||||||
name="reporter"
|
|
||||||
onChange={() => null}
|
|
||||||
value={reporter.email}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<Input
|
<Input
|
||||||
label="Reporter's Type"
|
label="Reporter's Type"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -218,34 +161,15 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<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"
|
|
||||||
color="red"
|
|
||||||
className="w-full md:max-w-[200px]"
|
|
||||||
variant="outline"
|
|
||||||
onClick={del}
|
|
||||||
isLoading={isLoading}
|
|
||||||
>
|
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</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"
|
|
||||||
color="red"
|
|
||||||
className="w-full md:max-w-[200px]"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onClose}
|
|
||||||
isLoading={isLoading}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="button" className="w-full md:max-w-[200px]" isLoading={isLoading} onClick={submit}>
|
||||||
type="button"
|
|
||||||
className="w-full md:max-w-[200px]"
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClick={submit}
|
|
||||||
>
|
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
36
src/components/List.tsx
Normal file
36
src/components/List.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {Column, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
export default function List<T>({data, columns}: {data: T[]; columns: any[]}) {
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns: columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<th className="p-4 text-left" key={header.id}>
|
||||||
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody className="px-2">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td className="px-4 py-2" key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
import { Permission } from "@/interfaces/permissions";
|
import { Permission } from "@/interfaces/permissions";
|
||||||
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
import {
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
Row,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { convertCamelCaseToReadable } from "@/utils/string";
|
import { convertCamelCaseToReadable } from "@/utils/string";
|
||||||
|
|
||||||
@@ -16,7 +23,8 @@ const defaultColumns = [
|
|||||||
<Link
|
<Link
|
||||||
href={`/permissions/${row.original.id}`}
|
href={`/permissions/${row.original.id}`}
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className="underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer">
|
className="underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
||||||
|
>
|
||||||
{convertCamelCaseToReadable(getValue() as string)}
|
{convertCamelCaseToReadable(getValue() as string)}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
@@ -29,6 +37,18 @@ export default function PermissionList({permissions}: Props) {
|
|||||||
columns: defaultColumns,
|
columns: defaultColumns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const groupedData: { [key: string]: Row<Permission>[] } = table
|
||||||
|
.getRowModel()
|
||||||
|
.rows.reduce((groups: { [key: string]: Row<Permission>[] }, row) => {
|
||||||
|
const parent = row.original.topic;
|
||||||
|
if (!groups[parent]) {
|
||||||
|
groups[parent] = [];
|
||||||
|
}
|
||||||
|
groups[parent].push(row);
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
@@ -38,22 +58,45 @@ export default function PermissionList({permissions}: Props) {
|
|||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th className="py-4 px-4 text-left" key={header.id}>
|
<th className="py-4 px-4 text-left" key={header.id}>
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="px-2">
|
<tbody className="px-2">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{Object.keys(groupedData).map((parent) => (
|
||||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
<React.Fragment key={parent}>
|
||||||
|
<tr>
|
||||||
|
<td className="px-2 py-2 items-center w-fit">
|
||||||
|
<strong>{parent}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{groupedData[parent].map((row, i) => (
|
||||||
|
<tr
|
||||||
|
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
||||||
|
key={row.id}
|
||||||
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
<td
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
className="px-4 py-2 items-center w-fit"
|
||||||
|
key={cell.id}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
BsCurrencyDollar,
|
BsCurrencyDollar,
|
||||||
BsClipboardData,
|
BsClipboardData,
|
||||||
BsFileLock,
|
BsFileLock,
|
||||||
|
BsPeople,
|
||||||
} 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";
|
||||||
@@ -28,6 +29,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 +82,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 +101,25 @@ 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, ["developer", "admin", "teacher", "student"], permissions) && (
|
||||||
|
<Nav disabled={disableNavigation} Icon={BsPeople} label="Groups" path={path} keyPath="/groups" isMinimized={isMinimized} />
|
||||||
|
)}
|
||||||
|
{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 +139,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}
|
||||||
@@ -144,8 +150,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
badge={totalAssignedTickets}
|
badge={totalAssignedTickets}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["developer", "admin"]) && (
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||||
<>
|
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsCloudFill}
|
Icon={BsCloudFill}
|
||||||
@@ -154,6 +159,8 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
keyPath="/generation"
|
keyPath="/generation"
|
||||||
isMinimized={isMinimized}
|
isMinimized={isMinimized}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate", "agent"]) && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsFileLock}
|
Icon={BsFileLock}
|
||||||
@@ -162,20 +169,19 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
keyPath="/permissions"
|
keyPath="/permissions"
|
||||||
isMinimized={isMinimized}
|
isMinimized={isMinimized}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="-xl:flex flex-col gap-3 xl:hidden">
|
<div className="-xl:flex flex-col gap-3 xl:hidden">
|
||||||
<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"])) && (
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
import {
|
import {CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type} from "@/interfaces/user";
|
||||||
CorporateInformation,
|
|
||||||
CorporateUser,
|
|
||||||
EMPLOYMENT_STATUS,
|
|
||||||
User,
|
|
||||||
Type,
|
|
||||||
} from "@/interfaces/user";
|
|
||||||
import {groupBySession, averageScore} from "@/utils/stats";
|
import {groupBySession, averageScore} from "@/utils/stats";
|
||||||
import {RadioGroup} from "@headlessui/react";
|
import {RadioGroup} from "@headlessui/react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -14,13 +8,7 @@ import moment from "moment";
|
|||||||
import {Divider} from "primereact/divider";
|
import {Divider} from "primereact/divider";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import {
|
import {BsFileEarmarkText, BsPencil, BsPerson, BsPersonAdd, BsStar} from "react-icons/bs";
|
||||||
BsFileEarmarkText,
|
|
||||||
BsPencil,
|
|
||||||
BsPerson,
|
|
||||||
BsPersonAdd,
|
|
||||||
BsStar,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import Button from "./Low/Button";
|
import Button from "./Low/Button";
|
||||||
import Checkbox from "./Low/Checkbox";
|
import Checkbox from "./Low/Checkbox";
|
||||||
@@ -35,17 +23,15 @@ import useCodes from "@/hooks/useCodes";
|
|||||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
import {PermissionType} from "@/interfaces/permissions";
|
import {PermissionType} from "@/interfaces/permissions";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
|
||||||
const expirationDateColor = (date: Date) => {
|
const expirationDateColor = (date: Date) => {
|
||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new Date());
|
const today = moment(new Date());
|
||||||
|
|
||||||
if (today.add(1, "days").isAfter(momentDate))
|
if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light";
|
||||||
return "!bg-mti-red-ultralight border-mti-red-light";
|
if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light";
|
||||||
if (today.add(3, "days").isAfter(momentDate))
|
if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
|
||||||
return "!bg-mti-rose-ultralight border-mti-rose-light";
|
|
||||||
if (today.add(7, "days").isAfter(momentDate))
|
|
||||||
return "!bg-mti-orange-ultralight border-mti-orange-light";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -86,81 +72,35 @@ const CURRENCIES_OPTIONS = CURRENCIES.map(({ label, currency }) => ({
|
|||||||
label,
|
label,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const UserCard = ({
|
const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, onViewCorporate, disabled = false, disabledFields = {}}: Props) => {
|
||||||
user,
|
const [expiryDate, setExpiryDate] = useState<Date | null | undefined>(user.subscriptionExpirationDate);
|
||||||
loggedInUser,
|
|
||||||
onClose,
|
|
||||||
onViewStudents,
|
|
||||||
onViewTeachers,
|
|
||||||
onViewCorporate,
|
|
||||||
disabled = false,
|
|
||||||
disabledFields = {},
|
|
||||||
}: Props) => {
|
|
||||||
const [expiryDate, setExpiryDate] = useState<Date | null | undefined>(
|
|
||||||
user.subscriptionExpirationDate
|
|
||||||
);
|
|
||||||
const [type, setType] = useState(user.type);
|
const [type, setType] = useState(user.type);
|
||||||
const [status, setStatus] = useState(user.status);
|
const [status, setStatus] = useState(user.status);
|
||||||
const [referralAgentLabel, setReferralAgentLabel] = useState<string>();
|
const [referralAgentLabel, setReferralAgentLabel] = useState<string>();
|
||||||
const [position, setPosition] = useState<string | undefined>(
|
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
|
||||||
user.type === "corporate"
|
const [passport_id, setPassportID] = useState<string | undefined>(user.type === "student" ? user.demographicInformation?.passport_id : undefined);
|
||||||
? user.demographicInformation?.position
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
const [passport_id, setPassportID] = useState<string | undefined>(
|
|
||||||
user.type === "student"
|
|
||||||
? user.demographicInformation?.passport_id
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const [referralAgent, setReferralAgent] = useState(
|
const [referralAgent, setReferralAgent] = useState(user.type === "corporate" ? user.corporateInformation?.referralAgent : undefined);
|
||||||
user.type === "corporate"
|
|
||||||
? user.corporateInformation?.referralAgent
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
const [companyName, setCompanyName] = useState(
|
const [companyName, setCompanyName] = useState(
|
||||||
user.type === "corporate"
|
user.type === "corporate"
|
||||||
? user.corporateInformation?.companyInformation.name
|
? user.corporateInformation?.companyInformation.name
|
||||||
: user.type === "agent"
|
: user.type === "agent"
|
||||||
? user.agentInformation?.companyName
|
? user.agentInformation?.companyName
|
||||||
: undefined
|
: undefined,
|
||||||
);
|
|
||||||
const [arabName, setArabName] = useState(
|
|
||||||
user.type === "agent" ? user.agentInformation?.companyArabName : undefined
|
|
||||||
);
|
);
|
||||||
|
const [arabName, setArabName] = useState(user.type === "agent" ? user.agentInformation?.companyArabName : undefined);
|
||||||
const [commercialRegistration, setCommercialRegistration] = useState(
|
const [commercialRegistration, setCommercialRegistration] = useState(
|
||||||
user.type === "agent"
|
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
|
||||||
? user.agentInformation?.commercialRegistration
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
const [userAmount, setUserAmount] = useState(
|
|
||||||
user.type === "corporate"
|
|
||||||
? user.corporateInformation?.companyInformation.userAmount
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
const [paymentValue, setPaymentValue] = useState(
|
|
||||||
user.type === "corporate"
|
|
||||||
? user.corporateInformation?.payment?.value
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
const [paymentCurrency, setPaymentCurrency] = useState(
|
|
||||||
user.type === "corporate"
|
|
||||||
? user.corporateInformation?.payment?.currency
|
|
||||||
: "EUR"
|
|
||||||
);
|
|
||||||
const [monthlyDuration, setMonthlyDuration] = useState(
|
|
||||||
user.type === "corporate"
|
|
||||||
? user.corporateInformation?.monthlyDuration
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
const [commissionValue, setCommission] = useState(
|
|
||||||
user.type === "corporate"
|
|
||||||
? user.corporateInformation?.payment?.commission
|
|
||||||
: undefined
|
|
||||||
);
|
);
|
||||||
|
const [userAmount, setUserAmount] = useState(user.type === "corporate" ? user.corporateInformation?.companyInformation.userAmount : undefined);
|
||||||
|
const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined);
|
||||||
|
const [paymentCurrency, setPaymentCurrency] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.currency : "EUR");
|
||||||
|
const [monthlyDuration, setMonthlyDuration] = useState(user.type === "corporate" ? user.corporateInformation?.monthlyDuration : undefined);
|
||||||
|
const [commissionValue, setCommission] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.commission : undefined);
|
||||||
const {stats} = useStats(user.id);
|
const {stats} = useStats(user.id);
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {codes} = useCodes(user.id);
|
const {codes} = useCodes(user.id);
|
||||||
|
const {permissions} = usePermissions(loggedInUser.id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (users && users.length > 0) {
|
if (users && users.length > 0) {
|
||||||
@@ -176,11 +116,8 @@ const UserCard = ({
|
|||||||
|
|
||||||
const updateUser = () => {
|
const updateUser = () => {
|
||||||
if (user.type === "corporate" && (!paymentValue || paymentValue < 0))
|
if (user.type === "corporate" && (!paymentValue || paymentValue < 0))
|
||||||
return toast.error(
|
return toast.error("Please set a price for the user's package before updating!");
|
||||||
"Please set a price for the user's package before updating!"
|
if (!confirm(`Are you sure you want to update ${user.name}'s account?`)) return;
|
||||||
);
|
|
||||||
if (!confirm(`Are you sure you want to update ${user.name}'s account?`))
|
|
||||||
return;
|
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
||||||
@@ -208,9 +145,7 @@ const UserCard = ({
|
|||||||
payment: {
|
payment: {
|
||||||
value: paymentValue,
|
value: paymentValue,
|
||||||
currency: paymentCurrency,
|
currency: paymentCurrency,
|
||||||
...(referralAgent === ""
|
...(referralAgent === "" ? {} : {commission: commissionValue}),
|
||||||
? {}
|
|
||||||
: { commission: commissionValue }),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -226,9 +161,7 @@ const UserCard = ({
|
|||||||
|
|
||||||
const generalProfileItems = [
|
const generalProfileItems = [
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
||||||
<BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
|
||||||
),
|
|
||||||
value: Object.keys(groupBySession(stats)).length,
|
value: Object.keys(groupBySession(stats)).length,
|
||||||
label: "Exams",
|
label: "Exams",
|
||||||
},
|
},
|
||||||
@@ -248,16 +181,12 @@ const UserCard = ({
|
|||||||
user.type === "corporate"
|
user.type === "corporate"
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsPerson className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
||||||
<BsPerson className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
|
||||||
),
|
|
||||||
value: codes.length,
|
value: codes.length,
|
||||||
label: "Users Used",
|
label: "Users Used",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsPersonAdd className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
||||||
<BsPersonAdd className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
|
||||||
),
|
|
||||||
value: user.corporateInformation.companyInformation.userAmount,
|
value: user.corporateInformation.companyInformation.userAmount,
|
||||||
label: "Number of Users",
|
label: "Number of Users",
|
||||||
},
|
},
|
||||||
@@ -270,14 +199,7 @@ const UserCard = ({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProfileSummary
|
<ProfileSummary user={user} items={user.type === "corporate" ? corporateProfileItems : generalProfileItems} />
|
||||||
user={user}
|
|
||||||
items={
|
|
||||||
user.type === "corporate"
|
|
||||||
? corporateProfileItems
|
|
||||||
: generalProfileItems
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{user.type === "agent" && (
|
{user.type === "agent" && (
|
||||||
<>
|
<>
|
||||||
@@ -347,9 +269,7 @@ const UserCard = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-3 w-full lg:col-span-3">
|
<div className="flex flex-col gap-3 w-full lg:col-span-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Pricing</label>
|
||||||
Pricing
|
|
||||||
</label>
|
|
||||||
<div className="w-full grid grid-cols-6 gap-2">
|
<div className="w-full grid grid-cols-6 gap-2">
|
||||||
<Input
|
<Input
|
||||||
name="paymentValue"
|
name="paymentValue"
|
||||||
@@ -362,13 +282,10 @@ const UserCard = ({
|
|||||||
<Select
|
<Select
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-4 py-4 col-span-3 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none",
|
"px-4 py-4 col-span-3 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none",
|
||||||
disabled &&
|
disabled && "!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
|
||||||
"!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed"
|
|
||||||
)}
|
)}
|
||||||
options={CURRENCIES_OPTIONS}
|
options={CURRENCIES_OPTIONS}
|
||||||
value={CURRENCIES_OPTIONS.find(
|
value={CURRENCIES_OPTIONS.find((c) => c.value === paymentCurrency)}
|
||||||
(c) => c.value === paymentCurrency
|
|
||||||
)}
|
|
||||||
onChange={(value) => setPaymentCurrency(value?.value)}
|
onChange={(value) => setPaymentCurrency(value?.value)}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -384,11 +301,7 @@ const UserCard = ({
|
|||||||
}),
|
}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
? "#D5D9F0"
|
|
||||||
: state.isSelected
|
|
||||||
? "#7872BF"
|
|
||||||
: "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
@@ -399,19 +312,13 @@ const UserCard = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 w-full">
|
<div className="flex gap-3 w-full">
|
||||||
<div className="flex flex-col gap-3 w-8/12">
|
<div className="flex flex-col gap-3 w-8/12">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Country Manager</label>
|
||||||
Country Manager
|
|
||||||
</label>
|
|
||||||
{referralAgentLabel && (
|
{referralAgentLabel && (
|
||||||
<Select
|
<Select
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none",
|
"px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none",
|
||||||
(checkAccess(
|
(checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"])) || disabledFields.countryManager) &&
|
||||||
loggedInUser,
|
"!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
|
||||||
getTypesOfUser(["developer", "admin"])
|
|
||||||
) ||
|
|
||||||
disabledFields.countryManager) &&
|
|
||||||
"!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed"
|
|
||||||
)}
|
)}
|
||||||
options={[
|
options={[
|
||||||
{value: "", label: "No referral"},
|
{value: "", label: "No referral"},
|
||||||
@@ -441,30 +348,19 @@ const UserCard = ({
|
|||||||
}),
|
}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
? "#D5D9F0"
|
|
||||||
: state.isSelected
|
|
||||||
? "#7872BF"
|
|
||||||
: "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
// editing country manager should only be available for dev/admin
|
// editing country manager should only be available for dev/admin
|
||||||
isDisabled={
|
isDisabled={checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"])) || disabledFields.countryManager}
|
||||||
checkAccess(
|
|
||||||
loggedInUser,
|
|
||||||
getTypesOfUser(["developer", "admin"])
|
|
||||||
) || disabledFields.countryManager
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-4/12">
|
<div className="flex flex-col gap-3 w-4/12">
|
||||||
{referralAgent !== "" && loggedInUser.type !== "corporate" ? (
|
{referralAgent !== "" && loggedInUser.type !== "corporate" ? (
|
||||||
<>
|
<>
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Commission</label>
|
||||||
Commission
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
name="commissionValue"
|
name="commissionValue"
|
||||||
onChange={(e) => setCommission(e ? parseInt(e) : undefined)}
|
onChange={(e) => setCommission(e ? parseInt(e) : undefined)}
|
||||||
@@ -506,13 +402,8 @@ const UserCard = ({
|
|||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Country</label>
|
||||||
Country
|
<CountrySelect disabled value={user.demographicInformation?.country} />
|
||||||
</label>
|
|
||||||
<CountrySelect
|
|
||||||
disabled
|
|
||||||
value={user.demographicInformation?.country}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="tel"
|
type="tel"
|
||||||
@@ -532,11 +423,7 @@ const UserCard = ({
|
|||||||
label="Passport/National ID"
|
label="Passport/National ID"
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
placeholder="Enter National ID or Passport number"
|
placeholder="Enter National ID or Passport number"
|
||||||
value={
|
value={user.type === "student" ? user.demographicInformation?.passport_id : undefined}
|
||||||
user.type === "student"
|
|
||||||
? user.demographicInformation?.passport_id
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
disabled
|
disabled
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -545,14 +432,11 @@ const UserCard = ({
|
|||||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||||
{user.type !== "corporate" && user.type !== "mastercorporate" && (
|
{user.type !== "corporate" && user.type !== "mastercorporate" && (
|
||||||
<div className="relative flex flex-col gap-3 w-full">
|
<div className="relative flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Employment Status</label>
|
||||||
Employment Status
|
|
||||||
</label>
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={user.demographicInformation?.employment}
|
value={user.demographicInformation?.employment}
|
||||||
className="grid grid-cols-2 items-center gap-4 place-items-center"
|
className="grid grid-cols-2 items-center gap-4 place-items-center"
|
||||||
disabled={disabled}
|
disabled={disabled}>
|
||||||
>
|
|
||||||
{EMPLOYMENT_STATUS.map(({status, label}) => (
|
{EMPLOYMENT_STATUS.map(({status, label}) => (
|
||||||
<RadioGroup.Option value={status} key={status}>
|
<RadioGroup.Option value={status} key={status}>
|
||||||
{({checked}) => (
|
{({checked}) => (
|
||||||
@@ -562,9 +446,8 @@ const UserCard = ({
|
|||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
!checked
|
!checked
|
||||||
? "bg-white border-mti-gray-platinum"
|
? "bg-white border-mti-gray-platinum"
|
||||||
: "bg-mti-purple-light border-mti-purple-dark text-white"
|
: "bg-mti-purple-light border-mti-purple-dark text-white",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -587,14 +470,11 @@ const UserCard = ({
|
|||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex flex-col gap-8 w-full">
|
||||||
<div className="relative flex flex-col gap-3 w-full">
|
<div className="relative flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Gender</label>
|
||||||
Gender
|
|
||||||
</label>
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={user.demographicInformation?.gender}
|
value={user.demographicInformation?.gender}
|
||||||
className="flex flex-row gap-4 justify-between"
|
className="flex flex-row gap-4 justify-between"
|
||||||
disabled={disabled}
|
disabled={disabled}>
|
||||||
>
|
|
||||||
<RadioGroup.Option value="male">
|
<RadioGroup.Option value="male">
|
||||||
{({checked}) => (
|
{({checked}) => (
|
||||||
<span
|
<span
|
||||||
@@ -603,9 +483,8 @@ const UserCard = ({
|
|||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
!checked
|
!checked
|
||||||
? "bg-white border-mti-gray-platinum"
|
? "bg-white border-mti-gray-platinum"
|
||||||
: "bg-mti-purple-light border-mti-purple-dark text-white"
|
: "bg-mti-purple-light border-mti-purple-dark text-white",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
Male
|
Male
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -618,9 +497,8 @@ const UserCard = ({
|
|||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
!checked
|
!checked
|
||||||
? "bg-white border-mti-gray-platinum"
|
? "bg-white border-mti-gray-platinum"
|
||||||
: "bg-mti-purple-light border-mti-purple-dark text-white"
|
: "bg-mti-purple-light border-mti-purple-dark text-white",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
Female
|
Female
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -633,9 +511,8 @@ const UserCard = ({
|
|||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
!checked
|
!checked
|
||||||
? "bg-white border-mti-gray-platinum"
|
? "bg-white border-mti-gray-platinum"
|
||||||
: "bg-mti-purple-light border-mti-purple-dark text-white"
|
: "bg-mti-purple-light border-mti-purple-dark text-white",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
Other
|
Other
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -644,20 +521,13 @@ const UserCard = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Expiry Date</label>
|
||||||
Expiry Date
|
|
||||||
</label>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isChecked={!!expiryDate}
|
isChecked={!!expiryDate}
|
||||||
onChange={(checked) =>
|
onChange={(checked) => setExpiryDate(checked ? user.subscriptionExpirationDate || new Date() : null)}
|
||||||
setExpiryDate(
|
disabled={
|
||||||
checked
|
disabled || (!["admin", "developer"].includes(loggedInUser.type) && !!loggedInUser.subscriptionExpirationDate)
|
||||||
? user.subscriptionExpirationDate || new Date()
|
}>
|
||||||
: null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
Enabled
|
Enabled
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -666,12 +536,9 @@ const UserCard = ({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-6 w-full flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-6 w-full flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
!expiryDate
|
!expiryDate ? "!bg-mti-green-ultralight !border-mti-green-light" : expirationDateColor(expiryDate),
|
||||||
? "!bg-mti-green-ultralight !border-mti-green-light"
|
"bg-white border-mti-gray-platinum",
|
||||||
: expirationDateColor(expiryDate),
|
)}>
|
||||||
"bg-white border-mti-gray-platinum"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!expiryDate && "Unlimited"}
|
{!expiryDate && "Unlimited"}
|
||||||
{expiryDate && moment(expiryDate).format("DD/MM/YYYY")}
|
{expiryDate && moment(expiryDate).format("DD/MM/YYYY")}
|
||||||
</div>
|
</div>
|
||||||
@@ -682,14 +549,12 @@ const UserCard = ({
|
|||||||
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"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",
|
"hover:border-mti-purple tooltip",
|
||||||
expirationDateColor(expiryDate),
|
expirationDateColor(expiryDate),
|
||||||
"transition duration-300 ease-in-out"
|
"transition duration-300 ease-in-out",
|
||||||
)}
|
)}
|
||||||
filterDate={(date) =>
|
filterDate={(date) =>
|
||||||
moment(date).isAfter(new Date()) &&
|
moment(date).isAfter(new Date()) &&
|
||||||
(loggedInUser.subscriptionExpirationDate
|
(loggedInUser.subscriptionExpirationDate
|
||||||
? moment(date).isBefore(
|
? moment(date).isBefore(moment(loggedInUser.subscriptionExpirationDate))
|
||||||
moment(loggedInUser.subscriptionExpirationDate)
|
|
||||||
)
|
|
||||||
: true)
|
: true)
|
||||||
}
|
}
|
||||||
dateFormat="dd/MM/yyyy"
|
dateFormat="dd/MM/yyyy"
|
||||||
@@ -701,22 +566,26 @@ const UserCard = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{checkAccess(loggedInUser, ["developer", "admin"]) && (
|
{checkAccess(
|
||||||
|
loggedInUser,
|
||||||
|
["developer", "admin", "corporate", "mastercorporate"],
|
||||||
|
permissions,
|
||||||
|
user.type === "teacher" ? "editTeacher" : user.type === "student" ? "editStudent" : undefined,
|
||||||
|
) && (
|
||||||
<>
|
<>
|
||||||
<Divider className="w-full !m-0" />
|
<Divider className="w-full !m-0" />
|
||||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Status</label>
|
||||||
Status
|
|
||||||
</label>
|
|
||||||
<Select
|
<Select
|
||||||
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||||
options={USER_STATUS_OPTIONS}
|
options={USER_STATUS_OPTIONS.filter((x) => {
|
||||||
|
if (checkAccess(loggedInUser, ["admin", "developer"])) return true;
|
||||||
|
return x.value !== "paymentDue";
|
||||||
|
})}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
|
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
|
||||||
onChange={(value) =>
|
onChange={(value) => setStatus(value?.value as typeof user.status)}
|
||||||
setStatus(value?.value as typeof user.status)
|
|
||||||
}
|
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
@@ -730,11 +599,7 @@ const UserCard = ({
|
|||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
? "#D5D9F0"
|
|
||||||
: state.isSelected
|
|
||||||
? "#7872BF"
|
|
||||||
: "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
@@ -742,17 +607,34 @@ const UserCard = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Type</label>
|
||||||
Type
|
|
||||||
</label>
|
|
||||||
<Select
|
<Select
|
||||||
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||||
options={USER_TYPE_OPTIONS}
|
options={USER_TYPE_OPTIONS.filter((x) => {
|
||||||
|
if (x.value === "student")
|
||||||
|
return checkAccess(
|
||||||
|
loggedInUser,
|
||||||
|
["developer", "admin", "corporate", "mastercorporate"],
|
||||||
|
permissions,
|
||||||
|
"editStudent",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (x.value === "teacher")
|
||||||
|
return checkAccess(
|
||||||
|
loggedInUser,
|
||||||
|
["developer", "admin", "corporate", "mastercorporate"],
|
||||||
|
permissions,
|
||||||
|
"editTeacher",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (x.value === "corporate")
|
||||||
|
return checkAccess(loggedInUser, ["developer", "admin", "mastercorporate"], permissions, "editCorporate");
|
||||||
|
|
||||||
|
return checkAccess(loggedInUser, ["developer", "admin"]);
|
||||||
|
})}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={document?.body}
|
||||||
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
|
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
|
||||||
onChange={(value) =>
|
onChange={(value) => setType(value?.value as typeof user.type)}
|
||||||
setType(value?.value as typeof user.type)
|
|
||||||
}
|
|
||||||
styles={{
|
styles={{
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
@@ -766,11 +648,7 @@ const UserCard = ({
|
|||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
? "#D5D9F0"
|
|
||||||
: state.isSelected
|
|
||||||
? "#7872BF"
|
|
||||||
: "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
@@ -785,56 +663,29 @@ const UserCard = ({
|
|||||||
<div className="flex gap-4 justify-between mt-4 w-full">
|
<div className="flex gap-4 justify-between mt-4 w-full">
|
||||||
<div className="self-start flex gap-4 justify-start items-center w-full">
|
<div className="self-start flex gap-4 justify-start items-center w-full">
|
||||||
{onViewCorporate && ["student", "teacher"].includes(user.type) && (
|
{onViewCorporate && ["student", "teacher"].includes(user.type) && (
|
||||||
<Button
|
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewCorporate}>
|
||||||
className="w-full max-w-[200px]"
|
|
||||||
variant="outline"
|
|
||||||
color="rose"
|
|
||||||
onClick={onViewCorporate}
|
|
||||||
>
|
|
||||||
View Corporate
|
View Corporate
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onViewStudents && ["corporate", "teacher"].includes(user.type) && (
|
{onViewStudents && ["corporate", "teacher"].includes(user.type) && (
|
||||||
<Button
|
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewStudents}>
|
||||||
className="w-full max-w-[200px]"
|
|
||||||
variant="outline"
|
|
||||||
color="rose"
|
|
||||||
onClick={onViewStudents}
|
|
||||||
>
|
|
||||||
View Students
|
View Students
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onViewTeachers && ["student", "corporate"].includes(user.type) && (
|
{onViewTeachers && ["student", "corporate"].includes(user.type) && (
|
||||||
<Button
|
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewTeachers}>
|
||||||
className="w-full max-w-[200px]"
|
|
||||||
variant="outline"
|
|
||||||
color="rose"
|
|
||||||
onClick={onViewTeachers}
|
|
||||||
>
|
|
||||||
View Teachers
|
View Teachers
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-end flex gap-4 w-full justify-end">
|
<div className="self-end flex gap-4 w-full justify-end">
|
||||||
<Button
|
<Button className="w-full max-w-[200px]" variant="outline" onClick={onClose}>
|
||||||
className="w-full max-w-[200px]"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={disabled || !checkAccess(loggedInUser, updateUserPermission.list, permissions, updateUserPermission.perm)}
|
||||||
disabled ||
|
|
||||||
!checkAccess(
|
|
||||||
loggedInUser,
|
|
||||||
updateUserPermission.list,
|
|
||||||
updateUserPermission.perm
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={updateUser}
|
onClick={updateUser}
|
||||||
className="w-full max-w-[200px]"
|
className="w-full max-w-[200px]">
|
||||||
>
|
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
|
|
||||||
const {stats} = useStats(user.id);
|
const {stats} = useStats(user.id);
|
||||||
const {users, reload} = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const { groups } = useGroups();
|
const {groups} = useGroups({});
|
||||||
const {pending, done} = usePaymentStatusUsers();
|
const {pending, done} = usePaymentStatusUsers();
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
@@ -52,24 +52,17 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
useEffect(reload, [page]);
|
useEffect(reload, [page]);
|
||||||
|
|
||||||
const inactiveCountryManagerFilter = (x: User) =>
|
const inactiveCountryManagerFilter = (x: User) =>
|
||||||
x.type === "agent" &&
|
x.type === "agent" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
||||||
(x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
|
||||||
|
|
||||||
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
|
|
||||||
src={displayUser.profilePicture}
|
|
||||||
alt={displayUser.name}
|
|
||||||
className="rounded-full w-10 h-10"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>
|
<span>
|
||||||
{displayUser.type === "corporate"
|
{displayUser.type === "corporate"
|
||||||
? displayUser.corporateInformation?.companyInformation?.name ||
|
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
|
||||||
displayUser.name
|
|
||||||
: displayUser.name}
|
: displayUser.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
@@ -82,11 +75,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
(!!selectedUser
|
(!!selectedUser
|
||||||
? groups
|
? groups
|
||||||
.filter(
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
(g) =>
|
|
||||||
g.admin === selectedUser.id ||
|
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id)
|
.includes(x.id)
|
||||||
: true);
|
: true);
|
||||||
@@ -99,8 +88,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -116,11 +104,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
x.type === "teacher" &&
|
x.type === "teacher" &&
|
||||||
(!!selectedUser
|
(!!selectedUser
|
||||||
? groups
|
? groups
|
||||||
.filter(
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
(g) =>
|
|
||||||
g.admin === selectedUser.id ||
|
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id) || false
|
.includes(x.id) || false
|
||||||
: true);
|
: true);
|
||||||
@@ -133,8 +117,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -156,14 +139,11 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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">
|
<h2 className="text-2xl font-semibold">Country Managers ({total})</h2>
|
||||||
Country Managers ({total})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -178,8 +158,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -201,8 +180,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -224,14 +202,11 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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">
|
<h2 className="text-2xl font-semibold">Inactive Country Managers ({total})</h2>
|
||||||
Inactive Country Managers ({total})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -239,10 +214,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const InactiveStudentsList = () => {
|
const InactiveStudentsList = () => {
|
||||||
const filter = (x: User) =>
|
const filter = (x: User) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
||||||
x.type === "student" &&
|
|
||||||
(x.status === "disabled" ||
|
|
||||||
moment().isAfter(x.subscriptionExpirationDate));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
@@ -252,14 +224,11 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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">
|
<h2 className="text-2xl font-semibold">Inactive Students ({total})</h2>
|
||||||
Inactive Students ({total})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -267,10 +236,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const InactiveCorporateList = () => {
|
const InactiveCorporateList = () => {
|
||||||
const filter = (x: User) =>
|
const filter = (x: User) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
||||||
x.type === "corporate" &&
|
|
||||||
(x.status === "disabled" ||
|
|
||||||
moment().isAfter(x.subscriptionExpirationDate));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
@@ -280,14 +246,11 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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">
|
<h2 className="text-2xl font-semibold">Inactive Corporate ({total})</h2>
|
||||||
Inactive Corporate ({total})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -300,14 +263,11 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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">
|
<h2 className="text-2xl font-semibold">Corporate Students Levels</h2>
|
||||||
Corporate Students Levels
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<CorporateStudentsLevels />
|
<CorporateStudentsLevels />
|
||||||
</>
|
</>
|
||||||
@@ -348,15 +308,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsGlobeCentralSouthAsia}
|
Icon={BsGlobeCentralSouthAsia}
|
||||||
label="Countries"
|
label="Countries"
|
||||||
value={
|
value={[...new Set(users.filter((x) => x.demographicInformation).map((x) => x.demographicInformation?.country))].length}
|
||||||
[
|
|
||||||
...new Set(
|
|
||||||
users
|
|
||||||
.filter((x) => x.demographicInformation)
|
|
||||||
.map((x) => x.demographicInformation?.country)
|
|
||||||
),
|
|
||||||
].length
|
|
||||||
}
|
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
@@ -364,12 +316,8 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
label="Inactive Students"
|
label="Inactive Students"
|
||||||
value={
|
value={
|
||||||
users.filter(
|
users.filter((x) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))
|
||||||
(x) =>
|
.length
|
||||||
x.type === "student" &&
|
|
||||||
(x.status === "disabled" ||
|
|
||||||
moment().isAfter(x.subscriptionExpirationDate))
|
|
||||||
).length
|
|
||||||
}
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
@@ -385,22 +333,12 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
Icon={BsBank}
|
Icon={BsBank}
|
||||||
label="Inactive Corporate"
|
label="Inactive Corporate"
|
||||||
value={
|
value={
|
||||||
users.filter(
|
users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))
|
||||||
(x) =>
|
.length
|
||||||
x.type === "corporate" &&
|
|
||||||
(x.status === "disabled" ||
|
|
||||||
moment().isAfter(x.subscriptionExpirationDate))
|
|
||||||
).length
|
|
||||||
}
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard onClick={() => setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" />
|
||||||
onClick={() => setPage("paymentdone")}
|
|
||||||
Icon={BsCurrencyDollar}
|
|
||||||
label="Payment Done"
|
|
||||||
value={done.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("paymentpending")}
|
onClick={() => setPage("paymentpending")}
|
||||||
Icon={BsCurrencyDollar}
|
Icon={BsCurrencyDollar}
|
||||||
@@ -414,12 +352,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
label="Content Management System (CMS)"
|
label="Content Management System (CMS)"
|
||||||
color="green"
|
color="green"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard onClick={() => setPage("corporatestudentslevels")} Icon={BsPersonFill} label="Corporate Students Levels" color="purple" />
|
||||||
onClick={() => setPage("corporatestudentslevels")}
|
|
||||||
Icon={BsPersonFill}
|
|
||||||
label="Corporate Students Levels"
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
</section>
|
</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">
|
||||||
@@ -464,9 +397,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<span className="p-4">Unpaid Corporate</span>
|
<span className="p-4">Unpaid Corporate</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(
|
.filter((x) => x.type === "corporate" && x.status === "paymentDue")
|
||||||
(x) => x.type === "corporate" && x.status === "paymentDue"
|
|
||||||
)
|
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -480,10 +411,8 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
(x) =>
|
(x) =>
|
||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
x.subscriptionExpirationDate &&
|
x.subscriptionExpirationDate &&
|
||||||
moment().isAfter(
|
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||||
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||||
) &&
|
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate))
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -498,10 +427,8 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
(x) =>
|
(x) =>
|
||||||
x.type === "teacher" &&
|
x.type === "teacher" &&
|
||||||
x.subscriptionExpirationDate &&
|
x.subscriptionExpirationDate &&
|
||||||
moment().isAfter(
|
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||||
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||||
) &&
|
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate))
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -516,10 +443,8 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
(x) =>
|
(x) =>
|
||||||
x.type === "agent" &&
|
x.type === "agent" &&
|
||||||
x.subscriptionExpirationDate &&
|
x.subscriptionExpirationDate &&
|
||||||
moment().isAfter(
|
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||||
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||||
) &&
|
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate))
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -534,10 +459,8 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
(x) =>
|
(x) =>
|
||||||
x.type === "corporate" &&
|
x.type === "corporate" &&
|
||||||
x.subscriptionExpirationDate &&
|
x.subscriptionExpirationDate &&
|
||||||
moment().isAfter(
|
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||||
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||||
) &&
|
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate))
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -549,10 +472,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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(
|
.filter(
|
||||||
(x) =>
|
(x) => x.type === "student" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
||||||
x.type === "student" &&
|
|
||||||
x.subscriptionExpirationDate &&
|
|
||||||
moment().isAfter(moment(x.subscriptionExpirationDate))
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -564,10 +484,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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(
|
.filter(
|
||||||
(x) =>
|
(x) => x.type === "teacher" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
||||||
x.type === "teacher" &&
|
|
||||||
x.subscriptionExpirationDate &&
|
|
||||||
moment().isAfter(moment(x.subscriptionExpirationDate))
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -579,10 +496,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
<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(
|
.filter(
|
||||||
(x) =>
|
(x) => x.type === "agent" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
||||||
x.type === "agent" &&
|
|
||||||
x.subscriptionExpirationDate &&
|
|
||||||
moment().isAfter(moment(x.subscriptionExpirationDate))
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -595,9 +509,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
{users
|
{users
|
||||||
.filter(
|
.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === "corporate" &&
|
x.type === "corporate" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
|
||||||
x.subscriptionExpirationDate &&
|
|
||||||
moment().isAfter(moment(x.subscriptionExpirationDate))
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -621,8 +533,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" ||
|
selectedUser.type === "corporate" || selectedUser.type === "teacher"
|
||||||
selectedUser.type === "teacher"
|
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
@@ -632,11 +543,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter(
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
(g) =>
|
|
||||||
g.admin === selectedUser.id ||
|
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -646,8 +553,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
selectedUser.type === "corporate" ||
|
selectedUser.type === "corporate" || selectedUser.type === "student"
|
||||||
selectedUser.type === "student"
|
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
@@ -657,11 +563,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter(
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
(g) =>
|
|
||||||
g.admin === selectedUser.id ||
|
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -671,8 +573,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewCorporate={
|
onViewCorporate={
|
||||||
selectedUser.type === "teacher" ||
|
selectedUser.type === "teacher" || selectedUser.type === "student"
|
||||||
selectedUser.type === "student"
|
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-corporate",
|
id: "view-corporate",
|
||||||
@@ -682,9 +583,7 @@ export default function AdminDashboard({ user }: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) =>
|
.filter((g) => g.participants.includes(selectedUser.id))
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => [g.admin, ...g.participants])
|
.flatMap((g) => [g.admin, ...g.participants])
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,12 +7,7 @@ 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, BsPersonFill, BsBank, BsCurrencyDollar} from "react-icons/bs";
|
||||||
BsArrowLeft,
|
|
||||||
BsPersonFill,
|
|
||||||
BsBank,
|
|
||||||
BsCurrencyDollar,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
|
|
||||||
@@ -30,7 +25,6 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
|
|
||||||
const {stats} = useStats();
|
const {stats} = useStats();
|
||||||
const {users, reload} = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const { groups } = useGroups(user.id);
|
|
||||||
const {pending, done} = usePaymentStatusUsers();
|
const {pending, done} = usePaymentStatusUsers();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,34 +33,19 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
|
|
||||||
const corporateFilter = (user: User) => user.type === "corporate";
|
const corporateFilter = (user: User) => user.type === "corporate";
|
||||||
const referredCorporateFilter = (x: User) =>
|
const referredCorporateFilter = (x: User) =>
|
||||||
x.type === "corporate" &&
|
x.type === "corporate" && !!x.corporateInformation && x.corporateInformation.referralAgent === user.id;
|
||||||
!!x.corporateInformation &&
|
|
||||||
x.corporateInformation.referralAgent === user.id;
|
|
||||||
const inactiveReferredCorporateFilter = (x: User) =>
|
const inactiveReferredCorporateFilter = (x: User) =>
|
||||||
referredCorporateFilter(x) &&
|
referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
||||||
(x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
|
||||||
|
|
||||||
const UserDisplay = ({
|
const UserDisplay = ({displayUser, allowClick = true}: {displayUser: User; allowClick?: boolean}) => (
|
||||||
displayUser,
|
|
||||||
allowClick = true,
|
|
||||||
}: {
|
|
||||||
displayUser: User;
|
|
||||||
allowClick?: boolean;
|
|
||||||
}) => (
|
|
||||||
<div
|
<div
|
||||||
onClick={() => allowClick && setSelectedUser(displayUser)}
|
onClick={() => allowClick && 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
|
|
||||||
src={displayUser.profilePicture}
|
|
||||||
alt={displayUser.name}
|
|
||||||
className="rounded-full w-10 h-10"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>
|
<span>
|
||||||
{displayUser.type === "corporate"
|
{displayUser.type === "corporate"
|
||||||
? displayUser.corporateInformation?.companyInformation?.name ||
|
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
|
||||||
displayUser.name
|
|
||||||
: displayUser.name}
|
: displayUser.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
@@ -83,14 +62,11 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
<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">
|
<h2 className="text-2xl font-semibold">Referred Corporate ({total})</h2>
|
||||||
Referred Corporate ({total})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -106,14 +82,11 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
<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">
|
<h2 className="text-2xl font-semibold">Inactive Referred Corporate ({total})</h2>
|
||||||
Inactive Referred Corporate ({total})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -131,8 +104,7 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -155,8 +127,7 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -193,13 +164,7 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
value={users.filter(corporateFilter).length}
|
value={users.filter(corporateFilter).length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard onClick={() => setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" />
|
||||||
onClick={() => setPage("paymentdone")}
|
|
||||||
Icon={BsCurrencyDollar}
|
|
||||||
label="Payment Done"
|
|
||||||
value={done.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("paymentpending")}
|
onClick={() => setPage("paymentpending")}
|
||||||
Icon={BsCurrencyDollar}
|
Icon={BsCurrencyDollar}
|
||||||
@@ -239,10 +204,8 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
.filter(
|
.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
referredCorporateFilter(x) &&
|
referredCorporateFilter(x) &&
|
||||||
moment().isAfter(
|
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||||
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||||
) &&
|
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate))
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} displayUser={x} />
|
<UserDisplay key={x.id} displayUser={x} />
|
||||||
@@ -266,16 +229,9 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
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")
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onViewTeachers={
|
|
||||||
selectedUser.type === "corporate"
|
|
||||||
? () => setPage("teachers")
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
|
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,9 +240,7 @@ export default function AgentDashboard({ user }: Props) {
|
|||||||
</Modal>
|
</Modal>
|
||||||
{page === "referredCorporate" && <ReferredCorporateList />}
|
{page === "referredCorporate" && <ReferredCorporateList />}
|
||||||
{page === "corporate" && <CorporateList />}
|
{page === "corporate" && <CorporateList />}
|
||||||
{page === "inactiveReferredCorporate" && (
|
{page === "inactiveReferredCorporate" && <InactiveReferredCorporateList />}
|
||||||
<InactiveReferredCorporateList />
|
|
||||||
)}
|
|
||||||
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
|
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
|
||||||
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
|
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
|
||||||
{page === "" && <DefaultDashboard />}
|
{page === "" && <DefaultDashboard />}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {usePDFDownload} from "@/hooks/usePDFDownload";
|
|||||||
import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
|
import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
|
||||||
import {uniqBy} from "lodash";
|
import {uniqBy} from "lodash";
|
||||||
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
|
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
|
||||||
|
import {getUserName} from "@/utils/users";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@@ -35,6 +36,8 @@ export default function AssignmentCard({
|
|||||||
allowArchive,
|
allowArchive,
|
||||||
allowUnarchive,
|
allowUnarchive,
|
||||||
}: Assignment & Props) {
|
}: Assignment & Props) {
|
||||||
|
const {users} = useUsers();
|
||||||
|
|
||||||
const renderPdfIcon = usePDFDownload("assignments");
|
const renderPdfIcon = usePDFDownload("assignments");
|
||||||
const renderArchiveIcon = useAssignmentArchive(id, reload);
|
const renderArchiveIcon = useAssignmentArchive(id, reload);
|
||||||
const renderUnarchiveIcon = useAssignmentUnarchive(id, reload);
|
const renderUnarchiveIcon = useAssignmentUnarchive(id, reload);
|
||||||
@@ -72,11 +75,14 @@ export default function AssignmentCard({
|
|||||||
textClassName={results.length / assignees.length < 0.5 ? "!text-mti-gray-dim font-light" : "text-white"}
|
textClassName={results.length / assignees.length < 0.5 ? "!text-mti-gray-dim font-light" : "text-white"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
<span className="flex justify-between gap-1">
|
<span className="flex justify-between gap-1">
|
||||||
<span>{moment(startDate).format("DD/MM/YY, HH:mm")}</span>
|
<span>{moment(startDate).format("DD/MM/YY, HH:mm")}</span>
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
<span>{moment(endDate).format("DD/MM/YY, HH:mm")}</span>
|
<span>{moment(endDate).format("DD/MM/YY, HH:mm")}</span>
|
||||||
</span>
|
</span>
|
||||||
|
<span>Assigner: {getUserName(users.find((x) => x.id === assigner))}</span>
|
||||||
|
</div>
|
||||||
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
|
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
|
||||||
{uniqBy(exams, (x) => x.module).map(({module}) => (
|
{uniqBy(exams, (x) => x.module).map(({module}) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
!startDate ||
|
!startDate ||
|
||||||
!endDate ||
|
!endDate ||
|
||||||
assignees.length === 0 ||
|
assignees.length === 0 ||
|
||||||
(!!examIDs && examIDs.length < selectedModules.length)
|
(!useRandomExams && examIDs.length < selectedModules.length)
|
||||||
}
|
}
|
||||||
className="w-full max-w-[200px]"
|
className="w-full max-w-[200px]"
|
||||||
onClick={createAssignment}
|
onClick={createAssignment}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {getExamById} from "@/utils/exams";
|
|||||||
import {sortByModule} from "@/utils/moduleUtils";
|
import {sortByModule} from "@/utils/moduleUtils";
|
||||||
import {calculateBandScore} from "@/utils/score";
|
import {calculateBandScore} from "@/utils/score";
|
||||||
import {convertToUserSolutions} from "@/utils/stats";
|
import {convertToUserSolutions} from "@/utils/stats";
|
||||||
|
import {getUserName} from "@/utils/users";
|
||||||
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";
|
||||||
@@ -241,6 +242,7 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
|
|||||||
<span>Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}</span>
|
<span>Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}</span>
|
||||||
<span>End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}</span>
|
<span>End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
<span>
|
<span>
|
||||||
Assignees:{" "}
|
Assignees:{" "}
|
||||||
{users
|
{users
|
||||||
@@ -248,6 +250,8 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
|
|||||||
.map((u) => `${u.name} (${u.email})`)
|
.map((u) => `${u.name} (${u.email})`)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
</span>
|
</span>
|
||||||
|
<span>Assigner: {getUserName(users.find((x) => x.id === assignment?.assigner))}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-xl font-bold">Average Scores</span>
|
<span className="text-xl font-bold">Average Scores</span>
|
||||||
|
|||||||
@@ -23,10 +23,13 @@ import {
|
|||||||
BsPersonBadge,
|
BsPersonBadge,
|
||||||
BsPersonCheck,
|
BsPersonCheck,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
|
BsArrowRepeat,
|
||||||
|
BsPlus,
|
||||||
|
BsEnvelopePaper,
|
||||||
} 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 {averageLevelCalculator, 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";
|
||||||
@@ -36,22 +39,133 @@ import useFilterStore from "@/stores/listFilterStore";
|
|||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import useCodes from "@/hooks/useCodes";
|
import useCodes from "@/hooks/useCodes";
|
||||||
import {getUserCorporate} from "@/utils/groups";
|
import {getUserCorporate} from "@/utils/groups";
|
||||||
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
|
import {Assignment} from "@/interfaces/results";
|
||||||
|
import AssignmentView from "./AssignmentView";
|
||||||
|
import AssignmentCreator from "./AssignmentCreator";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import AssignmentCard from "./AssignmentCard";
|
||||||
|
import {createColumnHelper} from "@tanstack/react-table";
|
||||||
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
|
import List from "@/components/List";
|
||||||
|
import {getUserCompanyName} from "@/resources/user";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: CorporateUser;
|
user: CorporateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StudentPerformanceItem = User & {corporateName: string; group: string};
|
||||||
|
const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]}) => {
|
||||||
|
const [isShowingAmount, setIsShowingAmount] = useState(false);
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<StudentPerformanceItem>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: "Student Name",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("email", {
|
||||||
|
header: "E-mail",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("demographicInformation.passport_id", {
|
||||||
|
header: "ID",
|
||||||
|
cell: (info) => info.getValue() || "N/A",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("group", {
|
||||||
|
header: "Group",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("corporateName", {
|
||||||
|
header: "Corporate",
|
||||||
|
cell: (info) => info.getValue() || "N/A",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.reading", {
|
||||||
|
header: "Reading",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? info.getValue() || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.listening", {
|
||||||
|
header: "Listening",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? info.getValue() || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.writing", {
|
||||||
|
header: "Writing",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? info.getValue() || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.speaking", {
|
||||||
|
header: "Speaking",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? info.getValue() || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.level", {
|
||||||
|
header: "Level",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? info.getValue() || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels", {
|
||||||
|
id: "overall_level",
|
||||||
|
header: "Overall",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? averageLevelCalculator(
|
||||||
|
users,
|
||||||
|
stats.filter((x) => x.user === info.row.original.id),
|
||||||
|
).toFixed(1)
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full h-full">
|
||||||
|
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
|
||||||
|
Show Utilization
|
||||||
|
</Checkbox>
|
||||||
|
<List<StudentPerformanceItem>
|
||||||
|
data={items.sort(
|
||||||
|
(a, b) =>
|
||||||
|
averageLevelCalculator(
|
||||||
|
users,
|
||||||
|
stats.filter((x) => x.user === b.id),
|
||||||
|
) -
|
||||||
|
averageLevelCalculator(
|
||||||
|
users,
|
||||||
|
stats.filter((x) => x.user === a.id),
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function CorporateDashboard({user}: Props) {
|
export default function CorporateDashboard({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 [corporateUserToShow, setCorporateUserToShow] =
|
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
|
||||||
useState<CorporateUser>();
|
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
||||||
|
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
|
const [userBalance, setUserBalance] = useState(0);
|
||||||
|
|
||||||
const {stats} = useStats();
|
const {stats} = useStats();
|
||||||
const { users, reload } = useUsers();
|
const {users, reload, isLoading} = useUsers();
|
||||||
const {codes} = useCodes(user.id);
|
const {codes} = useCodes(user.id);
|
||||||
const { groups } = useGroups(user.id);
|
const {groups} = useGroups({admin: user.id});
|
||||||
|
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -60,31 +174,29 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
}, [selectedUser, page]);
|
}, [selectedUser, page]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const relatedGroups = groups.filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate");
|
||||||
|
const usersInGroups = relatedGroups.map((x) => x.participants).flat();
|
||||||
|
const filteredCodes = codes.filter((x) => !x.userId || !usersInGroups.includes(x.userId));
|
||||||
|
|
||||||
|
setUserBalance(usersInGroups.length + filteredCodes.length);
|
||||||
|
}, [codes, groups]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// in this case it fetches the master corporate account
|
// in this case it fetches the master corporate account
|
||||||
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" &&
|
const teacherFilter = (user: User) => user.type === "teacher" && groups.flatMap((g) => g.participants).includes(user.id);
|
||||||
groups.flatMap((g) => g.participants).includes(user.id);
|
|
||||||
const teacherFilter = (user: User) =>
|
|
||||||
user.type === "teacher" &&
|
|
||||||
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
|
|
||||||
src={displayUser.profilePicture}
|
|
||||||
alt={displayUser.name}
|
|
||||||
className="rounded-full w-10 h-10"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>{displayUser.name}</span>
|
<span>{displayUser.name}</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
@@ -110,8 +222,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -140,8 +251,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -153,22 +263,18 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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">
|
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
|
||||||
Groups ({groups.filter(filter).length})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupList user={user} />
|
<GroupList user={user} />
|
||||||
@@ -176,6 +282,152 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AssignmentsPage = () => {
|
||||||
|
const activeFilter = (a: Assignment) =>
|
||||||
|
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
||||||
|
const pastFilter = (a: Assignment) => (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 (
|
||||||
|
<>
|
||||||
|
<AssignmentView
|
||||||
|
isOpen={!!selectedAssignment && !isCreatingAssignment}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedAssignment(undefined);
|
||||||
|
setIsCreatingAssignment(false);
|
||||||
|
reloadAssignments();
|
||||||
|
}}
|
||||||
|
assignment={selectedAssignment}
|
||||||
|
/>
|
||||||
|
<AssignmentCreator
|
||||||
|
assignment={selectedAssignment}
|
||||||
|
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
|
||||||
|
users={users.filter(
|
||||||
|
(x) =>
|
||||||
|
x.type === "student" &&
|
||||||
|
(!!selectedUser
|
||||||
|
? groups
|
||||||
|
.filter((g) => g.admin === selectedUser.id)
|
||||||
|
.flatMap((g) => g.participants)
|
||||||
|
.includes(x.id) || false
|
||||||
|
: groups.flatMap((g) => g.participants).includes(x.id)),
|
||||||
|
)}
|
||||||
|
assigner={user.id}
|
||||||
|
isCreating={isCreatingAssignment}
|
||||||
|
cancelCreation={() => {
|
||||||
|
setIsCreatingAssignment(false);
|
||||||
|
setSelectedAssignment(undefined);
|
||||||
|
reloadAssignments();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="w-full flex justify-between items-center">
|
||||||
|
<div
|
||||||
|
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">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={reloadAssignments}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<span>Reload</span>
|
||||||
|
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{assignments.filter(activeFilter).map((a) => (
|
||||||
|
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<div
|
||||||
|
onClick={() => setIsCreatingAssignment(true)}
|
||||||
|
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">
|
||||||
|
<BsPlus className="text-6xl" />
|
||||||
|
<span className="text-lg">New Assignment</span>
|
||||||
|
</div>
|
||||||
|
{assignments.filter(futureFilter).map((a) => (
|
||||||
|
<AssignmentCard
|
||||||
|
{...a}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedAssignment(a);
|
||||||
|
setIsCreatingAssignment(true);
|
||||||
|
}}
|
||||||
|
key={a.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{assignments.filter(pastFilter).map((a) => (
|
||||||
|
<AssignmentCard
|
||||||
|
{...a}
|
||||||
|
onClick={() => setSelectedAssignment(a)}
|
||||||
|
key={a.id}
|
||||||
|
allowDownload
|
||||||
|
reload={reloadAssignments}
|
||||||
|
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 StudentPerformancePage = () => {
|
||||||
|
const students = users
|
||||||
|
.filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
|
||||||
|
.map((u) => ({
|
||||||
|
...u,
|
||||||
|
group: groups.find((x) => x.participants.includes(u.id))?.name || "N/A",
|
||||||
|
corporateName: getUserCompanyName(u, users, groups),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full flex justify-between items-center">
|
||||||
|
<div
|
||||||
|
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">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={reload}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<span>Reload</span>
|
||||||
|
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<StudentPerformanceList items={students} stats={stats} users={users} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
const formattedStats = studentStats
|
const formattedStats = studentStats
|
||||||
.map((s) => ({
|
.map((s) => ({
|
||||||
@@ -186,12 +438,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
.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} = {
|
||||||
@@ -210,14 +457,10 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
<>
|
<>
|
||||||
{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>
|
|
||||||
{corporateUserToShow?.corporateInformation?.companyInformation
|
|
||||||
.name || corporateUserToShow.name}
|
|
||||||
</b>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
|
<section className="grid grid-cols-5 -md:grid-cols-2 gap-4 text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("students")}
|
onClick={() => setPage("students")}
|
||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
@@ -235,48 +478,47 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClipboard2Data}
|
Icon={BsClipboard2Data}
|
||||||
label="Exams Performed"
|
label="Exams Performed"
|
||||||
value={
|
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
||||||
stats.filter((s) =>
|
|
||||||
groups.flatMap((g) => g.participants).includes(s.user)
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPaperclip}
|
Icon={BsPaperclip}
|
||||||
label="Average Level"
|
label="Average Level"
|
||||||
value={averageLevelCalculator(
|
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
|
||||||
stats.filter((s) =>
|
|
||||||
groups.flatMap((g) => g.participants).includes(s.user)
|
|
||||||
)
|
|
||||||
).toFixed(1)}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
|
||||||
onClick={() => setPage("groups")}
|
|
||||||
Icon={BsPeople}
|
|
||||||
label="Groups"
|
|
||||||
value={groups.length}
|
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
|
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPersonCheck}
|
Icon={BsPersonCheck}
|
||||||
label="User Balance"
|
label="User Balance"
|
||||||
value={`${codes.length}/${
|
value={`${userBalance}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
|
||||||
user.corporateInformation?.companyInformation?.userAmount || 0
|
|
||||||
}`}
|
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClock}
|
Icon={BsClock}
|
||||||
label="Expiration Date"
|
label="Expiration Date"
|
||||||
value={
|
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
||||||
user.subscriptionExpirationDate
|
|
||||||
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
|
|
||||||
: "Unlimited"
|
|
||||||
}
|
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
|
label="Student Performance"
|
||||||
|
value={users.filter(studentFilter).length}
|
||||||
|
color="purple"
|
||||||
|
onClick={() => setPage("studentsPerformance")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
disabled={isAssignmentsLoading}
|
||||||
|
onClick={() => setPage("assignments")}
|
||||||
|
className="bg-white col-span-2 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">
|
||||||
|
{isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
@@ -307,11 +549,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
<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) =>
|
|
||||||
calculateAverageLevel(b.levels) -
|
|
||||||
calculateAverageLevel(a.levels)
|
|
||||||
)
|
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -324,8 +562,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
.filter(studentFilter)
|
.filter(studentFilter)
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
|
||||||
Object.keys(groupByExam(getStatsByStudent(a))).length
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -349,8 +586,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" ||
|
selectedUser.type === "corporate" || selectedUser.type === "teacher"
|
||||||
selectedUser.type === "teacher"
|
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
@@ -360,11 +596,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter(
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
(g) =>
|
|
||||||
g.admin === selectedUser.id ||
|
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -374,8 +606,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
selectedUser.type === "corporate" ||
|
selectedUser.type === "corporate" || selectedUser.type === "student"
|
||||||
selectedUser.type === "student"
|
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
@@ -385,11 +616,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter(
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
(g) =>
|
|
||||||
g.admin === selectedUser.id ||
|
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -407,6 +634,8 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
{page === "students" && <StudentsList />}
|
{page === "students" && <StudentsList />}
|
||||||
{page === "teachers" && <TeachersList />}
|
{page === "teachers" && <TeachersList />}
|
||||||
{page === "groups" && <GroupsList />}
|
{page === "groups" && <GroupsList />}
|
||||||
|
{page === "assignments" && <AssignmentsPage />}
|
||||||
|
{page === "studentsPerformance" && <StudentPerformancePage />}
|
||||||
{page === "" && <DefaultDashboard />}
|
{page === "" && <DefaultDashboard />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,20 +4,14 @@ import useGroups from "@/hooks/useGroups";
|
|||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import {
|
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||||
BsBook,
|
|
||||||
BsClipboard,
|
|
||||||
BsHeadphones,
|
|
||||||
BsMegaphone,
|
|
||||||
BsPen,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||||
import {capitalize} from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import {getLevelLabel} from "@/utils/score";
|
import {getLevelLabel} from "@/utils/score";
|
||||||
|
|
||||||
const Card = ({user}: {user: User}) => {
|
const Card = ({user}: {user: User}) => {
|
||||||
return (
|
return (
|
||||||
<div className="border-mti-gray-platinum flex flex-col h-fit w-full cursor-pointer flex-col gap-6 rounded-xl border bg-white p-4 transition duration-300 ease-in-out hover:drop-shadow">
|
<div className="border-mti-gray-platinum flex flex-col h-fit w-full cursor-pointer gap-6 rounded-xl border bg-white p-4 transition duration-300 ease-in-out hover:drop-shadow">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="text-xl font-semibold">{user.name}</h3>
|
<h3 className="text-xl font-semibold">{user.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,38 +20,19 @@ const Card = ({ user }: { user: User }) => {
|
|||||||
const desiredLevel = user.desiredLevels[module] || 9;
|
const desiredLevel = user.desiredLevels[module] || 9;
|
||||||
const level = user.levels[module] || 0;
|
const level = user.levels[module] || 0;
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]" key={module}>
|
||||||
className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]"
|
|
||||||
key={module}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 md:gap-3">
|
<div className="flex items-center gap-2 md:gap-3">
|
||||||
<div className="bg-mti-gray-smoke flex h-8 w-8 items-center justify-center rounded-lg md:h-12 md:w-12 md:rounded-xl">
|
<div className="bg-mti-gray-smoke flex h-8 w-8 items-center justify-center rounded-lg md:h-12 md:w-12 md:rounded-xl">
|
||||||
{module === "reading" && (
|
{module === "reading" && <BsBook className="text-ielts-reading h-4 w-4 md:h-5 md:w-5" />}
|
||||||
<BsBook className="text-ielts-reading h-4 w-4 md:h-5 md:w-5" />
|
{module === "listening" && <BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />}
|
||||||
)}
|
{module === "writing" && <BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />}
|
||||||
{module === "listening" && (
|
{module === "speaking" && <BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />}
|
||||||
<BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />
|
{module === "level" && <BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />}
|
||||||
)}
|
|
||||||
{module === "writing" && (
|
|
||||||
<BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />
|
|
||||||
)}
|
|
||||||
{module === "speaking" && (
|
|
||||||
<BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />
|
|
||||||
)}
|
|
||||||
{module === "level" && (
|
|
||||||
<BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<span className="text-sm font-bold md:font-extrabold w-full">
|
<span className="text-sm font-bold md:font-extrabold w-full">{capitalize(module)}</span>
|
||||||
{capitalize(module)}
|
|
||||||
</span>
|
|
||||||
<div className="text-mti-gray-dim text-sm font-normal">
|
<div className="text-mti-gray-dim text-sm font-normal">
|
||||||
{module === "level" && (
|
{module === "level" && <span>English Level: {getLevelLabel(level).join(" / ")}</span>}
|
||||||
<span>
|
|
||||||
English Level: {getLevelLabel(level).join(" / ")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{module !== "level" && (
|
{module !== "level" && (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>Level {level} / Level 9</span>
|
<span>Level {level} / Level 9</span>
|
||||||
@@ -87,16 +62,13 @@ const Card = ({ user }: { user: User }) => {
|
|||||||
|
|
||||||
const CorporateStudentsLevels = () => {
|
const CorporateStudentsLevels = () => {
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const { groups } = useGroups();
|
const {groups} = useGroups({});
|
||||||
|
|
||||||
const corporateUsers = users.filter((u) => u.type === "corporate") as User[];
|
const corporateUsers = users.filter((u) => u.type === "corporate") as User[];
|
||||||
const [corporateId, setCorporateId] = React.useState<string>("");
|
const [corporateId, setCorporateId] = React.useState<string>("");
|
||||||
const corporate =
|
const corporate = corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
|
||||||
corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
|
|
||||||
|
|
||||||
const groupsFromCorporate = corporate
|
const groupsFromCorporate = corporate ? groups.filter((g) => g.admin === corporate.id) : [];
|
||||||
? groups.filter((g) => g.admin === corporate.id)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const groupsParticipants = groupsFromCorporate
|
const groupsParticipants = groupsFromCorporate
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
@@ -121,11 +93,7 @@ const CorporateStudentsLevels = () => {
|
|||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
? "#D5D9F0"
|
|
||||||
: state.isSelected
|
|
||||||
? "#7872BF"
|
|
||||||
: "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
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 { Group, MasterCorporateUser, Stat, User } from "@/interfaces/user";
|
import {CorporateUser, Group, MasterCorporateUser, 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";
|
||||||
@@ -17,11 +17,15 @@ import {
|
|||||||
BsPersonCheck,
|
BsPersonCheck,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
BsBank,
|
BsBank,
|
||||||
|
BsEnvelopePaper,
|
||||||
|
BsArrowRepeat,
|
||||||
|
BsPlus,
|
||||||
|
BsPersonFillGear,
|
||||||
} 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 {averageLevelCalculator, 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";
|
||||||
@@ -30,29 +34,227 @@ import GroupList from "@/pages/(admin)/Lists/GroupList";
|
|||||||
import useFilterStore from "@/stores/listFilterStore";
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import useCodes from "@/hooks/useCodes";
|
import useCodes from "@/hooks/useCodes";
|
||||||
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
|
import {Assignment} from "@/interfaces/results";
|
||||||
|
import AssignmentView from "./AssignmentView";
|
||||||
|
import AssignmentCreator from "./AssignmentCreator";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import AssignmentCard from "./AssignmentCard";
|
||||||
|
import {createColumn, createColumnHelper} from "@tanstack/react-table";
|
||||||
|
import List from "@/components/List";
|
||||||
|
import {getUserCorporate} from "@/utils/groups";
|
||||||
|
import {getCorporateUser, getUserCompanyName} from "@/resources/user";
|
||||||
|
import {Switch} from "@headlessui/react";
|
||||||
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
|
import {uniq, uniqBy} from "lodash";
|
||||||
|
import Select from "@/components/Low/Select";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: MasterCorporateUser;
|
user: MasterCorporateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StudentPerformanceItem = User & {corporate?: CorporateUser; group: string};
|
||||||
|
const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]; groups: Group[]}) => {
|
||||||
|
const [isShowingAmount, setIsShowingAmount] = useState(false);
|
||||||
|
const [availableCorporates] = useState(
|
||||||
|
uniqBy(
|
||||||
|
items.map((x) => x.corporate),
|
||||||
|
"id",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedCorporate, setSelectedCorporate] = useState<CorporateUser | null | undefined>(null);
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<StudentPerformanceItem>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: "Student Name",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("email", {
|
||||||
|
header: "E-mail",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("demographicInformation.passport_id", {
|
||||||
|
header: "ID",
|
||||||
|
cell: (info) => info.getValue() || "N/A",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("group", {
|
||||||
|
header: "Group",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("corporate", {
|
||||||
|
header: "Corporate",
|
||||||
|
cell: (info) => (!!info.getValue() ? getUserCompanyName(info.getValue() as User, users, groups) : "N/A"),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.reading", {
|
||||||
|
header: "Reading",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? calculateBandScore(
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "reading" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.correct, 0),
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "reading" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.total, 0),
|
||||||
|
"level",
|
||||||
|
info.row.original.focus || "academic",
|
||||||
|
) || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.listening", {
|
||||||
|
header: "Listening",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? calculateBandScore(
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "listening" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.correct, 0),
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "listening" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.total, 0),
|
||||||
|
"level",
|
||||||
|
info.row.original.focus || "academic",
|
||||||
|
) || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.writing", {
|
||||||
|
header: "Writing",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? calculateBandScore(
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "writing" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.correct, 0),
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "writing" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.total, 0),
|
||||||
|
"level",
|
||||||
|
info.row.original.focus || "academic",
|
||||||
|
) || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.speaking", {
|
||||||
|
header: "Speaking",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? calculateBandScore(
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "speaking" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.correct, 0),
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "speaking" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.total, 0),
|
||||||
|
"level",
|
||||||
|
info.row.original.focus || "academic",
|
||||||
|
) || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.level", {
|
||||||
|
header: "Level",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? calculateBandScore(
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "level" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.correct, 0),
|
||||||
|
stats
|
||||||
|
.filter((x) => x.module === "level" && x.user === info.row.original.id)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.total, 0),
|
||||||
|
"level",
|
||||||
|
info.row.original.focus || "academic",
|
||||||
|
) || 0
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels", {
|
||||||
|
id: "overall_level",
|
||||||
|
header: "Overall",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? averageLevelCalculator(
|
||||||
|
users,
|
||||||
|
stats.filter((x) => x.user === info.row.original.id),
|
||||||
|
).toFixed(1)
|
||||||
|
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const filterUsers = (data: StudentPerformanceItem[]) => {
|
||||||
|
console.log(data, selectedCorporate);
|
||||||
|
const filterByCorporate = (item: StudentPerformanceItem) => item.corporate?.id === selectedCorporate?.id;
|
||||||
|
|
||||||
|
const filters: ((item: StudentPerformanceItem) => boolean)[] = [];
|
||||||
|
if (selectedCorporate !== null) filters.push(filterByCorporate);
|
||||||
|
|
||||||
|
return filters.reduce((d, f) => d.filter(f), data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full h-full">
|
||||||
|
<div className="w-full flex flex-col gap-4">
|
||||||
|
<Select
|
||||||
|
options={availableCorporates.map((x) => ({
|
||||||
|
value: x?.id || "N/A",
|
||||||
|
label: x?.corporateInformation?.companyInformation?.name || x?.name || "N/A",
|
||||||
|
}))}
|
||||||
|
isClearable
|
||||||
|
value={
|
||||||
|
selectedCorporate === null
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
value: selectedCorporate?.id || "N/A",
|
||||||
|
label: selectedCorporate?.corporateInformation?.companyInformation?.name || selectedCorporate?.name || "N/A",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
placeholder="Select a Corporate..."
|
||||||
|
onChange={(value) =>
|
||||||
|
!value
|
||||||
|
? setSelectedCorporate(null)
|
||||||
|
: setSelectedCorporate(value.value === "N/A" ? undefined : availableCorporates.find((x) => x?.id === value.value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
|
||||||
|
Show Utilization
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<List<StudentPerformanceItem>
|
||||||
|
data={filterUsers(
|
||||||
|
items.sort(
|
||||||
|
(a, b) =>
|
||||||
|
averageLevelCalculator(
|
||||||
|
users,
|
||||||
|
stats.filter((x) => x.user === b.id),
|
||||||
|
) -
|
||||||
|
averageLevelCalculator(
|
||||||
|
users,
|
||||||
|
stats.filter((x) => x.user === a.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function MasterCorporateDashboard({user}: Props) {
|
export default function MasterCorporateDashboard({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 [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
|
|
||||||
const {stats} = useStats();
|
const {stats} = useStats();
|
||||||
const {users, reload} = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const {codes} = useCodes(user.id);
|
const {codes} = useCodes(user.id);
|
||||||
const { groups } = useGroups(user.id, user.type);
|
const {groups} = useGroups({admin: user.id, userType: user.type});
|
||||||
|
|
||||||
const masterCorporateUserGroups = [
|
const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))];
|
||||||
...new Set(
|
const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))];
|
||||||
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
|
|
||||||
),
|
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
|
||||||
];
|
|
||||||
const corporateUserGroups = [
|
|
||||||
...new Set(groups.flatMap((g) => g.participants)),
|
|
||||||
];
|
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -61,24 +263,16 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
}, [selectedUser, page]);
|
}, [selectedUser, page]);
|
||||||
|
|
||||||
const studentFilter = (user: User) =>
|
const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id);
|
||||||
user.type === "student" && corporateUserGroups.includes(user.id);
|
const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id);
|
||||||
const teacherFilter = (user: User) =>
|
|
||||||
user.type === "teacher" && corporateUserGroups.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
|
|
||||||
src={displayUser.profilePicture}
|
|
||||||
alt={displayUser.name}
|
|
||||||
className="rounded-full w-10 h-10"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>{displayUser.name}</span>
|
<span>{displayUser.name}</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
@@ -88,10 +282,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
|
|
||||||
const StudentsList = () => {
|
const StudentsList = () => {
|
||||||
const filter = (x: User) =>
|
const filter = (x: User) =>
|
||||||
x.type === "student" &&
|
x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
|
||||||
(!!selectedUser
|
|
||||||
? corporateUserGroups.includes(x.id) || false
|
|
||||||
: corporateUserGroups.includes(x.id));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
@@ -101,8 +292,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -115,10 +305,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
|
|
||||||
const TeachersList = () => {
|
const TeachersList = () => {
|
||||||
const filter = (x: User) =>
|
const filter = (x: User) =>
|
||||||
x.type === "teacher" &&
|
x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
|
||||||
(!!selectedUser
|
|
||||||
? corporateUserGroups.includes(x.id) || false
|
|
||||||
: corporateUserGroups.includes(x.id));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
@@ -128,8 +315,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -141,10 +327,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const corporateUserFilter = (x: User) =>
|
const corporateUserFilter = (x: User) =>
|
||||||
x.type === "corporate" &&
|
x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id));
|
||||||
(!!selectedUser
|
|
||||||
? masterCorporateUserGroups.includes(x.id) || false
|
|
||||||
: masterCorporateUserGroups.includes(x.id));
|
|
||||||
|
|
||||||
const CorporateList = () => {
|
const CorporateList = () => {
|
||||||
return (
|
return (
|
||||||
@@ -155,8 +338,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -173,14 +355,11 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
<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">
|
<h2 className="text-2xl font-semibold">Groups ({groups.length})</h2>
|
||||||
Groups ({groups.length})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupList user={user} />
|
<GroupList user={user} />
|
||||||
@@ -188,34 +367,150 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
const StudentPerformancePage = () => {
|
||||||
const formattedStats = studentStats
|
const students = users
|
||||||
.map((s) => ({
|
.filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
|
||||||
focus: users.find((u) => u.id === s.user)?.focus,
|
.map((u) => ({
|
||||||
score: s.score,
|
...u,
|
||||||
module: s.module,
|
group: groups.find((x) => x.participants.includes(u.id))?.name || "N/A",
|
||||||
}))
|
corporate: getCorporateUser(u, users, groups),
|
||||||
.filter((f) => !!f.focus);
|
|
||||||
const bandScores = formattedStats.map((s) => ({
|
|
||||||
module: s.module,
|
|
||||||
level: calculateBandScore(
|
|
||||||
s.score.correct,
|
|
||||||
s.score.total,
|
|
||||||
s.module,
|
|
||||||
s.focus!
|
|
||||||
),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const levels: { [key in Module]: number } = {
|
return (
|
||||||
reading: 0,
|
<>
|
||||||
listening: 0,
|
<div className="w-full flex justify-between items-center">
|
||||||
writing: 0,
|
<div
|
||||||
speaking: 0,
|
onClick={() => setPage("")}
|
||||||
level: 0,
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={reloadAssignments}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<span>Reload</span>
|
||||||
|
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<StudentPerformanceList items={students} stats={stats} users={users} groups={groups} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
|
||||||
|
|
||||||
return calculateAverageLevel(levels);
|
const AssignmentsPage = () => {
|
||||||
|
const activeFilter = (a: Assignment) =>
|
||||||
|
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
||||||
|
const pastFilter = (a: Assignment) => (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 (
|
||||||
|
<>
|
||||||
|
<AssignmentView
|
||||||
|
isOpen={!!selectedAssignment && !isCreatingAssignment}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedAssignment(undefined);
|
||||||
|
setIsCreatingAssignment(false);
|
||||||
|
reloadAssignments();
|
||||||
|
}}
|
||||||
|
assignment={selectedAssignment}
|
||||||
|
/>
|
||||||
|
<AssignmentCreator
|
||||||
|
assignment={selectedAssignment}
|
||||||
|
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
|
||||||
|
users={users.filter(
|
||||||
|
(x) =>
|
||||||
|
x.type === "student" &&
|
||||||
|
(!!selectedUser
|
||||||
|
? groups
|
||||||
|
.filter((g) => g.admin === selectedUser.id)
|
||||||
|
.flatMap((g) => g.participants)
|
||||||
|
.includes(x.id) || false
|
||||||
|
: groups.flatMap((g) => g.participants).includes(x.id)),
|
||||||
|
)}
|
||||||
|
assigner={user.id}
|
||||||
|
isCreating={isCreatingAssignment}
|
||||||
|
cancelCreation={() => {
|
||||||
|
setIsCreatingAssignment(false);
|
||||||
|
setSelectedAssignment(undefined);
|
||||||
|
reloadAssignments();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="w-full flex justify-between items-center">
|
||||||
|
<div
|
||||||
|
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">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={reloadAssignments}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<span>Reload</span>
|
||||||
|
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{assignments.filter(activeFilter).map((a) => (
|
||||||
|
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<div
|
||||||
|
onClick={() => setIsCreatingAssignment(true)}
|
||||||
|
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">
|
||||||
|
<BsPlus className="text-6xl" />
|
||||||
|
<span className="text-lg">New Assignment</span>
|
||||||
|
</div>
|
||||||
|
{assignments.filter(futureFilter).map((a) => (
|
||||||
|
<AssignmentCard
|
||||||
|
{...a}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedAssignment(a);
|
||||||
|
setIsCreatingAssignment(true);
|
||||||
|
}}
|
||||||
|
key={a.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{assignments.filter(pastFilter).map((a) => (
|
||||||
|
<AssignmentCard
|
||||||
|
{...a}
|
||||||
|
onClick={() => setSelectedAssignment(a)}
|
||||||
|
key={a.id}
|
||||||
|
allowDownload
|
||||||
|
reload={reloadAssignments}
|
||||||
|
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 = () => (
|
||||||
@@ -238,46 +533,29 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClipboard2Data}
|
Icon={BsClipboard2Data}
|
||||||
label="Exams Performed"
|
label="Exams Performed"
|
||||||
value={
|
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
||||||
stats.filter((s) =>
|
|
||||||
groups.flatMap((g) => g.participants).includes(s.user)
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPaperclip}
|
Icon={BsPaperclip}
|
||||||
label="Average Level"
|
label="Average Level"
|
||||||
value={averageLevelCalculator(
|
value={averageLevelCalculator(
|
||||||
stats.filter((s) =>
|
users,
|
||||||
groups.flatMap((g) => g.participants).includes(s.user)
|
stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)),
|
||||||
)
|
|
||||||
).toFixed(1)}
|
).toFixed(1)}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
|
||||||
onClick={() => setPage("groups")}
|
|
||||||
Icon={BsPeople}
|
|
||||||
label="Groups"
|
|
||||||
value={groups.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPersonCheck}
|
Icon={BsPersonCheck}
|
||||||
label="User Balance"
|
label="User Balance"
|
||||||
value={`${codes.length}/${
|
value={`${codes.length}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
|
||||||
user.corporateInformation?.companyInformation?.userAmount || 0
|
|
||||||
}`}
|
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClock}
|
Icon={BsClock}
|
||||||
label="Expiration Date"
|
label="Expiration Date"
|
||||||
value={
|
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
||||||
user.subscriptionExpirationDate
|
|
||||||
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
|
|
||||||
: "Unlimited"
|
|
||||||
}
|
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
@@ -287,6 +565,25 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => setPage("corporate")}
|
onClick={() => setPage("corporate")}
|
||||||
/>
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
|
label="Student Performance"
|
||||||
|
value={users.filter(studentFilter).length}
|
||||||
|
color="purple"
|
||||||
|
onClick={() => setPage("studentsPerformance")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
disabled={isAssignmentsLoading}
|
||||||
|
onClick={() => setPage("assignments")}
|
||||||
|
className="bg-white col-span-2 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">
|
||||||
|
{isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
@@ -317,11 +614,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
<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) =>
|
|
||||||
calculateAverageLevel(b.levels) -
|
|
||||||
calculateAverageLevel(a.levels)
|
|
||||||
)
|
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -334,8 +627,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
.filter(studentFilter)
|
.filter(studentFilter)
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
|
||||||
Object.keys(groupByExam(getStatsByStudent(a))).length
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -359,8 +651,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" ||
|
selectedUser.type === "corporate" || selectedUser.type === "teacher"
|
||||||
selectedUser.type === "teacher"
|
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
@@ -370,11 +661,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter(
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
(g) =>
|
|
||||||
g.admin === selectedUser.id ||
|
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -384,8 +671,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
selectedUser.type === "corporate" ||
|
selectedUser.type === "corporate" || selectedUser.type === "student"
|
||||||
selectedUser.type === "student"
|
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
@@ -395,11 +681,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter(
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
(g) =>
|
|
||||||
g.admin === selectedUser.id ||
|
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -418,6 +700,8 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
{page === "teachers" && <TeachersList />}
|
{page === "teachers" && <TeachersList />}
|
||||||
{page === "groups" && <GroupsList />}
|
{page === "groups" && <GroupsList />}
|
||||||
{page === "corporate" && <CorporateList />}
|
{page === "corporate" && <CorporateList />}
|
||||||
|
{page === "assignments" && <AssignmentsPage />}
|
||||||
|
{page === "studentsPerformance" && <StudentPerformancePage />}
|
||||||
{page === "" && <DefaultDashboard />}
|
{page === "" && <DefaultDashboard />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ 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;
|
||||||
@@ -58,17 +59,13 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
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({admin: 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 === "");
|
||||||
@@ -78,23 +75,15 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
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
|
|
||||||
src={displayUser.profilePicture}
|
|
||||||
alt={displayUser.name}
|
|
||||||
className="rounded-full w-10 h-10"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<span>{displayUser.name}</span>
|
<span>{displayUser.name}</span>
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
@@ -120,8 +109,7 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
<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>
|
||||||
@@ -133,22 +121,18 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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">
|
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
|
||||||
Groups ({groups.filter(filter).length})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupList user={user} />
|
<GroupList user={user} />
|
||||||
@@ -166,12 +150,7 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
.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} = {
|
||||||
@@ -188,16 +167,10 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
|
|
||||||
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 pastFilter = (a: Assignment) =>
|
|
||||||
(moment(a.endDate).isBefore(moment()) ||
|
|
||||||
a.assignees.length === a.results.length) &&
|
|
||||||
!a.archived;
|
|
||||||
const archivedFilter = (a: Assignment) => a.archived;
|
const archivedFilter = (a: Assignment) => a.archived;
|
||||||
const futureFilter = (a: Assignment) =>
|
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
||||||
moment(a.startDate).isAfter(moment());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -212,9 +185,7 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
/>
|
/>
|
||||||
<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(
|
users={users.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
@@ -223,7 +194,7 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
.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)),
|
||||||
)}
|
)}
|
||||||
assigner={user.id}
|
assigner={user.id}
|
||||||
isCreating={isCreatingAssignment}
|
isCreating={isCreatingAssignment}
|
||||||
@@ -236,47 +207,31 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
<div className="w-full flex justify-between items-center">
|
<div className="w-full flex justify-between items-center">
|
||||||
<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>
|
||||||
<div
|
<div
|
||||||
onClick={reloadAssignments}
|
onClick={reloadAssignments}
|
||||||
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">
|
||||||
>
|
|
||||||
<span>Reload</span>
|
<span>Reload</span>
|
||||||
<BsArrowRepeat
|
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
||||||
className={clsx(
|
|
||||||
"text-xl",
|
|
||||||
isAssignmentsLoading && "animate-spin"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">
|
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
||||||
Active Assignments ({assignments.filter(activeFilter).length})
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(activeFilter).map((a) => (
|
{assignments.filter(activeFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
||||||
{...a}
|
|
||||||
onClick={() => setSelectedAssignment(a)}
|
|
||||||
key={a.id}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">
|
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
|
||||||
Planned Assignments ({assignments.filter(futureFilter).length})
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<div
|
<div
|
||||||
onClick={() => setIsCreatingAssignment(true)}
|
onClick={() => setIsCreatingAssignment(true)}
|
||||||
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"
|
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">
|
||||||
>
|
|
||||||
<BsPlus className="text-6xl" />
|
<BsPlus className="text-6xl" />
|
||||||
<span className="text-lg">New Assignment</span>
|
<span className="text-lg">New Assignment</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,9 +248,7 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">
|
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
||||||
Past Assignments ({assignments.filter(pastFilter).length})
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(pastFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
@@ -310,9 +263,7 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">
|
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedFilter).length})</h2>
|
||||||
Archived Assignments ({assignments.filter(archivedFilter).length})
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(archivedFilter).map((a) => (
|
{assignments.filter(archivedFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
@@ -334,19 +285,14 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
<>
|
<>
|
||||||
{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>
|
|
||||||
{corporateUserToShow?.corporateInformation?.companyInformation
|
|
||||||
.name || corporateUserToShow.name}
|
|
||||||
</b>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<section
|
<section
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center",
|
"flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center",
|
||||||
!!corporateUserToShow && "mt-12 xl:mt-6"
|
!!corporateUserToShow && "mt-12 xl:mt-6",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("students")}
|
onClick={() => setPage("students")}
|
||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
@@ -357,42 +303,25 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClipboard2Data}
|
Icon={BsClipboard2Data}
|
||||||
label="Exams Performed"
|
label="Exams Performed"
|
||||||
value={
|
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
||||||
stats.filter((s) =>
|
|
||||||
groups.flatMap((g) => g.participants).includes(s.user)
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPaperclip}
|
Icon={BsPaperclip}
|
||||||
label="Average Level"
|
label="Average Level"
|
||||||
value={averageLevelCalculator(
|
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
|
||||||
stats.filter((s) =>
|
|
||||||
groups.flatMap((g) => g.participants).includes(s.user)
|
|
||||||
)
|
|
||||||
).toFixed(1)}
|
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
{checkAccess(user, ["teacher", "developer"], "viewGroup") && (
|
{checkAccess(user, ["teacher", "developer"], permissions, "viewGroup") && (
|
||||||
<IconCard
|
<IconCard Icon={BsPeople} label="Groups" value={groups.length} color="purple" onClick={() => setPage("groups")} />
|
||||||
Icon={BsPeople}
|
|
||||||
label="Groups"
|
|
||||||
value={groups.length}
|
|
||||||
color="purple"
|
|
||||||
onClick={() => setPage("groups")}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("assignments")}
|
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"
|
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" />
|
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
|
||||||
<span className="flex flex-col gap-1 items-center text-xl">
|
<span className="flex flex-col gap-1 items-center text-xl">
|
||||||
<span className="text-lg">Assignments</span>
|
<span className="text-lg">Assignments</span>
|
||||||
<span className="font-semibold text-mti-purple-light">
|
<span className="font-semibold text-mti-purple-light">{assignments.filter((a) => !a.archived).length}</span>
|
||||||
{assignments.filter((a) => !a.archived).length}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -414,11 +343,7 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
<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) =>
|
|
||||||
calculateAverageLevel(b.levels) -
|
|
||||||
calculateAverageLevel(a.levels)
|
|
||||||
)
|
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -431,8 +356,7 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
.filter(studentFilter)
|
.filter(studentFilter)
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
|
||||||
Object.keys(groupByExam(getStatsByStudent(a))).length
|
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -456,16 +380,9 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
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")
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onViewTeachers={
|
|
||||||
selectedUser.type === "corporate"
|
|
||||||
? () => setPage("teachers")
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
|
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,17 +4,7 @@ import { Module } from "@/interfaces";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import {
|
import {BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
|
||||||
BsArrowRepeat,
|
|
||||||
BsBook,
|
|
||||||
BsCheck,
|
|
||||||
BsCheckCircle,
|
|
||||||
BsClipboard,
|
|
||||||
BsHeadphones,
|
|
||||||
BsMegaphone,
|
|
||||||
BsPen,
|
|
||||||
BsXCircle,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import {totalExamsByModule} from "@/utils/stats";
|
import {totalExamsByModule} from "@/utils/stats";
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
@@ -31,20 +21,11 @@ import moment from "moment";
|
|||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
page: "exercises" | "exams";
|
page: "exercises" | "exams";
|
||||||
onStart: (
|
onStart: (modules: Module[], avoidRepeated: boolean, variant: Variant) => void;
|
||||||
modules: Module[],
|
|
||||||
avoidRepeated: boolean,
|
|
||||||
variant: Variant,
|
|
||||||
) => void;
|
|
||||||
disableSelection?: boolean;
|
disableSelection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Selection({
|
export default function Selection({user, page, onStart, disableSelection = false}: Props) {
|
||||||
user,
|
|
||||||
page,
|
|
||||||
onStart,
|
|
||||||
disableSelection = false,
|
|
||||||
}: Props) {
|
|
||||||
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||||
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
||||||
const [variant, setVariant] = useState<Variant>("full");
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
@@ -56,9 +37,7 @@ export default function Selection({
|
|||||||
|
|
||||||
const toggleModule = (module: Module) => {
|
const toggleModule = (module: Module) => {
|
||||||
const modules = selectedModules.filter((x) => x !== module);
|
const modules = selectedModules.filter((x) => x !== module);
|
||||||
setSelectedModules((prev) =>
|
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
|
||||||
prev.includes(module) ? modules : [...modules, module],
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSession = async (session: Session) => {
|
const loadSession = async (session: Session) => {
|
||||||
@@ -84,41 +63,31 @@ export default function Selection({
|
|||||||
user={user}
|
user={user}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />,
|
||||||
<BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />
|
|
||||||
),
|
|
||||||
label: "Reading",
|
label: "Reading",
|
||||||
value: totalExamsByModule(stats, "reading"),
|
value: totalExamsByModule(stats, "reading"),
|
||||||
tooltip: "The amount of reading exams performed.",
|
tooltip: "The amount of reading exams performed.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />,
|
||||||
<BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />
|
|
||||||
),
|
|
||||||
label: "Listening",
|
label: "Listening",
|
||||||
value: totalExamsByModule(stats, "listening"),
|
value: totalExamsByModule(stats, "listening"),
|
||||||
tooltip: "The amount of listening exams performed.",
|
tooltip: "The amount of listening exams performed.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />,
|
||||||
<BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />
|
|
||||||
),
|
|
||||||
label: "Writing",
|
label: "Writing",
|
||||||
value: totalExamsByModule(stats, "writing"),
|
value: totalExamsByModule(stats, "writing"),
|
||||||
tooltip: "The amount of writing exams performed.",
|
tooltip: "The amount of writing exams performed.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />,
|
||||||
<BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />
|
|
||||||
),
|
|
||||||
label: "Speaking",
|
label: "Speaking",
|
||||||
value: totalExamsByModule(stats, "speaking"),
|
value: totalExamsByModule(stats, "speaking"),
|
||||||
tooltip: "The amount of speaking exams performed.",
|
tooltip: "The amount of speaking exams performed.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />,
|
||||||
<BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />
|
|
||||||
),
|
|
||||||
label: "Level",
|
label: "Level",
|
||||||
value: totalExamsByModule(stats, "level"),
|
value: totalExamsByModule(stats, "level"),
|
||||||
tooltip: "The amount of level exams performed.",
|
tooltip: "The amount of level exams performed.",
|
||||||
@@ -132,35 +101,23 @@ export default function Selection({
|
|||||||
<span className="text-mti-gray-taupe">
|
<span className="text-mti-gray-taupe">
|
||||||
{page === "exercises" && (
|
{page === "exercises" && (
|
||||||
<>
|
<>
|
||||||
In the realm of language acquisition, practice makes perfect,
|
In the realm of language acquisition, practice makes perfect, and our exercises are the key to unlocking your full
|
||||||
and our exercises are the key to unlocking your full potential.
|
potential. Dive into a world of interactive and engaging exercises that cater to diverse learning styles. From grammar
|
||||||
Dive into a world of interactive and engaging exercises that
|
drills that build a strong foundation to vocabulary challenges that broaden your lexicon, our exercises are carefully
|
||||||
cater to diverse learning styles. From grammar drills that build
|
designed to make learning English both enjoyable and effective. Whether you're looking to reinforce specific
|
||||||
a strong foundation to vocabulary challenges that broaden your
|
skills or embark on a holistic language journey, our exercises are your companions in the pursuit of excellence.
|
||||||
lexicon, our exercises are carefully designed to make learning
|
Embrace the joy of learning as you navigate through a variety of activities that cater to every facet of language
|
||||||
English both enjoyable and effective. Whether you're
|
acquisition. Your linguistic adventure starts here!
|
||||||
looking to reinforce specific skills or embark on a holistic
|
|
||||||
language journey, our exercises are your companions in the
|
|
||||||
pursuit of excellence. Embrace the joy of learning as you
|
|
||||||
navigate through a variety of activities that cater to every
|
|
||||||
facet of language acquisition. Your linguistic adventure starts
|
|
||||||
here!
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{page === "exams" && (
|
{page === "exams" && (
|
||||||
<>
|
<>
|
||||||
Welcome to the heart of success on your English language
|
Welcome to the heart of success on your English language journey! Our exams are crafted with precision to assess and
|
||||||
journey! Our exams are crafted with precision to assess and
|
enhance your language skills. Each test is a passport to your linguistic prowess, designed to challenge and elevate
|
||||||
enhance your language skills. Each test is a passport to your
|
your abilities. Whether you're a beginner or a seasoned learner, our exams cater to all levels, providing a
|
||||||
linguistic prowess, designed to challenge and elevate your
|
comprehensive evaluation of your reading, writing, speaking, and listening skills. Prepare to embark on a journey of
|
||||||
abilities. Whether you're a beginner or a seasoned learner,
|
self-discovery and language mastery as you navigate through our thoughtfully curated exams. Your success is not just a
|
||||||
our exams cater to all levels, providing a comprehensive
|
destination; it's a testament to your dedication and our commitment to empowering you with the English language.
|
||||||
evaluation of your reading, writing, speaking, and listening
|
|
||||||
skills. Prepare to embark on a journey of self-discovery and
|
|
||||||
language mastery as you navigate through our thoughtfully
|
|
||||||
curated exams. Your success is not just a destination; it's
|
|
||||||
a testament to your dedication and our commitment to empowering
|
|
||||||
you with the English language.
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -171,26 +128,16 @@ export default function Selection({
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={reload}
|
onClick={reload}
|
||||||
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out"
|
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
|
||||||
>
|
<span className="text-mti-black text-lg font-bold">Unfinished Sessions</span>
|
||||||
<span className="text-mti-black text-lg font-bold">
|
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
|
||||||
Unfinished Sessions
|
|
||||||
</span>
|
|
||||||
<BsArrowRepeat
|
|
||||||
className={clsx("text-xl", isLoading && "animate-spin")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
{sessions
|
{sessions
|
||||||
.sort((a, b) => moment(b.date).diff(moment(a.date)))
|
.sort((a, b) => moment(b.date).diff(moment(a.date)))
|
||||||
.map((session) => (
|
.map((session) => (
|
||||||
<SessionCard
|
<SessionCard session={session} key={session.sessionId} reload={reload} loadSession={loadSession} />
|
||||||
session={session}
|
|
||||||
key={session.sessionId}
|
|
||||||
reload={reload}
|
|
||||||
loadSession={loadSession}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
@@ -198,170 +145,108 @@ export default function Selection({
|
|||||||
|
|
||||||
<section className="-lg:flex-col -lg:items-center -lg:gap-12 mt-4 flex w-full justify-between gap-8">
|
<section className="-lg:flex-col -lg:items-center -lg:gap-12 mt-4 flex w-full justify-between gap-8">
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
|
||||||
!disableSelection && !selectedModules.includes("level")
|
|
||||||
? () => toggleModule("reading")
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("reading") || disableSelection
|
selectedModules.includes("reading") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
? "border-mti-purple-light"
|
)}>
|
||||||
: "border-mti-gray-platinum",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="bg-ielts-reading absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
<div className="bg-ielts-reading absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsBook className="h-7 w-7 text-white" />
|
<BsBook className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Reading:</span>
|
<span className="font-semibold">Reading:</span>
|
||||||
<p className="text-left text-xs">
|
<p className="text-left text-xs">
|
||||||
Expand your vocabulary, improve your reading comprehension and
|
Expand your vocabulary, improve your reading comprehension and improve your ability to interpret texts in English.
|
||||||
improve your ability to interpret texts in English.
|
|
||||||
</p>
|
</p>
|
||||||
{!selectedModules.includes("reading") &&
|
{!selectedModules.includes("reading") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
!selectedModules.includes("level") &&
|
|
||||||
!disableSelection && (
|
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("reading") || disableSelection) && (
|
{(selectedModules.includes("reading") || disableSelection) && (
|
||||||
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && (
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
|
||||||
!disableSelection && !selectedModules.includes("level")
|
|
||||||
? () => toggleModule("listening")
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("listening") || disableSelection
|
selectedModules.includes("listening") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
? "border-mti-purple-light"
|
)}>
|
||||||
: "border-mti-gray-platinum",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="bg-ielts-listening absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
<div className="bg-ielts-listening absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsHeadphones className="h-7 w-7 text-white" />
|
<BsHeadphones className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Listening:</span>
|
<span className="font-semibold">Listening:</span>
|
||||||
<p className="text-left text-xs">
|
<p className="text-left text-xs">
|
||||||
Improve your ability to follow conversations in English and your
|
Improve your ability to follow conversations in English and your ability to understand different accents and intonations.
|
||||||
ability to understand different accents and intonations.
|
|
||||||
</p>
|
</p>
|
||||||
{!selectedModules.includes("listening") &&
|
{!selectedModules.includes("listening") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
!selectedModules.includes("level") &&
|
|
||||||
!disableSelection && (
|
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("listening") || disableSelection) && (
|
{(selectedModules.includes("listening") || disableSelection) && (
|
||||||
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && (
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
|
||||||
!disableSelection && !selectedModules.includes("level")
|
|
||||||
? () => toggleModule("writing")
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("writing") || disableSelection
|
selectedModules.includes("writing") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
? "border-mti-purple-light"
|
)}>
|
||||||
: "border-mti-gray-platinum",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="bg-ielts-writing absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
<div className="bg-ielts-writing absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsPen className="h-7 w-7 text-white" />
|
<BsPen className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Writing:</span>
|
<span className="font-semibold">Writing:</span>
|
||||||
<p className="text-left text-xs">
|
<p className="text-left text-xs">
|
||||||
Allow you to practice writing in a variety of formats, from simple
|
Allow you to practice writing in a variety of formats, from simple paragraphs to complex essays.
|
||||||
paragraphs to complex essays.
|
|
||||||
</p>
|
</p>
|
||||||
{!selectedModules.includes("writing") &&
|
{!selectedModules.includes("writing") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
!selectedModules.includes("level") &&
|
|
||||||
!disableSelection && (
|
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("writing") || disableSelection) && (
|
{(selectedModules.includes("writing") || disableSelection) && (
|
||||||
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && (
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
|
||||||
!disableSelection && !selectedModules.includes("level")
|
|
||||||
? () => toggleModule("speaking")
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("speaking") || disableSelection
|
selectedModules.includes("speaking") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
? "border-mti-purple-light"
|
)}>
|
||||||
: "border-mti-gray-platinum",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="bg-ielts-speaking absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
<div className="bg-ielts-speaking absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsMegaphone className="h-7 w-7 text-white" />
|
<BsMegaphone className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Speaking:</span>
|
<span className="font-semibold">Speaking:</span>
|
||||||
<p className="text-left text-xs">
|
<p className="text-left text-xs">
|
||||||
You'll have access to interactive dialogs, pronunciation
|
You'll have access to interactive dialogs, pronunciation exercises and speech recordings.
|
||||||
exercises and speech recordings.
|
|
||||||
</p>
|
</p>
|
||||||
{!selectedModules.includes("speaking") &&
|
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
!selectedModules.includes("level") &&
|
|
||||||
!disableSelection && (
|
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("speaking") || disableSelection) && (
|
{(selectedModules.includes("speaking") || disableSelection) && (
|
||||||
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && (
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{!disableSelection && (
|
{!disableSelection && (
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={selectedModules.length === 0 || selectedModules.includes("level") ? () => toggleModule("level") : undefined}
|
||||||
selectedModules.length === 0 ||
|
|
||||||
selectedModules.includes("level")
|
|
||||||
? () => toggleModule("level")
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("level") || disableSelection
|
selectedModules.includes("level") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
? "border-mti-purple-light"
|
)}>
|
||||||
: "border-mti-gray-platinum",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="bg-ielts-level absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
<div className="bg-ielts-level absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsClipboard className="h-7 w-7 text-white" />
|
<BsClipboard className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Level:</span>
|
<span className="font-semibold">Level:</span>
|
||||||
<p className="text-left text-xs">
|
<p className="text-left text-xs">You'll be able to test your english level with multiple choice questions.</p>
|
||||||
You'll be able to test your english level with multiple
|
{!selectedModules.includes("level") && selectedModules.length === 0 && !disableSelection && (
|
||||||
choice questions.
|
|
||||||
</p>
|
|
||||||
{!selectedModules.includes("level") &&
|
|
||||||
selectedModules.length === 0 &&
|
|
||||||
!disableSelection && (
|
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("level") || disableSelection) && (
|
{(selectedModules.includes("level") || disableSelection) && (
|
||||||
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{!selectedModules.includes("level") &&
|
{!selectedModules.includes("level") && selectedModules.length > 0 && (
|
||||||
selectedModules.length > 0 && (
|
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -371,68 +256,51 @@ export default function Selection({
|
|||||||
<div className="flex w-full flex-col items-center gap-3">
|
<div className="flex w-full flex-col items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="text-mti-gray-dim -md:justify-center flex w-full cursor-pointer items-center gap-3 text-sm"
|
className="text-mti-gray-dim -md:justify-center flex w-full cursor-pointer items-center gap-3 text-sm"
|
||||||
onClick={() => setAvoidRepeatedExams((prev) => !prev)}
|
onClick={() => setAvoidRepeatedExams((prev) => !prev)}>
|
||||||
>
|
|
||||||
<input type="checkbox" className="hidden" />
|
<input type="checkbox" className="hidden" />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
|
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
avoidRepeatedExams && "!bg-mti-purple-light ",
|
avoidRepeatedExams && "!bg-mti-purple-light ",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
<BsCheck color="white" className="h-full w-full" />
|
<BsCheck color="white" className="h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span className="tooltip" data-tip="If possible, the platform will choose exams not yet done.">
|
||||||
className="tooltip"
|
|
||||||
data-tip="If possible, the platform will choose exams not yet done."
|
|
||||||
>
|
|
||||||
Avoid Repeated Questions
|
Avoid Repeated Questions
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="text-mti-gray-dim -md:justify-center flex w-full cursor-pointer items-center gap-3 text-sm"
|
className="text-mti-gray-dim -md:justify-center flex w-full cursor-pointer items-center gap-3 text-sm"
|
||||||
// onClick={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
|
onClick={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
|
||||||
>
|
<input type="checkbox" className="hidden" />
|
||||||
<input type="checkbox" className="hidden" disabled />
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
|
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
variant === "full" && "!bg-mti-purple-light ",
|
variant === "full" && "!bg-mti-purple-light ",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
<BsCheck color="white" className="h-full w-full" />
|
<BsCheck color="white" className="h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
<span>Full length exams</span>
|
<span>Full length exams</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="tooltip w-full" data-tip={`Your screen size is too small to do ${page}`}>
|
||||||
className="tooltip w-full"
|
<Button color="purple" className="w-full max-w-xs px-12 md:hidden" disabled>
|
||||||
data-tip={`Your screen size is too small to do ${page}`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
className="w-full max-w-xs px-12 md:hidden"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
Start Exam
|
Start Exam
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onStart(
|
onStart(
|
||||||
!disableSelection
|
!disableSelection ? selectedModules.sort(sortByModuleName) : ["reading", "listening", "writing", "speaking"],
|
||||||
? selectedModules.sort(sortByModuleName)
|
|
||||||
: ["reading", "listening", "writing", "speaking"],
|
|
||||||
avoidRepeatedExams,
|
avoidRepeatedExams,
|
||||||
variant,
|
variant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
color="purple"
|
color="purple"
|
||||||
className="-md:hidden w-full max-w-xs px-12 md:self-end"
|
className="-md:hidden w-full max-w-xs px-12 md:self-end"
|
||||||
disabled={selectedModules.length === 0 && !disableSelection}
|
disabled={selectedModules.length === 0 && !disableSelection}>
|
||||||
>
|
|
||||||
Start Exam
|
Start Exam
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {Assignment} from "@/interfaces/results";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
export default function useAssignments({assigner, assignees}: {assigner?: string; assignees?: string}) {
|
export default function useAssignments({assigner, assignees, corporate}: {assigner?: string; assignees?: string; corporate?: string}) {
|
||||||
const [assignments, setAssignments] = useState<Assignment[]>([]);
|
const [assignments, setAssignments] = useState<Assignment[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
@@ -10,12 +10,13 @@ export default function useAssignments({assigner, assignees}: {assigner?: string
|
|||||||
const getData = () => {
|
const getData = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.get<Assignment[]>("/api/assignments")
|
.get<Assignment[]>(!corporate ? "/api/assignments" : `/api/assignments/corporate?id=${corporate}`)
|
||||||
.then((response) => {
|
.then(async (response) => {
|
||||||
if (assigner) {
|
if (assigner) {
|
||||||
setAssignments(response.data.filter((a) => a.assigner === assigner));
|
setAssignments(response.data.filter((a) => a.assigner === assigner));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assignees) {
|
if (assignees) {
|
||||||
setAssignments(response.data.filter((a) => a.assignees.filter((x) => assignees.includes(x)).length > 0));
|
setAssignments(response.data.filter((a) => a.assignees.filter((x) => assignees.includes(x)).length > 0));
|
||||||
return;
|
return;
|
||||||
@@ -26,7 +27,7 @@ export default function useAssignments({assigner, assignees}: {assigner?: string
|
|||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(getData, [assignees, assigner]);
|
useEffect(getData, [assignees, assigner, corporate]);
|
||||||
|
|
||||||
return {assignments, isLoading, isError, reload: getData};
|
return {assignments, isLoading, isError, reload: getData};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,40 @@ import {Group, User} from "@/interfaces/user";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
export default function useGroups(admin?: string, userType?: string) {
|
interface Props {
|
||||||
|
admin?: string;
|
||||||
|
userType?: string;
|
||||||
|
adminAdmins?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useGroups({admin, userType, adminAdmins}: Props) {
|
||||||
const [groups, setGroups] = useState<Group[]>([]);
|
const [groups, setGroups] = useState<Group[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
const isMasterType = userType?.startsWith('master');
|
const isMasterType = userType?.startsWith("master");
|
||||||
|
|
||||||
const getData = () => {
|
const getData = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const url = admin ? `/api/groups?admin=${admin}` : "/api/groups";
|
const url = admin && !adminAdmins ? `/api/groups?admin=${admin}` : "/api/groups";
|
||||||
axios
|
axios
|
||||||
.get<Group[]>(url)
|
.get<Group[]>(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if(isMasterType) {
|
if (isMasterType) return setGroups(response.data);
|
||||||
return setGroups(response.data);
|
|
||||||
}
|
|
||||||
const filter = (g: Group) => g.admin === admin || g.participants.includes(admin || "");
|
|
||||||
|
|
||||||
const filteredGroups = admin ? response.data.filter(filter) : response.data;
|
const filterByAdmins = !!adminAdmins
|
||||||
|
? [adminAdmins, ...response.data.filter((g) => g.participants.includes(adminAdmins)).flatMap((g) => g.admin)]
|
||||||
|
: [admin];
|
||||||
|
const adminFilter = (g: Group) => filterByAdmins.includes(g.admin) || g.participants.includes(admin || "");
|
||||||
|
|
||||||
|
const filteredGroups = !!admin || !!adminAdmins ? response.data.filter(adminFilter) : response.data;
|
||||||
return setGroups(admin ? filteredGroups.map((g) => ({...g, disableEditing: g.disableEditing || g.admin !== admin})) : filteredGroups);
|
return setGroups(admin ? filteredGroups.map((g) => ({...g, disableEditing: g.disableEditing || g.admin !== admin})) : filteredGroups);
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(getData, [admin, isMasterType]);
|
useEffect(getData, [admin, adminAdmins, isMasterType]);
|
||||||
|
|
||||||
return {groups, isLoading, isError, reload: getData};
|
return {groups, isLoading, isError, reload: getData};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export function useListSearch<T>(fields: string[][], rows: T[]) {
|
|||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
return value.toLowerCase().includes(searchText);
|
return value.toLowerCase().includes(searchText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return (value as Number).toString().includes(searchText);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [fields, rows, text]);
|
}, [fields, rows, text]);
|
||||||
|
|||||||
28
src/hooks/usePermissions.tsx
Normal file
28
src/hooks/usePermissions.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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[]);
|
||||||
|
setPermissions(permissionTypes);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(getData, [user]);
|
||||||
|
|
||||||
|
return {permissions, isLoading, isError, reload: getData};
|
||||||
|
}
|
||||||
@@ -1,57 +1,89 @@
|
|||||||
export const markets = ["au", "br", "de"] as const;
|
export interface PermissionTopic {
|
||||||
|
topic: string;
|
||||||
|
list: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export const permissions = [
|
export const permissions = [
|
||||||
// generate codes are basicly invites
|
{
|
||||||
"createCodeStudent",
|
topic: "Manage Corporate",
|
||||||
"createCodeTeacher",
|
list: [
|
||||||
|
"viewCorporate",
|
||||||
|
"editCorporate",
|
||||||
|
"deleteCorporate",
|
||||||
"createCodeCorporate",
|
"createCodeCorporate",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: "Manage Admin",
|
||||||
|
list: ["viewAdmin", "editAdmin", "deleteAdmin", "createCodeAdmin"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: "Manage Student",
|
||||||
|
list: ["viewStudent", "editStudent", "deleteStudent", "createCodeStudent"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: "Manage Teacher",
|
||||||
|
list: ["viewTeacher", "editTeacher", "deleteTeacher", "createCodeTeacher"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: "Manage Country Manager",
|
||||||
|
list: [
|
||||||
|
"viewCountryManager",
|
||||||
|
"editCountryManager",
|
||||||
|
"deleteCountryManager",
|
||||||
"createCodeCountryManager",
|
"createCodeCountryManager",
|
||||||
"createCodeAdmin",
|
],
|
||||||
// exams
|
},
|
||||||
|
{
|
||||||
|
topic: "Manage Exams",
|
||||||
|
list: [
|
||||||
"createReadingExam",
|
"createReadingExam",
|
||||||
"createListeningExam",
|
"createListeningExam",
|
||||||
"createWritingExam",
|
"createWritingExam",
|
||||||
"createSpeakingExam",
|
"createSpeakingExam",
|
||||||
"createLevelExam",
|
"createLevelExam",
|
||||||
// view pages
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: "View Pages",
|
||||||
|
list: [
|
||||||
"viewExams",
|
"viewExams",
|
||||||
"viewExercises",
|
"viewExercises",
|
||||||
"viewRecords",
|
"viewRecords",
|
||||||
"viewStats",
|
"viewStats",
|
||||||
"viewTickets",
|
"viewTickets",
|
||||||
"viewPaymentRecords",
|
"viewPaymentRecords",
|
||||||
// view data
|
],
|
||||||
"viewStudent",
|
},
|
||||||
"viewTeacher",
|
{
|
||||||
"viewCorporate",
|
topic: "Manage Group",
|
||||||
"viewCountryManager",
|
list: ["viewGroup", "editGroup", "deleteGroup", "createGroup"],
|
||||||
"viewAdmin",
|
},
|
||||||
"viewGroup",
|
{
|
||||||
"viewCodes",
|
topic: "Manage Codes",
|
||||||
// edit data
|
list: ["viewCodes", "deleteCodes", "createCodes"],
|
||||||
"editStudent",
|
},
|
||||||
"editTeacher",
|
{
|
||||||
"editCorporate",
|
topic: "Others",
|
||||||
"editCountryManager",
|
list: ["all"],
|
||||||
"editAdmin",
|
},
|
||||||
"editGroup",
|
|
||||||
// delete data
|
|
||||||
"deleteStudent",
|
|
||||||
"deleteTeacher",
|
|
||||||
"deleteCorporate",
|
|
||||||
"deleteCountryManager",
|
|
||||||
"deleteAdmin",
|
|
||||||
"deleteGroup",
|
|
||||||
"deleteCodes",
|
|
||||||
// create options
|
|
||||||
"createGroup",
|
|
||||||
"createCodes"
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type PermissionType = (typeof permissions)[keyof typeof permissions];
|
const permissionsList = [
|
||||||
|
...new Set(
|
||||||
|
permissions.reduce(
|
||||||
|
(accm: string[], permission) => [...accm, ...permission.list],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
export type PermissionType =
|
||||||
|
(typeof permissionsList)[keyof typeof permissionsList];
|
||||||
|
|
||||||
export interface Permission {
|
export interface Permission {
|
||||||
id: string;
|
id: string;
|
||||||
type: PermissionType;
|
type: PermissionType;
|
||||||
|
topic: string;
|
||||||
users: string[];
|
users: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,7 @@ import { Module } from ".";
|
|||||||
import {InstructorGender, ShuffleMap} from "./exam";
|
import {InstructorGender, ShuffleMap} from "./exam";
|
||||||
import {PermissionType} from "./permissions";
|
import {PermissionType} from "./permissions";
|
||||||
|
|
||||||
export type User =
|
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser;
|
||||||
| StudentUser
|
|
||||||
| TeacherUser
|
|
||||||
| CorporateUser
|
|
||||||
| AgentUser
|
|
||||||
| AdminUser
|
|
||||||
| DeveloperUser
|
|
||||||
| MasterCorporateUser;
|
|
||||||
export type UserStatus = "active" | "disabled" | "paymentDue";
|
export type UserStatus = "active" | "disabled" | "paymentDue";
|
||||||
|
|
||||||
export interface BasicUser {
|
export interface BasicUser {
|
||||||
@@ -27,7 +20,8 @@ export interface BasicUser {
|
|||||||
subscriptionExpirationDate?: null | Date;
|
subscriptionExpirationDate?: null | Date;
|
||||||
registrationDate?: Date;
|
registrationDate?: Date;
|
||||||
status: UserStatus;
|
status: UserStatus;
|
||||||
permissions: PermissionType[],
|
permissions: PermissionType[];
|
||||||
|
lastLogin?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StudentUser extends BasicUser {
|
export interface StudentUser extends BasicUser {
|
||||||
@@ -112,15 +106,8 @@ export interface DemographicCorporateInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Gender = "male" | "female" | "other";
|
export type Gender = "male" | "female" | "other";
|
||||||
export type EmploymentStatus =
|
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
|
||||||
| "employed"
|
export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
|
||||||
| "student"
|
|
||||||
| "self-employed"
|
|
||||||
| "unemployed"
|
|
||||||
| "retired"
|
|
||||||
| "other";
|
|
||||||
export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] =
|
|
||||||
[
|
|
||||||
{status: "student", label: "Student"},
|
{status: "student", label: "Student"},
|
||||||
{status: "employed", label: "Employed"},
|
{status: "employed", label: "Employed"},
|
||||||
{status: "unemployed", label: "Unemployed"},
|
{status: "unemployed", label: "Unemployed"},
|
||||||
@@ -171,20 +158,5 @@ export interface Code {
|
|||||||
passport_id?: string;
|
passport_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Type =
|
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate";
|
||||||
| "student"
|
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"];
|
||||||
| "teacher"
|
|
||||||
| "corporate"
|
|
||||||
| "admin"
|
|
||||||
| "developer"
|
|
||||||
| "agent"
|
|
||||||
| "mastercorporate";
|
|
||||||
export const userTypes: Type[] = [
|
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"corporate",
|
|
||||||
"admin",
|
|
||||||
"developer",
|
|
||||||
"agent",
|
|
||||||
"mastercorporate",
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ 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[]};
|
||||||
@@ -47,44 +46,26 @@ const USER_TYPE_PERMISSIONS: {
|
|||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
perm: "createCodeAdmin",
|
perm: "createCodeAdmin",
|
||||||
list: [
|
list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"agent",
|
|
||||||
"corporate",
|
|
||||||
"admin",
|
|
||||||
"mastercorporate",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
developer: {
|
developer: {
|
||||||
perm: undefined,
|
perm: undefined,
|
||||||
list: [
|
list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
|
||||||
"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 [isLoading, setIsLoading] = useState(false);
|
||||||
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 [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
||||||
const [type, setType] = useState<Type>("student");
|
const [type, setType] = useState<Type>("student");
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
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",
|
||||||
@@ -104,14 +85,7 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
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,
|
|
||||||
lastName,
|
|
||||||
country,
|
|
||||||
passport_id,
|
|
||||||
email,
|
|
||||||
...phone
|
|
||||||
] = row as string[];
|
|
||||||
return EMAIL_REGEX.test(email.toString().trim())
|
return EMAIL_REGEX.test(email.toString().trim())
|
||||||
? {
|
? {
|
||||||
email: email.toString().trim().toLowerCase(),
|
email: email.toString().trim().toLowerCase(),
|
||||||
@@ -121,12 +95,12 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
: undefined;
|
: undefined;
|
||||||
})
|
})
|
||||||
.filter((x) => !!x) as typeof infos,
|
.filter((x) => !!x) as typeof infos,
|
||||||
(x) => x.email
|
(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();
|
||||||
}
|
}
|
||||||
@@ -134,7 +108,7 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
@@ -144,41 +118,24 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
}, [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
|
const existingUsers = infos
|
||||||
.filter((x) => users.map((u) => u.email).includes(x.email))
|
.filter((x) => users.map((u) => u.email).includes(x.email))
|
||||||
.map((i) => users.find((u) => u.email === i.email))
|
.map((i) => users.find((u) => u.email === i.email))
|
||||||
.filter((x) => !!x && x.type === "student") as User[];
|
.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 =
|
|
||||||
existingUsers.length > 0
|
|
||||||
? `invite ${existingUsers.length} registered student(s)`
|
|
||||||
: undefined;
|
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
`You are about to ${[newUsersSentence, existingUsersSentence]
|
`You are about to ${[newUsersSentence, existingUsersSentence].filter((x) => !!x).join(" and ")}, are you sure you want to continue?`,
|
||||||
.filter((x) => !!x)
|
|
||||||
.join(" and ")}, are you sure you want to continue?`
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
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) =>
|
|
||||||
await axios.post(`/api/invites`, { to: u.id, from: user.id })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(() =>
|
|
||||||
toast.success(
|
|
||||||
`Successfully invited ${existingUsers.length} registered student(s)!`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (newUsers.length === 0) setIsLoading(false);
|
if (newUsers.length === 0) setIsLoading(false);
|
||||||
});
|
});
|
||||||
@@ -202,10 +159,10 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
.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;
|
||||||
}
|
}
|
||||||
@@ -232,30 +189,18 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)} title="Excel File Format">
|
||||||
isOpen={showHelp}
|
|
||||||
onClose={() => setShowHelp(false)}
|
|
||||||
title="Excel File Format"
|
|
||||||
>
|
|
||||||
<div className="mt-4 flex flex-col gap-2">
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
<span>Please upload an Excel file with the following format:</span>
|
<span>Please upload an Excel file with the following format:</span>
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
<th className="border border-neutral-200 px-2 py-1">First Name</th>
|
||||||
First Name
|
<th className="border border-neutral-200 px-2 py-1">Last Name</th>
|
||||||
</th>
|
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
|
||||||
Last Name
|
|
||||||
</th>
|
|
||||||
<th className="border border-neutral-200 px-2 py-1">Country</th>
|
<th className="border border-neutral-200 px-2 py-1">Country</th>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
<th className="border border-neutral-200 px-2 py-1">Passport/National ID</th>
|
||||||
Passport/National ID
|
|
||||||
</th>
|
|
||||||
<th className="border border-neutral-200 px-2 py-1">E-mail</th>
|
<th className="border border-neutral-200 px-2 py-1">E-mail</th>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
<th className="border border-neutral-200 px-2 py-1">Phone Number</th>
|
||||||
Phone Number
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
@@ -264,55 +209,27 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
<ul>
|
<ul>
|
||||||
<li>- All incorrect e-mails will be ignored;</li>
|
<li>- All incorrect e-mails will be ignored;</li>
|
||||||
<li>- All already registered e-mails will be ignored;</li>
|
<li>- All already registered e-mails will be ignored;</li>
|
||||||
<li>
|
<li>- You may have a header row with the format above, however, it is not necessary;</li>
|
||||||
- You may have a header row with the format above, however, it
|
<li>- All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.</li>
|
||||||
is not necessary;
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
- All of the e-mails in the file will receive an e-mail to join
|
|
||||||
EnCoach with the role selected below.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
|
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
<label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label>
|
||||||
Choose an Excel file
|
<div className="tooltip cursor-pointer" data-tip="Excel File Format" onClick={() => setShowHelp(true)}>
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className="tooltip cursor-pointer"
|
|
||||||
data-tip="Excel File Format"
|
|
||||||
onClick={() => setShowHelp(true)}
|
|
||||||
>
|
|
||||||
<BsQuestionCircleFill />
|
<BsQuestionCircleFill />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
|
||||||
onClick={openFilePicker}
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
||||||
</Button>
|
</Button>
|
||||||
{user &&
|
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||||
checkAccess(user, [
|
|
||||||
"developer",
|
|
||||||
"admin",
|
|
||||||
"corporate",
|
|
||||||
"mastercorporate",
|
|
||||||
]) && (
|
|
||||||
<>
|
<>
|
||||||
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
<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">
|
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
||||||
Expiry Date
|
<Checkbox isChecked={isExpiryDateEnabled} onChange={setIsExpiryDateEnabled} disabled={!!user.subscriptionExpirationDate}>
|
||||||
</label>
|
|
||||||
<Checkbox
|
|
||||||
isChecked={isExpiryDateEnabled}
|
|
||||||
onChange={setIsExpiryDateEnabled}
|
|
||||||
disabled={!!user.subscriptionExpirationDate}
|
|
||||||
>
|
|
||||||
Enabled
|
Enabled
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -321,13 +238,11 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
|
"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",
|
"hover:border-mti-purple tooltip",
|
||||||
"transition duration-300 ease-in-out"
|
"transition duration-300 ease-in-out",
|
||||||
)}
|
)}
|
||||||
filterDate={(date) =>
|
filterDate={(date) =>
|
||||||
moment(date).isAfter(new Date()) &&
|
moment(date).isAfter(new Date()) &&
|
||||||
(user.subscriptionExpirationDate
|
(user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true)
|
||||||
? moment(date).isBefore(user.subscriptionExpirationDate)
|
|
||||||
: true)
|
|
||||||
}
|
}
|
||||||
dateFormat="dd/MM/yyyy"
|
dateFormat="dd/MM/yyyy"
|
||||||
selected={expiryDate}
|
selected={expiryDate}
|
||||||
@@ -336,19 +251,16 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
<label className="text-mti-gray-dim text-base font-normal">Select the type of user they should be</label>
|
||||||
Select the type of user they should be
|
|
||||||
</label>
|
|
||||||
{user && (
|
{user && (
|
||||||
<select
|
<select
|
||||||
defaultValue="student"
|
defaultValue="student"
|
||||||
onChange={(e) => setType(e.target.value as typeof user.type)}
|
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"
|
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)
|
{Object.keys(USER_TYPE_LABELS)
|
||||||
.filter((x) => {
|
.filter((x) => {
|
||||||
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
|
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
|
||||||
return checkAccess(user, getTypesOfUser(list), perm);
|
return checkAccess(user, getTypesOfUser(list), permissions, perm);
|
||||||
})
|
})
|
||||||
.map((type) => (
|
.map((type) => (
|
||||||
<option key={type} value={type}>
|
<option key={type} value={type}>
|
||||||
@@ -357,17 +269,8 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
{checkAccess(
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "createCodes") && (
|
||||||
user,
|
<Button onClick={generateAndInvite} disabled={infos.length === 0 || (isExpiryDateEnabled ? !expiryDate : false)}>
|
||||||
["developer", "admin", "corporate", "mastercorporate"],
|
|
||||||
"createCodes"
|
|
||||||
) && (
|
|
||||||
<Button
|
|
||||||
onClick={generateAndInvite}
|
|
||||||
disabled={
|
|
||||||
infos.length === 0 || (isExpiryDateEnabled ? !expiryDate : false)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Generate & Send
|
Generate & Send
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ import readXlsxFile from "read-excel-file";
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import {BsQuestionCircleFill} from "react-icons/bs";
|
import {BsQuestionCircleFill} from "react-icons/bs";
|
||||||
import {PermissionType} from "@/interfaces/permissions";
|
import {PermissionType} from "@/interfaces/permissions";
|
||||||
const EMAIL_REGEX = new RegExp(
|
import moment from "moment";
|
||||||
/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/
|
import {checkAccess} from "@/utils/permissions";
|
||||||
);
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
|
import ReactDatePicker from "react-datepicker";
|
||||||
|
import clsx from "clsx";
|
||||||
|
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
||||||
|
|
||||||
type Type = Exclude<UserType, "admin" | "developer" | "agent" | "mastercorporate">
|
type Type = Exclude<UserType, "admin" | "developer" | "agent" | "mastercorporate">;
|
||||||
|
|
||||||
const USER_TYPE_LABELS: {[key in Type]: string} = {
|
const USER_TYPE_LABELS: {[key in Type]: string} = {
|
||||||
student: "Student",
|
student: "Student",
|
||||||
@@ -41,13 +44,23 @@ const USER_TYPE_PERMISSIONS: {
|
|||||||
|
|
||||||
export default function BatchCreateUser({user}: {user: User}) {
|
export default function BatchCreateUser({user}: {user: User}) {
|
||||||
const [infos, setInfos] = useState<
|
const [infos, setInfos] = useState<
|
||||||
{ email: string; name: string; passport_id:string, type: Type, demographicInformation: {
|
{
|
||||||
country: string,
|
email: string;
|
||||||
passport_id:string,
|
name: string;
|
||||||
phone: string
|
passport_id: string;
|
||||||
} }[]
|
type: Type;
|
||||||
|
demographicInformation: {
|
||||||
|
country: string;
|
||||||
|
passport_id: string;
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
||||||
|
user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null,
|
||||||
|
);
|
||||||
|
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
||||||
const [type, setType] = useState<Type>("student");
|
const [type, setType] = useState<Type>("student");
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
|
|
||||||
@@ -59,6 +72,9 @@ export default function BatchCreateUser({ user }: { user: User }) {
|
|||||||
readAs: "ArrayBuffer",
|
readAs: "ArrayBuffer",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isExpiryDateEnabled) setExpiryDate(null);
|
||||||
|
}, [isExpiryDateEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filesContent.length > 0) {
|
if (filesContent.length > 0) {
|
||||||
@@ -68,19 +84,11 @@ export default function BatchCreateUser({ user }: { user: User }) {
|
|||||||
const information = uniqBy(
|
const information = uniqBy(
|
||||||
rows
|
rows
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
const [
|
const [firstName, lastName, country, passport_id, email, phone, group] = row as string[];
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
country,
|
|
||||||
passport_id,
|
|
||||||
email,
|
|
||||||
phone,
|
|
||||||
group
|
|
||||||
] = row as string[];
|
|
||||||
return EMAIL_REGEX.test(email.toString().trim())
|
return EMAIL_REGEX.test(email.toString().trim())
|
||||||
? {
|
? {
|
||||||
email: email.toString().trim().toLowerCase(),
|
email: email.toString().trim().toLowerCase(),
|
||||||
name: `${firstName ?? ""} ${lastName ?? ""}`.trim().toLowerCase(),
|
name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
|
||||||
type: type,
|
type: type,
|
||||||
passport_id: passport_id?.toString().trim() || undefined,
|
passport_id: passport_id?.toString().trim() || undefined,
|
||||||
groupName: group,
|
groupName: group,
|
||||||
@@ -88,17 +96,17 @@ export default function BatchCreateUser({ user }: { user: User }) {
|
|||||||
country: country,
|
country: country,
|
||||||
passport_id: passport_id?.toString().trim() || undefined,
|
passport_id: passport_id?.toString().trim() || undefined,
|
||||||
phone,
|
phone,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
})
|
})
|
||||||
.filter((x) => !!x) as typeof infos,
|
.filter((x) => !!x) as typeof infos,
|
||||||
(x) => x.email
|
(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();
|
||||||
}
|
}
|
||||||
@@ -106,7 +114,7 @@ export default function BatchCreateUser({ user }: { user: User }) {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
@@ -116,60 +124,40 @@ export default function BatchCreateUser({ user }: { user: User }) {
|
|||||||
}, [filesContent]);
|
}, [filesContent]);
|
||||||
|
|
||||||
const makeUsers = async () => {
|
const makeUsers = 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)
|
if (!confirm(`You are about to add ${newUsers.length}, are you sure you want to continue?`)) return;
|
||||||
);
|
|
||||||
const confirmed = confirm(
|
if (newUsers.length > 0) {
|
||||||
`You are about to add ${newUsers.length}, are you sure you want to continue?`
|
|
||||||
)
|
|
||||||
if (!confirmed)
|
|
||||||
return;
|
|
||||||
if (newUsers.length > 0)
|
|
||||||
{
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
Promise.all(newUsers.map(async (user) => {
|
|
||||||
await axios.post("/api/make_user", user)
|
try {
|
||||||
})).then((res) =>{
|
for (const newUser of newUsers) await axios.post("/api/make_user", {...newUser, type, expiryDate});
|
||||||
toast.success(
|
toast.success(`Successfully added ${newUsers.length} user(s)!`);
|
||||||
`Successfully added ${newUsers.length} user(s)!`
|
} catch {
|
||||||
)}).finally(() => {
|
toast.error("Something went wrong, please try again later!");
|
||||||
return clear();
|
} finally {
|
||||||
})
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setInfos([]);
|
setInfos([]);
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)} title="Excel File Format">
|
||||||
isOpen={showHelp}
|
|
||||||
onClose={() => setShowHelp(false)}
|
|
||||||
title="Excel File Format"
|
|
||||||
>
|
|
||||||
<div className="mt-4 flex flex-col gap-2">
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
<span>Please upload an Excel file with the following format:</span>
|
<span>Please upload an Excel file with the following format:</span>
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
<th className="border border-neutral-200 px-2 py-1">First Name</th>
|
||||||
First Name
|
<th className="border border-neutral-200 px-2 py-1">Last Name</th>
|
||||||
</th>
|
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
|
||||||
Last Name
|
|
||||||
</th>
|
|
||||||
<th className="border border-neutral-200 px-2 py-1">Country</th>
|
<th className="border border-neutral-200 px-2 py-1">Country</th>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
<th className="border border-neutral-200 px-2 py-1">Passport/National ID</th>
|
||||||
Passport/National ID
|
|
||||||
</th>
|
|
||||||
<th className="border border-neutral-200 px-2 py-1">E-mail</th>
|
<th className="border border-neutral-200 px-2 py-1">E-mail</th>
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
<th className="border border-neutral-200 px-2 py-1">Phone Number</th>
|
||||||
Phone Number
|
<th className="border border-neutral-200 px-2 py-1">Group Name</th>
|
||||||
</th>
|
|
||||||
<th className="border border-neutral-200 px-2 py-1">
|
|
||||||
Group Name
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
@@ -178,63 +166,62 @@ export default function BatchCreateUser({ user }: { user: User }) {
|
|||||||
<ul>
|
<ul>
|
||||||
<li>- All incorrect e-mails will be ignored;</li>
|
<li>- All incorrect e-mails will be ignored;</li>
|
||||||
<li>- All already registered e-mails will be ignored;</li>
|
<li>- All already registered e-mails will be ignored;</li>
|
||||||
<li>
|
<li>- You may have a header row with the format above, however, it is not necessary;</li>
|
||||||
- You may have a header row with the format above, however, it
|
<li>- All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.</li>
|
||||||
is not necessary;
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
- All of the e-mails in the file will receive an e-mail to join
|
|
||||||
EnCoach with the role selected below.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
|
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
<label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label>
|
||||||
Choose an Excel file
|
<div className="tooltip cursor-pointer" data-tip="Excel File Format" onClick={() => setShowHelp(true)}>
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className="tooltip cursor-pointer"
|
|
||||||
data-tip="Excel File Format"
|
|
||||||
onClick={() => setShowHelp(true)}
|
|
||||||
>
|
|
||||||
<BsQuestionCircleFill />
|
<BsQuestionCircleFill />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
|
||||||
onClick={openFilePicker}
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
||||||
</Button>
|
</Button>
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||||
Select the type of user they should be
|
<>
|
||||||
</label>
|
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
||||||
|
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
||||||
|
<Checkbox isChecked={isExpiryDateEnabled} onChange={setIsExpiryDateEnabled} disabled={!!user.subscriptionExpirationDate}>
|
||||||
|
Enabled
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
{isExpiryDateEnabled && (
|
||||||
|
<ReactDatePicker
|
||||||
|
className={clsx(
|
||||||
|
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
|
||||||
|
"hover:border-mti-purple tooltip",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
)}
|
||||||
|
filterDate={(date) =>
|
||||||
|
moment(date).isAfter(new Date()) &&
|
||||||
|
(user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true)
|
||||||
|
}
|
||||||
|
dateFormat="dd/MM/yyyy"
|
||||||
|
selected={expiryDate}
|
||||||
|
onChange={(date) => setExpiryDate(date)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<label className="text-mti-gray-dim text-base font-normal">Select the type of user they should be</label>
|
||||||
{user && (
|
{user && (
|
||||||
<select
|
<select
|
||||||
defaultValue="student"
|
defaultValue="student"
|
||||||
onChange={(e) => setType(e.target.value as Type)}
|
onChange={(e) => setType(e.target.value as 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"
|
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).map((type) => (
|
||||||
|
|
||||||
{Object.keys(USER_TYPE_LABELS)
|
|
||||||
.map((type) => (
|
|
||||||
<option key={type} value={type}>
|
<option key={type} value={type}>
|
||||||
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
|
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button className="my-auto" onClick={makeUsers} disabled={infos.length === 0}>
|
||||||
className="my-auto"
|
|
||||||
onClick={makeUsers}
|
|
||||||
disabled={
|
|
||||||
infos.length === 0
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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[]};
|
||||||
@@ -39,38 +40,22 @@ const USER_TYPE_PERMISSIONS: {
|
|||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
perm: "createCodeAdmin",
|
perm: "createCodeAdmin",
|
||||||
list: [
|
list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"agent",
|
|
||||||
"corporate",
|
|
||||||
"admin",
|
|
||||||
"mastercorporate",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
developer: {
|
developer: {
|
||||||
perm: undefined,
|
perm: undefined,
|
||||||
list: [
|
list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
|
||||||
"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 [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
||||||
const [type, setType] = useState<Type>("student");
|
const [type, setType] = useState<Type>("student");
|
||||||
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isExpiryDateEnabled) setExpiryDate(null);
|
if (!isExpiryDateEnabled) setExpiryDate(null);
|
||||||
@@ -109,19 +94,16 @@ export default function CodeGenerator({ user }: { user: User }) {
|
|||||||
|
|
||||||
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
|
|
||||||
</label>
|
|
||||||
{user && (
|
{user && (
|
||||||
<select
|
<select
|
||||||
defaultValue="student"
|
defaultValue="student"
|
||||||
onChange={(e) => setType(e.target.value as typeof user.type)}
|
onChange={(e) => setType(e.target.value as typeof user.type)}
|
||||||
className="p-6 w-full min-w-[350px] min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white"
|
className="p-6 w-full min-w-[350px] min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
|
||||||
>
|
|
||||||
{Object.keys(USER_TYPE_LABELS)
|
{Object.keys(USER_TYPE_LABELS)
|
||||||
.filter((x) => {
|
.filter((x) => {
|
||||||
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
|
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
|
||||||
return checkAccess(user, list, perm);
|
return checkAccess(user, list, permissions, perm);
|
||||||
})
|
})
|
||||||
.map((type) => (
|
.map((type) => (
|
||||||
<option key={type} value={type}>
|
<option key={type} value={type}>
|
||||||
@@ -133,14 +115,8 @@ export default function CodeGenerator({ user }: { user: User }) {
|
|||||||
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||||
<>
|
<>
|
||||||
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
<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">
|
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
||||||
Expiry Date
|
<Checkbox isChecked={isExpiryDateEnabled} onChange={setIsExpiryDateEnabled} disabled={!!user.subscriptionExpirationDate}>
|
||||||
</label>
|
|
||||||
<Checkbox
|
|
||||||
isChecked={isExpiryDateEnabled}
|
|
||||||
onChange={setIsExpiryDateEnabled}
|
|
||||||
disabled={!!user.subscriptionExpirationDate}
|
|
||||||
>
|
|
||||||
Enabled
|
Enabled
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,13 +125,11 @@ export default function CodeGenerator({ user }: { user: User }) {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
|
"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",
|
"hover:border-mti-purple tooltip",
|
||||||
"transition duration-300 ease-in-out"
|
"transition duration-300 ease-in-out",
|
||||||
)}
|
)}
|
||||||
filterDate={(date) =>
|
filterDate={(date) =>
|
||||||
moment(date).isAfter(new Date()) &&
|
moment(date).isAfter(new Date()) &&
|
||||||
(user.subscriptionExpirationDate
|
(user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true)
|
||||||
? moment(date).isBefore(user.subscriptionExpirationDate)
|
|
||||||
: true)
|
|
||||||
}
|
}
|
||||||
dateFormat="dd/MM/yyyy"
|
dateFormat="dd/MM/yyyy"
|
||||||
selected={expiryDate}
|
selected={expiryDate}
|
||||||
@@ -164,35 +138,25 @@ export default function CodeGenerator({ user }: { user: User }) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], 'createCodes') && (
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "createCodes") && (
|
||||||
<Button
|
<Button onClick={() => generateCode(type)} disabled={isExpiryDateEnabled ? !expiryDate : false}>
|
||||||
onClick={() => generateCode(type)}
|
|
||||||
disabled={isExpiryDateEnabled ? !expiryDate : false}
|
|
||||||
>
|
|
||||||
Generate
|
Generate
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Generated Code:</label>
|
||||||
Generated Code:
|
|
||||||
</label>
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"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",
|
"hover:border-mti-purple tooltip",
|
||||||
"transition duration-300 ease-in-out"
|
"transition duration-300 ease-in-out",
|
||||||
)}
|
)}
|
||||||
data-tip="Click to copy"
|
data-tip="Click to copy"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (generatedCode) navigator.clipboard.writeText(generatedCode);
|
if (generatedCode) navigator.clipboard.writeText(generatedCode);
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{generatedCode}
|
{generatedCode}
|
||||||
</div>
|
</div>
|
||||||
{generatedCode && (
|
{generatedCode && <span className="text-sm text-mti-gray-dim font-light">Give this code to the user to complete their registration</span>}
|
||||||
<span className="text-sm text-mti-gray-dim font-light">
|
|
||||||
Give this code to the user to complete their registration
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ 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";
|
||||||
@@ -20,6 +15,7 @@ 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>();
|
||||||
|
|
||||||
@@ -32,9 +28,7 @@ const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(creatorUser?.type === "corporate"
|
{(creatorUser?.type === "corporate" ? creatorUser?.corporateInformation?.companyInformation?.name : creatorUser?.name || "N/A") || "N/A"}{" "}
|
||||||
? creatorUser?.corporateInformation?.companyInformation?.name
|
|
||||||
: creatorUser?.name || "N/A") || "N/A"}{" "}
|
|
||||||
{creatorUser && `(${USER_TYPE_LABELS[creatorUser.type]})`}
|
{creatorUser && `(${USER_TYPE_LABELS[creatorUser.type]})`}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -43,19 +37,15 @@ const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
|
|||||||
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<
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
"in-use" | "unused"
|
|
||||||
>();
|
|
||||||
|
|
||||||
// const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
|
// const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
|
||||||
|
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const { codes, reload } = useCodes(
|
const {codes, reload} = useCodes(user?.type === "corporate" ? user?.id : undefined);
|
||||||
user?.type === "corporate" ? user?.id : undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
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());
|
||||||
@@ -80,25 +70,17 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
}, [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));
|
||||||
@@ -126,8 +108,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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}`)
|
||||||
@@ -148,11 +129,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
.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", {
|
||||||
@@ -161,21 +138,15 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
<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>
|
</Checkbox>
|
||||||
),
|
),
|
||||||
cell: (info) =>
|
cell: (info) =>
|
||||||
!info.row.original.userId ? (
|
!info.row.original.userId ? (
|
||||||
<Checkbox
|
<Checkbox isChecked={selectedCodes.includes(info.getValue())} onChange={() => toggleCode(info.getValue())}>
|
||||||
isChecked={selectedCodes.includes(info.getValue())}
|
|
||||||
onChange={() => toggleCode(info.getValue())}
|
|
||||||
>
|
|
||||||
{""}
|
{""}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
) : null,
|
) : null,
|
||||||
@@ -186,8 +157,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("creationDate", {
|
columnHelper.accessor("creationDate", {
|
||||||
header: "Creation Date",
|
header: "Creation Date",
|
||||||
cell: (info) =>
|
cell: (info) => (info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A"),
|
||||||
info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A",
|
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
columnHelper.accessor("email", {
|
||||||
header: "Invited E-mail",
|
header: "Invited E-mail",
|
||||||
@@ -217,11 +187,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{allowedToDelete && !row.original.userId && (
|
{allowedToDelete && !row.original.userId && (
|
||||||
<div
|
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteCode(row.original)}>
|
||||||
data-tip="Delete"
|
|
||||||
className="cursor-pointer tooltip"
|
|
||||||
onClick={() => deleteCode(row.original)}
|
|
||||||
>
|
|
||||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -251,8 +217,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
? {
|
? {
|
||||||
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,
|
||||||
@@ -260,23 +225,15 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
options={users
|
options={users
|
||||||
.filter((x) =>
|
.filter((x) => ["admin", "developer", "corporate"].includes(x.type))
|
||||||
["admin", "developer", "corporate"].includes(x.type)
|
|
||||||
)
|
|
||||||
.map((x) => ({
|
.map((x) => ({
|
||||||
label: `${
|
label: `${x.type === "corporate" ? x.corporateInformation?.companyInformation?.name || x.name : x.name} (${
|
||||||
x.type === "corporate"
|
USER_TYPE_LABELS[x.type]
|
||||||
? x.corporateInformation?.companyInformation?.name || x.name
|
})`,
|
||||||
: x.name
|
|
||||||
} (${USER_TYPE_LABELS[x.type]})`,
|
|
||||||
value: x.id,
|
value: x.id,
|
||||||
user: x,
|
user: x,
|
||||||
}))}
|
}))}
|
||||||
onChange={(value) =>
|
onChange={(value) => setFilteredCorporate(value ? users.find((x) => x.id === value?.value) : undefined)}
|
||||||
setFilteredCorporate(
|
|
||||||
value ? users.find((x) => x.id === value?.value) : undefined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
className="!w-96 !py-1"
|
className="!w-96 !py-1"
|
||||||
@@ -286,11 +243,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
{label: "In Use", value: "in-use"},
|
{label: "In Use", value: "in-use"},
|
||||||
{label: "Unused", value: "unused"},
|
{label: "Unused", value: "unused"},
|
||||||
]}
|
]}
|
||||||
onChange={(value) =>
|
onChange={(value) => setFilterAvailability(value ? (value.value as typeof filterAvailability) : undefined)}
|
||||||
setFilterAvailability(
|
|
||||||
value ? (value.value as typeof filterAvailability) : undefined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
dateFormat="dd/MM/yyyy"
|
dateFormat="dd/MM/yyyy"
|
||||||
@@ -300,9 +253,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
selectsRange
|
selectsRange
|
||||||
showMonthDropdown
|
showMonthDropdown
|
||||||
filterDate={(date: Date) =>
|
filterDate={(date: Date) => moment(date).isSameOrBefore(moment(new Date()))}
|
||||||
moment(date).isSameOrBefore(moment(new Date()))
|
|
||||||
}
|
|
||||||
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
||||||
setStartDate(initialDate ?? moment("01/01/2023").toDate());
|
setStartDate(initialDate ?? moment("01/01/2023").toDate());
|
||||||
if (finalDate) {
|
if (finalDate) {
|
||||||
@@ -323,8 +274,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
color="red"
|
color="red"
|
||||||
className="!py-1 px-10"
|
className="!py-1 px-10"
|
||||||
onClick={() => deleteCodes(selectedCodes)}
|
onClick={() => deleteCodes(selectedCodes)}>
|
||||||
>
|
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,12 +286,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th className="p-4 text-left" key={header.id}>
|
<th className="p-4 text-left" key={header.id}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -349,10 +294,7 @@ export default function CodeList({ user }: { user: User }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="px-2">
|
<tbody className="px-2">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<tr
|
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||||
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
|
||||||
key={row.id}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td className="px-4 py-2" key={cell.id}>
|
<td className="px-4 py-2" key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
|||||||
@@ -4,12 +4,7 @@ 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";
|
||||||
@@ -21,21 +16,12 @@ 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,
|
|
||||||
users,
|
|
||||||
groups,
|
|
||||||
}: {
|
|
||||||
userId: string;
|
|
||||||
users: User[];
|
|
||||||
groups: Group[];
|
|
||||||
}) => {
|
|
||||||
const [companyName, setCompanyName] = useState("");
|
const [companyName, setCompanyName] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@@ -43,33 +29,19 @@ const LinkedCorporate = ({
|
|||||||
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 {
|
||||||
@@ -80,13 +52,9 @@ interface CreateDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 [admin, setAdmin] = useState<string>(group?.admin || user.id);
|
||||||
const [participants, setParticipants] = useState<string[]>(
|
const [participants, setParticipants] = useState<string[]>(group?.participants || []);
|
||||||
group?.participants || []
|
|
||||||
);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const {openFilePicker, filesContent, clear} = useFilePicker({
|
const {openFilePicker, filesContent, clear} = useFilePicker({
|
||||||
@@ -105,12 +73,9 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
|
|||||||
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()
|
|
||||||
: undefined;
|
|
||||||
})
|
})
|
||||||
.filter((x) => !!x)
|
.filter((x) => !!x),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (emails.length === 0) {
|
if (emails.length === 0) {
|
||||||
@@ -120,17 +85,12 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
|
|||||||
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))
|
|
||||||
.filter((x) => x !== undefined);
|
|
||||||
const filteredUsers = emailUsers.filter(
|
const filteredUsers = emailUsers.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
((user.type === "developer" ||
|
((user.type === "developer" || user.type === "admin" || user.type === "corporate" || user.type === "mastercorporate") &&
|
||||||
user.type === "admin" ||
|
|
||||||
user.type === "corporate" ||
|
|
||||||
user.type === "mastercorporate") &&
|
|
||||||
(x?.type === "student" || x?.type === "teacher")) ||
|
(x?.type === "student" || x?.type === "teacher")) ||
|
||||||
(user.type === "teacher" && x?.type === "student")
|
(user.type === "teacher" && x?.type === "student"),
|
||||||
);
|
);
|
||||||
|
|
||||||
setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id));
|
setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id));
|
||||||
@@ -138,7 +98,7 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
|
|||||||
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);
|
||||||
});
|
});
|
||||||
@@ -150,21 +110,14 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
|
|||||||
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);
|
setIsLoading(false);
|
||||||
return;
|
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",
|
|
||||||
{ name, admin, participants }
|
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(
|
toast.success(`Group "${name}" ${group ? "edited" : "created"} successfully`);
|
||||||
`Group "${name}" ${group ? "edited" : "created"} successfully`
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -180,24 +133,11 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
|
|||||||
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"
|
|
||||||
type="text"
|
|
||||||
label="Name"
|
|
||||||
defaultValue={name}
|
|
||||||
onChange={setName}
|
|
||||||
required
|
|
||||||
disabled={group?.disableEditing}
|
|
||||||
/>
|
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">
|
<label className="text-mti-gray-dim text-base font-normal">Participants</label>
|
||||||
Participants
|
<div className="tooltip" data-tip="The Excel file should only include a column with the desired e-mails.">
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className="tooltip"
|
|
||||||
data-tip="The Excel file should only include a column with the desired e-mails."
|
|
||||||
>
|
|
||||||
<BsQuestionCircleFill />
|
<BsQuestionCircleFill />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,22 +146,20 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
value={participants.map((x) => ({
|
value={participants.map((x) => ({
|
||||||
value: x,
|
value: x,
|
||||||
label: `${users.find((y) => y.id === x)?.email} - ${
|
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
|
||||||
users.find((y) => y.id === x)?.name
|
|
||||||
}`,
|
|
||||||
}))}
|
}))}
|
||||||
placeholder="Participants..."
|
placeholder="Participants..."
|
||||||
defaultValue={participants.map((x) => ({
|
defaultValue={participants.map((x) => ({
|
||||||
value: x,
|
value: x,
|
||||||
label: `${users.find((y) => y.id === x)?.email} - ${
|
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
|
||||||
users.find((y) => y.id === x)?.name
|
|
||||||
}`,
|
|
||||||
}))}
|
}))}
|
||||||
options={users
|
options={users
|
||||||
.filter((x) =>
|
.filter((x) =>
|
||||||
user.type === "teacher"
|
user.type === "teacher"
|
||||||
? x.type === "student"
|
? x.type === "student"
|
||||||
: x.type === "student" || x.type === "teacher"
|
: user.type === "corporate"
|
||||||
|
? x.type === "student" || x.type === "teacher"
|
||||||
|
: x.type === "student" || x.type === "teacher" || x.type === "corporate",
|
||||||
)
|
)
|
||||||
.map((x) => ({value: x.id, label: `${x.email} - ${x.name}`}))}
|
.map((x) => ({value: x.id, label: `${x.email} - ${x.name}`}))}
|
||||||
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
||||||
@@ -240,36 +178,18 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{user.type !== "teacher" && (
|
{user.type !== "teacher" && (
|
||||||
<Button
|
<Button className="w-full max-w-[300px]" onClick={openFilePicker} isLoading={isLoading} variant="outline">
|
||||||
className="w-full max-w-[300px]"
|
{filesContent.length === 0 ? "Upload participants Excel file" : filesContent[0].name}
|
||||||
onClick={openFilePicker}
|
|
||||||
isLoading={isLoading}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{filesContent.length === 0
|
|
||||||
? "Upload participants Excel file"
|
|
||||||
: filesContent[0].name}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 flex w-full items-center justify-end gap-8">
|
<div className="mt-8 flex w-full items-center justify-end gap-8">
|
||||||
<Button
|
<Button variant="outline" color="red" className="w-full max-w-[200px]" isLoading={isLoading} onClick={onClose}>
|
||||||
variant="outline"
|
|
||||||
color="red"
|
|
||||||
className="w-full max-w-[200px]"
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button className="w-full max-w-[200px]" onClick={submit} isLoading={isLoading} disabled={!name}>
|
||||||
className="w-full max-w-[200px]"
|
|
||||||
onClick={submit}
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={!name}
|
|
||||||
>
|
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,17 +204,17 @@ export default function GroupList({ user }: { user: User }) {
|
|||||||
const [editingGroup, setEditingGroup] = useState<Group>();
|
const [editingGroup, setEditingGroup] = useState<Group>();
|
||||||
const [filterByUser, setFilterByUser] = useState(false);
|
const [filterByUser, setFilterByUser] = useState(false);
|
||||||
|
|
||||||
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
|
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const { groups, reload } = useGroups(
|
const {groups, reload} = useGroups({
|
||||||
user && filterTypes.includes(user?.type) ? user.id : undefined,
|
admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
|
||||||
user?.type
|
userType: user?.type,
|
||||||
);
|
adminAdmins: user?.type === "teacher" ? user?.id : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) {
|
||||||
user &&
|
|
||||||
["corporate", "teacher", "mastercorporate"].includes(user.type)
|
|
||||||
) {
|
|
||||||
setFilterByUser(true);
|
setFilterByUser(true);
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
@@ -321,25 +241,14 @@ export default function GroupList({ user }: { user: User }) {
|
|||||||
columnHelper.accessor("admin", {
|
columnHelper.accessor("admin", {
|
||||||
header: "Admin",
|
header: "Admin",
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div
|
<div className="tooltip" data-tip={capitalize(users.find((x) => x.id === info.getValue())?.type)}>
|
||||||
className="tooltip"
|
|
||||||
data-tip={capitalize(
|
|
||||||
users.find((x) => x.id === info.getValue())?.type
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{users.find((x) => x.id === info.getValue())?.name}
|
{users.find((x) => x.id === info.getValue())?.name}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("admin", {
|
columnHelper.accessor("admin", {
|
||||||
header: "Linked Corporate",
|
header: "Linked Corporate",
|
||||||
cell: (info) => (
|
cell: (info) => <LinkedCorporate userId={info.getValue()} users={users} groups={groups} />,
|
||||||
<LinkedCorporate
|
|
||||||
userId={info.getValue()}
|
|
||||||
users={users}
|
|
||||||
groups={groups}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("participants", {
|
columnHelper.accessor("participants", {
|
||||||
header: "Participants",
|
header: "Participants",
|
||||||
@@ -355,29 +264,15 @@ export default function GroupList({ user }: { user: User }) {
|
|||||||
cell: ({row}: {row: {original: Group}}) => {
|
cell: ({row}: {row: {original: Group}}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{user &&
|
{user && (checkAccess(user, ["developer", "admin"]) || user.id === row.original.admin) && (
|
||||||
(checkAccess(user, ["developer", "admin"]) ||
|
|
||||||
user.id === row.original.admin) && (
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{(!row.original.disableEditing ||
|
{(!row.original.disableEditing || checkAccess(user, ["developer", "admin"]), "editGroup") && (
|
||||||
checkAccess(user, ["developer", "admin"]),
|
<div data-tip="Edit" className="tooltip cursor-pointer" onClick={() => setEditingGroup(row.original)}>
|
||||||
"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" />
|
<BsPencil className="hover:text-mti-purple-light transition duration-300 ease-in-out" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(!row.original.disableEditing ||
|
{(!row.original.disableEditing || checkAccess(user, ["developer", "admin"]), "deleteGroup") && (
|
||||||
checkAccess(user, ["developer", "admin"]),
|
<div data-tip="Delete" className="tooltip cursor-pointer" onClick={() => deleteGroup(row.original)}>
|
||||||
"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" />
|
<BsTrash className="hover:text-mti-purple-light transition duration-300 ease-in-out" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -403,11 +298,7 @@ export default function GroupList({ user }: { user: User }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full rounded-xl">
|
<div className="h-full w-full rounded-xl">
|
||||||
<Modal
|
<Modal isOpen={isCreating || !!editingGroup} onClose={closeModal} title={editingGroup ? `Editing ${editingGroup.name}` : "New Group"}>
|
||||||
isOpen={isCreating || !!editingGroup}
|
|
||||||
onClose={closeModal}
|
|
||||||
title={editingGroup ? `Editing ${editingGroup.name}` : "New Group"}
|
|
||||||
>
|
|
||||||
<CreatePanel
|
<CreatePanel
|
||||||
group={editingGroup}
|
group={editingGroup}
|
||||||
user={user}
|
user={user}
|
||||||
@@ -419,8 +310,7 @@ export default function GroupList({ user }: { user: User }) {
|
|||||||
groups
|
groups
|
||||||
.filter((g) => g.admin === user.id)
|
.filter((g) => g.admin === user.id)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(u.id) ||
|
.includes(u.id) || groups.flatMap((g) => g.participants).includes(u.id),
|
||||||
groups.flatMap((g) => g.participants).includes(u.id)
|
|
||||||
)
|
)
|
||||||
: users
|
: users
|
||||||
}
|
}
|
||||||
@@ -432,12 +322,7 @@ export default function GroupList({ user }: { user: User }) {
|
|||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th className="py-4" key={header.id}>
|
<th className="py-4" key={header.id}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -445,10 +330,7 @@ export default function GroupList({ user }: { user: User }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="px-2">
|
<tbody className="px-2">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<tr
|
<tr className="even:bg-mti-purple-ultralight/40 rounded-lg py-2 odd:bg-white" key={row.id}>
|
||||||
className="even:bg-mti-purple-ultralight/40 rounded-lg py-2 odd:bg-white"
|
|
||||||
key={row.id}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td className="px-4 py-2" key={cell.id}>
|
<td className="px-4 py-2" key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
@@ -459,15 +341,10 @@ export default function GroupList({ user }: { user: User }) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{checkAccess(
|
{checkAccess(user, ["teacher", "corporate", "mastercorporate", "admin", "developer"], permissions, "createGroup") && (
|
||||||
user,
|
|
||||||
["teacher", "corporate", "mastercorporate", "admin", "developer"],
|
|
||||||
"createGroup"
|
|
||||||
) && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCreating(true)}
|
onClick={() => setIsCreating(true)}
|
||||||
className="bg-mti-purple-light hover:bg-mti-purple w-full py-2 text-white transition duration-300 ease-in-out"
|
className="bg-mti-purple-light hover:bg-mti-purple w-full py-2 text-white transition duration-300 ease-in-out">
|
||||||
>
|
|
||||||
New Group
|
New Group
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,38 +4,19 @@ import useGroups from "@/hooks/useGroups";
|
|||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {Type, User, userTypes, CorporateUser, Group} from "@/interfaces/user";
|
import {Type, User, userTypes, CorporateUser, Group} from "@/interfaces/user";
|
||||||
import {Popover, Transition} from "@headlessui/react";
|
import {Popover, Transition} from "@headlessui/react";
|
||||||
import {
|
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||||
createColumnHelper,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {capitalize, reverse} from "lodash";
|
import {capitalize, reverse} from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import {
|
import {BsArrowDown, BsArrowDownUp, BsArrowUp, BsCheck, BsCheckCircle, BsEye, BsFillExclamationOctagonFill, BsPerson, BsTrash} from "react-icons/bs";
|
||||||
BsArrowDown,
|
|
||||||
BsArrowDownUp,
|
|
||||||
BsArrowUp,
|
|
||||||
BsCheck,
|
|
||||||
BsCheckCircle,
|
|
||||||
BsEye,
|
|
||||||
BsFillExclamationOctagonFill,
|
|
||||||
BsPerson,
|
|
||||||
BsTrash,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {countries, TCountries} from "countries-list";
|
import {countries, TCountries} from "countries-list";
|
||||||
import countryCodes from "country-codes-list";
|
import countryCodes from "country-codes-list";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import {
|
import {getUserCompanyName, isAgentUser, USER_TYPE_LABELS} from "@/resources/user";
|
||||||
getUserCompanyName,
|
|
||||||
isAgentUser,
|
|
||||||
USER_TYPE_LABELS,
|
|
||||||
} from "@/resources/user";
|
|
||||||
import useFilterStore from "@/stores/listFilterStore";
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {isCorporateUser} from "@/resources/user";
|
import {isCorporateUser} from "@/resources/user";
|
||||||
@@ -45,22 +26,11 @@ import { asyncSorter } from "@/utils";
|
|||||||
import {exportListToExcel, UserListRow} from "@/utils/users";
|
import {exportListToExcel, UserListRow} from "@/utils/users";
|
||||||
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 columnHelper = createColumnHelper<User>();
|
const columnHelper = createColumnHelper<User>();
|
||||||
const searchFields = [
|
const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]];
|
||||||
["name"],
|
|
||||||
["email"],
|
|
||||||
["corporateInformation", "companyInformation", "name"],
|
|
||||||
];
|
|
||||||
|
|
||||||
const CompanyNameCell = ({
|
const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; groups: Group[]}) => {
|
||||||
users,
|
|
||||||
user,
|
|
||||||
groups,
|
|
||||||
}: {
|
|
||||||
user: User;
|
|
||||||
users: User[];
|
|
||||||
groups: Group[];
|
|
||||||
}) => {
|
|
||||||
const [companyName, setCompanyName] = useState("");
|
const [companyName, setCompanyName] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@@ -69,11 +39,7 @@ const CompanyNameCell = ({
|
|||||||
setCompanyName(name);
|
setCompanyName(name);
|
||||||
}, [user, users, groups]);
|
}, [user, users, groups]);
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>;
|
||||||
<span className="animate-pulse">Loading...</span>
|
|
||||||
) : (
|
|
||||||
<>{companyName}</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UserList({
|
export default function UserList({
|
||||||
@@ -85,18 +51,14 @@ export default function UserList({
|
|||||||
filters?: ((user: User) => boolean)[];
|
filters?: ((user: User) => boolean)[];
|
||||||
renderHeader?: (total: number) => JSX.Element;
|
renderHeader?: (total: number) => JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
const [showDemographicInformation, setShowDemographicInformation] =
|
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
|
||||||
useState(false);
|
|
||||||
const [sorter, setSorter] = useState<string>();
|
const [sorter, setSorter] = useState<string>();
|
||||||
const [displayUsers, setDisplayUsers] = useState<User[]>([]);
|
const [displayUsers, setDisplayUsers] = useState<User[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
|
|
||||||
const {users, reload} = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const { groups } = useGroups(
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
user && ["corporate", "teacher", "mastercorporate"].includes(user?.type)
|
const {groups} = useGroups({admin: user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined});
|
||||||
? user.id
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -105,13 +67,10 @@ export default function UserList({
|
|||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new Date());
|
const today = moment(new Date());
|
||||||
|
|
||||||
if (today.isAfter(momentDate))
|
if (today.isAfter(momentDate)) return "!text-mti-red-light font-bold line-through";
|
||||||
return "!text-mti-red-light font-bold line-through";
|
|
||||||
if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light";
|
if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light";
|
||||||
if (today.add(2, "weeks").isAfter(momentDate))
|
if (today.add(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-light";
|
||||||
return "!text-mti-rose-light";
|
if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light";
|
||||||
if (today.add(1, "months").isAfter(momentDate))
|
|
||||||
return "!text-mti-orange-light";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -119,19 +78,11 @@ export default function UserList({
|
|||||||
if (user && users) {
|
if (user && users) {
|
||||||
const filterUsers =
|
const filterUsers =
|
||||||
user.type === "corporate" || user.type === "teacher"
|
user.type === "corporate" || user.type === "teacher"
|
||||||
? users.filter((u) =>
|
? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id))
|
||||||
groups.flatMap((g) => g.participants).includes(u.id)
|
|
||||||
)
|
|
||||||
: users;
|
: users;
|
||||||
|
|
||||||
const filteredUsers = filters.reduce(
|
const filteredUsers = filters.reduce((d, f) => d.filter(f), filterUsers);
|
||||||
(d, f) => d.filter(f),
|
const sortedUsers = await asyncSorter<User>(filteredUsers, sortFunction);
|
||||||
filterUsers
|
|
||||||
);
|
|
||||||
const sortedUsers = await asyncSorter<User>(
|
|
||||||
filteredUsers,
|
|
||||||
sortFunction
|
|
||||||
);
|
|
||||||
|
|
||||||
setDisplayUsers([...sortedUsers]);
|
setDisplayUsers([...sortedUsers]);
|
||||||
}
|
}
|
||||||
@@ -140,8 +91,7 @@ export default function UserList({
|
|||||||
}, [user, users, sorter, groups]);
|
}, [user, users, sorter, groups]);
|
||||||
|
|
||||||
const deleteAccount = (user: User) => {
|
const deleteAccount = (user: User) => {
|
||||||
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`))
|
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
|
||||||
return;
|
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete<{ok: boolean}>(`/api/user?id=${user.id}`)
|
.delete<{ok: boolean}>(`/api/user?id=${user.id}`)
|
||||||
@@ -156,14 +106,7 @@ export default function UserList({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateAccountType = (user: User, type: Type) => {
|
const updateAccountType = (user: User, type: Type) => {
|
||||||
if (
|
if (!confirm(`Are you sure you want to update ${user.name}'s account from ${capitalize(user.type)} to ${capitalize(type)}?`)) return;
|
||||||
!confirm(
|
|
||||||
`Are you sure you want to update ${
|
|
||||||
user.name
|
|
||||||
}'s account from ${capitalize(user.type)} to ${capitalize(type)}?`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
|
||||||
@@ -197,11 +140,9 @@ export default function UserList({
|
|||||||
const toggleDisableAccount = (user: User) => {
|
const toggleDisableAccount = (user: User) => {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
`Are you sure you want to ${
|
`Are you sure you want to ${user.status === "disabled" ? "enable" : "disable"} ${
|
||||||
user.status === "disabled" ? "enable" : "disable"
|
|
||||||
} ${
|
|
||||||
user.name
|
user.name
|
||||||
}'s account? This change is usually related to their payment state.`
|
}'s account? This change is usually related to their payment state.`,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
@@ -212,11 +153,7 @@ export default function UserList({
|
|||||||
status: user.status === "disabled" ? "active" : "disabled",
|
status: user.status === "disabled" ? "active" : "disabled",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(
|
toast.success(`User ${user.status === "disabled" ? "enabled" : "disabled"} successfully!`);
|
||||||
`User ${
|
|
||||||
user.status === "disabled" ? "enabled" : "disabled"
|
|
||||||
} successfully!`
|
|
||||||
);
|
|
||||||
reload();
|
reload();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -242,11 +179,7 @@ export default function UserList({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{checkAccess(
|
{checkAccess(user, updateUserPermission.list, permissions, updateUserPermission.perm) && (
|
||||||
user,
|
|
||||||
updateUserPermission.list,
|
|
||||||
updateUserPermission.perm
|
|
||||||
) && (
|
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
<Popover.Button>
|
<Popover.Button>
|
||||||
<div data-tip="Change Type" className="cursor-pointer tooltip">
|
<div data-tip="Change Type" className="cursor-pointer tooltip">
|
||||||
@@ -260,48 +193,31 @@ export default function UserList({
|
|||||||
enterTo="opacity-100 translate-y-0"
|
enterTo="opacity-100 translate-y-0"
|
||||||
leave="transition ease-in duration-150"
|
leave="transition ease-in duration-150"
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1">
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute z-10 w-screen right-1/2 translate-x-1/3 max-w-sm">
|
<Popover.Panel className="absolute z-10 w-screen right-1/2 translate-x-1/3 max-w-sm">
|
||||||
<div className="bg-white p-4 rounded-lg grid grid-cols-2 gap-2 w-full drop-shadow-xl">
|
<div className="bg-white p-4 rounded-lg grid grid-cols-2 gap-2 w-full drop-shadow-xl">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "student")}
|
onClick={() => updateAccountType(row.original, "student")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={
|
disabled={row.original.type === "student" || !PERMISSIONS.generateCode["student"].includes(user.type)}>
|
||||||
row.original.type === "student" ||
|
|
||||||
!PERMISSIONS.generateCode["student"].includes(user.type)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Student
|
Student
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "teacher")}
|
onClick={() => updateAccountType(row.original, "teacher")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={
|
disabled={row.original.type === "teacher" || !PERMISSIONS.generateCode["teacher"].includes(user.type)}>
|
||||||
row.original.type === "teacher" ||
|
|
||||||
!PERMISSIONS.generateCode["teacher"].includes(user.type)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Teacher
|
Teacher
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "corporate")}
|
onClick={() => updateAccountType(row.original, "corporate")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={
|
disabled={row.original.type === "corporate" || !PERMISSIONS.generateCode["corporate"].includes(user.type)}>
|
||||||
row.original.type === "corporate" ||
|
|
||||||
!PERMISSIONS.generateCode["corporate"].includes(user.type)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Corporate
|
Corporate
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "admin")}
|
onClick={() => updateAccountType(row.original, "admin")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={
|
disabled={row.original.type === "admin" || !PERMISSIONS.generateCode["admin"].includes(user.type)}>
|
||||||
row.original.type === "admin" ||
|
|
||||||
!PERMISSIONS.generateCode["admin"].includes(user.type)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Admin
|
Admin
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -309,34 +225,16 @@ export default function UserList({
|
|||||||
</Transition>
|
</Transition>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
{!row.original.isVerified &&
|
{!row.original.isVerified && checkAccess(user, updateUserPermission.list, permissions, updateUserPermission.perm) && (
|
||||||
checkAccess(
|
<div data-tip="Verify User" className="cursor-pointer tooltip" onClick={() => verifyAccount(row.original)}>
|
||||||
user,
|
|
||||||
updateUserPermission.list,
|
|
||||||
updateUserPermission.perm
|
|
||||||
) && (
|
|
||||||
<div
|
|
||||||
data-tip="Verify User"
|
|
||||||
className="cursor-pointer tooltip"
|
|
||||||
onClick={() => verifyAccount(row.original)}
|
|
||||||
>
|
|
||||||
<BsCheck className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsCheck className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{checkAccess(
|
{checkAccess(user, updateUserPermission.list, permissions, updateUserPermission.perm) && (
|
||||||
user,
|
|
||||||
updateUserPermission.list,
|
|
||||||
updateUserPermission.perm
|
|
||||||
) && (
|
|
||||||
<div
|
<div
|
||||||
data-tip={
|
data-tip={row.original.status === "disabled" ? "Enable User" : "Disable User"}
|
||||||
row.original.status === "disabled"
|
|
||||||
? "Enable User"
|
|
||||||
: "Disable User"
|
|
||||||
}
|
|
||||||
className="cursor-pointer tooltip"
|
className="cursor-pointer tooltip"
|
||||||
onClick={() => toggleDisableAccount(row.original)}
|
onClick={() => toggleDisableAccount(row.original)}>
|
||||||
>
|
|
||||||
{row.original.status === "disabled" ? (
|
{row.original.status === "disabled" ? (
|
||||||
<BsCheckCircle className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsCheckCircle className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
) : (
|
) : (
|
||||||
@@ -344,16 +242,8 @@ export default function UserList({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{checkAccess(
|
{checkAccess(user, deleteUserPermission.list, permissions, deleteUserPermission.perm) && (
|
||||||
user,
|
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteAccount(row.original)}>
|
||||||
deleteUserPermission.list,
|
|
||||||
deleteUserPermission.perm
|
|
||||||
) && (
|
|
||||||
<div
|
|
||||||
data-tip="Delete"
|
|
||||||
className="cursor-pointer tooltip"
|
|
||||||
onClick={() => deleteAccount(row.original)}
|
|
||||||
>
|
|
||||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -364,10 +254,7 @@ export default function UserList({
|
|||||||
const demographicColumns = [
|
const demographicColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
|
||||||
className="flex gap-2 items-center"
|
|
||||||
onClick={() => setSorter((prev) => selectSorter(prev, "name"))}
|
|
||||||
>
|
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
<SorterArrow name="name" />
|
<SorterArrow name="name" />
|
||||||
</button>
|
</button>
|
||||||
@@ -375,100 +262,77 @@ export default function UserList({
|
|||||||
cell: ({row, getValue}) => (
|
cell: ({row, getValue}) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(
|
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) &&
|
||||||
user.type
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
||||||
) &&
|
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type)
|
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) ? setSelectedUser(row.original) : null
|
||||||
? setSelectedUser(row.original)
|
}>
|
||||||
: null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{getValue()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.country", {
|
columnHelper.accessor("demographicInformation.country", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "country"))}>
|
||||||
className="flex gap-2 items-center"
|
|
||||||
onClick={() => setSorter((prev) => selectSorter(prev, "country"))}
|
|
||||||
>
|
|
||||||
<span>Country</span>
|
<span>Country</span>
|
||||||
<SorterArrow name="country" />
|
<SorterArrow name="country" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) =>
|
cell: (info) =>
|
||||||
info.getValue()
|
info.getValue()
|
||||||
? `${
|
? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${
|
||||||
countryCodes.findOne("countryCode" as any, info.getValue()).flag
|
countries[info.getValue() as unknown as keyof TCountries]?.name
|
||||||
} ${
|
} (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})`
|
||||||
countries[info.getValue() as unknown as keyof TCountries].name
|
: "N/A",
|
||||||
} (+${
|
|
||||||
countryCodes.findOne("countryCode" as any, info.getValue())
|
|
||||||
.countryCallingCode
|
|
||||||
})`
|
|
||||||
: "Not available",
|
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.phone", {
|
columnHelper.accessor("demographicInformation.phone", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "phone"))}>
|
||||||
className="flex gap-2 items-center"
|
|
||||||
onClick={() => setSorter((prev) => selectSorter(prev, "phone"))}
|
|
||||||
>
|
|
||||||
<span>Phone</span>
|
<span>Phone</span>
|
||||||
<SorterArrow name="phone" />
|
<SorterArrow name="phone" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => info.getValue() || "Not available",
|
cell: (info) => info.getValue() || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor(
|
columnHelper.accessor(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === "corporate" || x.type === "mastercorporate"
|
x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment,
|
||||||
? x.demographicInformation?.position
|
|
||||||
: x.demographicInformation?.employment,
|
|
||||||
{
|
{
|
||||||
id: "employment",
|
id: "employment",
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "employment"))}>
|
||||||
className="flex gap-2 items-center"
|
<span>Employment</span>
|
||||||
onClick={() =>
|
|
||||||
setSorter((prev) => selectSorter(prev, "employment"))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>Employment/Position</span>
|
|
||||||
<SorterArrow name="employment" />
|
<SorterArrow name="employment" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) =>
|
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
|
||||||
(info.row.original.type === "corporate"
|
|
||||||
? info.getValue()
|
|
||||||
: capitalize(info.getValue())) || "Not available",
|
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
|
columnHelper.accessor("lastLogin", {
|
||||||
|
header: (
|
||||||
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "lastLogin"))}>
|
||||||
|
<span>Last Login</span>
|
||||||
|
<SorterArrow name="lastLogin" />
|
||||||
|
</button>
|
||||||
|
) as any,
|
||||||
|
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
|
||||||
|
}),
|
||||||
columnHelper.accessor("demographicInformation.gender", {
|
columnHelper.accessor("demographicInformation.gender", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}>
|
||||||
className="flex gap-2 items-center"
|
|
||||||
onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}
|
|
||||||
>
|
|
||||||
<span>Gender</span>
|
<span>Gender</span>
|
||||||
<SorterArrow name="gender" />
|
<SorterArrow name="gender" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => capitalize(info.getValue()) || "Not available",
|
cell: (info) => capitalize(info.getValue()) || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
header: (
|
header: (
|
||||||
<span
|
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => setShowDemographicInformation((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Switch
|
Switch
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -480,10 +344,7 @@ export default function UserList({
|
|||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
|
||||||
className="flex gap-2 items-center"
|
|
||||||
onClick={() => setSorter((prev) => selectSorter(prev, "name"))}
|
|
||||||
>
|
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
<SorterArrow name="name" />
|
<SorterArrow name="name" />
|
||||||
</button>
|
</button>
|
||||||
@@ -491,30 +352,19 @@ export default function UserList({
|
|||||||
cell: ({row, getValue}) => (
|
cell: ({row, getValue}) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(
|
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) &&
|
||||||
user.type
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
||||||
) &&
|
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type)
|
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) ? setSelectedUser(row.original) : null
|
||||||
? setSelectedUser(row.original)
|
}>
|
||||||
: null
|
{getValue()}
|
||||||
}
|
|
||||||
>
|
|
||||||
{row.original.type === "corporate"
|
|
||||||
? row.original.corporateInformation?.companyInformation?.name ||
|
|
||||||
getValue()
|
|
||||||
: getValue()}
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
columnHelper.accessor("email", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "email"))}>
|
||||||
className="flex gap-2 items-center"
|
|
||||||
onClick={() => setSorter((prev) => selectSorter(prev, "email"))}
|
|
||||||
>
|
|
||||||
<span>E-mail</span>
|
<span>E-mail</span>
|
||||||
<SorterArrow name="email" />
|
<SorterArrow name="email" />
|
||||||
</button>
|
</button>
|
||||||
@@ -522,27 +372,17 @@ export default function UserList({
|
|||||||
cell: ({row, getValue}) => (
|
cell: ({row, getValue}) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(
|
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) &&
|
||||||
user.type
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
||||||
) &&
|
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() => (PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}>
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type)
|
|
||||||
? setSelectedUser(row.original)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{getValue()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("type", {
|
columnHelper.accessor("type", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "type"))}>
|
||||||
className="flex gap-2 items-center"
|
|
||||||
onClick={() => setSorter((prev) => selectSorter(prev, "type"))}
|
|
||||||
>
|
|
||||||
<span>Type</span>
|
<span>Type</span>
|
||||||
<SorterArrow name="type" />
|
<SorterArrow name="type" />
|
||||||
</button>
|
</button>
|
||||||
@@ -551,55 +391,30 @@ export default function UserList({
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("corporateInformation.companyInformation.name", {
|
columnHelper.accessor("corporateInformation.companyInformation.name", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}>
|
||||||
className="flex gap-2 items-center"
|
<span>Company</span>
|
||||||
onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}
|
|
||||||
>
|
|
||||||
<span>Company Name</span>
|
|
||||||
<SorterArrow name="companyName" />
|
<SorterArrow name="companyName" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => (
|
cell: (info) => <CompanyNameCell user={info.row.original} users={users} groups={groups} />,
|
||||||
<CompanyNameCell
|
|
||||||
user={info.row.original}
|
|
||||||
users={users}
|
|
||||||
groups={groups}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("subscriptionExpirationDate", {
|
columnHelper.accessor("subscriptionExpirationDate", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
|
||||||
className="flex gap-2 items-center"
|
<span>Expiration</span>
|
||||||
onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}
|
|
||||||
>
|
|
||||||
<span>Expiry Date</span>
|
|
||||||
<SorterArrow name="expiryDate" />
|
<SorterArrow name="expiryDate" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<span
|
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
||||||
className={clsx(
|
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
||||||
info.getValue()
|
|
||||||
? expirationDateColor(moment(info.getValue()).toDate())
|
|
||||||
: ""
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!info.getValue()
|
|
||||||
? "No expiry date"
|
|
||||||
: moment(info.getValue()).format("DD/MM/YYYY")}
|
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("isVerified", {
|
columnHelper.accessor("isVerified", {
|
||||||
header: (
|
header: (
|
||||||
<button
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "verification"))}>
|
||||||
className="flex gap-2 items-center"
|
<span>Verified</span>
|
||||||
onClick={() =>
|
|
||||||
setSorter((prev) => selectSorter(prev, "verification"))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>Verification</span>
|
|
||||||
<SorterArrow name="verification" />
|
<SorterArrow name="verification" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
@@ -609,9 +424,8 @@ export default function UserList({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
info.getValue() && "!bg-mti-purple-light "
|
info.getValue() && "!bg-mti-purple-light ",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
<BsCheck color="white" className="w-full h-full" />
|
<BsCheck color="white" className="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -619,10 +433,7 @@ export default function UserList({
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
header: (
|
header: (
|
||||||
<span
|
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => setShowDemographicInformation((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Switch
|
Switch
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -642,21 +453,15 @@ export default function UserList({
|
|||||||
|
|
||||||
const sortFunction = async (a: User, b: User) => {
|
const sortFunction = async (a: User, b: User) => {
|
||||||
if (sorter === "name" || sorter === reverseString("name"))
|
if (sorter === "name" || sorter === reverseString("name"))
|
||||||
return sorter === "name"
|
return sorter === "name" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||||
? a.name.localeCompare(b.name)
|
|
||||||
: b.name.localeCompare(a.name);
|
|
||||||
|
|
||||||
if (sorter === "email" || sorter === reverseString("email"))
|
if (sorter === "email" || sorter === reverseString("email"))
|
||||||
return sorter === "email"
|
return sorter === "email" ? a.email.localeCompare(b.email) : b.email.localeCompare(a.email);
|
||||||
? a.email.localeCompare(b.email)
|
|
||||||
: b.email.localeCompare(a.email);
|
|
||||||
|
|
||||||
if (sorter === "type" || sorter === reverseString("type"))
|
if (sorter === "type" || sorter === reverseString("type"))
|
||||||
return sorter === "type"
|
return sorter === "type"
|
||||||
? userTypes.findIndex((t) => a.type === t) -
|
? userTypes.findIndex((t) => a.type === t) - userTypes.findIndex((t) => b.type === t)
|
||||||
userTypes.findIndex((t) => b.type === t)
|
: userTypes.findIndex((t) => b.type === t) - userTypes.findIndex((t) => a.type === t);
|
||||||
: userTypes.findIndex((t) => b.type === t) -
|
|
||||||
userTypes.findIndex((t) => a.type === t);
|
|
||||||
|
|
||||||
if (sorter === "verification" || sorter === reverseString("verification"))
|
if (sorter === "verification" || sorter === reverseString("verification"))
|
||||||
return sorter === "verification"
|
return sorter === "verification"
|
||||||
@@ -664,138 +469,84 @@ export default function UserList({
|
|||||||
: b.isVerified.toString().localeCompare(a.isVerified.toString());
|
: b.isVerified.toString().localeCompare(a.isVerified.toString());
|
||||||
|
|
||||||
if (sorter === "expiryDate" || sorter === reverseString("expiryDate")) {
|
if (sorter === "expiryDate" || sorter === reverseString("expiryDate")) {
|
||||||
if (!a.subscriptionExpirationDate && b.subscriptionExpirationDate)
|
if (!a.subscriptionExpirationDate && b.subscriptionExpirationDate) return sorter === "expiryDate" ? -1 : 1;
|
||||||
return sorter === "expiryDate" ? -1 : 1;
|
if (a.subscriptionExpirationDate && !b.subscriptionExpirationDate) return sorter === "expiryDate" ? 1 : -1;
|
||||||
if (a.subscriptionExpirationDate && !b.subscriptionExpirationDate)
|
if (!a.subscriptionExpirationDate && !b.subscriptionExpirationDate) return 0;
|
||||||
return sorter === "expiryDate" ? 1 : -1;
|
if (moment(a.subscriptionExpirationDate).isAfter(b.subscriptionExpirationDate)) return sorter === "expiryDate" ? -1 : 1;
|
||||||
if (!a.subscriptionExpirationDate && !b.subscriptionExpirationDate)
|
if (moment(b.subscriptionExpirationDate).isAfter(a.subscriptionExpirationDate)) return sorter === "expiryDate" ? 1 : -1;
|
||||||
return 0;
|
return 0;
|
||||||
if (
|
}
|
||||||
moment(a.subscriptionExpirationDate).isAfter(
|
|
||||||
b.subscriptionExpirationDate
|
if (sorter === "lastLogin" || sorter === reverseString("lastLogin")) {
|
||||||
)
|
if (!a.lastLogin && b.lastLogin) return sorter === "lastLogin" ? -1 : 1;
|
||||||
)
|
if (a.lastLogin && !b.lastLogin) return sorter === "lastLogin" ? 1 : -1;
|
||||||
return sorter === "expiryDate" ? -1 : 1;
|
if (!a.lastLogin && !b.lastLogin) return 0;
|
||||||
if (
|
if (moment(a.lastLogin).isAfter(b.lastLogin)) return sorter === "lastLogin" ? -1 : 1;
|
||||||
moment(b.subscriptionExpirationDate).isAfter(
|
if (moment(b.lastLogin).isAfter(a.lastLogin)) return sorter === "lastLogin" ? 1 : -1;
|
||||||
a.subscriptionExpirationDate
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return sorter === "expiryDate" ? 1 : -1;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "country" || sorter === reverseString("country")) {
|
if (sorter === "country" || sorter === reverseString("country")) {
|
||||||
if (
|
if (!a.demographicInformation?.country && b.demographicInformation?.country) return sorter === "country" ? -1 : 1;
|
||||||
!a.demographicInformation?.country &&
|
if (a.demographicInformation?.country && !b.demographicInformation?.country) return sorter === "country" ? 1 : -1;
|
||||||
b.demographicInformation?.country
|
if (!a.demographicInformation?.country && !b.demographicInformation?.country) return 0;
|
||||||
)
|
|
||||||
return sorter === "country" ? -1 : 1;
|
|
||||||
if (
|
|
||||||
a.demographicInformation?.country &&
|
|
||||||
!b.demographicInformation?.country
|
|
||||||
)
|
|
||||||
return sorter === "country" ? 1 : -1;
|
|
||||||
if (
|
|
||||||
!a.demographicInformation?.country &&
|
|
||||||
!b.demographicInformation?.country
|
|
||||||
)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return sorter === "country"
|
return sorter === "country"
|
||||||
? a.demographicInformation!.country.localeCompare(
|
? a.demographicInformation!.country.localeCompare(b.demographicInformation!.country)
|
||||||
b.demographicInformation!.country
|
: b.demographicInformation!.country.localeCompare(a.demographicInformation!.country);
|
||||||
)
|
|
||||||
: b.demographicInformation!.country.localeCompare(
|
|
||||||
a.demographicInformation!.country
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "phone" || sorter === reverseString("phone")) {
|
if (sorter === "phone" || sorter === reverseString("phone")) {
|
||||||
if (!a.demographicInformation?.phone && b.demographicInformation?.phone)
|
if (!a.demographicInformation?.phone && b.demographicInformation?.phone) return sorter === "phone" ? -1 : 1;
|
||||||
return sorter === "phone" ? -1 : 1;
|
if (a.demographicInformation?.phone && !b.demographicInformation?.phone) return sorter === "phone" ? 1 : -1;
|
||||||
if (a.demographicInformation?.phone && !b.demographicInformation?.phone)
|
if (!a.demographicInformation?.phone && !b.demographicInformation?.phone) return 0;
|
||||||
return sorter === "phone" ? 1 : -1;
|
|
||||||
if (!a.demographicInformation?.phone && !b.demographicInformation?.phone)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return sorter === "phone"
|
return sorter === "phone"
|
||||||
? a.demographicInformation!.phone.localeCompare(
|
? a.demographicInformation!.phone.localeCompare(b.demographicInformation!.phone)
|
||||||
b.demographicInformation!.phone
|
: b.demographicInformation!.phone.localeCompare(a.demographicInformation!.phone);
|
||||||
)
|
|
||||||
: b.demographicInformation!.phone.localeCompare(
|
|
||||||
a.demographicInformation!.phone
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "employment" || sorter === reverseString("employment")) {
|
if (sorter === "employment" || sorter === reverseString("employment")) {
|
||||||
const aSortingItem =
|
const aSortingItem =
|
||||||
a.type === "corporate" || a.type === "mastercorporate"
|
a.type === "corporate" || a.type === "mastercorporate" ? a.demographicInformation?.position : a.demographicInformation?.employment;
|
||||||
? a.demographicInformation?.position
|
|
||||||
: a.demographicInformation?.employment;
|
|
||||||
const bSortingItem =
|
const bSortingItem =
|
||||||
b.type === "corporate" || b.type === "mastercorporate"
|
b.type === "corporate" || b.type === "mastercorporate" ? b.demographicInformation?.position : b.demographicInformation?.employment;
|
||||||
? b.demographicInformation?.position
|
|
||||||
: b.demographicInformation?.employment;
|
|
||||||
|
|
||||||
if (!aSortingItem && bSortingItem)
|
if (!aSortingItem && bSortingItem) return sorter === "employment" ? -1 : 1;
|
||||||
return sorter === "employment" ? -1 : 1;
|
if (aSortingItem && !bSortingItem) return sorter === "employment" ? 1 : -1;
|
||||||
if (aSortingItem && !bSortingItem)
|
|
||||||
return sorter === "employment" ? 1 : -1;
|
|
||||||
if (!aSortingItem && !bSortingItem) return 0;
|
if (!aSortingItem && !bSortingItem) return 0;
|
||||||
|
|
||||||
return sorter === "employment"
|
return sorter === "employment" ? aSortingItem!.localeCompare(bSortingItem!) : bSortingItem!.localeCompare(aSortingItem!);
|
||||||
? aSortingItem!.localeCompare(bSortingItem!)
|
|
||||||
: bSortingItem!.localeCompare(aSortingItem!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "gender" || sorter === reverseString("gender")) {
|
if (sorter === "gender" || sorter === reverseString("gender")) {
|
||||||
if (!a.demographicInformation?.gender && b.demographicInformation?.gender)
|
if (!a.demographicInformation?.gender && b.demographicInformation?.gender) return sorter === "employment" ? -1 : 1;
|
||||||
return sorter === "employment" ? -1 : 1;
|
if (a.demographicInformation?.gender && !b.demographicInformation?.gender) return sorter === "employment" ? 1 : -1;
|
||||||
if (a.demographicInformation?.gender && !b.demographicInformation?.gender)
|
if (!a.demographicInformation?.gender && !b.demographicInformation?.gender) return 0;
|
||||||
return sorter === "employment" ? 1 : -1;
|
|
||||||
if (
|
|
||||||
!a.demographicInformation?.gender &&
|
|
||||||
!b.demographicInformation?.gender
|
|
||||||
)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return sorter === "gender"
|
return sorter === "gender"
|
||||||
? a.demographicInformation!.gender.localeCompare(
|
? a.demographicInformation!.gender.localeCompare(b.demographicInformation!.gender)
|
||||||
b.demographicInformation!.gender
|
: b.demographicInformation!.gender.localeCompare(a.demographicInformation!.gender);
|
||||||
)
|
|
||||||
: b.demographicInformation!.gender.localeCompare(
|
|
||||||
a.demographicInformation!.gender
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "companyName" || sorter === reverseString("companyName")) {
|
if (sorter === "companyName" || sorter === reverseString("companyName")) {
|
||||||
const aCorporateName = getUserCompanyName(a, users, groups);
|
const aCorporateName = getUserCompanyName(a, users, groups);
|
||||||
const bCorporateName = getUserCompanyName(b, users, groups);
|
const bCorporateName = getUserCompanyName(b, users, groups);
|
||||||
if (!aCorporateName && bCorporateName)
|
if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1;
|
||||||
return sorter === "companyName" ? -1 : 1;
|
if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1;
|
||||||
if (aCorporateName && !bCorporateName)
|
|
||||||
return sorter === "companyName" ? 1 : -1;
|
|
||||||
if (!aCorporateName && !bCorporateName) return 0;
|
if (!aCorporateName && !bCorporateName) return 0;
|
||||||
|
|
||||||
return sorter === "companyName"
|
return sorter === "companyName" ? aCorporateName.localeCompare(bCorporateName) : bCorporateName.localeCompare(aCorporateName);
|
||||||
? aCorporateName.localeCompare(bCorporateName)
|
|
||||||
: bCorporateName.localeCompare(aCorporateName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.id.localeCompare(b.id);
|
return a.id.localeCompare(b.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { rows: filteredRows, renderSearch } = useListSearch<User>(
|
const {rows: filteredRows, renderSearch} = useListSearch<User>(searchFields, displayUsers);
|
||||||
searchFields,
|
|
||||||
displayUsers
|
|
||||||
);
|
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredRows,
|
data: filteredRows,
|
||||||
columns: (!showDemographicInformation
|
columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any,
|
||||||
? defaultColumns
|
|
||||||
: demographicColumns) as any,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -816,19 +567,13 @@ export default function UserList({
|
|||||||
const belongsToAdminFilter = (x: User) => {
|
const belongsToAdminFilter = (x: User) => {
|
||||||
if (!selectedUser) return false;
|
if (!selectedUser) return false;
|
||||||
return groups
|
return groups
|
||||||
.filter(
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
(g) =>
|
|
||||||
g.admin === selectedUser.id ||
|
|
||||||
g.participants.includes(selectedUser.id)
|
|
||||||
)
|
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id);
|
.includes(x.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewStudentFilterBelongsToAdmin = (x: User) =>
|
const viewStudentFilterBelongsToAdmin = (x: User) => x.type === "student" && belongsToAdminFilter(x);
|
||||||
x.type === "student" && belongsToAdminFilter(x);
|
const viewTeacherFilterBelongsToAdmin = (x: User) => x.type === "teacher" && belongsToAdminFilter(x);
|
||||||
const viewTeacherFilterBelongsToAdmin = (x: User) =>
|
|
||||||
x.type === "teacher" && belongsToAdminFilter(x);
|
|
||||||
|
|
||||||
const renderUserCard = (selectedUser: User) => {
|
const renderUserCard = (selectedUser: User) => {
|
||||||
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
|
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
|
||||||
@@ -838,9 +583,7 @@ export default function UserList({
|
|||||||
<UserCard
|
<UserCard
|
||||||
loggedInUser={user}
|
loggedInUser={user}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
(selectedUser.type === "corporate" ||
|
(selectedUser.type === "corporate" || selectedUser.type === "teacher") && studentsFromAdmin.length > 0
|
||||||
selectedUser.type === "teacher") &&
|
|
||||||
studentsFromAdmin.length > 0
|
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
@@ -856,9 +599,7 @@ export default function UserList({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
(selectedUser.type === "corporate" ||
|
(selectedUser.type === "corporate" || selectedUser.type === "student") && teachersFromAdmin.length > 0
|
||||||
selectedUser.type === "student") &&
|
|
||||||
teachersFromAdmin.length > 0
|
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
@@ -907,20 +648,13 @@ export default function UserList({
|
|||||||
<>
|
<>
|
||||||
{renderHeader && renderHeader(displayUsers.length)}
|
{renderHeader && renderHeader(displayUsers.length)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Modal
|
<Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}>
|
||||||
isOpen={!!selectedUser}
|
|
||||||
onClose={() => setSelectedUser(undefined)}
|
|
||||||
>
|
|
||||||
{selectedUser && renderUserCard(selectedUser)}
|
{selectedUser && renderUserCard(selectedUser)}
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
<div className="w-full flex gap-2 items-end">
|
<div className="w-full flex gap-2 items-end">
|
||||||
{renderSearch()}
|
{renderSearch()}
|
||||||
<Button
|
<Button className="w-full max-w-[200px] mb-1" variant="outline" onClick={downloadExcel}>
|
||||||
className="w-full max-w-[200px] mb-1"
|
|
||||||
variant="outline"
|
|
||||||
onClick={downloadExcel}
|
|
||||||
>
|
|
||||||
Download List
|
Download List
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -930,12 +664,7 @@ export default function UserList({
|
|||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th className="py-4 px-4 text-left" key={header.id}>
|
<th className="py-4 px-4 text-left" key={header.id}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -943,16 +672,10 @@ export default function UserList({
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="px-2">
|
<tbody className="px-2">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<tr
|
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||||
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
|
||||||
key={row.id}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
||||||
{flexRender(
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext()
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ 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}) {
|
||||||
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
|
||||||
@@ -19,27 +22,21 @@ export default function Lists({ user }: { user: User }) {
|
|||||||
"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
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
? "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", "admin", "corporate", "mastercorporate", "teacher"]) && (
|
||||||
<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
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
? "bg-white shadow"
|
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
Exam List
|
Exam List
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
@@ -49,12 +46,9 @@ export default function Lists({ user }: { user: User }) {
|
|||||||
"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
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
? "bg-white shadow"
|
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
Group List
|
Group List
|
||||||
</Tab>
|
</Tab>
|
||||||
{checkAccess(user, ["developer", "admin", "corporate"]) && (
|
{checkAccess(user, ["developer", "admin", "corporate"]) && (
|
||||||
@@ -64,12 +58,9 @@ export default function Lists({ user }: { user: User }) {
|
|||||||
"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
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
? "bg-white shadow"
|
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
Code List
|
Code List
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
@@ -80,12 +71,9 @@ export default function Lists({ user }: { user: User }) {
|
|||||||
"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
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
? "bg-white shadow"
|
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
Package List
|
Package List
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
@@ -96,12 +84,9 @@ export default function Lists({ user }: { user: User }) {
|
|||||||
"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
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
|
||||||
? "bg-white shadow"
|
|
||||||
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
|
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
Discount List
|
Discount List
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
@@ -118,11 +103,7 @@ export default function Lists({ user }: { user: User }) {
|
|||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
<GroupList user={user} />
|
<GroupList user={user} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
{checkAccess(
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "viewCodes") && (
|
||||||
user,
|
|
||||||
["developer", "admin", "corporate", "mastercorporate"],
|
|
||||||
"viewCodes"
|
|
||||||
) && (
|
|
||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||||
<CodeList user={user} />
|
<CodeList user={user} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|||||||
@@ -230,7 +230,11 @@ const TaskTab = ({section, setSection}: {section: LevelSection; setSection: (sec
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LevelGeneration = () => {
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LevelGeneration = ({ id } : Props) => {
|
||||||
const [generatedExam, setGeneratedExam] = useState<LevelExam>();
|
const [generatedExam, setGeneratedExam] = useState<LevelExam>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<LevelExam>();
|
const [resultingExam, setResultingExam] = useState<LevelExam>();
|
||||||
@@ -420,10 +424,16 @@ const LevelGeneration = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!id) {
|
||||||
|
toast.error("Please insert a title before submitting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const exam = {
|
const exam = {
|
||||||
...generatedExam,
|
...generatedExam,
|
||||||
|
id,
|
||||||
parts: generatedExam.parts.map((p, i) => ({...p, exercises: parts[i].part!.exercises})),
|
parts: generatedExam.parts.map((p, i) => ({...p, exercises: parts[i].part!.exercises})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,11 @@ interface ListeningPart {
|
|||||||
| string;
|
| string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListeningGeneration = () => {
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListeningGeneration = ({ id } : Props) => {
|
||||||
const [part1, setPart1] = useState<ListeningPart>();
|
const [part1, setPart1] = useState<ListeningPart>();
|
||||||
const [part2, setPart2] = useState<ListeningPart>();
|
const [part2, setPart2] = useState<ListeningPart>();
|
||||||
const [part3, setPart3] = useState<ListeningPart>();
|
const [part3, setPart3] = useState<ListeningPart>();
|
||||||
@@ -258,11 +262,16 @@ const ListeningGeneration = () => {
|
|||||||
console.log({parts});
|
console.log({parts});
|
||||||
if (parts.length === 0) return toast.error("Please generate at least one section!");
|
if (parts.length === 0) return toast.error("Please generate at least one section!");
|
||||||
|
|
||||||
|
if(!id) {
|
||||||
|
toast.error("Please insert a title before submitting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(`/api/exam/listening/generate/listening`, {
|
.post(`/api/exam/listening/generate/listening`, {
|
||||||
id: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}),
|
id,
|
||||||
parts,
|
parts,
|
||||||
minTimer,
|
minTimer,
|
||||||
difficulty,
|
difficulty,
|
||||||
|
|||||||
@@ -258,7 +258,11 @@ const PartTab = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReadingGeneration = () => {
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReadingGeneration = ({ id } : Props) => {
|
||||||
const [part1, setPart1] = useState<ReadingPart>();
|
const [part1, setPart1] = useState<ReadingPart>();
|
||||||
const [part2, setPart2] = useState<ReadingPart>();
|
const [part2, setPart2] = useState<ReadingPart>();
|
||||||
const [part3, setPart3] = useState<ReadingPart>();
|
const [part3, setPart3] = useState<ReadingPart>();
|
||||||
@@ -300,13 +304,18 @@ const ReadingGeneration = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!id) {
|
||||||
|
toast.error("Please insert a title before submitting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const exam: ReadingExam = {
|
const exam: ReadingExam = {
|
||||||
parts,
|
parts,
|
||||||
isDiagnostic: false,
|
isDiagnostic: false,
|
||||||
minTimer,
|
minTimer,
|
||||||
module: "reading",
|
module: "reading",
|
||||||
id: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}),
|
id,
|
||||||
type: "academic",
|
type: "academic",
|
||||||
variant: parts.length === 3 ? "full" : "partial",
|
variant: parts.length === 3 ? "full" : "partial",
|
||||||
difficulty,
|
difficulty,
|
||||||
@@ -328,7 +337,7 @@ const ReadingGeneration = () => {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
toast.error("Something went wrong while generating, please try again later.");
|
toast.error(error.response.data.error || "Something went wrong while generating, please try again later.");
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -221,7 +221,11 @@ interface SpeakingPart {
|
|||||||
avatar?: (typeof AVATARS)[number];
|
avatar?: (typeof AVATARS)[number];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpeakingGeneration = () => {
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpeakingGeneration = ({ id } : Props) => {
|
||||||
const [part1, setPart1] = useState<SpeakingPart>();
|
const [part1, setPart1] = useState<SpeakingPart>();
|
||||||
const [part2, setPart2] = useState<SpeakingPart>();
|
const [part2, setPart2] = useState<SpeakingPart>();
|
||||||
const [part3, setPart3] = useState<SpeakingPart>();
|
const [part3, setPart3] = useState<SpeakingPart>();
|
||||||
@@ -243,6 +247,11 @@ const SpeakingGeneration = () => {
|
|||||||
const submitExam = () => {
|
const submitExam = () => {
|
||||||
if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!");
|
if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!");
|
||||||
|
|
||||||
|
if(!id) {
|
||||||
|
toast.error("Please insert a title before submitting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x);
|
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x);
|
||||||
@@ -256,7 +265,7 @@ const SpeakingGeneration = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const exam: SpeakingExam = {
|
const exam: SpeakingExam = {
|
||||||
id: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}),
|
id,
|
||||||
isDiagnostic: false,
|
isDiagnostic: false,
|
||||||
exercises: exercises as (SpeakingExercise | InteractiveSpeakingExercise)[],
|
exercises: exercises as (SpeakingExercise | InteractiveSpeakingExercise)[],
|
||||||
minTimer,
|
minTimer,
|
||||||
|
|||||||
@@ -75,7 +75,11 @@ const TaskTab = ({task, index, difficulty, setTask}: {task?: string; difficulty:
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WritingGeneration = () => {
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WritingGeneration = ({ id } : Props) => {
|
||||||
const [task1, setTask1] = useState<string>();
|
const [task1, setTask1] = useState<string>();
|
||||||
const [task2, setTask2] = useState<string>();
|
const [task2, setTask2] = useState<string>();
|
||||||
const [minTimer, setMinTimer] = useState(60);
|
const [minTimer, setMinTimer] = useState(60);
|
||||||
@@ -116,6 +120,11 @@ const WritingGeneration = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!id) {
|
||||||
|
toast.error("Please insert a title before submitting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const exercise1 = task1
|
const exercise1 = task1
|
||||||
? ({
|
? ({
|
||||||
id: v4(),
|
id: v4(),
|
||||||
@@ -152,7 +161,7 @@ const WritingGeneration = () => {
|
|||||||
minTimer,
|
minTimer,
|
||||||
module: "writing",
|
module: "writing",
|
||||||
exercises: [...(exercise1 ? [exercise1] : []), ...(exercise2 ? [exercise2] : [])],
|
exercises: [...(exercise1 ? [exercise1] : []), ...(exercise2 ? [exercise2] : [])],
|
||||||
id: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}),
|
id,
|
||||||
variant: exercise1 && exercise2 ? "full" : "partial",
|
variant: exercise1 && exercise2 ? "full" : "partial",
|
||||||
difficulty,
|
difficulty,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) {
|
|||||||
const {packages} = usePackages();
|
const {packages} = usePackages();
|
||||||
const {discounts} = useDiscounts();
|
const {discounts} = useDiscounts();
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {groups} = useGroups();
|
const {groups} = useGroups({});
|
||||||
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
|
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
40
src/pages/api/assignments/corporate.ts
Normal file
40
src/pages/api/assignments/corporate.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
|
import {app} from "@/firebase";
|
||||||
|
import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc} from "firebase/firestore";
|
||||||
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
import {uuidv4} from "@firebase/util";
|
||||||
|
import {Module} from "@/interfaces";
|
||||||
|
import {getExams} from "@/utils/exams.be";
|
||||||
|
import {Exam, InstructorGender, Variant} from "@/interfaces/exam";
|
||||||
|
import {capitalize, flatten, uniqBy} from "lodash";
|
||||||
|
import {User} from "@/interfaces/user";
|
||||||
|
import moment from "moment";
|
||||||
|
import {sendEmail} from "@/email";
|
||||||
|
import {getAllAssignersByCorporate} from "@/utils/groups.be";
|
||||||
|
import {getAssignmentsByAssigners} from "@/utils/assignments.be";
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
|
|
||||||
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!req.session.user) {
|
||||||
|
res.status(401).json({ok: false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET") return GET(req, res);
|
||||||
|
|
||||||
|
res.status(404).json({ok: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const {id} = req.query as {id: string};
|
||||||
|
|
||||||
|
const assigners = await getAllAssignersByCorporate(id);
|
||||||
|
const assignments = await getAssignmentsByAssigners([...assigners, id]);
|
||||||
|
|
||||||
|
res.status(200).json(assignments);
|
||||||
|
}
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
// 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 {
|
import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, deleteDoc} from "firebase/firestore";
|
||||||
getFirestore,
|
|
||||||
setDoc,
|
|
||||||
doc,
|
|
||||||
query,
|
|
||||||
collection,
|
|
||||||
where,
|
|
||||||
getDocs,
|
|
||||||
getDoc,
|
|
||||||
deleteDoc,
|
|
||||||
} 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 { Code, Type } from "@/interfaces/user";
|
import {Code, Group, Type} from "@/interfaces/user";
|
||||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
import {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import {prepareMailer, prepareMailOptions} from "@/email";
|
import {prepareMailer, prepareMailOptions} from "@/email";
|
||||||
@@ -33,17 +23,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res
|
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
||||||
.status(401)
|
|
||||||
.json({ ok: false, reason: "You must be logged in to generate a code!" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {creator} = req.query as {creator?: string};
|
const {creator} = req.query as {creator?: string};
|
||||||
const q = query(
|
const q = query(collection(db, "codes"), where("creator", "==", creator || ""));
|
||||||
collection(db, "codes"),
|
|
||||||
where("creator", "==", creator || ""),
|
|
||||||
);
|
|
||||||
const snapshot = await getDocs(creator ? q : collection(db, "codes"));
|
const snapshot = await getDocs(creator ? q : collection(db, "codes"));
|
||||||
|
|
||||||
res.status(200).json(snapshot.docs.map((doc) => doc.data()));
|
res.status(200).json(snapshot.docs.map((doc) => doc.data()));
|
||||||
@@ -51,9 +36,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res
|
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
||||||
.status(401)
|
|
||||||
.json({ ok: false, reason: "You must be logged in to generate a code!" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,23 +51,28 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (!permission.includes(req.session.user.type)) {
|
if (!permission.includes(req.session.user.type)) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
ok: false,
|
ok: false,
|
||||||
reason:
|
reason: "Your account type does not have permissions to generate a code for that type of user!",
|
||||||
"Your account type does not have permissions to generate a code for that type of user!",
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const codesGeneratedByUserSnapshot = await getDocs(
|
const codesGeneratedByUserSnapshot = await getDocs(query(collection(db, "codes"), where("creator", "==", req.session.user.id)));
|
||||||
query(collection(db, "codes"), where("creator", "==", req.session.user.id)),
|
const creatorGroupsSnapshot = await getDocs(query(collection(db, "groups"), where("admin", "==", req.session.user.id)));
|
||||||
);
|
|
||||||
|
const creatorGroups = (
|
||||||
|
creatorGroupsSnapshot.docs.map((x) => ({
|
||||||
|
...x.data(),
|
||||||
|
})) as Group[]
|
||||||
|
).filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate");
|
||||||
|
|
||||||
|
const usersInGroups = creatorGroups.flatMap((x) => x.participants);
|
||||||
const userCodes = codesGeneratedByUserSnapshot.docs.map((x) => ({
|
const userCodes = codesGeneratedByUserSnapshot.docs.map((x) => ({
|
||||||
...x.data(),
|
...x.data(),
|
||||||
}));
|
})) as Code[];
|
||||||
|
|
||||||
if (req.session.user.type === "corporate") {
|
if (req.session.user.type === "corporate") {
|
||||||
const totalCodes = codesGeneratedByUserSnapshot.docs.length + codes.length;
|
const totalCodes = userCodes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)).length + usersInGroups.length + codes.length;
|
||||||
const allowedCodes =
|
const allowedCodes = req.session.user.corporateInformation?.companyInformation.userAmount || 0;
|
||||||
req.session.user.corporateInformation?.companyInformation.userAmount || 0;
|
|
||||||
|
|
||||||
if (totalCodes > allowedCodes) {
|
if (totalCodes > allowedCodes) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
@@ -155,9 +143,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
async function del(req: NextApiRequest, res: NextApiResponse) {
|
async function del(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res
|
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
||||||
.status(401)
|
|
||||||
.json({ ok: false, reason: "You must be logged in to generate a code!" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
// 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, setDoc, doc} from "firebase/firestore";
|
import {
|
||||||
|
getFirestore,
|
||||||
|
setDoc,
|
||||||
|
doc,
|
||||||
|
runTransaction,
|
||||||
|
collection,
|
||||||
|
query,
|
||||||
|
where,
|
||||||
|
getDocs,
|
||||||
|
} 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 { Exam, InstructorGender, Variant } from "@/interfaces/exam";
|
import { Exam, InstructorGender, Variant } from "@/interfaces/exam";
|
||||||
@@ -31,7 +40,14 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
instructorGender?: InstructorGender;
|
instructorGender?: InstructorGender;
|
||||||
};
|
};
|
||||||
|
|
||||||
const exams: Exam[] = await getExams(db, module, avoidRepeated, req.session.user.id, variant, instructorGender);
|
const exams: Exam[] = await getExams(
|
||||||
|
db,
|
||||||
|
module,
|
||||||
|
avoidRepeated,
|
||||||
|
req.session.user.id,
|
||||||
|
variant,
|
||||||
|
instructorGender
|
||||||
|
);
|
||||||
res.status(200).json(exams);
|
res.status(200).json(exams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +63,27 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
const { module } = req.query as { module: string };
|
const { module } = req.query as { module: string };
|
||||||
|
|
||||||
const exam = {...req.body, module: module, createdBy: req.session.user.id, createdAt: new Date().toISOString()};
|
try {
|
||||||
await setDoc(doc(db, module, req.body.id), exam);
|
const exam = {
|
||||||
|
...req.body,
|
||||||
|
module: module,
|
||||||
|
createdBy: req.session.user.id,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
await runTransaction(db, async (transaction) => {
|
||||||
|
const docRef = doc(db, module, req.body.id);
|
||||||
|
const docSnap = await transaction.get(docRef);
|
||||||
|
|
||||||
res.status(200).json(exam);
|
if (docSnap.exists()) {
|
||||||
|
throw new Error("Name already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDocRef = doc(db, module, req.body.id);
|
||||||
|
transaction.set(newDocRef, exam);
|
||||||
|
});
|
||||||
|
res.status(200).json(exam);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Transaction failed: ", error);
|
||||||
|
res.status(500).json({ ok: false, error: (error as any).message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,6 @@
|
|||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {app} from "@/firebase";
|
import {app} from "@/firebase";
|
||||||
import {
|
import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, deleteDoc, limit, updateDoc} from "firebase/firestore";
|
||||||
getFirestore,
|
|
||||||
setDoc,
|
|
||||||
doc,
|
|
||||||
query,
|
|
||||||
collection,
|
|
||||||
where,
|
|
||||||
getDocs,
|
|
||||||
getDoc,
|
|
||||||
deleteDoc,
|
|
||||||
limit,
|
|
||||||
updateDoc,
|
|
||||||
} 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 {v4} from "uuid";
|
import {v4} from "uuid";
|
||||||
@@ -39,7 +27,6 @@ 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 === "POST") return post(req, res);
|
if (req.method === "POST") return post(req, res);
|
||||||
|
|
||||||
return res.status(404).json({ok: false});
|
return res.status(404).json({ok: false});
|
||||||
@@ -48,19 +35,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const maker = req.session.user;
|
const maker = req.session.user;
|
||||||
if (!maker) {
|
if (!maker) {
|
||||||
return res
|
return res.status(401).json({ok: false, reason: "You must be logged in to make user!"});
|
||||||
.status(401)
|
|
||||||
.json({ ok: false, reason: "You must be logged in to make user!" });
|
|
||||||
}
|
}
|
||||||
const { email, passport_id, type, groupName } = req.body as {
|
const {email, passport_id, type, groupName, expiryDate} = req.body as {
|
||||||
email: string;
|
email: string;
|
||||||
passport_id: string;
|
passport_id: string;
|
||||||
type: string,
|
type: string;
|
||||||
groupName: string
|
groupName: string;
|
||||||
|
expiryDate: null | Date;
|
||||||
};
|
};
|
||||||
// cleaning data
|
// cleaning data
|
||||||
delete req.body.passport_id;
|
delete req.body.passport_id;
|
||||||
delete req.body.groupName;
|
delete req.body.groupName;
|
||||||
|
delete req.body.expiryDate;
|
||||||
|
|
||||||
await createUserWithEmailAndPassword(auth, email.toLowerCase(), passport_id)
|
await createUserWithEmailAndPassword(auth, email.toLowerCase(), passport_id)
|
||||||
.then(async (userCredentials) => {
|
.then(async (userCredentials) => {
|
||||||
@@ -71,11 +58,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
bio: "",
|
bio: "",
|
||||||
type: type,
|
type: type,
|
||||||
focus: "academic",
|
focus: "academic",
|
||||||
status: "paymentDue",
|
status: "active",
|
||||||
desiredLevels: DEFAULT_DESIRED_LEVELS,
|
desiredLevels: DEFAULT_DESIRED_LEVELS,
|
||||||
levels: DEFAULT_LEVELS,
|
levels: DEFAULT_LEVELS,
|
||||||
isFirstLogin: false,
|
isFirstLogin: false,
|
||||||
isVerified: true
|
isVerified: true,
|
||||||
|
registrationDate: new Date(),
|
||||||
|
subscriptionExpirationDate: expiryDate || null,
|
||||||
};
|
};
|
||||||
await setDoc(doc(db, "users", userId), user);
|
await setDoc(doc(db, "users", userId), user);
|
||||||
if (type === "corporate") {
|
if (type === "corporate") {
|
||||||
@@ -103,16 +92,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
disableEditing: true,
|
disableEditing: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup);
|
await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup);
|
||||||
await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup);
|
await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup);
|
||||||
await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup);
|
await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof groupName === 'string' && groupName.trim().length > 0){
|
if (typeof groupName === "string" && groupName.trim().length > 0) {
|
||||||
|
const q = query(collection(db, "groups"), where("admin", "==", maker.id), where("name", "==", groupName.trim()), limit(1));
|
||||||
const q = query(collection(db, "groups"), where("admin", "==", maker.id), where("name", "==", groupName.trim()), limit(1))
|
const snapshot = await getDocs(q);
|
||||||
const snapshot = await getDocs(q)
|
|
||||||
|
|
||||||
if (snapshot.empty) {
|
if (snapshot.empty) {
|
||||||
const values = {
|
const values = {
|
||||||
@@ -121,32 +108,27 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
name: groupName.trim(),
|
name: groupName.trim(),
|
||||||
participants: [userId],
|
participants: [userId],
|
||||||
disableEditing: false,
|
disableEditing: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
await setDoc(doc(db, "groups", values.id) , values)
|
|
||||||
|
|
||||||
|
|
||||||
|
await setDoc(doc(db, "groups", values.id), values);
|
||||||
} else {
|
} else {
|
||||||
|
const doc = snapshot.docs[0];
|
||||||
|
const participants: string[] = doc.get("participants");
|
||||||
const doc = snapshot.docs[0]
|
|
||||||
const participants : string[] = doc.get('participants');
|
|
||||||
|
|
||||||
|
|
||||||
if (!participants.includes(userId)) {
|
if (!participants.includes(userId)) {
|
||||||
|
|
||||||
|
|
||||||
updateDoc(doc.ref, {
|
updateDoc(doc.ref, {
|
||||||
participants: [...participants, userId]
|
participants: [...participants, userId],
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Returning - ${email}`);
|
||||||
|
return res.status(200).json({ok: true});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
console.log(`Failing - ${email}`);
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return res.status(401).json({error});
|
return res.status(401).json({error});
|
||||||
});
|
});
|
||||||
return res.status(200).json({ ok: true });
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// 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);
|
||||||
|
|
||||||
@@ -11,6 +12,19 @@ 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) {
|
||||||
@@ -18,8 +32,10 @@ async function patch(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
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 {users} = req.body;
|
const {users} = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setDoc(doc(db, "permissions", id), {users}, {merge: true});
|
await setDoc(doc(db, "permissions", id), {users}, {merge: true});
|
||||||
return res.status(200).json({ok: true});
|
return res.status(200).json({ok: true});
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -2,17 +2,7 @@ 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,
|
|
||||||
deleteDoc,
|
|
||||||
doc,
|
|
||||||
getDoc,
|
|
||||||
getDocs,
|
|
||||||
getFirestore,
|
|
||||||
query,
|
|
||||||
setDoc,
|
|
||||||
where,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import {getAuth} from "firebase-admin/auth";
|
import {getAuth} from "firebase-admin/auth";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {NextApiRequest, NextApiResponse} from "next";
|
import {NextApiRequest, NextApiResponse} from "next";
|
||||||
@@ -54,18 +44,10 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
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" &&
|
|
||||||
(targetUser.type === "student" || targetUser.type === "teacher")
|
|
||||||
) {
|
|
||||||
res.json({ok: true});
|
res.json({ok: true});
|
||||||
|
|
||||||
const userParticipantGroup = await getDocs(
|
const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id)));
|
||||||
query(
|
|
||||||
collection(db, "groups"),
|
|
||||||
where("participants", "array-contains", id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...userParticipantGroup.docs
|
...userParticipantGroup.docs
|
||||||
.filter((x) => (x.data() as Group).admin === user.id)
|
.filter((x) => (x.data() as Group).admin === user.id)
|
||||||
@@ -74,12 +56,10 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
await setDoc(
|
await setDoc(
|
||||||
x.ref,
|
x.ref,
|
||||||
{
|
{
|
||||||
participants: x
|
participants: x.data().participants.filter((y: string) => y !== id),
|
||||||
.data()
|
|
||||||
.participants.filter((y: string) => y !== id),
|
|
||||||
},
|
},
|
||||||
{ merge: true }
|
{merge: true},
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -96,18 +76,10 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
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)),
|
||||||
@@ -120,8 +92,8 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
{
|
{
|
||||||
participants: x.data().participants.filter((y: string) => y !== id),
|
participants: x.data().participants.filter((y: string) => y !== id),
|
||||||
},
|
},
|
||||||
{ merge: true }
|
{merge: true},
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -135,20 +107,16 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = docUser.data() as User;
|
const user = docUser.data() as User;
|
||||||
|
await setDoc(docUser.ref, {lastLogin: new Date().toISOString()}, {merge: true});
|
||||||
|
|
||||||
const permissionDocs = await getPermissionDocs();
|
|
||||||
|
|
||||||
const userWithPermissions = {
|
|
||||||
...user,
|
|
||||||
permissions: getPermissions(req.session.user.id, permissionDocs),
|
|
||||||
};
|
|
||||||
req.session.user = {
|
req.session.user = {
|
||||||
...userWithPermissions,
|
...user,
|
||||||
id: req.session.user.id,
|
id: req.session.user.id,
|
||||||
|
lastLogin: new Date(),
|
||||||
};
|
};
|
||||||
await req.session.save();
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default function Generation() {
|
|||||||
|
|
||||||
const { user } = useUser({ redirectTo: "/login" });
|
const { user } = useUser({ redirectTo: "/login" });
|
||||||
|
|
||||||
|
const [title, setTitle] = useState<string>("");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -73,6 +74,17 @@ export default function Generation() {
|
|||||||
<Layout user={user} className="gap-6">
|
<Layout user={user} className="gap-6">
|
||||||
<h1 className="text-2xl font-semibold">Exam Generation</h1>
|
<h1 className="text-2xl font-semibold">Exam Generation</h1>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Insert a title here"
|
||||||
|
name="title"
|
||||||
|
label="Title"
|
||||||
|
onChange={setTitle}
|
||||||
|
roundness="xl"
|
||||||
|
defaultValue={title}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
Module
|
Module
|
||||||
</label>
|
</label>
|
||||||
@@ -117,11 +129,11 @@ export default function Generation() {
|
|||||||
))}
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
{module === "reading" && <ReadingGeneration />}
|
{module === "reading" && <ReadingGeneration id={title} />}
|
||||||
{module === "listening" && <ListeningGeneration />}
|
{module === "listening" && <ListeningGeneration id={title} />}
|
||||||
{module === "writing" && <WritingGeneration />}
|
{module === "writing" && <WritingGeneration id={title} />}
|
||||||
{module === "speaking" && <SpeakingGeneration />}
|
{module === "speaking" && <SpeakingGeneration id={title} />}
|
||||||
{module === "level" && <LevelGeneration />}
|
{module === "level" && <LevelGeneration id={title} />}
|
||||||
</Layout>
|
</Layout>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
119
src/pages/groups.tsx
Normal file
119
src/pages/groups.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Head from "next/head";
|
||||||
|
import Navbar from "@/components/Navbar";
|
||||||
|
import {BsFileEarmarkText, BsPencil, BsStar, BsBook, BsHeadphones, BsPen, BsMegaphone} from "react-icons/bs";
|
||||||
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import useStats from "@/hooks/useStats";
|
||||||
|
import {averageScore, groupBySession, totalExams} from "@/utils/stats";
|
||||||
|
import useUser from "@/hooks/useUser";
|
||||||
|
import Diagnostic from "@/components/Diagnostic";
|
||||||
|
import {ToastContainer} from "react-toastify";
|
||||||
|
import {capitalize} from "lodash";
|
||||||
|
import {Module} from "@/interfaces";
|
||||||
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
|
import Layout from "@/components/High/Layout";
|
||||||
|
import {calculateAverageLevel} from "@/utils/score";
|
||||||
|
import axios from "axios";
|
||||||
|
import DemographicInformationInput from "@/components/DemographicInformationInput";
|
||||||
|
import moment from "moment";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||||
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
|
import StudentDashboard from "@/dashboards/Student";
|
||||||
|
import AdminDashboard from "@/dashboards/Admin";
|
||||||
|
import CorporateDashboard from "@/dashboards/Corporate";
|
||||||
|
import TeacherDashboard from "@/dashboards/Teacher";
|
||||||
|
import AgentDashboard from "@/dashboards/Agent";
|
||||||
|
import MasterCorporateDashboard from "@/dashboards/MasterCorporate";
|
||||||
|
import PaymentDue from "./(status)/PaymentDue";
|
||||||
|
import {useRouter} from "next/router";
|
||||||
|
import {PayPalScriptProvider} from "@paypal/react-paypal-js";
|
||||||
|
import {CorporateUser, MasterCorporateUser, Type, User, userTypes} from "@/interfaces/user";
|
||||||
|
import Select from "react-select";
|
||||||
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
|
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||||
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
|
import useGroups from "@/hooks/useGroups";
|
||||||
|
import useUsers from "@/hooks/useUsers";
|
||||||
|
import {getUserName} from "@/utils/users";
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
|
||||||
|
if (!user || !user.isVerified) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/login",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRedirectHome(user)) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {user: req.session.user},
|
||||||
|
};
|
||||||
|
}, sessionOptions);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
envVariables: {[key: string]: string};
|
||||||
|
}
|
||||||
|
export default function Home(props: Props) {
|
||||||
|
const {user, mutateUser} = useUser({redirectTo: "/login"});
|
||||||
|
const {groups} = useGroups({});
|
||||||
|
const {users} = useUsers();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(groups);
|
||||||
|
}, [groups]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>EnCoach</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<ToastContainer />
|
||||||
|
{user && (
|
||||||
|
<Layout user={user}>
|
||||||
|
<div className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{groups
|
||||||
|
.filter((x) => x.participants.includes(user.id))
|
||||||
|
.map((group) => (
|
||||||
|
<div key={group.id} className="p-4 border rounded-xl flex flex-col gap-2">
|
||||||
|
<span>
|
||||||
|
<b>Group: </b>
|
||||||
|
{group.name}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<b>Admin: </b>
|
||||||
|
{getUserName(users.find((x) => x.id === group.admin))}
|
||||||
|
</span>
|
||||||
|
<b>Participants: </b>
|
||||||
|
<span>{group.participants.map((x) => getUserName(users.find((u) => u.id === x))).join(", ")}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -390,7 +390,7 @@ interface PaypalPaymentWithUserData extends PaypalPayment {
|
|||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paypalFilterRows = [["email"], ["name"]];
|
const paypalFilterRows = [["email"], ["name"], ["orderId"], ["value"]];
|
||||||
export default function PaymentRecord() {
|
export default function PaymentRecord() {
|
||||||
const [selectedCorporateUser, setSelectedCorporateUser] = useState<User>();
|
const [selectedCorporateUser, setSelectedCorporateUser] = useState<User>();
|
||||||
const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
|
const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
|
||||||
@@ -414,6 +414,14 @@ export default function PaymentRecord() {
|
|||||||
const [endDate, setEndDate] = useState<Date | null>(
|
const [endDate, setEndDate] = useState<Date | null>(
|
||||||
moment().endOf("day").toDate()
|
moment().endOf("day").toDate()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [startDatePaymob, setStartDatePaymob] = useState<Date | null>(
|
||||||
|
moment("01/01/2023").toDate()
|
||||||
|
);
|
||||||
|
const [endDatePaymob, setEndDatePaymob] = useState<Date | null>(
|
||||||
|
moment().endOf("day").toDate()
|
||||||
|
);
|
||||||
|
|
||||||
const [paid, setPaid] = useState<Boolean | null>(IS_PAID_OPTIONS[0].value);
|
const [paid, setPaid] = useState<Boolean | null>(IS_PAID_OPTIONS[0].value);
|
||||||
const [commissionTransfer, setCommissionTransfer] = useState<Boolean | null>(
|
const [commissionTransfer, setCommissionTransfer] = useState<Boolean | null>(
|
||||||
IS_FILE_SUBMITTED_OPTIONS[0].value
|
IS_FILE_SUBMITTED_OPTIONS[0].value
|
||||||
@@ -866,11 +874,16 @@ export default function PaymentRecord() {
|
|||||||
|
|
||||||
const updatedPaypalPayments = useMemo(
|
const updatedPaypalPayments = useMemo(
|
||||||
() =>
|
() =>
|
||||||
paypalPayments.map((p) => {
|
paypalPayments
|
||||||
|
.filter((p) => {
|
||||||
|
const date = moment(p.createdAt);
|
||||||
|
return date.isAfter(startDatePaymob) && date.isBefore(endDatePaymob);
|
||||||
|
})
|
||||||
|
.map((p) => {
|
||||||
const user = users.find((x) => x.id === p.userId) as User;
|
const user = users.find((x) => x.id === p.userId) as User;
|
||||||
return { ...p, name: user?.name, email: user?.email };
|
return { ...p, name: user?.name, email: user?.email };
|
||||||
}),
|
}),
|
||||||
[paypalPayments, users]
|
[paypalPayments, users, startDatePaymob, endDatePaymob]
|
||||||
);
|
);
|
||||||
|
|
||||||
const paypalColumns = [
|
const paypalColumns = [
|
||||||
@@ -1469,6 +1482,44 @@ export default function PaymentRecord() {
|
|||||||
{renderTable(table as Table<Payment>)}
|
{renderTable(table as Table<Payment>)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide flex flex-col gap-8">
|
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide flex flex-col gap-8">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"grid grid-cols-1 md:grid-cols-2 gap-8 w-full",
|
||||||
|
user.type !== "corporate" && "lg:grid-cols-3"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Date
|
||||||
|
</label>
|
||||||
|
<ReactDatePicker
|
||||||
|
dateFormat="dd/MM/yyyy"
|
||||||
|
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"
|
||||||
|
selected={startDatePaymob}
|
||||||
|
startDate={startDatePaymob}
|
||||||
|
endDate={endDatePaymob}
|
||||||
|
selectsRange
|
||||||
|
showMonthDropdown
|
||||||
|
filterDate={(date: Date) =>
|
||||||
|
moment(date).isSameOrBefore(moment(new Date()))
|
||||||
|
}
|
||||||
|
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
||||||
|
setStartDatePaymob(
|
||||||
|
initialDate ?? moment("01/01/2023").toDate()
|
||||||
|
);
|
||||||
|
if (finalDate) {
|
||||||
|
// basicly selecting a final day works as if I'm selecting the first
|
||||||
|
// minute of that day. this way it covers the whole day
|
||||||
|
setEndDatePaymob(
|
||||||
|
moment(finalDate).endOf("day").toDate()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEndDatePaymob(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{renderSearch()}
|
{renderSearch()}
|
||||||
{renderTable(paypalTable as Table<PaypalPaymentWithUserData>)}
|
{renderTable(paypalTable as Table<PaypalPaymentWithUserData>)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Page(props: Props) {
|
export default function Page(props: Props) {
|
||||||
|
|
||||||
const { permissions, user } = props;
|
const { permissions, user } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
|
|||||||
@@ -2,15 +2,7 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {
|
import {ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState} from "react";
|
||||||
ChangeEvent,
|
|
||||||
Dispatch,
|
|
||||||
ReactNode,
|
|
||||||
SetStateAction,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import {toast, ToastContainer} from "react-toastify";
|
import {toast, ToastContainer} from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
@@ -20,14 +12,7 @@ import Link from "next/link";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {ErrorMessage} from "@/constants/errors";
|
import {ErrorMessage} from "@/constants/errors";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {CorporateUser, EmploymentStatus, EMPLOYMENT_STATUS, Gender, User, DemographicInformation} from "@/interfaces/user";
|
||||||
CorporateUser,
|
|
||||||
EmploymentStatus,
|
|
||||||
EMPLOYMENT_STATUS,
|
|
||||||
Gender,
|
|
||||||
User,
|
|
||||||
DemographicInformation,
|
|
||||||
} from "@/interfaces/user";
|
|
||||||
import CountrySelect from "@/components/Low/CountrySelect";
|
import CountrySelect from "@/components/Low/CountrySelect";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -81,9 +66,7 @@ interface Props {
|
|||||||
mutateUser: Function;
|
mutateUser: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DoubleColumnRow = ({ children }: { children: ReactNode }) => (
|
const DoubleColumnRow = ({children}: {children: ReactNode}) => <div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>;
|
||||||
<div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
function UserProfile({user, mutateUser}: Props) {
|
function UserProfile({user, mutateUser}: Props) {
|
||||||
const [bio, setBio] = useState(user.bio || "");
|
const [bio, setBio] = useState(user.bio || "");
|
||||||
@@ -94,75 +77,41 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
|
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
|
||||||
|
|
||||||
const [desiredLevels, setDesiredLevels] = useState<
|
const [desiredLevels, setDesiredLevels] = useState<{[key in Module]: number} | undefined>(
|
||||||
{ [key in Module]: number } | undefined
|
checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined,
|
||||||
>(
|
|
||||||
checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined
|
|
||||||
);
|
);
|
||||||
const [focus, setFocus] = useState<"academic" | "general">(user.focus);
|
const [focus, setFocus] = useState<"academic" | "general">(user.focus);
|
||||||
|
|
||||||
const [country, setCountry] = useState<string>(
|
const [country, setCountry] = useState<string>(user.demographicInformation?.country || "");
|
||||||
user.demographicInformation?.country || ""
|
const [phone, setPhone] = useState<string>(user.demographicInformation?.phone || "");
|
||||||
);
|
const [gender, setGender] = useState<Gender | undefined>(user.demographicInformation?.gender || undefined);
|
||||||
const [phone, setPhone] = useState<string>(
|
|
||||||
user.demographicInformation?.phone || ""
|
|
||||||
);
|
|
||||||
const [gender, setGender] = useState<Gender | undefined>(
|
|
||||||
user.demographicInformation?.gender || undefined
|
|
||||||
);
|
|
||||||
const [employment, setEmployment] = useState<EmploymentStatus | undefined>(
|
const [employment, setEmployment] = useState<EmploymentStatus | undefined>(
|
||||||
checkAccess(user, ["corporate", "mastercorporate"])
|
checkAccess(user, ["corporate", "mastercorporate"]) ? undefined : (user.demographicInformation as DemographicInformation)?.employment,
|
||||||
? undefined
|
|
||||||
: (user.demographicInformation as DemographicInformation)?.employment
|
|
||||||
);
|
);
|
||||||
const [passport_id, setPassportID] = useState<string | undefined>(
|
const [passport_id, setPassportID] = useState<string | undefined>(
|
||||||
checkAccess(user, ["student"])
|
checkAccess(user, ["student"]) ? (user.demographicInformation as DemographicInformation)?.passport_id : undefined,
|
||||||
? (user.demographicInformation as DemographicInformation)?.passport_id
|
|
||||||
: undefined
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [preferredGender, setPreferredGender] = useState<
|
const [preferredGender, setPreferredGender] = useState<InstructorGender | undefined>(
|
||||||
InstructorGender | undefined
|
user.type === "student" || user.type === "developer" ? user.preferredGender || "varied" : undefined,
|
||||||
>(
|
|
||||||
user.type === "student" || user.type === "developer"
|
|
||||||
? user.preferredGender || "varied"
|
|
||||||
: undefined
|
|
||||||
);
|
);
|
||||||
const [preferredTopics, setPreferredTopics] = useState<string[] | undefined>(
|
const [preferredTopics, setPreferredTopics] = useState<string[] | undefined>(
|
||||||
user.type === "student" || user.type === "developer"
|
user.type === "student" || user.type === "developer" ? user.preferredTopics : undefined,
|
||||||
? user.preferredTopics
|
|
||||||
: undefined
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [position, setPosition] = useState<string | undefined>(
|
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
|
||||||
user.type === "corporate"
|
const [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : undefined);
|
||||||
? user.demographicInformation?.position
|
const [companyName, setCompanyName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyName : undefined);
|
||||||
: undefined
|
const [commercialRegistration, setCommercialRegistration] = useState<string | undefined>(
|
||||||
);
|
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
|
||||||
const [corporateInformation, setCorporateInformation] = useState(
|
|
||||||
user.type === "corporate" ? user.corporateInformation : undefined
|
|
||||||
);
|
|
||||||
const [companyName, setCompanyName] = useState<string | undefined>(
|
|
||||||
user.type === "agent" ? user.agentInformation?.companyName : undefined
|
|
||||||
);
|
|
||||||
const [commercialRegistration, setCommercialRegistration] = useState<
|
|
||||||
string | undefined
|
|
||||||
>(
|
|
||||||
user.type === "agent"
|
|
||||||
? user.agentInformation?.commercialRegistration
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
const [arabName, setArabName] = useState<string | undefined>(
|
|
||||||
user.type === "agent" ? user.agentInformation?.companyArabName : undefined
|
|
||||||
);
|
);
|
||||||
|
const [arabName, setArabName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyArabName : undefined);
|
||||||
|
|
||||||
const [timezone, setTimezone] = useState<string>(
|
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || moment.tz.guess());
|
||||||
user.demographicInformation?.timezone || moment.tz.guess()
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false);
|
const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false);
|
||||||
|
|
||||||
const { groups } = useGroups();
|
const {groups} = useGroups({});
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
|
|
||||||
const profilePictureInput = useRef(null);
|
const profilePictureInput = useRef(null);
|
||||||
@@ -170,12 +119,9 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new Date());
|
const today = moment(new Date());
|
||||||
|
|
||||||
if (today.add(1, "days").isAfter(momentDate))
|
if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light";
|
||||||
return "!bg-mti-red-ultralight border-mti-red-light";
|
if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light";
|
||||||
if (today.add(3, "days").isAfter(momentDate))
|
if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
|
||||||
return "!bg-mti-rose-ultralight border-mti-rose-light";
|
|
||||||
if (today.add(7, "days").isAfter(momentDate))
|
|
||||||
return "!bg-mti-orange-ultralight border-mti-orange-light";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadProfilePicture = async (event: ChangeEvent<HTMLInputElement>) => {
|
const uploadProfilePicture = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -195,20 +141,15 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword && !password) {
|
if (newPassword && !password) {
|
||||||
toast.error(
|
toast.error("To update your password you need to input your current one!");
|
||||||
"To update your password you need to input your current one!"
|
|
||||||
);
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (email !== user?.email) {
|
if (email !== user?.email) {
|
||||||
const userAdmins = groups
|
const userAdmins = groups.filter((x) => x.participants.includes(user.id)).map((x) => x.admin);
|
||||||
.filter((x) => x.participants.includes(user.id))
|
|
||||||
.map((x) => x.admin);
|
|
||||||
const message =
|
const message =
|
||||||
users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate")
|
users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate").length > 0
|
||||||
.length > 0
|
|
||||||
? "If you change your e-mail address, you will lose all benefits from your university/institute. Are you sure you want to continue?"
|
? "If you change your e-mail address, you will lose all benefits from your university/institute. Are you sure you want to continue?"
|
||||||
: "Are you sure you want to update your e-mail address?";
|
: "Are you sure you want to update your e-mail address?";
|
||||||
|
|
||||||
@@ -269,9 +210,7 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
|
|
||||||
const ExpirationDate = () => (
|
const ExpirationDate = () => (
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Expiry Date (click to purchase)</label>
|
||||||
Expiry Date (click to purchase)
|
|
||||||
</label>
|
|
||||||
<Link
|
<Link
|
||||||
href="/payment"
|
href="/payment"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -280,30 +219,22 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
!user.subscriptionExpirationDate
|
!user.subscriptionExpirationDate
|
||||||
? "!bg-mti-green-ultralight !border-mti-green-light"
|
? "!bg-mti-green-ultralight !border-mti-green-light"
|
||||||
: expirationDateColor(user.subscriptionExpirationDate),
|
: expirationDateColor(user.subscriptionExpirationDate),
|
||||||
"bg-white border-mti-gray-platinum"
|
"bg-white border-mti-gray-platinum",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
{!user.subscriptionExpirationDate && "Unlimited"}
|
{!user.subscriptionExpirationDate && "Unlimited"}
|
||||||
{user.subscriptionExpirationDate &&
|
{user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
|
||||||
moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const TimezoneInput = () => (
|
const TimezoneInput = () => (
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Timezone</label>
|
||||||
Timezone
|
|
||||||
</label>
|
|
||||||
<TimezoneSelect value={timezone} onChange={setTimezone} />
|
<TimezoneSelect value={timezone} onChange={setTimezone} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const manualDownloadLink = ["student", "teacher", "corporate"].includes(
|
const manualDownloadLink = ["student", "teacher", "corporate"].includes(user.type) ? `/manuals/${user.type}.pdf` : "";
|
||||||
user.type
|
|
||||||
)
|
|
||||||
? `/manuals/${user.type}.pdf`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
@@ -312,10 +243,7 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
<div className="flex -md:flex-col-reverse -md:items-center w-full justify-between">
|
<div className="flex -md:flex-col-reverse -md:items-center w-full justify-between">
|
||||||
<div className="flex flex-col gap-8 w-full md:w-2/3">
|
<div className="flex flex-col gap-8 w-full md:w-2/3">
|
||||||
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
|
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
|
||||||
<form
|
<form className="flex flex-col items-center gap-6 w-full" onSubmit={(e) => e.preventDefault()}>
|
||||||
className="flex flex-col items-center gap-6 w-full"
|
|
||||||
onSubmit={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
{user.type !== "corporate" ? (
|
{user.type !== "corporate" ? (
|
||||||
<Input
|
<Input
|
||||||
@@ -411,9 +339,7 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
|
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Country *</label>
|
||||||
Country *
|
|
||||||
</label>
|
|
||||||
<CountrySelect value={country} onChange={setCountry} />
|
<CountrySelect value={country} onChange={setCountry} />
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
@@ -446,37 +372,26 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{desiredLevels &&
|
{desiredLevels && ["developer", "student"].includes(user.type) && (
|
||||||
["developer", "student"].includes(user.type) && (
|
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Desired Levels</label>
|
||||||
Desired Levels
|
|
||||||
</label>
|
|
||||||
<ModuleLevelSelector
|
<ModuleLevelSelector
|
||||||
levels={desiredLevels}
|
levels={desiredLevels}
|
||||||
setLevels={
|
setLevels={setDesiredLevels as Dispatch<SetStateAction<{[key in Module]: number}>>}
|
||||||
setDesiredLevels as Dispatch<
|
|
||||||
SetStateAction<{ [key in Module]: number }>
|
|
||||||
>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Focus</label>
|
||||||
Focus
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-16 w-full">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-16 w-full">
|
||||||
<button
|
<button
|
||||||
onClick={() => setFocus("academic")}
|
onClick={() => setFocus("academic")}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
|
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
|
||||||
"hover:bg-mti-purple-light hover:text-white",
|
"hover:bg-mti-purple-light hover:text-white",
|
||||||
focus === "academic" &&
|
focus === "academic" && "!bg-mti-purple-light !text-white",
|
||||||
"!bg-mti-purple-light !text-white",
|
"transition duration-300 ease-in-out",
|
||||||
"transition duration-300 ease-in-out"
|
)}>
|
||||||
)}
|
|
||||||
>
|
|
||||||
Academic
|
Academic
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -484,11 +399,9 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
|
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
|
||||||
"hover:bg-mti-purple-light hover:text-white",
|
"hover:bg-mti-purple-light hover:text-white",
|
||||||
focus === "general" &&
|
focus === "general" && "!bg-mti-purple-light !text-white",
|
||||||
"!bg-mti-purple-light !text-white",
|
"transition duration-300 ease-in-out",
|
||||||
"transition duration-300 ease-in-out"
|
)}>
|
||||||
)}
|
|
||||||
>
|
|
||||||
General
|
General
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -496,27 +409,18 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{preferredGender &&
|
{preferredGender && ["developer", "student"].includes(user.type) && (
|
||||||
["developer", "student"].includes(user.type) && (
|
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor's Gender</label>
|
||||||
Speaking Instructor's Gender
|
|
||||||
</label>
|
|
||||||
<Select
|
<Select
|
||||||
value={{
|
value={{
|
||||||
value: preferredGender,
|
value: preferredGender,
|
||||||
label: capitalize(preferredGender),
|
label: capitalize(preferredGender),
|
||||||
}}
|
}}
|
||||||
onChange={(value) =>
|
onChange={(value) => (value ? setPreferredGender(value.value as InstructorGender) : null)}
|
||||||
value
|
|
||||||
? setPreferredGender(
|
|
||||||
value.value as InstructorGender
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
options={[
|
options={[
|
||||||
{value: "male", label: "Male"},
|
{value: "male", label: "Male"},
|
||||||
{value: "female", label: "Female"},
|
{value: "female", label: "Female"},
|
||||||
@@ -529,18 +433,12 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
Preferred Topics{" "}
|
Preferred Topics{" "}
|
||||||
<span
|
<span
|
||||||
className="tooltip"
|
className="tooltip"
|
||||||
data-tip="These topics will be considered for speaking and writing modules, aiming to include at least one exercise containing of the these in the selected exams."
|
data-tip="These topics will be considered for speaking and writing modules, aiming to include at least one exercise containing of the these in the selected exams.">
|
||||||
>
|
|
||||||
<BsQuestionCircleFill />
|
<BsQuestionCircleFill />
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<Button
|
<Button className="w-full" variant="outline" onClick={() => setIsPreferredTopicsOpen(true)}>
|
||||||
className="w-full"
|
Select Topics ({preferredTopics?.length || "All"} selected)
|
||||||
variant="outline"
|
|
||||||
onClick={() => setIsPreferredTopicsOpen(true)}
|
|
||||||
>
|
|
||||||
Select Topics ({preferredTopics?.length || "All"}{" "}
|
|
||||||
selected)
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DoubleColumnRow>
|
</DoubleColumnRow>
|
||||||
@@ -565,9 +463,7 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
name="companyUsers"
|
name="companyUsers"
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
label="Number of users"
|
label="Number of users"
|
||||||
defaultValue={
|
defaultValue={user.corporateInformation.companyInformation.userAmount}
|
||||||
user.corporateInformation.companyInformation.userAmount
|
|
||||||
}
|
|
||||||
disabled
|
disabled
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -611,20 +507,14 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user.type === "corporate" &&
|
{user.type === "corporate" && user.corporateInformation.referralAgent && (
|
||||||
user.corporateInformation.referralAgent && (
|
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<Input
|
<Input
|
||||||
name="agentName"
|
name="agentName"
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
defaultValue={
|
defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.name}
|
||||||
users.find(
|
|
||||||
(x) =>
|
|
||||||
x.id === user.corporateInformation.referralAgent
|
|
||||||
)?.name
|
|
||||||
}
|
|
||||||
type="text"
|
type="text"
|
||||||
label="Country Manager's Name"
|
label="Country Manager's Name"
|
||||||
placeholder="Not available"
|
placeholder="Not available"
|
||||||
@@ -634,12 +524,7 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
<Input
|
<Input
|
||||||
name="agentEmail"
|
name="agentEmail"
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
defaultValue={
|
defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.email}
|
||||||
users.find(
|
|
||||||
(x) =>
|
|
||||||
x.id === user.corporateInformation.referralAgent
|
|
||||||
)?.email
|
|
||||||
}
|
|
||||||
type="text"
|
type="text"
|
||||||
label="Country Manager's E-mail"
|
label="Country Manager's E-mail"
|
||||||
placeholder="Not available"
|
placeholder="Not available"
|
||||||
@@ -649,15 +534,11 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
</DoubleColumnRow>
|
</DoubleColumnRow>
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">
|
<label className="font-normal text-base text-mti-gray-dim">Country Manager's Country *</label>
|
||||||
Country Manager's Country *
|
|
||||||
</label>
|
|
||||||
<CountrySelect
|
<CountrySelect
|
||||||
value={
|
value={
|
||||||
users.find(
|
users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation
|
||||||
(x) =>
|
?.country
|
||||||
x.id === user.corporateInformation.referralAgent
|
|
||||||
)?.demographicInformation?.country
|
|
||||||
}
|
}
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
disabled
|
disabled
|
||||||
@@ -671,10 +552,7 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
placeholder="Not available"
|
placeholder="Not available"
|
||||||
defaultValue={
|
defaultValue={
|
||||||
users.find(
|
users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation?.phone
|
||||||
(x) =>
|
|
||||||
x.id === user.corporateInformation.referralAgent
|
|
||||||
)?.demographicInformation?.phone
|
|
||||||
}
|
}
|
||||||
disabled
|
disabled
|
||||||
required
|
required
|
||||||
@@ -685,10 +563,7 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
|
|
||||||
{user.type !== "corporate" && (
|
{user.type !== "corporate" && (
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<EmploymentStatusInput
|
<EmploymentStatusInput value={employment} onChange={setEmployment} />
|
||||||
value={employment}
|
|
||||||
onChange={setEmployment}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex flex-col gap-8 w-full">
|
||||||
<GenderInput value={gender} onChange={setGender} />
|
<GenderInput value={gender} onChange={setGender} />
|
||||||
@@ -701,62 +576,37 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
<div className="flex flex-col gap-6 w-48">
|
<div className="flex flex-col gap-6 w-48">
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-3 items-center h-fit cursor-pointer group"
|
className="flex flex-col gap-3 items-center h-fit cursor-pointer group"
|
||||||
onClick={() => (profilePictureInput.current as any)?.click()}
|
onClick={() => (profilePictureInput.current as any)?.click()}>
|
||||||
>
|
|
||||||
<div className="relative overflow-hidden h-48 w-48 rounded-full">
|
<div className="relative overflow-hidden h-48 w-48 rounded-full">
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute top-0 left-0 bg-mti-purple-light/60 w-full h-full z-20 flex items-center justify-center opacity-0 group-hover:opacity-100",
|
"absolute top-0 left-0 bg-mti-purple-light/60 w-full h-full z-20 flex items-center justify-center opacity-0 group-hover:opacity-100",
|
||||||
"transition ease-in-out duration-300"
|
"transition ease-in-out duration-300",
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
<BsCamera className="text-6xl text-mti-purple-ultralight/80" />
|
<BsCamera className="text-6xl text-mti-purple-ultralight/80" />
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img src={profilePicture} alt={user.name} className="aspect-square drop-shadow-xl self-end object-cover" />
|
||||||
src={profilePicture}
|
|
||||||
alt={user.name}
|
|
||||||
className="aspect-square drop-shadow-xl self-end object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="file" className="hidden" onChange={uploadProfilePicture} accept="image/*" ref={profilePictureInput} />
|
||||||
type="file"
|
|
||||||
className="hidden"
|
|
||||||
onChange={uploadProfilePicture}
|
|
||||||
accept="image/*"
|
|
||||||
ref={profilePictureInput}
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
onClick={() => (profilePictureInput.current as any)?.click()}
|
onClick={() => (profilePictureInput.current as any)?.click()}
|
||||||
className="cursor-pointer text-mti-purple-light text-sm"
|
className="cursor-pointer text-mti-purple-light text-sm">
|
||||||
>
|
|
||||||
Change picture
|
Change picture
|
||||||
</span>
|
</span>
|
||||||
<h6 className="font-normal text-base text-mti-gray-taupe">
|
<h6 className="font-normal text-base text-mti-gray-taupe">{USER_TYPE_LABELS[user.type]}</h6>
|
||||||
{USER_TYPE_LABELS[user.type]}
|
|
||||||
</h6>
|
|
||||||
</div>
|
</div>
|
||||||
{user.type === "agent" && (
|
{user.type === "agent" && (
|
||||||
<div className="flag items-center h-fit">
|
<div className="flag items-center h-fit">
|
||||||
<img
|
<img
|
||||||
alt={
|
alt={user.demographicInformation?.country.toLowerCase() + "_flag"}
|
||||||
user.demographicInformation?.country.toLowerCase() + "_flag"
|
|
||||||
}
|
|
||||||
src={`https://flagcdn.com/w320/${user.demographicInformation?.country.toLowerCase()}.png`}
|
src={`https://flagcdn.com/w320/${user.demographicInformation?.country.toLowerCase()}.png`}
|
||||||
width="320"
|
width="320"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{manualDownloadLink && (
|
{manualDownloadLink && (
|
||||||
<a
|
<a href={manualDownloadLink} className="max-w-[200px] self-end w-full" download>
|
||||||
href={manualDownloadLink}
|
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
|
||||||
className="max-w-[200px] self-end w-full"
|
|
||||||
download
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
className="max-w-[200px] self-end w-full"
|
|
||||||
>
|
|
||||||
Download Manual
|
Download Manual
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
@@ -775,20 +625,11 @@ function UserProfile({ user, mutateUser }: Props) {
|
|||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
<Link href="/" className="max-w-[200px] self-end w-full">
|
<Link href="/" className="max-w-[200px] self-end w-full">
|
||||||
<Button
|
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
className="max-w-[200px] self-end w-full"
|
|
||||||
>
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button
|
<Button color="purple" className="max-w-[200px] self-end w-full" onClick={updateUser} disabled={isLoading}>
|
||||||
color="purple"
|
|
||||||
className="max-w-[200px] self-end w-full"
|
|
||||||
onClick={updateUser}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import useRecordStore from "@/stores/recordStore";
|
|||||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||||
import StatsGridItem from "@/components/StatGridItem";
|
import StatsGridItem from "@/components/StatGridItem";
|
||||||
|
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
@@ -56,7 +55,12 @@ const defaultSelectableCorporate = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function History({user}: {user: User}) {
|
export default function History({user}: {user: User}) {
|
||||||
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.training, state.setTraining]);
|
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
||||||
|
state.selectedUser,
|
||||||
|
state.setSelectedUser,
|
||||||
|
state.training,
|
||||||
|
state.setTraining,
|
||||||
|
]);
|
||||||
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
||||||
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
|
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
|
||||||
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
||||||
@@ -64,7 +68,7 @@ export default function History({ user }: { user: User }) {
|
|||||||
|
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {stats, isLoading: isStatsLoading} = useStats(statsUserId);
|
const {stats, isLoading: isStatsLoading} = useStats(statsUserId);
|
||||||
const { groups: allGroups } = useGroups();
|
const {groups: allGroups} = useGroups({});
|
||||||
|
|
||||||
const groups = allGroups.filter((x) => x.admin === user.id);
|
const groups = allGroups.filter((x) => x.admin === user.id);
|
||||||
|
|
||||||
@@ -137,13 +141,13 @@ export default function History({ user }: { user: User }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = (url: string) => {
|
const handleRouteChange = (url: string) => {
|
||||||
setTraining(false)
|
setTraining(false);
|
||||||
}
|
};
|
||||||
router.events.on('routeChangeStart', handleRouteChange)
|
router.events.on("routeChangeStart", handleRouteChange);
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off('routeChangeStart', handleRouteChange)
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
}
|
};
|
||||||
}, [router.events, setTraining])
|
}, [router.events, setTraining]);
|
||||||
|
|
||||||
const handleTrainingContentSubmission = () => {
|
const handleTrainingContentSubmission = () => {
|
||||||
if (groupedStats) {
|
if (groupedStats) {
|
||||||
@@ -156,11 +160,10 @@ export default function History({ user }: { user: User }) {
|
|||||||
}
|
}
|
||||||
return accumulator;
|
return accumulator;
|
||||||
}, {});
|
}, {});
|
||||||
setTrainingStats(Object.values(selectedStats).flat())
|
setTrainingStats(Object.values(selectedStats).flat());
|
||||||
router.push("/training");
|
router.push("/training");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const customContent = (timestamp: string) => {
|
const customContent = (timestamp: string) => {
|
||||||
if (!groupedStats) return <></>;
|
if (!groupedStats) return <></>;
|
||||||
@@ -323,10 +326,12 @@ export default function History({ user }: { user: User }) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(training && (
|
{training && (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="font-semibold text-2xl mr-4">Select up to 10 exercises
|
<div className="font-semibold text-2xl mr-4">
|
||||||
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}</div>
|
Select up to 10 exercises
|
||||||
|
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4 disabled:cursor-not-allowed",
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4 disabled:cursor-not-allowed",
|
||||||
@@ -337,7 +342,7 @@ export default function History({ user }: { user: User }) {
|
|||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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;
|
||||||
@@ -27,7 +28,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldRedirectHome(user) || !["developer", "admin", "corporate", "agent", "mastercorporate"].includes(user.type)) {
|
if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"])) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/",
|
destination: "/",
|
||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
@@ -58,10 +60,10 @@ export default function Admin() {
|
|||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<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 grid grid-cols-2 -md:grid-cols-1 gap-8">
|
||||||
<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} />
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function Stats() {
|
|||||||
|
|
||||||
const {user} = useUser({redirectTo: "/login"});
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {groups} = useGroups(user?.id);
|
const {groups} = useGroups({admin: user?.id});
|
||||||
const {stats} = useStats(statsUserId, !statsUserId);
|
const {stats} = useStats(statsUserId, !statsUserId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -202,7 +202,7 @@ export default function Stats() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(["corporate", "teacher", "mastercorporate"].includes(user.type) ) && groups.length > 0 && (
|
{["corporate", "teacher", "mastercorporate"].includes(user.type) && groups.length > 0 && (
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
options={users
|
options={users
|
||||||
|
|||||||
@@ -57,8 +57,12 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
// Record stuff
|
// Record stuff
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
|
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
|
||||||
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.setTraining]);
|
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [
|
||||||
const { groups: allGroups } = useGroups();
|
state.selectedUser,
|
||||||
|
state.setSelectedUser,
|
||||||
|
state.setTraining,
|
||||||
|
]);
|
||||||
|
const {groups: allGroups} = useGroups({});
|
||||||
const groups = allGroups.filter((x) => x.admin === user.id);
|
const groups = allGroups.filter((x) => x.admin === user.id);
|
||||||
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
|
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
|
||||||
|
|
||||||
@@ -74,13 +78,13 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = (url: string) => {
|
const handleRouteChange = (url: string) => {
|
||||||
setTrainingStats([])
|
setTrainingStats([]);
|
||||||
}
|
};
|
||||||
router.events.on('routeChangeStart', handleRouteChange)
|
router.events.on("routeChangeStart", handleRouteChange);
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off('routeChangeStart', handleRouteChange)
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
}
|
};
|
||||||
}, [router.events, setTrainingStats])
|
}, [router.events, setTrainingStats]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const postStats = async () => {
|
const postStats = async () => {
|
||||||
@@ -93,19 +97,19 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isNewContentLoading) {
|
if (isNewContentLoading) {
|
||||||
postStats().then(id => {
|
postStats().then((id) => {
|
||||||
setTrainingStats([]);
|
setTrainingStats([]);
|
||||||
if (id) {
|
if (id) {
|
||||||
router.push(`/training/${id}`)
|
router.push(`/training/${id}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isNewContentLoading])
|
}, [isNewContentLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadTrainingContent = async () => {
|
const loadTrainingContent = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<ITrainingContent[]>('/api/training');
|
const response = await axios.get<ITrainingContent[]>("/api/training");
|
||||||
setTrainingContent(response.data);
|
setTrainingContent(response.data);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -118,9 +122,8 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
|
|
||||||
const handleNewTrainingContent = () => {
|
const handleNewTrainingContent = () => {
|
||||||
setRecordTraining(true);
|
setRecordTraining(true);
|
||||||
router.push('/record')
|
router.push("/record");
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const filterTrainingContentByDate = (trainingContent: {[key: string]: ITrainingContent}) => {
|
const filterTrainingContentByDate = (trainingContent: {[key: string]: ITrainingContent}) => {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
@@ -146,8 +149,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
|
|
||||||
setGroupedByTrainingContent(grouped);
|
setGroupedByTrainingContent(grouped);
|
||||||
}
|
}
|
||||||
}, [trainingContent])
|
}, [trainingContent]);
|
||||||
|
|
||||||
|
|
||||||
// Record Stuff
|
// Record Stuff
|
||||||
const selectableCorporates = [
|
const selectableCorporates = [
|
||||||
@@ -213,21 +215,20 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectTrainingContent = (trainingContent: ITrainingContent) => {
|
const selectTrainingContent = (trainingContent: ITrainingContent) => {
|
||||||
router.push(`/training/${trainingContent.id}`)
|
router.push(`/training/${trainingContent.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const trainingContentContainer = (timestamp: string) => {
|
const trainingContentContainer = (timestamp: string) => {
|
||||||
if (!groupedByTrainingContent) return <></>;
|
if (!groupedByTrainingContent) return <></>;
|
||||||
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
|
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
|
||||||
const uniqueModules = [...new Set(trainingContent.exams.map(exam => exam.module))];
|
const uniqueModules = [...new Set(trainingContent.exams.map((exam) => exam.module))];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
key={uuidv4()}
|
key={uuidv4()}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden"
|
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
|
||||||
)}
|
)}
|
||||||
onClick={() => selectTrainingContent(trainingContent)}
|
onClick={() => selectTrainingContent(trainingContent)}
|
||||||
role="button">
|
role="button">
|
||||||
@@ -243,10 +244,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TrainingScore
|
<TrainingScore trainingContent={trainingContent} gridView={true} />
|
||||||
trainingContent={trainingContent}
|
|
||||||
gridView={true}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -266,12 +264,12 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
{(isNewContentLoading || isLoading ? (
|
{isNewContentLoading || isLoading ? (
|
||||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
||||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||||
{isNewContentLoading && (<span className="text-center text-2xl font-bold text-mti-green-light">
|
{isNewContentLoading && (
|
||||||
Assessing your exams, please be patient...
|
<span className="text-center text-2xl font-bold text-mti-green-light">Assessing your exams, please be patient...</span>
|
||||||
</span>)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -337,7 +335,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(user.type === "student" && (
|
{user.type === "student" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="font-semibold text-2xl">Generate New Training Material</div>
|
<div className="font-semibold text-2xl">Generate New Training Material</div>
|
||||||
@@ -351,7 +349,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
||||||
<button
|
<button
|
||||||
@@ -396,10 +394,10 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
))}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Training;
|
export default Training;
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ export const USER_TYPE_LABELS: {[key in Type]: string} = {
|
|||||||
teacher: "Teacher",
|
teacher: "Teacher",
|
||||||
corporate: "Corporate",
|
corporate: "Corporate",
|
||||||
agent: "Country Manager",
|
agent: "Country Manager",
|
||||||
admin: "Admin",
|
admin: "Super Admin",
|
||||||
developer: "Developer",
|
developer: "Developer",
|
||||||
mastercorporate: "Master Corporate"
|
mastercorporate: "Master Corporate",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isCorporateUser(user: User): user is CorporateUser {
|
export function isCorporateUser(user: User): user is CorporateUser {
|
||||||
return (user as CorporateUser).corporateInformation !== undefined;
|
return (user as CorporateUser)?.corporateInformation !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAgentUser(user: User): user is AgentUser {
|
export function isAgentUser(user: User): user is AgentUser {
|
||||||
return (user as AgentUser).agentInformation !== undefined;
|
return (user as AgentUser)?.agentInformation !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserCompanyName(user: User, users: User[], groups: Group[]) {
|
export function getUserCompanyName(user: User, users: User[], groups: Group[]) {
|
||||||
@@ -30,3 +30,15 @@ export function getUserCompanyName(user: User, users: User[], groups: Group[]) {
|
|||||||
const admin = belongingGroupsAdmins[0] as CorporateUser;
|
const admin = belongingGroupsAdmins[0] as CorporateUser;
|
||||||
return admin.corporateInformation?.companyInformation.name || admin.name;
|
return admin.corporateInformation?.companyInformation.name || admin.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCorporateUser(user: User, users: User[], groups: Group[]) {
|
||||||
|
if (isCorporateUser(user)) return user;
|
||||||
|
|
||||||
|
const belongingGroups = groups.filter((x) => x.participants.includes(user.id));
|
||||||
|
const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x));
|
||||||
|
|
||||||
|
if (belongingGroupsAdmins.length === 0) return undefined;
|
||||||
|
|
||||||
|
const admin = belongingGroupsAdmins[0] as CorporateUser;
|
||||||
|
return admin;
|
||||||
|
}
|
||||||
|
|||||||
14
src/utils/assignments.be.ts
Normal file
14
src/utils/assignments.be.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {app} from "@/firebase";
|
||||||
|
import {Assignment} from "@/interfaces/results";
|
||||||
|
import {collection, getDocs, getFirestore, query, where} from "firebase/firestore";
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
|
|
||||||
|
export const getAssignmentsByAssigner = async (id: string) => {
|
||||||
|
const {docs} = await getDocs(query(collection(db, "assignments"), where("assigner", "==", id)));
|
||||||
|
return docs.map((x) => ({...x.data(), id: x.id})) as Assignment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAssignmentsByAssigners = async (ids: string[]) => {
|
||||||
|
return (await Promise.all(ids.map(getAssignmentsByAssigner))).flat();
|
||||||
|
};
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import {app} from "@/firebase";
|
import {app} from "@/firebase";
|
||||||
import { CorporateUser, StudentUser, TeacherUser } from "@/interfaces/user";
|
import {CorporateUser, Group, StudentUser, TeacherUser} from "@/interfaces/user";
|
||||||
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore";
|
import {collection, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import {getUser} from "./users.be";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
export const updateExpiryDateOnGroup = async (
|
export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => {
|
||||||
participantID: string,
|
|
||||||
corporateID: string,
|
|
||||||
) => {
|
|
||||||
const corporateRef = await getDoc(doc(db, "users", corporateID));
|
const corporateRef = await getDoc(doc(db, "users", corporateID));
|
||||||
const participantRef = await getDoc(doc(db, "users", participantID));
|
const participantRef = await getDoc(doc(db, "users", participantID));
|
||||||
|
|
||||||
@@ -18,36 +16,36 @@ export const updateExpiryDateOnGroup = async (
|
|||||||
...corporateRef.data(),
|
...corporateRef.data(),
|
||||||
id: corporateRef.id,
|
id: corporateRef.id,
|
||||||
} as CorporateUser;
|
} as CorporateUser;
|
||||||
const participant = { ...participantRef.data(), id: participantRef.id } as
|
const participant = {...participantRef.data(), id: participantRef.id} as StudentUser | TeacherUser;
|
||||||
| StudentUser
|
|
||||||
| TeacherUser;
|
|
||||||
|
|
||||||
if (
|
if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return;
|
||||||
corporate.type !== "corporate" ||
|
|
||||||
(participant.type !== "student" && participant.type !== "teacher")
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (
|
if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate) {
|
||||||
!corporate.subscriptionExpirationDate ||
|
return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: null}, {merge: true});
|
||||||
!participant.subscriptionExpirationDate
|
|
||||||
) {
|
|
||||||
return await setDoc(
|
|
||||||
doc(db, "users", participant.id),
|
|
||||||
{ subscriptionExpirationDate: null },
|
|
||||||
{ merge: true },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const corporateDate = moment(corporate.subscriptionExpirationDate);
|
const corporateDate = moment(corporate.subscriptionExpirationDate);
|
||||||
const participantDate = moment(participant.subscriptionExpirationDate);
|
const participantDate = moment(participant.subscriptionExpirationDate);
|
||||||
|
|
||||||
if (corporateDate.isAfter(participantDate))
|
if (corporateDate.isAfter(participantDate))
|
||||||
return await setDoc(
|
return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: corporateDate.toISOString()}, {merge: true});
|
||||||
doc(db, "users", participant.id),
|
|
||||||
{ subscriptionExpirationDate: corporateDate.toISOString() },
|
|
||||||
{ merge: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserGroups = async (id: string): Promise<Group[]> => {
|
||||||
|
const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id)));
|
||||||
|
return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllAssignersByCorporate = async (corporateID: string): Promise<string[]> => {
|
||||||
|
const groups = await getUserGroups(corporateID);
|
||||||
|
const groupUsers = (await Promise.all(groups.map(async (g) => await Promise.all(g.participants.map(getUser))))).flat();
|
||||||
|
const teacherPromises = await Promise.all(
|
||||||
|
groupUsers.map(async (u) =>
|
||||||
|
u.type === "teacher" ? u.id : u.type === "corporate" ? [...(await getAllAssignersByCorporate(u.id)), u.id] : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return teacherPromises.filter((x) => !!x).flat() as string[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ import {
|
|||||||
Permission,
|
Permission,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
permissions,
|
permissions,
|
||||||
|
PermissionTopic,
|
||||||
} from "@/interfaces/permissions";
|
} from "@/interfaces/permissions";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
async function createPermission(type: string) {
|
async function createPermission(topic: string, type: string) {
|
||||||
const permData = doc(db, "permissions", v4());
|
const permData = doc(db, "permissions", v4());
|
||||||
const permDoc = await getDoc(permData);
|
const permDoc = await getDoc(permData);
|
||||||
if (permDoc.exists()) {
|
if (permDoc.exists()) {
|
||||||
@@ -30,9 +31,14 @@ async function createPermission(type: string) {
|
|||||||
|
|
||||||
await setDoc(permData, {
|
await setDoc(permData, {
|
||||||
type,
|
type,
|
||||||
|
topic,
|
||||||
users: [],
|
users: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
interface PermissionsHelperList {
|
||||||
|
topic: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
export function getPermissions(userId: string | undefined, docs: Permission[]) {
|
export function getPermissions(userId: string | undefined, docs: Permission[]) {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return [];
|
return [];
|
||||||
@@ -52,8 +58,18 @@ export function getPermissions(userId: string | undefined, docs: Permission[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function bootstrap() {
|
export async function bootstrap() {
|
||||||
await permissions.forEach(async (type) => {
|
await permissions
|
||||||
await createPermission(type);
|
.reduce((accm: PermissionsHelperList[], permissionData) => {
|
||||||
|
return [
|
||||||
|
...accm,
|
||||||
|
...permissionData.list.map((type) => ({
|
||||||
|
topic: permissionData.topic,
|
||||||
|
type,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}, [])
|
||||||
|
.forEach(async ({ topic, type }) => {
|
||||||
|
await createPermission(topic, type);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
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,
|
|
||||||
types: Type[],
|
|
||||||
permission?: PermissionType
|
|
||||||
) {
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -29,7 +26,7 @@ export function checkAccess(
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,5 +38,5 @@ 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);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {LevelScore} from "@/constants/ielts";
|
import {LevelScore} from "@/constants/ielts";
|
||||||
|
import {Stat, User} from "@/interfaces/user";
|
||||||
|
|
||||||
type Type = "academic" | "general";
|
type Type = "academic" | "general";
|
||||||
|
|
||||||
@@ -178,3 +179,28 @@ export const getLevelLabel = (level: number) => {
|
|||||||
|
|
||||||
return ["Proficiency", "C2"];
|
return ["Proficiency", "C2"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const averageLevelCalculator = (users: User[], studentStats: Stat[]) => {
|
||||||
|
const formattedStats = studentStats
|
||||||
|
.map((s) => ({
|
||||||
|
focus: users.find((u) => u.id === s.user)?.focus,
|
||||||
|
score: s.score,
|
||||||
|
module: s.module,
|
||||||
|
}))
|
||||||
|
.filter((f) => !!f.focus);
|
||||||
|
const bandScores = formattedStats.map((s) => ({
|
||||||
|
module: s.module,
|
||||||
|
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const levels: {[key in Module]: number} = {
|
||||||
|
reading: 0,
|
||||||
|
listening: 0,
|
||||||
|
writing: 0,
|
||||||
|
speaking: 0,
|
||||||
|
level: 0,
|
||||||
|
};
|
||||||
|
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||||
|
|
||||||
|
return calculateAverageLevel(levels);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {app} from "@/firebase";
|
import {app} from "@/firebase";
|
||||||
|
|
||||||
import { collection, getDocs, getFirestore } from "firebase/firestore";
|
import {collection, doc, getDoc, getDocs, getFirestore} from "firebase/firestore";
|
||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -12,3 +12,9 @@ export async function getUsers() {
|
|||||||
...doc.data(),
|
...doc.data(),
|
||||||
})) as User[];
|
})) as User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUser(id: string) {
|
||||||
|
const userDoc = await getDoc(doc(db, "users", id));
|
||||||
|
|
||||||
|
return {...userDoc.data(), id} as User;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group
|
|||||||
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
|
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
|
||||||
country: user.demographicInformation?.country || "N/A",
|
country: user.demographicInformation?.country || "N/A",
|
||||||
phone: user.demographicInformation?.phone || "N/A",
|
phone: user.demographicInformation?.phone || "N/A",
|
||||||
employmentPosition: (user.type === "corporate" || user.type === "mastercorporate" ? user.demographicInformation?.position : user.demographicInformation?.employment) || "N/A",
|
employmentPosition:
|
||||||
|
(user.type === "corporate" || user.type === "mastercorporate"
|
||||||
|
? user.demographicInformation?.position
|
||||||
|
: user.demographicInformation?.employment) || "N/A",
|
||||||
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
|
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
|
||||||
verified: user.isVerified?.toString() || "FALSE",
|
verified: user.isVerified?.toString() || "FALSE",
|
||||||
}));
|
}));
|
||||||
@@ -34,3 +37,9 @@ export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group
|
|||||||
|
|
||||||
return `${header}\n${rowsString}`;
|
return `${header}\n${rowsString}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserName = (user?: User) => {
|
||||||
|
if (!user) return "N/A";
|
||||||
|
if (user.type === "corporate" || user.type === "mastercorporate") return user.corporateInformation?.companyInformation?.name || user.name;
|
||||||
|
return user.name;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user