Improved the overall stability and speed of the app

This commit is contained in:
Tiago Ribeiro
2024-08-29 23:21:20 +01:00
parent b57e11bec4
commit 39710aaea1
17 changed files with 315 additions and 280 deletions

View File

@@ -212,7 +212,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
)} )}
</div> </div>
<div className="fixed bottom-12 flex flex-col gap-0"> <div className="2xl:fixed bottom-12 flex flex-col gap-0 -2xl:mt-8">
<div <div
role="button" role="button"
tabIndex={1} tabIndex={1}

View File

@@ -55,7 +55,14 @@ const USER_TYPE_PERMISSIONS: {
}, },
}; };
export default function BatchCodeGenerator({user, onFinish}: {user: User; onFinish: () => void}) { interface Props {
user: User;
users: User[];
permissions: PermissionType[];
onFinish: () => void;
}
export default function BatchCodeGenerator({user, users, permissions, onFinish}: Props) {
const [infos, setInfos] = useState<{email: string; name: string; passport_id: string}[]>([]); const [infos, setInfos] = useState<{email: string; name: string; passport_id: string}[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [expiryDate, setExpiryDate] = useState<Date | null>( const [expiryDate, setExpiryDate] = useState<Date | null>(
@@ -65,9 +72,6 @@ export default function BatchCodeGenerator({user, onFinish}: {user: User; onFini
const [type, setType] = useState<Type>("student"); const [type, setType] = useState<Type>("student");
const [showHelp, setShowHelp] = useState(false); const [showHelp, setShowHelp] = useState(false);
const {users} = useUsers();
const {permissions} = usePermissions(user?.id || "");
const {openFilePicker, filesContent, clear} = useFilePicker({ const {openFilePicker, filesContent, clear} = useFilePicker({
accept: ".xlsx", accept: ".xlsx",
multiple: false, multiple: false,

View File

@@ -61,7 +61,14 @@ const USER_TYPE_PERMISSIONS: {
}, },
}; };
export default function BatchCreateUser({user, onFinish}: {user: User; onFinish: () => void}) { interface Props {
user: User;
users: User[];
permissions: PermissionType[];
onFinish: () => void;
}
export default function BatchCreateUser({user, users, permissions, onFinish}: Props) {
const [infos, setInfos] = useState< const [infos, setInfos] = useState<
{ {
email: string; email: string;
@@ -83,9 +90,6 @@ export default function BatchCreateUser({user, onFinish}: {user: User; onFinish:
const [type, setType] = useState<Type>("student"); const [type, setType] = useState<Type>("student");
const [showHelp, setShowHelp] = useState(false); const [showHelp, setShowHelp] = useState(false);
const {users} = useUsers();
const {permissions} = usePermissions(user?.id || "");
const {openFilePicker, filesContent, clear} = useFilePicker({ const {openFilePicker, filesContent, clear} = useFilePicker({
accept: ".xlsx", accept: ".xlsx",
multiple: false, multiple: false,

View File

@@ -48,14 +48,19 @@ const USER_TYPE_PERMISSIONS: {
}, },
}; };
export default function CodeGenerator({user, onFinish}: {user: User; onFinish: () => void}) { interface Props {
user: User;
permissions: PermissionType[];
onFinish: () => void;
}
export default function CodeGenerator({user, permissions, onFinish}: Props) {
const [generatedCode, setGeneratedCode] = useState<string>(); const [generatedCode, setGeneratedCode] = useState<string>();
const [expiryDate, setExpiryDate] = useState<Date | null>( const [expiryDate, setExpiryDate] = useState<Date | null>(
user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null, user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null,
); );
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true); const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
const [type, setType] = useState<Type>("student"); const [type, setType] = useState<Type>("student");
const {permissions} = usePermissions(user?.id || "");
useEffect(() => { useEffect(() => {
if (!isExpiryDateEnabled) setExpiryDate(null); if (!isExpiryDateEnabled) setExpiryDate(null);

View File

@@ -9,10 +9,15 @@ import PackageList from "./PackageList";
import UserList from "./UserList"; import UserList from "./UserList";
import {checkAccess} from "@/utils/permissions"; import {checkAccess} from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import {PermissionType} from "@/interfaces/permissions";
export default function Lists({user}: {user: User}) { interface Props {
const {permissions} = usePermissions(user?.id || ""); user: User;
users: User[];
permissions: PermissionType[];
}
export default function Lists({user, users, permissions}: Props) {
return ( return (
<TabGroup> <TabGroup>
<TabList className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1"> <TabList className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">

View File

@@ -54,7 +54,14 @@ const USER_TYPE_PERMISSIONS: {
}, },
}; };
export default function UserCreator({user, onFinish}: {user: User; onFinish: () => void}) { interface Props {
user: User;
users: User[];
permissions: PermissionType[];
onFinish: () => void;
}
export default function UserCreator({user, users, 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>();
@@ -74,9 +81,7 @@ export default function UserCreator({user, onFinish}: {user: User; onFinish: ()
const [type, setType] = useState<Type>("student"); const [type, setType] = useState<Type>("student");
const [position, setPosition] = useState<string>(); const [position, setPosition] = useState<string>();
const {permissions} = usePermissions(user?.id || "");
const {groups} = useGroups({admin: ["developer", "admin"].includes(user?.type) ? undefined : user?.id, userType: user?.type}); const {groups} = useGroups({admin: ["developer", "admin"].includes(user?.type) ? undefined : user?.id, userType: user?.type});
const {users} = useUsers();
useEffect(() => { useEffect(() => {
if (!isExpiryDateEnabled) setExpiryDate(null); if (!isExpiryDateEnabled) setExpiryDate(null);

View File

@@ -13,7 +13,7 @@ import Speaking from "@/exams/Speaking";
import Writing from "@/exams/Writing"; import Writing from "@/exams/Writing";
import useUser from "@/hooks/useUser"; import useUser from "@/hooks/useUser";
import {Exam, LevelExam, UserSolution, Variant} from "@/interfaces/exam"; import {Exam, LevelExam, UserSolution, Variant} from "@/interfaces/exam";
import {Stat} from "@/interfaces/user"; import {Stat, User} from "@/interfaces/user";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
import {defaultExamUserSolutions, getExam} from "@/utils/exams"; import {defaultExamUserSolutions, getExam} from "@/utils/exams";
@@ -28,9 +28,10 @@ import useGradingSystem from "@/hooks/useGrading";
interface Props { interface Props {
page: "exams" | "exercises"; page: "exams" | "exercises";
user: User;
} }
export default function ExamPage({page}: Props) { export default function ExamPage({page, user}: Props) {
const [variant, setVariant] = useState<Variant>("full"); const [variant, setVariant] = useState<Variant>("full");
const [avoidRepeated, setAvoidRepeated] = useState(false); const [avoidRepeated, setAvoidRepeated] = useState(false);
const [hasBeenUploaded, setHasBeenUploaded] = useState(false); const [hasBeenUploaded, setHasBeenUploaded] = useState(false);
@@ -59,7 +60,6 @@ export default function ExamPage({page}: Props) {
const {bgColor, setBgColor} = useExamStore((state) => state); const {bgColor, setBgColor} = useExamStore((state) => state);
const setShuffleMaps = useExamStore((state) => state.setShuffles); const setShuffleMaps = useExamStore((state) => state.setShuffles);
const {user} = useUser({redirectTo: "/login"});
const router = useRouter(); const router = useRouter();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -5,6 +5,7 @@ import {sessionOptions} from "@/lib/session";
import {shouldRedirectHome} from "@/utils/navigation.disabled"; import {shouldRedirectHome} from "@/utils/navigation.disabled";
import ExamPage from "./(exam)/ExamPage"; import ExamPage from "./(exam)/ExamPage";
import Head from "next/head"; import Head from "next/head";
import {User} from "@/interfaces/user";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
@@ -14,7 +15,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
redirect: { redirect: {
destination: "/login", destination: "/login",
permanent: false, permanent: false,
} },
}; };
} }
@@ -23,7 +24,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
redirect: { redirect: {
destination: "/", destination: "/",
permanent: false, permanent: false,
} },
}; };
} }
@@ -32,7 +33,11 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
}; };
}, sessionOptions); }, sessionOptions);
export default function Page() { interface Props {
user: User;
}
export default function Page({user}: Props) {
return ( return (
<> <>
<Head> <Head>
@@ -44,7 +49,7 @@ export default function Page() {
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<ExamPage page="exams" /> <ExamPage page="exams" user={user} />
</> </>
); );
} }

View File

@@ -5,6 +5,7 @@ import {sessionOptions} from "@/lib/session";
import {shouldRedirectHome} from "@/utils/navigation.disabled"; import {shouldRedirectHome} from "@/utils/navigation.disabled";
import ExamPage from "./(exam)/ExamPage"; import ExamPage from "./(exam)/ExamPage";
import Head from "next/head"; import Head from "next/head";
import {User} from "@/interfaces/user";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
@@ -14,7 +15,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
redirect: { redirect: {
destination: "/login", destination: "/login",
permanent: false, permanent: false,
} },
}; };
} }
@@ -23,7 +24,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
redirect: { redirect: {
destination: "/", destination: "/",
permanent: false, permanent: false,
} },
}; };
} }
@@ -32,7 +33,11 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
}; };
}, sessionOptions); }, sessionOptions);
export default function Page() { interface Props {
user: User;
}
export default function Page({user}: Props) {
return ( return (
<> <>
<Head> <Head>
@@ -44,7 +49,7 @@ export default function Page() {
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<ExamPage page="exercises" /> <ExamPage page="exercises" user={user} />
</> </>
); );
} }

View File

@@ -22,6 +22,7 @@ import WritingGeneration from "./(generation)/WritingGeneration";
import LevelGeneration from "./(generation)/LevelGeneration"; import LevelGeneration from "./(generation)/LevelGeneration";
import SpeakingGeneration from "./(generation)/SpeakingGeneration"; import SpeakingGeneration from "./(generation)/SpeakingGeneration";
import {checkAccess} from "@/utils/permissions"; import {checkAccess} from "@/utils/permissions";
import {User} from "@/interfaces/user";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
@@ -49,10 +50,12 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
}; };
}, sessionOptions); }, sessionOptions);
export default function Generation() { interface Props {
const [module, setModule] = useState<Module>("reading"); user: User;
}
const {user} = useUser({redirectTo: "/login"}); export default function Generation({user}: Props) {
const [module, setModule] = useState<Module>("reading");
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
return ( return (

View File

@@ -29,7 +29,7 @@ import MasterCorporateDashboard from "@/dashboards/MasterCorporate";
import PaymentDue from "./(status)/PaymentDue"; import PaymentDue from "./(status)/PaymentDue";
import {useRouter} from "next/router"; import {useRouter} from "next/router";
import {PayPalScriptProvider} from "@paypal/react-paypal-js"; import {PayPalScriptProvider} from "@paypal/react-paypal-js";
import {CorporateUser, MasterCorporateUser, Type, User, userTypes} from "@/interfaces/user"; import {CorporateUser, Group, MasterCorporateUser, Type, User, userTypes} from "@/interfaces/user";
import Select from "react-select"; import Select from "react-select";
import {USER_TYPE_LABELS} from "@/resources/user"; import {USER_TYPE_LABELS} from "@/resources/user";
import {checkAccess, getTypesOfUser} from "@/utils/permissions"; import {checkAccess, getTypesOfUser} from "@/utils/permissions";
@@ -37,8 +37,10 @@ import {shouldRedirectHome} from "@/utils/navigation.disabled";
import useGroups from "@/hooks/useGroups"; 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 {getParticipantGroups, getUserGroups} from "@/utils/groups.be";
import {getUsers} from "@/utils/users.be";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
@@ -59,26 +61,20 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
}; };
} }
const groups = await getParticipantGroups(user.id);
const users = await getUsers();
return { return {
props: {user: req.session.user}, props: {user, groups, users},
}; };
}, sessionOptions); }, sessionOptions);
interface Props { interface Props {
user: User; user: User;
envVariables: {[key: string]: string}; groups: Group[];
users: User[];
} }
export default function Home(props: Props) { export default function Home({user, groups, users}: Props) {
const {user, mutateUser} = useUser({redirectTo: "/login"});
const {groups} = useGroups({});
const {users} = useUsers();
const router = useRouter();
useEffect(() => {
console.log(groups);
}, [groups]);
return ( return (
<> <>
<Head> <Head>

View File

@@ -12,7 +12,16 @@ import Link from "next/link";
import axios from "axios"; import axios from "axios";
import {ErrorMessage} from "@/constants/errors"; import {ErrorMessage} from "@/constants/errors";
import clsx from "clsx"; import clsx from "clsx";
import {CorporateUser, EmploymentStatus, EMPLOYMENT_STATUS, Gender, User, DemographicInformation, MasterCorporateUser} from "@/interfaces/user"; import {
CorporateUser,
EmploymentStatus,
EMPLOYMENT_STATUS,
Gender,
User,
DemographicInformation,
MasterCorporateUser,
Group,
} from "@/interfaces/user";
import CountrySelect from "@/components/Low/CountrySelect"; import CountrySelect from "@/components/Low/CountrySelect";
import {shouldRedirectHome} from "@/utils/navigation.disabled"; import {shouldRedirectHome} from "@/utils/navigation.disabled";
import moment from "moment"; import moment from "moment";
@@ -34,8 +43,9 @@ import {capitalize} from "lodash";
import TopicModal from "@/components/Medium/TopicModal"; import TopicModal from "@/components/Medium/TopicModal";
import {v4} from "uuid"; import {v4} from "uuid";
import {checkAccess, getTypesOfUser} from "@/utils/permissions"; import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import {getUserCorporate} from "@/utils/groups.be"; import {getParticipantGroups, getUserCorporate} from "@/utils/groups.be";
import {InferGetServerSidePropsType} from "next"; import {InferGetServerSidePropsType} from "next";
import {getUsers} from "@/utils/users.be";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const user = req.session.user; const user = req.session.user;
@@ -59,19 +69,26 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
} }
return { return {
props: {user, linkedCorporate: (await getUserCorporate(user.id)) || null}, props: {
user,
linkedCorporate: (await getUserCorporate(user.id)) || null,
groups: await getParticipantGroups(user.id),
users: await getUsers(),
},
}; };
}, sessionOptions); }, sessionOptions);
interface Props { interface Props {
user: User; user: User;
groups: Group[];
users: User[];
mutateUser: Function; mutateUser: Function;
linkedCorporate?: CorporateUser | MasterCorporateUser; linkedCorporate?: CorporateUser | MasterCorporateUser;
} }
const DoubleColumnRow = ({children}: {children: ReactNode}) => <div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>; const DoubleColumnRow = ({children}: {children: ReactNode}) => <div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>;
function UserProfile({user, mutateUser, linkedCorporate}: Props) { function UserProfile({user, mutateUser, linkedCorporate, groups, users}: Props) {
const [bio, setBio] = useState(user.bio || ""); const [bio, setBio] = useState(user.bio || "");
const [name, setName] = useState(user.name || ""); const [name, setName] = useState(user.name || "");
const [email, setEmail] = useState(user.email || ""); const [email, setEmail] = useState(user.email || "");
@@ -114,9 +131,6 @@ function UserProfile({user, mutateUser, linkedCorporate}: Props) {
const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false); const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false);
const {groups} = useGroups({});
const {users} = useUsers();
const profilePictureInput = useRef(null); const profilePictureInput = useRef(null);
const expirationDateColor = (date: Date) => { const expirationDateColor = (date: Date) => {
const momentDate = moment(date); const momentDate = moment(date);
@@ -643,7 +657,7 @@ function UserProfile({user, mutateUser, linkedCorporate}: Props) {
); );
} }
export default function Home({linkedCorporate}: {linkedCorporate?: CorporateUser | MasterCorporateUser}) { export default function Home(props: {linkedCorporate?: CorporateUser | MasterCorporateUser; groups: Group[]; users: User[]}) {
const {user, mutateUser} = useUser({redirectTo: "/login"}); const {user, mutateUser} = useUser({redirectTo: "/login"});
return ( return (
@@ -658,7 +672,7 @@ export default function Home({linkedCorporate}: {linkedCorporate?: CorporateUser
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<ToastContainer /> <ToastContainer />
{user && <UserProfile linkedCorporate={linkedCorporate} user={user} mutateUser={mutateUser} />} {user && <UserProfile user={user} mutateUser={mutateUser} {...props} />}
</> </>
); );
} }

View File

@@ -1,28 +1,31 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import Head from "next/head"; 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, 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";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import { ToastContainer } from "react-toastify"; import {ToastContainer} from "react-toastify";
import Layout from "@/components/High/Layout"; import Layout from "@/components/High/Layout";
import clsx from "clsx"; import clsx from "clsx";
import { shouldRedirectHome } from "@/utils/navigation.disabled"; import {shouldRedirectHome} from "@/utils/navigation.disabled";
import useAssignments from "@/hooks/useAssignments"; import useAssignments from "@/hooks/useAssignments";
import { uuidv4 } from "@firebase/util"; import {uuidv4} from "@firebase/util";
import { usePDFDownload } from "@/hooks/usePDFDownload"; import {usePDFDownload} from "@/hooks/usePDFDownload";
import useRecordStore from "@/stores/recordStore"; import useRecordStore from "@/stores/recordStore";
import StatsGridItem from "@/components/Medium/StatGridItem"; import StatsGridItem from "@/components/Medium/StatGridItem";
import RecordFilter from "@/components/Medium/RecordFilter"; 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 {getUsers} from "@/utils/users.be";
import {getAssignments, getAssignmentsByAssigner} from "@/utils/assignments.be";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => { export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
@@ -43,14 +46,23 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}; };
} }
const users = await getUsers();
const assignments = await getAssignments();
return { return {
props: { user: req.session.user }, props: {user, users, assignments},
}; };
}, sessionOptions); }, sessionOptions);
type Filter = "months" | "weeks" | "days" | "assignments" | undefined; type Filter = "months" | "weeks" | "days" | "assignments" | undefined;
export default function History({ user }: { user: User }) { interface Props {
user: User;
users: User[];
assignments: Assignment[];
}
export default function History({user, users, assignments}: 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,
@@ -60,12 +72,10 @@ export default function History({ user }: { user: User }) {
]); ]);
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id); // const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
const [groupedStats, setGroupedStats] = useState<{ [key: string]: Stat[] }>(); const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
const [filter, setFilter] = useState<Filter>(); const [filter, setFilter] = useState<Filter>();
const { assignments } = useAssignments({});
const { users } = useUsers(); const {data: stats, isLoading: isStatsLoading} = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
const setExams = useExamStore((state) => state.setExams); const setExams = useExamStore((state) => state.setExams);
const setShowSolutions = useExamStore((state) => state.setShowSolutions); const setShowSolutions = useExamStore((state) => state.setShowSolutions);
@@ -100,12 +110,12 @@ export default function History({ user }: { user: User }) {
// if (!statsUserId) setStatsUserId(user.id); // if (!statsUserId) setStatsUserId(user.id);
// }, []); // }, []);
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => { const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
if (filter && filter !== "assignments") { if (filter && filter !== "assignments") {
const filterDate = moment() const filterDate = moment()
.subtract({ [filter as string]: 1 }) .subtract({[filter as string]: 1})
.format("x"); .format("x");
const filteredStats: { [key: string]: Stat[] } = {}; const filteredStats: {[key: string]: Stat[]} = {};
Object.keys(stats).forEach((timestamp) => { Object.keys(stats).forEach((timestamp) => {
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp]; if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
@@ -114,7 +124,7 @@ export default function History({ user }: { user: User }) {
} }
if (filter && filter === "assignments") { if (filter && filter === "assignments") {
const filteredStats: { [key: string]: Stat[] } = {}; const filteredStats: {[key: string]: Stat[]} = {};
Object.keys(stats).forEach((timestamp) => { Object.keys(stats).forEach((timestamp) => {
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false)) if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
@@ -127,7 +137,6 @@ export default function History({ user }: { user: User }) {
return stats; return stats;
}; };
const MAX_TRAINING_EXAMS = 10; const MAX_TRAINING_EXAMS = 10;
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]); const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
const setTrainingStats = useTrainingContentStore((state) => state.setStats); const setTrainingStats = useTrainingContentStore((state) => state.setStats);
@@ -200,7 +209,7 @@ export default function History({ user }: { user: User }) {
<ToastContainer /> <ToastContainer />
{user && ( {user && (
<Layout user={user}> <Layout user={user}>
<RecordFilter user={user} filterState={{ filter: filter, setFilter: setFilter }} > <RecordFilter user={user} 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">

View File

@@ -24,8 +24,12 @@ import UserCreator from "./(admin)/UserCreator";
import CorporateGradingSystem from "./(admin)/CorporateGradingSystem"; import CorporateGradingSystem from "./(admin)/CorporateGradingSystem";
import useGradingSystem from "@/hooks/useGrading"; import useGradingSystem from "@/hooks/useGrading";
import {CEFR_STEPS} from "@/resources/grading"; import {CEFR_STEPS} from "@/resources/grading";
import {User} from "@/interfaces/user";
import {getUserPermissions} from "@/utils/permissions.be";
import {Permission, PermissionType} from "@/interfaces/permissions";
import {getUsers} from "@/utils/users.be";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
return { return {
@@ -45,14 +49,21 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
}; };
} }
const permissions = await getUserPermissions(user.id);
const users = await getUsers();
return { return {
props: {user: req.session.user}, props: {user, permissions, users},
}; };
}, sessionOptions); }, sessionOptions);
export default function Admin() { interface Props {
const {user} = useUser({redirectTo: "/login"}); user: User;
const {permissions} = usePermissions(user?.id || ""); users: User[];
permissions: PermissionType[];
}
export default function Admin({user, users, permissions}: Props) {
const {gradingSystem, mutate} = useGradingSystem(); const {gradingSystem, mutate} = useGradingSystem();
const [modalOpen, setModalOpen] = useState<string>(); const [modalOpen, setModalOpen] = useState<string>();
@@ -69,80 +80,78 @@ export default function Admin() {
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<ToastContainer /> <ToastContainer />
{user && ( <Layout user={user} className="gap-6">
<Layout user={user} className="gap-6"> <Modal isOpen={modalOpen === "batchCreateUser"} onClose={() => setModalOpen(undefined)}>
<Modal isOpen={modalOpen === "batchCreateUser"} onClose={() => setModalOpen(undefined)}> <BatchCreateUser user={user} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
<BatchCreateUser user={user} onFinish={() => setModalOpen(undefined)} /> </Modal>
</Modal> <Modal isOpen={modalOpen === "batchCreateCode"} onClose={() => setModalOpen(undefined)}>
<Modal isOpen={modalOpen === "batchCreateCode"} onClose={() => setModalOpen(undefined)}> <BatchCodeGenerator user={user} users={users} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
<BatchCodeGenerator user={user} onFinish={() => setModalOpen(undefined)} /> </Modal>
</Modal> <Modal isOpen={modalOpen === "createCode"} onClose={() => setModalOpen(undefined)}>
<Modal isOpen={modalOpen === "createCode"} onClose={() => setModalOpen(undefined)}> <CodeGenerator user={user} permissions={permissions} onFinish={() => setModalOpen(undefined)} />
<CodeGenerator user={user} 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} onFinish={() => setModalOpen(undefined)} /> </Modal>
</Modal> <Modal isOpen={modalOpen === "gradingSystem"} onClose={() => setModalOpen(undefined)}>
<Modal isOpen={modalOpen === "gradingSystem"} onClose={() => setModalOpen(undefined)}> <CorporateGradingSystem
<CorporateGradingSystem user={user}
user={user} defaultSteps={gradingSystem?.steps || CEFR_STEPS}
defaultSteps={gradingSystem?.steps || CEFR_STEPS} mutate={(steps) => {
mutate={(steps) => { mutate({user: user.id, steps});
mutate({user: user.id, steps}); setModalOpen(undefined);
setModalOpen(undefined); }}
}} />
/> </Modal>
</Modal>
<section className="w-full grid grid-cols-2 -md:grid-cols-1 gap-8"> <section className="w-full grid grid-cols-2 -md:grid-cols-1 gap-8">
<ExamLoader /> <ExamLoader />
{checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && ( {checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && (
<div className="w-full grid grid-cols-2 gap-4"> <div className="w-full grid grid-cols-2 gap-4">
<IconCard
Icon={BsCode}
label="Generate Single Code"
color="purple"
className="w-full h-full"
onClick={() => setModalOpen("createCode")}
/>
<IconCard
Icon={BsCodeSquare}
label="Generate Codes in Batch"
color="purple"
className="w-full h-full"
onClick={() => setModalOpen("batchCreateCode")}
/>
<IconCard
Icon={BsPersonFill}
label="Create Single User"
color="purple"
className="w-full h-full"
onClick={() => setModalOpen("createUser")}
/>
<IconCard
Icon={BsPeopleFill}
label="Create Users in Batch"
color="purple"
className="w-full h-full"
onClick={() => setModalOpen("batchCreateUser")}
/>
{checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) && (
<IconCard <IconCard
Icon={BsCode} Icon={BsGearFill}
label="Generate Single Code" label="Grading System"
color="purple" color="purple"
className="w-full h-full" className="w-full h-full col-span-2"
onClick={() => setModalOpen("createCode")} onClick={() => setModalOpen("gradingSystem")}
/> />
<IconCard )}
Icon={BsCodeSquare} </div>
label="Generate Codes in Batch" )}
color="purple" </section>
className="w-full h-full" <section className="w-full">
onClick={() => setModalOpen("batchCreateCode")} <Lists user={user} users={users} permissions={permissions} />
/> </section>
<IconCard </Layout>
Icon={BsPersonFill}
label="Create Single User"
color="purple"
className="w-full h-full"
onClick={() => setModalOpen("createUser")}
/>
<IconCard
Icon={BsPeopleFill}
label="Create Users in Batch"
color="purple"
className="w-full h-full"
onClick={() => setModalOpen("batchCreateUser")}
/>
{checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) && (
<IconCard
Icon={BsGearFill}
label="Grading System"
color="purple"
className="w-full h-full col-span-2"
onClick={() => setModalOpen("gradingSystem")}
/>
)}
</div>
)}
</section>
<section className="w-full">
<Lists user={user} />
</section>
</Layout>
)}
</> </>
); );
} }

View File

@@ -1,58 +1,36 @@
import { app } from "@/firebase"; import {app} from "@/firebase";
import { Assignment } from "@/interfaces/results"; import {Assignment} from "@/interfaces/results";
import { import {collection, getDocs, getFirestore, query, where} from "firebase/firestore";
collection,
getDocs,
getFirestore,
query,
where,
} from "firebase/firestore";
const db = getFirestore(app); const db = getFirestore(app);
export const getAssignmentsByAssigner = async ( export const getAssignmentsByAssigner = async (id: string, startDate?: Date, endDate?: Date) => {
id: string, const {docs} = await getDocs(
startDate?: Date, query(
endDate?: Date collection(db, "assignments"),
) => { ...[
const { docs } = await getDocs( where("assigner", "==", id),
query( ...(startDate ? [where("startDate", ">=", startDate.toISOString())] : []),
collection(db, "assignments"), // firebase doesnt accept compound queries so we have to filter on the server
...[ // ...endDate ? [where("endDate", "<=", endDate)] : [],
where("assigner", "==", id), ],
...(startDate ? [where("startDate", ">=", startDate.toISOString())] : []), ),
// firebase doesnt accept compound queries so we have to filter on the server );
// ...endDate ? [where("endDate", "<=", endDate)] : [], if (endDate) {
] return docs.map((x) => ({...(x.data() as Assignment), id: x.id})).filter((x) => new Date(x.endDate) <= endDate) as Assignment[];
) }
); return docs.map((x) => ({...x.data(), id: x.id})) as Assignment[];
if (endDate) { };
return docs export const getAssignments = async () => {
.map((x) => ({ ...(x.data() as Assignment), id: x.id })) const {docs} = await getDocs(collection(db, "assignments"));
.filter((x) => new Date(x.endDate) <= endDate) as Assignment[]; return docs.map((x) => ({...x.data(), id: x.id})) as Assignment[];
}
return docs.map((x) => ({ ...x.data(), id: x.id })) as Assignment[];
}; };
export const getAssignmentsByAssignerBetweenDates = async ( export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate: Date, endDate: Date) => {
id: string, const {docs} = await getDocs(query(collection(db, "assignments"), where("assigner", "==", id)));
startDate: Date, return docs.map((x) => ({...x.data(), id: x.id})) as Assignment[];
endDate: Date
) => {
const { docs } = await getDocs(
query(collection(db, "assignments"), where("assigner", "==", id))
);
return docs.map((x) => ({ ...x.data(), id: x.id })) as Assignment[];
}; };
export const getAssignmentsByAssigners = async ( export const getAssignmentsByAssigners = async (ids: string[], startDate?: Date, endDate?: Date) => {
ids: string[], return (await Promise.all(ids.map((id) => getAssignmentsByAssigner(id, startDate, endDate)))).flat();
startDate?: Date,
endDate?: Date
) => {
return (
await Promise.all(
ids.map((id) => getAssignmentsByAssigner(id, startDate, endDate))
)
).flat();
}; };

View File

@@ -1,99 +1,89 @@
import { app, adminApp } from "@/firebase"; import {app, adminApp} from "@/firebase";
import { getAuth } from "firebase-admin/auth"; import {getAuth} from "firebase-admin/auth";
import { import {collection, deleteDoc, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore";
collection, import {Permission, PermissionType, permissions, PermissionTopic} from "@/interfaces/permissions";
deleteDoc, import {v4} from "uuid";
doc,
getDoc,
getDocs,
getFirestore,
query,
setDoc,
where,
} from "firebase/firestore";
import {
Permission,
PermissionType,
permissions,
PermissionTopic,
} from "@/interfaces/permissions";
import { v4 } from "uuid";
const db = getFirestore(app); const db = getFirestore(app);
async function createPermission(topic: string, type: string) { async function createPermission(topic: string, type: string) {
const permData = doc(db, "permissions", v4()); const permData = doc(db, "permissions", v4());
const permDoc = await getDoc(permData); const permDoc = await getDoc(permData);
if (permDoc.exists()) { if (permDoc.exists()) {
return true; return true;
} }
await setDoc(permData, { await setDoc(permData, {
type, type,
topic, topic,
users: [], users: [],
}); });
} }
interface PermissionsHelperList { interface PermissionsHelperList {
topic: string; topic: string;
type: string; type: string;
} }
export function getPermissions(userId: string | undefined, docs: Permission[]) { export function getPermissions(userId: string | undefined, docs: Permission[]) {
if (!userId) { if (!userId) {
return []; return [];
} }
// the concept is like a blacklist // the concept is like a blacklist
// if the user exists in the list, he can't access this permission // if the user exists in the list, he can't access this permission
// even if his profile allows // even if his profile allows
const permissions = docs.reduce((acc: PermissionType[], doc: Permission) => { const permissions = docs.reduce((acc: PermissionType[], doc: Permission) => {
// typescript was complaining even with the validation on the top // typescript was complaining even with the validation on the top
if (doc.users.includes(userId)) { if (doc.users.includes(userId)) {
return acc; return acc;
} }
return [...acc, doc.type]; return [...acc, doc.type];
}, []) as PermissionType[]; }, []) as PermissionType[];
return permissions; return permissions;
}
export async function getUserPermissions(id: string) {
const permissions = await getPermissionDocs();
return getPermissions(id, permissions);
} }
export async function bootstrap() { export async function bootstrap() {
await permissions await permissions
.reduce((accm: PermissionsHelperList[], permissionData) => { .reduce((accm: PermissionsHelperList[], permissionData) => {
return [ return [
...accm, ...accm,
...permissionData.list.map((type) => ({ ...permissionData.list.map((type) => ({
topic: permissionData.topic, topic: permissionData.topic,
type, type,
})), })),
]; ];
}, []) }, [])
.forEach(async ({ topic, type }) => { .forEach(async ({topic, type}) => {
await createPermission(topic, type); await createPermission(topic, type);
}); });
} }
export async function getPermissionDoc(id: string) { export async function getPermissionDoc(id: string) {
const docRef = doc(db, "permissions", id); const docRef = doc(db, "permissions", id);
const docSnap = await getDoc(docRef); const docSnap = await getDoc(docRef);
if (docSnap.exists()) { if (docSnap.exists()) {
return docSnap.data() as Permission; return docSnap.data() as Permission;
} }
throw new Error("Permission not found"); throw new Error("Permission not found");
} }
export async function getPermissionDocs() { export async function getPermissionDocs() {
const q = query(collection(db, "permissions")); const q = query(collection(db, "permissions"));
// firebase is missing something like array-not-contains // firebase is missing something like array-not-contains
const snapshot = await getDocs(q); const snapshot = await getDocs(q);
const docs = snapshot.docs.map((doc) => ({ const docs = snapshot.docs.map((doc) => ({
id: doc.id, id: doc.id,
...doc.data(), ...doc.data(),
})) as Permission[]; })) as Permission[];
return docs; return docs;
} }

View File

@@ -5,21 +5,23 @@ import {CorporateUser, Group, User} from "@/interfaces/user";
import {getGroupsForUser} from "./groups.be"; import {getGroupsForUser} from "./groups.be";
import {uniq, uniqBy} from "lodash"; import {uniq, uniqBy} from "lodash";
import {getUserCodes} from "./codes.be"; import {getUserCodes} from "./codes.be";
import moment from "moment";
const db = getFirestore(app); const db = getFirestore(app);
export async function getUsers() { export async function getUsers() {
const snapshot = await getDocs(collection(db, "users")); const snapshot = await getDocs(collection(db, "users"));
return snapshot.docs.map((doc) => ({ return snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(), ...doc.data(),
})) as User[]; id: doc.id,
registrationDate: moment(doc.data().registrationDate).toISOString(),
})) as unknown as User[];
} }
export async function getUser(id: string) { export async function getUser(id: string) {
const userDoc = await getDoc(doc(db, "users", id)); const userDoc = await getDoc(doc(db, "users", id));
return {...userDoc.data(), id} as User; return {...userDoc.data(), id, registrationDate: moment(userDoc.data()?.registrationDate).toISOString()} as unknown as User;
} }
export async function getSpecificUsers(ids: string[]) { export async function getSpecificUsers(ids: string[]) {
@@ -28,9 +30,10 @@ export async function getSpecificUsers(ids: string[]) {
const snapshot = await getDocs(query(collection(db, "users"), where("id", "in", ids))); const snapshot = await getDocs(query(collection(db, "users"), where("id", "in", ids)));
const groups = snapshot.docs.map((doc) => ({ const groups = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(), ...doc.data(),
})) as User[]; id: doc.id,
registrationDate: moment(doc.data().registrationDate).toISOString(),
})) as unknown as User[];
return groups; return groups;
} }