diff --git a/src/interfaces/entity.ts b/src/interfaces/entity.ts index da1e8e5e..b10d547e 100644 --- a/src/interfaces/entity.ts +++ b/src/interfaces/entity.ts @@ -4,6 +4,11 @@ export interface Entity { id: string; label: string; licenses: number; + expiryDate?: Date | null + payment?: { + currency: string + price: number + } } export interface Role { diff --git a/src/interfaces/paypal.ts b/src/interfaces/paypal.ts index b1b3e8b0..d7c7c902 100644 --- a/src/interfaces/paypal.ts +++ b/src/interfaces/paypal.ts @@ -32,6 +32,7 @@ export type DurationUnit = "weeks" | "days" | "months" | "years"; export interface Payment { id: string; corporate: string; + entity?: string agent?: string; agentCommission: number; agentValue: number; diff --git a/src/pages/api/entities/[id]/index.ts b/src/pages/api/entities/[id]/index.ts index c329f832..2f438f20 100644 --- a/src/pages/api/entities/[id]/index.ts +++ b/src/pages/api/entities/[id]/index.ts @@ -6,9 +6,11 @@ import { deleteEntity, getEntity, getEntityWithRoles } from "@/utils/entities.be import client from "@/lib/mongodb"; import { Entity } from "@/interfaces/entity"; import { doesEntityAllow } from "@/utils/permissions"; -import { getUser } from "@/utils/users.be"; +import { getEntityUsers, getUser } from "@/utils/users.be"; import { requestUser } from "@/utils/api"; import { isAdmin } from "@/utils/users"; +import { filterBy, mapBy } from "@/utils"; +import { User } from "@/interfaces/user"; const db = client.db(process.env.MONGODB_DB); @@ -66,5 +68,22 @@ async function patch(req: NextApiRequest, res: NextApiResponse) { return res.status(200).json({ ok: entity.acknowledged }); } + if (req.body.expiryDate !== undefined) { + const entity = await getEntity(id) + const result = await db.collection("entities").updateOne({ id }, { $set: { expiryDate: req.body.expiryDate } }); + + const users = await getEntityUsers(id, 0, { + subscriptionExpirationDate: entity?.expiryDate, + $and: [ + { type: { $ne: "admin" } }, + { type: { $ne: "developer" } }, + ] + }) + + await db.collection("users").updateMany({ id: { $in: mapBy(users, 'id') } }, { $set: { subscriptionExpirationDate: req.body.expiryDate } }) + + return res.status(200).json({ ok: result.acknowledged }); + } + return res.status(200).json({ ok: true }); } diff --git a/src/pages/entities/[id]/index.tsx b/src/pages/entities/[id]/index.tsx index 1546d933..324c2227 100644 --- a/src/pages/entities/[id]/index.tsx +++ b/src/pages/entities/[id]/index.tsx @@ -2,6 +2,7 @@ import CardList from "@/components/High/CardList"; import Layout from "@/components/High/Layout"; import Select from "@/components/Low/Select"; +import Checkbox from "@/components/Low/Checkbox"; import Tooltip from "@/components/Low/Tooltip"; import { useEntityPermission } from "@/hooks/useEntityPermissions"; import { useListSearch } from "@/hooks/useListSearch"; @@ -27,7 +28,10 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { Divider } from "primereact/divider"; import { useEffect, useMemo, useState } from "react"; +import ReactDatePicker from "react-datepicker"; + import { + BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, @@ -43,6 +47,15 @@ import { } from "react-icons/bs"; import { toast } from "react-toastify"; +const expirationDateColor = (date: Date) => { + const momentDate = moment(date); + const today = moment(new Date()); + + if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light"; + if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light"; + if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light"; +}; + export const getServerSideProps = withIronSessionSsr(async ({ req, params }) => { const user = req.session.user as User; @@ -88,6 +101,7 @@ export default function Home({ user, entity, users, linkedUsers }: Props) { const [isAdding, setIsAdding] = useState(false); const [isLoading, setIsLoading] = useState(false); const [selectedUsers, setSelectedUsers] = useState([]); + const [expiryDate, setExpiryDate] = useState(entity?.expiryDate) const router = useRouter(); @@ -99,6 +113,7 @@ export default function Home({ user, entity, users, linkedUsers }: Props) { const canRemoveMembers = useEntityPermission(user, entity, "remove_from_entity") const canAssignRole = useEntityPermission(user, entity, "assign_to_role") + const canPay = useEntityPermission(user, entity, 'pay_entity') const toggleUser = (u: User) => setSelectedUsers((prev) => (prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id])); @@ -166,6 +181,23 @@ export default function Home({ user, entity, users, linkedUsers }: Props) { .finally(() => setIsLoading(false)); }; + const updateExpiryDate = () => { + if (!isAdmin(user)) return; + + setIsLoading(true); + axios + .patch(`/api/entities/${entity.id}`, { expiryDate }) + .then(() => { + toast.success("The entity has been updated successfully!"); + router.replace(router.asPath); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + const editLicenses = () => { if (!isAdmin(user)) return; @@ -289,7 +321,7 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
-
+

{entity.label} {isAdmin(user) && `- ${entity.licenses || 0} licenses`}

+ + {!isAdmin(user) && canPay && ( + + {!entity.expiryDate && "Unlimited"} + {entity.expiryDate && moment(entity.expiryDate).format("DD/MM/YYYY")} + + )}
+ + {isAdmin(user) && ( + <> + + +
+
+ {!!expiryDate && ( + moment(date).isAfter(new Date())} + dateFormat="dd/MM/yyyy" + selected={expiryDate ? moment(expiryDate).toDate() : null} + onChange={(date) => setExpiryDate(date)} + /> + )} + + {!expiryDate && ( +
+ Unlimited +
+ )} + + setExpiryDate(checked ? entity.expiryDate || new Date() : null)} + > + Enable expiry date + +
+ + + +
+ + )} +
Members ({users.length}) diff --git a/src/pages/entities/[id]/roles/[role].tsx b/src/pages/entities/[id]/roles/[role].tsx index 037fb571..5c41954f 100644 --- a/src/pages/entities/[id]/roles/[role].tsx +++ b/src/pages/entities/[id]/roles/[role].tsx @@ -98,7 +98,9 @@ const ENTITY_MANAGEMENT: PermissionLayout[] = [ { label: "Delete Entity Role", key: "delete_entity_role" }, { label: "Download Statistics Report", key: "download_statistics_report" }, { label: "Edit Grading System", key: "edit_grading_system" }, - { label: "View Student Performance", key: "view_student_performance" } + { label: "View Student Performance", key: "view_student_performance" }, + { label: "Pay for Entity", key: "pay_entity" }, + { label: "View Payment Record", key: "view_payment_record" } ] const ASSIGNMENT_MANAGEMENT: PermissionLayout[] = [ diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 2be5369a..daf277ce 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -30,9 +30,12 @@ import { toFixedNumber } from "@/utils/number"; import { CSVLink } from "react-csv"; import { Tab } from "@headlessui/react"; import { useListSearch } from "@/hooks/useListSearch"; -import { checkAccess, getTypesOfUser } from "@/utils/permissions"; +import { checkAccess, findAllowedEntities, getTypesOfUser } from "@/utils/permissions"; import { requestUser } from "@/utils/api"; -import { redirect } from "@/utils"; +import { mapBy, redirect, serialize } from "@/utils"; +import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be"; +import { isAdmin } from "@/utils/users"; +import { Entity, EntityWithRoles } from "@/interfaces/entity"; export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { const user = await requestUser(req, res) @@ -42,8 +45,13 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { return redirect("/") } + const entityIDs = mapBy(user.entities, 'id') + const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs) + + const allowedEntities = findAllowedEntities(user, entities, "view_payment_record") + return { - props: { user }, + props: serialize({ user, entities: allowedEntities }), }; }, sessionOptions); @@ -273,7 +281,13 @@ interface PaypalPaymentWithUserData extends PaypalPayment { } const paypalFilterRows = [["email"], ["name"], ["orderId"], ["value"]]; -export default function PaymentRecord() { + +interface Props { + user: User + entities: EntityWithRoles[] +} + +export default function PaymentRecord({ user, entities }: Props) { const [selectedCorporateUser, setSelectedCorporateUser] = useState(); const [selectedAgentUser, setSelectedAgentUser] = useState(); const [isCreatingPayment, setIsCreatingPayment] = useState(false); @@ -281,9 +295,9 @@ export default function PaymentRecord() { const [displayPayments, setDisplayPayments] = useState([]); const [corporate, setCorporate] = useState(); + const [entity, setEntity] = useState(); const [agent, setAgent] = useState(); - const { user } = useUser({ redirectTo: "/login" }); const { users, reload: reloadUsers } = useUsers(); const { payments: originalPayments, reload: reloadPayment } = usePayments(); const { payments: paypalPayments, reload: reloadPaypalPayment } = usePaypalPayments(); @@ -341,17 +355,17 @@ export default function PaymentRecord() { useEffect(() => { setFilters((prev) => [ - ...prev.filter((x) => x.id !== "corporate-filter"), - ...(!corporate + ...prev.filter((x) => x.id !== "entity-filter"), + ...(!entity ? [] : [ { - id: "corporate-filter", - filter: (p: Payment) => p.corporate === corporate.id, + id: "entity-filter", + filter: (p: Payment) => p.entity === entity.id, }, ]), ]); - }, [corporate]); + }, [entity]); useEffect(() => { setFilters((prev) => [ @@ -675,7 +689,7 @@ export default function PaymentRecord() { { - if (user?.type === agent || user?.type === "corporate" || value) return null; + if (user?.type === "agent" || user?.type === "corporate" || value) return null; if (!info.row.original.commissionTransfer || !info.row.original.corporateTransfer) return alert("All files need to be uploaded to consider it paid!"); if (!confirm(`Are you sure you want to consider this payment paid?`)) return null; diff --git a/src/pages/payment.tsx b/src/pages/payment.tsx index 49854d22..579a88db 100644 --- a/src/pages/payment.tsx +++ b/src/pages/payment.tsx @@ -7,7 +7,7 @@ import PaymentDue from "./(status)/PaymentDue"; import { useRouter } from "next/router"; import { requestUser } from "@/utils/api"; import { mapBy, redirect, serialize } from "@/utils"; -import { getEntities } from "@/utils/entities.be"; +import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be"; import { isAdmin } from "@/utils/users"; import { EntityWithRoles } from "@/interfaces/entity"; import { User } from "@/interfaces/user"; @@ -17,7 +17,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { if (!user) return redirect("/login") const entityIDs = mapBy(user.entities, 'id') - const entities = await getEntities(isAdmin(user) ? undefined : entityIDs) + const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs) return { props: serialize({ user, entities }), diff --git a/src/resources/entityPermissions.ts b/src/resources/entityPermissions.ts index 484672d1..b9c6c165 100644 --- a/src/resources/entityPermissions.ts +++ b/src/resources/entityPermissions.ts @@ -63,7 +63,9 @@ export type RolePermission = "upload_classroom" | "download_user_list" | "view_student_record" | - "download_student_record" + "download_student_record" | + "pay_entity" | + "view_payment_record" export const DEFAULT_PERMISSIONS: RolePermission[] = [ "view_students", @@ -140,5 +142,7 @@ export const ADMIN_PERMISSIONS: RolePermission[] = [ "upload_classroom", "download_user_list", "view_student_record", - "download_student_record" + "download_student_record", + "pay_entity", + "view_payment_record" ]