From 578d29066f920a2c3d608e6deb06e565122d3aee Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 12 Dec 2024 09:42:21 +0000 Subject: [PATCH] ENCOA-271 --- src/components/High/Table.tsx | 2 +- src/hooks/useEntitiesCodes.tsx | 25 ++++ src/pages/(admin)/Lists/CodeList.tsx | 179 +++++---------------------- src/pages/api/code/entities.ts | 36 ++++++ src/pages/api/code/index.ts | 10 +- src/utils/users.ts | 2 +- 6 files changed, 97 insertions(+), 157 deletions(-) create mode 100644 src/hooks/useEntitiesCodes.tsx create mode 100644 src/pages/api/code/entities.ts diff --git a/src/components/High/Table.tsx b/src/components/High/Table.tsx index 64e166ef..479e614c 100644 --- a/src/components/High/Table.tsx +++ b/src/components/High/Table.tsx @@ -1,7 +1,7 @@ import { useListSearch } from "@/hooks/useListSearch" import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, useReactTable } from "@tanstack/react-table" import clsx from "clsx" -import { useState } from "react" +import { useEffect, useState } from "react" import { BsArrowDown, BsArrowUp } from "react-icons/bs" import Button from "../Low/Button" diff --git a/src/hooks/useEntitiesCodes.tsx b/src/hooks/useEntitiesCodes.tsx new file mode 100644 index 00000000..e286b6a7 --- /dev/null +++ b/src/hooks/useEntitiesCodes.tsx @@ -0,0 +1,25 @@ +import { Code, Group, User } from "@/interfaces/user"; +import axios from "axios"; +import { useEffect, useState } from "react"; + +export default function useEntitiesCodes(entities?: string[]) { + const [codes, setCodes] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = () => { + setIsLoading(true); + const params = new URLSearchParams() + if (entities) + entities.forEach(e => params.append("entities", e)) + + axios + .get(`/api/code/entities${entities ? `?${params.toString()}` : ""}`) + .then((response) => setCodes(response.data)) + .finally(() => setIsLoading(false)); + }; + + useEffect(getData, [entities]); + + return { codes, isLoading, isError, reload: getData }; +} diff --git a/src/pages/(admin)/Lists/CodeList.tsx b/src/pages/(admin)/Lists/CodeList.tsx index 3d71b533..383f3a29 100644 --- a/src/pages/(admin)/Lists/CodeList.tsx +++ b/src/pages/(admin)/Lists/CodeList.tsx @@ -1,83 +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 } from "@/utils"; +import { findBy, mapBy } from "@/utils"; +import useEntitiesCodes from "@/hooks/useEntitiesCodes"; +import Table from "@/components/High/Table"; -const columnHelper = createColumnHelper(); - -const CreatorCell = ({ id, users }: { id: string; users: User[] }) => { - const [creatorUser, setCreatorUser] = useState(); - - useEffect(() => { - setCreatorUser(users.find((x) => x.id === id)); - }, [id, users]); - - return ( - <> - {(creatorUser?.name || "N/A") || "N/A"}{" "} - {creatorUser && `(${USER_TYPE_LABELS[creatorUser?.type]})`} - - ); -}; +type TableData = Code & { entity?: EntityWithRoles, creator?: User } +const columnHelper = createColumnHelper(); export default function CodeList({ user, entities, canDeleteCodes } : { user: User, entities: EntityWithRoles[], canDeleteCodes?: boolean }) { const [selectedCodes, setSelectedCodes] = useState([]); - const [filteredCorporate, setFilteredCorporate] = useState(user?.type === "corporate" ? user : undefined); - const [filterAvailability, setFilterAvailability] = useState<"in-use" | "unused">(); + const entityIDs = useMemo(() => mapBy(entities, 'id'), [entities]) const { users } = useUsers(); - const { codes, reload } = useCodes(); + const { codes, reload } = useEntitiesCodes(isAdmin(user) ? undefined : entityIDs) - const [startDate, setStartDate] = useState(moment("01/01/2023").toDate()); - const [endDate, setEndDate] = useState(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 @@ -134,16 +98,8 @@ export default function CodeList({ user, entities, canDeleteCodes } const defaultColumns = [ columnHelper.accessor("code", { id: "codeCheckbox", - header: () => ( - !x.userId).length === 0} - isChecked={ - selectedCodes.length === filteredCodes.filter((x) => !x.userId).length && filteredCodes.filter((x) => !x.userId).length > 0 - } - onChange={(checked) => toggleAllCodes(checked)}> - {""} - - ), + enableSorting: false, + header: () => (""), cell: (info) => !info.row.original.userId ? ( toggleCode(info.getValue())}> @@ -165,11 +121,11 @@ export default function CodeList({ user, entities, canDeleteCodes } }), columnHelper.accessor("creator", { header: "Creator", - cell: (info) => , + cell: (info) => info.getValue() ? `${info.getValue().name} (${USER_TYPE_LABELS[info.getValue().type]})` : "N/A", }), columnHelper.accessor("entity", { header: "Entity", - cell: (info) => findBy(entities, 'id', info.getValue())?.label || "N/A", + cell: (info) => info.getValue()?.label || "N/A", }), columnHelper.accessor("userId", { header: "Availability", @@ -201,71 +157,11 @@ export default function CodeList({ user, entities, canDeleteCodes } }, ]; - const table = useReactTable({ - data: filteredCodes, - columns: defaultColumns, - getCoreRowModel: getCoreRowModel(), - }); - return ( <>
-
- setFilterAvailability(value ? (value.value as typeof filterAvailability) : undefined)} - /> - 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); - }} - /> -
{canDeleteCodes && ( -
+
{selectedCodes.length} code(s) selected
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ + data={data} + columns={defaultColumns} + searchFields={[["code"], ["email"], ["entity", "label"], ["creator", "name"], ['creator', 'type']]} + /> ); } diff --git a/src/pages/api/code/entities.ts b/src/pages/api/code/entities.ts new file mode 100644 index 00000000..b1a26ba0 --- /dev/null +++ b/src/pages/api/code/entities.ts @@ -0,0 +1,36 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import client from "@/lib/mongodb"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Code, Group, Type } from "@/interfaces/user"; +import { PERMISSIONS } from "@/constants/userPermissions"; +import { prepareMailer, prepareMailOptions } from "@/email"; +import { isAdmin } from "@/utils/users"; +import { requestUser } from "@/utils/api"; +import { doesEntityAllow } from "@/utils/permissions"; +import { getEntity, getEntityWithRoles } from "@/utils/entities.be"; +import { findBy } from "@/utils"; +import { EntityWithRoles } from "@/interfaces/entity"; + +const db = client.db(process.env.MONGODB_DB); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return get(req, res); + + return res.status(404).json({ ok: false }); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + const user = await requestUser(req, res) + if (!user) + return res.status(401).json({ ok: false, reason: "You must be logged in!" }) + + const { entities } = req.query as { entities?: string[] }; + if (entities) + return res.status(200).json(await db.collection("codes").find({ entity: { $in: entities } }).toArray()); + + return res.status(200).json(await db.collection("codes").find({}).toArray()); +} diff --git a/src/pages/api/code/index.ts b/src/pages/api/code/index.ts index d2eaf8e8..dee64299 100644 --- a/src/pages/api/code/index.ts +++ b/src/pages/api/code/index.ts @@ -40,6 +40,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { const generateAndSendCode = async ( code: string, type: Type, + creator: string, expiryDate: null | Date, entity?: string, info?: { @@ -47,7 +48,7 @@ const generateAndSendCode = async ( }) => { if (!info) { await db.collection("codes").insertOne({ - code, type, expiryDate, entity + code, type, creator, expiryDate, entity, creationDate: new Date().toISOString() }) return true } @@ -70,8 +71,9 @@ const generateAndSendCode = async ( await transport.sendMail(mailOptions); if (!previousCode) { await db.collection("codes").insertOne({ - code, type, expiryDate, entity, name: info.name.trim(), email: info.email.trim().toLowerCase(), - ...(info.passport_id ? { passport_id: info.passport_id.trim() } : {}) + code, type, creator, expiryDate, entity, name: info.name.trim(), email: info.email.trim().toLowerCase(), + ...(info.passport_id ? { passport_id: info.passport_id.trim() } : {}), + creationDate: new Date().toISOString() }) } @@ -118,7 +120,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const valid = [] for (const code of codes) { const info = findBy(infos || [], 'code', code) - const isValid = await generateAndSendCode(code, type, expiryDate, entity, info) + const isValid = await generateAndSendCode(code, type, user.id, expiryDate, entity, info) valid.push(isValid) } diff --git a/src/utils/users.ts b/src/utils/users.ts index 5472afa0..75ec3e7d 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -48,4 +48,4 @@ export const getUserName = (user?: User) => { return user.name; }; -export const isAdmin = (user: User) => ["admin", "developer"].includes(user.type) +export const isAdmin = (user: User) => ["admin", "developer"].includes(user?.type)