Merged with develop
This commit is contained in:
@@ -35,7 +35,7 @@ export default function Layout({
|
||||
return (
|
||||
<main className={clsx("w-full min-h-full h-screen flex flex-col bg-mti-gray-smoke relative")}>
|
||||
<ToastContainer />
|
||||
{!hideSidebar && (
|
||||
{!hideSidebar && user && (
|
||||
<Navbar
|
||||
path={router.pathname}
|
||||
user={user}
|
||||
@@ -45,7 +45,7 @@ export default function Layout({
|
||||
/>
|
||||
)}
|
||||
<div className={clsx("h-full w-full flex gap-2")}>
|
||||
{!hideSidebar && (
|
||||
{!hideSidebar && user && (
|
||||
<Sidebar
|
||||
path={router.pathname}
|
||||
navDisabled={navDisabled}
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Props {
|
||||
disabled?: boolean;
|
||||
max?: number;
|
||||
min?: number;
|
||||
thin?: boolean
|
||||
name: string;
|
||||
onChange: (value: string) => 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}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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";
|
||||
@@ -47,6 +48,8 @@ const RecordFilter: React.FC<Props> = ({
|
||||
state.setSelectedUser
|
||||
]);
|
||||
|
||||
const allowedViewEntities = useAllowedEntities(user, entities, 'view_student_record')
|
||||
|
||||
const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [users, entity])
|
||||
|
||||
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user.id])
|
||||
@@ -64,7 +67,7 @@ const RecordFilter: React.FC<Props> = ({
|
||||
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
||||
|
||||
<Select
|
||||
options={entities.map((e) => ({value: e.id, label: e.label}))}
|
||||
options={allowedViewEntities.map((e) => ({ value: e.id, label: e.label }))}
|
||||
onChange={(value) => setEntity(value?.value || undefined)}
|
||||
isClearable
|
||||
styles={{
|
||||
|
||||
@@ -82,7 +82,7 @@ interface StatsGridItemProps {
|
||||
selectedTrainingExams?: string[];
|
||||
maxTrainingExams?: number;
|
||||
setSelectedTrainingExams?: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
renderPdfIcon: (session: string, color: string, textColor: string) => React.ReactNode;
|
||||
renderPdfIcon?: (session: string, color: string, textColor: string) => React.ReactNode;
|
||||
}
|
||||
|
||||
const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
||||
@@ -236,7 +236,7 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
||||
{renderLevelScore()}
|
||||
</span>
|
||||
)}
|
||||
{shouldRenderPDFIcon() && renderPdfIcon(session, textColor, textColor)}
|
||||
{shouldRenderPDFIcon() && renderPdfIcon && renderPdfIcon(session, textColor, textColor)}
|
||||
</div>
|
||||
{examNumber === undefined ? (
|
||||
<>
|
||||
|
||||
@@ -65,28 +65,28 @@ export default function Navbar({ user, path, navDisabled = false, focusMode = fa
|
||||
{
|
||||
module: "reading",
|
||||
icon: () => <BsBook className="h-4 w-4 text-white" />,
|
||||
achieved: user.levels?.reading || 0 >= user.desiredLevels?.reading || 9,
|
||||
achieved: user?.levels?.reading || 0 >= user?.desiredLevels?.reading || 9,
|
||||
},
|
||||
|
||||
{
|
||||
module: "listening",
|
||||
icon: () => <BsHeadphones className="h-4 w-4 text-white" />,
|
||||
achieved: user.levels?.listening || 0 >= user.desiredLevels?.listening || 9,
|
||||
achieved: user?.levels?.listening || 0 >= user?.desiredLevels?.listening || 9,
|
||||
},
|
||||
{
|
||||
module: "writing",
|
||||
icon: () => <BsPen className="h-4 w-4 text-white" />,
|
||||
achieved: user.levels?.writing || 0 >= user.desiredLevels?.writing || 9,
|
||||
achieved: user?.levels?.writing || 0 >= user?.desiredLevels?.writing || 9,
|
||||
},
|
||||
{
|
||||
module: "speaking",
|
||||
icon: () => <BsMegaphone className="h-4 w-4 text-white" />,
|
||||
achieved: user.levels?.speaking || 0 >= user.desiredLevels?.speaking || 9,
|
||||
achieved: user?.levels?.speaking || 0 >= user?.desiredLevels?.speaking || 9,
|
||||
},
|
||||
{
|
||||
module: "level",
|
||||
icon: () => <BsClipboard className="h-4 w-4 text-white" />,
|
||||
achieved: user.levels?.level || 0 >= user.desiredLevels?.level || 9,
|
||||
achieved: user?.levels?.level || 0 >= user?.desiredLevels?.level || 9,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Entity } from "@/interfaces/entity";
|
||||
import { PaymentIntention } from "@/interfaces/paymob";
|
||||
import { DurationUnit } from "@/interfaces/paypal";
|
||||
import { User } from "@/interfaces/user";
|
||||
@@ -10,6 +11,7 @@ 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,6 +58,7 @@ export default function PaymobPayment({user, price, setIsPaymentLoading, currenc
|
||||
userID: user.id,
|
||||
duration,
|
||||
duration_unit,
|
||||
entity: entity?.id
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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<ExamProps<LevelExam>> = ({ exam, showSolutions = false, preview = false }) => {
|
||||
@@ -66,7 +67,6 @@ const Level: React.FC<ExamProps<LevelExam>> = ({ 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<ExamProps<LevelExam>> = ({ 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<ExamProps<LevelExam>> = ({ exam, showSolutions = false, pr
|
||||
return (
|
||||
<>
|
||||
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
|
||||
<PracticeModal key={`${partIndex}_${showPartDivider}`} open={hasPractice} />
|
||||
<Modal
|
||||
className={"!w-2/6 !p-8"}
|
||||
titleClassName={"font-bold text-3xl text-mti-rose-light"}
|
||||
|
||||
@@ -4,6 +4,11 @@ export interface Entity {
|
||||
id: string;
|
||||
label: string;
|
||||
licenses: number;
|
||||
expiryDate?: Date | null
|
||||
payment?: {
|
||||
currency: string
|
||||
price: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ExamBase {
|
||||
variant?: Variant;
|
||||
difficulty?: Difficulty;
|
||||
owners?: string[];
|
||||
entities?: string[]
|
||||
shuffle?: boolean;
|
||||
createdBy?: string; // option as it has been added later
|
||||
createdAt?: string; // option as it has been added later
|
||||
|
||||
@@ -28,7 +28,7 @@ interface Customer {
|
||||
extras: IntentionExtras;
|
||||
}
|
||||
|
||||
type IntentionExtras = {[key: string]: string | number};
|
||||
type IntentionExtras = { [key: string]: string | number | undefined };
|
||||
|
||||
export interface IntentionResult {
|
||||
payment_keys: PaymentKeysItem[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface BasicUser {
|
||||
export interface StudentUser extends BasicUser {
|
||||
type: "student";
|
||||
studentID?: string;
|
||||
averageLevel?: number
|
||||
preferredGender?: InstructorGender;
|
||||
demographicInformation?: DemographicInformation;
|
||||
preferredTopics?: string[];
|
||||
|
||||
@@ -24,6 +24,7 @@ import { WithLabeledEntities } from "@/interfaces/entity";
|
||||
import Table from "@/components/High/Table";
|
||||
import useEntities from "@/hooks/useEntities";
|
||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||
import { findAllowedEntities } from "@/utils/permissions";
|
||||
|
||||
const columnHelper = createColumnHelper<WithLabeledEntities<User>>();
|
||||
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<User>[]) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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<EntityWithRoles>()
|
||||
|
||||
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,7 +78,6 @@ export default function PaymentDue({ user, hasExpired = false, reload }: Props)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{user ? (
|
||||
<Layout user={user} navDisabled={hasExpired}>
|
||||
{invites.length > 0 && (
|
||||
<section className="flex flex-col gap-1 md:gap-3">
|
||||
@@ -104,7 +107,7 @@ export default function PaymentDue({ user, hasExpired = false, reload }: Props)
|
||||
|
||||
<div className="flex w-full flex-col items-center justify-center gap-4 text-center">
|
||||
{hasExpired && <span className="text-lg font-bold">You do not have time credits for your account type!</span>}
|
||||
{isIndividual() && (
|
||||
{isIndividual && (
|
||||
<div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll">
|
||||
<span className="max-w-lg">
|
||||
To add to your use of EnCoach, please purchase one of the time packages available below:
|
||||
@@ -162,10 +165,20 @@ export default function PaymentDue({ user, hasExpired = false, reload }: Props)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isIndividual() &&
|
||||
(user?.type === "corporate" || user?.type === "mastercorporate") &&
|
||||
user?.corporateInformation.payment && (
|
||||
<div className="flex flex-col items-center">
|
||||
|
||||
{!isIndividual && entitiesThatCanBePaid.length > 0 &&
|
||||
entity?.payment && (
|
||||
<div className="flex flex-col items-center gap-8">
|
||||
<div className={clsx("flex flex-col items-center gap-4 w-full")}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
||||
<Select
|
||||
defaultValue={{ value: entity?.id, label: entity?.label }}
|
||||
options={entitiesThatCanBePaid.map((e) => ({ value: e.id, label: e.label, entity: e }))}
|
||||
onChange={(e) => e?.value ? setEntity(e?.entity) : null}
|
||||
className="!w-full max-w-[400px] self-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="max-w-lg">
|
||||
To add to your use of EnCoach and that of your students and teachers, please pay your designated package
|
||||
below:
|
||||
@@ -179,13 +192,14 @@ export default function PaymentDue({ user, hasExpired = false, reload }: Props)
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start gap-2">
|
||||
<span className="text-2xl">
|
||||
{user.corporateInformation.payment.value} {user.corporateInformation.payment.currency}
|
||||
{entity.payment.price} {entity.payment.currency}
|
||||
</span>
|
||||
<PaymobPayment
|
||||
user={user}
|
||||
setIsPaymentLoading={setIsLoading}
|
||||
currency={user.corporateInformation.payment.currency}
|
||||
price={user.corporateInformation.payment.value}
|
||||
entity={entity}
|
||||
currency={entity.payment.currency}
|
||||
price={entity.payment.price}
|
||||
duration={12}
|
||||
duration_unit="months"
|
||||
onSuccess={() => {
|
||||
@@ -198,7 +212,7 @@ export default function PaymentDue({ user, hasExpired = false, reload }: Props)
|
||||
<span>This includes:</span>
|
||||
<ul className="flex flex-col items-start text-sm">
|
||||
<li>
|
||||
- Allow a total of 0 students and teachers to use EnCoach
|
||||
- Allow a total of {entity.licenses} students and teachers to use EnCoach
|
||||
</li>
|
||||
<li>- Train their abilities for the IELTS exam</li>
|
||||
<li>- Gain insights into your students' weaknesses and strengths</li>
|
||||
@@ -208,7 +222,7 @@ export default function PaymentDue({ user, hasExpired = false, reload }: Props)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isIndividual() && !(user?.type === "corporate" || user?.type === "mastercorporate") && (
|
||||
{!isIndividual && entitiesThatCanBePaid.length === 0 && (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="max-w-lg">
|
||||
You are not the person in charge of your time credits, please contact your administrator about this situation.
|
||||
@@ -219,10 +233,19 @@ export default function PaymentDue({ user, hasExpired = false, reload }: Props)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!isIndividual() &&
|
||||
(user?.type === "corporate" || user?.type === "mastercorporate") &&
|
||||
!user.corporateInformation.payment && (
|
||||
<div className="flex flex-col items-center">
|
||||
{!isIndividual &&
|
||||
entitiesThatCanBePaid.length > 0 &&
|
||||
!entity?.payment && (
|
||||
<div className="flex flex-col items-center gap-8">
|
||||
<div className={clsx("flex flex-col items-center gap-4 w-full")}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
||||
<Select
|
||||
defaultValue={{ value: entity?.id || "", label: entity?.label || "" }}
|
||||
options={entitiesThatCanBePaid.map((e) => ({ value: e.id, label: e.label, entity: e }))}
|
||||
onChange={(e) => e?.value ? setEntity(e?.entity) : null}
|
||||
className="!w-full max-w-[400px] self-center"
|
||||
/>
|
||||
</div>
|
||||
<span className="max-w-lg">
|
||||
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.
|
||||
@@ -234,9 +257,6 @@ export default function PaymentDue({ user, hasExpired = false, reload }: Props)
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Entity>("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<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 });
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ import axios from "axios";
|
||||
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);
|
||||
|
||||
@@ -25,10 +29,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
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<User>({ id: userID as string });
|
||||
@@ -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<Group>({ admin: user.id }).toArray();
|
||||
if (entityID) {
|
||||
const entity = await getEntity(entityID)
|
||||
await db.collection<Entity>("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<User>("users").updateMany({ id: { $in: mapBy(users, 'id') } }, { $set: { subscriptionExpirationDate: req.body.expiryDate } })
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||
import { groupByModule, groupBySession } from "@/utils/stats";
|
||||
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
||||
import client from "@/lib/mongodb";
|
||||
@@ -100,9 +100,11 @@ 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} }
|
||||
{ $set: { levels, averageLevel } }
|
||||
);
|
||||
|
||||
res.status(200).json({ ok: true });
|
||||
|
||||
@@ -139,7 +139,6 @@ export default function Dashboard({
|
||||
value={entities.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
<IconCard Icon={BsPersonFillGear}
|
||||
onClick={() => router.push("/statistical")}
|
||||
label="Entity Statistics"
|
||||
@@ -149,7 +148,7 @@ export default function Dashboard({
|
||||
<IconCard Icon={BsPersonFillGear}
|
||||
onClick={() => router.push("/users/performance")}
|
||||
label="Student Performance"
|
||||
value={students.length}
|
||||
value={usersCount.student}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard
|
||||
|
||||
@@ -3,18 +3,18 @@ import Layout from "@/components/High/Layout";
|
||||
import UserDisplayList from "@/components/UserDisplayList";
|
||||
import IconCard from "@/components/IconCard";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { Stat, Type, User } from "@/interfaces/user";
|
||||
import { Stat, StudentUser, Type, User } from "@/interfaces/user";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils";
|
||||
import { requestUser } from "@/utils/api";
|
||||
import { countEntitiesAssignments } from "@/utils/assignments.be";
|
||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { countGroupsByEntities } from "@/utils/groups.be";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
||||
import { calculateAverageLevel } from "@/utils/score";
|
||||
import { groupByExam } from "@/utils/stats";
|
||||
import { getStatsByUsers } from "@/utils/stats.be";
|
||||
import { countAllowedUsers, filterAllowedUsers } from "@/utils/users.be";
|
||||
import { countAllowedUsers, filterAllowedUsers, getUsers } from "@/utils/users.be";
|
||||
import { withIronSessionSsr } from "iron-session/next";
|
||||
import { uniqBy } from "lodash";
|
||||
import moment from "moment";
|
||||
@@ -37,7 +37,9 @@ import { isAdmin } from "@/utils/users";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
students: StudentUser[]
|
||||
latestStudents: User[]
|
||||
latestTeachers: User[]
|
||||
userCounts: { [key in Type]: number }
|
||||
entities: EntityWithRoles[];
|
||||
assignmentsCount: number;
|
||||
@@ -53,24 +55,28 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
|
||||
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"
|
||||
/>
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
{allowedEntityStatistics.length > 0 && (
|
||||
<IconCard Icon={BsPersonFillGear}
|
||||
onClick={() => router.push("/statistical")}
|
||||
@@ -160,15 +165,15 @@ export default function Dashboard({ user, users, userCounts, entities, assignmen
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
users={latestStudents}
|
||||
title="Latest Students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
users={latestTeachers}
|
||||
title="Latest Teachers"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||
users={students}
|
||||
title="Highest level students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
|
||||
@@ -4,7 +4,7 @@ import UserDisplayList from "@/components/UserDisplayList";
|
||||
import IconCard from "@/components/IconCard";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
import { Group, Stat, Type, User } from "@/interfaces/user";
|
||||
import { Group, Stat, StudentUser, Type, User } from "@/interfaces/user";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils";
|
||||
import { requestUser } from "@/utils/api";
|
||||
@@ -51,7 +51,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
|
||||
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"
|
||||
/>
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
<IconCard Icon={BsPersonFillGear}
|
||||
onClick={() => router.push("/statistical")}
|
||||
label="Entity Statistics"
|
||||
@@ -171,7 +168,7 @@ export default function Dashboard({
|
||||
title="Latest Teachers"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||
users={students}
|
||||
title="Highest level students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||
import { Module } from "@/interfaces";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
import { Group, Stat, Type, User } from "@/interfaces/user";
|
||||
import { Group, Stat, StudentUser, Type, User } from "@/interfaces/user";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils";
|
||||
import { requestUser } from "@/utils/api";
|
||||
@@ -17,7 +17,7 @@ import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||
import { groupByExam } from "@/utils/stats";
|
||||
import { getStatsByUsers } from "@/utils/stats.be";
|
||||
import { countAllowedUsers, filterAllowedUsers } from "@/utils/users.be";
|
||||
import { countAllowedUsers, filterAllowedUsers, getUsers } from "@/utils/users.be";
|
||||
import { getEntitiesUsers } from "@/utils/users.be";
|
||||
import { clsx } from "clsx";
|
||||
import { withIronSessionSsr } from "iron-session/next";
|
||||
@@ -44,7 +44,9 @@ import { isAdmin } from "@/utils/users";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
students: StudentUser[]
|
||||
latestStudents: User[]
|
||||
latestTeachers: User[]
|
||||
userCounts: { [key in Type]: number }
|
||||
entities: EntityWithRoles[];
|
||||
assignmentsCount: number;
|
||||
@@ -56,29 +58,34 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
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"
|
||||
/>
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
{allowedStudentPerformance.length > 0 && (
|
||||
<IconCard Icon={BsPersonFillGear}
|
||||
onClick={() => router.push("/users/performance")}
|
||||
@@ -168,15 +174,15 @@ export default function Dashboard({ user, users, userCounts, entities, assignmen
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
users={latestStudents}
|
||||
title="Latest Students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={teachers.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
||||
users={latestTeachers}
|
||||
title="Latest Teachers"
|
||||
/>
|
||||
<UserDisplayList
|
||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
||||
users={students}
|
||||
title="Highest level students"
|
||||
/>
|
||||
<UserDisplayList
|
||||
|
||||
@@ -96,7 +96,6 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
||||
value={groups.length}
|
||||
color="purple"
|
||||
/>
|
||||
<IconCard Icon={BsClipboard2Data} label="Exams Performed" value={uniqBy(stats, "exam").length} color="purple" />
|
||||
{allowedStudentPerformance.length > 0 && (
|
||||
<IconCard Icon={BsPersonFillGear}
|
||||
onClick={() => router.push("/users/performance")}
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
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) {
|
||||
<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 +356,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 +404,91 @@ 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="w-full flex items-center justify-between gap-8">
|
||||
<div className="w-full max-w-xl flex items-center gap-4">
|
||||
<Input
|
||||
name="paymentValue"
|
||||
onChange={(e) => setPaymentPrice(e ? parseInt(e) : undefined)}
|
||||
type="number"
|
||||
defaultValue={entity.payment?.price || 0}
|
||||
thin
|
||||
/>
|
||||
<Select
|
||||
className={clsx(
|
||||
"px-4 !py-2 !w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none",
|
||||
)}
|
||||
options={CURRENCIES_OPTIONS}
|
||||
value={CURRENCIES_OPTIONS.find((c) => c.value === paymentCurrency)}
|
||||
onChange={(value) => setPaymentCurrency(value?.value ?? undefined)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={updatePayment}
|
||||
disabled={!paymentPrice || paymentPrice <= 0 || !paymentCurrency}
|
||||
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>
|
||||
|
||||
@@ -47,6 +47,7 @@ const USER_MANAGEMENT: PermissionLayout[] = [
|
||||
{ label: "Create Users in Batch", key: "create_user_batch" },
|
||||
{ label: "Create a Single Code", key: "create_code" },
|
||||
{ label: "Create Codes in Batch", key: "create_code_batch" },
|
||||
{ label: "Download User List", key: "download_user_list" },
|
||||
{ label: "View Code List", key: "view_code_list" },
|
||||
{ label: "Delete Code", key: "delete_code" },
|
||||
]
|
||||
@@ -75,8 +76,11 @@ const CLASSROOM_MANAGEMENT: PermissionLayout[] = [
|
||||
{ label: "Create Classrooms", key: "create_classroom" },
|
||||
{ label: "Rename Classrooms", key: "rename_classrooms" },
|
||||
{ label: "Add to Classroom", key: "add_to_classroom" },
|
||||
{ label: "Upload to Classroom", key: "upload_classroom" },
|
||||
{ label: "Remove from Classroom", key: "remove_from_classroom" },
|
||||
{ label: "Delete Classroom", key: "delete_classroom" },
|
||||
{ label: "View Student Record", key: "view_student_record" },
|
||||
{ label: "Download Student Report", key: "download_student_record" },
|
||||
]
|
||||
|
||||
const ENTITY_MANAGEMENT: PermissionLayout[] = [
|
||||
@@ -94,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[] = [
|
||||
|
||||
@@ -32,7 +32,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, query })
|
||||
const { assignment: assignmentID, destination } = query as { assignment?: string, destination?: string }
|
||||
const destinationURL = !!destination ? Buffer.from(destination, 'base64').toString() : undefined
|
||||
|
||||
if (assignmentID) {
|
||||
if (!!assignmentID) {
|
||||
const assignment = await getAssignment(assignmentID)
|
||||
|
||||
if (!assignment) return redirect(destinationURL || "/exam")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,29 +7,39 @@ 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";
|
||||
import client from "@/lib/mongodb";
|
||||
import { Discount, Package } from "@/interfaces/paypal";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
const user = await requestUser(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)
|
||||
|
||||
const domain = user.email.split("@").pop()
|
||||
const discounts = await db.collection<Discount>("discounts").find({ domain }).toArray()
|
||||
const packages = await db.collection<Package>("packages").find().toArray()
|
||||
|
||||
return {
|
||||
props: serialize({ user, entities }),
|
||||
props: serialize({ user, entities, discounts, packages }),
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
interface Props {
|
||||
user: User,
|
||||
entities: EntityWithRoles[]
|
||||
discounts: Discount[]
|
||||
packages: Package[]
|
||||
}
|
||||
|
||||
export default function Home({ user, entities }: Props) {
|
||||
export default function Home(props: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
@@ -43,7 +53,8 @@ export default function Home({ user, entities }: Props) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<PaymentDue entities={entities} user={user} reload={router.reload} />
|
||||
|
||||
<PaymentDue {...props} reload={router.reload} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Assignment } from "@/interfaces/results";
|
||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
||||
import useGradingSystem from "@/hooks/useGrading";
|
||||
import { mapBy, redirect, serialize } from "@/utils";
|
||||
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||
@@ -31,6 +31,7 @@ import { Grading } from "@/interfaces";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import CardList from "@/components/High/CardList";
|
||||
import { requestUser } from "@/utils/api";
|
||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
const user = await requestUser(req, res)
|
||||
@@ -74,6 +75,7 @@ export default function History({ user, users, assignments, entities, gradingSys
|
||||
const [filter, setFilter] = useState<Filter>();
|
||||
|
||||
const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
|
||||
const allowedDownloadEntities = useAllowedEntities(user, entities, 'download_student_record')
|
||||
|
||||
const renderPdfIcon = usePDFDownload("stats");
|
||||
|
||||
@@ -155,6 +157,9 @@ export default function History({ user, users, assignments, entities, gradingSys
|
||||
|
||||
const customContent = (timestamp: string) => {
|
||||
const dateStats = groupedStats[timestamp];
|
||||
const statUser = findBy(users, 'id', dateStats[0]?.user)
|
||||
|
||||
const canDownload = mapBy(statUser?.entities, 'id').some(e => mapBy(allowedDownloadEntities, 'id').includes(e))
|
||||
|
||||
return (
|
||||
<StatsGridItem
|
||||
@@ -169,7 +174,7 @@ export default function History({ user, users, assignments, entities, gradingSys
|
||||
selectedTrainingExams={selectedTrainingExams}
|
||||
setSelectedTrainingExams={setSelectedTrainingExams}
|
||||
maxTrainingExams={MAX_TRAINING_EXAMS}
|
||||
renderPdfIcon={renderPdfIcon}
|
||||
renderPdfIcon={canDownload ? renderPdfIcon : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -59,7 +59,13 @@ export type RolePermission =
|
||||
"view_statistics" |
|
||||
"download_statistics_report" |
|
||||
"edit_grading_system" |
|
||||
"view_student_performance"
|
||||
"view_student_performance" |
|
||||
"upload_classroom" |
|
||||
"download_user_list" |
|
||||
"view_student_record" |
|
||||
"download_student_record" |
|
||||
"pay_entity" |
|
||||
"view_payment_record"
|
||||
|
||||
export const DEFAULT_PERMISSIONS: RolePermission[] = [
|
||||
"view_students",
|
||||
@@ -132,5 +138,11 @@ export const ADMIN_PERMISSIONS: RolePermission[] = [
|
||||
"view_statistics",
|
||||
"download_statistics_report",
|
||||
"edit_grading_system",
|
||||
"view_student_performance"
|
||||
"view_student_performance",
|
||||
"upload_classroom",
|
||||
"download_user_list",
|
||||
"view_student_record",
|
||||
"download_student_record",
|
||||
"pay_entity",
|
||||
"view_payment_record"
|
||||
]
|
||||
|
||||
@@ -158,7 +158,7 @@ const defaultModuleSettings = (module: Module, minTimer: number): ModuleState =>
|
||||
examLabel: defaultExamLabel(module),
|
||||
minTimer,
|
||||
difficulty: sample(["A1", "A2", "B1", "B2", "C1", "C2"] as Difficulty[])!,
|
||||
isPrivate: false,
|
||||
isPrivate: true,
|
||||
sectionLabels: sectionLabels(module),
|
||||
expandedSections: [1],
|
||||
focusedSection: 1,
|
||||
|
||||
@@ -82,6 +82,17 @@ export const addUsersToEntity = async (users: string[], entity: string, role: st
|
||||
},
|
||||
);
|
||||
|
||||
export const removeUsersFromEntity = async (users: string[], entity: string) =>
|
||||
await db.collection("users").updateMany(
|
||||
{ id: { $in: users } },
|
||||
{
|
||||
// @ts-expect-error
|
||||
$pull: {
|
||||
entities: { id: entity },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteEntity = async (entity: Entity) => {
|
||||
await db.collection("entities").deleteOne({ id: entity.id })
|
||||
await db.collection("roles").deleteMany({ entityID: entity.id })
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Db, ObjectId } from "mongodb";
|
||||
import client from "@/lib/mongodb";
|
||||
import { MODULE_ARRAY } from "./moduleUtils";
|
||||
import { mapBy } from ".";
|
||||
import { getUser } from "./users.be";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
@@ -76,7 +77,7 @@ export const getExams = async (
|
||||
})) as Exam[],
|
||||
).filter((x) => !x.private);
|
||||
|
||||
let exams: Exam[] = await filterByOwners(shuffledPublicExams, userId);
|
||||
let exams: Exam[] = await filterByEntities(shuffledPublicExams, userId);
|
||||
exams = filterByVariant(exams, variant);
|
||||
exams = filterByInstructorGender(exams, instructorGender);
|
||||
exams = await filterByDifficulty(db, exams, module, userId);
|
||||
@@ -109,16 +110,17 @@ const filterByVariant = (exams: Exam[], variant?: Variant) => {
|
||||
return filtered.length > 0 ? filtered : exams;
|
||||
};
|
||||
|
||||
const filterByOwners = async (exams: Exam[], userID?: string) => {
|
||||
if (!userID) return exams.filter((x) => !x.owners || x.owners.length === 0);
|
||||
const filterByEntities = async (exams: Exam[], userID?: string) => {
|
||||
if (!userID) return exams.filter((x) => !x.entities || x.entities.length === 0);
|
||||
|
||||
const user = await getUser(userID)
|
||||
|
||||
return await Promise.all(
|
||||
exams.filter(async (x) => {
|
||||
if (!x.owners) return true;
|
||||
if (x.owners.length === 0) return true;
|
||||
if (x.owners.includes(userID)) return true;
|
||||
if (!x.entities) return true;
|
||||
if (x.entities.length === 0) return true;
|
||||
|
||||
const corporate = await getUserCorporate(userID);
|
||||
return !corporate ? false : x.owners.includes(corporate.id);
|
||||
return mapBy(user?.entities || [], 'id').some(e => x.entities!.includes(e))
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { WithEntity } from "@/interfaces/entity";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
import { CorporateUser, Group, GroupWithUsers, MasterCorporateUser, StudentUser, TeacherUser, Type, User } from "@/interfaces/user";
|
||||
import client from "@/lib/mongodb";
|
||||
import { uniq } from "lodash";
|
||||
import moment from "moment";
|
||||
import { getLinkedUsers, getUser } from "./users.be";
|
||||
import { getSpecificUsers } from "./users.be";
|
||||
@@ -116,7 +117,7 @@ export const getUsersGroups = async (ids: string[]) => {
|
||||
export const convertToUsers = (group: Group, users: User[]): GroupWithUsers =>
|
||||
Object.assign(group, {
|
||||
admin: users.find((u) => u.id === group.admin),
|
||||
participants: group.participants.map((p) => users.find((u) => u.id === p)).filter((x) => !!x) as User[],
|
||||
participants: uniq(group.participants).map((p) => users.find((u) => u.id === p)).filter((x) => !!x) as User[],
|
||||
});
|
||||
|
||||
export const getAllAssignersByCorporate = async (corporateID: string, type: Type): Promise<string[]> => {
|
||||
|
||||
@@ -14,6 +14,8 @@ export const getRolesByEntity = async (entityID: string) => await db.collection(
|
||||
export const getRoles = async (ids?: string[]) => await db.collection("roles").find<Role>(!ids ? {} : { id: { $in: ids } }).toArray();
|
||||
export const getRole = async (id: string) => (await db.collection("roles").findOne<Role>({ id })) ?? undefined;
|
||||
|
||||
export const getDefaultRole = async (entityID: string) => await db.collection("roles").findOne<Role>({ isDefault: true, entityID })
|
||||
|
||||
export const createRole = async (role: Role) => await db.collection("roles").insertOne(role)
|
||||
export const deleteRole = async (id: string) => await db.collection("roles").deleteOne({ id })
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ export const countAllowedUsers = async (user: User, entities: EntityWithRoles[])
|
||||
const student = await countEntitiesUsers(mapBy(studentsAllowedEntities, 'id'), { type: "student" })
|
||||
const teacher = await countEntitiesUsers(mapBy(teachersAllowedEntities, 'id'), { type: "teacher" })
|
||||
const corporate = await countEntitiesUsers(mapBy(corporateAllowedEntities, 'id'), { type: "corporate" })
|
||||
const masterCorporate = await countEntitiesUsers(mapBy(masterCorporateAllowedEntities, 'id'), { type: "mastercorporate" })
|
||||
const mastercorporate = await countEntitiesUsers(mapBy(masterCorporateAllowedEntities, 'id'), { type: "mastercorporate" })
|
||||
|
||||
return { student, teacher, corporate, masterCorporate }
|
||||
return { student, teacher, corporate, mastercorporate }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user