ENCOA-275
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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)}>
|
||||||
|
|||||||
48
src/pages/api/grading/multiple.ts
Normal file
48
src/pages/api/grading/multiple.ts
Normal 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 });
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user