Continued updating the code to work with entities better
This commit is contained in:
@@ -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>
|
||||
|
||||
107
src/components/High/Table.tsx
Normal file
107
src/components/High/Table.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
30
src/components/UserDisplayList.tsx
Normal file
30
src/components/UserDisplayList.tsx
Normal 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>)
|
||||
}
|
||||
Reference in New Issue
Block a user