Merge branch 'develop' into feature-paymentAssetManagement

This commit is contained in:
Joao Ramos
2023-12-13 23:43:52 +00:00
8 changed files with 264 additions and 153 deletions

View File

@@ -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";

View File

@@ -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,8 +195,31 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
placeholder="Enter monthly duration"
defaultValue={monthlyDuration}
/>
<div className="flex flex-col gap-3 w-full">
<div className="flex flex-col gap-3 w-full lg:col-span-2">
<label className="font-normal text-base text-mti-gray-dim">Pricing</label>
<div className="w-full grid grid-cols-5 gap-2">
<Input
name="paymentValue"
onChange={(e) => setPaymentValue(e ? parseInt(e) : undefined)}
type="number"
defaultValue={paymentValue || 0}
className="col-span-3"
/>
<select
defaultValue={paymentCurrency}
onChange={(e) => setPaymentCurrency(e.target.value)}
className="p-6 col-span-2 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
{CURRENCIES.map(({label, currency}) => (
<option value={currency} key={currency}>
{label}
</option>
))}
</select>
</div>
</div>
</div>
<div className="flex gap-3 w-full">
<div className="flex flex-col gap-3 w-8/12">
<label className="font-normal text-base text-mti-gray-dim">Country Manager</label>
{referralAgentLabel && (
<Select
@@ -228,28 +252,19 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
/>
)}
</div>
<div className="flex flex-col gap-3 w-full lg:col-span-2">
<label className="font-normal text-base text-mti-gray-dim">Pricing</label>
<div className="w-full grid grid-cols-5 gap-2">
<div className="flex flex-col gap-3 w-4/12">
{referralAgent !== '' ? (
<>
<label className="font-normal text-base text-mti-gray-dim">Commission</label>
<Input
name="paymentValue"
onChange={(e) => setPaymentValue(e ? parseInt(e) : undefined)}
name="commissionValue"
onChange={(e) => setCommission(e ? parseInt(e) : undefined)}
type="number"
defaultValue={paymentValue || 0}
defaultValue={commissionValue || 0}
className="col-span-3"
/>
<select
defaultValue={paymentCurrency}
onChange={(e) => setPaymentCurrency(e.target.value)}
className="p-6 col-span-2 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
{CURRENCIES.map(({label, currency}) => (
<option value={currency} key={currency}>
{label}
</option>
))}
</select>
</div>
</>
) : <div />}
</div>
</div>
<Divider className="w-full !m-0" />

View File

@@ -2,20 +2,20 @@ 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 (
export type LevelScore = "Advanced" | "Upper-Intermediate" | "Intermediate" | "Pre-Intermediate" | "Elementary" | "Beginner";
const generateHighestScoreText = () : React.ReactNode => (
<>
Congratulations on your exam performance! You achieved an impressive <span className="font-bold">level {level}</span>, demonstrating
excellent mastery of the assessed knowledge.
<br />
<br />
If you disagree with the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of
@@ -26,13 +26,9 @@ export const moduleResultText = (level: number) => {
academic journey.
</>
);
}
if (level >= 6) {
return (
const generateAverageScoreText = () : React.ReactNode => (
<>
Congratulations on your exam performance! You achieved a commendable <span className="font-bold">level {level}</span>, demonstrating a
good understanding of the assessed knowledge.
<br />
<br />
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
@@ -43,29 +39,9 @@ export const moduleResultText = (level: number) => {
journey.
</>
);
}
if (level >= 3) {
return (
const generateLowestScoreText = () : React.ReactNode => (
<>
Congratulations on your exam performance! You achieved a <span className="font-bold">level of {level}</span>, demonstrating a
satisfactory understanding of the assessed knowledge.
<br />
<br />
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.
<br />
<br />
Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic
journey.
</>
);
}
return (
<>
Thank you for taking the exam. You achieved a <span className="font-bold">level {level}</span>, but unfortunately, it did not meet the
required standards.
<br />
<br />
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
@@ -75,23 +51,70 @@ export const moduleResultText = (level: number) => {
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 <span className="font-bold">level {level}</span>, demonstrating
excellent mastery of the assessed knowledge.
<br />
<br />
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.
<br />
<br />
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 <span className="font-bold">level {level}</span>, demonstrating a
good understanding of the assessed knowledge.
<br />
<br />
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.
<br />
<br />
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 <span className="font-bold">level of {level}</span>, demonstrating a
satisfactory understanding of the assessed knowledge.
<br />
<br />
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.
<br />
<br />
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 <span className="font-bold">level {level}</span>, but unfortunately, it did not meet the
required standards.
<br />
<br />
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.
<br />
<br />
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()}
</>
);
};

View File

@@ -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 (
<div className="flex flex-col items-center justify-center gap-1">
<span className="text-xl font-bold">{levelStr}</span>
<span className="text-xl">{grade}</span>
</div>
)
}
return <span className="text-3xl font-bold">{level}</span>;
}
return (
<>
<div className="w-full min-h-full h-fit flex flex-col items-center justify-between gap-8">
@@ -142,7 +162,7 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
{!isLoading && (
<div className="w-full flex gap-9 mt-32 items-center justify-between mb-20">
<span className="max-w-3xl">
{moduleResultText(calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus))}
{moduleResultText(selectedModule, bandScore)}
</span>
<div className="flex gap-9 px-16">
<div
@@ -156,9 +176,7 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
moduleColors[selectedModule].inner,
)}>
<span className="text-xl">Level</span>
<span className="text-3xl font-bold">
{calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus)}
</span>
{showLevel(bandScore)}
</div>
</div>
<div className="flex flex-col gap-5">

View File

@@ -31,7 +31,7 @@ export interface Payment {
currency: string;
value: number;
isPaid: boolean;
date: Date;
date: Date | string;
corporateTransfer?: string;
commissionTransfer?: string;
}

View File

@@ -57,6 +57,7 @@ export interface CorporateInformation {
payment?: {
value: number;
currency: string;
commission: number;
};
referralAgent?: string;
}

View File

@@ -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<boolean> => {
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});
}

View File

@@ -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 [];
}
}