ENCOA-275

This commit is contained in:
Tiago Ribeiro
2024-12-13 15:13:27 +00:00
parent 61d1bbbe13
commit f3057c675f
4 changed files with 109 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
import clsx from "clsx"; import clsx from "clsx";
import {ComponentProps, useEffect, useState} from "react"; import { ComponentProps, useEffect, useState } from "react";
import ReactSelect, {GroupBase, StylesConfig} from "react-select"; import ReactSelect, { GroupBase, StylesConfig } from "react-select";
import Option from "@/interfaces/option"; import Option from "@/interfaces/option";
interface Props { interface Props {
@@ -9,14 +9,23 @@ interface Props {
options: Option[]; options: Option[];
disabled?: boolean; disabled?: boolean;
placeholder?: string; placeholder?: string;
onChange: (value: Option | null) => void;
isClearable?: boolean; isClearable?: boolean;
styles?: StylesConfig<Option, boolean, GroupBase<Option>>; styles?: StylesConfig<Option, boolean, GroupBase<Option>>;
className?: string; className?: string;
label?: string; label?: string;
} }
export default function Select({value, defaultValue, options, placeholder, disabled, onChange, styles, isClearable, label, className}: Props) { interface MultiProps {
isMulti: true
onChange: (value: Option[] | null) => void
}
interface SingleProps {
isMulti?: false
onChange: (value: Option | null) => void
}
export default function Select({ value, isMulti, defaultValue, options, placeholder, disabled, onChange, styles, isClearable, label, className }: Props & (MultiProps | SingleProps)) {
const [target, setTarget] = useState<HTMLElement>(); const [target, setTarget] = useState<HTMLElement>();
useEffect(() => { useEffect(() => {
@@ -27,14 +36,15 @@ export default function Select({value, defaultValue, options, placeholder, disab
<div className="w-full flex flex-col gap-3"> <div className="w-full flex flex-col gap-3">
{label && <label className="font-normal text-base text-mti-gray-dim">{label}</label>} {label && <label className="font-normal text-base text-mti-gray-dim">{label}</label>}
<ReactSelect <ReactSelect
isMulti={isMulti}
className={ className={
styles styles
? undefined ? undefined
: clsx( : clsx(
"placeholder:text-mti-gray-cool border-mti-gray-platinum w-full rounded-full border bg-white px-4 py-4 text-sm font-normal focus:outline-none", "placeholder:text-mti-gray-cool border-mti-gray-platinum w-full rounded-full border bg-white px-4 py-4 text-sm font-normal focus:outline-none",
disabled && "!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed", disabled && "!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
className, className,
) )
} }
options={options} options={options}
value={value} value={value}
@@ -44,7 +54,7 @@ export default function Select({value, defaultValue, options, placeholder, disab
defaultValue={defaultValue} defaultValue={defaultValue}
styles={ styles={
styles || { styles || {
menuPortal: (base) => ({...base, zIndex: 9999}), menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (styles) => ({ control: (styles) => ({
...styles, ...styles,
paddingLeft: "4px", paddingLeft: "4px",

View File

@@ -1,13 +1,16 @@
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import Separator from "@/components/Low/Separator";
import { Grading, Step } from "@/interfaces"; import { Grading, Step } from "@/interfaces";
import { Entity } from "@/interfaces/entity"; import { Entity } from "@/interfaces/entity";
import { User } from "@/interfaces/user"; import { User } from "@/interfaces/user";
import { CEFR_STEPS, GENERAL_STEPS, IELTS_STEPS, TOFEL_STEPS } from "@/resources/grading"; import { CEFR_STEPS, GENERAL_STEPS, IELTS_STEPS, TOFEL_STEPS } from "@/resources/grading";
import { mapBy } from "@/utils";
import { checkAccess } from "@/utils/permissions"; import { checkAccess } from "@/utils/permissions";
import axios from "axios"; import axios from "axios";
import clsx from "clsx"; import clsx from "clsx";
import { Divider } from "primereact/divider";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { BsPlusCircle, BsTrash } from "react-icons/bs"; import { BsPlusCircle, BsTrash } from "react-icons/bs";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -36,6 +39,7 @@ export default function CorporateGradingSystem({ user, entitiesGrading = [], ent
const [entity, setEntity] = useState(entitiesGrading[0]?.entity || undefined) const [entity, setEntity] = useState(entitiesGrading[0]?.entity || undefined)
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [steps, setSteps] = useState<Step[]>([]); const [steps, setSteps] = useState<Step[]>([]);
const [otherEntities, setOtherEntities] = useState<string[]>([])
useEffect(() => { useEffect(() => {
if (entity) { if (entity) {
@@ -63,6 +67,27 @@ export default function CorporateGradingSystem({ user, entitiesGrading = [], ent
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}; };
const applyToOtherEntities = () => {
if (!steps.every((x) => x.min < x.max)) return toast.error("One of your steps has a minimum threshold inferior to its superior threshold.");
if (areStepsOverlapped(steps)) return toast.error("There seems to be an overlap in one of your steps.");
if (
steps.reduce((acc, curr) => {
return acc - (curr.max - curr.min + 1);
}, 100) > 0
)
return toast.error("There seems to be an open interval in your steps.");
if (otherEntities.length === 0) return toast.error("Select at least one entity")
setIsLoading(true);
axios
.post("/api/grading/multiple", { user: user.id, entities: otherEntities, steps })
.then(() => toast.success("Your grading system has been saved!"))
.then(mutate)
.catch(() => toast.error("Something went wrong, please try again later"))
.finally(() => setIsLoading(false));
};
return ( return (
<div className="flex flex-col gap-4 border p-4 border-mti-gray-platinum rounded-xl"> <div className="flex flex-col gap-4 border p-4 border-mti-gray-platinum rounded-xl">
<label className="font-normal text-base text-mti-gray-dim">Grading System</label> <label className="font-normal text-base text-mti-gray-dim">Grading System</label>
@@ -76,6 +101,22 @@ export default function CorporateGradingSystem({ user, entitiesGrading = [], ent
/> />
</div> </div>
{entities.length > 1 && (
<>
<Separator />
<label className="font-normal text-base text-mti-gray-dim">Apply this grading system to other entities</label>
<Select
options={entities.map((e) => ({ value: e.id, label: e.label }))}
onChange={(e) => !e ? setOtherEntities([]) : setOtherEntities(e.map(o => o.value!))}
isMulti
/>
<Button onClick={applyToOtherEntities} isLoading={isLoading} disabled={isLoading || otherEntities.length === 0} variant="outline">
Apply to {otherEntities.length} other entities
</Button>
<Separator />
</>
)}
<label className="font-normal text-base text-mti-gray-dim">Preset Systems</label> <label className="font-normal text-base text-mti-gray-dim">Preset Systems</label>
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
<Button variant="outline" onClick={() => setSteps(CEFR_STEPS)}> <Button variant="outline" onClick={() => setSteps(CEFR_STEPS)}>

View File

@@ -0,0 +1,48 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { CorporateUser, Group } from "@/interfaces/user";
import { Discount, Package } from "@/interfaces/paypal";
import { v4 } from "uuid";
import { checkAccess } from "@/utils/permissions";
import { CEFR_STEPS } from "@/resources/grading";
import { getCorporateUser } from "@/resources/user";
import { getUserCorporate } from "@/utils/groups.be";
import { Grading, Step } from "@/interfaces";
import { getGroupsForUser } from "@/utils/groups.be";
import { uniq } from "lodash";
import { getSpecificUsers, getUser } from "@/utils/users.be";
import client from "@/lib/mongodb";
import { getGradingSystemByEntity } from "@/utils/grading.be";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") await post(req, res);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
if (!checkAccess(req.session.user, ["admin", "developer", "mastercorporate", "corporate"]))
return res.status(403).json({
ok: false,
reason: "You do not have permission to create a new grading system",
});
const body = req.body as {
entities: string[]
steps: Step[];
};
await db.collection("grading").updateMany({ entity: { $in: body.entities } }, { $set: { steps: body.steps } }, { upsert: true });
res.status(200).json({ ok: true });
}

View File

@@ -149,7 +149,7 @@ export default function Dashboard({
<IconCard Icon={BsPersonFillGear} <IconCard Icon={BsPersonFillGear}
onClick={() => router.push("/users/performance")} onClick={() => router.push("/users/performance")}
label="Student Performance" label="Student Performance"
value={students.length} value={usersCount.student}
color="purple" color="purple"
/> />
<IconCard <IconCard