Updated the expiry date to be based on the expiry date

This commit is contained in:
Tiago Ribeiro
2024-12-30 15:36:20 +00:00
parent b52259794e
commit 17154be8bf
8 changed files with 164 additions and 18 deletions

View File

@@ -4,6 +4,11 @@ export interface Entity {
id: string;
label: string;
licenses: number;
expiryDate?: Date | null
payment?: {
currency: string
price: number
}
}
export interface Role {

View File

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

View File

@@ -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<Entity>("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<User>("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 });
}

View File

@@ -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<string[]>([]);
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) {
<Layout user={user}>
<section className="flex flex-col gap-0">
<div className="flex flex-col gap-3">
<div className="flex items-end justify-between">
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<Link
href="/entities"
@@ -298,6 +330,20 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
</Link>
<h2 className="font-bold text-2xl">{entity.label} {isAdmin(user) && `- ${entity.licenses || 0} licenses`}</h2>
</div>
{!isAdmin(user) && canPay && (
<Link
href="/payment"
className={clsx(
"p-2 w-full max-w-[200px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
!entity.expiryDate ? "!bg-mti-green-ultralight !border-mti-green-light" : expirationDateColor(entity.expiryDate),
"bg-white border-mti-gray-platinum",
)}>
{!entity.expiryDate && "Unlimited"}
{entity.expiryDate && moment(entity.expiryDate).format("DD/MM/YYYY")}
</Link>
)}
</div>
<div className="flex items-center gap-2">
<button
@@ -332,6 +378,61 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
</button>
</div>
</div>
{isAdmin(user) && (
<>
<Divider />
<div className="w-full flex justify-between items-center">
<div className="flex items-center gap-4 w-full">
{!!expiryDate && (
<ReactDatePicker
className={clsx(
"p-2 w-full max-w-[200px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"hover:border-mti-purple tooltip",
!expiryDate ? "!bg-mti-green-ultralight !border-mti-green-light" : expirationDateColor(expiryDate),
"transition duration-300 ease-in-out",
)}
filterDate={(date) => moment(date).isAfter(new Date())}
dateFormat="dd/MM/yyyy"
selected={expiryDate ? moment(expiryDate).toDate() : null}
onChange={(date) => setExpiryDate(date)}
/>
)}
{!expiryDate && (
<div
className={clsx(
"p-2 w-full max-w-[200px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
!expiryDate ? "!bg-mti-green-ultralight !border-mti-green-light" : expirationDateColor(expiryDate),
"bg-white border-mti-gray-platinum",
)}
>
Unlimited
</div>
)}
<Checkbox
isChecked={!!expiryDate}
onChange={(checked: boolean) => setExpiryDate(checked ? entity.expiryDate || new Date() : null)}
>
Enable expiry date
</Checkbox>
</div>
<button
onClick={updateExpiryDate}
disabled={expiryDate === entity.expiryDate}
className="flex w-fit text-nowrap items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
<BsCheck />
<span className="text-xs">Apply Change</span>
</button>
</div>
</>
)}
<Divider />
<div className="flex items-center justify-between mb-4">
<span className="font-semibold text-xl">Members ({users.length})</span>

View File

@@ -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[] = [

View File

@@ -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<User>();
const [selectedAgentUser, setSelectedAgentUser] = useState<User>();
const [isCreatingPayment, setIsCreatingPayment] = useState(false);
@@ -281,9 +295,9 @@ export default function PaymentRecord() {
const [displayPayments, setDisplayPayments] = useState<Payment[]>([]);
const [corporate, setCorporate] = useState<User>();
const [entity, setEntity] = useState<Entity>();
const [agent, setAgent] = useState<User>();
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() {
<Checkbox
isChecked={value}
onChange={(e) => {
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;

View File

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

View File

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