Merge branch 'develop' of https://bitbucket.org/ecropdev/ielts-ui into feature/level-file-upload
This commit is contained in:
BIN
public/orange-stock-photo.jpg
Normal file
BIN
public/orange-stock-photo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
BIN
public/red-stock-photo.jpg
Normal file
BIN
public/red-stock-photo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 MiB |
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
235
src/exams/pdf/level.test.report.tsx
Normal file
235
src/exams/pdf/level.test.report.tsx
Normal 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;
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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")}>
|
||||||
|
|||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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 '';
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user