Continued updating the code to work with entities better

This commit is contained in:
Tiago Ribeiro
2024-10-07 15:49:58 +01:00
parent b5200c88fc
commit 1ef4efcacf
36 changed files with 2489 additions and 3012 deletions

View File

@@ -1,5 +1,6 @@
import {useListSearch} from "@/hooks/useListSearch";
import usePagination from "@/hooks/usePagination";
import { clsx } from "clsx";
import {ReactNode} from "react";
import Checkbox from "../Low/Checkbox";
import Separator from "../Low/Separator";
@@ -10,20 +11,21 @@ interface Props<T> {
pageSize?: number;
firstCard?: () => ReactNode;
renderCard: (item: T) => ReactNode;
className?: string
}
export default function CardList<T>({list, searchFields, renderCard, firstCard, pageSize = 20}: Props<T>) {
export default function CardList<T>({list, searchFields, renderCard, firstCard, className, pageSize = 20}: Props<T>) {
const {rows, renderSearch} = useListSearch(searchFields, list);
const {items, page, renderMinimal} = usePagination(rows, pageSize);
const {items, page, render, renderMinimal} = usePagination(rows, pageSize);
return (
<section className="flex flex-col gap-4 w-full">
<div className="w-full flex items-center gap-4">
{renderSearch()}
{renderMinimal()}
{searchFields.length > 0 && renderSearch()}
{searchFields.length > 0 ? renderMinimal() : render()}
</div>
<div className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className={clsx("w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4", className)}>
{page === 0 && !!firstCard && firstCard()}
{items.map(renderCard)}
</div>

View File

@@ -0,0 +1,107 @@
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 { BsArrowDown, BsArrowUp } from "react-icons/bs"
import Button from "../Low/Button"
interface Props<T> {
data: T[]
columns: ColumnDef<any, any>[]
searchFields: string[][]
size?: number
onDownload?: (rows: T[]) => void
}
export default function Table<T>({ data, columns, searchFields, size = 16, onDownload }: Props<T>) {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 16,
})
const { rows, renderSearch } = useListSearch<T>(searchFields, data);
const table = useReactTable({
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
state: {
pagination
}
});
return (
<div className="w-full flex flex-col gap-2">
<div className="w-full flex gap-2 items-end">
{renderSearch()}
{onDownload && (
<Button className="w-full max-w-[200px] mb-1" variant="outline" onClick={() => onDownload(rows)}>
Download List
</Button>
)
}
</div>
<div className="w-full flex gap-2 justify-between items-center">
<div className="flex items-center gap-4 w-fit">
<Button className="w-[200px] h-fit" disabled={!table.getCanPreviousPage()} onClick={() => table.previousPage()}>
Previous Page
</Button>
</div>
<div className="flex items-center gap-4 w-fit">
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount().toLocaleString()}
</strong>
<div>| Total: {table.getRowCount().toLocaleString()}</div>
</span>
<Button className="w-[200px]" disabled={!table.getCanNextPage()} onClick={() => table.nextPage()}>
Next Page
</Button>
</div>
</div>
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th className="py-4 px-4 text-left" key={header.id} colSpan={header.colSpan}>
<div
className={clsx(header.column.getCanSort() && 'cursor-pointer select-none', 'flex items-center gap-2')}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: <BsArrowUp />,
desc: <BsArrowDown />,
}[header.column.getIsSorted() as string] ?? null}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody className="px-2 w-full">
{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 items-center w-fit" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}

View File

@@ -1,11 +1,13 @@
import { User } from "@/interfaces/user";
import { checkAccess } from "@/utils/permissions";
import Select from "../Low/Select";
import { ReactNode, useEffect, useState } from "react";
import { ReactNode, useEffect, useMemo, useState } from "react";
import clsx from "clsx";
import useUsers from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups";
import useRecordStore from "@/stores/recordStore";
import { EntityWithRoles } from "@/interfaces/entity";
import { mapBy } from "@/utils";
type TimeFilter = "months" | "weeks" | "days";
@@ -13,6 +15,8 @@ type Filter = TimeFilter | "assignments" | undefined;
interface Props {
user: User;
entities: EntityWithRoles[]
users: User[]
filterState: {
filter: Filter,
setFilter: React.Dispatch<React.SetStateAction<Filter>>
@@ -28,83 +32,41 @@ const defaultSelectableCorporate = {
const RecordFilter: React.FC<Props> = ({
user,
entities,
users,
filterState,
assignments = true,
children
}) => {
const { filter, setFilter } = filterState;
const [statsUserId, setStatsUserId] = useRecordStore((state) => [
const [entity, setEntity] = useState<string>()
const [, setStatsUserId] = useRecordStore((state) => [
state.selectedUser,
state.setSelectedUser
]);
const { users } = useUsers();
const { groups: allGroups } = useGroups({});
const { groups } = useGroups({ admin: user?.id, userType: user?.type });
const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [users, entity])
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user.id])
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
setFilter((prev) => (prev === value ? undefined : value));
};
const selectableCorporates = [
defaultSelectableCorporate,
...users
.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id))
.filter((x) => x.type === "corporate")
.map((x) => ({
value: x.id,
label: `${x.name} - ${x.email}`,
})),
];
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
const getUsersList = (): User[] => {
if (selectedCorporate) {
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate);
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
return userListWithUsers.filter((x) => x);
}
return user.type !== "mastercorporate" ? users : users.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id));
};
const corporateFilteredUserList = getUsersList();
const getSelectedUser = () => {
if (selectedCorporate) {
const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId);
return userInCorporate || corporateFilteredUserList[0];
}
return users.find((x) => x.id === statsUserId) || user;
};
const selectedUser = getSelectedUser();
const selectedUserSelectValue = selectedUser
? {
value: selectedUser.id,
label: `${selectedUser.name} - ${selectedUser.email}`,
}
: {
value: "",
label: "",
};
return (
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
<div className="xl:w-3/4">
<div className="xl:w-3/4 flex gap-2">
{checkAccess(user, ["developer", "admin", "mastercorporate"]) && !children && (
<>
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
<div className="flex flex-col gap-2 w-full">
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
<Select
options={selectableCorporates}
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
onChange={(value) => setSelectedCorporate(value?.value || "")}
options={entities.map((e) => ({value: e.id, label: e.label}))}
onChange={(value) => setEntity(value?.value || undefined)}
isClearable
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
option: (styles, state) => ({
@@ -112,15 +74,17 @@ const RecordFilter: React.FC<Props> = ({
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
color: state.isFocused ? "black" : styles.color,
}),
}}></Select>
}} />
</div>
<div className="flex flex-col gap-2 w-full">
<label className="font-normal text-base text-mti-gray-dim">User</label>
<Select
options={corporateFilteredUserList.map((x) => ({
options={entityUsers.map((x) => ({
value: x.id,
label: `${x.name} - ${x.email}`,
}))}
value={selectedUserSelectValue}
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
@@ -131,20 +95,20 @@ const RecordFilter: React.FC<Props> = ({
}),
}}
/>
</div>
</>
)}
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && !children && (
<>
{(user.type === "corporate" || user.type === "teacher") && !children && (
<div className="flex flex-col gap-2">
<label className="font-normal text-base text-mti-gray-dim">User</label>
<Select
options={users
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
.map((x) => ({
value: x.id,
label: `${x.name} - ${x.email}`,
}))}
value={selectedUserSelectValue}
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
@@ -155,7 +119,7 @@ const RecordFilter: React.FC<Props> = ({
}),
}}
/>
</>
</div>
)}
{children}
</div>
@@ -203,4 +167,4 @@ const RecordFilter: React.FC<Props> = ({
);
}
export default RecordFilter;
export default RecordFilter;

View File

@@ -0,0 +1,30 @@
/** eslint-disable @next/next/no-img-element */
import { User } from "@/interfaces/user"
interface Props {
users: User[]
title: string;
}
const UserDisplay = (displayUser: User) => (
<div className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
export default function UserDisplayList({ title, users }: Props) {
return (<div className="bg-white border shadow flex flex-col rounded-xl w-full">
<span className="p-4">{title}</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.slice(0, 10)
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
</div>
</div>)
}