Merge branch 'develop' of https://bitbucket.org/ecropdev/ielts-ui into feature/level-file-upload

This commit is contained in:
Carlos Mesquita
2024-08-22 22:05:53 +01:00
17 changed files with 1090 additions and 704 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
public/red-stock-photo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

View File

@@ -78,7 +78,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
const [status, setStatus] = useState(user.status); const [status, setStatus] = useState(user.status);
const [referralAgentLabel, setReferralAgentLabel] = useState<string>(); const [referralAgentLabel, setReferralAgentLabel] = useState<string>();
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined); const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
const [passport_id, setPassportID] = useState<string | undefined>(user.type === "student" ? user.demographicInformation?.passport_id : undefined); const [studentID, setStudentID] = useState<string | undefined>(user.type === "student" ? user.studentID : undefined);
const [referralAgent, setReferralAgent] = useState(user.type === "corporate" ? user.corporateInformation?.referralAgent : undefined); const [referralAgent, setReferralAgent] = useState(user.type === "corporate" ? user.corporateInformation?.referralAgent : undefined);
const [companyName, setCompanyName] = useState( const [companyName, setCompanyName] = useState(
@@ -123,6 +123,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
.post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, { .post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, {
...user, ...user,
subscriptionExpirationDate: expiryDate, subscriptionExpirationDate: expiryDate,
studentID,
type, type,
status, status,
agentInformation: agentInformation:
@@ -248,7 +249,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
onChange={setCompanyName} onChange={setCompanyName}
placeholder="Enter corporate name" placeholder="Enter corporate name"
defaultValue={companyName} defaultValue={companyName}
disabled={disabled} disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))}
/> />
<Input <Input
label="Number of Users" label="Number of Users"
@@ -257,7 +258,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
onChange={(e) => setUserAmount(e ? parseInt(e) : undefined)} onChange={(e) => setUserAmount(e ? parseInt(e) : undefined)}
placeholder="Enter number of users" placeholder="Enter number of users"
defaultValue={userAmount} defaultValue={userAmount}
disabled={disabled} disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))}
/> />
<Input <Input
label="Monthly Duration" label="Monthly Duration"
@@ -266,7 +267,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
onChange={(e) => setMonthlyDuration(e ? parseInt(e) : undefined)} onChange={(e) => setMonthlyDuration(e ? parseInt(e) : undefined)}
placeholder="Enter monthly duration" placeholder="Enter monthly duration"
defaultValue={monthlyDuration} defaultValue={monthlyDuration}
disabled={disabled} disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))}
/> />
<div className="flex flex-col gap-3 w-full lg:col-span-3"> <div className="flex flex-col gap-3 w-full lg:col-span-3">
<label className="font-normal text-base text-mti-gray-dim">Pricing</label> <label className="font-normal text-base text-mti-gray-dim">Pricing</label>
@@ -277,7 +278,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
type="number" type="number"
defaultValue={paymentValue || 0} defaultValue={paymentValue || 0}
className="col-span-3" className="col-span-3"
disabled={disabled} disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))}
/> />
<Select <Select
className={clsx( className={clsx(
@@ -305,7 +306,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
color: state.isFocused ? "black" : styles.color, color: state.isFocused ? "black" : styles.color,
}), }),
}} }}
isDisabled={disabled} isDisabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))}
/> />
</div> </div>
</div> </div>
@@ -417,6 +418,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
</div> </div>
{user.type === "student" && ( {user.type === "student" && (
<div className="flex flex-col md:flex-row gap-8 w-full">
<Input <Input
type="text" type="text"
name="passport_id" name="passport_id"
@@ -427,6 +429,16 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
disabled disabled
required required
/> />
<Input
type="text"
name="studentID"
label="Student ID"
onChange={setStudentID}
placeholder="Enter Student ID"
disabled={!checkAccess(loggedInUser, getTypesOfUser(["teacher", "agent", "student"]), permissions, "editStudent")}
value={studentID}
/>
</div>
)} )}
<div className="flex flex-col md:flex-row gap-8 w-full"> <div className="flex flex-col md:flex-row gap-8 w-full">

View File

@@ -1,4 +1,4 @@
import { Type } from "@/interfaces/user"; import {Type} from "@/interfaces/user";
export const PERMISSIONS = { export const PERMISSIONS = {
generateCode: { generateCode: {
@@ -45,16 +45,16 @@ export const PERMISSIONS = {
updateUser: { updateUser: {
student: { student: {
perm: "editStudent", perm: "editStudent",
list: ["developer", "admin"], list: ["developer", "admin", "corporate", "mastercorporate", "teacher"],
}, },
teacher: { teacher: {
perm: "editTeacher", perm: "editTeacher",
list: ["developer", "admin"], list: ["developer", "admin", "corporate", "mastercorporate"],
}, },
corporate: { corporate: {
perm: "editCorporate", perm: "editCorporate",
list: ["admin", "developer"], list: ["developer", "admin", "mastercorporate"],
}, },
mastercorporate: { mastercorporate: {
perm: undefined, perm: undefined,

View File

@@ -0,0 +1,235 @@
/* eslint-disable jsx-a11y/alt-text */
import React from "react";
import { Document, Page, View, Text, Image } from "@react-pdf/renderer";
import { ModuleScore } from "@/interfaces/module.scores";
import { styles } from "./styles";
import TestReportFooter from "./test.report.footer";
import { StyleSheet } from "@react-pdf/renderer";
const customStyles = StyleSheet.create({
testDetails: {
display: "flex",
gap: 4,
},
testDetailsContainer: {
display: "flex",
gap: 16,
},
table: {
width: "100%",
},
tableRow: {
flexDirection: "row",
},
tableCol70: {
width: "70%", // First column width (50%)
borderStyle: "solid",
borderWidth: 1,
borderColor: "#000",
// padding: 5,
},
tableCol25: {
width: "16.67%", // Remaining four columns each get 1/6 of the total width (50% / 3 = 16.67%)
borderStyle: "solid",
borderWidth: 1,
borderColor: "#000",
padding: 5,
},
tableCol20: {
width: "20%", // Width for each of the 5 sub-columns (50% / 5 = 20%)
borderStyle: "solid",
borderWidth: 1,
borderColor: "#000",
padding: 5,
},
tableCol10: {
width: "10%", // Width for each of the 5 sub-columns (50% / 5 = 20%)
borderStyle: "solid",
borderWidth: 1,
borderColor: "#000",
padding: 5,
},
tableCellHeader: {
fontSize: 12,
textAlign: "center",
},
tableCellHeaderColor: {
backgroundColor: "#d3d3d3",
},
tableCell: {
fontSize: 10,
textAlign: "center",
},
});
interface Props {
date: string;
name: string;
email: string;
id: string;
gender?: string;
passportId: string;
corporateName: string;
downloadDate: string;
userId: string;
uniqueExercises: { name: string; result: string }[];
timeSpent: string;
score: string;
}
const LevelTestReport = ({
date,
name,
email,
id,
gender,
passportId,
corporateName,
downloadDate,
userId,
uniqueExercises,
timeSpent,
score,
}: Props) => {
const defaultTextStyle = [styles.textFont, { fontSize: 8 }];
return (
<Document>
<Page style={styles.body} orientation="landscape">
<Text style={[styles.textFont, styles.textBold, { fontSize: 11 }]}>
Corporate Name: {corporateName}
</Text>
<View style={styles.textMargin}>
<Text style={defaultTextStyle}>
Report Download date: {downloadDate}
</Text>
</View>
<Text style={[styles.textFont, styles.textBold, { fontSize: 11 }]}>
Test Information: {id}
</Text>
<View style={styles.textMargin}>
<Text style={defaultTextStyle}>Date of Test: {date}</Text>
<Text style={defaultTextStyle}>Candidates name: {name}</Text>
<Text style={defaultTextStyle}>Email: {email}</Text>
<Text style={defaultTextStyle}>National ID: {passportId}</Text>
<Text style={defaultTextStyle}>Gender: {gender}</Text>
<Text style={defaultTextStyle}>Candidate ID: {userId}</Text>
</View>
<View style={customStyles.table}>
{/* Header Row */}
<View style={customStyles.tableRow}>
<View
style={[
customStyles.tableCol70,
customStyles.tableCellHeaderColor,
]}
>
<Text style={[customStyles.tableCellHeader, { padding: 5 }]}>
Test sections
</Text>
</View>
<View
style={[
customStyles.tableCol20,
customStyles.tableCellHeaderColor,
]}
>
<Text style={customStyles.tableCellHeader}>Time spent</Text>
</View>
<View
style={[
customStyles.tableCol10,
customStyles.tableCellHeaderColor,
]}
>
<Text style={customStyles.tableCellHeader}>Score</Text>
</View>
</View>
<View style={customStyles.tableRow}>
<View style={customStyles.tableCol70}>
<View style={customStyles.tableRow}>
{uniqueExercises.map((exercise, index) => (
<View
style={[
customStyles.tableCol20,
index !== uniqueExercises.length - 1
? { borderWidth: 0, borderRightWidth: 1 }
: { borderWidth: 0 },
]}
key={index}
>
<Text style={customStyles.tableCell}>Part {index + 1}</Text>
</View>
))}
</View>
</View>
<View style={customStyles.tableCol20}>
<Text style={customStyles.tableCell}></Text>
</View>
<View style={customStyles.tableCol10}>
<Text style={customStyles.tableCell}></Text>
</View>
</View>
<View style={customStyles.tableRow}>
<View style={customStyles.tableCol70}>
<View style={customStyles.tableRow}>
{uniqueExercises.map((exercise, index) => (
<View
style={[
customStyles.tableCol20,
index !== uniqueExercises.length - 1
? { borderWidth: 0, borderRightWidth: 1 }
: { borderWidth: 0 },
]}
key={index}
>
<Text style={customStyles.tableCell}>{exercise.name}</Text>
</View>
))}
</View>
</View>
<View style={customStyles.tableCol20}>
<Text style={customStyles.tableCell}></Text>
</View>
<View style={customStyles.tableCol10}>
<Text style={customStyles.tableCell}></Text>
</View>
</View>
<View style={customStyles.tableRow}>
<View style={customStyles.tableCol70}>
<View style={customStyles.tableRow}>
{uniqueExercises.map((exercise, index) => (
<View
style={[
customStyles.tableCol20,
index !== uniqueExercises.length - 1
? { borderWidth: 0, borderRightWidth: 1 }
: { borderWidth: 0 },
]}
key={index}
>
<Text style={customStyles.tableCell}>
{exercise.result}
</Text>
</View>
))}
</View>
</View>
<View style={customStyles.tableCol20}>
<Text style={customStyles.tableCell}>{timeSpent}</Text>
</View>
<View style={customStyles.tableCol10}>
<Text style={customStyles.tableCell}>{score}</Text>
</View>
</View>
</View>
<TestReportFooter userId={userId} />
</Page>
</Document>
);
};
export default LevelTestReport;

View File

@@ -26,6 +26,7 @@ export interface BasicUser {
export interface StudentUser extends BasicUser { export interface StudentUser extends BasicUser {
type: "student"; type: "student";
studentID?: string;
preferredGender?: InstructorGender; preferredGender?: InstructorGender;
demographicInformation?: DemographicInformation; demographicInformation?: DemographicInformation;
preferredTopics?: string[]; preferredTopics?: string[];
@@ -136,6 +137,10 @@ export interface Stat {
}; };
isDisabled?: boolean; isDisabled?: boolean;
shuffleMaps?: ShuffleMap[]; shuffleMaps?: ShuffleMap[];
pdf?: {
path: string;
version: string;
};
} }
export interface Group { export interface Group {

View File

@@ -19,6 +19,7 @@ import {BsFileEarmarkEaselFill, BsQuestionCircleFill} from "react-icons/bs";
import {checkAccess, getTypesOfUser} from "@/utils/permissions"; import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import {PermissionType} from "@/interfaces/permissions"; import {PermissionType} from "@/interfaces/permissions";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
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 USER_TYPE_PERMISSIONS: { const USER_TYPE_PERMISSIONS: {
@@ -34,7 +35,7 @@ const USER_TYPE_PERMISSIONS: {
}, },
agent: { agent: {
perm: "createCodeCountryManager", perm: "createCodeCountryManager",
list: [], list: ["student", "teacher", "corporate", "mastercorporate"],
}, },
corporate: { corporate: {
perm: "createCodeCorporate", perm: "createCodeCorporate",
@@ -85,7 +86,7 @@ export default function BatchCodeGenerator({user}: {user: User}) {
const information = uniqBy( const information = uniqBy(
rows rows
.map((row) => { .map((row) => {
const [firstName, lastName, country, passport_id, email, ...phone] = row as string[]; const [firstName, lastName, country, passport_id, email, phone] = row as string[];
return EMAIL_REGEX.test(email.toString().trim()) return EMAIL_REGEX.test(email.toString().trim())
? { ? {
email: email.toString().trim().toLowerCase(), email: email.toString().trim().toLowerCase(),

View File

@@ -11,10 +11,13 @@ import Modal from "@/components/Modal";
import {BsQuestionCircleFill} from "react-icons/bs"; import {BsQuestionCircleFill} from "react-icons/bs";
import {PermissionType} from "@/interfaces/permissions"; import {PermissionType} from "@/interfaces/permissions";
import moment from "moment"; import moment from "moment";
import {checkAccess} from "@/utils/permissions"; import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import Checkbox from "@/components/Low/Checkbox"; import Checkbox from "@/components/Low/Checkbox";
import ReactDatePicker from "react-datepicker"; import ReactDatePicker from "react-datepicker";
import clsx from "clsx"; import clsx from "clsx";
import usePermissions from "@/hooks/usePermissions";
import countryCodes from "country-codes-list";
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]+)*$/);
type Type = Exclude<UserType, "admin" | "developer" | "agent" | "mastercorporate">; type Type = Exclude<UserType, "admin" | "developer" | "agent" | "mastercorporate">;
@@ -26,7 +29,7 @@ const USER_TYPE_LABELS: {[key in Type]: string} = {
}; };
const USER_TYPE_PERMISSIONS: { const USER_TYPE_PERMISSIONS: {
[key in Type]: {perm: PermissionType | undefined; list: Type[]}; [key in UserType]: {perm: PermissionType | undefined; list: UserType[]};
} = { } = {
student: { student: {
perm: "createCodeStudent", perm: "createCodeStudent",
@@ -36,10 +39,26 @@ const USER_TYPE_PERMISSIONS: {
perm: "createCodeTeacher", perm: "createCodeTeacher",
list: [], list: [],
}, },
agent: {
perm: "createCodeCountryManager",
list: ["student", "teacher", "corporate", "mastercorporate"],
},
corporate: { corporate: {
perm: "createCodeCorporate", perm: "createCodeCorporate",
list: ["student", "teacher"], list: ["student", "teacher"],
}, },
mastercorporate: {
perm: undefined,
list: ["student", "teacher", "corporate"],
},
admin: {
perm: "createCodeAdmin",
list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"],
},
developer: {
perm: undefined,
list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"],
},
}; };
export default function BatchCreateUser({user}: {user: User}) { export default function BatchCreateUser({user}: {user: User}) {
@@ -65,6 +84,7 @@ export default function BatchCreateUser({user}: {user: User}) {
const [showHelp, setShowHelp] = useState(false); const [showHelp, setShowHelp] = useState(false);
const {users} = useUsers(); const {users} = useUsers();
const {permissions} = usePermissions(user?.id || "");
const {openFilePicker, filesContent, clear} = useFilePicker({ const {openFilePicker, filesContent, clear} = useFilePicker({
accept: ".xlsx", accept: ".xlsx",
@@ -84,7 +104,11 @@ export default function BatchCreateUser({user}: {user: User}) {
const information = uniqBy( const information = uniqBy(
rows rows
.map((row) => { .map((row) => {
const [firstName, lastName, country, passport_id, email, phone, group] = row as string[]; const [firstName, lastName, country, passport_id, email, phone, group, studentID] = row as string[];
const countryItem =
countryCodes.findOne("countryCode" as any, country.toUpperCase()) ||
countryCodes.all().find((x) => x.countryNameEn.toLowerCase() === country.toLowerCase());
return EMAIL_REGEX.test(email.toString().trim()) return EMAIL_REGEX.test(email.toString().trim())
? { ? {
email: email.toString().trim().toLowerCase(), email: email.toString().trim().toLowerCase(),
@@ -92,8 +116,9 @@ export default function BatchCreateUser({user}: {user: User}) {
type: type, type: type,
passport_id: passport_id?.toString().trim() || undefined, passport_id: passport_id?.toString().trim() || undefined,
groupName: group, groupName: group,
studentID,
demographicInformation: { demographicInformation: {
country: country, country: countryItem?.countryCode,
passport_id: passport_id?.toString().trim() || undefined, passport_id: passport_id?.toString().trim() || undefined,
phone, phone,
}, },
@@ -158,6 +183,7 @@ export default function BatchCreateUser({user}: {user: User}) {
<th className="border border-neutral-200 px-2 py-1">E-mail</th> <th className="border border-neutral-200 px-2 py-1">E-mail</th>
<th className="border border-neutral-200 px-2 py-1">Phone Number</th> <th className="border border-neutral-200 px-2 py-1">Phone Number</th>
<th className="border border-neutral-200 px-2 py-1">Group Name</th> <th className="border border-neutral-200 px-2 py-1">Group Name</th>
<th className="border border-neutral-200 px-2 py-1">Student ID</th>
</tr> </tr>
</thead> </thead>
</table> </table>
@@ -214,7 +240,13 @@ export default function BatchCreateUser({user}: {user: User}) {
defaultValue="student" defaultValue="student"
onChange={(e) => setType(e.target.value as Type)} onChange={(e) => setType(e.target.value as Type)}
className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none"> className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none">
{Object.keys(USER_TYPE_LABELS).map((type) => ( {Object.keys(USER_TYPE_LABELS)
.filter((x) => {
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
// if (x === "corporate") console.log(list, perm, checkAccess(user, list, permissions, perm));
return checkAccess(user, getTypesOfUser(list), permissions, perm);
})
.map((type) => (
<option key={type} value={type}> <option key={type} value={type}>
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]} {USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
</option> </option>

View File

@@ -11,7 +11,7 @@ import {useEffect, useState} from "react";
import ReactDatePicker from "react-datepicker"; import ReactDatePicker from "react-datepicker";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import {checkAccess} from "@/utils/permissions"; import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import {PermissionType} from "@/interfaces/permissions"; import {PermissionType} from "@/interfaces/permissions";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
@@ -28,7 +28,7 @@ const USER_TYPE_PERMISSIONS: {
}, },
agent: { agent: {
perm: "createCodeCountryManager", perm: "createCodeCountryManager",
list: [], list: ["student", "teacher", "corporate", "mastercorporate"],
}, },
corporate: { corporate: {
perm: "createCodeCorporate", perm: "createCodeCorporate",
@@ -103,7 +103,7 @@ export default function CodeGenerator({user}: {user: User}) {
{Object.keys(USER_TYPE_LABELS) {Object.keys(USER_TYPE_LABELS)
.filter((x) => { .filter((x) => {
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type]; const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
return checkAccess(user, list, permissions, perm); return checkAccess(user, getTypesOfUser(list), permissions, perm);
}) })
.map((type) => ( .map((type) => (
<option key={type} value={type}> <option key={type} value={type}>

View File

@@ -391,6 +391,15 @@ export default function UserList({
) as any, ) as any,
cell: (info) => USER_TYPE_LABELS[info.getValue()], cell: (info) => USER_TYPE_LABELS[info.getValue()],
}), }),
columnHelper.accessor("studentID", {
header: (
<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",
}),
columnHelper.accessor("corporateInformation.companyInformation.name", { columnHelper.accessor("corporateInformation.companyInformation.name", {
header: ( header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}> <button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}>
@@ -465,6 +474,11 @@ export default function UserList({
? userTypes.findIndex((t) => a.type === t) - userTypes.findIndex((t) => b.type === t) ? userTypes.findIndex((t) => a.type === t) - userTypes.findIndex((t) => b.type === t)
: userTypes.findIndex((t) => b.type === t) - userTypes.findIndex((t) => a.type === t); : 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")) if (sorter === "verification" || sorter === reverseString("verification"))
return sorter === "verification" return sorter === "verification"
? a.isVerified.toString().localeCompare(b.isVerified.toString()) ? a.isVerified.toString().localeCompare(b.isVerified.toString())

View File

@@ -1,22 +1,40 @@
import type {NextApiRequest, NextApiResponse} from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import {app, storage} from "@/firebase"; import { app, storage } from "@/firebase";
import {getFirestore, doc, getDoc, updateDoc, getDocs, query, collection, where} from "firebase/firestore"; import {
import {withIronSessionApiRoute} from "iron-session/next"; getFirestore,
import {sessionOptions} from "@/lib/session"; doc,
getDoc,
updateDoc,
getDocs,
query,
collection,
where,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import ReactPDF from "@react-pdf/renderer"; import ReactPDF from "@react-pdf/renderer";
import TestReport from "@/exams/pdf/test.report"; import TestReport from "@/exams/pdf/test.report";
import {ref, uploadBytes, getDownloadURL} from "firebase/storage"; import LevelTestReport from "@/exams/pdf/level.test.report";
import {DemographicInformation, User} from "@/interfaces/user"; import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import {Module} from "@/interfaces"; import {
import {ModuleScore} from "@/interfaces/module.scores"; DemographicInformation,
import {SkillExamDetails} from "@/exams/pdf/details/skill.exam"; Stat,
import {LevelExamDetails} from "@/exams/pdf/details/level.exam"; StudentUser,
import {calculateBandScore} from "@/utils/score"; User,
} from "@/interfaces/user";
import { Module } from "@/interfaces";
import { ModuleScore } from "@/interfaces/module.scores";
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
import { calculateBandScore } from "@/utils/score";
import axios from "axios"; import axios from "axios";
import {moduleLabels} from "@/utils/moduleUtils"; import { moduleLabels } from "@/utils/moduleUtils";
import {generateQRCode, getRadialProgressPNG, streamToBuffer} from "@/utils/pdf"; import {
generateQRCode,
getRadialProgressPNG,
streamToBuffer,
} from "@/utils/pdf";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { getCorporateNameForStudent } from "@/utils/groups.be";
const db = getFirestore(app); const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions); export default withIronSessionApiRoute(handler, sessionOptions);
@@ -85,19 +103,21 @@ interface SkillsFeedbackResponse extends SkillsFeedbackRequest {
const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => { const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => {
const backendRequest = await axios.post( const backendRequest = await axios.post(
`${process.env.BACKEND_URL}/grading_summary`, `${process.env.BACKEND_URL}/grading_summary`,
{sections}, { sections },
{ {
headers: { headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`, Authorization: `Bearer ${process.env.BACKEND_JWT}`,
}, },
}, }
); );
return backendRequest.data?.sections; return backendRequest.data?.sections;
}; };
// perform the request with several retries if needed // perform the request with several retries if needed
const handleSkillsFeedbackRequest = async (sections: SkillsFeedbackRequest[]): Promise<SkillsFeedbackResponse[] | null> => { const handleSkillsFeedbackRequest = async (
sections: SkillsFeedbackRequest[]
): Promise<SkillsFeedbackResponse[] | null> => {
let i = 0; let i = 0;
try { try {
const data = await getSkillsFeedback(sections); const data = await getSkillsFeedback(sections);
@@ -112,59 +132,24 @@ const handleSkillsFeedbackRequest = async (sections: SkillsFeedbackRequest[]): P
} }
}; };
async function post(req: NextApiRequest, res: NextApiResponse) { async function getDefaultPDFStream(
// verify if it's a logged user that is trying to export stats: Stat[],
if (req.session.user) { user: User,
const {id} = req.query as {id: string}; qrcodeUrl: string
// fetch stats entries for this particular user with the requested exam session ) {
const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id))); const [stat] = stats;
if (docsSnap.empty) {
res.status(400).end();
return;
}
const stats = docsSnap.docs.map((d) => d.data());
// verify if the stats already have a pdf generated
const hasPDF = stats.find((s) => s.pdf?.path && s.pdf?.version === process.env.PDF_VERSION);
// find the user that generated the stats
const statIndex = stats.findIndex((s) => s.user);
if(statIndex === -1) {
res.status(401).json({ok: false});
return;
}
const userId = stats[statIndex].user;
if (hasPDF) {
// if it does, return the pdf url
const fileRef = ref(storage, hasPDF.pdf.path);
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
try {
// generate the pdf report
const docUser = await getDoc(doc(db, "users", userId));
if (docUser.exists()) {
// we'll need the user in order to get the user data (name, email, focus, etc);
const user = docUser.data() as User;
// generate the QR code for the report // generate the QR code for the report
const qrcode = await generateQRCode((req.headers.origin || "") + req.url); const qrcode = await generateQRCode(qrcodeUrl);
if (!qrcode) { if (!qrcode) {
res.status(500).json({ok: false}); throw new Error("Failed to generate QR code");
return;
} }
// stats may contain multiple exams of the same type so we need to aggregate them // stats may contain multiple exams of the same type so we need to aggregate them
const results = ( const results = stats
stats.reduce((accm: ModuleScore[], {module, score}) => { .reduce((accm: ModuleScore[], stat: Stat) => {
const { module, score } = stat;
const fixedModuleStr = module[0].toUpperCase() + module.substring(1); const fixedModuleStr = module[0].toUpperCase() + module.substring(1);
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) { if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
return accm.map((e: ModuleScore) => { return accm.map((e: ModuleScore) => {
@@ -180,20 +165,24 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}); });
} }
return [ const value = {
...accm,
{
module: fixedModuleStr, module: fixedModuleStr,
score: score.correct, score: score.correct,
total: score.total, total: score.total,
code: module, code: module,
}, } as ModuleScore;
];
}, []) as ModuleScore[] return [...accm, value];
).map((moduleScore) => { }, [])
const {score, total} = moduleScore; .map((moduleScore: ModuleScore) => {
const { score, total } = moduleScore;
// with all the scores aggreated we can calculate the band score for each module // with all the scores aggreated we can calculate the band score for each module
const bandScore = calculateBandScore(score, total, moduleScore.code as Module, user.focus); const bandScore = calculateBandScore(
score,
total,
moduleScore.code as Module,
user.focus
);
return { return {
...moduleScore, ...moduleScore,
@@ -205,21 +194,22 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
// get the skills feedback from the backend based on the module grade // get the skills feedback from the backend based on the module grade
const skillsFeedback = (await handleSkillsFeedbackRequest( const skillsFeedback = (await handleSkillsFeedbackRequest(
results.map(({code, bandScore}) => ({ results.map(({ code, bandScore }) => ({
code, code,
name: moduleLabels[code], name: moduleLabels[code],
grade: bandScore, grade: bandScore,
})), }))
)) as SkillsFeedbackResponse[]; )) as SkillsFeedbackResponse[];
if (!skillsFeedback) { if (!skillsFeedback) {
res.status(500).json({ok: false}); throw new Error("Failed to get skills feedback");
return;
} }
// assign the feedback to the results // assign the feedback to the results
const finalResults = results.map((result) => { const finalResults = results.map((result) => {
const feedback = skillsFeedback.find((f: SkillsFeedbackResponse) => f.code === result.code); const feedback = skillsFeedback.find(
(f: SkillsFeedbackResponse) => f.code === result.code
);
if (feedback) { if (feedback) {
return { return {
@@ -234,11 +224,15 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}); });
// calculate the overall score out of all the aggregated results // calculate the overall score out of all the aggregated results
const overallScore = results.reduce((accm, {score}) => accm + score, 0); const overallScore = results.reduce((accm, { score }) => accm + score, 0);
const overallTotal = results.reduce((accm, {total}) => accm + total, 0); const overallTotal = results.reduce((accm, { total }) => accm + total, 0);
const overallResult = overallScore / overallTotal; const overallResult = overallScore / overallTotal;
const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal); const overallPNG = getRadialProgressPNG(
"laranja",
overallScore,
overallTotal
);
// generate the overall detail report // generate the overall detail report
const overallDetail = { const overallDetail = {
@@ -249,30 +243,15 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
} as ModuleScore; } as ModuleScore;
const testDetails = [overallDetail, ...finalResults]; const testDetails = [overallDetail, ...finalResults];
const [stat] = stats;
// generate the performance summary based on the overall result // generate the performance summary based on the overall result
const performanceSummary = getPerformanceSummary(stat.module, overallResult); const performanceSummary = getPerformanceSummary(stat.module, overallResult);
// level exams have a different report structure than the skill exams const title = "ENGLISH SKILLS TEST RESULT REPORT";
const getCustomData = () => { const details = <SkillExamDetails testDetails={testDetails} />;
if (stat.module === "level") {
return {
title: "ENGLISH LEVEL TEST RESULT REPORT ",
details: <LevelExamDetails detail={overallDetail} title="Level as per CEFR Levels" />,
};
}
return { const demographicInformation =
title: "ENGLISH SKILLS TEST RESULT REPORT", user.demographicInformation as DemographicInformation;
details: <SkillExamDetails testDetails={testDetails} />, return ReactPDF.renderToStream(
};
};
const {title, details} = getCustomData();
const demographicInformation = user.demographicInformation as DemographicInformation;
const pdfStream = await ReactPDF.renderToStream(
<TestReport <TestReport
title={title} title={title}
date={moment(stat.date) date={moment(stat.date)
@@ -280,7 +259,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
.format("ll HH:mm:ss")} .format("ll HH:mm:ss")}
name={user.name} name={user.name}
email={user.email} email={user.email}
id={userId} id={user.id}
gender={demographicInformation?.gender} gender={demographicInformation?.gender}
summary={performanceSummary} summary={performanceSummary}
testDetails={testDetails} testDetails={testDetails}
@@ -290,9 +269,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
summaryPNG={overallPNG} summaryPNG={overallPNG}
summaryScore={`${Math.floor(overallResult * 100)}%`} summaryScore={`${Math.floor(overallResult * 100)}%`}
passportId={demographicInformation?.passport_id || ""} passportId={demographicInformation?.passport_id || ""}
/>, />
); );
}
async function getPdfUrl(pdfStream: any, docsSnap: any) {
// generate the file ref for storage // generate the file ref for storage
const fileName = `${Date.now().toString()}.pdf`; const fileName = `${Date.now().toString()}.pdf`;
const refName = `exam_report/${fileName}`; const refName = `exam_report/${fileName}`;
@@ -305,7 +286,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}); });
// update the stats entries with the pdf url to prevent duplication // update the stats entries with the pdf url to prevent duplication
docsSnap.docs.forEach(async (doc) => { docsSnap.docs.forEach(async (doc: any) => {
await updateDoc(doc.ref, { await updateDoc(doc.ref, {
pdf: { pdf: {
path: refName, path: refName,
@@ -313,26 +294,126 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}, },
}); });
}); });
return getDownloadURL(fileRef);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to export
if (req.session.user) {
const { id } = req.query as { id: string };
// fetch stats entries for this particular user with the requested exam session
const docsSnap = await getDocs(
query(collection(db, "stats"), where("session", "==", id))
);
if (docsSnap.empty) {
res.status(400).end();
return;
}
const stats = docsSnap.docs.map((d) => d.data()) as Stat[];
// verify if the stats already have a pdf generated
const hasPDF = stats.find(
(s) => s.pdf?.path && s.pdf?.version === process.env.PDF_VERSION
);
// find the user that generated the stats
const statIndex = stats.findIndex((s) => s.user);
if (statIndex === -1) {
res.status(401).json({ ok: false });
return;
}
const userId = stats[statIndex].user;
if (hasPDF) {
// if it does, return the pdf url
const fileRef = ref(storage, hasPDF.pdf!.path);
const url = await getDownloadURL(fileRef); const url = await getDownloadURL(fileRef);
res.status(200).end(url); res.status(200).end(url);
return; return;
} }
res.status(401).json({ok: false}); try {
// generate the pdf report
const docUser = await getDoc(doc(db, "users", userId));
if (docUser.exists()) {
// we'll need the user in order to get the user data (name, email, focus, etc);
const [stat] = stats;
if (stat.module === "level") {
const user = docUser.data() as StudentUser;
const uniqueExercises = stats.map((s) => ({
name: "Gramar & Vocabulary",
result: `${s.score.correct}/${s.score.total}`,
}));
const dates = stats.map((s) => moment(s.date));
const timeSpent = `${
stats.reduce((accm, s: Stat) => accm + (s.timeSpent || 0), 0) / 60
} minutes`;
const score = stats.reduce((accm, s) => accm + s.score.correct, 0);
const corporateName = await getCorporateNameForStudent(userId);
const pdfStream = await ReactPDF.renderToStream(
<LevelTestReport
date={moment.max(dates).format("DD/MM/YYYY")}
name={user.name}
email={user.email}
id={stat.exam}
gender={user.demographicInformation?.gender || ""}
passportId={user.demographicInformation?.passport_id || ""}
corporateName={corporateName}
downloadDate={moment().format("DD/MM/YYYY")}
userId={userId}
uniqueExercises={uniqueExercises}
timeSpent={timeSpent}
score={score.toString()}
/>
);
const url = await getPdfUrl(pdfStream, docsSnap);
res.status(200).end(url);
return;
}
const user = docUser.data() as User;
try {
const pdfStream = await getDefaultPDFStream(
stats,
user,
`${req.headers.origin || ""}${req.url}`
);
const url = await getPdfUrl(pdfStream, docsSnap);
res.status(200).end(url);
return; return;
} catch (err) { } catch (err) {
res.status(500).json({ok: false}); console.error(err);
res.status(500).json({ ok: false });
return; return;
} }
} }
res.status(401).json({ok: false}); res.status(401).json({ ok: false });
return;
} catch (err) {
console.error(err);
res.status(500).json({ ok: false });
return;
}
}
res.status(401).json({ ok: false });
return; return;
} }
async function get(req: NextApiRequest, res: NextApiResponse) { async function get(req: NextApiRequest, res: NextApiResponse) {
const {id} = req.query as {id: string}; const { id } = req.query as { id: string };
const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id))); const docsSnap = await getDocs(
query(collection(db, "stats"), where("session", "==", id))
);
if (docsSnap.empty) { if (docsSnap.empty) {
res.status(404).end(); res.status(404).end();

View File

@@ -1,29 +1,27 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { User } from "@/interfaces/user"; import {User} from "@/interfaces/user";
import { toast, ToastContainer } from "react-toastify"; import {toast, ToastContainer} from "react-toastify";
import axios from "axios"; import axios from "axios";
import { FormEvent, useEffect, useState } from "react"; import {FormEvent, useEffect, useState} from "react";
import Head from "next/head"; import Head from "next/head";
import useUser from "@/hooks/useUser"; import useUser from "@/hooks/useUser";
import { Divider } from "primereact/divider"; import {Divider} from "primereact/divider";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import { BsArrowRepeat, BsCheck } from "react-icons/bs"; import {BsArrowRepeat, BsCheck} from "react-icons/bs";
import Link from "next/link"; import Link from "next/link";
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
import clsx from "clsx"; import clsx from "clsx";
import { useRouter } from "next/router"; import {useRouter} from "next/router";
import EmailVerification from "./(auth)/EmailVerification"; import EmailVerification from "./(auth)/EmailVerification";
import { withIronSessionSsr } from "iron-session/next"; import {withIronSessionSsr} from "iron-session/next";
import { sessionOptions } from "@/lib/session"; import {sessionOptions} from "@/lib/session";
const EMAIL_REGEX = new RegExp( const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/g);
/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/g,
);
export const getServerSideProps = withIronSessionSsr(({ req, res }) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
const envVariables: { [key: string]: string } = {}; const envVariables: {[key: string]: string} = {};
Object.keys(process.env) Object.keys(process.env)
.filter((x) => x.startsWith("NEXT_PUBLIC")) .filter((x) => x.startsWith("NEXT_PUBLIC"))
.forEach((x: string) => { .forEach((x: string) => {
@@ -35,12 +33,12 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
redirect: { redirect: {
destination: "/", destination: "/",
permanent: false, permanent: false,
} },
}; };
} }
return { return {
props: { user: null, envVariables }, props: {user: null, envVariables},
}; };
}, sessionOptions); }, sessionOptions);
@@ -52,7 +50,7 @@ export default function Login() {
const router = useRouter(); const router = useRouter();
const { user, mutateUser } = useUser({ const {user, mutateUser} = useUser({
redirectTo: "/", redirectTo: "/",
redirectIfFound: true, redirectIfFound: true,
}); });
@@ -70,13 +68,10 @@ export default function Login() {
} }
axios axios
.post<{ ok: boolean }>("/api/reset", { email }) .post<{ok: boolean}>("/api/reset", {email})
.then((response) => { .then((response) => {
if (response.data.ok) { if (response.data.ok) {
toast.success( toast.success("You should receive an e-mail to reset your password!", {toastId: "forgot-success"});
"You should receive an e-mail to reset your password!",
{ toastId: "forgot-success" },
);
return; return;
} }
@@ -96,7 +91,7 @@ export default function Login() {
setIsLoading(true); setIsLoading(true);
axios axios
.post<User>("/api/login", { email, password }) .post<User>("/api/login", {email, password})
.then((response) => { .then((response) => {
toast.success("You have been logged in!", { toast.success("You have been logged in!", {
toastId: "login-successful", toastId: "login-successful",
@@ -109,7 +104,7 @@ export default function Login() {
toastId: "wrong-credentials", toastId: "wrong-credentials",
}); });
} else { } else {
toast.error("Something went wrong!", { toastId: "server-error" }); toast.error("Something went wrong!", {toastId: "server-error"});
} }
setIsLoading(false); setIsLoading(false);
}) })
@@ -127,82 +122,45 @@ export default function Login() {
<main className="flex h-[100vh] w-full bg-white text-black"> <main className="flex h-[100vh] w-full bg-white text-black">
<ToastContainer /> <ToastContainer />
<section className="relative hidden h-full w-fit min-w-fit lg:flex"> <section className="relative hidden h-full w-fit min-w-fit lg:flex">
<div className="bg-mti-rose-light absolute z-10 h-full w-full bg-opacity-50" /> {/* <div className="bg-mti-rose-light absolute z-10 h-full w-full bg-opacity-50" /> */}
<img <img src="/red-stock-photo.jpg" alt="People smiling looking at a tablet" className="aspect-auto h-full" />
src="/people-talking-tablet.png"
alt="People smiling looking at a tablet"
className="aspect-auto h-full"
/>
</section> </section>
<section className="flex h-full w-full flex-col items-center justify-center gap-2"> <section className="flex h-full w-full flex-col items-center justify-center gap-2">
<div className={clsx("flex flex-col items-center", !user && "mb-4")}> <div className={clsx("flex flex-col items-center", !user && "mb-4")}>
<img <img src="/logo_title.png" alt="EnCoach's Logo" className="w-36 lg:w-56" />
src="/logo_title.png" <h1 className="text-2xl font-bold lg:text-4xl">Login to your account</h1>
alt="EnCoach's Logo" <p className="text-mti-gray-cool self-start text-sm font-normal lg:text-base">with your registered Email Address</p>
className="w-36 lg:w-56"
/>
<h1 className="text-2xl font-bold lg:text-4xl">
Login to your account
</h1>
<p className="text-mti-gray-cool self-start text-sm font-normal lg:text-base">
with your registered Email Address
</p>
</div> </div>
<Divider className="max-w-xs lg:max-w-md" /> <Divider className="max-w-xs lg:max-w-md" />
{!user && ( {!user && (
<> <>
<form <form className="-lg:px-8 flex w-full flex-col items-center gap-6 lg:w-1/2" onSubmit={login}>
className="-lg:px-8 flex w-full flex-col items-center gap-6 lg:w-1/2" <Input type="email" name="email" onChange={(e) => setEmail(e.toLowerCase())} placeholder="Enter email address" />
onSubmit={login} <Input type="password" name="password" onChange={(e) => setPassword(e)} placeholder="Password" />
>
<Input
type="email"
name="email"
onChange={(e) => setEmail(e.toLowerCase())}
placeholder="Enter email address"
/>
<Input
type="password"
name="password"
onChange={(e) => setPassword(e)}
placeholder="Password"
/>
<div className="flex w-full justify-between px-4"> <div className="flex w-full justify-between px-4">
<div <div
className="text-mti-gray-dim flex cursor-pointer gap-3 text-xs" className="text-mti-gray-dim flex cursor-pointer gap-3 text-xs"
onClick={() => setRememberPassword((prev) => !prev)} onClick={() => setRememberPassword((prev) => !prev)}>
>
<input type="checkbox" className="hidden" /> <input type="checkbox" className="hidden" />
<div <div
className={clsx( className={clsx(
"border-mti-purple-light flex h-4 w-4 items-center justify-center rounded-sm border bg-white", "border-mti-purple-light flex h-4 w-4 items-center justify-center rounded-sm border bg-white",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
rememberPassword && "!bg-mti-purple-light ", rememberPassword && "!bg-mti-purple-light ",
)} )}>
>
<BsCheck color="white" className="h-full w-full" /> <BsCheck color="white" className="h-full w-full" />
</div> </div>
<span>Remember my password</span> <span>Remember my password</span>
</div> </div>
<span <span className="text-mti-purple-light cursor-pointer text-xs hover:underline" onClick={forgotPassword}>
className="text-mti-purple-light cursor-pointer text-xs hover:underline"
onClick={forgotPassword}
>
Forgot Password? Forgot Password?
</span> </span>
</div> </div>
<Button <Button className="mt-8 w-full" color="purple" disabled={isLoading}>
className="mt-8 w-full"
color="purple"
disabled={isLoading}
>
{!isLoading && "Login"} {!isLoading && "Login"}
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<BsArrowRepeat <BsArrowRepeat className="animate-spin text-white" size={25} />
className="animate-spin text-white"
size={25}
/>
</div> </div>
)} )}
</Button> </Button>
@@ -215,13 +173,7 @@ export default function Login() {
</span> </span>
</> </>
)} )}
{user && !user.isVerified && ( {user && !user.isVerified && <EmailVerification user={user} isLoading={isLoading} setIsLoading={setIsLoading} />}
<EmailVerification
user={user}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
)}
</section> </section>
</main> </main>
</> </>

View File

@@ -22,6 +22,7 @@ import {usePDFDownload} from "@/hooks/usePDFDownload";
import useRecordStore from "@/stores/recordStore"; import useRecordStore from "@/stores/recordStore";
import useTrainingContentStore from "@/stores/trainingContentStore"; import useTrainingContentStore from "@/stores/trainingContentStore";
import StatsGridItem from "@/components/StatGridItem"; import StatsGridItem from "@/components/StatGridItem";
import {checkAccess} from "@/utils/permissions";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
@@ -68,10 +69,9 @@ export default function History({user}: {user: User}) {
const {assignments} = useAssignments({}); const {assignments} = useAssignments({});
const {users} = useUsers(); const {users} = useUsers();
const {stats, isLoading: isStatsLoading} = useStats(user?.type === "student" ? user?.id : statsUserId); const {stats, isLoading: isStatsLoading} = useStats(statsUserId || user?.id);
const {groups: allGroups} = useGroups({}); const {groups: allGroups} = useGroups({});
const {groups} = useGroups({admin: user?.id, userType: user?.type});
const groups = allGroups.filter((x) => x.admin === 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);
@@ -82,6 +82,8 @@ export default function History({user}: {user: User}) {
const renderPdfIcon = usePDFDownload("stats"); const renderPdfIcon = usePDFDownload("stats");
const router = useRouter(); const router = useRouter();
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
useEffect(() => { useEffect(() => {
if (stats && !isStatsLoading) { if (stats && !isStatsLoading) {
setGroupedStats( setGroupedStats(
@@ -197,6 +199,7 @@ export default function History({user}: {user: User}) {
const selectableCorporates = [ const selectableCorporates = [
defaultSelectableCorporate, defaultSelectableCorporate,
...users ...users
.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id))
.filter((x) => x.type === "corporate") .filter((x) => x.type === "corporate")
.map((x) => ({ .map((x) => ({
value: x.id, value: x.id,
@@ -208,26 +211,14 @@ export default function History({user}: {user: User}) {
const getUsersList = (): User[] => { const getUsersList = (): User[] => {
if (selectedCorporate) { if (selectedCorporate) {
// get groups for that corporate
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate); const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate);
// get the teacher ids for that group
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants); const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
// // search for groups for these teachers
// const teacherGroups = allGroups.filter((x) => {
// return selectedCorporateGroupsParticipants.includes(x.admin);
// });
// const usersList = [
// ...selectedCorporateGroupsParticipants,
// ...teacherGroups.flatMap((x) => x.participants),
// ];
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[]; const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
return userListWithUsers.filter((x) => x); return userListWithUsers.filter((x) => x);
} }
return users || []; return user.type !== "mastercorporate" ? users : users.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id));
}; };
const corporateFilteredUserList = getUsersList(); const corporateFilteredUserList = getUsersList();
@@ -267,7 +258,7 @@ export default function History({user}: {user: User}) {
<Layout user={user}> <Layout user={user}>
<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">
{(user.type === "developer" || user.type === "admin") && !training && ( {checkAccess(user, ["developer", "admin", "mastercorporate"]) && !training && (
<> <>
<label className="font-normal text-base text-mti-gray-dim">Corporate</label> <label className="font-normal text-base text-mti-gray-dim">Corporate</label>

View File

@@ -55,8 +55,7 @@ export default function Register({code: queryCode}: {code: string}) {
<main className="w-full h-[100vh] flex bg-white text-black"> <main className="w-full h-[100vh] flex bg-white text-black">
<ToastContainer /> <ToastContainer />
<section className="h-full w-fit min-w-fit relative hidden lg:flex"> <section className="h-full w-fit min-w-fit relative hidden lg:flex">
<div className="absolute h-full w-full bg-mti-rose-light z-10 bg-opacity-50" /> <img src="/red-stock-photo.jpg" alt="People smiling looking at a tablet" className="aspect-auto h-full" />
<img src="/people-talking-tablet.png" alt="People smiling looking at a tablet" className="h-full aspect-auto" />
</section> </section>
<section className="h-full w-full flex flex-col items-center justify-center gap-4"> <section className="h-full w-full flex flex-col items-center justify-center gap-4">
<div className={clsx("flex flex-col items-center", !user && "mb-4")}> <div className={clsx("flex flex-col items-center", !user && "mb-4")}>

View File

@@ -62,9 +62,9 @@ export default function Admin() {
<Layout user={user} className="gap-6"> <Layout user={user} className="gap-6">
<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 />
<BatchCreateUser user={user} />
{checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && ( {checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && (
<> <>
<BatchCreateUser user={user} />
<CodeGenerator user={user} /> <CodeGenerator user={user} />
<BatchCodeGenerator user={user} /> <BatchCodeGenerator user={user} />
</> </>

View File

@@ -1,12 +1,29 @@
import {app} from "@/firebase"; import { app } from "@/firebase";
import {CorporateUser, Group, StudentUser, TeacherUser} from "@/interfaces/user"; import {
import {collection, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore"; CorporateUser,
Group,
StudentUser,
TeacherUser,
} from "@/interfaces/user";
import {
collection,
doc,
getDoc,
getDocs,
getFirestore,
query,
setDoc,
where,
} from "firebase/firestore";
import moment from "moment"; import moment from "moment";
import {getUser} from "./users.be"; import { getUser } from "./users.be";
import { getSpecificUsers } from "./users.be";
const db = getFirestore(app); const db = getFirestore(app);
export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => { export const updateExpiryDateOnGroup = async (
participantID: string,
corporateID: string
) => {
const corporateRef = await getDoc(doc(db, "users", corporateID)); const corporateRef = await getDoc(doc(db, "users", corporateID));
const participantRef = await getDoc(doc(db, "users", participantID)); const participantRef = await getDoc(doc(db, "users", participantID));
@@ -16,40 +33,69 @@ export const updateExpiryDateOnGroup = async (participantID: string, corporateID
...corporateRef.data(), ...corporateRef.data(),
id: corporateRef.id, id: corporateRef.id,
} as CorporateUser; } as CorporateUser;
const participant = {...participantRef.data(), id: participantRef.id} as StudentUser | TeacherUser; const participant = { ...participantRef.data(), id: participantRef.id } as
| StudentUser
| TeacherUser;
if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return; if (
corporate.type !== "corporate" ||
(participant.type !== "student" && participant.type !== "teacher")
)
return;
if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate) { if (
return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: null}, {merge: true}); !corporate.subscriptionExpirationDate ||
!participant.subscriptionExpirationDate
) {
return await setDoc(
doc(db, "users", participant.id),
{ subscriptionExpirationDate: null },
{ merge: true }
);
} }
const corporateDate = moment(corporate.subscriptionExpirationDate); const corporateDate = moment(corporate.subscriptionExpirationDate);
const participantDate = moment(participant.subscriptionExpirationDate); const participantDate = moment(participant.subscriptionExpirationDate);
if (corporateDate.isAfter(participantDate)) if (corporateDate.isAfter(participantDate))
return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: corporateDate.toISOString()}, {merge: true}); return await setDoc(
doc(db, "users", participant.id),
{ subscriptionExpirationDate: corporateDate.toISOString() },
{ merge: true }
);
return; return;
}; };
export const getGroups = async () => { export const getGroups = async () => {
const groupDocs = await getDocs(collection(db, "groups")); const groupDocs = await getDocs(collection(db, "groups"));
return groupDocs.docs.map((x) => ({...x.data(), id: x.id})) as Group[]; return groupDocs.docs.map((x) => ({ ...x.data(), id: x.id })) as Group[];
}; };
export const getUserGroups = async (id: string): Promise<Group[]> => { export const getUserGroups = async (id: string): Promise<Group[]> => {
const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id))); const groupDocs = await getDocs(
return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[]; query(collection(db, "groups"), where("admin", "==", id))
);
return groupDocs.docs.map((x) => ({ ...x.data(), id })) as Group[];
}; };
export const getAllAssignersByCorporate = async (corporateID: string): Promise<string[]> => { export const getAllAssignersByCorporate = async (
corporateID: string
): Promise<string[]> => {
const groups = await getUserGroups(corporateID); const groups = await getUserGroups(corporateID);
const groupUsers = (await Promise.all(groups.map(async (g) => await Promise.all(g.participants.map(getUser))))).flat(); const groupUsers = (
await Promise.all(
groups.map(async (g) => await Promise.all(g.participants.map(getUser)))
)
).flat();
const teacherPromises = await Promise.all( const teacherPromises = await Promise.all(
groupUsers.map(async (u) => groupUsers.map(async (u) =>
u.type === "teacher" ? u.id : u.type === "corporate" ? [...(await getAllAssignersByCorporate(u.id)), u.id] : undefined, u.type === "teacher"
), ? u.id
: u.type === "corporate"
? [...(await getAllAssignersByCorporate(u.id)), u.id]
: undefined
)
); );
return teacherPromises.filter((x) => !!x).flat() as string[]; return teacherPromises.filter((x) => !!x).flat() as string[];
@@ -78,9 +124,12 @@ export const getGroupsForUser = async (admin: string, participant: string) => {
console.error(e); console.error(e);
return []; return [];
} }
}; };
export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => { export const getStudentGroupsForUsersWithoutAdmin = async (
admin: string,
participants: string[]
) => {
try { try {
const queryConstraints = [ const queryConstraints = [
...(admin ? [where("admin", "!=", admin)] : []), ...(admin ? [where("admin", "!=", admin)] : []),
@@ -104,4 +153,21 @@ export const getGroupsForUser = async (admin: string, participant: string) => {
console.error(e); console.error(e);
return []; return [];
} }
}; };
export const getCorporateNameForStudent = async (studentID: string) => {
const groups = await getStudentGroupsForUsersWithoutAdmin("", [studentID]);
if (groups.length === 0) return '';
const adminUserIds = [...new Set(groups.map((g) => g.admin))];
const adminUsersData = await getSpecificUsers(adminUserIds);
if(adminUsersData.length === 0) return '';
const admins = adminUsersData.filter((x) => x.type === 'corporate');
if(admins.length > 0) {
return (admins[0] as CorporateUser).corporateInformation.companyInformation.name;
}
return '';
};

View File

@@ -9,12 +9,10 @@ export function checkAccess(user: User, types: Type[], permissions?: PermissionT
// if(user.type === '') { // if(user.type === '') {
if (!user.type) { if (!user.type) {
console.warn("User type is empty");
return false; return false;
} }
if (types.length === 0) { if (types.length === 0) {
console.warn("No types provided");
return false; return false;
} }