Did the same to all of the dashboards

This commit is contained in:
Tiago Ribeiro
2024-09-06 15:35:26 +01:00
parent de35e1a8b7
commit 4530e4079f
12 changed files with 237 additions and 241 deletions

View File

@@ -51,8 +51,7 @@ export default function AdminDashboard({user}: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(reload, [page]); useEffect(reload, [page]);
const inactiveCountryManagerFilter = (x: User) => const inactiveCountryManagerFilter = (x: User) => x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate);
x.type === "agent" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
const UserDisplay = (displayUser: User) => ( const UserDisplay = (displayUser: User) => (
<div <div
@@ -72,17 +71,17 @@ export default function AdminDashboard({user}: Props) {
const StudentsList = () => { const StudentsList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "student" && !!selectedUser
(!!selectedUser
? groups ? 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)
: true); : true;
return ( return (
<UserList <UserList
user={user} user={user}
type="student"
filters={[filter]} filters={[filter]}
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -101,17 +100,17 @@ export default function AdminDashboard({user}: Props) {
const TeachersList = () => { const TeachersList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "teacher" && !!selectedUser
(!!selectedUser
? groups ? 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) || false .includes(x.id) || false
: true); : true;
return ( return (
<UserList <UserList
user={user} user={user}
type="teacher"
filters={[filter]} filters={[filter]}
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -129,12 +128,10 @@ export default function AdminDashboard({user}: Props) {
}; };
const AgentsList = () => { const AgentsList = () => {
const filter = (x: User) => x.type === "agent";
return ( return (
<UserList <UserList
user={user} user={user}
filters={[filter]} type="agent"
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
@@ -153,7 +150,7 @@ export default function AdminDashboard({user}: Props) {
const CorporateList = () => ( const CorporateList = () => (
<UserList <UserList
user={user} user={user}
filters={[(x) => x.type === "corporate"]} type="corporate"
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
@@ -170,11 +167,12 @@ export default function AdminDashboard({user}: Props) {
const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => { const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => {
const list = paid ? done : pending; const list = paid ? done : pending;
const filter = (x: User) => x.type === "corporate" && list.includes(x.id); const filter = (x: User) => list.includes(x.id);
return ( return (
<UserList <UserList
user={user} user={user}
type="corporate"
filters={[filter]} filters={[filter]}
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -197,6 +195,7 @@ export default function AdminDashboard({user}: Props) {
return ( return (
<UserList <UserList
user={user} user={user}
type="agent"
filters={[inactiveCountryManagerFilter]} filters={[inactiveCountryManagerFilter]}
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -214,11 +213,12 @@ export default function AdminDashboard({user}: Props) {
}; };
const InactiveStudentsList = () => { const InactiveStudentsList = () => {
const filter = (x: User) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); const filter = (x: User) => x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate);
return ( return (
<UserList <UserList
user={user} user={user}
type="student"
filters={[filter]} filters={[filter]}
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -236,12 +236,13 @@ export default function AdminDashboard({user}: Props) {
}; };
const InactiveCorporateList = () => { const InactiveCorporateList = () => {
const filter = (x: User) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); const filter = (x: User) => x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate);
return ( return (
<UserList <UserList
user={user} user={user}
filters={[filter]} filters={[filter]}
type="corporate"
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div

View File

@@ -161,11 +161,13 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const {data: stats} = useFilterRecordsByUser<Stat[]>(); const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {users, reload, isLoading} = useUsers();
const {groups} = useGroups({admin: user.id}); const {groups} = useGroups({admin: user.id});
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
const {balance} = useUserBalance(); const {balance} = useUserBalance();
const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers({type: "student"});
const {users: teachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers({type: "teacher"});
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();
@@ -173,26 +175,21 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
const assignmentsUsers = useMemo( const assignmentsUsers = useMemo(
() => () =>
users.filter( [...teachers, ...students].filter((x) =>
(x) => !!selectedUser
(x.type === "student" || x.type === "teacher") &&
(!!selectedUser
? groups ? groups
.filter((g) => g.admin === selectedUser.id) .filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id) || false .includes(x.id) || false
: groups.flatMap((g) => g.participants).includes(x.id)), : groups.flatMap((g) => g.participants).includes(x.id),
), ),
[groups, users, selectedUser], [groups, teachers, students, selectedUser],
); );
useEffect(() => { useEffect(() => {
setShowModal(!!selectedUser && router.asPath === "/#"); setShowModal(!!selectedUser && router.asPath === "/#");
}, [selectedUser, router.asPath]); }, [selectedUser, router.asPath]);
const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id);
const teacherFilter = (user: User) => user.type === "teacher" && groups.flatMap((g) => g.participants).includes(user.id);
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => ( const UserDisplay = (displayUser: User) => (
@@ -228,12 +225,10 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
}; };
const StudentPerformancePage = () => { const StudentPerformancePage = () => {
const students = users const performanceStudents = students.map((u) => ({
.filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
.map((u) => ({
...u, ...u,
group: groups.find((x) => x.participants.includes(u.id))?.name || "N/A", group: groups.find((x) => x.participants.includes(u.id))?.name || "N/A",
corporateName: getUserCompanyName(u, users, groups), corporateName: getUserCompanyName(user, [], groups),
})); }));
return ( return (
@@ -246,13 +241,13 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
<span>Back</span> <span>Back</span>
</div> </div>
<div <div
onClick={reload} onClick={reloadStudents}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<span>Reload</span> <span>Reload</span>
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} /> <BsArrowRepeat className={clsx("text-xl", isStudentsLoading && "animate-spin")} />
</div> </div>
</div> </div>
<StudentPerformanceList items={students} stats={stats} users={users} /> <StudentPerformanceList items={performanceStudents} stats={stats} users={students} />
</> </>
); );
}; };
@@ -260,7 +255,7 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
const averageLevelCalculator = (studentStats: Stat[]) => { const averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats const formattedStats = studentStats
.map((s) => ({ .map((s) => ({
focus: users.find((u) => u.id === s.user)?.focus, focus: students.find((u) => u.id === s.user)?.focus,
score: s.score, score: s.score,
module: s.module, module: s.module,
})) }))
@@ -292,16 +287,18 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
<section className="grid grid-cols-5 -md:grid-cols-2 gap-4 text-center"> <section className="grid grid-cols-5 -md:grid-cols-2 gap-4 text-center">
<IconCard <IconCard
onClick={() => router.push("/#students")} onClick={() => router.push("/#students")}
isLoading={isStudentsLoading}
Icon={BsPersonFill} Icon={BsPersonFill}
label="Students" label="Students"
value={users.filter(studentFilter).length} value={students.length}
color="purple" color="purple"
/> />
<IconCard <IconCard
onClick={() => router.push("/#teachers")} onClick={() => router.push("/#teachers")}
isLoading={isTeachersLoading}
Icon={BsPencilSquare} Icon={BsPencilSquare}
label="Teachers" label="Teachers"
value={users.filter(teacherFilter).length} value={teachers.length}
color="purple" color="purple"
/> />
<IconCard <IconCard
@@ -312,6 +309,7 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
/> />
<IconCard <IconCard
Icon={BsPaperclip} Icon={BsPaperclip}
isLoading={isStudentsLoading}
label="Average Level" label="Average Level"
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)} value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
color="purple" color="purple"
@@ -331,8 +329,9 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
/> />
<IconCard <IconCard
Icon={BsPersonFillGear} Icon={BsPersonFillGear}
isLoading={isStudentsLoading}
label="Student Performance" label="Student Performance"
value={users.filter(studentFilter).length} value={students.length}
color="purple" color="purple"
onClick={() => router.push("/#studentsPerformance")} onClick={() => router.push("/#studentsPerformance")}
/> />
@@ -354,8 +353,7 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest students</span> <span className="p-4">Latest students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {students
.filter(studentFilter)
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) .sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -365,8 +363,7 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest teachers</span> <span className="p-4">Latest teachers</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {teachers
.filter(teacherFilter)
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) .sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -376,8 +373,7 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest level students</span> <span className="p-4">Highest level students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {students
.filter(studentFilter)
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -387,8 +383,7 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest exam count students</span> <span className="p-4">Highest exam count students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {students
.filter(studentFilter)
.sort( .sort(
(a, b) => (a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length, Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
@@ -412,7 +407,8 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
loggedInUser={user} loggedInUser={user}
onClose={(shouldReload) => { onClose={(shouldReload) => {
setSelectedUser(undefined); setSelectedUser(undefined);
if (shouldReload) reload(); if (shouldReload && selectedUser!.type === "student") reloadStudents();
if (shouldReload && selectedUser!.type === "teacher") reloadTeachers();
}} }}
onViewStudents={ onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "teacher" selectedUser.type === "corporate" || selectedUser.type === "teacher"
@@ -463,7 +459,7 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
{router.asPath === "/#students" && ( {router.asPath === "/#students" && (
<UserList <UserList
user={user} user={user}
filters={[(x) => x.type === "student"]} type="student"
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
@@ -480,7 +476,7 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
{router.asPath === "/#teachers" && ( {router.asPath === "/#teachers" && (
<UserList <UserList
user={user} user={user}
filters={[(x) => x.type === "teacher"]} type="teacher"
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div

View File

@@ -1,4 +1,4 @@
import React from "react"; import React, {useMemo} from "react";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import {User} from "@/interfaces/user"; import {User} from "@/interfaces/user";
@@ -61,29 +61,17 @@ const Card = ({user}: {user: User}) => {
}; };
const CorporateStudentsLevels = () => { const CorporateStudentsLevels = () => {
const {users} = useUsers();
const {groups} = useGroups({});
const corporateUsers = users.filter((u) => u.type === "corporate") as User[];
const [corporateId, setCorporateId] = React.useState<string>(""); const [corporateId, setCorporateId] = React.useState<string>("");
const corporate = corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
const groupsFromCorporate = corporate ? groups.filter((g) => g.admin === corporate.id) : []; const {users: students} = useUsers({type: "student"});
const {users: corporates} = useUsers({type: "corporate"});
const groupsParticipants = groupsFromCorporate const corporate = useMemo(() => corporates.find((u) => u.id === corporateId) || corporates[0], [corporates, corporateId]);
.flatMap((g) => g.participants)
.reduce((accm: User[], p) => {
const user = users.find((u) => u.id === p) as User;
if (user) {
return [...accm, user];
}
return accm;
}, []);
return ( return (
<> <>
<Select <Select
options={corporateUsers.map((x: User) => ({ options={corporates.map((x: User) => ({
value: x.id, value: x.id,
label: `${x.name} - ${x.email}`, label: `${x.name} - ${x.email}`,
}))} }))}
@@ -98,7 +86,7 @@ const CorporateStudentsLevels = () => {
}), }),
}} }}
/> />
{groupsParticipants.map((u) => ( {students.map((u) => (
<Card user={u} key={u.id} /> <Card user={u} key={u.id} />
))} ))}
</> </>

View File

@@ -9,19 +9,11 @@ interface Props {
tooltip?: string; tooltip?: string;
onClick?: () => void; onClick?: () => void;
isSelected?: boolean; isSelected?: boolean;
isLoading?: boolean;
className?: string; className?: string;
} }
export default function IconCard({ export default function IconCard({Icon, label, value, color, tooltip, onClick, className, isLoading, isSelected}: Props) {
Icon,
label,
value,
color,
tooltip,
onClick,
className,
isSelected,
}: Props) {
const colorClasses: {[key in typeof color]: string} = { const colorClasses: {[key in typeof color]: string} = {
purple: "mti-purple-light", purple: "mti-purple-light",
red: "mti-red-light", red: "mti-red-light",
@@ -38,13 +30,12 @@ export default function IconCard({
isSelected && `border border-solid border-${colorClasses[color]}`, isSelected && `border border-solid border-${colorClasses[color]}`,
className, className,
)} )}
data-tip={tooltip} data-tip={tooltip}>
>
<Icon className={clsx("text-6xl", `text-${colorClasses[color]}`)} /> <Icon className={clsx("text-6xl", `text-${colorClasses[color]}`)} />
<span className="flex flex-col gap-1 items-center text-xl"> <span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">{label}</span> <span className="text-lg">{label}</span>
<span className={clsx("font-semibold", `text-${colorClasses[color]}`)}> <span className={clsx("font-semibold", `text-${colorClasses[color]}`, isLoading && "animate-pulse")}>
{value} {isLoading ? "..." : value}
</span> </span>
</span> </span>
</div> </div>

View File

@@ -296,21 +296,20 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
}; };
export default function MasterCorporateDashboard({user}: Props) { export default function MasterCorporateDashboard({user}: Props) {
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 [corporateAssignments, setCorporateAssignments] = useState<(Assignment & {corporate?: CorporateUser})[]>([]); const [corporateAssignments, setCorporateAssignments] = useState<(Assignment & {corporate?: CorporateUser})[]>([]);
const {data: stats} = useFilterRecordsByUser<Stat[]>(); const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {users: students, reload: reloadStudents} = useUsers({type: "student"}); const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers({type: "student"});
const {users: teachers, reload: reloadTeachers} = useUsers({type: "teacher"}); const {users: teachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers({type: "teacher"});
const {users: corporates, reload: reloadCorporates} = useUsers({type: "corporate"}); const {users: corporates, reload: reloadCorporates, isLoading: isCorporatesLoading} = useUsers({type: "corporate"});
const {groups} = useGroups({admin: user.id, userType: user.type}); const {groups} = useGroups({admin: user.id, userType: user.type});
const {balance} = useUserBalance(); const {balance} = useUserBalance();
const users = useMemo(() => [...students, ...teachers, ...corporates], [corporates, students, teachers]); const users = useMemo(() => uniqBy([...students, ...teachers, ...corporates, user], "id"), [corporates, students, teachers, user]);
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
@@ -423,8 +422,22 @@ export default function MasterCorporateDashboard({user}: Props) {
const DefaultDashboard = () => ( const DefaultDashboard = () => (
<> <>
<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 onClick={() => router.push("/#students")} Icon={BsPersonFill} label="Students" value={students.length} color="purple" /> <IconCard
<IconCard onClick={() => router.push("/#teachers")} Icon={BsPencilSquare} label="Teachers" value={teachers.length} color="purple" /> onClick={() => router.push("/#students")}
Icon={BsPersonFill}
isLoading={isStudentsLoading}
label="Students"
value={students.length}
color="purple"
/>
<IconCard
onClick={() => router.push("/#teachers")}
Icon={BsPencilSquare}
isLoading={isTeachersLoading}
label="Teachers"
value={teachers.length}
color="purple"
/>
<IconCard <IconCard
Icon={BsClipboard2Data} Icon={BsClipboard2Data}
label="Exams Performed" label="Exams Performed"
@@ -453,9 +466,17 @@ export default function MasterCorporateDashboard({user}: Props) {
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"} value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
color="rose" color="rose"
/> />
<IconCard Icon={BsBank} label="Corporate" value={corporates.length} color="purple" onClick={() => router.push("/#corporate")} /> <IconCard
Icon={BsBank}
label="Corporate"
value={corporates.length}
isLoading={isCorporatesLoading}
color="purple"
onClick={() => router.push("/#corporate")}
/>
<IconCard <IconCard
Icon={BsPersonFillGear} Icon={BsPersonFillGear}
isLoading={isStudentsLoading}
label="Student Performance" label="Student Performance"
value={students.length} value={students.length}
color="purple" color="purple"
@@ -598,7 +619,7 @@ export default function MasterCorporateDashboard({user}: Props) {
{router.asPath === "/#students" && ( {router.asPath === "/#students" && (
<UserList <UserList
user={user} user={user}
filters={[(x) => x.type === "student"]} type="student"
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
@@ -615,7 +636,7 @@ export default function MasterCorporateDashboard({user}: Props) {
{router.asPath === "/#teachers" && ( {router.asPath === "/#teachers" && (
<UserList <UserList
user={user} user={user}
filters={[(x) => x.type === "teacher"]} type="teacher"
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
@@ -633,7 +654,7 @@ export default function MasterCorporateDashboard({user}: Props) {
{router.asPath === "/#corporate" && ( {router.asPath === "/#corporate" && (
<UserList <UserList
user={user} user={user}
filters={[(x) => x.type === "corporate"]} type="corporate"
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div

View File

@@ -24,7 +24,7 @@ import {capitalize} from "lodash";
import moment from "moment"; import moment from "moment";
import Link from "next/link"; import Link from "next/link";
import {useRouter} from "next/router"; import {useRouter} from "next/router";
import {useEffect, useState} from "react"; import {useEffect, useMemo, useState} from "react";
import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs"; import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {activeAssignmentFilter} from "@/utils/assignments"; import {activeAssignmentFilter} from "@/utils/assignments";
@@ -33,17 +33,20 @@ import useSessions from "@/hooks/useSessions";
interface Props { interface Props {
user: User; user: User;
users: User[];
linkedCorporate?: CorporateUser | MasterCorporateUser; linkedCorporate?: CorporateUser | MasterCorporateUser;
} }
export default function StudentDashboard({user, users, linkedCorporate}: Props) { export default function StudentDashboard({user, linkedCorporate}: Props) {
const {gradingSystem} = useGradingSystem(); const {gradingSystem} = useGradingSystem();
const {sessions} = useSessions(user.id); const {sessions} = useSessions(user.id);
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id, !user?.id); const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id, !user?.id);
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id}); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id});
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id}); const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id});
const {users: teachers} = useUsers({type: "teacher"});
const {users: corporates} = useUsers({type: "corporate"});
const users = useMemo(() => [...teachers, ...corporates], [teachers, corporates]);
const router = useRouter(); const router = useRouter();
const setExams = useExamStore((state) => state.setExams); const setExams = useExamStore((state) => state.setExams);

View File

@@ -63,11 +63,12 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const {data: stats} = useFilterRecordsByUser<Stat[]>(); const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {users, reload} = useUsers();
const {groups} = useGroups({adminAdmins: user.id}); const {groups} = useGroups({adminAdmins: user.id});
const {permissions} = usePermissions(user.id); const {permissions} = usePermissions(user.id);
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id}); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers({type: "student"});
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();
@@ -75,25 +76,21 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
const assignmentsUsers = useMemo( const assignmentsUsers = useMemo(
() => () =>
users.filter( students.filter((x) =>
(x) => !!selectedUser
x.type === "student" &&
(!!selectedUser
? groups ? groups
.filter((g) => g.admin === selectedUser.id) .filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id) .includes(x.id)
: groups.flatMap((g) => g.participants).includes(x.id)), : groups.flatMap((g) => g.participants).includes(x.id),
), ),
[groups, users, selectedUser], [groups, students, selectedUser],
); );
useEffect(() => { useEffect(() => {
setShowModal(!!selectedUser && router.asPath === "/#"); setShowModal(!!selectedUser && router.asPath === "/#");
}, [selectedUser, router.asPath]); }, [selectedUser, router.asPath]);
const studentFilter = (user: User) => user.type === "student";
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => ( const UserDisplay = (displayUser: User) => (
@@ -131,7 +128,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
const averageLevelCalculator = (studentStats: Stat[]) => { const averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats const formattedStats = studentStats
.map((s) => ({ .map((s) => ({
focus: users.find((u) => u.id === s.user)?.focus, focus: students.find((u) => u.id === s.user)?.focus,
score: s.score, score: s.score,
module: s.module, module: s.module,
})) }))
@@ -167,9 +164,10 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
)}> )}>
<IconCard <IconCard
onClick={() => router.push("/#students")} onClick={() => router.push("/#students")}
isLoading={isStudentsLoading}
Icon={BsPersonFill} Icon={BsPersonFill}
label="Students" label="Students"
value={users.filter(studentFilter).length} value={students.length}
color="purple" color="purple"
/> />
<IconCard <IconCard
@@ -181,6 +179,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
<IconCard <IconCard
Icon={BsPaperclip} Icon={BsPaperclip}
label="Average Level" label="Average Level"
isLoading={isStudentsLoading}
value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)} value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
color="purple" color="purple"
/> />
@@ -208,8 +207,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest students</span> <span className="p-4">Latest students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {students
.filter(studentFilter)
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) .sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -219,8 +217,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest level students</span> <span className="p-4">Highest level students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {students
.filter(studentFilter)
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -230,8 +227,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Highest exam count students</span> <span className="p-4">Highest exam count students</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {students
.filter(studentFilter)
.sort( .sort(
(a, b) => (a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length, Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
@@ -255,7 +251,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
loggedInUser={user} loggedInUser={user}
onClose={(shouldReload) => { onClose={(shouldReload) => {
setSelectedUser(undefined); setSelectedUser(undefined);
if (shouldReload) reload(); if (shouldReload && selectedUser!.type === "student") reloadStudents();
}} }}
onViewStudents={ onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "teacher" selectedUser.type === "corporate" || selectedUser.type === "teacher"
@@ -306,7 +302,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
{router.asPath === "/#students" && ( {router.asPath === "/#students" && (
<UserList <UserList
user={user} user={user}
filters={[(x) => x.type === "student"]} type="student"
renderHeader={(total) => ( renderHeader={(total) => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div

View File

@@ -1,7 +1,11 @@
import {Group, User} from "@/interfaces/user"; import {Group, User} from "@/interfaces/user";
import axios from "axios"; import Axios from "axios";
import {setupCache} from "axios-cache-interceptor";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
const instance = Axios.create();
const axios = setupCache(instance);
interface Props { interface Props {
admin?: string; admin?: string;
userType?: string; userType?: string;

View File

@@ -46,10 +46,12 @@ const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; grou
export default function UserList({ export default function UserList({
user, user,
filters = [], filters = [],
type,
renderHeader, renderHeader,
}: { }: {
user: User; user: User;
filters?: ((user: User) => boolean)[]; filters?: ((user: User) => boolean)[];
type?: Type;
renderHeader?: (total: number) => JSX.Element; renderHeader?: (total: number) => JSX.Element;
}) { }) {
const [showDemographicInformation, setShowDemographicInformation] = useState(false); const [showDemographicInformation, setShowDemographicInformation] = useState(false);
@@ -57,7 +59,7 @@ export default function UserList({
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({type});
const {permissions} = usePermissions(user?.id || ""); const {permissions} = usePermissions(user?.id || "");
const {balance} = useUserBalance(); const {balance} = useUserBalance();
const {groups} = useGroups({ const {groups} = useGroups({

View File

@@ -56,10 +56,9 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
} }
const linkedCorporate = (await getUserCorporate(user.id)) || null; const linkedCorporate = (await getUserCorporate(user.id)) || null;
const users = await getUsers();
return { return {
props: {user, envVariables, linkedCorporate, users}, props: {user, envVariables, linkedCorporate},
}; };
}, sessionOptions); }, sessionOptions);
@@ -67,10 +66,9 @@ interface Props {
user: User; user: User;
envVariables: {[key: string]: string}; envVariables: {[key: string]: string};
linkedCorporate?: CorporateUser | MasterCorporateUser; linkedCorporate?: CorporateUser | MasterCorporateUser;
users: User[];
} }
export default function Home({users, linkedCorporate}: Props) { export default function Home({linkedCorporate}: Props) {
const [showDiagnostics, setShowDiagnostics] = useState(false); const [showDiagnostics, setShowDiagnostics] = useState(false);
const [showDemographicInput, setShowDemographicInput] = useState(false); const [showDemographicInput, setShowDemographicInput] = useState(false);
const [selectedScreen, setSelectedScreen] = useState<Type>("admin"); const [selectedScreen, setSelectedScreen] = useState<Type>("admin");
@@ -176,7 +174,7 @@ export default function Home({users, linkedCorporate}: Props) {
<ToastContainer /> <ToastContainer />
{user && ( {user && (
<Layout user={user}> <Layout user={user}>
{checkAccess(user, ["student"]) && <StudentDashboard users={users} linkedCorporate={linkedCorporate} user={user} />} {checkAccess(user, ["student"]) && <StudentDashboard linkedCorporate={linkedCorporate} user={user} />}
{checkAccess(user, ["teacher"]) && <TeacherDashboard linkedCorporate={linkedCorporate} user={user} />} {checkAccess(user, ["teacher"]) && <TeacherDashboard linkedCorporate={linkedCorporate} user={user} />}
{checkAccess(user, ["corporate"]) && <CorporateDashboard linkedCorporate={linkedCorporate} user={user as CorporateUser} />} {checkAccess(user, ["corporate"]) && <CorporateDashboard linkedCorporate={linkedCorporate} user={user as CorporateUser} />}
{checkAccess(user, ["mastercorporate"]) && <MasterCorporateDashboard user={user as MasterCorporateUser} />} {checkAccess(user, ["mastercorporate"]) && <MasterCorporateDashboard user={user as MasterCorporateUser} />}
@@ -196,7 +194,7 @@ export default function Home({users, linkedCorporate}: Props) {
onChange={(value) => (value ? setSelectedScreen(value.value) : setSelectedScreen("admin"))} onChange={(value) => (value ? setSelectedScreen(value.value) : setSelectedScreen("admin"))}
/> />
{selectedScreen === "student" && <StudentDashboard users={users} linkedCorporate={linkedCorporate} user={user} />} {selectedScreen === "student" && <StudentDashboard linkedCorporate={linkedCorporate} user={user} />}
{selectedScreen === "teacher" && <TeacherDashboard linkedCorporate={linkedCorporate} user={user} />} {selectedScreen === "teacher" && <TeacherDashboard linkedCorporate={linkedCorporate} user={user} />}
{selectedScreen === "corporate" && ( {selectedScreen === "corporate" && (
<CorporateDashboard linkedCorporate={linkedCorporate} user={user as unknown as CorporateUser} /> <CorporateDashboard linkedCorporate={linkedCorporate} user={user as unknown as CorporateUser} />

View File

@@ -37,11 +37,9 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
export default function UsersListPage() { export default function UsersListPage() {
const {user} = useUser(); const {user} = useUser();
const { users } = useUsers();
const [filters, clearFilters] = useFilterStore((state) => [ const [filters, clearFilters] = useFilterStore((state) => [state.userFilters, state.clearUserFilters]);
state.userFilters,
state.clearUserFilters,
]);
const router = useRouter(); const router = useRouter();
return ( return (
@@ -69,8 +67,7 @@ export default function UsersListPage() {
clearFilters(); clearFilters();
router.back(); router.back();
}} }}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>

View File

@@ -53,8 +53,15 @@ export async function getSpecificUsers(ids: string[]) {
} }
export async function getLinkedUsers(userID?: string, userType?: Type, type?: Type, page?: number, size?: number) { export async function getLinkedUsers(userID?: string, userType?: Type, type?: Type, page?: number, size?: number) {
const q = [
...(!!type ? [where("type", "==", type)] : []),
orderBy(documentId()),
...(page !== undefined && !!size ? [startAt(page * size)] : []),
...(page !== undefined && !!size ? [limit(page + 1 * size)] : []),
];
if (!userID || userType === "admin" || userType === "developer") { if (!userID || userType === "admin" || userType === "developer") {
const snapshot = await getDocs(collection(db, "users")); const snapshot = await getDocs(query(collection(db, "users"), ...q));
const users = snapshot.docs.map((doc) => ({ const users = snapshot.docs.map((doc) => ({
id: doc.id, id: doc.id,
...doc.data(), ...doc.data(),
@@ -73,15 +80,7 @@ export async function getLinkedUsers(userID?: string, userType?: Type, type?: Ty
...(userType === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []), ...(userType === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []),
]); ]);
const q = [ const snapshot = await getDocs(query(collection(db, "users"), ...[where(documentId(), "in", participants), ...q]));
where(documentId(), "in", participants),
...(!!type ? [where("type", "==", type)] : []),
orderBy(documentId()),
...(page !== undefined && !!size ? [startAt(page * size)] : []),
...(page !== undefined && !!size ? [limit(page + 1 * size)] : []),
];
const snapshot = await getDocs(query(collection(db, "users"), ...q));
const users = snapshot.docs.map((doc) => ({ const users = snapshot.docs.map((doc) => ({
id: doc.id, id: doc.id,
...doc.data(), ...doc.data(),