diff --git a/package-lock.json b/package-lock.json index c765241c..37ca0e83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@mui/styled-engine": "^5.13.2", "@mui/x-data-grid": "^5.17.26", "@react-pdf-viewer/core": "^3.12.0", + "@tanstack/react-table": "^8.20.5", "@testing-library/dom": "^8.20.1", "@testing-library/user-event": "^14.4.3", "@types/node": "^20.4.2", @@ -4719,6 +4720,25 @@ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==" }, + "node_modules/@tanstack/react-table": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz", + "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.10.8", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz", @@ -4735,6 +4755,18 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-core": { "version": "3.10.8", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz", diff --git a/package.json b/package.json index 39a90777..827299bc 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@mui/styled-engine": "^5.13.2", "@mui/x-data-grid": "^5.17.26", "@react-pdf-viewer/core": "^3.12.0", + "@tanstack/react-table": "^8.20.5", "@testing-library/dom": "^8.20.1", "@testing-library/user-event": "^14.4.3", "@types/node": "^20.4.2", diff --git a/src/components/InvitationDataPagination.tsx b/src/components/InvitationDataPagination.tsx new file mode 100644 index 00000000..d753f8ac --- /dev/null +++ b/src/components/InvitationDataPagination.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import { + ArrowCircleLeftIcon, + ArrowCircleRightIcon, +} from '@heroicons/react/outline'; +import { Table } from "@tanstack/react-table"; + +interface PaginationProps { + tableLib: Table; + totalPages: number; + pageSize: number; + pageIndex: number; + sizes: number[]; +} + +export const Pagination: React.FC = ({ + tableLib, + totalPages, + pageSize, + pageIndex, + sizes, +}) => ( +
+ + + + + + +
+
+ + {/* Previous / Next Page Buttons */} +
+ + + +
+ + {/* Page Information */} +
+ + Page{' '} + + {pageIndex + 1} of {` ${totalPages}`} + {' '} + +
+ + {/* Page Size Selector */} +
+ +
+
+
+
+); diff --git a/src/components/InvitationTable.tsx b/src/components/InvitationTable.tsx index a358539e..16112219 100644 --- a/src/components/InvitationTable.tsx +++ b/src/components/InvitationTable.tsx @@ -1,132 +1,136 @@ -// @ts-nocheck -import React, { useState, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useGlobalFilter, usePagination, useSortBy, useTable } from 'react-table'; -import { toast } from 'react-toastify'; -import DataPagination from './DataPagination'; -import SkeletonTable from '../Skeletons/SkeletonTable'; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { Pagination } from "../components/InvitationDataPagination"; +import SkeletonTable from '../Skeletons/SkeletonTable'; +import React, { useMemo } from "react"; -interface TableData { +interface Column { + accessor?: string | ((row: any) => any); // Make accessor optional + header: React.ReactNode; + cell: (info: any) => React.ReactNode; +} + +interface TableProps { + cols: Column[]; data: any[]; - columns: any; - error: string | null; - loading?: boolean; - className?: string; + loading: boolean; + rowCount: number; + onPaginationChange: (pagination: { pageIndex: number; pageSize: number }) => void; + pagination: { pageSize: number; pageIndex: number }; } -function DataTableStats({ data, columns, error, loading }: TableData) { - const [filterInput, setFilterInput] = useState(''); - const { t } = useTranslation(); - const [pageIndex, setPageIndex] = useState(0); - // Memoize columns and data to prevent unnecessary re-renders - const memoizedColumns = useMemo(() => [...columns], [columns]); - const memoizedData = useMemo(() => [...data], [data]); +const columnHelper = createColumnHelper(); - // Table instance - const tableInstance = useTable( - { - data: memoizedData, - columns: memoizedColumns, - initialState: { pageIndex, pageSize: 3, globalFilter: filterInput }, - }, - useGlobalFilter, - useSortBy, - usePagination, - ); +export const Table: React.FC = ({ + cols, + data, + loading, + rowCount, + onPaginationChange, + pagination, +}) => { + const { pageSize, pageIndex } = pagination; - const { - getTableProps, - setGlobalFilter, - getTableBodyProps, - page, - nextPage, - previousPage, - canPreviousPage, - canNextPage, - gotoPage, - pageCount, - setPageSize, - pageOptions, - headerGroups, - prepareRow, - state: { pageIndex: currentPageIndex, pageSize }, - } = tableInstance; + // Calculate the total number of pages + const totalPages = Math.ceil(rowCount / pageSize); - useEffect(() => { - setPageIndex(currentPageIndex); - }, [currentPageIndex]); + const tableColumns = useMemo( + () => + cols.map((column) => { + if (typeof column.accessor === 'string') { + return columnHelper.accessor(column.accessor, { + header: typeof column.header === 'string' ? column.header : column.header(), // Adjust the header type + cell: column.cell, + }); + } else if (typeof column.accessor === 'function') { + return columnHelper.accessor((row) => column.accessor(row), { + header: typeof column.header === 'string' ? column.header : column.header(), // Adjust the header type + cell: column.cell, + }); + } else { + return columnHelper.display({ + header: typeof column.header === 'string' ? column.header : column.header(), // Adjust the header type + cell: column.cell, + }); + } + }), + [cols] + ); - const handleFilterChange = (e) => { - const value = e.target.value || ''; - setGlobalFilter(value); - setFilterInput(value); - }; + // React table instance + const tableLib = useReactTable({ + data, + columns: tableColumns, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + state: { + pagination, + }, + onPaginationChange, + }); return ( -
+
{loading ? ( - + ) : ( - +
- {headerGroups.map((headerGroup) => ( - - {headerGroup.headers.map((column) => ( + {tableLib.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( ))} ))} - - {memoizedData.length === 0 && ( + + {data.length === 0 && ( )} - {memoizedData.length > 0 && - page.map((row) => { - prepareRow(row); - return ( - ( + + {row.getVisibleCells().map((cell) => ( + - ))} - - ); - })} - {!loading && !error && data.length === 0 && ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + {!loading && data.length === 0 && ( -
- {column.render('Header')} + {flexRender(header.column.columnDef.header, header.getContext())}
 {' '} {/* Non-breaking space to ensure it's not an empty tag */}
- {row.cells.map((cell) => ( - - {cell.render('Cell')} -
+

No records available @@ -140,22 +144,14 @@ function DataTableStats({ data, columns, error, loading }: TableData) { )}

-
- + ); -} - -export default DataTableStats; \ No newline at end of file +}; diff --git a/src/pages/invitation.tsx b/src/pages/invitation.tsx index 526af1dd..fcba58b4 100644 --- a/src/pages/invitation.tsx +++ b/src/pages/invitation.tsx @@ -9,7 +9,6 @@ import { LuHourglass } from 'react-icons/lu'; import { BsPersonFillX } from 'react-icons/bs'; import { toast } from 'react-toastify'; import InvitationCard from '../components/InvitationCard'; -import DataTableStats from '../components/InvitationTable'; import InvitationModal from './invitationModalComponet'; import { GET_INVITATIONS_STATISTICS_QUERY } from '../queries/invitationStats.queries'; import InvitationCardSkeleton from '../Skeletons/InvitationCardSkeleton'; @@ -28,6 +27,8 @@ import { GET_ROLES_AND_STATUSES, } from '../queries/invitation.queries'; import { isValid } from 'date-fns'; +import {Table} from '../components/InvitationTable' +import stack from '../utils/Stack'; interface Invitee { email: string; @@ -40,6 +41,23 @@ interface Invitationn { id: string; } +// Pagination Hook +function usePagination(initialSize = 3) { + const [pagination, setPagination] = useState({ + pageSize: initialSize, + pageIndex: 0, + }); + + const { pageSize, pageIndex } = pagination; + + return { + onPaginationChange: setPagination, + pagination, + limit: pageSize, + skip: pageSize * pageIndex, + }; +} + function Invitation() { const [invitationStats, setInvitationStats] = useState(null); const [sortBy,setSortBy]=useState(-1) @@ -85,6 +103,17 @@ function Invitation() { role: '', status: '', }); + const [invitationData, setInvitationData] = useState<{invitations: any[], totalInvitations: number}>({invitations:[], totalInvitations: 0}) + const usedQuery = stack.isEmpty() ? 'all' : stack.peek(); + const [previousQuery, setPreviousQuery] = useState(usedQuery); + const [previousFilter, setPreviousFilter] = useState({ role: '', status: '' }); + const [previousSearchQuery, setPreviousSearchQuery] = useState(""); + + + const [isFiltering, setIsFiltering] = useState(false); + const { limit, skip, pagination, onPaginationChange } = usePagination(3); + const [invitationsNew, setInvitationsNew] = useState([]); + const[filterDisabled,setFilterDisabled]=useState(true) const modalRef = useRef(null); const organizationToken = localStorage.getItem('orgToken'); @@ -128,11 +157,72 @@ function Invitation() { } = useQuery(GET_ALL_INVITATIONS, { variables:{ orgToken: organizationToken, - sortBy:sortBy + sortBy:sortBy, + limit, + offset: skip + }, + fetchPolicy: 'network-only', + skip: isFiltering + }); + + const [ + fetchInvitations, + { data: searchData, loading: searchLoading, error: searchError }, + ] = useLazyQuery(GET_INVITATIONS, { + variables: { + query: searchQuery, + orgToken: organizationToken, + sortBy:sortBy }, fetchPolicy: 'network-only', }); + const [ + filterInvitations, + { data: filterData, loading: filterLoad, error: filterError, refetch: refetchFiltered }, + ] = useLazyQuery(GET_ROLES_AND_STATUSES, { + variables:{ + ...filterVariables, + limit, + offset: skip, + }, + fetchPolicy: 'network-only', + }); + + const isSearching = searchQuery && searchQuery.trim() !== ""; + +// Refetch data on pagination change +useEffect(() => { + if (isSearching) { + // Refetch search data on pagination change + fetchInvitations({ + variables: { + query: searchQuery, + orgToken: organizationToken, + sortBy: sortBy, + limit, + offset: skip, + }, + }); + } else if (isFiltering) { + // Refetch filtered data on pagination change + refetchFiltered({ + ...filterVariables, + limit, + offset: skip, + }); + } else { + // Refetch all data on pagination change + refetch({ + orgToken: organizationToken, + sortBy: sortBy, + limit, + offset: skip, + }); + } +}, [limit, skip, refetch, refetchFiltered, fetchInvitations, isFiltering, filterVariables, searchQuery, isSearching]); + + useEffect(() => { if (invitationStats) { setSelectedStatus(''); // Set the fetched status as the default value @@ -152,7 +242,6 @@ function Invitation() { } } }, [data, selectedInvitationId]); - useEffect(() => { const handleClickOutside = (event: any) => { if (modalRef.current && !modalRef.current.contains(event.target)) { @@ -197,35 +286,55 @@ function Invitation() { setUpdateInviteeModel(newState); }; - const [ - fetchInvitations, - { data: searchData, loading: searchLoading, error: searchError }, - ] = useLazyQuery(GET_INVITATIONS, { - variables: { - query: searchQuery, - orgToken: organizationToken - }, - fetchPolicy: 'network-only', - }); - const [ - filterInvitations, - { data: filterData, loading: filterLoad, error: filterError }, - ] = useLazyQuery(GET_ROLES_AND_STATUSES, { - variables:filterVariables, - fetchPolicy: 'network-only', - }); + //////herererfdsgzfsbvfdv + // useEffect(() => { + // if (filterVariables.role || filterVariables.status) { + // filterInvitations({ + // variables: { + // role: filterVariables.role || "", + // status:filterVariables.status || "", + // orgToken: organizationToken + // }, + // }); + // } + // }, [filterVariables, filterInvitations,organizationToken]); - useEffect(() => { - if (filterVariables.role || filterVariables.status) { - filterInvitations({ - variables: { - role: filterVariables.role || null, - status:filterVariables.status || null, - orgToken: organizationToken - }, - }); - } - }, [filterVariables, filterInvitations,organizationToken]); + // Refetching Invitation + // const fetchNextInvitationsHandler = ({ limit, offset }: InvitationVariables) => { + // const previousUsedQuery = stack.peek(); + + // if (previousUsedQuery === 'filter') { + // filterInvitations({ + // variables: { + // role: filterVariables.role || "", + // status: filterVariables.status || "", + // limit, + // offset, + // orgToken: organizationToken, + // }, + // }).then(response => { + // if (response.data.filterInvitations) { + // setInvitations(response.data.filterInvitations.invitations); + // setTotalInvitations(response.data.filterInvitations.totalInvitations); + // } + // }).catch(error => { + // console.error('Error refetching filter invitations:', error); + // }); + // } else { + // refetch({ + // limit, + // offset + // }).then(response => { + // if (response.data.getAllInvitations) { + // setInvitations(response.data.getAllInvitations.invitations); + // setTotalInvitations(response.data.getAllInvitations.totalInvitations); + // } + // }).catch(error => { + // console.error('Error refetching invitations:', error); + // }); + // } + // }; + // Consolidated effect to handle query and search data useEffect(() => { @@ -246,6 +355,7 @@ function Invitation() { Array.isArray(searchData.getInvitations.invitations) ) { setInvitations(searchData.getInvitations.invitations); + setTotalInvitations(searchData.getInvitations.totalInvitations); } else if (filterData && filterData.filterInvitations) { setInvitations(filterData.filterInvitations.invitations); setTotalInvitations(filterData.filterInvitations.totalInvitations); @@ -265,6 +375,27 @@ function Invitation() { filterError, ]); + // useEffect(() => { + + // if (usedQuery === 'all' && data && data.getAllInvitations) { + // // If the previous query was 'all', append the new invitations + // if (previousQuery === 'all') { + // setInvitationData(prevData => ({ + // invitations: [...prevData.invitations, ...data.getAllInvitations.invitations], + // totalInvitations: data.getAllInvitations.totalInvitations + // })); + // } else { + // // If the previous query was not 'all', reset and set the new invitations + // setInvitationData({ + // invitations: [...data.getAllInvitations.invitations], + // totalInvitations: data.getAllInvitations.totalInvitations + // }); + // } + // } + // // Update the previous query after processing + // setPreviousQuery(usedQuery); + // }, [data, usedQuery]); + const handleSearch = () => { if (!searchQuery.trim()) { if (data && data.getAllInvitations) { @@ -276,6 +407,9 @@ function Invitation() { setInvitations([]); setError(null); setLoading(false); + stack.push("search"); + + onPaginationChange({ pageSize: pagination.pageSize, pageIndex: 0 }); fetchInvitations({ variables: { query: searchQuery } }); }; @@ -307,26 +441,36 @@ const handleStatusChange=(e:React.ChangeEvent)=>{ setSelectedStatus(status) } - const handleFilter = () => { - if (!selectedRole && !selectedStatus) { - toast.info('Please select role or status.'); - return; - } - setInvitations([]); - setError(null); - setLoading(false); +// Handle filter application +const handleFilter = () => { + if (!selectedRole && !selectedStatus) { + toast.info('Please select role or status.'); + return; + } - setFilterVariables({ - role: selectedRole, - status: typeof selectedStatus === 'string' ? selectedStatus : '', - }); - filterInvitations({ - variables:{ - sortBy:sortBy - } - }) - - }; + // Reset pagination to the first page when a filter is applied + onPaginationChange({ pageSize: pagination.pageSize, pageIndex: 0 }); + + // Activate filtering mode + setIsFiltering(true); + + // Set filter variables for the lazy query + setFilterVariables({ + role: selectedRole, + status: typeof selectedStatus === 'string' ? selectedStatus : '', + }); + + // Fetch filtered invitations + filterInvitations({ + variables: { + role: selectedRole || "", + status: selectedStatus || "", + orgToken: organizationToken, + limit, + offset: skip, + }, + }); +}; const toggleOptions = (row: string) => { setSelectedRow(selectedRow === row ? null : row); @@ -360,28 +504,28 @@ const handleStatusChange=(e:React.ChangeEvent)=>{ return str.charAt(0).toUpperCase() + str.slice(1); }; const columns = [ - { Header: t('email'), accessor: 'email' }, - { Header: t('role'), accessor: 'role' }, { - Header: t('Status'), - accessor: 'status', - Cell: ({ row }: any) => { - return ( -
0 ? ' flex' : ' hidden') - } - > - {row.original.Status} -
- ); - }, + id: "email", + header: t('email'), + accessor: (row) => row.email, // Handle dynamic case for status + cell: (info) =>
{info.getValue()}
, }, { - Header: t('action'), - accessor: '', - Cell: ({ row }: any) => ( + id: "role", + header: t('role'), + accessor: (row) => row.role, // Handle dynamic case for status + cell: (info) =>
{info.getValue()}
, + }, + { + id: "Status", + header: t('Status'), + accessor: (row) => row.Status, // Handle dynamic case for status + cell: (info) =>
{info.getValue()}
, // Render the status + }, + { + id: "Action", + header: t('Action'), + cell: ({ row }: any) => (
)=>{ ]; const datum: any = []; - if (invitations && invitations.length > 0) { - invitations.forEach((invitation) => { - invitation.invitees?.forEach((data: any) => { - let entry: any = {}; - entry.email = data.email; - entry.role = capitalizeStrings(data.role); - entry.Status = capitalizeStrings(invitation.status); - entry.id = invitation.id; - datum.push(entry); - }); + +// Determine whether to display search, filter, or all invitations +const currentInvitations = isSearching && searchData?.getInvitations + ? searchData.getInvitations.invitations + : isFiltering && filterData?.filterInvitations + ? filterData.filterInvitations.invitations + : data?.getAllInvitations?.invitations; + +const currentInvitationsTotal = isSearching && searchData?.getInvitations + ? searchData.getInvitations.totalInvitations + : isFiltering && filterData?.filterInvitations + ? filterData.filterInvitations.totalInvitations + : data?.getAllInvitations?.totalInvitations; + + + // console.log("currentInvitations:", currentInvitations); + +// Process current invitations (whether from search, filter, or all) +if (currentInvitations && currentInvitations.length > 0) { + currentInvitations.forEach((invitation) => { + invitation.invitees?.forEach((invitee: any) => { + let entry: any = {}; + entry.email = invitee.email; + entry.role = capitalizeStrings(invitee.role); + entry.Status = capitalizeStrings(invitation.status); + entry.id = invitation.id; + datum.push(entry); }); - } + }); +} + +// content = ( +// 0 ? datum : []} // Pass empty array if no data +// cols={columns} +// loading={loading || searchLoading} // Add search loading state +// rowCount={currentInvitationsTotal} // Handle total invitations count for pagination +// onPaginationChange={onPaginationChange} +// pagination={pagination} +// /> +// ); + if (loading || searchLoading || filterLoad) { content = ( - 0 ? datum : []} - columns={columns} - loading={loading} - error={error} +
0 ? datum : []} + cols={columns} + loading={loading || searchLoading} + rowCount={currentInvitationsTotal} + onPaginationChange={onPaginationChange} + pagination={pagination} /> ); } else if (error || searchError || filterError) { content = ( - 0 ? datum : []} - columns={columns} - loading={loading} - error={error} +
0 ? datum : []} + cols={columns} + loading={loading || searchLoading} + rowCount={currentInvitationsTotal} + onPaginationChange={onPaginationChange} + pagination={pagination} /> ); } else { content = ( <> - 0 ? datum : []} - columns={columns} - loading={loading} - error={error} +
0 ? datum : []} + cols={columns} + loading={loading || searchLoading} + rowCount={currentInvitationsTotal} + onPaginationChange={onPaginationChange} + pagination={pagination} /> ); diff --git a/src/queries/invitation.queries.tsx b/src/queries/invitation.queries.tsx index b1b97200..f9e9b1d3 100644 --- a/src/queries/invitation.queries.tsx +++ b/src/queries/invitation.queries.tsx @@ -28,6 +28,7 @@ export const GET_INVITATIONS = gql` id status } + totalInvitations } } `; @@ -43,6 +44,7 @@ export const GET_ROLES_AND_STATUSES = gql` id status } + totalInvitations } } `; diff --git a/src/utils/Stack.tsx b/src/utils/Stack.tsx new file mode 100644 index 00000000..daa20f9e --- /dev/null +++ b/src/utils/Stack.tsx @@ -0,0 +1,26 @@ +class Stack { + private items: T[] = []; + + push(item: T): void { + this.items.push(item); + } + + pop(): T | undefined { + return this.items.pop(); + } + + peek(): T | undefined { + return this.items[this.items.length - 1]; + } + + isEmpty(): boolean { + return this.items.length === 0; + } + + size(): number { + return this.items.length; + } +} + +const stack = new Stack(); +export default stack; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a591f517..3655a383 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,6 @@ "src/tests/About.test.tsx", "src/components/TraineeDashboardChart.tsx", "src/pages/TraineeRatingDashboard.tsx" -, "src/constants/SkeletonTable.tsx" ], +, "src/constants/SkeletonTable.tsx", "src/utils/Stack.tsx" ], "exclude": ["node_modules"] }