Merged develop into feature/ExamGenRework
This commit is contained in:
@@ -1,29 +1,31 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import Checkbox from "@/components/Low/Checkbox";
|
||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import {Type, User} from "@/interfaces/user";
|
||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||
import { Type, User } from "@/interfaces/user";
|
||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import {capitalize, uniqBy} from "lodash";
|
||||
import { capitalize, uniqBy } from "lodash";
|
||||
import moment from "moment";
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import {toast} from "react-toastify";
|
||||
import { toast } from "react-toastify";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import {useFilePicker} from "use-file-picker";
|
||||
import { useFilePicker } from "use-file-picker";
|
||||
import readXlsxFile from "read-excel-file";
|
||||
import Modal from "@/components/Modal";
|
||||
import {BsFileEarmarkEaselFill, BsQuestionCircleFill} from "react-icons/bs";
|
||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||
import {PermissionType} from "@/interfaces/permissions";
|
||||
import { BsFileEarmarkEaselFill, BsQuestionCircleFill } from "react-icons/bs";
|
||||
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
||||
import { PermissionType } from "@/interfaces/permissions";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import Select from "@/components/Low/Select";
|
||||
|
||||
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
||||
|
||||
const USER_TYPE_PERMISSIONS: {
|
||||
[key in Type]: {perm: PermissionType | undefined; list: Type[]};
|
||||
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
|
||||
} = {
|
||||
student: {
|
||||
perm: "createCodeStudent",
|
||||
@@ -59,11 +61,12 @@ interface Props {
|
||||
user: User;
|
||||
users: User[];
|
||||
permissions: PermissionType[];
|
||||
entities: EntityWithRoles[]
|
||||
onFinish: () => void;
|
||||
}
|
||||
|
||||
export default function BatchCodeGenerator({user, users, permissions, onFinish}: Props) {
|
||||
const [infos, setInfos] = useState<{email: string; name: string; passport_id: string}[]>([]);
|
||||
export default function BatchCodeGenerator({ user, users, entities = [], permissions, onFinish }: Props) {
|
||||
const [infos, setInfos] = useState<{ email: string; name: string; passport_id: string }[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
||||
user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null,
|
||||
@@ -71,8 +74,9 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}:
|
||||
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
||||
const [type, setType] = useState<Type>("student");
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [entity, setEntity] = useState((entities || [])[0]?.id || undefined)
|
||||
|
||||
const {openFilePicker, filesContent, clear} = useFilePicker({
|
||||
const { openFilePicker, filesContent, clear } = useFilePicker({
|
||||
accept: ".xlsx",
|
||||
multiple: false,
|
||||
readAs: "ArrayBuffer",
|
||||
@@ -93,10 +97,10 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}:
|
||||
const [firstName, lastName, country, passport_id, email, phone] = row as string[];
|
||||
return EMAIL_REGEX.test(email.toString().trim())
|
||||
? {
|
||||
email: email.toString().trim().toLowerCase(),
|
||||
name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
|
||||
passport_id: passport_id?.toString().trim() || undefined,
|
||||
}
|
||||
email: email.toString().trim().toLowerCase(),
|
||||
name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
|
||||
passport_id: passport_id?.toString().trim() || undefined,
|
||||
}
|
||||
: undefined;
|
||||
})
|
||||
.filter((x) => !!x) as typeof infos,
|
||||
@@ -139,7 +143,7 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}:
|
||||
return;
|
||||
|
||||
setIsLoading(true);
|
||||
Promise.all(existingUsers.map(async (u) => await axios.post(`/api/invites`, {to: u.id, from: user.id})))
|
||||
Promise.all(existingUsers.map(async (u) => await axios.post(`/api/invites`, { to: u.id, from: user.id })))
|
||||
.then(() => toast.success(`Successfully invited ${existingUsers.length} registered student(s)!`))
|
||||
.finally(() => {
|
||||
if (newUsers.length === 0) setIsLoading(false);
|
||||
@@ -155,19 +159,20 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}:
|
||||
|
||||
setIsLoading(true);
|
||||
axios
|
||||
.post<{ok: boolean; valid?: number; reason?: string}>("/api/code", {
|
||||
.post<{ ok: boolean; valid?: number; reason?: string }>("/api/code", {
|
||||
type,
|
||||
codes,
|
||||
infos: informations,
|
||||
infos: informations.map((info, index) => ({ ...info, code: codes[index] })),
|
||||
expiryDate,
|
||||
entity
|
||||
})
|
||||
.then(({data, status}) => {
|
||||
.then(({ data, status }) => {
|
||||
if (data.ok) {
|
||||
toast.success(
|
||||
`Successfully generated${data.valid ? ` ${data.valid}/${informations.length}` : ""} ${capitalize(
|
||||
type,
|
||||
)} codes and they have been notified by e-mail!`,
|
||||
{toastId: "success"},
|
||||
{ toastId: "success" },
|
||||
);
|
||||
|
||||
onFinish();
|
||||
@@ -175,12 +180,12 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}:
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
toast.error(data.reason, {toastId: "forbidden"});
|
||||
toast.error(data.reason, { toastId: "forbidden" });
|
||||
}
|
||||
})
|
||||
.catch(({response: {status, data}}) => {
|
||||
.catch(({ response: { status, data } }) => {
|
||||
if (status === 403) {
|
||||
toast.error(data.reason, {toastId: "forbidden"});
|
||||
toast.error(data.reason, { toastId: "forbidden" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,6 +263,15 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}:
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className={clsx("flex flex-col gap-4")}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
||||
<Select
|
||||
defaultValue={{ value: (entities || [])[0]?.id, label: (entities || [])[0]?.label }}
|
||||
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
||||
onChange={(e) => setEntity(e?.value || undefined)}
|
||||
isClearable={checkAccess(user, ["admin", "developer"])}
|
||||
/>
|
||||
</div>
|
||||
<label className="text-mti-gray-dim text-base font-normal">Select the type of user they should be</label>
|
||||
{user && (
|
||||
<select
|
||||
@@ -266,7 +280,7 @@ export default function BatchCodeGenerator({user, users, permissions, onFinish}:
|
||||
className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none">
|
||||
{Object.keys(USER_TYPE_LABELS)
|
||||
.filter((x) => {
|
||||
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
|
||||
const { list, perm } = USER_TYPE_PERMISSIONS[x as Type];
|
||||
return checkAccess(user, getTypesOfUser(list), permissions, perm);
|
||||
})
|
||||
.map((type) => (
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import Checkbox from "@/components/Low/Checkbox";
|
||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||
import {Type, User} from "@/interfaces/user";
|
||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
||||
import { Type, User } from "@/interfaces/user";
|
||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import {capitalize} from "lodash";
|
||||
import { capitalize } from "lodash";
|
||||
import moment from "moment";
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import {toast} from "react-toastify";
|
||||
import { toast } from "react-toastify";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
|
||||
import {PermissionType} from "@/interfaces/permissions";
|
||||
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
||||
import { PermissionType } from "@/interfaces/permissions";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import Select from "@/components/Low/Select";
|
||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||
|
||||
const USER_TYPE_PERMISSIONS: {
|
||||
[key in Type]: {perm: PermissionType | undefined; list: Type[]};
|
||||
[key in Type]: { perm: PermissionType | undefined; list: Type[] };
|
||||
} = {
|
||||
student: {
|
||||
perm: "createCodeStudent",
|
||||
@@ -51,16 +54,19 @@ const USER_TYPE_PERMISSIONS: {
|
||||
interface Props {
|
||||
user: User;
|
||||
permissions: PermissionType[];
|
||||
entities: EntityWithRoles[]
|
||||
onFinish: () => void;
|
||||
}
|
||||
|
||||
export default function CodeGenerator({user, permissions, onFinish}: Props) {
|
||||
export default function CodeGenerator({ user, entities = [], permissions, onFinish }: Props) {
|
||||
const [generatedCode, setGeneratedCode] = useState<string>();
|
||||
|
||||
const [expiryDate, setExpiryDate] = useState<Date | null>(
|
||||
user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null,
|
||||
);
|
||||
const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true);
|
||||
const [type, setType] = useState<Type>("student");
|
||||
const [entity, setEntity] = useState((entities || [])[0]?.id || undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpiryDateEnabled) setExpiryDate(null);
|
||||
@@ -71,8 +77,8 @@ export default function CodeGenerator({user, permissions, onFinish}: Props) {
|
||||
const code = uid.randomUUID(6);
|
||||
|
||||
axios
|
||||
.post("/api/code", {type, codes: [code], expiryDate})
|
||||
.then(({data, status}) => {
|
||||
.post("/api/code", { type, codes: [code], expiryDate, entity })
|
||||
.then(({ data, status }) => {
|
||||
if (data.ok) {
|
||||
toast.success(`Successfully generated a ${capitalize(type)} code!`, {
|
||||
toastId: "success",
|
||||
@@ -82,12 +88,12 @@ export default function CodeGenerator({user, permissions, onFinish}: Props) {
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
toast.error(data.reason, {toastId: "forbidden"});
|
||||
toast.error(data.reason, { toastId: "forbidden" });
|
||||
}
|
||||
})
|
||||
.catch(({response: {status, data}}) => {
|
||||
.catch(({ response: { status, data } }) => {
|
||||
if (status === 403) {
|
||||
toast.error(data.reason, {toastId: "forbidden"});
|
||||
toast.error(data.reason, { toastId: "forbidden" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,14 +106,25 @@ export default function CodeGenerator({user, permissions, onFinish}: Props) {
|
||||
return (
|
||||
<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">User Code Generator</label>
|
||||
{user && (
|
||||
<div className={clsx("flex flex-col gap-4")}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
||||
<Select
|
||||
defaultValue={{ value: (entities || [])[0]?.id, label: (entities || [])[0]?.label }}
|
||||
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
||||
onChange={(e) => setEntity(e?.value || undefined)}
|
||||
isClearable={checkAccess(user, ["admin", "developer"])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={clsx("flex flex-col gap-4")}>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Type</label>
|
||||
<select
|
||||
defaultValue="student"
|
||||
onChange={(e) => setType(e.target.value as typeof user.type)}
|
||||
className="p-6 w-full min-w-[350px] min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
|
||||
{Object.keys(USER_TYPE_LABELS)
|
||||
.filter((x) => {
|
||||
const {list, perm} = USER_TYPE_PERMISSIONS[x as Type];
|
||||
const { list, perm } = USER_TYPE_PERMISSIONS[x as Type];
|
||||
return checkAccess(user, getTypesOfUser(list), permissions, perm);
|
||||
})
|
||||
.map((type) => (
|
||||
@@ -116,8 +133,9 @@ export default function CodeGenerator({user, permissions, onFinish}: Props) {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||
</div>
|
||||
|
||||
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||
<>
|
||||
<div className="-md:flex-row -md:items-center flex justify-between gap-2 md:flex-col 2xl:flex-row 2xl:items-center">
|
||||
<label className="text-mti-gray-dim text-base font-normal">Expiry Date</label>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import Input from "@/components/Low/Input";
|
||||
import Select from "@/components/Low/Select";
|
||||
import Separator from "@/components/Low/Separator";
|
||||
import { Grading, Step } from "@/interfaces";
|
||||
import { Entity } from "@/interfaces/entity";
|
||||
import { User } from "@/interfaces/user";
|
||||
import { CEFR_STEPS, GENERAL_STEPS, IELTS_STEPS, TOFEL_STEPS } from "@/resources/grading";
|
||||
import { mapBy } from "@/utils";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import { Divider } from "primereact/divider";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BsPlusCircle, BsTrash } from "react-icons/bs";
|
||||
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 [isLoading, setIsLoading] = useState(false);
|
||||
const [steps, setSteps] = useState<Step[]>([]);
|
||||
const [otherEntities, setOtherEntities] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (entity) {
|
||||
@@ -63,6 +67,27 @@ export default function CorporateGradingSystem({ user, entitiesGrading = [], ent
|
||||
.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 (
|
||||
<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>
|
||||
@@ -76,6 +101,22 @@ export default function CorporateGradingSystem({ user, entitiesGrading = [], ent
|
||||
/>
|
||||
</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>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Button variant="outline" onClick={() => setSteps(CEFR_STEPS)}>
|
||||
|
||||
@@ -1,81 +1,47 @@
|
||||
import Button from "@/components/Low/Button";
|
||||
import Checkbox from "@/components/Low/Checkbox";
|
||||
import Select from "@/components/Low/Select";
|
||||
import useCodes from "@/hooks/useCodes";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import { Code, User } from "@/interfaces/user";
|
||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { BsTrash } from "react-icons/bs";
|
||||
import { toast } from "react-toastify";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import clsx from "clsx";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { isAdmin } from "@/utils/users";
|
||||
import { findBy, mapBy } from "@/utils";
|
||||
import useEntitiesCodes from "@/hooks/useEntitiesCodes";
|
||||
import Table from "@/components/High/Table";
|
||||
|
||||
const columnHelper = createColumnHelper<Code>();
|
||||
type TableData = Code & { entity?: EntityWithRoles, creator?: User }
|
||||
const columnHelper = createColumnHelper<TableData>();
|
||||
|
||||
const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
|
||||
const [creatorUser, setCreatorUser] = useState<User>();
|
||||
|
||||
useEffect(() => {
|
||||
setCreatorUser(users.find((x) => x.id === id));
|
||||
}, [id, users]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(creatorUser?.name || "N/A") || "N/A"}{" "}
|
||||
{creatorUser && `(${USER_TYPE_LABELS[creatorUser?.type]})`}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function CodeList({ user, canDeleteCodes }: { user: User, canDeleteCodes?: boolean }) {
|
||||
export default function CodeList({ user, entities, canDeleteCodes }
|
||||
: { user: User, entities: EntityWithRoles[], canDeleteCodes?: boolean }) {
|
||||
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
||||
|
||||
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(user?.type === "corporate" ? user : undefined);
|
||||
const [filterAvailability, setFilterAvailability] = useState<"in-use" | "unused">();
|
||||
|
||||
const { permissions } = usePermissions(user?.id || "");
|
||||
const entityIDs = useMemo(() => mapBy(entities, 'id'), [entities])
|
||||
|
||||
const { users } = useUsers();
|
||||
const { codes, reload } = useCodes(user?.type === "corporate" ? user?.id : undefined);
|
||||
const { codes, reload } = useEntitiesCodes(isAdmin(user) ? undefined : entityIDs)
|
||||
|
||||
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
|
||||
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
|
||||
const filteredCodes = useMemo(() => {
|
||||
return codes.filter((x) => {
|
||||
// TODO: if the expiry date is missing, it does not make sense to filter by date
|
||||
// so we need to find a way to handle this edge case
|
||||
if (startDate && endDate && x.expiryDate) {
|
||||
const date = moment(x.expiryDate);
|
||||
if (date.isBefore(startDate) || date.isAfter(endDate)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filteredCorporate && x.creator !== filteredCorporate.id) return false;
|
||||
if (filterAvailability) {
|
||||
if (filterAvailability === "in-use" && !x.userId) return false;
|
||||
if (filterAvailability === "unused" && x.userId) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [codes, startDate, endDate, filteredCorporate, filterAvailability]);
|
||||
const data: TableData[] = useMemo(() => codes.map((code) => ({
|
||||
...code,
|
||||
entity: findBy(entities, 'id', code.entity),
|
||||
creator: findBy(users, 'id', code.creator)
|
||||
})) as TableData[], [codes, entities, users])
|
||||
|
||||
const toggleCode = (id: string) => {
|
||||
setSelectedCodes((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
||||
};
|
||||
|
||||
const toggleAllCodes = (checked: boolean) => {
|
||||
if (checked) return setSelectedCodes(filteredCodes.filter((x) => !x.userId).map((x) => x.code));
|
||||
// const toggleAllCodes = (checked: boolean) => {
|
||||
// if (checked) return setSelectedCodes(visibleRows.filter((x) => !x.userId).map((x) => x.code));
|
||||
|
||||
return setSelectedCodes([]);
|
||||
};
|
||||
// return setSelectedCodes([]);
|
||||
// };
|
||||
|
||||
const deleteCodes = async (codes: string[]) => {
|
||||
if (!canDeleteCodes) return
|
||||
@@ -132,16 +98,8 @@ export default function CodeList({ user, canDeleteCodes }: { user: User, canDele
|
||||
const defaultColumns = [
|
||||
columnHelper.accessor("code", {
|
||||
id: "codeCheckbox",
|
||||
header: () => (
|
||||
<Checkbox
|
||||
disabled={filteredCodes.filter((x) => !x.userId).length === 0}
|
||||
isChecked={
|
||||
selectedCodes.length === filteredCodes.filter((x) => !x.userId).length && filteredCodes.filter((x) => !x.userId).length > 0
|
||||
}
|
||||
onChange={(checked) => toggleAllCodes(checked)}>
|
||||
{""}
|
||||
</Checkbox>
|
||||
),
|
||||
enableSorting: false,
|
||||
header: () => (""),
|
||||
cell: (info) =>
|
||||
!info.row.original.userId ? (
|
||||
<Checkbox isChecked={selectedCodes.includes(info.getValue())} onChange={() => toggleCode(info.getValue())}>
|
||||
@@ -158,12 +116,16 @@ export default function CodeList({ user, canDeleteCodes }: { user: User, canDele
|
||||
cell: (info) => (info.getValue() ? moment(info.getValue()).format("DD/MM/YYYY") : "N/A"),
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: "Invited E-mail",
|
||||
header: "E-mail",
|
||||
cell: (info) => info.getValue() || "N/A",
|
||||
}),
|
||||
columnHelper.accessor("creator", {
|
||||
header: "Creator",
|
||||
cell: (info) => <CreatorCell id={info.getValue()} users={users} />,
|
||||
cell: (info) => info.getValue() ? `${info.getValue().name} (${USER_TYPE_LABELS[info.getValue().type]})` : "N/A",
|
||||
}),
|
||||
columnHelper.accessor("entity", {
|
||||
header: "Entity",
|
||||
cell: (info) => info.getValue()?.label || "N/A",
|
||||
}),
|
||||
columnHelper.accessor("userId", {
|
||||
header: "Availability",
|
||||
@@ -195,71 +157,11 @@ export default function CodeList({ user, canDeleteCodes }: { user: User, canDele
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredCodes,
|
||||
columns: defaultColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between pb-4 pt-1">
|
||||
<div className="flex items-center gap-4">
|
||||
<Select
|
||||
className="!w-96 !py-1"
|
||||
disabled={user?.type === "corporate"}
|
||||
isClearable
|
||||
placeholder="Corporate"
|
||||
value={
|
||||
filteredCorporate
|
||||
? {
|
||||
label: `${filteredCorporate.name} (${USER_TYPE_LABELS[filteredCorporate?.type]})`,
|
||||
value: filteredCorporate.id,
|
||||
}
|
||||
: null
|
||||
}
|
||||
options={users
|
||||
.filter((x) => ["admin", "developer", "corporate"].includes(x.type))
|
||||
.map((x) => ({
|
||||
label: `${x.name} (${USER_TYPE_LABELS[x.type]})`,
|
||||
value: x.id,
|
||||
user: x,
|
||||
}))}
|
||||
onChange={(value) => setFilteredCorporate(value ? users.find((x) => x.id === value?.value) : undefined)}
|
||||
/>
|
||||
<Select
|
||||
className="!w-96 !py-1"
|
||||
placeholder="Availability"
|
||||
isClearable
|
||||
options={[
|
||||
{ label: "In Use", value: "in-use" },
|
||||
{ label: "Unused", value: "unused" },
|
||||
]}
|
||||
onChange={(value) => setFilterAvailability(value ? (value.value as typeof filterAvailability) : undefined)}
|
||||
/>
|
||||
<ReactDatePicker
|
||||
dateFormat="dd/MM/yyyy"
|
||||
className="px-4 py-6 w-full text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||
selected={startDate}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
selectsRange
|
||||
showMonthDropdown
|
||||
filterDate={(date: Date) => moment(date).isSameOrBefore(moment(new Date()))}
|
||||
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
||||
setStartDate(initialDate ?? moment("01/01/2023").toDate());
|
||||
if (finalDate) {
|
||||
// basicly selecting a final day works as if I'm selecting the first
|
||||
// minute of that day. this way it covers the whole day
|
||||
setEndDate(moment(finalDate).endOf("day").toDate());
|
||||
return;
|
||||
}
|
||||
setEndDate(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{canDeleteCodes && (
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex gap-4 items-center w-full justify-end">
|
||||
<span>{selectedCodes.length} code(s) selected</span>
|
||||
<Button
|
||||
disabled={selectedCodes.length === 0}
|
||||
@@ -272,30 +174,11 @@ export default function CodeList({ user, canDeleteCodes }: { user: User, canDele
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th className="p-4 text-left" key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="px-2">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td className="px-4 py-2" key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Table<TableData>
|
||||
data={data}
|
||||
columns={defaultColumns}
|
||||
searchFields={[["code"], ["email"], ["entity", "label"], ["creator", "name"], ['creator', 'type']]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,31 +3,30 @@ import Checkbox from "@/components/Low/Checkbox";
|
||||
import Input from "@/components/Low/Input";
|
||||
import Select from "@/components/Low/Select";
|
||||
import Modal from "@/components/Modal";
|
||||
import useCodes from "@/hooks/useCodes";
|
||||
import useDiscounts from "@/hooks/useDiscounts";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import {Discount} from "@/interfaces/paypal";
|
||||
import {Code, User} from "@/interfaces/user";
|
||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
|
||||
import { Discount } from "@/interfaces/paypal";
|
||||
import { Code, User } from "@/interfaces/user";
|
||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import moment from "moment";
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import {BsPencil, BsTrash} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
import { BsPencil, BsTrash } from "react-icons/bs";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const columnHelper = createColumnHelper<Discount>();
|
||||
|
||||
const DiscountCreator = ({discount, onClose}: {discount?: Discount; onClose: () => void}) => {
|
||||
const DiscountCreator = ({ discount, onClose }: { discount?: Discount; onClose: () => void }) => {
|
||||
const [percentage, setPercentage] = useState(discount?.percentage);
|
||||
const [domain, setDomain] = useState(discount?.domain);
|
||||
const [validUntil, setValidUntil] = useState(discount?.validUntil);
|
||||
|
||||
const submit = async () => {
|
||||
const body = {percentage, domain, validUntil: validUntil?.toISOString() || undefined};
|
||||
const body = { percentage, domain, validUntil: validUntil?.toISOString() || undefined };
|
||||
|
||||
if (discount) {
|
||||
return axios
|
||||
@@ -112,7 +111,7 @@ const DiscountCreator = ({discount, onClose}: {discount?: Discount; onClose: ()
|
||||
);
|
||||
};
|
||||
|
||||
export default function DiscountList({user}: {user: User}) {
|
||||
export default function DiscountList({ user }: { user: User }) {
|
||||
const [selectedDiscounts, setSelectedDiscounts] = useState<string[]>([]);
|
||||
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
@@ -120,8 +119,8 @@ export default function DiscountList({user}: {user: User}) {
|
||||
|
||||
const [filteredDiscounts, setFilteredDiscounts] = useState<Discount[]>([]);
|
||||
|
||||
const {users} = useUsers();
|
||||
const {discounts, reload} = useDiscounts();
|
||||
const { users } = useUsers();
|
||||
const { discounts, reload } = useDiscounts();
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredDiscounts(discounts);
|
||||
@@ -220,7 +219,7 @@ export default function DiscountList({user}: {user: User}) {
|
||||
{
|
||||
header: "",
|
||||
id: "actions",
|
||||
cell: ({row}: {row: {original: Discount}}) => {
|
||||
cell: ({ row }: { row: { original: Discount } }) => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function Lists({ user, entities = [], permissions }: Props) {
|
||||
Exam List
|
||||
</Tab>
|
||||
)}
|
||||
{checkAccess(user, ["developer", "admin", "corporate"]) && entitiesViewCodes.length > 0 && (
|
||||
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && entitiesViewCodes.length > 0 && (
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
clsx(
|
||||
@@ -106,7 +106,7 @@ export default function Lists({ user, entities = [], permissions }: Props) {
|
||||
)}
|
||||
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && entitiesViewCodes.length > 0 && (
|
||||
<TabPanel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide">
|
||||
<CodeList user={user} canDeleteCodes={entitiesDeleteCodes.length > 0} />
|
||||
<CodeList user={user} entities={entitiesViewCodes} canDeleteCodes={entitiesDeleteCodes.length > 0} />
|
||||
</TabPanel>
|
||||
)}
|
||||
{checkAccess(user, ["developer", "admin"]) && (
|
||||
|
||||
Reference in New Issue
Block a user