diff --git a/src/components/High/Layout.tsx b/src/components/High/Layout.tsx index c29d62c4..502384e1 100644 --- a/src/components/High/Layout.tsx +++ b/src/components/High/Layout.tsx @@ -1,8 +1,8 @@ import useEntities from "@/hooks/useEntities"; import { EntityWithRoles } from "@/interfaces/entity"; -import {User} from "@/interfaces/user"; +import { User } from "@/interfaces/user"; import clsx from "clsx"; -import {useRouter} from "next/router"; +import { useRouter } from "next/router"; import { ToastContainer } from "react-toastify"; import Navbar from "../Navbar"; import Sidebar from "../Sidebar"; @@ -23,19 +23,19 @@ export default function Layout({ user, children, className, - bgColor="bg-white", + bgColor = "bg-white", hideSidebar, navDisabled = false, focusMode = false, onFocusLayerMouseEnter }: Props) { const router = useRouter(); - const {entities} = useEntities() + const { entities } = useEntities() return (
- {!hideSidebar && ( + {!hideSidebar && user && ( )}
- {!hideSidebar && ( + {!hideSidebar && user && ( void; } @@ -29,6 +30,7 @@ export default function Input({ className, roundness = "full", disabled = false, + thin = false, min, onChange, }: Props) { @@ -95,9 +97,10 @@ export default function Input({ min={type === "number" ? (min ?? 0) : undefined} placeholder={placeholder} className={clsx( - "px-8 py-6 text-sm font-normal bg-white border border-mti-gray-platinum focus:outline-none", + "px-8 text-sm font-normal bg-white border border-mti-gray-platinum focus:outline-none", "placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed", roundness === "full" ? "rounded-full" : "rounded-xl", + thin ? 'py-4' : 'py-6' )} required={required} defaultValue={defaultValue} diff --git a/src/components/Medium/RecordFilter.tsx b/src/components/Medium/RecordFilter.tsx index a8e5ce89..d3cbdc31 100644 --- a/src/components/Medium/RecordFilter.tsx +++ b/src/components/Medium/RecordFilter.tsx @@ -8,163 +8,166 @@ import useGroups from "@/hooks/useGroups"; import useRecordStore from "@/stores/recordStore"; import { EntityWithRoles } from "@/interfaces/entity"; import { mapBy } from "@/utils"; +import { useAllowedEntities } from "@/hooks/useEntityPermissions"; type TimeFilter = "months" | "weeks" | "days"; type Filter = TimeFilter | "assignments" | undefined; interface Props { - user: User; - entities: EntityWithRoles[] - users: User[] - filterState: { - filter: Filter, - setFilter: React.Dispatch> - }, - assignments?: boolean; - children?: ReactNode + user: User; + entities: EntityWithRoles[] + users: User[] + filterState: { + filter: Filter, + setFilter: React.Dispatch> + }, + assignments?: boolean; + children?: ReactNode } const defaultSelectableCorporate = { - value: "", - label: "All", + value: "", + label: "All", }; const RecordFilter: React.FC = ({ - user, - entities, - users, - filterState, - assignments = true, - children + user, + entities, + users, + filterState, + assignments = true, + children }) => { - const { filter, setFilter } = filterState; + const { filter, setFilter } = filterState; - const [entity, setEntity] = useState() + const [entity, setEntity] = useState() - const [, setStatsUserId] = useRecordStore((state) => [ - state.selectedUser, - state.setSelectedUser - ]); + const [, setStatsUserId] = useRecordStore((state) => [ + state.selectedUser, + state.setSelectedUser + ]); - const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [users, entity]) + const allowedViewEntities = useAllowedEntities(user, entities, 'view_student_record') - useEffect(() => setStatsUserId(user.id), [setStatsUserId, user.id]) + const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [users, entity]) - const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => { - setFilter((prev) => (prev === value ? undefined : value)); - }; + useEffect(() => setStatsUserId(user.id), [setStatsUserId, user.id]) - return ( -
-
- {checkAccess(user, ["developer", "admin", "mastercorporate"]) && !children && ( - <> -
- + const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => { + setFilter((prev) => (prev === value ? undefined : value)); + }; - ({ - value: x.id, - label: `${x.name} - ${x.email}`, - }))} - defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}} - onChange={(value) => setStatsUserId(value?.value!)} - styles={{ - menuPortal: (base) => ({ ...base, zIndex: 9999 }), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} - /> -
- - )} - {(user.type === "corporate" || user.type === "teacher") && !children && ( -
- + ({ - value: x.id, - label: `${x.name} - ${x.email}`, - }))} - defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}} - onChange={(value) => setStatsUserId(value?.value!)} - styles={{ - menuPortal: (base) => ({ ...base, zIndex: 9999 }), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} - /> -
- )} - {children} -
-
- {assignments && ( - - )} - - - -
-
- ); + ({ + value: x.id, + label: `${x.name} - ${x.email}`, + }))} + defaultValue={{ value: user.id, label: `${user.name} - ${user.email}` }} + onChange={(value) => setStatsUserId(value?.value!)} + styles={{ + menuPortal: (base) => ({ ...base, zIndex: 9999 }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> +
+ )} + {children} + +
+ {assignments && ( + + )} + + + +
+ + ); } export default RecordFilter; diff --git a/src/components/Medium/StatGridItem.tsx b/src/components/Medium/StatGridItem.tsx index aa025517..976edd99 100644 --- a/src/components/Medium/StatGridItem.tsx +++ b/src/components/Medium/StatGridItem.tsx @@ -82,7 +82,7 @@ interface StatsGridItemProps { selectedTrainingExams?: string[]; maxTrainingExams?: number; setSelectedTrainingExams?: React.Dispatch>; - renderPdfIcon: (session: string, color: string, textColor: string) => React.ReactNode; + renderPdfIcon?: (session: string, color: string, textColor: string) => React.ReactNode; } const StatsGridItem: React.FC = ({ @@ -236,7 +236,7 @@ const StatsGridItem: React.FC = ({ {renderLevelScore()} )} - {shouldRenderPDFIcon() && renderPdfIcon(session, textColor, textColor)} + {shouldRenderPDFIcon() && renderPdfIcon && renderPdfIcon(session, textColor, textColor)} {examNumber === undefined ? ( <> diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index f2bc5a42..a18f11e5 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -65,28 +65,28 @@ export default function Navbar({ user, path, navDisabled = false, focusMode = fa { module: "reading", icon: () => , - achieved: user.levels?.reading || 0 >= user.desiredLevels?.reading || 9, + achieved: user?.levels?.reading || 0 >= user?.desiredLevels?.reading || 9, }, { module: "listening", icon: () => , - achieved: user.levels?.listening || 0 >= user.desiredLevels?.listening || 9, + achieved: user?.levels?.listening || 0 >= user?.desiredLevels?.listening || 9, }, { module: "writing", icon: () => , - achieved: user.levels?.writing || 0 >= user.desiredLevels?.writing || 9, + achieved: user?.levels?.writing || 0 >= user?.desiredLevels?.writing || 9, }, { module: "speaking", icon: () => , - achieved: user.levels?.speaking || 0 >= user.desiredLevels?.speaking || 9, + achieved: user?.levels?.speaking || 0 >= user?.desiredLevels?.speaking || 9, }, { module: "level", icon: () => , - achieved: user.levels?.level || 0 >= user.desiredLevels?.level || 9, + achieved: user?.levels?.level || 0 >= user?.desiredLevels?.level || 9, }, ]; diff --git a/src/components/PaymobPayment.tsx b/src/components/PaymobPayment.tsx index 47cec9d6..9df12935 100644 --- a/src/components/PaymobPayment.tsx +++ b/src/components/PaymobPayment.tsx @@ -1,15 +1,17 @@ -import {PaymentIntention} from "@/interfaces/paymob"; -import {DurationUnit} from "@/interfaces/paypal"; -import {User} from "@/interfaces/user"; +import { Entity } from "@/interfaces/entity"; +import { PaymentIntention } from "@/interfaces/paymob"; +import { DurationUnit } from "@/interfaces/paypal"; +import { User } from "@/interfaces/user"; import axios from "axios"; -import {useRouter} from "next/router"; -import {useState} from "react"; +import { useRouter } from "next/router"; +import { useState } from "react"; import Button from "./Low/Button"; import Input from "./Low/Input"; import Modal from "./Modal"; interface Props { user: User; + entity?: Entity currency: string; price: number; setIsPaymentLoading: (v: boolean) => void; @@ -18,7 +20,7 @@ interface Props { onSuccess: (duration: number, duration_unit: DurationUnit) => void; } -export default function PaymobPayment({user, price, setIsPaymentLoading, currency, duration, duration_unit, onSuccess}: Props) { +export default function PaymobPayment({ user, entity, price, setIsPaymentLoading, currency, duration, duration_unit, onSuccess }: Props) { const [isLoading, setIsLoading] = useState(false); const router = useRouter(); @@ -56,10 +58,11 @@ export default function PaymobPayment({user, price, setIsPaymentLoading, currenc userID: user.id, duration, duration_unit, + entity: entity?.id }, }; - const response = await axios.post<{iframeURL: string}>(`/api/paymob`, paymentIntention); + const response = await axios.post<{ iframeURL: string }>(`/api/paymob`, paymentIntention); router.push(response.data.iframeURL); } catch (error) { diff --git a/src/exams/Level/index.tsx b/src/exams/Level/index.tsx index bfd8d739..008897ae 100644 --- a/src/exams/Level/index.tsx +++ b/src/exams/Level/index.tsx @@ -25,6 +25,7 @@ import ProgressButtons from "../components/ProgressButtons"; import useExamNavigation from "../Navigation/useExamNavigation"; import { calculateExerciseIndex } from "../utils/calculateExerciseIndex"; import { defaultExamUserSolutions } from "@/utils/exams"; +import PracticeModal from "@/components/PracticeModal"; const Level: React.FC> = ({ exam, showSolutions = false, preview = false }) => { @@ -66,7 +67,6 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr const [continueAnyways, setContinueAnyways] = useState(false); const [textRender, setTextRender] = useState(false); - const [questionModalKwargs, setQuestionModalKwargs] = useState<{ type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; }>({ @@ -101,6 +101,14 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr } ); + const hasPractice = useMemo(() => { + if (partIndex > -1 && partIndex < exam.parts.length && !showPartDivider) { + console.log(exam.parts[partIndex].exercises.some(e => e.isPractice)) + return exam.parts[partIndex].exercises.some(e => e.isPractice) + } + return false + }, [partIndex, showPartDivider, exam.parts]) + const registerSolution = useCallback((updateSolution: () => UserSolution) => { userSolutionRef.current = updateSolution; setSolutionWasUpdated(true); @@ -337,6 +345,7 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr return ( <>
+ > = ({ exam, showSolutions = false, pr (!showPartDivider && !startNow) && } - {(showPartDivider || (startNow && partIndex === 0)) ? + {(showPartDivider || (startNow && partIndex === 0)) ? >(); const searchFields = [["name"], ["email"], ["entities", ""]]; @@ -45,8 +46,6 @@ export default function UserList({ const { users, reload } = useEntitiesUsers(type) const { entities } = useEntities() - const { balance } = useUserBalance(); - const isAdmin = useMemo(() => ["admin", "developer"].includes(user?.type), [user?.type]) const entitiesViewStudents = useAllowedEntities(user, entities, "view_students") @@ -65,6 +64,8 @@ export default function UserList({ const entitiesEditMasterCorporates = useAllowedEntities(user, entities, "edit_mastercorporates") const entitiesDeleteMasterCorporates = useAllowedEntities(user, entities, "delete_mastercorporates") + const entitiesDownloadUsers = useAllowedEntities(user, entities, "download_user_list") + const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); @@ -342,7 +343,10 @@ export default function UserList({ ]; const downloadExcel = (rows: WithLabeledEntities[]) => { - const csv = exportListToExcel(rows); + if (entitiesDownloadUsers.length === 0) return toast.error("You are not allowed to download the user list.") + + const allowedRows = rows.filter(r => mapBy(r.entities, 'id').some(e => mapBy(entitiesDownloadUsers, 'id').includes(e))) + const csv = exportListToExcel(allowedRows); const element = document.createElement("a"); const file = new Blob([csv], { type: "text/csv" }); @@ -437,7 +441,7 @@ export default function UserList({ data={displayUsers} columns={(!showDemographicInformation ? defaultColumns : demographicColumns) as any} searchFields={searchFields} - onDownload={downloadExcel} + onDownload={entitiesDownloadUsers.length > 0 ? downloadExcel : undefined} />
diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index 82a081dc..5dfc9842 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -5,8 +5,8 @@ import usePackages from "@/hooks/usePackages"; import useUsers from "@/hooks/useUsers"; import { User } from "@/interfaces/user"; import clsx from "clsx"; -import { capitalize } from "lodash"; -import { useEffect, useState } from "react"; +import { capitalize, sortBy } from "lodash"; +import { useEffect, useMemo, useState } from "react"; import useInvites from "@/hooks/useInvites"; import { BsArrowRepeat } from "react-icons/bs"; import InviteCard from "@/components/Medium/InviteCard"; @@ -16,46 +16,50 @@ import useDiscounts from "@/hooks/useDiscounts"; import PaymobPayment from "@/components/PaymobPayment"; import moment from "moment"; import { EntityWithRoles } from "@/interfaces/entity"; +import { Discount, Package } from "@/interfaces/paypal"; +import { isAdmin } from "@/utils/users"; +import { useAllowedEntities } from "@/hooks/useEntityPermissions"; +import Select from "@/components/Low/Select"; interface Props { - user: User; + user: User + discounts: Discount[] + packages: Package[] entities: EntityWithRoles[] hasExpired?: boolean; reload: () => void; } -export default function PaymentDue({ user, hasExpired = false, reload }: Props) { +export default function PaymentDue({ user, discounts = [], entities = [], packages = [], hasExpired = false, reload }: Props) { const [isLoading, setIsLoading] = useState(false); - const [appliedDiscount, setAppliedDiscount] = useState(0); + const [entity, setEntity] = useState() const router = useRouter(); - const { packages } = usePackages(); - const { discounts } = useDiscounts(); const { users } = useUsers(); - const { groups } = useGroups({}); const { invites, isLoading: isInvitesLoading, reload: reloadInvites } = useInvites({ to: user?.id }); - useEffect(() => { - const userDiscounts = discounts.filter((x) => user.email.endsWith(`@${x.domain}`)); - if (userDiscounts.length === 0) return; - - const biggestDiscount = [...userDiscounts].sort((a, b) => b.percentage - a.percentage).shift(); - if (!biggestDiscount || (biggestDiscount.validUntil && moment(biggestDiscount.validUntil).isBefore(moment()))) return; - - setAppliedDiscount(biggestDiscount.percentage); - }, [discounts, user]); - - const isIndividual = () => { - if (user?.type === "developer") return true; + const isIndividual = useMemo(() => { + if (isAdmin(user)) return false; if (user?.type !== "student") return false; - const userGroups = groups.filter((g) => g.participants.includes(user?.id)); - if (userGroups.length === 0) return true; + return user.entities.length === 0 + }, [user]) - const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t); - return userGroupsAdminTypes.every((t) => t !== "corporate"); - }; + const appliedDiscount = useMemo(() => { + const biggestDiscount = [...discounts].sort((a, b) => b.percentage - a.percentage).shift(); + + if (!biggestDiscount || (biggestDiscount.validUntil && moment(biggestDiscount.validUntil).isBefore(moment()))) + return 0; + + return biggestDiscount.percentage + }, [discounts]) + + const entitiesThatCanBePaid = useAllowedEntities(user, entities, 'pay_entity') + + useEffect(() => { + if (entitiesThatCanBePaid.length > 0) setEntity(entitiesThatCanBePaid[0]) + }, [entitiesThatCanBePaid]) return ( <> @@ -74,169 +78,185 @@ export default function PaymentDue({ user, hasExpired = false, reload }: Props) )} - {user ? ( - - {invites.length > 0 && ( -
-
-
- Invites - -
+ + {invites.length > 0 && ( +
+
+
+ Invites +
- - {invites.map((invite) => ( - { - reloadInvites(); - router.reload(); - }} - /> - ))} - -
- )} +
+ + {invites.map((invite) => ( + { + reloadInvites(); + router.reload(); + }} + /> + ))} + +
+ )} -
- {hasExpired && You do not have time credits for your account type!} - {isIndividual() && ( -
- - To add to your use of EnCoach, please purchase one of the time packages available below: - -
- {packages.map((p) => ( -
-
- EnCoach's Logo - - EnCoach - {p.duration}{" "} - {capitalize( - p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit, - )} - -
-
- {appliedDiscount === 0 && ( - - {p.price} {p.currency} - - )} - {appliedDiscount > 0 && ( -
- - {p.price} {p.currency} - - - {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} {p.currency} - -
- )} - { - setTimeout(reload, 500); - }} - currency={p.currency} - duration={p.duration} - duration_unit={p.duration_unit} - price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} - /> -
-
- This includes: -
    -
  • - Train your abilities for the IELTS exam
  • -
  • - Gain insights into your weaknesses and strengths
  • -
  • - Allow yourself to correctly prepare for the exam
  • -
-
-
- ))} -
-
- )} - {!isIndividual() && - (user?.type === "corporate" || user?.type === "mastercorporate") && - user?.corporateInformation.payment && ( -
- - To add to your use of EnCoach and that of your students and teachers, please pay your designated package - below: - -
+
+ {hasExpired && You do not have time credits for your account type!} + {isIndividual && ( +
+ + To add to your use of EnCoach, please purchase one of the time packages available below: + +
+ {packages.map((p) => ( +
EnCoach's Logo - EnCoach - {12} Months + EnCoach - {p.duration}{" "} + {capitalize( + p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit, + )}
- - {user.corporateInformation.payment.value} {user.corporateInformation.payment.currency} - + {appliedDiscount === 0 && ( + + {p.price} {p.currency} + + )} + {appliedDiscount > 0 && ( +
+ + {p.price} {p.currency} + + + {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} {p.currency} + +
+ )} { - setIsLoading(false); setTimeout(reload, 500); }} + currency={p.currency} + duration={p.duration} + duration_unit={p.duration_unit} + price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} />
This includes:
    -
  • - - Allow a total of 0 students and teachers to use EnCoach -
  • -
  • - Train their abilities for the IELTS exam
  • -
  • - Gain insights into your students' weaknesses and strengths
  • -
  • - Allow them to correctly prepare for the exam
  • +
  • - Train your abilities for the IELTS exam
  • +
  • - Gain insights into your weaknesses and strengths
  • +
  • - Allow yourself to correctly prepare for the exam
+ ))} +
+
+ )} + + {!isIndividual && entitiesThatCanBePaid.length > 0 && + entity?.payment && ( +
+
+ + ({ value: e.id, label: e.label, entity: e }))} + onChange={(e) => e?.value ? setEntity(e?.entity) : null} + className="!w-full max-w-[400px] self-center" + /> +
+ + An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users + you desire and your expected monthly duration. - If you believe this to be a mistake, please contact the platform's administration, thank you for your - patience. + Please try again later or contact your agent or an admin, thank you for your patience.
)} - {!isIndividual() && - (user?.type === "corporate" || user?.type === "mastercorporate") && - !user.corporateInformation.payment && ( -
- - An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users - you desire and your expected monthly duration. - - - Please try again later or contact your agent or an admin, thank you for your patience. - -
- )} -
- - ) : ( -
- )} +
+ ); } diff --git a/src/pages/api/entities/[id]/index.ts b/src/pages/api/entities/[id]/index.ts index c329f832..0323ac05 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,27 @@ async function patch(req: NextApiRequest, res: NextApiResponse) { return res.status(200).json({ ok: entity.acknowledged }); } + if (req.body.payment) { + const entity = await db.collection("entities").updateOne({ id }, { $set: { payment: req.body.payment } }); + 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/api/exam/[module]/index.ts b/src/pages/api/exam/[module]/index.ts index d54e7835..563b953b 100644 --- a/src/pages/api/exam/[module]/index.ts +++ b/src/pages/api/exam/[module]/index.ts @@ -7,6 +7,9 @@ import { Exam, ExamBase, InstructorGender, Variant } from "@/interfaces/exam"; import { getExams } from "@/utils/exams.be"; import { Module } from "@/interfaces"; import { getUserCorporate } from "@/utils/groups.be"; +import { requestUser } from "@/utils/api"; +import { isAdmin } from "@/utils/users"; +import { mapBy } from "@/utils"; const db = client.db(process.env.MONGODB_DB); @@ -37,25 +40,20 @@ async function GET(req: NextApiRequest, res: NextApiResponse) { } async function POST(req: NextApiRequest, res: NextApiResponse) { - if (!req.session.user) { - res.status(401).json({ ok: false }); - return; - } + const user = await requestUser(req, res) + if (!user) return res.status(401).json({ ok: false }); const { module } = req.query as { module: string }; - const corporate = await getUserCorporate(req.session.user.id); const session = client.startSession(); + const entities = isAdmin(user) ? [] : mapBy(user.entities, 'id') try { const exam = { ...req.body, module: module, - owners: [ - ...(["mastercorporate", "corporate"].includes(req.session.user.type) ? [req.session.user.id] : []), - ...(!!corporate ? [corporate.id] : []), - ], - createdBy: req.session.user.id, + entities, + createdBy: user.id, createdAt: new Date().toISOString(), }; diff --git a/src/pages/api/hello.ts b/src/pages/api/hello.ts index 76280a02..0a75b094 100644 --- a/src/pages/api/hello.ts +++ b/src/pages/api/hello.ts @@ -1,6 +1,13 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import { CorporateUser, User } from "@/interfaces/user"; import client from "@/lib/mongodb"; +import { getLinkedUsers } from "@/utils/users.be"; +import { uniqBy } from "lodash"; import type { NextApiRequest, NextApiResponse } from "next"; +import { v4 } from "uuid"; +import fs from 'fs' +import { findBy, mapBy } from "@/utils"; +import { addUsersToEntity, getEntitiesWithRoles } from "@/utils/entities.be"; const db = client.db(process.env.MONGODB_DB); diff --git a/src/pages/api/paymob/webhook.ts b/src/pages/api/paymob/webhook.ts index 7b428c6c..0fd4b416 100644 --- a/src/pages/api/paymob/webhook.ts +++ b/src/pages/api/paymob/webhook.ts @@ -1,15 +1,19 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type {NextApiRequest, NextApiResponse} from "next"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; -import {Group, User} from "@/interfaces/user"; -import {DurationUnit, Package, Payment} from "@/interfaces/paypal"; -import {v4} from "uuid"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Group, User } from "@/interfaces/user"; +import { DurationUnit, Package, Payment } from "@/interfaces/paypal"; +import { v4 } from "uuid"; import ShortUniqueId from "short-unique-id"; import axios from "axios"; -import {IntentionResult, PaymentIntention, TransactionResult} from "@/interfaces/paymob"; +import { IntentionResult, PaymentIntention, TransactionResult } from "@/interfaces/paymob"; import moment from "moment"; import client from "@/lib/mongodb"; +import { getEntity } from "@/utils/entities.be"; +import { Entity } from "@/interfaces/entity"; +import { getEntityUsers } from "@/utils/users.be"; +import { mapBy } from "@/utils"; const db = client.db(process.env.MONGODB_DB); @@ -22,21 +26,22 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const authToken = await authenticatePaymob(); console.log("WEBHOOK: ", transactionResult); - if (!checkTransaction(authToken, transactionResult.transaction.order.id)) return res.status(404).json({ok: false}); - if (!transactionResult.transaction.success) return res.status(400).json({ok: false}); + if (!checkTransaction(authToken, transactionResult.transaction.order.id)) return res.status(404).json({ ok: false }); + if (!transactionResult.transaction.success) return res.status(400).json({ ok: false }); - const {userID, duration, duration_unit} = transactionResult.intention.extras.creation_extras as { + const { userID, duration, duration_unit, entity: entityID } = transactionResult.intention.extras.creation_extras as { userID: string; duration: number; duration_unit: DurationUnit; + entity: string }; const user = await db.collection("users").findOne({ id: userID as string }); - if (!user || !duration || !duration_unit) return res.status(404).json({ok: false}); + if (!user || !duration || !duration_unit) return res.status(404).json({ ok: false }); const subscriptionExpirationDate = user.subscriptionExpirationDate; - if (!subscriptionExpirationDate) return res.status(200).json({ok: false}); + if (!subscriptionExpirationDate) return res.status(200).json({ ok: false }); const initialDate = moment(subscriptionExpirationDate).isAfter(moment()) ? moment(subscriptionExpirationDate) : moment(); @@ -44,8 +49,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) { await db.collection("users").updateOne( { id: userID as string }, - { $set: {subscriptionExpirationDate: updatedSubscriptionExpirationDate, status: "active"} } - ); + { $set: { subscriptionExpirationDate: updatedSubscriptionExpirationDate, status: "active" } } + ); await db.collection("paypalpayments").insertOne({ id: v4(), @@ -60,22 +65,19 @@ async function post(req: NextApiRequest, res: NextApiResponse) { value: transactionResult.transaction.amount_cents / 1000, }); - if (user.type === "corporate") { - const groups = await db.collection("groups").find({ admin: user.id }).toArray(); + if (entityID) { + const entity = await getEntity(entityID) + await db.collection("entities").updateOne({ id: entityID }, { $set: { expiryDate: req.body.expiryDate } }); - const participants = (await Promise.all( - groups.flatMap((x) => x.participants).map(async (x) => ({...(await db.collection("users").findOne({ id: x}))})), - )) as User[]; - const sameExpiryDateParticipants = participants.filter( - (x) => x.subscriptionExpirationDate === subscriptionExpirationDate && x.status !== "disabled", - ); + const users = await getEntityUsers(entityID, 0, { + subscriptionExpirationDate: entity?.expiryDate, + $and: [ + { type: { $ne: "admin" } }, + { type: { $ne: "developer" } }, + ] + }) - for (const participant of sameExpiryDateParticipants) { - await db.collection("users").updateOne( - { id: participant.id }, - { $set: {subscriptionExpirationDate: updatedSubscriptionExpirationDate, status: "active"} } - ); - } + await db.collection("users").updateMany({ id: { $in: mapBy(users, 'id') } }, { $set: { subscriptionExpirationDate: req.body.expiryDate } }) } res.status(200).json({ @@ -84,19 +86,19 @@ async function post(req: NextApiRequest, res: NextApiResponse) { } const authenticatePaymob = async () => { - const response = await axios.post<{token: string}>( + const response = await axios.post<{ token: string }>( "https://oman.paymob.com/api/auth/tokens", { api_key: process.env.PAYMOB_API_KEY, }, - {headers: {Authorization: `Bearer ${process.env.PAYMOB_SECRET_KEY}`}}, + { headers: { Authorization: `Bearer ${process.env.PAYMOB_SECRET_KEY}` } }, ); return response.data.token; }; const checkTransaction = async (token: string, orderID: number) => { - const response = await axios.post("https://oman.paymob.com/api/ecommerce/orders/transaction_inquiry", {auth_token: token, order_id: orderID}); + const response = await axios.post("https://oman.paymob.com/api/ecommerce/orders/transaction_inquiry", { auth_token: token, order_id: orderID }); return response.status === 200; }; diff --git a/src/pages/api/stats/update.ts b/src/pages/api/stats/update.ts index f3d27fbb..eeff1855 100644 --- a/src/pages/api/stats/update.ts +++ b/src/pages/api/stats/update.ts @@ -1,15 +1,15 @@ -import {MODULES} from "@/constants/ielts"; -import {app} from "@/firebase"; -import {Module} from "@/interfaces"; -import {Stat, User} from "@/interfaces/user"; -import {sessionOptions} from "@/lib/session"; -import {calculateBandScore} from "@/utils/score"; -import {groupByModule, groupBySession} from "@/utils/stats"; +import { MODULES } from "@/constants/ielts"; +import { app } from "@/firebase"; +import { Module } from "@/interfaces"; +import { Stat, User } from "@/interfaces/user"; +import { sessionOptions } from "@/lib/session"; +import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; +import { groupByModule, groupBySession } from "@/utils/stats"; import { MODULE_ARRAY } from "@/utils/moduleUtils"; import client from "@/lib/mongodb"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {groupBy} from "lodash"; -import {NextApiRequest, NextApiResponse} from "next"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { groupBy } from "lodash"; +import { NextApiRequest, NextApiResponse } from "next"; import { requestUser } from "@/utils/api"; const db = client.db(process.env.MONGODB_DB); @@ -29,8 +29,8 @@ async function update(req: NextApiRequest, res: NextApiResponse) { const stats = await db.collection("stats").find({ user: user.id }).toArray(); const groupedStats = groupBySession(stats); - const sessionLevels: {[key in Module]: {correct: number; total: number}}[] = Object.keys(groupedStats).map((key) => { - const sessionStats = groupedStats[key].map((stat) => ({module: stat.module, correct: stat.score.correct, total: stat.score.total})); + const sessionLevels: { [key in Module]: { correct: number; total: number } }[] = Object.keys(groupedStats).map((key) => { + const sessionStats = groupedStats[key].map((stat) => ({ module: stat.module, correct: stat.score.correct, total: stat.score.total })); const sessionLevels = { reading: { correct: 0, @@ -59,8 +59,8 @@ async function update(req: NextApiRequest, res: NextApiResponse) { if (moduleStats.length === 0) return; const moduleScore = moduleStats.reduce( - (accumulator, current) => ({correct: accumulator.correct + current.correct, total: accumulator.total + current.total}), - {correct: 0, total: 0}, + (accumulator, current) => ({ correct: accumulator.correct + current.correct, total: accumulator.total + current.total }), + { correct: 0, total: 0 }, ); sessionLevels[module] = moduleScore; @@ -72,24 +72,24 @@ async function update(req: NextApiRequest, res: NextApiResponse) { const readingLevel = sessionLevels .map((x) => x.reading) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); const listeningLevel = sessionLevels .map((x) => x.listening) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); const writingLevel = sessionLevels .map((x) => x.writing) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); const speakingLevel = sessionLevels .map((x) => x.speaking) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); - const levelLevel = sessionLevels + const levelLevel = sessionLevels .map((x) => x.level) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); const levels = { @@ -100,12 +100,14 @@ async function update(req: NextApiRequest, res: NextApiResponse) { level: calculateBandScore(levelLevel.correct, levelLevel.total, "level", user.focus), }; + const averageLevel = calculateAverageLevel(levels) + await db.collection("users").updateOne( - { id: user.id}, - { $set: {levels} } + { id: user.id }, + { $set: { levels, averageLevel } } ); - res.status(200).json({ok: true}); + res.status(200).json({ ok: true }); } else { res.status(401).json(undefined); } diff --git a/src/pages/dashboard/admin.tsx b/src/pages/dashboard/admin.tsx index 2e53d579..ab5e7931 100644 --- a/src/pages/dashboard/admin.tsx +++ b/src/pages/dashboard/admin.tsx @@ -139,7 +139,6 @@ export default function Dashboard({ value={entities.length} color="purple" /> - router.push("/statistical")} label="Entity Statistics" @@ -149,7 +148,7 @@ export default function Dashboard({ router.push("/users/performance")} label="Student Performance" - value={students.length} + value={usersCount.student} color="purple" /> { const entityIDS = mapBy(user.entities, "id") || []; const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); - const users = await filterAllowedUsers(user, entities) + + const allowedStudentEntities = findAllowedEntities(user, entities, "view_students") + const allowedTeacherEntities = findAllowedEntities(user, entities, "view_teachers") + + const students = + await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { averageLevel: -1 }); + const latestStudents = + await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { registrationDate: -1 }) + const latestTeachers = + await getUsers({ type: 'teacher', "entities.id": { $in: mapBy(allowedTeacherEntities, 'id') } }, 10, { registrationDate: -1 }) const userCounts = await countAllowedUsers(user, entities) const assignmentsCount = await countEntitiesAssignments(mapBy(entities, "id"), { archived: { $ne: true } }); const groupsCount = await countGroupsByEntities(mapBy(entities, "id")); - const stats = await getStatsByUsers(users.map((u) => u.id)); - - return { props: serialize({ user, users, userCounts, entities, assignmentsCount, stats, groupsCount }) }; + return { props: serialize({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, groupsCount }) }; }, sessionOptions); -export default function Dashboard({ user, users, userCounts, entities, assignmentsCount, stats, groupsCount }: Props) { - const students = useMemo(() => users.filter((u) => u.type === "student"), [users]); - const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]); - +export default function Dashboard({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, stats = [], groupsCount }: Props) { const totalCount = useMemo(() => userCounts.corporate + userCounts.mastercorporate + userCounts.student + userCounts.teacher, [userCounts]) - const totalLicenses = useMemo(() => entities.reduce((acc, curr) => acc + curr.licenses, 0), [entities]) + const totalLicenses = useMemo(() => entities.reduce((acc, curr) => acc + parseInt(curr.licenses.toString()), 0), [entities]) const allowedEntityStatistics = useAllowedEntities(user, entities, 'view_entity_statistics') const allowedStudentPerformance = useAllowedEntities(user, entities, 'view_student_performance') @@ -124,7 +130,6 @@ export default function Dashboard({ user, users, userCounts, entities, assignmen value={`${entities.length} - ${totalCount}/${totalLicenses}`} color="purple" /> - {allowedEntityStatistics.length > 0 && ( router.push("/statistical")} @@ -160,15 +165,15 @@ export default function Dashboard({ user, users, userCounts, entities, assignmen
dateSorter(a, b, "desc", "registrationDate"))} + users={latestStudents} title="Latest Students" /> dateSorter(a, b, "desc", "registrationDate"))} + users={latestTeachers} title="Latest Teachers" /> calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + users={students} title="Highest level students" /> { if (!checkAccess(user, ["admin", "developer"])) return redirect("/") - const students = await getUsers({ type: 'student' }); + const students = await getUsers({ type: 'student' }, 10, { averageLevel: -1 }); const usersCount = { student: await countUsers({ type: "student" }), teacher: await countUsers({ type: "teacher" }), @@ -66,20 +66,18 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { const assignmentsCount = await countEntitiesAssignments(mapBy(entities, 'id'), { archived: { $ne: true } }); const groupsCount = await countGroups(); - const stats = await getStatsByUsers(mapBy(students, 'id')); - - return { props: serialize({ user, students, latestStudents, latestTeachers, usersCount, entities, assignmentsCount, stats, groupsCount }) }; + return { props: serialize({ user, students, latestStudents, latestTeachers, usersCount, entities, assignmentsCount, groupsCount }) }; }, sessionOptions); export default function Dashboard({ user, - students, + students = [], latestStudents, latestTeachers, usersCount, entities, assignmentsCount, - stats, + stats = [], groupsCount }: Props) { const router = useRouter(); @@ -139,7 +137,6 @@ export default function Dashboard({ value={entities.length} color="purple" /> - router.push("/statistical")} label="Entity Statistics" @@ -171,7 +168,7 @@ export default function Dashboard({ title="Latest Teachers" /> calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + users={students} title="Highest level students" /> { const user = await requestUser(req, res) if (!user || !user.isVerified) return redirect("/login") - if (!checkAccess(user, ["admin", "developer", "mastercorporate"])) - return redirect("/") + if (!checkAccess(user, ["admin", "developer", "mastercorporate"])) return redirect("/") const entityIDS = mapBy(user.entities, "id") || []; const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); - const users = await filterAllowedUsers(user, entities) + + const allowedStudentEntities = findAllowedEntities(user, entities, "view_students") + const allowedTeacherEntities = findAllowedEntities(user, entities, "view_teachers") + + const students = + await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { averageLevel: -1 }); + const latestStudents = + await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { registrationDate: -1 }) + const latestTeachers = + await getUsers({ type: 'teacher', "entities.id": { $in: mapBy(allowedTeacherEntities, 'id') } }, 10, { registrationDate: -1 }) const userCounts = await countAllowedUsers(user, entities) const assignmentsCount = await countEntitiesAssignments(mapBy(entities, "id"), { archived: { $ne: true } }); const groupsCount = await countGroupsByEntities(mapBy(entities, "id")); - const stats = await getStatsByUsers(users.map((u) => u.id)); - - return { props: serialize({ user, users, userCounts, entities, assignmentsCount, stats, groupsCount }) }; + return { props: serialize({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, groupsCount }) }; }, sessionOptions); -export default function Dashboard({ user, users, userCounts, entities, assignmentsCount, stats, groupsCount }: Props) { - const students = useMemo(() => users.filter((u) => u.type === "student"), [users]); - const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]); +export default function Dashboard({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, stats = [], groupsCount }: Props) { const totalCount = useMemo(() => userCounts.corporate + userCounts.mastercorporate + userCounts.student + userCounts.teacher, [userCounts]) - const totalLicenses = useMemo(() => entities.reduce((acc, curr) => acc + curr.licenses, 0), [entities]) + + const totalLicenses = useMemo(() => entities.reduce((acc, curr) => acc + parseInt(curr.licenses.toString()), 0), [entities]) const router = useRouter(); @@ -133,7 +140,6 @@ export default function Dashboard({ user, users, userCounts, entities, assignmen value={`${entities.length} - ${totalCount}/${totalLicenses}`} color="purple" /> - {allowedStudentPerformance.length > 0 && ( router.push("/users/performance")} @@ -168,15 +174,15 @@ export default function Dashboard({ user, users, userCounts, entities, assignmen
dateSorter(a, b, "desc", "registrationDate"))} + users={latestStudents} title="Latest Students" /> dateSorter(a, b, "desc", "registrationDate"))} + users={latestTeachers} title="Latest Teachers" /> calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + users={students} title="Highest level students" /> - {allowedStudentPerformance.length > 0 && ( router.push("/users/performance")} diff --git a/src/pages/entities/[id]/index.tsx b/src/pages/entities/[id]/index.tsx index 1546d933..34b35978 100644 --- a/src/pages/entities/[id]/index.tsx +++ b/src/pages/entities/[id]/index.tsx @@ -2,6 +2,8 @@ import CardList from "@/components/High/CardList"; import Layout from "@/components/High/Layout"; import Select from "@/components/Low/Select"; +import Input from "@/components/Low/Input"; +import Checkbox from "@/components/Low/Checkbox"; import Tooltip from "@/components/Low/Tooltip"; import { useEntityPermission } from "@/hooks/useEntityPermissions"; import { useListSearch } from "@/hooks/useListSearch"; @@ -27,7 +29,11 @@ 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 { CURRENCIES } from "@/resources/paypal"; + import { + BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, @@ -43,6 +49,20 @@ 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"; +}; + +const CURRENCIES_OPTIONS = CURRENCIES.map(({ label, currency }) => ({ + value: currency, + label, +})); + export const getServerSideProps = withIronSessionSsr(async ({ req, params }) => { const user = req.session.user as User; @@ -88,6 +108,9 @@ 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 [paymentPrice, setPaymentPrice] = useState(entity?.payment?.price) + const [paymentCurrency, setPaymentCurrency] = useState(entity?.payment?.currency) const router = useRouter(); @@ -99,6 +122,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 +190,40 @@ 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 updatePayment = () => { + if (!isAdmin(user)) return; + + setIsLoading(true); + axios + .patch(`/api/entities/${entity.id}`, { payment: { price: paymentPrice, currency: paymentCurrency } }) + .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 +347,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 + +
+ + + +
+ + + +
+
+ setPaymentPrice(e ? parseInt(e) : undefined)} + type="number" + defaultValue={entity.payment?.price || 0} + thin + /> +