Merged in master-corporate (pull request #54)
Master corporate Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -104,7 +104,7 @@ export default function MobileMenu({isOpen, onClose, path, user, disableNavigati
|
|||||||
)}>
|
)}>
|
||||||
Record
|
Record
|
||||||
</Link>
|
</Link>
|
||||||
{["admin", "developer", "agent", "corporate"].includes(user.type) && (
|
{["admin", "developer", "agent", "corporate", "mastercorporate"].includes(user.type) && (
|
||||||
<Link
|
<Link
|
||||||
href={disableNavigation ? "" : "/payment-record"}
|
href={disableNavigation ? "" : "/payment-record"}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -115,7 +115,7 @@ export default function MobileMenu({isOpen, onClose, path, user, disableNavigati
|
|||||||
Payment Record
|
Payment Record
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{["admin", "developer", "corporate", "teacher"].includes(user.type) && (
|
{["admin", "developer", "corporate", "teacher", "mastercorporate"].includes(user.type) && (
|
||||||
<Link
|
<Link
|
||||||
href={disableNavigation ? "" : "/settings"}
|
href={disableNavigation ? "" : "/settings"}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{["admin", "developer", "agent", "corporate"].includes(userType || "") && (
|
{["admin", "developer", "agent", "corporate", "mastercorporate"].includes(userType || "") && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsCurrencyDollar}
|
Icon={BsCurrencyDollar}
|
||||||
@@ -132,7 +132,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
isMinimized={isMinimized}
|
isMinimized={isMinimized}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{["admin", "developer", "corporate", "teacher"].includes(userType || "") && (
|
{["admin", "developer", "corporate", "teacher", "mastercorporate"].includes(userType || "") && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsShieldFill}
|
Icon={BsShieldFill}
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
<div className="flex flex-col md:flex-row gap-8 w-full">
|
||||||
{user.type !== "corporate" && (
|
{user.type !== "corporate" && user.type !== 'mastercorporate' && (
|
||||||
<div className="relative flex flex-col gap-3 w-full">
|
<div className="relative flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Employment Status</label>
|
<label className="font-normal text-base text-mti-gray-dim">Employment Status</label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ import {Type} from "@/interfaces/user";
|
|||||||
|
|
||||||
export const PERMISSIONS = {
|
export const PERMISSIONS = {
|
||||||
generateCode: {
|
generateCode: {
|
||||||
student: ["corporate", "developer", "admin"],
|
student: ["corporate", "developer", "admin", "mastercorporate"],
|
||||||
teacher: ["corporate", "developer", "admin"],
|
teacher: ["corporate", "developer", "admin", "mastercorporate"],
|
||||||
corporate: ["admin", "developer"],
|
corporate: ["admin", "developer"],
|
||||||
|
mastercorporate: ["admin", "developer"],
|
||||||
|
|
||||||
admin: ["developer", "admin"],
|
admin: ["developer", "admin"],
|
||||||
agent: ["developer", "admin"],
|
agent: ["developer", "admin"],
|
||||||
developer: ["developer"],
|
developer: ["developer"],
|
||||||
},
|
},
|
||||||
deleteUser: {
|
deleteUser: {
|
||||||
student: ["corporate", "developer", "admin"],
|
student: ["corporate", "developer", "admin", "mastercorporate"],
|
||||||
teacher: ["corporate", "developer", "admin"],
|
teacher: ["corporate", "developer", "admin", "mastercorporate"],
|
||||||
corporate: ["admin", "developer"],
|
corporate: ["admin", "developer"],
|
||||||
|
mastercorporate: ["admin", "developer"],
|
||||||
|
|
||||||
admin: ["developer", "admin"],
|
admin: ["developer", "admin"],
|
||||||
agent: ["developer", "admin"],
|
agent: ["developer", "admin"],
|
||||||
developer: ["developer"],
|
developer: ["developer"],
|
||||||
@@ -20,7 +24,10 @@ export const PERMISSIONS = {
|
|||||||
updateUser: {
|
updateUser: {
|
||||||
student: ["developer", "admin"],
|
student: ["developer", "admin"],
|
||||||
teacher: ["developer", "admin"],
|
teacher: ["developer", "admin"],
|
||||||
|
|
||||||
corporate: ["admin", "developer"],
|
corporate: ["admin", "developer"],
|
||||||
|
mastercorporate: ["admin", "developer"],
|
||||||
|
|
||||||
admin: ["developer", "admin"],
|
admin: ["developer", "admin"],
|
||||||
agent: ["developer", "admin"],
|
agent: ["developer", "admin"],
|
||||||
developer: ["developer"],
|
developer: ["developer"],
|
||||||
@@ -29,6 +36,8 @@ export const PERMISSIONS = {
|
|||||||
student: ["developer", "admin"],
|
student: ["developer", "admin"],
|
||||||
teacher: ["developer", "admin"],
|
teacher: ["developer", "admin"],
|
||||||
corporate: ["admin", "developer"],
|
corporate: ["admin", "developer"],
|
||||||
|
mastercorporate: ["admin", "developer"],
|
||||||
|
|
||||||
admin: ["developer", "admin"],
|
admin: ["developer", "admin"],
|
||||||
agent: ["developer", "admin"],
|
agent: ["developer", "admin"],
|
||||||
developer: ["developer"],
|
developer: ["developer"],
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import GroupList from "@/pages/(admin)/Lists/GroupList";
|
|||||||
import useFilterStore from "@/stores/listFilterStore";
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useCodes from "@/hooks/useCodes";
|
import useCodes from "@/hooks/useCodes";
|
||||||
|
import { getUserCorporate } from "@/utils/groups";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: CorporateUser;
|
user: CorporateUser;
|
||||||
@@ -44,6 +45,8 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
const [page, setPage] = useState("");
|
const [page, setPage] = useState("");
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [corporateUserToShow, setCorporateUserToShow] =
|
||||||
|
useState<CorporateUser>();
|
||||||
|
|
||||||
const { stats } = useStats();
|
const { stats } = useStats();
|
||||||
const { users, reload } = useUsers();
|
const { users, reload } = useUsers();
|
||||||
@@ -57,6 +60,11 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
}, [selectedUser, page]);
|
}, [selectedUser, page]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// in this case it fetches the master corporate account
|
||||||
|
getUserCorporate(user.id).then(setCorporateUserToShow);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
const studentFilter = (user: User) =>
|
const studentFilter = (user: User) =>
|
||||||
user.type === "student" &&
|
user.type === "student" &&
|
||||||
groups.flatMap((g) => g.participants).includes(user.id);
|
groups.flatMap((g) => g.participants).includes(user.id);
|
||||||
@@ -200,6 +208,15 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
|
|
||||||
const DefaultDashboard = () => (
|
const DefaultDashboard = () => (
|
||||||
<>
|
<>
|
||||||
|
{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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
|
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("students")}
|
onClick={() => setPage("students")}
|
||||||
|
|||||||
424
src/dashboards/MasterCorporate.tsx
Normal file
424
src/dashboards/MasterCorporate.tsx
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
|
import useStats from "@/hooks/useStats";
|
||||||
|
import useUsers from "@/hooks/useUsers";
|
||||||
|
import { Group, MasterCorporateUser, Stat, User } from "@/interfaces/user";
|
||||||
|
import UserList from "@/pages/(admin)/Lists/UserList";
|
||||||
|
import { dateSorter } from "@/utils";
|
||||||
|
import moment from "moment";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
BsArrowLeft,
|
||||||
|
BsClipboard2Data,
|
||||||
|
BsClock,
|
||||||
|
BsPaperclip,
|
||||||
|
BsPersonFill,
|
||||||
|
BsPencilSquare,
|
||||||
|
BsPersonCheck,
|
||||||
|
BsPeople,
|
||||||
|
BsBank,
|
||||||
|
} 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 IconCard from "./IconCard";
|
||||||
|
import GroupList from "@/pages/(admin)/Lists/GroupList";
|
||||||
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import useCodes from "@/hooks/useCodes";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: MasterCorporateUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MasterCorporateDashboard({ user }: Props) {
|
||||||
|
const [page, setPage] = useState("");
|
||||||
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
|
const { stats } = useStats();
|
||||||
|
const { users, reload } = useUsers();
|
||||||
|
const { codes } = useCodes(user.id);
|
||||||
|
const { groups } = useGroups(user.id, 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 appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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 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"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-1 items-start">
|
||||||
|
<span>{displayUser.name}</span>
|
||||||
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StudentsList = () => {
|
||||||
|
const filter = (x: User) =>
|
||||||
|
x.type === "student" &&
|
||||||
|
(!!selectedUser
|
||||||
|
? corporateUserGroups.includes(x.id) || false
|
||||||
|
: corporateUserGroups.includes(x.id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TeachersList = () => {
|
||||||
|
const filter = (x: User) =>
|
||||||
|
x.type === "teacher" &&
|
||||||
|
(!!selectedUser
|
||||||
|
? corporateUserGroups.includes(x.id) || false
|
||||||
|
: corporateUserGroups.includes(x.id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[filter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const corporateUserFilter = (x: User) =>
|
||||||
|
x.type === "corporate" &&
|
||||||
|
(!!selectedUser
|
||||||
|
? masterCorporateUserGroups.includes(x.id) || false
|
||||||
|
: masterCorporateUserGroups.includes(x.id));
|
||||||
|
|
||||||
|
const CorporateList = () => {
|
||||||
|
return (
|
||||||
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[corporateUserFilter]}
|
||||||
|
renderHeader={(total) => (
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Corporates ({total})</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GroupsList = () => {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Groups ({groups.length})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GroupList user={user} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 levels: { [key in Module]: number } = {
|
||||||
|
reading: 0,
|
||||||
|
listening: 0,
|
||||||
|
writing: 0,
|
||||||
|
speaking: 0,
|
||||||
|
level: 0,
|
||||||
|
};
|
||||||
|
bandScores.forEach((b) => (levels[b.module] += b.level));
|
||||||
|
|
||||||
|
return calculateAverageLevel(levels);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultDashboard = () => (
|
||||||
|
<>
|
||||||
|
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
|
||||||
|
<IconCard
|
||||||
|
onClick={() => setPage("students")}
|
||||||
|
Icon={BsPersonFill}
|
||||||
|
label="Students"
|
||||||
|
value={users.filter(studentFilter).length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
onClick={() => setPage("teachers")}
|
||||||
|
Icon={BsPencilSquare}
|
||||||
|
label="Teachers"
|
||||||
|
value={users.filter(teacherFilter).length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsClipboard2Data}
|
||||||
|
label="Exams Performed"
|
||||||
|
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}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsPersonCheck}
|
||||||
|
label="User Balance"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
color="rose"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsBank}
|
||||||
|
label="Corporate"
|
||||||
|
value={masterCorporateUserGroups.length}
|
||||||
|
color="purple"
|
||||||
|
onClick={() => setPage("corporate")}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
|
<span className="p-4">Latest students</span>
|
||||||
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
|
{users
|
||||||
|
.filter(studentFilter)
|
||||||
|
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||||
|
.map((x) => (
|
||||||
|
<UserDisplay key={x.id} {...x} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
|
<span className="p-4">Latest teachers</span>
|
||||||
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
|
{users
|
||||||
|
.filter(teacherFilter)
|
||||||
|
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||||
|
.map((x) => (
|
||||||
|
<UserDisplay key={x.id} {...x} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
|
<span className="p-4">Highest level students</span>
|
||||||
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
|
{users
|
||||||
|
.filter(studentFilter)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
calculateAverageLevel(b.levels) -
|
||||||
|
calculateAverageLevel(a.levels)
|
||||||
|
)
|
||||||
|
.map((x) => (
|
||||||
|
<UserDisplay key={x.id} {...x} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
|
<span className="p-4">Highest exam count students</span>
|
||||||
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
|
{users
|
||||||
|
.filter(studentFilter)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
||||||
|
Object.keys(groupByExam(getStatsByStudent(a))).length
|
||||||
|
)
|
||||||
|
.map((x) => (
|
||||||
|
<UserDisplay key={x.id} {...x} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
|
||||||
|
<>
|
||||||
|
{selectedUser && (
|
||||||
|
<div className="w-full flex flex-col gap-8">
|
||||||
|
<UserCard
|
||||||
|
loggedInUser={user}
|
||||||
|
onClose={(shouldReload) => {
|
||||||
|
setSelectedUser(undefined);
|
||||||
|
if (shouldReload) reload();
|
||||||
|
}}
|
||||||
|
onViewStudents={
|
||||||
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "teacher"
|
||||||
|
? () => {
|
||||||
|
appendUserFilters({
|
||||||
|
id: "view-students",
|
||||||
|
filter: (x: User) => x.type === "student",
|
||||||
|
});
|
||||||
|
appendUserFilters({
|
||||||
|
id: "belongs-to-admin",
|
||||||
|
filter: (x: User) =>
|
||||||
|
groups
|
||||||
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
|
.flatMap((g) => g.participants)
|
||||||
|
.includes(x.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push("/list/users");
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onViewTeachers={
|
||||||
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "student"
|
||||||
|
? () => {
|
||||||
|
appendUserFilters({
|
||||||
|
id: "view-teachers",
|
||||||
|
filter: (x: User) => x.type === "teacher",
|
||||||
|
});
|
||||||
|
appendUserFilters({
|
||||||
|
id: "belongs-to-admin",
|
||||||
|
filter: (x: User) =>
|
||||||
|
groups
|
||||||
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
|
.flatMap((g) => g.participants)
|
||||||
|
.includes(x.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push("/list/users");
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
user={selectedUser}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
{page === "students" && <StudentsList />}
|
||||||
|
{page === "teachers" && <TeachersList />}
|
||||||
|
{page === "groups" && <GroupsList />}
|
||||||
|
{page === "corporate" && <CorporateList />}
|
||||||
|
{page === "" && <DefaultDashboard />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,16 +2,23 @@ import {Group, User} from "@/interfaces/user";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
export default function useGroups(admin?: string) {
|
export default function useGroups(admin?: string, userType?: string) {
|
||||||
const [groups, setGroups] = useState<Group[]>([]);
|
const [groups, setGroups] = useState<Group[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const isMasterType = userType?.startsWith('master');
|
||||||
|
|
||||||
const getData = () => {
|
const getData = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const url = admin ? `/api/groups?admin=${admin}` : "/api/groups";
|
||||||
axios
|
axios
|
||||||
.get<Group[]>("/api/groups")
|
.get<Group[]>(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
if(isMasterType) {
|
||||||
|
return setGroups(response.data);
|
||||||
|
}
|
||||||
const filter = (g: Group) => g.admin === admin || g.participants.includes(admin || "");
|
const filter = (g: Group) => g.admin === admin || g.participants.includes(admin || "");
|
||||||
|
|
||||||
const filteredGroups = admin ? response.data.filter(filter) : response.data;
|
const filteredGroups = admin ? response.data.filter(filter) : response.data;
|
||||||
@@ -20,7 +27,7 @@ export default function useGroups(admin?: string) {
|
|||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(getData, [admin]);
|
useEffect(getData, [admin, isMasterType]);
|
||||||
|
|
||||||
return {groups, isLoading, isError, reload: getData};
|
return {groups, isLoading, isError, reload: getData};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { Module } from ".";
|
import { Module } from ".";
|
||||||
import { InstructorGender } from "./exam";
|
import { InstructorGender } from "./exam";
|
||||||
|
|
||||||
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser;
|
export type User =
|
||||||
|
| StudentUser
|
||||||
|
| TeacherUser
|
||||||
|
| CorporateUser
|
||||||
|
| AgentUser
|
||||||
|
| AdminUser
|
||||||
|
| DeveloperUser
|
||||||
|
| MasterCorporateUser;
|
||||||
export type UserStatus = "active" | "disabled" | "paymentDue";
|
export type UserStatus = "active" | "disabled" | "paymentDue";
|
||||||
|
|
||||||
export interface BasicUser {
|
export interface BasicUser {
|
||||||
@@ -39,6 +46,12 @@ export interface CorporateUser extends BasicUser {
|
|||||||
demographicInformation?: DemographicCorporateInformation;
|
demographicInformation?: DemographicCorporateInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MasterCorporateUser extends BasicUser {
|
||||||
|
type: "mastercorporate";
|
||||||
|
corporateInformation: CorporateInformation;
|
||||||
|
demographicInformation?: DemographicCorporateInformation;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentUser extends BasicUser {
|
export interface AgentUser extends BasicUser {
|
||||||
type: "agent";
|
type: "agent";
|
||||||
agentInformation: AgentInformation;
|
agentInformation: AgentInformation;
|
||||||
@@ -97,8 +110,15 @@ export interface DemographicCorporateInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Gender = "male" | "female" | "other";
|
export type Gender = "male" | "female" | "other";
|
||||||
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
|
export type EmploymentStatus =
|
||||||
export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
|
| "employed"
|
||||||
|
| "student"
|
||||||
|
| "self-employed"
|
||||||
|
| "unemployed"
|
||||||
|
| "retired"
|
||||||
|
| "other";
|
||||||
|
export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] =
|
||||||
|
[
|
||||||
{ status: "student", label: "Student" },
|
{ status: "student", label: "Student" },
|
||||||
{ status: "employed", label: "Employed" },
|
{ status: "employed", label: "Employed" },
|
||||||
{ status: "unemployed", label: "Unemployed" },
|
{ status: "unemployed", label: "Unemployed" },
|
||||||
@@ -148,5 +168,20 @@ export interface Code {
|
|||||||
passport_id?: string;
|
passport_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent";
|
export type Type =
|
||||||
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent"];
|
| "student"
|
||||||
|
| "teacher"
|
||||||
|
| "corporate"
|
||||||
|
| "admin"
|
||||||
|
| "developer"
|
||||||
|
| "agent"
|
||||||
|
| "mastercorporate";
|
||||||
|
export const userTypes: Type[] = [
|
||||||
|
"student",
|
||||||
|
"teacher",
|
||||||
|
"corporate",
|
||||||
|
"admin",
|
||||||
|
"developer",
|
||||||
|
"agent",
|
||||||
|
"mastercorporate",
|
||||||
|
];
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ const USER_TYPE_PERMISSIONS: {[key in Type]: Type[]} = {
|
|||||||
teacher: [],
|
teacher: [],
|
||||||
agent: [],
|
agent: [],
|
||||||
corporate: ["student", "teacher"],
|
corporate: ["student", "teacher"],
|
||||||
admin: ["student", "teacher", "agent", "corporate", "admin"],
|
mastercorporate: ["student", "teacher", "corporate"],
|
||||||
developer: ["student", "teacher", "agent", "corporate", "admin", "developer"],
|
admin: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
|
||||||
|
developer: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BatchCodeGenerator({user}: {user: User}) {
|
export default function BatchCodeGenerator({user}: {user: User}) {
|
||||||
@@ -198,7 +199,7 @@ export default function BatchCodeGenerator({user}: {user: User}) {
|
|||||||
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
|
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
|
||||||
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
||||||
</Button>
|
</Button>
|
||||||
{user && (user.type === "developer" || user.type === "admin" || user.type === "corporate") && (
|
{user && (["developer","admin","corporate", "mastercorporate"].includes(user.type)) && (
|
||||||
<>
|
<>
|
||||||
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ const USER_TYPE_PERMISSIONS: {[key in Type]: Type[]} = {
|
|||||||
teacher: [],
|
teacher: [],
|
||||||
agent: [],
|
agent: [],
|
||||||
corporate: ["student", "teacher"],
|
corporate: ["student", "teacher"],
|
||||||
admin: ["student", "teacher", "agent", "corporate", "admin"],
|
mastercorporate: ["student", "teacher", "corporate"],
|
||||||
developer: ["student", "teacher", "agent", "corporate", "admin", "developer"],
|
admin: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
|
||||||
|
developer: ["student", "teacher", "agent", "corporate", "admin", "developer","mastercorporate"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CodeGenerator({user}: {user: User}) {
|
export default function CodeGenerator({user}: {user: User}) {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
|
|||||||
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(
|
const filteredUsers = emailUsers.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
((user.type === "developer" || user.type === "admin" || user.type === "corporate") &&
|
((user.type === "developer" || user.type === "admin" || user.type === "corporate" || user.type === "mastercorporate") &&
|
||||||
(x?.type === "student" || x?.type === "teacher")) ||
|
(x?.type === "student" || x?.type === "teacher")) ||
|
||||||
(user.type === "teacher" && x?.type === "student"),
|
(user.type === "teacher" && x?.type === "student"),
|
||||||
);
|
);
|
||||||
@@ -189,7 +189,7 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterTypes = ["corporate", "teacher"];
|
const filterTypes = ["corporate", "teacher", "mastercorporate"];
|
||||||
|
|
||||||
export default function GroupList({user}: {user: User}) {
|
export default function GroupList({user}: {user: User}) {
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
@@ -197,10 +197,10 @@ export default function GroupList({user}: {user: User}) {
|
|||||||
const [filterByUser, setFilterByUser] = useState(false);
|
const [filterByUser, setFilterByUser] = useState(false);
|
||||||
|
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {groups, reload} = useGroups(user && filterTypes.includes(user?.type) ? user.id : undefined);
|
const {groups, reload} = useGroups(user && filterTypes.includes(user?.type) ? user.id : undefined, user?.type);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && (user.type === "corporate" || user.type === "teacher")) {
|
if (user && (['corporate', 'teacher', 'mastercorporate'].includes(user.type))) {
|
||||||
setFilterByUser(true);
|
setFilterByUser(true);
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|||||||
@@ -4,19 +4,38 @@ import useGroups from "@/hooks/useGroups";
|
|||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { Type, User, userTypes, CorporateUser, Group } from "@/interfaces/user";
|
import { Type, User, userTypes, CorporateUser, Group } from "@/interfaces/user";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
import {
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { capitalize, reverse } from "lodash";
|
import { capitalize, reverse } from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import {BsArrowDown, BsArrowDownUp, BsArrowUp, BsCheck, BsCheckCircle, BsEye, BsFillExclamationOctagonFill, BsPerson, BsTrash} from "react-icons/bs";
|
import {
|
||||||
|
BsArrowDown,
|
||||||
|
BsArrowDownUp,
|
||||||
|
BsArrowUp,
|
||||||
|
BsCheck,
|
||||||
|
BsCheckCircle,
|
||||||
|
BsEye,
|
||||||
|
BsFillExclamationOctagonFill,
|
||||||
|
BsPerson,
|
||||||
|
BsTrash,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { countries, TCountries } from "countries-list";
|
import { countries, TCountries } from "countries-list";
|
||||||
import countryCodes from "country-codes-list";
|
import countryCodes from "country-codes-list";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import {getUserCompanyName, isAgentUser, USER_TYPE_LABELS} from "@/resources/user";
|
import {
|
||||||
|
getUserCompanyName,
|
||||||
|
isAgentUser,
|
||||||
|
USER_TYPE_LABELS,
|
||||||
|
} from "@/resources/user";
|
||||||
import useFilterStore from "@/stores/listFilterStore";
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { isCorporateUser } from "@/resources/user";
|
import { isCorporateUser } from "@/resources/user";
|
||||||
@@ -26,9 +45,21 @@ import {asyncSorter} from "@/utils";
|
|||||||
import { exportListToExcel, UserListRow } from "@/utils/users";
|
import { exportListToExcel, UserListRow } from "@/utils/users";
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<User>();
|
const columnHelper = createColumnHelper<User>();
|
||||||
const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]];
|
const searchFields = [
|
||||||
|
["name"],
|
||||||
|
["email"],
|
||||||
|
["corporateInformation", "companyInformation", "name"],
|
||||||
|
];
|
||||||
|
|
||||||
const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; groups: Group[]}) => {
|
const CompanyNameCell = ({
|
||||||
|
users,
|
||||||
|
user,
|
||||||
|
groups,
|
||||||
|
}: {
|
||||||
|
user: User;
|
||||||
|
users: User[];
|
||||||
|
groups: Group[];
|
||||||
|
}) => {
|
||||||
const [companyName, setCompanyName] = useState("");
|
const [companyName, setCompanyName] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@@ -37,7 +68,11 @@ const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; grou
|
|||||||
setCompanyName(name);
|
setCompanyName(name);
|
||||||
}, [user, users, groups]);
|
}, [user, users, groups]);
|
||||||
|
|
||||||
return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>;
|
return isLoading ? (
|
||||||
|
<span className="animate-pulse">Loading...</span>
|
||||||
|
) : (
|
||||||
|
<>{companyName}</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UserList({
|
export default function UserList({
|
||||||
@@ -49,13 +84,18 @@ export default function UserList({
|
|||||||
filters?: ((user: User) => boolean)[];
|
filters?: ((user: User) => boolean)[];
|
||||||
renderHeader?: (total: number) => JSX.Element;
|
renderHeader?: (total: number) => JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
|
const [showDemographicInformation, setShowDemographicInformation] =
|
||||||
|
useState(false);
|
||||||
const [sorter, setSorter] = useState<string>();
|
const [sorter, setSorter] = useState<string>();
|
||||||
const [displayUsers, setDisplayUsers] = useState<User[]>([]);
|
const [displayUsers, setDisplayUsers] = useState<User[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
|
|
||||||
const { users, reload } = useUsers();
|
const { users, reload } = useUsers();
|
||||||
const {groups} = useGroups(user && (user?.type === "corporate" || user?.type === "teacher") ? user.id : undefined);
|
const { groups } = useGroups(
|
||||||
|
user && (['corporate', 'teacher', 'mastercorporate'].includes(user?.type))
|
||||||
|
? user.id
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -64,10 +104,13 @@ export default function UserList({
|
|||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new Date());
|
const today = moment(new Date());
|
||||||
|
|
||||||
if (today.isAfter(momentDate)) return "!text-mti-red-light font-bold line-through";
|
if (today.isAfter(momentDate))
|
||||||
|
return "!text-mti-red-light font-bold line-through";
|
||||||
if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light";
|
if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light";
|
||||||
if (today.add(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-light";
|
if (today.add(2, "weeks").isAfter(momentDate))
|
||||||
if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light";
|
return "!text-mti-rose-light";
|
||||||
|
if (today.add(1, "months").isAfter(momentDate))
|
||||||
|
return "!text-mti-orange-light";
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -75,11 +118,19 @@ export default function UserList({
|
|||||||
if (user && users) {
|
if (user && users) {
|
||||||
const filterUsers =
|
const filterUsers =
|
||||||
user.type === "corporate" || user.type === "teacher"
|
user.type === "corporate" || user.type === "teacher"
|
||||||
? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id))
|
? users.filter((u) =>
|
||||||
|
groups.flatMap((g) => g.participants).includes(u.id)
|
||||||
|
)
|
||||||
: users;
|
: users;
|
||||||
|
|
||||||
const filteredUsers = filters.reduce((d, f) => d.filter(f), filterUsers);
|
const filteredUsers = filters.reduce(
|
||||||
const sortedUsers = await asyncSorter<User>(filteredUsers, sortFunction);
|
(d, f) => d.filter(f),
|
||||||
|
filterUsers
|
||||||
|
);
|
||||||
|
const sortedUsers = await asyncSorter<User>(
|
||||||
|
filteredUsers,
|
||||||
|
sortFunction
|
||||||
|
);
|
||||||
|
|
||||||
setDisplayUsers([...sortedUsers]);
|
setDisplayUsers([...sortedUsers]);
|
||||||
}
|
}
|
||||||
@@ -88,7 +139,8 @@ export default function UserList({
|
|||||||
}, [user, users, sorter, groups]);
|
}, [user, users, sorter, groups]);
|
||||||
|
|
||||||
const deleteAccount = (user: User) => {
|
const deleteAccount = (user: User) => {
|
||||||
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
|
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`))
|
||||||
|
return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete<{ ok: boolean }>(`/api/user?id=${user.id}`)
|
.delete<{ ok: boolean }>(`/api/user?id=${user.id}`)
|
||||||
@@ -103,7 +155,14 @@ export default function UserList({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateAccountType = (user: User, type: Type) => {
|
const updateAccountType = (user: User, type: Type) => {
|
||||||
if (!confirm(`Are you sure you want to update ${user.name}'s account from ${capitalize(user.type)} to ${capitalize(type)}?`)) return;
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to update ${
|
||||||
|
user.name
|
||||||
|
}'s account from ${capitalize(user.type)} to ${capitalize(type)}?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
|
.post<{ user?: User; ok?: boolean }>(`/api/users/update?id=${user.id}`, {
|
||||||
@@ -137,9 +196,11 @@ export default function UserList({
|
|||||||
const toggleDisableAccount = (user: User) => {
|
const toggleDisableAccount = (user: User) => {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
`Are you sure you want to ${user.status === "disabled" ? "enable" : "disable"} ${
|
`Are you sure you want to ${
|
||||||
|
user.status === "disabled" ? "enable" : "disable"
|
||||||
|
} ${
|
||||||
user.name
|
user.name
|
||||||
}'s account? This change is usually related to their payment state.`,
|
}'s account? This change is usually related to their payment state.`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
@@ -150,7 +211,11 @@ export default function UserList({
|
|||||||
status: user.status === "disabled" ? "active" : "disabled",
|
status: user.status === "disabled" ? "active" : "disabled",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`User ${user.status === "disabled" ? "enabled" : "disabled"} successfully!`);
|
toast.success(
|
||||||
|
`User ${
|
||||||
|
user.status === "disabled" ? "enabled" : "disabled"
|
||||||
|
} successfully!`
|
||||||
|
);
|
||||||
reload();
|
reload();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -182,31 +247,48 @@ export default function UserList({
|
|||||||
enterTo="opacity-100 translate-y-0"
|
enterTo="opacity-100 translate-y-0"
|
||||||
leave="transition ease-in duration-150"
|
leave="transition ease-in duration-150"
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1">
|
leaveTo="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
<Popover.Panel className="absolute z-10 w-screen right-1/2 translate-x-1/3 max-w-sm">
|
<Popover.Panel className="absolute z-10 w-screen right-1/2 translate-x-1/3 max-w-sm">
|
||||||
<div className="bg-white p-4 rounded-lg grid grid-cols-2 gap-2 w-full drop-shadow-xl">
|
<div className="bg-white p-4 rounded-lg grid grid-cols-2 gap-2 w-full drop-shadow-xl">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "student")}
|
onClick={() => updateAccountType(row.original, "student")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={row.original.type === "student" || !PERMISSIONS.generateCode["student"].includes(user.type)}>
|
disabled={
|
||||||
|
row.original.type === "student" ||
|
||||||
|
!PERMISSIONS.generateCode["student"].includes(user.type)
|
||||||
|
}
|
||||||
|
>
|
||||||
Student
|
Student
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "teacher")}
|
onClick={() => updateAccountType(row.original, "teacher")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={row.original.type === "teacher" || !PERMISSIONS.generateCode["teacher"].includes(user.type)}>
|
disabled={
|
||||||
|
row.original.type === "teacher" ||
|
||||||
|
!PERMISSIONS.generateCode["teacher"].includes(user.type)
|
||||||
|
}
|
||||||
|
>
|
||||||
Teacher
|
Teacher
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "corporate")}
|
onClick={() => updateAccountType(row.original, "corporate")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={row.original.type === "corporate" || !PERMISSIONS.generateCode["corporate"].includes(user.type)}>
|
disabled={
|
||||||
|
row.original.type === "corporate" ||
|
||||||
|
!PERMISSIONS.generateCode["corporate"].includes(user.type)
|
||||||
|
}
|
||||||
|
>
|
||||||
Corporate
|
Corporate
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => updateAccountType(row.original, "admin")}
|
onClick={() => updateAccountType(row.original, "admin")}
|
||||||
className="text-sm !py-2 !px-4"
|
className="text-sm !py-2 !px-4"
|
||||||
disabled={row.original.type === "admin" || !PERMISSIONS.generateCode["admin"].includes(user.type)}>
|
disabled={
|
||||||
|
row.original.type === "admin" ||
|
||||||
|
!PERMISSIONS.generateCode["admin"].includes(user.type)
|
||||||
|
}
|
||||||
|
>
|
||||||
Admin
|
Admin
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,16 +296,26 @@ export default function UserList({
|
|||||||
</Transition>
|
</Transition>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
{!row.original.isVerified && PERMISSIONS.updateUser[row.original.type]?.includes(user.type) && (
|
{!row.original.isVerified &&
|
||||||
<div data-tip="Verify User" className="cursor-pointer tooltip" onClick={() => verifyAccount(row.original)}>
|
PERMISSIONS.updateUser[row.original.type]?.includes(user.type) && (
|
||||||
|
<div
|
||||||
|
data-tip="Verify User"
|
||||||
|
className="cursor-pointer tooltip"
|
||||||
|
onClick={() => verifyAccount(row.original)}
|
||||||
|
>
|
||||||
<BsCheck className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsCheck className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{PERMISSIONS.updateUser[row.original.type]?.includes(user.type) && (
|
{PERMISSIONS.updateUser[row.original.type]?.includes(user.type) && (
|
||||||
<div
|
<div
|
||||||
data-tip={row.original.status === "disabled" ? "Enable User" : "Disable User"}
|
data-tip={
|
||||||
|
row.original.status === "disabled"
|
||||||
|
? "Enable User"
|
||||||
|
: "Disable User"
|
||||||
|
}
|
||||||
className="cursor-pointer tooltip"
|
className="cursor-pointer tooltip"
|
||||||
onClick={() => toggleDisableAccount(row.original)}>
|
onClick={() => toggleDisableAccount(row.original)}
|
||||||
|
>
|
||||||
{row.original.status === "disabled" ? (
|
{row.original.status === "disabled" ? (
|
||||||
<BsCheckCircle className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsCheckCircle className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
) : (
|
) : (
|
||||||
@@ -232,7 +324,11 @@ export default function UserList({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{PERMISSIONS.deleteUser[row.original.type]?.includes(user.type) && (
|
{PERMISSIONS.deleteUser[row.original.type]?.includes(user.type) && (
|
||||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteAccount(row.original)}>
|
<div
|
||||||
|
data-tip="Delete"
|
||||||
|
className="cursor-pointer tooltip"
|
||||||
|
onClick={() => deleteAccount(row.original)}
|
||||||
|
>
|
||||||
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -243,7 +339,10 @@ export default function UserList({
|
|||||||
const demographicColumns = [
|
const demographicColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "name"))}
|
||||||
|
>
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
<SorterArrow name="name" />
|
<SorterArrow name="name" />
|
||||||
</button>
|
</button>
|
||||||
@@ -251,31 +350,49 @@ export default function UserList({
|
|||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) &&
|
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
user.type
|
||||||
|
) &&
|
||||||
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
||||||
)}
|
)}
|
||||||
onClick={() => (PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}>
|
onClick={() =>
|
||||||
|
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type)
|
||||||
|
? setSelectedUser(row.original)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
{getValue()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.country", {
|
columnHelper.accessor("demographicInformation.country", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "country"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "country"))}
|
||||||
|
>
|
||||||
<span>Country</span>
|
<span>Country</span>
|
||||||
<SorterArrow name="country" />
|
<SorterArrow name="country" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) =>
|
cell: (info) =>
|
||||||
info.getValue()
|
info.getValue()
|
||||||
? `${countryCodes.findOne("countryCode" as any, info.getValue()).flag} ${
|
? `${
|
||||||
|
countryCodes.findOne("countryCode" as any, info.getValue()).flag
|
||||||
|
} ${
|
||||||
countries[info.getValue() as unknown as keyof TCountries].name
|
countries[info.getValue() as unknown as keyof TCountries].name
|
||||||
} (+${countryCodes.findOne("countryCode" as any, info.getValue()).countryCallingCode})`
|
} (+${
|
||||||
|
countryCodes.findOne("countryCode" as any, info.getValue())
|
||||||
|
.countryCallingCode
|
||||||
|
})`
|
||||||
: "Not available",
|
: "Not available",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.phone", {
|
columnHelper.accessor("demographicInformation.phone", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "phone"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "phone"))}
|
||||||
|
>
|
||||||
<span>Phone</span>
|
<span>Phone</span>
|
||||||
<SorterArrow name="phone" />
|
<SorterArrow name="phone" />
|
||||||
</button>
|
</button>
|
||||||
@@ -283,20 +400,37 @@ export default function UserList({
|
|||||||
cell: (info) => info.getValue() || "Not available",
|
cell: (info) => info.getValue() || "Not available",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor((x) => (x.type === "corporate" ? x.demographicInformation?.position : x.demographicInformation?.employment), {
|
columnHelper.accessor(
|
||||||
|
(x) =>
|
||||||
|
x.type === "corporate" || x.type === "mastercorporate"
|
||||||
|
? x.demographicInformation?.position
|
||||||
|
: x.demographicInformation?.employment,
|
||||||
|
{
|
||||||
id: "employment",
|
id: "employment",
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "employment"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() =>
|
||||||
|
setSorter((prev) => selectSorter(prev, "employment"))
|
||||||
|
}
|
||||||
|
>
|
||||||
<span>Employment/Position</span>
|
<span>Employment/Position</span>
|
||||||
<SorterArrow name="employment" />
|
<SorterArrow name="employment" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "Not available",
|
cell: (info) =>
|
||||||
|
(info.row.original.type === "corporate"
|
||||||
|
? info.getValue()
|
||||||
|
: capitalize(info.getValue())) || "Not available",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
columnHelper.accessor("demographicInformation.gender", {
|
columnHelper.accessor("demographicInformation.gender", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}
|
||||||
|
>
|
||||||
<span>Gender</span>
|
<span>Gender</span>
|
||||||
<SorterArrow name="gender" />
|
<SorterArrow name="gender" />
|
||||||
</button>
|
</button>
|
||||||
@@ -306,7 +440,10 @@ export default function UserList({
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
header: (
|
header: (
|
||||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setShowDemographicInformation((prev) => !prev)}
|
||||||
|
>
|
||||||
Switch
|
Switch
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -318,7 +455,10 @@ export default function UserList({
|
|||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "name"))}
|
||||||
|
>
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
<SorterArrow name="name" />
|
<SorterArrow name="name" />
|
||||||
</button>
|
</button>
|
||||||
@@ -326,17 +466,30 @@ export default function UserList({
|
|||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) &&
|
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
user.type
|
||||||
|
) &&
|
||||||
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
||||||
)}
|
)}
|
||||||
onClick={() => (PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}>
|
onClick={() =>
|
||||||
{row.original.type === "corporate" ? row.original.corporateInformation?.companyInformation?.name || getValue() : getValue()}
|
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type)
|
||||||
|
? setSelectedUser(row.original)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.original.type === "corporate"
|
||||||
|
? row.original.corporateInformation?.companyInformation?.name ||
|
||||||
|
getValue()
|
||||||
|
: getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
columnHelper.accessor("email", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "email"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "email"))}
|
||||||
|
>
|
||||||
<span>E-mail</span>
|
<span>E-mail</span>
|
||||||
<SorterArrow name="email" />
|
<SorterArrow name="email" />
|
||||||
</button>
|
</button>
|
||||||
@@ -344,17 +497,27 @@ export default function UserList({
|
|||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) &&
|
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(
|
||||||
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
|
user.type
|
||||||
|
) &&
|
||||||
|
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer"
|
||||||
)}
|
)}
|
||||||
onClick={() => (PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}>
|
onClick={() =>
|
||||||
|
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type)
|
||||||
|
? setSelectedUser(row.original)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
{getValue()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("type", {
|
columnHelper.accessor("type", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "type"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "type"))}
|
||||||
|
>
|
||||||
<span>Type</span>
|
<span>Type</span>
|
||||||
<SorterArrow name="type" />
|
<SorterArrow name="type" />
|
||||||
</button>
|
</button>
|
||||||
@@ -363,29 +526,54 @@ export default function UserList({
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("corporateInformation.companyInformation.name", {
|
columnHelper.accessor("corporateInformation.companyInformation.name", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}
|
||||||
|
>
|
||||||
<span>Company Name</span>
|
<span>Company Name</span>
|
||||||
<SorterArrow name="companyName" />
|
<SorterArrow name="companyName" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => <CompanyNameCell user={info.row.original} users={users} groups={groups} />,
|
cell: (info) => (
|
||||||
|
<CompanyNameCell
|
||||||
|
user={info.row.original}
|
||||||
|
users={users}
|
||||||
|
groups={groups}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("subscriptionExpirationDate", {
|
columnHelper.accessor("subscriptionExpirationDate", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}
|
||||||
|
>
|
||||||
<span>Expiry Date</span>
|
<span>Expiry Date</span>
|
||||||
<SorterArrow name="expiryDate" />
|
<SorterArrow name="expiryDate" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
<span
|
||||||
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
className={clsx(
|
||||||
|
info.getValue()
|
||||||
|
? expirationDateColor(moment(info.getValue()).toDate())
|
||||||
|
: ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!info.getValue()
|
||||||
|
? "No expiry date"
|
||||||
|
: moment(info.getValue()).format("DD/MM/YYYY")}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("isVerified", {
|
columnHelper.accessor("isVerified", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "verification"))}>
|
<button
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() =>
|
||||||
|
setSorter((prev) => selectSorter(prev, "verification"))
|
||||||
|
}
|
||||||
|
>
|
||||||
<span>Verification</span>
|
<span>Verification</span>
|
||||||
<SorterArrow name="verification" />
|
<SorterArrow name="verification" />
|
||||||
</button>
|
</button>
|
||||||
@@ -396,8 +584,9 @@ export default function UserList({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
info.getValue() && "!bg-mti-purple-light ",
|
info.getValue() && "!bg-mti-purple-light "
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<BsCheck color="white" className="w-full h-full" />
|
<BsCheck color="white" className="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -405,7 +594,10 @@ export default function UserList({
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
header: (
|
header: (
|
||||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setShowDemographicInformation((prev) => !prev)}
|
||||||
|
>
|
||||||
Switch
|
Switch
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -425,15 +617,21 @@ export default function UserList({
|
|||||||
|
|
||||||
const sortFunction = async (a: User, b: User) => {
|
const sortFunction = async (a: User, b: User) => {
|
||||||
if (sorter === "name" || sorter === reverseString("name"))
|
if (sorter === "name" || sorter === reverseString("name"))
|
||||||
return sorter === "name" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
return sorter === "name"
|
||||||
|
? a.name.localeCompare(b.name)
|
||||||
|
: b.name.localeCompare(a.name);
|
||||||
|
|
||||||
if (sorter === "email" || sorter === reverseString("email"))
|
if (sorter === "email" || sorter === reverseString("email"))
|
||||||
return sorter === "email" ? a.email.localeCompare(b.email) : b.email.localeCompare(a.email);
|
return sorter === "email"
|
||||||
|
? a.email.localeCompare(b.email)
|
||||||
|
: b.email.localeCompare(a.email);
|
||||||
|
|
||||||
if (sorter === "type" || sorter === reverseString("type"))
|
if (sorter === "type" || sorter === reverseString("type"))
|
||||||
return sorter === "type"
|
return sorter === "type"
|
||||||
? userTypes.findIndex((t) => a.type === t) - userTypes.findIndex((t) => b.type === t)
|
? userTypes.findIndex((t) => a.type === t) -
|
||||||
: userTypes.findIndex((t) => b.type === t) - userTypes.findIndex((t) => a.type === t);
|
userTypes.findIndex((t) => b.type === t)
|
||||||
|
: userTypes.findIndex((t) => b.type === t) -
|
||||||
|
userTypes.findIndex((t) => a.type === t);
|
||||||
|
|
||||||
if (sorter === "verification" || sorter === reverseString("verification"))
|
if (sorter === "verification" || sorter === reverseString("verification"))
|
||||||
return sorter === "verification"
|
return sorter === "verification"
|
||||||
@@ -441,73 +639,138 @@ export default function UserList({
|
|||||||
: b.isVerified.toString().localeCompare(a.isVerified.toString());
|
: b.isVerified.toString().localeCompare(a.isVerified.toString());
|
||||||
|
|
||||||
if (sorter === "expiryDate" || sorter === reverseString("expiryDate")) {
|
if (sorter === "expiryDate" || sorter === reverseString("expiryDate")) {
|
||||||
if (!a.subscriptionExpirationDate && b.subscriptionExpirationDate) return sorter === "expiryDate" ? -1 : 1;
|
if (!a.subscriptionExpirationDate && b.subscriptionExpirationDate)
|
||||||
if (a.subscriptionExpirationDate && !b.subscriptionExpirationDate) return sorter === "expiryDate" ? 1 : -1;
|
return sorter === "expiryDate" ? -1 : 1;
|
||||||
if (!a.subscriptionExpirationDate && !b.subscriptionExpirationDate) return 0;
|
if (a.subscriptionExpirationDate && !b.subscriptionExpirationDate)
|
||||||
if (moment(a.subscriptionExpirationDate).isAfter(b.subscriptionExpirationDate)) return sorter === "expiryDate" ? -1 : 1;
|
return sorter === "expiryDate" ? 1 : -1;
|
||||||
if (moment(b.subscriptionExpirationDate).isAfter(a.subscriptionExpirationDate)) return sorter === "expiryDate" ? 1 : -1;
|
if (!a.subscriptionExpirationDate && !b.subscriptionExpirationDate)
|
||||||
|
return 0;
|
||||||
|
if (
|
||||||
|
moment(a.subscriptionExpirationDate).isAfter(
|
||||||
|
b.subscriptionExpirationDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sorter === "expiryDate" ? -1 : 1;
|
||||||
|
if (
|
||||||
|
moment(b.subscriptionExpirationDate).isAfter(
|
||||||
|
a.subscriptionExpirationDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sorter === "expiryDate" ? 1 : -1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "country" || sorter === reverseString("country")) {
|
if (sorter === "country" || sorter === reverseString("country")) {
|
||||||
if (!a.demographicInformation?.country && b.demographicInformation?.country) return sorter === "country" ? -1 : 1;
|
if (
|
||||||
if (a.demographicInformation?.country && !b.demographicInformation?.country) return sorter === "country" ? 1 : -1;
|
!a.demographicInformation?.country &&
|
||||||
if (!a.demographicInformation?.country && !b.demographicInformation?.country) return 0;
|
b.demographicInformation?.country
|
||||||
|
)
|
||||||
|
return sorter === "country" ? -1 : 1;
|
||||||
|
if (
|
||||||
|
a.demographicInformation?.country &&
|
||||||
|
!b.demographicInformation?.country
|
||||||
|
)
|
||||||
|
return sorter === "country" ? 1 : -1;
|
||||||
|
if (
|
||||||
|
!a.demographicInformation?.country &&
|
||||||
|
!b.demographicInformation?.country
|
||||||
|
)
|
||||||
|
return 0;
|
||||||
|
|
||||||
return sorter === "country"
|
return sorter === "country"
|
||||||
? a.demographicInformation!.country.localeCompare(b.demographicInformation!.country)
|
? a.demographicInformation!.country.localeCompare(
|
||||||
: b.demographicInformation!.country.localeCompare(a.demographicInformation!.country);
|
b.demographicInformation!.country
|
||||||
|
)
|
||||||
|
: b.demographicInformation!.country.localeCompare(
|
||||||
|
a.demographicInformation!.country
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "phone" || sorter === reverseString("phone")) {
|
if (sorter === "phone" || sorter === reverseString("phone")) {
|
||||||
if (!a.demographicInformation?.phone && b.demographicInformation?.phone) return sorter === "phone" ? -1 : 1;
|
if (!a.demographicInformation?.phone && b.demographicInformation?.phone)
|
||||||
if (a.demographicInformation?.phone && !b.demographicInformation?.phone) return sorter === "phone" ? 1 : -1;
|
return sorter === "phone" ? -1 : 1;
|
||||||
if (!a.demographicInformation?.phone && !b.demographicInformation?.phone) return 0;
|
if (a.demographicInformation?.phone && !b.demographicInformation?.phone)
|
||||||
|
return sorter === "phone" ? 1 : -1;
|
||||||
|
if (!a.demographicInformation?.phone && !b.demographicInformation?.phone)
|
||||||
|
return 0;
|
||||||
|
|
||||||
return sorter === "phone"
|
return sorter === "phone"
|
||||||
? a.demographicInformation!.phone.localeCompare(b.demographicInformation!.phone)
|
? a.demographicInformation!.phone.localeCompare(
|
||||||
: b.demographicInformation!.phone.localeCompare(a.demographicInformation!.phone);
|
b.demographicInformation!.phone
|
||||||
|
)
|
||||||
|
: b.demographicInformation!.phone.localeCompare(
|
||||||
|
a.demographicInformation!.phone
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "employment" || sorter === reverseString("employment")) {
|
if (sorter === "employment" || sorter === reverseString("employment")) {
|
||||||
const aSortingItem = a.type === "corporate" ? a.demographicInformation?.position : a.demographicInformation?.employment;
|
const aSortingItem =
|
||||||
const bSortingItem = b.type === "corporate" ? b.demographicInformation?.position : b.demographicInformation?.employment;
|
a.type === "corporate" || a.type === "mastercorporate"
|
||||||
|
? a.demographicInformation?.position
|
||||||
|
: a.demographicInformation?.employment;
|
||||||
|
const bSortingItem =
|
||||||
|
b.type === "corporate" || b.type === "mastercorporate"
|
||||||
|
? b.demographicInformation?.position
|
||||||
|
: b.demographicInformation?.employment;
|
||||||
|
|
||||||
if (!aSortingItem && bSortingItem) return sorter === "employment" ? -1 : 1;
|
if (!aSortingItem && bSortingItem)
|
||||||
if (aSortingItem && !bSortingItem) return sorter === "employment" ? 1 : -1;
|
return sorter === "employment" ? -1 : 1;
|
||||||
|
if (aSortingItem && !bSortingItem)
|
||||||
|
return sorter === "employment" ? 1 : -1;
|
||||||
if (!aSortingItem && !bSortingItem) return 0;
|
if (!aSortingItem && !bSortingItem) return 0;
|
||||||
|
|
||||||
return sorter === "employment" ? aSortingItem!.localeCompare(bSortingItem!) : bSortingItem!.localeCompare(aSortingItem!);
|
return sorter === "employment"
|
||||||
|
? aSortingItem!.localeCompare(bSortingItem!)
|
||||||
|
: bSortingItem!.localeCompare(aSortingItem!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "gender" || sorter === reverseString("gender")) {
|
if (sorter === "gender" || sorter === reverseString("gender")) {
|
||||||
if (!a.demographicInformation?.gender && b.demographicInformation?.gender) return sorter === "employment" ? -1 : 1;
|
if (!a.demographicInformation?.gender && b.demographicInformation?.gender)
|
||||||
if (a.demographicInformation?.gender && !b.demographicInformation?.gender) return sorter === "employment" ? 1 : -1;
|
return sorter === "employment" ? -1 : 1;
|
||||||
if (!a.demographicInformation?.gender && !b.demographicInformation?.gender) return 0;
|
if (a.demographicInformation?.gender && !b.demographicInformation?.gender)
|
||||||
|
return sorter === "employment" ? 1 : -1;
|
||||||
|
if (
|
||||||
|
!a.demographicInformation?.gender &&
|
||||||
|
!b.demographicInformation?.gender
|
||||||
|
)
|
||||||
|
return 0;
|
||||||
|
|
||||||
return sorter === "gender"
|
return sorter === "gender"
|
||||||
? a.demographicInformation!.gender.localeCompare(b.demographicInformation!.gender)
|
? a.demographicInformation!.gender.localeCompare(
|
||||||
: b.demographicInformation!.gender.localeCompare(a.demographicInformation!.gender);
|
b.demographicInformation!.gender
|
||||||
|
)
|
||||||
|
: b.demographicInformation!.gender.localeCompare(
|
||||||
|
a.demographicInformation!.gender
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorter === "companyName" || sorter === reverseString("companyName")) {
|
if (sorter === "companyName" || sorter === reverseString("companyName")) {
|
||||||
const aCorporateName = getUserCompanyName(a, users, groups);
|
const aCorporateName = getUserCompanyName(a, users, groups);
|
||||||
const bCorporateName = getUserCompanyName(b, users, groups);
|
const bCorporateName = getUserCompanyName(b, users, groups);
|
||||||
if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1;
|
if (!aCorporateName && bCorporateName)
|
||||||
if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1;
|
return sorter === "companyName" ? -1 : 1;
|
||||||
|
if (aCorporateName && !bCorporateName)
|
||||||
|
return sorter === "companyName" ? 1 : -1;
|
||||||
if (!aCorporateName && !bCorporateName) return 0;
|
if (!aCorporateName && !bCorporateName) return 0;
|
||||||
|
|
||||||
return sorter === "companyName" ? aCorporateName.localeCompare(bCorporateName) : bCorporateName.localeCompare(aCorporateName);
|
return sorter === "companyName"
|
||||||
|
? aCorporateName.localeCompare(bCorporateName)
|
||||||
|
: bCorporateName.localeCompare(aCorporateName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.id.localeCompare(b.id);
|
return a.id.localeCompare(b.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {rows: filteredRows, renderSearch} = useListSearch<User>(searchFields, displayUsers);
|
const { rows: filteredRows, renderSearch } = useListSearch<User>(
|
||||||
|
searchFields,
|
||||||
|
displayUsers
|
||||||
|
);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredRows,
|
data: filteredRows,
|
||||||
columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any,
|
columns: (!showDemographicInformation
|
||||||
|
? defaultColumns
|
||||||
|
: demographicColumns) as any,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -528,13 +791,19 @@ export default function UserList({
|
|||||||
const belongsToAdminFilter = (x: User) => {
|
const belongsToAdminFilter = (x: User) => {
|
||||||
if (!selectedUser) return false;
|
if (!selectedUser) return false;
|
||||||
return groups
|
return groups
|
||||||
.filter((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)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id);
|
.includes(x.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewStudentFilterBelongsToAdmin = (x: User) => x.type === "student" && belongsToAdminFilter(x);
|
const viewStudentFilterBelongsToAdmin = (x: User) =>
|
||||||
const viewTeacherFilterBelongsToAdmin = (x: User) => x.type === "teacher" && belongsToAdminFilter(x);
|
x.type === "student" && belongsToAdminFilter(x);
|
||||||
|
const viewTeacherFilterBelongsToAdmin = (x: User) =>
|
||||||
|
x.type === "teacher" && belongsToAdminFilter(x);
|
||||||
|
|
||||||
const renderUserCard = (selectedUser: User) => {
|
const renderUserCard = (selectedUser: User) => {
|
||||||
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
|
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
|
||||||
@@ -544,7 +813,9 @@ export default function UserList({
|
|||||||
<UserCard
|
<UserCard
|
||||||
loggedInUser={user}
|
loggedInUser={user}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
(selectedUser.type === "corporate" || selectedUser.type === "teacher") && studentsFromAdmin.length > 0
|
(selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "teacher") &&
|
||||||
|
studentsFromAdmin.length > 0
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
@@ -560,7 +831,9 @@ export default function UserList({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
(selectedUser.type === "corporate" || selectedUser.type === "student") && teachersFromAdmin.length > 0
|
(selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "student") &&
|
||||||
|
teachersFromAdmin.length > 0
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
@@ -609,13 +882,20 @@ export default function UserList({
|
|||||||
<>
|
<>
|
||||||
{renderHeader && renderHeader(displayUsers.length)}
|
{renderHeader && renderHeader(displayUsers.length)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Modal isOpen={!!selectedUser} onClose={() => setSelectedUser(undefined)}>
|
<Modal
|
||||||
|
isOpen={!!selectedUser}
|
||||||
|
onClose={() => setSelectedUser(undefined)}
|
||||||
|
>
|
||||||
{selectedUser && renderUserCard(selectedUser)}
|
{selectedUser && renderUserCard(selectedUser)}
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className="w-full flex flex-col gap-2">
|
<div className="w-full flex flex-col gap-2">
|
||||||
<div className="w-full flex gap-2 items-end">
|
<div className="w-full flex gap-2 items-end">
|
||||||
{renderSearch()}
|
{renderSearch()}
|
||||||
<Button className="w-full max-w-[200px] mb-1" variant="outline" onClick={downloadExcel}>
|
<Button
|
||||||
|
className="w-full max-w-[200px] mb-1"
|
||||||
|
variant="outline"
|
||||||
|
onClick={downloadExcel}
|
||||||
|
>
|
||||||
Download List
|
Download List
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -625,7 +905,12 @@ export default function UserList({
|
|||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th className="py-4 px-4 text-left" key={header.id}>
|
<th className="py-4 px-4 text-left" key={header.id}>
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -633,10 +918,16 @@ export default function UserList({
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="px-2">
|
<tbody className="px-2">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<tr 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) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -30,12 +30,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (req.method === "POST") await post(req, res);
|
if (req.method === "POST") await post(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
const getGroupsForUser = async (admin: string, participant: string) => {
|
||||||
const { admin, participant } = req.query as {
|
try {
|
||||||
admin: string;
|
|
||||||
participant: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryConstraints = [
|
const queryConstraints = [
|
||||||
...(admin ? [where("admin", "==", admin)] : []),
|
...(admin ? [where("admin", "==", admin)] : []),
|
||||||
...(participant
|
...(participant
|
||||||
@@ -45,14 +41,63 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const snapshot = await getDocs(
|
const snapshot = await getDocs(
|
||||||
queryConstraints.length > 0
|
queryConstraints.length > 0
|
||||||
? query(collection(db, "groups"), ...queryConstraints)
|
? query(collection(db, "groups"), ...queryConstraints)
|
||||||
: collection(db, "groups"),
|
: collection(db, "groups")
|
||||||
);
|
);
|
||||||
const groups = snapshot.docs.map((doc) => ({
|
const groups = snapshot.docs.map((doc) => ({
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
...doc.data(),
|
...doc.data(),
|
||||||
})) as Group[];
|
})) as Group[];
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { admin, participant } = req.query as {
|
||||||
|
admin: string;
|
||||||
|
participant: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.session?.user?.type === "mastercorporate") {
|
||||||
|
try {
|
||||||
|
const masterCorporateGroups = await getGroupsForUser(admin, participant);
|
||||||
|
const corporatesFromMaster = masterCorporateGroups
|
||||||
|
.filter((g) => g.name === "Corporate")
|
||||||
|
.flatMap((g) => g.participants);
|
||||||
|
|
||||||
|
if (corporatesFromMaster.length === 0) {
|
||||||
|
res.status(200).json([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Promise.all(
|
||||||
|
corporatesFromMaster.map((c) => getGroupsForUser(c, participant))
|
||||||
|
)
|
||||||
|
.then((groups) => {
|
||||||
|
res.status(200).json([...masterCorporateGroups, ...groups.flat()]);
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
res.status(500).json({ ok: false });
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
res.status(500).json({ ok: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const groups = await getGroupsForUser(admin, participant);
|
||||||
res.status(200).json(groups);
|
res.status(200).json(groups);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
res.status(500).json({ ok: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -60,8 +105,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
body.participants.map(
|
body.participants.map(
|
||||||
async (p) => await updateExpiryDateOnGroup(p, body.admin),
|
async (p) => await updateExpiryDateOnGroup(p, body.admin)
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await setDoc(doc(db, "groups", v4()), {
|
await setDoc(doc(db, "groups", v4()), {
|
||||||
|
|||||||
@@ -143,9 +143,18 @@ async function registerCorporate(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
disableEditing: true,
|
disableEditing: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultCorporateGroup: Group = {
|
||||||
|
admin: userId,
|
||||||
|
id: v4(),
|
||||||
|
name: "Corporate",
|
||||||
|
participants: [],
|
||||||
|
disableEditing: true,
|
||||||
|
};
|
||||||
|
|
||||||
await setDoc(doc(db, "users", userId), user);
|
await setDoc(doc(db, "users", userId), user);
|
||||||
await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup);
|
await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup);
|
||||||
await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup);
|
await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup);
|
||||||
|
await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup);
|
||||||
|
|
||||||
req.session.user = {...user, id: userId};
|
req.session.user = {...user, id: userId};
|
||||||
await req.session.save();
|
await req.session.save();
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ import AdminDashboard from "@/dashboards/Admin";
|
|||||||
import CorporateDashboard from "@/dashboards/Corporate";
|
import CorporateDashboard from "@/dashboards/Corporate";
|
||||||
import TeacherDashboard from "@/dashboards/Teacher";
|
import TeacherDashboard from "@/dashboards/Teacher";
|
||||||
import AgentDashboard from "@/dashboards/Agent";
|
import AgentDashboard from "@/dashboards/Agent";
|
||||||
|
import MasterCorporateDashboard from "@/dashboards/MasterCorporate";
|
||||||
import PaymentDue from "./(status)/PaymentDue";
|
import PaymentDue from "./(status)/PaymentDue";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {PayPalScriptProvider} from "@paypal/react-paypal-js";
|
import {PayPalScriptProvider} from "@paypal/react-paypal-js";
|
||||||
import {CorporateUser, Type, userTypes} from "@/interfaces/user";
|
import {CorporateUser, MasterCorporateUser, Type, userTypes} from "@/interfaces/user";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
|
|
||||||
@@ -172,6 +173,7 @@ export default function Home(props: Props) {
|
|||||||
{user.type === "student" && <StudentDashboard user={user} />}
|
{user.type === "student" && <StudentDashboard user={user} />}
|
||||||
{user.type === "teacher" && <TeacherDashboard user={user} />}
|
{user.type === "teacher" && <TeacherDashboard user={user} />}
|
||||||
{user.type === "corporate" && <CorporateDashboard user={user} />}
|
{user.type === "corporate" && <CorporateDashboard user={user} />}
|
||||||
|
{user.type === "mastercorporate" && <MasterCorporateDashboard user={user} />}
|
||||||
{user.type === "agent" && <AgentDashboard user={user} />}
|
{user.type === "agent" && <AgentDashboard user={user} />}
|
||||||
{user.type === "admin" && <AdminDashboard user={user} />}
|
{user.type === "admin" && <AdminDashboard user={user} />}
|
||||||
{user.type === "developer" && (
|
{user.type === "developer" && (
|
||||||
@@ -185,6 +187,7 @@ export default function Home(props: Props) {
|
|||||||
{selectedScreen === "student" && <StudentDashboard user={user} />}
|
{selectedScreen === "student" && <StudentDashboard user={user} />}
|
||||||
{selectedScreen === "teacher" && <TeacherDashboard user={user} />}
|
{selectedScreen === "teacher" && <TeacherDashboard user={user} />}
|
||||||
{selectedScreen === "corporate" && <CorporateDashboard user={user as unknown as CorporateUser} />}
|
{selectedScreen === "corporate" && <CorporateDashboard user={user as unknown as CorporateUser} />}
|
||||||
|
{selectedScreen === "mastercorporate" && <MasterCorporateDashboard user={user as unknown as MasterCorporateUser} />}
|
||||||
{selectedScreen === "agent" && <AgentDashboard user={user} />}
|
{selectedScreen === "agent" && <AgentDashboard user={user} />}
|
||||||
{selectedScreen === "admin" && <AdminDashboard user={user} />}
|
{selectedScreen === "admin" && <AdminDashboard user={user} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldRedirectHome(user) || !["admin", "developer", "agent", "corporate"].includes(user.type)) {
|
if (shouldRedirectHome(user) || !["admin", "developer", "agent", "corporate", "mastercorporate"].includes(user.type)) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/",
|
destination: "/",
|
||||||
@@ -941,7 +941,7 @@ export default function PaymentRecord() {
|
|||||||
<div className="w-full flex flex-end justify-between p-2">
|
<div className="w-full flex flex-end justify-between p-2">
|
||||||
<h1 className="text-2xl font-semibold">Payment Record</h1>
|
<h1 className="text-2xl font-semibold">Payment Record</h1>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
{(user.type === "developer" || user.type === "admin" || user.type === "agent" || user.type === "corporate") && (
|
{(["developer", "admin", "agent", "corporate", "mastercorporate"].includes(user.type)) && (
|
||||||
<Button className="max-w-[200px]" variant="outline">
|
<Button className="max-w-[200px]" variant="outline">
|
||||||
<CSVLink data={csvRows} headers={csvColumns} filename="payment-records.csv">
|
<CSVLink data={csvRows} headers={csvColumns} filename="payment-records.csv">
|
||||||
Download CSV
|
Download CSV
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
const [phone, setPhone] = useState<string>(user.demographicInformation?.phone || "");
|
const [phone, setPhone] = useState<string>(user.demographicInformation?.phone || "");
|
||||||
const [gender, setGender] = useState<Gender | undefined>(user.demographicInformation?.gender || undefined);
|
const [gender, setGender] = useState<Gender | undefined>(user.demographicInformation?.gender || undefined);
|
||||||
const [employment, setEmployment] = useState<EmploymentStatus | undefined>(
|
const [employment, setEmployment] = useState<EmploymentStatus | undefined>(
|
||||||
user.type === "corporate" ? undefined : user.demographicInformation?.employment,
|
user.type === "corporate" || user.type === "mastercorporate" ? undefined : user.demographicInformation?.employment,
|
||||||
);
|
);
|
||||||
const [passport_id, setPassportID] = useState<string | undefined>(user.type === "student" ? user.demographicInformation?.passport_id : undefined);
|
const [passport_id, setPassportID] = useState<string | undefined>(user.type === "student" ? user.demographicInformation?.passport_id : undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldRedirectHome(user) || !["developer", "admin", "corporate", "agent"].includes(user.type)) {
|
if (shouldRedirectHome(user) || !["developer", "admin", "corporate", "agent", "mastercorporate"].includes(user.type)) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/",
|
destination: "/",
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export default function Stats() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && (
|
{(["corporate", "teacher", "mastercorporate"].includes(user.type) ) && groups.length > 0 && (
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
options={users
|
options={users
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const USER_TYPE_LABELS: {[key in Type]: string} = {
|
|||||||
agent: "Country Manager",
|
agent: "Country Manager",
|
||||||
admin: "Admin",
|
admin: "Admin",
|
||||||
developer: "Developer",
|
developer: "Developer",
|
||||||
|
mastercorporate: "Master Corporate"
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isCorporateUser(user: User): user is CorporateUser {
|
export function isCorporateUser(user: User): user is CorporateUser {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CorporateUser, Group, User } from "@/interfaces/user";
|
import { CorporateUser, Group, User, Type } from "@/interfaces/user";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const isUserFromCorporate = async (userID: string) => {
|
export const isUserFromCorporate = async (userID: string) => {
|
||||||
@@ -7,20 +7,12 @@ export const isUserFromCorporate = async (userID: string) => {
|
|||||||
const users = (await axios.get<User[]>("/api/users/list")).data;
|
const users = (await axios.get<User[]>("/api/users/list")).data;
|
||||||
|
|
||||||
const adminTypes = groups.map(
|
const adminTypes = groups.map(
|
||||||
(g) => users.find((u) => u.id === g.admin)?.type,
|
(g) => users.find((u) => u.id === g.admin)?.type
|
||||||
);
|
);
|
||||||
return adminTypes.includes("corporate");
|
return adminTypes.includes("corporate");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserCorporate = async (
|
const getAdminForGroup = async (userID: string, role: Type) => {
|
||||||
userID: string,
|
|
||||||
): Promise<CorporateUser | undefined> => {
|
|
||||||
const userRequest = await axios.get<User>(`/api/users/${userID}`);
|
|
||||||
if (userRequest.status === 200) {
|
|
||||||
const user = userRequest.data;
|
|
||||||
if (user.type === "corporate") return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`))
|
const groups = (await axios.get<Group[]>(`/api/groups?participant=${userID}`))
|
||||||
.data;
|
.data;
|
||||||
|
|
||||||
@@ -29,9 +21,23 @@ export const getUserCorporate = async (
|
|||||||
const userRequest = await axios.get<User>(`/api/users/${g.admin}`);
|
const userRequest = await axios.get<User>(`/api/users/${g.admin}`);
|
||||||
if (userRequest.status === 200) return userRequest.data;
|
if (userRequest.status === 200) return userRequest.data;
|
||||||
return undefined;
|
return undefined;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const admins = adminRequests.filter((x) => x?.type === "corporate");
|
const admins = adminRequests.filter((x) => x?.type === role);
|
||||||
return admins.length > 0 ? (admins[0] as CorporateUser) : undefined;
|
return admins.length > 0 ? (admins[0] as CorporateUser) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserCorporate = async (
|
||||||
|
userID: string
|
||||||
|
): Promise<CorporateUser | undefined> => {
|
||||||
|
const userRequest = await axios.get<User>(`/api/users/${userID}`);
|
||||||
|
if (userRequest.status === 200) {
|
||||||
|
const user = userRequest.data;
|
||||||
|
if (user.type === "corporate") {
|
||||||
|
return getAdminForGroup(userID, "mastercorporate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAdminForGroup(userID, "corporate");
|
||||||
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group
|
|||||||
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
|
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
|
||||||
country: user.demographicInformation?.country || "N/A",
|
country: user.demographicInformation?.country || "N/A",
|
||||||
phone: user.demographicInformation?.phone || "N/A",
|
phone: user.demographicInformation?.phone || "N/A",
|
||||||
employmentPosition: (user.type === "corporate" ? user.demographicInformation?.position : user.demographicInformation?.employment) || "N/A",
|
employmentPosition: (user.type === "corporate" || user.type === "mastercorporate" ? user.demographicInformation?.position : user.demographicInformation?.employment) || "N/A",
|
||||||
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
|
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
|
||||||
verified: user.isVerified?.toString() || "FALSE",
|
verified: user.isVerified?.toString() || "FALSE",
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user