Merge branch 'develop' of https://bitbucket.org/ecropdev/ielts-ui into feature/level-file-upload

This commit is contained in:
Carlos Mesquita
2024-08-19 16:53:39 +01:00
58 changed files with 7815 additions and 8224 deletions

View File

@@ -1,22 +1,16 @@
import useUsers from "@/hooks/useUsers";
import {
Ticket,
TicketStatus,
TicketStatusLabel,
TicketType,
TicketTypeLabel,
} from "@/interfaces/ticket";
import { User } from "@/interfaces/user";
import { USER_TYPE_LABELS } from "@/resources/user";
import {Ticket, TicketStatus, TicketStatusLabel, TicketType, TicketTypeLabel} from "@/interfaces/ticket";
import {User} from "@/interfaces/user";
import {USER_TYPE_LABELS} from "@/resources/user";
import axios from "axios";
import moment from "moment";
import { useState } from "react";
import { toast } from "react-toastify";
import {useState} from "react";
import {toast} from "react-toastify";
import ShortUniqueId from "short-unique-id";
import Button from "../Low/Button";
import Input from "../Low/Input";
import Select from "../Low/Select";
import { checkAccess } from "@/utils/permissions";
import {checkAccess} from "@/utils/permissions";
interface Props {
user: User;
@@ -24,23 +18,20 @@ interface Props {
onClose: () => void;
}
export default function TicketDisplay({ user, ticket, onClose }: Props) {
export default function TicketDisplay({user, ticket, onClose}: Props) {
const [subject] = useState(ticket.subject);
const [type, setType] = useState<TicketType>(ticket.type);
const [description] = useState(ticket.description);
const [reporter] = useState(ticket.reporter);
const [reportedFrom] = useState(ticket.reportedFrom);
const [status, setStatus] = useState(ticket.status);
const [assignedTo, setAssignedTo] = useState<string | null>(
ticket.assignedTo || null,
);
const [assignedTo, setAssignedTo] = useState<string | null>(ticket.assignedTo || null);
const [isLoading, setIsLoading] = useState(false);
const { users } = useUsers();
const {users} = useUsers();
const submit = () => {
if (!type)
return toast.error("Please choose a type!", { toastId: "missing-type" });
if (!type) return toast.error("Please choose a type!", {toastId: "missing-type"});
setIsLoading(true);
axios
@@ -54,7 +45,7 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
assignedTo,
})
.then(() => {
toast.success(`The ticket has been updated!`, { toastId: "submitted" });
toast.success(`The ticket has been updated!`, {toastId: "submitted"});
onClose();
})
.catch((e) => {
@@ -73,7 +64,7 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
axios
.delete(`/api/tickets/${ticket.id}`)
.then(() => {
toast.success(`The ticket has been deleted!`, { toastId: "submitted" });
toast.success(`The ticket has been deleted!`, {toastId: "submitted"});
onClose();
})
.catch((e) => {
@@ -87,43 +78,29 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
return (
<form className="flex flex-col gap-4 pt-8">
<Input
label="Subject"
type="text"
name="subject"
placeholder="Subject..."
value={subject}
onChange={(e) => null}
disabled
/>
<Input 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="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Status
</label>
<label className="text-mti-gray-dim text-base font-normal">Status</label>
<Select
options={Object.keys(TicketStatusLabel).map((x) => ({
value: x,
label: TicketStatusLabel[x as keyof typeof TicketStatusLabel],
}))}
value={{ value: status, label: TicketStatusLabel[status] }}
onChange={(value) =>
setStatus((value?.value as TicketStatus) ?? undefined)
}
value={{value: status, label: TicketStatusLabel[status]}}
onChange={(value) => setStatus((value?.value as TicketStatus) ?? undefined)}
placeholder="Status..."
/>
</div>
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Type
</label>
<label className="text-mti-gray-dim text-base font-normal">Type</label>
<Select
options={Object.keys(TicketTypeLabel).map((x) => ({
value: x,
label: TicketTypeLabel[x as keyof typeof TicketTypeLabel],
}))}
value={{ value: type, label: TicketTypeLabel[type] }}
value={{value: type, label: TicketTypeLabel[type]}}
onChange={(value) => setType(value!.value as TicketType)}
placeholder="Type..."
/>
@@ -131,12 +108,10 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
</div>
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">
Assignee
</label>
<label className="text-mti-gray-dim text-base font-normal">Assignee</label>
<Select
options={[
{ value: "me", label: "Assign to me" },
{value: "me", label: "Assign to me"},
...users
.filter((x) => checkAccess(x, ["admin", "developer", "agent"]))
.map((u) => ({
@@ -153,52 +128,20 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
}
: null
}
onChange={(value) =>
value
? setAssignedTo(value.value === "me" ? user.id : value.value)
: setAssignedTo(null)
}
onChange={(value) => (value ? setAssignedTo(value.value === "me" ? user.id : value.value) : setAssignedTo(null))}
placeholder="Assignee..."
isClearable
/>
</div>
<div className="-md:flex-col flex w-full items-center gap-4">
<Input
label="Reported From"
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
/>
<Input label="Reported From" 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 className="-md:flex-col flex w-full items-center gap-4">
<Input
label="Reporter's Name"
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 label="Reporter's Name" 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
label="Reporter's Type"
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">
<Button
type="button"
color="red"
className="w-full md:max-w-[200px]"
variant="outline"
onClick={del}
isLoading={isLoading}
>
<Button type="button" color="red" className="w-full md:max-w-[200px]" variant="outline" onClick={del} isLoading={isLoading}>
Delete
</Button>
<div className="-md:flex-col-reverse flex w-full items-center justify-end gap-4">
<Button
type="button"
color="red"
className="w-full md:max-w-[200px]"
variant="outline"
onClick={onClose}
isLoading={isLoading}
>
<Button type="button" color="red" className="w-full md:max-w-[200px]" variant="outline" onClick={onClose} isLoading={isLoading}>
Cancel
</Button>
<Button
type="button"
className="w-full md:max-w-[200px]"
isLoading={isLoading}
onClick={submit}
>
<Button type="button" className="w-full md:max-w-[200px]" isLoading={isLoading} onClick={submit}>
Update
</Button>
</div>

36
src/components/List.tsx Normal file
View 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>
);
}

View File

@@ -1,7 +1,14 @@
import {Permission} from "@/interfaces/permissions";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import React from "react";
import { Permission } from "@/interfaces/permissions";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
Row,
} from "@tanstack/react-table";
import Link from "next/link";
import {convertCamelCaseToReadable} from "@/utils/string";
import { convertCamelCaseToReadable } from "@/utils/string";
interface Props {
permissions: Permission[];
@@ -12,23 +19,36 @@ const columnHelper = createColumnHelper<Permission>();
const defaultColumns = [
columnHelper.accessor("type", {
header: () => <span>Type</span>,
cell: ({row, getValue}) => (
cell: ({ row, getValue }) => (
<Link
href={`/permissions/${row.original.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)}
</Link>
),
}),
];
export default function PermissionList({permissions}: Props) {
export default function PermissionList({ permissions }: Props) {
const table = useReactTable({
data: permissions,
columns: defaultColumns,
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 (
<div className="w-full">
<div className="w-full flex flex-col gap-2">
@@ -38,22 +58,45 @@ export default function PermissionList({permissions}: Props) {
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<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>
))}
</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}>
{Object.keys(groupedData).map((parent) => (
<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) => (
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
<td
className="px-4 py-2 items-center w-fit"
key={cell.id}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>

View File

@@ -13,8 +13,9 @@ import {
BsCurrencyDollar,
BsClipboardData,
BsFileLock,
BsPeople,
} from "react-icons/bs";
import { CiDumbbell } from "react-icons/ci";
import {CiDumbbell} from "react-icons/ci";
import {RiLogoutBoxFill} from "react-icons/ri";
import {SlPencil} from "react-icons/sl";
import {FaAward} from "react-icons/fa";
@@ -28,6 +29,7 @@ import usePreferencesStore from "@/stores/preferencesStore";
import {User} from "@/interfaces/user";
import useTicketsListener from "@/hooks/useTicketsListener";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions";
interface Props {
path: string;
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 {totalAssignedTickets} = useTicketsListener(user.id);
const {permissions} = usePermissions(user.id);
const logout = async () => {
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">
<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} />
)}
{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} />
)}
{checkAccess(user, getTypesOfUser(["agent"]), "viewStats") && (
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
<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} />
)}
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
<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
disabled={disableNavigation}
Icon={BsCurrencyDollar}
@@ -133,7 +139,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
isMinimized={isMinimized}
/>
)}
{checkAccess(user, ["admin", "developer", "agent"], "viewTickets") && (
{checkAccess(user, ["admin", "developer", "agent"], permissions, "viewTickets") && (
<Nav
disabled={disableNavigation}
Icon={BsClipboardData}
@@ -144,8 +150,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
badge={totalAssignedTickets}
/>
)}
{checkAccess(user, ["developer", "admin"]) && (
<>
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
<Nav
disabled={disableNavigation}
Icon={BsCloudFill}
@@ -154,6 +159,8 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
keyPath="/generation"
isMinimized={isMinimized}
/>
)}
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate", "agent"]) && (
<Nav
disabled={disableNavigation}
Icon={BsFileLock}
@@ -162,20 +169,19 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
keyPath="/permissions"
isMinimized={isMinimized}
/>
</>
)}
</div>
<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={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" 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} />
)}
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
<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} />
)}
{checkAccess(user, getTypesOfUser(["student"])) && (

View File

@@ -1,27 +1,15 @@
import useStats from "@/hooks/useStats";
import {
CorporateInformation,
CorporateUser,
EMPLOYMENT_STATUS,
User,
Type,
} from "@/interfaces/user";
import { groupBySession, averageScore } from "@/utils/stats";
import { RadioGroup } from "@headlessui/react";
import {CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type} from "@/interfaces/user";
import {groupBySession, averageScore} from "@/utils/stats";
import {RadioGroup} from "@headlessui/react";
import axios from "axios";
import clsx from "clsx";
import moment from "moment";
import { Divider } from "primereact/divider";
import { useEffect, useState } from "react";
import {Divider} from "primereact/divider";
import {useEffect, useState} from "react";
import ReactDatePicker from "react-datepicker";
import {
BsFileEarmarkText,
BsPencil,
BsPerson,
BsPersonAdd,
BsStar,
} from "react-icons/bs";
import { toast } from "react-toastify";
import {BsFileEarmarkText, BsPencil, BsPerson, BsPersonAdd, BsStar} from "react-icons/bs";
import {toast} from "react-toastify";
import Button from "./Low/Button";
import Checkbox from "./Low/Checkbox";
import CountrySelect from "./Low/CountrySelect";
@@ -29,23 +17,21 @@ import Input from "./Low/Input";
import ProfileSummary from "./ProfileSummary";
import Select from "react-select";
import useUsers from "@/hooks/useUsers";
import { USER_TYPE_LABELS } from "@/resources/user";
import { CURRENCIES } from "@/resources/paypal";
import {USER_TYPE_LABELS} from "@/resources/user";
import {CURRENCIES} from "@/resources/paypal";
import useCodes from "@/hooks/useCodes";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import { PERMISSIONS } from "@/constants/userPermissions";
import { PermissionType } from "@/interfaces/permissions";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import {PERMISSIONS} from "@/constants/userPermissions";
import {PermissionType} from "@/interfaces/permissions";
import usePermissions from "@/hooks/usePermissions";
const expirationDateColor = (date: Date) => {
const momentDate = moment(date);
const today = moment(new Date());
if (today.add(1, "days").isAfter(momentDate))
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(7, "days").isAfter(momentDate))
return "!bg-mti-orange-ultralight border-mti-orange-light";
if (today.add(1, "days").isAfter(momentDate)) 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(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
};
interface Props {
@@ -81,86 +67,40 @@ const USER_TYPE_OPTIONS = Object.keys(USER_TYPE_LABELS).map((type) => ({
label: USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS],
}));
const CURRENCIES_OPTIONS = CURRENCIES.map(({ label, currency }) => ({
const CURRENCIES_OPTIONS = CURRENCIES.map(({label, currency}) => ({
value: currency,
label,
}));
const UserCard = ({
user,
loggedInUser,
onClose,
onViewStudents,
onViewTeachers,
onViewCorporate,
disabled = false,
disabledFields = {},
}: Props) => {
const [expiryDate, setExpiryDate] = useState<Date | null | undefined>(
user.subscriptionExpirationDate
);
const UserCard = ({user, 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 [status, setStatus] = useState(user.status);
const [referralAgentLabel, setReferralAgentLabel] = useState<string>();
const [position, setPosition] = useState<string | undefined>(
user.type === "corporate"
? user.demographicInformation?.position
: undefined
);
const [passport_id, setPassportID] = useState<string | undefined>(
user.type === "student"
? user.demographicInformation?.passport_id
: undefined
);
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
const [passport_id, setPassportID] = useState<string | undefined>(user.type === "student" ? user.demographicInformation?.passport_id : undefined);
const [referralAgent, setReferralAgent] = useState(
user.type === "corporate"
? user.corporateInformation?.referralAgent
: undefined
);
const [referralAgent, setReferralAgent] = useState(user.type === "corporate" ? user.corporateInformation?.referralAgent : undefined);
const [companyName, setCompanyName] = useState(
user.type === "corporate"
? user.corporateInformation?.companyInformation.name
: user.type === "agent"
? user.agentInformation?.companyName
: undefined
);
const [arabName, setArabName] = useState(
user.type === "agent" ? user.agentInformation?.companyArabName : undefined
: undefined,
);
const [arabName, setArabName] = useState(user.type === "agent" ? user.agentInformation?.companyArabName : undefined);
const [commercialRegistration, setCommercialRegistration] = useState(
user.type === "agent"
? user.agentInformation?.commercialRegistration
: undefined
user.type === "agent" ? 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 { stats } = useStats(user.id);
const { users } = useUsers();
const { codes } = useCodes(user.id);
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 {users} = useUsers();
const {codes} = useCodes(user.id);
const {permissions} = usePermissions(loggedInUser.id);
useEffect(() => {
if (users && users.length > 0) {
@@ -176,14 +116,11 @@ const UserCard = ({
const updateUser = () => {
if (user.type === "corporate" && (!paymentValue || paymentValue < 0))
return toast.error(
"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;
return toast.error("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;
axios
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
...user,
subscriptionExpirationDate: expiryDate,
type,
@@ -208,9 +145,7 @@ const UserCard = ({
payment: {
value: paymentValue,
currency: paymentCurrency,
...(referralAgent === ""
? {}
: { commission: commissionValue }),
...(referralAgent === "" ? {} : {commission: commissionValue}),
},
}
: undefined,
@@ -220,15 +155,13 @@ const UserCard = ({
onClose(true);
})
.catch(() => {
toast.error("Something went wrong!", { toastId: "update-error" });
toast.error("Something went wrong!", {toastId: "update-error"});
});
};
const generalProfileItems = [
{
icon: (
<BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
),
icon: <BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: Object.keys(groupBySession(stats)).length,
label: "Exams",
},
@@ -248,16 +181,12 @@ const UserCard = ({
user.type === "corporate"
? [
{
icon: (
<BsPerson className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
),
icon: <BsPerson className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: codes.length,
label: "Users Used",
},
{
icon: (
<BsPersonAdd className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
),
icon: <BsPersonAdd className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: user.corporateInformation.companyInformation.userAmount,
label: "Number of Users",
},
@@ -270,14 +199,7 @@ const UserCard = ({
};
return (
<>
<ProfileSummary
user={user}
items={
user.type === "corporate"
? corporateProfileItems
: generalProfileItems
}
/>
<ProfileSummary user={user} items={user.type === "corporate" ? corporateProfileItems : generalProfileItems} />
{user.type === "agent" && (
<>
@@ -347,9 +269,7 @@ const UserCard = ({
disabled={disabled}
/>
<div className="flex flex-col gap-3 w-full lg:col-span-3">
<label className="font-normal text-base text-mti-gray-dim">
Pricing
</label>
<label className="font-normal text-base text-mti-gray-dim">Pricing</label>
<div className="w-full grid grid-cols-6 gap-2">
<Input
name="paymentValue"
@@ -362,17 +282,14 @@ const UserCard = ({
<Select
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",
disabled &&
"!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed"
disabled && "!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
)}
options={CURRENCIES_OPTIONS}
value={CURRENCIES_OPTIONS.find(
(c) => c.value === paymentCurrency
)}
value={CURRENCIES_OPTIONS.find((c) => c.value === paymentCurrency)}
onChange={(value) => setPaymentCurrency(value?.value)}
menuPortalTarget={document?.body}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -384,11 +301,7 @@ const UserCard = ({
}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
color: state.isFocused ? "black" : styles.color,
}),
}}
@@ -399,22 +312,16 @@ const UserCard = ({
</div>
<div className="flex gap-3 w-full">
<div className="flex flex-col gap-3 w-8/12">
<label className="font-normal text-base text-mti-gray-dim">
Country Manager
</label>
<label className="font-normal text-base text-mti-gray-dim">Country Manager</label>
{referralAgentLabel && (
<Select
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",
(checkAccess(
loggedInUser,
getTypesOfUser(["developer", "admin"])
) ||
disabledFields.countryManager) &&
"!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed"
(checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"])) || disabledFields.countryManager) &&
"!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
)}
options={[
{ value: "", label: "No referral" },
{value: "", label: "No referral"},
...users
.filter((u) => u.type === "agent")
.map((x) => ({
@@ -429,7 +336,7 @@ const UserCard = ({
menuPortalTarget={document?.body}
onChange={(value) => setReferralAgent(value?.value)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
control: (styles) => ({
...styles,
paddingLeft: "4px",
@@ -441,30 +348,19 @@ const UserCard = ({
}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
color: state.isFocused ? "black" : styles.color,
}),
}}
// editing country manager should only be available for dev/admin
isDisabled={
checkAccess(
loggedInUser,
getTypesOfUser(["developer", "admin"])
) || disabledFields.countryManager
}
isDisabled={checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"])) || disabledFields.countryManager}
/>
)}
</div>
<div className="flex flex-col gap-3 w-4/12">
{referralAgent !== "" && loggedInUser.type !== "corporate" ? (
<>
<label className="font-normal text-base text-mti-gray-dim">
Commission
</label>
<label className="font-normal text-base text-mti-gray-dim">Commission</label>
<Input
name="commissionValue"
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 gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Country
</label>
<CountrySelect
disabled
value={user.demographicInformation?.country}
/>
<label className="font-normal text-base text-mti-gray-dim">Country</label>
<CountrySelect disabled value={user.demographicInformation?.country} />
</div>
<Input
type="tel"
@@ -532,11 +423,7 @@ const UserCard = ({
label="Passport/National ID"
onChange={() => null}
placeholder="Enter National ID or Passport number"
value={
user.type === "student"
? user.demographicInformation?.passport_id
: undefined
}
value={user.type === "student" ? user.demographicInformation?.passport_id : undefined}
disabled
required
/>
@@ -545,26 +432,22 @@ const UserCard = ({
<div className="flex flex-col md:flex-row gap-8 w-full">
{user.type !== "corporate" && user.type !== "mastercorporate" && (
<div className="relative flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Employment Status
</label>
<label className="font-normal text-base text-mti-gray-dim">Employment Status</label>
<RadioGroup
value={user.demographicInformation?.employment}
className="grid grid-cols-2 items-center gap-4 place-items-center"
disabled={disabled}
>
{EMPLOYMENT_STATUS.map(({ status, label }) => (
disabled={disabled}>
{EMPLOYMENT_STATUS.map(({status, label}) => (
<RadioGroup.Option value={status} key={status}>
{({ checked }) => (
{({checked}) => (
<span
className={clsx(
"px-6 py-4 w-40 md:w-48 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
!checked
? "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}
</span>
)}
@@ -587,55 +470,49 @@ const UserCard = ({
)}
<div className="flex flex-col gap-8 w-full">
<div className="relative flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Gender
</label>
<label className="font-normal text-base text-mti-gray-dim">Gender</label>
<RadioGroup
value={user.demographicInformation?.gender}
className="flex flex-row gap-4 justify-between"
disabled={disabled}
>
disabled={disabled}>
<RadioGroup.Option value="male">
{({ checked }) => (
{({checked}) => (
<span
className={clsx(
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
!checked
? "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
</span>
)}
</RadioGroup.Option>
<RadioGroup.Option value="female">
{({ checked }) => (
{({checked}) => (
<span
className={clsx(
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
!checked
? "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
</span>
)}
</RadioGroup.Option>
<RadioGroup.Option value="other">
{({ checked }) => (
{({checked}) => (
<span
className={clsx(
"px-6 py-4 w-28 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
!checked
? "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
</span>
)}
@@ -644,20 +521,13 @@ const UserCard = ({
</div>
<div className="flex flex-col gap-3">
<div className="flex justify-between items-center">
<label className="font-normal text-base text-mti-gray-dim">
Expiry Date
</label>
<label className="font-normal text-base text-mti-gray-dim">Expiry Date</label>
<Checkbox
isChecked={!!expiryDate}
onChange={(checked) =>
setExpiryDate(
checked
? user.subscriptionExpirationDate || new Date()
: null
)
}
disabled={disabled}
>
onChange={(checked) => setExpiryDate(checked ? user.subscriptionExpirationDate || new Date() : null)}
disabled={
disabled || (!["admin", "developer"].includes(loggedInUser.type) && !!loggedInUser.subscriptionExpirationDate)
}>
Enabled
</Checkbox>
</div>
@@ -666,12 +536,9 @@ const UserCard = ({
className={clsx(
"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",
!expiryDate
? "!bg-mti-green-ultralight !border-mti-green-light"
: expirationDateColor(expiryDate),
"bg-white border-mti-gray-platinum"
)}
>
!expiryDate ? "!bg-mti-green-ultralight !border-mti-green-light" : expirationDateColor(expiryDate),
"bg-white border-mti-gray-platinum",
)}>
{!expiryDate && "Unlimited"}
{expiryDate && moment(expiryDate).format("DD/MM/YYYY")}
</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",
"hover:border-mti-purple tooltip",
expirationDateColor(expiryDate),
"transition duration-300 ease-in-out"
"transition duration-300 ease-in-out",
)}
filterDate={(date) =>
moment(date).isAfter(new Date()) &&
(loggedInUser.subscriptionExpirationDate
? moment(date).isBefore(
moment(loggedInUser.subscriptionExpirationDate)
)
? moment(date).isBefore(moment(loggedInUser.subscriptionExpirationDate))
: true)
}
dateFormat="dd/MM/yyyy"
@@ -701,22 +566,26 @@ const UserCard = ({
</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" />
<div className="flex flex-col md:flex-row gap-8 w-full">
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Status
</label>
<label className="font-normal text-base text-mti-gray-dim">Status</label>
<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"
options={USER_STATUS_OPTIONS}
options={USER_STATUS_OPTIONS.filter((x) => {
if (checkAccess(loggedInUser, ["admin", "developer"])) return true;
return x.value !== "paymentDue";
})}
menuPortalTarget={document?.body}
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
onChange={(value) =>
setStatus(value?.value as typeof user.status)
}
onChange={(value) => setStatus(value?.value as typeof user.status)}
styles={{
control: (styles) => ({
...styles,
@@ -727,14 +596,10 @@ const UserCard = ({
outline: "none",
},
}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
color: state.isFocused ? "black" : styles.color,
}),
}}
@@ -742,17 +607,34 @@ const UserCard = ({
/>
</div>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Type
</label>
<label className="font-normal text-base text-mti-gray-dim">Type</label>
<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"
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}
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
onChange={(value) =>
setType(value?.value as typeof user.type)
}
onChange={(value) => setType(value?.value as typeof user.type)}
styles={{
control: (styles) => ({
...styles,
@@ -763,14 +645,10 @@ const UserCard = ({
outline: "none",
},
}),
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
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="self-start flex gap-4 justify-start items-center w-full">
{onViewCorporate && ["student", "teacher"].includes(user.type) && (
<Button
className="w-full max-w-[200px]"
variant="outline"
color="rose"
onClick={onViewCorporate}
>
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewCorporate}>
View Corporate
</Button>
)}
{onViewStudents && ["corporate", "teacher"].includes(user.type) && (
<Button
className="w-full max-w-[200px]"
variant="outline"
color="rose"
onClick={onViewStudents}
>
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewStudents}>
View Students
</Button>
)}
{onViewTeachers && ["student", "corporate"].includes(user.type) && (
<Button
className="w-full max-w-[200px]"
variant="outline"
color="rose"
onClick={onViewTeachers}
>
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewTeachers}>
View Teachers
</Button>
)}
</div>
<div className="self-end flex gap-4 w-full justify-end">
<Button
className="w-full max-w-[200px]"
variant="outline"
onClick={onClose}
>
<Button className="w-full max-w-[200px]" variant="outline" onClick={onClose}>
Close
</Button>
<Button
disabled={
disabled ||
!checkAccess(
loggedInUser,
updateUserPermission.list,
updateUserPermission.perm
)
}
disabled={disabled || !checkAccess(loggedInUser, updateUserPermission.list, permissions, updateUserPermission.perm)}
onClick={updateUser}
className="w-full max-w-[200px]"
>
className="w-full max-w-[200px]">
Update
</Button>
</div>

View File

@@ -2,11 +2,11 @@
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers";
import { User } from "@/interfaces/user";
import {User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import { dateSorter } from "@/utils";
import {dateSorter} from "@/utils";
import moment from "moment";
import { useEffect, useState } from "react";
import {useEffect, useState} from "react";
import {
BsArrowLeft,
BsBriefcaseFill,
@@ -23,7 +23,7 @@ import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import IconCard from "./IconCard";
import useFilterStore from "@/stores/listFilterStore";
import { useRouter } from "next/router";
import {useRouter} from "next/router";
import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers";
import CorporateStudentsLevels from "./CorporateStudentsLevels";
@@ -31,15 +31,15 @@ interface Props {
user: User;
}
export default function AdminDashboard({ user }: Props) {
export default function AdminDashboard({user}: Props) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const { stats } = useStats(user.id);
const { users, reload } = useUsers();
const { groups } = useGroups();
const { pending, done } = usePaymentStatusUsers();
const {stats} = useStats(user.id);
const {users, reload} = useUsers();
const {groups} = useGroups({});
const {pending, done} = usePaymentStatusUsers();
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter();
@@ -52,24 +52,17 @@ export default function AdminDashboard({ user }: Props) {
useEffect(reload, [page]);
const inactiveCountryManagerFilter = (x: User) =>
x.type === "agent" &&
(x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
x.type === "agent" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
const UserDisplay = (displayUser: User) => (
<div
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"
>
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>
{displayUser.type === "corporate"
? displayUser.corporateInformation?.companyInformation?.name ||
displayUser.name
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
: displayUser.name}
</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
@@ -82,11 +75,7 @@ export default function AdminDashboard({ user }: Props) {
x.type === "student" &&
(!!selectedUser
? groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id)
: true);
@@ -99,8 +88,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -116,11 +104,7 @@ export default function AdminDashboard({ user }: Props) {
x.type === "teacher" &&
(!!selectedUser
? groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id) || false
: true);
@@ -133,8 +117,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -156,14 +139,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Country Managers ({total})
</h2>
<h2 className="text-2xl font-semibold">Country Managers ({total})</h2>
</div>
)}
/>
@@ -178,8 +158,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -189,7 +168,7 @@ export default function AdminDashboard({ user }: Props) {
/>
);
const CorporatePaidStatusList = ({ paid }: { paid: Boolean }) => {
const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => {
const list = paid ? done : pending;
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
@@ -201,8 +180,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -224,14 +202,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Inactive Country Managers ({total})
</h2>
<h2 className="text-2xl font-semibold">Inactive Country Managers ({total})</h2>
</div>
)}
/>
@@ -239,10 +214,7 @@ export default function AdminDashboard({ user }: Props) {
};
const InactiveStudentsList = () => {
const filter = (x: User) =>
x.type === "student" &&
(x.status === "disabled" ||
moment().isAfter(x.subscriptionExpirationDate));
const filter = (x: User) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
return (
<UserList
@@ -252,14 +224,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Inactive Students ({total})
</h2>
<h2 className="text-2xl font-semibold">Inactive Students ({total})</h2>
</div>
)}
/>
@@ -267,10 +236,7 @@ export default function AdminDashboard({ user }: Props) {
};
const InactiveCorporateList = () => {
const filter = (x: User) =>
x.type === "corporate" &&
(x.status === "disabled" ||
moment().isAfter(x.subscriptionExpirationDate));
const filter = (x: User) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
return (
<UserList
@@ -280,14 +246,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Inactive Corporate ({total})
</h2>
<h2 className="text-2xl font-semibold">Inactive Corporate ({total})</h2>
</div>
)}
/>
@@ -300,14 +263,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Corporate Students Levels
</h2>
<h2 className="text-2xl font-semibold">Corporate Students Levels</h2>
</div>
<CorporateStudentsLevels />
</>
@@ -348,15 +308,7 @@ export default function AdminDashboard({ user }: Props) {
<IconCard
Icon={BsGlobeCentralSouthAsia}
label="Countries"
value={
[
...new Set(
users
.filter((x) => x.demographicInformation)
.map((x) => x.demographicInformation?.country)
),
].length
}
value={[...new Set(users.filter((x) => x.demographicInformation).map((x) => x.demographicInformation?.country))].length}
color="purple"
/>
<IconCard
@@ -364,12 +316,8 @@ export default function AdminDashboard({ user }: Props) {
Icon={BsPersonFill}
label="Inactive Students"
value={
users.filter(
(x) =>
x.type === "student" &&
(x.status === "disabled" ||
moment().isAfter(x.subscriptionExpirationDate))
).length
users.filter((x) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))
.length
}
color="rose"
/>
@@ -385,22 +333,12 @@ export default function AdminDashboard({ user }: Props) {
Icon={BsBank}
label="Inactive Corporate"
value={
users.filter(
(x) =>
x.type === "corporate" &&
(x.status === "disabled" ||
moment().isAfter(x.subscriptionExpirationDate))
).length
users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))
.length
}
color="rose"
/>
<IconCard
onClick={() => setPage("paymentdone")}
Icon={BsCurrencyDollar}
label="Payment Done"
value={done.length}
color="purple"
/>
<IconCard onClick={() => setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" />
<IconCard
onClick={() => setPage("paymentpending")}
Icon={BsCurrencyDollar}
@@ -414,12 +352,7 @@ export default function AdminDashboard({ user }: Props) {
label="Content Management System (CMS)"
color="green"
/>
<IconCard
onClick={() => setPage("corporatestudentslevels")}
Icon={BsPersonFill}
label="Corporate Students Levels"
color="purple"
/>
<IconCard onClick={() => setPage("corporatestudentslevels")} Icon={BsPersonFill} label="Corporate Students Levels" color="purple" />
</section>
<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>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(
(x) => x.type === "corporate" && x.status === "paymentDue"
)
.filter((x) => x.type === "corporate" && x.status === "paymentDue")
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
@@ -480,10 +411,8 @@ export default function AdminDashboard({ user }: Props) {
(x) =>
x.type === "student" &&
x.subscriptionExpirationDate &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -498,10 +427,8 @@ export default function AdminDashboard({ user }: Props) {
(x) =>
x.type === "teacher" &&
x.subscriptionExpirationDate &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -516,10 +443,8 @@ export default function AdminDashboard({ user }: Props) {
(x) =>
x.type === "agent" &&
x.subscriptionExpirationDate &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -534,10 +459,8 @@ export default function AdminDashboard({ user }: Props) {
(x) =>
x.type === "corporate" &&
x.subscriptionExpirationDate &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((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">
{users
.filter(
(x) =>
x.type === "student" &&
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate))
(x) => x.type === "student" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
)
.map((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">
{users
.filter(
(x) =>
x.type === "teacher" &&
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate))
(x) => x.type === "teacher" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
)
.map((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">
{users
.filter(
(x) =>
x.type === "agent" &&
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate))
(x) => x.type === "agent" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -595,9 +509,7 @@ export default function AdminDashboard({ user }: Props) {
{users
.filter(
(x) =>
x.type === "corporate" &&
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate))
x.type === "corporate" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -621,8 +533,7 @@ export default function AdminDashboard({ user }: Props) {
if (shouldReload) reload();
}}
onViewStudents={
selectedUser.type === "corporate" ||
selectedUser.type === "teacher"
selectedUser.type === "corporate" || selectedUser.type === "teacher"
? () => {
appendUserFilters({
id: "view-students",
@@ -632,11 +543,7 @@ export default function AdminDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
@@ -646,8 +553,7 @@ export default function AdminDashboard({ user }: Props) {
: undefined
}
onViewTeachers={
selectedUser.type === "corporate" ||
selectedUser.type === "student"
selectedUser.type === "corporate" || selectedUser.type === "student"
? () => {
appendUserFilters({
id: "view-teachers",
@@ -657,11 +563,7 @@ export default function AdminDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
@@ -671,8 +573,7 @@ export default function AdminDashboard({ user }: Props) {
: undefined
}
onViewCorporate={
selectedUser.type === "teacher" ||
selectedUser.type === "student"
selectedUser.type === "teacher" || selectedUser.type === "student"
? () => {
appendUserFilters({
id: "view-corporate",
@@ -682,9 +583,7 @@ export default function AdminDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter((g) =>
g.participants.includes(selectedUser.id)
)
.filter((g) => g.participants.includes(selectedUser.id))
.flatMap((g) => [g.admin, ...g.participants])
.includes(x.id),
});

View File

@@ -2,17 +2,12 @@
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers";
import { User } from "@/interfaces/user";
import {User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import { dateSorter } from "@/utils";
import {dateSorter} from "@/utils";
import moment from "moment";
import { useEffect, useState } from "react";
import {
BsArrowLeft,
BsPersonFill,
BsBank,
BsCurrencyDollar,
} from "react-icons/bs";
import {useEffect, useState} from "react";
import {BsArrowLeft, BsPersonFill, BsBank, BsCurrencyDollar} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
@@ -23,15 +18,14 @@ interface Props {
user: User;
}
export default function AgentDashboard({ user }: Props) {
export default function AgentDashboard({user}: Props) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const { stats } = useStats();
const { users, reload } = useUsers();
const { groups } = useGroups(user.id);
const { pending, done } = usePaymentStatusUsers();
const {stats} = useStats();
const {users, reload} = useUsers();
const {pending, done} = usePaymentStatusUsers();
useEffect(() => {
setShowModal(!!selectedUser && page === "");
@@ -39,34 +33,19 @@ export default function AgentDashboard({ user }: Props) {
const corporateFilter = (user: User) => user.type === "corporate";
const referredCorporateFilter = (x: User) =>
x.type === "corporate" &&
!!x.corporateInformation &&
x.corporateInformation.referralAgent === user.id;
x.type === "corporate" && !!x.corporateInformation && x.corporateInformation.referralAgent === user.id;
const inactiveReferredCorporateFilter = (x: User) =>
referredCorporateFilter(x) &&
(x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
const UserDisplay = ({
displayUser,
allowClick = true,
}: {
displayUser: User;
allowClick?: boolean;
}) => (
const UserDisplay = ({displayUser, allowClick = true}: {displayUser: User; allowClick?: boolean}) => (
<div
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"
>
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>
{displayUser.type === "corporate"
? displayUser.corporateInformation?.companyInformation?.name ||
displayUser.name
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
: displayUser.name}
</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
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" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Referred Corporate ({total})
</h2>
<h2 className="text-2xl font-semibold">Referred Corporate ({total})</h2>
</div>
)}
/>
@@ -106,14 +82,11 @@ export default function AgentDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Inactive Referred Corporate ({total})
</h2>
<h2 className="text-2xl font-semibold">Inactive Referred Corporate ({total})</h2>
</div>
)}
/>
@@ -131,8 +104,7 @@ export default function AgentDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -143,7 +115,7 @@ export default function AgentDashboard({ user }: Props) {
);
};
const CorporatePaidStatusList = ({ paid }: { paid: Boolean }) => {
const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => {
const list = paid ? done : pending;
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
@@ -155,8 +127,7 @@ export default function AgentDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -193,13 +164,7 @@ export default function AgentDashboard({ user }: Props) {
value={users.filter(corporateFilter).length}
color="purple"
/>
<IconCard
onClick={() => setPage("paymentdone")}
Icon={BsCurrencyDollar}
label="Payment Done"
value={done.length}
color="purple"
/>
<IconCard onClick={() => setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" />
<IconCard
onClick={() => setPage("paymentpending")}
Icon={BsCurrencyDollar}
@@ -239,10 +204,8 @@ export default function AgentDashboard({ user }: Props) {
.filter(
(x) =>
referredCorporateFilter(x) &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} displayUser={x} />
@@ -266,16 +229,9 @@ export default function AgentDashboard({ user }: Props) {
if (shouldReload) reload();
}}
onViewStudents={
selectedUser.type === "corporate" ||
selectedUser.type === "teacher"
? () => setPage("students")
: undefined
}
onViewTeachers={
selectedUser.type === "corporate"
? () => setPage("teachers")
: undefined
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
}
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
user={selectedUser}
/>
</div>
@@ -284,9 +240,7 @@ export default function AgentDashboard({ user }: Props) {
</Modal>
{page === "referredCorporate" && <ReferredCorporateList />}
{page === "corporate" && <CorporateList />}
{page === "inactiveReferredCorporate" && (
<InactiveReferredCorporateList />
)}
{page === "inactiveReferredCorporate" && <InactiveReferredCorporateList />}
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
{page === "" && <DefaultDashboard />}

View File

@@ -10,6 +10,7 @@ import {usePDFDownload} from "@/hooks/usePDFDownload";
import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
import {uniqBy} from "lodash";
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
import {getUserName} from "@/utils/users";
interface Props {
onClick?: () => void;
@@ -35,6 +36,8 @@ export default function AssignmentCard({
allowArchive,
allowUnarchive,
}: Assignment & Props) {
const {users} = useUsers();
const renderPdfIcon = usePDFDownload("assignments");
const renderArchiveIcon = useAssignmentArchive(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"}
/>
</div>
<div className="flex flex-col gap-1">
<span className="flex justify-between gap-1">
<span>{moment(startDate).format("DD/MM/YY, HH:mm")}</span>
<span>-</span>
<span>{moment(endDate).format("DD/MM/YY, HH:mm")}</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">
{uniqBy(exams, (x) => x.module).map(({module}) => (
<div

View File

@@ -371,7 +371,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
!startDate ||
!endDate ||
assignees.length === 0 ||
(!!examIDs && examIDs.length < selectedModules.length)
(!useRandomExams && examIDs.length < selectedModules.length)
}
className="w-full max-w-[200px]"
onClick={createAssignment}

View File

@@ -10,6 +10,7 @@ import {getExamById} from "@/utils/exams";
import {sortByModule} from "@/utils/moduleUtils";
import {calculateBandScore} from "@/utils/score";
import {convertToUserSolutions} from "@/utils/stats";
import {getUserName} from "@/utils/users";
import axios from "axios";
import clsx from "clsx";
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>End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}</span>
</div>
<div className="flex flex-col gap-2">
<span>
Assignees:{" "}
{users
@@ -248,6 +250,8 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
.map((u) => `${u.name} (${u.email})`)
.join(", ")}
</span>
<span>Assigner: {getUserName(users.find((x) => x.id === assignment?.assigner))}</span>
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-xl font-bold">Average Scores</span>

View File

@@ -2,11 +2,11 @@
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers";
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
import {CorporateUser, Group, Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import { dateSorter } from "@/utils";
import {dateSorter} from "@/utils";
import moment from "moment";
import { useEffect, useState } from "react";
import {useEffect, useState} from "react";
import {
BsArrowLeft,
BsClipboard2Data,
@@ -23,35 +23,149 @@ import {
BsPersonBadge,
BsPersonCheck,
BsPeople,
BsArrowRepeat,
BsPlus,
BsEnvelopePaper,
} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { Module } from "@/interfaces";
import { groupByExam } from "@/utils/stats";
import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {Module} from "@/interfaces";
import {groupByExam} from "@/utils/stats";
import IconCard from "./IconCard";
import GroupList from "@/pages/(admin)/Lists/GroupList";
import useFilterStore from "@/stores/listFilterStore";
import { useRouter } from "next/router";
import {useRouter} from "next/router";
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 {
user: CorporateUser;
}
export default function CorporateDashboard({ user }: Props) {
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) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const [corporateUserToShow, setCorporateUserToShow] =
useState<CorporateUser>();
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const [userBalance, setUserBalance] = useState(0);
const { stats } = useStats();
const { users, reload } = useUsers();
const { codes } = useCodes(user.id);
const { groups } = useGroups(user.id);
const {stats} = useStats();
const {users, reload, isLoading} = useUsers();
const {codes} = useCodes(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 router = useRouter();
@@ -60,31 +174,29 @@ export default function CorporateDashboard({ user }: Props) {
setShowModal(!!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(() => {
// in this case it fetches the master corporate account
getUserCorporate(user.id).then(setCorporateUserToShow);
}, [user]);
const studentFilter = (user: User) =>
user.type === "student" &&
groups.flatMap((g) => g.participants).includes(user.id);
const teacherFilter = (user: User) =>
user.type === "teacher" &&
groups.flatMap((g) => g.participants).includes(user.id);
const studentFilter = (user: User) => user.type === "student" && 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) =>
stats.filter((s) => s.user === user.id);
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => (
<div
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"
>
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
@@ -110,8 +222,7 @@ export default function CorporateDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -140,8 +251,7 @@ export default function CorporateDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -153,22 +263,18 @@ export default function CorporateDashboard({ user }: Props) {
};
const GroupsList = () => {
const filter = (x: Group) =>
x.admin === user.id || x.participants.includes(user.id);
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id);
return (
<>
<div className="flex flex-col gap-4">
<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"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Groups ({groups.filter(filter).length})
</h2>
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
</div>
<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 formattedStats = studentStats
.map((s) => ({
@@ -186,15 +438,10 @@ export default function CorporateDashboard({ user }: Props) {
.filter((f) => !!f.focus);
const bandScores = formattedStats.map((s) => ({
module: s.module,
level: calculateBandScore(
s.score.correct,
s.score.total,
s.module,
s.focus!
),
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
}));
const levels: { [key in Module]: number } = {
const levels: {[key in Module]: number} = {
reading: 0,
listening: 0,
writing: 0,
@@ -210,14 +457,10 @@ export default function CorporateDashboard({ user }: Props) {
<>
{corporateUserToShow && (
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
Linked to:{" "}
<b>
{corporateUserToShow?.corporateInformation?.companyInformation
.name || corporateUserToShow.name}
</b>
Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
</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
onClick={() => setPage("students")}
Icon={BsPersonFill}
@@ -235,48 +478,47 @@ export default function CorporateDashboard({ user }: Props) {
<IconCard
Icon={BsClipboard2Data}
label="Exams Performed"
value={
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
).length
}
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
color="purple"
/>
<IconCard
Icon={BsPaperclip}
label="Average Level"
value={averageLevelCalculator(
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}
value={averageLevelCalculator(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" />
<IconCard
Icon={BsPersonCheck}
label="User Balance"
value={`${codes.length}/${
user.corporateInformation?.companyInformation?.userAmount || 0
}`}
value={`${userBalance}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
color="purple"
/>
<IconCard
Icon={BsClock}
label="Expiration Date"
value={
user.subscriptionExpirationDate
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
: "Unlimited"
}
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
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 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">
{users
.filter(studentFilter)
.sort(
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
@@ -324,8 +562,7 @@ export default function CorporateDashboard({ user }: Props) {
.filter(studentFilter)
.sort(
(a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length -
Object.keys(groupByExam(getStatsByStudent(a))).length
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -349,8 +586,7 @@ export default function CorporateDashboard({ user }: Props) {
if (shouldReload) reload();
}}
onViewStudents={
selectedUser.type === "corporate" ||
selectedUser.type === "teacher"
selectedUser.type === "corporate" || selectedUser.type === "teacher"
? () => {
appendUserFilters({
id: "view-students",
@@ -360,11 +596,7 @@ export default function CorporateDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
@@ -374,8 +606,7 @@ export default function CorporateDashboard({ user }: Props) {
: undefined
}
onViewTeachers={
selectedUser.type === "corporate" ||
selectedUser.type === "student"
selectedUser.type === "corporate" || selectedUser.type === "student"
? () => {
appendUserFilters({
id: "view-teachers",
@@ -385,11 +616,7 @@ export default function CorporateDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
@@ -407,6 +634,8 @@ export default function CorporateDashboard({ user }: Props) {
{page === "students" && <StudentsList />}
{page === "teachers" && <TeachersList />}
{page === "groups" && <GroupsList />}
{page === "assignments" && <AssignmentsPage />}
{page === "studentsPerformance" && <StudentPerformancePage />}
{page === "" && <DefaultDashboard />}
</>
);

View File

@@ -1,23 +1,17 @@
import React from "react";
import useUsers from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups";
import { User } from "@/interfaces/user";
import {User} from "@/interfaces/user";
import Select from "@/components/Low/Select";
import ProgressBar from "@/components/Low/ProgressBar";
import {
BsBook,
BsClipboard,
BsHeadphones,
BsMegaphone,
BsPen,
} from "react-icons/bs";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { capitalize } from "lodash";
import { getLevelLabel } from "@/utils/score";
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {capitalize} from "lodash";
import {getLevelLabel} from "@/utils/score";
const Card = ({ user }: { user: User }) => {
const Card = ({user}: {user: User}) => {
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">
<h3 className="text-xl font-semibold">{user.name}</h3>
</div>
@@ -26,38 +20,19 @@ const Card = ({ user }: { user: User }) => {
const desiredLevel = user.desiredLevels[module] || 9;
const level = user.levels[module] || 0;
return (
<div
className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]"
key={module}
>
<div 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="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" && (
<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 === "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" />
)}
{module === "reading" && <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 === "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 className="flex w-full flex-col">
<span className="text-sm font-bold md:font-extrabold w-full">
{capitalize(module)}
</span>
<span className="text-sm font-bold md:font-extrabold w-full">{capitalize(module)}</span>
<div className="text-mti-gray-dim text-sm font-normal">
{module === "level" && (
<span>
English Level: {getLevelLabel(level).join(" / ")}
</span>
)}
{module === "level" && <span>English Level: {getLevelLabel(level).join(" / ")}</span>}
{module !== "level" && (
<div className="flex flex-col">
<span>Level {level} / Level 9</span>
@@ -86,17 +61,14 @@ const Card = ({ user }: { user: User }) => {
};
const CorporateStudentsLevels = () => {
const { users } = useUsers();
const { groups } = useGroups();
const {users} = useUsers();
const {groups} = useGroups({});
const corporateUsers = users.filter((u) => u.type === "corporate") as User[];
const [corporateId, setCorporateId] = React.useState<string>("");
const corporate =
corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
const corporate = corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
const groupsFromCorporate = corporate
? groups.filter((g) => g.admin === corporate.id)
: [];
const groupsFromCorporate = corporate ? groups.filter((g) => g.admin === corporate.id) : [];
const groupsParticipants = groupsFromCorporate
.flatMap((g) => g.participants)
@@ -115,17 +87,13 @@ const CorporateStudentsLevels = () => {
value: x.id,
label: `${x.name} - ${x.email}`,
}))}
value={corporate ? { value: corporate.id, label: corporate.name } : null}
value={corporate ? {value: corporate.id, label: corporate.name} : null}
onChange={(value) => setCorporateId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
color: state.isFocused ? "black" : styles.color,
}),
}}

View File

@@ -2,11 +2,11 @@
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
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 { dateSorter } from "@/utils";
import {dateSorter} from "@/utils";
import moment from "moment";
import { useEffect, useState } from "react";
import {useEffect, useState} from "react";
import {
BsArrowLeft,
BsClipboard2Data,
@@ -17,42 +17,244 @@ import {
BsPersonCheck,
BsPeople,
BsBank,
BsEnvelopePaper,
BsArrowRepeat,
BsPlus,
BsPersonFillGear,
} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { Module } from "@/interfaces";
import { groupByExam } from "@/utils/stats";
import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {Module} from "@/interfaces";
import {groupByExam} from "@/utils/stats";
import IconCard from "./IconCard";
import GroupList from "@/pages/(admin)/Lists/GroupList";
import useFilterStore from "@/stores/listFilterStore";
import { useRouter } from "next/router";
import {useRouter} from "next/router";
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 {
user: MasterCorporateUser;
}
export default function MasterCorporateDashboard({ user }: Props) {
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) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const { stats } = useStats();
const { users, reload } = useUsers();
const { codes } = useCodes(user.id);
const { groups } = useGroups(user.id, user.type);
const {stats} = useStats();
const {users, reload} = useUsers();
const {codes} = useCodes(user.id);
const {groups} = useGroups({admin: user.id, userType: user.type});
const masterCorporateUserGroups = [
...new Set(
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
),
];
const corporateUserGroups = [
...new Set(groups.flatMap((g) => g.participants)),
];
const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))];
const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))];
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter();
@@ -61,24 +263,16 @@ export default function MasterCorporateDashboard({ user }: Props) {
setShowModal(!!selectedUser && page === "");
}, [selectedUser, page]);
const studentFilter = (user: User) =>
user.type === "student" && corporateUserGroups.includes(user.id);
const teacherFilter = (user: User) =>
user.type === "teacher" && corporateUserGroups.includes(user.id);
const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id);
const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id);
const getStatsByStudent = (user: User) =>
stats.filter((s) => s.user === user.id);
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => (
<div
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"
>
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
@@ -88,10 +282,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
const StudentsList = () => {
const filter = (x: User) =>
x.type === "student" &&
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
return (
<UserList
@@ -101,8 +292,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -115,10 +305,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
const TeachersList = () => {
const filter = (x: User) =>
x.type === "teacher" &&
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
return (
<UserList
@@ -128,8 +315,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -141,10 +327,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
};
const corporateUserFilter = (x: User) =>
x.type === "corporate" &&
(!!selectedUser
? masterCorporateUserGroups.includes(x.id) || false
: masterCorporateUserGroups.includes(x.id));
x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id));
const CorporateList = () => {
return (
@@ -155,8 +338,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -173,14 +355,11 @@ export default function MasterCorporateDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Groups ({groups.length})
</h2>
<h2 className="text-2xl font-semibold">Groups ({groups.length})</h2>
</div>
<GroupList user={user} />
@@ -188,34 +367,150 @@ export default function MasterCorporateDashboard({ user }: Props) {
);
};
const averageLevelCalculator = (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 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",
corporate: getCorporateUser(u, users, groups),
}));
const levels: { [key in Module]: number } = {
reading: 0,
listening: 0,
writing: 0,
speaking: 0,
level: 0,
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={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 = () => (
@@ -238,46 +533,29 @@ export default function MasterCorporateDashboard({ user }: Props) {
<IconCard
Icon={BsClipboard2Data}
label="Exams Performed"
value={
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
).length
}
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
color="purple"
/>
<IconCard
Icon={BsPaperclip}
label="Average Level"
value={averageLevelCalculator(
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
)
users,
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"
/>
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
<IconCard
Icon={BsPersonCheck}
label="User Balance"
value={`${codes.length}/${
user.corporateInformation?.companyInformation?.userAmount || 0
}`}
value={`${codes.length}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
color="purple"
/>
<IconCard
Icon={BsClock}
label="Expiration Date"
value={
user.subscriptionExpirationDate
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
: "Unlimited"
}
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
color="rose"
/>
<IconCard
@@ -287,6 +565,25 @@ export default function MasterCorporateDashboard({ user }: Props) {
color="purple"
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 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">
{users
.filter(studentFilter)
.sort(
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
@@ -334,8 +627,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
.filter(studentFilter)
.sort(
(a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length -
Object.keys(groupByExam(getStatsByStudent(a))).length
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -359,8 +651,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
if (shouldReload) reload();
}}
onViewStudents={
selectedUser.type === "corporate" ||
selectedUser.type === "teacher"
selectedUser.type === "corporate" || selectedUser.type === "teacher"
? () => {
appendUserFilters({
id: "view-students",
@@ -370,11 +661,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
@@ -384,8 +671,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
: undefined
}
onViewTeachers={
selectedUser.type === "corporate" ||
selectedUser.type === "student"
selectedUser.type === "corporate" || selectedUser.type === "student"
? () => {
appendUserFilters({
id: "view-teachers",
@@ -395,11 +681,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
@@ -418,6 +700,8 @@ export default function MasterCorporateDashboard({ user }: Props) {
{page === "teachers" && <TeachersList />}
{page === "groups" && <GroupsList />}
{page === "corporate" && <CorporateList />}
{page === "assignments" && <AssignmentsPage />}
{page === "studentsPerformance" && <StudentPerformancePage />}
{page === "" && <DefaultDashboard />}
</>
);

View File

@@ -2,11 +2,11 @@
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers";
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
import {CorporateUser, Group, Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import { dateSorter } from "@/utils";
import {dateSorter} from "@/utils";
import moment from "moment";
import { useEffect, useState } from "react";
import {useEffect, useState} from "react";
import {
BsArrowLeft,
BsArrowRepeat,
@@ -31,44 +31,41 @@ import {
} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { Module } from "@/interfaces";
import { groupByExam } from "@/utils/stats";
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {Module} from "@/interfaces";
import {groupByExam} from "@/utils/stats";
import IconCard from "./IconCard";
import GroupList from "@/pages/(admin)/Lists/GroupList";
import useAssignments from "@/hooks/useAssignments";
import { Assignment } from "@/interfaces/results";
import {Assignment} from "@/interfaces/results";
import AssignmentCard from "./AssignmentCard";
import Button from "@/components/Low/Button";
import clsx from "clsx";
import ProgressBar from "@/components/Low/ProgressBar";
import AssignmentCreator from "./AssignmentCreator";
import AssignmentView from "./AssignmentView";
import { getUserCorporate } from "@/utils/groups";
import { checkAccess } from "@/utils/permissions";
import {getUserCorporate} from "@/utils/groups";
import {checkAccess} from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions";
interface Props {
user: User;
}
export default function TeacherDashboard({ user }: Props) {
export default function TeacherDashboard({user}: Props) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const [corporateUserToShow, setCorporateUserToShow] =
useState<CorporateUser>();
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
const { stats } = useStats();
const { users, reload } = useUsers();
const { groups } = useGroups(user.id);
const {
assignments,
isLoading: isAssignmentsLoading,
reload: reloadAssignments,
} = useAssignments({ assigner: user.id });
const {stats} = useStats();
const {users, reload} = useUsers();
const {groups} = useGroups({admin: user.id});
const {permissions} = usePermissions(user.id);
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
useEffect(() => {
setShowModal(!!selectedUser && page === "");
@@ -78,23 +75,15 @@ export default function TeacherDashboard({ user }: Props) {
getUserCorporate(user.id).then(setCorporateUserToShow);
}, [user]);
const studentFilter = (user: User) =>
user.type === "student" &&
groups.flatMap((g) => g.participants).includes(user.id);
const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id);
const getStatsByStudent = (user: User) =>
stats.filter((s) => s.user === user.id);
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => (
<div
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"
>
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
@@ -120,8 +109,7 @@ export default function TeacherDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<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"
>
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>
@@ -133,22 +121,18 @@ export default function TeacherDashboard({ user }: Props) {
};
const GroupsList = () => {
const filter = (x: Group) =>
x.admin === user.id || x.participants.includes(user.id);
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id);
return (
<>
<div className="flex flex-col gap-4">
<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"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Groups ({groups.filter(filter).length})
</h2>
<h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
</div>
<GroupList user={user} />
@@ -166,15 +150,10 @@ export default function TeacherDashboard({ user }: Props) {
.filter((f) => !!f.focus);
const bandScores = formattedStats.map((s) => ({
module: s.module,
level: calculateBandScore(
s.score.correct,
s.score.total,
s.module,
s.focus!
),
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
}));
const levels: { [key in Module]: number } = {
const levels: {[key in Module]: number} = {
reading: 0,
listening: 0,
writing: 0,
@@ -188,16 +167,10 @@ export default function TeacherDashboard({ 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;
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());
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
return (
<>
@@ -212,9 +185,7 @@ export default function TeacherDashboard({ user }: Props) {
/>
<AssignmentCreator
assignment={selectedAssignment}
groups={groups.filter(
(x) => x.admin === user.id || x.participants.includes(user.id)
)}
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
users={users.filter(
(x) =>
x.type === "student" &&
@@ -223,7 +194,7 @@ export default function TeacherDashboard({ user }: Props) {
.filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants)
.includes(x.id) || false
: groups.flatMap((g) => g.participants).includes(x.id))
: groups.flatMap((g) => g.participants).includes(x.id)),
)}
assigner={user.id}
isCreating={isCreatingAssignment}
@@ -236,47 +207,31 @@ export default function TeacherDashboard({ user }: Props) {
<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"
>
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"
>
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"
)}
/>
<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>
<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}
/>
<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>
<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"
>
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>
@@ -293,9 +248,7 @@ export default function TeacherDashboard({ user }: Props) {
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Past Assignments ({assignments.filter(pastFilter).length})
</h2>
<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
@@ -310,9 +263,7 @@ export default function TeacherDashboard({ user }: Props) {
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Archived Assignments ({assignments.filter(archivedFilter).length})
</h2>
<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
@@ -334,19 +285,14 @@ export default function TeacherDashboard({ user }: Props) {
<>
{corporateUserToShow && (
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
Linked to:{" "}
<b>
{corporateUserToShow?.corporateInformation?.companyInformation
.name || corporateUserToShow.name}
</b>
Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
</div>
)}
<section
className={clsx(
"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
onClick={() => setPage("students")}
Icon={BsPersonFill}
@@ -357,42 +303,25 @@ export default function TeacherDashboard({ user }: Props) {
<IconCard
Icon={BsClipboard2Data}
label="Exams Performed"
value={
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
).length
}
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
color="purple"
/>
<IconCard
Icon={BsPaperclip}
label="Average Level"
value={averageLevelCalculator(
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
)
).toFixed(1)}
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
color="purple"
/>
{checkAccess(user, ["teacher", "developer"], "viewGroup") && (
<IconCard
Icon={BsPeople}
label="Groups"
value={groups.length}
color="purple"
onClick={() => setPage("groups")}
/>
{checkAccess(user, ["teacher", "developer"], permissions, "viewGroup") && (
<IconCard Icon={BsPeople} label="Groups" value={groups.length} color="purple" onClick={() => setPage("groups")} />
)}
<div
onClick={() => setPage("assignments")}
className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"
>
className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300">
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light">
{assignments.filter((a) => !a.archived).length}
</span>
<span className="font-semibold text-mti-purple-light">{assignments.filter((a) => !a.archived).length}</span>
</span>
</div>
</section>
@@ -414,11 +343,7 @@ export default function TeacherDashboard({ user }: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(studentFilter)
.sort(
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
@@ -431,8 +356,7 @@ export default function TeacherDashboard({ user }: Props) {
.filter(studentFilter)
.sort(
(a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length -
Object.keys(groupByExam(getStatsByStudent(a))).length
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -456,16 +380,9 @@ export default function TeacherDashboard({ user }: Props) {
if (shouldReload) reload();
}}
onViewStudents={
selectedUser.type === "corporate" ||
selectedUser.type === "teacher"
? () => setPage("students")
: undefined
}
onViewTeachers={
selectedUser.type === "corporate"
? () => setPage("teachers")
: undefined
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
}
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
user={selectedUser}
/>
</div>

View File

@@ -1,29 +1,19 @@
/* eslint-disable @next/next/no-img-element */
import { useState } from "react";
import { Module } from "@/interfaces";
import {useState} from "react";
import {Module} from "@/interfaces";
import clsx from "clsx";
import { User } from "@/interfaces/user";
import {User} from "@/interfaces/user";
import ProgressBar from "@/components/Low/ProgressBar";
import {
BsArrowRepeat,
BsBook,
BsCheck,
BsCheckCircle,
BsClipboard,
BsHeadphones,
BsMegaphone,
BsPen,
BsXCircle,
} from "react-icons/bs";
import { totalExamsByModule } from "@/utils/stats";
import {BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
import {totalExamsByModule} from "@/utils/stats";
import useStats from "@/hooks/useStats";
import Button from "@/components/Low/Button";
import { calculateAverageLevel } from "@/utils/score";
import { sortByModuleName } from "@/utils/moduleUtils";
import { capitalize } from "lodash";
import {calculateAverageLevel} from "@/utils/score";
import {sortByModuleName} from "@/utils/moduleUtils";
import {capitalize} from "lodash";
import ProfileSummary from "@/components/ProfileSummary";
import { Variant } from "@/interfaces/exam";
import useSessions, { Session } from "@/hooks/useSessions";
import {Variant} from "@/interfaces/exam";
import useSessions, {Session} from "@/hooks/useSessions";
import SessionCard from "@/components/Medium/SessionCard";
import useExamStore from "@/stores/examStore";
import moment from "moment";
@@ -31,34 +21,23 @@ import moment from "moment";
interface Props {
user: User;
page: "exercises" | "exams";
onStart: (
modules: Module[],
avoidRepeated: boolean,
variant: Variant,
) => void;
onStart: (modules: Module[], avoidRepeated: boolean, variant: Variant) => void;
disableSelection?: boolean;
}
export default function Selection({
user,
page,
onStart,
disableSelection = false,
}: Props) {
export default function Selection({user, page, onStart, disableSelection = false}: Props) {
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
const [variant, setVariant] = useState<Variant>("full");
const { stats } = useStats(user?.id);
const { sessions, isLoading, reload } = useSessions(user.id);
const {stats} = useStats(user?.id);
const {sessions, isLoading, reload} = useSessions(user.id);
const state = useExamStore((state) => state);
const toggleModule = (module: Module) => {
const modules = selectedModules.filter((x) => x !== module);
setSelectedModules((prev) =>
prev.includes(module) ? modules : [...modules, module],
);
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
};
const loadSession = async (session: Session) => {
@@ -84,41 +63,31 @@ export default function Selection({
user={user}
items={[
{
icon: (
<BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />,
label: "Reading",
value: totalExamsByModule(stats, "reading"),
tooltip: "The amount of reading exams performed.",
},
{
icon: (
<BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />,
label: "Listening",
value: totalExamsByModule(stats, "listening"),
tooltip: "The amount of listening exams performed.",
},
{
icon: (
<BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />,
label: "Writing",
value: totalExamsByModule(stats, "writing"),
tooltip: "The amount of writing exams performed.",
},
{
icon: (
<BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />,
label: "Speaking",
value: totalExamsByModule(stats, "speaking"),
tooltip: "The amount of speaking exams performed.",
},
{
icon: (
<BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />,
label: "Level",
value: totalExamsByModule(stats, "level"),
tooltip: "The amount of level exams performed.",
@@ -132,35 +101,23 @@ export default function Selection({
<span className="text-mti-gray-taupe">
{page === "exercises" && (
<>
In the realm of language acquisition, practice makes perfect,
and our exercises are the key to unlocking your full potential.
Dive into a world of interactive and engaging exercises that
cater to diverse learning styles. From grammar drills that build
a strong foundation to vocabulary challenges that broaden your
lexicon, our exercises are carefully designed to make learning
English both enjoyable and effective. Whether you&apos;re
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!
In the realm of language acquisition, practice makes perfect, and our exercises are the key to unlocking your full
potential. Dive into a world of interactive and engaging exercises that cater to diverse learning styles. From grammar
drills that build a strong foundation to vocabulary challenges that broaden your lexicon, our exercises are carefully
designed to make learning English both enjoyable and effective. Whether you&apos;re 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" && (
<>
Welcome to the heart of success on your English language
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 your
abilities. Whether you&apos;re a beginner or a seasoned learner,
our exams cater to all levels, providing a comprehensive
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&apos;s
a testament to your dedication and our commitment to empowering
you with the English language.
Welcome to the heart of success on your English language 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
your abilities. Whether you&apos;re a beginner or a seasoned learner, our exams cater to all levels, providing a
comprehensive 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&apos;s a testament to your dedication and our commitment to empowering you with the English language.
</>
)}
</span>
@@ -171,26 +128,16 @@ export default function Selection({
<div className="flex items-center gap-4">
<div
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"
>
<span className="text-mti-black text-lg font-bold">
Unfinished Sessions
</span>
<BsArrowRepeat
className={clsx("text-xl", isLoading && "animate-spin")}
/>
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>
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
</div>
</div>
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
{sessions
.sort((a, b) => moment(b.date).diff(moment(a.date)))
.map((session) => (
<SessionCard
session={session}
key={session.sessionId}
reload={reload}
loadSession={loadSession}
/>
<SessionCard session={session} key={session.sessionId} reload={reload} loadSession={loadSession} />
))}
</span>
</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">
<div
onClick={
!disableSelection && !selectedModules.includes("level")
? () => toggleModule("reading")
: undefined
}
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
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",
selectedModules.includes("reading") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("reading") || disableSelection ? "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">
<BsBook className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Reading:</span>
<p className="text-left text-xs">
Expand your vocabulary, improve your reading comprehension and
improve your ability to interpret texts in English.
Expand your vocabulary, improve your reading comprehension and improve your ability to interpret texts in English.
</p>
{!selectedModules.includes("reading") &&
!selectedModules.includes("level") &&
!disableSelection && (
{!selectedModules.includes("reading") && !selectedModules.includes("level") && !disableSelection && (
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
)}
{(selectedModules.includes("reading") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
</div>
<div
onClick={
!disableSelection && !selectedModules.includes("level")
? () => toggleModule("listening")
: undefined
}
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
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",
selectedModules.includes("listening") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("listening") || disableSelection ? "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">
<BsHeadphones className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Listening:</span>
<p className="text-left text-xs">
Improve your ability to follow conversations in English and your
ability to understand different accents and intonations.
Improve your ability to follow conversations in English and your ability to understand different accents and intonations.
</p>
{!selectedModules.includes("listening") &&
!selectedModules.includes("level") &&
!disableSelection && (
{!selectedModules.includes("listening") && !selectedModules.includes("level") && !disableSelection && (
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
)}
{(selectedModules.includes("listening") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
</div>
<div
onClick={
!disableSelection && !selectedModules.includes("level")
? () => toggleModule("writing")
: undefined
}
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
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",
selectedModules.includes("writing") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("writing") || disableSelection ? "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">
<BsPen className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Writing:</span>
<p className="text-left text-xs">
Allow you to practice writing in a variety of formats, from simple
paragraphs to complex essays.
Allow you to practice writing in a variety of formats, from simple paragraphs to complex essays.
</p>
{!selectedModules.includes("writing") &&
!selectedModules.includes("level") &&
!disableSelection && (
{!selectedModules.includes("writing") && !selectedModules.includes("level") && !disableSelection && (
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
)}
{(selectedModules.includes("writing") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
</div>
<div
onClick={
!disableSelection && !selectedModules.includes("level")
? () => toggleModule("speaking")
: undefined
}
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
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",
selectedModules.includes("speaking") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("speaking") || disableSelection ? "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">
<BsMegaphone className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Speaking:</span>
<p className="text-left text-xs">
You&apos;ll have access to interactive dialogs, pronunciation
exercises and speech recordings.
You&apos;ll have access to interactive dialogs, pronunciation exercises and speech recordings.
</p>
{!selectedModules.includes("speaking") &&
!selectedModules.includes("level") &&
!disableSelection && (
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && !disableSelection && (
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
)}
{(selectedModules.includes("speaking") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
</div>
{!disableSelection && (
<div
onClick={
selectedModules.length === 0 ||
selectedModules.includes("level")
? () => toggleModule("level")
: undefined
}
onClick={selectedModules.length === 0 || selectedModules.includes("level") ? () => toggleModule("level") : undefined}
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",
selectedModules.includes("level") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("level") || disableSelection ? "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">
<BsClipboard className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Level:</span>
<p className="text-left text-xs">
You&apos;ll be able to test your english level with multiple
choice questions.
</p>
{!selectedModules.includes("level") &&
selectedModules.length === 0 &&
!disableSelection && (
<p className="text-left text-xs">You&apos;ll be able to test your english level with multiple 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" />
)}
{(selectedModules.includes("level") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{!selectedModules.includes("level") &&
selectedModules.length > 0 && (
{!selectedModules.includes("level") && selectedModules.length > 0 && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
</div>
@@ -371,68 +256,51 @@ export default function Selection({
<div className="flex w-full flex-col items-center gap-3">
<div
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" />
<div
className={clsx(
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
"transition duration-300 ease-in-out",
avoidRepeatedExams && "!bg-mti-purple-light ",
)}
>
)}>
<BsCheck color="white" className="h-full w-full" />
</div>
<span
className="tooltip"
data-tip="If possible, the platform will choose exams not yet done."
>
<span className="tooltip" data-tip="If possible, the platform will choose exams not yet done.">
Avoid Repeated Questions
</span>
</div>
<div
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"))}>
>
<input type="checkbox" className="hidden" disabled />
onClick={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
<input type="checkbox" className="hidden" />
<div
className={clsx(
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
"transition duration-300 ease-in-out",
variant === "full" && "!bg-mti-purple-light ",
)}
>
)}>
<BsCheck color="white" className="h-full w-full" />
</div>
<span>Full length exams</span>
</div>
</div>
<div
className="tooltip w-full"
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
>
<div className="tooltip w-full" 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
</Button>
</div>
<Button
onClick={() =>
onStart(
!disableSelection
? selectedModules.sort(sortByModuleName)
: ["reading", "listening", "writing", "speaking"],
!disableSelection ? selectedModules.sort(sortByModuleName) : ["reading", "listening", "writing", "speaking"],
avoidRepeatedExams,
variant,
)
}
color="purple"
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
</Button>
</div>

View File

@@ -2,7 +2,7 @@ import {Assignment} from "@/interfaces/results";
import axios from "axios";
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 [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
@@ -10,12 +10,13 @@ export default function useAssignments({assigner, assignees}: {assigner?: string
const getData = () => {
setIsLoading(true);
axios
.get<Assignment[]>("/api/assignments")
.then((response) => {
.get<Assignment[]>(!corporate ? "/api/assignments" : `/api/assignments/corporate?id=${corporate}`)
.then(async (response) => {
if (assigner) {
setAssignments(response.data.filter((a) => a.assigner === assigner));
return;
}
if (assignees) {
setAssignments(response.data.filter((a) => a.assignees.filter((x) => assignees.includes(x)).length > 0));
return;
@@ -26,7 +27,7 @@ export default function useAssignments({assigner, assignees}: {assigner?: string
.finally(() => setIsLoading(false));
};
useEffect(getData, [assignees, assigner]);
useEffect(getData, [assignees, assigner, corporate]);
return {assignments, isLoading, isError, reload: getData};
}

View File

@@ -2,32 +2,40 @@ import {Group, User} from "@/interfaces/user";
import axios from "axios";
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 [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const isMasterType = userType?.startsWith('master');
const isMasterType = userType?.startsWith("master");
const getData = () => {
setIsLoading(true);
const url = admin ? `/api/groups?admin=${admin}` : "/api/groups";
const url = admin && !adminAdmins ? `/api/groups?admin=${admin}` : "/api/groups";
axios
.get<Group[]>(url)
.then((response) => {
if(isMasterType) {
return setGroups(response.data);
}
const filter = (g: Group) => g.admin === admin || g.participants.includes(admin || "");
if (isMasterType) return setGroups(response.data);
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);
})
.finally(() => setIsLoading(false));
};
useEffect(getData, [admin, isMasterType]);
useEffect(getData, [admin, adminAdmins, isMasterType]);
return {groups, isLoading, isError, reload: getData};
}

View File

@@ -27,6 +27,10 @@ export function useListSearch<T>(fields: string[][], rows: T[]) {
if (typeof value === "string") {
return value.toLowerCase().includes(searchText);
}
if (typeof value === "number") {
return (value as Number).toString().includes(searchText);
}
});
});
}, [fields, rows, text]);

View 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};
}

View File

@@ -1,57 +1,89 @@
export const markets = ["au", "br", "de"] as const;
export interface PermissionTopic {
topic: string;
list: string[];
}
export const permissions = [
// generate codes are basicly invites
"createCodeStudent",
"createCodeTeacher",
{
topic: "Manage Corporate",
list: [
"viewCorporate",
"editCorporate",
"deleteCorporate",
"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",
"createCodeAdmin",
// exams
],
},
{
topic: "Manage Exams",
list: [
"createReadingExam",
"createListeningExam",
"createWritingExam",
"createSpeakingExam",
"createLevelExam",
// view pages
],
},
{
topic: "View Pages",
list: [
"viewExams",
"viewExercises",
"viewRecords",
"viewStats",
"viewTickets",
"viewPaymentRecords",
// view data
"viewStudent",
"viewTeacher",
"viewCorporate",
"viewCountryManager",
"viewAdmin",
"viewGroup",
"viewCodes",
// edit data
"editStudent",
"editTeacher",
"editCorporate",
"editCountryManager",
"editAdmin",
"editGroup",
// delete data
"deleteStudent",
"deleteTeacher",
"deleteCorporate",
"deleteCountryManager",
"deleteAdmin",
"deleteGroup",
"deleteCodes",
// create options
"createGroup",
"createCodes"
],
},
{
topic: "Manage Group",
list: ["viewGroup", "editGroup", "deleteGroup", "createGroup"],
},
{
topic: "Manage Codes",
list: ["viewCodes", "deleteCodes", "createCodes"],
},
{
topic: "Others",
list: ["all"],
},
] 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 {
id: string;
type: PermissionType;
topic: string;
users: string[];
}

View File

@@ -1,15 +1,8 @@
import { Module } from ".";
import { InstructorGender, ShuffleMap } from "./exam";
import { PermissionType } from "./permissions";
import {Module} from ".";
import {InstructorGender, ShuffleMap} from "./exam";
import {PermissionType} from "./permissions";
export type User =
| StudentUser
| TeacherUser
| CorporateUser
| AgentUser
| AdminUser
| DeveloperUser
| MasterCorporateUser;
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser;
export type UserStatus = "active" | "disabled" | "paymentDue";
export interface BasicUser {
@@ -19,15 +12,16 @@ export interface BasicUser {
id: string;
isFirstLogin: boolean;
focus: "academic" | "general";
levels: { [key in Module]: number };
desiredLevels: { [key in Module]: number };
levels: {[key in Module]: number};
desiredLevels: {[key in Module]: number};
type: Type;
bio: string;
isVerified: boolean;
subscriptionExpirationDate?: null | Date;
registrationDate?: Date;
status: UserStatus;
permissions: PermissionType[],
permissions: PermissionType[];
lastLogin?: Date;
}
export interface StudentUser extends BasicUser {
@@ -112,22 +106,15 @@ export interface DemographicCorporateInformation {
}
export type Gender = "male" | "female" | "other";
export type EmploymentStatus =
| "employed"
| "student"
| "self-employed"
| "unemployed"
| "retired"
| "other";
export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] =
[
{ status: "student", label: "Student" },
{ status: "employed", label: "Employed" },
{ status: "unemployed", label: "Unemployed" },
{ status: "self-employed", label: "Self-employed" },
{ status: "retired", label: "Retired" },
{ status: "other", label: "Other" },
];
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
{status: "student", label: "Student"},
{status: "employed", label: "Employed"},
{status: "unemployed", label: "Unemployed"},
{status: "self-employed", label: "Self-employed"},
{status: "retired", label: "Retired"},
{status: "other", label: "Other"},
];
export interface Stat {
id: string;
@@ -171,20 +158,5 @@ export interface Code {
passport_id?: string;
}
export type Type =
| "student"
| "teacher"
| "corporate"
| "admin"
| "developer"
| "agent"
| "mastercorporate";
export const userTypes: Type[] = [
"student",
"teacher",
"corporate",
"admin",
"developer",
"agent",
"mastercorporate",
];
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate";
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"];

View File

@@ -1,29 +1,28 @@
import Button from "@/components/Low/Button";
import Checkbox from "@/components/Low/Checkbox";
import { PERMISSIONS } from "@/constants/userPermissions";
import {PERMISSIONS} from "@/constants/userPermissions";
import useUsers from "@/hooks/useUsers";
import { Type, User } from "@/interfaces/user";
import { USER_TYPE_LABELS } from "@/resources/user";
import {Type, User} from "@/interfaces/user";
import {USER_TYPE_LABELS} from "@/resources/user";
import axios from "axios";
import clsx from "clsx";
import { capitalize, uniqBy } from "lodash";
import {capitalize, uniqBy} from "lodash";
import moment from "moment";
import { useEffect, useState } from "react";
import {useEffect, useState} from "react";
import ReactDatePicker from "react-datepicker";
import { toast } from "react-toastify";
import {toast} from "react-toastify";
import ShortUniqueId from "short-unique-id";
import { useFilePicker } from "use-file-picker";
import {useFilePicker} from "use-file-picker";
import readXlsxFile from "read-excel-file";
import Modal from "@/components/Modal";
import { BsFileEarmarkEaselFill, BsQuestionCircleFill } from "react-icons/bs";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import { PermissionType } from "@/interfaces/permissions";
const EMAIL_REGEX = new RegExp(
/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/
);
import {BsFileEarmarkEaselFill, BsQuestionCircleFill} from "react-icons/bs";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import {PermissionType} from "@/interfaces/permissions";
import usePermissions from "@/hooks/usePermissions";
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: {
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
[key in Type]: {perm: PermissionType | undefined; list: Type[]};
} = {
student: {
perm: "createCodeStudent",
@@ -47,46 +46,28 @@ const USER_TYPE_PERMISSIONS: {
},
admin: {
perm: "createCodeAdmin",
list: [
"student",
"teacher",
"agent",
"corporate",
"admin",
"mastercorporate",
],
list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
},
developer: {
perm: undefined,
list: [
"student",
"teacher",
"agent",
"corporate",
"admin",
"developer",
"mastercorporate",
],
list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
},
};
export default function BatchCodeGenerator({ user }: { user: User }) {
const [infos, setInfos] = useState<
{ email: string; name: string; passport_id: string }[]
>([]);
export default function BatchCodeGenerator({user}: {user: User}) {
const [infos, setInfos] = useState<{email: string; name: string; passport_id: string}[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [expiryDate, setExpiryDate] = useState<Date | null>(
user?.subscriptionExpirationDate
? moment(user.subscriptionExpirationDate).toDate()
: null
user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null,
);
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
const [type, setType] = useState<Type>("student");
const [showHelp, setShowHelp] = useState(false);
const { users } = useUsers();
const {users} = useUsers();
const {permissions} = usePermissions(user?.id || "");
const { openFilePicker, filesContent, clear } = useFilePicker({
const {openFilePicker, filesContent, clear} = useFilePicker({
accept: ".xlsx",
multiple: false,
readAs: "ArrayBuffer",
@@ -104,14 +85,7 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
const information = uniqBy(
rows
.map((row) => {
const [
firstName,
lastName,
country,
passport_id,
email,
...phone
] = row as string[];
const [firstName, lastName, country, passport_id, email, ...phone] = row as string[];
return EMAIL_REGEX.test(email.toString().trim())
? {
email: email.toString().trim().toLowerCase(),
@@ -121,12 +95,12 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
: undefined;
})
.filter((x) => !!x) as typeof infos,
(x) => x.email
(x) => x.email,
);
if (information.length === 0) {
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();
}
@@ -134,7 +108,7 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
setInfos(information);
} catch {
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();
}
@@ -144,41 +118,24 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
}, [filesContent]);
const generateAndInvite = async () => {
const newUsers = infos.filter(
(x) => !users.map((u) => u.email).includes(x.email)
);
const newUsers = infos.filter((x) => !users.map((u) => u.email).includes(x.email));
const existingUsers = infos
.filter((x) => users.map((u) => u.email).includes(x.email))
.map((i) => users.find((u) => u.email === i.email))
.filter((x) => !!x && x.type === "student") as User[];
const newUsersSentence =
newUsers.length > 0 ? `generate ${newUsers.length} code(s)` : undefined;
const existingUsersSentence =
existingUsers.length > 0
? `invite ${existingUsers.length} registered student(s)`
: undefined;
const newUsersSentence = newUsers.length > 0 ? `generate ${newUsers.length} code(s)` : undefined;
const existingUsersSentence = existingUsers.length > 0 ? `invite ${existingUsers.length} registered student(s)` : undefined;
if (
!confirm(
`You are about to ${[newUsersSentence, existingUsersSentence]
.filter((x) => !!x)
.join(" and ")}, are you sure you want to continue?`
`You are about to ${[newUsersSentence, existingUsersSentence].filter((x) => !!x).join(" and ")}, are you sure you want to continue?`,
)
)
return;
setIsLoading(true);
Promise.all(
existingUsers.map(
async (u) =>
await axios.post(`/api/invites`, { to: u.id, from: user.id })
)
)
.then(() =>
toast.success(
`Successfully invited ${existingUsers.length} registered student(s)!`
)
)
Promise.all(existingUsers.map(async (u) => await axios.post(`/api/invites`, {to: u.id, from: user.id})))
.then(() => toast.success(`Successfully invited ${existingUsers.length} registered student(s)!`))
.finally(() => {
if (newUsers.length === 0) setIsLoading(false);
});
@@ -193,30 +150,30 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
setIsLoading(true);
axios
.post<{ ok: boolean; valid?: number; reason?: string }>("/api/code", {
.post<{ok: boolean; valid?: number; reason?: string}>("/api/code", {
type,
codes,
infos: informations,
expiryDate,
})
.then(({ data, status }) => {
.then(({data, status}) => {
if (data.ok) {
toast.success(
`Successfully generated${
data.valid ? ` ${data.valid}/${informations.length}` : ""
} ${capitalize(type)} codes and they have been notified by e-mail!`,
{ toastId: "success" }
`Successfully generated${data.valid ? ` ${data.valid}/${informations.length}` : ""} ${capitalize(
type,
)} codes and they have been notified by e-mail!`,
{toastId: "success"},
);
return;
}
if (status === 403) {
toast.error(data.reason, { toastId: "forbidden" });
toast.error(data.reason, {toastId: "forbidden"});
}
})
.catch(({ response: { status, data } }) => {
.catch(({response: {status, data}}) => {
if (status === 403) {
toast.error(data.reason, { toastId: "forbidden" });
toast.error(data.reason, {toastId: "forbidden"});
return;
}
@@ -232,30 +189,18 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
return (
<>
<Modal
isOpen={showHelp}
onClose={() => setShowHelp(false)}
title="Excel File Format"
>
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)} title="Excel File Format">
<div className="mt-4 flex flex-col gap-2">
<span>Please upload an Excel file with the following format:</span>
<table className="w-full">
<thead>
<tr>
<th className="border border-neutral-200 px-2 py-1">
First Name
</th>
<th className="border border-neutral-200 px-2 py-1">
Last Name
</th>
<th className="border border-neutral-200 px-2 py-1">First Name</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">
Passport/National ID
</th>
<th className="border border-neutral-200 px-2 py-1">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">
Phone Number
</th>
<th className="border border-neutral-200 px-2 py-1">Phone Number</th>
</tr>
</thead>
</table>
@@ -264,55 +209,27 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
<ul>
<li>- All incorrect e-mails will be ignored;</li>
<li>- All already registered e-mails will be ignored;</li>
<li>
- You may have a header row with the format above, however, it
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>
<li>- You may have a header row with the format above, however, it 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>
</span>
</div>
</Modal>
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
<div className="flex items-end justify-between">
<label className="text-mti-gray-dim text-base font-normal">
Choose an Excel file
</label>
<div
className="tooltip cursor-pointer"
data-tip="Excel File Format"
onClick={() => setShowHelp(true)}
>
<label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label>
<div className="tooltip cursor-pointer" data-tip="Excel File Format" onClick={() => setShowHelp(true)}>
<BsQuestionCircleFill />
</div>
</div>
<Button
onClick={openFilePicker}
isLoading={isLoading}
disabled={isLoading}
>
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
</Button>
{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">
<label className="text-mti-gray-dim text-base font-normal">
Expiry Date
</label>
<Checkbox
isChecked={isExpiryDateEnabled}
onChange={setIsExpiryDateEnabled}
disabled={!!user.subscriptionExpirationDate}
>
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
<Checkbox isChecked={isExpiryDateEnabled} onChange={setIsExpiryDateEnabled} disabled={!!user.subscriptionExpirationDate}>
Enabled
</Checkbox>
</div>
@@ -321,13 +238,11 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
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"
"transition duration-300 ease-in-out",
)}
filterDate={(date) =>
moment(date).isAfter(new Date()) &&
(user.subscriptionExpirationDate
? moment(date).isBefore(user.subscriptionExpirationDate)
: true)
(user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true)
}
dateFormat="dd/MM/yyyy"
selected={expiryDate}
@@ -336,19 +251,16 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
)}
</>
)}
<label className="text-mti-gray-dim text-base font-normal">
Select the type of user they should be
</label>
<label className="text-mti-gray-dim text-base font-normal">Select the type of user they should be</label>
{user && (
<select
defaultValue="student"
onChange={(e) => setType(e.target.value as typeof user.type)}
className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none"
>
className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none">
{Object.keys(USER_TYPE_LABELS)
.filter((x) => {
const { list, perm } = USER_TYPE_PERMISSIONS[x as Type];
return checkAccess(user, getTypesOfUser(list), perm);
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
return checkAccess(user, getTypesOfUser(list), permissions, perm);
})
.map((type) => (
<option key={type} value={type}>
@@ -357,17 +269,8 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
))}
</select>
)}
{checkAccess(
user,
["developer", "admin", "corporate", "mastercorporate"],
"createCodes"
) && (
<Button
onClick={generateAndInvite}
disabled={
infos.length === 0 || (isExpiryDateEnabled ? !expiryDate : false)
}
>
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "createCodes") && (
<Button onClick={generateAndInvite} disabled={infos.length === 0 || (isExpiryDateEnabled ? !expiryDate : false)}>
Generate & Send
</Button>
)}

View File

@@ -1,20 +1,23 @@
import Button from "@/components/Low/Button";
import useUsers from "@/hooks/useUsers";
import { Type as UserType, User } from "@/interfaces/user";
import {Type as UserType, User} from "@/interfaces/user";
import axios from "axios";
import { uniqBy } from "lodash";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useFilePicker } from "use-file-picker";
import {uniqBy} from "lodash";
import {useEffect, useState} from "react";
import {toast} from "react-toastify";
import {useFilePicker} from "use-file-picker";
import readXlsxFile from "read-excel-file";
import Modal from "@/components/Modal";
import { BsQuestionCircleFill } from "react-icons/bs";
import { PermissionType } from "@/interfaces/permissions";
const EMAIL_REGEX = new RegExp(
/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/
);
import {BsQuestionCircleFill} from "react-icons/bs";
import {PermissionType} from "@/interfaces/permissions";
import moment from "moment";
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} = {
student: "Student",
@@ -23,7 +26,7 @@ const USER_TYPE_LABELS: {[key in Type]: string} = {
};
const USER_TYPE_PERMISSIONS: {
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
[key in Type]: {perm: PermissionType | undefined; list: Type[]};
} = {
student: {
perm: "createCodeStudent",
@@ -39,26 +42,39 @@ const USER_TYPE_PERMISSIONS: {
},
};
export default function BatchCreateUser({ user }: { user: User }) {
export default function BatchCreateUser({user}: {user: User}) {
const [infos, setInfos] = useState<
{ email: string; name: string; passport_id:string, type: Type, demographicInformation: {
country: string,
passport_id:string,
phone: string
} }[]
{
email: string;
name: string;
passport_id: string;
type: Type;
demographicInformation: {
country: string;
passport_id: string;
phone: string;
};
}[]
>([]);
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 [showHelp, setShowHelp] = useState(false);
const { users } = useUsers();
const {users} = useUsers();
const { openFilePicker, filesContent, clear } = useFilePicker({
const {openFilePicker, filesContent, clear} = useFilePicker({
accept: ".xlsx",
multiple: false,
readAs: "ArrayBuffer",
});
useEffect(() => {
if (!isExpiryDateEnabled) setExpiryDate(null);
}, [isExpiryDateEnabled]);
useEffect(() => {
if (filesContent.length > 0) {
@@ -68,19 +84,11 @@ export default function BatchCreateUser({ user }: { user: User }) {
const information = uniqBy(
rows
.map((row) => {
const [
firstName,
lastName,
country,
passport_id,
email,
phone,
group
] = row as string[];
const [firstName, lastName, country, passport_id, email, phone, group] = row as string[];
return EMAIL_REGEX.test(email.toString().trim())
? {
email: email.toString().trim().toLowerCase(),
name: `${firstName ?? ""} ${lastName ?? ""}`.trim().toLowerCase(),
name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
type: type,
passport_id: passport_id?.toString().trim() || undefined,
groupName: group,
@@ -88,17 +96,17 @@ export default function BatchCreateUser({ user }: { user: User }) {
country: country,
passport_id: passport_id?.toString().trim() || undefined,
phone,
}
},
}
: undefined;
})
.filter((x) => !!x) as typeof infos,
(x) => x.email
(x) => x.email,
);
if (information.length === 0) {
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();
}
@@ -106,7 +114,7 @@ export default function BatchCreateUser({ user }: { user: User }) {
setInfos(information);
} catch {
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();
}
@@ -116,60 +124,40 @@ export default function BatchCreateUser({ user }: { user: User }) {
}, [filesContent]);
const makeUsers = async () => {
const newUsers = infos.filter(
(x) => !users.map((u) => u.email).includes(x.email)
);
const confirmed = confirm(
`You are about to add ${newUsers.length}, are you sure you want to continue?`
)
if (!confirmed)
return;
if (newUsers.length > 0)
{
const newUsers = infos.filter((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;
if (newUsers.length > 0) {
setIsLoading(true);
Promise.all(newUsers.map(async (user) => {
await axios.post("/api/make_user", user)
})).then((res) =>{
toast.success(
`Successfully added ${newUsers.length} user(s)!`
)}).finally(() => {
return clear();
})
}
try {
for (const newUser of newUsers) await axios.post("/api/make_user", {...newUser, type, expiryDate});
toast.success(`Successfully added ${newUsers.length} user(s)!`);
} catch {
toast.error("Something went wrong, please try again later!");
} finally {
setIsLoading(false);
setInfos([]);
clear();
}
}
};
return (
<>
<Modal
isOpen={showHelp}
onClose={() => setShowHelp(false)}
title="Excel File Format"
>
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)} title="Excel File Format">
<div className="mt-4 flex flex-col gap-2">
<span>Please upload an Excel file with the following format:</span>
<table className="w-full">
<thead>
<tr>
<th className="border border-neutral-200 px-2 py-1">
First Name
</th>
<th className="border border-neutral-200 px-2 py-1">
Last Name
</th>
<th className="border border-neutral-200 px-2 py-1">First Name</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">
Passport/National ID
</th>
<th className="border border-neutral-200 px-2 py-1">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">
Phone Number
</th>
<th className="border border-neutral-200 px-2 py-1">
Group Name
</th>
<th className="border border-neutral-200 px-2 py-1">Phone Number</th>
<th className="border border-neutral-200 px-2 py-1">Group Name</th>
</tr>
</thead>
</table>
@@ -178,63 +166,62 @@ export default function BatchCreateUser({ user }: { user: User }) {
<ul>
<li>- All incorrect e-mails will be ignored;</li>
<li>- All already registered e-mails will be ignored;</li>
<li>
- You may have a header row with the format above, however, it
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>
<li>- You may have a header row with the format above, however, it 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>
</span>
</div>
</Modal>
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
<div className="flex items-end justify-between">
<label className="text-mti-gray-dim text-base font-normal">
Choose an Excel file
</label>
<div
className="tooltip cursor-pointer"
data-tip="Excel File Format"
onClick={() => setShowHelp(true)}
>
<label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label>
<div className="tooltip cursor-pointer" data-tip="Excel File Format" onClick={() => setShowHelp(true)}>
<BsQuestionCircleFill />
</div>
</div>
<Button
onClick={openFilePicker}
isLoading={isLoading}
disabled={isLoading}
>
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
</Button>
<label className="text-mti-gray-dim text-base font-normal">
Select the type of user they should be
</label>
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
<>
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
<Checkbox isChecked={isExpiryDateEnabled} onChange={setIsExpiryDateEnabled} disabled={!!user.subscriptionExpirationDate}>
Enabled
</Checkbox>
</div>
{isExpiryDateEnabled && (
<ReactDatePicker
className={clsx(
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
"hover:border-mti-purple tooltip",
"transition duration-300 ease-in-out",
)}
filterDate={(date) =>
moment(date).isAfter(new Date()) &&
(user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true)
}
dateFormat="dd/MM/yyyy"
selected={expiryDate}
onChange={(date) => setExpiryDate(date)}
/>
)}
</>
)}
<label className="text-mti-gray-dim text-base font-normal">Select the type of user they should be</label>
{user && (
<select
defaultValue="student"
onChange={(e) => setType(e.target.value as Type)}
className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none"
>
{Object.keys(USER_TYPE_LABELS)
.map((type) => (
className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none">
{Object.keys(USER_TYPE_LABELS).map((type) => (
<option key={type} value={type}>
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
</option>
))}
</select>
)}
<Button
className="my-auto"
onClick={makeUsers}
disabled={
infos.length === 0
}
>
<Button className="my-auto" onClick={makeUsers} disabled={infos.length === 0}>
Create
</Button>
</div>

View File

@@ -1,21 +1,22 @@
import Button from "@/components/Low/Button";
import Checkbox from "@/components/Low/Checkbox";
import { PERMISSIONS } from "@/constants/userPermissions";
import { Type, User } from "@/interfaces/user";
import { USER_TYPE_LABELS } from "@/resources/user";
import {PERMISSIONS} from "@/constants/userPermissions";
import {Type, User} from "@/interfaces/user";
import {USER_TYPE_LABELS} from "@/resources/user";
import axios from "axios";
import clsx from "clsx";
import { capitalize } from "lodash";
import {capitalize} from "lodash";
import moment from "moment";
import { useEffect, useState } from "react";
import {useEffect, useState} from "react";
import ReactDatePicker from "react-datepicker";
import { toast } from "react-toastify";
import {toast} from "react-toastify";
import ShortUniqueId from "short-unique-id";
import { checkAccess } from "@/utils/permissions";
import { PermissionType } from "@/interfaces/permissions";
import {checkAccess} from "@/utils/permissions";
import {PermissionType} from "@/interfaces/permissions";
import usePermissions from "@/hooks/usePermissions";
const USER_TYPE_PERMISSIONS: {
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
[key in Type]: {perm: PermissionType | undefined; list: Type[]};
} = {
student: {
perm: "createCodeStudent",
@@ -39,38 +40,22 @@ const USER_TYPE_PERMISSIONS: {
},
admin: {
perm: "createCodeAdmin",
list: [
"student",
"teacher",
"agent",
"corporate",
"admin",
"mastercorporate",
],
list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
},
developer: {
perm: undefined,
list: [
"student",
"teacher",
"agent",
"corporate",
"admin",
"developer",
"mastercorporate",
],
list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
},
};
export default function CodeGenerator({ user }: { user: User }) {
export default function CodeGenerator({user}: {user: User}) {
const [generatedCode, setGeneratedCode] = useState<string>();
const [expiryDate, setExpiryDate] = useState<Date | null>(
user?.subscriptionExpirationDate
? moment(user.subscriptionExpirationDate).toDate()
: null
user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null,
);
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
const [type, setType] = useState<Type>("student");
const {permissions} = usePermissions(user?.id || "");
useEffect(() => {
if (!isExpiryDateEnabled) setExpiryDate(null);
@@ -81,8 +66,8 @@ export default function CodeGenerator({ user }: { user: User }) {
const code = uid.randomUUID(6);
axios
.post("/api/code", { type, codes: [code], expiryDate })
.then(({ data, status }) => {
.post("/api/code", {type, codes: [code], expiryDate})
.then(({data, status}) => {
if (data.ok) {
toast.success(`Successfully generated a ${capitalize(type)} code!`, {
toastId: "success",
@@ -92,12 +77,12 @@ export default function CodeGenerator({ user }: { user: User }) {
}
if (status === 403) {
toast.error(data.reason, { toastId: "forbidden" });
toast.error(data.reason, {toastId: "forbidden"});
}
})
.catch(({ response: { status, data } }) => {
.catch(({response: {status, data}}) => {
if (status === 403) {
toast.error(data.reason, { toastId: "forbidden" });
toast.error(data.reason, {toastId: "forbidden"});
return;
}
@@ -109,19 +94,16 @@ export default function CodeGenerator({ user }: { user: User }) {
return (
<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">
User Code Generator
</label>
<label className="font-normal text-base text-mti-gray-dim">User Code Generator</label>
{user && (
<select
defaultValue="student"
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)
.filter((x) => {
const { list, perm } = USER_TYPE_PERMISSIONS[x as Type];
return checkAccess(user, list, perm);
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
return checkAccess(user, list, permissions, perm);
})
.map((type) => (
<option key={type} value={type}>
@@ -133,14 +115,8 @@ export default function CodeGenerator({ user }: { user: User }) {
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
<>
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
<label className="text-mti-gray-dim text-base font-normal">
Expiry Date
</label>
<Checkbox
isChecked={isExpiryDateEnabled}
onChange={setIsExpiryDateEnabled}
disabled={!!user.subscriptionExpirationDate}
>
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
<Checkbox isChecked={isExpiryDateEnabled} onChange={setIsExpiryDateEnabled} disabled={!!user.subscriptionExpirationDate}>
Enabled
</Checkbox>
</div>
@@ -149,13 +125,11 @@ export default function CodeGenerator({ user }: { user: User }) {
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"
"transition duration-300 ease-in-out",
)}
filterDate={(date) =>
moment(date).isAfter(new Date()) &&
(user.subscriptionExpirationDate
? moment(date).isBefore(user.subscriptionExpirationDate)
: true)
(user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true)
}
dateFormat="dd/MM/yyyy"
selected={expiryDate}
@@ -164,35 +138,25 @@ export default function CodeGenerator({ user }: { user: User }) {
)}
</>
)}
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], 'createCodes') && (
<Button
onClick={() => generateCode(type)}
disabled={isExpiryDateEnabled ? !expiryDate : false}
>
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "createCodes") && (
<Button onClick={() => generateCode(type)} disabled={isExpiryDateEnabled ? !expiryDate : false}>
Generate
</Button>
)}
<label className="font-normal text-base text-mti-gray-dim">
Generated Code:
</label>
<label className="font-normal text-base text-mti-gray-dim">Generated Code:</label>
<div
className={clsx(
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"hover:border-mti-purple tooltip",
"transition duration-300 ease-in-out"
"transition duration-300 ease-in-out",
)}
data-tip="Click to copy"
onClick={() => {
if (generatedCode) navigator.clipboard.writeText(generatedCode);
}}
>
}}>
{generatedCode}
</div>
{generatedCode && (
<span className="text-sm text-mti-gray-dim font-light">
Give this code to the user to complete their registration
</span>
)}
{generatedCode && <span className="text-sm text-mti-gray-dim font-light">Give this code to the user to complete their registration</span>}
</div>
);
}

View File

@@ -4,26 +4,22 @@ import Select from "@/components/Low/Select";
import useCodes from "@/hooks/useCodes";
import useUser from "@/hooks/useUser";
import useUsers from "@/hooks/useUsers";
import { Code, User } from "@/interfaces/user";
import { USER_TYPE_LABELS } from "@/resources/user";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {Code, User} from "@/interfaces/user";
import {USER_TYPE_LABELS} from "@/resources/user";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import axios from "axios";
import moment from "moment";
import { useEffect, useState, useMemo } from "react";
import { BsTrash } from "react-icons/bs";
import { toast } from "react-toastify";
import {useEffect, useState, useMemo} from "react";
import {BsTrash} from "react-icons/bs";
import {toast} from "react-toastify";
import ReactDatePicker from "react-datepicker";
import clsx from "clsx";
import { checkAccess } from "@/utils/permissions";
import {checkAccess} from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions";
const columnHelper = createColumnHelper<Code>();
const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
const CreatorCell = ({id, users}: {id: string; users: User[]}) => {
const [creatorUser, setCreatorUser] = useState<User>();
useEffect(() => {
@@ -32,30 +28,24 @@ const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
return (
<>
{(creatorUser?.type === "corporate"
? creatorUser?.corporateInformation?.companyInformation?.name
: creatorUser?.name || "N/A") || "N/A"}{" "}
{(creatorUser?.type === "corporate" ? creatorUser?.corporateInformation?.companyInformation?.name : creatorUser?.name || "N/A") || "N/A"}{" "}
{creatorUser && `(${USER_TYPE_LABELS[creatorUser.type]})`}
</>
);
};
export default function CodeList({ user }: { user: User }) {
export default function CodeList({user}: {user: User}) {
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(
user?.type === "corporate" ? user : undefined
);
const [filterAvailability, setFilterAvailability] = useState<
"in-use" | "unused"
>();
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(user?.type === "corporate" ? user : undefined);
const [filterAvailability, setFilterAvailability] = useState<"in-use" | "unused">();
const {permissions} = usePermissions(user?.id || "");
// const [filteredCodes, setFilteredCodes] = useState<Code[]>([]);
const { users } = useUsers();
const { codes, reload } = useCodes(
user?.type === "corporate" ? user?.id : undefined
);
const {users} = useUsers();
const {codes, reload} = useCodes(user?.type === "corporate" ? user?.id : undefined);
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
@@ -63,9 +53,9 @@ export default function CodeList({ user }: { user: User }) {
return codes.filter((x) => {
// TODO: if the expiry date is missing, it does not make sense to filter by date
// so we need to find a way to handle this edge case
if(startDate && endDate && x.expiryDate) {
if (startDate && endDate && x.expiryDate) {
const date = moment(x.expiryDate);
if(date.isBefore(startDate) || date.isAfter(endDate)) {
if (date.isBefore(startDate) || date.isAfter(endDate)) {
return false;
}
}
@@ -80,25 +70,17 @@ export default function CodeList({ user }: { user: User }) {
}, [codes, startDate, endDate, filteredCorporate, filterAvailability]);
const toggleCode = (id: string) => {
setSelectedCodes((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
);
setSelectedCodes((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
};
const toggleAllCodes = (checked: boolean) => {
if (checked)
return setSelectedCodes(
filteredCodes.filter((x) => !x.userId).map((x) => x.code)
);
if (checked) return setSelectedCodes(filteredCodes.filter((x) => !x.userId).map((x) => x.code));
return setSelectedCodes([]);
};
const deleteCodes = async (codes: string[]) => {
if (
!confirm(`Are you sure you want to delete these ${codes.length} code(s)?`)
)
return;
if (!confirm(`Are you sure you want to delete these ${codes.length} code(s)?`)) return;
const params = new URLSearchParams();
codes.forEach((code) => params.append("code", code));
@@ -126,8 +108,7 @@ export default function CodeList({ user }: { user: User }) {
};
const deleteCode = async (code: Code) => {
if (!confirm(`Are you sure you want to delete this "${code.code}" code?`))
return;
if (!confirm(`Are you sure you want to delete this "${code.code}" code?`)) return;
axios
.delete(`/api/code/${code.code}`)
@@ -148,11 +129,7 @@ export default function CodeList({ user }: { user: User }) {
.finally(reload);
};
const allowedToDelete = checkAccess(
user,
["developer", "admin", "corporate", "mastercorporate"],
"deleteCodes"
);
const allowedToDelete = checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "deleteCodes");
const defaultColumns = [
columnHelper.accessor("code", {
@@ -161,21 +138,15 @@ export default function CodeList({ user }: { user: User }) {
<Checkbox
disabled={filteredCodes.filter((x) => !x.userId).length === 0}
isChecked={
selectedCodes.length ===
filteredCodes.filter((x) => !x.userId).length &&
filteredCodes.filter((x) => !x.userId).length > 0
selectedCodes.length === filteredCodes.filter((x) => !x.userId).length && filteredCodes.filter((x) => !x.userId).length > 0
}
onChange={(checked) => toggleAllCodes(checked)}
>
onChange={(checked) => toggleAllCodes(checked)}>
{""}
</Checkbox>
),
cell: (info) =>
!info.row.original.userId ? (
<Checkbox
isChecked={selectedCodes.includes(info.getValue())}
onChange={() => toggleCode(info.getValue())}
>
<Checkbox isChecked={selectedCodes.includes(info.getValue())} onChange={() => toggleCode(info.getValue())}>
{""}
</Checkbox>
) : null,
@@ -186,8 +157,7 @@ export default function CodeList({ user }: { user: User }) {
}),
columnHelper.accessor("creationDate", {
header: "Creation Date",
cell: (info) =>
info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A",
cell: (info) => (info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A"),
}),
columnHelper.accessor("email", {
header: "Invited E-mail",
@@ -213,15 +183,11 @@ export default function CodeList({ user }: { user: User }) {
{
header: "",
id: "actions",
cell: ({ row }: { row: { original: Code } }) => {
cell: ({row}: {row: {original: Code}}) => {
return (
<div className="flex gap-4">
{allowedToDelete && !row.original.userId && (
<div
data-tip="Delete"
className="cursor-pointer tooltip"
onClick={() => deleteCode(row.original)}
>
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteCode(row.original)}>
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</div>
)}
@@ -251,8 +217,7 @@ export default function CodeList({ user }: { user: User }) {
? {
label: `${
filteredCorporate.type === "corporate"
? filteredCorporate.corporateInformation
?.companyInformation?.name || filteredCorporate.name
? filteredCorporate.corporateInformation?.companyInformation?.name || filteredCorporate.name
: filteredCorporate.name
} (${USER_TYPE_LABELS[filteredCorporate.type]})`,
value: filteredCorporate.id,
@@ -260,37 +225,25 @@ export default function CodeList({ user }: { user: User }) {
: null
}
options={users
.filter((x) =>
["admin", "developer", "corporate"].includes(x.type)
)
.filter((x) => ["admin", "developer", "corporate"].includes(x.type))
.map((x) => ({
label: `${
x.type === "corporate"
? x.corporateInformation?.companyInformation?.name || x.name
: x.name
} (${USER_TYPE_LABELS[x.type]})`,
label: `${x.type === "corporate" ? x.corporateInformation?.companyInformation?.name || x.name : x.name} (${
USER_TYPE_LABELS[x.type]
})`,
value: x.id,
user: x,
}))}
onChange={(value) =>
setFilteredCorporate(
value ? users.find((x) => x.id === value?.value) : undefined
)
}
onChange={(value) => setFilteredCorporate(value ? users.find((x) => x.id === value?.value) : undefined)}
/>
<Select
className="!w-96 !py-1"
placeholder="Availability"
isClearable
options={[
{ label: "In Use", value: "in-use" },
{ label: "Unused", value: "unused" },
{label: "In Use", value: "in-use"},
{label: "Unused", value: "unused"},
]}
onChange={(value) =>
setFilterAvailability(
value ? (value.value as typeof filterAvailability) : undefined
)
}
onChange={(value) => setFilterAvailability(value ? (value.value as typeof filterAvailability) : undefined)}
/>
<ReactDatePicker
dateFormat="dd/MM/yyyy"
@@ -300,9 +253,7 @@ export default function CodeList({ user }: { user: User }) {
endDate={endDate}
selectsRange
showMonthDropdown
filterDate={(date: Date) =>
moment(date).isSameOrBefore(moment(new Date()))
}
filterDate={(date: Date) => moment(date).isSameOrBefore(moment(new Date()))}
onChange={([initialDate, finalDate]: [Date, Date]) => {
setStartDate(initialDate ?? moment("01/01/2023").toDate());
if (finalDate) {
@@ -323,8 +274,7 @@ export default function CodeList({ user }: { user: User }) {
variant="outline"
color="red"
className="!py-1 px-10"
onClick={() => deleteCodes(selectedCodes)}
>
onClick={() => deleteCodes(selectedCodes)}>
Delete
</Button>
</div>
@@ -336,12 +286,7 @@ export default function CodeList({ user }: { user: User }) {
<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()
)}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
@@ -349,10 +294,7 @@ export default function CodeList({ user }: { user: User }) {
</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}
>
<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())}

View File

@@ -3,39 +3,25 @@ import Input from "@/components/Low/Input";
import Modal from "@/components/Modal";
import useGroups from "@/hooks/useGroups";
import useUsers from "@/hooks/useUsers";
import { CorporateUser, Group, User } from "@/interfaces/user";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {CorporateUser, Group, User} from "@/interfaces/user";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import axios from "axios";
import { capitalize, uniq } from "lodash";
import { useEffect, useState } from "react";
import { BsPencil, BsQuestionCircleFill, BsTrash } from "react-icons/bs";
import {capitalize, uniq} from "lodash";
import {useEffect, useState} from "react";
import {BsPencil, BsQuestionCircleFill, BsTrash} from "react-icons/bs";
import Select from "react-select";
import { toast } from "react-toastify";
import {toast} from "react-toastify";
import readXlsxFile from "read-excel-file";
import { useFilePicker } from "use-file-picker";
import { getUserCorporate } from "@/utils/groups";
import { isAgentUser, isCorporateUser } from "@/resources/user";
import { checkAccess } from "@/utils/permissions";
import {useFilePicker} from "use-file-picker";
import {getUserCorporate} from "@/utils/groups";
import {isAgentUser, isCorporateUser} from "@/resources/user";
import {checkAccess} from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions";
const columnHelper = createColumnHelper<Group>();
const EMAIL_REGEX = new RegExp(
/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/
);
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
const LinkedCorporate = ({
userId,
users,
groups,
}: {
userId: string;
users: User[];
groups: Group[];
}) => {
const LinkedCorporate = ({userId, users, groups}: {userId: string; users: User[]; groups: Group[]}) => {
const [companyName, setCompanyName] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -43,33 +29,19 @@ const LinkedCorporate = ({
const user = users.find((u) => u.id === userId);
if (!user) return setCompanyName("");
if (isCorporateUser(user))
return setCompanyName(
user.corporateInformation?.companyInformation?.name || user.name
);
if (isAgentUser(user))
return setCompanyName(user.agentInformation?.companyName || user.name);
if (isCorporateUser(user)) return setCompanyName(user.corporateInformation?.companyInformation?.name || user.name);
if (isAgentUser(user)) return setCompanyName(user.agentInformation?.companyName || user.name);
const belongingGroups = groups.filter((x) =>
x.participants.includes(userId)
);
const belongingGroupsAdmins = belongingGroups
.map((x) => users.find((u) => u.id === x.admin))
.filter((x) => !!x && isCorporateUser(x));
const belongingGroups = groups.filter((x) => x.participants.includes(userId));
const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x));
if (belongingGroupsAdmins.length === 0) return setCompanyName("");
const admin = belongingGroupsAdmins[0] as CorporateUser;
setCompanyName(
admin.corporateInformation?.companyInformation.name || admin.name
);
setCompanyName(admin.corporateInformation?.companyInformation.name || admin.name);
}, [userId, users, groups]);
return isLoading ? (
<span className="animate-pulse">Loading...</span>
) : (
<>{companyName}</>
);
return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>;
};
interface CreateDialogProps {
@@ -79,17 +51,13 @@ interface CreateDialogProps {
onClose: () => void;
}
const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
const [name, setName] = useState<string | undefined>(
group?.name || undefined
);
const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
const [name, setName] = useState<string | undefined>(group?.name || undefined);
const [admin, setAdmin] = useState<string>(group?.admin || user.id);
const [participants, setParticipants] = useState<string[]>(
group?.participants || []
);
const [participants, setParticipants] = useState<string[]>(group?.participants || []);
const [isLoading, setIsLoading] = useState(false);
const { openFilePicker, filesContent, clear } = useFilePicker({
const {openFilePicker, filesContent, clear} = useFilePicker({
accept: ".xlsx",
multiple: false,
readAs: "ArrayBuffer",
@@ -105,12 +73,9 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
rows
.map((row) => {
const [email] = row as string[];
return EMAIL_REGEX.test(email) &&
!users.map((u) => u.email).includes(email)
? email.toString().trim()
: undefined;
return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email) ? email.toString().trim() : undefined;
})
.filter((x) => !!x)
.filter((x) => !!x),
);
if (emails.length === 0) {
@@ -120,17 +85,12 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
return;
}
const emailUsers = [...new Set(emails)]
.map((x) => users.find((y) => y.email.toLowerCase() === x))
.filter((x) => x !== undefined);
const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined);
const filteredUsers = emailUsers.filter(
(x) =>
((user.type === "developer" ||
user.type === "admin" ||
user.type === "corporate" ||
user.type === "mastercorporate") &&
((user.type === "developer" || user.type === "admin" || user.type === "corporate" || user.type === "mastercorporate") &&
(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));
@@ -138,7 +98,7 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
user.type !== "teacher"
? "Added all teachers and students found in the file you've provided!"
: "Added all students found in the file you've provided!",
{ toastId: "upload-success" }
{toastId: "upload-success"},
);
setIsLoading(false);
});
@@ -150,21 +110,14 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
setIsLoading(true);
if (name !== group?.name && (name === "Students" || name === "Teachers")) {
toast.error(
"That group name is reserved and cannot be used, please enter another one."
);
toast.error("That group name is reserved and cannot be used, please enter another one.");
setIsLoading(false);
return;
}
(group ? axios.patch : axios.post)(
group ? `/api/groups/${group.id}` : "/api/groups",
{ name, admin, participants }
)
(group ? axios.patch : axios.post)(group ? `/api/groups/${group.id}` : "/api/groups", {name, admin, participants})
.then(() => {
toast.success(
`Group "${name}" ${group ? "edited" : "created"} successfully`
);
toast.success(`Group "${name}" ${group ? "edited" : "created"} successfully`);
return true;
})
.catch(() => {
@@ -180,24 +133,11 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
return (
<div className="mt-4 flex w-full flex-col gap-12 px-4 py-2">
<div className="flex flex-col gap-8">
<Input
name="name"
type="text"
label="Name"
defaultValue={name}
onChange={setName}
required
disabled={group?.disableEditing}
/>
<Input name="name" type="text" label="Name" defaultValue={name} onChange={setName} required disabled={group?.disableEditing} />
<div className="flex w-full flex-col gap-3">
<div className="flex items-center gap-2">
<label className="text-mti-gray-dim text-base font-normal">
Participants
</label>
<div
className="tooltip"
data-tip="The Excel file should only include a column with the desired e-mails."
>
<label className="text-mti-gray-dim text-base font-normal">Participants</label>
<div className="tooltip" data-tip="The Excel file should only include a column with the desired e-mails.">
<BsQuestionCircleFill />
</div>
</div>
@@ -206,30 +146,28 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
className="w-full"
value={participants.map((x) => ({
value: x,
label: `${users.find((y) => y.id === x)?.email} - ${
users.find((y) => y.id === x)?.name
}`,
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
}))}
placeholder="Participants..."
defaultValue={participants.map((x) => ({
value: x,
label: `${users.find((y) => y.id === x)?.email} - ${
users.find((y) => y.id === x)?.name
}`,
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
}))}
options={users
.filter((x) =>
user.type === "teacher"
? 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))}
isMulti
isSearchable
menuPortalTarget={document?.body}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
control: (styles) => ({
...styles,
backgroundColor: "white",
@@ -240,36 +178,18 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
}}
/>
{user.type !== "teacher" && (
<Button
className="w-full max-w-[300px]"
onClick={openFilePicker}
isLoading={isLoading}
variant="outline"
>
{filesContent.length === 0
? "Upload participants Excel file"
: filesContent[0].name}
<Button className="w-full max-w-[300px]" onClick={openFilePicker} isLoading={isLoading} variant="outline">
{filesContent.length === 0 ? "Upload participants Excel file" : filesContent[0].name}
</Button>
)}
</div>
</div>
</div>
<div className="mt-8 flex w-full items-center justify-end gap-8">
<Button
variant="outline"
color="red"
className="w-full max-w-[200px]"
isLoading={isLoading}
onClick={onClose}
>
<Button variant="outline" color="red" className="w-full max-w-[200px]" isLoading={isLoading} onClick={onClose}>
Cancel
</Button>
<Button
className="w-full max-w-[200px]"
onClick={submit}
isLoading={isLoading}
disabled={!name}
>
<Button className="w-full max-w-[200px]" onClick={submit} isLoading={isLoading} disabled={!name}>
Submit
</Button>
</div>
@@ -279,22 +199,22 @@ const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => {
const filterTypes = ["corporate", "teacher", "mastercorporate"];
export default function GroupList({ user }: { user: User }) {
export default function GroupList({user}: {user: User}) {
const [isCreating, setIsCreating] = useState(false);
const [editingGroup, setEditingGroup] = useState<Group>();
const [filterByUser, setFilterByUser] = useState(false);
const { users } = useUsers();
const { groups, reload } = useGroups(
user && filterTypes.includes(user?.type) ? user.id : undefined,
user?.type
);
const {permissions} = usePermissions(user?.id || "");
const {users} = useUsers();
const {groups, reload} = useGroups({
admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
userType: user?.type,
adminAdmins: user?.type === "teacher" ? user?.id : undefined,
});
useEffect(() => {
if (
user &&
["corporate", "teacher", "mastercorporate"].includes(user.type)
) {
if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) {
setFilterByUser(true);
}
}, [user]);
@@ -303,7 +223,7 @@ export default function GroupList({ user }: { user: User }) {
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
axios
.delete<{ ok: boolean }>(`/api/groups/${group.id}`)
.delete<{ok: boolean}>(`/api/groups/${group.id}`)
.then(() => toast.success(`Group "${group.name}" deleted successfully`))
.catch(() => toast.error("Something went wrong, please try again later!"))
.finally(reload);
@@ -321,25 +241,14 @@ export default function GroupList({ user }: { user: User }) {
columnHelper.accessor("admin", {
header: "Admin",
cell: (info) => (
<div
className="tooltip"
data-tip={capitalize(
users.find((x) => x.id === info.getValue())?.type
)}
>
<div className="tooltip" data-tip={capitalize(users.find((x) => x.id === info.getValue())?.type)}>
{users.find((x) => x.id === info.getValue())?.name}
</div>
),
}),
columnHelper.accessor("admin", {
header: "Linked Corporate",
cell: (info) => (
<LinkedCorporate
userId={info.getValue()}
users={users}
groups={groups}
/>
),
cell: (info) => <LinkedCorporate userId={info.getValue()} users={users} groups={groups} />,
}),
columnHelper.accessor("participants", {
header: "Participants",
@@ -352,32 +261,18 @@ export default function GroupList({ user }: { user: User }) {
{
header: "",
id: "actions",
cell: ({ row }: { row: { original: Group } }) => {
cell: ({row}: {row: {original: Group}}) => {
return (
<>
{user &&
(checkAccess(user, ["developer", "admin"]) ||
user.id === row.original.admin) && (
{user && (checkAccess(user, ["developer", "admin"]) || user.id === row.original.admin) && (
<div className="flex gap-2">
{(!row.original.disableEditing ||
checkAccess(user, ["developer", "admin"]),
"editGroup") && (
<div
data-tip="Edit"
className="tooltip cursor-pointer"
onClick={() => setEditingGroup(row.original)}
>
{(!row.original.disableEditing || checkAccess(user, ["developer", "admin"]), "editGroup") && (
<div data-tip="Edit" className="tooltip cursor-pointer" onClick={() => setEditingGroup(row.original)}>
<BsPencil className="hover:text-mti-purple-light transition duration-300 ease-in-out" />
</div>
)}
{(!row.original.disableEditing ||
checkAccess(user, ["developer", "admin"]),
"deleteGroup") && (
<div
data-tip="Delete"
className="tooltip cursor-pointer"
onClick={() => deleteGroup(row.original)}
>
{(!row.original.disableEditing || checkAccess(user, ["developer", "admin"]), "deleteGroup") && (
<div data-tip="Delete" className="tooltip cursor-pointer" onClick={() => deleteGroup(row.original)}>
<BsTrash className="hover:text-mti-purple-light transition duration-300 ease-in-out" />
</div>
)}
@@ -403,11 +298,7 @@ export default function GroupList({ user }: { user: User }) {
return (
<div className="h-full w-full rounded-xl">
<Modal
isOpen={isCreating || !!editingGroup}
onClose={closeModal}
title={editingGroup ? `Editing ${editingGroup.name}` : "New Group"}
>
<Modal isOpen={isCreating || !!editingGroup} onClose={closeModal} title={editingGroup ? `Editing ${editingGroup.name}` : "New Group"}>
<CreatePanel
group={editingGroup}
user={user}
@@ -419,8 +310,7 @@ export default function GroupList({ user }: { user: User }) {
groups
.filter((g) => g.admin === user.id)
.flatMap((g) => g.participants)
.includes(u.id) ||
groups.flatMap((g) => g.participants).includes(u.id)
.includes(u.id) || groups.flatMap((g) => g.participants).includes(u.id),
)
: users
}
@@ -432,12 +322,7 @@ export default function GroupList({ user }: { user: User }) {
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th className="py-4" key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
@@ -445,10 +330,7 @@ export default function GroupList({ user }: { user: User }) {
</thead>
<tbody className="px-2">
{table.getRowModel().rows.map((row) => (
<tr
className="even:bg-mti-purple-ultralight/40 rounded-lg py-2 odd:bg-white"
key={row.id}
>
<tr className="even:bg-mti-purple-ultralight/40 rounded-lg py-2 odd:bg-white" key={row.id}>
{row.getVisibleCells().map((cell) => (
<td className="px-4 py-2" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -459,15 +341,10 @@ export default function GroupList({ user }: { user: User }) {
</tbody>
</table>
{checkAccess(
user,
["teacher", "corporate", "mastercorporate", "admin", "developer"],
"createGroup"
) && (
{checkAccess(user, ["teacher", "corporate", "mastercorporate", "admin", "developer"], permissions, "createGroup") && (
<button
onClick={() => setIsCreating(true)}
className="bg-mti-purple-light hover:bg-mti-purple w-full py-2 text-white transition duration-300 ease-in-out"
>
className="bg-mti-purple-light hover:bg-mti-purple w-full py-2 text-white transition duration-300 ease-in-out">
New Group
</button>
)}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { User } from "@/interfaces/user";
import { Tab } from "@headlessui/react";
import {User} from "@/interfaces/user";
import {Tab} from "@headlessui/react";
import clsx from "clsx";
import CodeList from "./CodeList";
import DiscountList from "./DiscountList";
@@ -7,101 +7,86 @@ import ExamList from "./ExamList";
import GroupList from "./GroupList";
import PackageList from "./PackageList";
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}) {
const {permissions} = usePermissions(user?.id || "");
export default function Lists({ user }: { user: User }) {
return (
<Tab.Group>
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
<Tab
className={({ selected }) =>
className={({selected}) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out",
selected
? "bg-white shadow"
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
)
}
>
}>
User List
</Tab>
{checkAccess(user, ["developer"]) && (
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate", "teacher"]) && (
<Tab
className={({ selected }) =>
className={({selected}) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out",
selected
? "bg-white shadow"
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
)
}
>
}>
Exam List
</Tab>
)}
<Tab
className={({ selected }) =>
className={({selected}) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out",
selected
? "bg-white shadow"
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
)
}
>
}>
Group List
</Tab>
{checkAccess(user, ["developer", "admin", "corporate"]) && (
<Tab
className={({ selected }) =>
className={({selected}) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out",
selected
? "bg-white shadow"
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
)
}
>
}>
Code List
</Tab>
)}
{checkAccess(user, ["developer", "admin"]) && (
<Tab
className={({ selected }) =>
className={({selected}) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out",
selected
? "bg-white shadow"
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
)
}
>
}>
Package List
</Tab>
)}
{checkAccess(user, ["developer", "admin"]) && (
<Tab
className={({ selected }) =>
className={({selected}) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out",
selected
? "bg-white shadow"
: "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark"
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
)
}
>
}>
Discount List
</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">
<GroupList user={user} />
</Tab.Panel>
{checkAccess(
user,
["developer", "admin", "corporate", "mastercorporate"],
"viewCodes"
) && (
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"], permissions, "viewCodes") && (
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
<CodeList user={user} />
</Tab.Panel>

View File

@@ -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 [isLoading, setIsLoading] = useState(false);
const [resultingExam, setResultingExam] = useState<LevelExam>();
@@ -420,10 +424,16 @@ const LevelGeneration = () => {
return;
}
if(!id) {
toast.error("Please insert a title before submitting");
return;
}
setIsLoading(true);
const exam = {
...generatedExam,
id,
parts: generatedExam.parts.map((p, i) => ({...p, exercises: parts[i].part!.exercises})),
};

View File

@@ -228,7 +228,11 @@ interface ListeningPart {
| string;
}
const ListeningGeneration = () => {
interface Props {
id: string;
}
const ListeningGeneration = ({ id } : Props) => {
const [part1, setPart1] = useState<ListeningPart>();
const [part2, setPart2] = useState<ListeningPart>();
const [part3, setPart3] = useState<ListeningPart>();
@@ -258,11 +262,16 @@ const ListeningGeneration = () => {
console.log({parts});
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);
axios
.post(`/api/exam/listening/generate/listening`, {
id: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}),
id,
parts,
minTimer,
difficulty,

View File

@@ -258,7 +258,11 @@ const PartTab = ({
);
};
const ReadingGeneration = () => {
interface Props {
id: string;
}
const ReadingGeneration = ({ id } : Props) => {
const [part1, setPart1] = useState<ReadingPart>();
const [part2, setPart2] = useState<ReadingPart>();
const [part3, setPart3] = useState<ReadingPart>();
@@ -300,13 +304,18 @@ const ReadingGeneration = () => {
return;
}
if(!id) {
toast.error("Please insert a title before submitting");
return;
}
setIsLoading(true);
const exam: ReadingExam = {
parts,
isDiagnostic: false,
minTimer,
module: "reading",
id: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}),
id,
type: "academic",
variant: parts.length === 3 ? "full" : "partial",
difficulty,
@@ -328,7 +337,7 @@ const ReadingGeneration = () => {
})
.catch((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));
};

View File

@@ -221,7 +221,11 @@ interface SpeakingPart {
avatar?: (typeof AVATARS)[number];
}
const SpeakingGeneration = () => {
interface Props {
id: string;
}
const SpeakingGeneration = ({ id } : Props) => {
const [part1, setPart1] = useState<SpeakingPart>();
const [part2, setPart2] = useState<SpeakingPart>();
const [part3, setPart3] = useState<SpeakingPart>();
@@ -243,6 +247,11 @@ const SpeakingGeneration = () => {
const submitExam = () => {
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);
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x);
@@ -256,7 +265,7 @@ const SpeakingGeneration = () => {
}));
const exam: SpeakingExam = {
id: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}),
id,
isDiagnostic: false,
exercises: exercises as (SpeakingExercise | InteractiveSpeakingExercise)[],
minTimer,

View File

@@ -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 [task2, setTask2] = useState<string>();
const [minTimer, setMinTimer] = useState(60);
@@ -116,6 +120,11 @@ const WritingGeneration = () => {
return;
}
if(!id) {
toast.error("Please insert a title before submitting");
return;
}
const exercise1 = task1
? ({
id: v4(),
@@ -152,7 +161,7 @@ const WritingGeneration = () => {
minTimer,
module: "writing",
exercises: [...(exercise1 ? [exercise1] : []), ...(exercise2 ? [exercise2] : [])],
id: generate({minLength: 4, maxLength: 8, min: 3, max: 5, join: " ", formatter: capitalize}),
id,
variant: exercise1 && exercise2 ? "full" : "partial",
difficulty,
};

View File

@@ -31,7 +31,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) {
const {packages} = usePackages();
const {discounts} = useDiscounts();
const {users} = useUsers();
const {groups} = useGroups();
const {groups} = useGroups({});
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
useEffect(() => {

View 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);
}

View File

@@ -1,23 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import {
getFirestore,
setDoc,
doc,
query,
collection,
where,
getDocs,
getDoc,
deleteDoc,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Code, Type } from "@/interfaces/user";
import { PERMISSIONS } from "@/constants/userPermissions";
import { uuidv4 } from "@firebase/util";
import { prepareMailer, prepareMailOptions } from "@/email";
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, deleteDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Code, Group, Type} from "@/interfaces/user";
import {PERMISSIONS} from "@/constants/userPermissions";
import {uuidv4} from "@firebase/util";
import {prepareMailer, prepareMailOptions} from "@/email";
const db = getFirestore(app);
@@ -28,22 +18,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
if (req.method === "DELETE") return del(req, res);
return res.status(404).json({ ok: false });
return res.status(404).json({ok: false});
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res
.status(401)
.json({ ok: false, reason: "You must be logged in to generate a code!" });
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
return;
}
const { creator } = req.query as { creator?: string };
const q = query(
collection(db, "codes"),
where("creator", "==", creator || ""),
);
const {creator} = req.query as {creator?: string};
const q = query(collection(db, "codes"), where("creator", "==", creator || ""));
const snapshot = await getDocs(creator ? q : collection(db, "codes"));
res.status(200).json(snapshot.docs.map((doc) => doc.data()));
@@ -51,16 +36,14 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res
.status(401)
.json({ ok: false, reason: "You must be logged in to generate a code!" });
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
return;
}
const { type, codes, infos, expiryDate } = req.body as {
const {type, codes, infos, expiryDate} = req.body as {
type: Type;
codes: string[];
infos?: { email: string; name: string; passport_id?: string }[];
infos?: {email: string; name: string; passport_id?: string}[];
expiryDate: null | Date;
};
const permission = PERMISSIONS.generateCode[type];
@@ -68,23 +51,28 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
if (!permission.includes(req.session.user.type)) {
res.status(403).json({
ok: false,
reason:
"Your account type does not have permissions to generate a code for that type of user!",
reason: "Your account type does not have permissions to generate a code for that type of user!",
});
return;
}
const codesGeneratedByUserSnapshot = await getDocs(
query(collection(db, "codes"), where("creator", "==", req.session.user.id)),
);
const codesGeneratedByUserSnapshot = await getDocs(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) => ({
...x.data(),
}));
})) as Code[];
if (req.session.user.type === "corporate") {
const totalCodes = codesGeneratedByUserSnapshot.docs.length + codes.length;
const allowedCodes =
req.session.user.corporateInformation?.companyInformation.userAmount || 0;
const totalCodes = userCodes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)).length + usersInGroups.length + codes.length;
const allowedCodes = req.session.user.corporateInformation?.companyInformation.userAmount || 0;
if (totalCodes > allowedCodes) {
res.status(403).json({
@@ -108,7 +96,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
};
if (infos && infos.length > index) {
const { email, name, passport_id } = infos[index];
const {email, name, passport_id} = infos[index];
const previousCode = userCodes.find((x) => x.email === email) as Code;
const transport = prepareMailer();
@@ -133,9 +121,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
...codeInformation,
email: email.trim().toLowerCase(),
name: name.trim(),
...(passport_id ? { passport_id: passport_id.trim() } : {}),
...(passport_id ? {passport_id: passport_id.trim()} : {}),
},
{ merge: true },
{merge: true},
);
}
@@ -149,15 +137,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
});
Promise.all(codePromises).then((results) => {
res.status(200).json({ ok: true, valid: results.filter((x) => x).length });
res.status(200).json({ok: true, valid: results.filter((x) => x).length});
});
}
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res
.status(401)
.json({ ok: false, reason: "You must be logged in to generate a code!" });
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
return;
}
@@ -170,5 +156,5 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
await deleteDoc(snapshot.ref);
}
res.status(200).json({ codes });
res.status(200).json({codes});
}

View File

@@ -1,12 +1,21 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, setDoc, doc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Exam, InstructorGender, Variant} from "@/interfaces/exam";
import {getExams} from "@/utils/exams.be";
import {Module} from "@/interfaces";
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import {
getFirestore,
setDoc,
doc,
runTransaction,
collection,
query,
where,
getDocs,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Exam, InstructorGender, Variant } from "@/interfaces/exam";
import { getExams } from "@/utils/exams.be";
import { Module } from "@/interfaces";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
@@ -15,40 +24,66 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await GET(req, res);
if (req.method === "POST") return await POST(req, res);
res.status(404).json({ok: false});
res.status(404).json({ ok: false });
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
res.status(401).json({ ok: false });
return;
}
const {module, avoidRepeated, variant, instructorGender} = req.query as {
const { module, avoidRepeated, variant, instructorGender } = req.query as {
module: Module;
avoidRepeated: string;
variant?: Variant;
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);
}
async function POST(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
res.status(401).json({ ok: false });
return;
}
if (req.session.user.type !== "developer") {
res.status(403).json({ok: false});
res.status(403).json({ ok: false });
return;
}
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()};
await setDoc(doc(db, module, req.body.id), exam);
try {
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);
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 });
}
}

View File

@@ -1,20 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import {
getFirestore,
setDoc,
doc,
query,
collection,
where,
getDocs,
getDoc,
deleteDoc,
limit,
updateDoc,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, deleteDoc, limit, updateDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {v4} from "uuid";
import {Group} from "@/interfaces/user";
import {createUserWithEmailAndPassword, getAuth} from "firebase/auth";
@@ -39,28 +27,27 @@ const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
return res.status(404).json({ ok: false });
return res.status(404).json({ok: false});
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const maker = req.session.user;
if (!maker) {
return res
.status(401)
.json({ ok: false, reason: "You must be logged in to make user!" });
return res.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;
passport_id: string;
type: string,
groupName: string
type: string;
groupName: string;
expiryDate: null | Date;
};
// cleaning data
delete req.body.passport_id;
delete req.body.groupName;
delete req.body.expiryDate;
await createUserWithEmailAndPassword(auth, email.toLowerCase(), passport_id)
.then(async (userCredentials) => {
@@ -71,11 +58,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
bio: "",
type: type,
focus: "academic",
status: "paymentDue",
status: "active",
desiredLevels: DEFAULT_DESIRED_LEVELS,
levels: DEFAULT_LEVELS,
isFirstLogin: false,
isVerified: true
isVerified: true,
registrationDate: new Date(),
subscriptionExpirationDate: expiryDate || null,
};
await setDoc(doc(db, "users", userId), user);
if (type === "corporate") {
@@ -103,50 +92,43 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
disableEditing: true,
};
await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup);
await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup);
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 snapshot = await getDocs(q);
const q = query(collection(db, "groups"), where("admin", "==", maker.id), where("name", "==", groupName.trim()), limit(1))
const snapshot = await getDocs(q)
if(snapshot.empty){
if (snapshot.empty) {
const values = {
id: v4(),
admin: maker.id,
name: groupName.trim(),
participants: [userId],
disableEditing: false,
}
await setDoc(doc(db, "groups", values.id) , values)
}else{
const doc = snapshot.docs[0]
const participants : string[] = doc.get('participants');
if(!participants.includes(userId)){
};
await setDoc(doc(db, "groups", values.id), values);
} else {
const doc = snapshot.docs[0];
const participants: string[] = doc.get("participants");
if (!participants.includes(userId)) {
updateDoc(doc.ref, {
participants: [...participants, userId]
})
participants: [...participants, userId],
});
}
}
}
console.log(`Returning - ${email}`);
return res.status(200).json({ok: true});
})
.catch((error) => {
console.log(`Failing - ${email}`);
console.log(error);
return res.status(401).json({error});
});
return res.status(200).json({ ok: true });
}

View File

@@ -1,9 +1,10 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import { getFirestore, doc, setDoc } from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, doc, setDoc, getDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {getPermissionDoc} from "@/utils/permissions.be";
const db = getFirestore(app);
@@ -11,20 +12,35 @@ export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
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) {
if (!req.session.user) {
res.status(401).json({ ok: false });
res.status(401).json({ok: false});
return;
}
const { id } = req.query as { id: string };
const { users } = req.body;
const {id} = req.query as {id: string};
const {users} = req.body;
try {
await setDoc(doc(db, "permissions", id), { users }, { merge: true });
return res.status(200).json({ ok: true });
await setDoc(doc(db, "permissions", id), {users}, {merge: true});
return res.status(200).json({ok: true});
} catch (err) {
console.error(err);
return res.status(500).json({ ok: false });
return res.status(500).json({ok: false});
}
}

View File

@@ -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
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 docsWithAdmins = docs.map((d) => {

View File

@@ -1,22 +1,12 @@
import { PERMISSIONS } from "@/constants/userPermissions";
import { app, adminApp } from "@/firebase";
import { Group, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import {
collection,
deleteDoc,
doc,
getDoc,
getDocs,
getFirestore,
query,
setDoc,
where,
} from "firebase/firestore";
import { getAuth } from "firebase-admin/auth";
import { withIronSessionApiRoute } from "iron-session/next";
import { NextApiRequest, NextApiResponse } from "next";
import { getPermissions, getPermissionDocs } from "@/utils/permissions.be";
import {PERMISSIONS} from "@/constants/userPermissions";
import {app, adminApp} from "@/firebase";
import {Group, User} from "@/interfaces/user";
import {sessionOptions} from "@/lib/session";
import {collection, deleteDoc, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore";
import {getAuth} from "firebase-admin/auth";
import {withIronSessionApiRoute} from "iron-session/next";
import {NextApiRequest, NextApiResponse} from "next";
import {getPermissions, getPermissionDocs} from "@/utils/permissions.be";
const db = getFirestore(app);
const auth = getAuth(adminApp);
@@ -32,15 +22,15 @@ async function user(req: NextApiRequest, res: NextApiResponse) {
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
res.status(401).json({ok: false});
return;
}
const { id } = req.query as { id: string };
const {id} = req.query as {id: string};
const docUser = await getDoc(doc(db, "users", req.session.user.id));
if (!docUser.exists()) {
res.status(401).json({ ok: false });
res.status(401).json({ok: false});
return;
}
@@ -48,24 +38,16 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
const docTargetUser = await getDoc(doc(db, "users", id));
if (!docTargetUser.exists()) {
res.status(404).json({ ok: false });
res.status(404).json({ok: false});
return;
}
const targetUser = { ...docTargetUser.data(), id: docTargetUser.id } as User;
const targetUser = {...docTargetUser.data(), id: docTargetUser.id} as User;
if (
user.type === "corporate" &&
(targetUser.type === "student" || targetUser.type === "teacher")
) {
res.json({ ok: true });
if (user.type === "corporate" && (targetUser.type === "student" || targetUser.type === "teacher")) {
res.json({ok: true});
const userParticipantGroup = await getDocs(
query(
collection(db, "groups"),
where("participants", "array-contains", id)
)
);
const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id)));
await Promise.all([
...userParticipantGroup.docs
.filter((x) => (x.data() as Group).admin === user.id)
@@ -74,12 +56,10 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
await setDoc(
x.ref,
{
participants: x
.data()
.participants.filter((y: string) => y !== id),
participants: x.data().participants.filter((y: string) => y !== id),
},
{ merge: true }
)
{merge: true},
),
),
]);
@@ -88,26 +68,18 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
const permission = PERMISSIONS.deleteUser[targetUser.type];
if (!permission.list.includes(user.type)) {
res.status(403).json({ ok: false });
res.status(403).json({ok: false});
return;
}
res.json({ ok: true });
res.json({ok: true});
await auth.deleteUser(id);
await deleteDoc(doc(db, "users", id));
const userCodeDocs = await getDocs(
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 userStatsDocs = await getDocs(
query(collection(db, "stats"), where("user", "==", id))
);
const userCodeDocs = await getDocs(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 userStatsDocs = await getDocs(query(collection(db, "stats"), where("user", "==", id)));
await Promise.all([
...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),
},
{ merge: true }
)
{merge: true},
),
),
]);
}
@@ -135,20 +107,16 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
}
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 = {
...userWithPermissions,
...user,
id: req.session.user.id,
lastLogin: new Date(),
};
await req.session.save();
res.json({ ...userWithPermissions, id: req.session.user.id });
res.json({...user, id: req.session.user.id});
} else {
res.status(401).json(undefined);
}

View File

@@ -57,6 +57,7 @@ export default function Generation() {
const { user } = useUser({ redirectTo: "/login" });
const [title, setTitle] = useState<string>("");
return (
<>
<Head>
@@ -73,6 +74,17 @@ export default function Generation() {
<Layout user={user} className="gap-6">
<h1 className="text-2xl font-semibold">Exam Generation</h1>
<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">
Module
</label>
@@ -117,11 +129,11 @@ export default function Generation() {
))}
</RadioGroup>
</div>
{module === "reading" && <ReadingGeneration />}
{module === "listening" && <ListeningGeneration />}
{module === "writing" && <WritingGeneration />}
{module === "speaking" && <SpeakingGeneration />}
{module === "level" && <LevelGeneration />}
{module === "reading" && <ReadingGeneration id={title} />}
{module === "listening" && <ListeningGeneration id={title} />}
{module === "writing" && <WritingGeneration id={title} />}
{module === "speaking" && <SpeakingGeneration id={title} />}
{module === "level" && <LevelGeneration id={title} />}
</Layout>
)}
</>

119
src/pages/groups.tsx Normal file
View 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>
)}
</>
);
}

View File

@@ -390,7 +390,7 @@ interface PaypalPaymentWithUserData extends PaypalPayment {
email: string;
}
const paypalFilterRows = [["email"], ["name"]];
const paypalFilterRows = [["email"], ["name"], ["orderId"], ["value"]];
export default function PaymentRecord() {
const [selectedCorporateUser, setSelectedCorporateUser] = useState<User>();
const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
@@ -414,6 +414,14 @@ export default function PaymentRecord() {
const [endDate, setEndDate] = useState<Date | null>(
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 [commissionTransfer, setCommissionTransfer] = useState<Boolean | null>(
IS_FILE_SUBMITTED_OPTIONS[0].value
@@ -866,11 +874,16 @@ export default function PaymentRecord() {
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;
return { ...p, name: user?.name, email: user?.email };
}),
[paypalPayments, users]
[paypalPayments, users, startDatePaymob, endDatePaymob]
);
const paypalColumns = [
@@ -1469,6 +1482,44 @@ export default function PaymentRecord() {
{renderTable(table as Table<Payment>)}
</Tab.Panel>
<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()}
{renderTable(paypalTable as Table<PaypalPaymentWithUserData>)}
</Tab.Panel>

View File

@@ -55,9 +55,7 @@ interface Props {
}
export default function Page(props: Props) {
const { permissions, user } = props;
return (
<>
<Head>

View File

@@ -1,56 +1,41 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import {
ChangeEvent,
Dispatch,
ReactNode,
SetStateAction,
useEffect,
useRef,
useState,
} from "react";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState} from "react";
import useUser from "@/hooks/useUser";
import { toast, ToastContainer } from "react-toastify";
import {toast, ToastContainer} from "react-toastify";
import Layout from "@/components/High/Layout";
import Input from "@/components/Low/Input";
import Button from "@/components/Low/Button";
import Link from "next/link";
import axios from "axios";
import { ErrorMessage } from "@/constants/errors";
import {ErrorMessage} from "@/constants/errors";
import clsx from "clsx";
import {
CorporateUser,
EmploymentStatus,
EMPLOYMENT_STATUS,
Gender,
User,
DemographicInformation,
} from "@/interfaces/user";
import {CorporateUser, EmploymentStatus, EMPLOYMENT_STATUS, Gender, User, DemographicInformation} from "@/interfaces/user";
import CountrySelect from "@/components/Low/CountrySelect";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import moment from "moment";
import { BsCamera, BsQuestionCircleFill } from "react-icons/bs";
import { USER_TYPE_LABELS } from "@/resources/user";
import {BsCamera, BsQuestionCircleFill} from "react-icons/bs";
import {USER_TYPE_LABELS} from "@/resources/user";
import useGroups from "@/hooks/useGroups";
import useUsers from "@/hooks/useUsers";
import { convertBase64 } from "@/utils";
import { Divider } from "primereact/divider";
import {convertBase64} from "@/utils";
import {Divider} from "primereact/divider";
import GenderInput from "@/components/High/GenderInput";
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
import TimezoneSelect from "@/components/Low/TImezoneSelect";
import Modal from "@/components/Modal";
import { Module } from "@/interfaces";
import {Module} from "@/interfaces";
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
import Select from "@/components/Low/Select";
import { InstructorGender } from "@/interfaces/exam";
import { capitalize } from "lodash";
import {InstructorGender} from "@/interfaces/exam";
import {capitalize} from "lodash";
import TopicModal from "@/components/Medium/TopicModal";
import { v4 } from "uuid";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import {v4} from "uuid";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
if (!user || !user.isVerified) {
@@ -72,7 +57,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}
return {
props: { user: req.session.user },
props: {user: req.session.user},
};
}, sessionOptions);
@@ -81,11 +66,9 @@ interface Props {
mutateUser: Function;
}
const DoubleColumnRow = ({ children }: { children: ReactNode }) => (
<div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>
);
const DoubleColumnRow = ({children}: {children: ReactNode}) => <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 [name, setName] = useState(user.name || "");
const [email, setEmail] = useState(user.email || "");
@@ -94,88 +77,51 @@ function UserProfile({ user, mutateUser }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
const [desiredLevels, setDesiredLevels] = useState<
{ [key in Module]: number } | undefined
>(
checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined
const [desiredLevels, setDesiredLevels] = useState<{[key in Module]: number} | undefined>(
checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined,
);
const [focus, setFocus] = useState<"academic" | "general">(user.focus);
const [country, setCountry] = useState<string>(
user.demographicInformation?.country || ""
);
const [phone, setPhone] = useState<string>(
user.demographicInformation?.phone || ""
);
const [gender, setGender] = useState<Gender | undefined>(
user.demographicInformation?.gender || undefined
);
const [country, setCountry] = useState<string>(user.demographicInformation?.country || "");
const [phone, setPhone] = useState<string>(user.demographicInformation?.phone || "");
const [gender, setGender] = useState<Gender | undefined>(user.demographicInformation?.gender || undefined);
const [employment, setEmployment] = useState<EmploymentStatus | undefined>(
checkAccess(user, ["corporate", "mastercorporate"])
? undefined
: (user.demographicInformation as DemographicInformation)?.employment
checkAccess(user, ["corporate", "mastercorporate"]) ? undefined : (user.demographicInformation as DemographicInformation)?.employment,
);
const [passport_id, setPassportID] = useState<string | undefined>(
checkAccess(user, ["student"])
? (user.demographicInformation as DemographicInformation)?.passport_id
: undefined
checkAccess(user, ["student"]) ? (user.demographicInformation as DemographicInformation)?.passport_id : undefined,
);
const [preferredGender, setPreferredGender] = useState<
InstructorGender | undefined
>(
user.type === "student" || user.type === "developer"
? user.preferredGender || "varied"
: undefined
const [preferredGender, setPreferredGender] = useState<InstructorGender | undefined>(
user.type === "student" || user.type === "developer" ? user.preferredGender || "varied" : undefined,
);
const [preferredTopics, setPreferredTopics] = useState<string[] | undefined>(
user.type === "student" || user.type === "developer"
? user.preferredTopics
: undefined
user.type === "student" || user.type === "developer" ? user.preferredTopics : undefined,
);
const [position, setPosition] = useState<string | undefined>(
user.type === "corporate"
? user.demographicInformation?.position
: 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 [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : 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 [timezone, setTimezone] = useState<string>(
user.demographicInformation?.timezone || moment.tz.guess()
);
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || moment.tz.guess());
const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false);
const { groups } = useGroups();
const { users } = useUsers();
const {groups} = useGroups({});
const {users} = useUsers();
const profilePictureInput = useRef(null);
const expirationDateColor = (date: Date) => {
const momentDate = moment(date);
const today = moment(new Date());
if (today.add(1, "days").isAfter(momentDate))
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(7, "days").isAfter(momentDate))
return "!bg-mti-orange-ultralight border-mti-orange-light";
if (today.add(1, "days").isAfter(momentDate)) 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(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
};
const uploadProfilePicture = async (event: ChangeEvent<HTMLInputElement>) => {
@@ -195,20 +141,15 @@ function UserProfile({ user, mutateUser }: Props) {
}
if (newPassword && !password) {
toast.error(
"To update your password you need to input your current one!"
);
toast.error("To update your password you need to input your current one!");
setIsLoading(false);
return;
}
if (email !== user?.email) {
const userAdmins = groups
.filter((x) => x.participants.includes(user.id))
.map((x) => x.admin);
const userAdmins = groups.filter((x) => x.participants.includes(user.id)).map((x) => x.admin);
const message =
users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate")
.length > 0
users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate").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?"
: "Are you sure you want to update your e-mail address?";
@@ -239,7 +180,7 @@ function UserProfile({ user, mutateUser }: Props) {
passport_id,
timezone,
},
...(user.type === "corporate" ? { corporateInformation } : {}),
...(user.type === "corporate" ? {corporateInformation} : {}),
...(user.type === "agent"
? {
agentInformation: {
@@ -253,7 +194,7 @@ function UserProfile({ user, mutateUser }: Props) {
.then((response) => {
if (response.status === 200) {
toast.success("Your profile has been updated!");
mutateUser((response.data as { user: User }).user);
mutateUser((response.data as {user: User}).user);
setIsLoading(false);
return;
}
@@ -269,9 +210,7 @@ function UserProfile({ user, mutateUser }: Props) {
const ExpirationDate = () => (
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Expiry Date (click to purchase)
</label>
<label className="font-normal text-base text-mti-gray-dim">Expiry Date (click to purchase)</label>
<Link
href="/payment"
className={clsx(
@@ -280,30 +219,22 @@ function UserProfile({ user, mutateUser }: Props) {
!user.subscriptionExpirationDate
? "!bg-mti-green-ultralight !border-mti-green-light"
: expirationDateColor(user.subscriptionExpirationDate),
"bg-white border-mti-gray-platinum"
)}
>
"bg-white border-mti-gray-platinum",
)}>
{!user.subscriptionExpirationDate && "Unlimited"}
{user.subscriptionExpirationDate &&
moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
{user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
</Link>
</div>
);
const TimezoneInput = () => (
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Timezone
</label>
<label className="font-normal text-base text-mti-gray-dim">Timezone</label>
<TimezoneSelect value={timezone} onChange={setTimezone} />
</div>
);
const manualDownloadLink = ["student", "teacher", "corporate"].includes(
user.type
)
? `/manuals/${user.type}.pdf`
: "";
const manualDownloadLink = ["student", "teacher", "corporate"].includes(user.type) ? `/manuals/${user.type}.pdf` : "";
return (
<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 flex-col gap-8 w-full md:w-2/3">
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
<form
className="flex flex-col items-center gap-6 w-full"
onSubmit={(e) => e.preventDefault()}
>
<form className="flex flex-col items-center gap-6 w-full" onSubmit={(e) => e.preventDefault()}>
<DoubleColumnRow>
{user.type !== "corporate" ? (
<Input
@@ -411,9 +339,7 @@ function UserProfile({ user, mutateUser }: Props) {
<DoubleColumnRow>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Country *
</label>
<label className="font-normal text-base text-mti-gray-dim">Country *</label>
<CountrySelect value={country} onChange={setCountry} />
</div>
<Input
@@ -446,37 +372,26 @@ function UserProfile({ user, mutateUser }: Props) {
<Divider />
{desiredLevels &&
["developer", "student"].includes(user.type) && (
{desiredLevels && ["developer", "student"].includes(user.type) && (
<>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Desired Levels
</label>
<label className="font-normal text-base text-mti-gray-dim">Desired Levels</label>
<ModuleLevelSelector
levels={desiredLevels}
setLevels={
setDesiredLevels as Dispatch<
SetStateAction<{ [key in Module]: number }>
>
}
setLevels={setDesiredLevels as Dispatch<SetStateAction<{[key in Module]: number}>>}
/>
</div>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Focus
</label>
<label className="font-normal text-base text-mti-gray-dim">Focus</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-16 w-full">
<button
onClick={() => setFocus("academic")}
className={clsx(
"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",
focus === "academic" &&
"!bg-mti-purple-light !text-white",
"transition duration-300 ease-in-out"
)}
>
focus === "academic" && "!bg-mti-purple-light !text-white",
"transition duration-300 ease-in-out",
)}>
Academic
</button>
<button
@@ -484,11 +399,9 @@ function UserProfile({ user, mutateUser }: Props) {
className={clsx(
"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",
focus === "general" &&
"!bg-mti-purple-light !text-white",
"transition duration-300 ease-in-out"
)}
>
focus === "general" && "!bg-mti-purple-light !text-white",
"transition duration-300 ease-in-out",
)}>
General
</button>
</div>
@@ -496,31 +409,22 @@ function UserProfile({ user, mutateUser }: Props) {
</>
)}
{preferredGender &&
["developer", "student"].includes(user.type) && (
{preferredGender && ["developer", "student"].includes(user.type) && (
<>
<Divider />
<DoubleColumnRow>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Speaking Instructor&apos;s Gender
</label>
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor&apos;s Gender</label>
<Select
value={{
value: preferredGender,
label: capitalize(preferredGender),
}}
onChange={(value) =>
value
? setPreferredGender(
value.value as InstructorGender
)
: null
}
onChange={(value) => (value ? setPreferredGender(value.value as InstructorGender) : null)}
options={[
{ value: "male", label: "Male" },
{ value: "female", label: "Female" },
{ value: "varied", label: "Varied" },
{value: "male", label: "Male"},
{value: "female", label: "Female"},
{value: "varied", label: "Varied"},
]}
/>
</div>
@@ -529,18 +433,12 @@ function UserProfile({ user, mutateUser }: Props) {
Preferred Topics{" "}
<span
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 />
</span>
</label>
<Button
className="w-full"
variant="outline"
onClick={() => setIsPreferredTopicsOpen(true)}
>
Select Topics ({preferredTopics?.length || "All"}{" "}
selected)
<Button className="w-full" variant="outline" onClick={() => setIsPreferredTopicsOpen(true)}>
Select Topics ({preferredTopics?.length || "All"} selected)
</Button>
</div>
</DoubleColumnRow>
@@ -565,9 +463,7 @@ function UserProfile({ user, mutateUser }: Props) {
name="companyUsers"
onChange={() => null}
label="Number of users"
defaultValue={
user.corporateInformation.companyInformation.userAmount
}
defaultValue={user.corporateInformation.companyInformation.userAmount}
disabled
required
/>
@@ -611,20 +507,14 @@ function UserProfile({ user, mutateUser }: Props) {
</>
)}
{user.type === "corporate" &&
user.corporateInformation.referralAgent && (
{user.type === "corporate" && user.corporateInformation.referralAgent && (
<>
<Divider />
<DoubleColumnRow>
<Input
name="agentName"
onChange={() => null}
defaultValue={
users.find(
(x) =>
x.id === user.corporateInformation.referralAgent
)?.name
}
defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.name}
type="text"
label="Country Manager's Name"
placeholder="Not available"
@@ -634,12 +524,7 @@ function UserProfile({ user, mutateUser }: Props) {
<Input
name="agentEmail"
onChange={() => null}
defaultValue={
users.find(
(x) =>
x.id === user.corporateInformation.referralAgent
)?.email
}
defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.email}
type="text"
label="Country Manager's E-mail"
placeholder="Not available"
@@ -649,15 +534,11 @@ function UserProfile({ user, mutateUser }: Props) {
</DoubleColumnRow>
<DoubleColumnRow>
<div className="flex flex-col gap-2 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Country Manager&apos;s Country *
</label>
<label className="font-normal text-base text-mti-gray-dim">Country Manager&apos;s Country *</label>
<CountrySelect
value={
users.find(
(x) =>
x.id === user.corporateInformation.referralAgent
)?.demographicInformation?.country
users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation
?.country
}
onChange={() => null}
disabled
@@ -671,10 +552,7 @@ function UserProfile({ user, mutateUser }: Props) {
onChange={() => null}
placeholder="Not available"
defaultValue={
users.find(
(x) =>
x.id === user.corporateInformation.referralAgent
)?.demographicInformation?.phone
users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation?.phone
}
disabled
required
@@ -685,10 +563,7 @@ function UserProfile({ user, mutateUser }: Props) {
{user.type !== "corporate" && (
<DoubleColumnRow>
<EmploymentStatusInput
value={employment}
onChange={setEmployment}
/>
<EmploymentStatusInput value={employment} onChange={setEmployment} />
<div className="flex flex-col gap-8 w-full">
<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-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={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",
"transition ease-in-out duration-300"
)}
>
"transition ease-in-out duration-300",
)}>
<BsCamera className="text-6xl text-mti-purple-ultralight/80" />
</div>
<img
src={profilePicture}
alt={user.name}
className="aspect-square drop-shadow-xl self-end object-cover"
/>
<img src={profilePicture} alt={user.name} className="aspect-square drop-shadow-xl self-end object-cover" />
</div>
<input
type="file"
className="hidden"
onChange={uploadProfilePicture}
accept="image/*"
ref={profilePictureInput}
/>
<input type="file" className="hidden" onChange={uploadProfilePicture} accept="image/*" ref={profilePictureInput} />
<span
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
</span>
<h6 className="font-normal text-base text-mti-gray-taupe">
{USER_TYPE_LABELS[user.type]}
</h6>
<h6 className="font-normal text-base text-mti-gray-taupe">{USER_TYPE_LABELS[user.type]}</h6>
</div>
{user.type === "agent" && (
<div className="flag items-center h-fit">
<img
alt={
user.demographicInformation?.country.toLowerCase() + "_flag"
}
alt={user.demographicInformation?.country.toLowerCase() + "_flag"}
src={`https://flagcdn.com/w320/${user.demographicInformation?.country.toLowerCase()}.png`}
width="320"
/>
</div>
)}
{manualDownloadLink && (
<a
href={manualDownloadLink}
className="max-w-[200px] self-end w-full"
download
>
<Button
color="purple"
variant="outline"
className="max-w-[200px] self-end w-full"
>
<a href={manualDownloadLink} className="max-w-[200px] self-end w-full" download>
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
Download Manual
</Button>
</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">
<Link href="/" className="max-w-[200px] self-end w-full">
<Button
color="purple"
variant="outline"
className="max-w-[200px] self-end w-full"
>
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
Back
</Button>
</Link>
<Button
color="purple"
className="max-w-[200px] self-end w-full"
onClick={updateUser}
disabled={isLoading}
>
<Button color="purple" className="max-w-[200px] self-end w-full" onClick={updateUser} disabled={isLoading}>
Save Changes
</Button>
</div>
@@ -798,7 +639,7 @@ function UserProfile({ user, mutateUser }: Props) {
}
export default function Home() {
const { user, mutateUser } = useUser({ redirectTo: "/login" });
const {user, mutateUser} = useUser({redirectTo: "/login"});
return (
<>

View File

@@ -1,30 +1,29 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Stat, User } from "@/interfaces/user";
import { useEffect, useRef, useState } from "react";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Stat, User} from "@/interfaces/user";
import {useEffect, useRef, useState} from "react";
import useStats from "@/hooks/useStats";
import { groupByDate } from "@/utils/stats";
import {groupByDate} from "@/utils/stats";
import moment from "moment";
import useUsers from "@/hooks/useUsers";
import useExamStore from "@/stores/examStore";
import { ToastContainer } from "react-toastify";
import { useRouter } from "next/router";
import {ToastContainer} from "react-toastify";
import {useRouter} from "next/router";
import Layout from "@/components/High/Layout";
import clsx from "clsx";
import Select from "@/components/Low/Select";
import useGroups from "@/hooks/useGroups";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import useAssignments from "@/hooks/useAssignments";
import { uuidv4 } from "@firebase/util";
import { usePDFDownload } from "@/hooks/usePDFDownload";
import {uuidv4} from "@firebase/util";
import {usePDFDownload} from "@/hooks/usePDFDownload";
import useRecordStore from "@/stores/recordStore";
import useTrainingContentStore from "@/stores/trainingContentStore";
import StatsGridItem from "@/components/StatGridItem";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
if (!user || !user.isVerified) {
@@ -46,7 +45,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}
return {
props: { user: req.session.user },
props: {user: req.session.user},
};
}, sessionOptions);
@@ -55,16 +54,21 @@ const defaultSelectableCorporate = {
label: "All",
};
export default function History({ user }: { user: User }) {
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.training, state.setTraining]);
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] = 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 { assignments } = useAssignments({});
const {assignments} = useAssignments({});
const { users } = useUsers();
const { stats, isLoading: isStatsLoading } = useStats(statsUserId);
const { groups: allGroups } = useGroups();
const {users} = useUsers();
const {stats, isLoading: isStatsLoading} = useStats(statsUserId);
const {groups: allGroups} = useGroups({});
const groups = allGroups.filter((x) => x.admin === user.id);
@@ -104,12 +108,12 @@ export default function History({ user }: { user: User }) {
setFilter((prev) => (prev === value ? undefined : value));
};
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
if (filter && filter !== "assignments") {
const filterDate = moment()
.subtract({ [filter as string]: 1 })
.subtract({[filter as string]: 1})
.format("x");
const filteredStats: { [key: string]: Stat[] } = {};
const filteredStats: {[key: string]: Stat[]} = {};
Object.keys(stats).forEach((timestamp) => {
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
@@ -118,7 +122,7 @@ export default function History({ user }: { user: User }) {
}
if (filter && filter === "assignments") {
const filteredStats: { [key: string]: Stat[] } = {};
const filteredStats: {[key: string]: Stat[]} = {};
Object.keys(stats).forEach((timestamp) => {
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
@@ -137,13 +141,13 @@ export default function History({ user }: { user: User }) {
useEffect(() => {
const handleRouteChange = (url: string) => {
setTraining(false)
}
router.events.on('routeChangeStart', handleRouteChange)
setTraining(false);
};
router.events.on("routeChangeStart", handleRouteChange);
return () => {
router.events.off('routeChangeStart', handleRouteChange)
}
}, [router.events, setTraining])
router.events.off("routeChangeStart", handleRouteChange);
};
}, [router.events, setTraining]);
const handleTrainingContentSubmission = () => {
if (groupedStats) {
@@ -156,11 +160,10 @@ export default function History({ user }: { user: User }) {
}
return accumulator;
}, {});
setTrainingStats(Object.values(selectedStats).flat())
setTrainingStats(Object.values(selectedStats).flat());
router.push("/training");
}
}
};
const customContent = (timestamp: string) => {
if (!groupedStats) return <></>;
@@ -272,7 +275,7 @@ export default function History({ user }: { user: User }) {
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
onChange={(value) => setSelectedCorporate(value?.value || "")}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -289,7 +292,7 @@ export default function History({ user }: { user: User }) {
value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -313,7 +316,7 @@ export default function History({ user }: { user: User }) {
value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -323,10 +326,12 @@ export default function History({ user }: { user: User }) {
/>
</>
)}
{(training && (
{training && (
<div className="flex flex-row">
<div className="font-semibold text-2xl mr-4">Select up to 10 exercises
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}</div>
<div className="font-semibold text-2xl mr-4">
Select up to 10 exercises
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}
</div>
<button
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",
@@ -337,7 +342,7 @@ export default function History({ user }: { user: User }) {
Submit
</button>
</div>
))}
)}
</div>
<div className="flex gap-4 w-full justify-center xl:justify-end">
<button

View File

@@ -14,7 +14,8 @@ import BatchCodeGenerator from "./(admin)/BatchCodeGenerator";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import ExamGenerator from "./(admin)/ExamGenerator";
import BatchCreateUser from "./(admin)/BatchCreateUser";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
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 {
redirect: {
destination: "/",
@@ -43,6 +44,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
export default function Admin() {
const {user} = useUser({redirectTo: "/login"});
const {permissions} = usePermissions(user?.id || "");
return (
<>
@@ -58,10 +60,10 @@ export default function Admin() {
<ToastContainer />
{user && (
<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 />
<BatchCreateUser user={user} />
{checkAccess(user, getTypesOfUser(["teacher"]), 'viewCodes') && (
{checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && (
<>
<CodeGenerator user={user} />
<BatchCodeGenerator user={user} />

View File

@@ -71,7 +71,7 @@ export default function Stats() {
const {user} = useUser({redirectTo: "/login"});
const {users} = useUsers();
const {groups} = useGroups(user?.id);
const {groups} = useGroups({admin: user?.id});
const {stats} = useStats(statsUserId, !statsUserId);
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
className="w-full"
options={users

View File

@@ -1,14 +1,14 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Stat, User } from "@/interfaces/user";
import { ToastContainer } from "react-toastify";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Stat, User} from "@/interfaces/user";
import {ToastContainer} from "react-toastify";
import Layout from "@/components/High/Layout";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { use, useEffect, useState } from "react";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import {use, useEffect, useState} from "react";
import clsx from "clsx";
import { FaPlus } from "react-icons/fa";
import {FaPlus} from "react-icons/fa";
import useRecordStore from "@/stores/recordStore";
import router from "next/router";
import useTrainingContentStore from "@/stores/trainingContentStore";
@@ -16,13 +16,13 @@ import axios from "axios";
import Select from "@/components/Low/Select";
import useUsers from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups";
import { ITrainingContent } from "@/training/TrainingInterfaces";
import {ITrainingContent} from "@/training/TrainingInterfaces";
import moment from "moment";
import { uuidv4 } from "@firebase/util";
import {uuidv4} from "@firebase/util";
import TrainingScore from "@/training/TrainingScore";
import ModuleBadge from "@/components/ModuleBadge";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
if (!user || !user.isVerified) {
@@ -44,7 +44,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}
return {
props: { user: req.session.user },
props: {user: req.session.user},
};
}, sessionOptions);
@@ -53,12 +53,16 @@ const defaultSelectableCorporate = {
label: "All",
};
const Training: React.FC<{ user: User }> = ({ user }) => {
const Training: React.FC<{user: User}> = ({user}) => {
// Record stuff
const { users } = useUsers();
const {users} = useUsers();
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.setTraining]);
const { groups: allGroups } = useGroups();
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [
state.selectedUser,
state.setSelectedUser,
state.setTraining,
]);
const {groups: allGroups} = useGroups({});
const groups = allGroups.filter((x) => x.admin === user.id);
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
@@ -70,22 +74,22 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
const [trainingContent, setTrainingContent] = useState<ITrainingContent[]>([]);
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{ [key: string]: ITrainingContent }>();
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{[key: string]: ITrainingContent}>();
useEffect(() => {
const handleRouteChange = (url: string) => {
setTrainingStats([])
}
router.events.on('routeChangeStart', handleRouteChange)
setTrainingStats([]);
};
router.events.on("routeChangeStart", handleRouteChange);
return () => {
router.events.off('routeChangeStart', handleRouteChange)
}
}, [router.events, setTrainingStats])
router.events.off("routeChangeStart", handleRouteChange);
};
}, [router.events, setTrainingStats]);
useEffect(() => {
const postStats = async () => {
try {
const response = await axios.post<{ id: string }>(`/api/training`, stats);
const response = await axios.post<{id: string}>(`/api/training`, stats);
return response.data.id;
} catch (error) {
setIsNewContentLoading(false);
@@ -93,19 +97,19 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
};
if (isNewContentLoading) {
postStats().then(id => {
postStats().then((id) => {
setTrainingStats([]);
if (id) {
router.push(`/training/${id}`)
router.push(`/training/${id}`);
}
});
}
}, [isNewContentLoading])
}, [isNewContentLoading]);
useEffect(() => {
const loadTrainingContent = async () => {
try {
const response = await axios.get<ITrainingContent[]>('/api/training');
const response = await axios.get<ITrainingContent[]>("/api/training");
setTrainingContent(response.data);
setIsLoading(false);
} catch (error) {
@@ -118,16 +122,15 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
const handleNewTrainingContent = () => {
setRecordTraining(true);
router.push('/record')
}
router.push("/record");
};
const filterTrainingContentByDate = (trainingContent: { [key: string]: ITrainingContent }) => {
const filterTrainingContentByDate = (trainingContent: {[key: string]: ITrainingContent}) => {
if (filter) {
const filterDate = moment()
.subtract({ [filter as string]: 1 })
.subtract({[filter as string]: 1})
.format("x");
const filteredTrainingContent: { [key: string]: ITrainingContent } = {};
const filteredTrainingContent: {[key: string]: ITrainingContent} = {};
Object.keys(trainingContent).forEach((timestamp) => {
if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp];
@@ -142,12 +145,11 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
const grouped = trainingContent.reduce((acc, content) => {
acc[content.created_at] = content;
return acc;
}, {} as { [key: number]: ITrainingContent });
}, {} as {[key: number]: ITrainingContent});
setGroupedByTrainingContent(grouped);
}
}, [trainingContent])
}, [trainingContent]);
// Record Stuff
const selectableCorporates = [
@@ -213,21 +215,20 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
};
const selectTrainingContent = (trainingContent: ITrainingContent) => {
router.push(`/training/${trainingContent.id}`)
router.push(`/training/${trainingContent.id}`);
};
const trainingContentContainer = (timestamp: string) => {
if (!groupedByTrainingContent) return <></>;
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 (
<>
<div
key={uuidv4()}
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)}
role="button">
@@ -243,10 +244,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
</div>
</div>
</div>
<TrainingScore
trainingContent={trainingContent}
gridView={true}
/>
<TrainingScore trainingContent={trainingContent} gridView={true} />
</div>
</>
);
@@ -266,12 +264,12 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
<ToastContainer />
<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">
<span className="loading loading-infinity w-32 bg-mti-green-light" />
{isNewContentLoading && (<span className="text-center text-2xl font-bold text-mti-green-light">
Assessing your exams, please be patient...
</span>)}
{isNewContentLoading && (
<span className="text-center text-2xl font-bold text-mti-green-light">Assessing your exams, please be patient...</span>
)}
</div>
) : (
<>
@@ -286,7 +284,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
onChange={(value) => setSelectedCorporate(value?.value || "")}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -303,7 +301,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -327,7 +325,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -337,7 +335,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
/>
</>
)}
{(user.type === "student" && (
{user.type === "student" && (
<>
<div className="flex items-center">
<div className="font-semibold text-2xl">Generate New Training Material</div>
@@ -351,7 +349,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
</button>
</div>
</>
))}
)}
</div>
<div className="flex gap-4 w-full justify-center xl:justify-end">
<button
@@ -396,10 +394,10 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
</div>
)}
</>
))}
)}
</Layout>
</>
);
}
};
export default Training;

View File

@@ -5,17 +5,17 @@ export const USER_TYPE_LABELS: {[key in Type]: string} = {
teacher: "Teacher",
corporate: "Corporate",
agent: "Country Manager",
admin: "Admin",
admin: "Super Admin",
developer: "Developer",
mastercorporate: "Master Corporate"
mastercorporate: "Master Corporate",
};
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 {
return (user as AgentUser).agentInformation !== undefined;
return (user as AgentUser)?.agentInformation !== undefined;
}
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;
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;
}

View 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();
};

View File

@@ -1,14 +1,12 @@
import { app } from "@/firebase";
import { CorporateUser, StudentUser, TeacherUser } from "@/interfaces/user";
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore";
import {app} from "@/firebase";
import {CorporateUser, Group, StudentUser, TeacherUser} from "@/interfaces/user";
import {collection, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore";
import moment from "moment";
import {getUser} from "./users.be";
const db = getFirestore(app);
export const updateExpiryDateOnGroup = async (
participantID: string,
corporateID: string,
) => {
export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => {
const corporateRef = await getDoc(doc(db, "users", corporateID));
const participantRef = await getDoc(doc(db, "users", participantID));
@@ -18,36 +16,36 @@ export const updateExpiryDateOnGroup = async (
...corporateRef.data(),
id: corporateRef.id,
} as CorporateUser;
const participant = { ...participantRef.data(), id: participantRef.id } as
| StudentUser
| TeacherUser;
const participant = {...participantRef.data(), id: participantRef.id} as StudentUser | TeacherUser;
if (
corporate.type !== "corporate" ||
(participant.type !== "student" && participant.type !== "teacher")
)
return;
if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return;
if (
!corporate.subscriptionExpirationDate ||
!participant.subscriptionExpirationDate
) {
return await setDoc(
doc(db, "users", participant.id),
{ subscriptionExpirationDate: null },
{ merge: true },
);
if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate) {
return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: null}, {merge: true});
}
const corporateDate = moment(corporate.subscriptionExpirationDate);
const participantDate = moment(participant.subscriptionExpirationDate);
if (corporateDate.isAfter(participantDate))
return await setDoc(
doc(db, "users", participant.id),
{ subscriptionExpirationDate: corporateDate.toISOString() },
{ merge: true },
);
return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: corporateDate.toISOString()}, {merge: true});
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[];
};

View File

@@ -16,12 +16,13 @@ import {
Permission,
PermissionType,
permissions,
PermissionTopic,
} from "@/interfaces/permissions";
import {v4} from "uuid";
import { v4 } from "uuid";
const db = getFirestore(app);
async function createPermission(type: string) {
async function createPermission(topic: string, type: string) {
const permData = doc(db, "permissions", v4());
const permDoc = await getDoc(permData);
if (permDoc.exists()) {
@@ -30,9 +31,14 @@ async function createPermission(type: string) {
await setDoc(permData, {
type,
topic,
users: [],
});
}
interface PermissionsHelperList {
topic: string;
type: string;
}
export function getPermissions(userId: string | undefined, docs: Permission[]) {
if (!userId) {
return [];
@@ -52,8 +58,18 @@ export function getPermissions(userId: string | undefined, docs: Permission[]) {
}
export async function bootstrap() {
await permissions.forEach(async (type) => {
await createPermission(type);
await permissions
.reduce((accm: PermissionsHelperList[], permissionData) => {
return [
...accm,
...permissionData.list.map((type) => ({
topic: permissionData.topic,
type,
})),
];
}, [])
.forEach(async ({ topic, type }) => {
await createPermission(topic, type);
});
}

View File

@@ -1,11 +1,8 @@
import { PermissionType } from "@/interfaces/permissions";
import { User, Type, userTypes } from "@/interfaces/user";
import {PermissionType} from "@/interfaces/permissions";
import {User, Type, userTypes} from "@/interfaces/user";
import axios from "axios";
export function checkAccess(
user: User,
types: Type[],
permission?: PermissionType
) {
export function checkAccess(user: User, types: Type[], permissions?: PermissionType[], permission?: PermissionType) {
if (!user) {
return false;
}
@@ -29,7 +26,7 @@ export function checkAccess(
if (permission) {
// this works more like a blacklist
// 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;
}
}
@@ -41,5 +38,5 @@ export function getTypesOfUser(types: Type[]) {
// basicly generate a list of all types except the excluded ones
return userTypes.filter((userType) => {
return !types.includes(userType);
})
});
}

View File

@@ -1,5 +1,6 @@
import {Module} from "@/interfaces";
import {LevelScore} from "@/constants/ielts";
import {Stat, User} from "@/interfaces/user";
type Type = "academic" | "general";
@@ -178,3 +179,28 @@ export const getLevelLabel = (level: number) => {
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);
};

View File

@@ -1,7 +1,7 @@
import { app } from "@/firebase";
import {app} from "@/firebase";
import { collection, getDocs, getFirestore } from "firebase/firestore";
import { User } from "@/interfaces/user";
import {collection, doc, getDoc, getDocs, getFirestore} from "firebase/firestore";
import {User} from "@/interfaces/user";
const db = getFirestore(app);
export async function getUsers() {
@@ -12,3 +12,9 @@ export async function getUsers() {
...doc.data(),
})) as User[];
}
export async function getUser(id: string) {
const userDoc = await getDoc(doc(db, "users", id));
return {...userDoc.data(), id} as User;
}

View File

@@ -25,7 +25,10 @@ export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
country: user.demographicInformation?.country || "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",
verified: user.isVerified?.toString() || "FALSE",
}));
@@ -34,3 +37,9 @@ export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group
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;
};