Exam generation rework, batch user tables, fastapi endpoint switch

This commit is contained in:
Carlos-Mesquita
2024-11-04 23:29:14 +00:00
parent a2bc997e8f
commit 15c9c4d4bd
148 changed files with 11348 additions and 3901 deletions

View File

@@ -0,0 +1,18 @@
import { Type as UserType} from "@/interfaces/user";
export type Type = Exclude<UserType, "admin" | "developer" | "agent" | "mastercorporate">;
export interface UserImport {
email: string;
name: string;
passport_id: string;
type: Type;
groupName: string;
corporate: string;
studentID: string;
demographicInformation: {
country: string;
passport_id: string;
phone: string;
};
}

View File

@@ -0,0 +1,200 @@
import React, { useState } from 'react';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
FilterFn,
} from '@tanstack/react-table';
import { UserImport } from "./IUserImport";
const globalFilterFn: FilterFn<any> = (row, columnId, filterValue: string) => {
const value = row.getValue(columnId);
return String(value).toLowerCase().includes(filterValue.toLowerCase());
};
const columnHelper = createColumnHelper<UserImport>();
const columns = [
columnHelper.accessor('name', {
cell: info => info.getValue(),
header: () => 'Name',
}),
columnHelper.accessor('studentID', {
cell: info => info.getValue(),
header: () => 'Student ID',
}),
columnHelper.accessor('demographicInformation.passport_id', {
cell: info => info.getValue(),
header: () => 'Passport/National ID',
}),
columnHelper.accessor('email', {
cell: info => info.getValue(),
header: () => 'Email',
}),
columnHelper.accessor('demographicInformation.phone', {
cell: info => info.getValue(),
header: () => 'Phone Number',
}),
columnHelper.accessor('corporate', {
cell: info => info.getValue(),
header: () => 'Corporate (e-mail)',
}),
columnHelper.accessor('groupName', {
cell: info => info.getValue(),
header: () => 'Group Name',
}),
columnHelper.accessor('demographicInformation.country', {
cell: info => info.getValue(),
header: () => 'Country',
}),
];
const UserTable: React.FC<{ users: UserImport[] }> = ({ users }) => {
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data: users,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
globalFilterFn: globalFilterFn,
state: {
globalFilter,
},
onGlobalFilterChange: setGlobalFilter,
initialState: {
pagination: {
pageSize: 5,
},
}
});
return (
<div className='flex flex-col'>
<div className="flex flew-row w-full mb-4 justify-between gap-4">
<input
type="text"
value={globalFilter ?? ''}
onChange={e => setGlobalFilter(e.target.value)}
placeholder="Search ..."
className="p-2 border rounded flex-grow"
/>
<select
value={table.getState().pagination.pageSize}
onChange={e => {
table.setPageSize(Number(e.target.value));
}}
className="p-2 border rounded"
>
{[5, 10, 15, 20].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
<table className="w-full">
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
className='bg-mti-purple-ultralight/80 first:rounded-tl-3xl last:rounded-tr-3xl py-4 first:pl-6 text-mti-purple-light cursor-pointer'
onClick={header.column.getToggleSortingHandler()}
>
{header.isPlaceholder ? null : (
<div className='flex flex-row justify-between'>
<span>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</span>
<span className='pr-6'>
{{
asc: ' 🔼',
desc: ' 🔽',
}[header.column.getIsSorted() as string] ?? null}
</span>
</div>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row, index, array) => {
const isLastRow = index === array.length - 1;
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<td
key={cell.id}
className={
isLastRow
? `first:rounded-bl-3xl last:rounded-br-3xl py-4 first:pl-6 bg-mti-purple-ultralight/40`
: `first:pl-6 py-4 border-b bg-mti-purple-ultralight/40`
}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
<div className="mt-4 flex items-center gap-4 mx-auto">
<button
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
className="px-4 py-2 bg-mti-purple-light text-white rounded disabled:opacity-50"
>
{'<<'}
</button>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
className="px-4 py-2 bg-mti-purple-light text-white rounded disabled:opacity-50"
>
{'<'}
</button>
<span>
Page{' '}
<strong>
{table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</strong>
</span>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
className="px-4 py-2 bg-mti-purple-light text-white rounded disabled:opacity-50"
>
{'>'}
</button>
<button
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
className="px-4 py-2 bg-mti-purple-light text-white rounded disabled:opacity-50"
>
{'>>'}
</button>
</div>
</div>
);
};
export default UserTable;

View File

@@ -1,35 +1,33 @@
import Button from "@/components/Low/Button";
import useUsers from "@/hooks/useUsers";
import {Type as UserType, User} from "@/interfaces/user";
import axios from "axios";
import {uniqBy} from "lodash";
import {useEffect, useState} from "react";
import {toast} from "react-toastify";
import {useFilePicker} from "use-file-picker";
import { uniqBy } from "lodash";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useFilePicker } from "use-file-picker";
import readXlsxFile from "read-excel-file";
import Modal from "@/components/Modal";
import {BsQuestionCircleFill} from "react-icons/bs";
import {PermissionType} from "@/interfaces/permissions";
import { BsQuestionCircleFill } from "react-icons/bs";
import { PermissionType } from "@/interfaces/permissions";
import moment from "moment";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import Checkbox from "@/components/Low/Checkbox";
import ReactDatePicker from "react-datepicker";
import clsx from "clsx";
import usePermissions from "@/hooks/usePermissions";
import countryCodes from "country-codes-list";
import { User, Type as UserType } from "@/interfaces/user";
import { Type, UserImport } from "./IUserImport";
import UserTable from "./UserTable";
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
type Type = Exclude<UserType, "admin" | "developer" | "agent" | "mastercorporate">;
const USER_TYPE_LABELS: {[key in Type]: string} = {
const USER_TYPE_LABELS: { [key in Type]: string } = {
student: "Student",
teacher: "Teacher",
corporate: "Corporate",
};
const USER_TYPE_PERMISSIONS: {
[key in UserType]: {perm: PermissionType | undefined; list: UserType[]};
[key in UserType]: { perm: PermissionType | undefined; list: UserType[] };
} = {
student: {
perm: "createCodeStudent",
@@ -63,25 +61,16 @@ const USER_TYPE_PERMISSIONS: {
interface Props {
user: User;
users: User[];
permissions: PermissionType[];
onFinish: () => void;
}
export default function BatchCreateUser({user, users, permissions, onFinish}: Props) {
const [infos, setInfos] = useState<
{
email: string;
name: string;
passport_id: string;
type: Type;
demographicInformation: {
country: string;
passport_id: string;
phone: string;
};
}[]
>([]);
export default function BatchCreateUser({ user, permissions, onFinish }: Props) {
const [infos, setInfos] = useState<UserImport[]>([]);
const [duplicatedUsers, setDuplicatedUsers] = useState<UserImport[]>([]);
const [newUsers, setNewUsers] = useState<UserImport[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [expiryDate, setExpiryDate] = useState<Date | null>(
user?.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).toDate() : null,
@@ -90,7 +79,7 @@ export default function BatchCreateUser({user, users, permissions, onFinish}: Pr
const [type, setType] = useState<Type>("student");
const [showHelp, setShowHelp] = useState(false);
const {openFilePicker, filesContent, clear} = useFilePicker({
const { openFilePicker, filesContent, clear } = useFilePicker({
accept: ".xlsx",
multiple: false,
readAs: "ArrayBuffer",
@@ -115,19 +104,19 @@ export default function BatchCreateUser({user, users, permissions, onFinish}: Pr
return EMAIL_REGEX.test(email.toString().trim())
? {
email: email.toString().trim().toLowerCase(),
name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
type: type,
email: email.toString().trim().toLowerCase(),
name: `${firstName ?? ""} ${lastName ?? ""}`.trim(),
type: type,
passport_id: passport_id?.toString().trim() || undefined,
groupName: group,
corporate,
studentID,
demographicInformation: {
country: countryItem?.countryCode,
passport_id: passport_id?.toString().trim() || undefined,
groupName: group,
corporate,
studentID,
demographicInformation: {
country: countryItem?.countryCode,
passport_id: passport_id?.toString().trim() || undefined,
phone: phone.toString(),
},
}
phone: phone.toString(),
},
}
: undefined;
})
.filter((x) => !!x) as typeof infos,
@@ -153,15 +142,41 @@ export default function BatchCreateUser({user, users, permissions, onFinish}: Pr
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filesContent]);
useEffect(() => {
const crossReferenceEmails = async () => {
try {
const response = await axios.post("/api/users/controller?op=crossRefEmails", {
emails: infos.map((x) => x.email)
});
const crossRefEmails = response.data;
if (!!crossRefEmails) {
const existingEmails = new Set(crossRefEmails.map((x: any)=> x.email));
const dupes = infos.filter(info => existingEmails.has(info.email));
const newUsersList = infos.filter(info => !existingEmails.has(info.email));
setNewUsers(newUsersList);
setDuplicatedUsers(dupes);
} else {
setNewUsers(infos);
}
} catch (error) {
toast.error("Something went wrong, please try again later!");
}
};
if (infos.length > 0) {
crossReferenceEmails();
}
}, [infos]);
const makeUsers = async () => {
const newUsers = infos.filter((x) => !users.map((u) => u.email).includes(x.email));
if (!confirm(`You are about to add ${newUsers.length}, are you sure you want to continue?`)) return;
if (!confirm(`You are about to add ${newUsers.length} user${newUsers.length !== 1 ? 's' : ''}, are you sure you want to continue?`)) return;
if (newUsers.length > 0) {
setIsLoading(true);
try {
await axios.post("/api/batch_users", {users: newUsers.map((user) => ({...user, type, expiryDate}))});
//await axios.post("/api/batch_users", {users: newUsers.map((user) => ({...user, type, expiryDate}))});
toast.success(`Successfully added ${newUsers.length} user(s)!`);
onFinish();
} catch {
@@ -249,7 +264,7 @@ export default function BatchCreateUser({user, users, permissions, onFinish}: Pr
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];
// if (x === "corporate") console.log(list, perm, checkAccess(user, list, permissions, perm));
return checkAccess(user, getTypesOfUser(list), permissions, perm);
})
@@ -260,8 +275,20 @@ export default function BatchCreateUser({user, users, permissions, onFinish}: Pr
))}
</select>
)}
<Button className="my-auto" onClick={makeUsers} disabled={infos.length === 0}>
Create
{newUsers.length !== 0 && (
<div className="flex w-full flex-col gap-4">
<span className="text-mti-gray-dim text-base font-normal">New Users:</span>
<UserTable users={newUsers} />
</div>
)}
{duplicatedUsers.length !== 0 && (
<div className="flex w-full flex-col gap-4">
<span className="text-mti-gray-dim text-base font-normal">Duplicated Users:</span>
<UserTable users={duplicatedUsers} />
</div>
)}
<Button className="my-auto mt-4" onClick={makeUsers} disabled={newUsers.length === 0}>
Create {newUsers.length !== 0 ? `${newUsers.length} New Users` : ''}
</Button>
</div>
</>