From b757cbbed727668e120199dcda589788b68fcf84 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 20 Jan 2024 01:09:03 +0000 Subject: [PATCH 1/6] Solved a date sorting bug --- src/dashboards/Admin.tsx | 33 ++++++++++++++++++++------------- src/pages/api/register.ts | 2 +- src/utils/index.ts | 8 ++++---- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/dashboards/Admin.tsx b/src/dashboards/Admin.tsx index 8737cb0b..e01360b7 100644 --- a/src/dashboards/Admin.tsx +++ b/src/dashboards/Admin.tsx @@ -7,13 +7,22 @@ import UserList from "@/pages/(admin)/Lists/UserList"; import {dateSorter} from "@/utils"; import moment from "moment"; import {useEffect, useState} from "react"; -import {BsArrowLeft, BsBriefcaseFill, BsGlobeCentralSouthAsia, BsPerson, BsPersonFill, BsPencilSquare, BsBank, BsCurrencyDollar} from "react-icons/bs"; +import { + BsArrowLeft, + BsBriefcaseFill, + BsGlobeCentralSouthAsia, + BsPerson, + BsPersonFill, + BsPencilSquare, + BsBank, + BsCurrencyDollar, +} from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; import IconCard from "./IconCard"; import useFilterStore from "@/stores/listFilterStore"; import {useRouter} from "next/router"; -import usePaymentStatusUsers from '@/hooks/usePaymentStatusUsers'; +import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers"; interface Props { user: User; @@ -27,7 +36,7 @@ export default function AdminDashboard({user}: Props) { const {stats} = useStats(user.id); const {users, reload} = useUsers(); const {groups} = useGroups(); - const { pending, done } = usePaymentStatusUsers(); + const {pending, done} = usePaymentStatusUsers(); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); @@ -148,7 +157,7 @@ export default function AdminDashboard({user}: Props) { ); - const CorporatePaidStatusList = ({ paid }: {paid: Boolean}) => { + const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => { const list = paid ? done : pending; const filter = (x: User) => x.type === "corporate" && list.includes(x.id); @@ -161,7 +170,9 @@ export default function AdminDashboard({user}: Props) { Back -

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

+

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

@@ -290,13 +301,7 @@ export default function AdminDashboard({user}: Props) { } color="rose" /> - setPage("paymentdone")} - Icon={BsCurrencyDollar} - label="Payment Done" - value={done.length} - color="purple" - /> + setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" /> setPage("paymentpending")} Icon={BsCurrencyDollar} @@ -323,7 +328,9 @@ export default function AdminDashboard({user}: Props) {
{users .filter((x) => x.type === "corporate") - .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .sort((a, b) => { + return dateSorter(a, b, "desc", "registrationDate"); + }) .map((x) => ( ))} diff --git a/src/pages/api/register.ts b/src/pages/api/register.ts index de026210..ec609d50 100644 --- a/src/pages/api/register.ts +++ b/src/pages/api/register.ts @@ -69,7 +69,7 @@ async function registerIndividual(req: NextApiRequest, res: NextApiResponse) { type: email.endsWith("@ecrop.dev") ? "developer" : codeData ? codeData.type : "student", subscriptionExpirationDate: codeData ? codeData.expiryDate : moment().subtract(1, "days").toISOString(), ...(passport_id ? {demographicInformation: {passport_id}} : {}), - registrationDate: new Date(), + registrationDate: new Date().toISOString(), status: code ? "active" : "paymentDue", }; diff --git a/src/utils/index.ts b/src/utils/index.ts index cb52f2a1..8c3650f2 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,11 +1,11 @@ import moment from "moment"; export function dateSorter(a: any, b: any, direction: "asc" | "desc", key: string) { - if (!a[key] && b[key]) return direction === "asc" ? -1 : 1; - if (a[key] && !b[key]) return direction === "asc" ? 1 : -1; if (!a[key] && !b[key]) return 0; - if (moment(a[key]).isAfter(b[key])) return direction === "asc" ? -1 : 1; - if (moment(b[key]).isAfter(a[key])) return direction === "asc" ? 1 : -1; + if (a[key] && !b[key]) return direction === "asc" ? -1 : 1; + if (!a[key] && b[key]) return direction === "asc" ? 1 : -1; + if (moment(a[key]).isAfter(b[key])) return direction === "asc" ? 1 : -1; + if (moment(b[key]).isAfter(a[key])) return direction === "asc" ? -1 : 1; return 0; } From 5e8b6f96bbe5cf905bf4b8537e22f4a435dd97ee Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 20 Jan 2024 01:15:12 +0000 Subject: [PATCH 2/6] Added a timezone selector to the Demographic Input --- src/components/DemographicInformationInput.tsx | 12 +++++++++++- src/pages/profile.tsx | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/DemographicInformationInput.tsx b/src/components/DemographicInformationInput.tsx index c97a4aef..0728ca4e 100644 --- a/src/components/DemographicInformationInput.tsx +++ b/src/components/DemographicInformationInput.tsx @@ -1,5 +1,5 @@ import {EmploymentStatus, EMPLOYMENT_STATUS, Gender, User} from "@/interfaces/user"; -import {FormEvent, useState} from "react"; +import {FormEvent, useEffect, useState} from "react"; import countryCodes from "country-codes-list"; import {RadioGroup} from "@headlessui/react"; import Input from "./Low/Input"; @@ -12,6 +12,8 @@ import {KeyedMutator} from "swr"; import CountrySelect from "./Low/CountrySelect"; import GenderInput from "@/components/High/GenderInput"; import EmploymentStatusInput from "@/components/High/EmploymentStatusInput"; +import TimezoneSelect from "./Low/TImezoneSelect"; +import moment from "moment"; interface Props { user: User; @@ -25,6 +27,7 @@ export default function DemographicInformationInput({user, mutateUser}: Props) { const [gender, setGender] = useState(); const [employment, setEmployment] = useState(); const [position, setPosition] = useState(); + const [timezone, setTimezone] = useState(moment.tz.guess()); const [isLoading, setIsLoading] = useState(false); const [companyName, setCompanyName] = useState(); @@ -43,6 +46,7 @@ export default function DemographicInformationInput({user, mutateUser}: Props) { employment: user.type === "corporate" ? undefined : employment, position: user.type === "corporate" ? position : undefined, passport_id, + timezone, }, agentInformation: user.type === "agent" ? {companyName, commercialRegistration} : undefined, }) @@ -94,6 +98,12 @@ export default function DemographicInformationInput({user, mutateUser}: Props) { required /> )} + +
+ + +
+ {user.type === "corporate" && ( diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 12806fbb..8273741b 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -82,7 +82,7 @@ function UserProfile({user, mutateUser}: Props) { const [commercialRegistration, setCommercialRegistration] = useState( user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, ); - const [timezone, setTimezone] = useState(user.demographicInformation?.timezone || "UTC"); + const [timezone, setTimezone] = useState(user.demographicInformation?.timezone || moment.tz.guess()); const {groups} = useGroups(); const {users} = useUsers(); From 2ef86344cd590f54a1dcd50d40928ae66eb35046 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 20 Jan 2024 01:17:22 +0000 Subject: [PATCH 3/6] Updated the Assignment default start date to be the current time --- src/dashboards/AssignmentCreator.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/dashboards/AssignmentCreator.tsx b/src/dashboards/AssignmentCreator.tsx index ad0799e7..fd721ab4 100644 --- a/src/dashboards/AssignmentCreator.tsx +++ b/src/dashboards/AssignmentCreator.tsx @@ -34,9 +34,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro const [assignees, setAssignees] = useState(assignment?.assignees || []); const [name, setName] = useState(assignment?.name || generate({minLength: 6, maxLength: 8, min: 2, max: 3, join: " ", formatter: capitalize})); const [isLoading, setIsLoading] = useState(false); - const [startDate, setStartDate] = useState( - assignment ? moment(assignment.startDate).toDate() : moment().hours(0).minutes(0).add(1, "day").toDate(), - ); + const [startDate, setStartDate] = useState(assignment ? moment(assignment.startDate).toDate() : new Date()); const [endDate, setEndDate] = useState( assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(), ); @@ -200,7 +198,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro "transition duration-300 ease-in-out", )} popperClassName="!z-20" - filterDate={(date) => moment(date).isAfter(new Date())} + filterTime={(date) => moment(date).isSameOrAfter(new Date())} dateFormat="dd/MM/yyyy HH:mm" selected={startDate} showTimeSelect @@ -216,7 +214,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro "transition duration-300 ease-in-out", )} popperClassName="!z-20" - filterDate={(date) => moment(date).isAfter(startDate)} + filterTime={(date) => moment(date).isAfter(startDate)} dateFormat="dd/MM/yyyy HH:mm" selected={endDate} showTimeSelect From 9773f1da725d846c65732d7a69ba485285f61c44 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 20 Jan 2024 13:33:22 +0000 Subject: [PATCH 4/6] Updated the user deletion to allow corporate to remove users from their groups, instead of deleting them --- src/constants/userPermissions.ts | 2 +- src/pages/api/user.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/constants/userPermissions.ts b/src/constants/userPermissions.ts index b75ea983..e0251e8c 100644 --- a/src/constants/userPermissions.ts +++ b/src/constants/userPermissions.ts @@ -10,7 +10,7 @@ export const PERMISSIONS = { developer: ["developer"], }, deleteUser: { - student: ["teacher", "corporate", "developer", "admin"], + student: ["corporate", "developer", "admin"], teacher: ["corporate", "developer", "admin"], corporate: ["admin", "developer"], admin: ["developer", "admin"], diff --git a/src/pages/api/user.ts b/src/pages/api/user.ts index 1e975ac4..921c0222 100644 --- a/src/pages/api/user.ts +++ b/src/pages/api/user.ts @@ -1,6 +1,6 @@ import {PERMISSIONS} from "@/constants/userPermissions"; import {app, adminApp} from "@/firebase"; -import {User} from "@/interfaces/user"; +import {Group, User} from "@/interfaces/user"; import {sessionOptions} from "@/lib/session"; import {collection, deleteDoc, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore"; import {getAuth} from "firebase-admin/auth"; @@ -43,6 +43,19 @@ async function del(req: NextApiRequest, res: NextApiResponse) { const targetUser = {...docTargetUser.data(), id: docTargetUser.id} as User; + if (user.type === "corporate" && (targetUser.type === "student" || targetUser.type === "teacher")) { + res.json({ok: true}); + + const userParticipantGroup = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id))); + await Promise.all([ + ...userParticipantGroup.docs + .filter((x) => (x.data() as Group).admin === user.id) + .map(async (x) => await setDoc(x.ref, {participants: x.data().participants.filter((y: string) => y !== id)}, {merge: true})), + ]); + + return; + } + const permission = PERMISSIONS.deleteUser[targetUser.type]; if (!permission.includes(user.type)) { res.status(403).json({ok: false}); From 8eb8a7af46aef812707173c67a4ffe00658320b1 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 20 Jan 2024 15:09:42 +0000 Subject: [PATCH 5/6] Added a new card for the Corporate to show their user balance --- src/dashboards/Corporate.tsx | 17 ++++++++++++++--- src/dashboards/IconCard.tsx | 9 +++++++-- src/dashboards/Teacher.tsx | 3 ++- src/hooks/useCodes.tsx | 21 +++++++++++++++++++++ src/interfaces/user.ts | 11 +++++++++++ src/pages/api/code/index.ts | 20 ++++++++++++++++++++ 6 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 src/hooks/useCodes.tsx diff --git a/src/dashboards/Corporate.tsx b/src/dashboards/Corporate.tsx index 600d0f89..49bad1e8 100644 --- a/src/dashboards/Corporate.tsx +++ b/src/dashboards/Corporate.tsx @@ -2,7 +2,7 @@ import Modal from "@/components/Modal"; import useStats from "@/hooks/useStats"; import useUsers from "@/hooks/useUsers"; -import {Group, Stat, User} from "@/interfaces/user"; +import {CorporateUser, Group, Stat, User} from "@/interfaces/user"; import UserList from "@/pages/(admin)/Lists/UserList"; import {dateSorter} from "@/utils"; import moment from "moment"; @@ -20,6 +20,9 @@ import { BsPersonFillGear, BsPersonGear, BsPencilSquare, + BsPersonBadge, + BsPersonCheck, + BsPeople, } from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; @@ -31,9 +34,10 @@ import IconCard from "./IconCard"; import GroupList from "@/pages/(admin)/Lists/GroupList"; import useFilterStore from "@/stores/listFilterStore"; import {useRouter} from "next/router"; +import useCodes from "@/hooks/useCodes"; interface Props { - user: User; + user: CorporateUser; } export default function CorporateDashboard({user}: Props) { @@ -43,6 +47,7 @@ export default function CorporateDashboard({user}: Props) { const {stats} = useStats(); const {users, reload} = useUsers(); + const {codes} = useCodes(user.id); const {groups} = useGroups(user.id); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); @@ -187,7 +192,13 @@ export default function CorporateDashboard({user}: Props) { value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)} color="purple" /> - setPage("groups")} Icon={BsPersonAdd} label="Groups" value={groups.length} color="purple" /> + setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" /> + void; } -export default function IconCard({Icon, label, value, color, onClick}: Props) { +export default function IconCard({Icon, label, value, color, tooltip, onClick}: Props) { const colorClasses: {[key in typeof color]: string} = { purple: "text-mti-purple-light", red: "text-mti-red-light", @@ -19,7 +20,11 @@ export default function IconCard({Icon, label, value, color, onClick}: Props) { return (
+ className={clsx( + "bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center text-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300", + tooltip && "tooltip tooltip-bottom", + )} + data-tip={tooltip}> {label} diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index be616639..e40d5a28 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -19,6 +19,7 @@ import { BsEnvelopePaper, BsGlobeCentralSouthAsia, BsPaperclip, + BsPeople, BsPerson, BsPersonAdd, BsPersonFill, @@ -271,7 +272,7 @@ export default function TeacherDashboard({user}: Props) { value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)} color="purple" /> - setPage("groups")} /> + setPage("groups")} />
setPage("assignments")} className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"> diff --git a/src/hooks/useCodes.tsx b/src/hooks/useCodes.tsx new file mode 100644 index 00000000..b88416a1 --- /dev/null +++ b/src/hooks/useCodes.tsx @@ -0,0 +1,21 @@ +import {Code, Group, User} from "@/interfaces/user"; +import axios from "axios"; +import {useEffect, useState} from "react"; + +export default function useCodes(creator?: string) { + const [codes, setCodes] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = () => { + setIsLoading(true); + axios + .get(`/api/code${creator ? `?creator=${creator}` : ""}`) + .then((response) => setCodes(response.data)) + .finally(() => setIsLoading(false)); + }; + + useEffect(getData, [creator]); + + return {codes, isLoading, isError, reload: getData}; +} diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index dec052ef..b14bf510 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -127,5 +127,16 @@ export interface Group { disableEditing?: boolean; } +export interface Code { + code: string; + creator: string; + expiryDate: Date; + type: Type; + userId?: string; + email?: string; + name?: string; + passport_id?: string; +} + export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent"; export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent"]; diff --git a/src/pages/api/code/index.ts b/src/pages/api/code/index.ts index 2e315cb6..39787f11 100644 --- a/src/pages/api/code/index.ts +++ b/src/pages/api/code/index.ts @@ -14,6 +14,26 @@ const db = getFirestore(app); export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return get(req, res); + if (req.method === "POST") return post(req, res); + + return res.status(404).json({ok: false}); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"}); + return; + } + + const {creator} = req.query as {creator?: string}; + const q = query(collection(db, "codes"), where("creator", "==", creator)); + const snapshot = await getDocs(creator ? q : collection(db, "codes")); + + res.status(200).json(snapshot.docs.map((doc) => doc.data())); +} + +async function post(req: NextApiRequest, res: NextApiResponse) { if (!req.session.user) { res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"}); return; From 3de0357369e33ae4266160ee2e373827218e72b6 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 20 Jan 2024 15:24:02 +0000 Subject: [PATCH 6/6] Oops, left an ID accidentally --- src/dashboards/Student.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index f55bf16e..75d4bd89 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -42,7 +42,7 @@ export default function StudentDashboard({user}: Props) { const setAssignment = useExamStore((state) => state.setAssignment); useEffect(() => { - getUserCorporate("IXdh9EQziAVXXh0jOiC5cPVlgS82").then(setCorporateUserToShow); + getUserCorporate(user.id).then(setCorporateUserToShow); }, [user]); const startAssignment = (assignment: Assignment) => {