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,16 +418,27 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
</div> </div>
{user.type === "student" && ( {user.type === "student" && (
<Input <div className="flex flex-col md:flex-row gap-8 w-full">
type="text" <Input
name="passport_id" type="text"
label="Passport/National ID" name="passport_id"
onChange={() => null} label="Passport/National ID"
placeholder="Enter National ID or Passport number" onChange={() => null}
value={user.type === "student" ? user.demographicInformation?.passport_id : undefined} placeholder="Enter National ID or Passport number"
disabled value={user.type === "student" ? user.demographicInformation?.passport_id : undefined}
required disabled
/> 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,91 +1,91 @@
import { Type } from "@/interfaces/user"; import {Type} from "@/interfaces/user";
export const PERMISSIONS = { export const PERMISSIONS = {
generateCode: { generateCode: {
student: ["corporate", "developer", "admin", "mastercorporate"], student: ["corporate", "developer", "admin", "mastercorporate"],
teacher: ["corporate", "developer", "admin", "mastercorporate"], teacher: ["corporate", "developer", "admin", "mastercorporate"],
corporate: ["admin", "developer"], corporate: ["admin", "developer"],
mastercorporate: ["admin", "developer"], mastercorporate: ["admin", "developer"],
admin: ["developer", "admin"], admin: ["developer", "admin"],
agent: ["developer", "admin"], agent: ["developer", "admin"],
developer: ["developer"], developer: ["developer"],
}, },
deleteUser: { deleteUser: {
student: { student: {
perm: "deleteStudent", perm: "deleteStudent",
list: ["corporate", "developer", "admin", "mastercorporate"], list: ["corporate", "developer", "admin", "mastercorporate"],
}, },
teacher: { teacher: {
perm: "deleteTeacher", perm: "deleteTeacher",
list: ["corporate", "developer", "admin", "mastercorporate"], list: ["corporate", "developer", "admin", "mastercorporate"],
}, },
corporate: { corporate: {
perm: "deleteCorporate", perm: "deleteCorporate",
list: ["admin", "developer"], list: ["admin", "developer"],
}, },
mastercorporate: { mastercorporate: {
perm: undefined, perm: undefined,
list: ["admin", "developer"], list: ["admin", "developer"],
}, },
admin: { admin: {
perm: "deleteAdmin", perm: "deleteAdmin",
list: ["developer", "admin"], list: ["developer", "admin"],
}, },
agent: { agent: {
perm: "deleteCountryManager", perm: "deleteCountryManager",
list: ["developer", "admin"], list: ["developer", "admin"],
}, },
developer: { developer: {
perm: undefined, perm: undefined,
list: ["developer"], list: ["developer"],
}, },
}, },
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,
list: ["admin", "developer"], list: ["admin", "developer"],
}, },
admin: { admin: {
perm: "editAdmin", perm: "editAdmin",
list: ["developer", "admin"], list: ["developer", "admin"],
}, },
agent: { agent: {
perm: "editCountryManager", perm: "editCountryManager",
list: ["developer", "admin"], list: ["developer", "admin"],
}, },
developer: { developer: {
perm: undefined, perm: undefined,
list: ["developer"], list: ["developer"],
}, },
}, },
updateExpiryDate: { updateExpiryDate: {
student: ["developer", "admin"], student: ["developer", "admin"],
teacher: ["developer", "admin"], teacher: ["developer", "admin"],
corporate: ["admin", "developer"], corporate: ["admin", "developer"],
mastercorporate: ["admin", "developer"], mastercorporate: ["admin", "developer"],
admin: ["developer", "admin"], admin: ["developer", "admin"],
agent: ["developer", "admin"], agent: ["developer", "admin"],
developer: ["developer"], developer: ["developer"],
}, },
examManagement: { examManagement: {
delete: ["developer", "admin"], delete: ["developer", "admin"],
}, },
}; };

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[];
@@ -135,7 +136,11 @@ export interface Stat {
missing: number; missing: number;
}; };
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,11 +240,17 @@ 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)
<option key={type} value={type}> .filter((x) => {
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]} const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
</option> // 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}>
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
</option>
))}
</select> </select>
)} )}
<Button className="my-auto" onClick={makeUsers} disabled={infos.length === 0}> <Button className="my-auto" onClick={makeUsers} disabled={infos.length === 0}>

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,353 +1,434 @@
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);
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res); if (req.method === "GET") return get(req, res);
if (req.method === "POST") return post(req, res); if (req.method === "POST") return post(req, res);
} }
const getExamSummary = (score: number) => { const getExamSummary = (score: number) => {
if (score > 0.8) { if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language."; return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language.";
} }
if (score > 0.6) { if (score > 0.6) {
return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery."; return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery.";
} }
if (score > 0.4) { if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended."; return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended.";
} }
if (score > 0.2) { if (score > 0.2) {
return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency."; return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency.";
} }
return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional support in each area can contribute to substantial progress."; return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional support in each area can contribute to substantial progress.";
}; };
const getLevelSummary = (score: number) => { const getLevelSummary = (score: number) => {
if (score > 0.8) { if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability."; return "Scoring between 81% and 100% on the English exam showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability.";
} }
if (score > 0.6) { if (score > 0.6) {
return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies."; return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies.";
} }
if (score > 0.4) { if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies."; return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies.";
} }
if (score > 0.2) { if (score > 0.2) {
return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency."; return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency.";
} }
return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments."; return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments.";
}; };
const getPerformanceSummary = (module: Module, score: number) => { const getPerformanceSummary = (module: Module, score: number) => {
if (module === "level") return getLevelSummary(score); if (module === "level") return getLevelSummary(score);
return getExamSummary(score); return getExamSummary(score);
}; };
interface SkillsFeedbackRequest { interface SkillsFeedbackRequest {
code: Module; code: Module;
name: string; name: string;
grade: number; grade: number;
} }
interface SkillsFeedbackResponse extends SkillsFeedbackRequest { interface SkillsFeedbackResponse extends SkillsFeedbackRequest {
evaluation: string; evaluation: string;
suggestions: string; suggestions: string;
bullet_points?: string[]; bullet_points?: string[];
} }
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 (
let i = 0; sections: SkillsFeedbackRequest[]
try { ): Promise<SkillsFeedbackResponse[] | null> => {
const data = await getSkillsFeedback(sections); let i = 0;
return data; try {
} catch (err) { const data = await getSkillsFeedback(sections);
if (i < 3) { return data;
i++; } catch (err) {
return handleSkillsFeedbackRequest(sections); if (i < 3) {
} i++;
return handleSkillsFeedbackRequest(sections);
}
return null; return null;
} }
}; };
async function getDefaultPDFStream(
stats: Stat[],
user: User,
qrcodeUrl: string
) {
const [stat] = stats;
// generate the QR code for the report
const qrcode = await generateQRCode(qrcodeUrl);
if (!qrcode) {
throw new Error("Failed to generate QR code");
}
// stats may contain multiple exams of the same type so we need to aggregate them
const results = stats
.reduce((accm: ModuleScore[], stat: Stat) => {
const { module, score } = stat;
const fixedModuleStr = module[0].toUpperCase() + module.substring(1);
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
return accm.map((e: ModuleScore) => {
if (e.module === fixedModuleStr) {
return {
...e,
score: e.score + score.correct,
total: e.total + score.total,
};
}
return e;
});
}
const value = {
module: fixedModuleStr,
score: score.correct,
total: score.total,
code: module,
} as ModuleScore;
return [...accm, value];
}, [])
.map((moduleScore: ModuleScore) => {
const { score, total } = moduleScore;
// 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
);
return {
...moduleScore,
// generate the closest radial progress png for the score
png: getRadialProgressPNG("azul", score, total),
bandScore,
};
});
// get the skills feedback from the backend based on the module grade
const skillsFeedback = (await handleSkillsFeedbackRequest(
results.map(({ code, bandScore }) => ({
code,
name: moduleLabels[code],
grade: bandScore,
}))
)) as SkillsFeedbackResponse[];
if (!skillsFeedback) {
throw new Error("Failed to get skills feedback");
}
// assign the feedback to the results
const finalResults = results.map((result) => {
const feedback = skillsFeedback.find(
(f: SkillsFeedbackResponse) => f.code === result.code
);
if (feedback) {
return {
...result,
evaluation: feedback?.evaluation,
suggestions: feedback?.suggestions,
bullet_points: feedback?.bullet_points,
};
}
return result;
});
// calculate the overall score out of all the aggregated results
const overallScore = results.reduce((accm, { score }) => accm + score, 0);
const overallTotal = results.reduce((accm, { total }) => accm + total, 0);
const overallResult = overallScore / overallTotal;
const overallPNG = getRadialProgressPNG(
"laranja",
overallScore,
overallTotal
);
// generate the overall detail report
const overallDetail = {
module: "Overall",
score: overallScore,
total: overallTotal,
png: overallPNG,
} as ModuleScore;
const testDetails = [overallDetail, ...finalResults];
// generate the performance summary based on the overall result
const performanceSummary = getPerformanceSummary(stat.module, overallResult);
const title = "ENGLISH SKILLS TEST RESULT REPORT";
const details = <SkillExamDetails testDetails={testDetails} />;
const demographicInformation =
user.demographicInformation as DemographicInformation;
return ReactPDF.renderToStream(
<TestReport
title={title}
date={moment(stat.date)
.tz(user.demographicInformation?.timezone || "UTC")
.format("ll HH:mm:ss")}
name={user.name}
email={user.email}
id={user.id}
gender={demographicInformation?.gender}
summary={performanceSummary}
testDetails={testDetails}
renderDetails={details}
logo={"public/logo_title.png"}
qrcode={qrcode}
summaryPNG={overallPNG}
summaryScore={`${Math.floor(overallResult * 100)}%`}
passportId={demographicInformation?.passport_id || ""}
/>
);
}
async function getPdfUrl(pdfStream: any, docsSnap: any) {
// generate the file ref for storage
const fileName = `${Date.now().toString()}.pdf`;
const refName = `exam_report/${fileName}`;
const fileRef = ref(storage, refName);
// upload the pdf to storage
const pdfBuffer = await streamToBuffer(pdfStream);
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
contentType: "application/pdf",
});
// update the stats entries with the pdf url to prevent duplication
docsSnap.docs.forEach(async (doc: any) => {
await updateDoc(doc.ref, {
pdf: {
path: refName,
version: process.env.PDF_VERSION,
},
});
});
return getDownloadURL(fileRef);
}
async function post(req: NextApiRequest, res: NextApiResponse) { async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to export // verify if it's a logged user that is trying to export
if (req.session.user) { if (req.session.user) {
const {id} = req.query as {id: string}; const { id } = req.query as { id: string };
// fetch stats entries for this particular user with the requested exam session // fetch stats entries for this particular user with the requested exam session
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(400).end(); res.status(400).end();
return; return;
} }
const stats = docsSnap.docs.map((d) => d.data()); const stats = docsSnap.docs.map((d) => d.data()) as Stat[];
// verify if the stats already have a pdf generated // verify if the stats already have a pdf generated
const hasPDF = stats.find((s) => s.pdf?.path && s.pdf?.version === process.env.PDF_VERSION); const hasPDF = stats.find(
// find the user that generated the stats (s) => s.pdf?.path && s.pdf?.version === process.env.PDF_VERSION
const statIndex = stats.findIndex((s) => s.user); );
// find the user that generated the stats
const statIndex = stats.findIndex((s) => s.user);
if(statIndex === -1) { if (statIndex === -1) {
res.status(401).json({ok: false}); res.status(401).json({ ok: false });
return; return;
} }
const userId = stats[statIndex].user; 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);
if (hasPDF) { res.status(200).end(url);
// if it does, return the pdf url return;
const fileRef = ref(storage, hasPDF.pdf.path); }
const url = await getDownloadURL(fileRef);
res.status(200).end(url); try {
return; // generate the pdf report
} const docUser = await getDoc(doc(db, "users", userId));
try { if (docUser.exists()) {
// generate the pdf report // we'll need the user in order to get the user data (name, email, focus, etc);
const docUser = await getDoc(doc(db, "users", userId));
if (docUser.exists()) { const [stat] = stats;
// 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 if (stat.module === "level") {
const qrcode = await generateQRCode((req.headers.origin || "") + req.url); const user = docUser.data() as StudentUser;
if (!qrcode) { const uniqueExercises = stats.map((s) => ({
res.status(500).json({ok: false}); name: "Gramar & Vocabulary",
return; 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()}
/>
);
// stats may contain multiple exams of the same type so we need to aggregate them const url = await getPdfUrl(pdfStream, docsSnap);
const results = ( res.status(200).end(url);
stats.reduce((accm: ModuleScore[], {module, score}) => { return;
const fixedModuleStr = module[0].toUpperCase() + module.substring(1); }
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) { const user = docUser.data() as User;
return accm.map((e: ModuleScore) => {
if (e.module === fixedModuleStr) {
return {
...e,
score: e.score + score.correct,
total: e.total + score.total,
};
}
return e; try {
}); const pdfStream = await getDefaultPDFStream(
} stats,
user,
`${req.headers.origin || ""}${req.url}`
);
return [ const url = await getPdfUrl(pdfStream, docsSnap);
...accm, res.status(200).end(url);
{ return;
module: fixedModuleStr, } catch (err) {
score: score.correct, console.error(err);
total: score.total, res.status(500).json({ ok: false });
code: module, return;
}, }
]; }
}, []) as ModuleScore[]
).map((moduleScore) => {
const {score, total} = moduleScore;
// 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);
return { res.status(401).json({ ok: false });
...moduleScore, return;
// generate the closest radial progress png for the score } catch (err) {
png: getRadialProgressPNG("azul", score, total), console.error(err);
bandScore, res.status(500).json({ ok: false });
}; return;
}); }
}
// get the skills feedback from the backend based on the module grade res.status(401).json({ ok: false });
const skillsFeedback = (await handleSkillsFeedbackRequest( return;
results.map(({code, bandScore}) => ({
code,
name: moduleLabels[code],
grade: bandScore,
})),
)) as SkillsFeedbackResponse[];
if (!skillsFeedback) {
res.status(500).json({ok: false});
return;
}
// assign the feedback to the results
const finalResults = results.map((result) => {
const feedback = skillsFeedback.find((f: SkillsFeedbackResponse) => f.code === result.code);
if (feedback) {
return {
...result,
evaluation: feedback?.evaluation,
suggestions: feedback?.suggestions,
bullet_points: feedback?.bullet_points,
};
}
return result;
});
// calculate the overall score out of all the aggregated results
const overallScore = results.reduce((accm, {score}) => accm + score, 0);
const overallTotal = results.reduce((accm, {total}) => accm + total, 0);
const overallResult = overallScore / overallTotal;
const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal);
// generate the overall detail report
const overallDetail = {
module: "Overall",
score: overallScore,
total: overallTotal,
png: overallPNG,
} as ModuleScore;
const testDetails = [overallDetail, ...finalResults];
const [stat] = stats;
// generate the performance summary based on the overall result
const performanceSummary = getPerformanceSummary(stat.module, overallResult);
// level exams have a different report structure than the skill exams
const getCustomData = () => {
if (stat.module === "level") {
return {
title: "ENGLISH LEVEL TEST RESULT REPORT ",
details: <LevelExamDetails detail={overallDetail} title="Level as per CEFR Levels" />,
};
}
return {
title: "ENGLISH SKILLS TEST RESULT REPORT",
details: <SkillExamDetails testDetails={testDetails} />,
};
};
const {title, details} = getCustomData();
const demographicInformation = user.demographicInformation as DemographicInformation;
const pdfStream = await ReactPDF.renderToStream(
<TestReport
title={title}
date={moment(stat.date)
.tz(user.demographicInformation?.timezone || "UTC")
.format("ll HH:mm:ss")}
name={user.name}
email={user.email}
id={userId}
gender={demographicInformation?.gender}
summary={performanceSummary}
testDetails={testDetails}
renderDetails={details}
logo={"public/logo_title.png"}
qrcode={qrcode}
summaryPNG={overallPNG}
summaryScore={`${Math.floor(overallResult * 100)}%`}
passportId={demographicInformation?.passport_id || ""}
/>,
);
// generate the file ref for storage
const fileName = `${Date.now().toString()}.pdf`;
const refName = `exam_report/${fileName}`;
const fileRef = ref(storage, refName);
// upload the pdf to storage
const pdfBuffer = await streamToBuffer(pdfStream);
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
contentType: "application/pdf",
});
// update the stats entries with the pdf url to prevent duplication
docsSnap.docs.forEach(async (doc) => {
await updateDoc(doc.ref, {
pdf: {
path: refName,
version: process.env.PDF_VERSION,
},
});
});
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
res.status(401).json({ok: false});
return;
} catch (err) {
res.status(500).json({ok: false});
return;
}
}
res.status(401).json({ok: false});
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();
return; return;
} }
const stats = docsSnap.docs.map((d) => d.data()); const stats = docsSnap.docs.map((d) => d.data());
const hasPDF = stats.find((s) => s.pdf?.path); const hasPDF = stats.find((s) => s.pdf?.path);
if (hasPDF) { if (hasPDF) {
const fileRef = ref(storage, hasPDF.pdf.path); const fileRef = ref(storage, hasPDF.pdf.path);
const url = await getDownloadURL(fileRef); const url = await getDownloadURL(fileRef);
return res.redirect(url); return res.redirect(url);
} }
res.status(500).end(); res.status(500).end();
} }

View File

@@ -1,229 +1,181 @@
/* 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) => {
envVariables[x] = process.env[x]!; envVariables[x] = process.env[x]!;
}); });
if (user && user.isVerified) { if (user && user.isVerified) {
return { return {
redirect: { redirect: {
destination: "/", destination: "/",
permanent: false, permanent: false,
} },
}; };
} }
return { return {
props: { user: null, envVariables }, props: {user: null, envVariables},
}; };
}, sessionOptions); }, sessionOptions);
export default function Login() { export default function Login() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [rememberPassword, setRememberPassword] = useState(false); const [rememberPassword, setRememberPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const { user, mutateUser } = useUser({ const {user, mutateUser} = useUser({
redirectTo: "/", redirectTo: "/",
redirectIfFound: true, redirectIfFound: true,
}); });
useEffect(() => { useEffect(() => {
if (user && user.isVerified) router.push("/"); if (user && user.isVerified) router.push("/");
}, [router, user]); }, [router, user]);
const forgotPassword = () => { const forgotPassword = () => {
if (!email || email.length < 0 || !EMAIL_REGEX.test(email)) { if (!email || email.length < 0 || !EMAIL_REGEX.test(email)) {
toast.error("Please enter your e-mail to reset your password!", { toast.error("Please enter your e-mail to reset your password!", {
toastId: "forgot-invalid-email", toastId: "forgot-invalid-email",
}); });
return; return;
} }
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!", return;
{ toastId: "forgot-success" }, }
);
return;
}
toast.error("That e-mail address is not connected to an account!", { toast.error("That e-mail address is not connected to an account!", {
toastId: "forgot-error", toastId: "forgot-error",
}); });
}) })
.catch(() => .catch(() =>
toast.error("That e-mail address is not connected to an account!", { toast.error("That e-mail address is not connected to an account!", {
toastId: "forgot-error", toastId: "forgot-error",
}), }),
); );
}; };
const login = (e: FormEvent<HTMLFormElement>) => { const login = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
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",
}); });
mutateUser(response.data); mutateUser(response.data);
}) })
.catch((e) => { .catch((e) => {
if (e.response.status === 401) { if (e.response.status === 401) {
toast.error("Wrong login credentials!", { toast.error("Wrong login credentials!", {
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);
}) })
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}; };
return ( return (
<> <>
<Head> <Head>
<title>Login | EnCoach</title> <title>Login | EnCoach</title>
<meta name="description" content="Generated by create next app" /> <meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<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" </section>
alt="People smiling looking at a tablet" <section className="flex h-full w-full flex-col items-center justify-center gap-2">
className="aspect-auto h-full" <div className={clsx("flex flex-col items-center", !user && "mb-4")}>
/> <img src="/logo_title.png" alt="EnCoach's Logo" className="w-36 lg:w-56" />
</section> <h1 className="text-2xl font-bold lg:text-4xl">Login to your account</h1>
<section className="flex h-full w-full flex-col items-center justify-center gap-2"> <p className="text-mti-gray-cool self-start text-sm font-normal lg:text-base">with your registered Email Address</p>
<div className={clsx("flex flex-col items-center", !user && "mb-4")}> </div>
<img <Divider className="max-w-xs lg:max-w-md" />
src="/logo_title.png" {!user && (
alt="EnCoach's Logo" <>
className="w-36 lg:w-56" <form className="-lg:px-8 flex w-full flex-col items-center gap-6 lg:w-1/2" onSubmit={login}>
/> <Input type="email" name="email" onChange={(e) => setEmail(e.toLowerCase())} placeholder="Enter email address" />
<h1 className="text-2xl font-bold lg:text-4xl"> <Input type="password" name="password" onChange={(e) => setPassword(e)} placeholder="Password" />
Login to your account <div className="flex w-full justify-between px-4">
</h1> <div
<p className="text-mti-gray-cool self-start text-sm font-normal lg:text-base"> className="text-mti-gray-dim flex cursor-pointer gap-3 text-xs"
with your registered Email Address onClick={() => setRememberPassword((prev) => !prev)}>
</p> <input type="checkbox" className="hidden" />
</div> <div
<Divider className="max-w-xs lg:max-w-md" /> className={clsx(
{!user && ( "border-mti-purple-light flex h-4 w-4 items-center justify-center rounded-sm border bg-white",
<> "transition duration-300 ease-in-out",
<form rememberPassword && "!bg-mti-purple-light ",
className="-lg:px-8 flex w-full flex-col items-center gap-6 lg:w-1/2" )}>
onSubmit={login} <BsCheck color="white" className="h-full w-full" />
> </div>
<Input <span>Remember my password</span>
type="email" </div>
name="email" <span className="text-mti-purple-light cursor-pointer text-xs hover:underline" onClick={forgotPassword}>
onChange={(e) => setEmail(e.toLowerCase())} Forgot Password?
placeholder="Enter email address" </span>
/> </div>
<Input <Button className="mt-8 w-full" color="purple" disabled={isLoading}>
type="password" {!isLoading && "Login"}
name="password" {isLoading && (
onChange={(e) => setPassword(e)} <div className="flex items-center justify-center">
placeholder="Password" <BsArrowRepeat className="animate-spin text-white" size={25} />
/> </div>
<div className="flex w-full justify-between px-4"> )}
<div </Button>
className="text-mti-gray-dim flex cursor-pointer gap-3 text-xs" </form>
onClick={() => setRememberPassword((prev) => !prev)} <span className="text-mti-gray-cool mt-8 text-sm font-normal">
> Don&apos;t have an account?{" "}
<input type="checkbox" className="hidden" /> <Link className="text-mti-purple-light" href="/register">
<div Sign up
className={clsx( </Link>
"border-mti-purple-light flex h-4 w-4 items-center justify-center rounded-sm border bg-white", </span>
"transition duration-300 ease-in-out", </>
rememberPassword && "!bg-mti-purple-light ", )}
)} {user && !user.isVerified && <EmailVerification user={user} isLoading={isLoading} setIsLoading={setIsLoading} />}
> </section>
<BsCheck color="white" className="h-full w-full" /> </main>
</div> </>
<span>Remember my password</span> );
</div>
<span
className="text-mti-purple-light cursor-pointer text-xs hover:underline"
onClick={forgotPassword}
>
Forgot Password?
</span>
</div>
<Button
className="mt-8 w-full"
color="purple"
disabled={isLoading}
>
{!isLoading && "Login"}
{isLoading && (
<div className="flex items-center justify-center">
<BsArrowRepeat
className="animate-spin text-white"
size={25}
/>
</div>
)}
</Button>
</form>
<span className="text-mti-gray-cool mt-8 text-sm font-normal">
Don&apos;t have an account?{" "}
<Link className="text-mti-purple-light" href="/register">
Sign up
</Link>
</span>
</>
)}
{user && !user.isVerified && (
<EmailVerification
user={user}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
)}
</section>
</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,107 +1,173 @@
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 (
const corporateRef = await getDoc(doc(db, "users", corporateID)); participantID: string,
const participantRef = await getDoc(doc(db, "users", participantID)); corporateID: string
) => {
const corporateRef = await getDoc(doc(db, "users", corporateID));
const participantRef = await getDoc(doc(db, "users", participantID));
if (!corporateRef.exists() || !participantRef.exists()) return; if (!corporateRef.exists() || !participantRef.exists()) return;
const corporate = { const corporate = {
...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 (
const groups = await getUserGroups(corporateID); corporateID: string
const groupUsers = (await Promise.all(groups.map(async (g) => await Promise.all(g.participants.map(getUser))))).flat(); ): Promise<string[]> => {
const teacherPromises = await Promise.all( const groups = await getUserGroups(corporateID);
groupUsers.map(async (u) => const groupUsers = (
u.type === "teacher" ? u.id : u.type === "corporate" ? [...(await getAllAssignersByCorporate(u.id)), u.id] : undefined, await Promise.all(
), groups.map(async (g) => await Promise.all(g.participants.map(getUser)))
); )
).flat();
const teacherPromises = await Promise.all(
groupUsers.map(async (u) =>
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[];
}; };
export const getGroupsForUser = async (admin: string, participant: string) => { export const getGroupsForUser = async (admin: string, participant: string) => {
try { try {
const queryConstraints = [ const queryConstraints = [
...(admin ? [where("admin", "==", admin)] : []), ...(admin ? [where("admin", "==", admin)] : []),
...(participant ...(participant
? [where("participants", "array-contains", participant)] ? [where("participants", "array-contains", participant)]
: []), : []),
]; ];
const snapshot = await getDocs( const snapshot = await getDocs(
queryConstraints.length > 0 queryConstraints.length > 0
? query(collection(db, "groups"), ...queryConstraints) ? query(collection(db, "groups"), ...queryConstraints)
: collection(db, "groups") : collection(db, "groups")
); );
const groups = snapshot.docs.map((doc) => ({ const groups = snapshot.docs.map((doc) => ({
id: doc.id, id: doc.id,
...doc.data(), ...doc.data(),
})) as Group[]; })) as Group[];
return groups; return groups;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return []; return [];
} }
}; };
export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => { export const getStudentGroupsForUsersWithoutAdmin = async (
try { admin: string,
const queryConstraints = [ participants: string[]
...(admin ? [where("admin", "!=", admin)] : []), ) => {
...(participants try {
? [where("participants", "array-contains-any", participants)] const queryConstraints = [
: []), ...(admin ? [where("admin", "!=", admin)] : []),
where("name", "==", "Students"), ...(participants
]; ? [where("participants", "array-contains-any", participants)]
const snapshot = await getDocs( : []),
queryConstraints.length > 0 where("name", "==", "Students"),
? query(collection(db, "groups"), ...queryConstraints) ];
: collection(db, "groups") const snapshot = await getDocs(
); queryConstraints.length > 0
const groups = snapshot.docs.map((doc) => ({ ? query(collection(db, "groups"), ...queryConstraints)
id: doc.id, : collection(db, "groups")
...doc.data(), );
})) as Group[]; const groups = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as Group[];
return groups; return groups;
} catch (e) { } catch (e) {
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;
} }