Continued updating the code to work with entities better
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {useListSearch} from "@/hooks/useListSearch";
|
import {useListSearch} from "@/hooks/useListSearch";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import usePagination from "@/hooks/usePagination";
|
||||||
|
import { clsx } from "clsx";
|
||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
import Checkbox from "../Low/Checkbox";
|
import Checkbox from "../Low/Checkbox";
|
||||||
import Separator from "../Low/Separator";
|
import Separator from "../Low/Separator";
|
||||||
@@ -10,20 +11,21 @@ interface Props<T> {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
firstCard?: () => ReactNode;
|
firstCard?: () => ReactNode;
|
||||||
renderCard: (item: T) => ReactNode;
|
renderCard: (item: T) => ReactNode;
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CardList<T>({list, searchFields, renderCard, firstCard, pageSize = 20}: Props<T>) {
|
export default function CardList<T>({list, searchFields, renderCard, firstCard, className, pageSize = 20}: Props<T>) {
|
||||||
const {rows, renderSearch} = useListSearch(searchFields, list);
|
const {rows, renderSearch} = useListSearch(searchFields, list);
|
||||||
|
|
||||||
const {items, page, renderMinimal} = usePagination(rows, pageSize);
|
const {items, page, render, renderMinimal} = usePagination(rows, pageSize);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col gap-4 w-full">
|
<section className="flex flex-col gap-4 w-full">
|
||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
{renderSearch()}
|
{searchFields.length > 0 && renderSearch()}
|
||||||
{renderMinimal()}
|
{searchFields.length > 0 ? renderMinimal() : render()}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className={clsx("w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4", className)}>
|
||||||
{page === 0 && !!firstCard && firstCard()}
|
{page === 0 && !!firstCard && firstCard()}
|
||||||
{items.map(renderCard)}
|
{items.map(renderCard)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
107
src/components/High/Table.tsx
Normal file
107
src/components/High/Table.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { useListSearch } from "@/hooks/useListSearch"
|
||||||
|
import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, useReactTable } from "@tanstack/react-table"
|
||||||
|
import clsx from "clsx"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { BsArrowDown, BsArrowUp } from "react-icons/bs"
|
||||||
|
import Button from "../Low/Button"
|
||||||
|
|
||||||
|
interface Props<T> {
|
||||||
|
data: T[]
|
||||||
|
columns: ColumnDef<any, any>[]
|
||||||
|
searchFields: string[][]
|
||||||
|
size?: number
|
||||||
|
onDownload?: (rows: T[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Table<T>({ data, columns, searchFields, size = 16, onDownload }: Props<T>) {
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 16,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rows, renderSearch } = useListSearch<T>(searchFields, data);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: rows,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
state: {
|
||||||
|
pagination
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col gap-2">
|
||||||
|
<div className="w-full flex gap-2 items-end">
|
||||||
|
{renderSearch()}
|
||||||
|
{onDownload && (
|
||||||
|
<Button className="w-full max-w-[200px] mb-1" variant="outline" onClick={() => onDownload(rows)}>
|
||||||
|
Download List
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex gap-2 justify-between items-center">
|
||||||
|
<div className="flex items-center gap-4 w-fit">
|
||||||
|
<Button className="w-[200px] h-fit" disabled={!table.getCanPreviousPage()} onClick={() => table.previousPage()}>
|
||||||
|
Previous Page
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 w-fit">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div>Page</div>
|
||||||
|
<strong>
|
||||||
|
{table.getState().pagination.pageIndex + 1} of{' '}
|
||||||
|
{table.getPageCount().toLocaleString()}
|
||||||
|
</strong>
|
||||||
|
<div>| Total: {table.getRowCount().toLocaleString()}</div>
|
||||||
|
</span>
|
||||||
|
<Button className="w-[200px]" disabled={!table.getCanNextPage()} onClick={() => table.nextPage()}>
|
||||||
|
Next Page
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<th className="py-4 px-4 text-left" key={header.id} colSpan={header.colSpan}>
|
||||||
|
<div
|
||||||
|
className={clsx(header.column.getCanSort() && 'cursor-pointer select-none', 'flex items-center gap-2')}
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
{{
|
||||||
|
asc: <BsArrowUp />,
|
||||||
|
desc: <BsArrowDown />,
|
||||||
|
}[header.column.getIsSorted() as string] ?? null}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody className="px-2 w-full">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import Select from "../Low/Select";
|
import Select from "../Low/Select";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import useRecordStore from "@/stores/recordStore";
|
import useRecordStore from "@/stores/recordStore";
|
||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import { mapBy } from "@/utils";
|
||||||
|
|
||||||
|
|
||||||
type TimeFilter = "months" | "weeks" | "days";
|
type TimeFilter = "months" | "weeks" | "days";
|
||||||
@@ -13,6 +15,8 @@ type Filter = TimeFilter | "assignments" | undefined;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
|
entities: EntityWithRoles[]
|
||||||
|
users: User[]
|
||||||
filterState: {
|
filterState: {
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
setFilter: React.Dispatch<React.SetStateAction<Filter>>
|
setFilter: React.Dispatch<React.SetStateAction<Filter>>
|
||||||
@@ -28,83 +32,41 @@ const defaultSelectableCorporate = {
|
|||||||
|
|
||||||
const RecordFilter: React.FC<Props> = ({
|
const RecordFilter: React.FC<Props> = ({
|
||||||
user,
|
user,
|
||||||
|
entities,
|
||||||
|
users,
|
||||||
filterState,
|
filterState,
|
||||||
assignments = true,
|
assignments = true,
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const { filter, setFilter } = filterState;
|
const { filter, setFilter } = filterState;
|
||||||
|
|
||||||
const [statsUserId, setStatsUserId] = useRecordStore((state) => [
|
const [entity, setEntity] = useState<string>()
|
||||||
|
|
||||||
|
const [, setStatsUserId] = useRecordStore((state) => [
|
||||||
state.selectedUser,
|
state.selectedUser,
|
||||||
state.setSelectedUser
|
state.setSelectedUser
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { users } = useUsers();
|
const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [users, entity])
|
||||||
const { groups: allGroups } = useGroups({});
|
|
||||||
const { groups } = useGroups({ admin: user?.id, userType: user?.type });
|
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user.id])
|
||||||
|
|
||||||
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
|
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
|
||||||
setFilter((prev) => (prev === value ? undefined : value));
|
setFilter((prev) => (prev === value ? undefined : value));
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectableCorporates = [
|
|
||||||
defaultSelectableCorporate,
|
|
||||||
...users
|
|
||||||
.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id))
|
|
||||||
.filter((x) => x.type === "corporate")
|
|
||||||
.map((x) => ({
|
|
||||||
value: x.id,
|
|
||||||
label: `${x.name} - ${x.email}`,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
|
|
||||||
|
|
||||||
const getUsersList = (): User[] => {
|
|
||||||
if (selectedCorporate) {
|
|
||||||
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate);
|
|
||||||
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
|
|
||||||
|
|
||||||
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
|
|
||||||
return userListWithUsers.filter((x) => x);
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.type !== "mastercorporate" ? users : users.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const corporateFilteredUserList = getUsersList();
|
|
||||||
|
|
||||||
const getSelectedUser = () => {
|
|
||||||
if (selectedCorporate) {
|
|
||||||
const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId);
|
|
||||||
return userInCorporate || corporateFilteredUserList[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return users.find((x) => x.id === statsUserId) || user;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedUser = getSelectedUser();
|
|
||||||
const selectedUserSelectValue = selectedUser
|
|
||||||
? {
|
|
||||||
value: selectedUser.id,
|
|
||||||
label: `${selectedUser.name} - ${selectedUser.email}`,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
value: "",
|
|
||||||
label: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
|
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
|
||||||
<div className="xl:w-3/4">
|
<div className="xl:w-3/4 flex gap-2">
|
||||||
{checkAccess(user, ["developer", "admin", "mastercorporate"]) && !children && (
|
{checkAccess(user, ["developer", "admin", "mastercorporate"]) && !children && (
|
||||||
<>
|
<>
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
options={selectableCorporates}
|
options={entities.map((e) => ({value: e.id, label: e.label}))}
|
||||||
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
onChange={(value) => setEntity(value?.value || undefined)}
|
||||||
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
isClearable
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
@@ -112,15 +74,17 @@ const RecordFilter: React.FC<Props> = ({
|
|||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}></Select>
|
}} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
options={corporateFilteredUserList.map((x) => ({
|
options={entityUsers.map((x) => ({
|
||||||
value: x.id,
|
value: x.id,
|
||||||
label: `${x.name} - ${x.email}`,
|
label: `${x.name} - ${x.email}`,
|
||||||
}))}
|
}))}
|
||||||
value={selectedUserSelectValue}
|
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
onChange={(value) => setStatsUserId(value?.value!)}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
@@ -131,20 +95,20 @@ const RecordFilter: React.FC<Props> = ({
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && !children && (
|
{(user.type === "corporate" || user.type === "teacher") && !children && (
|
||||||
<>
|
<div className="flex flex-col gap-2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
options={users
|
options={users
|
||||||
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
|
||||||
.map((x) => ({
|
.map((x) => ({
|
||||||
value: x.id,
|
value: x.id,
|
||||||
label: `${x.name} - ${x.email}`,
|
label: `${x.name} - ${x.email}`,
|
||||||
}))}
|
}))}
|
||||||
value={selectedUserSelectValue}
|
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
onChange={(value) => setStatsUserId(value?.value!)}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
@@ -155,7 +119,7 @@ const RecordFilter: React.FC<Props> = ({
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
src/components/UserDisplayList.tsx
Normal file
30
src/components/UserDisplayList.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/** eslint-disable @next/next/no-img-element */
|
||||||
|
import { User } from "@/interfaces/user"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
users: User[]
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserDisplay = (displayUser: User) => (
|
||||||
|
<div 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>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function UserDisplayList({ title, users }: Props) {
|
||||||
|
return (<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
||||||
|
<span className="p-4">{title}</span>
|
||||||
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
|
{users
|
||||||
|
.slice(0, 10)
|
||||||
|
.map((x) => (
|
||||||
|
<UserDisplay key={x.id} {...x} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
23
src/hooks/useEntities.tsx
Normal file
23
src/hooks/useEntities.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import { Discount } from "@/interfaces/paypal";
|
||||||
|
import { Code, Group, User } from "@/interfaces/user";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function useEntities(creator?: string) {
|
||||||
|
const [entities, setEntities] = useState<EntityWithRoles[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
axios
|
||||||
|
.get<EntityWithRoles[]>("/api/entities?showRoles=true")
|
||||||
|
.then((response) => setEntities(response.data))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(getData, [creator]);
|
||||||
|
|
||||||
|
return { entities, isLoading, isError, reload: getData };
|
||||||
|
}
|
||||||
23
src/hooks/useEntitiesGroups.tsx
Normal file
23
src/hooks/useEntitiesGroups.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { EntityWithRoles, WithEntity, WithLabeledEntities } from "@/interfaces/entity";
|
||||||
|
import { Discount } from "@/interfaces/paypal";
|
||||||
|
import { Code, Group, Type, User } from "@/interfaces/user";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function useEntitiesGroups() {
|
||||||
|
const [groups, setGroups] = useState<WithEntity<Group>[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
axios
|
||||||
|
.get<WithEntity<Group>[]>(`/api/entities/groups`)
|
||||||
|
.then((response) => setGroups(response.data))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(getData, []);
|
||||||
|
|
||||||
|
return { groups, isLoading, isError, reload: getData };
|
||||||
|
}
|
||||||
23
src/hooks/useEntitiesUsers.tsx
Normal file
23
src/hooks/useEntitiesUsers.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { EntityWithRoles, WithLabeledEntities } from "@/interfaces/entity";
|
||||||
|
import { Discount } from "@/interfaces/paypal";
|
||||||
|
import { Code, Group, Type, User } from "@/interfaces/user";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function useEntitiesUsers(type?: Type) {
|
||||||
|
const [users, setUsers] = useState<WithLabeledEntities<User>[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
axios
|
||||||
|
.get<WithLabeledEntities<User>[]>(`/api/entities/users${type ? "?type=" + type : ""}`)
|
||||||
|
.then((response) => setUsers(response.data))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(getData, [type]);
|
||||||
|
|
||||||
|
return { users, isLoading, isError, reload: getData };
|
||||||
|
}
|
||||||
@@ -12,8 +12,17 @@ export interface Role {
|
|||||||
|
|
||||||
export interface EntityWithRoles extends Entity {
|
export interface EntityWithRoles extends Entity {
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type WithEntity<T> = T extends {entities: {id: string; role: string}[]}
|
export type WithLabeledEntities<T> = T extends { entities: { id: string; role: string }[] }
|
||||||
|
? Omit<T, "entities"> & { entities: { id: string; label?: string; role: string, roleLabel?: string }[] }
|
||||||
|
: T;
|
||||||
|
|
||||||
|
|
||||||
|
export type WithEntity<T> = T extends { entity?: string }
|
||||||
|
? Omit<T, "entity"> & { entity: Entity }
|
||||||
|
: T;
|
||||||
|
|
||||||
|
export type WithEntities<T> = T extends { entities: { id: string; role: string }[] }
|
||||||
? Omit<T, "entities"> & { entities: { entity?: Entity; role?: Role }[] }
|
? Omit<T, "entities"> & { entities: { entity?: Entity; role?: Role }[] }
|
||||||
: T;
|
: T;
|
||||||
|
|||||||
@@ -18,34 +18,15 @@ import {isAgentUser, isCorporateUser, USER_TYPE_LABELS} from "@/resources/user";
|
|||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import { useListSearch } from "@/hooks/useListSearch";
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
|
import Table from "@/components/High/Table";
|
||||||
|
import useEntitiesGroups from "@/hooks/useEntitiesGroups";
|
||||||
|
import useEntitiesUsers from "@/hooks/useEntitiesUsers";
|
||||||
|
import { WithEntity } from "@/interfaces/entity";
|
||||||
const searchFields = [["name"]];
|
const searchFields = [["name"]];
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Group>();
|
const columnHelper = createColumnHelper<WithEntity<Group>>();
|
||||||
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
||||||
|
|
||||||
const LinkedCorporate = ({userId, users, groups}: {userId: string; users: User[]; groups: Group[]}) => {
|
|
||||||
const [companyName, setCompanyName] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const user = users.find((u) => u.id === userId);
|
|
||||||
if (!user) return setCompanyName("");
|
|
||||||
|
|
||||||
if (isCorporateUser(user)) return setCompanyName(user.corporateInformation?.companyInformation?.name || user.name);
|
|
||||||
if (isAgentUser(user)) return setCompanyName(user.agentInformation?.companyName || user.name);
|
|
||||||
|
|
||||||
const belongingGroups = groups.filter((x) => x.participants.includes(userId));
|
|
||||||
const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x));
|
|
||||||
|
|
||||||
if (belongingGroupsAdmins.length === 0) return setCompanyName("");
|
|
||||||
|
|
||||||
const admin = belongingGroupsAdmins[0] as CorporateUser;
|
|
||||||
setCompanyName(admin.corporateInformation?.companyInformation.name || admin.name);
|
|
||||||
}, [userId, users, groups]);
|
|
||||||
|
|
||||||
return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CreateDialogProps {
|
interface CreateDialogProps {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
@@ -198,8 +179,6 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterTypes = ["corporate", "teacher", "mastercorporate"];
|
|
||||||
|
|
||||||
export default function GroupList({ user }: { user: User }) {
|
export default function GroupList({ user }: { user: User }) {
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [editingGroup, setEditingGroup] = useState<Group>();
|
const [editingGroup, setEditingGroup] = useState<Group>();
|
||||||
@@ -207,19 +186,8 @@ export default function GroupList({user}: {user: User}) {
|
|||||||
|
|
||||||
const { permissions } = usePermissions(user?.id || "");
|
const { permissions } = usePermissions(user?.id || "");
|
||||||
|
|
||||||
const {users} = useUsers();
|
const { users } = useEntitiesUsers();
|
||||||
const {groups, reload} = useGroups({
|
const { groups, reload } = useEntitiesGroups();
|
||||||
admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
|
|
||||||
userType: user?.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {groups: corporateGroups} = useGroups({
|
|
||||||
admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
|
|
||||||
userType: user?.type,
|
|
||||||
adminAdmins: user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {rows: filteredRows, renderSearch} = useListSearch<Group>(searchFields, groups);
|
|
||||||
|
|
||||||
const deleteGroup = (group: Group) => {
|
const deleteGroup = (group: Group) => {
|
||||||
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
|
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
|
||||||
@@ -248,9 +216,9 @@ export default function GroupList({user}: {user: User}) {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("admin", {
|
columnHelper.accessor("entity.label", {
|
||||||
header: "Linked Corporate",
|
header: "Entity",
|
||||||
cell: (info) => <LinkedCorporate userId={info.getValue()} users={users} groups={groups} />,
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("participants", {
|
columnHelper.accessor("participants", {
|
||||||
header: "Participants",
|
header: "Participants",
|
||||||
@@ -304,12 +272,6 @@ export default function GroupList({user}: {user: User}) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: filteredRows,
|
|
||||||
columns: defaultColumns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setEditingGroup(undefined);
|
setEditingGroup(undefined);
|
||||||
@@ -323,45 +285,10 @@ export default function GroupList({user}: {user: User}) {
|
|||||||
group={editingGroup}
|
group={editingGroup}
|
||||||
user={user}
|
user={user}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
users={
|
users={users}
|
||||||
checkAccess(user, ["corporate", "teacher", "mastercorporate"])
|
|
||||||
? users.filter(
|
|
||||||
(u) =>
|
|
||||||
groups
|
|
||||||
.filter((g) => g.admin === user.id)
|
|
||||||
.flatMap((g) => g.participants)
|
|
||||||
.includes(u.id) ||
|
|
||||||
(user?.type === "teacher" ? corporateGroups : groups).flatMap((g) => g.participants).includes(u.id),
|
|
||||||
)
|
|
||||||
: users
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
{renderSearch()}
|
<Table data={groups} columns={defaultColumns} searchFields={searchFields} />
|
||||||
<table className="bg-mti-purple-ultralight/40 w-full rounded-xl">
|
|
||||||
<thead>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<tr key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map((header) => (
|
|
||||||
<th className="py-4" key={header.id}>
|
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
<tbody className="px-2">
|
|
||||||
{table.getRowModel().rows.map((row) => (
|
|
||||||
<tr className="even:bg-mti-purple-ultralight/40 rounded-lg py-2 odd:bg-white" key={row.id}>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<td className="px-4 py-2" key={cell.id}>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{checkAccess(user, ["teacher", "corporate", "mastercorporate", "admin", "developer"], permissions, "createGroup") && (
|
{checkAccess(user, ["teacher", "corporate", "mastercorporate", "admin", "developer"], permissions, "createGroup") && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,52 +1,32 @@
|
|||||||
import Button from "@/components/Low/Button";
|
|
||||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
import { PERMISSIONS } from "@/constants/userPermissions";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import { Type, User } from "@/interfaces/user";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
import {Type, User, userTypes, CorporateUser, Group} from "@/interfaces/user";
|
|
||||||
import {Popover, Transition} from "@headlessui/react";
|
|
||||||
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 } from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {Fragment, useEffect, useState, useMemo} from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {BsArrowDown, BsArrowDownUp, BsArrowUp, BsCheck, BsCheckCircle, BsEye, BsFillExclamationOctagonFill, BsPerson, BsTrash} from "react-icons/bs";
|
import { BsCheck, BsCheckCircle, BsFillExclamationOctagonFill, 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 { 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 { mapBy } from "@/utils";
|
||||||
import {useListSearch} from "@/hooks/useListSearch";
|
import { exportListToExcel } from "@/utils/users";
|
||||||
import {getUserCorporate} from "@/utils/groups";
|
|
||||||
import {asyncSorter} from "@/utils";
|
|
||||||
import {exportListToExcel, UserListRow} from "@/utils/users";
|
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { PermissionType } from "@/interfaces/permissions";
|
import { PermissionType } from "@/interfaces/permissions";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import useUserBalance from "@/hooks/useUserBalance";
|
import useUserBalance from "@/hooks/useUserBalance";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import useEntitiesUsers from "@/hooks/useEntitiesUsers";
|
||||||
const columnHelper = createColumnHelper<User>();
|
import { WithLabeledEntities } from "@/interfaces/entity";
|
||||||
const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]];
|
import Table from "@/components/High/Table";
|
||||||
|
|
||||||
const corporatesHash = {
|
const columnHelper = createColumnHelper<WithLabeledEntities<User>>();
|
||||||
type: "corporate",
|
const searchFields = [["name"], ["email"], ["entities", ""]];
|
||||||
};
|
|
||||||
|
|
||||||
const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; groups: Group[]}) => {
|
|
||||||
const [companyName, setCompanyName] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const name = getUserCompanyName(user, users, groups);
|
|
||||||
setCompanyName(name);
|
|
||||||
}, [user, users, groups]);
|
|
||||||
|
|
||||||
return isLoading ? <span className="animate-pulse">Loading...</span> : <>{companyName}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function UserList({
|
export default function UserList({
|
||||||
user,
|
user,
|
||||||
@@ -60,28 +40,12 @@ export default function UserList({
|
|||||||
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 [displayUsers, setDisplayUsers] = useState<User[]>([]);
|
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
|
|
||||||
const userHash = useMemo(
|
const { users, reload } = useEntitiesUsers(type)
|
||||||
() => ({
|
|
||||||
type,
|
|
||||||
}),
|
|
||||||
[type],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {users, total, isLoading, reload} = useUsers(userHash);
|
|
||||||
const {users: corporates} = useUsers(corporatesHash);
|
|
||||||
|
|
||||||
const totalUsers = useMemo(() => [...users, ...corporates], [users, corporates]);
|
|
||||||
|
|
||||||
const { permissions } = usePermissions(user?.id || "");
|
const { permissions } = usePermissions(user?.id || "");
|
||||||
const { balance } = useUserBalance();
|
const { balance } = useUserBalance();
|
||||||
const {groups} = useGroups({
|
|
||||||
admin: user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined,
|
|
||||||
userType: user?.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -96,17 +60,7 @@ export default function UserList({
|
|||||||
if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light";
|
if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light";
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const displayUsers = useMemo(() => filters.length > 0 ? filters.reduce((d, f) => d.filter(f), users) : users, [filters, users])
|
||||||
(async () => {
|
|
||||||
if (users && users.length > 0) {
|
|
||||||
const filteredUsers = filters.reduce((d, f) => d.filter(f), users);
|
|
||||||
// const sortedUsers = await asyncSorter<User>(filteredUsers, sortFunction);
|
|
||||||
|
|
||||||
setDisplayUsers([...filteredUsers]);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [users, sorter]);
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -115,7 +69,7 @@ export default function UserList({
|
|||||||
.delete<{ ok: boolean }>(`/api/user?id=${user.id}`)
|
.delete<{ ok: boolean }>(`/api/user?id=${user.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("User deleted successfully!");
|
toast.success("User deleted successfully!");
|
||||||
reload();
|
reload()
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Something went wrong!", { toastId: "delete-error" });
|
toast.error("Something went wrong!", { toastId: "delete-error" });
|
||||||
@@ -141,8 +95,7 @@ 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.`,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -162,13 +115,6 @@ export default function UserList({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const SorterArrow = ({name}: {name: string}) => {
|
|
||||||
if (sorter === name) return <BsArrowUp />;
|
|
||||||
if (sorter === reverseString(name)) return <BsArrowDown />;
|
|
||||||
|
|
||||||
return <BsArrowDownUp />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionColumn = ({ row }: { row: { original: User } }) => {
|
const actionColumn = ({ row }: { row: { original: User } }) => {
|
||||||
const updateUserPermission = PERMISSIONS.updateUser[row.original.type] as {
|
const updateUserPermission = PERMISSIONS.updateUser[row.original.type] as {
|
||||||
list: Type[];
|
list: Type[];
|
||||||
@@ -208,12 +154,7 @@ export default function UserList({
|
|||||||
|
|
||||||
const demographicColumns = [
|
const demographicColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: (
|
header: "Name",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
|
|
||||||
<span>Name</span>
|
|
||||||
<SorterArrow name="name" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -228,26 +169,15 @@ export default function UserList({
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.country", {
|
columnHelper.accessor("demographicInformation.country", {
|
||||||
header: (
|
header: "Country",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "country"))}>
|
|
||||||
<span>Country</span>
|
|
||||||
<SorterArrow name="country" />
|
|
||||||
</button>
|
|
||||||
) 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})`
|
||||||
: "N/A",
|
: "N/A",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.phone", {
|
columnHelper.accessor("demographicInformation.phone", {
|
||||||
header: (
|
header: "Phone",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "phone"))}>
|
|
||||||
<span>Phone</span>
|
|
||||||
<SorterArrow name="phone" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => info.getValue() || "N/A",
|
cell: (info) => info.getValue() || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
@@ -256,32 +186,17 @@ export default function UserList({
|
|||||||
x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment,
|
x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment,
|
||||||
{
|
{
|
||||||
id: "employment",
|
id: "employment",
|
||||||
header: (
|
header: "Employment",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "employment"))}>
|
|
||||||
<span>Employment</span>
|
|
||||||
<SorterArrow name="employment" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
|
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
columnHelper.accessor("lastLogin", {
|
columnHelper.accessor("lastLogin", {
|
||||||
header: (
|
header: "Last Login",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "lastLogin"))}>
|
|
||||||
<span>Last Login</span>
|
|
||||||
<SorterArrow name="lastLogin" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
|
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.gender", {
|
columnHelper.accessor("demographicInformation.gender", {
|
||||||
header: (
|
header: "Gender",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}>
|
|
||||||
<span>Gender</span>
|
|
||||||
<SorterArrow name="gender" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => capitalize(info.getValue()) || "N/A",
|
cell: (info) => capitalize(info.getValue()) || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
@@ -293,17 +208,13 @@ export default function UserList({
|
|||||||
),
|
),
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: actionColumn,
|
cell: actionColumn,
|
||||||
|
sortable: false
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: (
|
header: "Name",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "name"))}>
|
|
||||||
<span>Name</span>
|
|
||||||
<SorterArrow name="name" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -318,12 +229,7 @@ export default function UserList({
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
columnHelper.accessor("email", {
|
||||||
header: (
|
header: "E-mail",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "email"))}>
|
|
||||||
<span>E-mail</span>
|
|
||||||
<SorterArrow name="email" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: ({ row, getValue }) => (
|
cell: ({ row, getValue }) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -336,39 +242,19 @@ export default function UserList({
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("type", {
|
columnHelper.accessor("type", {
|
||||||
header: (
|
header: "Type",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "type"))}>
|
|
||||||
<span>Type</span>
|
|
||||||
<SorterArrow name="type" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => USER_TYPE_LABELS[info.getValue()],
|
cell: (info) => USER_TYPE_LABELS[info.getValue()],
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("studentID", {
|
columnHelper.accessor("studentID", {
|
||||||
header: (
|
header: "Student ID",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "studentID"))}>
|
|
||||||
<span>Student ID</span>
|
|
||||||
<SorterArrow name="studentID" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => info.getValue() || "N/A",
|
cell: (info) => info.getValue() || "N/A",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("corporateInformation.companyInformation.name", {
|
columnHelper.accessor("entities", {
|
||||||
header: (
|
header: "Entities",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}>
|
cell: ({ getValue }) => mapBy(getValue(), 'label').join(', '),
|
||||||
<span>Company</span>
|
|
||||||
<SorterArrow name="companyName" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => <CompanyNameCell user={info.row.original} users={totalUsers} groups={groups} />,
|
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("subscriptionExpirationDate", {
|
columnHelper.accessor("subscriptionExpirationDate", {
|
||||||
header: (
|
header: "Expiration",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
|
|
||||||
<span>Expiration</span>
|
|
||||||
<SorterArrow name="expiryDate" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
||||||
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
||||||
@@ -376,12 +262,7 @@ export default function UserList({
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("isVerified", {
|
columnHelper.accessor("isVerified", {
|
||||||
header: (
|
header: "Verified",
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "verification"))}>
|
|
||||||
<span>Verified</span>
|
|
||||||
<SorterArrow name="verification" />
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
|
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
|
||||||
<div
|
<div
|
||||||
@@ -403,128 +284,12 @@ export default function UserList({
|
|||||||
),
|
),
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: actionColumn,
|
cell: actionColumn,
|
||||||
|
sortable: false
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const reverseString = (str: string) => reverse(str.split("")).join("");
|
const downloadExcel = (rows: WithLabeledEntities<User>[]) => {
|
||||||
|
const csv = exportListToExcel(rows);
|
||||||
const selectSorter = (previous: string | undefined, name: string) => {
|
|
||||||
if (!previous) return name;
|
|
||||||
if (previous === name) return reverseString(name);
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortFunction = async (a: User, b: User) => {
|
|
||||||
if (sorter === "name" || sorter === reverseString("name"))
|
|
||||||
return sorter === "name" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
|
||||||
|
|
||||||
if (sorter === "email" || sorter === reverseString("email"))
|
|
||||||
return sorter === "email" ? a.email.localeCompare(b.email) : b.email.localeCompare(a.email);
|
|
||||||
|
|
||||||
if (sorter === "type" || sorter === reverseString("type"))
|
|
||||||
return sorter === "type"
|
|
||||||
? 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 === "studentID" || sorter === reverseString("studentID"))
|
|
||||||
return sorter === "studentID"
|
|
||||||
? (a.type === "student" ? a.studentID || "N/A" : "N/A").localeCompare(b.type === "student" ? b.studentID || "N/A" : "N/A")
|
|
||||||
: (b.type === "student" ? b.studentID || "N/A" : "N/A").localeCompare(a.type === "student" ? a.studentID || "N/A" : "N/A");
|
|
||||||
|
|
||||||
if (sorter === "verification" || sorter === reverseString("verification"))
|
|
||||||
return sorter === "verification"
|
|
||||||
? a.isVerified.toString().localeCompare(b.isVerified.toString())
|
|
||||||
: b.isVerified.toString().localeCompare(a.isVerified.toString());
|
|
||||||
|
|
||||||
if (sorter === "expiryDate" || sorter === reverseString("expiryDate")) {
|
|
||||||
if (!a.subscriptionExpirationDate && b.subscriptionExpirationDate) return sorter === "expiryDate" ? -1 : 1;
|
|
||||||
if (a.subscriptionExpirationDate && !b.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sorter === "lastLogin" || sorter === reverseString("lastLogin")) {
|
|
||||||
if (!a.lastLogin && b.lastLogin) return sorter === "lastLogin" ? -1 : 1;
|
|
||||||
if (a.lastLogin && !b.lastLogin) return sorter === "lastLogin" ? 1 : -1;
|
|
||||||
if (!a.lastLogin && !b.lastLogin) return 0;
|
|
||||||
if (moment(a.lastLogin).isAfter(b.lastLogin)) return sorter === "lastLogin" ? -1 : 1;
|
|
||||||
if (moment(b.lastLogin).isAfter(a.lastLogin)) return sorter === "lastLogin" ? 1 : -1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sorter === "country" || sorter === reverseString("country")) {
|
|
||||||
if (!a.demographicInformation?.country && 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"
|
|
||||||
? a.demographicInformation!.country.localeCompare(b.demographicInformation!.country)
|
|
||||||
: b.demographicInformation!.country.localeCompare(a.demographicInformation!.country);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) return sorter === "phone" ? 1 : -1;
|
|
||||||
if (!a.demographicInformation?.phone && !b.demographicInformation?.phone) return 0;
|
|
||||||
|
|
||||||
return sorter === "phone"
|
|
||||||
? a.demographicInformation!.phone.localeCompare(b.demographicInformation!.phone)
|
|
||||||
: b.demographicInformation!.phone.localeCompare(a.demographicInformation!.phone);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sorter === "employment" || sorter === reverseString("employment")) {
|
|
||||||
const aSortingItem =
|
|
||||||
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) return sorter === "employment" ? 1 : -1;
|
|
||||||
if (!aSortingItem && !bSortingItem) return 0;
|
|
||||||
|
|
||||||
return sorter === "employment" ? aSortingItem!.localeCompare(bSortingItem!) : bSortingItem!.localeCompare(aSortingItem!);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) return sorter === "employment" ? 1 : -1;
|
|
||||||
if (!a.demographicInformation?.gender && !b.demographicInformation?.gender) return 0;
|
|
||||||
|
|
||||||
return sorter === "gender"
|
|
||||||
? a.demographicInformation!.gender.localeCompare(b.demographicInformation!.gender)
|
|
||||||
: b.demographicInformation!.gender.localeCompare(a.demographicInformation!.gender);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sorter === "companyName" || sorter === reverseString("companyName")) {
|
|
||||||
const aCorporateName = getUserCompanyName(a, users, groups);
|
|
||||||
const bCorporateName = getUserCompanyName(b, users, groups);
|
|
||||||
if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1;
|
|
||||||
if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1;
|
|
||||||
if (!aCorporateName && !bCorporateName) return 0;
|
|
||||||
|
|
||||||
return sorter === "companyName" ? aCorporateName.localeCompare(bCorporateName) : bCorporateName.localeCompare(aCorporateName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.id.localeCompare(b.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {rows: filteredRows, renderSearch, text: searchText} = useListSearch<User>(searchFields, displayUsers);
|
|
||||||
const {items, setPage, render: renderPagination} = usePagination<User>(filteredRows, 16);
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
useEffect(() => setPage(0), [searchText]);
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: items,
|
|
||||||
columns: (!showDemographicInformation ? defaultColumns : demographicColumns) as any,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const downloadExcel = () => {
|
|
||||||
const csv = exportListToExcel(filteredRows, users, groups);
|
|
||||||
|
|
||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
const file = new Blob([csv], { type: "text/csv" });
|
const file = new Blob([csv], { type: "text/csv" });
|
||||||
@@ -537,16 +302,10 @@ export default function UserList({
|
|||||||
|
|
||||||
const viewStudentFilter = (x: User) => x.type === "student";
|
const viewStudentFilter = (x: User) => x.type === "student";
|
||||||
const viewTeacherFilter = (x: User) => x.type === "teacher";
|
const viewTeacherFilter = (x: User) => x.type === "teacher";
|
||||||
const belongsToAdminFilter = (x: User) => {
|
const belongsToAdminFilter = (x: User) => x.entities.some(({ id }) => mapBy(selectedUser?.entities || [], 'id').includes(id));
|
||||||
if (!selectedUser) return false;
|
|
||||||
return groups
|
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
|
||||||
.flatMap((g) => g.participants)
|
|
||||||
.includes(x.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const viewStudentFilterBelongsToAdmin = (x: User) => x.type === "student" && belongsToAdminFilter(x);
|
const viewStudentFilterBelongsToAdmin = (x: User) => viewStudentFilter(x) && belongsToAdminFilter(x);
|
||||||
const viewTeacherFilterBelongsToAdmin = (x: User) => x.type === "teacher" && belongsToAdminFilter(x);
|
const viewTeacherFilterBelongsToAdmin = (x: User) => viewTeacherFilter(x) && belongsToAdminFilter(x);
|
||||||
|
|
||||||
const renderUserCard = (selectedUser: User) => {
|
const renderUserCard = (selectedUser: User) => {
|
||||||
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
|
const studentsFromAdmin = users.filter(viewStudentFilterBelongsToAdmin);
|
||||||
@@ -599,11 +358,7 @@ export default function UserList({
|
|||||||
});
|
});
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: belongsToAdminFilter
|
||||||
groups
|
|
||||||
.filter((g) => g.participants.includes(selectedUser.id))
|
|
||||||
.flatMap((g) => [g.admin, ...g.participants])
|
|
||||||
.includes(x.id),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push("/list/users");
|
router.push("/list/users");
|
||||||
@@ -622,44 +377,17 @@ export default function UserList({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderHeader && renderHeader(total)}
|
{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">
|
<Table<WithLabeledEntities<User>>
|
||||||
<div className="w-full flex gap-2 items-end">
|
data={displayUsers}
|
||||||
{renderSearch()}
|
columns={(!showDemographicInformation ? defaultColumns : demographicColumns) as any}
|
||||||
<Button className="w-full max-w-[200px] mb-1" variant="outline" onClick={downloadExcel}>
|
searchFields={searchFields}
|
||||||
Download List
|
onDownload={downloadExcel}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
|
||||||
{renderPagination()}
|
|
||||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
|
||||||
<thead>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<tr key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map((header) => (
|
|
||||||
<th className="py-4 px-4 text-left" key={header.id}>
|
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
<tbody className="px-2 w-full">
|
|
||||||
{table.getRowModel().rows.map((row) => (
|
|
||||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import useGroups from "@/hooks/useGroups";
|
|||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { getUserName } from "@/utils/users";
|
import { getUserName } from "@/utils/users";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import useEntitiesGroups from "@/hooks/useEntitiesGroups";
|
||||||
|
|
||||||
const USER_TYPE_PERMISSIONS: {
|
const USER_TYPE_PERMISSIONS: {
|
||||||
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
|
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
|
||||||
@@ -57,11 +59,12 @@ const USER_TYPE_PERMISSIONS: {
|
|||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
|
entities: EntityWithRoles[]
|
||||||
permissions: PermissionType[];
|
permissions: PermissionType[];
|
||||||
onFinish: () => void;
|
onFinish: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserCreator({user, users, permissions, onFinish}: Props) {
|
export default function UserCreator({ user, users, entities = [], permissions, onFinish }: Props) {
|
||||||
const [name, setName] = useState<string>();
|
const [name, setName] = useState<string>();
|
||||||
const [email, setEmail] = useState<string>();
|
const [email, setEmail] = useState<string>();
|
||||||
const [phone, setPhone] = useState<string>();
|
const [phone, setPhone] = useState<string>();
|
||||||
@@ -69,8 +72,6 @@ export default function UserCreator({user, users, permissions, onFinish}: Props)
|
|||||||
const [studentID, setStudentID] = useState<string>();
|
const [studentID, setStudentID] = useState<string>();
|
||||||
const [country, setCountry] = useState(user?.demographicInformation?.country);
|
const [country, setCountry] = useState(user?.demographicInformation?.country);
|
||||||
const [group, setGroup] = useState<string | null>();
|
const [group, setGroup] = useState<string | null>();
|
||||||
const [availableCorporates, setAvailableCorporates] = useState<User[]>([]);
|
|
||||||
const [selectedCorporate, setSelectedCorporate] = useState<string | null>();
|
|
||||||
const [password, setPassword] = useState<string>();
|
const [password, setPassword] = useState<string>();
|
||||||
const [confirmPassword, setConfirmPassword] = useState<string>();
|
const [confirmPassword, setConfirmPassword] = useState<string>();
|
||||||
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
||||||
@@ -80,22 +81,14 @@ export default function UserCreator({user, users, permissions, onFinish}: Props)
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [type, setType] = useState<Type>("student");
|
const [type, setType] = useState<Type>("student");
|
||||||
const [position, setPosition] = useState<string>();
|
const [position, setPosition] = useState<string>();
|
||||||
|
const [entity, setEntity] = useState((entities || [])[0]?.id || undefined)
|
||||||
|
|
||||||
const {groups} = useGroups({admin: ["developer", "admin"].includes(user?.type) ? undefined : user?.id, userType: user?.type});
|
const { groups } = useEntitiesGroups();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isExpiryDateEnabled) setExpiryDate(null);
|
if (!isExpiryDateEnabled) setExpiryDate(null);
|
||||||
}, [isExpiryDateEnabled]);
|
}, [isExpiryDateEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAvailableCorporates(
|
|
||||||
uniqBy(
|
|
||||||
users.filter((u) => u.type === "corporate" && groups.flatMap((g) => g.participants).includes(u.id)),
|
|
||||||
"id",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, [users, groups]);
|
|
||||||
|
|
||||||
const createUser = () => {
|
const createUser = () => {
|
||||||
if (!name || name.trim().length === 0) return toast.error("Please enter a valid name!");
|
if (!name || name.trim().length === 0) return toast.error("Please enter a valid name!");
|
||||||
if (!email || email.trim().length === 0) return toast.error("Please enter a valid e-mail address!");
|
if (!email || email.trim().length === 0) return toast.error("Please enter a valid e-mail address!");
|
||||||
@@ -110,7 +103,7 @@ export default function UserCreator({user, users, permissions, onFinish}: Props)
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
groupID: group,
|
groupID: group,
|
||||||
corporate: selectedCorporate || user.id,
|
entity,
|
||||||
type,
|
type,
|
||||||
studentID: type === "student" ? studentID : undefined,
|
studentID: type === "student" ? studentID : undefined,
|
||||||
expiryDate,
|
expiryDate,
|
||||||
@@ -135,7 +128,7 @@ export default function UserCreator({user, users, permissions, onFinish}: Props)
|
|||||||
setStudentID("");
|
setStudentID("");
|
||||||
setCountry(user?.demographicInformation?.country);
|
setCountry(user?.demographicInformation?.country);
|
||||||
setGroup(null);
|
setGroup(null);
|
||||||
setSelectedCorporate(null);
|
setEntity((entities || [])[0]?.id || undefined)
|
||||||
setExpiryDate(user?.subscriptionExpirationDate ? moment(user?.subscriptionExpirationDate).toDate() : null);
|
setExpiryDate(user?.subscriptionExpirationDate ? moment(user?.subscriptionExpirationDate).toDate() : null);
|
||||||
setIsExpiryDateEnabled(true);
|
setIsExpiryDateEnabled(true);
|
||||||
setType("student");
|
setType("student");
|
||||||
@@ -188,38 +181,30 @@ export default function UserCreator({user, users, permissions, onFinish}: Props)
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{["student", "teacher"].includes(type) && !["corporate", "teacher"].includes(user?.type) && (
|
|
||||||
<div className={clsx("flex flex-col gap-4")}>
|
<div className={clsx("flex flex-col gap-4")}>
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
|
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
||||||
<Select
|
<Select
|
||||||
options={availableCorporates.map((u) => ({value: u.id, label: getUserName(u)}))}
|
defaultValue={{ value: (entities || [])[0]?.id, label: (entities || [])[0]?.label }}
|
||||||
isClearable
|
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
||||||
onChange={(e) => setSelectedCorporate(e?.value || undefined)}
|
onChange={(e) => setEntity(e?.value || undefined)}
|
||||||
|
isClearable={checkAccess(user, ["admin", "developer"])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{["corporate", "mastercorporate"].includes(type) && (
|
{["corporate", "mastercorporate"].includes(type) && (
|
||||||
<Input type="text" name="department" label="Department" onChange={setPosition} value={position} placeholder="Department" />
|
<Input type="text" name="department" label="Department" onChange={setPosition} value={position} placeholder="Department" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!(type === "corporate" && user.type === "corporate") && (
|
<div className={clsx("flex flex-col gap-4")}>
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"flex flex-col gap-4",
|
|
||||||
(!["student", "teacher"].includes(type) || ["corporate", "teacher"].includes(user?.type)) &&
|
|
||||||
!["corporate", "mastercorporate"].includes(type) &&
|
|
||||||
"col-span-2",
|
|
||||||
)}>
|
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Group</label>
|
<label className="font-normal text-base text-mti-gray-dim">Group</label>
|
||||||
<Select
|
<Select
|
||||||
options={groups
|
options={groups
|
||||||
.filter((x) => (!selectedCorporate ? true : x.admin === selectedCorporate))
|
.filter((x) => x.entity?.id === entity)
|
||||||
.map((g) => ({ value: g.id, label: g.name }))}
|
.map((g) => ({ value: g.id, label: g.name }))}
|
||||||
onChange={(e) => setGroup(e?.value || undefined)}
|
onChange={(e) => setGroup(e?.value || undefined)}
|
||||||
|
isClearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
passport_id?: string;
|
passport_id?: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
};
|
};
|
||||||
|
entity?: string
|
||||||
|
entities: { id: string, role: string }[]
|
||||||
passwordHash: string | undefined;
|
passwordHash: string | undefined;
|
||||||
passwordSalt: string | undefined;
|
passwordSalt: string | undefined;
|
||||||
}[];
|
}[];
|
||||||
@@ -45,6 +47,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const salt = crypto.randomBytes(16).toString('base64');
|
const salt = crypto.randomBytes(16).toString('base64');
|
||||||
const hash = await scrypt.hash(user.passport_id, salt);
|
const hash = await scrypt.hash(user.passport_id, salt);
|
||||||
|
|
||||||
|
currentUser.entities = [{ id: currentUser.entity!, role: "90ce8f08-08c8-41e4-9848-f1500ddc3930" }]
|
||||||
|
delete currentUser.entity
|
||||||
|
|
||||||
currentUser.email = currentUser.email.toLowerCase();
|
currentUser.email = currentUser.email.toLowerCase();
|
||||||
currentUser.passwordHash = hash;
|
currentUser.passwordHash = hash;
|
||||||
currentUser.passwordSalt = salt;
|
currentUser.passwordSalt = salt;
|
||||||
|
|||||||
32
src/pages/api/entities/groups.ts
Normal file
32
src/pages/api/entities/groups.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
|
import { Entity, WithEntities, WithEntity, WithLabeledEntities } from "@/interfaces/entity";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import { mapBy } from "@/utils";
|
||||||
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
|
import { Group, User } from "@/interfaces/user";
|
||||||
|
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||||
|
|
||||||
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "GET") return await get(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!req.session.user) {
|
||||||
|
res.status(401).json({ ok: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = req.session.user;
|
||||||
|
|
||||||
|
const groups: WithEntity<Group>[] = ["admin", "developer"].includes(user.type)
|
||||||
|
? await getGroups()
|
||||||
|
: await getGroupsByEntities(mapBy(user.entities || [], 'id'))
|
||||||
|
|
||||||
|
res.status(200).json(groups);
|
||||||
|
}
|
||||||
47
src/pages/api/entities/users.ts
Normal file
47
src/pages/api/entities/users.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
|
import { Entity, WithEntities, WithLabeledEntities } from "@/interfaces/entity";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import { mapBy } from "@/utils";
|
||||||
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
|
import { User } from "@/interfaces/user";
|
||||||
|
|
||||||
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "GET") return await get(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!req.session.user) {
|
||||||
|
res.status(401).json({ ok: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = req.session.user;
|
||||||
|
|
||||||
|
const { type } = req.query as { type: string }
|
||||||
|
const entities = await getEntitiesWithRoles(mapBy(user.entities || [], 'id'))
|
||||||
|
|
||||||
|
const filter = !type ? undefined : { type }
|
||||||
|
const users = ["admin", "developer"].includes(user.type)
|
||||||
|
? await getUsers(filter)
|
||||||
|
: await getEntitiesUsers(mapBy(entities, 'id') as string[], filter)
|
||||||
|
|
||||||
|
const usersWithEntities: WithLabeledEntities<User>[] = users.map((u) => {
|
||||||
|
return {
|
||||||
|
...u, entities: (u.entities || []).map((e) => {
|
||||||
|
const entity = entities.find((x) => x.id === e.id)
|
||||||
|
if (!entity) return e
|
||||||
|
|
||||||
|
const role = entity.roles.find((x) => x.id === e.role)
|
||||||
|
return { id: e.id, label: entity.label, role: e.role, roleLabel: role?.label }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json(usersWithEntities);
|
||||||
|
}
|
||||||
@@ -30,14 +30,6 @@ const db = client.db(process.env.MONGODB_DB);
|
|||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
const getUsersOfType = async (admin: string, type: Type) => {
|
|
||||||
const groups = await getUserGroups(admin);
|
|
||||||
const participants = groups.flatMap((x) => x.participants);
|
|
||||||
const users = await getSpecificUsers(participants);
|
|
||||||
|
|
||||||
return users.filter((x) => x?.type === type).map((x) => x?.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method === "POST") return post(req, res);
|
if (req.method === "POST") return post(req, res);
|
||||||
|
|
||||||
@@ -50,30 +42,29 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" });
|
return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const corporateCorporate = await getUserCorporate(maker.id);
|
const { email, passport_id, password, type, groupID, entity, expiryDate, corporate } = req.body as {
|
||||||
|
|
||||||
const {email, passport_id, password, type, groupID, expiryDate, corporate} = req.body as {
|
|
||||||
email: string;
|
email: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
passport_id: string;
|
passport_id: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
entity: string;
|
||||||
groupID?: string;
|
groupID?: string;
|
||||||
corporate?: string;
|
corporate?: string;
|
||||||
expiryDate: null | Date;
|
expiryDate: null | Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
// cleaning data
|
// cleaning data
|
||||||
delete req.body.passport_id;
|
delete req.body.passport_id;
|
||||||
delete req.body.groupID;
|
delete req.body.groupID;
|
||||||
delete req.body.expiryDate;
|
delete req.body.expiryDate;
|
||||||
delete req.body.password;
|
delete req.body.password;
|
||||||
delete req.body.corporate;
|
delete req.body.corporate;
|
||||||
|
delete req.body.entity
|
||||||
|
|
||||||
await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id)
|
await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id)
|
||||||
.then(async (userCredentials) => {
|
.then(async (userCredentials) => {
|
||||||
const userId = userCredentials.user.uid;
|
const userId = userCredentials.user.uid;
|
||||||
|
|
||||||
const profilePicture = !corporateCorporate ? "/defaultAvatar.png" : corporateCorporate.profilePicture;
|
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
...req.body,
|
...req.body,
|
||||||
bio: "",
|
bio: "",
|
||||||
@@ -82,11 +73,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
focus: "academic",
|
focus: "academic",
|
||||||
status: "active",
|
status: "active",
|
||||||
desiredLevels: DEFAULT_DESIRED_LEVELS,
|
desiredLevels: DEFAULT_DESIRED_LEVELS,
|
||||||
profilePicture,
|
profilePicture: "/defaultAvatar.png",
|
||||||
levels: DEFAULT_LEVELS,
|
levels: DEFAULT_LEVELS,
|
||||||
isFirstLogin: false,
|
isFirstLogin: false,
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
registrationDate: new Date(),
|
registrationDate: new Date(),
|
||||||
|
entities: [{ id: entity, role: "90ce8f08-08c8-41e4-9848-f1500ddc3930" }],
|
||||||
subscriptionExpirationDate: expiryDate || null,
|
subscriptionExpirationDate: expiryDate || null,
|
||||||
...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate"
|
...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate"
|
||||||
? {
|
? {
|
||||||
@@ -116,99 +108,6 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
...(!!passport_id ? { passport_id } : {}),
|
...(!!passport_id ? { passport_id } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (type === "corporate") {
|
|
||||||
const students = maker.type === "corporate" ? await getUsersOfType(maker.id, "student") : [];
|
|
||||||
const teachers = maker.type === "corporate" ? await getUsersOfType(maker.id, "teacher") : [];
|
|
||||||
|
|
||||||
const defaultTeachersGroup: Group = {
|
|
||||||
admin: userId,
|
|
||||||
id: v4(),
|
|
||||||
name: "Teachers",
|
|
||||||
participants: teachers,
|
|
||||||
disableEditing: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultStudentsGroup: Group = {
|
|
||||||
admin: userId,
|
|
||||||
id: v4(),
|
|
||||||
name: "Students",
|
|
||||||
participants: students,
|
|
||||||
disableEditing: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.collection("groups").insertMany([defaultStudentsGroup, defaultTeachersGroup]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!corporate) {
|
|
||||||
const corporateUser = await db.collection("users").findOne<CorporateUser>({email: corporate.trim().toLowerCase()});
|
|
||||||
|
|
||||||
if (!!corporateUser) {
|
|
||||||
await db.collection("codes").updateOne({code}, {$set: {creator: corporateUser.id}});
|
|
||||||
const typeGroup = await db
|
|
||||||
.collection("groups")
|
|
||||||
.findOne<Group>({creator: corporateUser.id, name: type === "student" ? "Students" : "Teachers"});
|
|
||||||
|
|
||||||
if (!!typeGroup) {
|
|
||||||
if (!typeGroup.participants.includes(userId)) {
|
|
||||||
await db.collection("groups").updateOne({id: typeGroup.id}, {$set: {participants: [...typeGroup.participants, userId]}});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const defaultGroup: Group = {
|
|
||||||
admin: corporateUser.id,
|
|
||||||
id: v4(),
|
|
||||||
name: type === "student" ? "Students" : "Teachers",
|
|
||||||
participants: [userId],
|
|
||||||
disableEditing: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.collection("groups").insertOne(defaultGroup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maker.type === "corporate") {
|
|
||||||
await db.collection("codes").updateOne({code}, {$set: {creator: maker.id}});
|
|
||||||
const typeGroup = await getUserNamedGroup(maker.id, type === "student" ? "Students" : "Teachers");
|
|
||||||
|
|
||||||
if (!!typeGroup) {
|
|
||||||
if (!typeGroup.participants.includes(userId)) {
|
|
||||||
await db.collection("groups").updateOne({id: typeGroup.id}, {$set: {participants: [...typeGroup.participants, userId]}});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const defaultGroup: Group = {
|
|
||||||
admin: maker.id,
|
|
||||||
id: v4(),
|
|
||||||
name: type === "student" ? "Students" : "Teachers",
|
|
||||||
participants: [userId],
|
|
||||||
disableEditing: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.collection("groups").insertOne(defaultGroup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!corporateCorporate && corporateCorporate.type === "mastercorporate" && type === "corporate") {
|
|
||||||
const corporateGroup = await getUserNamedGroup(corporateCorporate.id, "Corporate");
|
|
||||||
|
|
||||||
if (!!corporateGroup) {
|
|
||||||
if (!corporateGroup.participants.includes(userId)) {
|
|
||||||
await db
|
|
||||||
.collection("groups")
|
|
||||||
.updateOne({id: corporateGroup.id}, {$set: {participants: [...corporateGroup.participants, userId]}});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const defaultGroup: Group = {
|
|
||||||
admin: corporateCorporate.id,
|
|
||||||
id: v4(),
|
|
||||||
name: "Corporate",
|
|
||||||
participants: [userId],
|
|
||||||
disableEditing: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.collection("groups").insertOne(defaultGroup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!groupID) {
|
if (!!groupID) {
|
||||||
const group = await getGroup(groupID);
|
const group = await getGroup(groupID);
|
||||||
if (!!group) await db.collection("groups").updateOne({ id: group.id }, { $set: { participants: [...group.participants, userId] } });
|
if (!!group) await db.collection("groups").updateOne({ id: group.id }, { $set: { participants: [...group.participants, userId] } });
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {withIronSessionApiRoute} from "iron-session/next";
|
|||||||
import {NextApiRequest, NextApiResponse} from "next";
|
import {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {getPermissions, getPermissionDocs} from "@/utils/permissions.be";
|
import {getPermissions, getPermissionDocs} from "@/utils/permissions.be";
|
||||||
import client from "@/lib/mongodb";
|
import client from "@/lib/mongodb";
|
||||||
import {getGroupsForUser, getParticipantGroups} from "@/utils/groups.be";
|
import {getGroupsForUser, getParticipantGroups, removeParticipantFromGroup} from "@/utils/groups.be";
|
||||||
|
import { mapBy } from "@/utils";
|
||||||
|
|
||||||
const auth = getAuth(adminApp);
|
const auth = getAuth(adminApp);
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
@@ -41,20 +42,6 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.type === "corporate" && (targetUser.type === "student" || targetUser.type === "teacher")) {
|
|
||||||
const groups = await getGroupsForUser(user.id, targetUser.id);
|
|
||||||
await Promise.all([
|
|
||||||
...groups
|
|
||||||
.filter((x) => x.admin === user.id)
|
|
||||||
.map(
|
|
||||||
async (x) =>
|
|
||||||
await db.collection("groups").updateOne({id: x.id}, {$set: {participants: x.participants.filter((y: string) => y !== id)}}),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await auth.deleteUser(id);
|
await auth.deleteUser(id);
|
||||||
await db.collection("users").deleteOne({id: targetUser.id});
|
await db.collection("users").deleteOne({id: targetUser.id});
|
||||||
await db.collection("codes").deleteMany({userId: targetUser.id});
|
await db.collection("codes").deleteMany({userId: targetUser.id});
|
||||||
@@ -63,9 +50,8 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
const groups = await getParticipantGroups(targetUser.id);
|
const groups = await getParticipantGroups(targetUser.id);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groups.map(
|
groups
|
||||||
async (x) => await db.collection("groups").updateOne({id: x.id}, {$set: {participants: x.participants.filter((y: string) => y !== id)}}),
|
.map(async (g) => await removeParticipantFromGroup(g.id, targetUser.id)),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({ok: true});
|
res.json({ok: true});
|
||||||
|
|||||||
195
src/pages/dashboard/admin.tsx
Normal file
195
src/pages/dashboard/admin.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Layout from "@/components/High/Layout";
|
||||||
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
|
import IconCard from "@/dashboards/IconCard";
|
||||||
|
import { Module } from "@/interfaces";
|
||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import { Assignment } from "@/interfaces/results";
|
||||||
|
import { Group, Stat, User } from "@/interfaces/user";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import { dateSorter, filterBy, mapBy, serialize } from "@/utils";
|
||||||
|
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
|
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||||
|
import { checkAccess } from "@/utils/permissions";
|
||||||
|
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||||
|
import { groupByExam } from "@/utils/stats";
|
||||||
|
import { getStatsByUsers } from "@/utils/stats.be";
|
||||||
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
|
import { uniqBy } from "lodash";
|
||||||
|
import moment from "moment";
|
||||||
|
import Head from "next/head";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
BsBank,
|
||||||
|
BsClipboard2Data,
|
||||||
|
BsClock,
|
||||||
|
BsEnvelopePaper,
|
||||||
|
BsPaperclip,
|
||||||
|
BsPencilSquare,
|
||||||
|
BsPeople,
|
||||||
|
BsPeopleFill,
|
||||||
|
BsPersonFill,
|
||||||
|
BsPersonFillGear,
|
||||||
|
} from "react-icons/bs";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
users: User[];
|
||||||
|
entities: EntityWithRoles[];
|
||||||
|
assignments: Assignment[];
|
||||||
|
stats: Stat[];
|
||||||
|
groups: Group[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
|
const user = req.session.user as User | undefined;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/login",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkAccess(user, ["admin", "developer"]))
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/dashboard",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = await getUsers();
|
||||||
|
const entities = await getEntitiesWithRoles();
|
||||||
|
const assignments = await getAssignments();
|
||||||
|
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||||
|
const groups = await getGroups();
|
||||||
|
|
||||||
|
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
||||||
|
}, sessionOptions);
|
||||||
|
|
||||||
|
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
||||||
|
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||||
|
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
||||||
|
const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]);
|
||||||
|
const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
|
const formattedStats = studentStats
|
||||||
|
.map((s) => ({
|
||||||
|
focus: students.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);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>EnCoach</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<ToastContainer />
|
||||||
|
<Layout user={user}>
|
||||||
|
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||||
|
<IconCard
|
||||||
|
onClick={() => router.push("/list/users?type=student")}
|
||||||
|
Icon={BsPersonFill}
|
||||||
|
label="Students"
|
||||||
|
value={students.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
onClick={() => router.push("/list/users?type=teacher")}
|
||||||
|
Icon={BsPencilSquare}
|
||||||
|
label="Teachers"
|
||||||
|
value={teachers.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsBank}
|
||||||
|
onClick={() => router.push("/list/users?type=corporate")}
|
||||||
|
label="Corporates"
|
||||||
|
value={corporates.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsBank}
|
||||||
|
onClick={() => router.push("/list/users?type=mastercorporate")}
|
||||||
|
label="Master Corporates"
|
||||||
|
value={masterCorporates.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" />
|
||||||
|
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
||||||
|
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||||
|
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||||
|
<IconCard Icon={BsPersonFillGear} label="Student Performance" value={students.length} color="purple" />
|
||||||
|
<IconCard
|
||||||
|
Icon={BsEnvelopePaper}
|
||||||
|
onClick={() => router.push("/assignments")}
|
||||||
|
label="Assignments"
|
||||||
|
value={assignments.filter((a) => !a.archived).length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
|
<UserDisplayList
|
||||||
|
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
|
title="Latest Students"
|
||||||
|
/>
|
||||||
|
<UserDisplayList
|
||||||
|
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
|
title="Latest Teachers"
|
||||||
|
/>
|
||||||
|
<UserDisplayList
|
||||||
|
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||||
|
title="Highest level students"
|
||||||
|
/>
|
||||||
|
<UserDisplayList
|
||||||
|
users={
|
||||||
|
students
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title="Highest exam count students"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import IconCard from "@/dashboards/IconCard";
|
|
||||||
import {Module} from "@/interfaces";
|
|
||||||
import {EntityWithRoles} from "@/interfaces/entity";
|
|
||||||
import {Assignment} from "@/interfaces/results";
|
|
||||||
import {Group, Stat, User} from "@/interfaces/user";
|
|
||||||
import {sessionOptions} from "@/lib/session";
|
|
||||||
import {dateSorter, filterBy, mapBy, serialize} from "@/utils";
|
|
||||||
import {getAssignments, getEntitiesAssignments} from "@/utils/assignments.be";
|
|
||||||
import {getEntitiesWithRoles} from "@/utils/entities.be";
|
|
||||||
import {getGroups, getGroupsByEntities} from "@/utils/groups.be";
|
|
||||||
import {checkAccess} from "@/utils/permissions";
|
|
||||||
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
|
||||||
import {groupByExam} from "@/utils/stats";
|
|
||||||
import {getStatsByUsers} from "@/utils/stats.be";
|
|
||||||
import {getEntitiesUsers, getUsers} from "@/utils/users.be";
|
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
|
||||||
import {uniqBy} from "lodash";
|
|
||||||
import moment from "moment";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {useRouter} from "next/router";
|
|
||||||
import {useMemo} from "react";
|
|
||||||
import {
|
|
||||||
BsBank,
|
|
||||||
BsClipboard2Data,
|
|
||||||
BsClock,
|
|
||||||
BsEnvelopePaper,
|
|
||||||
BsPaperclip,
|
|
||||||
BsPencilSquare,
|
|
||||||
BsPeople,
|
|
||||||
BsPeopleFill,
|
|
||||||
BsPersonFill,
|
|
||||||
BsPersonFillGear,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import {ToastContainer} from "react-toastify";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
user: User;
|
|
||||||
users: User[];
|
|
||||||
entities: EntityWithRoles[];
|
|
||||||
assignments: Assignment[];
|
|
||||||
stats: Stat[];
|
|
||||||
groups: Group[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
|
||||||
const user = req.session.user as User | undefined;
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer"]))
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/dashboard",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const users = await getUsers();
|
|
||||||
const entities = await getEntitiesWithRoles();
|
|
||||||
const assignments = await getAssignments();
|
|
||||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
|
||||||
const groups = await getGroups();
|
|
||||||
|
|
||||||
return {props: serialize({user, users, entities, assignments, stats, groups})};
|
|
||||||
}, sessionOptions);
|
|
||||||
|
|
||||||
export default function Dashboard({user, users, entities, assignments, stats, groups}: Props) {
|
|
||||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
|
||||||
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
|
||||||
const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]);
|
|
||||||
const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
|
||||||
const formattedStats = studentStats
|
|
||||||
.map((s) => ({
|
|
||||||
focus: students.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 UserDisplay = (displayUser: User) => (
|
|
||||||
<div 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>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>EnCoach</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
|
||||||
/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<ToastContainer />
|
|
||||||
<Layout user={user}>
|
|
||||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
|
||||||
<IconCard
|
|
||||||
onClick={() => router.push("/lists/users?type=student")}
|
|
||||||
Icon={BsPersonFill}
|
|
||||||
label="Students"
|
|
||||||
value={students.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
|
||||||
onClick={() => router.push("/lists/users?type=teacher")}
|
|
||||||
Icon={BsPencilSquare}
|
|
||||||
label="Teachers"
|
|
||||||
value={teachers.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
|
||||||
Icon={BsBank}
|
|
||||||
onClick={() => router.push("/lists/users?type=corporate")}
|
|
||||||
label="Corporates"
|
|
||||||
value={corporates.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
|
||||||
Icon={BsBank}
|
|
||||||
onClick={() => router.push("/lists/users?type=mastercorporate")}
|
|
||||||
label="Master Corporates"
|
|
||||||
value={masterCorporates.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" />
|
|
||||||
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
|
||||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
|
||||||
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
|
||||||
<IconCard Icon={BsPersonFillGear} label="Student Performance" value={students.length} color="purple" />
|
|
||||||
<IconCard
|
|
||||||
Icon={BsEnvelopePaper}
|
|
||||||
onClick={() => router.push("/assignments")}
|
|
||||||
label="Assignments"
|
|
||||||
value={assignments.filter((a) => !a.archived).length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{teachers
|
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
|
||||||
)
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Layout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/dashboards/IconCard";
|
import IconCard from "@/dashboards/IconCard";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
@@ -136,14 +137,14 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
|||||||
)}
|
)}
|
||||||
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/lists/users?type=student")}
|
onClick={() => router.push("/list/users?type=student")}
|
||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
label="Students"
|
label="Students"
|
||||||
value={students.length}
|
value={students.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/lists/users?type=teacher")}
|
onClick={() => router.push("/list/users?type=teacher")}
|
||||||
Icon={BsPencilSquare}
|
Icon={BsPencilSquare}
|
||||||
label="Teachers"
|
label="Teachers"
|
||||||
value={teachers.length}
|
value={teachers.length}
|
||||||
@@ -178,50 +179,29 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
<UserDisplayList
|
||||||
<span className="p-4">Latest students</span>
|
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
title="Latest Students"
|
||||||
{students
|
/>
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
<UserDisplayList
|
||||||
.map((x) => (
|
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
<UserDisplay key={x.id} {...x} />
|
title="Latest Teachers"
|
||||||
))}
|
/>
|
||||||
</div>
|
<UserDisplayList
|
||||||
</div>
|
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
title="Highest level students"
|
||||||
<span className="p-4">Latest teachers</span>
|
/>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<UserDisplayList
|
||||||
{teachers
|
users={
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
students
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||||
)
|
)
|
||||||
.map((x) => (
|
}
|
||||||
<UserDisplay key={x.id} {...x} />
|
title="Highest exam count students"
|
||||||
))}
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
201
src/pages/dashboard/developer.tsx
Normal file
201
src/pages/dashboard/developer.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Layout from "@/components/High/Layout";
|
||||||
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
|
import IconCard from "@/dashboards/IconCard";
|
||||||
|
import { Module } from "@/interfaces";
|
||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import { Assignment } from "@/interfaces/results";
|
||||||
|
import { Group, Stat, User } from "@/interfaces/user";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import { dateSorter, filterBy, mapBy, serialize } from "@/utils";
|
||||||
|
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
|
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||||
|
import { checkAccess } from "@/utils/permissions";
|
||||||
|
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||||
|
import { groupByExam } from "@/utils/stats";
|
||||||
|
import { getStatsByUsers } from "@/utils/stats.be";
|
||||||
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
|
import { uniqBy } from "lodash";
|
||||||
|
import moment from "moment";
|
||||||
|
import Head from "next/head";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
BsBank,
|
||||||
|
BsClipboard2Data,
|
||||||
|
BsClock,
|
||||||
|
BsEnvelopePaper,
|
||||||
|
BsPaperclip,
|
||||||
|
BsPencilSquare,
|
||||||
|
BsPeople,
|
||||||
|
BsPeopleFill,
|
||||||
|
BsPersonFill,
|
||||||
|
BsPersonFillGear,
|
||||||
|
} from "react-icons/bs";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
users: User[];
|
||||||
|
entities: EntityWithRoles[];
|
||||||
|
assignments: Assignment[];
|
||||||
|
stats: Stat[];
|
||||||
|
groups: Group[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
|
const user = req.session.user as User | undefined;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/login",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkAccess(user, ["admin", "developer"]))
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/dashboard",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = await getUsers();
|
||||||
|
const entities = await getEntitiesWithRoles();
|
||||||
|
const assignments = await getAssignments();
|
||||||
|
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||||
|
const groups = await getGroups();
|
||||||
|
|
||||||
|
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
||||||
|
}, sessionOptions);
|
||||||
|
|
||||||
|
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
||||||
|
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||||
|
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
||||||
|
const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]);
|
||||||
|
const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
|
const formattedStats = studentStats
|
||||||
|
.map((s) => ({
|
||||||
|
focus: students.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);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>EnCoach</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<ToastContainer />
|
||||||
|
<Layout user={user}>
|
||||||
|
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||||
|
<IconCard
|
||||||
|
onClick={() => router.push("/list/users?type=student")}
|
||||||
|
Icon={BsPersonFill}
|
||||||
|
label="Students"
|
||||||
|
value={students.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
onClick={() => router.push("/list/users?type=teacher")}
|
||||||
|
Icon={BsPencilSquare}
|
||||||
|
label="Teachers"
|
||||||
|
value={teachers.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsBank}
|
||||||
|
onClick={() => router.push("/list/users?type=corporate")}
|
||||||
|
label="Corporates"
|
||||||
|
value={corporates.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsBank}
|
||||||
|
onClick={() => router.push("/list/users?type=mastercorporate")}
|
||||||
|
label="Master Corporates"
|
||||||
|
value={masterCorporates.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsPeople}
|
||||||
|
onClick={() => router.push("/entities")}
|
||||||
|
label="Classrooms"
|
||||||
|
value={groups.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
||||||
|
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||||
|
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||||
|
<IconCard Icon={BsPersonFillGear} label="Student Performance" value={students.length} color="purple" />
|
||||||
|
<IconCard
|
||||||
|
Icon={BsEnvelopePaper}
|
||||||
|
onClick={() => router.push("/assignments")}
|
||||||
|
label="Assignments"
|
||||||
|
value={assignments.filter((a) => !a.archived).length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
|
<UserDisplayList
|
||||||
|
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
|
title="Latest Students"
|
||||||
|
/>
|
||||||
|
<UserDisplayList
|
||||||
|
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
|
title="Latest Teachers"
|
||||||
|
/>
|
||||||
|
<UserDisplayList
|
||||||
|
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||||
|
title="Highest level students"
|
||||||
|
/>
|
||||||
|
<UserDisplayList
|
||||||
|
users={
|
||||||
|
students
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title="Highest exam count students"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import IconCard from "@/dashboards/IconCard";
|
|
||||||
import {Module} from "@/interfaces";
|
|
||||||
import {EntityWithRoles} from "@/interfaces/entity";
|
|
||||||
import {Assignment} from "@/interfaces/results";
|
|
||||||
import {Group, Stat, User} from "@/interfaces/user";
|
|
||||||
import {sessionOptions} from "@/lib/session";
|
|
||||||
import {dateSorter, filterBy, mapBy, serialize} from "@/utils";
|
|
||||||
import {getAssignments, getEntitiesAssignments} from "@/utils/assignments.be";
|
|
||||||
import {getEntitiesWithRoles} from "@/utils/entities.be";
|
|
||||||
import {getGroups, getGroupsByEntities} from "@/utils/groups.be";
|
|
||||||
import {checkAccess} from "@/utils/permissions";
|
|
||||||
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
|
||||||
import {groupByExam} from "@/utils/stats";
|
|
||||||
import {getStatsByUsers} from "@/utils/stats.be";
|
|
||||||
import {getEntitiesUsers, getUsers} from "@/utils/users.be";
|
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
|
||||||
import {uniqBy} from "lodash";
|
|
||||||
import moment from "moment";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {useRouter} from "next/router";
|
|
||||||
import {useMemo} from "react";
|
|
||||||
import {
|
|
||||||
BsBank,
|
|
||||||
BsClipboard2Data,
|
|
||||||
BsClock,
|
|
||||||
BsEnvelopePaper,
|
|
||||||
BsPaperclip,
|
|
||||||
BsPencilSquare,
|
|
||||||
BsPeople,
|
|
||||||
BsPeopleFill,
|
|
||||||
BsPersonFill,
|
|
||||||
BsPersonFillGear,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import {ToastContainer} from "react-toastify";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
user: User;
|
|
||||||
users: User[];
|
|
||||||
entities: EntityWithRoles[];
|
|
||||||
assignments: Assignment[];
|
|
||||||
stats: Stat[];
|
|
||||||
groups: Group[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
|
||||||
const user = req.session.user as User | undefined;
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer"]))
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/dashboard",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const users = await getUsers();
|
|
||||||
const entities = await getEntitiesWithRoles();
|
|
||||||
const assignments = await getAssignments();
|
|
||||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
|
||||||
const groups = await getGroups();
|
|
||||||
|
|
||||||
return {props: serialize({user, users, entities, assignments, stats, groups})};
|
|
||||||
}, sessionOptions);
|
|
||||||
|
|
||||||
export default function Dashboard({user, users, entities, assignments, stats, groups}: Props) {
|
|
||||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
|
||||||
const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]);
|
|
||||||
const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]);
|
|
||||||
const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
|
||||||
const formattedStats = studentStats
|
|
||||||
.map((s) => ({
|
|
||||||
focus: students.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 UserDisplay = (displayUser: User) => (
|
|
||||||
<div 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>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>EnCoach</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
|
||||||
/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<ToastContainer />
|
|
||||||
<Layout user={user}>
|
|
||||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
|
||||||
<IconCard
|
|
||||||
onClick={() => router.push("/lists/users?type=student")}
|
|
||||||
Icon={BsPersonFill}
|
|
||||||
label="Students"
|
|
||||||
value={students.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
|
||||||
onClick={() => router.push("/lists/users?type=teacher")}
|
|
||||||
Icon={BsPencilSquare}
|
|
||||||
label="Teachers"
|
|
||||||
value={teachers.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
|
||||||
Icon={BsBank}
|
|
||||||
onClick={() => router.push("/lists/users?type=corporate")}
|
|
||||||
label="Corporates"
|
|
||||||
value={corporates.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard
|
|
||||||
Icon={BsBank}
|
|
||||||
onClick={() => router.push("/lists/users?type=mastercorporate")}
|
|
||||||
label="Master Corporates"
|
|
||||||
value={masterCorporates.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" />
|
|
||||||
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
|
||||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
|
||||||
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
|
||||||
<IconCard Icon={BsPersonFillGear} label="Student Performance" value={students.length} color="purple" />
|
|
||||||
<IconCard
|
|
||||||
Icon={BsEnvelopePaper}
|
|
||||||
onClick={() => router.push("/assignments")}
|
|
||||||
label="Assignments"
|
|
||||||
value={assignments.filter((a) => !a.archived).length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{teachers
|
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
|
||||||
)
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Layout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/dashboards/IconCard";
|
import IconCard from "@/dashboards/IconCard";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
@@ -133,21 +134,21 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
|||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/lists/users?type=student")}
|
onClick={() => router.push("/list/users?type=student")}
|
||||||
Icon={BsPersonFill}
|
Icon={BsPersonFill}
|
||||||
label="Students"
|
label="Students"
|
||||||
value={students.length}
|
value={students.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/lists/users?type=teacher")}
|
onClick={() => router.push("/list/users?type=teacher")}
|
||||||
Icon={BsPencilSquare}
|
Icon={BsPencilSquare}
|
||||||
label="Teachers"
|
label="Teachers"
|
||||||
value={teachers.length}
|
value={teachers.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/lists/users?type=corporate")} Icon={BsBank} label="Corporate Accounts" value={corporates.length} color="purple" />
|
onClick={() => router.push("/list/users?type=corporate")} Icon={BsBank} label="Corporate Accounts" value={corporates.length} color="purple" />
|
||||||
<IconCard onClick={() => router.push("/classrooms")} Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" />
|
<IconCard onClick={() => router.push("/classrooms")} Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" />
|
||||||
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
<IconCard Icon={BsPeopleFill} label="Entities" value={entities.length} color="purple" />
|
||||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||||
@@ -169,50 +170,29 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
<UserDisplayList
|
||||||
<span className="p-4">Latest students</span>
|
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
title="Latest Students"
|
||||||
{students
|
/>
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
<UserDisplayList
|
||||||
.map((x) => (
|
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
<UserDisplay key={x.id} {...x} />
|
title="Latest Teachers"
|
||||||
))}
|
/>
|
||||||
</div>
|
<UserDisplayList
|
||||||
</div>
|
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||||
<div className="bg-white border shadow flex flex-col rounded-xl w-full">
|
title="Highest level students"
|
||||||
<span className="p-4">Latest teachers</span>
|
/>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<UserDisplayList
|
||||||
{teachers
|
users={
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
students
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||||
)
|
)
|
||||||
.map((x) => (
|
}
|
||||||
<UserDisplay key={x.id} {...x} />
|
title="Highest exam count students"
|
||||||
))}
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
170
src/pages/dashboard/teacher.tsx
Normal file
170
src/pages/dashboard/teacher.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Layout from "@/components/High/Layout";
|
||||||
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
|
import IconCard from "@/dashboards/IconCard";
|
||||||
|
import { Module } from "@/interfaces";
|
||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import { Assignment } from "@/interfaces/results";
|
||||||
|
import { Group, Stat, User } from "@/interfaces/user";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import { dateSorter, filterBy, mapBy, serialize } from "@/utils";
|
||||||
|
import { getEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
|
import { getGroupsByEntities } from "@/utils/groups.be";
|
||||||
|
import { checkAccess } from "@/utils/permissions";
|
||||||
|
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||||
|
import { groupByExam } from "@/utils/stats";
|
||||||
|
import { getStatsByUsers } from "@/utils/stats.be";
|
||||||
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
|
import { uniqBy } from "lodash";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { BsClipboard2Data, BsEnvelopePaper, BsPaperclip, BsPeople, BsPersonFill } from "react-icons/bs";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
users: User[];
|
||||||
|
entities: EntityWithRoles[];
|
||||||
|
assignments: Assignment[];
|
||||||
|
stats: Stat[];
|
||||||
|
groups: Group[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
|
const user = req.session.user as User | undefined;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/login",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkAccess(user, ["admin", "developer", "teacher"]))
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/dashboard",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
|
|
||||||
|
const users = await getEntitiesUsers(entityIDS);
|
||||||
|
const entities = await getEntitiesWithRoles(entityIDS);
|
||||||
|
const assignments = await getEntitiesAssignments(entityIDS);
|
||||||
|
const stats = await getStatsByUsers(users.map((u) => u.id));
|
||||||
|
const groups = await getGroupsByEntities(entityIDS);
|
||||||
|
|
||||||
|
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
||||||
|
}, sessionOptions);
|
||||||
|
|
||||||
|
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
||||||
|
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
|
const formattedStats = studentStats
|
||||||
|
.map((s) => ({
|
||||||
|
focus: students.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 UserDisplay = (displayUser: User) => (
|
||||||
|
<div 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>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>EnCoach</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<ToastContainer />
|
||||||
|
<Layout user={user}>
|
||||||
|
<div className="w-full flex flex-col gap-4">
|
||||||
|
{entities.length > 0 && (
|
||||||
|
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
||||||
|
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
||||||
|
<IconCard Icon={BsPersonFill} label="Students" value={students.length} color="purple" />
|
||||||
|
<IconCard
|
||||||
|
onClick={() => router.push("/classrooms")}
|
||||||
|
Icon={BsPeople}
|
||||||
|
label="Classrooms"
|
||||||
|
value={groups.length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||||
|
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
||||||
|
<IconCard
|
||||||
|
Icon={BsEnvelopePaper}
|
||||||
|
onClick={() => router.push("/assignments")}
|
||||||
|
label="Assignments"
|
||||||
|
value={assignments.filter((a) => !a.archived).length}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
|
<UserDisplayList
|
||||||
|
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||||
|
title="Latest Students"
|
||||||
|
/>
|
||||||
|
<UserDisplayList
|
||||||
|
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||||
|
title="Highest level students"
|
||||||
|
/>
|
||||||
|
<UserDisplayList
|
||||||
|
users={
|
||||||
|
students
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title="Highest exam count students"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import IconCard from "@/dashboards/IconCard";
|
|
||||||
import {Module} from "@/interfaces";
|
|
||||||
import {EntityWithRoles} from "@/interfaces/entity";
|
|
||||||
import {Assignment} from "@/interfaces/results";
|
|
||||||
import {Group, Stat, User} from "@/interfaces/user";
|
|
||||||
import {sessionOptions} from "@/lib/session";
|
|
||||||
import {dateSorter, filterBy, mapBy, serialize} from "@/utils";
|
|
||||||
import {getEntitiesAssignments} from "@/utils/assignments.be";
|
|
||||||
import {getEntitiesWithRoles} from "@/utils/entities.be";
|
|
||||||
import {getGroupsByEntities} from "@/utils/groups.be";
|
|
||||||
import {checkAccess} from "@/utils/permissions";
|
|
||||||
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
|
||||||
import {groupByExam} from "@/utils/stats";
|
|
||||||
import {getStatsByUsers} from "@/utils/stats.be";
|
|
||||||
import {getEntitiesUsers} from "@/utils/users.be";
|
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
|
||||||
import {uniqBy} from "lodash";
|
|
||||||
import Head from "next/head";
|
|
||||||
import {useRouter} from "next/router";
|
|
||||||
import {useMemo} from "react";
|
|
||||||
import {BsClipboard2Data, BsEnvelopePaper, BsPaperclip, BsPeople, BsPersonFill} from "react-icons/bs";
|
|
||||||
import {ToastContainer} from "react-toastify";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
user: User;
|
|
||||||
users: User[];
|
|
||||||
entities: EntityWithRoles[];
|
|
||||||
assignments: Assignment[];
|
|
||||||
stats: Stat[];
|
|
||||||
groups: Group[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
|
||||||
const user = req.session.user as User | undefined;
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer", "teacher"]))
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/dashboard",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
|
||||||
|
|
||||||
const users = await getEntitiesUsers(entityIDS);
|
|
||||||
const entities = await getEntitiesWithRoles(entityIDS);
|
|
||||||
const assignments = await getEntitiesAssignments(entityIDS);
|
|
||||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
|
||||||
const groups = await getGroupsByEntities(entityIDS);
|
|
||||||
|
|
||||||
return {props: serialize({user, users, entities, assignments, stats, groups})};
|
|
||||||
}, sessionOptions);
|
|
||||||
|
|
||||||
export default function Dashboard({user, users, entities, assignments, stats, groups}: Props) {
|
|
||||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
|
||||||
const formattedStats = studentStats
|
|
||||||
.map((s) => ({
|
|
||||||
focus: students.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 UserDisplay = (displayUser: User) => (
|
|
||||||
<div 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>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>EnCoach</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
|
||||||
/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<ToastContainer />
|
|
||||||
<Layout user={user}>
|
|
||||||
<div className="w-full flex flex-col gap-4">
|
|
||||||
{entities.length > 0 && (
|
|
||||||
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
|
||||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
|
||||||
<IconCard Icon={BsPersonFill} label="Students" value={students.length} color="purple" />
|
|
||||||
<IconCard
|
|
||||||
onClick={() => router.push("/classrooms")}
|
|
||||||
Icon={BsPeople}
|
|
||||||
label="Classrooms"
|
|
||||||
value={groups.length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
|
||||||
<IconCard Icon={BsPaperclip} label="Average Level" value={averageLevelCalculator(stats).toFixed(1)} color="purple" />
|
|
||||||
<IconCard
|
|
||||||
Icon={BsEnvelopePaper}
|
|
||||||
onClick={() => router.push("/assignments")}
|
|
||||||
label="Assignments"
|
|
||||||
value={assignments.filter((a) => !a.archived).length}
|
|
||||||
color="purple"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white border 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">
|
|
||||||
{students
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
|
||||||
)
|
|
||||||
.map((x) => (
|
|
||||||
<UserDisplay key={x.id} {...x} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Layout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,21 @@
|
|||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
|
import { Type, User } from "@/interfaces/user";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import useFilterStore from "@/stores/listFilterStore";
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
|
import { serialize } from "@/utils";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
import {BsArrowLeft} from "react-icons/bs";
|
import {BsArrowLeft, BsChevronLeft} from "react-icons/bs";
|
||||||
import {ToastContainer} from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
import UserList from "../(admin)/Lists/UserList";
|
import UserList from "../(admin)/Lists/UserList";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res, query}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
const envVariables: {[key: string]: string} = {};
|
|
||||||
Object.keys(process.env)
|
|
||||||
.filter((x) => x.startsWith("NEXT_PUBLIC"))
|
|
||||||
.forEach((x: string) => {
|
|
||||||
envVariables[x] = process.env[x]!;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -30,17 +25,22 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {type} = query as {type?: Type}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user: req.session.user, envVariables},
|
props: serialize({user: req.session.user, type}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function UsersListPage() {
|
interface Props {
|
||||||
const {user} = useUser();
|
user: User
|
||||||
|
type?: Type
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UsersListPage({ user, type }: Props) {
|
||||||
const [filters, clearFilters] = useFilterStore((state) => [state.userFilters, state.clearUserFilters]);
|
const [filters, clearFilters] = useFilterStore((state) => [state.userFilters, state.clearUserFilters]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -59,19 +59,19 @@ export default function UsersListPage() {
|
|||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<UserList
|
<UserList
|
||||||
user={user}
|
user={user}
|
||||||
|
type={type}
|
||||||
filters={filters.map((f) => f.filter)}
|
filters={filters.map((f) => f.filter)}
|
||||||
renderHeader={(total) => (
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
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="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
||||||
<BsArrowLeft className="text-xl" />
|
<BsChevronLeft />
|
||||||
<span>Back</span>
|
</button>
|
||||||
</div>
|
<h2 className="font-bold text-2xl">Users ({ total })</h2>
|
||||||
<h2 className="text-2xl font-semibold">Users ({total})</h2>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,259 +0,0 @@
|
|||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import List from "@/components/List";
|
|
||||||
import useUser from "@/hooks/useUser";
|
|
||||||
import useUsers from "@/hooks/useUsers";
|
|
||||||
import {Type, User} from "@/interfaces/user";
|
|
||||||
import {sessionOptions} from "@/lib/session";
|
|
||||||
import useFilterStore from "@/stores/listFilterStore";
|
|
||||||
import {mapBy, serialize} from "@/utils";
|
|
||||||
import {getEntitiesUsers, getEntityUsers} from "@/utils/users.be";
|
|
||||||
import {createColumnHelper} from "@tanstack/react-table";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
|
||||||
import Head from "next/head";
|
|
||||||
import {useRouter} from "next/router";
|
|
||||||
import {useEffect, useState} from "react";
|
|
||||||
import {BsArrowLeft, BsCheck, BsChevronLeft} from "react-icons/bs";
|
|
||||||
import {ToastContainer} from "react-toastify";
|
|
||||||
import UserList from "../(admin)/Lists/UserList";
|
|
||||||
import {countries, TCountries} from "countries-list";
|
|
||||||
import countryCodes from "country-codes-list";
|
|
||||||
import {capitalize} from "lodash";
|
|
||||||
import moment from "moment";
|
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
|
||||||
import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be";
|
|
||||||
import {EntityWithRoles} from "@/interfaces/entity";
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<User>();
|
|
||||||
|
|
||||||
const expirationDateColor = (date: Date) => {
|
|
||||||
const momentDate = moment(date);
|
|
||||||
const today = moment(new Date());
|
|
||||||
|
|
||||||
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(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-light";
|
|
||||||
if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light";
|
|
||||||
};
|
|
||||||
|
|
||||||
const SEARCH_FIELDS = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]];
|
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => {
|
|
||||||
const user = req.session.user as User | undefined;
|
|
||||||
|
|
||||||
if (!user)
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const {type} = query as {type?: Type};
|
|
||||||
const users = await getEntitiesUsers(mapBy(user.entities, "id"));
|
|
||||||
const entities = await getEntitiesWithRoles();
|
|
||||||
|
|
||||||
const filters: ((u: User) => boolean)[] = [];
|
|
||||||
if (type) filters.push((u) => u.type === type);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: serialize({user, users: filters.reduce((d, f) => d.filter(f), users), entities}),
|
|
||||||
};
|
|
||||||
}, sessionOptions);
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
user: User;
|
|
||||||
users: User[];
|
|
||||||
entities: EntityWithRoles[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UsersList({user, users, entities}: Props) {
|
|
||||||
const [showDemographicInformation, setShowDemographicInformation] = useState(false);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const defaultColumns = [
|
|
||||||
columnHelper.accessor("name", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Name</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: ({row, getValue}) => <div>{getValue()}</div>,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("email", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>E-mail</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: ({row, getValue}) => <div>{getValue()}</div>,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("type", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Type</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => USER_TYPE_LABELS[info.getValue()],
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("studentID", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Student ID</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => info.getValue() || "N/A",
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("entities", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Entities</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: ({getValue}) =>
|
|
||||||
getValue()
|
|
||||||
.map((e) => entities.find((x) => x.id === e.id)?.label)
|
|
||||||
.join(", "),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("subscriptionExpirationDate", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Expiration</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => (
|
|
||||||
<span className={clsx(info.getValue() ? expirationDateColor(moment(info.getValue()).toDate()) : "")}>
|
|
||||||
{!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("isVerified", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Verified</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => (
|
|
||||||
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
info.getValue() && "!bg-mti-purple-light ",
|
|
||||||
)}>
|
|
||||||
<BsCheck color="white" className="w-full h-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
header: (
|
|
||||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
|
||||||
Switch
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
id: "actions",
|
|
||||||
cell: () => "",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const demographicColumns = [
|
|
||||||
columnHelper.accessor("name", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Name</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: ({row, getValue}) => <div>{getValue()}</div>,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("demographicInformation.country", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Country</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) =>
|
|
||||||
info.getValue()
|
|
||||||
? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${
|
|
||||||
countries[info.getValue() as unknown as keyof TCountries]?.name
|
|
||||||
} (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})`
|
|
||||||
: "N/A",
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("demographicInformation.phone", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Phone</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => info.getValue() || "N/A",
|
|
||||||
enableSorting: true,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor(
|
|
||||||
(x) =>
|
|
||||||
x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment,
|
|
||||||
{
|
|
||||||
id: "employment",
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Employment</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
|
|
||||||
enableSorting: true,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
columnHelper.accessor("lastLogin", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Last Login</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("demographicInformation.gender", {
|
|
||||||
header: (
|
|
||||||
<button className="flex gap-2 items-center">
|
|
||||||
<span>Gender</span>
|
|
||||||
</button>
|
|
||||||
) as any,
|
|
||||||
cell: (info) => capitalize(info.getValue()) || "N/A",
|
|
||||||
enableSorting: true,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
header: (
|
|
||||||
<span className="cursor-pointer" onClick={() => setShowDemographicInformation((prev) => !prev)}>
|
|
||||||
Switch
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
id: "actions",
|
|
||||||
cell: () => "",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>EnCoach</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
|
||||||
/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<ToastContainer />
|
|
||||||
|
|
||||||
<Layout user={user} className="!gap-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => router.back()}
|
|
||||||
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
|
||||||
<BsChevronLeft />
|
|
||||||
</button>
|
|
||||||
<h2 className="font-bold text-2xl">User List</h2>
|
|
||||||
</div>
|
|
||||||
<List<User> data={users} searchFields={SEARCH_FIELDS} columns={showDemographicInformation ? demographicColumns : defaultColumns} />
|
|
||||||
</Layout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ import Head from "next/head";
|
|||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {Stat, User} from "@/interfaces/user";
|
import {Stat, User} from "@/interfaces/user";
|
||||||
import {useEffect, useRef, useState} from "react";
|
import {useEffect, useMemo, useRef, useState} from "react";
|
||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import {groupByDate} from "@/utils/stats";
|
import {groupByDate} from "@/utils/stats";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -22,9 +22,18 @@ import RecordFilter from "@/components/Medium/RecordFilter";
|
|||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import {Assignment} from "@/interfaces/results";
|
||||||
import {getUsers} from "@/utils/users.be";
|
import {getEntitiesUsers, getUsers} from "@/utils/users.be";
|
||||||
import {getAssignments, getAssignmentsByAssigner} from "@/utils/assignments.be";
|
import {getAssignments, getAssignmentsByAssigner, getEntitiesAssignments} from "@/utils/assignments.be";
|
||||||
import useGradingSystem from "@/hooks/useGrading";
|
import useGradingSystem from "@/hooks/useGrading";
|
||||||
|
import { mapBy, serialize } from "@/utils";
|
||||||
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
|
import { checkAccess } from "@/utils/permissions";
|
||||||
|
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||||
|
import { getGradingSystemByEntity } from "@/utils/grading.be";
|
||||||
|
import { Grading } from "@/interfaces";
|
||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
|
import CardList from "@/components/High/CardList";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
@@ -47,11 +56,16 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await getUsers();
|
const entityIDs = mapBy(user.entities, 'id')
|
||||||
const assignments = await getAssignments();
|
|
||||||
|
const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs)
|
||||||
|
const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id')))
|
||||||
|
const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id')))
|
||||||
|
const assignments = await (checkAccess(user, ["admin", "developer"]) ? getAssignments() : getEntitiesAssignments(mapBy(entities, 'id')))
|
||||||
|
const gradingSystems = await Promise.all(entityIDs.map(getGradingSystemByEntity))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user, users, assignments},
|
props: serialize({user, users, assignments, entities, gradingSystems}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
@@ -61,9 +75,13 @@ interface Props {
|
|||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
assignments: Assignment[];
|
assignments: Assignment[];
|
||||||
|
gradingSystems: Grading[]
|
||||||
|
entities: EntityWithRoles[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function History({user, users, assignments}: Props) {
|
const MAX_TRAINING_EXAMS = 10;
|
||||||
|
|
||||||
|
export default function History({user, users, assignments, entities, gradingSystems}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
||||||
state.selectedUser,
|
state.selectedUser,
|
||||||
@@ -72,8 +90,6 @@ export default function History({user, users, assignments}: Props) {
|
|||||||
state.setTraining,
|
state.setTraining,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
|
||||||
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
|
|
||||||
const [filter, setFilter] = useState<Filter>();
|
const [filter, setFilter] = useState<Filter>();
|
||||||
|
|
||||||
const {data: stats, isLoading: isStatsLoading} = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
|
const {data: stats, isLoading: isStatsLoading} = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
|
||||||
@@ -87,12 +103,10 @@ export default function History({user, users, assignments}: Props) {
|
|||||||
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
|
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
|
||||||
const renderPdfIcon = usePDFDownload("stats");
|
const renderPdfIcon = usePDFDownload("stats");
|
||||||
|
|
||||||
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
|
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
|
||||||
|
const setTrainingStats = useTrainingContentStore((state) => state.setStats);
|
||||||
|
|
||||||
useEffect(() => {
|
const groupedStats = useMemo(() => groupByDate(
|
||||||
if (stats && !isStatsLoading) {
|
|
||||||
setGroupedStats(
|
|
||||||
groupByDate(
|
|
||||||
stats.filter((x) => {
|
stats.filter((x) => {
|
||||||
if (
|
if (
|
||||||
(x.module === "writing" || x.module === "speaking") &&
|
(x.module === "writing" || x.module === "speaking") &&
|
||||||
@@ -102,15 +116,19 @@ export default function History({user, users, assignments}: Props) {
|
|||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
),
|
), [stats])
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [stats, isStatsLoading]);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
|
||||||
// // just set this initially
|
|
||||||
// if (!statsUserId) setStatsUserId(user.id);
|
useEffect(() => {
|
||||||
// }, []);
|
const handleRouteChange = (url: string) => {
|
||||||
|
setTraining(false);
|
||||||
|
};
|
||||||
|
router.events.on("routeChangeStart", handleRouteChange);
|
||||||
|
return () => {
|
||||||
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
|
};
|
||||||
|
}, [router.events, setTraining]);
|
||||||
|
|
||||||
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
|
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
|
||||||
if (filter && filter !== "assignments") {
|
if (filter && filter !== "assignments") {
|
||||||
@@ -139,10 +157,6 @@ export default function History({user, users, assignments}: Props) {
|
|||||||
return stats;
|
return stats;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_TRAINING_EXAMS = 10;
|
|
||||||
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
|
|
||||||
const setTrainingStats = useTrainingContentStore((state) => state.setStats);
|
|
||||||
|
|
||||||
const handleTrainingContentSubmission = () => {
|
const handleTrainingContentSubmission = () => {
|
||||||
if (groupedStats) {
|
if (groupedStats) {
|
||||||
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
||||||
@@ -159,19 +173,13 @@ export default function History({user, users, assignments}: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const filteredStats = useMemo(() =>
|
||||||
const handleRouteChange = (url: string) => {
|
Object.keys(filterStatsByDate(groupedStats))
|
||||||
setTraining(false);
|
.sort((a, b) => parseInt(b) - parseInt(a)),
|
||||||
};
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
router.events.on("routeChangeStart", handleRouteChange);
|
[groupedStats, filter])
|
||||||
return () => {
|
|
||||||
router.events.off("routeChangeStart", handleRouteChange);
|
|
||||||
};
|
|
||||||
}, [router.events, setTraining]);
|
|
||||||
|
|
||||||
const customContent = (timestamp: string) => {
|
const customContent = (timestamp: string) => {
|
||||||
if (!groupedStats) return <></>;
|
|
||||||
|
|
||||||
const dateStats = groupedStats[timestamp];
|
const dateStats = groupedStats[timestamp];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -212,7 +220,7 @@ export default function History({user, users, assignments}: Props) {
|
|||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<RecordFilter user={user} filterState={{filter: filter, setFilter: setFilter}}>
|
<RecordFilter user={user} users={users} entities={entities} filterState={{filter: filter, setFilter: setFilter}}>
|
||||||
{training && (
|
{training && (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="font-semibold text-2xl mr-4">
|
<div className="font-semibold text-2xl mr-4">
|
||||||
@@ -231,14 +239,12 @@ export default function History({user, users, assignments}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</RecordFilter>
|
</RecordFilter>
|
||||||
{groupedStats && Object.keys(groupedStats).length > 0 && !isStatsLoading && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
|
||||||
{Object.keys(filterStatsByDate(groupedStats))
|
{filteredStats.length > 0 && !isStatsLoading && (
|
||||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
<CardList list={filteredStats} renderCard={customContent} searchFields={[]} pageSize={30} className="lg:!grid-cols-3" />
|
||||||
.map(customContent)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{groupedStats && Object.keys(groupedStats).length === 0 && !isStatsLoading && (
|
{filteredStats.length === 0 && !isStatsLoading && (
|
||||||
<span className="font-semibold ml-1">No record to display...</span>
|
<span className="font-semibold ml-1">No record to display...</span>
|
||||||
)}
|
)}
|
||||||
{isStatsLoading && (
|
{isStatsLoading && (
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ import {getUserPermissions} from "@/utils/permissions.be";
|
|||||||
import { Permission, PermissionType } from "@/interfaces/permissions";
|
import { Permission, PermissionType } from "@/interfaces/permissions";
|
||||||
import { getUsers } from "@/utils/users.be";
|
import { getUsers } from "@/utils/users.be";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
|
import { getEntitiesWithRoles, getEntityWithRoles } from "@/utils/entities.be";
|
||||||
|
import { mapBy, serialize } from "@/utils";
|
||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user as User;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -50,18 +53,20 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const permissions = await getUserPermissions(user.id);
|
const permissions = await getUserPermissions(user.id);
|
||||||
|
const entities = await getEntitiesWithRoles(mapBy(user.entities, 'id')) || []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user, permissions},
|
props: serialize({ user, permissions, entities }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
permissions: PermissionType[];
|
permissions: PermissionType[];
|
||||||
|
entities: EntityWithRoles[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Admin({user, permissions}: Props) {
|
export default function Admin({ user, entities, permissions }: Props) {
|
||||||
const { gradingSystem, mutate } = useGradingSystem();
|
const { gradingSystem, mutate } = useGradingSystem();
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
|
|
||||||
@@ -90,7 +95,7 @@ export default function Admin({user, permissions}: Props) {
|
|||||||
<CodeGenerator user={user} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
<CodeGenerator user={user} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal isOpen={modalOpen === "createUser"} onClose={() => setModalOpen(undefined)}>
|
<Modal isOpen={modalOpen === "createUser"} onClose={() => setModalOpen(undefined)}>
|
||||||
<UserCreator user={user} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
<UserCreator user={user} entities={entities} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal isOpen={modalOpen === "gradingSystem"} onClose={() => setModalOpen(undefined)}>
|
<Modal isOpen={modalOpen === "gradingSystem"} onClose={() => setModalOpen(undefined)}>
|
||||||
<CorporateGradingSystem
|
<CorporateGradingSystem
|
||||||
|
|||||||
@@ -17,22 +17,28 @@ import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
|||||||
import {countExamModules, countFullExams, MODULE_ARRAY, sortByModule} from "@/utils/moduleUtils";
|
import {countExamModules, countFullExams, MODULE_ARRAY, sortByModule} from "@/utils/moduleUtils";
|
||||||
import {Chart} from "react-chartjs-2";
|
import {Chart} from "react-chartjs-2";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import Select from "react-select";
|
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {Stat} from "@/interfaces/user";
|
import {Group, Stat, User} from "@/interfaces/user";
|
||||||
import {Divider} from "primereact/divider";
|
import {Divider} from "primereact/divider";
|
||||||
import Badge from "@/components/Low/Badge";
|
import Badge from "@/components/Low/Badge";
|
||||||
|
import { mapBy, serialize } from "@/utils";
|
||||||
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
|
import { checkAccess } from "@/utils/permissions";
|
||||||
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||||
|
import Select from "@/components/Low/Select";
|
||||||
|
|
||||||
ChartJS.register(LinearScale, CategoryScale, PointElement, LineElement, LineController, Legend, Tooltip);
|
ChartJS.register(LinearScale, CategoryScale, PointElement, LineElement, LineController, Legend, Tooltip);
|
||||||
|
|
||||||
const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8", "#414288"];
|
const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8", "#414288"];
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user as User;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@@ -52,13 +58,25 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const entityIDs = mapBy(user.entities, 'id')
|
||||||
|
const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs)
|
||||||
|
const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id')))
|
||||||
|
const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id')))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user: req.session.user},
|
props: serialize({user, entities, users, groups}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Stats() {
|
interface Props {
|
||||||
const [statsUserId, setStatsUserId] = useState<string>();
|
user: User
|
||||||
|
users: User[]
|
||||||
|
entities: EntityWithRoles[]
|
||||||
|
groups: Group[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Stats({ user, entities, users, groups }: Props) {
|
||||||
|
const [statsUserId, setStatsUserId] = useState<string>(user.id);
|
||||||
const [startDate, setStartDate] = useState<Date | null>(moment(new Date()).subtract(1, "weeks").toDate());
|
const [startDate, setStartDate] = useState<Date | null>(moment(new Date()).subtract(1, "weeks").toDate());
|
||||||
const [endDate, setEndDate] = useState<Date | null>(new Date());
|
const [endDate, setEndDate] = useState<Date | null>(new Date());
|
||||||
const [initialStatDate, setInitialStatDate] = useState<Date>();
|
const [initialStatDate, setInitialStatDate] = useState<Date>();
|
||||||
@@ -69,15 +87,8 @@ export default function Stats() {
|
|||||||
const [dailyScoreDate, setDailyScoreDate] = useState<Date | null>(new Date());
|
const [dailyScoreDate, setDailyScoreDate] = useState<Date | null>(new Date());
|
||||||
const [intervalDates, setIntervalDates] = useState<Date[]>([]);
|
const [intervalDates, setIntervalDates] = useState<Date[]>([]);
|
||||||
|
|
||||||
const {user} = useUser({redirectTo: "/login"});
|
|
||||||
const {users} = useUsers();
|
|
||||||
const {groups} = useGroups({admin: user?.id});
|
|
||||||
const {data: stats} = useFilterRecordsByUser<Stat[]>(statsUserId, !statsUserId);
|
const {data: stats} = useFilterRecordsByUser<Stat[]>(statsUserId, !statsUserId);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) setStatsUserId(user.id);
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInitialStatDate(
|
setInitialStatDate(
|
||||||
stats
|
stats
|
||||||
@@ -190,16 +201,7 @@ export default function Stats() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||||
onChange={(value) => setStatsUserId(value?.value)}
|
onChange={(value) => setStatsUserId(value?.value || user.id)}
|
||||||
menuPortalTarget={document?.body}
|
|
||||||
styles={{
|
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
|
||||||
option: (styles, state) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{["corporate", "teacher", "mastercorporate"].includes(user.type) && groups.length > 0 && (
|
{["corporate", "teacher", "mastercorporate"].includes(user.type) && groups.length > 0 && (
|
||||||
@@ -209,16 +211,7 @@ export default function Stats() {
|
|||||||
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
||||||
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||||
onChange={(value) => setStatsUserId(value?.value)}
|
onChange={(value) => setStatsUserId(value?.value || user.id)}
|
||||||
menuPortalTarget={document?.body}
|
|
||||||
styles={{
|
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
|
||||||
option: (styles, state) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -20,9 +20,15 @@ import TrainingScore from "@/training/TrainingScore";
|
|||||||
import ModuleBadge from "@/components/ModuleBadge";
|
import ModuleBadge from "@/components/ModuleBadge";
|
||||||
import RecordFilter from "@/components/Medium/RecordFilter";
|
import RecordFilter from "@/components/Medium/RecordFilter";
|
||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
|
import { mapBy, serialize } from "@/utils";
|
||||||
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
|
import { getAssignmentsByAssignee } from "@/utils/assignments.be";
|
||||||
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
import { Assignment } from "@/interfaces/results";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user as User;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@@ -42,12 +48,16 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const entityIDs = mapBy(user.entities, 'id')
|
||||||
|
const entities = await getEntitiesWithRoles(entityIDs)
|
||||||
|
const users = await getEntitiesUsers(entityIDs)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user: req.session.user},
|
props: serialize({user, users, entities}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
const Training: React.FC<{user: User}> = ({user}) => {
|
const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[] }> = ({user, entities, users}) => {
|
||||||
const [recordUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setTraining]);
|
const [recordUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setTraining]);
|
||||||
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
||||||
|
|
||||||
@@ -193,7 +203,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<RecordFilter user={user} filterState={{filter: filter, setFilter: setFilter}} assignments={false}>
|
<RecordFilter users={users} entities={entities} user={user} filterState={{filter: filter, setFilter: setFilter}} assignments={false}>
|
||||||
{user.type === "student" && (
|
{user.type === "student" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
export const AVATARS = [
|
export const AVATARS = [
|
||||||
{
|
{
|
||||||
name: "Matthew Noah",
|
name: "Gia",
|
||||||
id: "5912afa7c77c47d3883af3d874047aaf",
|
id: "gia.business",
|
||||||
gender: "male",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Vera Cerise",
|
|
||||||
id: "9e58d96a383e4568a7f1e49df549e0e4",
|
|
||||||
gender: "female",
|
gender: "female",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Edward Tony",
|
name: "Vadim",
|
||||||
id: "d2cdd9c0379a4d06ae2afb6e5039bd0c",
|
id: "vadim.business",
|
||||||
gender: "male",
|
gender: "male",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Tanya Molly",
|
name: "Orhan",
|
||||||
id: "045cb5dcd00042b3a1e4f3bc1c12176b",
|
id: "orhan.business",
|
||||||
gender: "female",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Kayla Abbi",
|
|
||||||
id: "1ae1e5396cc444bfad332155fdb7a934",
|
|
||||||
gender: "female",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Jerome Ryan",
|
|
||||||
id: "0ee6aa7cc1084063a630ae514fccaa31",
|
|
||||||
gender: "male",
|
gender: "male",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Tyler Christopher",
|
name: "Flora",
|
||||||
id: "5772cff935844516ad7eeff21f839e43",
|
id: "flora.business",
|
||||||
|
gender: "female",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Scarlett",
|
||||||
|
id: "scarlett.business",
|
||||||
|
gender: "female",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parker",
|
||||||
|
id: "parker.casual",
|
||||||
|
gender: "male",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ethan",
|
||||||
|
id: "ethan.business",
|
||||||
gender: "male",
|
gender: "male",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { app } from "@/firebase";
|
import { app } from "@/firebase";
|
||||||
|
import { WithEntity } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import { CorporateUser, Group, GroupWithUsers, MasterCorporateUser, StudentUser, TeacherUser, Type, User } from "@/interfaces/user";
|
import { CorporateUser, Group, GroupWithUsers, MasterCorporateUser, StudentUser, TeacherUser, Type, User } from "@/interfaces/user";
|
||||||
import client from "@/lib/mongodb";
|
import client from "@/lib/mongodb";
|
||||||
@@ -8,6 +9,33 @@ import {getSpecificUsers} from "./users.be";
|
|||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
|
const addEntityToGroupPipeline = [
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "entities",
|
||||||
|
localField: "entity",
|
||||||
|
foreignField: "id",
|
||||||
|
as: "entity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$addFields: {
|
||||||
|
entity: { $arrayElemAt: ["$entity", 0] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$addFields: {
|
||||||
|
entity: {
|
||||||
|
$cond: {
|
||||||
|
if: { $isArray: "$entity" },
|
||||||
|
then: undefined,
|
||||||
|
else: "$entity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => {
|
export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => {
|
||||||
const corporate = await db.collection("users").findOne<User>({ id: corporateID });
|
const corporate = await db.collection("users").findOne<User>({ id: corporateID });
|
||||||
const participant = await db.collection("users").findOne<User>({ id: participantID });
|
const participant = await db.collection("users").findOne<User>({ id: participantID });
|
||||||
@@ -48,8 +76,9 @@ export const getGroup = async (id: string) => {
|
|||||||
return await db.collection("groups").findOne<Group>({ id });
|
return await db.collection("groups").findOne<Group>({ id });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGroups = async () => {
|
export const getGroups = async (): Promise<WithEntity<Group>[]> => {
|
||||||
return await db.collection("groups").find<Group>({}).toArray();
|
return await db.collection("groups")
|
||||||
|
.aggregate<WithEntity<Group>>(addEntityToGroupPipeline).toArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getParticipantGroups = async (id: string) => {
|
export const getParticipantGroups = async (id: string) => {
|
||||||
@@ -64,6 +93,15 @@ export const getUserNamedGroup = async (id: string, name: string) => {
|
|||||||
return await db.collection("groups").findOne<Group>({ admin: id, name });
|
return await db.collection("groups").findOne<Group>({ admin: id, name });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeParticipantFromGroup = async (id: string, user: string) => {
|
||||||
|
return await db.collection("groups").updateOne({id}, {
|
||||||
|
// @ts-expect-error
|
||||||
|
$pull: {
|
||||||
|
participants: user
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const getUsersGroups = async (ids: string[]) => {
|
export const getUsersGroups = async (ids: string[]) => {
|
||||||
return await db
|
return await db
|
||||||
.collection("groups")
|
.collection("groups")
|
||||||
@@ -125,8 +163,9 @@ export const getCorporateNameForStudent = async (studentID: string) => {
|
|||||||
|
|
||||||
export const getGroupsByEntity = async (id: string) => await db.collection("groups").find<Group>({ entity: id }).toArray();
|
export const getGroupsByEntity = async (id: string) => await db.collection("groups").find<Group>({ entity: id }).toArray();
|
||||||
|
|
||||||
export const getGroupsByEntities = async (ids: string[]) =>
|
export const getGroupsByEntities = async (ids: string[]): Promise<WithEntity<Group>[]> =>
|
||||||
await db
|
await db.collection("groups")
|
||||||
.collection("groups")
|
.aggregate<WithEntity<Group>>([
|
||||||
.find<Group>({entity: {$in: ids}})
|
{ $match: { entity: { $in: ids } } },
|
||||||
.toArray();
|
...addEntityToGroupPipeline
|
||||||
|
]).toArray()
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const convertBase64 = (file: File) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapBy = <T>(obj: T[] | undefined, key: keyof T) => (obj || []).map((i) => i[key]);
|
export const mapBy = <T, K extends keyof T>(obj: T[] | undefined, key: K) => (obj || []).map((i) => i[key] as T[K]);
|
||||||
export const filterBy = <T>(obj: T[], key: keyof T, value: any) => obj.filter((i) => i[key] === value);
|
export const filterBy = <T>(obj: T[], key: keyof T, value: any) => obj.filter((i) => i[key] === value);
|
||||||
|
|
||||||
export const serialize = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
export const serialize = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ import {getGroupsForUser, getParticipantGroups, getUserGroups, getUsersGroups} f
|
|||||||
import { uniq } from "lodash";
|
import { uniq } from "lodash";
|
||||||
import { getUserCodes } from "./codes.be";
|
import { getUserCodes } from "./codes.be";
|
||||||
import client from "@/lib/mongodb";
|
import client from "@/lib/mongodb";
|
||||||
import {WithEntity} from "@/interfaces/entity";
|
import { WithEntities } from "@/interfaces/entity";
|
||||||
import { getEntity } from "./entities.be";
|
import { getEntity } from "./entities.be";
|
||||||
import { getRole } from "./roles.be";
|
import { getRole } from "./roles.be";
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
export async function getUsers() {
|
export async function getUsers(filter?: object) {
|
||||||
return await db
|
return await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>({}, {projection: {_id: 0}})
|
.find<User>(filter || {}, { projection: { _id: 0 } })
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserWithEntity(id: string): Promise<WithEntity<User> | undefined> {
|
export async function getUserWithEntity(id: string): Promise<WithEntities<User> | undefined> {
|
||||||
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
|
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
|
||||||
if (!user) return undefined;
|
if (!user) return undefined;
|
||||||
|
|
||||||
@@ -58,10 +58,10 @@ export async function countEntityUsers(id: string) {
|
|||||||
return await db.collection("users").countDocuments({ "entities.id": id });
|
return await db.collection("users").countDocuments({ "entities.id": id });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntitiesUsers(ids: string[], limit?: number) {
|
export async function getEntitiesUsers(ids: string[], filter?: object, limit?: number) {
|
||||||
return await db
|
return await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>({"entities.id": {$in: ids}})
|
.find<User>({ "entities.id": { $in: ids }, ...(filter || {}) })
|
||||||
.limit(limit || 0)
|
.limit(limit || 0)
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { WithLabeledEntities } from "@/interfaces/entity";
|
||||||
import { Group, User } from "@/interfaces/user";
|
import { Group, User } from "@/interfaces/user";
|
||||||
import { getUserCompanyName, USER_TYPE_LABELS } from "@/resources/user";
|
import { getUserCompanyName, USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
@@ -7,7 +8,7 @@ export interface UserListRow {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
type: string;
|
type: string;
|
||||||
companyName: string;
|
entities: string;
|
||||||
expiryDate: string;
|
expiryDate: string;
|
||||||
verified: string;
|
verified: string;
|
||||||
country: string;
|
country: string;
|
||||||
@@ -16,12 +17,12 @@ export interface UserListRow {
|
|||||||
gender: string;
|
gender: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group[]) => {
|
export const exportListToExcel = (rowUsers: WithLabeledEntities<User>[]) => {
|
||||||
const rows: UserListRow[] = rowUsers.map((user) => ({
|
const rows: UserListRow[] = rowUsers.map((user) => ({
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
type: USER_TYPE_LABELS[user.type],
|
type: USER_TYPE_LABELS[user.type],
|
||||||
companyName: getUserCompanyName(user, users, groups),
|
entities: user.entities.map((e) => e.label).join(', '),
|
||||||
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",
|
||||||
@@ -32,7 +33,7 @@ export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group
|
|||||||
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",
|
||||||
}));
|
}));
|
||||||
const header = "Name,Email,Type,Company Name,Expiry Date,Country,Phone,Employment/Department,Gender,Verification";
|
const header = "Name,Email,Type,Entities,Expiry Date,Country,Phone,Employment/Department,Gender,Verification";
|
||||||
const rowsString = rows.map((x) => Object.values(x).join(",")).join("\n");
|
const rowsString = rows.map((x) => Object.values(x).join(",")).join("\n");
|
||||||
|
|
||||||
return `${header}\n${rowsString}`;
|
return `${header}\n${rowsString}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user