Merged in master-corporate (pull request #54)

Master corporate

Approved-by: Tiago Ribeiro
This commit is contained in:
João Ramos
2024-07-25 21:00:04 +00:00
committed by Tiago Ribeiro
22 changed files with 1621 additions and 772 deletions

View File

@@ -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(

View File

@@ -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}

View File

@@ -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

View File

@@ -1,39 +1,48 @@
import {Type} from "@/interfaces/user"; 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"],
admin: ["developer", "admin"], mastercorporate: ["admin", "developer"],
agent: ["developer", "admin"],
developer: ["developer"], admin: ["developer", "admin"],
}, agent: ["developer", "admin"],
deleteUser: { developer: ["developer"],
student: ["corporate", "developer", "admin"], },
teacher: ["corporate", "developer", "admin"], deleteUser: {
corporate: ["admin", "developer"], student: ["corporate", "developer", "admin", "mastercorporate"],
admin: ["developer", "admin"], teacher: ["corporate", "developer", "admin", "mastercorporate"],
agent: ["developer", "admin"], corporate: ["admin", "developer"],
developer: ["developer"], mastercorporate: ["admin", "developer"],
},
updateUser: { admin: ["developer", "admin"],
student: ["developer", "admin"], agent: ["developer", "admin"],
teacher: ["developer", "admin"], developer: ["developer"],
corporate: ["admin", "developer"], },
admin: ["developer", "admin"], updateUser: {
agent: ["developer", "admin"], student: ["developer", "admin"],
developer: ["developer"], teacher: ["developer", "admin"],
},
updateExpiryDate: { corporate: ["admin", "developer"],
student: ["developer", "admin"], mastercorporate: ["admin", "developer"],
teacher: ["developer", "admin"],
corporate: ["admin", "developer"], admin: ["developer", "admin"],
admin: ["developer", "admin"], agent: ["developer", "admin"],
agent: ["developer", "admin"], developer: ["developer"],
developer: ["developer"], },
}, updateExpiryDate: {
examManagement: { student: ["developer", "admin"],
delete: ["developer", "admin"], teacher: ["developer", "admin"],
}, corporate: ["admin", "developer"],
mastercorporate: ["admin", "developer"],
admin: ["developer", "admin"],
agent: ["developer", "admin"],
developer: ["developer"],
},
examManagement: {
delete: ["developer", "admin"],
},
}; };

View File

@@ -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")}

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

View File

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

View File

@@ -1,152 +1,187 @@
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 {
email: string; email: string;
name: string; name: string;
profilePicture: string; profilePicture: string;
id: string; id: string;
isFirstLogin: boolean; isFirstLogin: boolean;
focus: "academic" | "general"; focus: "academic" | "general";
levels: {[key in Module]: number}; levels: { [key in Module]: number };
desiredLevels: {[key in Module]: number}; desiredLevels: { [key in Module]: number };
type: Type; type: Type;
bio: string; bio: string;
isVerified: boolean; isVerified: boolean;
subscriptionExpirationDate?: null | Date; subscriptionExpirationDate?: null | Date;
registrationDate?: Date; registrationDate?: Date;
status: UserStatus; status: UserStatus;
} }
export interface StudentUser extends BasicUser { export interface StudentUser extends BasicUser {
type: "student"; type: "student";
preferredGender?: InstructorGender; preferredGender?: InstructorGender;
demographicInformation?: DemographicInformation; demographicInformation?: DemographicInformation;
preferredTopics?: string[]; preferredTopics?: string[];
} }
export interface TeacherUser extends BasicUser { export interface TeacherUser extends BasicUser {
type: "teacher"; type: "teacher";
demographicInformation?: DemographicInformation; demographicInformation?: DemographicInformation;
} }
export interface CorporateUser extends BasicUser { export interface CorporateUser extends BasicUser {
type: "corporate"; type: "corporate";
corporateInformation: CorporateInformation; corporateInformation: CorporateInformation;
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;
demographicInformation?: DemographicInformation; demographicInformation?: DemographicInformation;
} }
export interface AdminUser extends BasicUser { export interface AdminUser extends BasicUser {
type: "admin"; type: "admin";
demographicInformation?: DemographicInformation; demographicInformation?: DemographicInformation;
} }
export interface DeveloperUser extends BasicUser { export interface DeveloperUser extends BasicUser {
type: "developer"; type: "developer";
preferredGender?: InstructorGender; preferredGender?: InstructorGender;
demographicInformation?: DemographicInformation; demographicInformation?: DemographicInformation;
preferredTopics?: string[]; preferredTopics?: string[];
} }
export interface CorporateInformation { export interface CorporateInformation {
companyInformation: CompanyInformation; companyInformation: CompanyInformation;
monthlyDuration: number; monthlyDuration: number;
payment?: { payment?: {
value: number; value: number;
currency: string; currency: string;
commission: number; commission: number;
}; };
referralAgent?: string; referralAgent?: string;
} }
export interface AgentInformation { export interface AgentInformation {
companyName: string; companyName: string;
commercialRegistration: string; commercialRegistration: string;
companyArabName?: string; companyArabName?: string;
} }
export interface CompanyInformation { export interface CompanyInformation {
name: string; name: string;
userAmount: number; userAmount: number;
} }
export interface DemographicInformation { export interface DemographicInformation {
country: string; country: string;
phone: string; phone: string;
gender: Gender; gender: Gender;
employment: EmploymentStatus; employment: EmploymentStatus;
passport_id?: string; passport_id?: string;
timezone?: string; timezone?: string;
} }
export interface DemographicCorporateInformation { export interface DemographicCorporateInformation {
country: string; country: string;
phone: string; phone: string;
gender: Gender; gender: Gender;
position: string; position: string;
timezone?: string; timezone?: string;
} }
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"
{status: "student", label: "Student"}, | "student"
{status: "employed", label: "Employed"}, | "self-employed"
{status: "unemployed", label: "Unemployed"}, | "unemployed"
{status: "self-employed", label: "Self-employed"}, | "retired"
{status: "retired", label: "Retired"}, | "other";
{status: "other", label: "Other"}, export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] =
]; [
{ status: "student", label: "Student" },
{ status: "employed", label: "Employed" },
{ status: "unemployed", label: "Unemployed" },
{ status: "self-employed", label: "Self-employed" },
{ status: "retired", label: "Retired" },
{ status: "other", label: "Other" },
];
export interface Stat { export interface Stat {
id: string; id: string;
user: string; user: string;
exam: string; exam: string;
exercise: string; exercise: string;
session: string; session: string;
date: number; date: number;
module: Module; module: Module;
solutions: any[]; solutions: any[];
type: string; type: string;
timeSpent?: number; timeSpent?: number;
inactivity?: number; inactivity?: number;
assignment?: string; assignment?: string;
score: { score: {
correct: number; correct: number;
total: number; total: number;
missing: number; missing: number;
}; };
isDisabled?: boolean; isDisabled?: boolean;
} }
export interface Group { export interface Group {
admin: string; admin: string;
name: string; name: string;
participants: string[]; participants: string[];
id: string; id: string;
disableEditing?: boolean; disableEditing?: boolean;
} }
export interface Code { export interface Code {
code: string; code: string;
creator: string; creator: string;
expiryDate: Date; expiryDate: Date;
type: Type; type: Type;
creationDate?: string; creationDate?: string;
userId?: string; userId?: string;
email?: string; email?: string;
name?: string; name?: string;
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",
];

View File

@@ -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>

View File

@@ -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}) {

View File

@@ -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]);

File diff suppressed because it is too large Load Diff

View File

@@ -30,29 +30,74 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") await post(req, res); if (req.method === "POST") await post(req, res);
} }
const getGroupsForUser = async (admin: string, participant: string) => {
try {
const queryConstraints = [
...(admin ? [where("admin", "==", admin)] : []),
...(participant
? [where("participants", "array-contains", participant)]
: []),
];
const snapshot = await getDocs(
queryConstraints.length > 0
? query(collection(db, "groups"), ...queryConstraints)
: collection(db, "groups")
);
const groups = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Group[];
return groups;
} catch (e) {
console.error(e);
return [];
}
};
async function get(req: NextApiRequest, res: NextApiResponse) { async function get(req: NextApiRequest, res: NextApiResponse) {
const { admin, participant } = req.query as { const { admin, participant } = req.query as {
admin: string; admin: string;
participant: string; participant: string;
}; };
const queryConstraints = [ if (req.session?.user?.type === "mastercorporate") {
...(admin ? [where("admin", "==", admin)] : []), try {
...(participant const masterCorporateGroups = await getGroupsForUser(admin, participant);
? [where("participants", "array-contains", participant)] const corporatesFromMaster = masterCorporateGroups
: []), .filter((g) => g.name === "Corporate")
]; .flatMap((g) => g.participants);
const snapshot = await getDocs(
queryConstraints.length > 0
? query(collection(db, "groups"), ...queryConstraints)
: collection(db, "groups"),
);
const groups = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Group[];
res.status(200).json(groups); 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);
} 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()), {

View File

@@ -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();

View File

@@ -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} />}
</> </>

View File

@@ -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

View File

@@ -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);

View File

@@ -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: "/",

View File

@@ -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

View File

@@ -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 {

View File

@@ -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");
};

View File

@@ -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",
})); }));