diff --git a/next.config.js b/next.config.js index 6eefe6ab..4a6bb16f 100644 --- a/next.config.js +++ b/next.config.js @@ -1,27 +1,26 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, - output: "standalone", - async headers() { - return [ - { - source: "/api/packages", - headers: [ - { key: "Access-Control-Allow-Credentials", value: "false" }, - { key: "Access-Control-Allow-Origin", value: process.env.WEBSITE_URL }, - { - key: "Access-Control-Allow-Methods", - value: "GET", - }, - { - key: "Access-Control-Allow-Headers", - value: - "Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date", - }, - ], - }, - ]; - }, + reactStrictMode: true, + output: "standalone", + async headers() { + return [ + { + source: "/api/packages", + headers: [ + {key: "Access-Control-Allow-Credentials", value: "false"}, + {key: "Access-Control-Allow-Origin", value: "https://encoach.com"}, + { + key: "Access-Control-Allow-Methods", + value: "GET", + }, + { + key: "Access-Control-Allow-Headers", + value: "Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date", + }, + ], + }, + ]; + }, }; module.exports = nextConfig; diff --git a/src/dashboards/Admin.tsx b/src/dashboards/Admin.tsx index b91291d7..c72550b0 100644 --- a/src/dashboards/Admin.tsx +++ b/src/dashboards/Admin.tsx @@ -260,7 +260,7 @@ export default function AdminDashboard({user}: Props) { /> setPage("inactiveCorporate")} - Icon={BsPerson} + Icon={BsBank} label="Inactive Corporate" value={ users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate))) diff --git a/src/dashboards/Agent.tsx b/src/dashboards/Agent.tsx index dbfb56cc..b32158c4 100644 --- a/src/dashboards/Agent.tsx +++ b/src/dashboards/Agent.tsx @@ -2,20 +2,17 @@ import Modal from "@/components/Modal"; import useStats from "@/hooks/useStats"; import useUsers from "@/hooks/useUsers"; -import {Group, Stat, User} from "@/interfaces/user"; +import { User} from "@/interfaces/user"; import UserList from "@/pages/(admin)/Lists/UserList"; import {dateSorter} from "@/utils"; import moment from "moment"; import {useEffect, useState} from "react"; -import {BsArrowLeft, BsPersonFill, BsBank} from "react-icons/bs"; +import {BsArrowLeft, BsPersonFill, BsBank, BsCurrencyDollar} from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; -import {calculateAverageLevel, calculateBandScore} from "@/utils/score"; -import {MODULE_ARRAY} from "@/utils/moduleUtils"; -import {Module} from "@/interfaces"; -import {groupByExam} from "@/utils/stats"; + import IconCard from "./IconCard"; -import GroupList from "@/pages/(admin)/Lists/GroupList"; +import usePaymentStatusUsers from '@/hooks/usePaymentStatusUsers'; interface Props { user: User; @@ -29,6 +26,7 @@ export default function AgentDashboard({user}: Props) { const {stats} = useStats(); const {users, reload} = useUsers(); const {groups} = useGroups(user.id); + const { pending, done } = usePaymentStatusUsers(); useEffect(() => { setShowModal(!!selectedUser && page === ""); @@ -40,9 +38,9 @@ export default function AgentDashboard({user}: Props) { const inactiveReferredCorporateFilter = (x: User) => referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); - const UserDisplay = (displayUser: User) => ( + const UserDisplay = ({ displayUser, allowClick = true }: {displayUser: User, allowClick?: boolean}) => (
setSelectedUser(displayUser)} + onClick={() => allowClick && setSelectedUser(displayUser)} className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"> {displayUser.name}
@@ -66,7 +64,7 @@ export default function AgentDashboard({user}: Props) { Back
-

Corporate ({users.filter(referredCorporateFilter).length})

+

Referred Corporate ({users.filter(referredCorporateFilter).length})

@@ -84,7 +82,7 @@ export default function AgentDashboard({user}: Props) { Back -

Inactive Corporate ({users.filter(inactiveReferredCorporateFilter).length})

+

Inactive Referred Corporate ({users.filter(inactiveReferredCorporateFilter).length})

@@ -106,7 +104,26 @@ export default function AgentDashboard({user}: Props) {

Corporate ({users.filter(filter).length})

+ + + ); + }; + const CorporatePaidStatusList = ({ paid }: {paid: Boolean}) => { + const list = paid ? done : pending; + const filter = (x: User) => x.type === "corporate" && list.includes(x.id); + + return ( + <> +
+
setPage("")} + className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> + + Back +
+

{paid ? 'Payment Done' : 'Pending Payment'} ({list.length})

+
); @@ -117,15 +134,15 @@ export default function AgentDashboard({user}: Props) {
setPage("referredCorporate")} - Icon={BsPersonFill} - label="Corporate" + Icon={BsBank} + label="Referred Corporate" value={users.filter(referredCorporateFilter).length} color="purple" /> setPage("inactiveReferredCorporate")} - Icon={BsPersonFill} - label="Inactive Corporate" + Icon={BsBank} + label="Inactive Referred Corporate" value={users.filter(inactiveReferredCorporateFilter).length} color="rose" /> @@ -136,17 +153,31 @@ export default function AgentDashboard({user}: Props) { value={users.filter(corporateFilter).length} color="purple" /> + setPage("paymentdone")} + Icon={BsCurrencyDollar} + label="Payment Done" + value={done.length} + color="purple" + /> + setPage("paymentdone")} + Icon={BsCurrencyDollar} + label="Pending Payment" + value={pending.length} + color="rose" + />
- Latest Corporate + Latest Referred Corporate
{users .filter(referredCorporateFilter) .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) .map((x) => ( - + ))}
@@ -157,12 +188,12 @@ export default function AgentDashboard({user}: Props) { .filter(corporateFilter) .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) .map((x) => ( - + ))}
- Corporate expiring in 1 month + Referenced corporate expiring in 1 month
{users .filter( @@ -172,7 +203,7 @@ export default function AgentDashboard({user}: Props) { moment().isBefore(moment(x.subscriptionExpirationDate)), ) .map((x) => ( - + ))}
@@ -205,6 +236,8 @@ export default function AgentDashboard({user}: Props) { {page === "referredCorporate" && } {page === "corporate" && } {page === "inactiveReferredCorporate" && } + {page === "paymentdone" && } + {page === "paymentpending" && } {page === "" && } ); diff --git a/src/exams/pdf/group.test.report.tsx b/src/exams/pdf/group.test.report.tsx index 9b0d0405..4b865494 100644 --- a/src/exams/pdf/group.test.report.tsx +++ b/src/exams/pdf/group.test.report.tsx @@ -103,13 +103,13 @@ const GroupTestReport = ({ {title} - + Date of Test: {date} Candidate Information: - + Name: {name} ID: {id} Email: {email} @@ -193,10 +193,8 @@ const GroupTestReport = ({ ]} key={label} > - - {label} - - + {label} + ( - - + + Validity @@ -52,4 +57,4 @@ const TestReportFooter = () => ( ); -export default TestReportFooter; \ No newline at end of file +export default TestReportFooter; diff --git a/src/exams/pdf/test.report.tsx b/src/exams/pdf/test.report.tsx index 34ceddc7..acd14481 100644 --- a/src/exams/pdf/test.report.tsx +++ b/src/exams/pdf/test.report.tsx @@ -68,19 +68,19 @@ const TestReport = ({ {title} - + Date of Test: {date} Candidate Information: - + Name: {name} ID: {id} Email: {email} Gender: {gender} - + {testDetails - .filter(({ suggestions, evaluation }) => suggestions || evaluation) + .filter( + ({ suggestions, evaluation }) => suggestions || evaluation + ) .map(({ module, suggestions, evaluation }) => ( @@ -139,15 +141,11 @@ const TestReport = ({ {evaluation} {suggestions} - ))} - + diff --git a/src/hooks/usePaymentStatusUsers.tsx b/src/hooks/usePaymentStatusUsers.tsx new file mode 100644 index 00000000..5b2bbd94 --- /dev/null +++ b/src/hooks/usePaymentStatusUsers.tsx @@ -0,0 +1,20 @@ +import axios from "axios"; +import { useEffect, useState } from "react"; +import { PaymentsStatus } from "@/interfaces/user.payments"; + +export default function usePaymentStatusUsers() { + const [{ pending, done }, setStatus] = useState({ + pending: [], + done: [], + }); + + const getData = () => { + axios.get("/api/payments/assigned").then((response) => { + setStatus(response.data); + }); + }; + + useEffect(getData, []); + + return { pending, done }; +} diff --git a/src/interfaces/user.payments.ts b/src/interfaces/user.payments.ts new file mode 100644 index 00000000..c555b919 --- /dev/null +++ b/src/interfaces/user.payments.ts @@ -0,0 +1,5 @@ +// these arrays contain the ids of the corporates that have paid or not paid +export interface PaymentsStatus { + pending: string[]; + done: string[]; +} \ No newline at end of file diff --git a/src/pages/api/payments/assigned.ts b/src/pages/api/payments/assigned.ts new file mode 100644 index 00000000..107e619e --- /dev/null +++ b/src/pages/api/payments/assigned.ts @@ -0,0 +1,81 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { + getFirestore, + collection, + getDocs, + query, + where, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Payment } from "@/interfaces/paypal"; +import { PaymentsStatus } from "@/interfaces/user.payments"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return await get(req, res); + + res.status(404).json(undefined); +} + +// user can fetch payments assigned to him as an agent +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + const codeQuery = query( + collection(db, "payments"), + // where("agent", "==", "xRMirufz6PPQqxKBgvPTWiWKBD63"), + where(req.session.user.type, "==", req.session.user.id) + // Based on the logic of query we should be able to do this: + // where("isPaid", "==", paid === "paid"), + // but for some reason it is ignoring all but the first clause + // I opted into only fetching relevant content for the user + // and then filter it with JS + ); + + const snapshot = await getDocs(codeQuery); + if (snapshot.empty) { + res.status(200).json({ + pending: [], + done: [], + }); + return; + } + + const docs = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Payment[]; + + const paidStatusEntries = docs.reduce( + (acc: PaymentsStatus, doc) => { + if (doc.isPaid) { + return { + ...acc, + done: [...acc.done, doc.corporate], + }; + } + + return { + ...acc, + pending: [...acc.pending, doc.corporate], + }; + }, + { + pending: [], + done: [], + } + ); + res.status(200).json({ + pending: [...new Set(paidStatusEntries.pending)], + done: [...new Set(paidStatusEntries.done)], + }); +}