ENCOA-263

This commit is contained in:
Tiago Ribeiro
2024-12-11 22:00:43 +00:00
parent ce35ba71f4
commit 1a7d35317b
10 changed files with 234 additions and 194 deletions

View File

@@ -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) => (

View File

@@ -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>

View File

@@ -16,6 +16,9 @@ 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 } from "@/utils";
const columnHelper = createColumnHelper<Code>();
@@ -34,16 +37,15 @@ const CreatorCell = ({ id, users }: { id: string; users: User[] }) => {
);
};
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 { users } = useUsers();
const { codes, reload } = useCodes(user?.type === "corporate" ? user?.id : undefined);
const { codes, reload } = useCodes();
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate());
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate());
@@ -158,13 +160,17 @@ 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} />,
}),
columnHelper.accessor("entity", {
header: "Entity",
cell: (info) => findBy(entities, 'id', info.getValue())?.label || "N/A",
}),
columnHelper.accessor("userId", {
header: "Availability",
cell: (info) =>

View File

@@ -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

View File

@@ -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"]) && (