ENCOA-271
This commit is contained in:
@@ -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"
|
||||||
|
|
||||||
|
|||||||
25
src/hooks/useEntitiesCodes.tsx
Normal file
25
src/hooks/useEntitiesCodes.tsx
Normal 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 };
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/pages/api/code/entities.ts
Normal file
36
src/pages/api/code/entities.ts
Normal 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());
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user