ENCOA-271

This commit is contained in:
Tiago Ribeiro
2024-12-12 09:42:21 +00:00
parent 1a7d35317b
commit 578d29066f
6 changed files with 97 additions and 157 deletions

View File

@@ -1,7 +1,7 @@
import { useListSearch } from "@/hooks/useListSearch" import { useListSearch } from "@/hooks/useListSearch"
import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, useReactTable } from "@tanstack/react-table" import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, useReactTable } from "@tanstack/react-table"
import clsx from "clsx" import clsx from "clsx"
import { useState } from "react" import { useEffect, useState } from "react"
import { BsArrowDown, BsArrowUp } from "react-icons/bs" import { BsArrowDown, BsArrowUp } from "react-icons/bs"
import Button from "../Low/Button" import Button from "../Low/Button"

View File

@@ -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<Code[]>([]);
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<Code[]>(`/api/code/entities${entities ? `?${params.toString()}` : ""}`)
.then((response) => setCodes(response.data))
.finally(() => setIsLoading(false));
};
useEffect(getData, [entities]);
return { codes, isLoading, isError, reload: getData };
}

View File

@@ -1,83 +1,47 @@
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import Checkbox from "@/components/Low/Checkbox"; 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 useUsers from "@/hooks/useUsers";
import { Code, User } from "@/interfaces/user"; import { Code, User } from "@/interfaces/user";
import { USER_TYPE_LABELS } from "@/resources/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 axios from "axios";
import moment from "moment"; import moment from "moment";
import { useEffect, useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { BsTrash } from "react-icons/bs"; import { BsTrash } from "react-icons/bs";
import { toast } from "react-toastify"; 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 { EntityWithRoles } from "@/interfaces/entity";
import { isAdmin } from "@/utils/users"; 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<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, entities, canDeleteCodes } export default function CodeList({ user, entities, canDeleteCodes }
: { user: User, entities: EntityWithRoles[], canDeleteCodes?: boolean }) { : { user: User, entities: EntityWithRoles[], canDeleteCodes?: boolean }) {
const [selectedCodes, setSelectedCodes] = useState<string[]>([]); const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
const [filteredCorporate, setFilteredCorporate] = useState<User | undefined>(user?.type === "corporate" ? user : undefined); const entityIDs = useMemo(() => mapBy(entities, 'id'), [entities])
const [filterAvailability, setFilterAvailability] = useState<"in-use" | "unused">();
const { users } = useUsers(); const { users } = useUsers();
const { codes, reload } = useCodes(); const { codes, reload } = useEntitiesCodes(isAdmin(user) ? undefined : entityIDs)
const [startDate, setStartDate] = useState<Date | null>(moment("01/01/2023").toDate()); const data: TableData[] = useMemo(() => codes.map((code) => ({
const [endDate, setEndDate] = useState<Date | null>(moment().endOf("day").toDate()); ...code,
const filteredCodes = useMemo(() => { entity: findBy(entities, 'id', code.entity),
return codes.filter((x) => { creator: findBy(users, 'id', code.creator)
// TODO: if the expiry date is missing, it does not make sense to filter by date })) as TableData[], [codes, entities, users])
// 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 toggleCode = (id: string) => { const toggleCode = (id: string) => {
setSelectedCodes((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])); setSelectedCodes((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
}; };
const toggleAllCodes = (checked: boolean) => { // const toggleAllCodes = (checked: boolean) => {
if (checked) return setSelectedCodes(filteredCodes.filter((x) => !x.userId).map((x) => x.code)); // if (checked) return setSelectedCodes(visibleRows.filter((x) => !x.userId).map((x) => x.code));
return setSelectedCodes([]); // return setSelectedCodes([]);
}; // };
const deleteCodes = async (codes: string[]) => { const deleteCodes = async (codes: string[]) => {
if (!canDeleteCodes) return if (!canDeleteCodes) return
@@ -134,16 +98,8 @@ export default function CodeList({ user, entities, canDeleteCodes }
const defaultColumns = [ const defaultColumns = [
columnHelper.accessor("code", { columnHelper.accessor("code", {
id: "codeCheckbox", id: "codeCheckbox",
header: () => ( enableSorting: false,
<Checkbox header: () => (""),
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>
),
cell: (info) => cell: (info) =>
!info.row.original.userId ? ( !info.row.original.userId ? (
<Checkbox isChecked={selectedCodes.includes(info.getValue())} onChange={() => toggleCode(info.getValue())}> <Checkbox isChecked={selectedCodes.includes(info.getValue())} onChange={() => toggleCode(info.getValue())}>
@@ -165,11 +121,11 @@ export default function CodeList({ user, entities, canDeleteCodes }
}), }),
columnHelper.accessor("creator", { columnHelper.accessor("creator", {
header: "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", { columnHelper.accessor("entity", {
header: "Entity", header: "Entity",
cell: (info) => findBy(entities, 'id', info.getValue())?.label || "N/A", cell: (info) => info.getValue()?.label || "N/A",
}), }),
columnHelper.accessor("userId", { columnHelper.accessor("userId", {
header: "Availability", header: "Availability",
@@ -201,71 +157,11 @@ export default function CodeList({ user, entities, canDeleteCodes }
}, },
]; ];
const table = useReactTable({
data: filteredCodes,
columns: defaultColumns,
getCoreRowModel: getCoreRowModel(),
});
return ( return (
<> <>
<div className="flex items-center justify-between pb-4 pt-1"> <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 && ( {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> <span>{selectedCodes.length} code(s) selected</span>
<Button <Button
disabled={selectedCodes.length === 0} disabled={selectedCodes.length === 0}
@@ -278,30 +174,11 @@ export default function CodeList({ user, entities, canDeleteCodes }
</div> </div>
)} )}
</div> </div>
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full"> <Table<TableData>
<thead> data={data}
{table.getHeaderGroups().map((headerGroup) => ( columns={defaultColumns}
<tr key={headerGroup.id}> searchFields={[["code"], ["email"], ["entity", "label"], ["creator", "name"], ['creator', 'type']]}
{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>
</> </>
); );
} }

View File

@@ -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<Code>({ entity: { $in: entities } }).toArray());
return res.status(200).json(await db.collection("codes").find<Code>({}).toArray());
}

View File

@@ -40,6 +40,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
const generateAndSendCode = async ( const generateAndSendCode = async (
code: string, code: string,
type: Type, type: Type,
creator: string,
expiryDate: null | Date, expiryDate: null | Date,
entity?: string, entity?: string,
info?: { info?: {
@@ -47,7 +48,7 @@ const generateAndSendCode = async (
}) => { }) => {
if (!info) { if (!info) {
await db.collection("codes").insertOne({ await db.collection("codes").insertOne({
code, type, expiryDate, entity code, type, creator, expiryDate, entity, creationDate: new Date().toISOString()
}) })
return true return true
} }
@@ -70,8 +71,9 @@ const generateAndSendCode = async (
await transport.sendMail(mailOptions); await transport.sendMail(mailOptions);
if (!previousCode) { if (!previousCode) {
await db.collection("codes").insertOne({ await db.collection("codes").insertOne({
code, type, expiryDate, entity, name: info.name.trim(), email: info.email.trim().toLowerCase(), code, type, creator, expiryDate, entity, name: info.name.trim(), email: info.email.trim().toLowerCase(),
...(info.passport_id ? { passport_id: info.passport_id.trim() } : {}) ...(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 = [] const valid = []
for (const code of codes) { for (const code of codes) {
const info = findBy(infos || [], 'code', code) 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) valid.push(isValid)
} }

View File

@@ -48,4 +48,4 @@ export const getUserName = (user?: User) => {
return user.name; return user.name;
}; };
export const isAdmin = (user: User) => ["admin", "developer"].includes(user.type) export const isAdmin = (user: User) => ["admin", "developer"].includes(user?.type)