Merge branch 'develop' into feature/62/upload-users-with-excel

This commit is contained in:
Tiago Ribeiro
2024-01-12 13:42:25 +00:00
10 changed files with 209 additions and 70 deletions

View File

@@ -1,27 +1,26 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
output: "standalone", output: "standalone",
async headers() { async headers() {
return [ return [
{ {
source: "/api/packages", source: "/api/packages",
headers: [ headers: [
{ key: "Access-Control-Allow-Credentials", value: "false" }, {key: "Access-Control-Allow-Credentials", value: "false"},
{ key: "Access-Control-Allow-Origin", value: process.env.WEBSITE_URL }, {key: "Access-Control-Allow-Origin", value: "https://encoach.com"},
{ {
key: "Access-Control-Allow-Methods", key: "Access-Control-Allow-Methods",
value: "GET", value: "GET",
}, },
{ {
key: "Access-Control-Allow-Headers", key: "Access-Control-Allow-Headers",
value: value: "Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date",
"Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date", },
}, ],
], },
}, ];
]; },
},
}; };
module.exports = nextConfig; module.exports = nextConfig;

View File

@@ -260,7 +260,7 @@ export default function AdminDashboard({user}: Props) {
/> />
<IconCard <IconCard
onClick={() => setPage("inactiveCorporate")} onClick={() => setPage("inactiveCorporate")}
Icon={BsPerson} Icon={BsBank}
label="Inactive Corporate" label="Inactive Corporate"
value={ value={
users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate))) users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))

View File

@@ -2,20 +2,17 @@
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats"; import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers"; 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 UserList from "@/pages/(admin)/Lists/UserList";
import {dateSorter} from "@/utils"; import {dateSorter} from "@/utils";
import moment from "moment"; import moment from "moment";
import {useEffect, useState} from "react"; 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 UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups"; 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 IconCard from "./IconCard";
import GroupList from "@/pages/(admin)/Lists/GroupList"; import usePaymentStatusUsers from '@/hooks/usePaymentStatusUsers';
interface Props { interface Props {
user: User; user: User;
@@ -29,6 +26,7 @@ export default function AgentDashboard({user}: Props) {
const {stats} = useStats(); const {stats} = useStats();
const {users, reload} = useUsers(); const {users, reload} = useUsers();
const {groups} = useGroups(user.id); const {groups} = useGroups(user.id);
const { pending, done } = usePaymentStatusUsers();
useEffect(() => { useEffect(() => {
setShowModal(!!selectedUser && page === ""); setShowModal(!!selectedUser && page === "");
@@ -40,9 +38,9 @@ export default function AgentDashboard({user}: Props) {
const inactiveReferredCorporateFilter = (x: User) => const inactiveReferredCorporateFilter = (x: User) =>
referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)); referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
const UserDisplay = (displayUser: User) => ( const UserDisplay = ({ displayUser, allowClick = true }: {displayUser: User, allowClick?: boolean}) => (
<div <div
onClick={() => 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"> className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" /> <img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col gap-1 items-start">
@@ -66,7 +64,7 @@ export default function AgentDashboard({user}: Props) {
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<h2 className="text-2xl font-semibold">Corporate ({users.filter(referredCorporateFilter).length})</h2> <h2 className="text-2xl font-semibold">Referred Corporate ({users.filter(referredCorporateFilter).length})</h2>
</div> </div>
<UserList user={user} filters={[referredCorporateFilter]} /> <UserList user={user} filters={[referredCorporateFilter]} />
@@ -84,7 +82,7 @@ export default function AgentDashboard({user}: Props) {
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<h2 className="text-2xl font-semibold">Inactive Corporate ({users.filter(inactiveReferredCorporateFilter).length})</h2> <h2 className="text-2xl font-semibold">Inactive Referred Corporate ({users.filter(inactiveReferredCorporateFilter).length})</h2>
</div> </div>
<UserList user={user} filters={[inactiveReferredCorporateFilter]} /> <UserList user={user} filters={[inactiveReferredCorporateFilter]} />
@@ -106,7 +104,26 @@ export default function AgentDashboard({user}: Props) {
</div> </div>
<h2 className="text-2xl font-semibold">Corporate ({users.filter(filter).length})</h2> <h2 className="text-2xl font-semibold">Corporate ({users.filter(filter).length})</h2>
</div> </div>
<UserList user={user} filters={[filter]} />
</>
);
};
const CorporatePaidStatusList = ({ paid }: {paid: Boolean}) => {
const list = paid ? done : pending;
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
return (
<>
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">{paid ? 'Payment Done' : 'Pending Payment'} ({list.length})</h2>
</div>
<UserList user={user} filters={[filter]} /> <UserList user={user} filters={[filter]} />
</> </>
); );
@@ -117,15 +134,15 @@ export default function AgentDashboard({user}: Props) {
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:gap-4 text-center"> <section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:gap-4 text-center">
<IconCard <IconCard
onClick={() => setPage("referredCorporate")} onClick={() => setPage("referredCorporate")}
Icon={BsPersonFill} Icon={BsBank}
label="Corporate" label="Referred Corporate"
value={users.filter(referredCorporateFilter).length} value={users.filter(referredCorporateFilter).length}
color="purple" color="purple"
/> />
<IconCard <IconCard
onClick={() => setPage("inactiveReferredCorporate")} onClick={() => setPage("inactiveReferredCorporate")}
Icon={BsPersonFill} Icon={BsBank}
label="Inactive Corporate" label="Inactive Referred Corporate"
value={users.filter(inactiveReferredCorporateFilter).length} value={users.filter(inactiveReferredCorporateFilter).length}
color="rose" color="rose"
/> />
@@ -136,17 +153,31 @@ export default function AgentDashboard({user}: Props) {
value={users.filter(corporateFilter).length} value={users.filter(corporateFilter).length}
color="purple" color="purple"
/> />
<IconCard
onClick={() => setPage("paymentdone")}
Icon={BsCurrencyDollar}
label="Payment Done"
value={done.length}
color="purple"
/>
<IconCard
onClick={() => setPage("paymentdone")}
Icon={BsCurrencyDollar}
label="Pending Payment"
value={pending.length}
color="rose"
/>
</section> </section>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between"> <section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Latest Corporate</span> <span className="p-4">Latest Referred Corporate</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {users
.filter(referredCorporateFilter) .filter(referredCorporateFilter)
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) .sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} displayUser={x} />
))} ))}
</div> </div>
</div> </div>
@@ -157,12 +188,12 @@ export default function AgentDashboard({user}: Props) {
.filter(corporateFilter) .filter(corporateFilter)
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) .sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} displayUser={x} allowClick={false} />
))} ))}
</div> </div>
</div> </div>
<div className="bg-white shadow flex flex-col rounded-xl w-full"> <div className="bg-white shadow flex flex-col rounded-xl w-full">
<span className="p-4">Corporate expiring in 1 month</span> <span className="p-4">Referenced corporate expiring in 1 month</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {users
.filter( .filter(
@@ -172,7 +203,7 @@ export default function AgentDashboard({user}: Props) {
moment().isBefore(moment(x.subscriptionExpirationDate)), moment().isBefore(moment(x.subscriptionExpirationDate)),
) )
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} displayUser={x} />
))} ))}
</div> </div>
</div> </div>
@@ -205,6 +236,8 @@ export default function AgentDashboard({user}: Props) {
{page === "referredCorporate" && <ReferredCorporateList />} {page === "referredCorporate" && <ReferredCorporateList />}
{page === "corporate" && <CorporateList />} {page === "corporate" && <CorporateList />}
{page === "inactiveReferredCorporate" && <InactiveReferredCorporateList />} {page === "inactiveReferredCorporate" && <InactiveReferredCorporateList />}
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
{page === "" && <DefaultDashboard />} {page === "" && <DefaultDashboard />}
</> </>
); );

View File

@@ -103,13 +103,13 @@ const GroupTestReport = ({
{title} {title}
</Text> </Text>
</View> </View>
<View style={styles.textPadding}> <View style={styles.textMargin}>
<Text style={defaultTextStyle}>Date of Test: {date}</Text> <Text style={defaultTextStyle}>Date of Test: {date}</Text>
</View> </View>
<Text style={[styles.textFont, styles.textBold, { fontSize: 11 }]}> <Text style={[styles.textFont, styles.textBold, { fontSize: 11 }]}>
Candidate Information: Candidate Information:
</Text> </Text>
<View style={styles.textPadding}> <View style={styles.textMargin}>
<Text style={defaultTextStyle}>Name: {name}</Text> <Text style={defaultTextStyle}>Name: {name}</Text>
<Text style={defaultTextStyle}>ID: {id}</Text> <Text style={defaultTextStyle}>ID: {id}</Text>
<Text style={defaultTextStyle}>Email: {email}</Text> <Text style={defaultTextStyle}>Email: {email}</Text>
@@ -193,10 +193,8 @@ const GroupTestReport = ({
]} ]}
key={label} key={label}
> >
<Text style={customStyles.tableCell}> <Text style={customStyles.tableCell}>{label}</Text>
{label} <View style={[customStyles.tableCell, { flex: 2 }]}>
</Text>
<View style={[customStyles.tableCell, { flex: 2}]}>
<ProgressBar <ProgressBar
width={200} width={200}
height={18} height={18}

View File

@@ -14,8 +14,8 @@ export const styles = StyleSheet.create({
title: { title: {
textTransform: "uppercase", textTransform: "uppercase",
}, },
textPadding: { textMargin: {
margin: "8px", marginVertical: "8px",
}, },
separator: { separator: {
width: "100%", width: "100%",

View File

@@ -3,15 +3,20 @@ import { styles } from "./styles";
import { View, Text } from "@react-pdf/renderer"; import { View, Text } from "@react-pdf/renderer";
const TestReportFooter = () => ( const TestReportFooter = () => (
<View style={[{ paddingTop: 30, fontSize: 5, position: 'absolute', bottom: 30, left: 35, right: 35 }, styles.textFont,]}> <View
<View style={[
style={[ {
styles.spacedRow, paddingTop: 30,
{ fontSize: 5,
paddingHorizontal: 10, position: "absolute",
}, bottom: 30,
]} left: 35,
> right: 35,
},
styles.textFont,
]}
>
<View style={[styles.spacedRow, styles.textMargin]}>
<View> <View>
<Text>Validity</Text> <Text>Validity</Text>
<Text> <Text>

View File

@@ -68,19 +68,19 @@ const TestReport = ({
{title} {title}
</Text> </Text>
</View> </View>
<View style={styles.textPadding}> <View style={styles.textMargin}>
<Text style={defaultTextStyle}>Date of Test: {date}</Text> <Text style={defaultTextStyle}>Date of Test: {date}</Text>
</View> </View>
<Text style={[styles.textFont, styles.textBold, { fontSize: 11 }]}> <Text style={[styles.textFont, styles.textBold, { fontSize: 11 }]}>
Candidate Information: Candidate Information:
</Text> </Text>
<View style={styles.textPadding}> <View style={styles.textMargin}>
<Text style={defaultTextStyle}>Name: {name}</Text> <Text style={defaultTextStyle}>Name: {name}</Text>
<Text style={defaultTextStyle}>ID: {id}</Text> <Text style={defaultTextStyle}>ID: {id}</Text>
<Text style={defaultTextStyle}>Email: {email}</Text> <Text style={defaultTextStyle}>Email: {email}</Text>
<Text style={defaultTextStyle}>Gender: {gender}</Text> <Text style={defaultTextStyle}>Gender: {gender}</Text>
</View> </View>
<View style={{ height: '120px' }}> <View style={{ height: "120px" }}>
<Text <Text
style={[ style={[
styles.textFont, styles.textFont,
@@ -131,7 +131,9 @@ const TestReport = ({
}} }}
> >
{testDetails {testDetails
.filter(({ suggestions, evaluation }) => suggestions || evaluation) .filter(
({ suggestions, evaluation }) => suggestions || evaluation
)
.map(({ module, suggestions, evaluation }) => ( .map(({ module, suggestions, evaluation }) => (
<View key={module} style={customStyles.testDetails}> <View key={module} style={customStyles.testDetails}>
<Text style={[...defaultSkillsTitleStyle, styles.textBold]}> <Text style={[...defaultSkillsTitleStyle, styles.textBold]}>
@@ -139,15 +141,11 @@ const TestReport = ({
</Text> </Text>
<Text style={defaultSkillsTextStyle}>{evaluation}</Text> <Text style={defaultSkillsTextStyle}>{evaluation}</Text>
<Text style={defaultSkillsTextStyle}>{suggestions}</Text> <Text style={defaultSkillsTextStyle}>{suggestions}</Text>
</View> </View>
))} ))}
</View> </View>
<View style={styles.alignRightRow}> <View style={styles.alignRightRow}>
<Image <Image src={qrcode} style={styles.qrcode} />
src={qrcode}
style={styles.qrcode}
/>
</View> </View>
</View> </View>
<View style={[{ paddingBottom: 30 }, styles.separator]}></View> <View style={[{ paddingBottom: 30 }, styles.separator]}></View>

View File

@@ -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<PaymentsStatus>({
pending: [],
done: [],
});
const getData = () => {
axios.get<PaymentsStatus>("/api/payments/assigned").then((response) => {
setStatus(response.data);
});
};
useEffect(getData, []);
return { pending, done };
}

View File

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

View File

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