Updated the Batch Create User to also have an expiry date

This commit is contained in:
Tiago Ribeiro
2024-08-12 19:49:18 +01:00
parent 58300e32ff
commit 8162567e12
3 changed files with 301 additions and 335 deletions

View File

@@ -10,11 +10,14 @@ import readXlsxFile from "read-excel-file";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import {BsQuestionCircleFill} from "react-icons/bs"; import {BsQuestionCircleFill} from "react-icons/bs";
import {PermissionType} from "@/interfaces/permissions"; import {PermissionType} from "@/interfaces/permissions";
const EMAIL_REGEX = new RegExp( import moment from "moment";
/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/ import {checkAccess} from "@/utils/permissions";
); import Checkbox from "@/components/Low/Checkbox";
import ReactDatePicker from "react-datepicker";
import clsx from "clsx";
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"> 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", student: "Student",
@@ -41,13 +44,23 @@ const USER_TYPE_PERMISSIONS: {
export default function BatchCreateUser({user}: {user: User}) { export default function BatchCreateUser({user}: {user: User}) {
const [infos, setInfos] = useState< const [infos, setInfos] = useState<
{ email: string; name: string; passport_id:string, type: Type, demographicInformation: { {
country: string, email: string;
passport_id:string, name: string;
phone: string passport_id: string;
} }[] type: Type;
demographicInformation: {
country: string;
passport_id: string;
phone: string;
};
}[]
>([]); >([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
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 [type, setType] = useState<Type>("student");
const [showHelp, setShowHelp] = useState(false); const [showHelp, setShowHelp] = useState(false);
@@ -59,6 +72,9 @@ export default function BatchCreateUser({ user }: { user: User }) {
readAs: "ArrayBuffer", readAs: "ArrayBuffer",
}); });
useEffect(() => {
if (!isExpiryDateEnabled) setExpiryDate(null);
}, [isExpiryDateEnabled]);
useEffect(() => { useEffect(() => {
if (filesContent.length > 0) { if (filesContent.length > 0) {
@@ -68,15 +84,7 @@ export default function BatchCreateUser({ user }: { user: User }) {
const information = uniqBy( const information = uniqBy(
rows rows
.map((row) => { .map((row) => {
const [ const [firstName, lastName, country, passport_id, email, phone, group] = row as string[];
firstName,
lastName,
country,
passport_id,
email,
phone,
group
] = row as string[];
return EMAIL_REGEX.test(email.toString().trim()) return EMAIL_REGEX.test(email.toString().trim())
? { ? {
email: email.toString().trim().toLowerCase(), email: email.toString().trim().toLowerCase(),
@@ -88,17 +96,17 @@ export default function BatchCreateUser({ user }: { user: User }) {
country: country, country: country,
passport_id: passport_id?.toString().trim() || undefined, passport_id: passport_id?.toString().trim() || undefined,
phone, phone,
} },
} }
: undefined; : undefined;
}) })
.filter((x) => !!x) as typeof infos, .filter((x) => !!x) as typeof infos,
(x) => x.email (x) => x.email,
); );
if (information.length === 0) { if (information.length === 0) {
toast.error( toast.error(
"Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!" "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!",
); );
return clear(); return clear();
} }
@@ -106,7 +114,7 @@ export default function BatchCreateUser({ user }: { user: User }) {
setInfos(information); setInfos(information);
} catch { } catch {
toast.error( toast.error(
"Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!" "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!",
); );
return clear(); return clear();
} }
@@ -116,60 +124,42 @@ export default function BatchCreateUser({ user }: { user: User }) {
}, [filesContent]); }, [filesContent]);
const makeUsers = async () => { const makeUsers = async () => {
const newUsers = infos.filter( const newUsers = infos.filter((x) => !users.map((u) => u.email).includes(x.email));
(x) => !users.map((u) => u.email).includes(x.email) const confirmed = confirm(`You are about to add ${newUsers.length}, are you sure you want to continue?`);
); if (!confirmed) return;
const confirmed = confirm( if (newUsers.length > 0) {
`You are about to add ${newUsers.length}, are you sure you want to continue?`
)
if (!confirmed)
return;
if (newUsers.length > 0)
{
setIsLoading(true); setIsLoading(true);
Promise.all(newUsers.map(async (user) => { Promise.all(
await axios.post("/api/make_user", user) newUsers.map(async (user) => {
})).then((res) =>{ await axios.post("/api/make_user", user);
toast.success( }),
`Successfully added ${newUsers.length} user(s)!` )
)}).finally(() => { .then((res) => {
return clear(); toast.success(`Successfully added ${newUsers.length} user(s)!`);
}) })
.finally(() => {
return clear();
});
} }
setIsLoading(false); setIsLoading(false);
setInfos([]); setInfos([]);
}; };
return ( return (
<> <>
<Modal <Modal isOpen={showHelp} onClose={() => setShowHelp(false)} title="Excel File Format">
isOpen={showHelp}
onClose={() => setShowHelp(false)}
title="Excel File Format"
>
<div className="mt-4 flex flex-col gap-2"> <div className="mt-4 flex flex-col gap-2">
<span>Please upload an Excel file with the following format:</span> <span>Please upload an Excel file with the following format:</span>
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr> <tr>
<th className="border border-neutral-200 px-2 py-1"> <th className="border border-neutral-200 px-2 py-1">First Name</th>
First Name <th className="border border-neutral-200 px-2 py-1">Last Name</th>
</th>
<th className="border border-neutral-200 px-2 py-1">
Last Name
</th>
<th className="border border-neutral-200 px-2 py-1">Country</th> <th className="border border-neutral-200 px-2 py-1">Country</th>
<th className="border border-neutral-200 px-2 py-1"> <th className="border border-neutral-200 px-2 py-1">Passport/National ID</th>
Passport/National ID
</th>
<th className="border border-neutral-200 px-2 py-1">E-mail</th> <th className="border border-neutral-200 px-2 py-1">E-mail</th>
<th className="border border-neutral-200 px-2 py-1"> <th className="border border-neutral-200 px-2 py-1">Phone Number</th>
Phone Number <th className="border border-neutral-200 px-2 py-1">Group Name</th>
</th>
<th className="border border-neutral-200 px-2 py-1">
Group Name
</th>
</tr> </tr>
</thead> </thead>
</table> </table>
@@ -178,63 +168,62 @@ export default function BatchCreateUser({ user }: { user: User }) {
<ul> <ul>
<li>- All incorrect e-mails will be ignored;</li> <li>- All incorrect e-mails will be ignored;</li>
<li>- All already registered e-mails will be ignored;</li> <li>- All already registered e-mails will be ignored;</li>
<li> <li>- You may have a header row with the format above, however, it is not necessary;</li>
- You may have a header row with the format above, however, it <li>- All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.</li>
is not necessary;
</li>
<li>
- All of the e-mails in the file will receive an e-mail to join
EnCoach with the role selected below.
</li>
</ul> </ul>
</span> </span>
</div> </div>
</Modal> </Modal>
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4"> <div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
<div className="flex items-end justify-between"> <div className="flex items-end justify-between">
<label className="text-mti-gray-dim text-base font-normal"> <label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label>
Choose an Excel file <div className="tooltip cursor-pointer" data-tip="Excel File Format" onClick={() => setShowHelp(true)}>
</label>
<div
className="tooltip cursor-pointer"
data-tip="Excel File Format"
onClick={() => setShowHelp(true)}
>
<BsQuestionCircleFill /> <BsQuestionCircleFill />
</div> </div>
</div> </div>
<Button <Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
onClick={openFilePicker}
isLoading={isLoading}
disabled={isLoading}
>
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"} {filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
</Button> </Button>
<label className="text-mti-gray-dim text-base font-normal"> {user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
Select the type of user they should be <>
</label> <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>
<Checkbox isChecked={isExpiryDateEnabled} onChange={setIsExpiryDateEnabled} disabled={!!user.subscriptionExpirationDate}>
Enabled
</Checkbox>
</div>
{isExpiryDateEnabled && (
<ReactDatePicker
className={clsx(
"flex min-h-[70px] w-full cursor-pointer justify-center rounded-full border p-6 text-sm font-normal focus:outline-none",
"hover:border-mti-purple tooltip",
"transition duration-300 ease-in-out",
)}
filterDate={(date) =>
moment(date).isAfter(new Date()) &&
(user.subscriptionExpirationDate ? moment(date).isBefore(user.subscriptionExpirationDate) : true)
}
dateFormat="dd/MM/yyyy"
selected={expiryDate}
onChange={(date) => setExpiryDate(date)}
/>
)}
</>
)}
<label className="text-mti-gray-dim text-base font-normal">Select the type of user they should be</label>
{user && ( {user && (
<select <select
defaultValue="student" defaultValue="student"
onChange={(e) => setType(e.target.value as Type)} onChange={(e) => setType(e.target.value as Type)}
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" 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).map((type) => (
{Object.keys(USER_TYPE_LABELS)
.map((type) => (
<option key={type} value={type}> <option key={type} value={type}>
{USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]} {USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS]}
</option> </option>
))} ))}
</select> </select>
)} )}
<Button <Button className="my-auto" onClick={makeUsers} disabled={infos.length === 0}>
className="my-auto"
onClick={makeUsers}
disabled={
infos.length === 0
}
>
Create Create
</Button> </Button>
</div> </div>

View File

@@ -1,18 +1,6 @@
import type {NextApiRequest, NextApiResponse} from "next"; import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase"; import {app} from "@/firebase";
import { import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, deleteDoc, limit, updateDoc} from "firebase/firestore";
getFirestore,
setDoc,
doc,
query,
collection,
where,
getDocs,
getDoc,
deleteDoc,
limit,
updateDoc,
} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next"; import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {v4} from "uuid"; import {v4} from "uuid";
@@ -39,7 +27,6 @@ const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions); export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res); if (req.method === "POST") return post(req, res);
return res.status(404).json({ok: false}); return res.status(404).json({ok: false});
@@ -48,15 +35,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
async function post(req: NextApiRequest, res: NextApiResponse) { async function post(req: NextApiRequest, res: NextApiResponse) {
const maker = req.session.user; const maker = req.session.user;
if (!maker) { if (!maker) {
return res return res.status(401).json({ok: false, reason: "You must be logged in to make user!"});
.status(401)
.json({ ok: false, reason: "You must be logged in to make user!" });
} }
const { email, passport_id, type, groupName } = req.body as { const {email, passport_id, type, groupName, expiryDate} = req.body as {
email: string; email: string;
passport_id: string; passport_id: string;
type: string, type: string;
groupName: string groupName: string;
expiryDate: null | Date;
}; };
// cleaning data // cleaning data
delete req.body.passport_id; delete req.body.passport_id;
@@ -75,7 +61,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
desiredLevels: DEFAULT_DESIRED_LEVELS, desiredLevels: DEFAULT_DESIRED_LEVELS,
levels: DEFAULT_LEVELS, levels: DEFAULT_LEVELS,
isFirstLogin: false, isFirstLogin: false,
isVerified: true isVerified: true,
subscriptionExpirationDate: expiryDate || null,
}; };
await setDoc(doc(db, "users", userId), user); await setDoc(doc(db, "users", userId), user);
if (type === "corporate") { if (type === "corporate") {
@@ -103,16 +90,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
disableEditing: true, disableEditing: true,
}; };
await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup); await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup);
await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup); await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup);
await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup); await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup);
} }
if(typeof groupName === 'string' && groupName.trim().length > 0){ if (typeof groupName === "string" && groupName.trim().length > 0) {
const q = query(collection(db, "groups"), where("admin", "==", maker.id), where("name", "==", groupName.trim()), limit(1));
const q = query(collection(db, "groups"), where("admin", "==", maker.id), where("name", "==", groupName.trim()), limit(1)) const snapshot = await getDocs(q);
const snapshot = await getDocs(q)
if (snapshot.empty) { if (snapshot.empty) {
const values = { const values = {
@@ -121,24 +106,17 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
name: groupName.trim(), name: groupName.trim(),
participants: [userId], participants: [userId],
disableEditing: false, disableEditing: false,
} };
await setDoc(doc(db, "groups", values.id) , values)
await setDoc(doc(db, "groups", values.id), values);
} else { } else {
const doc = snapshot.docs[0];
const participants: string[] = doc.get("participants");
const doc = snapshot.docs[0]
const participants : string[] = doc.get('participants');
if (!participants.includes(userId)) { if (!participants.includes(userId)) {
updateDoc(doc.ref, { updateDoc(doc.ref, {
participants: [...participants, userId] participants: [...participants, userId],
}) });
} }
} }
} }
@@ -148,5 +126,4 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
return res.status(401).json({error}); return res.status(401).json({error});
}); });
return res.status(200).json({ok: true}); return res.status(200).json({ok: true});
} }

View File

@@ -60,7 +60,7 @@ export default function Admin() {
<ToastContainer /> <ToastContainer />
{user && ( {user && (
<Layout user={user} className="gap-6"> <Layout user={user} className="gap-6">
<section className="w-full flex -md:flex-col -xl:gap-2 gap-8 justify-between"> <section className="w-full grid grid-cols-2 -md:grid-cols-1 gap-8">
<ExamLoader /> <ExamLoader />
<BatchCreateUser user={user} /> <BatchCreateUser user={user} />
{checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && ( {checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && (