diff --git a/src/components/Diagnostic.tsx b/src/components/Diagnostic.tsx index 94b00e02..943d90e7 100644 --- a/src/components/Diagnostic.tsx +++ b/src/components/Diagnostic.tsx @@ -1,5 +1,4 @@ import {infoButtonStyle} from "@/constants/buttonStyles"; -import {BAND_SCORES} from "@/constants/ielts"; import {Module} from "@/interfaces"; import {User} from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 98db4a77..c703d99c 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -60,7 +60,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined); const [paymentCurrency, setPaymentCurrency] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.currency : "EUR"); const [monthlyDuration, setMonthlyDuration] = useState(user.type === "corporate" ? user.corporateInformation?.monthlyDuration : undefined); - + const [commissionValue, setCommission] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.commission : undefined); const {stats} = useStats(user.id); const {users} = useUsers(); @@ -106,6 +106,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, payment: { value: paymentValue, currency: paymentCurrency, + ...referralAgent === '' ? {} : { commission: commissionValue } }, } : undefined, @@ -194,41 +195,6 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, placeholder="Enter monthly duration" defaultValue={monthlyDuration} /> - -
- - {referralAgentLabel && ( - u.type === "agent").map((x) => ({value: x.id, label: `${x.name} - ${x.email}`})), + ]} + defaultValue={{ + value: referralAgent, + label: referralAgentLabel, + }} + onChange={(value) => setReferralAgent(value?.value)} + styles={{ + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> + )} +
+
+ {referralAgent !== '' ? ( + <> + + setCommission(e ? parseInt(e) : undefined)} + type="number" + defaultValue={commissionValue || 0} + className="col-span-3" + /> + + ) :
} +
+
)} diff --git a/src/constants/ielts.tsx b/src/constants/ielts.tsx index e3f6347a..630100c5 100644 --- a/src/constants/ielts.tsx +++ b/src/constants/ielts.tsx @@ -2,96 +2,119 @@ import {Module} from "@/interfaces"; export const MODULES: Module[] = ["reading", "listening", "writing", "speaking"]; -export const BAND_SCORES: {[key in Module]: number[]} = { - reading: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9], - listening: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9], - writing: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - speaking: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - level: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], -}; +// BAND SCORES is not in use anymore and level scoring is made based on thresholds +// export const BAND_SCORES: {[key in Module]: number[]} = { +// reading: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9], +// listening: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9], +// writing: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], +// speaking: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], +// level: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], +// }; -export const moduleResultText = (level: number) => { - if (level === 9) { - return ( - <> - Congratulations on your exam performance! You achieved an impressive level {level}, demonstrating - excellent mastery of the assessed knowledge. -
-
- If you disagree with the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of - the results. -
-
- Please contact us for further information. Congratulations again on your outstanding achievement! We are here to support you on your - academic journey. - - ); - } +export type LevelScore = "Advanced" | "Upper-Intermediate" | "Intermediate" | "Pre-Intermediate" | "Elementary" | "Beginner"; - if (level >= 6) { - return ( - <> - Congratulations on your exam performance! You achieved a commendable level {level}, demonstrating a - good understanding of the assessed knowledge. -
-
- If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and - transparency of the results. -
-
- Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic - journey. - - ); - } - if (level >= 3) { - return ( - <> - Congratulations on your exam performance! You achieved a level of {level}, demonstrating a - satisfactory understanding of the assessed knowledge. -
-
- If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and - transparency of the results. -
-
- Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic - journey. - - ); - } +const generateHighestScoreText = () : React.ReactNode => ( + <> +
+
+ If you disagree with the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of + the results. +
+
+ Please contact us for further information. Congratulations again on your outstanding achievement! We are here to support you on your + academic journey. + +); - return ( - <> - Thank you for taking the exam. You achieved a level {level}, but unfortunately, it did not meet the - required standards. -
-
- If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and - transparency of the results. -
-
- Please contact us for further information. We encourage you to continue your studies and wish you the best of luck in your future - endeavors. - - ); -}; +const generateAverageScoreText = () : React.ReactNode => ( + <> +
+
+ If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and + transparency of the results. +
+
+ Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic + journey. + +); + +const generateLowestScoreText = () : React.ReactNode => ( + <> +
+
+ If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and + transparency of the results. +
+
+ Please contact us for further information. We encourage you to continue your studies and wish you the best of luck in your future + endeavors. + +) export const levelResultText = (level: number) => { + if(level === 9) { + return ( + <> + {"Outstanding! Your command of English is excellent. Focus on fine-tuning subtle language nuances and exploring sophisticated vocabulary. Keep up the excellent work!"} + {generateHighestScoreText()} + + ); + } + if(level >= 8) { + return ( + <> + {"Impressive! You're approaching fluency. Continue refining nuances in grammar and expanding your vocabulary to express ideas more precisely."} + {generateAverageScoreText()} + + ); + } + if(level >= 6) { + return ( + <> + {"Great job! You're navigating the complexities of English. Keep honing your grammar skills and exploring more advanced vocabulary."} + {generateAverageScoreText()} + + ); + } + if(level >= 4) { + return ( + <> + {"Well done! You're moving beyond the basics. Work on expanding your vocabulary and refining your understanding of grammar structures."} + {generateAverageScoreText()} + + ); + } + if(level >= 2) { + return ( + <> + {"Good effort! You're making progress. Continue studying and pay attention to common vocabulary and fundamental grammar rules."} + {generateAverageScoreText()} + + ); + } + if(level >= 0) { + return ( + <> + {"Keep practicing! You're just starting, and improvement takes time. Focus on building your vocabulary and basic grammar skills."} + {generateLowestScoreText()} + + ); + } + + return null; +}; + + +export const moduleResultText = (module: Module, level: number) => { + if(module === 'level') return levelResultText(level); if (level === 9) { return ( <> Congratulations on your exam performance! You achieved an impressive level {level}, demonstrating excellent mastery of the assessed knowledge. -
-
- If you disagree with the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of - the results. -
-
- Please contact us for further information. Congratulations again on your outstanding achievement! We are here to support you on your - academic journey. + {generateHighestScoreText()} ); } @@ -101,14 +124,7 @@ export const levelResultText = (level: number) => { <> Congratulations on your exam performance! You achieved a commendable level {level}, demonstrating a good understanding of the assessed knowledge. -
-
- If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and - transparency of the results. -
-
- Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic - journey. + {generateAverageScoreText()} ); } @@ -118,14 +134,7 @@ export const levelResultText = (level: number) => { <> Congratulations on your exam performance! You achieved a level of {level}, demonstrating a satisfactory understanding of the assessed knowledge. -
-
- If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and - transparency of the results. -
-
- Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic - journey. + {generateAverageScoreText()} ); } @@ -134,14 +143,7 @@ export const levelResultText = (level: number) => { <> Thank you for taking the exam. You achieved a level {level}, but unfortunately, it did not meet the required standards. -
-
- If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and - transparency of the results. -
-
- Please contact us for further information. We encourage you to continue your studies and wish you the best of luck in your future - endeavors. + {generateLowestScoreText()} ); -}; +}; \ No newline at end of file diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx index 2d4c94c3..47bc559f 100644 --- a/src/exams/Finish.tsx +++ b/src/exams/Finish.tsx @@ -10,6 +10,8 @@ import Link from "next/link"; import {useRouter} from "next/router"; import {Fragment, useEffect, useState} from "react"; import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs"; +import { LevelScore } from "@/constants/ielts"; +import { getLevelScore } from "@/utils/score"; interface Score { module: Module; @@ -66,6 +68,24 @@ export default function Finish({user, scores, modules, isLoading, onViewResults} return exam.exercises.length; }; + const bandScore: number = calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus); + + const showLevel = (level: number) => { + if(selectedModule === "level") { + const [levelStr, grade] = getLevelScore(level); + return ( +
+ {levelStr} + {grade} +
+ ) + } + + + return {level}; + + } + return ( <>
@@ -142,7 +162,7 @@ export default function Finish({user, scores, modules, isLoading, onViewResults} {!isLoading && (
- {moduleResultText(calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus))} + {moduleResultText(selectedModule, bandScore)}
Level - - {calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus)} - + {showLevel(bandScore)}
diff --git a/src/interfaces/paypal.ts b/src/interfaces/paypal.ts index 56bf2688..3c1770b9 100644 --- a/src/interfaces/paypal.ts +++ b/src/interfaces/paypal.ts @@ -31,7 +31,7 @@ export interface Payment { currency: string; value: number; isPaid: boolean; - date: Date; + date: Date | string; corporateTransfer?: string; commissionTransfer?: string; } diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 5594f273..cd337ac5 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -57,6 +57,7 @@ export interface CorporateInformation { payment?: { value: number; currency: string; + commission: number; }; referralAgent?: string; } diff --git a/src/pages/api/users/update.ts b/src/pages/api/users/update.ts index a4f80fc8..315f84a1 100644 --- a/src/pages/api/users/update.ts +++ b/src/pages/api/users/update.ts @@ -1,19 +1,72 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type {NextApiRequest, NextApiResponse} from "next"; import {app, storage} from "@/firebase"; -import {getFirestore, collection, getDocs, getDoc, doc, setDoc} from "firebase/firestore"; +import {getFirestore, collection, getDocs, getDoc, doc, setDoc, query, where} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {User} from "@/interfaces/user"; import {getDownloadURL, getStorage, ref, uploadBytes} from "firebase/storage"; import {getAuth, signInWithEmailAndPassword, updateEmail, updatePassword} from "firebase/auth"; import {errorMessages} from "@/constants/errors"; +import moment from "moment"; +import ShortUniqueId from "short-unique-id"; +import {Payment} from "@/interfaces/paypal"; const db = getFirestore(app); const auth = getAuth(app); export default withIronSessionApiRoute(handler, sessionOptions); +// TODO: Data is set as any as data cannot be parsed to Payment +// because the id is not a par of the hash and payment expects date to be of type Date +// but if it is not inserted as a string, some UI components will not work (Invalid Date) +const addPaymentRecord = async (data: any) => { + await setDoc(doc(db, "payments", data.id), data); +} +const managePaymentRecords = async (user: User, userId: string | undefined): Promise => { + try { + if(user.type === 'corporate' && userId) { + const shortUID = new ShortUniqueId(); + const data: Payment = { + id: shortUID.randomUUID(8), + corporate: userId, + agent: user.corporateInformation.referralAgent, + agentCommission: user.corporateInformation.payment!.commission, + agentValue: (user.corporateInformation.payment!.commission / 100) * user.corporateInformation.payment!.value, + currency: user.corporateInformation.payment!.currency, + value: user.corporateInformation.payment!.value, + isPaid: false, + date: new Date().toISOString(), + }; + + const corporatePayments = await getDocs(query(collection(db, "payments"), where("corporate", "==", userId))); + if(corporatePayments.docs.length === 0) { + await addPaymentRecord(data); + return true; + } + + const hasPaymentPaidAndExpiring = corporatePayments.docs.filter((doc) => { + const data = doc.data(); + return data.isPaid + && moment().isAfter(moment(user.subscriptionExpirationDate).subtract(30, "days")) + && moment().isBefore(moment(user.subscriptionExpirationDate)); + }); + + if(hasPaymentPaidAndExpiring.length > 0) { + await addPaymentRecord(data); + return true; + } + } + + return false; + } catch(e) { + // if this process fails it should not stop the rest of the process + console.log(e); + return false; + } + +} + async function handler(req: NextApiRequest, res: NextApiResponse) { if (!req.session.user) { res.status(401).json({ok: false}); @@ -24,7 +77,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const updatedUser = req.body as User & {password?: string; newPassword?: string}; if (!!req.query.id) { - await setDoc(userRef, updatedUser, {merge: true}); + const user = await setDoc(userRef, updatedUser, {merge: true}); + await managePaymentRecords(updatedUser, updatedUser.id); res.status(200).json({ok: true}); return; } @@ -73,6 +127,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { await req.session.save(); } + await managePaymentRecords(user, req.query.id); + res.status(200).json({user}); } diff --git a/src/utils/score.ts b/src/utils/score.ts index 18a13c63..08707312 100644 --- a/src/utils/score.ts +++ b/src/utils/score.ts @@ -1,4 +1,5 @@ import {Module} from "@/interfaces"; +import { LevelScore } from "@/constants/ielts"; type Type = "academic" | "general"; @@ -94,11 +95,12 @@ const academicMarking: {[key: number]: number} = { }; const levelMarking: {[key: number]: number} = { - 88: 9, - 64: 8, - 52: 6, - 32: 4, - 16: 2, + 88: 9, // Advanced + 64: 8 , // Upper-Intermediate + 52: 6, // Intermediate + 32: 4, // Pre-Intermediate + 16: 2, // Elementary + 0: 0, // Beginner }; const moduleMarkings: {[key in Module]: {[key in Type]: {[key: number]: number}}} = { @@ -142,3 +144,21 @@ export const calculateBandScore = (correct: number, total: number, module: Modul export const calculateAverageLevel = (levels: {[key in Module]: number}) => { return Object.keys(levels).reduce((accumulator, current) => levels[current as Module] + accumulator, 0) / 4; }; + +export const getLevelScore = (level: number) => { + switch(level) { + case 0: + return ['Beginner', 'Low A1']; + case 2: + return ['Elementary', 'High A1/Low A2']; + case 4: + return ['Pre-Intermediate', 'High A2/Low B1']; + case 6: + return ['Intermediate', 'High B1/Low B2']; + case 8: + return ['Upper-Intermediate', 'High B2/Low C1']; + case 9: + return ['Advanced', 'C1']; + default: return []; + } +} \ No newline at end of file